@reldens/skills
Version:
277 lines (253 loc) • 11.1 kB
JavaScript
/**
*
* Reldens - Skills - LevelsSet
*
*/
const SkillsEvents = require('./skills-events');
const Level = require('./level');
const { EventsManagerSingleton, Logger, sc } = require('@reldens/utils');
class LevelsSet
{
constructor(props = {})
{
this.events = sc.get(props, 'events', EventsManagerSingleton);
this.owner = sc.get(props, 'owner', false);
}
setOwner(props)
{
let owner = sc.get(props, 'owner');
if(!owner){
Logger.critical('Undefined owner for levels.');
return false;
}
// @NOTE: getPosition must return an object with x and y props, like {x: 123, y: 456}.
if('function' !== typeof owner.getPosition){
Logger.critical('Undefined owner position method.');
return false;
}
this.owner = owner;
this.ownerIdProperty = sc.get(props, 'ownerIdProperty', 'id');
}
async init(props)
{
if(!this.owner){
Logger.critical('Undefined owner for levels.');
return false;
}
if('function' !== typeof this.owner.getPosition){
Logger.critical('Undefined owner position method.');
return false;
}
let levels = sc.get(props, 'levels', {});
if(!levels || !Object.keys(levels).length){
Logger.critical('Levels were not specified.');
return false;
}
// fireEvent has to be executed after the owner was set since it will call the owner data to relate the events:
await this.fireEvent(SkillsEvents.INIT_LEVEL_SET_START, this);
// @NOTE: if autoFillRanges is enabled, you could have the first level with key = 1 and the next with key = 5,
// this way the setLevels will fill up levels 2, 3 and 4 with the same level Object so the same level modifiers
// will be applied automatically (same calculation for multiple levels).
this.autoFillRanges = sc.get(props, 'autoFillRanges', false);
this.autoFillExperienceMultiplier = sc.get(props, 'autoFillExperienceMultiplier', 1.5);
this.currentLevel = sc.get(props, 'currentLevel', 0);
this.currentExp = sc.get(props, 'currentExp', 0);
this.setRequiredExperienceLimit = sc.get(props, 'setRequiredExperienceLimit', false);
this.ownerIdProperty = sc.get(props, 'ownerIdProperty', 'id');
// set levels is almost the last thing to happen here; first we need to validate all the other parameters:
this.levels = await this.setLevels(levels);
// @NOTE: if you set up levelsByExperience and autoFillRanges at the same time, the levels autogenerated will be
// skipped because the experience on those will be lowed one. For example, level 1 required experience = 10p,
// level 5 required experience = 20p; levels 2, 3 and 4 are auto-generated with required exp. = 20p, so when the
// player reaches 20p of experience, it will jump to level 5. In summary, you shouldn't mix these options unless
// you expect the described behavior.
this.increaseLevelsWithExperience = sc.get(props, 'increaseLevelsWithExperience', true);
// unless specified, levels by experience must just be sorted by their key attribute (without autofill):
this.levelsByExperience = sc.get(props, 'levelsByExperience', this.sortLevelsBy(this.levels));
await this.fireEvent(SkillsEvents.INIT_LEVEL_SET_END, this);
}
async setLevels(levels)
{
!this.autoFillRanges ? (this.levels = levels) : await this.createLevels(levels);
await this.fireEvent(SkillsEvents.SET_LEVELS, this, levels);
return this.levels;
}
async createLevels(levels)
{
this.levels = {};
let levelsKeys = Object.keys(levels);
for(let i of levelsKeys){
let level = levels[i];
this.levels[i] = level;
let levelsKeysNextIndex = levelsKeys.indexOf(i) + 1;
let nextLevelKey = levelsKeys.length > levelsKeysNextIndex ? levelsKeys[levelsKeysNextIndex] : false;
let nextLevel = false;
if(sc.hasOwn(levels, (nextLevelKey))){
nextLevel = levels[nextLevelKey];
}
if(sc.hasOwn(nextLevel, 'key') && (nextLevel.key - level.key) > 0){
await this.createAndAutofillLevelsInstances(level, nextLevel);
}
}
}
async createAndAutofillLevelsInstances(level, nextLevel)
{
for(let n = level.key + 1; n < nextLevel.key; n++){
let requiredExp = Math.round(this.levels[n-1].requiredExperience * this.autoFillExperienceMultiplier);
let newLevelProps = {
key: n,
modifiers: level.modifiers,
requiredExperience: requiredExp,
};
this.levels[n] = new Level(newLevelProps);
await this.fireEvent(SkillsEvents.GENERATED_LEVELS, this);
}
}
sortLevelsBy(levels, sortField = 'key')
{
return Object.keys(levels).sort((a,b) => {
return (levels[a][sortField] > levels[b][sortField]) ? 1 : -1;
});
}
async levelUp()
{
let lastLevelKey = Object.keys(this.levels).pop();
if(this.currentLevel >= this.levels[lastLevelKey].key){
// reached top level:
return false;
}
// first we need to increase the level:
this.currentLevel++;
// then apply the new level modifiers:
await this.applyLevelModifiers();
await this.fireEvent(SkillsEvents.LEVEL_UP, this);
}
async levelDown()
{
if(1 >= this.currentLevel){
// reached first level:
return false;
}
// here we first need to revert the modifiers of the current level:
await this.applyLevelModifiers(true);
// then reduce the level:
this.currentLevel--;
await this.fireEvent(SkillsEvents.LEVEL_DOWN, this);
}
async applyLevelModifiers(revert)
{
let currentLevelInstance = this.getLevelInstance(this.currentLevel);
if(!currentLevelInstance){
// this will usually happen if you don't set a proper end level without required experience.
Logger.error(['Current level instance not found.', 'Current Level Key:', this.currentLevel]);
return false;
}
await this.fireEvent(SkillsEvents.LEVEL_APPLY_MODIFIERS, this, currentLevelInstance);
if(!currentLevelInstance.modifiers || 0 === currentLevelInstance.modifiers.length){
return false;
}
for(let modifier of currentLevelInstance.modifiers){
revert ? modifier.revert(this.owner) : modifier.apply(this.owner);
}
}
getLevelInstance(level)
{
// @NOTE: if you only specify 1 level, the same calculation will be used for all the levels.
return sc.get(this.levels, level, false);
}
async addExperience(number)
{
let newTotalExp = this.currentExp + number;
let currentLevelIndex = this.levelsByExperience.indexOf(this.currentLevel.toString());
let nextLevelIndex = currentLevelIndex + 1;
let nextLevelKey = this.levelsByExperience[nextLevelIndex];
let nextLevel = this.levels[nextLevelKey];
let nextLevelExp = 0;
let isLevelUp = false;
if(
this.increaseLevelsWithExperience
&& sc.hasOwn(this.levels, this.currentLevel)
&& nextLevel && newTotalExp >= nextLevel.requiredExperience
){
for(let levelByExp of this.levelsByExperience.slice(nextLevelIndex)){
if(newTotalExp >= this.levels[levelByExp].requiredExperience){
isLevelUp = true;
await this.levelUp();
}
if(newTotalExp < this.levels[levelByExp].requiredExperience){
nextLevelExp = this.levels[levelByExp].requiredExperience;
break;
}
}
}
let expNextLevelIndex = !isLevelUp ? nextLevelIndex : (nextLevelIndex + 1)
if((!nextLevel || expNextLevelIndex === this.levelsByExperience.length) && this.setRequiredExperienceLimit){
newTotalExp = this.getLevelInstance(this.currentLevel).requiredExperience;
}
this.currentExp = newTotalExp;
await this.fireEvent(SkillsEvents.LEVEL_EXPERIENCE_ADDED,
this,
number,
newTotalExp,
currentLevelIndex,
nextLevelIndex,
nextLevelKey,
nextLevel,
nextLevelExp,
isLevelUp
);
}
getNextLevelExperience()
{
let currentLevelIndex = this.levelsByExperience.indexOf(this.currentLevel.toString());
let currentLevel = this.getLevelInstance(this.currentLevel);
let nextLevelIndex = currentLevelIndex + 1;
let nextLevelKey = this.levelsByExperience[nextLevelIndex];
let nextLevel = this.getLevelInstance(nextLevelKey);
if(!nextLevel){
return currentLevel.requiredExperience;
}
if(nextLevel && this.currentExp < nextLevel.requiredExperience){
return nextLevel.requiredExperience;
}
if(!sc.hasOwn(this.levels, this.currentLevel) || !nextLevel){
return nextLevel.requiredExperience;
}
for(let levelByExp of this.levelsByExperience.slice(nextLevelIndex)){
if(this.currentExp < this.levels[levelByExp].requiredExperience){
return this.levels[levelByExp].requiredExperience;
}
}
return nextLevel.requiredExperience;
}
getOwnerId()
{
return this.owner[this.ownerIdProperty];
}
getOwnerEventKey()
{
return sc.get(this.owner, 'eventsPrefix', 'skills.ownerId.'+this.getOwnerId());
}
getOwnerUniqueEventKey(suffix)
{
let uniqueKey = sc.isFunction(this.owner.eventUniqueKey)
? this.owner.eventUniqueKey()
: 'skills.ownerId.'+this.getOwnerId()+'.uKey.'+sc.getTime();
return uniqueKey+(suffix ? '.'+suffix : '');
}
async fireEvent(eventName, ...args)
{
return await this.events.emit(this.eventFullName(eventName), ...args);
}
listenEvent(eventName, callback, removeKey, masterKey)
{
return this.events.onWithKey(this.eventFullName(eventName), callback, removeKey, masterKey);
}
eventFullName(eventName)
{
// @NOTE: appending the level set owner ID to the event name, so we can pick up this event only for this
// owner automatically:
return this.getOwnerEventKey()+'.'+eventName;
}
}
module.exports = LevelsSet;