MWI_Expected_Quantity_Helper

对三采和烹饪冲泡,添加一个数量栏显示期望产物数量,也可输入期望数量反推期望采集次数。

// ==UserScript==
// @name         MWI_Expected_Quantity_Helper
// @namespace    http://tampermonkey.net/
// @version      4.1.1
// @description  对三采和烹饪冲泡,添加一个数量栏显示期望产物数量,也可输入期望数量反推期望采集次数。
// @author       zqzhang1996
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @require      https://update.greasyfork.org/scripts/550719/MWI_Toolkit.user.js
// @grant        none
// @run-at       document-body
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // 硬编码加工表
    const processingItems = {
        "milk": "cheese",
        "verdant_milk": "verdant_cheese",
        "azure_milk": "azure_cheese",
        "burble_milk": "burble_cheese",
        "crimson_milk": "crimson_cheese",
        "rainbow_milk": "rainbow_cheese",
        "holy_milk": "holy_cheese",
        "log": "lumber",
        "birch_log": "birch_lumber",
        "cedar_log": "cedar_lumber",
        "purpleheart_log": "purpleheart_lumber",
        "ginkgo_log": "ginkgo_lumber",
        "redwood_log": "redwood_lumber",
        "arcane_log": "arcane_lumber",
        "cotton": "cotton_fabric",
        "flax": "linen_fabric",
        "bamboo_branch": "bamboo_fabric",
        "cocoon": "silk_fabric",
        "radiant_fiber": "radiant_fabric"
    };

    function insertQuantityInput() {
        const skillType = getCurrentSkillType();
        // 仅处理五种情况,其余直接返回
        if (!['milking', 'foraging', 'woodcutting', 'cooking', 'brewing'].includes(skillType)) return;

        const origBlocks = document.querySelectorAll('[class^="SkillActionDetail_maxActionCountInput"]');
        if (origBlocks.length != 1) return; // 大于1说明已经插入过
        const origBlock = origBlocks[0];

        // 获取所有产出物品
        const items = getAllDropItems();
        if (!items.length) return;

        // 对所有产出物品都插入数量栏,并与“次数”栏联动
        let lastBlock = origBlock;
        // 获取“次数”输入框和按钮
        const timesInput = origBlock.querySelector('input');
        const buttons = origBlock.querySelectorAll('button');
        // 联动循环保护
        let linking = false;
        // 保存所有数量栏及其对应的averageCount
        const qtyInputs = [];
        items.forEach((item, idx) => {
            const { newBlock, input: qtyInput } = createQuantityRowBlock(item);
            if (newBlock && qtyInput) {
                lastBlock.parentNode.insertBefore(newBlock, lastBlock.nextSibling);
                lastBlock = newBlock;
                qtyInputs.push({ input: qtyInput, avg: item.averageCount });
            }
        });

        // “次数”栏内容变化时,更新所有“数量”栏
        function updateQtyFromTimes() {
            if (linking) return;
            linking = true;
            let times = timesInput.value;
            if (times === '∞' || times === '' || times === undefined || times === null) {
                qtyInputs.forEach(({ input }) => input.value = '∞');
                linking = false;
                return;
            }
            times = parseTimes(times);
            if (!isFinite(times) || times <= 0) {
                qtyInputs.forEach(({ input }) => input.value = '');
                linking = false;
                return;
            }
            qtyInputs.forEach(({ input, avg }) => {
                const expected = times * (avg || 1);
                if (!isFinite(expected)) {
                    input.value = '∞';
                } else {
                    input.value = Math.round(expected);
                }
            });
            linking = false;
        }
        // “数量”栏变化时,更新“次数”栏和所有数量栏(完全联动)
        function updateTimesFromQty(e) {
            if (linking) return;
            linking = true;
            // 找到当前触发的input
            const idx = qtyInputs.findIndex(q => q.input === e.target);
            if (idx === -1) {
                linking = false;
                return;
            }
            let qty = e.target.value;
            if (qty === '∞' || qty === '' || qty === undefined || qty === null) {
                reactInputTriggerHack(timesInput, '∞');
                qtyInputs.forEach(({ input }) => { if (input !== e.target) input.value = '∞'; });
                linking = false;
                return;
            }
            qty = parseQty(qty);
            if (!isFinite(qty) || qty <= 0) {
                reactInputTriggerHack(timesInput, '');
                qtyInputs.forEach(({ input }) => { if (input !== e.target) input.value = ''; });
                linking = false;
                return;
            }
            const avg = qtyInputs[idx].avg || 1;
            const times = Math.max(Math.ceil(qty / avg - 1e-6), 1);
            reactInputTriggerHack(timesInput, times.toString());
            // 重新计算所有数量栏
            qtyInputs.forEach(({ input, avg: otherAvg }, i) => {
                if (i !== idx) {
                    const expected = times * (otherAvg || 1);
                    input.value = Math.round(expected);
                }
            });
            linking = false;
        }

        // “次数”输入框联动
        timesInput.addEventListener('input', updateQtyFromTimes);
        // “数量”输入框联动
        qtyInputs.forEach(({ input }) => {
            input.addEventListener('input', updateTimesFromQty);
        });
        // 按钮联动监听
        for (const btn of buttons) {
            btn.addEventListener('click', () => {
                setTimeout(() => {
                    updateQtyFromTimes();
                }, 20);
            });
        }
        // 初次填充
        setTimeout(updateQtyFromTimes, 120);
    }

    // 创建一个数量栏,返回 {newBlock, input}
    function createQuantityRowBlock(item) {
        const origBlock = document.querySelector('[class^="SkillActionDetail_maxActionCountInput"]');
        if (!origBlock) return null;

        // 克隆外层div(不带子内容)
        const newBlock = origBlock.cloneNode(false);

        // 图标
        const origIconContainer = document.querySelector('[class^="SkillActionDetail_drop"] [class*="Item_iconContainer"]');
        const iconContainer = origIconContainer.cloneNode(true);
        const svg = iconContainer.querySelector('svg');
        if (svg) {
            svg.setAttribute('aria-label', item.displayName); // 修改 aria-label
            const use = svg.querySelector('use');
            if (use) {
                use.setAttribute('href', item.iconHref); // 修改 href
            }
        }
        const originalActionLabel = document.querySelector('[class^="SkillActionDetail_actionContainer"] [class^="SkillActionDetail_label"]');
        const label = originalActionLabel.cloneNode(false); // 不带原内容
        iconContainer.style.width = window.getComputedStyle(originalActionLabel).width;
        iconContainer.style.height = window.getComputedStyle(originalActionLabel).height;

        if (Object.values(processingItems).includes(item.name)) {
            const tab = iconContainer.cloneNode(false);
            tab.className = 'SkillActionDetail_tab';
            tab.textContent = '┗';
            newBlock.appendChild(tab);
        }

        label.appendChild(iconContainer);
        newBlock.appendChild(label);

        // 输入框
        const origInputWrap = origBlock.querySelector('[class^="SkillActionDetail_input"]');
        const inputWrap = origInputWrap.cloneNode(true);
        const origInput = origInputWrap.querySelector('input');
        const input = inputWrap.querySelector('input');
        input.addEventListener('focus', function () {
            setTimeout(() => {
                input.select();
            }, 0);
        });
        input.addEventListener('keydown', function (e) {
            if (e.key === 'Enter' || e.keyCode === 13) {
                if (origInput) {
                    const event = new KeyboardEvent('keydown', {
                        bubbles: true,
                        cancelable: true,
                        key: 'Enter',
                        code: 'Enter',
                        keyCode: 13,
                        which: 13
                    });
                    origInput.dispatchEvent(event);
                }
            }
        });
        newBlock.appendChild(inputWrap);

        if (!Object.values(processingItems).includes(item.name)) {
            // 快捷填充按钮
            const btns = [
                { val: 1000, txt: '1k' },
                { val: 2000, txt: '2k' },
                { val: 5000, txt: '5k' }
            ];
            const origButtons = origBlock.querySelectorAll('button');
            let buttonClass = '';
            if (origButtons.length > 0) buttonClass = origButtons[0].className;

            btns.forEach(({ val, txt }) => {
                const btn = document.createElement('button');
                btn.className = buttonClass;
                btn.textContent = txt;
                btn.addEventListener('click', () => {
                    input.value = val;
                    input.dispatchEvent(new Event('input', { bubbles: true }));
                });
                newBlock.appendChild(btn);
            });
        }

        return { newBlock, input };
    }

    // 获取所有产出物品,返回数组 [{name, iconHref, averageCount}]
    function getAllDropItems() {
        const skillType = getCurrentSkillType();
        if (!['milking', 'foraging', 'woodcutting', 'cooking', 'brewing'].includes(skillType)) return [];

        const SkillActionDetail = document.querySelector('[class^="SkillActionDetail_outputItems"]') || document.querySelector('[class^="SkillActionDetail_dropTable"]');
        const itemContainers = SkillActionDetail?.querySelectorAll('[class^="Item_itemContainer"]');
        if (!itemContainers || itemContainers.length === 0) return [];

        const drinkSlots = getDrinkSlots();
        const drinkConcentration = getDrinkConcentration();

        const items = [];
        itemContainers.forEach(itemContainer => {
            const iconHref = itemContainer.querySelector('svg use').getAttribute('href');
            const name = iconHref.split('#')[1];

            if (skillType === "cooking" || skillType === "brewing") {
                // 美食茶
                let avg = 1;
                if (drinkSlots.includes("gourmet_tea")) {
                    avg += 0.12 * drinkConcentration;
                }
                items.push({ name, iconHref, averageCount: avg });
                return;
            }

            // 采集/伐木/挤奶
            let dropCount = processingItems.hasOwnProperty(name) ? 2 : (() => {
                const init_client_data = window.MWI_Toolkit_init_client_data;
                const actionDetail = init_client_data?.actionDetailMap?.[`/actions/foraging/${name}`];
                const minCount = actionDetail?.dropTable[0]?.minCount;
                const maxCount = actionDetail?.dropTable[0]?.maxCount;
                return (minCount + maxCount) / 2;
            })();

            // 概率修正
            const probability = itemContainers.length === 1 ? 1 : 1.2 / itemContainers.length;
            dropCount *= probability;

            // 数量加成
            let multiplier = 1.0 + getGatheringQuantity();
            const communityLevel = getCommunityGatheringBuffLevel();
            if (communityLevel) multiplier += 0.20 + (communityLevel - 1) * 0.005;
            if (drinkSlots.includes("gathering_tea")) multiplier += 0.15 * drinkConcentration;

            // 加工茶修正
            let processedMultiplier = 0;
            if (drinkSlots.includes("processing_tea") && processingItems.hasOwnProperty(name)) {
                processedMultiplier = multiplier * 0.15 * drinkConcentration;
                multiplier -= processedMultiplier;
            }

            // 原物品
            items.push({
                name,
                iconHref,
                averageCount: dropCount * multiplier
            });

            // 加工产物
            if (processingItems.hasOwnProperty(name)) {
                items.push({
                    name: processingItems[name],
                    iconHref: iconHref.replace(name, processingItems[name]),
                    averageCount: dropCount * multiplier / 1.8 + dropCount * processedMultiplier / 2
                });
            }
        });
        return items;
    }

    // 获取操作类型
    function getCurrentSkillType() {
        const valDiv = document.querySelector('[class^="SkillActionDetail_value"]');
        if (!valDiv) return null;
        const use = valDiv.querySelector('svg use');
        if (!use) return null;
        const href = use.getAttribute('href') || '';
        const match = href.match(/#([a-zA-Z0-9_]+)$/);
        return match ? match[1] : null;
    }

    // 获取社区采集buff等级
    function getCommunityGatheringBuffLevel() {
        const buffDivs = document.querySelectorAll('[class^="CommunityBuff_communityBuff"]');
        for (const buffDiv of buffDivs) {
            const useEl = buffDiv.querySelector('svg use');
            if (!useEl) continue;
            const href = useEl.getAttribute('href') || '';
            if (href.includes('gathering')) {
                const levelDiv = buffDiv.querySelector('[class^="CommunityBuff_level"]');
                if (levelDiv) {
                    const match = levelDiv.textContent.match(/Lv\.(\d+)/);
                    if (match) return parseInt(match[1], 10);
                }
            }
        }
        return null;
    }

    // 获取茶列表
    function getDrinkSlots() {
        const teaContainers = document.querySelectorAll('[class^="ItemSelector_itemContainer"]');
        if (!teaContainers) return [];
        const teaList = [];
        for (const container of teaContainers) {
            const useEl = container.querySelector('svg use');
            if (useEl) {
                const href = useEl.getAttribute('href') || '';
                const countDiv = container.querySelector('[class^="Item_count"]');
                const count = countDiv ? parseInt(countDiv.textContent.replace(/,/g, ''), 10) : 0;
                if (count > 0) {
                    teaList.push(href.split('#')[1]);
                }
            }
        }
        return teaList;
    }

    // 获取饮料浓度
    function getDrinkConcentration() {
        let drinkConcentration = 1;
        const init_client_data = window.MWI_Toolkit_init_client_data;
        const init_character_data = window.MWI_Toolkit_init_character_data;
        if (init_client_data && init_character_data) {
            const guzzling_pouch = init_character_data.characterItems.find(item => item.itemHrid === "/items/guzzling_pouch");
            if (guzzling_pouch) {
                const enhancementLevel = guzzling_pouch.enhancementLevel || 0;
                drinkConcentration += init_client_data.itemDetailMap?.[`/items/guzzling_pouch`].equipmentDetail.noncombatStats.drinkConcentration
                    + init_client_data.itemDetailMap?.[`/items/guzzling_pouch`].equipmentDetail.noncombatEnhancementBonuses.drinkConcentration
                    * init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel];
            }
        }
        return drinkConcentration;
    }

    // 获取首饰采集数量加成
    function getGatheringQuantity() {
        let gatheringQuantity = 0;
        const init_client_data = window.MWI_Toolkit_init_client_data;
        const init_character_data = window.MWI_Toolkit_init_character_data;
        if (init_client_data && init_character_data) {
            // 检查耳环
            const philosophers_earrings = init_character_data.characterItems.find(item => item.itemHrid === "/items/philosophers_earrings");
            const earrings_of_gathering = init_character_data.characterItems.find(item => item.itemHrid === "/items/earrings_of_gathering");
            if (philosophers_earrings) {
                const enhancementLevel = philosophers_earrings.enhancementLevel || 0;
                gatheringQuantity += init_client_data.itemDetailMap?.[`/items/philosophers_earrings`].equipmentDetail.noncombatStats.gatheringQuantity
                    + init_client_data.itemDetailMap?.[`/items/philosophers_earrings`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity
                    * init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel];
            }
            else if (earrings_of_gathering) {
                const enhancementLevel = earrings_of_gathering.enhancementLevel || 0;
                gatheringQuantity += init_client_data.itemDetailMap?.[`/items/earrings_of_gathering`].equipmentDetail.noncombatStats.gatheringQuantity
                    + init_client_data.itemDetailMap?.[`/items/earrings_of_gathering`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity
                    * init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel];
            }

            // 检查戒指
            const philosophers_ring = init_character_data.characterItems.find(item => item.itemHrid === "/items/philosophers_ring");
            const ring_of_gathering = init_character_data.characterItems.find(item => item.itemHrid === "/items/ring_of_gathering");
            if (philosophers_ring) {
                const enhancementLevel = philosophers_ring.enhancementLevel || 0;
                gatheringQuantity += init_client_data.itemDetailMap?.[`/items/philosophers_ring`].equipmentDetail.noncombatStats.gatheringQuantity
                    + init_client_data.itemDetailMap?.[`/items/philosophers_ring`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity
                    * init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel];
            }
            else if (ring_of_gathering) {
                const enhancementLevel = ring_of_gathering.enhancementLevel || 0;
                gatheringQuantity += init_client_data.itemDetailMap?.[`/items/ring_of_gathering`].equipmentDetail.noncombatStats.gatheringQuantity
                    + init_client_data.itemDetailMap?.[`/items/ring_of_gathering`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity
                    * init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel];
            }
        }
        return gatheringQuantity;
    }

    // React input hack
    function reactInputTriggerHack(inputElem, value) {
        let lastValue = inputElem.value;
        inputElem.value = value;
        let event = new Event("input", { bubbles: true });
        event.simulated = true;
        let tracker = inputElem._valueTracker;
        if (tracker) {
            tracker.setValue(lastValue);
        }
        inputElem.dispatchEvent(event);
    }

    function parseTimes(val) {
        if (val === '∞' || val === '' || val === undefined || val === null) return Infinity;
        return parseInt(val.replace(/[^0-9]/g, ''), 10) || 0;
    }

    function parseQty(val) {
        if (val === '' || val === undefined || val === null) return 0;
        if (val === '∞') return Infinity;
        return parseInt(val.replace(/[^0-9]/g, ''), 10) || 0;
    }

    observePanel();
    setTimeout(insertQuantityInput, 500);

    // 监听页面变化
    function observePanel() {
        let lastPanel = null;
        const observer = new MutationObserver(() => {
            const panel = document.querySelector('[class^="SkillActionDetail_content"]');
            if (panel && panel !== lastPanel) {
                lastPanel = panel;
                setTimeout(insertQuantityInput, 100);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

})();