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.2.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) { // 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. }
  80. }, {capture: true});
  81.  
  82. // get the dataset thing they use for scoping css stuff
  83. // TODO add some better handling for if there's zero/multiple dataset attrs on that element in future
  84. const cssScopeDatasetThing = Object.keys(icMain.$el.dataset)[0];
  85.  
  86. function mkElementItem(element) {
  87. return elhelper.create('div', {
  88. classList: ['item'],
  89. dataset: {[cssScopeDatasetThing]: ''},
  90. children: [
  91. elhelper.create('span', {
  92. classList: ['item-emoji'],
  93. dataset: {[cssScopeDatasetThing]: ''},
  94. textContent: element.emoji,
  95. style: {
  96. pointerEvents: 'none',
  97. },
  98. }),
  99. document.createTextNode(` ${element.text} `),
  100. ],
  101. });
  102. }
  103.  
  104. // recipes popup
  105. const recipesListContainer = elhelper.create('div', {
  106. });
  107. function updateRecipesList() {
  108. while(recipesListContainer.firstChild !== null) recipesListContainer.removeChild(recipesListContainer.firstChild);
  109. // build a name -> element map
  110. const byName = {};
  111. for(const element of icMain._data.elements) byName[element.text] = element;
  112. function getByName(name) { return byName[name] ?? {emoji: "❌", text: `[userscript encountered an error trying to look up element '${name}']`}; }
  113. const combos = getCombos();
  114. function listItemClick(evt) {
  115. const elementName = evt.target.dataset.comboviewerElement;
  116. document.querySelector(`[data-comboviewer-section="${CSS.escape(elementName)}"]`).scrollIntoView({block: 'nearest'});
  117. }
  118. function mkLinkedElementItem(element) {
  119. return elhelper.setup(mkElementItem(element), {
  120. events: { click: listItemClick },
  121. dataset: { comboviewerElement: element.text },
  122. });
  123. }
  124. for(const comboResult in combos) {
  125. if(comboResult === 'Nothing') continue;
  126. // anchor for jumping to
  127. recipesListContainer.appendChild(elhelper.create('div', {
  128. dataset: { comboviewerSection: comboResult },
  129. }));
  130. for(const [lhs, rhs] of combos[comboResult]) {
  131. recipesListContainer.appendChild(elhelper.create('div', {
  132. children: [
  133. mkLinkedElementItem(getByName(comboResult)),
  134. document.createTextNode(' = '),
  135. mkLinkedElementItem(getByName(lhs)),
  136. document.createTextNode(' + '),
  137. mkLinkedElementItem(getByName(rhs)),
  138. ],
  139. }));
  140. }
  141. }
  142. }
  143. const recipesDialog = elhelper.create('dialog', {
  144. parent: document.body,
  145. children: [
  146. // close button
  147. elhelper.create('button', {
  148. textContent: 'x',
  149. events: {
  150. click: (evt) => recipesDialog.close(),
  151. },
  152. }),
  153. // the main content
  154. recipesListContainer,
  155. ],
  156. style: {
  157. // need to unset this one thing from the page css
  158. margin: 'auto',
  159. },
  160. });
  161.  
  162. // recipes button
  163. function addControlsButton(label, handler) {
  164. elhelper.create('div', {
  165. parent: document.querySelector('.side-controls'),
  166. textContent: label,
  167. style: {
  168. cursor: 'pointer',
  169. },
  170. events: {
  171. click: handler,
  172. },
  173. });
  174. }
  175. addControlsButton('recipes', () => {
  176. updateRecipesList();
  177. recipesDialog.showModal();
  178. });
  179.  
  180. // first discoveries list (just gonna hijack the recipes popup for simplicity)
  181. addControlsButton('discoveries', () => {
  182. while(recipesListContainer.firstChild !== null) recipesListContainer.removeChild(recipesListContainer.firstChild);
  183. elhelper.setup(recipesListContainer, {
  184. children: icMain._data.elements.filter(e => e.discovered).map(mkElementItem),
  185. });
  186. recipesDialog.showModal();
  187. });
  188. }
  189. // stores the object where most of the infinite craft functions live.
  190. // can be assumed to be set by the time main is called
  191. let icMain = null;
  192. // need to wait for stuff to be actually initialized.
  193. // might be an actual thing we can hook into to detect that
  194. // but for now just waiting until the function we want exists works well enough
  195. (function waitForReady(){
  196. icMain = unsafeWindow?.$nuxt?._route?.matched?.[0]?.instances?.default;
  197. if(icMain !== undefined && icMain !== null) main();
  198. else setTimeout(waitForReady, 10);
  199. })();
  200. })();