// Copyright 2019 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (dev@campbellcrowley.com) const crypto = require('crypto'); const c = require('./Constants.js'); /** * @description Contains information about a single user's pet. * @memberof Pets * @inner */ class Pet { /** * @description Create a pet for a user. * @param {string} owner ID of the user that owns this pet. * @param {string} name The name of this pet. * @param {string} species The species of this pet. * @param {?string} [petClass=null] The class name of this pet or null for * none. */ constructor(owner, name, species, petClass) { /** * @description ID of the owner of this pet. * @type {string} * @public */ this.owner = owner; /** * @description The name of this pet. * @type {string} * @public */ this.name = name; /** * @description The species ID of this pet. * @type {string} * @public */ this.species = species; /** * @description The character class for this pet. * @type {?string} * @public */ this.petClass = petClass || null; /** * @description The ID of this pet. Does not check for uniqueness, but * expects the ID to be unique per-user. * @type {string} * @public */ this.id = crypto.randomBytes(6).toString('base64').replace(/\//g, '='); /** * @description The number of experience points this pet has. * @public * @type {number} * @default */ this.xp = 0; /** * @description Attack modifier on top of base species stat. * @public * @type {number} * @default */ this.attackMod = 0; /** * @description Defense modifier on top of base species stat. * @public * @type {number} * @default */ this.defenseMod = 0; /** * @description Speed modifier on top of base species stat. * @public * @type {number} * @default */ this.speedMod = 0; /** * @description Health modifier on top of base species stat. * @public * @type {number} * @default */ this.healthMod = 0; /** * @description Current number of lives remaining. * @public * @type {number} * @default */ this.lives = 3; /** * @description Timestamp at most recent time the pet has begun resting. * Used for regenerating lives. * @public * @type {number} * @default */ this.restStartTimestamp = Date.now(); /** * @description The timestamp at which this object was last interacted with. * This is used for purging from memory when not used for a while. * @type {number} * @private * @default Date.now() */ this._lastInteractTime = Date.now(); this.touch = this.touch.bind(this); this.addXP = this.addXP.bind(this); this.numPoints = this.numPoints.bind(this); this.spendPoint = this.spendPoint.bind(this); this.autoLevelUp = this.autoLevelUp.bind(this); } /** * @description Get a serializable version of this class instance. Strips all * private variables, and all functions. Assumes all public variables are * serializable if they aren't a function. * @public * @returns {object} Serializable version of this instance. */ get serializable() { const all = Object.entries(Object.getOwnPropertyDescriptors(this)); const output = {}; for (const one of all) { if (typeof one[1].value === 'function' || one[0].startsWith('_')) { continue; } output[one[0]] = one[1].value; } return output; } /** * @description Touch this object to update the last `_lastInteractTime`. * @public */ touch() { this._lastInteractTime = Date.now(); } /** * @description Add XP to the pet, and trigger any level-up actions that may * need to occur. * * @public * @param {number} xp Amount of XP to add. * @param {boolean} [auto=false] Auto level up after adding XP. */ addXP(xp, auto = false) { if (typeof xp !== 'number' || isNaN(xp)) { throw new TypeError('xp is not a number'); } this.xp += xp; if (auto) this.autoLevelUp(); } /** * @description Get the number of remaining points available to spend on * modifiers. * * @public * @returns {number} Number of spendable points. */ get numPoints() { return this.getLevel(this.xp) - (this.attackMod + this.defenseMod + this.speedMod); } /** * @description Spend points on a given modifier. * * @public * @param {string} category The name of the modifier to spend the points on. * @param {number} [num=1] The number of points to spend. */ spendPoint(category, num = 1) { const remaining = this.numPoints; if (num > remaining) num = remaining; if (num == 0) return; if (num < 0) { throw new Error( 'Attempted to spend negative amount of points. (' + num + ')'); } switch (category) { default: throw new Error('Unknown modifier: ' + category); case 'speed': this.speedMod += num; break; case 'attack': this.attackMod += num; break; case 'defense': this.defenseMod += num; break; } } /** * @description Automatically spend all remaining modifier points * automatically until all are used up. * * @public */ autoLevelUp() { while (this.numPoints > 0) { let least = 'attack'; if (this.defenseMod < this.attackMod && this.defenseMod < this.speedMod) { least = 'defense'; } else if ( this.speedMod < this.defenseMod && this.speedMod < this.attackMod) { least = 'speed'; } this.spendPoint(least); } } /** * @description Signal this pet just won a battle, and update accordingly. * @public */ wonBattle() { this.xp += c.winXP; } /** * @description Signal this pet just lost a battle, and update accordingly. * @public */ lostBattle() { this.xp += c.loseXP; } } /** * @description Create a Pet from a Pet-like Object. Similar to * copy-constructor. * @public * @static * @param {object} obj The Pet-like object to copy. * @returns {Pet} Created Pet object. */ Pet.from = function(obj) { const output = new Pet(obj.owner, obj.name, obj.species); if (obj.id) output.id = obj.id; if (obj.xp) output.xp = obj.xp * 1; if (obj.attackMod) output.attackMod = obj.attackMod * 1; if (obj.defenseMod) output.defenseMod = obj.defenseMod * 1; if (obj.speedMod) output.speedMod = obj.speedMod * 1; if (obj.healthMod) output.healthMod = obj.healthMod * 1; if (typeof obj.lives === 'number') output.lives = obj.lives * 1; if (obj.restStartTimestamp && obj.restStartTimestamp < output.restStartTimestamp) { output.restStartTimestamp = obj.restStartTimestamp; } return output; }; /** * @description Calculate the required amount of XP for the given level. * * This function provides a very steep curve for levelling up. This is to help * prevent extremely high leveled characters, and to encourage players to play * a lot in order to level up. This is attempting to follow the similar * structure to D&D since I wish to have a similar timeline for character * development and play speed (characters can last for months and take weeks * to level up). * * @public * @static * @param {number} level The level number to calculate the required XP for. * @returns {number} XP required to be the given level. */ Pet.levelXP = function(level) { return c.levelXPFactor * (level * level) - (c.levelXPFactor * level); }; /** * @description Get the level number for the given amount of XP. Uses * {@link Pets~Pet.levelXP} to calculate. * * @public * @static * @param {number} xp The amount of XP to find the level number for. * @returns {number} The level number for the amount of XP. */ Pet.getLevel = function(xp) { let level = 0; do { level++; } while (Pet.levelXP(level) <= xp); return level - 1; }; module.exports = Pet;