Infinite craft QOL

Infinite craft Quality of life scripts

  1. // ==UserScript==
  2. // @name Infinite craft QOL
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024-02-07
  5. // @description Infinite craft Quality of life scripts
  6. // @author You
  7. // @match http://neal.fun/infinite-craft
  8. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12.  
  13. //----------------------------------------------------------------------------------------------------------------------------------------
  14. //CREATE SORTING FUNCTION
  15. const sortElements= () => {
  16. window.$nuxt.$root.$children[2].$children[0].$children[0]._data.elements.sort((a, b) => (a.text > b.text) ? 1 : -1);
  17. }
  18.  
  19. //----------------------------------------------------------------------------------------------------------------------------------------
  20. // CREATE SORTING BUTTON FUNCTION
  21. function createSortButton(){
  22. const buttonStyle = {
  23. appearance: 'none',
  24. position: 'absolute',
  25. width: '80px',
  26. height: '35px',
  27. backgroundColor: '#1A1B31',
  28. color: 'white',
  29. fontWeight: 'bold',
  30. fontFamily: 'Roboto,sans-serif',
  31. border: '0',
  32. outline: 'none',
  33. borderRadius: '5px',
  34. cursor: 'pointer',
  35. padding: 4,
  36. left: '24px',
  37. bottom: '24px',
  38. }
  39. function addSortButtonDOM() {
  40. var button = document.createElement("button");
  41. Object.keys(buttonStyle).forEach((attr) => {
  42. button.style[attr] = buttonStyle[attr];
  43. });
  44. button.innerText = "Sort elements"
  45. button.addEventListener('click', () => sortElements());
  46. document.body.appendChild(button);
  47. }
  48. addSortButtonDOM();
  49. }
  50.  
  51. //----------------------------------------------------------------------------------------------------------------------------------------
  52. // REPLACE ORIGINAL NON WORKING SEARCH BAR
  53. function getElementByPlaceholder(placeholder) {
  54. // Get all input elements in the document
  55. const inputElements = document.querySelectorAll('input');
  56. // Iterate through the input elements
  57. for (const input of inputElements) {
  58. // Check if the current input element has the specified placeholder
  59. if (input.placeholder === placeholder) {
  60. return input; // Return the element if found
  61. }
  62. }
  63. return null; // Return null if no element with the specified placeholder is found
  64. }
  65.  
  66. function replaceSearchBar(){
  67. let items = () => [...document.querySelectorAll('.item')]
  68. let show = (elt) => elt.style.display=''
  69. let hide = (elt) => elt.style.display='none'
  70. let search = (text) => (items().forEach(show), items().filter(e => !e.innerText.toLowerCase().includes(text.toLowerCase())).forEach(hide))
  71. let inputElt = document.createElement('input'); inputElt.type='text';
  72.  
  73. function handle(e) { search(e.target.value) }
  74.  
  75. inputElt.style.webkitFontSmoothing = 'antialiased';
  76. inputElt.style.userSelect = 'none';
  77. inputElt.style.boxSizing = 'border-box';
  78. inputElt.style.margin = '0';
  79. inputElt.style.width = '100%';
  80. inputElt.style.fontSize = '16px';
  81. inputElt.style.border = 'none';
  82. inputElt.style.borderTop = '1px solid #c8c8c8';
  83. inputElt.style.outline = '0';
  84. inputElt.style.padding = '0 20px 0 40px';
  85. inputElt.style.height = '40px';
  86. inputElt.style.lineHeight = '18px';
  87. inputElt.style.position = 'relative';
  88. inputElt.style.background = 'url(/infinite-craft/search.svg) no-repeat 22px 22px';
  89. inputElt.style.backgroundSize = '21px 21px';
  90. inputElt.style.backgroundPosition = '10px 10px';
  91.  
  92. inputElt.placeholder = "Search items.....";
  93.  
  94. inputElt.addEventListener('input', handle);
  95.  
  96. getElementByPlaceholder("Search items...").replaceWith(inputElt);
  97. }
  98.  
  99. // adrianmgg's SCRIPT TO ADD RECIPES LIST + DISCOVERIES LIST
  100.  
  101. (function() {
  102. 'use strict';
  103. const elhelper = (function() { /* via https://github.com/adrianmgg/elhelper */
  104. function setup(elem, { style: { vars: styleVars = {}, ...style } = {}, attrs = {}, dataset = {}, events = {}, classList = [], children = [], parent = null, insertBefore = null, ...props }) {
  105. for (const k in style) elem.style[k] = style[k];
  106. for (const k in styleVars) elem.style.setProperty(k, styleVars[k]);
  107. for (const k in attrs) elem.setAttribute(k, attrs[k]);
  108. for (const k in dataset) elem.dataset[k] = dataset[k];
  109. for (const k in events) elem.addEventListener(k, events[k]);
  110. for (const c of classList) elem.classList.add(c);
  111. for (const k in props) elem[k] = props[k];
  112. for (const c of children) elem.appendChild(c);
  113. if (parent !== null) {
  114. if (insertBefore !== null) parent.insertBefore(elem, insertBefore);
  115. else parent.appendChild(elem);
  116. }
  117. return elem;
  118. }
  119. function create(tagName, options = {}) { return setup(document.createElement(tagName), options); }
  120. function createNS(namespace, tagName, options = {}) { return setup(document.createElementNS(namespace, tagName), options); }
  121. return {setup, create, createNS};
  122. })();
  123. const GM_VALUE_KEY = 'infinitecraft_observed_combos';
  124. const GM_DATAVERSION_KEY = 'infinitecraft_data_version';
  125. const GM_DATAVERSION_LATEST = 1;
  126. // TODO this should probably use the async versions of getvalue/setvalue since we're already only calling it from async code
  127. function saveCombo(lhs, rhs, result) {
  128. console.log(`crafted ${lhs} + ${rhs} -> ${result}`);
  129. const data = getCombos();
  130. if(!(result in data)) data[result] = [];
  131. const sortedLhsRhs = sortRecipeIngredients([lhs, rhs]);
  132. for(const existingPair of data[result]) {
  133. if(sortedLhsRhs[0] === existingPair[0] && sortedLhsRhs[1] === existingPair[1]) return;
  134. }
  135. const pair = [lhs, rhs];
  136. pair.sort();
  137. data[result].push(pair);
  138. GM_setValue(GM_VALUE_KEY, data);
  139. GM_setValue(GM_DATAVERSION_KEY, GM_DATAVERSION_LATEST);
  140. }
  141. // !! this sorts in-place !!
  142. function sortRecipeIngredients(components) {
  143. // internally the site uses localeCompare() but that being locale-specific could cause some problems in our use case
  144. // it shouldn't matter though, since as long as we give these *some* consistent order it'll avoid duplicates,
  145. // that order doesn't need to be the same as the one the site uses
  146. return components.sort();
  147. }
  148. function getCombos() {
  149. const data = GM_getValue(GM_VALUE_KEY, {});
  150. const dataVersion = GM_getValue(GM_DATAVERSION_KEY, 0);
  151. if(dataVersion > GM_DATAVERSION_LATEST) {
  152. // uh oh
  153. // not gonna even try to handle this case, just toss up an error alert
  154. const msg = `Outdated script version, backup your save to continue. Press cancel`
  155. alert(msg);
  156. throw new Error(msg);
  157. }
  158. if(dataVersion < GM_DATAVERSION_LATEST) {
  159. // confirm that user wants to update save data
  160. const updateConfirm = confirm(`Outdated script version, backup your save to continue. Press cancel`);
  161. if(!updateConfirm) {
  162. throw new Error('user chose not to update save data');
  163. }
  164. // upgrade the data
  165. if(dataVersion <= 0) {
  166. // recipes in this version weren't sorted, and may contain duplicates once sorting has been applied
  167. for(const result in data) {
  168. // sort the recipes (just do it in place, since we're not gonna use the old data again
  169. for(const recipe of data[result]) {
  170. sortRecipeIngredients(recipe);
  171. }
  172. // build new list with just the ones that remain not duplicate
  173. const newRecipesList = [];
  174. for(const recipe of data[result]) {
  175. if(!(newRecipesList.some(r => recipe[0] === r[0] && recipe[1] === r[1]))) {
  176. newRecipesList.push(recipe);
  177. }
  178. }
  179. data[result] = newRecipesList;
  180. }
  181. }
  182. // now that it's upgraded, save the upgraded data & update the version
  183. GM_setValue(GM_VALUE_KEY, data);
  184. GM_setValue(GM_DATAVERSION_KEY, GM_DATAVERSION_LATEST);
  185. // (fall through to retun below)
  186. }
  187. // the data is definitely current now
  188. return data;
  189. }
  190. function main() {
  191. const _getCraftResponse = icMain.getCraftResponse;
  192. const _selectElement = icMain.selectElement;
  193. icMain.getCraftResponse = async function(lhs, rhs) {
  194. const resp = await _getCraftResponse.apply(this, arguments);
  195. saveCombo(lhs.text, rhs.text, resp.result);
  196. return resp;
  197. };
  198.  
  199. // random element thing
  200. document.documentElement.addEventListener('mousedown', e => {
  201. if(e.buttons === 1 && e.altKey && !e.shiftKey) { // left mouse + alt
  202. e.preventDefault();
  203. e.stopPropagation();
  204. const elements = icMain._data.elements;
  205. const randomElement = elements[Math.floor(Math.random() * elements.length)];
  206. _selectElement(e, randomElement);
  207. } else if(e.buttons === 1 && !e.altKey && e.shiftKey) { // lmb + shift
  208. e.preventDefault();
  209. e.stopPropagation();
  210. const instances = icMain._data.instances;
  211. const lastInstance = instances[instances.length - 1];
  212. const lastInstanceElement = icMain._data.elements.filter(e => e.text === lastInstance.text)[0];
  213. _selectElement(e, lastInstanceElement);
  214. }
  215. }, {capture: false});
  216.  
  217. // regex-based searching
  218. const _sortedElements__get = icMain?._computedWatchers?.sortedElements?.getter;
  219. // if that wasn't where we expected it to be, don't try to patch it
  220. if(_sortedElements__get !== null && _sortedElements__get !== undefined) {
  221. icMain._computedWatchers.sortedElements.getter = function() {
  222. if(this.searchQuery && this.searchQuery.startsWith('regex:')) {
  223. try {
  224. const pattern = new RegExp(this.searchQuery.substr(6));
  225. return this.elements.filter((element) => pattern.test(element.text));
  226. } catch(err) {
  227. return [];
  228. }
  229. } else {
  230. return _sortedElements__get.apply(this, arguments);
  231. }
  232. }
  233. }
  234.  
  235. // get the dataset thing they use for scoping css stuff
  236. // TODO add some better handling for if there's zero/multiple dataset attrs on that element in future
  237. const cssScopeDatasetThing = Object.keys(icMain.$el.dataset)[0];
  238.  
  239. function mkElementItem(element) {
  240. return elhelper.create('div', {
  241. classList: ['item'],
  242. dataset: {[cssScopeDatasetThing]: ''},
  243. children: [
  244. elhelper.create('span', {
  245. classList: ['item-emoji'],
  246. dataset: {[cssScopeDatasetThing]: ''},
  247. textContent: element.emoji,
  248. style: {
  249. pointerEvents: 'none',
  250. },
  251. }),
  252. document.createTextNode(` ${element.text} `),
  253. ],
  254. });
  255. }
  256.  
  257. /* this will call genFn and iterate all the way through it,
  258. but taking a break every chunkSize iterations to allow rendering and stuff to happen.
  259. returns a promise. */
  260. function nonBlockingChunked(chunkSize, genFn, timeout = 0) {
  261. return new Promise((resolve, reject) => {
  262. const gen = genFn();
  263. (function doChunk() {
  264. for(let i = 0; i < chunkSize; i++) {
  265. const next = gen.next();
  266. if(next.done) {
  267. resolve();
  268. return;
  269. }
  270. }
  271. setTimeout(doChunk, timeout);
  272. })();
  273. });
  274. }
  275.  
  276. // recipes popup
  277. const recipesListContainer = elhelper.create('div', {
  278. });
  279. function clearRecipesDialog() {
  280. while(recipesListContainer.firstChild !== null) recipesListContainer.removeChild(recipesListContainer.firstChild);
  281. }
  282. const recipesDialog = elhelper.create('dialog', {
  283. parent: document.body,
  284. children: [
  285. // close button
  286. elhelper.create('button', {
  287. textContent: 'x',
  288. events: {
  289. click: (evt) => recipesDialog.close(),
  290. },
  291. }),
  292. // the main content
  293. recipesListContainer,
  294. ],
  295. style: {
  296. // need to unset this one thing from the page css
  297. margin: 'auto',
  298. },
  299. events: {
  300. close: (e) => {
  301. clearRecipesDialog();
  302. },
  303. },
  304. });
  305. async function openRecipesDialog(childGenerator) {
  306. clearRecipesDialog();
  307. // create a child to add to for just this call,
  308. // as a lazy fix for the bug we'd otherwise have where opening a menu, quickly closing it, then opening it again
  309. // would lead to the old menu's task still adding stuff to the new menu.
  310. // (this doesn't actually stop any unnecessary work, but it at least prevents the possible visual bugs)
  311. const container = elhelper.create('div', {parent: recipesListContainer});
  312. // show the dialog
  313. recipesDialog.showModal();
  314. // populate the dialog
  315. await nonBlockingChunked(512, function*() {
  316. for(const child of childGenerator()) {
  317. container.appendChild(child);
  318. yield;
  319. }
  320. });
  321. }
  322.  
  323. // recipes button
  324. function addControlsButton(label, handler) {
  325. elhelper.create('div', {
  326. parent: document.querySelector('.side-controls'),
  327. textContent: label,
  328. style: {
  329. cursor: 'pointer',
  330. },
  331. events: {
  332. click: handler,
  333. },
  334. });
  335. }
  336.  
  337. addControlsButton('recipes', () => {
  338. // build a name -> element map
  339. const byName = {};
  340. for(const element of icMain._data.elements) byName[element.text] = element;
  341. function getByName(name) { return byName[name] ?? {emoji: "❌", text: `[userscript encountered an error trying to look up element '${name}']`}; }
  342. const combos = getCombos();
  343. function listItemClick(evt) {
  344. const elementName = evt.target.dataset.comboviewerElement;
  345. document.querySelector(`[data-comboviewer-section="${CSS.escape(elementName)}"]`).scrollIntoView({block: 'nearest'});
  346. }
  347. function mkLinkedElementItem(element) {
  348. return elhelper.setup(mkElementItem(element), {
  349. events: { click: listItemClick },
  350. dataset: { comboviewerElement: element.text },
  351. });
  352. }
  353. openRecipesDialog(function*(){
  354. for(const comboResult in combos) {
  355. if(comboResult === 'Nothing') continue;
  356. // anchor for jumping to
  357. yield elhelper.create('div', {
  358. dataset: { comboviewerSection: comboResult },
  359. });
  360. for(const [lhs, rhs] of combos[comboResult]) {
  361. yield elhelper.create('div', {
  362. children: [
  363. mkLinkedElementItem(getByName(comboResult)),
  364. document.createTextNode(' = '),
  365. mkLinkedElementItem(getByName(lhs)),
  366. document.createTextNode(' + '),
  367. mkLinkedElementItem(getByName(rhs)),
  368. ],
  369. });
  370. }
  371. }
  372. });
  373. });
  374.  
  375. // first discoveries list (just gonna hijack the recipes popup for simplicity)
  376. addControlsButton('discoveries', () => {
  377. openRecipesDialog(function*() {
  378. for(const element of icMain._data.elements) {
  379. if(element.discovered) {
  380. yield mkElementItem(element);
  381. }
  382. }
  383. });
  384. });
  385.  
  386. // pinned combos thing
  387. const sidebar = document.querySelector('.container > .sidebar');
  388. const pinnedCombos = elhelper.create('div', {
  389. parent: sidebar,
  390. insertBefore: sidebar.firstChild,
  391. style: {
  392. position: 'sticky',
  393. top: '0',
  394. background: 'white',
  395. width: '100%',
  396. maxHeight: '50%',
  397. overflowY: 'auto',
  398. },
  399. });
  400. icMain.selectElement = function(mouseEvent, element) {
  401. if(mouseEvent.buttons === 4 || (mouseEvent.buttons === 1 && mouseEvent.altKey && !mouseEvent.shiftKey)) {
  402. // this won't actually stop it since what gets passed into this is a mousedown event
  403. mouseEvent.preventDefault();
  404. mouseEvent.stopPropagation();
  405. // this isnt a good variable name but it's slightly funny and sometimes that's all that matters
  406. const elementElement = mkElementItem(element);
  407. elhelper.setup(elementElement, {
  408. parent: pinnedCombos,
  409. events: {
  410. mousedown: (e) => {
  411. if(e.buttons === 4 || (e.buttons === 1 && e.altKey && !e.shiftKey)) {
  412. pinnedCombos.removeChild(elementElement);
  413. return;
  414. }
  415. icMain.selectElement(e, element);
  416. },
  417. },
  418. });
  419. return;
  420. }
  421. return _selectElement.apply(this, arguments);
  422. };
  423. }
  424. // stores the object where most of the infinite craft functions live.
  425. // can be assumed to be set by the time main is called
  426. let icMain = null;
  427. // need to wait for stuff to be actually initialized.
  428. // might be an actual thing we can hook into to detect that
  429. // but for now just waiting until the function we want exists works well enough
  430. (function waitForReady(){
  431. icMain = Window?.$nuxt?._route?.matched?.[0]?.instances?.default;
  432. if(icMain !== undefined && icMain !== null) main();
  433. else setTimeout(waitForReady, 10);
  434. })();
  435. })();
  436.  
  437. //--------------------------------------------------------------------------------------------------------------------------------------------------
  438. // Calling previously declared functions
  439. createSortButton();
  440. replaceSearchBar();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址