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.
 
 
 

807 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. //Who should get xp?
  274. let aggroList = target.aggro.list;
  275. let aLen = aggroList.length;
  276. for (let i = 0; i < aLen; i++) {
  277. let a = aggroList[i];
  278. let dmg = a.damage;
  279. if (dmg <= 0)
  280. continue;
  281. let mult = 1;
  282. //How many party members contributed
  283. // Remember, maybe one of the aggro-ees might be a mob too
  284. let party = a.obj.social ? a.obj.social.party : null;
  285. if (party) {
  286. let partySize = aggroList.filter(function (f) {
  287. return ((a.damage > 0) && (party.indexOf(f.obj.serverId) > -1));
  288. }).length;
  289. partySize--;
  290. mult = (1 + (partySize * 0.1));
  291. }
  292. if (a.obj.player) {
  293. a.obj.auth.track('combat', 'kill', target.name);
  294. //Scale xp by source level so you can't just farm low level mobs (or get boosted on high level mobs).
  295. //Mobs that are farther then 10 levels from you, give no xp
  296. //We don't currently do this for quests/herb gathering
  297. let sourceLevel = a.obj.stats.values.level;
  298. let levelDelta = level - sourceLevel;
  299. let amount = null;
  300. if (Math.abs(levelDelta) <= 10)
  301. amount = ~~(((sourceLevel + levelDelta) * 10) * Math.pow(1 - (Math.abs(levelDelta) / 10), 2) * mult * mobDiffMult);
  302. else
  303. amount = 0;
  304. a.obj.stats.getXp(amount, this.obj, target);
  305. }
  306. a.obj.fireEvent('afterKillMob', target);
  307. }
  308. },
  309. preDeath: function (source) {
  310. const obj = this.obj;
  311. let killSource = source;
  312. if (source.follower)
  313. killSource = source.follower.master;
  314. if (killSource.stats)
  315. killSource.stats.kill(obj);
  316. const deathEvent = {
  317. target: obj,
  318. source: killSource
  319. };
  320. obj.instance.eventEmitter.emit('onAfterActorDies', deathEvent);
  321. obj.fireEvent('afterDeath', deathEvent);
  322. if (obj.player) {
  323. obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  324. if (deathEvent.permadeath) {
  325. obj.auth.permadie();
  326. obj.instance.syncer.queue('onGetMessages', {
  327. messages: {
  328. class: 'color-redA',
  329. message: `(level ${this.values.level}) ${obj.name} has forever left the shores of the living.`
  330. }
  331. }, -1);
  332. this.syncer.queue('onPermadeath', {
  333. source: killSource.name
  334. }, [obj.serverId]);
  335. } else
  336. this.values.hp = 0;
  337. obj.player.die(killSource, deathEvent.permadeath);
  338. } else {
  339. if (obj.effects)
  340. obj.effects.die();
  341. if (this.obj.spellbook)
  342. this.obj.spellbook.die();
  343. obj.destroyed = true;
  344. obj.destructionEvent = 'death';
  345. let deathAnimation = _.getDeepProperty(animations, ['mobs', obj.sheetName, obj.cell, 'death']);
  346. if (deathAnimation) {
  347. obj.instance.syncer.queue('onGetObject', {
  348. x: obj.x,
  349. y: obj.y,
  350. components: [deathAnimation]
  351. }, -1);
  352. }
  353. if (obj.inventory) {
  354. let aggroList = obj.aggro.list;
  355. let aLen = aggroList.length;
  356. for (let i = 0; i < aLen; i++) {
  357. let a = aggroList[i];
  358. if (a.damage <= 0 || !a.obj.has('serverId'))
  359. continue;
  360. obj.inventory.dropBag(a.obj.name, killSource);
  361. }
  362. }
  363. }
  364. },
  365. die: function (source) {
  366. die(this, source);
  367. },
  368. respawn: function () {
  369. if (!this.obj.dead)
  370. return;
  371. this.obj.syncer.set(true, null, 'dead', false);
  372. let obj = this.obj;
  373. let syncO = obj.syncer.o;
  374. this.obj.dead = false;
  375. let values = this.values;
  376. values.hp = values.hpMax;
  377. values.mana = values.manaMax;
  378. obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  379. obj.syncer.setObject(false, 'stats', 'values', 'mana', values.mana);
  380. obj.hidden = false;
  381. obj.nonSelectable = false;
  382. syncO.hidden = false;
  383. syncO.nonSelectable = false;
  384. process.send({
  385. method: 'object',
  386. serverId: this.obj.serverId,
  387. obj: {
  388. dead: false
  389. }
  390. });
  391. obj.instance.syncer.queue('onGetObject', {
  392. x: obj.x,
  393. y: obj.y,
  394. components: [{
  395. type: 'attackAnimation',
  396. row: 0,
  397. col: 4
  398. }]
  399. }, -1);
  400. this.obj.player.respawn();
  401. },
  402. addLevelAttributes: function (singleLevel) {
  403. const gainStats = spirits.stats[this.obj.class].gainStats;
  404. const count = singleLevel ? 1 : this.values.level;
  405. for (let s in gainStats)
  406. this.addStat(s, gainStats[s] * count);
  407. },
  408. takeDamage: function (damage, threatMult, source) {
  409. if (this.values.hp <= 0)
  410. return;
  411. let obj = this.obj;
  412. if (!damage.noEvents) {
  413. source.fireEvent('beforeDealDamage', damage, obj);
  414. obj.fireEvent('beforeTakeDamage', damage, source);
  415. }
  416. if (damage.failed || obj.destroyed)
  417. return;
  418. let amount = Math.min(this.values.hp, damage.amount);
  419. damage.dealt = amount;
  420. let msg = {
  421. id: obj.id,
  422. source: source.id,
  423. crit: damage.crit,
  424. amount: amount,
  425. element: damage.element
  426. };
  427. this.values.hp -= amount;
  428. let recipients = [];
  429. if (obj.serverId)
  430. recipients.push(obj.serverId);
  431. if (source.serverId)
  432. recipients.push(source.serverId);
  433. if (source.follower && source.follower.master.serverId) {
  434. recipients.push(source.follower.master.serverId);
  435. msg.masterSource = source.follower.master.id;
  436. }
  437. if (obj.follower && obj.follower.master.serverId) {
  438. recipients.push(obj.follower.master.serverId);
  439. msg.masterId = obj.follower.master.id;
  440. }
  441. if (recipients.length) {
  442. if (!damage.blocked && !damage.dodged)
  443. this.syncer.queue('onGetDamage', msg, recipients);
  444. else {
  445. this.syncer.queue('onGetDamage', {
  446. id: obj.id,
  447. source: source.id,
  448. event: true,
  449. text: damage.blocked ? 'blocked' : 'dodged'
  450. }, recipients);
  451. }
  452. }
  453. obj.aggro.tryEngage(source, amount, threatMult);
  454. let died = (this.values.hp <= 0);
  455. if (died) {
  456. let death = {
  457. success: true
  458. };
  459. obj.instance.eventEmitter.emit('onBeforeActorDies', death, obj, source);
  460. obj.fireEvent('beforeDeath', death);
  461. if (death.success)
  462. this.preDeath(source);
  463. } else {
  464. source.aggro.tryEngage(obj, 0);
  465. obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  466. }
  467. if (!damage.noEvents)
  468. source.fireEvent('afterDealDamage', damage, obj);
  469. },
  470. /*
  471. Gives hp to heal.target
  472. heal: Damage object returned by combat.getDamage
  473. source: Source object
  474. event: Optional config object. We want to eventually phase out the first 2 args.
  475. heal: Same as 1st parameter
  476. source: Same as 2nd parameter
  477. target: Target object (heal.target)
  478. spell: Optional spell object that caused this event
  479. */
  480. getHp: function (heal, source, event) {
  481. let amount = heal.amount;
  482. if (amount === 0)
  483. return;
  484. let threatMult = heal.threatMult;
  485. if (!heal.has('threatMult'))
  486. threatMult = 1;
  487. let values = this.values;
  488. let hpMax = values.hpMax;
  489. if (values.hp >= hpMax)
  490. return;
  491. if (hpMax - values.hp < amount)
  492. amount = hpMax - values.hp;
  493. values.hp += amount;
  494. if (values.hp > hpMax)
  495. values.hp = hpMax;
  496. let recipients = [];
  497. if (this.obj.serverId)
  498. recipients.push(this.obj.serverId);
  499. if (source.serverId)
  500. recipients.push(source.serverId);
  501. if (recipients.length > 0) {
  502. this.syncer.queue('onGetDamage', {
  503. id: this.obj.id,
  504. source: source.id,
  505. heal: true,
  506. amount: amount,
  507. crit: heal.crit,
  508. element: heal.element
  509. }, recipients);
  510. }
  511. //Add aggro to all our attackers
  512. let threat = amount * 0.4 * threatMult;
  513. if (threat !== 0) {
  514. let aggroList = this.obj.aggro.list;
  515. let aLen = aggroList.length;
  516. for (let i = 0; i < aLen; i++) {
  517. let a = aggroList[i].obj;
  518. a.aggro.tryEngage(source, threat);
  519. }
  520. }
  521. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  522. //We want to eventually replace the first two args with the event object
  523. // For now, only fire the event when the event object is specified.
  524. if (!heal.noEvents && event)
  525. source.fireEvent('afterGiveHp', event);
  526. },
  527. save: function () {
  528. if (this.sessionDuration) {
  529. this.stats.played = ~~(this.stats.played + this.sessionDuration);
  530. delete this.sessionDuration;
  531. }
  532. const values = this.values;
  533. return {
  534. type: 'stats',
  535. values: {
  536. level: values.level,
  537. xp: values.xp,
  538. xpTotal: values.xpTotal,
  539. hp: values.hp,
  540. mana: values.mana
  541. },
  542. stats: this.stats
  543. };
  544. },
  545. simplify: function (self) {
  546. let values = this.values;
  547. if (!self) {
  548. let result = {
  549. type: 'stats',
  550. values: {
  551. hp: values.hp,
  552. hpMax: values.hpMax,
  553. mana: values.mana,
  554. manaMax: values.manaMax,
  555. level: values.level
  556. }
  557. };
  558. return result;
  559. }
  560. return {
  561. type: 'stats',
  562. values: values,
  563. stats: this.stats,
  564. vitScale: this.vitScale
  565. };
  566. },
  567. simplifyTransfer: function () {
  568. return {
  569. type: 'stats',
  570. values: this.values,
  571. stats: this.stats
  572. };
  573. },
  574. onLogin: function () {
  575. let stats = this.stats;
  576. let time = scheduler.getTime();
  577. stats.lastLogin = time;
  578. },
  579. getKillStreakCoefficient: function (mobName) {
  580. let killStreak = this.stats.mobKillStreaks[mobName];
  581. if (!killStreak)
  582. return 1;
  583. return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000);
  584. },
  585. canGetMobLoot: function (mob) {
  586. if (!mob.inventory.dailyDrops)
  587. return true;
  588. let lootStats = this.stats.lootStats[mob.name];
  589. let time = scheduler.getTime();
  590. if (!lootStats)
  591. this.stats.lootStats[mob.name] = time;
  592. else
  593. return ((lootStats.day !== time.day) || (lootStats.month !== time.month));
  594. },
  595. events: {
  596. afterKillMob: function (mob) {
  597. let mobKillStreaks = this.stats.mobKillStreaks;
  598. let mobName = mob.name;
  599. if (!mobKillStreaks[mobName])
  600. mobKillStreaks[mobName] = 0;
  601. if (mobKillStreaks[mobName] < 100)
  602. mobKillStreaks[mobName]++;
  603. for (let p in mobKillStreaks) {
  604. if (p === mobName)
  605. continue;
  606. mobKillStreaks[p]--;
  607. if (mobKillStreaks[p] <= 0)
  608. delete mobKillStreaks[p];
  609. }
  610. },
  611. beforeGetXp: function (event) {
  612. if ((!event.target.mob) && (!event.target.player))
  613. return;
  614. event.amount *= this.getKillStreakCoefficient(event.target.name);
  615. },
  616. beforeGenerateLoot: function (event) {
  617. if (!event.source.mob)
  618. return;
  619. event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name);
  620. if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source)))
  621. event.chanceMultiplier = 0;
  622. },
  623. afterMove: function (event) {
  624. let mobKillStreaks = this.stats.mobKillStreaks;
  625. for (let p in mobKillStreaks) {
  626. mobKillStreaks[p] -= 0.085;
  627. if (mobKillStreaks[p] <= 0)
  628. delete mobKillStreaks[p];
  629. }
  630. },
  631. afterDealDamage: function (damageEvent, target) {
  632. if (damageEvent.element)
  633. return;
  634. const { obj, values: { lifeOnHit } } = this;
  635. if (target === obj || !lifeOnHit)
  636. return;
  637. this.getHp({ amount: lifeOnHit }, obj);
  638. }
  639. }
  640. };