// 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;