Real Profit Calculator

Calculates real NoRNG profit and exp gain for MWI battle emulator.

目前為 2025-05-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Real Profit Calculator
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Calculates real NoRNG profit and exp gain for MWI battle emulator.
// @author       guch8017
// @match        https://shykai.github.io/MWICombatSimulatorTest/dist/
// @license      MIT
// ==/UserScript==
 
(function () {
    'use strict';
 
    let globalBuffConfig = {
        expLevel: 20,
        battleDropLevel: 20,
        mooPass: true
    }
 
    const eliteMapExpBonus = {
        "/actions/combat/smelly_planet_elite": 0.1,
        "/actions/combat/swamp_planet_elite": 0.1,
        "/actions/combat/aqua_planet_elite": 0.1,
        "/actions/combat/jungle_planet_elite": 0.1,
        "/actions/combat/gobo_planet_elite": 0.15,
        "/actions/combat/planet_of_the_eyes_elite": 0.15,
        "/actions/combat/sorcerers_tower_elite": 0.15,
        "/actions/combat/bear_with_it_elite": 0.15,
        "/actions/combat/golem_cave_elite": 0.2,
        "/actions/combat/twilight_zone_elite": 0.2,
        "/actions/combat/infernal_abyss_elite": 0.2,
    }
 
    const enhancementLevelTotalMultiplierTable = [0, 1, 2.1, 3.3, 4.6, 6, 7.5, 9.1, 10.8, 12.600000000000001, 14.500000000000002, 16.5, 18.6, 20.8, 23.1, 25.5, 28, 30.6, 33.300000000000004, 36.1, 39];
 
    function parseNumber(text) {
        const numericText = text.replace(/[^\d.,\-]/g, '').replace(/,/g, '');
        return parseFloat(numericText);
    }
 
    function getDrinkConcentration(level) {
        return 0.1 + 0.002 * (enhancementLevelTotalMultiplierTable[level] || 0);
    }
 
    function getWisdomNeckBonus(level) {
        return 0.03 + 0.003 * (enhancementLevelTotalMultiplierTable[level] || 0);
    }
 
    function getBuffRate(level) {
        if (level === 0) return 0;
        return 0.2 + 0.005 * (level - 1);
    }
 
    async function calculateProfit() {
        const expenseText = document.getElementById('script_expense')?.textContent || '';
        const noRngProfitText = document.getElementById('noRngProfitPreview')?.textContent || '';
 
        const expense = parseNumber(expenseText);
        const noRngProfit = parseNumber(noRngProfitText);
        const buffRate = getBuffRate(globalBuffConfig.battleDropLevel);
 
        if (!isNaN(expense) && !isNaN(noRngProfit)) {
            const realProfit = ((noRngProfit + expense) * (1 + buffRate)) - expense;
            const formattedProfit = realProfit.toLocaleString(undefined, { maximumFractionDigits: 3 });
 
            const existingDiv = document.getElementById('realProfitDisplay');
            if (existingDiv) {
                existingDiv.textContent = `实际期望利润: ${formattedProfit}`;
                return;
            }
 
            const displayDiv = document.createElement('div');
            displayDiv.id = 'realProfitDisplay';
            displayDiv.style.backgroundColor = '#FFD700';
            displayDiv.style.color = 'black';
            displayDiv.style.fontWeight = 'bold';
            displayDiv.style.padding = '4px';
            displayDiv.textContent = `实际期望利润: ${formattedProfit}`;
 
            const targetDiv = document.getElementById('noRngProfitPreview').parentElement.parentElement;
            targetDiv.parentNode.insertBefore(displayDiv, targetDiv.nextSibling);
        }
    }
 
    function addRealExpBlock() {
        // "<div class="row"><b data-i18n="common:simulationResults.xpPerHour">每小时经验值</b></div>"
        let realExpTitle = document.createElement('div');
        realExpTitle.className = "row";
        realExpTitle.innerHTML = `<b id="realExpGainTitle">实际每小时经验值</b>`;
        // <div id="simulationResultExperienceGain" class="mb-2" style="background-color: rgb(205, 255, 221); color: black;"><div class="row"><div class="col-md-6" data-i18n="common:total">合计</div><div class="col-md-6 text-end">2299</div></div><div class="row"><div class="col-md-6" data-i18n="leaderboardCategoryNames.stamina">耐力</div><div class="col-md-6 text-end">448</div></div><div class="row"><div class="col-md-6" data-i18n="leaderboardCategoryNames.attack">攻击</div><div class="col-md-6 text-end">655</div></div><div class="row"><div class="col-md-6" data-i18n="leaderboardCategoryNames.power">力量</div><div class="col-md-6 text-end">655</div></div><div class="row"><div class="col-md-6" data-i18n="leaderboardCategoryNames.defense">防御</div><div class="col-md-6 text-end">541</div></div></div>
        let realExpDiv = document.createElement('div');
        realExpDiv.id = "simulationResultExperienceGainReal";
        realExpDiv.className = "mb-2";
        realExpDiv.style.backgroundColor = "rgb(205, 255, 221)";
        realExpDiv.style.color = "black";
        const targetDiv = document.getElementById('simulationResultExperienceGain');
        targetDiv.parentNode.insertBefore(realExpDiv, targetDiv.nextSibling);
        targetDiv.parentNode.insertBefore(realExpTitle, targetDiv.nextSibling);
    }
 
    function addCommunityBuffDialog() {
        const dialogHtml = `
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">社区增益</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" id="buttonCloseCommBuff1"></button>
                    </div>
                    <div class="modal-body">
                        <div class="container-fluid">
                            <div id="commBuffList">
                                <div class="row mb-2">
                                    <div class="col-md-4 offset-md-3 align-self-center">经验等级</div>
                                    <div class="col-md-3">
                                        <input class="form-control" type="number" placeholder="0" min="0" max="20" step="1" id="commExpLevel">
                                    </div>
                                </div>
                                <div class="row mb-2">
                                    <div class="col-md-4 offset-md-3 align-self-center">战斗掉落等级</div>
                                    <div class="col-md-3">
                                        <input class="form-control" type="number" placeholder="0" min="0" max="20" step="1" id="commBattleDropLevel">
                                    </div>
                                </div>
                                <div class="row mb-2">
                                    <div class="col-md-4 offset-md-3 align-self-center">哞卡</div>
                                    <div class="col-md-3">
                                        <input class="form-check-input" type="checkbox" id="commMooPass">
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="buttonCloseCommBuff2">关闭</button>
                    </div>
                </div>
            </div>
        `;
        const dialogDiv = document.createElement('div');
        dialogDiv.className = "modal";
        dialogDiv.id = "communityBuffModal";
        dialogDiv.tabIndex = "-1";
        dialogDiv.style.display = "none";
        dialogDiv.ariaHidden = "true";
        dialogDiv.innerHTML = dialogHtml;
        const targetDiv = document.getElementById('houseRoomsModal');
        targetDiv.parentNode.insertBefore(dialogDiv, targetDiv.nextSibling);
        // <button id="buttonImportExport" class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#importExportModal" data-i18n="common:controls.importExport">导入/导出</button>
        const button = document.createElement('button');
        button.id = "buttonCommunityBuff";
        button.className = "btn btn-primary";
        button.type = "button";
        button.textContent = "社区增益";
        button.onclick = showCommunityBuffDialog;
        const buttonDiv = document.createElement('div');
        buttonDiv.className = "col-md-auto";
        buttonDiv.appendChild(button);
        const targetButton = document.getElementById('buttonImportExport');
        targetButton.parentNode.parentNode.insertBefore(buttonDiv, targetButton.parentNode.nextSibling);
        document.getElementById('buttonCloseCommBuff1').onclick = hideCommunityBuffDialog;
        document.getElementById('buttonCloseCommBuff2').onclick = hideCommunityBuffDialog;
    }
 
    function saveBuffStatus() {
        const expLevel = parseInt(document.getElementById('commExpLevel').value);
        const battleDropLevel = parseInt(document.getElementById('commBattleDropLevel').value);
        const mooPass = document.getElementById('commMooPass').checked;
        globalBuffConfig.expLevel = expLevel;
        globalBuffConfig.battleDropLevel = battleDropLevel;
        globalBuffConfig.mooPass = mooPass;
        localStorage.setItem("rpc_buff_config", JSON.stringify(globalBuffConfig));
    }
 
    function loadBuffStatus() {
        const buffConfig = localStorage.getItem("rpc_buff_config");
        if (buffConfig) {
            let { expLevel, battleDropLevel, mooPass } = JSON.parse(buffConfig);
            if (expLevel === undefined) expLevel = 20;
            if (battleDropLevel === undefined) battleDropLevel = 20;
            if (mooPass === undefined) mooPass = true;
 
            document.getElementById('commExpLevel').value = expLevel;
            document.getElementById('commBattleDropLevel').value = battleDropLevel;
            document.getElementById('commMooPass').checked = mooPass;
 
            globalBuffConfig.expLevel = expLevel;
            globalBuffConfig.battleDropLevel = battleDropLevel;
            globalBuffConfig.mooPass = mooPass;
        } else {
            document.getElementById('commExpLevel').value = 20;
            document.getElementById('commBattleDropLevel').value = 20;
            document.getElementById('commMooPass').checked = true;
        }
    }
 
    function showCommunityBuffDialog() {
        const dialog = document.getElementById('communityBuffModal');
        if (dialog) {
            dialog.style.display = "block";
            dialog.className = "modal show";
            dialog.ariaModal = "true";
            dialog.role = "dialog";
            dialog.removeAttribute("aria-hidden");
            document.body.classList.add("modal-open");
            document.body.style.overflow = "hidden";
            const modalBackdrop = document.createElement('div');
            modalBackdrop.className = "modal-backdrop show";
            document.body.appendChild(modalBackdrop);
        } else {
            console.error("Dialog not found");
        }
    }
 
    function hideCommunityBuffDialog() {
        const dialog = document.getElementById('communityBuffModal');
        if (dialog) {
            saveBuffStatus();
            dialog.style.display = "none";
            dialog.className = "modal";
            dialog.removeAttribute("aria-modal");
            dialog.removeAttribute("role");
            document.body.classList.remove("modal-open");
            document.body.style.overflow = "";
            const modalBackdrop = document.querySelector('.modal-backdrop');
            if (modalBackdrop) {
                modalBackdrop.remove();
            }
        } else {
            console.error("Dialog not found");
        }
    }
 
    function clearRealExp() {
        const simulationResultExperienceGain = document.getElementById('simulationResultExperienceGainReal');
        if (simulationResultExperienceGain) {
            simulationResultExperienceGain.innerHTML = "";
        }
    }
 
    function addRealExp(key, value) {
        const simulationResultExperienceGain = document.getElementById('simulationResultExperienceGainReal');
        if (simulationResultExperienceGain) {
            const row = document.createElement('div');
            row.className = "row";
            const col1 = document.createElement('div');
            col1.className = "col-md-6";
            col1.textContent = key;
            const col2 = document.createElement('div');
            col2.className = "col-md-6 text-end";
            col2.textContent = value;
            row.appendChild(col1);
            row.appendChild(col2);
            simulationResultExperienceGain.appendChild(row);
        }
    }
 
    async function calculateExp() {
        let expBonus = 0;
        // 判断精英地图或地下城加成
        const isDungeon = document.getElementById("simDungeonToggle").checked;
        const mapSelected = document.querySelector("#selectZone").value;
        const isEliteMap = mapSelected.includes("elite");
        if (isDungeon) {
            expBonus = 0.2;
        }
        else if (isEliteMap) {
            let mapBonus = eliteMapExpBonus[mapSelected];
            if (mapBonus === undefined) {
                console.error(`No bonus found for elite map: ${mapSelected}`);
            } else {
                expBonus = mapBonus;
            }
        }
        // 判断咖啡加成,同时计算暴饮带来的额外增益
        let coffeeBonus = 0;
        for (let drinkId = 0; drinkId < 3; drinkId++) {
            const drink = document.querySelector(`#selectDrink_${drinkId}`);
            if (drink && drink.value === "/items/wisdom_coffee") {
                coffeeBonus += 0.12;
            }
        }
        const pouchSel = document.querySelector("#selectEquipment_pouch");
        if (pouchSel && pouchSel.value === "/items/guzzling_pouch") {
            const pouchEnhanceLv = Number.parseInt(document.querySelector("#inputEquipmentEnhancementLevel_pouch").value) || 0;
            const pouchConcentration = getDrinkConcentration(pouchEnhanceLv);
            coffeeBonus *= pouchConcentration;
        }
        expBonus += coffeeBonus;
        // 判断经验项链加成
        const neckSel = document.querySelector("#selectEquipment_neck").value;
        if (neckSel === "/items/necklace_of_wisdom" || neckSel === "/items/philosophers_necklace") {
            const neckEnhanceLv = Number.parseInt(document.querySelector("#inputEquipmentEnhancementLevel_neck").value) || 0;
            expBonus += getWisdomNeckBonus(neckEnhanceLv);
        }
        // 判断房屋加成
        let houseTotalLv = 0;
        for (const houseLv of document.querySelectorAll('input[data-house-hrid]')) {
            houseTotalLv += Number.parseInt(houseLv.value) || 0;
        }
        expBonus += houseTotalLv * 0.0005;
        // 读取经验值
        const expRateBonus = getBuffRate(globalBuffConfig.expLevel);
        const mooPassBonus = globalBuffConfig.mooPass ? 0.05 : 0;
        clearRealExp();
        for (let node of document.querySelector("#simulationResultExperienceGain").childNodes) {
            let node0 = node.childNodes[1];
            let expCurrent = Number.parseInt(node0.innerText);
            let expNew = Math.floor(expCurrent / (1 + expBonus) * (1 + expBonus + expRateBonus + mooPassBonus));
            addRealExp(node.childNodes[0].innerText, expNew);
        }
        let descText = "";
        if (globalBuffConfig.expLevel > 0) descText += `社区经验等级: ${globalBuffConfig.expLevel}`;
        if (globalBuffConfig.mooPass) {
            if (descText.length > 0) descText += ", ";
            descText += "哞卡";
        }
        if (descText.length > 0) {
            descText = `(${descText})`;
        }
        document.getElementById("realExpGainTitle").textContent = `实际每小时经验值 ${descText}`;
    }
 
    const obConfig = {characterData: true, subtree: true, childList: true};
    const ProfitNode = document.getElementById('noRngProfitPreview');
    const ExpNode = document.getElementById('simulationResultExperienceGain');
 
    addRealExpBlock();
    addCommunityBuffDialog();
    loadBuffStatus();
 
    new MutationObserver(() => {calculateProfit();}).observe(ProfitNode, obConfig);
    const expObserver = new MutationObserver(() => {
        calculateExp();
    });
    expObserver.observe(ExpNode, obConfig)
})();