IcePetsPlus

Some minor QOL improvements for icepets

  1. // ==UserScript==
  2. // @name IcePetsPlus
  3. // @namespace
  4. // @version 1.0
  5. // @description Some minor QOL improvements for icepets
  6. // @author Cullen
  7. // @match https://www.icepets.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=icepets.com
  9. // @grant GM_addStyle
  10. // @namespace https://greasyfork.org/users/428778
  11. // ==/UserScript==
  12. (function() {
  13. 'use strict';
  14.  
  15. const globals = {
  16. sortAsc: false
  17. }
  18.  
  19. // RUN THIS THROUGH THIS SOMETIMES
  20. // https://beautifier.io/
  21.  
  22. GM_addStyle(`
  23. input:not([type='text']), button {
  24. padding: 5px 10px;
  25. margin: 1px;
  26. cursor: pointer;
  27. }
  28.  
  29. .copyTooltip {
  30. position: fixed;
  31. z-index: 1000;
  32. padding: 5px 10px;
  33. border: 1px solid #111;
  34. background: #f2f2f5;
  35. font-size: 12px;
  36. font-family: verdana;
  37. }
  38.  
  39. .tableheader td {
  40. cursor:pointer;
  41. }
  42. `);
  43.  
  44. const routes = () => {
  45. // Globals
  46. setupToolstips()
  47.  
  48. // Routed
  49. const path = window.location.pathname;
  50. const search = window.location.search;
  51. switch (path) {
  52. case '/halipar-jungle/companion-reserve':
  53. case '/snowslide-mountains/snow-jar-igloo':
  54. case '/glacia/page-turners':
  55. case '/glacia/frozen-collectives-emporium':
  56. case '/glacia/post-office':
  57. case '/glacia/plushie-palace':
  58. case '/glacia/toy-trunk':
  59. case '/misty-isle/grooming-parlour':
  60. case '/glacia/glacial-grocer':
  61. case '/misty-isle/battle-shop':
  62. case '/snowslide-mountains/sugar-rush':
  63. case '/snowslide-mountains/affogato':
  64. case '/misty-isle/golden-touch':
  65. setupShopPage();
  66. break;
  67. case '/halipar-jungle/collectors-quest':
  68. case '/quests/collect.php':
  69. setupCollectorPage();
  70. break;
  71. case '/usershops.php':
  72. search == '?stock' ? setupShopStockPage() : null;
  73. break;
  74. case '/arcade/gs.php':
  75. setupSlotsPage();
  76. break;
  77. default:
  78. break;
  79. }
  80. }
  81.  
  82. // Notes/weirdthings in comments below
  83.  
  84. /*
  85. const apiURL = "https://new.icepets.com/api/v1/search/items?item=&category=Companion&perPage=9999";
  86.  
  87. fetch(apiURL)
  88. .then(response => response.json())
  89. .then(data => {
  90. const filteredObjects = data.data.map(obj => !obj.slug.includes("-") ? obj.name : null).filter(x=>x!=null);
  91. console.log(filteredObjects);
  92. })
  93. .catch(error => {
  94. console.error("Error:", error);
  95. });
  96.  
  97. */
  98.  
  99. // https://www.icepets.com/snowslide-mountains/found-snow-jar
  100.  
  101. const setupCollectorPage = () => {
  102. console.log('setupCollectorPage')
  103. document.querySelectorAll('table table tbody td img').forEach(questItem => {
  104. const substrStart = questItem.src.indexOf('/items/') + 7;
  105. const substrEnd = questItem.src.length - 4
  106.  
  107. const itemId = questItem.src.substring(substrStart, substrEnd);
  108.  
  109. const button = document.createElement('button')
  110. button.style.transition = "background-color 0.2s ease-out";
  111. button.style.width = '90px';
  112. button.style.margin = '1px'
  113. button.innerHTML = 'Buy';
  114. button.addEventListener('click', function() {
  115. button.innerHTML = 'Checking...';
  116. _buyScrapItem(itemId, button);
  117. });
  118.  
  119. questItem.parentElement.append(button)
  120. })
  121. }
  122.  
  123. const setupShopPage = () => {
  124. console.log('setupShopPage')
  125. const timerCountdown = () => {
  126. const timer = document.querySelector('b')
  127. const time = timer.innerText.split(' seconds')[0]
  128. let timeInSeconds = time.indexOf(':') != -1 ? Number(time.split(':')[0] * 60) + Number(time.split(':')[1]) : time;
  129. timeInSeconds--;
  130. const minutes = Math.floor(timeInSeconds / 60);
  131. const seconds = (timeInSeconds % 60).toLocaleString('en-US', {
  132. minimumIntegerDigits: 2,
  133. useGrouping: false
  134. })
  135. if (timeInSeconds == -1) return
  136. timer.innerHTML = minutes == 0 ? `${seconds} seconds` : `${minutes}:${seconds} seconds`
  137. // Wtf workers make timeouts work even in inactive tabs
  138. // https://stackoverflow.com/questions/5927284/how-can-i-make-setinterval-also-work-when-a-tab-is-inactive-in-chrome/5927432#12522580
  139. const blob = new Blob(["setTimeout(function(){postMessage('')}, 1000)"])
  140. const worker = new Worker(window.URL.createObjectURL(blob))
  141. worker.onmessage = timerCountdown;
  142. }
  143.  
  144. timerCountdown()
  145. }
  146.  
  147. const setupShopStockPage = () => {
  148. console.log('setupShopStockPage')
  149. // Copy "Update Stock" button to the top
  150.  
  151. const updateButtonCopy = document.querySelector('.submitbutton').cloneNode(true);
  152. document.querySelector('.submitbutton').parentElement.prepend(updateButtonCopy)
  153. updateButtonCopy.after(document.createElement('br'))
  154. updateButtonCopy.after(document.createElement('br'))
  155.  
  156. // Add sorting to the stock table
  157. // https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
  158. const headers = document.querySelectorAll('tr.tableheader td');
  159. headers.forEach(th => th.addEventListener('click', (() => _sortTableByHeader(th, headers))));
  160.  
  161. // Sort manually the first time to sort price low to high
  162. const th = document.querySelectorAll('tr.tableheader td')[2];
  163. _sortTableByHeader(th, headers);
  164. }
  165.  
  166. const setupSlotsPage = () => {
  167. const verify = document.querySelector('input[name="gs_verify"]')
  168.  
  169. if (verify) verify.focus();
  170. document.body.addEventListener('keyup', (e) => {
  171. if (e.key == 'Enter') document.querySelector('input[name="submit"]').click()
  172. })
  173.  
  174. }
  175.  
  176. const setupToolstips = () => {
  177. if (['/news.php', '/newboards/viewposts.php'] // Skip these
  178. .indexOf(window.location.pathname) != -1) return
  179.  
  180. console.log('setupToolstips')
  181. GM_addStyle(`
  182. img[src*="images/items"] {
  183. cursor: pointer;
  184. }
  185. `);
  186. Array.from(document.querySelectorAll(':not(a) img[src*="images/items"]'))
  187. .filter(item => item.parentElement.tagName != 'A')
  188. .forEach(shopItem => {
  189. const itemName = _getItemNameFromNode(shopItem);
  190. shopItem.addEventListener('click', e => {
  191. navigator.clipboard.writeText(itemName);
  192. _addTooltip(shopItem, 'Copied item name!', e)
  193. });
  194. })
  195. }
  196.  
  197. const _addTooltip = (element, text, e, speed) => {
  198. // Create a tooltip element.
  199. var tooltip = document.createElement('div');
  200. tooltip.textContent = text;
  201. tooltip.className = 'copyTooltip'
  202. tooltip.style.top = (e.clientY + 20) + "px";
  203. tooltip.style.left = (e.clientX + 20) + "px"
  204.  
  205. // Add the tooltip to the DOM.
  206. element.parentNode.prepend(tooltip);
  207. setTimeout(() => _removeFadeOut(tooltip, speed ?? 500), 1000);
  208. }
  209.  
  210. const _buyScrapItem = (itemId, button, price) => {
  211. const XHR = new XMLHttpRequest();
  212. const FD = new FormData();
  213. const dataFormat = {
  214. offer_price: price ?? '1618',
  215. action_buy: 'Buy Another'
  216. }
  217.  
  218. for (const [name, value] of Object.entries(dataFormat)) {
  219. FD.append(name, value);
  220. }
  221.  
  222. XHR.addEventListener("load", (event) => {
  223. //console.log("Yeah! Data sent and response loaded.", event);
  224. });
  225.  
  226. XHR.addEventListener("error", (event) => {
  227. _setButtonSuccess(false, 'Oops! Error', button)
  228. });
  229.  
  230. XHR.onreadystatechange = () => {
  231. if (XHR.readyState == 4) {
  232. if (XHR.status == 200) {
  233. // The request was successful
  234. if (XHR.responseText.indexOf('The item you were purchasing is no longer available')) {
  235. _setButtonSuccess(true, 'Bought! :)', button)
  236. } else {
  237. _setButtonSuccess(false, 'Nope :(', button)
  238. }
  239. } else {
  240. _setButtonSuccess(false, 'Oops! Error', button)
  241. }
  242. return false
  243. }
  244. };
  245.  
  246. XHR.open("POST", `https://www.icepets.com/scrapshop/index.php?act=buyitem&item=${itemId}`);
  247.  
  248. XHR.send(FD);
  249. }
  250.  
  251. const _checkAllWordsStartWithCapitalLetter = (str) => {
  252. const words = str?.split(' ');
  253. if (!words) return false
  254. for (const word of words) {
  255. if (word[0]?.toUpperCase() !== word[0]) {
  256. return false;
  257. }
  258. }
  259. return true;
  260. }
  261.  
  262. const _checkTokenDabuRewards = (shopItem) => {
  263. if (!shopItem?.parentElement?.childNodes) return undefined
  264. return Array.from(shopItem?.parentElement?.childNodes)
  265. .filter(node => node?.tagName == "STRONG" &&
  266. _checkAllWordsStartWithCapitalLetter(node?.textContent) &&
  267. node?.children?.length == 0 &&
  268. node?.textContent?.indexOf(':') == -1
  269. )[0]?.textContent
  270. }
  271.  
  272. const _checkNovitariaQuest = (shopItem) => {
  273. if (!shopItem?.parentElement?.childNodes) return undefined
  274. return Array.from(shopItem?.parentElement?.childNodes)
  275. .filter(node => node?.wholeText && node?.textContent.length > 2 && node?.textContent?.length < 100)[0]
  276. .textContent
  277. }
  278.  
  279. const _compareSortingValues = (v1, v2) => {
  280. const validVersions = v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)
  281. return validVersions ? v1 - v2 : v1.toString().localeCompare(v2)
  282. }
  283.  
  284. const _getCellValue = (tr, idx) => {
  285. return tr.children[idx].children[0]?.value || // input fields
  286. tr.children[idx]?.innerText || // text from first node
  287. tr.children[idx]?.textContent; // text from all child nodes
  288. }
  289.  
  290. const _getItemNameFromNode = (shopItem) => {
  291. return shopItem?.parentElement?.querySelector('b')?.innerHTML ?? // NPC Shops
  292. _checkTokenDabuRewards(shopItem) ?? // Token Dabu reward
  293. shopItem?.parentElement?.querySelectorAll('strong')?.[2]?.innerHTML ?? // Beauty King
  294. shopItem?.parentElement?.querySelector('strong')?.innerHTML ?? // Collector Quest, Shop Purchase Confirmation, Cube Grab Main Page, Storage
  295. (shopItem?.alt.length > 0 ? shopItem?.alt : null) ?? // Solitary Sprite
  296. _checkNovitariaQuest(shopItem) ?? // Novitaria Quest
  297. shopItem?.parentElement?.lastChild?.textContent ?? // Scratchcard prizes, Shop Stock
  298. ''; // Uhhhhh
  299. }
  300.  
  301. const _removeFadeOut = (el, speed) => {
  302. var seconds = speed / 1000;
  303. el.style.transition = "opacity " + seconds + "s ease";
  304.  
  305. el.style.opacity = 0;
  306. setTimeout(function() {
  307. el.parentNode.removeChild(el);
  308. }, speed);
  309. }
  310.  
  311. const _setButtonSuccess = (success, message, button) => {
  312. button.innerHTML = message;
  313. button.style.backgroundColor = success ? '#8fdf96' : '#df8f8f';
  314.  
  315. setTimeout(() => {
  316. button.style.backgroundColor = '#d6e7ff';
  317. button.innerHTML = 'Buy';
  318. }, 1250)
  319. }
  320.  
  321. const _sortTableByHeader = (th, headers) => {
  322. const table = th.closest('table');
  323. const tableRows = Array.from(th.parentNode.children);
  324. const thIndex = tableRows.indexOf(th)
  325.  
  326. headers.forEach(th2 => {
  327. th2.innerHTML = th2.innerHTML.replace(' ↓', '').replace(' ↑', '')
  328. })
  329. th.innerHTML += globals.sortAsc ? ' ↑' : ' ↓';
  330.  
  331. Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
  332. .sort(_sortTableSortHandler(thIndex))
  333. .forEach(tr => table.appendChild(tr));
  334. }
  335.  
  336. const _sortTableSortHandler = (idx) => {
  337. globals.sortAsc = !globals.sortAsc;
  338. // .sort((a, b) => yourFunctionBlock)
  339. return (a, b) => {
  340. return _compareSortingValues(_getCellValue(globals.sortAsc ? a : b, idx), _getCellValue(globals.sortAsc ? b : a, idx));
  341. }
  342. }
  343.  
  344. routes()
  345. })();