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.
 
 
 

719 lines
16 KiB

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