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