No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

762 líneas
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 obj = obj;
  314. let syncO = obj.syncer.o;
  315. obj.hidden = true;
  316. obj.nonSelectable = true;
  317. syncO.hidden = true;
  318. syncO.nonSelectable = true;
  319. let xpLoss = ~~Math.min(values.xp, values.xpMax / 10);
  320. values.xp -= xpLoss;
  321. obj.syncer.setObject(true, 'stats', 'values', 'xp', values.xp);
  322. this.syncer.queue('onDeath', {
  323. source: source.name,
  324. xpLoss: xpLoss
  325. }, [obj.serverId]);
  326. obj.instance.syncer.queue('onGetObject', {
  327. x: obj.x,
  328. y: obj.y,
  329. components: [{
  330. type: 'attackAnimation',
  331. row: 0,
  332. col: 4
  333. }]
  334. }, -1);
  335. },
  336. respawn: function () {
  337. this.obj.syncer.set(true, null, 'dead', false);
  338. let obj = this.obj;
  339. let syncO = obj.syncer.o;
  340. this.obj.dead = false;
  341. let values = this.values;
  342. values.hp = values.hpMax;
  343. values.mana = values.manaMax;
  344. obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  345. obj.syncer.setObject(false, 'stats', 'values', 'mana', values.mana);
  346. obj.hidden = false;
  347. obj.nonSelectable = false;
  348. syncO.hidden = false;
  349. syncO.nonSelectable = false;
  350. process.send({
  351. method: 'object',
  352. serverId: this.obj.serverId,
  353. obj: {
  354. dead: false
  355. }
  356. });
  357. obj.instance.syncer.queue('onGetObject', {
  358. x: obj.x,
  359. y: obj.y,
  360. components: [{
  361. type: 'attackAnimation',
  362. row: 0,
  363. col: 4
  364. }]
  365. }, -1);
  366. this.obj.player.respawn();
  367. },
  368. takeDamage: function (damage, threatMult, source) {
  369. source.fireEvent('beforeDealDamage', damage, this.obj);
  370. this.obj.fireEvent('beforeTakeDamage', damage, source);
  371. //Maybe the attacker was stunned?
  372. if (damage.failed)
  373. return;
  374. //Maybe something else killed this mob already?
  375. if (this.obj.destroyed)
  376. return;
  377. let amount = damage.amount;
  378. if (amount > this.values.hp)
  379. amount = this.values.hp;
  380. damage.dealt = amount;
  381. this.values.hp -= amount;
  382. let recipients = [];
  383. if (this.obj.serverId != null)
  384. recipients.push(this.obj.serverId);
  385. if (source.serverId != null)
  386. recipients.push(source.serverId);
  387. if ((source.follower) && (source.follower.master.serverId))
  388. recipients.push(source.follower.master.serverId);
  389. if ((this.obj.follower) && (this.obj.follower.master.serverId))
  390. recipients.push(this.obj.follower.master.serverId);
  391. if (recipients.length > 0) {
  392. if ((!damage.blocked) && (!damage.dodged)) {
  393. this.syncer.queue('onGetDamage', {
  394. id: this.obj.id,
  395. source: source.id,
  396. crit: damage.crit,
  397. amount: amount
  398. }, recipients);
  399. } else {
  400. this.syncer.queue('onGetDamage', {
  401. id: this.obj.id,
  402. source: source.id,
  403. event: true,
  404. text: 'blocked'
  405. }, recipients);
  406. }
  407. }
  408. this.obj.aggro.tryEngage(source, amount, threatMult);
  409. let died = (this.values.hp <= 0);
  410. if (died) {
  411. let death = {
  412. success: true
  413. };
  414. this.obj.fireEvent('beforeDeath', death);
  415. if (death.success) {
  416. let deathEvent = {};
  417. let killSource = source;
  418. if (source.follower)
  419. killSource = source.follower.master;
  420. if (killSource.player)
  421. killSource.stats.kill(this.obj);
  422. this.obj.fireEvent('afterDeath', deathEvent);
  423. if (this.obj.player) {
  424. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  425. if (deathEvent.permadeath) {
  426. this.obj.auth.permadie();
  427. this.obj.instance.syncer.queue('onGetMessages', {
  428. messages: {
  429. class: 'color-redA',
  430. message: `(level ${this.values.level}) ${this.obj.name} has forever left the shores of the living.`
  431. }
  432. }, -1);
  433. this.syncer.queue('onPermadeath', {
  434. source: killSource.name
  435. }, [this.obj.serverId]);
  436. } else
  437. this.values.hp = 0;
  438. this.obj.player.die(killSource, deathEvent.permadeath);
  439. } else {
  440. this.obj.effects.die();
  441. if (this.obj.spellbook)
  442. this.obj.spellbook.die();
  443. this.obj.destroyed = true;
  444. let deathAnimation = _.getDeepProperty(animations, ['mobs', this.obj.sheetName, this.obj.cell, 'death']);
  445. if (deathAnimation) {
  446. this.obj.instance.syncer.queue('onGetObject', {
  447. x: this.obj.x,
  448. y: this.obj.y,
  449. components: [deathAnimation]
  450. }, -1);
  451. }
  452. if (this.obj.inventory) {
  453. let aggroList = this.obj.aggro.list;
  454. let aLen = aggroList.length;
  455. for (let i = 0; i < aLen; i++) {
  456. let a = aggroList[i];
  457. if ((!a.threat) || (a.obj.serverId == null))
  458. continue;
  459. this.obj.inventory.dropBag(a.obj.serverId, killSource);
  460. }
  461. }
  462. }
  463. }
  464. } else {
  465. source.aggro.tryEngage(this.obj, 0);
  466. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', this.values.hp);
  467. }
  468. if (!damage.noEvents)
  469. source.fireEvent('afterDealDamage', damage, this.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.hasOwnProperty('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 != null)
  489. recipients.push(this.obj.serverId);
  490. if (source.serverId != null)
  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. }, recipients);
  500. }
  501. //Add aggro to all our attackers
  502. let threat = amount * 0.4 * threatMult;
  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. this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp);
  510. },
  511. save: function () {
  512. if (this.sessionDuration) {
  513. this.stats.played = ~~(this.stats.played + this.sessionDuration);
  514. delete this.sessionDuration;
  515. }
  516. let values = extend(true, {}, this.values);
  517. values.hp = this.values.hp;
  518. values.mana = this.values.mana;
  519. return {
  520. type: 'stats',
  521. values: values,
  522. stats: this.stats
  523. };
  524. },
  525. simplify: function (self) {
  526. let values = this.values;
  527. if (!self) {
  528. let result = {
  529. type: 'stats',
  530. values: {
  531. hp: values.hp,
  532. hpMax: values.hpMax,
  533. mana: values.mana,
  534. manaMax: values.manaMax,
  535. level: values.level
  536. }
  537. };
  538. return result;
  539. }
  540. return {
  541. type: 'stats',
  542. values: values,
  543. stats: this.stats,
  544. vitScale: this.vitScale
  545. };
  546. },
  547. onLogin: function () {
  548. let stats = this.stats;
  549. let time = scheduler.getTime();
  550. stats.lastLogin = time;
  551. this.obj.instance.mail.getMail(this.obj.name);
  552. },
  553. getKillStreakCoefficient: function (mobName) {
  554. let killStreak = this.stats.mobKillStreaks[mobName];
  555. if (!killStreak)
  556. return 1;
  557. return Math.max(0, (10000 - Math.pow(killStreak, 2)) / 10000);
  558. },
  559. canGetMobLoot: function (mob) {
  560. if (!mob.inventory.dailyDrops)
  561. return true;
  562. let lootStats = this.stats.lootStats[mob.name];
  563. let time = scheduler.getTime();
  564. if (!lootStats)
  565. this.stats.lootStats[mob.name] = time;
  566. else
  567. return ((lootStats.day != time.day), (lootStats.month != time.month));
  568. },
  569. events: {
  570. afterKillMob: function (mob) {
  571. let mobKillStreaks = this.stats.mobKillStreaks;
  572. let mobName = mob.name;
  573. if (!mobKillStreaks[mobName])
  574. mobKillStreaks.mobName = 0;
  575. if (mobKillStreaks[mobName] < 100)
  576. mobKillStreaks[mobName]++;
  577. for (let p in mobKillStreaks) {
  578. if (p == mobName)
  579. continue;
  580. mobKillStreaks[p]--;
  581. if (mobKillStreaks[p] <= 0)
  582. delete mobKillStreaks[p];
  583. }
  584. },
  585. beforeGetXp: function (event) {
  586. if ((!event.target.mob) && (!event.target.player))
  587. return;
  588. event.amount *= this.getKillStreakCoefficient(event.target.name);
  589. },
  590. beforeGenerateLoot: function (event) {
  591. if (!event.source.mob)
  592. return;
  593. event.chanceMultiplier *= this.getKillStreakCoefficient(event.source.name);
  594. if ((event.chanceMultiplier > 0) && (!this.canGetMobLoot(event.source)))
  595. event.chanceMultiplier = 0;
  596. },
  597. afterMove: function (event) {
  598. let mobKillStreaks = this.stats.mobKillStreaks;
  599. for (let p in mobKillStreaks) {
  600. mobKillStreaks[p] -= 0.085;
  601. if (mobKillStreaks[p] <= 0)
  602. delete mobKillStreaks[p];
  603. }
  604. }
  605. }
  606. };