UNPKG

@reldens/skills

Version:
277 lines (253 loc) 11.1 kB
/** * * 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;