//Methods const die = require('./stats/die'); let animations = require('../config/animations'); let spirits = require('../config/spirits'); let scheduler = require('../misc/scheduler'); let baseStats = { mana: 20, manaMax: 20, manaReservePercent: 0, hp: 5, hpMax: 5, xpTotal: 0, xp: 0, xpMax: 0, level: 1, str: 0, int: 0, dex: 0, magicFind: 0, itemQuantity: 0, regenHp: 0, regenMana: 5, addCritChance: 0, addCritMultiplier: 0, addAttackCritChance: 0, addAttackCritMultiplier: 0, addSpellCritChance: 0, addSpellCritMultiplier: 0, critChance: 5, critMultiplier: 150, attackCritChance: 0, attackCritMultiplier: 0, spellCritChance: 0, spellCritMultiplier: 0, armor: 0, vit: 0, blockAttackChance: 0, blockSpellChance: 0, dodgeAttackChance: 0, dodgeSpellChance: 0, attackSpeed: 0, castSpeed: 0, elementArcanePercent: 0, elementFrostPercent: 0, elementFirePercent: 0, elementHolyPercent: 0, elementPoisonPercent: 0, physicalPercent: 0, elementPercent: 0, spellPercent: 0, elementArcaneResist: 0, elementFrostResist: 0, elementFireResist: 0, elementHolyResist: 0, elementPoisonResist: 0, elementAllResist: 0, sprintChance: 0, xpIncrease: 0, lifeOnHit: 0, //Fishing stats catchChance: 0, catchSpeed: 0, fishRarity: 0, fishWeight: 0, fishItems: 0 }; module.exports = { type: 'stats', values: baseStats, statScales: { vitToHp: 10, strToArmor: 1, intToMana: (1 / 6), dexToDodge: (1 / 12) }, syncer: null, stats: { logins: 0, played: 0, lastLogin: null, loginStreak: 0, mobKillStreaks: {}, lootStats: {} }, dead: false, init: function (blueprint) { this.syncer = this.obj.instance.syncer; let values = (blueprint || {}).values || {}; for (let v in values) this.values[v] = values[v]; let stats = (blueprint || {}).stats || {}; for (let v in stats) this.stats[v] = stats[v]; if (this.obj.player) { this.calcXpMax(); this.addLevelAttributes(); this.calcHpMax(); } if (blueprint) delete blueprint.stats; }, resetHp: function () { let values = this.values; values.hp = values.hpMax; this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); }, update: function () { if ((this.obj.mob && !this.obj.follower) || this.obj.dead) return; let values = this.values; let manaMax = values.manaMax; manaMax -= (manaMax * values.manaReservePercent); let regen = { success: true }; this.obj.fireEvent('beforeRegen', regen); if (!regen.success) return; let isInCombat = this.obj.aggro && this.obj.aggro.list.length > 0; if (this.obj.follower) { isInCombat = (this.obj.follower.master.aggro.list.length > 0); if (isInCombat) return; } let regenHp = 0; let regenMana = 0; regenMana = values.regenMana / 50; if (!isInCombat) regenHp = Math.max(values.hpMax / 112, values.regenHp * 0.2); else regenHp = values.regenHp * 0.2; if (values.hp < values.hpMax) { values.hp += regenHp; this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); } if (values.hp > values.hpMax) { values.hp = values.hpMax; this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); } if (values.mana < manaMax) { values.mana += regenMana; this.obj.syncer.setObject(!this.obj.player, 'stats', 'values', 'mana', values.mana); } if (values.mana > manaMax) { values.mana = manaMax; this.obj.syncer.setObject(!this.obj.player, 'stats', 'values', 'mana', values.mana); } }, addStat: function (stat, value) { let values = this.values; if (['lvlRequire', 'allAttributes'].indexOf(stat) === -1) values[stat] += value; let sendOnlyToSelf = (['hp', 'hpMax', 'mana', 'manaMax', 'vit'].indexOf(stat) === -1); this.obj.syncer.setObject(sendOnlyToSelf, 'stats', 'values', stat, values[stat]); if (sendOnlyToSelf) this.obj.syncer.setObject(false, 'stats', 'values', stat, values[stat]); if (['addCritChance', 'addAttackCritChance', 'addSpellCritChance'].indexOf(stat) > -1) { let morphStat = stat.substr(3); morphStat = morphStat[0].toLowerCase() + morphStat.substr(1); this.addStat(morphStat, (0.05 * value)); } else if (['addCritMultiplier', 'addAttackCritMultiplier', 'addSpellCritMultiplier'].indexOf(stat) > -1) { let morphStat = stat.substr(3); morphStat = morphStat[0].toLowerCase() + morphStat.substr(1); this.addStat(morphStat, value); } else if (stat === 'vit') this.addStat('hpMax', (value * this.statScales.vitToHp)); else if (stat === 'allAttributes') { ['int', 'str', 'dex'].forEach(function (s) { this.addStat(s, value); }, this); } else if (stat === 'elementAllResist') { ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) { let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Resist'; this.addStat(element, value); }, this); } else if (stat === 'elementPercent') { ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) { let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Percent'; this.addStat(element, value); }, this); } else if (stat === 'str') this.addStat('armor', (value * this.statScales.strToArmor)); else if (stat === 'int') this.addStat('manaMax', (value * this.statScales.intToMana)); else if (stat === 'dex') this.addStat('dodgeAttackChance', (value * this.statScales.dexToDodge)); }, calcXpMax: function () { let level = this.values.level; this.values.xpMax = (level * 5) + ~~(level * 10 * Math.pow(level, 2.2)) - 5; this.obj.syncer.setObject(true, 'stats', 'values', 'xpMax', this.values.xpMax); }, calcHpMax: function () { const spiritConfig = spirits.stats[this.obj.class]; const initialHp = spiritConfig ? spiritConfig.values.hpMax : 32.7; let increase = spiritConfig ? spiritConfig.values.hpPerLevel : 32.7; this.values.hpMax = initialHp + (((this.values.level || 1) - 1) * increase); }, //Source is the object that caused you to gain xp (mostly yourself) //Target is the source of the xp (a mob or quest) getXp: function (amount, source, target) { let obj = this.obj; let values = this.values; if (values.level === consts.maxLevel) return; let xpEvent = { source: source, target: target, amount: amount, multiplier: 1 }; obj.fireEvent('beforeGetXp', xpEvent); if (xpEvent.amount === 0) return; obj.instance.eventEmitter.emit('onBeforeGetGlobalXpMultiplier', xpEvent); amount = ~~(xpEvent.amount * (1 + (values.xpIncrease / 100)) * xpEvent.multiplier); values.xpTotal = ~~(values.xpTotal + amount); values.xp = ~~(values.xp + amount); obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp); this.syncer.queue('onGetDamage', { id: obj.id, event: true, text: '+' + amount + ' xp' }, -1); let syncO = {}; let didLevelUp = false; while (values.xp >= values.xpMax) { didLevelUp = true; values.xp -= values.xpMax; obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp); values.level++; obj.fireEvent('onLevelUp', this.values.level); if (values.level === consts.maxLevel) values.xp = 0; this.calcHpMax(); obj.syncer.setObject(true, 'stats', 'values', 'hpMax', values.hpMax); this.addLevelAttributes(true); obj.spellbook.calcDps(); this.syncer.queue('onGetDamage', { id: obj.id, event: true, text: 'level up' }, -1); syncO.level = values.level; this.calcXpMax(); } if (didLevelUp) { let cellContents = obj.instance.physics.getCell(obj.x, obj.y); cellContents.forEach(function (c) { c.fireEvent('onCellPlayerLevelUp', obj); }); obj.auth.doSave(); } process.send({ method: 'object', serverId: this.obj.serverId, obj: syncO }); if (didLevelUp) { this.obj.syncer.setObject(true, 'stats', 'values', 'hpMax', values.hpMax); this.obj.syncer.setObject(true, 'stats', 'values', 'level', values.level); this.obj.syncer.setObject(false, 'stats', 'values', 'hpMax', values.hpMax); this.obj.syncer.setObject(false, 'stats', 'values', 'level', values.level); } }, kill: function (target) { if (target.player) return; let level = target.stats.values.level; let mobDiffMult = 1; if (target.isRare) mobDiffMult = 2; //Who should get xp? let aggroList = target.aggro.list; let aLen = aggroList.length; for (let i = 0; i < aLen; i++) { let a = aggroList[i]; let dmg = a.damage; if (dmg <= 0) continue; let mult = 1; //How many party members contributed // Remember, maybe one of the aggro-ees might be a mob too let party = a.obj.social ? a.obj.social.party : null; if (party) { let partySize = aggroList.filter(function (f) { return ((a.damage > 0) && (party.indexOf(f.obj.serverId) > -1)); }).length; partySize--; mult = (1 + (partySize * 0.1)); } if (a.obj.player) { a.obj.auth.track('combat', 'kill', target.name); //Scale xp by source level so you can't just farm low level mobs (or get boosted on high level mobs). //Mobs that are farther then 10 levels from you, give no xp //We don't currently do this for quests/herb gathering let sourceLevel = a.obj.stats.values.level; let levelDelta = level - sourceLevel; let amount = null; if (Math.abs(levelDelta) <= 10) amount = ~~(((sourceLevel + levelDelta) * 10) * Math.pow(1 - (Math.abs(levelDelta) / 10), 2) * mult * mobDiffMult); else amount = 0; a.obj.stats.getXp(amount, this.obj, target); } a.obj.fireEvent('afterKillMob', target); } }, preDeath: function (source) { const obj = this.obj; let killSource = source; if (source.follower) killSource = source.follower.master; if (killSource.stats) killSource.stats.kill(obj); const deathEvent = { target: obj, source: killSource }; obj.instance.eventEmitter.emit('onAfterActorDies', deathEvent); obj.fireEvent('afterDeath', deathEvent); if (obj.player) { obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp); if (deathEvent.permadeath) { obj.auth.permadie(); obj.instance.syncer.queue('onGetMessages', { messages: { class: 'color-redA', message: `(level ${this.values.level}) ${obj.name} has forever left the shores of the living.` } }, -1); this.syncer.queue('onPermadeath', { source: killSource.name }, [obj.serverId]); } else this.values.hp = 0; obj.player.die(killSource, deathEvent.permadeath); } else { if (obj.effects) obj.effects.die(); if (this.obj.spellbook) this.obj.spellbook.die(); obj.destroyed = true; obj.destructionEvent = 'death'; let deathAnimation = _.getDeepProperty(animations, ['mobs', obj.sheetName, obj.cell, 'death']); if (deathAnimation) { obj.instance.syncer.queue('onGetObject', { x: obj.x, y: obj.y, components: [deathAnimation] }, -1); } if (obj.inventory) { let aggroList = obj.aggro.list; let aLen = aggroList.length; for (let i = 0; i < aLen; i++) { let a = aggroList[i]; if (a.damage <= 0 || !a.obj.has('serverId')) continue; obj.inventory.dropBag(a.obj.name, killSource); } } } }, die: function (source) { die(this, source); }, respawn: function () { if (!this.obj.dead) return; this.obj.syncer.set(true, null, 'dead', false); let obj = this.obj; let syncO = obj.syncer.o; this.obj.dead = false; let values = this.values; values.hp = values.hpMax; values.mana = values.manaMax; obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); obj.syncer.setObject(false, 'stats', 'values', 'mana', values.mana); obj.hidden = false; obj.nonSelectable = false; syncO.hidden = false; syncO.nonSelectable = false; process.send({ method: 'object', serverId: this.obj.serverId, obj: { dead: false } }); obj.instance.syncer.queue('onGetObject', { x: obj.x, y: obj.y, components: [{ type: 'attackAnimation', row: 0, col: 4 }] }, -1); this.obj.player.respawn(); }, addLevelAttributes: function (singleLevel) { const gainStats = spirits.stats[this.obj.class].gainStats; const count = singleLevel ? 1 : this.values.level; for (let s in gainStats) this.addStat(s, gainStats[s] * count); }, takeDamage: function (damage, threatMult, source) { if (this.values.hp <= 0) return; let obj = this.obj; if (!damage.noEvents) { source.fireEvent('beforeDealDamage', damage, obj); obj.fireEvent('beforeTakeDamage', damage, source); } if (damage.failed || obj.destroyed) return; let amount = Math.min(this.values.hp, damage.amount); damage.dealt = amount; let msg = { id: obj.id, source: source.id, crit: damage.crit, amount: amount, element: damage.element }; this.values.hp -= amount; let recipients = []; if (obj.serverId) recipients.push(obj.serverId); if (source.serverId) recipients.push(source.serverId); if (source.follower && source.follower.master.serverId) { recipients.push(source.follower.master.serverId); msg.masterSource = source.follower.master.id; } if (obj.follower && obj.follower.master.serverId) { recipients.push(obj.follower.master.serverId); msg.masterId = obj.follower.master.id; } if (recipients.length) { if (!damage.blocked && !damage.dodged) this.syncer.queue('onGetDamage', msg, recipients); else { this.syncer.queue('onGetDamage', { id: obj.id, source: source.id, event: true, text: damage.blocked ? 'blocked' : 'dodged' }, recipients); } } obj.aggro.tryEngage(source, amount, threatMult); let died = (this.values.hp <= 0); if (died) { let death = { success: true }; obj.instance.eventEmitter.emit('onBeforeActorDies', death, obj, source); obj.fireEvent('beforeDeath', death); if (death.success) this.preDeath(source); } else { source.aggro.tryEngage(obj, 0); obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp); } if (!damage.noEvents) source.fireEvent('afterDealDamage', damage, obj); }, getHp: function (heal, source) { let amount = heal.amount; if (amount === 0) return; let threatMult = heal.threatMult; if (!heal.has('threatMult')) threatMult = 1; let values = this.values; let hpMax = values.hpMax; if (values.hp >= hpMax) return; if (hpMax - values.hp < amount) amount = hpMax - values.hp; values.hp += amount; if (values.hp > hpMax) values.hp = hpMax; let recipients = []; if (this.obj.serverId) recipients.push(this.obj.serverId); if (source.serverId) recipients.push(source.serverId); if (recipients.length > 0) { this.syncer.queue('onGetDamage', { id: this.obj.id, source: source.id, heal: true, amount: amount, crit: heal.crit, element: heal.element }, recipients); } //Add aggro to all our attackers let threat = amount * 0.4 * threatMult; if (threat !== 0) { let aggroList = this.obj.aggro.list; let aLen = aggroList.length; for (let i = 0; i < aLen; i++) { let a = aggroList[i].obj; a.aggro.tryEngage(source, threat); } } this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); }, save: function () { if (this.sessionDuration) { this.stats.played = ~~(this.stats.played + this.sessionDuration); delete this.sessionDuration; } const values = this.values; return { type: 'stats', values: { level: values.level, xp: values.xp, xpTotal: values.xpTotal, hp: values.hp, mana: values.mana }, stats: this.stats }; }, simplify: function (self) { let values = this.values; if (!self) { let result = { type: 'stats', values: { hp: values.hp, hpMax: values.hpMax, mana: values.mana, manaMax: values.manaMax, level: values.level } }; return result; } return { type: 'stats', values: values, stats: this.stats, vitScale: this.vitScale }; }, simplifyTransfer: function () { return { type: 'stats', values: this.values, stats: this.stats }; }, onLogin: function () { let stats = this.stats; let time = scheduler.getTime(); stats.lastLogin = time; }, getKillStreakCoefficient: function (mobName) { let killStreak = this.stats.mobKillStreaks[mobName]; if (!killStreak) return 1; return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000); }, canGetMobLoot: function (mob) { if (!mob.inventory.dailyDrops) return true; let lootStats = this.stats.lootStats[mob.name]; let time = scheduler.getTime(); if (!lootStats) this.stats.lootStats[mob.name] = time; else return ((lootStats.day !== time.day) || (lootStats.month !== time.month)); }, events: { afterKillMob: function (mob) { let mobKillStreaks = this.stats.mobKillStreaks; let mobName = mob.name; if (!mobKillStreaks[mobName]) mobKillStreaks[mobName] = 0; if (mobKillStreaks[mobName] < 100) mobKillStreaks[mobName]++; for (let p in mobKillStreaks) { if (p === mobName) continue; mobKillStreaks[p]--; if (mobKillStreaks[p] <= 0) delete mobKillStreaks[p]; } }, beforeGetXp: function (event) { if ((!event.target.mob) && (!event.target.player)) return; event.amount *= this.getKillStreakCoefficient(event.target.name); }, beforeGenerateLoot: function (event) { if (!event.source.mob) return; event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name); if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source))) event.chanceMultiplier = 0; }, afterMove: function (event) { let mobKillStreaks = this.stats.mobKillStreaks; for (let p in mobKillStreaks) { mobKillStreaks[p] -= 0.085; if (mobKillStreaks[p] <= 0) delete mobKillStreaks[p]; } }, afterDealDamage: function (damageEvent, target) { if (damageEvent.element) return; const { obj, values: { lifeOnHit } } = this; if (target === obj || !lifeOnHit) return; this.getHp({ amount: lifeOnHit }, obj); } } };