IQRPG Dungeon Companion

QoL enhancement for IQRPG Dungeons

目前为 2024-06-19 提交的版本。查看 最新版本

// ==UserScript==
// @name         IQRPG Dungeon Companion
// @namespace    https://www.iqrpg.com/
// @version      0.4.0
// @author       Tempest
// @description  QoL enhancement for IQRPG Dungeons
// @homepage     https://slyboots.studio/iqrpg-dungeon-companion/
// @source       https://github.com/SlybootsStudio/iqrpg-dungeon-companion
// @match        https://*.iqrpg.com/*
// @require      http://code.jquery.com/jquery-latest.js
// @license      unlicense
// @grant        none
// ==/UserScript==

/* global $ */

/*
 * Special thanks to Ciomegu for formula feedback
 */

//-----------------------------------------------------------------------
// Config
//-----------------------------------------------------------------------




/*              Tokens, Index, Name */
const TOKENS = [20, // 0 - Goblin
                25, // 1 - Mountain
                30, // 2 - Tomb
                35, // 3 - Lair
                40, // 4 - Ruins
                40, // 5 - Tower
                50, // 6 - Cells
                60, // 7 - Hall
               100, // 8 - Vault
               150];// 9 - Treasury

/**
 * Identify which key is used for the estimates on the Token Store
 * To change, use the Index (0-9) from the list above.
 */
const INDEX_FOR_ESTIMATE = 0; // Default, 1 - Goblin Key

/**
 * Modify the token store page to show additional keys estimates.
 */
const MODIFY_TOKEN_STORE = 1; // 1 - Yes, 0 - No

/**
 * Used to store the Dungeon Keeper stats when you navigate from
 * the Personnel page, to the Dungeons and Token Store page.
 */
const CACHE_DUNGEON = "cache_dungeon"; // Cache for all the


const RENDER_DELAY = 100; // Delay for modifying the page.


const RARITY_BONUS = 5; // Bonus (percent) per Rarity Level.
const MAX_BONUS = 1.65; // 65% is the maximum for bonus tokens.

//-----------------------------------------------------------------------
// CACHE
//-----------------------------------------------------------------------
/* We are caching the Dungeon Keeper stats,
 * which are updated each time the personnel page is visited.
 */
function writeCache( key, data ) {
  localStorage[key] = JSON.stringify(data);
}

function readCache( key ) {
  return JSON.parse(localStorage[key] || null) || localStorage[key];
}


//-----------------------------------------------------------------------
// Util
//-----------------------------------------------------------------------

/**
 * Bonus percent by level
 */
function bonusByLevel(level) {
    if(level < 50) return 0;
    if(level < 75) return 2;
    if(level < 100) return 5;
    if(level < 125) return 10;
    if(level < 150) return 20;

    return 40;
}

/**
 * Bonus percent by rarity
 */
function bonusTokens(rarity, level) {
    let percent = 0;
    rarity -= 1; // Rarity 1 (Common) doesn't apply any bonus, so we subtract it.
    percent += rarity * RARITY_BONUS; // Each rarity level after common.
    percent += bonusByLevel(level);

    return percent;
}

/**
 * The multiplier used in the total token calculation
 */
function bonusMultiplier() {

    const dkStats = readCache(CACHE_DUNGEON);

    const rarity = dkStats?.rarity || 0;
    const level = dkStats?.level || 0;
    let bonusPercent = bonusTokens(rarity, level);
    
    if(bonusPercent < 0) {
        bonusPercent = 0;
    }
    const bonusMultiplier = (1 + bonusPercent / 100);
    
    return bonusMultiplier;
}


/**
 * Used to debounce DOM modifications
 */
let loadDungeonsOnce = false;
let loadPersonnelOnce = false;
let loadTokenStoreOnce = false;

