Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

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