infinite craft tweaks

recipe tracking + other various tweaks for infinite craft

当前为 2024-02-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name infinite craft tweaks
  3. // @namespace https://github.com/adrianmgg
  4. // @version 2.3.0
  5. // @description recipe tracking + other various tweaks for infinite craft
  6. // @author amgg
  7. // @match https://neal.fun/infinite-craft/
  8. // @icon https://neal.fun/favicons/infinite-craft.png
  9. // @grant unsafeWindow
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @run-at document-idle
  13. // @compatible chrome
  14. // @compatible firefox
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20. const elhelper = (function() { /* via https://github.com/adrianmgg/elhelper */
  21. function setup(elem, { style: { vars: styleVars = {}, ...style } = {}, attrs = {}, dataset = {}, events = {}, classList = [], children = [], parent = null, insertBefore = null, ...props }) {
  22. for (const k in style) elem.style[k] = style[k];
  23. for (const k in styleVars) elem.style.setProperty(k, styleVars[k]);
  24. for (const k in attrs) elem.setAttribute(k, attrs[k]);
  25. for (const k in dataset) elem.dataset[k] = dataset[k];
  26. for (const k in events) elem.addEventListener(k, events[k]);
  27. for (const c of classList) elem.classList.add(c);
  28. for (const k in props) elem[k] = props[k];
  29. for (const c of children) elem.appendChild(c);
  30. if (parent !== null) {
  31. if (insertBefore !== null) parent.insertBefore(elem, insertBefore);
  32. else parent.appendChild(elem);
  33. }
  34. return elem;
  35. }
  36. function create(tagName, options = {}) { return setup(document.createElement(tagName), options); }
  37. function createNS(namespace, tagName, options = {}) { return setup(document.createElementNS(namespace, tagName), options); }
  38. return {setup, create, createNS};
  39. })();
  40. const GM_VALUE_KEY = 'infinitecraft_observed_combos';
  41. // TODO this should probably use the async versions of getvalue/setvalue since we're already only calling it from async code
  42. function saveCombo(lhs, rhs, result) {
  43. console.log(`crafted ${lhs} + ${rhs} -> ${result}`);
  44. const data = GM_getValue(GM_VALUE_KEY, {});
  45. if(!(result in data)) data[result] = [];
  46. for(const [a, b] in data[result]) {
  47. if(a === lhs && b === rhs) return;
  48. }
  49. data[result].push([lhs, rhs]);
  50. GM_setValue(GM_VALUE_KEY, data);
  51. }
  52. function getCombos() {
  53. return GM_getValue(GM_VALUE_KEY, {});
  54. }
  55. function* iterCombos() {
  56. const data = getCombos;
  57. for(const result in data) {
  58. for(const [lhs, rhs] of data[result]) {
  59. yield {lhs, rhs, result};
  60. }
  61. }
  62. }
  63. function main() {
  64. const _getCraftResponse = icMain.getCraftResponse;
  65. icMain.getCraftResponse = async function(lhs, rhs) {
  66. const resp = await _getCraftResponse.apply(this, arguments);
  67. saveCombo(lhs.text, rhs.text, resp.result);
  68. return resp;
  69. };
  70.  
  71. // random element thing
  72. document.documentElement.addEventListener('mousedown', e => {
  73. if(e.buttons === 1 && e.altKey && !e.shiftKey) { // left mouse + alt
  74. e.preventDefault();
  75. e.stopPropagation();
  76. const elements = icMain._data.elements;
  77. const randomElement = elements[Math.floor(Math.random() * elements.length)];
  78. icMain.selectElement(e, randomElement);
  79. } else if(e.buttons === 1 && !e.altKey && e.shiftKey) { // lmb + shift
  80. e.preventDefault();
  81. e.stopPropagation();
  82. const instances = icMain._data.instances;
  83. const lastInstance = instances[instances.length - 1];
  84. const lastInstanceElement = icMain._data.elements.filter(e => e.text === lastInstance.text)[0];
  85. icMain.selectElement(e, lastInstanceElement);
  86. }
  87. }, {capture: true});
  88.  
  89. // get the dataset thing they use for scoping css stuff
  90. // TODO add some better handling for if there's zero/multiple dataset attrs on that element in future
  91. const cssScopeDatasetThing = Object.keys(icMain.$el.dataset)[0];
  92.  
  93. function mkElementItem(element) {
  94. return elhelper.create('div', {
  95. classList: ['item'],
  96. dataset: {[cssScopeDatasetThing]: ''},
  97. children: [
  98. elhelper.create('span', {
  99. classList: ['item-emoji'],
  100. dataset: {[cssScopeDatasetThing]: ''},
  101. textContent: element.emoji,
  102. style: {
  103. pointerEvents: 'none',
  104. },
  105. }),
  106. document.createTextNode(` ${element.text} `),
  107. ],
  108. });
  109. }
  110.  
  111. // recipes popup
  112. const recipesListContainer = elhelper.create('div', {
  113. });
  114. function updateRecipesList() {
  115. while(recipesListContainer.firstChild !== null) recipesListContainer.removeChild(recipesListContainer.firstChild);
  116. // build a name -> element map
  117. const byName = {};
  118. for(const element of icMain._data.elements) byName[element.text] = element;
  119. function getByName(name) { return byName[name] ?? {emoji: "❌", text: `[userscript encountered an error trying to look up element '${name}']`}; }
  120. const combos = getCombos();
  121. function listItemClick(evt) {
  122. const elementName = evt.target.dataset.comboviewerElement;
  123. document.querySelector(`[data-comboviewer-section="${CSS.escape(elementName)}"]`).scrollIntoView({block: 'nearest'});
  124. }
  125. function mkLinkedElementItem(element) {
  126. return elhelper.setup(mkElementItem(element), {
  127. events: { click: listItemClick },
  128. dataset: { comboviewerElement: element.text },
  129. });
  130. }
  131. for(const comboResult in combos) {
  132. if(comboResult === 'Nothing') continue;
  133. // anchor for jumping to
  134. recipesListContainer.appendChild(elhelper.create('div', {
  135. dataset: { comboviewerSection: comboResult },
  136. }));
  137. for(const [lhs, rhs] of combos[comboResult]) {
  138. recipesListContainer.appendChild(elhelper.create('div', {
  139. children: [
  140. mkLinkedElementItem(getByName(comboResult)),
  141. document.createTextNode(' = '),
  142. mkLinkedElementItem(getByName(lhs)),
  143. document.createTextNode(' + '),
  144. mkLinkedElementItem(getByName(rhs)),
  145. ],
  146. }));
  147. }
  148. }
  149. }
  150. const recipesDialog = elhelper.create('dialog', {
  151. parent: document.body,
  152. children: [
  153. // close button
  154. elhelper.create('button', {
  155. textContent: 'x',
  156. events: {
  157. click: (evt) => recipesDialog.close(),
  158. },
  159. }),
  160. // the main content
  161. recipesListContainer,
  162. ],
  163. style: {
  164. // need to unset this one thing from the page css
  165. margin: 'auto',
  166. },
  167. });
  168.  
  169. // recipes button
  170. function addControlsButton(label, handler) {
  171. elhelper.create('div', {
  172. parent: document.querySelector('.side-controls'),
  173. textContent: label,
  174. style: {
  175. cursor: 'pointer',
  176. },
  177. events: {
  178. click: handler,
  179. },
  180. });
  181. }
  182. addControlsButton('recipes', () => {
  183. updateRecipesList();
  184. recipesDialog.showModal();
  185. });
  186.  
  187. // first discoveries list (just gonna hijack the recipes popup for simplicity)
  188. addControlsButton('discoveries', () => {
  189. while(recipesListContainer.firstChild !== null) recipesListContainer.removeChild(recipesListContainer.firstChild);
  190. elhelper.setup(recipesListContainer, {
  191. children: icMain._data.elements.filter(e => e.discovered).map(mkElementItem),
  192. });
  193. recipesDialog.showModal();
  194. });
  195. }
  196. // stores the object where most of the infinite craft functions live.
  197. // can be assumed to be set by the time main is called
  198. let icMain = null;
  199. // need to wait for stuff to be actually initialized.
  200. // might be an actual thing we can hook into to detect that
  201. // but for now just waiting until the function we want exists works well enough
  202. (function waitForReady(){
  203. icMain = unsafeWindow?.$nuxt?._route?.matched?.[0]?.instances?.default;
  204. if(icMain !== undefined && icMain !== null) main();
  205. else setTimeout(waitForReady, 10);
  206. })();
  207. })();