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.
 
 
 

776 lines
18 KiB

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