Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

739 строки
16 KiB

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