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.
 
 
 

750 lines
18 KiB

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