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.
 
 
 

794 lines
18 KiB

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