// ==UserScript==
// @name 工匠放置小工具之1:事件提醒
// @namespace http://tampermonkey.net/
// @version 1.11
// @description 工匠提醒 + 下一个事件 + 商人/商船提醒 + 可隐藏 + 等级阈值缓存
// @author Stella
// @match https://idleartisan.com/*
// @grant GM_getValue
// @grant GM_setValue
// @license CC-BY-NC-SA-4.0
// ==/UserScript==
(function() {
'use strict';
const COOLDOWN = 60 * 1000;
let cooldownUntil = 0;
// 默认配置
const defaultConfig = {
CB: true,
Siege: true,
TS: true,
M: true,
GLOBAL: true,
IDLE: false,
MERCHANT_LEVEL: 3 // 默认商人等级阈值
};
const config = {};
for (let k in defaultConfig) {
config[k] = GM_getValue(k, defaultConfig[k]);
}
// 中英文事件表
const eventDict = {
"Mining Bonus": "采矿加成",
"Woodcutting Bonus": "伐木加成",
"Thief": "盗贼",
"Battling Bonus": "战斗加成",
"The Great Smeltdown": "大熔炉",
"Crafting Bonus": "制作加成",
"Merchant": "商人",
"Purchasing Agent": "采购代理",
"Tax Season": "税收季节",
"Distant war drums": "遥远的战鼓",
"Goblin Siege": "哥布林围攻",
"Boss Fight": "Boss对抗",
"Ancient Treant": "远古树人",
"Runic Golem": "符文魔像",
"Trade ship": "贸易船"
};
const eventOrder = Object.keys(eventDict);
function getLangMode(text) {
return /^[\x00-\x7F]*$/.test(text) ? "en" : "zh";
}
function getEventName(enName, lang) {
return lang === "en" ? enName : (eventDict[enName] || enName);
}
// ========== UI面板 ==========
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.bottom = '20px';
panel.style.right = '20px';
panel.style.background = 'rgba(0,0,0,0.8)';
panel.style.color = 'white';
panel.style.padding = '12px';
panel.style.borderRadius = '10px';
panel.style.zIndex = 9999;
panel.style.fontFamily = 'sans-serif';
panel.style.fontSize = '14px';
panel.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
// 隐藏按钮单独一排
const hideContainer = document.createElement('div');
hideContainer.style.textAlign = 'right';
hideContainer.style.marginBottom = '8px';
const closeBtn = document.createElement('span');
closeBtn.textContent = '❌';
closeBtn.style.cursor = 'pointer';
closeBtn.onclick = () => {
panel.style.display = 'none';
bulb.style.display = 'block';
};
hideContainer.appendChild(closeBtn);
panel.appendChild(hideContainer);
function createSwitch(labelText, key) {
const container = document.createElement('div');
container.style.marginBottom = '8px';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'space-between';
const label = document.createElement('span');
label.textContent = labelText;
const switchContainer = document.createElement('div');
switchContainer.style.width = '50px';
switchContainer.style.height = '24px';
switchContainer.style.background = config[key] ? '#4caf50' : '#ccc';
switchContainer.style.borderRadius = '12px';
switchContainer.style.position = 'relative';
switchContainer.style.cursor = 'pointer';
switchContainer.style.transition = 'background 0.3s';
const knob = document.createElement('div');
knob.style.width = '20px';
knob.style.height = '20px';
knob.style.background = '#fff';
knob.style.borderRadius = '50%';
knob.style.position = 'absolute';
knob.style.top = '2px';
knob.style.left = config[key] ? '28px' : '2px';
knob.style.transition = 'left 0.3s';
switchContainer.appendChild(knob);
switchContainer.addEventListener('click', () => {
config[key] = !config[key];
GM_setValue(key, config[key]);
switchContainer.style.background = config[key] ? '#4caf50' : '#ccc';
knob.style.left = config[key] ? '28px' : '2px';
});
container.appendChild(label);
container.appendChild(switchContainer);
panel.appendChild(container);
}
createSwitch('制作提醒', 'CB');
createSwitch('围攻提醒', 'Siege');
createSwitch('商船提醒', 'TS');
createSwitch('商人提醒', 'M');
// 商人等级阈值输入
const levelContainer = document.createElement('div');
levelContainer.style.marginBottom = '8px';
const levelLabel = document.createElement('span');
levelLabel.textContent = '当物品等级大于X时提醒';
const levelInput = document.createElement('input');
levelInput.type = 'number';
levelInput.min = '1';
levelInput.value = config.MERCHANT_LEVEL;
levelInput.style.width = '40px';
levelInput.style.marginLeft = '6px';
levelInput.addEventListener('change', () => {
config.MERCHANT_LEVEL = parseInt(levelInput.value, 10) || 1;
GM_setValue('MERCHANT_LEVEL', config.MERCHANT_LEVEL);
});
levelContainer.appendChild(levelLabel);
levelContainer.appendChild(levelInput);
panel.appendChild(levelContainer);
panel.appendChild(document.createElement('hr'));
createSwitch('全局开关', 'GLOBAL');
createSwitch('摸鱼模式', 'IDLE');
document.body.appendChild(panel);
// 灯泡按钮
const bulb = document.createElement('div');
bulb.textContent = '💡';
bulb.style.position = 'fixed';
bulb.style.bottom = '20px';
bulb.style.right = '20px';
bulb.style.fontSize = '24px';
bulb.style.cursor = 'move';
bulb.style.zIndex = 10000;
bulb.style.display = 'none';
document.body.appendChild(bulb);
bulb.addEventListener('click', () => {
bulb.style.display = 'none';
panel.style.display = 'block';
});
bulb.onmousedown = function(e) {
let shiftX = e.clientX - bulb.getBoundingClientRect().left;
let shiftY = e.clientY - bulb.getBoundingClientRect().top;
function moveAt(pageX, pageY) {
bulb.style.left = pageX - shiftX + 'px';
bulb.style.top = pageY - shiftY + 'px';
bulb.style.right = 'auto';
bulb.style.bottom = 'auto';
}
function onMouseMove(e) {
moveAt(e.pageX, e.pageY);
}
document.addEventListener('mousemove', onMouseMove);
bulb.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
bulb.onmouseup = null;
};
};
bulb.ondragstart = () => false;
// 请求通知权限
if (Notification.permission !== "granted") Notification.requestPermission();
// ========== 定时提醒 ==========
setInterval(() => {
if (!config.GLOBAL) return;
const now = Date.now();
if (now < cooldownUntil) return;
const title = document.title.trim();
const langMode = getLangMode(title);
const notify = (msg) => {
if (config.IDLE) msg = langMode === "en" ? "Windows Update Reminder" : "Windows 更新提醒";
new Notification(config.IDLE ? (langMode === "en" ? "Windows Update" : "Windows 更新") : "Idle Artisan", {
body: msg,
icon: "https://idleartisan.com/favicon.ico"
});
cooldownUntil = now + COOLDOWN;
};
// 制作 / 围攻
if (config.CB && (title.includes("CB") || title.includes("制作") || title.includes("Crafting"))) {
notify(langMode === "en" ? "Crafting Bonus!" : "制作加成来了!");
} else if (config.Siege && (title.includes("Siege") || title.includes("围攻"))) {
notify(langMode === "en" ? "Prepare for Siege!" : "准备 BOSS 战斗!");
} else if (config.TS && (title.includes("TS") || title.includes("商船") || title.includes("Trade ship"))) {
// 先切换市场选项卡并选择全部物品
const marketplaceTab = document.getElementById("Marketplace");
const marketFilter = document.getElementById("marketItemFilter");
if (marketplaceTab) marketplaceTab.style.display = "block";
if (marketFilter) {
marketFilter.value = "all";
if (typeof updateMarketDisplay === "function") updateMarketDisplay();
}
setTimeout(() => {
const rows = document.querySelectorAll("#marketListingsDisplay tbody tr");
for (let row of rows) {
const seller = row.cells[3]?.textContent || "";
if (seller.includes("[NPC]贸易船")) {
const itemName = row.cells[0]?.textContent.trim() || "";
const price = row.cells[2]?.textContent.trim() || "";
notify(`船来!${itemName}@${price}`);
break;
}
}
}, 300);
} else if (config.M && (title === "Idle Artisan - M" || title.includes("商人") || title.includes("Merchant"))) {
const logDisplay = document.getElementById("statusLogDisplay");
if (logDisplay) {
const lastLine = logDisplay.innerHTML.split("<br>").reverse().find(line => line.includes("商人来了") || line.includes("Merchant arrived"));
if (lastLine) {
const match = lastLine.match(/\((\d+)级\)/);
const level = match ? parseInt(match[1], 10) : 0;
if (level >= config.MERCHANT_LEVEL) {
notify(langMode === "en" ? `Merchant arrived! Item Level: ${level}` : `商人来了!物品等级: ${level}`);
}
}
}
}
}, 10000);
// ========== 下一个事件显示 ==========
const nextEventLabel = document.createElement('div');
nextEventLabel.style.marginLeft = "15px";
nextEventLabel.style.color = "#ff4d4d";
nextEventLabel.style.fontWeight = "bold";
nextEventLabel.style.fontSize = "14px";
nextEventLabel.textContent = "Next Event: ...";
const eventWrapper = document.getElementById("event-wrapper");
if (eventWrapper && eventWrapper.parentNode) {
eventWrapper.parentNode.insertBefore(nextEventLabel, eventWrapper.nextSibling);
}
setInterval(() => {
const currentNameElem = document.getElementById("event-name");
if (!currentNameElem) return;
const currentEventRaw = currentNameElem.textContent.trim();
const langMode = getLangMode(currentEventRaw);
let currentEn = Object.keys(eventDict).find(en => en === currentEventRaw || eventDict[en] === currentEventRaw);
if (!currentEn) return;
const idx = eventOrder.findIndex(e => e === currentEn);
if (idx >= 0) {
const nextEventEn = eventOrder[(idx + 1) % eventOrder.length];
nextEventLabel.textContent = langMode === "en" ? "Next Event: " + getEventName(nextEventEn, langMode) : "下一个事件: " + getEventName(nextEventEn, langMode);
} else nextEventLabel.textContent = langMode === "en" ? "Next Event: Unknown" : "下一个事件: 未知";
}, 2000);
})();