- // ==UserScript==
- // @name IQRPG Labyrinth Companion
- // @namespace https://www.iqrpg.com/
- // @version 1.1.0
- // @author Tempest
- // @description QoL enhancement for IQRPG Labyrinth
- // @homepage https://slyboots.studio/iqrpg-labyrinth-companion/
- // @source https://github.com/SlybootsStudio/iqrpg-labyrinth-companion
- // @match https://*.iqrpg.com/*
- // @require http://code.jquery.com/jquery-latest.js
- // @license unlicense
- // @grant none
- // ==/UserScript==
-
- /* global $ */
-
-
- const SHOW_ROOM_NUMBER = 1;
- const SHOW_ROOM_TIER = 1;
- const SHOW_ROOM_CHANCE = 1;
- const SHOW_ROOM_ESTIMATE = 1;
- const SHOW_ROOM_TOTAL = 1;
-
- const MODIFY_NAVIGATION = 1; // 0 - Don't modify nav button, 1 - modify nav button (default)
-
-
- const NAVITEM_LABY = 7; // Which nav item to style
- const RENDER_DELAY = 200; // milliseconds after view loads to render companion
- const SKILL_BOX_INDEX = 3; // Will be 2, or 3, depending on Land status.
-
- const MAX_ACTIONS = 100; // This may change in the future if the developer allows an increase.
- // If that happens, we'll want to read this from the payload data, and not hard code it.
-
- const CACHE_LAB = "cache_lab"; // Cache for all the
- const CACHE_LAB_NAV = "cache_lab_nav";
-
- //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
- //
- // CACHE
- // We are caching the daily labyrinth,
- // which is updated each time to labyrinth page is visited.
- // The cached data is used when the labyrinth is being run.
- //
- function writeCache( key, data ) {
- localStorage[key] = JSON.stringify(data);
- }
-
- function readCache( key ) {
- return JSON.parse(localStorage[key] || null) || localStorage[key];
- }
-
- //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
-
- function renderLaby(data, skills) {
-
- // Create 3rd column
- let wrapper = $('.two-columns');
- let col3 = $('.two-columns__column', wrapper).eq(1).clone();
- $(wrapper).append(col3);
-
- // Cache data
- data = JSON.parse(data);
- writeCache(CACHE_LAB, data);
-
- // On the Laby page, 'currentRoom' refers to the completed rooms.
- // On Laby Run, currentRoom is the target room you're TRYING to pass.
- data.currentRoom += 1;
-
- // Render table
- renderLabyTable(data, col3, skills);
- writeNavCache(data.turns, data.maxTurns, data.rewardObtained);
- }
-
- function renderLabyFromCache(data, skills) {
-
- let col = $('.labytable');
-
- // Check to make sure a Laby table doesn't currently exist.
- // If it does, we'll update it.
- // If it doesn't, we'll create one.
- if( !col.length ) {
- let wrapper = $('.main-game-section');
- $(wrapper).css('position', 'relative');
- col = $('.main-section__body', wrapper).clone();
- $(col).css('position', 'absolute');
- $(col).css('top', '30px');
- $(col).css('right', '0');
- $(col).addClass('labytable');
- $(wrapper).append(col);
- }
-
- data = JSON.parse(data);
- let cached = readCache(CACHE_LAB);
-
- // Use the cached data for the Laby table, but update the `currentRoom` based
- // on our most up to date information about player progress.
- cached.currentRoom = data?.data?.currentRoom || 0;
- renderLabyTable(cached, col, skills);
-
- writeNavCache(data.data.turns, data.data.maxTurns, false);
- }
-
- /**
- * Lookup a skill name by ID.
- *
- * Labyrinth rooms use an ID to reference which
- * skill is used.
- *
- * @param {Number} id
- *
- * @return {String}
- */
- function getSkillNameById(id) {
-
- let name = "";
-
- switch(id) {
- case 1: name = "Battling"; break;
- case 2: name = "Dungeon"; break;
- case 3: name = "Mining"; break;
- case 4: name = "Wood"; break;
- case 5: name = "Quarry"; break;
- case 6: name = "Rune"; break;
- case 7: name = "Jewelry"; break;
- case 8: name = "Alchemy"; break;
- }
-
- return name;
- }
-
- /**
- * Lookup a skill name by ID.
- *
- * Labyrinth rooms use an ID to reference which
- * skill is used.
- *
- * @param {Number} roomId
- * @param {Number} currentRoomId
- *
- * @return {String} style
- */
- function getStyleByRoomState(roomId, currentRoomId) {
-
- let style = "";
-
- if(currentRoomId == roomId ) {
- // Active Rooms are Yellow Background
- // with Black Text
- style = "background-color: #cc0; color: #000;";
- }
-
- // Incomplete Rooms are Red Background
- else if(currentRoomId < roomId ) {
- style = "background-color: #600;";
- }
-
- // Completed Rooms are Green Background
- else if(currentRoomId > roomId ) {
- style = "background-color: #006400;";
- }
-
- return style;
- }
-
- /**
- * Get the percent chance you have to progress
- * through a room, based on the tier of the room
- * and skill level.
- *
- * @param {Number} tier
- * @param {Number} skill
- *
- * @return {Float} chance
- */
- function getChanceBySkill(tier, skill) {
-
- let base = 1;
-
- switch(tier) {
- case 1: base = 700; break;
- case 2: base = 1000; break;
- case 3: base = 1400; break;
- case 4: base = 1900; break;
- case 5: base = 2500; break;
- }
-
- let chance = 1000 / base * skill;
- chance = parseFloat(chance / 10).toFixed(2);
-
- return chance;
- }
-
- /**
- * Render the labyrinth table.
- *
- * @param {Object} data
- * @param {domElement} ele
- * @param {Array} skills
- */
- function renderLabyTable(data, ele, skills) {
-
- let remaining = data.maxTurns;
-
- const trStyle = 'border:1px solid black;';
- const tdStyle = 'padding:3px;border-top:1px solid black;';
-
- const showNumber = SHOW_ROOM_NUMBER === 1 ? "" : "display:none;";
- const showTier = SHOW_ROOM_TIER === 1 ? "" : "display:none;";
- const showChance = SHOW_ROOM_CHANCE === 1 ? "" : "display:none;";
- const showEstimate = SHOW_ROOM_ESTIMATE === 1 ? "" : "display:none;";
- const showTotal = SHOW_ROOM_TOTAL === 1 ? "" : "display:none;";
-
- let html = '';
- html += '<div style="width:100%;">'
- html += '<table style="border-spacing:0;float:right;">';
- html += `<tr style='${trStyle}'>`;
- html += `<td style='${tdStyle}'>Room</td>`;
- html += `<td style='${tdStyle}${showTotal}'>(T)Skill</td>`;
- html += `<td style='${tdStyle}${showChance}'>Chance</td>`;
- html += `<td style='${tdStyle}${showEstimate}'>Est</td>`;
- html += `<td style='${tdStyle}${showTotal}'>Total</td>`;
-
- data.data.map( (room, i) => {
-
- let skill = room[0];
- let tier = room[1];
- const style = getStyleByRoomState(i+1, data.currentRoom);
- const skillName = getSkillNameById(skill);
- const chance = getChanceBySkill(tier, skills[skill-1]);
- const estimated = Math.round(100/chance);
- remaining -= estimated;
-
-
- html += `<tr style='${style};${trStyle}'>`;
- html += `<td style='${tdStyle}'>${i+1}</td>`;
- html += `<td style='${tdStyle}${showTotal}'>(${tier})${skillName}</td>`;
- html += `<td style='${tdStyle}${showChance}'>${chance}%</td>`;
- html += `<td style='${tdStyle}${showEstimate}'>${estimated}</td>`;
- html += `<td style='${tdStyle}${showTotal}'>${100 - remaining}</td>`;
-
- html += `</tr>`;
- });
-
- html += "</table></div>";
-
- ele.html(html);
- }
-
- /**
- * Update the Labyrinth button in the navigation to match the state of progress.
- *
- * @param {Object} data
- * @param {domElement} ele
- * @param {Array} skills
- */
- function updateNavigation() {
-
- // Players can disable modification entirely using this setting.
- if(!MODIFY_NAVIGATION) {
- return;
- }
-
- // This is the payload we should be pulling out from cache
- /*
- * data : {
- * turns
- * maxTurns
- * rewardObtained
- * date
- * }
- */
- let data = readCache(CACHE_LAB_NAV);
-
-
- // We only want to use cached data from today.
- // Set to New York Timezone, which matches server time.
- let date = new Date();
- let _date = date.toLocaleString('en-GB', { timeZone: 'America/New_York' }).split(',')[0];
- if( _date != data?.date) {
- data = undefined;
- }
-
- //console.log(_date, data?.date);
-
- let link = $('.nav a').eq(NAVITEM_LABY);
-
- if(!data) {
- // We lack information until the user clicks on Labyrinth
- $(link).css('font-weight', 'bold');
- $(link).css('color', 'white');
- $(link).css('background-color', 'red');
- $(link).html("Labyrinth [Need Info]");
- } else if (data.turns < data.maxTurns) {
- // User has turns remaining
- $(link).css('color', 'yellow');
- $(link).css('font-weight', 'bold');
- $(link).css('background-color', 'red');
- $(link).html("Labyrinth [Unfinished]");
- } else if(!data.rewardObtained) {
- // Labyrinth is complete, but reward unclaimed
- $(link).css('color', 'red');
- $(link).css('font-weight', 'bold');
- $(link).css('background-color', 'white');
- $(link).html("Labyrinth [CLAIM LOOT]");
- } else if(data.rewardObtained) {
- // Labyrinth is complete, Reward claimed
- $(link).css('color', '#ccc');
- $(link).css('font-weight', 'normal');
- $(link).css('background-color', '');
- $(link).html("Labyrinth [Complete]");
- }
- }
-
- function writeNavCache(turns, maxTurns, rewardObtained) {
-
- let date = new Date();
- const _date = date.toLocaleString('en-GB', { timeZone: 'America/New_York' }).split(',')[0];
-
- const payload ={
- turns : turns ?? 0,
- maxTurns : maxTurns ?? MAX_ACTIONS,
- rewardObtained : rewardObtained ?? false,
- date : _date
- }
-
- //console.log("Cache", payload);
-
- writeCache(CACHE_LAB_NAV, payload);
- updateNavigation();
- }
-
- /**
- * Parse the Skill box into an array of your skill levels
- *
- * This works whether or not the box is collapsed.
- *
- * @return {Array} skills
- */
- function parseSkills() {
-
- // New players won't have the 'Land' main-section box,
- // which appears above 'Skills'. We need to check the text
- // To make sure we're in the right box.
-
- // For new players, this is skills.
- // For land players, this is land.
- let text = $('.main-section').eq(SKILL_BOX_INDEX - 1).text();
-
- if(!text.includes('Skills')) {
- // For new players, this is center content.
- // For land players, this is skills.
- text = $('.main-section').eq(SKILL_BOX_INDEX).text();
- }
-
- //
- // The html has been converted into a text string.
- // Now we parse the string into an array.
- text = text.replace('Skills', '');
- text = text.replaceAll('(', '');
-
- let skills = text.split(')');
-
- skills = skills.map( skill => {
- // "skillname (level)"
- skill = skill.trim();
- skill = skill.split(' ');
- return skill[1];
- });
-
- return skills;
- }
-
- let loadOnce = false;
-
- //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
-
- let send = window.XMLHttpRequest.prototype.send;
-
- function sendReplacement(data) {
- if(this.onreadystatechange) {
- this._onreadystatechange = this.onreadystatechange;
- }
-
- this.onreadystatechange = onReadyStateChangeReplacement;
- return send.apply(this, arguments);
- }
-
-
- function onReadyStateChangeReplacement() {
-
- //
- // This is called anytime there is an action complete, or a view (page) loads.
- // console.log('Response URL', this.responseURL);
- //
- setTimeout( () => {
- //
- // Player Loading the Labyrinth View
- //
- if(this.responseURL.includes("php/misc.php?mod=loadLabyrinth")) {
- // LoadOnce is tracked because this function can be triggered
- // multiple times in a single call.
- if(this.response && !loadOnce) {
- loadOnce = true;
- $('.labytable').remove(); // Remove old table
- let skills = parseSkills(); // Scans skill table
- renderLaby(this.response, skills); // Render a new table.
- }
- }
- //
- // Player is running the Labyrinth
- // - We're still on the labyrinth page, but the actions are being executed.
- //
- else if(this.responseURL.includes("php/actions/labyrinth.php")) {
- let skills = parseSkills();
- renderLabyFromCache(this.response, skills);
- }
- //
- // Player has claimed the Labyrinth reward
- //
- else if(this.responseURL.includes("misc.php?mod=getLabyrinthRewards")) {
- writeNavCache(MAX_ACTIONS, MAX_ACTIONS, true);
- }
- //
- // We're on another page, elsewhere in IQ
- //
- else {
- // Remove The Labyrinth table.
- $('.labytable').remove();
- loadOnce = false;
- }
-
-
-
- }, RENDER_DELAY );
-
- //
- // Update the navigation to make sure we're showing the latest information.
- //
- updateNavigation();
-
- /* Avoid errors in console */
- if(this._onreadystatechange) {
- return this._onreadystatechange.apply(this, arguments);
- } else {
- return this._onreadystatechange;
- }
- }
-
- window.XMLHttpRequest.prototype.send = sendReplacement;