ICKeyTool

铁牛碎片耗时计算

// ==UserScript==
// @name         ICKeyTool
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  铁牛碎片耗时计算
// @license      MIT
// @match        https://*/MWICombatSimulatorTest/*
// @match        https://www.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @author       Ratatatata
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    let initData_actionDetailMap = null;
    let initData_levelExperienceTable = null;
    let initData_actionCategoryDetailMap = null;
    let initData_abilityDetailMap = null;
    let initData_itemDetailMap = null;
    let initData_actionTypeDrinkSlotsMap = null;
    let initData_characterItems = null;
    let initData_characterSkills = null;
    let initData_characterHouseRoomMap = null;


    let itemEnNameToHridMap = {};

    let config = {
        //生活装备效率读不到,默认+5
        itemEffiBuff: 11.2,
        //20级采集buff
        GatheringQuantityBuff:29.5,
        //20级效率buff
        ProductionEfficiencyBuff:19.7
    }

    let language = "zh";
    const FragmentList=["蓝色钥匙碎片","绿色钥匙碎片","紫色钥匙碎片","白色钥匙碎片","橙色钥匙碎片","棕色钥匙碎片","石头钥匙碎片","黑暗钥匙碎片","燃烧钥匙碎片",
              "Blue Key Fragment","Green Key Fragment","Purple Key Fragment","White Key Fragment","Orange Key Fragment","Brown Key Fragment","Stone Key Fragment","Dark Key Fragment","Burning Key Fragment"];
    const CATEGORIES = {
        "Fragment": ["蓝色钥匙碎片","绿色钥匙碎片","紫色钥匙碎片","白色钥匙碎片","橙色钥匙碎片","棕色钥匙碎片","石头钥匙碎片","黑暗钥匙碎片","燃烧钥匙碎片",
              "Blue Key Fragment","Green Key Fragment","Purple Key Fragment","White Key Fragment","Orange Key Fragment","Brown Key Fragment","Stone Key Fragment","Dark Key Fragment","Burning Key Fragment"],
    };

    function calculateCost(){
        let result = "-";
        const inputTime = document.getElementById('inputSimulationTime');
        //总模拟时长
        const simTime = Number(inputTime.value);

        //每小时消耗品数量
        const consumablesUsedList = getConsumablesUsed();
        console.log(consumablesUsedList);
        //消耗品制作耗时 min
        const consumablesUsedCost=getConsumablesUsedCost(consumablesUsedList);

        const drop = getDropsData();
        console.log('drops:', getDropsData());
        if(drop.length>0){
            //总碎片掉落
            const dropFragmentNumber=drop[0].numericValue;
            result=(60+consumablesUsedCost)*simTime/dropFragmentNumber;
            result=result.toFixed(2);
        }else{
            result="-"
        }
        const realProfitDiv = document.getElementById('realCostDisplay');
        realProfitDiv.setAttribute("i18n-data", `: ${result}`);
        applyi18n();
    }

    function getConsumablesUsedCost(consumablesUsedList){
        let totalCost = 0;
        consumablesUsedList.forEach(item => {
            const SingelCost=getSingelMaterialsCost(item.englishName,item.quantity,false);
            console.log(`制作${item.name}耗时:${SingelCost.toFixed(2)}分钟`)
            totalCost += SingelCost;
        });
        return totalCost;
    }

    //单位min
    function getSingelMaterialsCost(material,quantity,teaLoop=false){
        if(material.includes('Tea Leaf')||material.includes('Crushed')||material.includes('Essence')) return 0;
        //console.log("当前计算材料:",material);
        //console.log("当前计算数量:",quantity);
        let costTime=0;
        let itemName=material;
        //是否烧饭
        const isProduction =
                initData_actionDetailMap[getActionHridFromItemName(itemName)].inputItems &&
                initData_actionDetailMap[getActionHridFromItemName(itemName)].inputItems.length > 0;

        const actionHrid = getActionHridFromItemName(itemName);
        // 茶效率
        const teaBuffs = getTeaBuffsByActionHrid(actionHrid);
        // 原料信息
        let inputItems = [];
        let totalResourcesPerAction = 0;

        if (isProduction) {
            inputItems = JSON.parse(JSON.stringify(initData_actionDetailMap[actionHrid].inputItems));
            for (const item of inputItems) {
                totalResourcesPerAction += getSingelMaterialsCost(initData_itemDetailMap[item.itemHrid].name,item.count,teaLoop);
            }
            // 茶减少原料消耗
            const lessResourceBuff = teaBuffs.lessResource;
            totalResourcesPerAction *= 1 - lessResourceBuff / 100;
        }

        // 消耗饮料
        let drinksConsumedPerHourCost = 0;
        const drinksList = initData_actionTypeDrinkSlotsMap[initData_actionDetailMap[actionHrid].type];
        for (const drink of drinksList) {
            if (!drink || !drink.itemHrid) {
                continue;
            }
            if(!teaLoop)drinksConsumedPerHourCost += getSingelMaterialsCost(initData_itemDetailMap[drink.itemHrid].name,12,true);
        }

        // 每小时动作数(包含工具缩减动作时间)
        const baseTimePerActionSec = initData_actionDetailMap[actionHrid].baseTimeCost / 1000000000;
        const toolPercent = getToolsSpeedBuffByActionHrid(actionHrid);
        const actualTimePerActionSec = baseTimePerActionSec / (1 + toolPercent / 100);

        let actionPerHour = 3600 / actualTimePerActionSec;

        // 每小时产品数
        let droprate = null;
        if (isProduction) {
            droprate = initData_actionDetailMap[actionHrid].outputItems[0].count;
        } else {
            droprate =
                (initData_actionDetailMap[actionHrid].dropTable[0].minCount + initData_actionDetailMap[actionHrid].dropTable[0].maxCount) / 2;
        }
        let itemPerHour = actionPerHour * droprate;

        // 等级碾压提高效率(人物等级不及最低要求等级时,按最低要求等级计算)
        const requiredLevel = initData_actionDetailMap[actionHrid].levelRequirement.level;
        let currentLevel = requiredLevel;
        for (const skill of initData_characterSkills) {
            if (skill.skillHrid === initData_actionDetailMap[actionHrid].levelRequirement.skillHrid) {
                currentLevel = skill.level;
                break;
            }
        }
        const levelEffBuff = currentLevel - requiredLevel > 0 ? currentLevel - requiredLevel : 0;
        // 房子效率
        const houseEffBuff = getHousesEffBuffByActionHrid(actionHrid);
        // 特殊装备效率
        const itemEffiBuff = config.itemEffiBuff;
        // 总效率影响动作数/生产物品数
        actionPerHour *= 1 + (levelEffBuff + houseEffBuff + teaBuffs.efficiency + itemEffiBuff + config.ProductionEfficiencyBuff) / 100;
        itemPerHour *= 1 + (levelEffBuff + houseEffBuff + teaBuffs.efficiency + itemEffiBuff + config.ProductionEfficiencyBuff) / 100;
        let extraFreeItemPerHour=0
        if (isProduction){
            // 茶额外产品数量(不消耗原料)
            extraFreeItemPerHour = (itemPerHour * teaBuffs.quantity) / 100;
        }else{
            // 茶额外产品数量+社区buff
            extraFreeItemPerHour = (itemPerHour * (teaBuffs.quantity+config.GatheringQuantityBuff) ) / 100;
        }
        costTime=quantity*60.0/(itemPerHour+extraFreeItemPerHour)+drinksConsumedPerHourCost;
        //console.log("当前计算材料:",material);
        //console.log("当前计算数量:",quantity);
        //console.log("当前计算耗时:",costTime);
        return costTime;
    }
    const actionHridToHouseNamesMap = {
        "/action_types/brewing": "/house_rooms/brewery",
        "/action_types/cheesesmithing": "/house_rooms/forge",
        "/action_types/cooking": "/house_rooms/kitchen",
        "/action_types/crafting": "/house_rooms/workshop",
        "/action_types/foraging": "/house_rooms/garden",
        "/action_types/milking": "/house_rooms/dairy_barn",
        "/action_types/tailoring": "/house_rooms/sewing_parlor",
        "/action_types/woodcutting": "/house_rooms/log_shed",
        "/action_types/alchemy": "/house_rooms/laboratory",
    };
    function getHousesEffBuffByActionHrid(actionHrid) {
        const houseName = actionHridToHouseNamesMap[initData_actionDetailMap[actionHrid].type];
        if (!houseName) {
            return 0;
        }
        const house = initData_characterHouseRoomMap[houseName];
        if (!house) {
            return 0;
        }
        return house.level * 1.5;
    }
    const actionHridToToolsSpeedBuffNamesMap = {
        "/action_types/brewing": "brewingSpeed",
        "/action_types/cheesesmithing": "cheesesmithingSpeed",
        "/action_types/cooking": "cookingSpeed",
        "/action_types/crafting": "craftingSpeed",
        "/action_types/foraging": "foragingSpeed",
        "/action_types/milking": "milkingSpeed",
        "/action_types/tailoring": "tailoringSpeed",
        "/action_types/woodcutting": "woodcuttingSpeed",
        "/action_types/alchemy": "alchemySpeed",
    };
    const itemEnhanceLevelToBuffBonusMap = {
        0: 0,
        1: 2,
        2: 4.2,
        3: 6.6,
        4: 9.2,
        5: 12.0,
        6: 15.0,
        7: 18.2,
        8: 21.6,
        9: 25.2,
        10: 29.0,
        11: 33.0,
        12: 37.2,
        13: 41.6,
        14: 46.2,
        15: 51.0,
        16: 56.0,
        17: 61.2,
        18: 66.6,
        19: 72.2,
        20: 78.0,
    };
    function getToolsSpeedBuffByActionHrid(actionHrid) {
        let totalBuff = 0;
        for (const item of initData_characterItems) {
            if (item.itemLocationHrid.includes("_tool")) {
                const buffName = actionHridToToolsSpeedBuffNamesMap[initData_actionDetailMap[actionHrid].type];
                const enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
                const buff = initData_itemDetailMap[item.itemHrid].equipmentDetail.noncombatStats[buffName] || 0;
                totalBuff += buff * enhanceBonus;
            }
        }
        return Number(totalBuff * 100).toFixed(1);
    }
    function getTeaBuffsByActionHrid(actionHrid) {
        const teaBuffs = {
            efficiency: 0, // Efficiency tea, specific teas, -Artisan tea.
            quantity: 0, // Gathering tea, Gourmet tea.
            lessResource: 0, // Artisan tea.
            extraExp: 0, // Wisdom tea. Not used.
            upgradedProduct: 0, // Processing tea. Not used.
        };

        const actionTypeId = initData_actionDetailMap[actionHrid].type;
        const teaList = initData_actionTypeDrinkSlotsMap[actionTypeId];
        for (const tea of teaList) {
            if (!tea || !tea.itemHrid) {
                continue;
            }

            for (const buff of initData_itemDetailMap[tea.itemHrid].consumableDetail.buffs) {
                if (buff.typeHrid === "/buff_types/artisan") {
                    teaBuffs.lessResource += buff.flatBoost * 100;
                } else if (buff.typeHrid === "/buff_types/action_level") {
                    teaBuffs.efficiency -= buff.flatBoost;
                } else if (buff.typeHrid === "/buff_types/gathering") {
                    teaBuffs.quantity += buff.flatBoost * 100;
                } else if (buff.typeHrid === "/buff_types/gourmet") {
                    teaBuffs.quantity += buff.flatBoost * 100;
                } else if (buff.typeHrid === "/buff_types/wisdom") {
                    teaBuffs.extraExp += buff.flatBoost * 100;
                } else if (buff.typeHrid === "/buff_types/processing") {
                    teaBuffs.upgradedProduct += buff.flatBoost * 100;
                } else if (buff.typeHrid === "/buff_types/efficiency") {
                    teaBuffs.efficiency += buff.flatBoost * 100;
                } else if (buff.typeHrid === `/buff_types/${actionTypeId.replace("/action_types/", "")}_level`) {
                    teaBuffs.efficiency += buff.flatBoost;
                }
            }
        }

        return teaBuffs;
    }


    const ConsumablesTranslations = {
        "itemNames./items/donut": "Donut",
        "itemNames./items/blueberry_donut": "Blueberry Donut",
        "itemNames./items/blackberry_donut": "Blackberry Donut",
        "itemNames./items/strawberry_donut": "Strawberry Donut",
        "itemNames./items/mooberry_donut": "Mooberry Donut",
        "itemNames./items/marsberry_donut": "Marsberry Donut",
        "itemNames./items/spaceberry_donut": "Spaceberry Donut",
        "itemNames./items/cupcake": "Cupcake",
        "itemNames./items/blueberry_cake": "Blueberry Cake",
        "itemNames./items/blackberry_cake": "Blackberry Cake",
        "itemNames./items/strawberry_cake": "Strawberry Cake",
        "itemNames./items/mooberry_cake": "Mooberry Cake",
        "itemNames./items/marsberry_cake": "Marsberry Cake",
        "itemNames./items/spaceberry_cake": "Spaceberry Cake",
        "itemNames./items/gummy": "Gummy",
        "itemNames./items/apple_gummy": "Apple Gummy",
        "itemNames./items/orange_gummy": "Orange Gummy",
        "itemNames./items/plum_gummy": "Plum Gummy",
        "itemNames./items/peach_gummy": "Peach Gummy",
        "itemNames./items/dragon_fruit_gummy": "Dragon Fruit Gummy",
        "itemNames./items/star_fruit_gummy": "Star Fruit Gummy",
        "itemNames./items/yogurt": "Yogurt",
        "itemNames./items/apple_yogurt": "Apple Yogurt",
        "itemNames./items/orange_yogurt": "Orange Yogurt",
        "itemNames./items/plum_yogurt": "Plum Yogurt",
        "itemNames./items/peach_yogurt": "Peach Yogurt",
        "itemNames./items/dragon_fruit_yogurt": "Dragon Fruit Yogurt",
        "itemNames./items/star_fruit_yogurt": "Star Fruit Yogurt",

        "itemNames./items/stamina_coffee": "Stamina Coffee",
        "itemNames./items/intelligence_coffee": "Intelligence Coffee",
        "itemNames./items/defense_coffee": "Defense Coffee",
        "itemNames./items/attack_coffee": "Attack Coffee",
        "itemNames./items/melee_coffee": "Melee Coffee",
        "itemNames./items/ranged_coffee": "Ranged Coffee",
        "itemNames./items/magic_coffee": "Magic Coffee",
        "itemNames./items/super_stamina_coffee": "Super Stamina Coffee",
        "itemNames./items/super_intelligence_coffee": "Super Intelligence Coffee",
        "itemNames./items/super_defense_coffee": "Super Defense Coffee",
        "itemNames./items/super_attack_coffee": "Super Attack Coffee",
        "itemNames./items/super_melee_coffee": "Super Melee Coffee",
        "itemNames./items/super_ranged_coffee": "Super Ranged Coffee",
        "itemNames./items/super_magic_coffee": "Super Magic Coffee",
        "itemNames./items/ultra_stamina_coffee": "Ultra Stamina Coffee",
        "itemNames./items/ultra_intelligence_coffee": "Ultra Intelligence Coffee",
        "itemNames./items/ultra_defense_coffee": "Ultra Defense Coffee",
        "itemNames./items/ultra_attack_coffee": "Ultra Attack Coffee",
        "itemNames./items/ultra_melee_coffee": "Ultra Melee Coffee",
        "itemNames./items/ultra_ranged_coffee": "Ultra Ranged Coffee",
        "itemNames./items/ultra_magic_coffee": "Ultra Magic Coffee",
        "itemNames./items/wisdom_coffee": "Wisdom Coffee",
        "itemNames./items/lucky_coffee": "Lucky Coffee",
        "itemNames./items/swiftness_coffee": "Swiftness Coffee",
        "itemNames./items/channeling_coffee": "Channeling Coffee",
        "itemNames./items/critical_coffee": "Critical Coffee"
    }
    function getConsumablesEnglishNames(items) {
        return items.map(item => {
            const englishName = ConsumablesTranslations[item.i18nKey] || item.displayName;
            return {
                ...item,
                englishName: englishName
            };
        });
    }

    function getConsumablesUsed(){
        const container = document.getElementById('simulationResultConsumablesUsed');
        const rows = container.querySelectorAll('.row');
        const materials = [];
        rows.forEach(row => {
            const nameElement = row.querySelector('.col-md-6:first-child');
            const quantityElement = row.querySelector('.col-md-6:last-child');
            const name = nameElement.textContent.trim();
            const quantity = parseInt(quantityElement.textContent.trim(), 10);
            const i18nKey = nameElement.getAttribute('data-i18n');
            materials.push({
                i18nKey: i18nKey,
                name: name,
                quantity: quantity
            });

        });
        return getConsumablesEnglishNames(materials);
    }

    function getDropsData() {
        const drops = Array.from(document.querySelectorAll('#noRngDrops .row')).map(row => {
            const name = row.querySelector('.col-md-6:first-child').textContent.trim();
            const value = row.querySelector('.col-md-6:last-child').textContent.trim();
            return {
                name: name,
                rawValue: value,
                numericValue: parseFloat(value.replace(/,/g, '')) || 0
            };
        });
        const filteredData = drops.filter(item => {
            return FragmentList.includes(item.name);
        });
        return filteredData;
    }

    const i18nText = {
        "en": {
            "i18n-realCostTitle": "Real No RNG Fragment Cost",
            "i18n-realCostUnit": " mins/fragment",
        },
        "zh": {
            "i18n-realCostTitle": "实际碎片期望",
            "i18n-realCostUnit": " 分钟/片",
        }
    }
    function addi18nListener() {
        setTimeout(() => {
            const i18nBtnList = document.querySelector("body > div.language-switcher").children;
            i18nBtnList[0].addEventListener("click", () => {
                language = "en";
                applyi18n();
            });
            i18nBtnList[1].addEventListener("click", () => {
                language = "zh";
                applyi18n();
            });
        }, 1000);
    }
    function applyi18n() {
        const i18nElements = document.querySelectorAll("[i18n-id]");
        i18nElements.forEach(element => {
            const i18nId = element.getAttribute("i18n-id");
            let text = "";
            if (i18nText[language] && i18nText[language][i18nId]) {
                text = i18nText[language][i18nId];
            } else {
                console.warn(`Missing translation for ${i18nId} in language ${language}`);
            }
            if (element.getAttribute("i18n-data")) {
                text += element.getAttribute("i18n-data");
            }
            if (element.getAttribute("i18n-data-unit")) {
                text += i18nText[language][element.getAttribute("i18n-data-unit")];
            }
            element.textContent = text;
        });
    }
    function addRealCostBlock() {
        const realProfitDiv = document.createElement('div');
        realProfitDiv.id = 'realCostDisplay';
        realProfitDiv.style.backgroundColor = '#FFD700';
        realProfitDiv.style.color = 'black';
        realProfitDiv.style.fontWeight = 'bold';
        realProfitDiv.style.padding = '4px';
        realProfitDiv.setAttribute("i18n-id", "i18n-realCostTitle");
        realProfitDiv.setAttribute("i18n-data", ":");
        realProfitDiv.setAttribute("i18n-data-unit", "i18n-realCostUnit");

        const targetDiv = document.getElementById('noRngProfitPreview').parentElement.parentElement;
        targetDiv.parentNode.insertBefore(realProfitDiv, targetDiv.nextSibling);

    }
    function getActionHridFromItemName(name) {
        let newName = name.replace("Milk", "Cow");
        newName = newName.replace("Log", "Tree");
        newName = newName.replace("Cowing", "Milking");
        newName = newName.replace("Rainbow Cow", "Unicow");
        newName = newName.replace("Collector's Boots", "Collectors Boots");
        newName = newName.replace("Knight's Aegis", "Knights Aegis");
        if (!initData_actionDetailMap) {
            console.error("getActionHridFromItemName no initData_actionDetailMap: " + name);
            return null;
        }
        for (const action of Object.values(initData_actionDetailMap)) {
            if (action.name === newName) {
                return action.hrid;
            }
        }
        return null;
    }

    function hookWS() {
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        const oriGet = dataProperty.get;

        dataProperty.get = hookedGet;
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);

        function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
                return oriGet.call(this);
            }

            const message = oriGet.call(this);
            Object.defineProperty(this, "data", { value: message }); // Anti-loop

            return handleMessage(message);
        }
    }
    function handleMessage(message) {
        let obj = JSON.parse(message);
        if (obj && obj.type === "init_character_data") {
            GM_setValue("init_character_data", message);
        }
        return message;
    }
    function init() {
        if (document.URL.includes("milkywayidle.com")) {
            GM_setValue("init_client_data", localStorage.getItem("initClientData"));
            hookWS();
        }else{
            try{
                const init_client_data = JSON.parse(GM_getValue("init_client_data"));
                console.log("=init_client_data==",init_client_data);
                initData_actionDetailMap = init_client_data.actionDetailMap;
                initData_levelExperienceTable = init_client_data.levelExperienceTable;
                initData_itemDetailMap = init_client_data.itemDetailMap;
                initData_actionCategoryDetailMap = init_client_data.actionCategoryDetailMap;
                initData_abilityDetailMap = init_client_data.abilityDetailMap;
                for (const [key, value] of Object.entries(initData_itemDetailMap)) {
                    itemEnNameToHridMap[value.name] = key;
                }
                const init_character_data = JSON.parse(GM_getValue("init_character_data"));
                console.log("=init_character_data==",init_character_data);
                initData_actionTypeDrinkSlotsMap = init_character_data.actionTypeDrinkSlotsMap;
                initData_characterItems = init_character_data.characterItems;
                initData_characterSkills = init_character_data.characterSkills;
                initData_characterHouseRoomMap = init_character_data.characterHouseRoomMap;

                addi18nListener();
                addRealCostBlock();
                language = localStorage.getItem("i18nextLng") || "en";
                const obConfig = { characterData: true, subtree: true, childList: true };
                const ProfitNode = document.getElementById('noRngProfitPreview');
                new MutationObserver(() => { calculateCost(); }).observe(ProfitNode, obConfig);

            } catch (e) {
                console.warn("先打开游戏获取数据,open the game first");
                return;
            }
        }
    }
    init();
})();