IdlePixel+

Idle-Pixel plugin framework

当前为 2022-08-31 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/441206/1088173/IdlePixel%2B.js

  1. // ==UserScript==
  2. // @name IdlePixel+
  3. // @namespace com.anwinity.idlepixel
  4. // @version 1.0.3
  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 original_onmessage = window.websocket.connected_socket.onmessage;
  350. $(function() {
  351. window.websocket.connected_socket.onmessage = function(event) {
  352. original_onmessage.apply(window.websocket.connected_socket, arguments);
  353. self.onMessageReceived(event.data);
  354. }
  355. });
  356.  
  357. /*
  358. const original_open_websocket = window.open_websocket;
  359. window.open_websocket = function() {
  360. original_open_websocket.apply(this, arguments);
  361. const original_onmessage = window.websocket.connected_socket.onmessage;
  362. window.websocket.connected_socket.onmessage = function(event) {
  363. original_onmessage.apply(window.websocket.connected_socket, arguments);
  364. self.onMessageReceived(event.data);
  365. }
  366. }
  367. */
  368.  
  369. // hook into Items.set, which is where var_ values are set
  370. const original_items_set = Items.set;
  371. Items.set = function(key, value) {
  372. let valueBefore = window["var_"+key];
  373. original_items_set.apply(this, arguments);
  374. let valueAfter = window["var_"+key];
  375. self.onVariableSet(key, valueBefore, valueAfter);
  376. }
  377.  
  378. // hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
  379. const original_switch_panels = window.switch_panels;
  380. window.switch_panels = function(id) {
  381. let panelBefore = Globals.currentPanel;
  382. if(panelBefore && panelBefore.startsWith("panel-")) {
  383. panelBefore = panelBefore.substring("panel-".length);
  384. }
  385. self.hideCustomPanels();
  386. original_switch_panels.apply(this, arguments);
  387. let panelAfter = Globals.currentPanel;
  388. if(panelAfter && panelAfter.startsWith("panel-")) {
  389. panelAfter = panelAfter.substring("panel-".length);
  390. }
  391. self.onPanelChanged(panelBefore, panelAfter);
  392. }
  393.  
  394. // create plugin menu item and panel
  395. const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
  396. lastMenuItem.after(`
  397. <div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
  398. <img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
  399. </div>
  400. `);
  401. self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
  402. let content = `
  403. <style>
  404. .idlepixelplus-plugin-box {
  405. display: block;
  406. position: relative;
  407. padding: 0.25em;
  408. color: white;
  409. background-color: rgb(107, 107, 107);
  410. border: 1px solid black;
  411. border-radius: 6px;
  412. margin-bottom: 0.5em;
  413. }
  414. .idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
  415. position: absolute;
  416. right: 2px;
  417. top: 2px;
  418. cursor: pointer;
  419. }
  420. .idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
  421. display: grid;
  422. grid-template-columns: minmax(100px, min-content) 1fr;
  423. row-gap: 0.5em;
  424. column-gap: 0.5em;
  425. white-space: nowrap;
  426. }
  427. </style>
  428. `;
  429. self.forEachPlugin(plugin => {
  430. let id = plugin.id;
  431. let name = "An IdlePixel+ Plugin!";
  432. let description = "";
  433. let author = "unknown";
  434. if(plugin.opts.about) {
  435. let about = plugin.opts.about;
  436. name = about.name || name;
  437. description = about.description || description;
  438. author = about.author || author;
  439. }
  440. content += `
  441. <div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
  442. <strong><u>${name||id}</u></strong> (by ${author})<br />
  443. <span>${description}</span><br />
  444. <div class="idlepixelplus-plugin-config-section" style="display: none">
  445. <hr style="grid-column: span 2">
  446. `;
  447. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  448. plugin.opts.config.forEach(cfg => {
  449. if(CONFIG_TYPES_LABEL.includes(cfg.type)) {
  450. content += `<h5 style="grid-column: span 2; margin-bottom: 0; font-weight: 600">${cfg.label}</h5>`;
  451. }
  452. else if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  453. content += `
  454. <div>
  455. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  456. </div>
  457. <div>
  458. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  459. </div>
  460. `;
  461. }
  462. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  463. content += `
  464. <div>
  465. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  466. </div>
  467. <div>
  468. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  469. </div>
  470. `;
  471. }
  472. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  473. content += `
  474. <div>
  475. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  476. </div>
  477. <div>
  478. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  479. </div>
  480. `;
  481. }
  482. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  483. content += `
  484. <div>
  485. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  486. </div>
  487. <div>
  488. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  489. </div>
  490. `;
  491. }
  492. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  493. content += `
  494. <div>
  495. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  496. </div>
  497. <div>
  498. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="color" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  499. </div>
  500. `;
  501. }
  502. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  503. content += `
  504. <div>
  505. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  506. </div>
  507. <div>
  508. <select id="idlepixelplus-config-${plugin.id}-${cfg.id}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)">
  509. `;
  510. if(cfg.options && Array.isArray(cfg.options)) {
  511. cfg.options.forEach(option => {
  512. if(typeof option === "string") {
  513. content += `<option value="${option}">${option}</option>`;
  514. }
  515. else {
  516. content += `<option value="${option.value}">${option.label || option.value}</option>`;
  517. }
  518. });
  519. }
  520. content += `
  521. </select>
  522. </div>
  523. `;
  524. }
  525. });
  526. content += `
  527. <div style="grid-column: span 2">
  528. <button id="idlepixelplus-configbutton-${plugin.id}-reload" onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
  529. <button id="idlepixelplus-configbutton-${plugin.id}-apply" onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
  530. </div>
  531. `;
  532. }
  533. content += "</div>";
  534. if(plugin.opts.config) {
  535. content += `
  536. <div class="idlepixelplus-plugin-settings-button">
  537. <button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
  538. </div>`;
  539. }
  540. content += "</div>";
  541. });
  542.  
  543. return content;
  544. });
  545.  
  546. logFancy(`(v${self.version}) initialized.`);
  547. }
  548. };
  549.  
  550. class IdlePixelPlus {
  551.  
  552. constructor() {
  553. this.version = GM_info.script.version;
  554. this.plugins = {};
  555. this.panels = {};
  556. this.debug = false;
  557. this.info = INFO;
  558. this.nextUniqueId = 1;
  559. this.customMessageCallbacks = {};
  560.  
  561. if(localStorage.getItem(LOCAL_STORAGE_KEY_DEBUG) == "1") {
  562. this.debug = true;
  563. }
  564. }
  565.  
  566. uniqueId() {
  567. return this.nextUniqueId++;
  568. }
  569.  
  570. setDebug(debug) {
  571. if(debug) {
  572. this.debug = true;
  573. localStorage.setItem(LOCAL_STORAGE_KEY_DEBUG, "1");
  574. }
  575. else {
  576. this.debug = false;
  577. localStorage.removeItem(LOCAL_STORAGE_KEY_DEBUG);
  578. }
  579. }
  580.  
  581. getVar(name, type) {
  582. let s = window[`var_${name}`];
  583. if(type) {
  584. switch(type) {
  585. case "int":
  586. case "integer":
  587. return parseInt(s);
  588. case "number":
  589. case "float":
  590. return parseFloat(s);
  591. case "boolean":
  592. case "bool":
  593. if(s=="true") return true;
  594. if(s=="false") return false;
  595. return undefined;
  596. }
  597. }
  598. return s;
  599. }
  600.  
  601. getVarOrDefault(name, defaultValue, type) {
  602. let s = window[`var_${name}`];
  603. if(s==null || typeof s === "undefined") {
  604. return defaultValue;
  605. }
  606. if(type) {
  607. let value;
  608. switch(type) {
  609. case "int":
  610. case "integer":
  611. value = parseInt(s);
  612. return isNaN(value) ? defaultValue : value;
  613. case "number":
  614. case "float":
  615. value = parseFloat(s);
  616. return isNaN(value) ? defaultValue : value;
  617. case "boolean":
  618. case "bool":
  619. if(s=="true") return true;
  620. if(s=="false") return false;
  621. return defaultValue;
  622. }
  623. }
  624. return s;
  625. }
  626.  
  627. setPluginConfigUIDirty(id, dirty) {
  628. if(typeof id !== "string" || typeof dirty !== "boolean") {
  629. throw new TypeError("IdlePixelPlus.setPluginConfigUIDirty takes the following arguments: (id:string, dirty:boolean)");
  630. }
  631. const plugin = this.plugins[id];
  632. const button = $(`#idlepixelplus-configbutton-${plugin.id}-apply`);
  633. if(button) {
  634. button.prop("disabled", !(dirty));
  635. }
  636. }
  637.  
  638. loadPluginConfigs(id) {
  639. if(typeof id !== "string") {
  640. throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
  641. }
  642. const plugin = this.plugins[id];
  643. const config = {};
  644. let stored;
  645. try {
  646. stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
  647. }
  648. catch(err) {
  649. console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
  650. stored = {};
  651. }
  652. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  653. plugin.opts.config.forEach(cfg => {
  654. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  655. let value = stored[cfg.id];
  656. if(value==null || typeof value === "undefined") {
  657. value = cfg.default;
  658. }
  659. config[cfg.id] = value;
  660.  
  661. if(el) {
  662. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
  663. el.prop("checked", value);
  664. }
  665. else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
  666. el.val(value);
  667. }
  668. else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
  669. el.val(value);
  670. }
  671. else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
  672. el.val(value);
  673. }
  674. else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
  675. el.val(value);
  676. }
  677. else if(CONFIG_TYPES_COLOR.includes(cfg.type) && typeof value === "string") {
  678. el.val(value);
  679. }
  680. }
  681. });
  682. }
  683. plugin.config = config;
  684. this.setPluginConfigUIDirty(id, false);
  685. if(typeof plugin.onConfigsChanged === "function") {
  686. plugin.onConfigsChanged();
  687. }
  688. }
  689.  
  690. savePluginConfigs(id) {
  691. if(typeof id !== "string") {
  692. throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
  693. }
  694. const plugin = this.plugins[id];
  695. const config = {};
  696. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  697. plugin.opts.config.forEach(cfg => {
  698. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  699. let value;
  700. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  701. config[cfg.id] = el.is(":checked");
  702. }
  703. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  704. config[cfg.id] = parseInt(el.val());
  705. }
  706. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  707. config[cfg.id] = parseFloat(el.val());
  708. }
  709. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  710. config[cfg.id] = el.val();
  711. }
  712. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  713. config[cfg.id] = el.val();
  714. }
  715. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  716. config[cfg.id] = el.val();
  717. }
  718. });
  719. }
  720. plugin.config = config;
  721. localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
  722. this.setPluginConfigUIDirty(id, false);
  723. if(typeof plugin.onConfigsChanged === "function") {
  724. plugin.onConfigsChanged();
  725. }
  726. }
  727.  
  728. addPanel(id, title, content) {
  729. if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
  730. throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
  731. }
  732. const panels = $("#panels");
  733. panels.append(`
  734. <div id="panel-${id}" style="display: none">
  735. <h1>${title}</h1>
  736. <hr>
  737. <div class="idlepixelplus-panel-content"></div>
  738. </div>
  739. `);
  740. this.panels[id] = {
  741. id: id,
  742. title: title,
  743. content: content
  744. };
  745. this.refreshPanel(id);
  746. }
  747.  
  748. refreshPanel(id) {
  749. if(typeof id !== "string") {
  750. throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
  751. }
  752. const panel = this.panels[id];
  753. if(!panel) {
  754. throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
  755. }
  756. let content = panel.content;
  757. if(!["string", "function"].includes(typeof content)) {
  758. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  759. }
  760. if(typeof content === "function") {
  761. content = content();
  762. if(typeof content !== "string") {
  763. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  764. }
  765. }
  766. const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
  767. panelContent.html(content);
  768. if(id === "idlepixelplus") {
  769. this.forEachPlugin(plugin => {
  770. this.loadPluginConfigs(plugin.id);
  771. });
  772. }
  773. }
  774.  
  775. registerPlugin(plugin) {
  776. if(!(plugin instanceof IdlePixelPlusPlugin)) {
  777. throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
  778. }
  779. if(plugin.id in this.plugins) {
  780. throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
  781. }
  782.  
  783. this.plugins[plugin.id] = plugin;
  784. this.loadPluginConfigs(plugin.id);
  785. let versionString = plugin.opts&&plugin.opts.about&&plugin.opts.about.version ? ` (v${plugin.opts.about.version})` : "";
  786. logFancy(`registered plugin "${plugin.id}"${versionString}`);
  787. }
  788.  
  789. forEachPlugin(f) {
  790. if(typeof f !== "function") {
  791. throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
  792. }
  793. Object.values(this.plugins).forEach(plugin => {
  794. try {
  795. f(plugin);
  796. }
  797. catch(err) {
  798. console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
  799. console.error(err);
  800. }
  801. });
  802. }
  803.  
  804. setPanel(panel) {
  805. if(typeof panel !== "string") {
  806. throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
  807. }
  808. window.switch_panels(`panel-${panel}`);
  809. }
  810.  
  811. sendMessage(message) {
  812. if(typeof message !== "string") {
  813. throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
  814. }
  815. if(window.websocket && window.websocket.connected_socket && window.websocket.connected_socket.readyState==1) {
  816. window.websocket.connected_socket.send(message);
  817. }
  818. }
  819.  
  820. showToast(title, content) {
  821. show_toast(title, content);
  822. }
  823.  
  824. hideCustomPanels() {
  825. Object.values(this.panels).forEach((panel) => {
  826. const el = $(`#panel-${panel.id}`);
  827. if(el) {
  828. el.css("display", "none");
  829. }
  830. });
  831. }
  832.  
  833. onMessageReceived(data) {
  834. if(this.debug) {
  835. console.log(`IP+ onMessageReceived: ${data}`);
  836. }
  837. if(data) {
  838. this.forEachPlugin((plugin) => {
  839. if(typeof plugin.onMessageReceived === "function") {
  840. plugin.onMessageReceived(data);
  841. }
  842. });
  843. if(data.startsWith("VALID_LOGIN")) {
  844. this.onLogin();
  845. }
  846. else if(data.startsWith("CHAT=")) {
  847. const split = data.substring("CHAT=".length).split("~");
  848. const chatData = {
  849. username: split[0],
  850. sigil: split[1],
  851. tag: split[2],
  852. level: parseInt(split[3]),
  853. message: split[4]
  854. };
  855. this.onChat(chatData);
  856. // CHAT=anwinity~none~none~1565~test
  857. }
  858. else if(data.startsWith("CUSTOM=")) {
  859. const customData = data.substring("CUSTOM=".length);
  860. const tilde = customData.indexOf("~");
  861. if(tilde > 0) {
  862. const fromPlayer = customData.substring(0, tilde);
  863. const content = customData.substring(tilde+1);
  864. this.onCustomMessageReceived(fromPlayer, content);
  865. }
  866. }
  867. }
  868. }
  869.  
  870. deleteCustomMessageCallback(callbackId) {
  871. if(this.debug) {
  872. console.log(`IP+ deleteCustomMessageCallback`, callbackId);
  873. }
  874. delete this.customMessageCallbacks[callbackId];
  875. }
  876.  
  877. requestPluginManifest(player, callback, pluginId) {
  878. if(typeof pluginId === "string") {
  879. pluginId = [pluginId];
  880. }
  881. if(Array.isArray(pluginId)) {
  882. pluginId = JSON.stringify(pluginId);
  883. }
  884. this.sendCustomMessage(player, {
  885. content: "PLUGIN_MANIFEST" + (pluginId ? `:${pluginId}` : ''),
  886. onResponse: function(respPlayer, content) {
  887. if(typeof callback === "function") {
  888. callback(respPlayer, JSON.parse(content));
  889. }
  890. else {
  891. console.log(`Plugin Manifest: ${respPlayer}`, content);
  892. }
  893. },
  894. onOffline: function(respPlayer, content) {
  895. if(typeof callback === "function") {
  896. callback(respPlayer, false);
  897. }
  898. },
  899. timeout: 10000
  900. });
  901. }
  902.  
  903. sendCustomMessage(toPlayer, opts) {
  904. if(this.debug) {
  905. console.log(`IP+ sendCustomMessage`, toPlayer, opts);
  906. }
  907. const reply = !!(opts.callbackId);
  908. const content = typeof opts.content === "string" ? opts.content : JSON.stringify(opts.content);
  909. const callbackId = reply ? opts.callbackId : this.uniqueId();
  910. const responseHandler = typeof opts.onResponse === "function" ? opts.onResponse : null;
  911. const offlineHandler = opts.onOffline===true ? () => { this.deleteCustomMessageCallback(callbackId); } : (typeof opts.onOffline === "function" ? opts.onOffline : null);
  912. const timeout = typeof opts.timeout === "number" ? opts.timeout : -1;
  913.  
  914. if(responseHandler || offlineHandler) {
  915. const handler = {
  916. id: callbackId,
  917. player: toPlayer,
  918. responseHandler: responseHandler,
  919. offlineHandler: offlineHandler,
  920. timeout: typeof timeout === "number" ? timeout : -1,
  921. };
  922. if(callbackId) {
  923. this.customMessageCallbacks[callbackId] = handler;
  924. if(handler.timeout > 0) {
  925. setTimeout(() => {
  926. this.deleteCustomMessageCallback(callbackId);
  927. }, handler.timeout);
  928. }
  929. }
  930. }
  931. const message = `CUSTOM=${toPlayer}~IPP${reply?'R':''}${callbackId}:${content}`;
  932. if(message.length > 255) {
  933. console.warn("The resulting websocket message from IdlePixelPlus.sendCustomMessage has a length limit of 255 characters. Recipients may not receive the full message!");
  934. }
  935. this.sendMessage(message);
  936. }
  937.  
  938. onCustomMessageReceived(fromPlayer, content) {
  939. if(this.debug) {
  940. console.log(`IP+ onCustomMessageReceived`, fromPlayer, content);
  941. }
  942. const offline = content == "PLAYER_OFFLINE";
  943. let callbackId = null;
  944. let originalCallbackId = null;
  945. let reply = false;
  946. const ippMatcher = content.match(/^IPP(\w+):/);
  947. if(ippMatcher) {
  948. originalCallbackId = callbackId = ippMatcher[1];
  949. let colon = content.indexOf(":");
  950. content = content.substring(colon+1);
  951. if(callbackId.startsWith("R")) {
  952. callbackId = callbackId.substring(1);
  953. reply = true;
  954. }
  955. }
  956.  
  957. // special built-in messages
  958. if(content.startsWith("PLUGIN_MANIFEST")) {
  959. const manifest = {};
  960. let filterPluginIds = null;
  961. if(content.includes(":")) {
  962. content = content.substring("PLUGIN_MANIFEST:".length);
  963. filterPluginIds = JSON.parse(content).map(s => s.replace("~", ""));
  964. }
  965. this.forEachPlugin(plugin => {
  966. let id = plugin.id.replace("~", "");
  967. if(filterPluginIds && !filterPluginIds.includes(id)) {
  968. return;
  969. }
  970. let version = "unknown";
  971. if(plugin.opts && plugin.opts.about && plugin.opts.about.version) {
  972. version = plugin.opts.about.version.replace("~", "");
  973. }
  974. manifest[id] = version;
  975. });
  976. this.sendCustomMessage(fromPlayer, {
  977. content: manifest,
  978. callbackId: callbackId
  979. });
  980. return;
  981. }
  982.  
  983. const callbacks = this.customMessageCallbacks;
  984. if(reply) {
  985. const handler = callbacks[callbackId];
  986. if(handler && typeof handler.responseHandler === "function") {
  987. try {
  988. if(handler.responseHandler(fromPlayer, content, originalCallbackId)) {
  989. this.deleteCustomMessageCallback(callbackId);
  990. }
  991. }
  992. catch(err) {
  993. console.error("Error executing custom message response handler.", {player: fromPlayer, content: content, handler: handler});
  994. }
  995. }
  996. }
  997. else if(offline) {
  998. Object.values(callbacks).forEach(handler => {
  999. try {
  1000. if(handler.player.toLowerCase()==fromPlayer.toLowerCase() && typeof handler.offlineHandler === "function" && handler.offlineHandler(fromPlayer, content)) {
  1001. this.deleteCustomMessageCallback(handler.id);
  1002. }
  1003. }
  1004. catch(err) {
  1005. console.error("Error executing custom message offline handler.", {player: fromPlayer, content: content, handler: handler});
  1006. }
  1007. });
  1008. }
  1009.  
  1010. if(offline) {
  1011. this.onCustomMessagePlayerOffline(fromPlayer, content);
  1012. }
  1013. else {
  1014. this.forEachPlugin((plugin) => {
  1015. if(typeof plugin.onCustomMessageReceived === "function") {
  1016. plugin.onCustomMessageReceived(fromPlayer, content, originalCallbackId);
  1017. }
  1018. });
  1019. }
  1020. }
  1021.  
  1022. onCustomMessagePlayerOffline(fromPlayer, content) {
  1023. if(this.debug) {
  1024. console.log(`IP+ onCustomMessagePlayerOffline`, fromPlayer, content);
  1025. }
  1026. this.forEachPlugin((plugin) => {
  1027. if(typeof plugin.onCustomMessagePlayerOffline === "function") {
  1028. plugin.onCustomMessagePlayerOffline(fromPlayer, content);
  1029. }
  1030. });
  1031. }
  1032.  
  1033. onCombatStart() {
  1034. if(this.debug) {
  1035. console.log(`IP+ onCombatStart`);
  1036. }
  1037. this.forEachPlugin((plugin) => {
  1038. if(typeof plugin.onCombatStart === "function") {
  1039. plugin.onCombatStart();
  1040. }
  1041. });
  1042. }
  1043.  
  1044. onCombatEnd() {
  1045. if(this.debug) {
  1046. console.log(`IP+ onCombatEnd`);
  1047. }
  1048. this.forEachPlugin((plugin) => {
  1049. if(typeof plugin.onCombatEnd === "function") {
  1050. plugin.onCombatEnd();
  1051. }
  1052. });
  1053. }
  1054.  
  1055. onLogin() {
  1056. if(this.debug) {
  1057. console.log(`IP+ onLogin`);
  1058. }
  1059. logFancy("login detected");
  1060. this.forEachPlugin((plugin) => {
  1061. if(typeof plugin.onLogin === "function") {
  1062. plugin.onLogin();
  1063. }
  1064. });
  1065. }
  1066.  
  1067. onVariableSet(key, valueBefore, valueAfter) {
  1068. if(this.debug) {
  1069. console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
  1070. }
  1071. this.forEachPlugin((plugin) => {
  1072. if(typeof plugin.onVariableSet === "function") {
  1073. plugin.onVariableSet(key, valueBefore, valueAfter);
  1074. }
  1075. });
  1076. if(key == "monster_name") {
  1077. const combatBefore = !!(valueBefore && valueBefore!="none");
  1078. const combatAfter = !!(valueAfter && valueAfter!="none");
  1079. if(!combatBefore && combatAfter) {
  1080. this.onCombatStart();
  1081. }
  1082. else if(combatBefore && !combatAfter) {
  1083. this.onCombatEnd();
  1084. }
  1085. }
  1086. }
  1087.  
  1088. onChat(data) {
  1089. if(this.debug) {
  1090. console.log(`IP+ onChat`, data);
  1091. }
  1092. this.forEachPlugin((plugin) => {
  1093. if(typeof plugin.onChat === "function") {
  1094. plugin.onChat(data);
  1095. }
  1096. });
  1097. }
  1098.  
  1099. onPanelChanged(panelBefore, panelAfter) {
  1100. if(this.debug) {
  1101. console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
  1102. }
  1103. if(panelAfter === "idlepixelplus") {
  1104. this.refreshPanel("idlepixelplus");
  1105. }
  1106. this.forEachPlugin((plugin) => {
  1107. if(typeof plugin.onPanelChanged === "function") {
  1108. plugin.onPanelChanged(panelBefore, panelAfter);
  1109. }
  1110. });
  1111. }
  1112.  
  1113. }
  1114.  
  1115. // Add to window and init
  1116. window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
  1117. window.IdlePixelPlus = new IdlePixelPlus();
  1118. internal.init.call(window.IdlePixelPlus);
  1119.  
  1120. })();