工匠放置小工具之1:事件提醒

工匠提醒 + 下一个事件 + 商人/商船提醒 + 可隐藏 + 等级阈值缓存

// ==UserScript==
// @name         工匠放置小工具之1:事件提醒 
// @namespace    http://tampermonkey.net/
// @version      1.10
// @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": "战斗加成",
        "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);

})();