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.
 
 
 

740 lines
17 KiB

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