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.
 
 
 

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