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.
 
 
 

828 lines
18 KiB

  1. let generator = require('../items/generator');
  2. let salvager = require('../items/salvager');
  3. let enchanter = require('../items/enchanter');
  4. let classes = require('../config/spirits');
  5. let mtx = require('../mtx/mtx');
  6. let factions = require('../config/factions');
  7. let itemEffects = require('../items/itemEffects');
  8. const events = require('../misc/events');
  9. const { isItemStackable } = require('./inventory/helpers');
  10. const transactions = require('../security/transactions');
  11. const { applyItemStats } = require('./equipment/helpers');
  12. const getItem = require('./inventory/getItem');
  13. const dropBag = require('./inventory/dropBag');
  14. const useItem = require('./inventory/useItem');
  15. module.exports = {
  16. type: 'inventory',
  17. inventorySize: 50,
  18. items: [],
  19. blueprint: null,
  20. init: function (blueprint, isTransfer) {
  21. let items = blueprint.items || [];
  22. let iLen = items.length;
  23. //Spells should be sorted so they're EQ'd in the right order
  24. items.sort(function (a, b) {
  25. let aId = a.has('spellId') ? ~~a.spellId : 9999;
  26. let bId = b.has('spellId') ? ~~b.spellId : 9999;
  27. return (aId - bId);
  28. });
  29. for (let i = 0; i < iLen; i++) {
  30. let item = items[i];
  31. if ((item.pos >= this.inventorySize) || (item.eq))
  32. delete item.pos;
  33. while (item.name.indexOf('\'\'') > -1)
  34. item.name = item.name.replace('\'\'', '\'');
  35. }
  36. this.hookItemEvents(items);
  37. //Hack to skip attr checks on equip
  38. let oldFn = this.canEquipItem;
  39. this.canEquipItem = () => {
  40. return true;
  41. };
  42. for (let i = 0; i < iLen; i++) {
  43. let item = items[i];
  44. let pos = item.has('pos') ? item.pos : null;
  45. let newItem = this.getItem(item, true, true);
  46. newItem.pos = pos;
  47. }
  48. //Hack to skip attr checks on equip
  49. this.canEquipItem = oldFn.bind(this);
  50. if ((this.obj.player) && (!isTransfer) && (this.obj.stats.values.level === 1))
  51. this.getDefaultAbilities();
  52. delete blueprint.items;
  53. this.blueprint = blueprint;
  54. if (this.obj.equipment)
  55. this.obj.equipment.unequipAttrRqrGear();
  56. },
  57. transfer: function () {
  58. this.hookItemEvents();
  59. },
  60. save: function () {
  61. return {
  62. type: 'inventory',
  63. items: this.items.map(this.simplifyItem.bind(this))
  64. };
  65. },
  66. simplify: function (self) {
  67. if (!self)
  68. return null;
  69. return {
  70. type: 'inventory',
  71. items: this.items.map(this.simplifyItem.bind(this))
  72. };
  73. },
  74. simplifyItem: function (item) {
  75. let result = extend({}, item);
  76. if (result.effects) {
  77. result.effects = result.effects.map(e => ({
  78. factionId: e.factionId || null,
  79. text: e.text || null,
  80. properties: e.properties || null,
  81. mtx: e.mtx || null,
  82. type: e.type || null,
  83. rolls: e.rolls || null
  84. }));
  85. }
  86. let reputation = this.obj.reputation;
  87. if (result.factions) {
  88. result.factions = result.factions.map(function (f) {
  89. let res = {
  90. id: f.id,
  91. tier: f.tier,
  92. tierName: ['Hated', 'Hostile', 'Unfriendly', 'Neutral', 'Friendly', 'Honored', 'Revered', 'Exalted'][f.tier]
  93. };
  94. if (reputation) {
  95. let faction = reputation.getBlueprint(f.id);
  96. let factionTier = reputation.getTier(f.id);
  97. let noEquip = null;
  98. if (factionTier < f.tier)
  99. noEquip = true;
  100. res.name = faction.name;
  101. res.noEquip = noEquip;
  102. }
  103. return res;
  104. }, this);
  105. }
  106. return result;
  107. },
  108. update: function () {
  109. let items = this.items;
  110. let iLen = items.length;
  111. for (let i = 0; i < iLen; i++) {
  112. let item = items[i];
  113. if (!item.cd)
  114. continue;
  115. item.cd--;
  116. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  117. }
  118. },
  119. enchantItem: function (msg) {
  120. const { itemId, action } = msg;
  121. const item = this.findItem(itemId);
  122. if (!item)
  123. return;
  124. const { eq, slot, power, noAugment } = item;
  125. if (!slot || noAugment || (action === 'scour' && !power)) {
  126. this.resolveCallback(msg);
  127. return;
  128. }
  129. const obj = this.obj;
  130. if (eq) {
  131. applyItemStats(obj, item, false);
  132. enchanter.enchant(obj, item, msg);
  133. applyItemStats(obj, item, true);
  134. if (item.slot !== slot)
  135. obj.equipment.unequip(itemId);
  136. else
  137. obj.spellbook.calcDps();
  138. } else
  139. enchanter.enchant(obj, item, msg);
  140. obj.equipment.unequipAttrRqrGear();
  141. },
  142. getEnchantMaterials: function (msg) {
  143. let result = [];
  144. let item = this.findItem(msg.itemId);
  145. if ((item) && (item.slot))
  146. result = enchanter.getEnchantMaterials(item, msg.action);
  147. this.resolveCallback(msg, result);
  148. },
  149. learnAbility: function (itemId, runeSlot) {
  150. if (itemId.has('itemId')) {
  151. let msg = itemId;
  152. itemId = msg.itemId;
  153. runeSlot = msg.slot;
  154. }
  155. let item = this.findItem(itemId);
  156. let statValues = this.obj.stats.values;
  157. if (!item)
  158. return;
  159. else if (!item.spell) {
  160. item.eq = false;
  161. return;
  162. } else if (item.level > statValues.level) {
  163. item.eq = false;
  164. return;
  165. }
  166. let learnMsg = {
  167. success: true,
  168. item: item
  169. };
  170. this.obj.fireEvent('beforeLearnAbility', learnMsg);
  171. if (!learnMsg.success) {
  172. this.obj.instance.syncer.queue('onGetMessages', {
  173. id: this.obj.id,
  174. messages: [{
  175. class: 'color-redA',
  176. message: learnMsg.msg || 'you cannot learn that ability',
  177. type: 'info'
  178. }]
  179. }, [this.obj.serverId]);
  180. return;
  181. }
  182. let spellbook = this.obj.spellbook;
  183. if ((item.slot === 'twoHanded') || (item.slot === 'oneHanded'))
  184. runeSlot = 0;
  185. else if (!runeSlot) {
  186. runeSlot = 4;
  187. for (let i = 1; i <= 4; i++) {
  188. if (!this.items.some(j => (j.runeSlot === i))) {
  189. runeSlot = i;
  190. break;
  191. }
  192. }
  193. }
  194. let currentEq = this.items.find(i => (i.runeSlot === runeSlot));
  195. if (currentEq) {
  196. spellbook.removeSpellById(runeSlot);
  197. delete currentEq.eq;
  198. delete currentEq.runeSlot;
  199. this.setItemPosition(currentEq.id);
  200. this.obj.syncer.setArray(true, 'inventory', 'getItems', currentEq);
  201. }
  202. item.eq = true;
  203. item.runeSlot = runeSlot;
  204. delete item.pos;
  205. spellbook.addSpellFromRune(item.spell, runeSlot);
  206. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  207. },
  208. activateMtx: function (itemId) {
  209. let item = this.findItem(itemId);
  210. if (!item)
  211. return;
  212. else if (item.type !== 'mtx') {
  213. delete item.active;
  214. return;
  215. }
  216. item.active = !item.active;
  217. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  218. },
  219. splitStack: function (msg) {
  220. let item = this.findItem(msg.itemId);
  221. if (!item || !item.quantity || item.quantity <= msg.stackSize || msg.stackSize < 1 || item.quest)
  222. return;
  223. const hasSpace = this.hasSpace(item, true);
  224. if (!hasSpace) {
  225. this.notifyNoBagSpace();
  226. return;
  227. }
  228. let newItem = extend({}, item);
  229. item.quantity -= msg.stackSize;
  230. newItem.quantity = msg.stackSize;
  231. this.getItem(newItem, true, true);
  232. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  233. },
  234. combineStacks: function (msg) {
  235. let fromItem = this.findItem(msg.fromId);
  236. let toItem = this.findItem(msg.toId);
  237. if ((!fromItem) || (!toItem))
  238. return;
  239. else if ((!isItemStackable(fromItem)) || (!isItemStackable(toItem)))
  240. return;
  241. toItem.quantity += fromItem.quantity;
  242. this.obj.syncer.setArray(true, 'inventory', 'getItems', toItem);
  243. this.destroyItem(fromItem.id, null, true);
  244. },
  245. useItem: function (itemId) {
  246. useItem(this, itemId);
  247. },
  248. unlearnAbility: function (itemId) {
  249. if (itemId.has('itemId'))
  250. itemId = itemId.itemId;
  251. let item = this.findItem(itemId);
  252. if (!item)
  253. return;
  254. else if (!item.spell) {
  255. item.eq = false;
  256. return;
  257. }
  258. let spellbook = this.obj.spellbook;
  259. spellbook.removeSpellById(item.runeSlot);
  260. delete item.eq;
  261. delete item.runeSlot;
  262. if (!item.slot)
  263. this.setItemPosition(itemId);
  264. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  265. },
  266. stashItem: function (id) {
  267. let item = this.findItem(id);
  268. if (!item || item.quest || item.noStash)
  269. return;
  270. delete item.pos;
  271. let stash = this.obj.stash;
  272. if (!stash.active)
  273. return;
  274. let clonedItem = extend({}, item);
  275. const success = stash.deposit(clonedItem);
  276. if (!success)
  277. return;
  278. this.destroyItem(id, null, true);
  279. },
  280. salvageItem: function (id) {
  281. let item = this.findItem(id);
  282. if ((!item) || (item.material) || (item.quest) || (item.noSalvage) || (item.eq))
  283. return;
  284. let messages = [];
  285. let items = salvager.salvage(item);
  286. let materialDrop = [];
  287. this.destroyItem(id);
  288. for (const material of items) {
  289. if (this.hasSpace(material))
  290. this.getItem(material, true);
  291. else
  292. materialDrop.push(material);
  293. messages.push({
  294. class: 'q' + material.quality,
  295. message: 'salvage (' + material.name + ' x' + material.quantity + ')'
  296. });
  297. }
  298. if (materialDrop.length > 0) {
  299. this.createBag(this.obj.x, this.obj.y, materialDrop);
  300. messages.push({
  301. class: 'color-redA',
  302. message: 'Your bags are too full for your salvaged materials, they have been dropped to the ground.'
  303. });
  304. }
  305. this.obj.instance.syncer.queue('onGetMessages', {
  306. id: this.obj.id,
  307. messages: messages
  308. }, [this.obj.serverId]);
  309. },
  310. destroyItem: function (id, amount, force) {
  311. let item = this.findItem(id);
  312. if (!item || (item.noDestroy && !force))
  313. return;
  314. amount = amount || item.quantity;
  315. if (item.eq)
  316. this.obj.equipment.unequip(id);
  317. if ((item.quantity) && (amount)) {
  318. item.quantity -= amount;
  319. if (item.quantity <= 0) {
  320. this.items.spliceWhere(i => i.id === id);
  321. this.obj.syncer.setArray(true, 'inventory', 'destroyItems', id);
  322. } else
  323. this.obj.syncer.setArray(true, 'inventory', 'getItems', item);
  324. } else {
  325. this.items.spliceWhere(i => i.id === id);
  326. this.obj.syncer.setArray(true, 'inventory', 'destroyItems', id);
  327. this.obj.syncer.deleteFromArray(true, 'inventory', 'getItems', i => i.id === id);
  328. }
  329. this.obj.fireEvent('afterDestroyItem', item, amount);
  330. events.emit('afterPlayerDestroyItem', this.obj, item, amount);
  331. return item;
  332. },
  333. dropItem: function (id) {
  334. let item = this.findItem(id);
  335. if ((!item) || (item.noDrop) || (item.quest))
  336. return;
  337. if (item.has('quickSlot')) {
  338. this.obj.equipment.setQuickSlot({
  339. itemId: null,
  340. slot: item.quickSlot
  341. });
  342. delete item.quickSlot;
  343. }
  344. delete item.pos;
  345. //Find close open position
  346. let x = this.obj.x;
  347. let y = this.obj.y;
  348. let dropCell = this.obj.instance.physics.getOpenCellInArea(x - 1, y - 1, x + 1, y + 1);
  349. if (!dropCell)
  350. return;
  351. if (item.eq)
  352. this.obj.equipment.unequip(id);
  353. this.items.spliceWhere(i => i.id === id);
  354. this.obj.syncer.setArray(true, 'inventory', 'destroyItems', id);
  355. this.createBag(dropCell.x, dropCell.y, [item]);
  356. events.emit('afterPlayerDropItem', this.obj, item);
  357. },
  358. moveItem: function (msgs) {
  359. msgs.forEach(function (m) {
  360. let item = this.findItem(m.id);
  361. if (!item)
  362. return;
  363. item.pos = m.pos;
  364. }, this);
  365. },
  366. mailItem: async function (msg) {
  367. let item = this.findItem(msg.itemId);
  368. if (!item || item.noDrop || item.quest) {
  369. this.resolveCallback(msg);
  370. return;
  371. }
  372. delete item.pos;
  373. let res = await io.getAsync({
  374. key: msg.recipient,
  375. table: 'character'
  376. });
  377. if (!res) {
  378. this.resolveCallback(msg, 'Recipient does not exist');
  379. return;
  380. } else if (!this.findItem(msg.itemId))
  381. return;
  382. const resolveTrans = transactions.register();
  383. let blocked = false;
  384. if (res.components) {
  385. let social = res.components.find(f => f.type === 'social');
  386. if (social.blockedPlayers && social.blockedPlayers.includes(this.obj.name))
  387. blocked = true;
  388. }
  389. const mappedItem = this.simplifyItem(item);
  390. this.destroyItem(item.id);
  391. if (!blocked)
  392. await this.obj.instance.mail.sendMail(msg.recipient, [mappedItem]);
  393. this.resolveCallback(msg);
  394. resolveTrans();
  395. },
  396. hookItemEvents: function (items) {
  397. items = items || this.items;
  398. if (!items.push)
  399. items = [ items ];
  400. let iLen = items.length;
  401. for (let i = 0; i < iLen; i++) {
  402. let item = items[i];
  403. if (item.effects) {
  404. item.effects.forEach(function (e) {
  405. if (e.mtx) {
  406. let mtxUrl = mtx.get(e.mtx);
  407. let mtxModule = require('../' + mtxUrl);
  408. e.events = mtxModule.events;
  409. } else if (e.factionId) {
  410. let faction = factions.getFaction(e.factionId);
  411. let statGenerator = faction.uniqueStat;
  412. statGenerator.generate(item);
  413. } else {
  414. let effectUrl = itemEffects.get(e.type);
  415. try {
  416. let effectModule = require('../' + effectUrl);
  417. e.events = effectModule.events;
  418. e.text = effectModule.events.onGetText(item, e);
  419. } catch (error) {}
  420. }
  421. });
  422. }
  423. if (!item.has('pos') && !item.eq) {
  424. let pos = i;
  425. for (let j = 0; j < iLen; j++) {
  426. if (!items.some(fj => (fj.pos === j))) {
  427. pos = j;
  428. break;
  429. }
  430. }
  431. item.pos = pos;
  432. } else if ((!item.eq) && (items.some(ii => ((ii !== item) && (ii.pos === item.pos))))) {
  433. let pos = item.pos;
  434. for (let j = 0; j < iLen; j++) {
  435. if (!items.some(fi => ((fi !== item) && (fi.pos === j)))) {
  436. pos = j;
  437. break;
  438. }
  439. }
  440. item.pos = pos;
  441. }
  442. }
  443. },
  444. setItemPosition: function (id) {
  445. let item = this.findItem(id);
  446. if (!item)
  447. return;
  448. let iSize = this.inventorySize;
  449. for (let i = 0; i < iSize; i++) {
  450. if (!this.items.some(j => (j.pos === i))) {
  451. item.pos = i;
  452. break;
  453. }
  454. }
  455. },
  456. sortInventory: function () {
  457. this.items
  458. .filter(i => !i.eq)
  459. .map(i => {
  460. //If we don't do this, [waist] goes before [undefined]
  461. const useSlot = i.slot ? i.slot : 'z';
  462. return {
  463. item: i,
  464. sortId: `${useSlot}${i.material}${i.quest}${i.spell}${i.quality}${i.level}${i.sprite}${i.id}`
  465. };
  466. })
  467. .sort((a, b) => {
  468. if (a.sortId < b.sortId)
  469. return 1;
  470. else if (a.sortId > b.sortId)
  471. return -1;
  472. return 0;
  473. })
  474. .forEach((i, index) => {
  475. i.item.pos = index;
  476. this.obj.syncer.setArray(true, 'inventory', 'getItems', this.simplifyItem(i.item));
  477. });
  478. },
  479. resolveCallback: function (msg, result) {
  480. let callbackId = msg.has('callbackId') ? msg.callbackId : msg;
  481. result = result || [];
  482. if (!callbackId)
  483. return;
  484. process.send({
  485. module: 'atlas',
  486. method: 'resolveCallback',
  487. msg: {
  488. id: callbackId,
  489. result: result
  490. }
  491. });
  492. },
  493. findItem: function (id) {
  494. if (id === null)
  495. return null;
  496. return this.items.find(i => i.id === id);
  497. },
  498. getDefaultAbilities: function () {
  499. let hasWeapon = this.items.some(i => {
  500. return (
  501. i.spell &&
  502. i.spell.rolls &&
  503. i.spell.rolls.has('damage') &&
  504. (
  505. i.slot === 'twoHanded' ||
  506. i.slot === 'oneHanded'
  507. )
  508. );
  509. });
  510. if (!hasWeapon) {
  511. let item = generator.generate({
  512. type: classes.weapons[this.obj.class],
  513. quality: 0,
  514. spellQuality: 'basic'
  515. });
  516. item.worth = 0;
  517. item.eq = true;
  518. item.noSalvage = true;
  519. this.getItem(item);
  520. }
  521. classes.spells[this.obj.class].forEach(spellName => {
  522. let hasSpell = this.items.some(i => {
  523. return (
  524. i.spell &&
  525. i.spell.name.toLowerCase() === spellName
  526. );
  527. });
  528. if (!hasSpell) {
  529. let item = generator.generate({
  530. spell: true,
  531. spellQuality: 'basic',
  532. spellName: spellName
  533. });
  534. item.worth = 0;
  535. item.eq = true;
  536. item.noSalvage = true;
  537. this.getItem(item);
  538. }
  539. });
  540. },
  541. createBag: function (x, y, items, ownerName) {
  542. let bagCell = 50;
  543. let topQuality = 0;
  544. let iLen = items.length;
  545. for (let i = 0; i < iLen; i++) {
  546. let quality = items[i].quality;
  547. items[i].fromMob = !!this.obj.mob;
  548. if (quality > topQuality)
  549. topQuality = ~~quality;
  550. }
  551. if (topQuality === 0)
  552. bagCell = 50;
  553. else if (topQuality === 1)
  554. bagCell = 51;
  555. else if (topQuality === 2)
  556. bagCell = 128;
  557. else if (topQuality === 3)
  558. bagCell = 52;
  559. else
  560. bagCell = 53;
  561. let obj = this.obj.instance.objects.buildObjects([{
  562. sheetName: 'objects',
  563. cell: bagCell,
  564. x: x,
  565. y: y,
  566. properties: {
  567. cpnChest: {
  568. ownerName: ownerName,
  569. ttl: 1710
  570. },
  571. cpnInventory: {
  572. items: extend([], items)
  573. }
  574. }
  575. }]);
  576. return obj;
  577. },
  578. hasSpace: function (item, noStack) {
  579. return this.hasSpaceList([item], noStack);
  580. },
  581. hasSpaceList: function (items, noStack) {
  582. if (this.inventorySize === -1)
  583. return true;
  584. let slots = this.inventorySize - this.obj.inventory.items.filter(f => !f.eq).length;
  585. for (const item of items) {
  586. if (isItemStackable(item) && (!noStack)) {
  587. let exists = this.items.find(owned => (owned.name === item.name) && (isItemStackable(owned)));
  588. if (exists)
  589. continue;
  590. }
  591. slots--;
  592. }
  593. return (slots >= 0);
  594. },
  595. getItem: function (item, hideMessage, noStack, hideAlert) {
  596. return getItem(this, item, hideMessage, noStack, hideAlert);
  597. },
  598. dropBag: function (ownerName, killSource) {
  599. dropBag(this, ownerName, killSource);
  600. },
  601. giveItems: function (obj, hideMessage) {
  602. let objInventory = obj.inventory;
  603. let items = this.items;
  604. let iLen = items.length;
  605. for (let i = 0; i < iLen; i++) {
  606. let item = items[i];
  607. if (objInventory.getItem(item, hideMessage)) {
  608. items.splice(i, 1);
  609. i--;
  610. iLen--;
  611. }
  612. }
  613. return !iLen;
  614. },
  615. fireEvent: function (event, args) {
  616. let items = this.items;
  617. let iLen = items.length;
  618. for (let i = 0; i < iLen; i++) {
  619. let item = items[i];
  620. if (!item.eq && !item.active) {
  621. if (event !== 'afterUnequipItem' || item !== args[0])
  622. continue;
  623. }
  624. let effects = item.effects;
  625. if (!effects)
  626. continue;
  627. let eLen = effects.length;
  628. for (let j = 0; j < eLen; j++) {
  629. let effect = effects[j];
  630. let effectEvent = effect.events[event];
  631. if (!effectEvent)
  632. continue;
  633. effectEvent.apply(this.obj, [item, ...args]);
  634. }
  635. }
  636. },
  637. clear: function () {
  638. delete this.items;
  639. this.items = [];
  640. },
  641. equipItemErrors: function (item) {
  642. let errors = [];
  643. if (!this.obj.player)
  644. return [];
  645. let stats = this.obj.stats.values;
  646. if (item.level > stats.level)
  647. errors.push('level');
  648. if ((item.requires) && (stats[item.requires[0].stat] < item.requires[0].value))
  649. errors.push(item.requires[0].stat);
  650. if (item.factions) {
  651. if (item.factions.some(function (f) {
  652. return f.noEquip;
  653. }))
  654. errors.push('faction');
  655. }
  656. return errors;
  657. },
  658. canEquipItem: function (item) {
  659. return (this.equipItemErrors(item).length === 0);
  660. },
  661. notifyNoBagSpace: function () {
  662. this.obj.instance.syncer.queue('onGetMessages', {
  663. id: this.obj.id,
  664. messages: [{
  665. class: 'color-redA',
  666. message: 'Your bags are too full to loot any more items',
  667. type: 'info'
  668. }]
  669. }, [this.obj.serverId]);
  670. }
  671. };