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.
 
 
 

756 lines
17 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. const { rolls } = e;
  321. if (rolls.textTemplate) {
  322. let text = rolls.textTemplate;
  323. while (text.includes('((')) {
  324. Object.entries(rolls).forEach(([k, v]) => {
  325. text = text.replaceAll(`((${k}))`, v);
  326. });
  327. if (rolls.applyEffect) {
  328. Object.entries(rolls.applyEffect).forEach(([k, v]) => {
  329. text = text.replaceAll(`((applyEffect.${k}))`, v);
  330. });
  331. }
  332. if (rolls.castSpell) {
  333. Object.entries(rolls.castSpell).forEach(([k, v]) => {
  334. text = text.replaceAll(`((castSpell.${k}))`, v);
  335. });
  336. }
  337. if (rolls.applyEffect?.scaleDamage) {
  338. Object.entries(rolls.applyEffect.scaleDamage).forEach(([k, v]) => {
  339. text = text.replaceAll(`((applyEffect.scaleDamage.${k}))`, v);
  340. });
  341. }
  342. if (rolls.castSpell?.scaleDamage) {
  343. Object.entries(rolls.castSpell.scaleDamage).forEach(([k, v]) => {
  344. text = text.replaceAll(`((castSpell.scaleDamage.${k}))`, v);
  345. });
  346. }
  347. }
  348. e.text = text;
  349. } else if (effectModule.events.onGetText)
  350. e.text = effectModule.events.onGetText(item, e);
  351. } catch (error) {
  352. _.log(`Effect not found: ${e.type}`);
  353. _.log(error);
  354. }
  355. }
  356. });
  357. item.effects.spliceWhere(e => !e.events);
  358. }
  359. if (!item.has('pos') && !item.eq) {
  360. let pos = i;
  361. for (let j = 0; j < iLen; j++) {
  362. if (!items.some(fj => (fj.pos === j))) {
  363. pos = j;
  364. break;
  365. }
  366. }
  367. item.pos = pos;
  368. } else if ((!item.eq) && (items.some(ii => ((ii !== item) && (ii.pos === item.pos))))) {
  369. let pos = item.pos;
  370. for (let j = 0; j < iLen; j++) {
  371. if (!items.some(fi => ((fi !== item) && (fi.pos === j)))) {
  372. pos = j;
  373. break;
  374. }
  375. }
  376. item.pos = pos;
  377. }
  378. }
  379. },
  380. setItemPosition: function (id) {
  381. let item = this.findItem(id);
  382. if (!item)
  383. return;
  384. let iSize = this.inventorySize;
  385. for (let i = 0; i < iSize; i++) {
  386. if (!this.items.some(j => (j.pos === i))) {
  387. item.pos = i;
  388. break;
  389. }
  390. }
  391. },
  392. sortInventory: function () {
  393. this.items
  394. .filter(i => !i.eq)
  395. .map(i => {
  396. //If we don't do this, [waist] goes before [undefined]
  397. const useSlot = i.slot ? i.slot : 'z';
  398. return {
  399. item: i,
  400. sortId: `${useSlot}${i.material}${i.quest}${i.spell}${i.quality}${i.level}${i.sprite}${i.id}`
  401. };
  402. })
  403. .sort((a, b) => {
  404. if (a.sortId < b.sortId)
  405. return 1;
  406. else if (a.sortId > b.sortId)
  407. return -1;
  408. return 0;
  409. })
  410. .forEach((i, index) => {
  411. i.item.pos = index;
  412. this.obj.syncer.setArray(true, 'inventory', 'getItems', this.simplifyItem(i.item));
  413. });
  414. },
  415. resolveCallback: function (msg, result) {
  416. let callbackId = msg.has('callbackId') ? msg.callbackId : msg;
  417. result = result || [];
  418. if (!callbackId)
  419. return;
  420. process.send({
  421. module: 'atlas',
  422. method: 'resolveCallback',
  423. msg: {
  424. id: callbackId,
  425. result: result
  426. }
  427. });
  428. },
  429. findItem: function (id) {
  430. if (id === null)
  431. return null;
  432. return this.items.find(i => i.id === id);
  433. },
  434. getDefaultAbilities: function () {
  435. let hasWeapon = this.items.some(i => {
  436. return (
  437. i.spell &&
  438. i.spell.rolls &&
  439. i.spell.rolls.has('damage') &&
  440. (
  441. i.slot === 'twoHanded' ||
  442. i.slot === 'oneHanded'
  443. )
  444. );
  445. });
  446. if (!hasWeapon) {
  447. let item = generator.generate({
  448. type: classes.weapons[this.obj.class],
  449. quality: 0,
  450. spellQuality: 0
  451. });
  452. item.worth = 0;
  453. item.eq = true;
  454. item.noSalvage = true;
  455. this.getItem(item);
  456. }
  457. classes.spells[this.obj.class].forEach(spellName => {
  458. let hasSpell = this.items.some(i => {
  459. return (
  460. i.spell &&
  461. i.spell.name.toLowerCase() === spellName
  462. );
  463. });
  464. if (!hasSpell) {
  465. let item = generator.generate({
  466. spell: true,
  467. quality: 0,
  468. spellName: spellName
  469. });
  470. item.worth = 0;
  471. item.eq = true;
  472. item.noSalvage = true;
  473. this.getItem(item);
  474. }
  475. });
  476. },
  477. createBag: function (x, y, items, ownerName) {
  478. let bagCell = 50;
  479. let topQuality = 0;
  480. let iLen = items.length;
  481. for (let i = 0; i < iLen; i++) {
  482. let quality = items[i].quality;
  483. items[i].fromMob = !!this.obj.mob;
  484. if (quality > topQuality)
  485. topQuality = ~~quality;
  486. }
  487. if (topQuality === 0)
  488. bagCell = 50;
  489. else if (topQuality === 1)
  490. bagCell = 51;
  491. else if (topQuality === 2)
  492. bagCell = 128;
  493. else if (topQuality === 3)
  494. bagCell = 52;
  495. else
  496. bagCell = 53;
  497. const createBagMsg = {
  498. ownerName,
  499. x,
  500. y,
  501. sprite: {
  502. sheetName: 'objects',
  503. cell: bagCell
  504. },
  505. dropSource: this.obj
  506. };
  507. this.obj.instance.eventEmitter.emit('onBeforeCreateBag', createBagMsg);
  508. let obj = this.obj.instance.objects.buildObjects([{
  509. sheetName: createBagMsg.sprite.sheetName,
  510. cell: createBagMsg.sprite.cell,
  511. x: createBagMsg.x,
  512. y: createBagMsg.y,
  513. properties: {
  514. cpnChest: {
  515. ownerName,
  516. ttl: 1710
  517. },
  518. cpnInventory: {
  519. items: extend([], items)
  520. }
  521. }
  522. }]);
  523. obj.canBeSeenBy = ownerName;
  524. return obj;
  525. },
  526. hasSpace: function (item, noStack) {
  527. const itemArray = item ? [item] : [];
  528. return this.hasSpaceList(itemArray, noStack);
  529. },
  530. hasSpaceList: function (items, noStack) {
  531. if (this.inventorySize === -1)
  532. return true;
  533. let slots = this.inventorySize - this.obj.inventory.items.filter(f => !f.eq).length;
  534. for (const item of items) {
  535. if (isItemStackable(item) && (!noStack)) {
  536. let exists = this.items.find(owned => (owned.name === item.name) && (isItemStackable(owned)));
  537. if (exists)
  538. continue;
  539. }
  540. slots--;
  541. }
  542. return (slots >= 0);
  543. },
  544. getItem: function (item, hideMessage, noStack, hideAlert, createBagIfFull) {
  545. return getItem.call(this, this, ...arguments);
  546. },
  547. dropBag: function (ownerName, killSource) {
  548. dropBag(this, ownerName, killSource);
  549. },
  550. giveItems: function (obj, hideMessage) {
  551. let objInventory = obj.inventory;
  552. let items = this.items;
  553. let iLen = items.length;
  554. for (let i = 0; i < iLen; i++) {
  555. let item = items[i];
  556. if (objInventory.getItem(item, hideMessage)) {
  557. items.splice(i, 1);
  558. i--;
  559. iLen--;
  560. }
  561. }
  562. return !iLen;
  563. },
  564. fireEvent: function (event, args) {
  565. let items = this.items;
  566. let iLen = items.length;
  567. for (let i = 0; i < iLen; i++) {
  568. let item = items[i];
  569. if (!item.eq && !item.active) {
  570. if (event !== 'afterUnequipItem' || item !== args[0])
  571. continue;
  572. }
  573. let effects = item.effects;
  574. if (!effects)
  575. continue;
  576. let eLen = effects.length;
  577. for (let j = 0; j < eLen; j++) {
  578. let effect = effects[j];
  579. let effectEvent = effect.events[event];
  580. if (!effectEvent)
  581. continue;
  582. effectEvent.apply(this.obj, [item, ...args]);
  583. }
  584. }
  585. },
  586. clear: function () {
  587. delete this.items;
  588. this.items = [];
  589. },
  590. equipItemErrors: function (item) {
  591. let errors = [];
  592. if (!this.obj.player)
  593. return [];
  594. let stats = this.obj.stats.values;
  595. if (item.level > stats.level)
  596. errors.push('level');
  597. if ((item.requires) && (stats[item.requires[0].stat] < item.requires[0].value))
  598. errors.push(item.requires[0].stat);
  599. if (item.factions) {
  600. if (item.factions.some(function (f) {
  601. return f.noEquip;
  602. }))
  603. errors.push('faction');
  604. }
  605. return errors;
  606. },
  607. canEquipItem: function (item) {
  608. return (this.equipItemErrors(item).length === 0);
  609. },
  610. notifyNoBagSpace: function (message = 'Your bags are too full to loot any more items') {
  611. this.obj.social.notifySelf({ message });
  612. }
  613. };