You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

742 rivejä
17 KiB

  1. let animations = require('../config/animations');
  2. let spirits = require('../config/spirits');
  3. let scheduler = require('../misc/scheduler');
  4. const baseStats = require('./stats/baseStats');
  5. module.exports = {
  6. type: 'stats',
  7. values: baseStats,
  8. statScales: {
  9. vitToHp: 10,
  10. strToArmor: 1,
  11. intToMana: (1 / 6),
  12. dexToDodge: (1 / 12)
  13. },
  14. syncer: null,
  15. stats: {
  16. logins: 0,
  17. played: 0,
  18. lastLogin: null,
  19. loginStreak: 0,
  20. mobKillStreaks: {},
  21. lootStats: {}
  22. },
  23. dead: false,
  24. init: function (blueprint) {
  25. this.syncer = this.obj.instance.syncer;
  26. let values = (blueprint || {}).values || {};
  27. for (let v in values)
  28. this.values[v] = values[v];
  29. let stats = (blueprint || {}).stats || {};
  30. for (let v in stats)
  31. this.stats[v] = stats[v];
  32. if (this.obj.player) {
  33. this.calcXpMax();
  34. this.addLevelAttributes();
  35. this.calcHpMax();
  36. }
  37. if (blueprint)
  38. delete blueprint.stats;
  39. },
  40. resetHp: function () {
  41. let values = this.values;
  42. values.hp = values.hpMax;
  43. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  44. },
  45. update: function () {
  46. if ((this.obj.mob && !this.obj.follower) || this.obj.dead)
  47. return;
  48. let values = this.values;
  49. let manaMax = values.manaMax;
  50. manaMax -= (manaMax * values.manaReservePercent);
  51. let regen = {
  52. success: true
  53. };
  54. this.obj.fireEvent('beforeRegen', regen);
  55. if (!regen.success)
  56. return;
  57. let isInCombat = this.obj.aggro && this.obj.aggro.list.length > 0;
  58. if (this.obj.follower) {
  59. isInCombat = (this.obj.follower.master.aggro.list.length > 0);
  60. if (isInCombat)
  61. return;
  62. }
  63. let regenHp = 0;
  64. let regenMana = 0;
  65. regenMana = values.regenMana / 50;
  66. if (!isInCombat)
  67. regenHp = Math.max(values.hpMax / 112, values.regenHp * 0.2);
  68. else
  69. regenHp = values.regenHp * 0.2;
  70. if (values.hp < values.hpMax) {
  71. values.hp += regenHp;
  72. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  73. }
  74. if (values.hp > values.hpMax) {
  75. values.hp = values.hpMax;
  76. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  77. }
  78. if (values.mana < manaMax) {
  79. values.mana += regenMana;
  80. this.obj.syncer.setObject(!this.obj.player, 'stats', 'values', 'mana', values.mana);
  81. }
  82. if (values.mana > manaMax) {
  83. values.mana = manaMax;
  84. this.obj.syncer.setObject(!this.obj.player, 'stats', 'values', 'mana', values.mana);
  85. }
  86. },
  87. addStat: function (stat, value) {
  88. let values = this.values;
  89. if (['lvlRequire', 'allAttributes'].indexOf(stat) === -1)
  90. values[stat] += value;
  91. let sendOnlyToSelf = (['hp', 'hpMax', 'mana', 'manaMax', 'vit'].indexOf(stat) === -1);
  92. this.obj.syncer.setObject(sendOnlyToSelf, 'stats', 'values', stat, values[stat]);
  93. if (sendOnlyToSelf)
  94. this.obj.syncer.setObject(false, 'stats', 'values', stat, values[stat]);
  95. if (['addCritChance', 'addAttackCritChance', 'addSpellCritChance'].indexOf(stat) > -1) {
  96. let morphStat = stat.substr(3);
  97. morphStat = morphStat[0].toLowerCase() + morphStat.substr(1);
  98. this.addStat(morphStat, (0.05 * value));
  99. } else if (['addCritMultiplier', 'addAttackCritMultiplier', 'addSpellCritMultiplier'].indexOf(stat) > -1) {
  100. let morphStat = stat.substr(3);
  101. morphStat = morphStat[0].toLowerCase() + morphStat.substr(1);
  102. this.addStat(morphStat, value);
  103. } else if (stat === 'vit')
  104. this.addStat('hpMax', (value * this.statScales.vitToHp));
  105. else if (stat === 'allAttributes') {
  106. ['int', 'str', 'dex'].forEach(function (s) {
  107. this.addStat(s, value);
  108. }, this);
  109. } else if (stat === 'elementAllResist') {
  110. ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) {
  111. let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Resist';
  112. this.addStat(element, value);
  113. }, this);
  114. } else if (stat === 'elementPercent') {
  115. ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) {
  116. let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Percent';
  117. this.addStat(element, value);
  118. }, this);
  119. } else if (stat === 'str')
  120. this.addStat('armor', (value * this.statScales.strToArmor));
  121. else if (stat === 'int')
  122. this.addStat('manaMax', (value * this.statScales.intToMana));
  123. else if (stat === 'dex')
  124. this.addStat('dodgeAttackChance', (value * this.statScales.dexToDodge));
  125. },
  126. calcXpMax: function () {
  127. let level = this.values.level;
  128. this.values.xpMax = (level * 5) + ~~(level * 10 * Math.pow(level, 2.2)) - 5;
  129. this.obj.syncer.setObject(true, 'stats', 'values', 'xpMax', this.values.xpMax);
  130. },
  131. calcHpMax: function () {
  132. const spiritConfig = spirits.stats[this.obj.class];
  133. const initialHp = spiritConfig ? spiritConfig.values.hpMax : 32.7;
  134. let increase = spiritConfig ? spiritConfig.values.hpPerLevel : 32.7;
  135. this.values.hpMax = initialHp + (((this.values.level || 1) - 1) * increase);
  136. },
  137. //Source is the object that caused you to gain xp (mostly yourself)
  138. //Target is the source of the xp (a mob or quest)
  139. getXp: function (amount, source, target) {
  140. let obj = this.obj;
  141. let values = this.values;
  142. if (values.level === consts.maxLevel)
  143. return;
  144. let xpEvent = {
  145. source: source,
  146. target: target,
  147. amount: amount,
  148. multiplier: 1
  149. };
  150. obj.fireEvent('beforeGetXp', xpEvent);
  151. if (xpEvent.amount === 0)
  152. return;
  153. obj.instance.eventEmitter.emitNoSticky('onBeforeGetGlobalXpMultiplier', xpEvent);
  154. amount = ~~(xpEvent.amount * (1 + (values.xpIncrease / 100)) * xpEvent.multiplier);
  155. values.xpTotal = ~~(values.xpTotal + amount);
  156. values.xp = ~~(values.xp + amount);
  157. obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  158. this.syncer.queue('onGetDamage', {
  159. id: obj.id,
  160. event: true,
  161. text: '+' + amount + ' xp'
  162. }, -1);
  163. let syncO = {};
  164. let didLevelUp = false;
  165. while (values.xp >= values.xpMax) {
  166. didLevelUp = true;
  167. values.xp -= values.xpMax;
  168. obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  169. values.level++;
  170. obj.fireEvent('onLevelUp', this.values.level);
  171. if (values.level === consts.maxLevel)
  172. values.xp = 0;
  173. this.calcHpMax();
  174. obj.syncer.setObject(true, 'stats', 'values', 'hpMax', values.hpMax);
  175. this.addLevelAttributes(true);
  176. obj.spellbook.calcDps();
  177. this.syncer.queue('onGetDamage', {
  178. id: obj.id,
  179. event: true,
  180. text: 'level up'
  181. }, -1);
  182. syncO.level = values.level;
  183. this.calcXpMax();
  184. }
  185. if (didLevelUp) {
  186. let cellContents = obj.instance.physics.getCell(obj.x, obj.y);
  187. cellContents.forEach(function (c) {
  188. c.fireEvent('onCellPlayerLevelUp', obj);
  189. });
  190. obj.auth.doSave();
  191. }
  192. process.send({
  193. method: 'object',
  194. serverId: this.obj.serverId,
  195. obj: syncO
  196. });
  197. if (didLevelUp) {
  198. this.obj.syncer.setObject(true, 'stats', 'values', 'hpMax', values.hpMax);
  199. this.obj.syncer.setObject(true, 'stats', 'values', 'level', values.level);
  200. this.obj.syncer.setObject(false, 'stats', 'values', 'hpMax', values.hpMax);
  201. this.obj.syncer.setObject(false, 'stats', 'values', 'level', values.level);
  202. }
  203. },
  204. kill: function (target) {
  205. this.obj.auth.track('combat', 'kill', target.name);
  206. if (target.player)
  207. return;
  208. let level = target.stats.values.level;
  209. let mobDiffMult = 1;
  210. if (target.isRare)
  211. mobDiffMult = 2;
  212. else if (target.isChampion)
  213. mobDiffMult = 5;
  214. //Who should get xp?
  215. let aggroList = target.aggro.list;
  216. let aLen = aggroList.length;
  217. for (let i = 0; i < aLen; i++) {
  218. let a = aggroList[i];
  219. let dmg = a.damage;
  220. if (dmg <= 0)
  221. continue;
  222. let mult = 1;
  223. //How many party members contributed
  224. // Remember, maybe one of the aggro-ees might be a mob too
  225. let party = a.obj.social ? a.obj.social.party : null;
  226. if (party) {
  227. let partySize = aggroList.filter(function (f) {
  228. return ((a.damage > 0) && (party.indexOf(f.obj.serverId) > -1));
  229. }).length;
  230. partySize--;
  231. mult = (1 + (partySize * 0.1));
  232. }
  233. if (a.obj.player) {
  234. //Scale xp by source level so you can't just farm low level mobs (or get boosted on high level mobs).
  235. //Mobs that are farther then 10 levels from you, give no xp
  236. //We don't currently do this for quests/herb gathering
  237. let sourceLevel = a.obj.stats.values.level;
  238. let levelDelta = level - sourceLevel;
  239. let amount = null;
  240. if (Math.abs(levelDelta) <= 10)
  241. amount = ~~(((sourceLevel + levelDelta) * 10) * Math.pow(1 - (Math.abs(levelDelta) / 10), 2) * mult * mobDiffMult);
  242. else
  243. amount = 0;
  244. a.obj.stats.getXp(amount, this.obj, target);
  245. }
  246. a.obj.fireEvent('afterKillMob', target);
  247. }
  248. },
  249. preDeath: function (source) {
  250. const obj = this.obj;
  251. let killSource = source;
  252. if (source.follower)
  253. killSource = source.follower.master;
  254. if (killSource.player)
  255. killSource.stats.kill(obj);
  256. const deathEvent = {
  257. source: killSource
  258. };
  259. obj.fireEvent('afterDeath', deathEvent);
  260. if (obj.player) {
  261. obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  262. if (deathEvent.permadeath) {
  263. obj.auth.permadie();
  264. obj.instance.syncer.queue('onGetMessages', {
  265. messages: {
  266. class: 'color-redA',
  267. message: `(level ${this.values.level}) ${obj.name} has forever left the shores of the living.`
  268. }
  269. }, -1);
  270. this.syncer.queue('onPermadeath', {
  271. source: killSource.name
  272. }, [obj.serverId]);
  273. } else
  274. this.values.hp = 0;
  275. obj.player.die(killSource, deathEvent.permadeath);
  276. } else {
  277. obj.effects.die();
  278. if (this.obj.spellbook)
  279. this.obj.spellbook.die();
  280. obj.destroyed = true;
  281. let deathAnimation = _.getDeepProperty(animations, ['mobs', obj.sheetName, obj.cell, 'death']);
  282. if (deathAnimation) {
  283. obj.instance.syncer.queue('onGetObject', {
  284. x: obj.x,
  285. y: obj.y,
  286. components: [deathAnimation]
  287. }, -1);
  288. }
  289. if (obj.inventory) {
  290. let aggroList = obj.aggro.list;
  291. let aLen = aggroList.length;
  292. for (let i = 0; i < aLen; i++) {
  293. let a = aggroList[i];
  294. if (a.damage <= 0 || !a.obj.has('serverId'))
  295. continue;
  296. obj.inventory.dropBag(a.obj.name, killSource);
  297. }
  298. }
  299. }
  300. },
  301. die: function (source) {
  302. let obj = this.obj;
  303. let values = this.values;
  304. this.syncer.queue('onGetDamage', {
  305. id: obj.id,
  306. event: true,
  307. text: 'death'
  308. }, -1);
  309. obj.syncer.set(true, null, 'dead', true);
  310. let syncO = obj.syncer.o;
  311. obj.hidden = true;
  312. obj.nonSelectable = true;
  313. syncO.hidden = true;
  314. syncO.nonSelectable = true;
  315. let xpLoss = ~~Math.min(values.xp, values.xpMax * 0.05);
  316. values.xp -= xpLoss;
  317. obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  318. this.syncer.queue('onDeath', {
  319. source: source.name,
  320. xpLoss: xpLoss
  321. }, [obj.serverId]);
  322. obj.instance.syncer.queue('onGetObject', {
  323. x: obj.x,
  324. y: obj.y,
  325. components: [{
  326. type: 'attackAnimation',
  327. row: 0,
  328. col: 4
  329. }]
  330. }, -1);
  331. },
  332. respawn: function () {
  333. if (!this.obj.dead)
  334. return;
  335. this.obj.syncer.set(true, null, 'dead', false);
  336. let obj = this.obj;
  337. let syncO = obj.syncer.o;
  338. this.obj.dead = false;
  339. let values = this.values;
  340. values.hp = values.hpMax;
  341. values.mana = values.manaMax;
  342. obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  343. obj.syncer.setObject(false, 'stats', 'values', 'mana', values.mana);
  344. obj.hidden = false;
  345. obj.nonSelectable = false;
  346. syncO.hidden = false;
  347. syncO.nonSelectable = false;
  348. process.send({
  349. method: 'object',
  350. serverId: this.obj.serverId,
  351. obj: {
  352. dead: false
  353. }
  354. });
  355. obj.instance.syncer.queue('onGetObject', {
  356. x: obj.x,
  357. y: obj.y,
  358. components: [{
  359. type: 'attackAnimation',
  360. row: 0,
  361. col: 4
  362. }]
  363. }, -1);
  364. this.obj.player.respawn();
  365. },
  366. addLevelAttributes: function (singleLevel) {
  367. const gainStats = spirits.stats[this.obj.class].gainStats;
  368. const count = singleLevel ? 1 : this.values.level;
  369. for (let s in gainStats)
  370. this.addStat(s, gainStats[s] * count);
  371. },
  372. takeDamage: function (damage, threatMult, source) {
  373. if (this.values.hp <= 0)
  374. return;
  375. let obj = this.obj;
  376. source.fireEvent('beforeDealDamage', damage, obj);
  377. obj.fireEvent('beforeTakeDamage', damage, source);
  378. if (damage.failed || obj.destroyed)
  379. return;
  380. let amount = Math.min(this.values.hp, damage.amount);
  381. damage.dealt = amount;
  382. let msg = {
  383. id: obj.id,
  384. source: source.id,
  385. crit: damage.crit,
  386. amount: amount
  387. };
  388. this.values.hp -= amount;
  389. let recipients = [];
  390. if (obj.serverId)
  391. recipients.push(obj.serverId);
  392. if (source.serverId)
  393. recipients.push(source.serverId);
  394. if (source.follower && source.follower.master.serverId) {
  395. recipients.push(source.follower.master.serverId);
  396. msg.masterSource = source.follower.master.id;
  397. }
  398. if (obj.follower && obj.follower.master.serverId) {
  399. recipients.push(obj.follower.master.serverId);
  400. msg.masterId = obj.follower.master.id;
  401. }
  402. if (recipients.length) {
  403. if (!damage.blocked && !damage.dodged)
  404. this.syncer.queue('onGetDamage', msg, recipients);
  405. else {
  406. this.syncer.queue('onGetDamage', {
  407. id: obj.id,
  408. source: source.id,
  409. event: true,
  410. text: damage.blocked ? 'blocked' : 'dodged'
  411. }, recipients);
  412. }
  413. }
  414. obj.aggro.tryEngage(source, amount, threatMult);
  415. let died = (this.values.hp <= 0);
  416. if (died) {
  417. let death = {
  418. success: true
  419. };
  420. obj.instance.eventEmitter.emitNoSticky('onBeforeActorDies', death, obj, source);
  421. obj.fireEvent('beforeDeath', death);
  422. if (death.success)
  423. this.preDeath(source);
  424. } else {
  425. source.aggro.tryEngage(obj, 0);
  426. obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  427. }
  428. if (!damage.noEvents)
  429. source.fireEvent('afterDealDamage', damage, obj);
  430. },
  431. getHp: function (heal, source) {
  432. let amount = heal.amount;
  433. if (amount === 0)
  434. return;
  435. let threatMult = heal.threatMult;
  436. if (!heal.has('threatMult'))
  437. threatMult = 1;
  438. let values = this.values;
  439. let hpMax = values.hpMax;
  440. if (values.hp >= hpMax)
  441. return;
  442. if (hpMax - values.hp < amount)
  443. amount = hpMax - values.hp;
  444. values.hp += amount;
  445. if (values.hp > hpMax)
  446. values.hp = hpMax;
  447. let recipients = [];
  448. if (this.obj.serverId)
  449. recipients.push(this.obj.serverId);
  450. if (source.serverId)
  451. recipients.push(source.serverId);
  452. if (recipients.length > 0) {
  453. this.syncer.queue('onGetDamage', {
  454. id: this.obj.id,
  455. source: source.id,
  456. heal: true,
  457. amount: amount,
  458. crit: heal.crit
  459. }, recipients);
  460. }
  461. //Add aggro to all our attackers
  462. let threat = amount * 0.4 * threatMult;
  463. if (threat !== 0) {
  464. let aggroList = this.obj.aggro.list;
  465. let aLen = aggroList.length;
  466. for (let i = 0; i < aLen; i++) {
  467. let a = aggroList[i].obj;
  468. a.aggro.tryEngage(source, threat);
  469. }
  470. }
  471. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  472. },
  473. save: function () {
  474. if (this.sessionDuration) {
  475. this.stats.played = ~~(this.stats.played + this.sessionDuration);
  476. delete this.sessionDuration;
  477. }
  478. const values = this.values;
  479. return {
  480. type: 'stats',
  481. values: {
  482. level: values.level,
  483. xp: values.xp,
  484. xpTotal: values.xpTotal,
  485. hp: values.hp,
  486. mana: values.mana
  487. },
  488. stats: this.stats
  489. };
  490. },
  491. simplify: function (self) {
  492. let values = this.values;
  493. if (!self) {
  494. let result = {
  495. type: 'stats',
  496. values: {
  497. hp: values.hp,
  498. hpMax: values.hpMax,
  499. mana: values.mana,
  500. manaMax: values.manaMax,
  501. level: values.level
  502. }
  503. };
  504. return result;
  505. }
  506. return {
  507. type: 'stats',
  508. values: values,
  509. stats: this.stats,
  510. vitScale: this.vitScale
  511. };
  512. },
  513. simplifyTransfer: function () {
  514. return {
  515. type: 'stats',
  516. values: this.values,
  517. stats: this.stats
  518. };
  519. },
  520. onLogin: function () {
  521. let stats = this.stats;
  522. let time = scheduler.getTime();
  523. stats.lastLogin = time;
  524. this.obj.instance.mail.getMail(this.obj.name);
  525. },
  526. getKillStreakCoefficient: function (mobName) {
  527. let killStreak = this.stats.mobKillStreaks[mobName];
  528. if (!killStreak)
  529. return 1;
  530. return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000);
  531. },
  532. canGetMobLoot: function (mob) {
  533. if (!mob.inventory.dailyDrops)
  534. return true;
  535. let lootStats = this.stats.lootStats[mob.name];
  536. let time = scheduler.getTime();
  537. if (!lootStats)
  538. this.stats.lootStats[mob.name] = time;
  539. else
  540. return ((lootStats.day !== time.day), (lootStats.month !== time.month));
  541. },
  542. events: {
  543. afterKillMob: function (mob) {
  544. let mobKillStreaks = this.stats.mobKillStreaks;
  545. let mobName = mob.name;
  546. if (!mobKillStreaks[mobName])
  547. mobKillStreaks[mobName] = 0;
  548. if (mobKillStreaks[mobName] < 100)
  549. mobKillStreaks[mobName]++;
  550. for (let p in mobKillStreaks) {
  551. if (p === mobName)
  552. continue;
  553. mobKillStreaks[p]--;
  554. if (mobKillStreaks[p] <= 0)
  555. delete mobKillStreaks[p];
  556. }
  557. },
  558. beforeGetXp: function (event) {
  559. if ((!event.target.mob) && (!event.target.player))
  560. return;
  561. event.amount *= this.getKillStreakCoefficient(event.target.name);
  562. },
  563. beforeGenerateLoot: function (event) {
  564. if (!event.source.mob)
  565. return;
  566. event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name);
  567. if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source)))
  568. event.chanceMultiplier = 0;
  569. },
  570. afterMove: function (event) {
  571. let mobKillStreaks = this.stats.mobKillStreaks;
  572. for (let p in mobKillStreaks) {
  573. mobKillStreaks[p] -= 0.085;
  574. if (mobKillStreaks[p] <= 0)
  575. delete mobKillStreaks[p];
  576. }
  577. },
  578. afterDealDamage: function (damageEvent, target) {
  579. const { obj, values: { lifeOnHit } } = this;
  580. if (target === obj || !lifeOnHit)
  581. return;
  582. this.getHp({ amount: lifeOnHit }, obj);
  583. }
  584. }
  585. };