IdlePixel+

Idle-Pixel plugin framework

目前為 2022-08-31 提交的版本,檢視 最新版本

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

  1. // ==UserScript==
  2. // @name IdlePixel+
  3. // @namespace com.anwinity.idlepixel
  4. // @version 1.0.4
  5. // @description Idle-Pixel plugin framework
  6. // @author Anwinity
  7. // @match *://idle-pixel.com/login/play*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. if(window.IdlePixelPlus) {
  15. // already loaded
  16. return;
  17. }
  18.  
  19. const LOCAL_STORAGE_KEY_DEBUG = "IdlePixelPlus:debug";
  20.  
  21. const CONFIG_TYPES_LABEL = ["label"];
  22. const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
  23. const CONFIG_TYPES_INTEGER = ["integer", "int"];
  24. const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
  25. const CONFIG_TYPES_STRING = ["string", "text"];
  26. const CONFIG_TYPES_SELECT = ["select"];
  27. const CONFIG_TYPES_COLOR = ["color"];
  28.  
  29. // Oh, I know this is disgusting. Sue me. Actually, sue Smitty. :)
  30. function createCombatZoneObjects() {
  31. const fallback = {
  32. field: {
  33. id: "field",
  34. commonMonsters: [
  35. "Chickens",
  36. "Rats",
  37. "Spiders"
  38. ],
  39. rareMonsters: [
  40. "Lizards",
  41. "Bees"
  42. ],
  43. energyCost: 50,
  44. fightPointCost: 300
  45. },
  46. forest: {
  47. id: "forest",
  48. commonMonsters: [
  49. "Snakes",
  50. "Ants",
  51. "Wolves"
  52. ],
  53. rareMonsters: [
  54. "Ents",
  55. "Thief"
  56. ],
  57. energyCost: 200,
  58. fightPointCost: 600
  59. },
  60. cave: {
  61. id: "cave",
  62. commonMonsters: [
  63. "Bears",
  64. "Goblins",
  65. "Bats"
  66. ],
  67. rareMonsters: [
  68. "Skeletons"
  69. ],
  70. energyCost: 500,
  71. fightPointCost: 900
  72. },
  73. volcano: {
  74. id: "volcano",
  75. commonMonsters: [
  76. "Fire Hawk",
  77. "Fire Snake",
  78. "Fire Golem"
  79. ],
  80. rareMonsters: [
  81. "Fire Witch"
  82. ],
  83. energyCost: 1000,
  84. fightPointCost: 1500
  85. },
  86. northern_field: {
  87. id: "northern_field",
  88. commonMonsters: [
  89. "Ice Hawk",
  90. "Ice Witch",
  91. "Golem"
  92. ],
  93. rareMonsters: [
  94. "Yeti"
  95. ],
  96. energyCost: 3000,
  97. fightPointCost: 2000
  98. }
  99. };
  100. try {
  101. const code = Combat._modal_load_area_data.toString().split(/\r?\n/g);
  102. const zones = {};
  103. let foundSwitch = false;
  104. let endSwitch = false;
  105. let current = null;
  106. code.forEach(line => {
  107. if(endSwitch) {
  108. return;
  109. }
  110. if(!foundSwitch) {
  111. if(line.includes("switch(area)")) {
  112. foundSwitch = true;
  113. }
  114. }
  115. else {
  116. line = line.trim();
  117. if(foundSwitch && !endSwitch && !current && line=='}') {
  118. endSwitch = true;
  119. }
  120. else if(/case /.test(line)) {
  121. // start of zone data
  122. let zoneId = line.replace(/^case\s+"/, "").replace(/":.*$/, "");
  123. current = zones[zoneId] = {id: zoneId};
  124. }
  125. else if(line.startsWith("break;")) {
  126. // end of zone data
  127. current = null;
  128. }
  129. else if(current) {
  130. if(line.startsWith("common_monsters_array")) {
  131. current.commonMonsters = line
  132. .replace("common_monsters_array = [", "")
  133. .replace("];", "")
  134. .split(/\s*,\s*/g)
  135. .map(s => s.substring(1, s.length-1));
  136. }
  137. else if(line.startsWith("rare_monsters_array")) {
  138. current.rareMonsters = line
  139. .replace("rare_monsters_array = [", "")
  140. .replace("];", "")
  141. .split(/\s*,\s*/g)
  142. .map(s => s.substring(1, s.length-1));
  143. }
  144. else if(line.startsWith("energy")) {
  145. current.energyCost = parseInt(line.match(/\d+/)[0]);
  146. }
  147. else if(line.startsWith("fightpoints")) {
  148. current.fightPointCost = parseInt(line.match(/\d+/)[0]);
  149. }
  150. }
  151. }
  152. });
  153. if(!zones || !Object.keys(zones).length) {
  154. console.error("IdlePixelPlus: Could not parse combat zone data, using fallback.");
  155. return fallback;
  156. }
  157. return zones;
  158. }
  159. catch(err) {
  160. console.error("IdlePixelPlus: Could not parse combat zone data, using fallback.", err);
  161. return fallback;
  162. }
  163. }
  164.  
  165. function createOreObjects() {
  166. const ores = {
  167. stone: { smeltable:false, bar: null },
  168. copper: { smeltable:true, bar: "bronze_bar" },
  169. iron: { smeltable:true, bar: "iron_bar" },
  170. silver: { smeltable:true, bar: "silver_bar" },
  171. gold: { smeltable:true, bar: "gold_bar" },
  172. promethium: { smeltable:true, bar: "promethium_bar" }
  173. };
  174. try {
  175. Object.keys(ores).forEach(id => {
  176. const obj = ores[id];
  177. obj.id = id;
  178. obj.oil = Crafting.getOilPerBar(id);
  179. obj.charcoal = Crafting.getCharcoalPerBar(id);
  180. });
  181. }
  182. catch(err) {
  183. console.error("IdlePixelPlus: Could not create ore data. This could adversely affect related functionality.", err);
  184. }
  185. return ores;
  186. }
  187.  
  188. function createSeedObjects() {
  189. // hardcoded for now.
  190. return {
  191. dotted_green_leaf_seeds: {
  192. id: "dotted_green_leaf_seeds",
  193. level: 1,
  194. stopsDying: 15,
  195. time: 15,
  196. bonemealCost: 0
  197. },
  198. stardust_seeds: {
  199. id: "stardust_seeds",
  200. level: 8,
  201. stopsDying: 0,
  202. time: 20,
  203. bonemealCost: 0
  204. },
  205. green_leaf_seeds: {
  206. id: "green_leaf_seeds",
  207. level: 10,
  208. stopsDying: 25,
  209. time: 30,
  210. bonemealCost: 0
  211. },
  212. lime_leaf_seeds: {
  213. id: "lime_leaf_seeds",
  214. level: 25,
  215. stopsDying: 40,
  216. time: 1*60,
  217. bonemealCost: 1
  218. },
  219. gold_leaf_seeds: {
  220. id: "gold_leaf_seeds",
  221. level: 50,
  222. stopsDying: 60,
  223. time: 2*60,
  224. bonemealCost: 10
  225. },
  226. crystal_leaf_seeds: {
  227. id: "crystal_leaf_seeds",
  228. level: 70,
  229. stopsDying: 80,
  230. time: 5*60,
  231. bonemealCost: 25
  232. },
  233. red_mushroom_seeds: {
  234. id: "red_mushroom_seeds",
  235. level: 1,
  236. stopsDying: 0,
  237. time: 5,
  238. bonemealCost: 0
  239. },
  240. tree_seeds: {
  241. id: "tree_seeds",
  242. level: 10,
  243. stopsDying: 25,
  244. time: 5*60,
  245. bonemealCost: 10
  246. },
  247. oak_tree_seeds: {
  248. id: "oak_tree_seeds",
  249. level: 25,
  250. stopsDying: 40,
  251. time: 4*60,
  252. bonemealCost: 25
  253. },
  254. willow_tree_seeds: {
  255. id: "willow_tree_seeds",
  256. level: 37,
  257. stopsDying: 55,
  258. time: 8*60,
  259. bonemealCost: 50
  260. },
  261. maple_tree_seeds: {
  262. id: "maple_tree_seeds",
  263. level: 50,
  264. stopsDying: 65,
  265. time: 12*60,
  266. bonemealCost: 120
  267. },
  268. stardust_tree_seeds: {
  269. id: "stardust_tree_seeds",
  270. level: 65,
  271. stopsDying: 80,
  272. time: 15*60,
  273. bonemealCost: 150
  274. },
  275. pine_tree_seeds: {
  276. id: "pine_tree_seeds",
  277. level: 70,
  278. stopsDying: 85,
  279. time: 17*60,
  280. bonemealCost: 180
  281. }
  282. };
  283. }
  284.  
  285. function createSpellObjects() {
  286. const spells = {};
  287. Object.keys(Magic.spell_info).forEach(id => {
  288. const info = Magic.spell_info[id];
  289. spells[id] = {
  290. id: id,
  291. manaCost: info.mana_cost,
  292. magicBonusRequired: info.magic_bonus
  293. };
  294. });
  295. return spells;
  296. }
  297.  
  298. const INFO = {
  299. ores: createOreObjects(),
  300. seeds: createSeedObjects(),
  301. combatZones: createCombatZoneObjects(),
  302. spells: createSpellObjects()
  303. };
  304.  
  305. function logFancy(s) {
  306. console.log("%cIdlePixelPlus: %c"+s, "color: #00f7ff; font-weight: bold; font-size: 12pt;", "color: black; font-weight: normal; font-size: 10pt;");
  307. }
  308.  
  309. class IdlePixelPlusPlugin {
  310.  
  311. constructor(id, opts) {
  312. if(typeof id !== "string") {
  313. throw new TypeError("IdlePixelPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
  314. }
  315. this.id = id;
  316. this.opts = opts || {};
  317. this.config = null;
  318. }
  319.  
  320. getConfig(name) {
  321. if(!this.config) {
  322. IdlePixelPlus.loadPluginConfigs(this.id);
  323. }
  324. if(this.config) {
  325. return this.config[name];
  326. }
  327. }
  328.  
  329. /*
  330. onConfigsChanged() { }
  331. onLogin() { }
  332. onMessageReceived(data) { }
  333. onVariableSet(key, valueBefore, valueAfter) { }
  334. onChat(data) { }
  335. onPanelChanged(panelBefore, panelAfter) { }
  336. onCombatStart() { }
  337. onCombatEnd() { }
  338. onCustomMessageReceived(player, content, callbackId) { }
  339. onCustomMessagePlayerOffline(player, content) { }
  340. */
  341.  
  342. }
  343.  
  344. const internal = {
  345. init() {
  346. const self = this;
  347.  
  348. // hook into websocket messages
  349. const hookIntoOnMessage = () => {
  350. try {
  351. const original_onmessage = window.websocket.connected_socket.onmessage;
  352. if(typeof original_onmessage === "function") {
  353. window.websocket.connected_socket.onmessage = function(event) {
  354. original_onmessage.apply(window.websocket.connected_socket, arguments);
  355. self.onMessageReceived(event.data);
  356. }
  357. return true;
  358. }
  359. else {
  360. return false;
  361. }
  362. }
  363. catch(err) {
  364. console.error("Had trouble hooking into websocket...");
  365. return false;
  366. }
  367. };
  368. $(function() {
  369. if(!hookIntoOnMessage()) {
  370. // try once more
  371. setTimeout(hookIntoOnMessage, 40);
  372. }
  373. });
  374.  
  375. /*
  376. const original_open_websocket = window.open_websocket;
  377. window.open_websocket = function() {
  378. original_open_websocket.apply(this, arguments);
  379. const original_onmessage = window.websocket.connected_socket.onmessage;
  380. window.websocket.connected_socket.onmessage = function(event) {
  381. original_onmessage.apply(window.websocket.connected_socket, arguments);
  382. self.onMessageReceived(event.data);
  383. }
  384. }
  385. */
  386.  
  387. // hook into Items.set, which is where var_ values are set
  388. const original_items_set = Items.set;
  389. Items.set = function(key, value) {
  390. let valueBefore = window["var_"+key];
  391. original_items_set.apply(this, arguments);
  392. let valueAfter = window["var_"+key];
  393. self.onVariableSet(key, valueBefore, valueAfter);
  394. }
  395.  
  396. // hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
  397. const original_switch_panels = window.switch_panels;
  398. window.switch_panels = function(id) {
  399. let panelBefore = Globals.currentPanel;
  400. if(panelBefore && panelBefore.startsWith("panel-")) {
  401. panelBefore = panelBefore.substring("panel-".length);
  402. }
  403. self.hideCustomPanels();
  404. original_switch_panels.apply(this, arguments);
  405. let panelAfter = Globals.currentPanel;
  406. if(panelAfter && panelAfter.startsWith("panel-")) {
  407. panelAfter = panelAfter.substring("panel-".length);
  408. }
  409. self.onPanelChanged(panelBefore, panelAfter);
  410. }
  411.  
  412. // create plugin menu item and panel
  413. const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
  414. lastMenuItem.after(`
  415. <div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
  416. <img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
  417. </div>
  418. `);
  419. self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
  420. let content = `
  421. <style>
  422. .idlepixelplus-plugin-box {
  423. display: block;
  424. position: relative;
  425. padding: 0.25em;
  426. color: white;
  427. background-color: rgb(107, 107, 107);
  428. border: 1px solid black;
  429. border-radius: 6px;
  430. margin-bottom: 0.5em;
  431. }
  432. .idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
  433. position: absolute;
  434. right: 2px;
  435. top: 2px;
  436. cursor: pointer;
  437. }
  438. .idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
  439. display: grid;
  440. grid-template-columns: minmax(100px, min-content) 1fr;
  441. row-gap: 0.5em;
  442. column-gap: 0.5em;
  443. white-space: nowrap;
  444. }
  445. </style>
  446. `;
  447. self.forEachPlugin(plugin => {
  448. let id = plugin.id;
  449. let name = "An IdlePixel+ Plugin!";
  450. let description = "";
  451. let author = "unknown";
  452. if(plugin.opts.about) {
  453. let about = plugin.opts.about;
  454. name = about.name || name;
  455. description = about.description || description;
  456. author = about.author || author;
  457. }
  458. content += `
  459. <div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
  460. <strong><u>${name||id}</u></strong> (by ${author})<br />
  461. <span>${description}</span><br />
  462. <div class="idlepixelplus-plugin-config-section" style="display: none">
  463. <hr style="grid-column: span 2">
  464. `;
  465. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  466. plugin.opts.config.forEach(cfg => {
  467. if(CONFIG_TYPES_LABEL.includes(cfg.type)) {
  468. content += `<h5 style="grid-column: span 2; margin-bottom: 0; font-weight: 600">${cfg.label}</h5>`;
  469. }
  470. else if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  471. content += `
  472. <div>
  473. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  474. </div>
  475. <div>
  476. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  477. </div>
  478. `;
  479. }
  480. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  481. content += `
  482. <div>
  483. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  484. </div>
  485. <div>
  486. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  487. </div>
  488. `;
  489. }
  490. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  491. content += `
  492. <div>
  493. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  494. </div>
  495. <div>
  496. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  497. </div>
  498. `;
  499. }
  500. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  501. content += `
  502. <div>
  503. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  504. </div>
  505. <div>
  506. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  507. </div>
  508. `;
  509. }
  510. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  511. content += `
  512. <div>
  513. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  514. </div>
  515. <div>
  516. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="color" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  517. </div>
  518. `;
  519. }
  520. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  521. content += `
  522. <div>
  523. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  524. </div>
  525. <div>
  526. <select id="idlepixelplus-config-${plugin.id}-${cfg.id}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)">
  527. `;
  528. if(cfg.options && Array.isArray(cfg.options)) {
  529. cfg.options.forEach(option => {
  530. if(typeof option === "string") {
  531. content += `<option value="${option}">${option}</option>`;
  532. }
  533. else {
  534. content += `<option value="${option.value}">${option.label || option.value}</option>`;
  535. }
  536. });
  537. }
  538. content += `
  539. </select>
  540. </div>
  541. `;
  542. }
  543. });
  544. content += `
  545. <div style="grid-column: span 2">
  546. <button id="idlepixelplus-configbutton-${plugin.id}-reload" onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
  547. <button id="idlepixelplus-configbutton-${plugin.id}-apply" onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
  548. </div>
  549. `;
  550. }
  551. content += "</div>";
  552. if(plugin.opts.config) {
  553. content += `
  554. <div class="idlepixelplus-plugin-settings-button">
  555. <button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
  556. </div>`;
  557. }
  558. content += "</div>";
  559. });
  560.  
  561. return content;
  562. });
  563.  
  564. logFancy(`(v${self.version}) initialized.`);
  565. }
  566. };
  567.  
  568. class IdlePixelPlus {
  569.  
  570. constructor() {
  571. this.version = GM_info.script.version;
  572. this.plugins = {};
  573. this.panels = {};
  574. this.debug = false;
  575. this.info = INFO;
  576. this.nextUniqueId = 1;
  577. this.customMessageCallbacks = {};
  578.  
  579. if(localStorage.getItem(LOCAL_STORAGE_KEY_DEBUG) == "1") {
  580. this.debug = true;
  581. }
  582. }
  583.  
  584. uniqueId() {
  585. return this.nextUniqueId++;
  586. }
  587.  
  588. setDebug(debug) {
  589. if(debug) {
  590. this.debug = true;
  591. localStorage.setItem(LOCAL_STORAGE_KEY_DEBUG, "1");
  592. }
  593. else {
  594. this.debug = false;
  595. localStorage.removeItem(LOCAL_STORAGE_KEY_DEBUG);
  596. }
  597. }
  598.  
  599. getVar(name, type) {
  600. let s = window[`var_${name}`];
  601. if(type) {
  602. switch(type) {
  603. case "int":
  604. case "integer":
  605. return parseInt(s);
  606. case "number":
  607. case "float":
  608. return parseFloat(s);
  609. case "boolean":
  610. case "bool":
  611. if(s=="true") return true;
  612. if(s=="false") return false;
  613. return undefined;
  614. }
  615. }
  616. return s;
  617. }
  618.  
  619. getVarOrDefault(name, defaultValue, type) {
  620. let s = window[`var_${name}`];
  621. if(s==null || typeof s === "undefined") {
  622. return defaultValue;
  623. }
  624. if(type) {
  625. let value;
  626. switch(type) {
  627. case "int":
  628. case "integer":
  629. value = parseInt(s);
  630. return isNaN(value) ? defaultValue : value;
  631. case "number":
  632. case "float":
  633. value = parseFloat(s);
  634. return isNaN(value) ? defaultValue : value;
  635. case "boolean":
  636. case "bool":
  637. if(s=="true") return true;
  638. if(s=="false") return false;
  639. return defaultValue;
  640. }
  641. }
  642. return s;
  643. }
  644.  
  645. setPluginConfigUIDirty(id, dirty) {
  646. if(typeof id !== "string" || typeof dirty !== "boolean") {
  647. throw new TypeError("IdlePixelPlus.setPluginConfigUIDirty takes the following arguments: (id:string, dirty:boolean)");
  648. }
  649. const plugin = this.plugins[id];
  650. const button = $(`#idlepixelplus-configbutton-${plugin.id}-apply`);
  651. if(button) {
  652. button.prop("disabled", !(dirty));
  653. }
  654. }
  655.  
  656. loadPluginConfigs(id) {
  657. if(typeof id !== "string") {
  658. throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
  659. }
  660. const plugin = this.plugins[id];
  661. const config = {};
  662. let stored;
  663. try {
  664. stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
  665. }
  666. catch(err) {
  667. console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
  668. stored = {};
  669. }
  670. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  671. plugin.opts.config.forEach(cfg => {
  672. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  673. let value = stored[cfg.id];
  674. if(value==null || typeof value === "undefined") {
  675. value = cfg.default;
  676. }
  677. config[cfg.id] = value;
  678.  
  679. if(el) {
  680. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
  681. el.prop("checked", value);
  682. }
  683. else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
  684. el.val(value);
  685. }
  686. else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
  687. el.val(value);
  688. }
  689. else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
  690. el.val(value);
  691. }
  692. else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
  693. el.val(value);
  694. }
  695. else if(CONFIG_TYPES_COLOR.includes(cfg.type) && typeof value === "string") {
  696. el.val(value);
  697. }
  698. }
  699. });
  700. }
  701. plugin.config = config;
  702. this.setPluginConfigUIDirty(id, false);
  703. if(typeof plugin.onConfigsChanged === "function") {
  704. plugin.onConfigsChanged();
  705. }
  706. }
  707.  
  708. savePluginConfigs(id) {
  709. if(typeof id !== "string") {
  710. throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
  711. }
  712. const plugin = this.plugins[id];
  713. const config = {};
  714. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  715. plugin.opts.config.forEach(cfg => {
  716. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  717. let value;
  718. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  719. config[cfg.id] = el.is(":checked");
  720. }
  721. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  722. config[cfg.id] = parseInt(el.val());
  723. }
  724. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  725. config[cfg.id] = parseFloat(el.val());
  726. }
  727. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  728. config[cfg.id] = el.val();
  729. }
  730. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  731. config[cfg.id] = el.val();
  732. }
  733. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  734. config[cfg.id] = el.val();
  735. }
  736. });
  737. }
  738. plugin.config = config;
  739. localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
  740. this.setPluginConfigUIDirty(id, false);
  741. if(typeof plugin.onConfigsChanged === "function") {
  742. plugin.onConfigsChanged();
  743. }
  744. }
  745.  
  746. addPanel(id, title, content) {
  747. if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
  748. throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
  749. }
  750. const panels = $("#panels");
  751. panels.append(`
  752. <div id="panel-${id}" style="display: none">
  753. <h1>${title}</h1>
  754. <hr>
  755. <div class="idlepixelplus-panel-content"></div>
  756. </div>
  757. `);
  758. this.panels[id] = {
  759. id: id,
  760. title: title,
  761. content: content
  762. };
  763. this.refreshPanel(id);
  764. }
  765.  
  766. refreshPanel(id) {
  767. if(typeof id !== "string") {
  768. throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
  769. }
  770. const panel = this.panels[id];
  771. if(!panel) {
  772. throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
  773. }
  774. let content = panel.content;
  775. if(!["string", "function"].includes(typeof content)) {
  776. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  777. }
  778. if(typeof content === "function") {
  779. content = content();
  780. if(typeof content !== "string") {
  781. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  782. }
  783. }
  784. const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
  785. panelContent.html(content);
  786. if(id === "idlepixelplus") {
  787. this.forEachPlugin(plugin => {
  788. this.loadPluginConfigs(plugin.id);
  789. });
  790. }
  791. }
  792.  
  793. registerPlugin(plugin) {
  794. if(!(plugin instanceof IdlePixelPlusPlugin)) {
  795. throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
  796. }
  797. if(plugin.id in this.plugins) {
  798. throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
  799. }
  800.  
  801. this.plugins[plugin.id] = plugin;
  802. this.loadPluginConfigs(plugin.id);
  803. let versionString = plugin.opts&&plugin.opts.about&&plugin.opts.about.version ? ` (v${plugin.opts.about.version})` : "";
  804. logFancy(`registered plugin "${plugin.id}"${versionString}`);
  805. }
  806.  
  807. forEachPlugin(f) {
  808. if(typeof f !== "function") {
  809. throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
  810. }
  811. Object.values(this.plugins).forEach(plugin => {
  812. try {
  813. f(plugin);
  814. }
  815. catch(err) {
  816. console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
  817. console.error(err);
  818. }
  819. });
  820. }
  821.  
  822. setPanel(panel) {
  823. if(typeof panel !== "string") {
  824. throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
  825. }
  826. window.switch_panels(`panel-${panel}`);
  827. }
  828.  
  829. sendMessage(message) {
  830. if(typeof message !== "string") {
  831. throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
  832. }
  833. if(window.websocket && window.websocket.connected_socket && window.websocket.connected_socket.readyState==1) {
  834. window.websocket.connected_socket.send(message);
  835. }
  836. }
  837.  
  838. showToast(title, content) {
  839. show_toast(title, content);
  840. }
  841.  
  842. hideCustomPanels() {
  843. Object.values(this.panels).forEach((panel) => {
  844. const el = $(`#panel-${panel.id}`);
  845. if(el) {
  846. el.css("display", "none");
  847. }
  848. });
  849. }
  850.  
  851. onMessageReceived(data) {
  852. if(this.debug) {
  853. console.log(`IP+ onMessageReceived: ${data}`);
  854. }
  855. if(data) {
  856. this.forEachPlugin((plugin) => {
  857. if(typeof plugin.onMessageReceived === "function") {
  858. plugin.onMessageReceived(data);
  859. }
  860. });
  861. if(data.startsWith("VALID_LOGIN")) {
  862. this.onLogin();
  863. }
  864. else if(data.startsWith("CHAT=")) {
  865. const split = data.substring("CHAT=".length).split("~");
  866. const chatData = {
  867. username: split[0],
  868. sigil: split[1],
  869. tag: split[2],
  870. level: parseInt(split[3]),
  871. message: split[4]
  872. };
  873. this.onChat(chatData);
  874. // CHAT=anwinity~none~none~1565~test
  875. }
  876. else if(data.startsWith("CUSTOM=")) {
  877. const customData = data.substring("CUSTOM=".length);
  878. const tilde = customData.indexOf("~");
  879. if(tilde > 0) {
  880. const fromPlayer = customData.substring(0, tilde);
  881. const content = customData.substring(tilde+1);
  882. this.onCustomMessageReceived(fromPlayer, content);
  883. }
  884. }
  885. }
  886. }
  887.  
  888. deleteCustomMessageCallback(callbackId) {
  889. if(this.debug) {
  890. console.log(`IP+ deleteCustomMessageCallback`, callbackId);
  891. }
  892. delete this.customMessageCallbacks[callbackId];
  893. }
  894.  
  895. requestPluginManifest(player, callback, pluginId) {
  896. if(typeof pluginId === "string") {
  897. pluginId = [pluginId];
  898. }
  899. if(Array.isArray(pluginId)) {
  900. pluginId = JSON.stringify(pluginId);
  901. }
  902. this.sendCustomMessage(player, {
  903. content: "PLUGIN_MANIFEST" + (pluginId ? `:${pluginId}` : ''),
  904. onResponse: function(respPlayer, content) {
  905. if(typeof callback === "function") {
  906. callback(respPlayer, JSON.parse(content));
  907. }
  908. else {
  909. console.log(`Plugin Manifest: ${respPlayer}`, content);
  910. }
  911. },
  912. onOffline: function(respPlayer, content) {
  913. if(typeof callback === "function") {
  914. callback(respPlayer, false);
  915. }
  916. },
  917. timeout: 10000
  918. });
  919. }
  920.  
  921. sendCustomMessage(toPlayer, opts) {
  922. if(this.debug) {
  923. console.log(`IP+ sendCustomMessage`, toPlayer, opts);
  924. }
  925. const reply = !!(opts.callbackId);
  926. const content = typeof opts.content === "string" ? opts.content : JSON.stringify(opts.content);
  927. const callbackId = reply ? opts.callbackId : this.uniqueId();
  928. const responseHandler = typeof opts.onResponse === "function" ? opts.onResponse : null;
  929. const offlineHandler = opts.onOffline===true ? () => { this.deleteCustomMessageCallback(callbackId); } : (typeof opts.onOffline === "function" ? opts.onOffline : null);
  930. const timeout = typeof opts.timeout === "number" ? opts.timeout : -1;
  931.  
  932. if(responseHandler || offlineHandler) {
  933. const handler = {
  934. id: callbackId,
  935. player: toPlayer,
  936. responseHandler: responseHandler,
  937. offlineHandler: offlineHandler,
  938. timeout: typeof timeout === "number" ? timeout : -1,
  939. };
  940. if(callbackId) {
  941. this.customMessageCallbacks[callbackId] = handler;
  942. if(handler.timeout > 0) {
  943. setTimeout(() => {
  944. this.deleteCustomMessageCallback(callbackId);
  945. }, handler.timeout);
  946. }
  947. }
  948. }
  949. const message = `CUSTOM=${toPlayer}~IPP${reply?'R':''}${callbackId}:${content}`;
  950. if(message.length > 255) {
  951. console.warn("The resulting websocket message from IdlePixelPlus.sendCustomMessage has a length limit of 255 characters. Recipients may not receive the full message!");
  952. }
  953. this.sendMessage(message);
  954. }
  955.  
  956. onCustomMessageReceived(fromPlayer, content) {
  957. if(this.debug) {
  958. console.log(`IP+ onCustomMessageReceived`, fromPlayer, content);
  959. }
  960. const offline = content == "PLAYER_OFFLINE";
  961. let callbackId = null;
  962. let originalCallbackId = null;
  963. let reply = false;
  964. const ippMatcher = content.match(/^IPP(\w+):/);
  965. if(ippMatcher) {
  966. originalCallbackId = callbackId = ippMatcher[1];
  967. let colon = content.indexOf(":");
  968. content = content.substring(colon+1);
  969. if(callbackId.startsWith("R")) {
  970. callbackId = callbackId.substring(1);
  971. reply = true;
  972. }
  973. }
  974.  
  975. // special built-in messages
  976. if(content.startsWith("PLUGIN_MANIFEST")) {
  977. const manifest = {};
  978. let filterPluginIds = null;
  979. if(content.includes(":")) {
  980. content = content.substring("PLUGIN_MANIFEST:".length);
  981. filterPluginIds = JSON.parse(content).map(s => s.replace("~", ""));
  982. }
  983. this.forEachPlugin(plugin => {
  984. let id = plugin.id.replace("~", "");
  985. if(filterPluginIds && !filterPluginIds.includes(id)) {
  986. return;
  987. }
  988. let version = "unknown";
  989. if(plugin.opts && plugin.opts.about && plugin.opts.about.version) {
  990. version = plugin.opts.about.version.replace("~", "");
  991. }
  992. manifest[id] = version;
  993. });
  994. this.sendCustomMessage(fromPlayer, {
  995. content: manifest,
  996. callbackId: callbackId
  997. });
  998. return;
  999. }
  1000.  
  1001. const callbacks = this.customMessageCallbacks;
  1002. if(reply) {
  1003. const handler = callbacks[callbackId];
  1004. if(handler && typeof handler.responseHandler === "function") {
  1005. try {
  1006. if(handler.responseHandler(fromPlayer, content, originalCallbackId)) {
  1007. this.deleteCustomMessageCallback(callbackId);
  1008. }
  1009. }
  1010. catch(err) {
  1011. console.error("Error executing custom message response handler.", {player: fromPlayer, content: content, handler: handler});
  1012. }
  1013. }
  1014. }
  1015. else if(offline) {
  1016. Object.values(callbacks).forEach(handler => {
  1017. try {
  1018. if(handler.player.toLowerCase()==fromPlayer.toLowerCase() && typeof handler.offlineHandler === "function" && handler.offlineHandler(fromPlayer, content)) {
  1019. this.deleteCustomMessageCallback(handler.id);
  1020. }
  1021. }
  1022. catch(err) {
  1023. console.error("Error executing custom message offline handler.", {player: fromPlayer, content: content, handler: handler});
  1024. }
  1025. });
  1026. }
  1027.  
  1028. if(offline) {
  1029. this.onCustomMessagePlayerOffline(fromPlayer, content);
  1030. }
  1031. else {
  1032. this.forEachPlugin((plugin) => {
  1033. if(typeof plugin.onCustomMessageReceived === "function") {
  1034. plugin.onCustomMessageReceived(fromPlayer, content, originalCallbackId);
  1035. }
  1036. });
  1037. }
  1038. }
  1039.  
  1040. onCustomMessagePlayerOffline(fromPlayer, content) {
  1041. if(this.debug) {
  1042. console.log(`IP+ onCustomMessagePlayerOffline`, fromPlayer, content);
  1043. }
  1044. this.forEachPlugin((plugin) => {
  1045. if(typeof plugin.onCustomMessagePlayerOffline === "function") {
  1046. plugin.onCustomMessagePlayerOffline(fromPlayer, content);
  1047. }
  1048. });
  1049. }
  1050.  
  1051. onCombatStart() {
  1052. if(this.debug) {
  1053. console.log(`IP+ onCombatStart`);
  1054. }
  1055. this.forEachPlugin((plugin) => {
  1056. if(typeof plugin.onCombatStart === "function") {
  1057. plugin.onCombatStart();
  1058. }
  1059. });
  1060. }
  1061.  
  1062. onCombatEnd() {
  1063. if(this.debug) {
  1064. console.log(`IP+ onCombatEnd`);
  1065. }
  1066. this.forEachPlugin((plugin) => {
  1067. if(typeof plugin.onCombatEnd === "function") {
  1068. plugin.onCombatEnd();
  1069. }
  1070. });
  1071. }
  1072.  
  1073. onLogin() {
  1074. if(this.debug) {
  1075. console.log(`IP+ onLogin`);
  1076. }
  1077. logFancy("login detected");
  1078. this.forEachPlugin((plugin) => {
  1079. if(typeof plugin.onLogin === "function") {
  1080. plugin.onLogin();
  1081. }
  1082. });
  1083. }
  1084.  
  1085. onVariableSet(key, valueBefore, valueAfter) {
  1086. if(this.debug) {
  1087. console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
  1088. }
  1089. this.forEachPlugin((plugin) => {
  1090. if(typeof plugin.onVariableSet === "function") {
  1091. plugin.onVariableSet(key, valueBefore, valueAfter);
  1092. }
  1093. });
  1094. if(key == "monster_name") {
  1095. const combatBefore = !!(valueBefore && valueBefore!="none");
  1096. const combatAfter = !!(valueAfter && valueAfter!="none");
  1097. if(!combatBefore && combatAfter) {
  1098. this.onCombatStart();
  1099. }
  1100. else if(combatBefore && !combatAfter) {
  1101. this.onCombatEnd();
  1102. }
  1103. }
  1104. }
  1105.  
  1106. onChat(data) {
  1107. if(this.debug) {
  1108. console.log(`IP+ onChat`, data);
  1109. }
  1110. this.forEachPlugin((plugin) => {
  1111. if(typeof plugin.onChat === "function") {
  1112. plugin.onChat(data);
  1113. }
  1114. });
  1115. }
  1116.  
  1117. onPanelChanged(panelBefore, panelAfter) {
  1118. if(this.debug) {
  1119. console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
  1120. }
  1121. if(panelAfter === "idlepixelplus") {
  1122. this.refreshPanel("idlepixelplus");
  1123. }
  1124. this.forEachPlugin((plugin) => {
  1125. if(typeof plugin.onPanelChanged === "function") {
  1126. plugin.onPanelChanged(panelBefore, panelAfter);
  1127. }
  1128. });
  1129. }
  1130.  
  1131. }
  1132.  
  1133. // Add to window and init
  1134. window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
  1135. window.IdlePixelPlus = new IdlePixelPlus();
  1136. internal.init.call(window.IdlePixelPlus);
  1137.  
  1138. })();