Guest User

Untitled

a guest
Jan 19th, 2020
761
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 157.29 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Steam Trade Offer Enhancer
  3. // @description Browser script to enhance Steam trade offers.
  4. // @version 1.9.5
  5. // @author Julia
  6. // @namespace http://steamcommunity.com/profiles/76561198080179568/
  7. // @updateURL https://github.com/juliarose/steam-trade-offer-enhancer/raw/master/steam.trade.offer.enhancer.meta.js
  8. // @downloadURL https://github.com/juliarose/steam-trade-offer-enhancer/raw/master/steam.trade.offer.enhancer.user.js
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant unsafeWindow
  13. // @run-at document-end
  14. // @include /^https?:\/\/(.*\.)?backpack\.tf(:\\d+)?\/(stats|classifieds).*/
  15. // @include /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/(?:id|profiles)\/.*/
  16. // @include /^https?:\/\/steamcommunity\.com\/(?:id|profiles)\/.*\/inventory/
  17. // @include /^https?:\/\/steamcommunity\.com\/(?:id|profiles)\/.*\/tradeoffers/
  18. // @include /^https?:\/\/steamcommunity\.com\/tradeoffer.*/
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. const scripts = [
  25. {
  26. includes: [
  27. /^https?:\/\/(.*\.)?backpack\.tf(:\\d+)?\/(stats|classifieds).*/
  28. ],
  29. fn: function({Utils}) {
  30. const dom = {
  31. listingsElList: document.getElementsByClassName('listing')
  32. };
  33.  
  34. Array.from(dom.listingsElList).forEach((listingEl) => {
  35. const itemEl = listingEl.getElementsByClassName('item')[0];
  36. const offerButtonEl = listingEl.getElementsByClassName('listing-buttons')[0].lastElementChild;
  37. const href = offerButtonEl.getAttribute('href');
  38. const {
  39. listing_intent,
  40. listing_price
  41. } = itemEl.dataset;
  42. const currencies = Utils.stringToCurrencies(listing_price);
  43.  
  44. if (currencies != null) {
  45. // array of query string parameters
  46. // e.g. ['listing_intent=1', 'listing_currencies_keys=2']
  47. const query = (function getQuery() {
  48. const params = {
  49. listing_intent: listing_intent === 'buy' ? 0 : 1
  50. };
  51.  
  52. for (let k in currencies) {
  53. params['listing_currencies_' + k] = currencies[k];
  54. }
  55.  
  56. return Object.entries(params).map(([k, v]) => {
  57. return k + '=' + v;
  58. });
  59. }());
  60. // url with query added
  61. const url = [
  62. href,
  63. ...query
  64. ].join('&');
  65.  
  66. offerButtonEl.setAttribute('href', url);
  67. }
  68. });
  69. }
  70. },
  71. {
  72. includes: [
  73. /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/(?:id|profiles)\/.*/
  74. ],
  75. fn: function({$, Utils, getStored, setStored}) {
  76. const urlParams = Utils.getURLParams();
  77. const stored = {
  78. key_price: 'getInventory.key_price'
  79. };
  80. const page = {
  81. $document: $(document),
  82. $backpack: $('#backpack'),
  83. $refined: $('.refined-value'),
  84. $inventorySortMenu: $('#inventory-sort-menu ul.dropdown-menu'),
  85. get: {
  86. $selected: () => page.$backpack.find('li.item:visible:not(.unselected)'),
  87. $listedItems: () => page.$backpack.find('li.item:visible:not(.unselected)[data-listing_price]'),
  88. $firstSelectPage: () => page.$backpack.find('span.select-page:first'),
  89. $backpackPage: () => page.$backpack.find('div.backpack-page'),
  90. $itemPricedInKeys: () => page.$backpack.find('li.item[data-p_bptf*="keys"]:first'),
  91. $crateKey: () => page.$backpack.find('.item[data-name="Mann Co. Supply Crate Key"]:first'),
  92. $inventoryCmpFrom: () => $('#inventory-cmp-from'),
  93. $inventoryCmpTo: () => $('#inventory-cmp-to')
  94. }
  95. };
  96. // the key value is used for displaying totals in keys
  97. // get key value from cache, if available
  98. let keyValue = getStored(stored.key_price);
  99.  
  100. // wait for the backpack to load
  101. (function waitForBackpack() {
  102. // the backpack was loaded
  103. function onBackpackLoad() {
  104. // get the value of keys in metal
  105. // this should be very approximate, but close enough
  106. function getKeyValue() {
  107. /**
  108. * Get pricing details from item.
  109. * @param {Object} item - DOM element of data.
  110. * @returns {Object} Object containing price details.
  111. */
  112. function parseItem(item) {
  113. // parse price string e.g. "1-1.2 keys"
  114. function parseString(string) {
  115. const match = string.match(/^([\d\.]*)[\-\u2013]?([\d\.]*)? (\w*)/);
  116. const currencyNames = {
  117. 'metal': 'metal',
  118. 'ref': 'metal',
  119. 'keys': 'keys',
  120. 'key': 'keys'
  121. };
  122.  
  123. if (match) {
  124. details.value = parseFloat(match[1]);
  125. details.average = details.value;
  126. details.currency = currencyNames[match[3]];
  127.  
  128. // if there are 3 match groups, there is a range
  129. if (match[2]) {
  130. details.value_high = parseFloat(match[2]);
  131. details.average = (details.value + details.value_high) / 2;
  132. }
  133. }
  134. }
  135.  
  136. function getRefinedValue(allStr) {
  137. const match = allStr.replace(/\,/g, '').match(/(\d+\.?\d*) ref/);
  138. const value = match && parseFloat(match[1]);
  139. const rawValue = details.raw;
  140. const canUseRawValue = Boolean(
  141. value &&
  142. rawValue &&
  143. value.toFixed(2) === rawValue.toFixed(2)
  144. );
  145.  
  146. // the raw value has extra precision but includes the value of paint/strange parts.
  147. // if it is close to the value of the price items,
  148. // we can use the raw value instead which is more precise
  149. if (canUseRawValue) {
  150. return rawValue;
  151. } else {
  152. return value || rawValue;
  153. }
  154. }
  155.  
  156. const data = item.dataset;
  157. const details = {};
  158.  
  159. if (data.price) {
  160. details.raw = parseFloat(data.price);
  161. }
  162.  
  163. if (data.p_bptf) {
  164. parseString(data.p_bptf);
  165. }
  166.  
  167. details.refined = getRefinedValue(data.p_bptf_all || '');
  168.  
  169. return details;
  170. }
  171.  
  172. // find item priced in keys
  173. const item = page.get.$itemPricedInKeys()[0];
  174. const price = item && parseItem(item);
  175. const useItemPricedInKeys = Boolean(
  176. price &&
  177. price.currency === 'keys' &&
  178. price.average &&
  179. price.refined
  180. );
  181.  
  182. // use an item priced in keys to extract the key value
  183. if (useItemPricedInKeys) {
  184. // to get the value of keys in refined metal...
  185. // take the price in metal divided by the price in keys
  186. return price.refined / price.average;
  187. } else {
  188. // set value using the value of a key, if no items in inventory are priced in keys
  189. const key = page.get.$crateKey()[0];
  190. const price = (
  191. key &&
  192. parseItem(key)
  193. );
  194.  
  195. return (
  196. price &&
  197. price.refined
  198. );
  199. }
  200. }
  201.  
  202. function filterItems($filtered) {
  203. // no items to filter
  204. if ($filtered.length === 0) {
  205. return;
  206. }
  207.  
  208. const $backpack = page.$backpack;
  209. const $items = $backpack.find('li.item:not(.spacer)');
  210. const $unfiltered = $items.not($filtered);
  211. const $spacers = $backpack.find('li.spacer');
  212. // all hidden items are moved to a temp page
  213. const $tempPage = $('<div class="temp-page" style="display:none;"/>');
  214.  
  215. // sort
  216. sortBy('price');
  217. // then add the temp page, it will be hidden
  218. $backpack.append($tempPage);
  219. // remove spacers
  220. $spacers.appendTo($tempPage);
  221. // add the unfiltered items to the temp page
  222. $unfiltered.appendTo($tempPage);
  223. // hide pages that contain no items
  224. page.get.$backpackPage().each((i, el) => {
  225. const $page = $(el);
  226. const $items = $page.find('.item-list .item');
  227.  
  228. if ($items.length === 0) {
  229. $page.hide();
  230. }
  231. });
  232. // then update totals
  233. // hackish way of updating totals
  234. page.get.$firstSelectPage().trigger('click');
  235. page.get.$firstSelectPage().trigger('click');
  236. }
  237.  
  238. /**
  239. * Select items on page matching IDs.
  240. * @param {Array} ids - Array of IDs to select.
  241. * @returns {undefined}
  242. */
  243. function selectItemsById(ids) {
  244. const $backpack = page.$backpack;
  245. const $items = $backpack.find('li.item:not(.spacer)');
  246. const selectors = ids.map(id => `[data-id="${id}"]`);
  247. // select items
  248. const $filtered = $items.filter(selectors.join(','));
  249.  
  250. filterItems($filtered);
  251. }
  252.  
  253. function sortBy(key) {
  254. page.$inventorySortMenu.find(`li[data-value="${key}"]`).trigger('click');
  255. }
  256.  
  257. /**
  258. * Change comparison.
  259. * @param {Boolean} up - Go to next day if true, previous day if false.
  260. * @returns {undefined}
  261. */
  262. function compare(up) {
  263. const $from = page.get.$inventoryCmpFrom();
  264. const $to = page.get.$inventoryCmpTo();
  265. const isAvailable = (
  266. $from.length > 0 &&
  267. !$from.hasClass('disabled')
  268. );
  269.  
  270. // no selections available
  271. if (!isAvailable) {
  272. return;
  273. }
  274.  
  275. const from = parseInt($from.val());
  276. const to = parseInt($to.val());
  277. const options = $from.find('option').map((i, el) => {
  278. return parseInt(el.value);
  279. }).get();
  280. const filtered = options.filter((option) => {
  281. if (option === to || option === from) {
  282. return false;
  283. } else if (up) {
  284. return option > to;
  285. }
  286.  
  287. return option < to;
  288. });
  289.  
  290. // no items
  291. if (filtered.length === 0) {
  292. return;
  293. }
  294.  
  295. const value = up ? Math.min(...filtered) : Math.max(...filtered);
  296. const abs = [from, to].map(a => Math.abs(a - value));
  297. // farthest... closest? I failed math, but it works
  298. const farthest = Math.min(...abs) === Math.abs(from - value) ? from : to;
  299.  
  300. if (farthest === from) {
  301. $to.val(value).trigger('change');
  302. } else if (farthest === to) {
  303. $from.val(value).trigger('change');
  304. }
  305. }
  306.  
  307. // get the id's of all selected items
  308. function getIDs() {
  309. return page.get.$selected().map((i, el) => {
  310. return el.dataset.id;
  311. }).get();
  312. }
  313.  
  314. function copyIDs() {
  315. Utils.copyToClipboard(getIDs().join(','));
  316. }
  317.  
  318. function keyPressed(e) {
  319. Utils.execHotKey(e, {
  320. // P
  321. 112: copyIDs,
  322. // 1
  323. 49: () => sortBy('bpslot'),
  324. // 2
  325. 50: () => sortBy('price'),
  326. // 3
  327. 51: () => sortBy('market'),
  328. // W
  329. 119: () => compare(true),
  330. // S
  331. 115: () => compare(false)
  332. });
  333. }
  334.  
  335. // disconnect observer since the backpack has been loaded
  336. observer.disconnect();
  337. // then callback
  338.  
  339. // ids are comma-seperated in select param
  340. const select = Utils.getIDsFromString(urlParams.select);
  341. // get key value using items in inventory
  342. const bpKeyValue = getKeyValue();
  343.  
  344. if (bpKeyValue) {
  345. // set keyValue to price obtained from inventory
  346. // this should be very approximate. but close enough
  347. keyValue = bpKeyValue;
  348. // then cache it
  349. setStored(stored.key_price, keyValue);
  350. }
  351.  
  352. if (select) {
  353. // select items if select param is present
  354. selectItemsById(select);
  355. }
  356.  
  357. page.$document.on('keypress', (e) => {
  358. keyPressed(e);
  359. });
  360. }
  361.  
  362. // perform actions
  363. // observe changes to refined value
  364. (function observeRefinedValue() {
  365. // get pretty value in keys
  366. function refinedToKeys(value) {
  367. return Math.round((value / keyValue) * 10) / 10;
  368. }
  369.  
  370. function refinedValueChanged() {
  371. // this will generally always be available other than the first load
  372. // if it isn't there's nothing we can do
  373. if (!keyValue) return;
  374.  
  375. // get total value of all items in keys by converting from ref value
  376. const text = $refined.text().replace(/,/g, '').trim();
  377. const refined = parseFloat(text);
  378. const keysValue = refinedToKeys(refined);
  379.  
  380. // disconnect so we can modify the object
  381. // without calling this function again
  382. observer.disconnect();
  383. // update the ref value
  384. $refined.text(keysValue);
  385. // observe changes again
  386. observeRefChanges();
  387. }
  388.  
  389. function observeRefChanges() {
  390. // observe changes to ref value
  391. observer.observe(refinedEl, {
  392. childList: true,
  393. subtree: true,
  394. attributes: false,
  395. characterData: false
  396. });
  397. }
  398.  
  399. // keeping this in a mouseover will speed things up a bit
  400. // especially if there are many items that are listed in the inventory
  401. function updatedListedPrice() {
  402. // this will generally always be available other than the first load
  403. // if it isn't there's nothing we can
  404. if (!keyValue) return;
  405.  
  406. function getKeysListedValue() {
  407. // get refined value from currencies
  408. const getRefinedValue = (currencies) => {
  409. const keys = currencies.keys || 0;
  410. const metal = currencies.metal || 0;
  411.  
  412. return (keys * keyValue) + metal;
  413. };
  414. const $listedItems = page.get.$listedItems();
  415. const prices = $listedItems.map((i, el) => {
  416. const listingPrice = el.dataset.listing_price;
  417. // get refined value of listing price
  418. const currencies = Utils.stringToCurrencies(listingPrice);
  419. const refined = (
  420. currencies &&
  421. getRefinedValue(currencies)
  422. ) || 0;
  423.  
  424. return refined;
  425. }).get();
  426. const sum = (a, b) => a + b;
  427. const refined = prices.reduce(sum, 0);
  428.  
  429. return refinedToKeys(refined);
  430. }
  431.  
  432. const listedKeysValue = getKeysListedValue();
  433. const listedValueStr = `${listedKeysValue} keys listed value`;
  434.  
  435. $refined.attr({
  436. 'title': listedValueStr,
  437. 'data-original-title': listedValueStr
  438. });
  439. // clear title
  440. $refined.attr('title', '');
  441. }
  442.  
  443. const observer = new MutationObserver(refinedValueChanged);
  444. const $refined = page.$refined;
  445. const refinedEl = $refined[0];
  446.  
  447. // change the text from "refined" to "keys"
  448. page.$refined.closest('li').find('small').text('keys');
  449. refinedValueChanged();
  450. $refined.on('mouseover', updatedListedPrice);
  451. }());
  452.  
  453. function handler(mutations) {
  454. // if the mutations include an item list, items have been added
  455. const hasItemList = mutations.some((mutation) => {
  456. return Boolean(
  457. mutation.addedNodes &&
  458. mutation.target.className === 'item-list'
  459. );
  460. });
  461.  
  462. if (hasItemList) {
  463. // backpack has loaded
  464. onBackpackLoad();
  465. }
  466. }
  467.  
  468. const observer = new MutationObserver(handler);
  469. const backpackEl = document.getElementById('backpack');
  470. const settings = {
  471. childList: true,
  472. subtree: true,
  473. attributes: false,
  474. characterData: false
  475. };
  476.  
  477. observer.observe(backpackEl, settings);
  478. }());
  479. }
  480. },
  481. {
  482. includes: [
  483. /^https?:\/\/steamcommunity\.com\/(?:id|profiles)\/.*\/inventory/
  484. ],
  485. styles: `
  486. .unusual {
  487. background-position: center !important;
  488. background-size: 100% 100%;
  489. }
  490. `,
  491. fn: function({$, WINDOW, shared}) {
  492. const dom = {
  493. tabContentInventory: document.getElementById('tabcontent_inventory'),
  494. get: {
  495. tf2Inventory: () => {
  496. const inventoriesList = document.querySelectorAll('#inventories > .inventory_ctn');
  497.  
  498. return Array.from(inventoriesList).find((el) => {
  499. return /_440_2$/.test(el.id);
  500. });
  501. },
  502. unusuals: () => {
  503. const inventory = dom.get.tf2Inventory();
  504. const isUnusualItem = (itemEl) => {
  505. const borderColor = itemEl.style.borderColor;
  506. const hasPurpleBorder = borderColor === 'rgb(134, 80, 172)';
  507.  
  508. return Boolean(
  509. hasPurpleBorder
  510. );
  511. };
  512.  
  513. if (!inventory) {
  514. return [];
  515. }
  516.  
  517. const itemsList = Array.from(inventory.querySelectorAll('.item:not(.unusual)'));
  518.  
  519. return itemsList.select(isUnusualItem);
  520. }
  521. }
  522. };
  523.  
  524. function onInventory() {
  525. function getAsset(assets, itemEl) {
  526. const [ , , assetid] = itemEl.id.split('_');
  527.  
  528. return assets[assetid];
  529. }
  530.  
  531. // tf2 assets
  532. const inventory = (
  533. WINDOW.g_rgAppContextData &&
  534. WINDOW.g_rgAppContextData[440] &&
  535. WINDOW.g_rgAppContextData[440].rgContexts &&
  536. WINDOW.g_rgAppContextData[440].rgContexts[2] &&
  537. WINDOW.g_rgAppContextData[440].rgContexts[2].inventory
  538. );
  539. const hasInventory = Boolean(
  540. inventory
  541. );
  542.  
  543. if (!hasInventory) {
  544. // no tf2 assets
  545. return;
  546. }
  547.  
  548. const {
  549. getEffectName,
  550. getEffectValue,
  551. modifyElement
  552. } = shared.offers.unusual;
  553. const assets = inventory.m_rgAssets;
  554. const itemsList = dom.get.unusuals();
  555. const addUnusualEffect = (itemEl) => {
  556. const asset = getAsset(assets, itemEl);
  557. const effectName = getEffectName(asset.description);
  558. const effectValue = getEffectValue(effectName);
  559. // the value for the effect was found
  560. const hasValue = Boolean(
  561. effectValue
  562. );
  563.  
  564. if (!hasValue) {
  565. return;
  566. }
  567.  
  568. // we can modify it
  569. modifyElement(itemEl, effectValue);
  570. };
  571.  
  572. itemsList.forEach(addUnusualEffect);
  573. }
  574.  
  575. // observe changes to dom
  576. (function observe() {
  577. const inventoryEl = dom.tabContentInventory;
  578. const hasInventory = Boolean(
  579. inventoryEl
  580. );
  581.  
  582. // no tf2 inventory on page
  583. if (!hasInventory) {
  584. return;
  585. }
  586.  
  587. const observer = new MutationObserver((mutations) => {
  588. const tf2Inventory = dom.get.tf2Inventory();
  589. const tf2InventoryVisible = Boolean(
  590. tf2Inventory &&
  591. tf2Inventory.style.display !== 'none'
  592. );
  593.  
  594. if (tf2InventoryVisible) {
  595. console.log('yes');
  596. onInventory();
  597. }
  598. });
  599.  
  600. observer.observe(inventoryEl, {
  601. childList: true,
  602. characterData: false,
  603. subtree: true
  604. });
  605. }());
  606. }
  607. },
  608. {
  609. includes: [
  610. /^https?:\/\/steamcommunity\.com\/(?:id|profiles)\/.*\/tradeoffers/
  611. ],
  612. styles: `
  613. .btn_user_link {
  614. background-position: 14px 0px;
  615. background-repeat: no-repeat !important;
  616. width: 0;
  617. margin-top: -8px;
  618. margin-left: 6px;
  619. padding-left: 44px;
  620. line-height: 30px;
  621. float: right;
  622. }
  623.  
  624. .btn_user_link:hover {
  625. background-position: 14px -30px !important;
  626. background-color: #808285;
  627. }
  628.  
  629. .rep_btn {
  630. background-image: url(https://i.imgur.com/OD9rRAB.png) !important;
  631. }
  632.  
  633. .backpack_btn {
  634. background-image: url(https://i.imgur.com/8LvnfuX.png) !important;
  635. }
  636.  
  637. .tradeoffer_items_summary {
  638. position: relative;
  639. background-color: #1D1D1D;
  640. border: 1px solid #3D3D3E;
  641. border-radius: 5px;
  642. padding: 17px;
  643. margin-top: 8px;
  644. width: 100%;
  645. font-size: 12px;
  646. color: #FFFFFF;
  647. display: flex;
  648. box-sizing: border-box;
  649. }
  650.  
  651. .items_summary {
  652. width: 50%;
  653. margin-right: 2.1%;
  654. display: inline-block;
  655. }
  656.  
  657. .items_summary:last-child {
  658. margin-right: 0;
  659. }
  660.  
  661. .summary_header {
  662. margin-bottom: 12px;
  663. }
  664.  
  665. .summary_item {
  666. display: inline-block;
  667. width: 44px;
  668. height: 44px;
  669. padding: 3px;
  670. margin: 0 2px 2px 0;
  671. border: 1px solid;
  672. background-color: #3C352E;
  673. background-position: center;
  674. background-size: 44px 44px;
  675. background-repeat: no-repeat;
  676. }
  677.  
  678. .summary_badge {
  679. position: absolute;
  680. top: 4px;
  681. left: 4px;
  682. padding: 1px 3px;
  683. color: #FFFFFF;
  684. border-radius: 4px;
  685. background-color: #209DE6;
  686. font-size: 14px;
  687. cursor: default;
  688. font-weight: bold;
  689. }
  690.  
  691. .unusual {
  692. background-position: center;
  693. background-size: 100% 100%;
  694. }
  695.  
  696. .decline_active_button {
  697. display: block;
  698. margin-top: 0.6em;
  699. text-align: center;
  700. }
  701. `,
  702. fn: function({$, VERSION, WINDOW, shared, getStored, setStored}) {
  703. const dom = {
  704. offers: document.getElementsByClassName('tradeoffer')
  705. };
  706. const stored = {
  707. effect_cache: VERSION + '.getTradeOffers.effect_cache'
  708. };
  709. const unusual = (function() {
  710. // take helper methods/objects
  711. const {
  712. effectsMap,
  713. modifyElement,
  714. getEffectName,
  715. getEffectURL
  716. } = shared.offers.unusual;
  717. const addImage = {
  718. fromValue(itemEl, value) {
  719. return modifyElement(itemEl, value);
  720. },
  721. /**
  722. * Adds an image using an item's object.
  723. * @param {Object} itemEl - DOM element of item.
  724. * @param {Object} item - Item object.
  725. * @returns {undefined}
  726. */
  727. fromItem(itemEl, item) {
  728. const name = getEffectName(item);
  729. const value = (
  730. name &&
  731. effectsMap[name]
  732. );
  733. const cacheKey = cache.key(itemEl);
  734.  
  735. // cache blank value if there is no value
  736. // so that we do not obsessively request data
  737. // for this item when no value is available
  738. cache.store(cacheKey, value || 'none');
  739. cache.save();
  740.  
  741. if (value) {
  742. modifyElement(itemEl, value);
  743. }
  744. }
  745. };
  746. const cache = (function() {
  747. // THE KEY TO SET/GET VALUES FROM
  748. const CACHE_INDEX = stored.effect_cache;
  749. // this will hold our cached values
  750. let values = {};
  751.  
  752. function save() {
  753. let value = JSON.stringify(values);
  754.  
  755. if (value.length >= 10000) {
  756. // clear cache when it becomes too big
  757. values = {};
  758. value = '{}';
  759. }
  760.  
  761. setStored(CACHE_INDEX, value);
  762. }
  763.  
  764. function store(key, value) {
  765. values[key] = value;
  766. }
  767.  
  768. function get() {
  769. values = JSON.parse(getStored(CACHE_INDEX) || '{}');
  770. }
  771.  
  772. function key(itemEl) {
  773. const classinfo = itemEl.getAttribute('data-economy-item');
  774. const [ , , classid, instanceid] = classinfo.split('/');
  775. const cacheKey = [classid, instanceid].join('-');
  776.  
  777. return cacheKey;
  778. }
  779.  
  780. function getValue(key) {
  781. return values[key];
  782. }
  783.  
  784. return {
  785. save,
  786. get,
  787. store,
  788. key,
  789. getValue
  790. };
  791. }());
  792.  
  793. return {
  794. addImage,
  795. cache,
  796. getEffectURL
  797. };
  798. }());
  799. // get all unusual elements
  800. const unusualItemsList = (function() {
  801. const itemElList = document.getElementsByClassName('trade_item');
  802. const isUnusualItem = (itemEl) => {
  803. const borderColor = itemEl.style.borderColor;
  804. const classinfo = itemEl.getAttribute('data-economy-item');
  805. const isTf2 = /^classinfo\/440\//.test(classinfo);
  806. const hasPurpleBorder = borderColor === 'rgb(134, 80, 172)';
  807.  
  808. return Boolean(
  809. isTf2 &&
  810. hasPurpleBorder
  811. );
  812. };
  813.  
  814. return Array.from(itemElList).filter(isUnusualItem);
  815. }());
  816.  
  817. // perform actions
  818. // get the cached effect values for stored classinfos
  819. unusual.cache.get();
  820. // get all unusual items on the page
  821. // then check each one for adding effect effect images
  822. unusualItemsList.forEach(function checkItem(itemEl) {
  823. const cache = unusual.cache;
  824. const cacheKey = cache.key(itemEl);
  825. const cachedValue = cache.getValue(cacheKey);
  826.  
  827. if (cachedValue === 'none') {
  828. // i am a do-nothing
  829. } else if (cachedValue) {
  830. // use cached value to display image
  831. unusual.addImage.fromValue(itemEl, cachedValue);
  832. } else {
  833. // get hover for item to get item information
  834. // this requires an ajax request
  835. // classinfo format - "classinfo/440/192234515/3041550843"
  836. const classinfo = itemEl.getAttribute('data-economy-item');
  837. const [ , appid, classid, instanceid] = classinfo.split('/');
  838. const itemStr = [appid, classid, instanceid].join('/');
  839. const uri = `economy/itemclasshover/${itemStr}?content_only=1&l=english`;
  840. const req = new WINDOW.CDelayedAJAXData(uri, 0);
  841.  
  842. req.QueueAjaxRequestIfNecessary();
  843. req.RunWhenAJAXReady(() => {
  844. // 3rd element is a script tag containing item data
  845. const html = req.m_$Data[2].innerHTML;
  846. // extract the json for item with pattern...
  847. const match = html.match(/BuildHover\(\s*?\'economy_item_[A-z0-9]+\',\s*?(.*)\s\);/);
  848.  
  849. try {
  850. // then parse it
  851. const json = JSON.parse(match[1]);
  852.  
  853. unusual.addImage.fromItem(itemEl, json);
  854. } catch (e) {
  855.  
  856. }
  857. });
  858. }
  859. });
  860. // modify each trade offer
  861. Array.from(dom.offers).forEach(function checkOffer(offerEl) {
  862. // add buttons to the offer
  863. (function addButtons() {
  864. const reportButtonEl = offerEl.getElementsByClassName('btn_report')[0];
  865.  
  866. // sent offers will not have a report button - we won't add any buttons to them
  867. if (reportButtonEl == null) {
  868. // stop
  869. return;
  870. }
  871.  
  872. // match steamid, personaname
  873. const pattern = /ReportTradeScam\( ?\'(\d{17})\', ?"(.*)"\ ?\)/;
  874. const match = (reportButtonEl.getAttribute('onclick') || '').match(pattern);
  875.  
  876. if (match) {
  877. const [ , steamid, personaname] = match;
  878.  
  879. // generate the html for the buttons
  880. const html = (function getButtons() {
  881. // generate html for button
  882. const getButton = (button) => {
  883. const makeReplacements = (string) => {
  884. // replace personaname and steamid
  885. return string.replace('%personaname%', personaname).replace('%steamid%', steamid);
  886. };
  887. const href = makeReplacements(button.url);
  888. const title = makeReplacements(button.title);
  889. const classes = [
  890. button.className,
  891. 'btn_grey_grey',
  892. 'btn_small',
  893. 'btn_user_link'
  894. ];
  895.  
  896. return `<a href="${href}" title="${title}" class="${classes.join(' ')}">&nbsp;</a>`;
  897. };
  898. // all the lovely buttons we want to add
  899. const buttons = [
  900. {
  901. title: 'View %personaname%\'s backpack',
  902. // %steamid% is replaced with user's steamid
  903. url: 'https://backpack.tf/profiles/%steamid%',
  904. // each button has a class name for which image to use
  905. className: 'backpack_btn'
  906. },
  907. {
  908. title: 'View %personaname%\'s Rep.tf page',
  909. url: 'https://rep.tf/%steamid%',
  910. className: 'rep_btn'
  911. }
  912. ].map(getButton);
  913. // reverse to preserve order
  914. const html = buttons.reverse().join('');
  915.  
  916. return html;
  917. }());
  918.  
  919. // insert html for buttons
  920. reportButtonEl.insertAdjacentHTML('beforebegin', html);
  921. }
  922.  
  923. // we don't really want it
  924. reportButtonEl.remove();
  925. }());
  926. // summarize the offer
  927. (function summarize() {
  928. const itemsList = offerEl.getElementsByClassName('tradeoffer_item_list');
  929.  
  930. // summarize each list
  931. Array.from(itemsList).forEach(function summarizeList(itemsEl) {
  932. const itemsArr = Array.from(itemsEl.getElementsByClassName('trade_item'));
  933. const getClassInfo = (itemEl) => {
  934. const classinfo = itemEl.getAttribute('data-economy-item');
  935. // I believe item classes always remain static
  936. const translateClass = {
  937. 'classinfo/440/339892/11040578': 'classinfo/440/101785959/11040578',
  938. 'classinfo/440/339892/11040559': 'classinfo/440/101785959/11040578',
  939. 'classinfo/440/107348667/11040578': 'classinfo/440/101785959/11040578'
  940. };
  941.  
  942. return (
  943. translateClass[classinfo] ||
  944. classinfo
  945. );
  946. };
  947. // has multiples of the same item
  948. const hasMultipleSameItems = Boolean(function() {
  949. let infos = [];
  950.  
  951. return itemsArr.some((itemEl) => {
  952. let classinfo = getClassInfo(itemEl);
  953.  
  954. if (infos.indexOf(classinfo) !== -1) {
  955. return true;
  956. } else {
  957. infos.push(classinfo);
  958. return false;
  959. }
  960. });
  961. }());
  962. const shouldModifyDOM = Boolean(
  963. itemsArr.length > 0 &&
  964. hasMultipleSameItems
  965. );
  966.  
  967. // only modify dom if necessary
  968. if (shouldModifyDOM) {
  969. const fragment = document.createDocumentFragment();
  970. const clearEl = document.createElement('div');
  971. // get summarized items and sort elements by properties
  972. // most of this stuff should be fairly optimized
  973. const items = (function getItems() {
  974. const getSort = (key, item) => {
  975. let index, value;
  976.  
  977. if (key === 'count') {
  978. index = -item.count;
  979. } else {
  980. value = item.props[key];
  981. index = sorts[key].indexOf(value);
  982.  
  983. if (index === -1) {
  984. sorts[key].push(value);
  985. index = sorts[key].indexOf(value);
  986. }
  987. }
  988.  
  989. return index;
  990. };
  991. const buildIndex = () => {
  992. const getItem = (classinfo, itemEl) => {
  993. return {
  994. classinfo: classinfo,
  995. app: classinfo.replace('classinfo/', '').split('/')[0],
  996. color: itemEl.style.borderColor
  997. };
  998. };
  999. const items = itemsArr.reduce((result, itemEl) => {
  1000. const classinfo = getClassInfo(itemEl);
  1001.  
  1002. if (result[classinfo]) {
  1003. result[classinfo].count += 1;
  1004. } else {
  1005. result[classinfo] = {
  1006. el: itemEl,
  1007. count: 1,
  1008. props: getItem(classinfo, itemEl)
  1009. };
  1010. }
  1011.  
  1012. return result;
  1013. }, {});
  1014.  
  1015. return items;
  1016. };
  1017. // some parameters to sort by
  1018. const sorts = {
  1019. app: [
  1020. // team fortress 2
  1021. '440',
  1022. // csgo
  1023. '730'
  1024. ],
  1025. color: [
  1026. // unusual
  1027. 'rgb(134, 80, 172)',
  1028. // collectors
  1029. 'rgb(170, 0, 0)',
  1030. // strange
  1031. 'rgb(207, 106, 50)',
  1032. // haunted
  1033. 'rgb(56, 243, 171)',
  1034. // genuine
  1035. 'rgb(77, 116, 85)',
  1036. // vintage
  1037. 'rgb(71, 98, 145)',
  1038. // decorated
  1039. 'rgb(250, 250, 250)',
  1040. // unique
  1041. 'rgb(125, 109, 0)'
  1042. ]
  1043. };
  1044. const items = Object.values(buildIndex());
  1045. const sorted = items.sort((a, b) => {
  1046. let index = 0;
  1047.  
  1048. // sort by these keys
  1049. // break when difference is found
  1050. [
  1051. 'app',
  1052. 'color',
  1053. 'count'
  1054. ].find((key) => {
  1055. // get the sort value for a and b
  1056. const [sortA, sortB] = [a, b].map((value) => {
  1057. return getSort(key, value);
  1058. });
  1059.  
  1060. // these are already sorted in the proper direction
  1061. if (sortA > sortB) {
  1062. index = 1;
  1063. return true;
  1064. } else if (sortA < sortB) {
  1065. index = -1;
  1066. return true;
  1067. }
  1068. });
  1069.  
  1070. return index;
  1071. });
  1072.  
  1073. return sorted;
  1074. }());
  1075.  
  1076. items.forEach(({el, count}) => {
  1077. if (count > 1) {
  1078. // add badge
  1079. const badgeEl = document.createElement('span');
  1080.  
  1081. badgeEl.classList.add('summary_badge');
  1082. badgeEl.textContent = count;
  1083.  
  1084. el.appendChild(badgeEl);
  1085. }
  1086.  
  1087. fragment.appendChild(el);
  1088. });
  1089.  
  1090. clearEl.style.clear = 'both';
  1091. // add clearfix to end of fragment
  1092. fragment.appendChild(clearEl);
  1093. // clear html before-hand to reduce dom manipulation
  1094. itemsEl.innerHTML = '';
  1095. itemsEl.appendChild(fragment);
  1096. }
  1097. });
  1098. }());
  1099. });
  1100. // add the button to decline all trade offers
  1101. (function addDeclineAllOffersButton() {
  1102. const {ShowConfirmDialog} = WINDOW;
  1103. // gets an array of id's of all active trade offers on page
  1104. const getActiveTradeOfferIDs = () => {
  1105. const getTradeOfferIDs = (tradeOffersList) => {
  1106. const getTradeOfferID = (el) => el.id.replace('tradeofferid_', '');
  1107.  
  1108. return tradeOffersList.map(getTradeOfferID);
  1109. };
  1110. const isActive = (el) => !el.querySelector('.inactive');
  1111. const tradeOffersList = Array.from(document.getElementsByClassName('tradeoffer'));
  1112. const activeTradeOffersList = tradeOffersList.filter(isActive);
  1113.  
  1114. return getTradeOfferIDs(activeTradeOffersList);
  1115. };
  1116. // declines any number of trades by their id
  1117. // first parameter is an object which provides method to act on trade offer
  1118. const declineOffers = ({ActOnTradeOffer}, tradeOfferIDs) => {
  1119. const declineOffer = (tradeOfferID) => {
  1120. ActOnTradeOffer(tradeOfferID, 'decline', 'Trade Declined', 'Decline Trade');
  1121. };
  1122.  
  1123. tradeOfferIDs.forEach(declineOffer);
  1124. };
  1125.  
  1126. // jquery elements
  1127. const $newTradeOfferBtn = $('.new_trade_offer_btn');
  1128. const canAct = Boolean(
  1129. // this should probably always be there...
  1130. // but maybe not always
  1131. $newTradeOfferBtn.length > 0 &&
  1132. // page must have active trade offers
  1133. getActiveTradeOfferIDs().length > 0
  1134. );
  1135.  
  1136. if (!canAct) {
  1137. // stop right there
  1138. return;
  1139. }
  1140.  
  1141. const $declineAllButton = $(`
  1142. <div class="btn_darkred_white_innerfade btn_medium decline_active_button">
  1143. <span>
  1144. Decline All Active...
  1145. </span>
  1146. </div>
  1147. `);
  1148.  
  1149. // add the button... after the "New Trade Offer" button
  1150. $declineAllButton.insertAfter($newTradeOfferBtn);
  1151.  
  1152. // add the handler to show the dialog on click
  1153. $declineAllButton.click(() => {
  1154. // yes
  1155. const yes = (str) => {
  1156. return str === 'OK';
  1157. };
  1158.  
  1159. ShowConfirmDialog(
  1160. 'Decline Active',
  1161. 'Are you sure you want to decline all active trade offers?',
  1162. 'Decline Trade Offers',
  1163. null
  1164. ).done((strButton) => {
  1165. if (yes(strButton)) {
  1166. const tradeOfferIDs = getActiveTradeOfferIDs();
  1167.  
  1168. declineOffers(WINDOW, tradeOfferIDs);
  1169. $declineAllButton.remove();
  1170. }
  1171. });
  1172. });
  1173. }());
  1174. }
  1175. },
  1176. {
  1177. includes: [
  1178. /^https?:\/\/steamcommunity\.com\/tradeoffer.*/
  1179. ],
  1180. styles: `
  1181. #tradeoffer_items_summary {
  1182. font-size: 12px;
  1183. color: #FFFFFF;
  1184. }
  1185.  
  1186. .btn_green {
  1187. background-color: #709D3C;
  1188. }
  1189.  
  1190. .btn_silver {
  1191. background-color: #676767;
  1192. }
  1193.  
  1194. .btn_blue {
  1195. background-color: #2E4766;
  1196. }
  1197.  
  1198. .summary_item {
  1199. display: inline-block;
  1200. width: 48px;
  1201. height: 48px;
  1202. padding: 3px;
  1203. margin: 0 2px 2px 0;
  1204. border: 1px solid;
  1205. background-color: #3C352E;
  1206. background-position: center;
  1207. background-size: 48px 48px, 100% 100%;
  1208. background-repeat: no-repeat;
  1209. }
  1210.  
  1211. .summary_badge {
  1212. padding: 1px 3px;
  1213. border-radius: 4px;
  1214. background-color: #209DE6;
  1215. font-size: 12px;
  1216. }
  1217.  
  1218. .items_summary {
  1219. margin-top: 8px
  1220. }
  1221.  
  1222. .summary_header {
  1223. margin-bottom: 4px;
  1224. }
  1225.  
  1226. .filter_full {
  1227. width: 200px;
  1228. }
  1229.  
  1230. .filter_number {
  1231. width: 110px;
  1232. }
  1233.  
  1234. .control_fields {
  1235. margin-top: 8px
  1236. }
  1237.  
  1238. .warning {
  1239. color: #FF4422;
  1240. }
  1241.  
  1242. .trade_area .item.unusual.hover {
  1243. background-position: center;
  1244. background-color: #474747 !important;
  1245. }
  1246.  
  1247. .unusual {
  1248. background-position: center;
  1249. background-size: 100% 100%;
  1250. }
  1251. `,
  1252. fn: function({WINDOW, $, Utils, shared, getStored, setStored}) {
  1253. const urlParams = Utils.getURLParams();
  1254. // these are never re-assigned in steam's source code
  1255. // only updated
  1256. const {UserYou, UserThem, RefreshTradeStatus} = WINDOW;
  1257. const STEAMID = UserYou.strSteamId;
  1258. const PARTNER_STEAMID = UserThem.strSteamId;
  1259. const INVENTORY = WINDOW.g_rgAppContextData;
  1260. const PARTNER_INVENTORY = WINDOW.g_rgPartnerAppContextData;
  1261. const TRADE_STATUS = WINDOW.g_rgCurrentTradeStatus;
  1262. const page = {
  1263. $document: $(document),
  1264. $body: $('body'),
  1265. $yourSlots: $('#your_slots'),
  1266. $theirSlots: $('#their_slots'),
  1267. $inventories: $('#inventories'),
  1268. $inventoryBox: $('#inventory_box'),
  1269. $inventoryDisplayControls: $('#inventory_displaycontrols'),
  1270. $inventorySelectYour: $('#inventory_select_your_inventory'),
  1271. $inventorySelectTheir: $('#inventory_select_their_inventory'),
  1272. $tradeBoxContents: $('#inventory_box div.trade_box_contents'),
  1273. $appSelectOption: $('.appselect_options .option'),
  1274. // get jquery elements which are constantly changing based on page state
  1275. get: {
  1276. $inventory: () => $('.inventory_ctn:visible'),
  1277. $activeInventoryTab: () => $('.inventory_user_tab.active'),
  1278. $modifyTradeOffer: () => $('div.modify_trade_offer:visible'),
  1279. $imgThrobber: () => $('img[src$="throbber.gif"]:visible'),
  1280. $appSelectImg: () => $('#appselect_activeapp img'),
  1281. $deadItem: () => $('a[href$="_undefined"]'),
  1282. $tradeItemBox: () => page.$tradeBoxContents.find('div.trade_item_box')
  1283. }
  1284. };
  1285. // keys for stored values
  1286. const stored = {
  1287. id_visible: 'getTradeOfferWindow.id_visible'
  1288. };
  1289. /**
  1290. * Interact with trade offer.
  1291. *
  1292. * @namespace tradeOfferWindow
  1293. */
  1294. const tradeOfferWindow = (function() {
  1295. /**
  1296. * Get summary of items.
  1297. * @param {Object} $items - JQuery object of collection of items.
  1298. * @param {Boolean} you - Are these your items?
  1299. * @returns {(Object|null)} Summary of items, null if inventory is not properly loaded.
  1300. */
  1301. function evaluateItems($items, you) {
  1302. let warningIdentifiers = [
  1303. {
  1304. name: 'rare TF2 key',
  1305. appid: '440',
  1306. check: function({appdata}) {
  1307. // array of rare TF2 keys (defindexes)
  1308. const rare440Keys = [
  1309. '5049', '5067', '5072', '5073',
  1310. '5079', '5081', '5628', '5631',
  1311. '5632', '5713', '5716', '5717',
  1312. '5762'
  1313. ];
  1314. const defindex = (
  1315. appdata &&
  1316. appdata.def_index
  1317. );
  1318.  
  1319. return Boolean(
  1320. typeof defindex === 'string' &&
  1321. rare440Keys.indexOf(defindex) !== -1
  1322. );
  1323. }
  1324. },
  1325. {
  1326. name: 'uncraftable item',
  1327. appid: '440',
  1328. check: function({descriptions}) {
  1329. const isUncraftable = (description) => {
  1330. return Boolean(
  1331. !description.color &&
  1332. description.value === '( Not Usable in Crafting )'
  1333. );
  1334. };
  1335.  
  1336. return Boolean(
  1337. typeof descriptions === 'object' &&
  1338. descriptions.some(isUncraftable)
  1339. );
  1340. }
  1341. },
  1342. {
  1343. name: 'spelled item',
  1344. appid: '440',
  1345. check: function({descriptions}) {
  1346. const isSpelled = (description) => {
  1347. return Boolean(
  1348. description.color === '7ea9d1' &&
  1349. description.value.indexOf('(spell only active during event)') !== -1
  1350. );
  1351. };
  1352.  
  1353. return Boolean(
  1354. typeof descriptions === 'object' &&
  1355. descriptions.some(isSpelled)
  1356. );
  1357. }
  1358. },
  1359. {
  1360. name: 'restricted gift',
  1361. appid: '753',
  1362. check: function({fraudwarnings}) {
  1363. const isRestricted = (text) => {
  1364. return text.indexOf('restricted gift') !== -1;
  1365. };
  1366.  
  1367. return Boolean(
  1368. typeof fraudwarnings === 'object' &&
  1369. fraudwarnings.some(isRestricted)
  1370. );
  1371. }
  1372. }
  1373. ];
  1374. const inventory = you ? INVENTORY : PARTNER_INVENTORY;
  1375. const total = $items.length;
  1376. let apps = {};
  1377. let items = {};
  1378. let warnings = [];
  1379. let valid = true;
  1380.  
  1381. $items.toArray().forEach((itemEl) => {
  1382. // array containing item identifiers e.g. ['440', '2', '123']
  1383. const split = itemEl.id.replace('item', '').split('_');
  1384. const [appid, contextid, assetid] = split;
  1385. const img = itemEl.getElementsByTagName('img')[0].getAttribute('src');
  1386. const quality = itemEl.style.borderColor;
  1387. const effect = itemEl.getAttribute('data-effect') || 'none';
  1388. const item = (
  1389. inventory[appid] &&
  1390. inventory[appid].rgContexts[contextid].inventory.rgInventory[assetid]
  1391. );
  1392.  
  1393. if (!item) {
  1394. // not properly loaded
  1395. valid = false;
  1396.  
  1397. // stop loop
  1398. return false;
  1399. }
  1400.  
  1401. if (!apps[appid]) {
  1402. apps[appid] = [];
  1403. }
  1404.  
  1405. items[img] = items[img] || {};
  1406. items[img][quality] = (items[img][quality] || {});
  1407. items[img][quality][effect] = (items[img][quality][effect] || 0) + 1;
  1408. apps[appid].push(assetid);
  1409.  
  1410. for (let i = warningIdentifiers.length - 1; i >= 0; i--) {
  1411. const identifier = warningIdentifiers[i];
  1412. const addWarning = Boolean(
  1413. identifier.appid === appid &&
  1414. identifier.check(item)
  1415. );
  1416.  
  1417. if (addWarning) {
  1418. // add the warning
  1419. warnings.push(`Offer contains ${identifier.name}(s).`);
  1420. // remove the identifier so we do not check for it
  1421. // or add it again after this point
  1422. warningIdentifiers.splice(i, 1);
  1423. }
  1424. }
  1425. });
  1426.  
  1427. if (valid) {
  1428. return {
  1429. total,
  1430. apps,
  1431. items,
  1432. warnings
  1433. };
  1434. }
  1435.  
  1436. return null;
  1437. }
  1438.  
  1439. /**
  1440. * Get summary HTML.
  1441. * @param {String} type - Name of user e.g. "Your" or "Their".
  1442. * @param {(Object|null)} summary - Result from evaluateItems.
  1443. * @param {Object} User - User object from steam that the items belong to.
  1444. * @returns {String} Summary HTML.
  1445. */
  1446. function dumpSummary(type, summary, User) {
  1447. // no summary or no items
  1448. if (summary === null || summary.total === 0) {
  1449. return '';
  1450. }
  1451.  
  1452. function getSummary(items, apps, steamid) {
  1453. // helper for getting effecting url
  1454. const {getEffectURL} = shared.offers.unusual;
  1455. const ids = apps['440'];
  1456. let html = '';
  1457.  
  1458. // super duper looper
  1459. for (let img in items) {
  1460. for (let quality in items[img]) {
  1461. for (let effect in items[img][quality]) {
  1462. // generate the html for this item
  1463. const count = items[img][quality][effect];
  1464. const imgs = [`url(${img})`];
  1465.  
  1466. if (effect !== 'none') {
  1467. imgs.push(`url('${getEffectURL(effect)}')`);
  1468. }
  1469.  
  1470. const styles = `background-image: ${imgs.join(', ')}; border-color: ${quality};`;
  1471. const badge = count > 1 ? `<span class="summary_badge">${count}</span>` : '&nbsp;';
  1472. const itemHTML = `<span class="summary_item" style="${styles}">${badge}</span>`;
  1473.  
  1474. // add the html for this item
  1475. html += itemHTML;
  1476. }
  1477. }
  1478. }
  1479.  
  1480. if (ids) {
  1481. // if tf2 items are in offer
  1482. // return summary items with backpack.tf link wrapped around
  1483. const url = `https://backpack.tf/profiles/${steamid}?select=${ids.join(',')}`;
  1484.  
  1485. // wrap the html
  1486. html = `<a title="Open on backpack.tf" href="${url}" target="_blank">${html}</a>`;
  1487. }
  1488.  
  1489. return html;
  1490. }
  1491.  
  1492. function getWarnings() {
  1493. // no warnings to display
  1494. if (warnings.length === 0) {
  1495. return '';
  1496. }
  1497.  
  1498. // so that descriptions are always in the same order
  1499. const descriptions = warnings.sort().join('<br/>');
  1500.  
  1501. return `<div class="warning">${descriptions}</span>`;
  1502. }
  1503.  
  1504. /**
  1505. * Get header for summary.
  1506. * @param {String} type - The name of trader e.g. "My" or "Them".
  1507. * @param {Number} total - Total number of items in offer.
  1508. * @returns {String} HTML string.
  1509. */
  1510. function getHeader(type, total) {
  1511. const itemsStr = total === 1 ? 'item' : 'items';
  1512.  
  1513. return `<div class="summary_header">${type} summary (${total} ${itemsStr}):</div>`;
  1514. }
  1515.  
  1516. // unpack summary...
  1517. const {total, apps, items, warnings} = summary;
  1518. const steamid = User.strSteamId;
  1519.  
  1520. // build html piece-by-piece
  1521. return [
  1522. getHeader(type, total),
  1523. getSummary(items, apps, steamid),
  1524. getWarnings(warnings)
  1525. ].join('');
  1526. }
  1527.  
  1528. /**
  1529. * Summarize a user's items in trade offer.
  1530. * @param {Boolen} you - Is this your summary?
  1531. * @returns {undefined}
  1532. * @memberOf tradeOfferWindow
  1533. */
  1534. function summarize(you) {
  1535. let config;
  1536.  
  1537. // define config based on user
  1538. if (you) {
  1539. config = {
  1540. name: 'My',
  1541. user: UserYou,
  1542. $slots: page.$yourSlots,
  1543. $container: page.$yourSummary
  1544. };
  1545. } else {
  1546. config = {
  1547. name: 'Their',
  1548. user: UserThem,
  1549. $slots: page.$theirSlots,
  1550. $container: page.$theirSummary
  1551. };
  1552. }
  1553.  
  1554. const $items = config.$slots.find('div.item');
  1555. const summary = evaluateItems($items, you);
  1556. const html = dumpSummary(config.name, summary, config.user);
  1557.  
  1558. config.$container.html(html);
  1559. }
  1560.  
  1561. /**
  1562. * Callback when chain has finished.
  1563. * @callback chain-callback
  1564. */
  1565.  
  1566. /**
  1567. * Call function for each item one after another.
  1568. * @param {Array} items - Array.
  1569. * @param {Number} timeout - Time between each call.
  1570. * @param {Function} fn - Function to call on item.
  1571. * @param {chain-callback} callback - Callback when chain has finished.
  1572. * @returns {undefined}
  1573. */
  1574. function chain(items, timeout, fn, callback) {
  1575. function getNext(callback) {
  1576. const item = items.shift();
  1577.  
  1578. if (item) {
  1579. fn(item);
  1580.  
  1581. setTimeout(getNext, timeout, callback);
  1582. } else {
  1583. return callback();
  1584. }
  1585. }
  1586.  
  1587. getNext(callback);
  1588. }
  1589.  
  1590. // clear items that were added to the offer
  1591. function clearItemsInOffer($addedItems) {
  1592. const items = $addedItems.find('div.item').get();
  1593.  
  1594. // remove all at once
  1595. WINDOW.GTradeStateManager.RemoveItemsFromTrade(items.reverse());
  1596.  
  1597. // remove by each item
  1598. // let Clear = WINDOW.MoveItemToInventory;
  1599. // chain(items.reverse(), 100, Clear, summarize);
  1600. }
  1601.  
  1602. // add items to the trade offer
  1603. function addItemsToOffer(items, callback) {
  1604. const MoveItem = WINDOW.MoveItemToTrade;
  1605.  
  1606. chain(items, 20, MoveItem, callback);
  1607. }
  1608.  
  1609. /**
  1610. * Callback when items have finished adding.
  1611. * @callback addItems-callback
  1612. */
  1613.  
  1614. /**
  1615. * Add items to trade.
  1616. * @param {Object} items - JQuery object of items.
  1617. * @param {addItems-callback} callback - Callback when items have finished adding.
  1618. * @returns {undefined}
  1619. * @memberOf tradeOfferWindow
  1620. */
  1621. function addItems(items, callback = function() {}) {
  1622. addItemsToOffer(items, callback);
  1623. }
  1624.  
  1625. /**
  1626. * Clear items in offer.
  1627. * @param {Object} $addedItems - JQuery object of items to remove.
  1628. * @returns {undefined}
  1629. * @memberOf tradeOfferWindow
  1630. */
  1631. function clear($addedItems) {
  1632. clearItemsInOffer($addedItems);
  1633. }
  1634.  
  1635. /**
  1636. * Update display of buttons.
  1637. * @param {Boolean} you - Is your inventory selected?
  1638. * @param {Number} appid - AppID of inventory selected.
  1639. * @returns {undefined}
  1640. * @memberOf tradeOfferWindow
  1641. */
  1642. function updateDisplay(you, appid) {
  1643. // update the state of the button
  1644. const updateState = ($btn, show) => {
  1645. if (show) {
  1646. $btn.show();
  1647. } else {
  1648. $btn.hide();
  1649. }
  1650. };
  1651. const isTF2 = appid == 440;
  1652. const isCSGO = appid == 730;
  1653. const listingIntent = urlParams.listing_intent;
  1654. // show keys button for tf2 and csgo
  1655. const showKeys = isTF2 || isCSGO;
  1656. const showMetal = isTF2;
  1657. // 0 = buy order
  1658. // 1 = sell order
  1659. // we are buying, add items from our inventory
  1660. const isBuying = Boolean(
  1661. you &&
  1662. listingIntent == 1
  1663. );
  1664. const isSelling = (
  1665. !you &&
  1666. listingIntent == 0
  1667. );
  1668. const showListingButton = Boolean(
  1669. isTF2 &&
  1670. (
  1671. isBuying ||
  1672. isSelling
  1673. )
  1674. );
  1675.  
  1676. updateState(page.btns.$items, true);
  1677. updateState(page.btns.$keys, showKeys);
  1678. updateState(page.btns.$metal, showMetal);
  1679. updateState(page.btns.$listing, showListingButton);
  1680. }
  1681.  
  1682. /**
  1683. * Call when a different user's inventory is selected.
  1684. * @param {Object} $inventoryTab - JQuery element of inventory tab selected.
  1685. * @returns {undefined}
  1686. * @memberOf tradeOfferWindow
  1687. */
  1688. function userChanged($inventoryTab) {
  1689. // fallback option for getting appid
  1690. function appIdFallback() {
  1691. // fallback to appid from image
  1692. const src = page.get.$appSelectImg().attr('src') || '';
  1693. const match = src.match(/public\/images\/apps\/(\d+)/);
  1694.  
  1695. return match && match[1];
  1696. }
  1697.  
  1698. const $inventory = page.get.$inventory();
  1699. const you = $inventoryTab.attr('id') === 'inventory_select_your_inventory';
  1700. const match = $inventory.attr('id').match(/(\d+)_(\d+)$/);
  1701. const appid = (match && match[1]) || appIdFallback();
  1702.  
  1703. // now update the dispaly
  1704. updateDisplay(you, appid);
  1705. }
  1706.  
  1707. return {
  1708. summarize,
  1709. addItems,
  1710. clear,
  1711. updateDisplay,
  1712. userChanged
  1713. };
  1714. }());
  1715. /**
  1716. * Manage inventory load events.
  1717. *
  1718. * @namespace inventoryManager
  1719. */
  1720. const inventoryManager = (function() {
  1721. const inventories = {};
  1722. const users = {};
  1723.  
  1724. users[STEAMID] = [];
  1725. users[PARTNER_STEAMID] = [];
  1726. inventories[STEAMID] = {};
  1727. inventories[PARTNER_STEAMID] = {};
  1728.  
  1729. /**
  1730. * An inventory has loaded, call all events according to parameters.
  1731. * @param {String} steamid - Steamid of user.
  1732. * @param {String} appid - Appid of inventory loaded.
  1733. * @param {String} contextid - Contextid of inventory loaded.
  1734. * @returns {undefined}
  1735. * @memberOf inventoryManager
  1736. */
  1737. function call(steamid, appid, contextid) {
  1738. const actions = [
  1739. ...users[steamid],
  1740. ...((inventories[steamid][appid] && inventories[steamid][appid][contextid]) || [])
  1741. ];
  1742.  
  1743. // clear
  1744. users[steamid] = [];
  1745. inventories[steamid][appid] = [];
  1746. // call all functions
  1747. actions.forEach(fn => fn(steamid, appid, contextid));
  1748. }
  1749.  
  1750. /**
  1751. * Register an event.
  1752. * @param {String} steamid - Steamid for user.
  1753. * @param {(String|Function)} appid - Appid of event, or app-agnostic function to be called.
  1754. * @param {(String|undefined)} [contextid] - Contextid of app.
  1755. * @param {(Function|undefined)} [fn] - Function to call when inventory is loaded.
  1756. * @returns {undefined}
  1757. * @memberOf inventoryManager
  1758. */
  1759. function register(steamid, appid, contextid, fn) {
  1760. if (!fn) {
  1761. fn = appid;
  1762. users[steamid].push(fn);
  1763. } else {
  1764. if (!inventories[steamid][appid]) {
  1765. inventories[steamid][appid] = {};
  1766. }
  1767.  
  1768. if (!inventories[steamid][appid][contextid]) {
  1769. inventories[steamid][appid][contextid] = [];
  1770. }
  1771.  
  1772. inventories[steamid][appid][contextid].push(fn);
  1773. }
  1774. }
  1775.  
  1776. return {
  1777. register,
  1778. call
  1779. };
  1780. }());
  1781. /**
  1782. * Collect items based on conditions.
  1783. * @param {String} mode - Mode.
  1784. * @param {Number} amount - Number of items to pick.
  1785. * @param {Number} index - Index to start picking items at.
  1786. * @param {Boolean} [you] - Your items?
  1787. * @returns {Array} First value is an array of items, second is whether the amount was satisfied.
  1788. */
  1789. const collectItems = (function() {
  1790. // used for identifying items
  1791. const identifiers = {
  1792. // item is key
  1793. isKey: function(item) {
  1794. switch (parseInt(item.appid)) {
  1795. case 440:
  1796. return item.market_hash_name === 'Mann Co. Supply Crate Key';
  1797. case 730:
  1798. return identifiers.hasTag(item, 'Type', 'Key');
  1799. }
  1800.  
  1801. return null;
  1802. },
  1803. // item has tag
  1804. hasTag: function(item, tagName, tagValue) {
  1805. if (!item.tags) return null;
  1806.  
  1807. const tags = item.tags;
  1808.  
  1809. for (let i = 0, n = tags.length; i < n; i++) {
  1810. const tag = tags[i];
  1811. const hasTag = Boolean(
  1812. tag.category === tagName &&
  1813. tagValue === tag.name
  1814. );
  1815.  
  1816. if (hasTag) {
  1817. return true;
  1818. }
  1819. }
  1820.  
  1821. return null;
  1822. }
  1823. };
  1824. // used for finding items
  1825. const finders = {
  1826. metal: (function() {
  1827. const hasMetal = (item, name) => {
  1828. return Boolean(
  1829. item.appid == 440 &&
  1830. item.market_hash_name === name
  1831. );
  1832. };
  1833.  
  1834. // find each type of metal
  1835. return function(you, amount, index, name) {
  1836. return pickItems(you, amount, index, (item) => {
  1837. return hasMetal(item, name);
  1838. });
  1839. };
  1840. }()),
  1841. // return items by array of id's
  1842. id: function(ids) {
  1843. const filter = (item) => {
  1844. return ids.indexOf(item.id) !== -1;
  1845. };
  1846. const items = pickItems(null, ids.length, 0, filter).sort((a, b) => {
  1847. return ids.indexOf(a.id) - ids.indexOf(b.id);
  1848. });
  1849.  
  1850. return items;
  1851. }
  1852. };
  1853.  
  1854. /**
  1855. * Pick items from inventory.
  1856. * @param {(Boolean|null)} you - Pick items from your inventory? Use null for both.
  1857. * @param {Number} amount - Amount of items to pick.
  1858. * @param {Number} index - Index to start picking items at.
  1859. * @param {Function} finder - Finder method.
  1860. * @returns {Array} Array of picked items.
  1861. */
  1862. function pickItems(you, amount, index, finder) {
  1863. // get inventory for selected app and context
  1864. function getInventory(user) {
  1865. return (user.rgAppInfo[appid] &&
  1866. user.rgAppInfo[appid].rgContexts[contextid].inventory &&
  1867. user.rgAppInfo[appid].rgContexts[contextid].inventory.rgInventory
  1868. ) || {};
  1869. }
  1870.  
  1871. function getItems(you) {
  1872. const user = you ? UserYou : UserThem;
  1873. const $items = (you ? page.$yourSlots : page.$theirSlots).find('.item');
  1874. const inventory = getInventory(user);
  1875. // get ids of items in trade offer matching app
  1876. const addedIDs = $items.toArray().reduce((arr, el) => {
  1877. const split = el.id.replace('item', '').split('_');
  1878. const [iAppid, , assetid] = split;
  1879.  
  1880. if (iAppid === appid) {
  1881. arr.push(assetid);
  1882. }
  1883.  
  1884. return arr;
  1885. }, []);
  1886. const ids = Object.keys(inventory);
  1887. let items = [];
  1888. let total = [];
  1889. let currentIndex = 0;
  1890.  
  1891. if (index < 0) {
  1892. // select in reverse
  1893. // since -1 is the starting position we add 1 to it before inverting it
  1894. index = (index + 1) * -1;
  1895. ids.reverse();
  1896. }
  1897.  
  1898. // items will always be sorted from front-to-back by default
  1899. for (let i = 0; i < ids.length; i++) {
  1900. const id = ids[i];
  1901. const item = inventory[id];
  1902.  
  1903. if (addedIDs.indexOf(id) !== -1) {
  1904. // id of item is already in trade offer
  1905. if (index !== 0 && finder(item)) {
  1906. currentIndex++; // increment if item matches
  1907. }
  1908.  
  1909. continue;
  1910. } else if (items.length >= amount) {
  1911. // break when amount has been reached
  1912. break;
  1913. } else if (finder(item)) {
  1914. if (currentIndex >= index) {
  1915. items.push(item);
  1916. }
  1917.  
  1918. // add items to total in case amount is not met
  1919. total.push(item);
  1920. currentIndex++;
  1921. }
  1922. }
  1923.  
  1924. if (items < amount) {
  1925. items = total.splice(offsetIndex(index, amount, total.length), amount);
  1926. }
  1927.  
  1928. return items;
  1929. }
  1930.  
  1931. const $inventory = page.get.$inventory();
  1932. const match = ($inventory.attr('id') || '').match(/(\d+)_(\d+)$/);
  1933. const [ , appid, contextid] = (match || []);
  1934.  
  1935. // inventory must be present
  1936. if (!appid) {
  1937. return;
  1938. } else if (you === null) {
  1939. // get items for both users
  1940. return Utils.flatten([
  1941. true,
  1942. false
  1943. ].map(getItems));
  1944. } else {
  1945. // get items for user based on whether 'you' is truthy or falsy
  1946. return getItems(you);
  1947. }
  1948. }
  1949.  
  1950. /**
  1951. * Offset index to pick items at based on amount and number of items available.
  1952. * @param {Number} index - Index.
  1953. * @param {Number} amount - Amount of items to pick.
  1954. * @param {Number} length - Number of items to pick from.
  1955. * @returns {Number} Modified index.
  1956. */
  1957. function offsetIndex(index, amount, length) {
  1958. if (index < 0) {
  1959. // pick from back if index is negative
  1960. return Math.max(0, length - (amount + index + 1));
  1961. } else if (index + amount >= length) {
  1962. // offset if index + the amount is greater than the number of items we can pick
  1963. return Math.max(0, length - amount);
  1964. } else {
  1965. // no offset needed
  1966. return index;
  1967. }
  1968. }
  1969.  
  1970. // map items to array of dom elements
  1971. function getElementsForItems(items) {
  1972. if (items.length === 0) return [];
  1973.  
  1974. // get element id for each item
  1975. const ids = items.map(item => `item${item.appid}_${item.contextid}_${item.id}`);
  1976. const elements = ids.map(id => document.getElementById(id)).map(a => a);
  1977.  
  1978. return elements;
  1979. }
  1980.  
  1981. /**
  1982. * Pick metal from items based on value in refined metal.
  1983. * @param {Boolean} you - Add to your side?
  1984. * @param {Number} amount - Value to make in metal (e.g. 13.33).
  1985. * @param {Number} index - Index to add at.
  1986. * @returns {Array} First value is an array of items, second is whether the amount was satisfied.
  1987. */
  1988. function getItemsForMetal(you, amount, index) {
  1989. // converts a metal value to the equivalent number of scrap emtals
  1990. // values are rounded
  1991. function toScrap(num) {
  1992. return Math.round(num / (1 / 9));
  1993. }
  1994.  
  1995. // value was met
  1996. function valueMet() {
  1997. return total === amount;
  1998. }
  1999.  
  2000. function getMetal(arr, type) {
  2001. if (valueMet()) {
  2002. // empty array
  2003. return arr;
  2004. }
  2005.  
  2006. // get number of metal to add based on how much more we need to add
  2007. // as well as the value of the metal we are adding
  2008. const curValue = values[type];
  2009. const valueNeeded = amount - total;
  2010. const amountToAdd = Math.floor(valueNeeded / curValue);
  2011. // get array of metal
  2012. const items = finder(you, amountToAdd, index, type);
  2013. const amountAdded = Math.min(
  2014. amountToAdd,
  2015. // there isn't quite enough there...
  2016. items.length
  2017. );
  2018.  
  2019. // add it to the total
  2020. total = total + (amountAdded * curValue);
  2021.  
  2022. // add the new items to the array
  2023. return arr.concat(items);
  2024. }
  2025.  
  2026. // convert the amount to the number of scrap metal
  2027. amount = toScrap(amount);
  2028.  
  2029. // total to be added to
  2030. let total = 0;
  2031. const finder = finders.metal;
  2032. // the value in scrap metal of each type of metal
  2033. const values = {
  2034. 'Refined Metal': 9,
  2035. 'Reclaimed Metal': 3,
  2036. 'Scrap Metal': 1
  2037. };
  2038. const metal = Object.keys(values).reduce(getMetal, []);
  2039. const items = getElementsForItems(metal);
  2040. const satisfied = valueMet();
  2041.  
  2042. return {
  2043. items,
  2044. satisfied
  2045. };
  2046. }
  2047.  
  2048. /**
  2049. * Collect items based on conditions.
  2050. * @param {String} mode - Mode.
  2051. * @param {Number} amount - Number of items to pick.
  2052. * @param {Number} index - Index to start picking items at.
  2053. * @param {Boolean} [you] - Your items?
  2054. * @returns {Array} First value is an array of items, second is whether the amount was satisfied.
  2055. */
  2056. function getItems(mode, amount, index, you) {
  2057. return {
  2058. // get keys
  2059. 'KEYS': function() {
  2060. const found = pickItems(you, amount, index, identifiers.isKey);
  2061. const items = getElementsForItems(found);
  2062. const satisfied = amount === items.length;
  2063.  
  2064. return {
  2065. items,
  2066. satisfied
  2067. };
  2068. },
  2069. // get amount of metal (keys, ref, scrap);
  2070. 'METAL': function() {
  2071. const {
  2072. items,
  2073. satisfied
  2074. } = getItemsForMetal(you, amount, index);
  2075.  
  2076. return {
  2077. items,
  2078. satisfied
  2079. };
  2080. },
  2081. // get items by id
  2082. 'ID': function() {
  2083. // list of id's is passed through index
  2084. const ids = index;
  2085. const found = finders.id(ids);
  2086. const items = getElementsForItems(found);
  2087. const satisfied = ids.length === items.length;
  2088.  
  2089. return {
  2090. items,
  2091. satisfied
  2092. };
  2093. },
  2094. // get items displayed in the inventory
  2095. 'ITEMS': function() {
  2096. // check if an items is visible on page
  2097. // the item iteself will not contain the display property, but its parent does
  2098. function isVisible(i, el) {
  2099. return el.parentNode.style.display !== 'none';
  2100. }
  2101.  
  2102. // select all visible items from active inventory
  2103. let found = page.get.$inventory().find('div.item').filter(isVisible).toArray();
  2104.  
  2105. // select in reverse
  2106. if (index < 0) {
  2107. index = (index + 1) * -1;
  2108. found = found.reverse();
  2109. }
  2110.  
  2111. const offset = offsetIndex(index, amount, found.length);
  2112. const items = found.splice(offset, amount);
  2113. const satisfied = amount === items.length;
  2114.  
  2115. return {
  2116. items,
  2117. satisfied
  2118. };
  2119. }
  2120. }[mode]();
  2121. }
  2122.  
  2123. return getItems;
  2124. }());
  2125. const unusual = (function() {
  2126. const {
  2127. effectsMap,
  2128. modifyElement,
  2129. getEffectName
  2130. } = shared.offers.unusual;
  2131.  
  2132. function addImagesToInventory(inventory) {
  2133. function addEffectImage(item, effectName) {
  2134. const value = effectsMap[effectName];
  2135.  
  2136. if (value) {
  2137. const {appid, contextid, id} = item;
  2138. const elId = `item${appid}_${contextid}_${id}`;
  2139. const itemEl = document.getElementById(elId);
  2140.  
  2141. modifyElement(itemEl, value);
  2142. }
  2143. }
  2144.  
  2145. for (let assetid in inventory) {
  2146. const item = inventory[assetid];
  2147. const effectName = getEffectName(item);
  2148.  
  2149. if (effectName) {
  2150. addEffectImage(item, effectName);
  2151. }
  2152. }
  2153. }
  2154.  
  2155. /**
  2156. * Get URL of image for effect.
  2157. * @param {Number} value - Value of effect.
  2158. * @returns {String} URL string.
  2159. */
  2160. function getEffectURL(value) {
  2161. return `https://backpack.tf/images/440/particles/${value}_188x188.png`;
  2162. }
  2163.  
  2164. return {
  2165. addImagesToInventory,
  2166. getEffectURL
  2167. };
  2168. }());
  2169.  
  2170. // perform actions
  2171. // add elements to page
  2172. (function addElements() {
  2173. const controlsHTML = `
  2174. <div id="controls">
  2175. <div class="trade_rule selectableNone"/>
  2176. <div class="selectableNone">Add multiple items:</div>
  2177. <div class="filter_ctn">
  2178. <input id="amount_control" class="filter_search_box" type="number" min="0" step="any" placeholder="amount"/>
  2179. <input id="index_control" class="filter_search_box" type="number" min="0" placeholder="index"/>
  2180. </div>
  2181. <div id="add_btns" class="control_fields">
  2182. <div id="btn_additems" class="btn_black btn_small">
  2183. <span>Add</span>
  2184. </div>
  2185. <div id="btn_addkeys" class="btn_green btn_black btn_small">
  2186. <span>Add Keys</span>
  2187. </div>
  2188. <div id="btn_addmetal" class="btn_silver btn_black btn_small">
  2189. <span>Add Metal</span>
  2190. </div>
  2191. <div id="btn_addlisting" class="btn_blue btn_black btn_small">
  2192. <span>Add Listing</span>
  2193. </div>
  2194. </div>
  2195. <div id="clear_btns" class="control_fields">
  2196. <div id="btn_clearmyitems" type="button" class="btn_black btn_small">
  2197. <span>Clear my items</span>
  2198. </div>
  2199. <div id="btn_cleartheiritems" type="button" class="btn_black btn_small">
  2200. <span>Clear their items</span>
  2201. </div>
  2202. </div>
  2203. <div id="id_fields" class="control_fields" style="display: none;">
  2204. <div class="filter_ctn">
  2205. <div class="filter_control_ctn">
  2206. <input id="ids_control" class="filter_search_box filter_full" type="text" placeholder="ids" autocomplete="off"/>
  2207. </div>
  2208. <div class="filter_tag_button_ctn filter_right_controls">
  2209. <div id="btn_addids" type="button" class="btn_black btn_small">
  2210. <span>Add</span>
  2211. </div>
  2212. <div id="btn_getids" type="button" class="btn_black btn_small">
  2213. <span>Get</span>
  2214. </div>
  2215. </div>
  2216. <div style="clear:both;"></div>
  2217. </div>
  2218. </div>
  2219. </div>
  2220. `;
  2221. const itemSummaryHTML = `
  2222. <div id="tradeoffer_items_summary">
  2223. <div class="items_summary" id="your_summary"></div>
  2224. <div class="items_summary" id="their_summary"></div>
  2225. </div>
  2226. `;
  2227. const $tradeBox = page.$tradeBoxContents;
  2228. // clearfix to add after inventories to fix height bug in firefox
  2229. const $clear = $('<div style="clear: both"/>');
  2230. const html = [
  2231. controlsHTML,
  2232. itemSummaryHTML
  2233. ].join('').replace(/\s{2,}/g, ' ');
  2234.  
  2235. // add it
  2236. $tradeBox.append(html);
  2237.  
  2238. // add the clear after inventories
  2239. $clear.insertAfter(page.$inventories);
  2240.  
  2241. // add newly created elements to page object
  2242. page.$offerSummary = $('#tradeoffer_items_summary');
  2243. page.$yourSummary = $('#your_summary');
  2244. page.$theirSummary = $('#their_summary');
  2245. page.$controls = $('#controls');
  2246. page.controls = {
  2247. $amount: $('#amount_control'),
  2248. $index: $('#index_control'),
  2249. $ids: $('#ids_control')
  2250. };
  2251. page.fields = {
  2252. $ids: $('#id_fields'),
  2253. $controls: $('#controls')
  2254. };
  2255. page.btns = {
  2256. $clearMy: $('#btn_clearmyitems'),
  2257. $clearTheir: $('#btn_cleartheiritems'),
  2258. $items: $('#btn_additems'),
  2259. $keys: $('#btn_addkeys'),
  2260. $metal: $('#btn_addmetal'),
  2261. $listing: $('#btn_addlisting'),
  2262. $addIDs: $('#btn_addids'),
  2263. $getIDs: $('#btn_getids')
  2264. };
  2265. }());
  2266. // binds events to elements
  2267. (function bindEvents() {
  2268. // the user changed from one app to another
  2269. function appChanged(app) {
  2270. const $app = $(app);
  2271. const id = $app.attr('id');
  2272. const match = id.match(/appselect_option_(you|them)_(\d+)_(\d+)/);
  2273.  
  2274. if (match) {
  2275. const you = match[1] === 'you';
  2276. const [ , , appid, contextid] = match;
  2277.  
  2278. tradeOfferWindow.updateDisplay(you, appid, contextid);
  2279. }
  2280. }
  2281.  
  2282. // add the listing price
  2283. function addListingPrice() {
  2284. /**
  2285. * Callback when items have finished adding.
  2286. * @callback addCurrencies-callback
  2287. * @param {Array} reasons - Array of reasons if value was not met for each currency.
  2288. */
  2289.  
  2290. /**
  2291. * Add currencies to the trade.
  2292. * @param {Boolean} you - Are we adding from your inventory?
  2293. * @param {Object} currencies - Object containing currencies.
  2294. * @param {addCurrencies-callback} callback - Callback when all items have been added.
  2295. * @returns {undefined}
  2296. */
  2297. function addCurrencies(you, currencies, callback) {
  2298. const names = Object.keys(currencies).filter((currency) => {
  2299. return currencies[currency] > 0;
  2300. });
  2301. const reasons = [];
  2302. const index = parseInt(page.controls.$index.val()) || 0;
  2303.  
  2304. function addCurrency(callback) {
  2305. let currency = names.shift(); // get first name and remove it from array
  2306. let amount = currencies[currency];
  2307.  
  2308. if (currency) {
  2309. addItems(currency, amount, index, you, (satisfied) => {
  2310. if (satisfied === false) {
  2311. reasons.push(`not enough ${currency.toLowerCase()}`);
  2312. }
  2313.  
  2314. addCurrency(callback); // recurse
  2315. });
  2316. } else {
  2317. return callback(reasons);
  2318. }
  2319. }
  2320.  
  2321. addCurrency(callback);
  2322. }
  2323.  
  2324. // 0 = buy order
  2325. // 1 = sell order
  2326. const listingIntent = urlParams.listing_intent;
  2327. // we are buying, add items from our inventory
  2328. const you = listingIntent == 1;
  2329.  
  2330. addCurrencies(you, {
  2331. KEYS: parseInt(urlParams.listing_currencies_keys) || 0,
  2332. METAL: parseFloat(urlParams.listing_currencies_metal) || 0
  2333. }, (reasons) => {
  2334. if (reasons.length > 0) {
  2335. // display message if any currencies were not met
  2336. alert(`Listing value could not be met: ${reasons.join(' and ')}`);
  2337. }
  2338. });
  2339. }
  2340.  
  2341. /**
  2342. * Add items by list of IDs.
  2343. * @param {String} idsStr - Comma-seperated list of IDs.
  2344. * @returns {undefined}
  2345. */
  2346. function addIDs(idsStr) {
  2347. const ids = Utils.getIDsFromString(idsStr);
  2348.  
  2349. if (ids) {
  2350. addItems('ID', 0, ids, null);
  2351. }
  2352. }
  2353.  
  2354. // get default amount and index value based on fields
  2355. function getDefaults() {
  2356. return [
  2357. // amount
  2358. parseFloat(page.controls.$amount.val()) || 1,
  2359. // index
  2360. parseInt(page.controls.$index.val()) || 0,
  2361. // your inventory is selected
  2362. page.$inventorySelectYour.hasClass('active')
  2363. ];
  2364. }
  2365.  
  2366. function toggleIDFields() {
  2367. const $controls = page.fields.$ids.toggle();
  2368. const isVisible = $controls.is(':visible') ? 1 : 0;
  2369.  
  2370. setStored(stored.id_visible, isVisible);
  2371. }
  2372.  
  2373. // get list of ids of items in trade offer
  2374. function getIDs() {
  2375. const $inventoryTab = page.get.$activeInventoryTab();
  2376. const you = $inventoryTab.attr('id') === 'inventory_select_your_inventory';
  2377. const $slots = you ? page.$yourSlots : page.$theirSlots;
  2378. const $items = $slots.find('div.item');
  2379.  
  2380. return $items.toArray().map((el) => {
  2381. // array containing item identifiers e.g. ['440', '2', '123']
  2382. const split = (el.id || '').replace('item', '').split('_');
  2383. const assetid = split[2];
  2384.  
  2385. return assetid;
  2386. });
  2387. }
  2388.  
  2389. function keyPressed(e) {
  2390. Utils.execHotKey(e, {
  2391. // P
  2392. 112: toggleIDFields
  2393. });
  2394. }
  2395.  
  2396. function addItems(
  2397. mode = 'ITEMS',
  2398. amount = 1,
  2399. index = 0,
  2400. you = true,
  2401. callback = function() {}
  2402. ) {
  2403. const canModify = Boolean(
  2404. // an inventory is not selected
  2405. (/(\d+)_(\d+)$/.test(page.get.$inventory().attr('id'))) ||
  2406. // the offer cannot be modified
  2407. page.get.$modifyTradeOffer().length === 0
  2408. );
  2409.  
  2410. // we can modify the items in the offer based on the current window state
  2411. if (canModify) {
  2412. const {
  2413. items,
  2414. satisfied
  2415. } = collectItems(...arguments);
  2416.  
  2417. tradeOfferWindow.addItems(items, () => {
  2418. return callback(satisfied);
  2419. });
  2420. } else {
  2421. return callback();
  2422. }
  2423. }
  2424.  
  2425. page.$appSelectOption.on('click', (e) => {
  2426. appChanged(e.target);
  2427. });
  2428. page.$inventorySelectYour.on('click', () => {
  2429. tradeOfferWindow.userChanged(page.$inventorySelectYour);
  2430. });
  2431. page.$inventorySelectTheir.on('click', () => {
  2432. tradeOfferWindow.userChanged(page.$inventorySelectTheir);
  2433. });
  2434. page.btns.$clearMy.on('click', () => {
  2435. tradeOfferWindow.clear(page.$yourSlots);
  2436. });
  2437. page.btns.$clearTheir.on('click', () => {
  2438. tradeOfferWindow.clear(page.$theirSlots);
  2439. });
  2440. page.btns.$items.on('click', () => {
  2441. addItems('ITEMS', ...getDefaults());
  2442. });
  2443. page.btns.$keys.on('click', () => {
  2444. addItems('KEYS', ...getDefaults());
  2445. });
  2446. page.btns.$metal.on('click', () => {
  2447. addItems('METAL', ...getDefaults());
  2448. });
  2449. page.btns.$listing.on('click', () => {
  2450. addListingPrice();
  2451. });
  2452. page.btns.$addIDs.on('click', () => {
  2453. addIDs(page.controls.$ids.val());
  2454. });
  2455. page.btns.$getIDs.on('click', () => {
  2456. page.controls.$ids.val(getIDs().join(','));
  2457. });
  2458. page.$document.on('keypress', (e) => {
  2459. keyPressed(e);
  2460. });
  2461. }());
  2462. // register inventory events
  2463. (function bindInventoryEvents() {
  2464. // this will force an inventory to load
  2465. function forceInventory(appid, contextid) {
  2466. TRADE_STATUS.them.assets.push({
  2467. appid: appid,
  2468. contextid: contextid,
  2469. assetid: '0',
  2470. amount: 1
  2471. });
  2472. RefreshTradeStatus(TRADE_STATUS, true);
  2473. TRADE_STATUS.them.assets = [];
  2474. RefreshTradeStatus(TRADE_STATUS, true);
  2475. }
  2476.  
  2477. function addEffectImages(steamid, appid, contextid) {
  2478. const you = steamid === STEAMID;
  2479. const inventory = you ? INVENTORY : PARTNER_INVENTORY;
  2480. const contextInventory = inventory[appid].rgContexts[contextid].inventory.rgInventory;
  2481.  
  2482. if (!you) {
  2483. // force the items in their inventory to be displayed so we can add images
  2484. // if their inventory has not been displayed
  2485. forceVisibility();
  2486. }
  2487.  
  2488. unusual.addImagesToInventory(contextInventory);
  2489. // re-summarize
  2490. tradeOfferWindow.summarize(you);
  2491. }
  2492.  
  2493. /**
  2494. * Force visibility of other user's inventory.
  2495. * @returns {undefined}
  2496. */
  2497. function forceVisibility() {
  2498. const $activeTab = page.get.$activeInventoryTab();
  2499. const $theirs = page.$inventorySelectTheir;
  2500.  
  2501. $theirs.trigger('click');
  2502. $activeTab.trigger('click');
  2503. }
  2504.  
  2505. inventoryManager.register(STEAMID, () => {
  2506. // something to do when your inventory is loaded...
  2507. });
  2508.  
  2509. if (urlParams.listing_intent !== undefined) {
  2510. // we are buying, add items from our inventory
  2511. const isSelling = urlParams.listing_intent == 0;
  2512.  
  2513. page.btns.$listing.addClass(isSelling ? 'selling' : 'buying');
  2514.  
  2515. // force their inventory to load if we are selling
  2516. if (isSelling) {
  2517. forceInventory('440', '2');
  2518. }
  2519. }
  2520.  
  2521. if (urlParams.for_item !== undefined) {
  2522. const [appid, contextid, assetid] = urlParams.for_item.split('_');
  2523. const item = {
  2524. appid,
  2525. contextid,
  2526. assetid,
  2527. amount: 1
  2528. };
  2529.  
  2530. TRADE_STATUS.them.assets.push(item);
  2531. RefreshTradeStatus(TRADE_STATUS, true);
  2532.  
  2533. // check for a dead item when this inventory is loaded
  2534. inventoryManager.register(PARTNER_STEAMID, appid, contextid, () => {
  2535. if (page.get.$deadItem().length === 0) {
  2536. return;
  2537. }
  2538.  
  2539. TRADE_STATUS.them.assets = [];
  2540. RefreshTradeStatus(TRADE_STATUS, true);
  2541. alert(
  2542. `Seems like the item you are looking to buy (ID: ${assetid}) is no longer available. ` +
  2543. 'You should check other user\'s backpack and see if it\'s still there.'
  2544. );
  2545. });
  2546. }
  2547.  
  2548. // why would you open this
  2549. inventoryManager.register(STEAMID, '578080', '2', () => {
  2550. alert('wow why are you looking at your pubg inventory');
  2551. });
  2552.  
  2553. [STEAMID, PARTNER_STEAMID].forEach((steamid) => {
  2554. inventoryManager.register(steamid, '440', '2', addEffectImages);
  2555. });
  2556. }());
  2557. // observe changes to dom
  2558. (function observe() {
  2559. // observe changes to trade slots
  2560. (function() {
  2561. function observeSlots(slotsEl, you) {
  2562. function summarize() {
  2563. tradeOfferWindow.summarize(you);
  2564. lastSummarized = new Date(); // add date
  2565. }
  2566.  
  2567. const observer = new MutationObserver(() => {
  2568. const canInstantSummarize = Boolean(
  2569. !lastSummarized ||
  2570. // compare with date when last summarized
  2571. new Date() - lastSummarized > 200 ||
  2572. // large summaries take longer to build and can hurt performance
  2573. slotsEl.children.length <= 204
  2574. );
  2575.  
  2576. if (canInstantSummarize) {
  2577. summarize();
  2578. } else {
  2579. clearTimeout(timer);
  2580. timer = setTimeout(summarize, 400);
  2581. }
  2582. });
  2583. let lastSummarized = new Date();
  2584. let timer;
  2585.  
  2586. observer.observe(slotsEl, {
  2587. childList: true,
  2588. characterData: false,
  2589. subtree: true
  2590. });
  2591. }
  2592.  
  2593. observeSlots(page.$yourSlots[0], true);
  2594. observeSlots(page.$theirSlots[0], false);
  2595. }());
  2596.  
  2597. // observe inventory changes
  2598. (function() {
  2599. const observer = new MutationObserver((mutations) => {
  2600. if (!mutations[0].addedNodes) return;
  2601.  
  2602. const mutation = mutations[0];
  2603. const inventory = mutation.addedNodes[0];
  2604. const split = inventory.id.replace('inventory_', '').split('_');
  2605. const [steamid, appid, contextid] = split;
  2606.  
  2607. inventoryManager.call(steamid, appid, contextid);
  2608. });
  2609.  
  2610. observer.observe(page.$inventories[0], {
  2611. childList: true,
  2612. characterData: false,
  2613. subtree: false
  2614. });
  2615. }());
  2616. }());
  2617. // configure state
  2618. (function configure() {
  2619. tradeOfferWindow.userChanged(page.get.$activeInventoryTab());
  2620.  
  2621. if (getStored(stored.id_visible) == 1) {
  2622. page.fields.$ids.show();
  2623. }
  2624.  
  2625. if (urlParams.listing_intent !== undefined) {
  2626. const isSelling = urlParams.listing_intent == 0;
  2627.  
  2628. page.btns.$listing.addClass(isSelling ? 'selling' : 'buying');
  2629. }
  2630. }());
  2631. // override page functions
  2632. (function overrides() {
  2633. // basically removes animation due to bugginess
  2634. // also it's a bit faster
  2635. WINDOW.EnsureSufficientTradeSlots = function(bYourSlots, cSlotsInUse, cCurrencySlotsInUse) {
  2636. const getDesiredSlots = () => {
  2637. const useResponsiveLayout = WINDOW.Economy_UseResponsiveLayout();
  2638.  
  2639. if (useResponsiveLayout) {
  2640. return cTotalSlotsInUse + 1;
  2641. } else {
  2642. const cTotalSlotsInUse = cSlotsInUse + cCurrencySlotsInUse;
  2643.  
  2644. return Math.max(Math.floor((cTotalSlotsInUse + 5) / 4) * 4, 8);
  2645. }
  2646. };
  2647. const $slots = bYourSlots ? page.$yourSlots : page.$theirSlots;
  2648. const elSlotContainer = $slots[0];
  2649. const cDesiredSlots = getDesiredSlots();
  2650. const cDesiredItemSlots = cDesiredSlots - cCurrencySlotsInUse;
  2651. const cCurrentItemSlots = elSlotContainer.childElements().length;
  2652. const cCurrentSlots = cCurrentItemSlots + cCurrencySlotsInUse;
  2653. const bElementsChanged = cDesiredSlots !== cCurrentSlots;
  2654. const rgElementsToRemove = [];
  2655.  
  2656. if (cDesiredSlots > cCurrentSlots) {
  2657. const Create = WINDOW.CreateTradeSlot;
  2658.  
  2659. for (let i = cCurrentItemSlots; i < cDesiredItemSlots; i++) {
  2660. Create(bYourSlots, i);
  2661. }
  2662. } else if (cDesiredSlots < cCurrentSlots) {
  2663. // going to compact
  2664. const prefix = bYourSlots ? 'your_slot_' : 'their_slot_';
  2665. const $parent = $slots.parent();
  2666.  
  2667. for (let i = cDesiredItemSlots; i < cCurrentItemSlots; i++) {
  2668. const element = $slots.find('#' + prefix + i)[0];
  2669.  
  2670. element.id = '';
  2671. $parent.append(element.remove());
  2672. rgElementsToRemove.push(element);
  2673. }
  2674. }
  2675.  
  2676. if (bElementsChanged && rgElementsToRemove.length > 0) {
  2677. rgElementsToRemove.invoke('remove');
  2678. }
  2679. };
  2680.  
  2681. // remove multiple items from a trade offer at once
  2682. // pretty much removes all items INSTANTLY
  2683. WINDOW.GTradeStateManager.RemoveItemsFromTrade = function(items) {
  2684. function checkItems(items, you) {
  2685. if (items.length === 0) {
  2686. return false;
  2687. }
  2688.  
  2689. function getGroups(rgItems) {
  2690. const groupBy = Utils.groupBy;
  2691. const grouped = groupBy(rgItems, 'appid');
  2692.  
  2693. for (let appid in grouped) {
  2694. grouped[appid] = groupBy(grouped[appid], 'contextid');
  2695.  
  2696. for (let contextid in grouped[appid]) {
  2697. grouped[appid][contextid] = groupBy(grouped[appid][contextid], 'id');
  2698. }
  2699. }
  2700.  
  2701. return grouped;
  2702. }
  2703.  
  2704. // iterate over dom elements and collect rgItems from items
  2705. function iterItems(items) {
  2706. let rgItems = [];
  2707. const revertItem = WINDOW.RevertItem;
  2708. const isInTradeSlot = WINDOW.BIsInTradeSlot;
  2709. const cleanSlot = WINDOW.CleanupSlot;
  2710. const setStackItemInTrade = WINDOW.SetStackableItemInTrade;
  2711.  
  2712. // this is done in reverse
  2713. for (let i = items.length - 1; i >= 0; i--) {
  2714. const elItem = items[i];
  2715. const item = elItem.rgItem;
  2716.  
  2717. if (isInTradeSlot(elItem)) {
  2718. cleanSlot(elItem.parentNode.parentNode);
  2719. }
  2720.  
  2721. if (item.is_stackable) {
  2722. // stackable items are fully removed by this call
  2723. setStackItemInTrade(item, 0);
  2724. continue;
  2725. }
  2726.  
  2727. revertItem(item);
  2728. item.homeElement.down('.slot_actionmenu_button').show();
  2729. rgItems.push(item);
  2730. }
  2731.  
  2732. return rgItems;
  2733. }
  2734.  
  2735. // iterate assets in slots
  2736. function iterAssets(rgItems) {
  2737. if (rgItems.length === 0) {
  2738. return false;
  2739. }
  2740.  
  2741. const getItem = ({appid, contextid, assetid}) => {
  2742. return (
  2743. groups[appid] &&
  2744. groups[appid][contextid] &&
  2745. groups[appid][contextid][assetid]
  2746. );
  2747. };
  2748. const slots = you ? TRADE_STATUS.me : TRADE_STATUS.them;
  2749. const groups = getGroups(rgItems);
  2750. let assets = slots.assets;
  2751. let bChanged;
  2752.  
  2753. for (let i = assets.length - 1; i >= 0; i--) {
  2754. const asset = assets[i];
  2755. const item = getItem(asset);
  2756.  
  2757. if (item) {
  2758. bChanged = true;
  2759. assets.splice(i, 1);
  2760. }
  2761. }
  2762.  
  2763. return bChanged;
  2764. }
  2765.  
  2766. // return true if any assets were removed from trade
  2767. return iterAssets(iterItems(items));
  2768. }
  2769.  
  2770. const manager = WINDOW.GTradeStateManager;
  2771. const [yours, theirs] = Utils.partition(items, (elItem) => {
  2772. return !elItem.rgItem.is_their_item;
  2773. });
  2774. const hasChanged = [
  2775. checkItems(yours, true),
  2776. checkItems(theirs, false)
  2777. ].some(Boolean);
  2778.  
  2779. if (hasChanged) {
  2780. manager.m_bChangesMade = true;
  2781. manager.UpdateTradeStatus();
  2782. }
  2783. };
  2784. }());
  2785. }
  2786. }
  2787. ];
  2788.  
  2789. (function() {
  2790. const DEPS = (function() {
  2791. // current version number of script
  2792. const VERSION = '1.9.5';
  2793. // our window object for accessing globals
  2794. const WINDOW = unsafeWindow;
  2795. // dependencies to provide to each page script
  2796. const $ = WINDOW.jQuery;
  2797. /**
  2798. * Utility functions
  2799. * @namespace Utils
  2800. */
  2801. const Utils = {
  2802. /**
  2803. * Get URL parameters
  2804. * @returns {Object} Object containing url parameters e.g. {'item': 'Fruit Shoot'}
  2805. */
  2806. getURLParams: function() {
  2807. let params = {};
  2808. let pattern = /[?&]+([^=&]+)=([^&]*)/gi;
  2809.  
  2810. window.location.search.replace(pattern, (str, key, value) => {
  2811. params[key] = decodeURIComponent(value);
  2812. });
  2813.  
  2814. return params;
  2815. },
  2816. /**
  2817. * Get difference between two arrays
  2818. * @param {Array} arr1 - First array
  2819. * @param {Array} arr2 - Second array
  2820. * @returns {Array} Array with values removed
  2821. */
  2822. difference: function(arr1, arr2) {
  2823. return arr1.filter((a) => {
  2824. return arr2.indexOf(a) === -1;
  2825. });
  2826. },
  2827. /**
  2828. * Check if a variable is undefined, null, or an empty string ('')
  2829. * @param {*} value - Value to check
  2830. * @returns {Boolean} Is empty?
  2831. */
  2832. isEmpty: function(value) {
  2833. return value === undefined || value === null || value === '';
  2834. },
  2835. /**
  2836. * Get unique values from array
  2837. * @param {Array} arr - Array of basic items (strings, numbers)
  2838. * @returns {Array} Array with unique values
  2839. */
  2840. uniq: function(arr) {
  2841. return [...new Set(arr)];
  2842. },
  2843. /**
  2844. * Get a list of IDs from a comma-seperated string
  2845. * @param {String} str - Comma-seperated string
  2846. * @returns {(Array|null)} Array if string is valid, null if not
  2847. */
  2848. getIDsFromString: function(str) {
  2849. if (/(\d+)(,\s*\d+)*/.test(str)) {
  2850. return str.split(',');
  2851. }
  2852.  
  2853. return null;
  2854. },
  2855. /**
  2856. * Execute hot key command
  2857. * @param {Object} e - Event
  2858. * @param {Object} hotKeys - Hot keys mapped to functions
  2859. * @returns {undefined}
  2860. */
  2861. execHotKey: function(e, hotKeys) {
  2862. let isTextField = /textarea|select/i.test(e.target.nodeName) ||
  2863. ['number', 'text'].indexOf(e.target.type) !== -1;
  2864. let code = e.keyCode || e.which;
  2865. let method = hotKeys[code];
  2866.  
  2867. if (!isTextField && method) {
  2868. method();
  2869. }
  2870. },
  2871. /**
  2872. * Flatten arrays
  2873. * @param {Array} arrays - Array of arrays
  2874. * @returns {Array} Flatten array
  2875. */
  2876. flatten: function(arrays) {
  2877. return [].concat(...arrays);
  2878. },
  2879. /**
  2880. * Partition array based on conditions
  2881. * @param {Array} arr - Array
  2882. * @param {Function} fn - Function to satisfy
  2883. * @returns {Array} Partitioned array
  2884. */
  2885. partition: function(arr, fn) {
  2886. let result = [[], []];
  2887.  
  2888. for (let i = 0; i < arr.length; i++) {
  2889. result[fn(arr[i]) ? 0 : 1].push(arr[i]);
  2890. }
  2891.  
  2892. return result;
  2893. },
  2894. /**
  2895. * Group an array by value from key
  2896. * @param {Array} arr - Array
  2897. * @param {String} key - Key to take value from
  2898. * @returns {Object} Object of groups
  2899. */
  2900. groupBy: function(arr, key) {
  2901. return arr.reduce((a, b) => {
  2902. (a[b[key]] = a[b[key]] || []).push(b);
  2903.  
  2904. return a;
  2905. }, {});
  2906. },
  2907. /**
  2908. * Copy a value to clipboard
  2909. * @param {String} str - String to copy
  2910. * @returns {undefined}
  2911. */
  2912. copyToClipboard: function(str) {
  2913. let el = document.createElement('textarea');
  2914.  
  2915. el.value = str;
  2916. document.body.appendChild(el);
  2917. el.select();
  2918. document.execCommand('copy');
  2919. document.body.removeChild(el);
  2920. },
  2921. /**
  2922. * Convert a currency string to a currency object
  2923. * @param {String} string - String to parse
  2924. * @returns {(Object|null)} Object of currencies if string is valid
  2925. */
  2926. stringToCurrencies: function(string) {
  2927. let prices = string.split(',');
  2928. let currencies = {};
  2929. let currencyNames = {
  2930. 'metal': 'metal',
  2931. 'ref': 'metal',
  2932. 'keys': 'keys',
  2933. 'key': 'keys'
  2934. };
  2935.  
  2936. for (let i = 0, n = prices.length; i < n; i++) {
  2937. // match currencies - the first value is the amount
  2938. // the second value is the currency name
  2939. let match = prices[i].trim().match(/^([\d\.]*) (\w*)$/i);
  2940. let currency = currencyNames[match[2]];
  2941. let value = parseFloat(match[1]);
  2942.  
  2943. if (currency) {
  2944. currencies[currency] = value;
  2945. } else {
  2946. // something isn't right
  2947. return null;
  2948. }
  2949. }
  2950.  
  2951. if (Object.keys(currencies).length) {
  2952. return currencies;
  2953. } else {
  2954. return null;
  2955. }
  2956. }
  2957. };
  2958. // these are shared between page scripts
  2959. const shared = {
  2960. // offers shared between offers pages
  2961. offers: {
  2962. // unusual helper functions
  2963. unusual: {
  2964. // all unusual effects as of nov 23, 19
  2965. // missing 2019 taunt effects since they are not availabe on backpack.tf yet
  2966. effectsMap: {
  2967. 'Green Confetti': 6,
  2968. 'Purple Confetti': 7,
  2969. 'Haunted Ghosts': 8,
  2970. 'Green Energy': 9,
  2971. 'Purple Energy': 10,
  2972. 'Circling TF Logo': 11,
  2973. 'Massed Flies': 12,
  2974. 'Burning Flames': 13,
  2975. 'Scorching Flames': 14,
  2976. 'Searing Plasma': 15,
  2977. 'Vivid Plasma': 16,
  2978. 'Sunbeams': 17,
  2979. 'Circling Peace Sign': 18,
  2980. 'Circling Heart': 19,
  2981. 'Stormy Storm': 29,
  2982. 'Blizzardy Storm': 30,
  2983. 'Nuts n\' Bolts': 31,
  2984. 'Orbiting Planets': 32,
  2985. 'Orbiting Fire': 33,
  2986. 'Bubbling': 34,
  2987. 'Smoking': 35,
  2988. 'Steaming': 36,
  2989. 'Flaming Lantern': 37,
  2990. 'Cloudy Moon': 38,
  2991. 'Cauldron Bubbles': 39,
  2992. 'Eerie Orbiting Fire': 40,
  2993. 'Knifestorm': 43,
  2994. 'Misty Skull': 44,
  2995. 'Harvest Moon': 45,
  2996. 'It\'s A Secret To Everybody': 46,
  2997. 'Stormy 13th Hour': 47,
  2998. 'Kill-a-Watt': 56,
  2999. 'Terror-Watt': 57,
  3000. 'Cloud 9': 58,
  3001. 'Aces High': 59,
  3002. 'Dead Presidents': 60,
  3003. 'Miami Nights': 61,
  3004. 'Disco Beat Down': 62,
  3005. 'Phosphorous': 63,
  3006. 'Sulphurous': 64,
  3007. 'Memory Leak': 65,
  3008. 'Overclocked': 66,
  3009. 'Electrostatic': 67,
  3010. 'Power Surge': 68,
  3011. 'Anti-Freeze': 69,
  3012. 'Time Warp': 70,
  3013. 'Green Black Hole': 71,
  3014. 'Roboactive': 72,
  3015. 'Arcana': 73,
  3016. 'Spellbound': 74,
  3017. 'Chiroptera Venenata': 75,
  3018. 'Poisoned Shadows': 76,
  3019. 'Something Burning This Way Comes': 77,
  3020. 'Hellfire': 78,
  3021. 'Darkblaze': 79,
  3022. 'Demonflame': 80,
  3023. 'Showstopper': 3001,
  3024. 'Holy Grail': 3003,
  3025. '\'72': 3004,
  3026. 'Fountain of Delight': 3005,
  3027. 'Screaming Tiger': 3006,
  3028. 'Skill Gotten Gains': 3007,
  3029. 'Midnight Whirlwind': 3008,
  3030. 'Silver Cyclone': 3009,
  3031. 'Mega Strike': 3010,
  3032. 'Bonzo The All-Gnawing': 81,
  3033. 'Amaranthine': 82,
  3034. 'Stare From Beyond': 83,
  3035. 'The Ooze': 84,
  3036. 'Ghastly Ghosts Jr': 85,
  3037. 'Haunted Phantasm Jr': 86,
  3038. 'Haunted Phantasm': 3011,
  3039. 'Ghastly Ghosts': 3012,
  3040. 'Frostbite': 87,
  3041. 'Molten Mallard': 88,
  3042. 'Morning Glory': 89,
  3043. 'Death at Dusk': 90,
  3044. 'Hot': 701,
  3045. 'Isotope': 702,
  3046. 'Cool': 703,
  3047. 'Energy Orb': 704,
  3048. 'Abduction': 91,
  3049. 'Atomic': 92,
  3050. 'Subatomic': 93,
  3051. 'Electric Hat Protector': 94,
  3052. 'Magnetic Hat Protector': 95,
  3053. 'Voltaic Hat Protector': 96,
  3054. 'Galactic Codex': 97,
  3055. 'Ancient Codex': 98,
  3056. 'Nebula': 99,
  3057. 'Death by Disco': 100,
  3058. 'It\'s a mystery to everyone': 101,
  3059. 'It\'s a puzzle to me': 102,
  3060. 'Ether Trail': 103,
  3061. 'Nether Trail': 104,
  3062. 'Ancient Eldritch': 105,
  3063. 'Eldritch Flame': 106,
  3064. 'Neutron Star': 107,
  3065. 'Tesla Coil': 108,
  3066. 'Starstorm Insomnia': 109,
  3067. 'Starstorm Slumber': 110,
  3068. 'Hellish Inferno': 3013,
  3069. 'Spectral Swirl': 3014,
  3070. 'Infernal Flames': 3015,
  3071. 'Infernal Smoke': 3016,
  3072. 'Brain Drain': 111,
  3073. 'Open Mind': 112,
  3074. 'Head of Steam': 113,
  3075. 'Galactic Gateway': 114,
  3076. 'The Eldritch Opening': 115,
  3077. 'The Dark Doorway': 116,
  3078. 'Ring of Fire': 117,
  3079. 'Vicious Circle': 118,
  3080. 'White Lightning': 119,
  3081. 'Omniscient Orb': 120,
  3082. 'Clairvoyance': 121,
  3083. 'Acidic Bubbles of Envy': 3017,
  3084. 'Flammable Bubbles of Attraction': 3018,
  3085. 'Poisonous Bubbles of Regret': 3019,
  3086. 'Roaring Rockets': 3020,
  3087. 'Spooky Night': 3021,
  3088. 'Ominous Night': 3022,
  3089. 'Fifth Dimension': 122,
  3090. 'Vicious Vortex': 123,
  3091. 'Menacing Miasma': 124,
  3092. 'Abyssal Aura': 125,
  3093. 'Wicked Wood': 126,
  3094. 'Ghastly Grove': 127,
  3095. 'Mystical Medley': 128,
  3096. 'Ethereal Essence': 129,
  3097. 'Twisted Radiance': 130,
  3098. 'Violet Vortex': 131,
  3099. 'Verdant Vortex': 132,
  3100. 'Valiant Vortex': 133,
  3101. 'Bewitched': 3023,
  3102. 'Accursed': 3024,
  3103. 'Enchanted': 3025,
  3104. 'Static Mist': 3026,
  3105. 'Eerie Lightning': 3027,
  3106. 'Terrifying Thunder': 3028,
  3107. 'Jarate Shock': 3029,
  3108. 'Nether Void': 3030
  3109. },
  3110. /**
  3111. * Includes effect image in element.
  3112. * @param {Object} itemEl - DOM element.
  3113. * @param {Object} value - Value for Unusual effect.
  3114. * @returns {undefined}
  3115. */
  3116. modifyElement: function(itemEl, value) {
  3117. const versions = {
  3118. // the 188x188 version does not work for purple confetti
  3119. 7: '380x380'
  3120. };
  3121. const version = versions[value];
  3122. const url = shared.offers.unusual.getEffectURL(value, version);
  3123.  
  3124. itemEl.style.backgroundImage = `url('${url}')`;
  3125. itemEl.setAttribute('data-effect', value);
  3126. itemEl.classList.add('unusual');
  3127. },
  3128. /**
  3129. * Gets the effect value from an effect name.
  3130. * @param {String} effectName - Effect name.
  3131. * @returns {(String|undefined)} Effect value, if available.
  3132. */
  3133. getEffectValue: function(effectName) {
  3134. return shared.offers.unusual.effectsMap[effectName];
  3135. },
  3136. /**
  3137. * Gets URL of image for effect.
  3138. * @param {Number} value - Value of effect.
  3139. * @param {Number} [version] - Size of image from backpack.tf.
  3140. * @returns {String} URL string
  3141. */
  3142. getEffectURL: function(value, version) {
  3143. return `https://backpack.tf/images/440/particles/${value}_${version || '188x188'}.png`;
  3144. },
  3145. /**
  3146. * Gets the effect name from an item.
  3147. * @param {Object} item - Item from steam.
  3148. * @returns {(String|null|undefined)} Effect name, if available.
  3149. */
  3150. getEffectName: function(item) {
  3151. const hasDescriptions = typeof item.descriptions === 'object';
  3152. const isUnique = (item.name_color || '').toUpperCase() === '7D6D00';
  3153.  
  3154. // unique items should probably never have effects
  3155. // though, cases have "Unusual Effect" descriptions and we want to exclude them
  3156. if (!hasDescriptions || isUnique) {
  3157. return null;
  3158. }
  3159.  
  3160. for (let i = 0; i < item.descriptions.length; i++) {
  3161. const description = item.descriptions[i];
  3162. const match = (
  3163. description.color === 'ffd700' &&
  3164. description.value.match(/^\u2605 Unusual Effect: (.+)$/)
  3165. );
  3166.  
  3167. if (match) {
  3168. return match[1];
  3169. }
  3170. }
  3171. }
  3172. }
  3173. }
  3174. };
  3175.  
  3176. // set a stored value
  3177. function setStored(name, value) {
  3178. GM_setValue(name, value);
  3179. }
  3180.  
  3181. // get a stored value
  3182. function getStored(name) {
  3183. return GM_getValue(name);
  3184. }
  3185.  
  3186. return {
  3187. VERSION,
  3188. WINDOW,
  3189. $,
  3190. Utils,
  3191. shared,
  3192. setStored,
  3193. getStored
  3194. };
  3195. }());
  3196. const script = scripts.find(({includes}) => {
  3197. return includes.some((pattern) => {
  3198. return Boolean(location.href.match(pattern));
  3199. });
  3200. });
  3201.  
  3202. if (script) {
  3203. if (script.styles) {
  3204. // add the styles
  3205. GM_addStyle(script.styles);
  3206. }
  3207.  
  3208. if (script.fn) {
  3209. // run the script
  3210. script.fn(DEPS);
  3211. }
  3212. }
  3213. }());
  3214. }());
Add Comment
Please, Sign In to add comment