IQRPG Labyrinth Companion

QoL enhancement for IQRPG Labyrinth

  1. // ==UserScript==
  2. // @name IQRPG Labyrinth Companion
  3. // @namespace https://www.iqrpg.com/
  4. // @version 1.1.0
  5. // @author Tempest
  6. // @description QoL enhancement for IQRPG Labyrinth
  7. // @homepage https://slyboots.studio/iqrpg-labyrinth-companion/
  8. // @source https://github.com/SlybootsStudio/iqrpg-labyrinth-companion
  9. // @match https://*.iqrpg.com/*
  10. // @require http://code.jquery.com/jquery-latest.js
  11. // @license unlicense
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. /* global $ */
  16.  
  17.  
  18. const SHOW_ROOM_NUMBER = 1;
  19. const SHOW_ROOM_TIER = 1;
  20. const SHOW_ROOM_CHANCE = 1;
  21. const SHOW_ROOM_ESTIMATE = 1;
  22. const SHOW_ROOM_TOTAL = 1;
  23.  
  24. const MODIFY_NAVIGATION = 1; // 0 - Don't modify nav button, 1 - modify nav button (default)
  25.  
  26.  
  27. const NAVITEM_LABY = 7; // Which nav item to style
  28. const RENDER_DELAY = 200; // milliseconds after view loads to render companion
  29. const SKILL_BOX_INDEX = 3; // Will be 2, or 3, depending on Land status.
  30.  
  31. const MAX_ACTIONS = 100; // This may change in the future if the developer allows an increase.
  32. // If that happens, we'll want to read this from the payload data, and not hard code it.
  33.  
  34. const CACHE_LAB = "cache_lab"; // Cache for all the
  35. const CACHE_LAB_NAV = "cache_lab_nav";
  36.  
  37. //-----------------------------------------------------------------------
  38. //-----------------------------------------------------------------------
  39. //
  40. // CACHE
  41. // We are caching the daily labyrinth,
  42. // which is updated each time to labyrinth page is visited.
  43. // The cached data is used when the labyrinth is being run.
  44. //
  45. function writeCache( key, data ) {
  46. localStorage[key] = JSON.stringify(data);
  47. }
  48.  
  49. function readCache( key ) {
  50. return JSON.parse(localStorage[key] || null) || localStorage[key];
  51. }
  52.  
  53. //-----------------------------------------------------------------------
  54. //-----------------------------------------------------------------------
  55.  
  56. function renderLaby(data, skills) {
  57.  
  58. // Create 3rd column
  59. let wrapper = $('.two-columns');
  60. let col3 = $('.two-columns__column', wrapper).eq(1).clone();
  61. $(wrapper).append(col3);
  62.  
  63. // Cache data
  64. data = JSON.parse(data);
  65. writeCache(CACHE_LAB, data);
  66.  
  67. // On the Laby page, 'currentRoom' refers to the completed rooms.
  68. // On Laby Run, currentRoom is the target room you're TRYING to pass.
  69. data.currentRoom += 1;
  70.  
  71. // Render table
  72. renderLabyTable(data, col3, skills);
  73. writeNavCache(data.turns, data.maxTurns, data.rewardObtained);
  74. }
  75.  
  76. function renderLabyFromCache(data, skills) {
  77.  
  78. let col = $('.labytable');
  79.  
  80. // Check to make sure a Laby table doesn't currently exist.
  81. // If it does, we'll update it.
  82. // If it doesn't, we'll create one.
  83. if( !col.length ) {
  84. let wrapper = $('.main-game-section');
  85. $(wrapper).css('position', 'relative');
  86. col = $('.main-section__body', wrapper).clone();
  87. $(col).css('position', 'absolute');
  88. $(col).css('top', '30px');
  89. $(col).css('right', '0');
  90. $(col).addClass('labytable');
  91. $(wrapper).append(col);
  92. }
  93.  
  94. data = JSON.parse(data);
  95. let cached = readCache(CACHE_LAB);
  96.  
  97. // Use the cached data for the Laby table, but update the `currentRoom` based
  98. // on our most up to date information about player progress.
  99. cached.currentRoom = data?.data?.currentRoom || 0;
  100. renderLabyTable(cached, col, skills);
  101.  
  102. writeNavCache(data.data.turns, data.data.maxTurns, false);
  103. }
  104.  
  105. /**
  106. * Lookup a skill name by ID.
  107. *
  108. * Labyrinth rooms use an ID to reference which
  109. * skill is used.
  110. *
  111. * @param {Number} id
  112. *
  113. * @return {String}
  114. */
  115. function getSkillNameById(id) {
  116.  
  117. let name = "";
  118.  
  119. switch(id) {
  120. case 1: name = "Battling"; break;
  121. case 2: name = "Dungeon"; break;
  122. case 3: name = "Mining"; break;
  123. case 4: name = "Wood"; break;
  124. case 5: name = "Quarry"; break;
  125. case 6: name = "Rune"; break;
  126. case 7: name = "Jewelry"; break;
  127. case 8: name = "Alchemy"; break;
  128. }
  129.  
  130. return name;
  131. }
  132.  
  133. /**
  134. * Lookup a skill name by ID.
  135. *
  136. * Labyrinth rooms use an ID to reference which
  137. * skill is used.
  138. *
  139. * @param {Number} roomId
  140. * @param {Number} currentRoomId
  141. *
  142. * @return {String} style
  143. */
  144. function getStyleByRoomState(roomId, currentRoomId) {
  145.  
  146. let style = "";
  147.  
  148. if(currentRoomId == roomId ) {
  149. // Active Rooms are Yellow Background
  150. // with Black Text
  151. style = "background-color: #cc0; color: #000;";
  152. }
  153.  
  154. // Incomplete Rooms are Red Background
  155. else if(currentRoomId < roomId ) {
  156. style = "background-color: #600;";
  157. }
  158.  
  159. // Completed Rooms are Green Background
  160. else if(currentRoomId > roomId ) {
  161. style = "background-color: #006400;";
  162. }
  163.  
  164. return style;
  165. }
  166.  
  167. /**
  168. * Get the percent chance you have to progress
  169. * through a room, based on the tier of the room
  170. * and skill level.
  171. *
  172. * @param {Number} tier
  173. * @param {Number} skill
  174. *
  175. * @return {Float} chance
  176. */
  177. function getChanceBySkill(tier, skill) {
  178.  
  179. let base = 1;
  180.  
  181. switch(tier) {
  182. case 1: base = 700; break;
  183. case 2: base = 1000; break;
  184. case 3: base = 1400; break;
  185. case 4: base = 1900; break;
  186. case 5: base = 2500; break;
  187. }
  188.  
  189. let chance = 1000 / base * skill;
  190. chance = parseFloat(chance / 10).toFixed(2);
  191.  
  192. return chance;
  193. }
  194.  
  195. /**
  196. * Render the labyrinth table.
  197. *
  198. * @param {Object} data
  199. * @param {domElement} ele
  200. * @param {Array} skills
  201. */
  202. function renderLabyTable(data, ele, skills) {
  203.  
  204. let remaining = data.maxTurns;
  205.  
  206. const trStyle = 'border:1px solid black;';
  207. const tdStyle = 'padding:3px;border-top:1px solid black;';
  208.  
  209. const showNumber = SHOW_ROOM_NUMBER === 1 ? "" : "display:none;";
  210. const showTier = SHOW_ROOM_TIER === 1 ? "" : "display:none;";
  211. const showChance = SHOW_ROOM_CHANCE === 1 ? "" : "display:none;";
  212. const showEstimate = SHOW_ROOM_ESTIMATE === 1 ? "" : "display:none;";
  213. const showTotal = SHOW_ROOM_TOTAL === 1 ? "" : "display:none;";
  214.  
  215. let html = '';
  216. html += '<div style="width:100%;">'
  217. html += '<table style="border-spacing:0;float:right;">';
  218. html += `<tr style='${trStyle}'>`;
  219. html += `<td style='${tdStyle}'>Room</td>`;
  220. html += `<td style='${tdStyle}${showTotal}'>(T)Skill</td>`;
  221. html += `<td style='${tdStyle}${showChance}'>Chance</td>`;
  222. html += `<td style='${tdStyle}${showEstimate}'>Est</td>`;
  223. html += `<td style='${tdStyle}${showTotal}'>Total</td>`;
  224.  
  225. data.data.map( (room, i) => {
  226.  
  227. let skill = room[0];
  228. let tier = room[1];
  229. const style = getStyleByRoomState(i+1, data.currentRoom);
  230. const skillName = getSkillNameById(skill);
  231. const chance = getChanceBySkill(tier, skills[skill-1]);
  232. const estimated = Math.round(100/chance);
  233. remaining -= estimated;
  234.  
  235.  
  236. html += `<tr style='${style};${trStyle}'>`;
  237. html += `<td style='${tdStyle}'>${i+1}</td>`;
  238. html += `<td style='${tdStyle}${showTotal}'>(${tier})${skillName}</td>`;
  239. html += `<td style='${tdStyle}${showChance}'>${chance}%</td>`;
  240. html += `<td style='${tdStyle}${showEstimate}'>${estimated}</td>`;
  241. html += `<td style='${tdStyle}${showTotal}'>${100 - remaining}</td>`;
  242.  
  243. html += `</tr>`;
  244. });
  245.  
  246. html += "</table></div>";
  247.  
  248. ele.html(html);
  249. }
  250.  
  251. /**
  252. * Update the Labyrinth button in the navigation to match the state of progress.
  253. *
  254. * @param {Object} data
  255. * @param {domElement} ele
  256. * @param {Array} skills
  257. */
  258. function updateNavigation() {
  259.  
  260. // Players can disable modification entirely using this setting.
  261. if(!MODIFY_NAVIGATION) {
  262. return;
  263. }
  264.  
  265. // This is the payload we should be pulling out from cache
  266. /*
  267. * data : {
  268. * turns
  269. * maxTurns
  270. * rewardObtained
  271. * date
  272. * }
  273. */
  274. let data = readCache(CACHE_LAB_NAV);
  275.  
  276.  
  277. // We only want to use cached data from today.
  278. // Set to New York Timezone, which matches server time.
  279. let date = new Date();
  280. let _date = date.toLocaleString('en-GB', { timeZone: 'America/New_York' }).split(',')[0];
  281. if( _date != data?.date) {
  282. data = undefined;
  283. }
  284.  
  285. //console.log(_date, data?.date);
  286.  
  287. let link = $('.nav a').eq(NAVITEM_LABY);
  288.  
  289. if(!data) {
  290. // We lack information until the user clicks on Labyrinth
  291. $(link).css('font-weight', 'bold');
  292. $(link).css('color', 'white');
  293. $(link).css('background-color', 'red');
  294. $(link).html("Labyrinth [Need Info]");
  295. } else if (data.turns < data.maxTurns) {
  296. // User has turns remaining
  297. $(link).css('color', 'yellow');
  298. $(link).css('font-weight', 'bold');
  299. $(link).css('background-color', 'red');
  300. $(link).html("Labyrinth [Unfinished]");
  301. } else if(!data.rewardObtained) {
  302. // Labyrinth is complete, but reward unclaimed
  303. $(link).css('color', 'red');
  304. $(link).css('font-weight', 'bold');
  305. $(link).css('background-color', 'white');
  306. $(link).html("Labyrinth [CLAIM LOOT]");
  307. } else if(data.rewardObtained) {
  308. // Labyrinth is complete, Reward claimed
  309. $(link).css('color', '#ccc');
  310. $(link).css('font-weight', 'normal');
  311. $(link).css('background-color', '');
  312. $(link).html("Labyrinth [Complete]");
  313. }
  314. }
  315.  
  316. function writeNavCache(turns, maxTurns, rewardObtained) {
  317.  
  318. let date = new Date();
  319. const _date = date.toLocaleString('en-GB', { timeZone: 'America/New_York' }).split(',')[0];
  320.  
  321. const payload ={
  322. turns : turns ?? 0,
  323. maxTurns : maxTurns ?? MAX_ACTIONS,
  324. rewardObtained : rewardObtained ?? false,
  325. date : _date
  326. }
  327.  
  328. //console.log("Cache", payload);
  329.  
  330. writeCache(CACHE_LAB_NAV, payload);
  331. updateNavigation();
  332. }
  333.  
  334. /**
  335. * Parse the Skill box into an array of your skill levels
  336. *
  337. * This works whether or not the box is collapsed.
  338. *
  339. * @return {Array} skills
  340. */
  341. function parseSkills() {
  342.  
  343. // New players won't have the 'Land' main-section box,
  344. // which appears above 'Skills'. We need to check the text
  345. // To make sure we're in the right box.
  346.  
  347. // For new players, this is skills.
  348. // For land players, this is land.
  349. let text = $('.main-section').eq(SKILL_BOX_INDEX - 1).text();
  350.  
  351. if(!text.includes('Skills')) {
  352. // For new players, this is center content.
  353. // For land players, this is skills.
  354. text = $('.main-section').eq(SKILL_BOX_INDEX).text();
  355. }
  356.  
  357. //
  358. // The html has been converted into a text string.
  359. // Now we parse the string into an array.
  360. text = text.replace('Skills', '');
  361. text = text.replaceAll('(', '');
  362.  
  363. let skills = text.split(')');
  364.  
  365. skills = skills.map( skill => {
  366. // "skillname (level)"
  367. skill = skill.trim();
  368. skill = skill.split(' ');
  369. return skill[1];
  370. });
  371.  
  372. return skills;
  373. }
  374.  
  375. let loadOnce = false;
  376.  
  377. //-----------------------------------------------------------------------
  378. //-----------------------------------------------------------------------
  379.  
  380. let send = window.XMLHttpRequest.prototype.send;
  381.  
  382. function sendReplacement(data) {
  383. if(this.onreadystatechange) {
  384. this._onreadystatechange = this.onreadystatechange;
  385. }
  386.  
  387. this.onreadystatechange = onReadyStateChangeReplacement;
  388. return send.apply(this, arguments);
  389. }
  390.  
  391.  
  392. function onReadyStateChangeReplacement() {
  393.  
  394. //
  395. // This is called anytime there is an action complete, or a view (page) loads.
  396. // console.log('Response URL', this.responseURL);
  397. //
  398. setTimeout( () => {
  399. //
  400. // Player Loading the Labyrinth View
  401. //
  402. if(this.responseURL.includes("php/misc.php?mod=loadLabyrinth")) {
  403. // LoadOnce is tracked because this function can be triggered
  404. // multiple times in a single call.
  405. if(this.response && !loadOnce) {
  406. loadOnce = true;
  407. $('.labytable').remove(); // Remove old table
  408. let skills = parseSkills(); // Scans skill table
  409. renderLaby(this.response, skills); // Render a new table.
  410. }
  411. }
  412. //
  413. // Player is running the Labyrinth
  414. // - We're still on the labyrinth page, but the actions are being executed.
  415. //
  416. else if(this.responseURL.includes("php/actions/labyrinth.php")) {
  417. let skills = parseSkills();
  418. renderLabyFromCache(this.response, skills);
  419. }
  420. //
  421. // Player has claimed the Labyrinth reward
  422. //
  423. else if(this.responseURL.includes("misc.php?mod=getLabyrinthRewards")) {
  424. writeNavCache(MAX_ACTIONS, MAX_ACTIONS, true);
  425. }
  426. //
  427. // We're on another page, elsewhere in IQ
  428. //
  429. else {
  430. // Remove The Labyrinth table.
  431. $('.labytable').remove();
  432. loadOnce = false;
  433. }
  434.  
  435.  
  436.  
  437. }, RENDER_DELAY );
  438.  
  439. //
  440. // Update the navigation to make sure we're showing the latest information.
  441. //
  442. updateNavigation();
  443.  
  444. /* Avoid errors in console */
  445. if(this._onreadystatechange) {
  446. return this._onreadystatechange.apply(this, arguments);
  447. } else {
  448. return this._onreadystatechange;
  449. }
  450. }
  451.  
  452. window.XMLHttpRequest.prototype.send = sendReplacement;