function onReadyStateChangeReplacement(e) {
    //
    // This is called anytime there is an action complete, or a view (page) loads.
    // console.log('Response URL', this.responseURL);
    //


    /**
     * Remove the message at the top of the Dungeon page, only if we've left the Dungeon page.
     */
    let accordians = $('.main-game-section .main-section__body .accordian');
    if(accordians.length !== TOKENS.length) { // Each dungeon has a accordion.
      $('.removeMe').remove();
    }

    setTimeout( () => {

        /**
         * Token Store page.
         *
         * Modify the table to show how many remaining keys need to be used, with various
         * amounts of bonus tokens applied.
         */
        if(e.responseURL.includes("php/store.php?mod=tokenStore")) {

            /**
             * User has disabled this modification in the config above.
             * Don't modify the page.
             */
            if(!MODIFY_TOKEN_STORE) {
                return;
            }


            if(e.response && !loadTokenStoreOnce) {

                loadTokenStoreOnce = true;


                /**
                 * Get current Dungeoneering Tokens
                 * [0] is the Boss Tokens, [1] is the Jewel Slots
                 */
                let header = $('.main-game-section .main-section__body .heading')[1]; // Jewel Slots header
                let currentTokens = $(header).next().text(); // Get siblining element below the header
                    currentTokens = currentTokens.replace('[Dungeoneering Tokens]: ', ''); // Remove text
                    currentTokens = currentTokens.replaceAll(',', ''); // Remove commas
                    currentTokens = parseInt(currentTokens); // Convert to int


                /**
                 * Modify the Jewel Slots table.
                 * [0] is the Boss Tokens, [1] is the Jewel Slots
                 */
                let table = $('.main-game-section .main-section__body table')[1];
                const trs = $('tr', table);

                let bonus = Math.round((bonusMultiplier() - 1) * 100);

                /**
                 * Extend table headers
                 */
                $(trs[0]).append(`<td class='text-rarity-1'>No Bonus</td>`);
                $(trs[0]).append(`<td class='text-rarity-1'>Current Bonus (${bonus}%)</td>`);
                $(trs[0]).append(`<td class='text-rarity-1'>Max Bonus (65%)</td>`);

                /**
                 * Extend the rows
                 */
                for(let i = 1; i < trs.length; i += 1) { // Skipping header
                    let td = $('td', trs[i]);
                    let tokens = $(td[2]).text(); // Type [0], Current/Max [1], Tokens [2]
                    tokens = tokens.replaceAll(',', ''); // Remove commas
                    tokens = parseInt(tokens); // Convert to int

                    tokens -= currentTokens; // Account for acrued tokens

                    if(tokens < 0) tokens = 0; // Avoid this delightful error.

                    const keys = Math.ceil(tokens / TOKENS[INDEX_FOR_ESTIMATE]);
                    const keysBonus = Math.ceil(tokens / Math.ceil(TOKENS[INDEX_FOR_ESTIMATE] * bonusMultiplier())); //1.x
                    const keysMax = Math.ceil(tokens / Math.ceil(TOKENS[INDEX_FOR_ESTIMATE] * MAX_BONUS)); // 1.65
                    $(trs[i]).append(`<td>${keys.toLocaleString()}</td>`);
                    $(trs[i]).append(`<td>${keysBonus.toLocaleString()}</td>`);
                    $(trs[i]).append(`<td>${keysMax.toLocaleString()}</td>`);
                    // toLocaleString() adds commas, or periods depending on locale.
                }

                /**
                 * Add a note to the page, explaining the extra columns.
                 */
                $(table).after("<br/><p>The `No`, `Current`, and `Max` numbers represent the keys to run to buy the slot.</p>");

            }
        } else {
            loadTokenStoreOnce = false;
        }

        /**
         * Personnel page.
         *
         * Read and cache the dungeon keeper information.
         */
        if(e.responseURL.includes("php/land.php?mod=loadPersonnel")) {
            if(e.response && !loadPersonnelOnce) {

                loadPersonnelOnce = true;

                const dkStats = JSON.parse(e.response).personnel.dungeon_keeper;
                const payload = {
                    level: dkStats.level, rarity : dkStats.rarity
                };

                writeCache(CACHE_DUNGEON, payload);
            }
        } else {
            loadPersonnelOnce = false;
        }

        /**
         * Dungeons page.
         *
         * Display the total tokens you could earn from each type of key, and total.
         */
        if(e.responseURL.includes("php/areas.php?mod=loadDungeons")) {

            if(e.response && !loadDungeonsOnce) {

                $('.removeMe').remove(); // Remove a previous one

                loadDungeonsOnce = true;

                const dungeonsCount = $('.accordian__item').length;

                let grandTotal = 0;

                for(let i = 0; i < dungeonsCount; i +=1) {
                    /**
                     * Find the amount of keys for each dungeon
                     */
                    let acc = $('.accordian__item')[i]; // Accordion
                    let rightContent = $('div', acc)[1]; // [0] - Dungeon Name, [1] - Amount and Key
                    let amount = $(rightContent).text().split('x ')[0]; // Amountx [Key Name] -- Get Amount
                    const total = amount * Math.ceil(TOKENS[i] * bonusMultiplier()); // total tokens

                    /**
                     * Modify existing DOM element with total tokens available.
                     */
                    $(rightContent).prepend(`<span class='text-rarity-1'>${total.toLocaleString()} tokens</span> - `);

                    grandTotal += total; // add total to running grand total
                }

                let header = `<span class='removeMe'>`;
                 /**
                  * Usually we're modifying an element which is re-rendered.
                  * In this case, we're not. So we need to remove it ourselves.
                  */

                /**
                 * Read from cache, set defaults in case Personnel page has not been visited.
                 */
                const dkStats = readCache(CACHE_DUNGEON);
                const rarity = dkStats?.rarity || 0;
                const level = dkStats?.level || 0; // Untrained, it would be level 1.
                const bonusPercent = bonusTokens(rarity, level);


                if(!level) {
                    /**
                     * User has not visited the Personnel page yet.
                     */
                    header += `<p>Userscript not synced with your <span class='green-text'>Dungeon Keeper</span>. `;
                    header += `Visit your Personnel to update the token calculation.</p><br/>`;
                } else {
                    /**
                     * Display the cached Dungeon Keeper stats.
                     */
                    header += `<p>Your <span class='text-rarity-${rarity}'>Dungeon Keeper</span> (Level <span class='green-text'>${level}</span>)</span> - ${bonusPercent}% Bonus</p></br>`;
                }

                /**
                 * Display the grand total amount of tokens.
                 */
                header += `<p><b><span class='text-rarity-1'>${grandTotal.toLocaleString()} tokens</b></span> total.</p><br/></span>`;

                $('.main-game-section .main-section__body').prepend(header);

            }
        } else {
            loadDungeonsOnce = false;
        }

    }, RENDER_DELAY );
}

//-----------------------------------------------------------------------
// HTTP Request Override -- DO NOT EDIT
//-----------------------------------------------------------------------
let send = window.XMLHttpRequest.prototype.send;

function sendReplacement() {
    let old = this.onreadystatechange;

    this.onreadystatechange = () => {
        onReadyStateChangeReplacement(this);
        if(old) {
            old();
        }
    }

    return send.apply(this, arguments);
}

window.XMLHttpRequest.prototype.send = sendReplacement;