MWI三采吃喝期望数量助手

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

当前为 2025-09-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         MWI三采吃喝期望数量助手
// @namespace    http://tampermonkey.net/
// @version      2.4.2
// @description  对三采和烹饪冲泡,添加一个数量栏显示期望产物数量,也可输入期望数量反推期望采集次数。
// @author       zqzhang1996
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        none
// @run-at       document-body
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // 判断操作类型
    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;
    }

    // 判断操作类型
    function getCurrentActionName() {
        const valDiv = document.querySelector('[class^="SkillActionDetail_name"]');
        if (!valDiv) return null;
        return valDiv.textContent.trim();
    }

    // 判断是否有采集茶(有才加15%),通过ItemSelector_itemContainer下查找
    function hasGatheringTea() {
        const teaContainers = document.querySelectorAll('[class^="ItemSelector_itemContainer"]');
        for (const container of teaContainers) {
            const teaSvg = container.querySelector('svg[aria-label="采集茶"]');
            if (teaSvg) {
                const countDiv = container.querySelector('[class^="Item_count"]');
                const count = countDiv ? parseInt(countDiv.textContent.replace(/,/g, ''), 10) : 0;
                if (count > 0) return true;
            }
        }
        return false;
    }
    // 判断是否有美食茶
    function hasGourmetTea() {
        const teaContainers = document.querySelectorAll('[class^="ItemSelector_itemContainer"]');
        for (const container of teaContainers) {
            const teaSvg = container.querySelector('svg[aria-label="美食茶"]');
            if (teaSvg) {
                const countDiv = container.querySelector('[class^="Item_count"]');
                const count = countDiv ? parseInt(countDiv.textContent.replace(/,/g, ''), 10) : 0;
                if (count > 0) return true;
            }
        }
        return false;
    }
    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 getBuffPercent() {
        // 判断操作类型
        const skillType = getCurrentSkillType();
        if (skillType === 'milking' || skillType === 'foraging' || skillType === 'woodcutting') {
            let total = 0;
            if (hasGatheringTea()) total += 0.15;
            const communityLevel = getCommunityGatheringBuffLevel();
            if (communityLevel) {
                total += 0.20 + (communityLevel - 1) * 0.005;
            }
            return total;
        } else if (skillType === 'cooking' || skillType === 'brewing') {
            if (hasGourmetTea()) {
                return 0.12;
            } else {
                return 0;
            }
        } else {
            return null; // 其他类型不处理
        }
    }

    // 获取采集区间(原始整数),返回 {rawMin, rawMax, minShow, maxShow, buff}
    function getGatherRangeWithRaw() {
        const skillType = getCurrentSkillType();

        if (skillType === 'cooking' || skillType === 'brewing') {
            const buff = getBuffPercent();
            if (buff === null) return null;
            return {rawMin: 1, rawMax: 1, minShow: 1 * (1 + buff), maxShow: 1 * (1 + buff), buff};
        }
        if (skillType === 'milking' || skillType === 'foraging' || skillType === 'woodcutting') {
            const dropTable = document.querySelector('[class^="SkillActionDetail_dropTable"]');
            if (!dropTable) return null;
            const drop = dropTable.querySelector('[class^="SkillActionDetail_drop"]');
            if (!drop) return null;
            const numDiv = drop.querySelector(':scope > div:first-child');
            if (!numDiv) return null;
            const txt = numDiv.textContent.trim();
            let minShow = 0, maxShow = 0;
            if (txt.includes('-')) {
                let [a, b] = txt.split('-').map(s => parseFloat(s));
                minShow = a;
                maxShow = b;
            } else {
                minShow = maxShow = parseFloat(txt);
            }
            const buff = getBuffPercent();
            if (buff === null) return null;
            const rawMin = Math.round(minShow / (1 + buff));
            const rawMax = Math.round(maxShow / (1 + buff));
            return {rawMin, rawMax, minShow, maxShow, buff};
        }
        // 其他类型不处理
        return null;
    }
    function getTotalRangeFromTimes(times) {
        const range = getGatherRangeWithRaw();
        if (!range) return {minTotal: '', maxTotal: '', expected: ''};
        const {rawMin, rawMax, buff} = range;
        const minTotal = Math.floor(times * rawMin * (1 + buff));
        const maxTotal = Math.floor(times * rawMax * (1 + buff));
        const expected = ((rawMin + rawMax) / 2) * times * (1 + buff);
        return {minTotal, maxTotal, expected};
    }
    function getTimesFromQty(qty) {
        const range = getGatherRangeWithRaw();
        if (!range) return 1;
        const {rawMin, rawMax, buff} = range;
        if (rawMax <= 0) return 1;
        const perExpected = ((rawMin + rawMax) / 2) * (1 + buff);
        return Math.ceil(qty / perExpected - 1e-6);
    }

    // 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);
    }

    // 复制原始结构并返回“数量”栏及input
    function createQuantityInputBlock() {
        const origBlock = document.querySelector('[class^="SkillActionDetail_maxActionCountInput"]');
        if (!origBlock) return null;

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

        // label
        const origLabel = origBlock.querySelector('[class^="SkillActionDetail_label"]');
        const label = origLabel.cloneNode(true);
        label.textContent = '数量';

        // 输入部分
        const origInputWrap = origBlock.querySelector('[class^="SkillActionDetail_input"]');
        const inputWrap = origInputWrap.cloneNode(false);

        // input container
        const origInputContainer = origInputWrap.querySelector('[class^="Input_inputContainer"]');
        const inputContainer = origInputContainer.cloneNode(false);

        // input
        const origInput = origInputContainer.querySelector('input');
        const input = origInput.cloneNode(false);
        input.value = '';
        input.placeholder = '输入期望数量';
        input.type = 'text';
        input.maxLength = '12';

        // === 新增:获得焦点时全选 ===
        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);
                }
            }
        });

        inputContainer.appendChild(input);
        inputWrap.appendChild(inputContainer);
        newBlock.appendChild(label);
        newBlock.appendChild(inputWrap);

        return {newBlock, input, origBlock}; // 返回origBlock用于按钮层级
    }

    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;
    }

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

        if (skillType === 'foraging'){
            const actionName = getCurrentActionName();
            if (actionName === '翠野农场' ||
                actionName === '波光湖泊' ||
                actionName === '迷雾森林' ||
                actionName === '深紫沙滩' ||
                actionName === '傻牛山谷' ||
                actionName === '奥林匹斯山' ||
                actionName === '小行星带'){
                return null;
            }
        }

        const origBlock = document.querySelector('[class^="SkillActionDetail_maxActionCountInput"]');
        if (!origBlock) return;
        // 已经有“数量”栏则不再插入
        if ([...origBlock.parentNode.children].some(e => {
            const lab = e.querySelector && e.querySelector('[class^="SkillActionDetail_label"]');
            return lab && lab.textContent.trim() === '数量';
        })) return;

        // 构造“数量”栏
        const {newBlock, input: qtyInput} = createQuantityInputBlock();
        if (!newBlock || !qtyInput) return;

        // 快捷按钮值与显示文本
        const btns = [
            {val: 100, txt: '100'},
            {val: 300, txt: '300'},
            {val: 500, txt: '500'},
            {val: 1000, txt: '1k'},
            {val: 2000, txt: '2k'}
        ];

        // 原始按钮
        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', () => {
                reactInputTriggerHack(qtyInput, val.toString());
            });
            newBlock.appendChild(btn); // 直接在SkillActionDetail_maxActionCountInput同级
        });

        // 插入到原始栏后
        origBlock.parentNode.insertBefore(newBlock, origBlock.nextSibling);

        // 获取“次数”输入框和按钮
        const timesInput = origBlock.querySelector('input');
        const buttons = origBlock.querySelectorAll('button');

        // 联动循环保护
        let linking = false;

        // “次数”栏内容变化时,更新“数量”
        function updateQtyFromTimes() {
            if (linking) return;
            linking = true;
            let times = timesInput.value;
            if (times === '∞' || times === '' || times === undefined || times === null) {
                qtyInput.value = '∞';
                linking = false;
                return;
            }
            times = parseTimes(times);
            if (!isFinite(times) || times <= 0) {
                qtyInput.value = '';
                linking = false;
                return;
            }
            const {expected} = getTotalRangeFromTimes(times);
            if (!isFinite(expected)) {
                qtyInput.value = '∞';
            } else {
                qtyInput.value = Math.round(expected);
            }
            linking = false;
        }
        // “数量”栏变化时,更新“次数”
        function updateTimesFromQty() {
            if (linking) return;
            linking = true;
            let qty = qtyInput.value;
            if (qty === '∞' || qty === '' || qty === undefined || qty === null) {
                reactInputTriggerHack(timesInput, '∞');
                linking = false;
                return;
            }
            qty = parseQty(qty);
            if (!isFinite(qty) || qty <= 0) {
                reactInputTriggerHack(timesInput, '');
                linking = false;
                return;
            }
            const times = Math.max(getTimesFromQty(qty), 1);
            reactInputTriggerHack(timesInput, times.toString());
            linking = false;
        }

        // “次数”输入框联动
        timesInput.addEventListener('input', updateQtyFromTimes);

        // “数量”输入框联动
        qtyInput.addEventListener('input', updateTimesFromQty);

        // 按钮联动监听
        for (const btn of buttons) {
            btn.addEventListener('click', () => {
                setTimeout(() => {
                    updateQtyFromTimes();
                }, 20);
            });
        }

        // 初次填充
        setTimeout(updateQtyFromTimes, 120);
    }

    // 监听页面变化
    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 });
    }

    observePanel();
    setTimeout(insertQuantityInput, 500);

})();