Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

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