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.

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