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.
 
 
 

696 lines
16 KiB

  1. let spellTemplate = require('../config/spells/spellTemplate');
  2. let animations = require('../config/animations');
  3. let playerSpells = require('../config/spells');
  4. let playerSpellsConfig = require('../config/spellsConfig');
  5. //Helpers
  6. const rotationManager = require('./spellbook/rotationManager');
  7. //Component
  8. module.exports = {
  9. type: 'spellbook',
  10. spells: [],
  11. physics: null,
  12. objects: null,
  13. closestRange: -1,
  14. furthestRange: -1,
  15. callbacks: [],
  16. rotation: null,
  17. init: function (blueprint) {
  18. this.objects = this.obj.instance.objects;
  19. this.physics = this.obj.instance.physics;
  20. (blueprint.spells || []).forEach(s => this.addSpell(s, -1));
  21. if (blueprint.rotation) {
  22. const { duration, spells } = blueprint.rotation;
  23. this.rotation = {
  24. currentTick: 0,
  25. duration,
  26. spells
  27. };
  28. }
  29. delete blueprint.spells;
  30. //External helpers that should form part of the component
  31. this.getSpellToCast = rotationManager.getSpellToCast.bind(null, this);
  32. this.getFurthestRange = rotationManager.getFurthestRange.bind(null, this);
  33. this.resetRotation = rotationManager.resetRotation.bind(null, this);
  34. },
  35. transfer: function () {
  36. let spells = this.spells;
  37. this.spells = [];
  38. spells.forEach(s => this.addSpell(s, -1));
  39. },
  40. die: function () {
  41. this.stopCasting();
  42. this.spells.forEach(s => {
  43. let reserve = s.manaReserve;
  44. if (reserve && reserve.percentage && s.active) {
  45. let reserveEvent = {
  46. spell: s.name,
  47. reservePercent: reserve.percentage
  48. };
  49. this.obj.fireEvent('onBeforeReserveMana', reserveEvent);
  50. this.obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
  51. }
  52. s.die();
  53. }, this);
  54. },
  55. simplify: function (self) {
  56. if (!self)
  57. return null;
  58. let s = {
  59. type: this.type,
  60. closestRange: this.closestRange,
  61. furthestRange: this.furthestRange
  62. };
  63. let spells = this.spells;
  64. if (spells.length && spells[0].obj)
  65. spells = spells.map(f => f.simplify());
  66. s.spells = spells;
  67. return s;
  68. },
  69. addSpell: function (options, spellId) {
  70. if (!options.type) {
  71. options = {
  72. type: options
  73. };
  74. }
  75. let type = options.type[0].toUpperCase() + options.type.substr(1);
  76. let typeTemplate = {
  77. type: type,
  78. template: null
  79. };
  80. this.obj.instance.eventEmitter.emit('onBeforeGetSpellTemplate', typeTemplate);
  81. if (!typeTemplate.template)
  82. typeTemplate.template = require('../config/spells/spell' + type);
  83. let builtSpell = extend({}, spellTemplate, typeTemplate.template, options);
  84. builtSpell.obj = this.obj;
  85. builtSpell.baseDamage = builtSpell.damage || 0;
  86. builtSpell.damage += (options.damageAdd || 0);
  87. if (options.damage)
  88. builtSpell.damage = options.damage;
  89. if (builtSpell.animation) {
  90. let animation = null;
  91. let sheetName = this.obj.sheetName || '../../../images/characters.png';
  92. let animationName = builtSpell.animation;
  93. if (sheetName === 'mobs')
  94. animation = animations.mobs;
  95. else if (sheetName === 'bosses')
  96. animation = animations.bosses;
  97. else if (sheetName.indexOf('/') > -1)
  98. animation = animations.mobs[sheetName];
  99. else
  100. animation = animations.classes;
  101. if ((animation) && (animation[this.obj.cell]) && (animation[this.obj.cell][animationName])) {
  102. builtSpell.animation = extend({}, animation[this.obj.cell][animationName]);
  103. builtSpell.animation.name = animationName;
  104. } else
  105. builtSpell.animation = null;
  106. }
  107. if (!builtSpell.castOnDeath && builtSpell.range) {
  108. if (this.closestRange === -1 || builtSpell.range < this.closestRange)
  109. this.closestRange = builtSpell.range;
  110. if (this.furthestRange === -1 || builtSpell.range > this.furthestRange)
  111. this.furthestRange = builtSpell.range;
  112. }
  113. if ((!options.has('id')) && (spellId === -1)) {
  114. spellId = 0;
  115. this.spells.forEach(function (s) {
  116. if (s.id >= spellId)
  117. spellId = s.id + 1;
  118. });
  119. }
  120. builtSpell.id = !options.has('id') ? spellId : options.id;
  121. //Mobs don't get abilities put on CD when they learn them
  122. if (!this.obj.mob && builtSpell.cdMax)
  123. builtSpell.cd = builtSpell.cdMax;
  124. this.spells.push(builtSpell);
  125. this.spells.sort(function (a, b) {
  126. return (a.id - b.id);
  127. });
  128. builtSpell.calcDps(null, true);
  129. if (builtSpell.init)
  130. builtSpell.init();
  131. if (this.obj.player)
  132. this.obj.syncer.setArray(true, 'spellbook', 'getSpells', builtSpell.simplify());
  133. return builtSpell.id;
  134. },
  135. addSpellFromRune: function (runeSpell, spellId) {
  136. let type = runeSpell.type;
  137. let playerSpell = playerSpells.spells.find(s => (s.name.toLowerCase() === runeSpell.name.toLowerCase())) || playerSpells.spells.find(s => (s.type === type));
  138. let playerSpellConfig = playerSpellsConfig.spells[runeSpell.name.toLowerCase()] || playerSpellsConfig.spells[runeSpell.type];
  139. if (!playerSpellConfig)
  140. return -1;
  141. if (!runeSpell.rolls)
  142. runeSpell.rolls = {};
  143. runeSpell.values = {};
  144. let builtSpell = extend({
  145. type: runeSpell.type,
  146. values: {}
  147. }, playerSpell, playerSpellConfig, runeSpell);
  148. for (let r in builtSpell.random) {
  149. let range = builtSpell.random[r];
  150. let roll = runeSpell.rolls[r] || 0;
  151. runeSpell.rolls[r] = roll;
  152. let int = r.indexOf('i_') === 0;
  153. let val = range[0] + ((range[1] - range[0]) * roll);
  154. if (int) {
  155. val = Math.round(val);
  156. r = r.replace('i_', '');
  157. } else
  158. val = ~~(val * 100) / 100;
  159. builtSpell[r] = val;
  160. builtSpell.values[r] = val;
  161. runeSpell.values[r] = val;
  162. }
  163. if (runeSpell.properties) {
  164. for (let p in runeSpell.properties)
  165. builtSpell[p] = runeSpell.properties[p];
  166. }
  167. if (runeSpell.cdMult)
  168. builtSpell.cdMax *= runeSpell.cdMult;
  169. delete builtSpell.rolls;
  170. delete builtSpell.random;
  171. return this.addSpell(builtSpell, spellId);
  172. },
  173. calcDps: function () {
  174. this.spells.forEach(s => s.calcDps());
  175. },
  176. removeSpellById: function (id) {
  177. let exists = this.spells.spliceFirstWhere(s => (s.id === id));
  178. if (exists) {
  179. if (exists.manaReserve && exists.active) {
  180. let reserve = exists.manaReserve;
  181. if (reserve.percentage) {
  182. let reserveEvent = {
  183. spell: exists.name,
  184. reservePercent: reserve.percentage
  185. };
  186. this.obj.fireEvent('onBeforeReserveMana', reserveEvent);
  187. this.obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
  188. }
  189. }
  190. if (exists.unlearn)
  191. exists.unlearn();
  192. this.obj.syncer.setArray(true, 'spellbook', 'removeSpells', id);
  193. }
  194. },
  195. queueAuto: function (action, spell) {
  196. if (!action.auto || spell.autoActive)
  197. return true;
  198. this.spells.forEach(s => s.setAuto(null));
  199. spell.setAuto({
  200. target: action.target,
  201. spell: spell.id
  202. });
  203. },
  204. getTarget: function (spell, action) {
  205. let target = action.target;
  206. //Cast on self?
  207. if (action.self) {
  208. if (spell.targetGround) {
  209. target = {
  210. x: this.obj.x,
  211. y: this.obj.y
  212. };
  213. } else if (spell.spellType === 'buff')
  214. target = this.obj;
  215. }
  216. if (!spell.aura && !spell.targetGround) {
  217. //Did we pass in the target id?
  218. if (target && !target.id) {
  219. target = this.objects.objects.find(o => o.id === target);
  220. if (!target)
  221. return null;
  222. }
  223. if (target === this.obj && spell.noTargetSelf)
  224. target = null;
  225. if (!target || !target.player) {
  226. if (spell.autoTargetFollower) {
  227. target = this.spells.find(s => s.minions && s.minions.length > 0);
  228. if (target)
  229. target = target.minions[0];
  230. else
  231. return null;
  232. }
  233. }
  234. if (target.aggro && (spell.spellType === 'buff' || spell.spellType === 'heal')) {
  235. if (this.obj.aggro.faction !== target.aggro.faction)
  236. return;
  237. } else if (target.aggro && !this.obj.aggro.canAttack(target)) {
  238. if (this.obj.player)
  239. this.sendAnnouncement("You don't feel like attacking that target");
  240. return;
  241. }
  242. }
  243. if (!spell.targetGround && target && !target.aggro && !spell.aura) {
  244. if (spell.spellType === 'heal')
  245. this.sendAnnouncement("You don't feel like healing that target");
  246. else
  247. this.sendAnnouncement("You don't feel like attacking that target");
  248. return;
  249. }
  250. if (spell.aura)
  251. target = this.obj;
  252. return target;
  253. },
  254. canCast: function (action) {
  255. if (!action.has('spell'))
  256. return false;
  257. let spell = this.spells.find(s => (s.id === action.spell));
  258. if (!spell)
  259. return false;
  260. let target = this.getTarget(spell, action);
  261. return spell.canCast(target);
  262. },
  263. cast: function (action, isAuto) {
  264. if (!action.has('spell')) {
  265. const isCasting = this.isCasting();
  266. this.stopCasting();
  267. const consumeTick = isCasting;
  268. return consumeTick;
  269. }
  270. let spell = this.spells.find(s => (s.id === action.spell));
  271. if (!spell)
  272. return false;
  273. action.target = this.getTarget(spell, action);
  274. //If a target has become nonSelectable, we need to stop attacks that are queued/auto
  275. if (!action.target || action.target.nonSelectable)
  276. return false;
  277. action.auto = spell.auto;
  278. let success = true;
  279. if (spell.cd > 0) {
  280. if (!isAuto) {
  281. let type = (spell.auto) ? 'Weapon' : 'Spell';
  282. this.sendAnnouncement(`${type} is on cooldown`);
  283. }
  284. success = false;
  285. } else if (spell.manaCost > this.obj.stats.values.mana) {
  286. if (!isAuto)
  287. this.sendAnnouncement('Insufficient mana to cast spell');
  288. success = false;
  289. } else if (spell.has('range')) {
  290. let distance = Math.max(Math.abs(action.target.x - this.obj.x), Math.abs(action.target.y - this.obj.y));
  291. let range = spell.range;
  292. if ((spell.useWeaponRange) && (this.obj.player)) {
  293. let weapon = this.obj.inventory.findItem(this.obj.equipment.eq.oneHanded) || this.obj.inventory.findItem(this.obj.equipment.eq.twoHanded);
  294. if (weapon)
  295. range = weapon.range || 1;
  296. }
  297. if (distance > range) {
  298. if (!isAuto)
  299. this.sendAnnouncement('Target out of range');
  300. success = false;
  301. }
  302. }
  303. //LoS check
  304. //Null means we don't have LoS and as such, we should move
  305. if (spell.needLos && success) {
  306. if (!this.physics.hasLos(~~this.obj.x, ~~this.obj.y, ~~action.target.x, ~~action.target.y)) {
  307. if (!isAuto)
  308. this.sendAnnouncement('Target not in line of sight');
  309. action.auto = false;
  310. success = null;
  311. }
  312. }
  313. if (!success) {
  314. this.queueAuto(action, spell);
  315. return success;
  316. } else if (!this.queueAuto(action, spell))
  317. return false;
  318. let castSuccess = {
  319. success: true
  320. };
  321. this.obj.fireEvent('beforeCastSpell', castSuccess);
  322. if (!castSuccess.success)
  323. return false;
  324. if (spell.manaReserve) {
  325. let reserve = spell.manaReserve;
  326. if (reserve.percentage) {
  327. let reserveEvent = {
  328. spell: spell.name,
  329. reservePercent: reserve.percentage
  330. };
  331. this.obj.fireEvent('onBeforeReserveMana', reserveEvent);
  332. if (!spell.active) {
  333. if (1 - this.obj.stats.values.manaReservePercent < reserve.percentage) {
  334. this.sendAnnouncement('Insufficient mana to cast spell');
  335. return;
  336. } this.obj.stats.addStat('manaReservePercent', reserveEvent.reservePercent);
  337. } else
  338. this.obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
  339. }
  340. }
  341. if (spell.targetFurthest)
  342. spell.target = this.obj.aggro.getFurthest();
  343. else if (spell.targetRandom)
  344. spell.target = this.obj.aggro.getRandom();
  345. success = spell.castBase(action);
  346. this.stopCasting(spell, true);
  347. if (success) {
  348. spell.consumeMana();
  349. spell.setCd();
  350. }
  351. this.obj.fireEvent('afterCastSpell', {
  352. castSuccess: success,
  353. spell,
  354. action
  355. });
  356. //Null means we didn't fail but are initiating casting
  357. return (success === null || success === true);
  358. },
  359. getClosestRange: function (spellNum) {
  360. if (spellNum)
  361. return this.spells[spellNum].range;
  362. return this.closestRange;
  363. },
  364. getCooldowns: function () {
  365. let cds = [];
  366. this.spells.forEach(
  367. s => cds.push({
  368. cd: s.cd,
  369. cdMax: s.cdMax,
  370. canCast: ((s.manaCost <= this.obj.stats.values.mana) && (s.cd === 0))
  371. }), this);
  372. return cds;
  373. },
  374. update: function () {
  375. let didCast = false;
  376. const isCasting = this.isCasting();
  377. if (this.rotation)
  378. rotationManager.tick(this);
  379. this.spells.forEach(s => {
  380. let auto = s.autoActive;
  381. if (auto) {
  382. if (!auto.target || auto.target.destroyed)
  383. s.setAuto(null);
  384. else if (!isCasting && this.cast(auto, true))
  385. didCast = true;
  386. }
  387. s.updateBase();
  388. if (s.update)
  389. s.update();
  390. });
  391. let callbacks = this.callbacks;
  392. let cLen = callbacks.length;
  393. for (let i = 0; i < cLen; i++) {
  394. let c = callbacks[i];
  395. //If a spellCallback kills a mob he'll unregister his callbacks
  396. if (!c) {
  397. i--;
  398. cLen--;
  399. continue;
  400. }
  401. c.time -= consts.tickTime;
  402. if (c.time <= 0) {
  403. if (c.callback)
  404. c.callback();
  405. if (c.destroyCallback)
  406. c.destroyCallback();
  407. callbacks.splice(i, 1);
  408. i--;
  409. cLen--;
  410. }
  411. }
  412. return didCast || isCasting;
  413. },
  414. registerCallback: function (sourceId, callback, time, destroyCallback, targetId, destroyOnRezone) {
  415. let obj = {
  416. sourceId: sourceId,
  417. targetId: targetId,
  418. callback: callback,
  419. destroyCallback: destroyCallback,
  420. destroyOnRezone: destroyOnRezone,
  421. time: time
  422. };
  423. this.callbacks.push(obj);
  424. return obj;
  425. },
  426. unregisterCallback: function (objId, isTarget) {
  427. let callbacks = this.callbacks;
  428. let cLen = callbacks.length;
  429. for (let i = 0; i < cLen; i++) {
  430. let c = callbacks[i];
  431. if (
  432. (
  433. isTarget &&
  434. c.targetId === objId
  435. ) ||
  436. (
  437. !isTarget &&
  438. c.sourceId === objId
  439. )
  440. ) {
  441. if (c.destroyCallback)
  442. c.destroyCallback();
  443. callbacks.splice(i, 1);
  444. i--;
  445. cLen--;
  446. }
  447. }
  448. },
  449. sendAnnouncement: function (msg) {
  450. process.send({
  451. method: 'events',
  452. data: {
  453. onGetAnnouncement: [{
  454. obj: {
  455. msg: msg
  456. },
  457. to: [this.obj.serverId]
  458. }]
  459. }
  460. });
  461. },
  462. fireEvent: function (event, args) {
  463. let spells = this.spells;
  464. let sLen = spells.length;
  465. for (let i = 0; i < sLen; i++) {
  466. let s = spells[i];
  467. let spellEvents = s.events;
  468. if (spellEvents) {
  469. let callback = spellEvents[event];
  470. if (!callback)
  471. continue;
  472. callback.apply(s, args);
  473. }
  474. if (s.castEvent === event)
  475. s.cast();
  476. }
  477. },
  478. isCasting: function () {
  479. return this.spells.some(s => s.currentAction);
  480. },
  481. stopCasting: function (ignore, skipAuto) {
  482. this.spells.forEach(s => {
  483. if (s === ignore)
  484. return;
  485. if (!skipAuto)
  486. s.setAuto(null);
  487. if (!s.currentAction)
  488. return;
  489. s.castTime = 0;
  490. s.currentAction = null;
  491. if (!ignore || !ignore.castTimeMax)
  492. this.obj.syncer.set(false, null, 'casting', 0);
  493. });
  494. },
  495. destroy: function () {
  496. this.spells.forEach(s => {
  497. if (s.destroy)
  498. s.destroy();
  499. });
  500. },
  501. events: {
  502. beforeMove: function () {
  503. this.stopCasting(null, true);
  504. },
  505. onBeforeUseItem: function () {
  506. this.stopCasting(null, true);
  507. },
  508. clearQueue: function () {
  509. this.stopCasting(null, true);
  510. },
  511. beforeDeath: function () {
  512. this.stopCasting(null, true);
  513. this.spells.forEach(function (s) {
  514. if (!s.castOnDeath)
  515. return;
  516. s.cast();
  517. });
  518. },
  519. beforeRezone: function () {
  520. this.spells.forEach(function (s) {
  521. if (s.active) {
  522. s.active = false;
  523. let reserve = s.manaReserve;
  524. if (reserve && reserve.percentage) {
  525. let reserveEvent = {
  526. spell: s.name,
  527. reservePercent: reserve.percentage
  528. };
  529. this.obj.fireEvent('onBeforeReserveMana', reserveEvent);
  530. this.obj.stats.addStat('manaReservePercent', -reserveEvent.reservePercent);
  531. }
  532. //Make sure to remove the buff from party members
  533. s.updateInactive();
  534. }
  535. }, this);
  536. let callbacks = this.callbacks;
  537. let cLen = callbacks.length;
  538. for (let i = 0; i < cLen; i++) {
  539. let c = callbacks[i];
  540. //If a spellCallback kills a mob he'll unregister his callbacks
  541. //Probably not needed since we aren't supposed to damage mobs in destroyCallback
  542. if (!c) {
  543. i--;
  544. cLen--;
  545. continue;
  546. }
  547. if (c.destroyOnRezone) {
  548. if (c.destroyCallback)
  549. c.destroyCallback();
  550. callbacks.splice(i, 1);
  551. i--;
  552. cLen--;
  553. }
  554. }
  555. }
  556. }
  557. };