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.
 
 
 

761 lines
17 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, isTransfer) {
  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. this.calcXpMax();
  92. if (blueprint)
  93. delete blueprint.stats;
  94. },
  95. resetHp: function () {
  96. let values = this.values;
  97. values.hp = values.hpMax;
  98. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  99. },
  100. update: function () {
  101. if (((this.obj.mob) && (!this.obj.follower)) || (this.obj.dead))
  102. return;
  103. let values = this.values;
  104. let manaMax = values.manaMax;
  105. manaMax -= (manaMax * values.manaReservePercent);
  106. let regen = {
  107. success: true
  108. };
  109. this.obj.fireEvent('beforeRegen', regen);
  110. if (!regen.success)
  111. return;
  112. let isInCombat = (this.obj.aggro.list.length > 0);
  113. if (this.obj.follower) {
  114. isInCombat = (this.obj.follower.master.aggro.list.length > 0);
  115. if (isInCombat)
  116. return;
  117. }
  118. let regenHp = 0;
  119. let regenMana = 0;
  120. regenMana = values.regenMana / 50;
  121. if (!isInCombat)
  122. regenHp = Math.max(values.hpMax / 112, values.regenHp * 0.2);
  123. else
  124. regenHp = values.regenHp * 0.2;
  125. if (values.hp < values.hpMax) {
  126. values.hp += regenHp;
  127. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  128. }
  129. if (values.hp > values.hpMax) {
  130. values.hp = values.hpMax;
  131. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  132. }
  133. if (values.mana < manaMax) {
  134. values.mana += regenMana;
  135. //Show others what mana is?
  136. let onlySelf = true;
  137. if (this.obj.player)
  138. onlySelf = false;
  139. this.obj.syncer.setObject(onlySelf, 'stats', 'values', 'mana', values.mana);
  140. }
  141. if (values.mana > manaMax) {
  142. values.mana = manaMax;
  143. if (this.obj.player)
  144. onlySelf = false;
  145. this.obj.syncer.setObject(onlySelf, 'stats', 'values', 'mana', values.mana);
  146. }
  147. },
  148. addStat: function (stat, value) {
  149. let values = this.values;
  150. if (['lvlRequire', 'allAttributes'].indexOf(stat) === -1)
  151. values[stat] += value;
  152. let sendOnlyToSelf = (['hp', 'hpMax', 'mana', 'manaMax', 'vit'].indexOf(stat) === -1);
  153. this.obj.syncer.setObject(sendOnlyToSelf, 'stats', 'values', stat, values[stat]);
  154. if (sendOnlyToSelf)
  155. this.obj.syncer.setObject(false, 'stats', 'values', stat, values[stat]);
  156. if (['addCritChance', 'addAttackCritChance', 'addSpellCritChance'].indexOf(stat) > -1) {
  157. let morphStat = stat.substr(3);
  158. morphStat = morphStat[0].toLowerCase() + morphStat.substr(1);
  159. this.addStat(morphStat, (0.05 * value));
  160. } else if (['addCritMultiplier', 'addAttackCritMultiplier', 'addSpellCritMultiplier'].indexOf(stat) > -1) {
  161. let morphStat = stat.substr(3);
  162. morphStat = morphStat[0].toLowerCase() + morphStat.substr(1);
  163. this.addStat(morphStat, value);
  164. } else if (stat === 'vit')
  165. this.addStat('hpMax', (value * this.statScales.vitToHp));
  166. else if (stat === 'allAttributes') {
  167. ['int', 'str', 'dex'].forEach(function (s) {
  168. this.addStat(s, value);
  169. }, this);
  170. } else if (stat === 'elementAllResist') {
  171. ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) {
  172. let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Resist';
  173. this.addStat(element, value);
  174. }, this);
  175. } else if (stat === 'elementPercent') {
  176. ['arcane', 'frost', 'fire', 'holy', 'poison'].forEach(function (s) {
  177. let element = 'element' + (s[0].toUpperCase() + s.substr(1)) + 'Percent';
  178. this.addStat(element, value);
  179. }, this);
  180. } else if (stat === 'str')
  181. this.addStat('armor', (value * this.statScales.strToArmor));
  182. else if (stat === 'int')
  183. this.addStat('manaMax', (value * this.statScales.intToMana));
  184. else if (stat === 'dex')
  185. this.addStat('dodgeAttackChance', (value * this.statScales.dexToDodge));
  186. this.obj.equipment.unequipAttrRqrGear();
  187. },
  188. calcXpMax: function () {
  189. let level = this.values.level;
  190. this.values.xpMax = (level * 5) + ~~(level * 10 * Math.pow(level, 2.2));
  191. this.obj.syncer.setObject(true, 'stats', 'values', 'xpMax', this.values.xpMax);
  192. },
  193. //Source is the object that caused you to gain xp (mostly yourself)
  194. //Target is the source of the xp (a mob or quest)
  195. getXp: function (amount, source, target) {
  196. let obj = this.obj;
  197. let values = this.values;
  198. if (values.level === 20)
  199. return;
  200. let xpEvent = {
  201. source: source,
  202. target: target,
  203. amount: amount
  204. };
  205. this.obj.fireEvent('beforeGetXp', xpEvent);
  206. if (xpEvent.amount === 0)
  207. return;
  208. amount = ~~(xpEvent.amount * (1 + (values.xpIncrease / 100)));
  209. values.xpTotal = ~~(values.xpTotal + amount);
  210. values.xp = ~~(values.xp + amount);
  211. this.obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  212. this.syncer.queue('onGetDamage', {
  213. id: obj.id,
  214. event: true,
  215. text: '+' + amount + ' xp'
  216. }, -1);
  217. let syncO = {};
  218. let didLevelUp = false;
  219. while (values.xp >= values.xpMax) {
  220. didLevelUp = true;
  221. values.xp -= values.xpMax;
  222. this.obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  223. values.level++;
  224. this.obj.fireEvent('onLevelUp', this.values.level);
  225. if (values.level === 20)
  226. values.xp = 0;
  227. values.hpMax = values.level * 32.7;
  228. let gainStats = classes.stats[this.obj.class].gainStats;
  229. for (let s in gainStats)
  230. this.addStat(s, gainStats[s]);
  231. this.obj.spellbook.calcDps();
  232. this.syncer.queue('onGetDamage', {
  233. id: obj.id,
  234. event: true,
  235. text: 'level up'
  236. }, -1);
  237. syncO.level = values.level;
  238. this.calcXpMax();
  239. }
  240. if (didLevelUp) {
  241. let cellContents = obj.instance.physics.getCell(obj.x, obj.y);
  242. cellContents.forEach(function (c) {
  243. c.fireEvent('onCellPlayerLevelUp', obj);
  244. });
  245. obj.auth.doSave();
  246. }
  247. process.send({
  248. method: 'object',
  249. serverId: this.obj.serverId,
  250. obj: syncO
  251. });
  252. if (didLevelUp) {
  253. this.obj.syncer.setObject(true, 'stats', 'values', 'hpMax', values.hpMax);
  254. this.obj.syncer.setObject(true, 'stats', 'values', 'level', values.level);
  255. this.obj.syncer.setObject(false, 'stats', 'values', 'hpMax', values.hpMax);
  256. this.obj.syncer.setObject(false, 'stats', 'values', 'level', values.level);
  257. }
  258. },
  259. kill: function (target) {
  260. if (target.player)
  261. return;
  262. let level = target.stats.values.level;
  263. let mobDiffMult = 1;
  264. if (target.isRare)
  265. mobDiffMult = 2;
  266. else if (target.isChampion)
  267. mobDiffMult = 5;
  268. //Who should get xp?
  269. let aggroList = target.aggro.list;
  270. let hpMax = target.stats.values.hpMax;
  271. let aLen = aggroList.length;
  272. for (let i = 0; i < aLen; i++) {
  273. let a = aggroList[i];
  274. let dmg = a.damage;
  275. if (dmg <= 0)
  276. continue;
  277. let mult = 1;
  278. //How many party members contributed
  279. // Remember, maybe one of the aggro-ees might be a mob too
  280. let party = a.obj.social ? a.obj.social.party : null;
  281. if (party) {
  282. let partySize = aggroList.filter(function (f) {
  283. return ((a.damage > 0) && (party.indexOf(f.obj.serverId) > -1));
  284. }).length;
  285. partySize--;
  286. mult = (1 + (partySize * 0.1));
  287. }
  288. if ((a.obj.stats) && (!a.obj.follower)) {
  289. //Scale xp by source level so you can't just farm low level mobs (or get boosted on high level mobs).
  290. //Mobs that are farther then 10 levels from you, give no xp
  291. //We don't currently do this for quests/herb gathering
  292. let sourceLevel = a.obj.stats.values.level;
  293. let levelDelta = level - sourceLevel;
  294. let amount = null;
  295. if (Math.abs(levelDelta) <= 10)
  296. amount = ~~(((sourceLevel + levelDelta) * 10) * Math.pow(1 - (Math.abs(levelDelta) / 10), 2) * mult * mobDiffMult);
  297. else
  298. amount = 0;
  299. a.obj.stats.getXp(amount, this.obj, target);
  300. }
  301. a.obj.fireEvent('afterKillMob', target);
  302. }
  303. },
  304. die: function (source) {
  305. let obj = this.obj;
  306. let values = this.values;
  307. this.syncer.queue('onGetDamage', {
  308. id: obj.id,
  309. event: true,
  310. text: 'death'
  311. }, -1);
  312. obj.syncer.set(true, null, 'dead', true);
  313. let syncO = obj.syncer.o;
  314. obj.hidden = true;
  315. obj.nonSelectable = true;
  316. syncO.hidden = true;
  317. syncO.nonSelectable = true;
  318. let xpLoss = ~~Math.min(values.xp, values.xpMax / 10);
  319. values.xp -= xpLoss;
  320. obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  321. this.syncer.queue('onDeath', {
  322. source: source.name,
  323. xpLoss: xpLoss
  324. }, [obj.serverId]);
  325. obj.instance.syncer.queue('onGetObject', {
  326. x: obj.x,
  327. y: obj.y,
  328. components: [{
  329. type: 'attackAnimation',
  330. row: 0,
  331. col: 4
  332. }]
  333. }, -1);
  334. },
  335. respawn: function () {
  336. this.obj.syncer.set(true, null, 'dead', false);
  337. let obj = this.obj;
  338. let syncO = obj.syncer.o;
  339. this.obj.dead = false;
  340. let values = this.values;
  341. values.hp = values.hpMax;
  342. values.mana = values.manaMax;
  343. obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  344. obj.syncer.setObject(false, 'stats', 'values', 'mana', values.mana);
  345. obj.hidden = false;
  346. obj.nonSelectable = false;
  347. syncO.hidden = false;
  348. syncO.nonSelectable = false;
  349. process.send({
  350. method: 'object',
  351. serverId: this.obj.serverId,
  352. obj: {
  353. dead: false
  354. }
  355. });
  356. obj.instance.syncer.queue('onGetObject', {
  357. x: obj.x,
  358. y: obj.y,
  359. components: [{
  360. type: 'attackAnimation',
  361. row: 0,
  362. col: 4
  363. }]
  364. }, -1);
  365. this.obj.player.respawn();
  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 !== null)
  383. recipients.push(this.obj.serverId);
  384. if (source.serverId !== null)
  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. }, -1);
  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. }, -1);
  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.serverId === null))
  457. continue;
  458. this.obj.inventory.dropBag(a.obj.serverId, 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.hasOwnProperty('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 !== null)
  488. recipients.push(this.obj.serverId);
  489. if (source.serverId !== null)
  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. let aggroList = this.obj.aggro.list;
  503. let aLen = aggroList.length;
  504. for (let i = 0; i < aLen; i++) {
  505. let a = aggroList[i].obj;
  506. a.aggro.tryEngage(source, threat);
  507. }
  508. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  509. },
  510. save: function () {
  511. if (this.sessionDuration) {
  512. this.stats.played = ~~(this.stats.played + this.sessionDuration);
  513. delete this.sessionDuration;
  514. }
  515. let values = extend(true, {}, this.values);
  516. values.hp = this.values.hp;
  517. values.mana = this.values.mana;
  518. return {
  519. type: 'stats',
  520. values: values,
  521. stats: this.stats
  522. };
  523. },
  524. simplify: function (self) {
  525. let values = this.values;
  526. if (!self) {
  527. let result = {
  528. type: 'stats',
  529. values: {
  530. hp: values.hp,
  531. hpMax: values.hpMax,
  532. mana: values.mana,
  533. manaMax: values.manaMax,
  534. level: values.level
  535. }
  536. };
  537. return result;
  538. }
  539. return {
  540. type: 'stats',
  541. values: values,
  542. stats: this.stats,
  543. vitScale: this.vitScale
  544. };
  545. },
  546. onLogin: function () {
  547. let stats = this.stats;
  548. let time = scheduler.getTime();
  549. stats.lastLogin = time;
  550. this.obj.instance.mail.getMail(this.obj.name);
  551. },
  552. getKillStreakCoefficient: function (mobName) {
  553. let killStreak = this.stats.mobKillStreaks[mobName];
  554. if (!killStreak)
  555. return 1;
  556. return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000);
  557. },
  558. canGetMobLoot: function (mob) {
  559. if (!mob.inventory.dailyDrops)
  560. return true;
  561. let lootStats = this.stats.lootStats[mob.name];
  562. let time = scheduler.getTime();
  563. if (!lootStats)
  564. this.stats.lootStats[mob.name] = time;
  565. else
  566. return ((lootStats.day !== time.day), (lootStats.month !== time.month));
  567. },
  568. events: {
  569. afterKillMob: function (mob) {
  570. let mobKillStreaks = this.stats.mobKillStreaks;
  571. let mobName = mob.name;
  572. if (!mobKillStreaks[mobName])
  573. mobKillStreaks.mobName = 0;
  574. if (mobKillStreaks[mobName] < 100)
  575. mobKillStreaks[mobName]++;
  576. for (let p in mobKillStreaks) {
  577. if (p === mobName)
  578. continue;
  579. mobKillStreaks[p]--;
  580. if (mobKillStreaks[p] <= 0)
  581. delete mobKillStreaks[p];
  582. }
  583. },
  584. beforeGetXp: function (event) {
  585. if ((!event.target.mob) && (!event.target.player))
  586. return;
  587. event.amount *= this.getKillStreakCoefficient(event.target.name);
  588. },
  589. beforeGenerateLoot: function (event) {
  590. if (!event.source.mob)
  591. return;
  592. event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name);
  593. if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source)))
  594. event.chanceMultiplier = 0;
  595. },
  596. afterMove: function (event) {
  597. let mobKillStreaks = this.stats.mobKillStreaks;
  598. for (let p in mobKillStreaks) {
  599. mobKillStreaks[p] -= 0.085;
  600. if (mobKillStreaks[p] <= 0)
  601. delete mobKillStreaks[p];
  602. }
  603. }
  604. }
  605. };