IdlePixel+

Idle-Pixel plugin framework

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

  1. // ==UserScript==
  2. // @name IdlePixel+
  3. // @namespace com.anwinity.idlepixel
  4. // @version 1.2.2
  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. const VERSION = "1.2.2";
  15.  
  16. if(window.IdlePixelPlus) {
  17. // already loaded
  18. return;
  19. }
  20.  
  21. const LOCAL_STORAGE_KEY_DEBUG = "IdlePixelPlus:debug";
  22.  
  23. const CONFIG_TYPES_LABEL = ["label"];
  24. const CONFIG_TYPES_BOOLEAN = ["boolean", "bool", "checkbox"];
  25. const CONFIG_TYPES_INTEGER = ["integer", "int"];
  26. const CONFIG_TYPES_FLOAT = ["number", "num", "float"];
  27. const CONFIG_TYPES_STRING = ["string", "text"];
  28. const CONFIG_TYPES_SELECT = ["select"];
  29. const CONFIG_TYPES_COLOR = ["color"];
  30.  
  31. const CHAT_COMMAND_NO_OVERRIDE = ["help", "mute", "ban", "pm"];
  32.  
  33. function createCombatZoneObjects() {
  34. const fallback = {
  35. field: {
  36. id: "field",
  37. commonMonsters: [
  38. "Chickens",
  39. "Rats",
  40. "Spiders"
  41. ],
  42. rareMonsters: [
  43. "Lizards",
  44. "Bees"
  45. ],
  46. energyCost: 50,
  47. fightPointCost: 300
  48. },
  49. blood_field: {
  50. id: "blood_field",
  51. blood: true,
  52. commonMonsters: [
  53. "Blood Chickens",
  54. "Blood Rats",
  55. "Blood Spiders"
  56. ],
  57. rareMonsters: [
  58. "Blood Lizards",
  59. "Blood Bees"
  60. ],
  61. energyCost: 5000,
  62. fightPointCost: 2000
  63. },
  64. forest: {
  65. id: "forest",
  66. commonMonsters: [
  67. "Snakes",
  68. "Ants",
  69. "Wolves"
  70. ],
  71. rareMonsters: [
  72. "Ents",
  73. "Thief"
  74. ],
  75. energyCost: 200,
  76. fightPointCost: 600
  77. },
  78. cave: {
  79. id: "cave",
  80. commonMonsters: [
  81. "Bears",
  82. "Goblins",
  83. "Bats"
  84. ],
  85. rareMonsters: [
  86. "Skeletons"
  87. ],
  88. energyCost: 500,
  89. fightPointCost: 900
  90. },
  91. volcano: {
  92. id: "volcano",
  93. commonMonsters: [
  94. "Fire Hawk",
  95. "Fire Snake",
  96. "Fire Golem"
  97. ],
  98. rareMonsters: [
  99. "Fire Witch"
  100. ],
  101. energyCost: 1000,
  102. fightPointCost: 1500
  103. },
  104. northern_field: {
  105. id: "northern_field",
  106. commonMonsters: [
  107. "Ice Hawk",
  108. "Ice Witch",
  109. "Golem"
  110. ],
  111. rareMonsters: [
  112. "Yeti"
  113. ],
  114. energyCost: 3000,
  115. fightPointCost: 2000
  116. }
  117. };
  118. try {
  119. const normalCode = Combat._modal_load_area_data.toString().split(/\r?\n/g);
  120. const bloodCode = Combat._modal_load_blood_area_data.toString().split(/\r?\n/g);
  121. const zones = {};
  122. [false, true].forEach(blood => {
  123. const code = blood ? bloodCode : normalCode;
  124. let foundSwitch = false;
  125. let endSwitch = false;
  126. let current = null;
  127. code.forEach(line => {
  128. if(endSwitch) {
  129. return;
  130. }
  131. if(!foundSwitch) {
  132. if(line.includes("switch(area)")) {
  133. foundSwitch = true;
  134. }
  135. }
  136. else {
  137. line = line.trim();
  138. if(foundSwitch && !endSwitch && !current && line=='}') {
  139. endSwitch = true;
  140. }
  141. else if(/case /.test(line)) {
  142. // start of zone data
  143. let zoneId = line.replace(/^case\s+"/, "").replace(/":.*$/, "");
  144. current = zones[zoneId] = {id: zoneId, blood: blood};
  145. }
  146. else if(line.startsWith("break;")) {
  147. // end of zone data
  148. current = null;
  149. }
  150. else if(current) {
  151. if(line.startsWith("common_monsters_array")) {
  152. current.commonMonsters = line
  153. .replace("common_monsters_array = [", "")
  154. .replace("];", "")
  155. .split(/\s*,\s*/g)
  156. .map(s => s.substring(1, s.length-1));
  157. }
  158. else if(line.startsWith("rare_monsters_array")) {
  159. current.rareMonsters = line
  160. .replace("rare_monsters_array = [", "")
  161. .replace("];", "")
  162. .split(/\s*,\s*/g)
  163. .map(s => s.substring(1, s.length-1));
  164. }
  165. else if(line.startsWith("energy")) {
  166. current.energyCost = parseInt(line.match(/\d+/)[0]);
  167. }
  168. else if(line.startsWith("fightpoints")) {
  169. current.fightPointCost = parseInt(line.match(/\d+/)[0]);
  170. }
  171. }
  172. }
  173. });
  174. });
  175.  
  176. if(!zones || !Object.keys(zones).length) {
  177. console.error("IdlePixelPlus: Could not parse combat zone data, using fallback.");
  178. return fallback;
  179. }
  180. return zones;
  181. }
  182. catch(err) {
  183. console.error("IdlePixelPlus: Could not parse combat zone data, using fallback.", err);
  184. return fallback;
  185. }
  186. }
  187.  
  188. function createOreObjects() {
  189. const ores = {
  190. stone: { smeltable:false, bar: null },
  191. copper: { smeltable:true, smeltTime: 3, bar: "bronze_bar" },
  192. iron: { smeltable:true, smeltTime: 6, bar: "iron_bar" },
  193. silver: { smeltable:true, smeltTIme: 15, bar: "silver_bar" },
  194. gold: { smeltable:true, smeltTIme: 50, bar: "gold_bar" },
  195. promethium: { smeltable:true, smeltTIme: 100, bar: "promethium_bar" }
  196. };
  197. try {
  198. Object.keys(ores).forEach(id => {
  199. const obj = ores[id];
  200. obj.id = id;
  201. obj.oil = Crafting.getOilPerBar(id);
  202. obj.charcoal = Crafting.getCharcoalPerBar(id);
  203. });
  204. }
  205. catch(err) {
  206. console.error("IdlePixelPlus: Could not create ore data. This could adversely affect related functionality.", err);
  207. }
  208. return ores;
  209. }
  210.  
  211. function createSeedObjects() {
  212. // hardcoded for now.
  213. return {
  214. dotted_green_leaf_seeds: {
  215. id: "dotted_green_leaf_seeds",
  216. level: 1,
  217. stopsDying: 15,
  218. time: 15,
  219. bonemealCost: 0
  220. },
  221. stardust_seeds: {
  222. id: "stardust_seeds",
  223. level: 8,
  224. stopsDying: 0,
  225. time: 20,
  226. bonemealCost: 0
  227. },
  228. green_leaf_seeds: {
  229. id: "green_leaf_seeds",
  230. level: 10,
  231. stopsDying: 25,
  232. time: 30,
  233. bonemealCost: 0
  234. },
  235. lime_leaf_seeds: {
  236. id: "lime_leaf_seeds",
  237. level: 25,
  238. stopsDying: 40,
  239. time: 1*60,
  240. bonemealCost: 1
  241. },
  242. gold_leaf_seeds: {
  243. id: "gold_leaf_seeds",
  244. level: 50,
  245. stopsDying: 60,
  246. time: 2*60,
  247. bonemealCost: 10
  248. },
  249. crystal_leaf_seeds: {
  250. id: "crystal_leaf_seeds",
  251. level: 70,
  252. stopsDying: 80,
  253. time: 5*60,
  254. bonemealCost: 25
  255. },
  256. red_mushroom_seeds: {
  257. id: "red_mushroom_seeds",
  258. level: 1,
  259. stopsDying: 0,
  260. time: 5,
  261. bonemealCost: 0
  262. },
  263. tree_seeds: {
  264. id: "tree_seeds",
  265. level: 10,
  266. stopsDying: 25,
  267. time: 5*60,
  268. bonemealCost: 10
  269. },
  270. oak_tree_seeds: {
  271. id: "oak_tree_seeds",
  272. level: 25,
  273. stopsDying: 40,
  274. time: 4*60,
  275. bonemealCost: 25
  276. },
  277. willow_tree_seeds: {
  278. id: "willow_tree_seeds",
  279. level: 37,
  280. stopsDying: 55,
  281. time: 8*60,
  282. bonemealCost: 50
  283. },
  284. maple_tree_seeds: {
  285. id: "maple_tree_seeds",
  286. level: 50,
  287. stopsDying: 65,
  288. time: 12*60,
  289. bonemealCost: 120
  290. },
  291. stardust_tree_seeds: {
  292. id: "stardust_tree_seeds",
  293. level: 65,
  294. stopsDying: 80,
  295. time: 15*60,
  296. bonemealCost: 150
  297. },
  298. pine_tree_seeds: {
  299. id: "pine_tree_seeds",
  300. level: 70,
  301. stopsDying: 85,
  302. time: 17*60,
  303. bonemealCost: 180
  304. }
  305. };
  306. }
  307.  
  308. function createSpellObjects() {
  309. const spells = {};
  310. Object.keys(Magic.spell_info).forEach(id => {
  311. const info = Magic.spell_info[id];
  312. spells[id] = {
  313. id: id,
  314. manaCost: info.mana_cost,
  315. magicBonusRequired: info.magic_bonus
  316. };
  317. });
  318. return spells;
  319. }
  320.  
  321. const INFO = {
  322. ores: createOreObjects(),
  323. seeds: createSeedObjects(),
  324. combatZones: createCombatZoneObjects(),
  325. spells: createSpellObjects()
  326. };
  327.  
  328. function logFancy(s, color="#00f7ff") {
  329. console.log("%cIdlePixelPlus: %c"+s, `color: ${color}; font-weight: bold; font-size: 12pt;`, "color: black; font-weight: normal; font-size: 10pt;");
  330. }
  331.  
  332. class IdlePixelPlusPlugin {
  333.  
  334. constructor(id, opts) {
  335. if(typeof id !== "string") {
  336. throw new TypeError("IdlePixelPlusPlugin constructor takes the following arguments: (id:string, opts?:object)");
  337. }
  338. this.id = id;
  339. this.opts = opts || {};
  340. this.config = null;
  341. }
  342.  
  343. getConfig(name) {
  344. if(!this.config) {
  345. IdlePixelPlus.loadPluginConfigs(this.id);
  346. }
  347. if(this.config) {
  348. return this.config[name];
  349. }
  350. }
  351.  
  352. /*
  353. onConfigsChanged() { }
  354. onLogin() { }
  355. onMessageReceived(data) { }
  356. onVariableSet(key, valueBefore, valueAfter) { }
  357. onChat(data) { }
  358. onPanelChanged(panelBefore, panelAfter) { }
  359. onCombatStart() { }
  360. onCombatEnd() { }
  361. onCustomMessageReceived(player, content, callbackId) { }
  362. onCustomMessagePlayerOffline(player, content) { }
  363. */
  364.  
  365. }
  366.  
  367. const internal = {
  368. init() {
  369. const self = this;
  370.  
  371. $("head").append(`
  372. <style>
  373. .ipp-chat-command-help {
  374. padding: 0.5em 0;
  375. }
  376. .ipp-chat-command-help:first-child {
  377. padding-top: 0;
  378. }
  379. .ipp-chat-command-help:last-child {
  380. padding-bottom: 0;
  381. }
  382. dialog.ipp-dialog {
  383. background-color: white;
  384. border: 1px solid rgba(0, 0, 0, 0.2);
  385. width: 500px;
  386. max-width: 800px;
  387. border-radius: 5px;
  388. display: flex;
  389. flex-direction: column;
  390. justify-content: flex-start;
  391. }
  392. dialog.ipp-dialog > div {
  393. width: 100%;
  394. }
  395. dialog.ipp-dialog > .ipp-dialog-header > h4 {
  396. margin-bottom: 0;
  397. }
  398. dialog.ipp-dialog > .ipp-dialog-header {
  399. border-bottom: 1px solid rgba(0, 0, 0, 0.2);
  400. padding-bottom: 0.25em;
  401. }
  402. dialog.ipp-dialog > .ipp-dialog-actions {
  403. padding-top: 0.25em;
  404. padding-bottom: 0.25em;
  405. }
  406. dialog.ipp-dialog > .ipp-dialog-actions {
  407. border-top: 1px solid rgba(0, 0, 0, 0.2);
  408. padding-top: 0.25em;
  409. text-align: right;
  410. }
  411. dialog.ipp-dialog > .ipp-dialog-actions > button {
  412. margin: 4px;
  413. }
  414. </style>
  415. `);
  416.  
  417. // hook into websocket messages
  418. const hookIntoOnMessage = () => {
  419. try {
  420. const original_onmessage = window.websocket.connected_socket.onmessage;
  421. if(typeof original_onmessage === "function") {
  422. window.websocket.connected_socket.onmessage = function(event) {
  423. original_onmessage.apply(window.websocket.connected_socket, arguments);
  424. self.onMessageReceived(event.data);
  425. }
  426. return true;
  427. }
  428. else {
  429. return false;
  430. }
  431. }
  432. catch(err) {
  433. console.error("Had trouble hooking into websocket...");
  434. return false;
  435. }
  436. };
  437. $(function() {
  438. if(!hookIntoOnMessage()) {
  439. // try once more
  440. setTimeout(hookIntoOnMessage, 40);
  441. }
  442. });
  443.  
  444. // hook into Chat.send
  445. const original_chat_send = Chat.send;
  446. Chat.send = function() {
  447. const input = $("#chat-area-input");
  448. let message = input.val();
  449. if(message.length == 0) {
  450. return;
  451. }
  452. if(message.startsWith("/")) {
  453. const space = message.indexOf(" ");
  454. let command;
  455. let data;
  456. if(space <= 0) {
  457. command = message.substring(1);
  458. data = "";
  459. }
  460. else {
  461. command = message.substring(1, space);
  462. data = message.substring(space+1);
  463. }
  464. if(window.IdlePixelPlus.handleCustomChatCommand(command, data)) {
  465. input.val("");
  466. }
  467. else {
  468. original_chat_send();
  469. }
  470. }
  471. else {
  472. original_chat_send();
  473. }
  474. };
  475.  
  476. // hook into Items.set, which is where var_ values are set
  477. const original_items_set = Items.set;
  478. Items.set = function(key, value) {
  479. let valueBefore = window["var_"+key];
  480. original_items_set.apply(this, arguments);
  481. let valueAfter = window["var_"+key];
  482. self.onVariableSet(key, valueBefore, valueAfter);
  483. }
  484.  
  485. // hook into switch_panels, which is called when the main panel is changed. This is also used for custom panels.
  486. const original_switch_panels = window.switch_panels;
  487. window.switch_panels = function(id) {
  488. let panelBefore = Globals.currentPanel;
  489. if(panelBefore && panelBefore.startsWith("panel-")) {
  490. panelBefore = panelBefore.substring("panel-".length);
  491. }
  492. self.hideCustomPanels();
  493. original_switch_panels.apply(this, arguments);
  494. let panelAfter = Globals.currentPanel;
  495. if(panelAfter && panelAfter.startsWith("panel-")) {
  496. panelAfter = panelAfter.substring("panel-".length);
  497. }
  498. self.onPanelChanged(panelBefore, panelAfter);
  499. }
  500.  
  501. // create plugin menu item and panel
  502. const lastMenuItem = $("#menu-bar-buttons > .hover-menu-bar-item").last();
  503. lastMenuItem.after(`
  504. <div onclick="IdlePixelPlus.setPanel('idlepixelplus')" class="hover hover-menu-bar-item">
  505. <img id="menu-bar-idlepixelplus-icon" src="https://anwinity.com/idlepixelplus/plugins.png"> PLUGINS
  506. </div>
  507. `);
  508. self.addPanel("idlepixelplus", "IdlePixel+ Plugins", function() {
  509. let content = `
  510. <style>
  511. .idlepixelplus-plugin-box {
  512. display: block;
  513. position: relative;
  514. padding: 0.25em;
  515. color: white;
  516. background-color: rgb(107, 107, 107);
  517. border: 1px solid black;
  518. border-radius: 6px;
  519. margin-bottom: 0.5em;
  520. }
  521. .idlepixelplus-plugin-box .idlepixelplus-plugin-settings-button {
  522. position: absolute;
  523. right: 2px;
  524. top: 2px;
  525. cursor: pointer;
  526. }
  527. .idlepixelplus-plugin-box .idlepixelplus-plugin-config-section {
  528. display: grid;
  529. grid-template-columns: minmax(100px, min-content) 1fr;
  530. row-gap: 0.5em;
  531. column-gap: 0.5em;
  532. white-space: nowrap;
  533. }
  534. </style>
  535. `;
  536. self.forEachPlugin(plugin => {
  537. let id = plugin.id;
  538. let name = "An IdlePixel+ Plugin!";
  539. let description = "";
  540. let author = "unknown";
  541. if(plugin.opts.about) {
  542. let about = plugin.opts.about;
  543. name = about.name || name;
  544. description = about.description || description;
  545. author = about.author || author;
  546. }
  547. content += `
  548. <div id="idlepixelplus-plugin-box-${id}" class="idlepixelplus-plugin-box">
  549. <strong><u>${name||id}</u></strong> (by ${author})<br />
  550. <span>${description}</span><br />
  551. <div class="idlepixelplus-plugin-config-section" style="display: none">
  552. <hr style="grid-column: span 2">
  553. `;
  554. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  555. plugin.opts.config.forEach(cfg => {
  556. if(CONFIG_TYPES_LABEL.includes(cfg.type)) {
  557. content += `<h5 style="grid-column: span 2; margin-bottom: 0; font-weight: 600">${cfg.label}</h5>`;
  558. }
  559. else if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  560. content += `
  561. <div>
  562. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  563. </div>
  564. <div>
  565. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="checkbox" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  566. </div>
  567. `;
  568. }
  569. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  570. content += `
  571. <div>
  572. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  573. </div>
  574. <div>
  575. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="1" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  576. </div>
  577. `;
  578. }
  579. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  580. content += `
  581. <div>
  582. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  583. </div>
  584. <div>
  585. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="number" step="${cfg.step || ''}" min="${cfg.min || ''}" max="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  586. </div>
  587. `;
  588. }
  589. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  590. content += `
  591. <div>
  592. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  593. </div>
  594. <div>
  595. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="text" maxlength="${cfg.max || ''}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  596. </div>
  597. `;
  598. }
  599. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  600. content += `
  601. <div>
  602. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  603. </div>
  604. <div>
  605. <input id="idlepixelplus-config-${plugin.id}-${cfg.id}" type="color" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)" />
  606. </div>
  607. `;
  608. }
  609. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  610. content += `
  611. <div>
  612. <label for="idlepixelplus-config-${plugin.id}-${cfg.id}">${cfg.label || cfg.id}</label>
  613. </div>
  614. <div>
  615. <select id="idlepixelplus-config-${plugin.id}-${cfg.id}" onchange="IdlePixelPlus.setPluginConfigUIDirty('${id}', true)">
  616. `;
  617. if(cfg.options && Array.isArray(cfg.options)) {
  618. cfg.options.forEach(option => {
  619. if(typeof option === "string") {
  620. content += `<option value="${option}">${option}</option>`;
  621. }
  622. else {
  623. content += `<option value="${option.value}">${option.label || option.value}</option>`;
  624. }
  625.  
  626. });
  627. }
  628. content += `
  629. </select>
  630. </div>
  631. `;
  632. }
  633. });
  634. content += `
  635. <div style="grid-column: span 2">
  636. <button id="idlepixelplus-configbutton-${plugin.id}-reload" onclick="IdlePixelPlus.loadPluginConfigs('${id}')">Reload</button>
  637. <button id="idlepixelplus-configbutton-${plugin.id}-apply" onclick="IdlePixelPlus.savePluginConfigs('${id}')">Apply</button>
  638. </div>
  639. `;
  640. }
  641. content += "</div>";
  642. if(plugin.opts.config) {
  643. content += `
  644. <div class="idlepixelplus-plugin-settings-button">
  645. <button onclick="$('#idlepixelplus-plugin-box-${id} .idlepixelplus-plugin-config-section').toggle()">Settings</button>
  646. </div>`;
  647. }
  648. content += "</div>";
  649. });
  650.  
  651. return content;
  652. });
  653.  
  654. $("#chat-area-input").attr("autocomplete", "off");
  655.  
  656. logFancy(`(v${self.version}) initialized.`);
  657. }
  658. };
  659.  
  660. class IdlePixelPlus {
  661.  
  662. constructor() {
  663. this.version = VERSION;
  664. this.plugins = {};
  665. this.panels = {};
  666. this.debug = false;
  667. this.info = INFO;
  668. this.nextUniqueId = 1;
  669. this.customMessageCallbacks = {};
  670. this.customChatCommands = {
  671. help: (command, data) => {
  672. console.log("help", command, data);
  673. }
  674. };
  675. this.customChatHelp = {};
  676. this.customDialogOptions = {};
  677.  
  678. if(localStorage.getItem(LOCAL_STORAGE_KEY_DEBUG) == "1") {
  679. this.debug = true;
  680. }
  681. }
  682.  
  683. getCustomDialogData(id) {
  684. const el = document.querySelector(`dialog#${id}.ipp-dialog`);
  685. if(el) {
  686. const result = {};
  687. $(el).find("[data-key]").each(function() {
  688. const dataElement = $(this);
  689. const dataKey = dataElement.attr("data-key");
  690. if(["INPUT", "SELECT", "TEXTAREA"].includes(dataElement.prop("tagName"))) {
  691. result[dataKey] = dataElement.val();
  692. }
  693. else {
  694. result[dataKey] = dataElement.text();
  695. }
  696. });
  697. return result;
  698. }
  699. }
  700.  
  701. openCustomDialog(id, noEvent=false) {
  702. this.closeCustomDialog(id, true);
  703. const el = document.querySelector(`dialog#${id}.ipp-dialog`);
  704. if(el) {
  705. el.style.display = "";
  706. el.showModal();
  707. const opts = this.customDialogOptions[id];
  708. if(!noEvent && opts && typeof opts.onOpen === "function") {
  709. opts.onOpen(opts);
  710. }
  711. }
  712. }
  713.  
  714. closeCustomDialog(id, noEvent=false) {
  715. const el = document.querySelector(`dialog#${id}.ipp-dialog`);
  716. if(el) {
  717. el.close();
  718. el.style.display = "none";
  719. const opts = this.customDialogOptions[id];
  720. if(!noEvent && opts && typeof opts.onClose === "function") {
  721. opts.onClose(opts);
  722. }
  723. }
  724. }
  725.  
  726. destroyCustomDialog(id, noEvent=false) {
  727. const el = document.querySelector(`dialog#${id}.ipp-dialog`);
  728. if(el) {
  729. el.remove();
  730. const opts = this.customDialogOptions[id];
  731. if(!noEvent && opts && typeof opts.onDestroy === "function") {
  732. opts.onDestroy(opts);
  733. }
  734. }
  735. delete this.customDialogOptions[id];
  736. }
  737.  
  738. createCustomDialog(id, opts={}) {
  739. const self = this;
  740. this.destroyCustomDialog(id);
  741. this.customDialogOptions[id] = opts;
  742. const el = $("body").append(`
  743. <dialog id="${id}" class="ipp-dialog" style="display: none">
  744. <div class="ipp-dialog-header">
  745. <h4>${opts.title||''}</h4>
  746. </div>
  747. <div class="ipp-dialog-content"></div>
  748. <div class="ipp-dialog-actions"></div>
  749. </dialog>
  750. `);
  751. const headerElement = el.find(".ipp-dialog-header");
  752. const contentElement = el.find(".ipp-dialog-content");
  753. const actionsElement = el.find(".ipp-dialog-actions");
  754.  
  755. if(!opts.title) {
  756. headerElement.hide();
  757. }
  758.  
  759. if(typeof opts.content === "string") {
  760. contentElement.append(opts.content);
  761. }
  762.  
  763. let actions = opts.actions;
  764. if(actions) {
  765. if(!Array.isArray(actions)) {
  766. actions = [actions];
  767. }
  768. actions.forEach(action => {
  769. let label;
  770. let primary = false;
  771. if(typeof action === "string") {
  772. label = action;
  773. }
  774. else {
  775. label = action.label || action.action;
  776. primary = action.primary===true;
  777. action = action.action;
  778. }
  779. actionsElement.append(`<button data-action="${action}" class="${primary?'background-primary':''}">${label}</button>`);
  780. });
  781. actionsElement.find("button").on("click", function(e) {
  782. if(typeof opts.onAction === "function") {
  783. e.stopPropagation();
  784. const button = $(this);
  785. const buttonAction = button.attr("data-action");
  786. const data = self.getCustomDialogData(id);
  787. const actionReturn = opts.onAction(buttonAction, data);
  788. if(actionReturn) {
  789. self.closeCustomDialog(id);
  790. }
  791. }
  792. });
  793. }
  794. else {
  795. el.find(".ipp-dialog-actions").hide();
  796. }
  797.  
  798. el.click(function(e) {
  799. const rect = e.target.getBoundingClientRect();
  800. const inside =
  801. rect.top <= e.clientY &&
  802. rect.left <= e.clientX &&
  803. e.clientX <= rect.left + rect.width &&
  804. e.clientY <= rect.top + rect.height;
  805. if(!inside) {
  806. self.closeCustomDialog(id);
  807. e.stopPropagation();
  808. }
  809. });
  810.  
  811. if(typeof opts.onCreate === "function") {
  812. opts.onCreate();
  813. }
  814. if(opts.openImmediately === true) {
  815. this.openCustomDialog(id);
  816. }
  817. }
  818.  
  819.  
  820. registerCustomChatCommand(command, f, help) {
  821. if(Array.isArray(command)) {
  822. command.forEach(cmd => this.registerCustomChatCommand(cmd, f, help));
  823. return;
  824. }
  825. if(typeof command !== "string" || typeof f !== "function") {
  826. throw new TypeError("IdlePixelPlus.registerCustomChatCommand takes the following arguments: (command:string, f:function)");
  827. }
  828. if(CHAT_COMMAND_NO_OVERRIDE.includes(command)) {
  829. throw new Error(`Cannot override the following chat commands: ${CHAT_COMMAND_NO_OVERRIDE.join(", ")}`);
  830. }
  831. if(command in this.customChatCommands) {
  832. console.warn(`IdlePixelPlus: re-registering custom chat command "${command}" which already exists.`);
  833. }
  834. this.customChatCommands[command] = f;
  835. if(help && typeof help === "string") {
  836. this.customChatHelp[command] = help.replace(/%COMMAND%/g, command);
  837. }
  838. else {
  839. delete this.customChatHelp[command];
  840. }
  841. }
  842.  
  843. handleCustomChatCommand(command, message) {
  844. // return true if command handler exists, false otherwise
  845. const f = this.customChatCommands[command];
  846. if(typeof f === "function") {
  847. try {
  848. f(command, message);
  849. }
  850. catch(err) {
  851. console.error(`Error executing custom command "${command}"`, err);
  852. }
  853. return true;
  854. }
  855. return false;
  856. }
  857.  
  858. uniqueId() {
  859. return this.nextUniqueId++;
  860. }
  861.  
  862. setDebug(debug) {
  863. if(debug) {
  864. this.debug = true;
  865. localStorage.setItem(LOCAL_STORAGE_KEY_DEBUG, "1");
  866. }
  867. else {
  868. this.debug = false;
  869. localStorage.removeItem(LOCAL_STORAGE_KEY_DEBUG);
  870. }
  871. }
  872.  
  873. getVar(name, type) {
  874. let s = window[`var_${name}`];
  875. if(type) {
  876. switch(type) {
  877. case "int":
  878. case "integer":
  879. return parseInt(s);
  880. case "number":
  881. case "float":
  882. return parseFloat(s);
  883. case "boolean":
  884. case "bool":
  885. if(s=="true") return true;
  886. if(s=="false") return false;
  887. return undefined;
  888. }
  889. }
  890. return s;
  891. }
  892.  
  893. getVarOrDefault(name, defaultValue, type) {
  894. let s = window[`var_${name}`];
  895. if(s==null || typeof s === "undefined") {
  896. return defaultValue;
  897. }
  898. if(type) {
  899. let value;
  900. switch(type) {
  901. case "int":
  902. case "integer":
  903. value = parseInt(s);
  904. return isNaN(value) ? defaultValue : value;
  905. case "number":
  906. case "float":
  907. value = parseFloat(s);
  908. return isNaN(value) ? defaultValue : value;
  909. case "boolean":
  910. case "bool":
  911. if(s=="true") return true;
  912. if(s=="false") return false;
  913. return defaultValue;
  914. }
  915. }
  916. return s;
  917. }
  918.  
  919. setPluginConfigUIDirty(id, dirty) {
  920. if(typeof id !== "string" || typeof dirty !== "boolean") {
  921. throw new TypeError("IdlePixelPlus.setPluginConfigUIDirty takes the following arguments: (id:string, dirty:boolean)");
  922. }
  923. const plugin = this.plugins[id];
  924. const button = $(`#idlepixelplus-configbutton-${plugin.id}-apply`);
  925. if(button) {
  926. button.prop("disabled", !(dirty));
  927. }
  928. }
  929.  
  930. loadPluginConfigs(id) {
  931. if(typeof id !== "string") {
  932. throw new TypeError("IdlePixelPlus.reloadPluginConfigs takes the following arguments: (id:string)");
  933. }
  934. const plugin = this.plugins[id];
  935. const config = {};
  936. let stored;
  937. try {
  938. stored = JSON.parse(localStorage.getItem(`idlepixelplus.${id}.config`) || "{}");
  939. }
  940. catch(err) {
  941. console.error(`Failed to load configs for plugin with id "${id} - will use defaults instead."`);
  942. stored = {};
  943. }
  944. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  945. plugin.opts.config.forEach(cfg => {
  946. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  947. let value = stored[cfg.id];
  948. if(value==null || typeof value === "undefined") {
  949. value = cfg.default;
  950. }
  951. config[cfg.id] = value;
  952.  
  953. if(el) {
  954. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type) && typeof value === "boolean") {
  955. el.prop("checked", value);
  956. }
  957. else if(CONFIG_TYPES_INTEGER.includes(cfg.type) && typeof value === "number") {
  958. el.val(value);
  959. }
  960. else if(CONFIG_TYPES_FLOAT.includes(cfg.type) && typeof value === "number") {
  961. el.val(value);
  962. }
  963. else if(CONFIG_TYPES_STRING.includes(cfg.type) && typeof value === "string") {
  964. el.val(value);
  965. }
  966. else if(CONFIG_TYPES_SELECT.includes(cfg.type) && typeof value === "string") {
  967. el.val(value);
  968. }
  969. else if(CONFIG_TYPES_COLOR.includes(cfg.type) && typeof value === "string") {
  970. el.val(value);
  971. }
  972. }
  973. });
  974. }
  975. plugin.config = config;
  976. this.setPluginConfigUIDirty(id, false);
  977. if(typeof plugin.onConfigsChanged === "function") {
  978. plugin.onConfigsChanged();
  979. }
  980. }
  981.  
  982. savePluginConfigs(id) {
  983. if(typeof id !== "string") {
  984. throw new TypeError("IdlePixelPlus.savePluginConfigs takes the following arguments: (id:string)");
  985. }
  986. const plugin = this.plugins[id];
  987. const config = {};
  988. if(plugin.opts.config && Array.isArray(plugin.opts.config)) {
  989. plugin.opts.config.forEach(cfg => {
  990. const el = $(`#idlepixelplus-config-${plugin.id}-${cfg.id}`);
  991. let value;
  992. if(CONFIG_TYPES_BOOLEAN.includes(cfg.type)) {
  993. config[cfg.id] = el.is(":checked");
  994. }
  995. else if(CONFIG_TYPES_INTEGER.includes(cfg.type)) {
  996. config[cfg.id] = parseInt(el.val());
  997. }
  998. else if(CONFIG_TYPES_FLOAT.includes(cfg.type)) {
  999. config[cfg.id] = parseFloat(el.val());
  1000. }
  1001. else if(CONFIG_TYPES_STRING.includes(cfg.type)) {
  1002. config[cfg.id] = el.val();
  1003. }
  1004. else if(CONFIG_TYPES_SELECT.includes(cfg.type)) {
  1005. config[cfg.id] = el.val();
  1006. }
  1007. else if(CONFIG_TYPES_COLOR.includes(cfg.type)) {
  1008. config[cfg.id] = el.val();
  1009. }
  1010. });
  1011. }
  1012. plugin.config = config;
  1013. localStorage.setItem(`idlepixelplus.${id}.config`, JSON.stringify(config));
  1014. this.setPluginConfigUIDirty(id, false);
  1015. if(typeof plugin.onConfigsChanged === "function") {
  1016. plugin.onConfigsChanged();
  1017. }
  1018. }
  1019.  
  1020. addPanel(id, title, content) {
  1021. if(typeof id !== "string" || typeof title !== "string" || (typeof content !== "string" && typeof content !== "function") ) {
  1022. throw new TypeError("IdlePixelPlus.addPanel takes the following arguments: (id:string, title:string, content:string|function)");
  1023. }
  1024. const panels = $("#panels");
  1025. panels.append(`
  1026. <div id="panel-${id}" style="display: none">
  1027. <h1>${title}</h1>
  1028. <hr>
  1029. <div class="idlepixelplus-panel-content"></div>
  1030. </div>
  1031. `);
  1032. this.panels[id] = {
  1033. id: id,
  1034. title: title,
  1035. content: content
  1036. };
  1037. this.refreshPanel(id);
  1038. }
  1039.  
  1040. refreshPanel(id) {
  1041. if(typeof id !== "string") {
  1042. throw new TypeError("IdlePixelPlus.refreshPanel takes the following arguments: (id:string)");
  1043. }
  1044. const panel = this.panels[id];
  1045. if(!panel) {
  1046. throw new TypeError(`Error rendering panel with id="${id}" - panel has not be added.`);
  1047. }
  1048. let content = panel.content;
  1049. if(!["string", "function"].includes(typeof content)) {
  1050. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  1051. }
  1052. if(typeof content === "function") {
  1053. content = content();
  1054. if(typeof content !== "string") {
  1055. throw new TypeError(`Error rendering panel with id="${id}" - panel.content must be a string or a function returning a string.`);
  1056. }
  1057. }
  1058. const panelContent = $(`#panel-${id} .idlepixelplus-panel-content`);
  1059. panelContent.html(content);
  1060. if(id === "idlepixelplus") {
  1061. this.forEachPlugin(plugin => {
  1062. this.loadPluginConfigs(plugin.id);
  1063. });
  1064. }
  1065. }
  1066.  
  1067. registerPlugin(plugin) {
  1068. if(!(plugin instanceof IdlePixelPlusPlugin)) {
  1069. throw new TypeError("IdlePixelPlus.registerPlugin takes the following arguments: (plugin:IdlePixelPlusPlugin)");
  1070. }
  1071. if(plugin.id in this.plugins) {
  1072. throw new Error(`IdlePixelPlusPlugin with id "${plugin.id}" is already registered. Make sure your plugin id is unique!`);
  1073. }
  1074.  
  1075. this.plugins[plugin.id] = plugin;
  1076. this.loadPluginConfigs(plugin.id);
  1077. let versionString = plugin.opts&&plugin.opts.about&&plugin.opts.about.version ? ` (v${plugin.opts.about.version})` : "";
  1078. logFancy(`registered plugin "${plugin.id}"${versionString}`);
  1079. }
  1080.  
  1081. forEachPlugin(f) {
  1082. if(typeof f !== "function") {
  1083. throw new TypeError("IdlePixelPlus.forEachPlugin takes the following arguments: (f:function)");
  1084. }
  1085. Object.values(this.plugins).forEach(plugin => {
  1086. try {
  1087. f(plugin);
  1088. }
  1089. catch(err) {
  1090. console.error(`Error occurred while executing function for plugin "${plugin.id}."`);
  1091. console.error(err);
  1092. }
  1093. });
  1094. }
  1095.  
  1096. setPanel(panel) {
  1097. if(typeof panel !== "string") {
  1098. throw new TypeError("IdlePixelPlus.setPanel takes the following arguments: (panel:string)");
  1099. }
  1100. window.switch_panels(`panel-${panel}`);
  1101. }
  1102.  
  1103. sendMessage(message) {
  1104. if(typeof message !== "string") {
  1105. throw new TypeError("IdlePixelPlus.sendMessage takes the following arguments: (message:string)");
  1106. }
  1107. if(window.websocket && window.websocket.connected_socket && window.websocket.connected_socket.readyState==1) {
  1108. window.websocket.connected_socket.send(message);
  1109. }
  1110. }
  1111.  
  1112. showToast(title, content) {
  1113. show_toast(title, content);
  1114. }
  1115.  
  1116. hideCustomPanels() {
  1117. Object.values(this.panels).forEach((panel) => {
  1118. const el = $(`#panel-${panel.id}`);
  1119. if(el) {
  1120. el.css("display", "none");
  1121. }
  1122. });
  1123. }
  1124.  
  1125. onMessageReceived(data) {
  1126. if(this.debug) {
  1127. console.log(`IP+ onMessageReceived: ${data}`);
  1128. }
  1129. if(data) {
  1130. this.forEachPlugin((plugin) => {
  1131. if(typeof plugin.onMessageReceived === "function") {
  1132. plugin.onMessageReceived(data);
  1133. }
  1134. });
  1135. if(data.startsWith("VALID_LOGIN")) {
  1136. this.onLogin();
  1137. }
  1138. else if(data.startsWith("CHAT=")) {
  1139. const split = data.substring("CHAT=".length).split("~");
  1140. const chatData = {
  1141. username: split[0],
  1142. sigil: split[1],
  1143. tag: split[2],
  1144. level: parseInt(split[3]),
  1145. message: split[4]
  1146. };
  1147. this.onChat(chatData);
  1148. // CHAT=anwinity~none~none~1565~test
  1149. }
  1150. else if(data.startsWith("CUSTOM=")) {
  1151. const customData = data.substring("CUSTOM=".length);
  1152. const tilde = customData.indexOf("~");
  1153. if(tilde > 0) {
  1154. const fromPlayer = customData.substring(0, tilde);
  1155. const content = customData.substring(tilde+1);
  1156. this.onCustomMessageReceived(fromPlayer, content);
  1157. }
  1158. }
  1159. }
  1160. }
  1161.  
  1162. deleteCustomMessageCallback(callbackId) {
  1163. if(this.debug) {
  1164. console.log(`IP+ deleteCustomMessageCallback`, callbackId);
  1165. }
  1166. delete this.customMessageCallbacks[callbackId];
  1167. }
  1168.  
  1169. requestPluginManifest(player, callback, pluginId) {
  1170. if(typeof pluginId === "string") {
  1171. pluginId = [pluginId];
  1172. }
  1173. if(Array.isArray(pluginId)) {
  1174. pluginId = JSON.stringify(pluginId);
  1175. }
  1176. this.sendCustomMessage(player, {
  1177. content: "PLUGIN_MANIFEST" + (pluginId ? `:${pluginId}` : ''),
  1178. onResponse: function(respPlayer, content) {
  1179. if(typeof callback === "function") {
  1180. callback(respPlayer, JSON.parse(content));
  1181. }
  1182. else {
  1183. console.log(`Plugin Manifest: ${respPlayer}`, content);
  1184. }
  1185. },
  1186. onOffline: function(respPlayer, content) {
  1187. if(typeof callback === "function") {
  1188. callback(respPlayer, false);
  1189. }
  1190. },
  1191. timeout: 10000
  1192. });
  1193. }
  1194.  
  1195. sendCustomMessage(toPlayer, opts) {
  1196. if(this.debug) {
  1197. console.log(`IP+ sendCustomMessage`, toPlayer, opts);
  1198. }
  1199. const reply = !!(opts.callbackId);
  1200. const content = typeof opts.content === "string" ? opts.content : JSON.stringify(opts.content);
  1201. const callbackId = reply ? opts.callbackId : this.uniqueId();
  1202. const responseHandler = typeof opts.onResponse === "function" ? opts.onResponse : null;
  1203. const offlineHandler = opts.onOffline===true ? () => { this.deleteCustomMessageCallback(callbackId); } : (typeof opts.onOffline === "function" ? opts.onOffline : null);
  1204. const timeout = typeof opts.timeout === "number" ? opts.timeout : -1;
  1205.  
  1206. if(responseHandler || offlineHandler) {
  1207. const handler = {
  1208. id: callbackId,
  1209. player: toPlayer,
  1210. responseHandler: responseHandler,
  1211. offlineHandler: offlineHandler,
  1212. timeout: typeof timeout === "number" ? timeout : -1,
  1213. };
  1214. if(callbackId) {
  1215. this.customMessageCallbacks[callbackId] = handler;
  1216. if(handler.timeout > 0) {
  1217. setTimeout(() => {
  1218. this.deleteCustomMessageCallback(callbackId);
  1219. }, handler.timeout);
  1220. }
  1221. }
  1222. }
  1223. const message = `CUSTOM=${toPlayer}~IPP${reply?'R':''}${callbackId}:${content}`;
  1224. if(message.length > 255) {
  1225. console.warn("The resulting websocket message from IdlePixelPlus.sendCustomMessage has a length limit of 255 characters. Recipients may not receive the full message!");
  1226. }
  1227. this.sendMessage(message);
  1228. }
  1229.  
  1230. onCustomMessageReceived(fromPlayer, content) {
  1231. if(this.debug) {
  1232. console.log(`IP+ onCustomMessageReceived`, fromPlayer, content);
  1233. }
  1234. const offline = content == "PLAYER_OFFLINE";
  1235. let callbackId = null;
  1236. let originalCallbackId = null;
  1237. let reply = false;
  1238. const ippMatcher = content.match(/^IPP(\w+):/);
  1239. if(ippMatcher) {
  1240. originalCallbackId = callbackId = ippMatcher[1];
  1241. let colon = content.indexOf(":");
  1242. content = content.substring(colon+1);
  1243. if(callbackId.startsWith("R")) {
  1244. callbackId = callbackId.substring(1);
  1245. reply = true;
  1246. }
  1247. }
  1248.  
  1249. // special built-in messages
  1250. if(content.startsWith("PLUGIN_MANIFEST")) {
  1251. const manifest = {};
  1252. let filterPluginIds = null;
  1253. if(content.includes(":")) {
  1254. content = content.substring("PLUGIN_MANIFEST:".length);
  1255. filterPluginIds = JSON.parse(content).map(s => s.replace("~", ""));
  1256. }
  1257. this.forEachPlugin(plugin => {
  1258. let id = plugin.id.replace("~", "");
  1259. if(filterPluginIds && !filterPluginIds.includes(id)) {
  1260. return;
  1261. }
  1262. let version = "unknown";
  1263. if(plugin.opts && plugin.opts.about && plugin.opts.about.version) {
  1264. version = plugin.opts.about.version.replace("~", "");
  1265. }
  1266. manifest[id] = version;
  1267. });
  1268. manifest.IdlePixelPlus = IdlePixelPlus.version;
  1269. this.sendCustomMessage(fromPlayer, {
  1270. content: manifest,
  1271. callbackId: callbackId
  1272. });
  1273. return;
  1274. }
  1275.  
  1276. const callbacks = this.customMessageCallbacks;
  1277. if(reply) {
  1278. const handler = callbacks[callbackId];
  1279. if(handler && typeof handler.responseHandler === "function") {
  1280. try {
  1281. if(handler.responseHandler(fromPlayer, content, originalCallbackId)) {
  1282. this.deleteCustomMessageCallback(callbackId);
  1283. }
  1284. }
  1285. catch(err) {
  1286. console.error("Error executing custom message response handler.", {player: fromPlayer, content: content, handler: handler});
  1287. }
  1288. }
  1289. }
  1290. else if(offline) {
  1291. Object.values(callbacks).forEach(handler => {
  1292. try {
  1293. if(handler.player.toLowerCase()==fromPlayer.toLowerCase() && typeof handler.offlineHandler === "function" && handler.offlineHandler(fromPlayer, content)) {
  1294. this.deleteCustomMessageCallback(handler.id);
  1295. }
  1296. }
  1297. catch(err) {
  1298. console.error("Error executing custom message offline handler.", {player: fromPlayer, content: content, handler: handler});
  1299. }
  1300. });
  1301. }
  1302.  
  1303. if(offline) {
  1304. this.onCustomMessagePlayerOffline(fromPlayer, content);
  1305. }
  1306. else {
  1307. this.forEachPlugin((plugin) => {
  1308. if(typeof plugin.onCustomMessageReceived === "function") {
  1309. plugin.onCustomMessageReceived(fromPlayer, content, originalCallbackId);
  1310. }
  1311. });
  1312. }
  1313. }
  1314.  
  1315. onCustomMessagePlayerOffline(fromPlayer, content) {
  1316. if(this.debug) {
  1317. console.log(`IP+ onCustomMessagePlayerOffline`, fromPlayer, content);
  1318. }
  1319. this.forEachPlugin((plugin) => {
  1320. if(typeof plugin.onCustomMessagePlayerOffline === "function") {
  1321. plugin.onCustomMessagePlayerOffline(fromPlayer, content);
  1322. }
  1323. });
  1324. }
  1325.  
  1326. onCombatStart() {
  1327. if(this.debug) {
  1328. console.log(`IP+ onCombatStart`);
  1329. }
  1330. this.forEachPlugin((plugin) => {
  1331. if(typeof plugin.onCombatStart === "function") {
  1332. plugin.onCombatStart();
  1333. }
  1334. });
  1335. }
  1336.  
  1337. onCombatEnd() {
  1338. if(this.debug) {
  1339. console.log(`IP+ onCombatEnd`);
  1340. }
  1341. this.forEachPlugin((plugin) => {
  1342. if(typeof plugin.onCombatEnd === "function") {
  1343. plugin.onCombatEnd();
  1344. }
  1345. });
  1346. }
  1347.  
  1348. onLogin() {
  1349. if(this.debug) {
  1350. console.log(`IP+ onLogin`);
  1351. }
  1352. logFancy("login detected");
  1353. this.forEachPlugin((plugin) => {
  1354. if(typeof plugin.onLogin === "function") {
  1355. plugin.onLogin();
  1356. }
  1357. });
  1358. $("#chat-area").append(`
  1359. <div class="ipp-chat-command-help">
  1360. <span><strong>FYI: </strong> Use the /help command to see information on available chat commands.</span>
  1361. </div>
  1362. `);
  1363. if(Chat._auto_scroll) {
  1364. $("#chat-area").scrollTop($("#chat-area")[0].scrollHeight);
  1365. }
  1366.  
  1367. }
  1368.  
  1369. onVariableSet(key, valueBefore, valueAfter) {
  1370. if(this.debug) {
  1371. console.log(`IP+ onVariableSet "${key}": "${valueBefore}" -> "${valueAfter}"`);
  1372. }
  1373. this.forEachPlugin((plugin) => {
  1374. if(typeof plugin.onVariableSet === "function") {
  1375. plugin.onVariableSet(key, valueBefore, valueAfter);
  1376. }
  1377. });
  1378. if(key == "monster_name") {
  1379. const combatBefore = !!(valueBefore && valueBefore!="none");
  1380. const combatAfter = !!(valueAfter && valueAfter!="none");
  1381. if(!combatBefore && combatAfter) {
  1382. this.onCombatStart();
  1383. }
  1384. else if(combatBefore && !combatAfter) {
  1385. this.onCombatEnd();
  1386. }
  1387. }
  1388. }
  1389.  
  1390. onChat(data) {
  1391. if(this.debug) {
  1392. console.log(`IP+ onChat`, data);
  1393. }
  1394. this.forEachPlugin((plugin) => {
  1395. if(typeof plugin.onChat === "function") {
  1396. plugin.onChat(data);
  1397. }
  1398. });
  1399. }
  1400.  
  1401. onPanelChanged(panelBefore, panelAfter) {
  1402. if(this.debug) {
  1403. console.log(`IP+ onPanelChanged "${panelBefore}" -> "${panelAfter}"`);
  1404. }
  1405. if(panelAfter === "idlepixelplus") {
  1406. this.refreshPanel("idlepixelplus");
  1407. }
  1408. this.forEachPlugin((plugin) => {
  1409. if(typeof plugin.onPanelChanged === "function") {
  1410. plugin.onPanelChanged(panelBefore, panelAfter);
  1411. }
  1412. });
  1413. }
  1414.  
  1415. }
  1416.  
  1417. // Add to window and init
  1418. window.IdlePixelPlusPlugin = IdlePixelPlusPlugin;
  1419. window.IdlePixelPlus = new IdlePixelPlus();
  1420.  
  1421. window.IdlePixelPlus.customChatCommands["help"] = (command, data='') => {
  1422. let help;
  1423. if(data && data!="help") {
  1424. let helpContent = window.IdlePixelPlus.customChatHelp[data.trim()] || "No help content was found for this command.";
  1425. help = `
  1426. <div class="ipp-chat-command-help">
  1427. <strong><u>Command Help:</u></strong><br />
  1428. <strong>/${data}:</strong> <span>${helpContent}</span>
  1429. </div>
  1430. `;
  1431. }
  1432. else {
  1433. help = `
  1434. <div class="ipp-chat-command-help">
  1435. <strong><u>Command Help:</u></strong><br />
  1436. <strong>Available Commands:</strong> <span>${Object.keys(window.IdlePixelPlus.customChatCommands).sort().map(s => "/"+s).join(" ")}</span><br />
  1437. <span>Use the /help command for more information about a specific command: /help &lt;command&gt;</span>
  1438. </div>
  1439. `;
  1440. }
  1441. $("#chat-area").append(help);
  1442. if(Chat._auto_scroll) {
  1443. $("#chat-area").scrollTop($("#chat-area")[0].scrollHeight);
  1444. }
  1445. };
  1446.  
  1447. const SHRUG = "¯\\_(ツ)_/¯";
  1448. window.IdlePixelPlus.registerCustomChatCommand(["shrug", "rshrug"], (command, data='') => {
  1449. data=data.replace(/~/g, " ");
  1450. const margin = SHRUG.length + 1;
  1451. data = data.substring(0, 250-margin);
  1452. window.IdlePixelPlus.sendMessage(`CHAT=${data} ${SHRUG}`);
  1453. }, `Adds a ${SHRUG} to the end of your chat message.<br /><strong>Usage:</strong> /%COMMAND% &lt;message&gt;`);
  1454.  
  1455. window.IdlePixelPlus.registerCustomChatCommand("lshrug", (command, data='') => {
  1456. data=data.replace(/~/g, " ");
  1457. const margin = SHRUG.length + 1;
  1458. data = data.substring(0, 250-margin);
  1459. window.IdlePixelPlus.sendMessage(`CHAT=${SHRUG} ${data}`);
  1460. }, `Adds a ${SHRUG} to the beginning of your chat message.<br /><strong>Usage:</strong> /%COMMAND% &lt;message&gt;`);
  1461.  
  1462. window.IdlePixelPlus.registerCustomChatCommand("clear", (command, data='') => {
  1463. $("#chat-area").empty();
  1464. }, `Clears all messages in chat.`);
  1465.  
  1466.  
  1467. internal.init.call(window.IdlePixelPlus);
  1468.  
  1469. })();