IdlePixel+

Idle-Pixel plugin framework

目前為 2022-03-20 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/441206/1030265/IdlePixel%2B.js

  1. // ==UserScript==
  2. // @name IdlePixel+
  3. // @namespace com.anwinity.idlepixel
  4. // @version 0.0.6
  5. // @description Idle-Pixel plugin framework
  6. // @author Anwinity
  7. // @match https://idle-pixel.com/play.php*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const CONFIG_TYPES_LABEL = ["label"];
  15. const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
  16. const CONFIG_TYPES_INTEGER = ["integer", "int"];
  17. const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
  18. const CONFIG_TYPES_STRING = ["string", "text"];
  19. const CONFIG_TYPES_SELECT = ["select"];
  20.  
  21. const INFO = {
  22. ores: ["stone", "copper", "iron", "silver", "gold"],
  23. smeltableOres: ["copper", "iron", "silver", "gold"],
  24. smeltOilCost: {
  25. copper: 1,
  26. iron: 5,
  27. silver: 20,
  28. gold: 100
  29. },
  30. bars: ["bronze_bar", "iron_bar", "silver_bar", "gold_bar"],
  31. seeds: ["dotted_green_leaf_seeds", "green_leaf_seeds", "lime_leaf_seeds", "red_mushroom_seeds"],
  32. combatZones: ["field","forest","cave"],
  33. combatZoneBaseEnergyCost: {
  34. field: 50,
  35. forest: 200,
  36. cave: 500
  37. },
  38. spellManaCost: {
  39. heal: 2
  40. }
  41. };
  42.  
  43. if(window.IdlePixelPlus) {
  44. console.error(`Another version of IdlePixelPlus (v${window.IdlePixelPlus.version}) is already loaded. This may cause conflicts for some plugins.`);
  45. return;
  46. }
  47.  
  48. class IdlePixelPlusPlugin {
  49.  
  50. constructor(id, opts) {
  51. if(typeof id !== "string") {
  52. throw new TypeError("IdlePixelPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
  53. }
  54. this.id = id;
  55. this.opts = opts || {};
  56. this.config = null;
  57. }
  58.  
  59. getConfig(name) {
  60. if(!this.config) {
  61. IdlePixelPlus.loadPluginConfigs(this.id);
  62. }
  63. if(this.config) {
  64. return this.config[name];
  65. }
  66. }
  67.  
  68. /*
  69. onConfigsChanged() { }
  70. onLogin() { }
  71. onMessageReceived(data) { }
  72. onVariableSet(key, valueBefore, valueAfter) { }
  73. onChat(data) { }
  74. onPanelChanged(panelBefore, panelAfter) { }
  75. onCombatStart() { }
  76. onCombatEnd() { }
  77. */
  78.  
  79. }
  80.  
  81. const internal = {
  82. init() {
  83. const self = this;
  84.  
  85. // hook into websocket messages
  86. const original_open_websocket = window.open_websocket;
  87. window.open_websocket = function() {
  88. original_open_websocket.apply(this, arguments);
  89. const original_onmessage = window.websocket.websocket.onmessage;
  90. window.websocket.websocket.onmessage = function(event) {
  91. original_onmessage.apply(window.websocket.websocket, arguments);
  92. self.onMessageReceived(event.data);
  93. }
  94. }
  95.  
  96. // hook into Items.set, which is where var_ values are set
  97. const original_items_set = Items.set;
  98. Items.set = function(key, value) {
  99. let valueBefore = window["var_"+key];
  100. original_items_set.apply(this, arguments);
  101. let valueAfter = window["var_"+key];
  102. self.onVariableSet(key, valueBefore, valueAfter);
  103. }
  104.  
  105. // hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
  106. const original_switch_panels = window.switch_panels;
  107. window.switch_panels = function(id) {
  108. let panelBefore = Globals.currentPanel;
  109. if(panelBefore && panelBefore.startsWith("panel-")) {
  110. panelBefore = panelBefore.substring("panel-".length);
  111. }
  112. self.hideCustomPanels();
  113. original_switch_panels.apply(this, arguments);
  114. let panelAfter = Globals.currentPanel;
  115. if(panelAfter && panelAfter.startsWith("panel-")) {
  116. panelAfter = panelAfter.substring("panel-".length);
  117. }
  118. self.onPanelChanged(panelBefore, panelAfter);
  119. }
  120.  
  121. // create plugin menu item and panel
  122. const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
  123. lastMenuItem.after(`
  124. <div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
  125. <img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
  126. </div>
  127. `);
  128. self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
  129. let content = `
  130. <style>
  131. .idlepixelplus-plugin-box {
  132. display: block;
  133. position: relative;
  134. padding: 0.25em;
  135. color: white;
  136. background-color: rgb(107, 107, 107);
  137. border: 1px solid black;
  138. border-radius: 6px;
  139. margin-bottom: 0.5em;
  140. }
  141. .idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
  142. position: absolute;
  143. right: 2px;
  144. top: 2px;
  145. cursor: pointer;
  146. }
  147. .idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
  148. display: grid;
  149. grid-template-columns: minmax(100px, min-content) 1fr;
  150. row-gap: 0.5em;
  151. column-gap: 0.5em;
  152. white-space: nowrap;
  153. }
  154. </style>
  155. `;
  156. self.forEachPlugin(plugin => {
  157. let id = plugin.id;
  158. let name = "An IdlePixel+ Plugin!";
  159. let description = "";
  160. let author = "unknown";
  161. if(plugin.opts.about) {
  162. let about = plugin.opts.about;
  163. name = about.name || name;
  164. description = about.description || description;
  165. author = about.author || author;
  166. }
  167. content += `
  168. <div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
  169. <strong><u>${name||id}</u></strong> (by ${author})<br />
  170. <span>${description}</span><br />
  171. <div class="idlepixelplus-plugin-config-section" style="display: none">
  172. <hr style="grid-column: span 2">
  173. `;
  174. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  175. plugin.opts.config.forEach(cfg => {
  176. if(CONFIG_TYPES_LABEL.includes(cfg.type)) {
  177. content += `<h5 style="grid-column: span 2; margin-bottom: 0; font-weight: 600">${cfg.label}</h5>`;
  178. }
  179. else if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  180. content += `
  181. <div>
  182. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  183. </div>
  184. <div>
  185. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  186. </div>
  187. `;
  188. }
  189. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  190. content += `
  191. <div>
  192. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  193. </div>
  194. <div>
  195. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  196. </div>
  197. `;
  198. }
  199. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  200. content += `
  201. <div>
  202. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  203. </div>
  204. <div>
  205. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  206. </div>
  207. `;
  208. }
  209. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  210. content += `
  211. <div>
  212. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  213. </div>
  214. <div>
  215. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  216. </div>
  217. `;
  218. }
  219. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  220. content += `
  221. <div>
  222. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  223. </div>
  224. <div>
  225. <select id="idlepixelplus-config-${plugin.id}-${cfg.id}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)">
  226. `;
  227. if(cfg.options && Array.isArray(cfg.options)) {
  228. cfg.options.forEach(option => {
  229. if(typeof option === "string") {
  230. content += `<option value="${option}">${option}</option>`;
  231. }
  232. else {
  233. content += `<option value="${option.value}">${option.label || option.value}</option>`;
  234. }
  235. });
  236. }
  237. content += `
  238. </select>
  239. </div>
  240. `;
  241. }
  242. });
  243. content += `
  244. <div style="grid-column: span 2">
  245. <button id="idlepixelplus-configbutton-${plugin.id}-reload" onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
  246. <button id="idlepixelplus-configbutton-${plugin.id}-apply" onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
  247. </div>
  248. `;
  249. }
  250. content += "</div>";
  251. if(plugin.opts.config) {
  252. content += `
  253. <div class="idlepixelplus-plugin-settings-button">
  254. <button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
  255. </div>`;
  256. }
  257. content += "</div>";
  258. });
  259.  
  260. return content;
  261. });
  262.  
  263. console.log(`IdlePixelPlus (v${self.version}) initialized.`);
  264. }
  265. };
  266.  
  267. class IdlePixelPlus {
  268.  
  269. constructor() {
  270. this.version = GM_info.script.version;
  271. this.plugins = {};
  272. this.panels = {};
  273. this.debug = false;
  274. this.info = INFO;
  275. }
  276.  
  277. getVar(name, type) {
  278. let s = window[`var_${name}`];
  279. if(type) {
  280. switch(type) {
  281. case "int":
  282. case "integer":
  283. return parseInt(s);
  284. case "number":
  285. case "float":
  286. return parseFloat(s);
  287. case "boolean":
  288. case "bool":
  289. if(s=="true") return true;
  290. if(s=="false") return false;
  291. return undefined;
  292. }
  293. }
  294. return s;
  295. }
  296.  
  297. getVarOrDefault(name, defaultValue, type) {
  298. let s = window[`var_${name}`];
  299. if(s==null || typeof s === "undefined") {
  300. return defaultValue;
  301. }
  302. if(type) {
  303. let value;
  304. switch(type) {
  305. case "int":
  306. case "integer":
  307. value = parseInt(s);
  308. return isNaN(value) ? defaultValue : value;
  309. case "number":
  310. case "float":
  311. value = parseFloat(s);
  312. return isNaN(value) ? defaultValue : value;
  313. case "boolean":
  314. case "bool":
  315. if(s=="true") return true;
  316. if(s=="false") return false;
  317. return defaultValue;
  318. }
  319. }
  320. return s;
  321. }
  322.  
  323. setPluginConfigUIDirty(id, dirty) {
  324. if(typeof id !== "string" || typeof dirty !== "boolean") {
  325. throw new TypeError("IdlePixelPlus.setPluginConfigUIDirty takes the following arguments: (id:string, dirty:boolean)");
  326. }
  327. const plugin = this.plugins[id];
  328. const button = $(`#idlepixelplus-configbutton-${plugin.id}-apply`);
  329. if(button) {
  330. button.prop("disabled", !(dirty));
  331. }
  332. }
  333.  
  334. loadPluginConfigs(id) {
  335. if(typeof id !== "string") {
  336. throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
  337. }
  338. const plugin = this.plugins[id];
  339. const config = {};
  340. let stored;
  341. try {
  342. stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
  343. }
  344. catch(err) {
  345. console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
  346. stored = {};
  347. }
  348. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  349. plugin.opts.config.forEach(cfg => {
  350. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  351. let value = stored[cfg.id];
  352. if(value==null || typeof value === "undefined") {
  353. value = cfg.default;
  354. }
  355. config[cfg.id] = value;
  356.  
  357. if(el) {
  358. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
  359. el.prop("checked", value);
  360. }
  361. else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
  362. el.val(value);
  363. }
  364. else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
  365. el.val(value);
  366. }
  367. else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
  368. el.val(value);
  369. }
  370. else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
  371. el.val(value);
  372. }
  373. }
  374. });
  375. }
  376. plugin.config = config;
  377. this.setPluginConfigUIDirty(id, false);
  378. if(typeof plugin.onConfigsChanged === "function") {
  379. plugin.onConfigsChanged();
  380. }
  381. }
  382.  
  383. savePluginConfigs(id) {
  384. if(typeof id !== "string") {
  385. throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
  386. }
  387. const plugin = this.plugins[id];
  388. const config = {};
  389. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  390. plugin.opts.config.forEach(cfg => {
  391. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  392. let value;
  393. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  394. config[cfg.id] = el.is(":checked");
  395. }
  396. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  397. config[cfg.id] = parseInt(el.val());
  398. }
  399. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  400. config[cfg.id] = parseFloat(el.val());
  401. }
  402. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  403. config[cfg.id] = el.val();
  404. }
  405. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  406. config[cfg.id] = el.val();
  407. }
  408. });
  409. }
  410. plugin.config = config;
  411. localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
  412. this.setPluginConfigUIDirty(id, false);
  413. if(typeof plugin.onConfigsChanged === "function") {
  414. plugin.onConfigsChanged();
  415. }
  416. }
  417.  
  418. addPanel(id, title, content) {
  419. if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
  420. throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
  421. }
  422. const panels = $("#panels");
  423. panels.append(`
  424. <div id="panel-${id}" style="display: none">
  425. <h1>${title}</h1>
  426. <hr>
  427. <div class="idlepixelplus-panel-content"></div>
  428. </div>
  429. `);
  430. this.panels[id] = {
  431. id: id,
  432. title: title,
  433. content: content
  434. };
  435. this.refreshPanel(id);
  436. }
  437.  
  438. refreshPanel(id) {
  439. if(typeof id !== "string") {
  440. throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
  441. }
  442. const panel = this.panels[id];
  443. if(!panel) {
  444. throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
  445. }
  446. let content = panel.content;
  447. if(!["string", "function"].includes(typeof content)) {
  448. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  449. }
  450. if(typeof content === "function") {
  451. content = content();
  452. if(typeof content !== "string") {
  453. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  454. }
  455. }
  456. const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
  457. panelContent.html(content);
  458. if(id === "idlepixelplus") {
  459. this.forEachPlugin(plugin => {
  460. this.loadPluginConfigs(plugin.id);
  461. });
  462. }
  463. }
  464.  
  465. registerPlugin(plugin) {
  466. if(!(plugin instanceof IdlePixelPlusPlugin)) {
  467. throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
  468. }
  469. if(plugin.id in this.plugins) {
  470. throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
  471. }
  472.  
  473. // TODO: easy config system
  474. // TODO: custom panels
  475.  
  476. this.plugins[plugin.id] = plugin;
  477. this.loadPluginConfigs(plugin.id);
  478. console.log(`IdlePixelPlus registered plugin "${plugin.id}"`);
  479. }
  480.  
  481. forEachPlugin(f) {
  482. if(typeof f !== "function") {
  483. throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
  484. }
  485. Object.values(this.plugins).forEach(plugin => {
  486. try {
  487. f(plugin);
  488. }
  489. catch(err) {
  490. console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
  491. console.error(err);
  492. }
  493. });
  494. }
  495.  
  496. setPanel(panel) {
  497. if(typeof panel !== "string") {
  498. throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
  499. }
  500. window.switch_panels(`panel-${panel}`);
  501. }
  502.  
  503. sendMessage(message) {
  504. if(typeof message !== "string") {
  505. throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
  506. }
  507. if(window.websocket && window.websocket.websocket && window.websocket.websocket.readyState==1) {
  508. window.websocket.websocket.send(message);
  509. }
  510. }
  511.  
  512. showToast(title, content) {
  513. show_toast(title, content);
  514. }
  515.  
  516. hideCustomPanels() {
  517. Object.values(this.panels).forEach((panel) => {
  518. const el = $(`#panel-${panel.id}`);
  519. if(el) {
  520. el.css("display", "none");
  521. }
  522. });
  523. }
  524.  
  525. onMessageReceived(data) {
  526. if(this.debug) {
  527. console.log(`IP+ onMessageReceived: ${data}`);
  528. }
  529. if(data) {
  530. this.forEachPlugin((plugin) => {
  531. if(typeof plugin.onMessageReceived === "function") {
  532. plugin.onMessageReceived(data);
  533. }
  534. });
  535. if(data.startsWith("VALID_LOGIN")) {
  536. this.onLogin();
  537. }
  538. else if(data.startsWith("CHAT=")) {
  539. const split = data.substring("CHAT=".length).split("~");
  540. const chatData = {
  541. username: split[0],
  542. tag: null,
  543. sigil: null,
  544. level: split[3],
  545. message: split[4]
  546. };
  547. this.onChat(chatData);
  548. // CHAT=anwinity~none~none~1565~test
  549. // TODO: none and none, probably for tag and sigil
  550. }
  551. }
  552. }
  553.  
  554. onCombatStart() {
  555. if(this.debug) {
  556. console.log(`IP+ onCombatStart`);
  557. }
  558. this.forEachPlugin((plugin) => {
  559. if(typeof plugin.onCombatStart === "function") {
  560. plugin.onCombatStart();
  561. }
  562. });
  563. }
  564.  
  565. onCombatEnd() {
  566. if(this.debug) {
  567. console.log(`IP+ onCombatEnd`);
  568. }
  569. this.forEachPlugin((plugin) => {
  570. if(typeof plugin.onCombatEnd === "function") {
  571. plugin.onCombatEnd();
  572. }
  573. });
  574. }
  575.  
  576. onLogin() {
  577. if(this.debug) {
  578. console.log(`IP+ onLogin`);
  579. }
  580. this.forEachPlugin((plugin) => {
  581. if(typeof plugin.onLogin === "function") {
  582. plugin.onLogin();
  583. }
  584. });
  585. }
  586.  
  587. onVariableSet(key, valueBefore, valueAfter) {
  588. if(this.debug) {
  589. console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
  590. }
  591. this.forEachPlugin((plugin) => {
  592. if(typeof plugin.onVariableSet === "function") {
  593. plugin.onVariableSet(key, valueBefore, valueAfter);
  594. }
  595. });
  596. if(key == "monster_name") {
  597. const combatBefore = !!(valueBefore && valueBefore!="none");
  598. const combatAfter = !!(valueAfter && valueAfter!="none");
  599. if(!combatBefore && combatAfter) {
  600. this.onCombatStart();
  601. }
  602. else if(combatBefore && !combatAfter) {
  603. this.onCombatEnd();
  604. }
  605. }
  606. }
  607.  
  608. onChat(data) {
  609. if(this.debug) {
  610. console.log(`IP+ onChat`, data);
  611. }
  612. this.forEachPlugin((plugin) => {
  613. if(typeof plugin.onChat === "function") {
  614. plugin.onChat(data);
  615. }
  616. });
  617. }
  618.  
  619. onPanelChanged(panelBefore, panelAfter) {
  620. if(this.debug) {
  621. console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
  622. }
  623. if(panelAfter === "idlepixelplus") {
  624. this.refreshPanel("idlepixelplus");
  625. }
  626. this.forEachPlugin((plugin) => {
  627. if(typeof plugin.onPanelChanged === "function") {
  628. plugin.onPanelChanged(panelBefore, panelAfter);
  629. }
  630. });
  631. }
  632.  
  633. }
  634.  
  635. // Add to window and init
  636. window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
  637. window.IdlePixelPlus = new IdlePixelPlus();
  638. internal.init.call(window.IdlePixelPlus);
  639.  
  640. })();