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.
 
 
 

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