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.
 
 
 

792 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. getHp: function (heal, source) {
  471. let amount = heal.amount;
  472. if (amount === 0)
  473. return;
  474. let threatMult = heal.threatMult;
  475. if (!heal.has('threatMult'))
  476. threatMult = 1;
  477. let values = this.values;
  478. let hpMax = values.hpMax;
  479. if (values.hp >= hpMax)
  480. return;
  481. if (hpMax - values.hp < amount)
  482. amount = hpMax - values.hp;
  483. values.hp += amount;
  484. if (values.hp > hpMax)
  485. values.hp = hpMax;
  486. let recipients = [];
  487. if (this.obj.serverId)
  488. recipients.push(this.obj.serverId);
  489. if (source.serverId)
  490. recipients.push(source.serverId);
  491. if (recipients.length > 0) {
  492. this.syncer.queue('onGetDamage', {
  493. id: this.obj.id,
  494. source: source.id,
  495. heal: true,
  496. amount: amount,
  497. crit: heal.crit,
  498. element: heal.element
  499. }, recipients);
  500. }
  501. //Add aggro to all our attackers
  502. let threat = amount * 0.4 * threatMult;
  503. if (threat !== 0) {
  504. let aggroList = this.obj.aggro.list;
  505. let aLen = aggroList.length;
  506. for (let i = 0; i < aLen; i++) {
  507. let a = aggroList[i].obj;
  508. a.aggro.tryEngage(source, threat);
  509. }
  510. }
  511. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  512. },
  513. save: function () {
  514. if (this.sessionDuration) {
  515. this.stats.played = ~~(this.stats.played + this.sessionDuration);
  516. delete this.sessionDuration;
  517. }
  518. const values = this.values;
  519. return {
  520. type: 'stats',
  521. values: {
  522. level: values.level,
  523. xp: values.xp,
  524. xpTotal: values.xpTotal,
  525. hp: values.hp,
  526. mana: values.mana
  527. },
  528. stats: this.stats
  529. };
  530. },
  531. simplify: function (self) {
  532. let values = this.values;
  533. if (!self) {
  534. let result = {
  535. type: 'stats',
  536. values: {
  537. hp: values.hp,
  538. hpMax: values.hpMax,
  539. mana: values.mana,
  540. manaMax: values.manaMax,
  541. level: values.level
  542. }
  543. };
  544. return result;
  545. }
  546. return {
  547. type: 'stats',
  548. values: values,
  549. stats: this.stats,
  550. vitScale: this.vitScale
  551. };
  552. },
  553. simplifyTransfer: function () {
  554. return {
  555. type: 'stats',
  556. values: this.values,
  557. stats: this.stats
  558. };
  559. },
  560. onLogin: function () {
  561. let stats = this.stats;
  562. let time = scheduler.getTime();
  563. stats.lastLogin = time;
  564. },
  565. getKillStreakCoefficient: function (mobName) {
  566. let killStreak = this.stats.mobKillStreaks[mobName];
  567. if (!killStreak)
  568. return 1;
  569. return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000);
  570. },
  571. canGetMobLoot: function (mob) {
  572. if (!mob.inventory.dailyDrops)
  573. return true;
  574. let lootStats = this.stats.lootStats[mob.name];
  575. let time = scheduler.getTime();
  576. if (!lootStats)
  577. this.stats.lootStats[mob.name] = time;
  578. else
  579. return ((lootStats.day !== time.day) || (lootStats.month !== time.month));
  580. },
  581. events: {
  582. afterKillMob: function (mob) {
  583. let mobKillStreaks = this.stats.mobKillStreaks;
  584. let mobName = mob.name;
  585. if (!mobKillStreaks[mobName])
  586. mobKillStreaks[mobName] = 0;
  587. if (mobKillStreaks[mobName] < 100)
  588. mobKillStreaks[mobName]++;
  589. for (let p in mobKillStreaks) {
  590. if (p === mobName)
  591. continue;
  592. mobKillStreaks[p]--;
  593. if (mobKillStreaks[p] <= 0)
  594. delete mobKillStreaks[p];
  595. }
  596. },
  597. beforeGetXp: function (event) {
  598. if ((!event.target.mob) && (!event.target.player))
  599. return;
  600. event.amount *= this.getKillStreakCoefficient(event.target.name);
  601. },
  602. beforeGenerateLoot: function (event) {
  603. if (!event.source.mob)
  604. return;
  605. event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name);
  606. if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source)))
  607. event.chanceMultiplier = 0;
  608. },
  609. afterMove: function (event) {
  610. let mobKillStreaks = this.stats.mobKillStreaks;
  611. for (let p in mobKillStreaks) {
  612. mobKillStreaks[p] -= 0.085;
  613. if (mobKillStreaks[p] <= 0)
  614. delete mobKillStreaks[p];
  615. }
  616. },
  617. afterDealDamage: function (damageEvent, target) {
  618. if (damageEvent.element)
  619. return;
  620. const { obj, values: { lifeOnHit } } = this;
  621. if (target === obj || !lifeOnHit)
  622. return;
  623. this.getHp({ amount: lifeOnHit }, obj);
  624. }
  625. }
  626. };