ZedTools Notifier

ZedTools notifier with toolbar icon, sound alerts, XP (percentage), booster, raid, junk store, and temporary effects countdowns (Relaxed / Rested)

目前為 2025-10-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ZedTools Notifier
// @namespace    http://tampermonkey.net/
// @version      3.1.2
// @description  ZedTools notifier with toolbar icon, sound alerts, XP (percentage), booster, raid, junk store, and temporary effects countdowns (Relaxed / Rested)
// @author       You
// @match        https://zed.city/*
// @match        https://*.zed.city/*
// @grant        GM_xmlhttpRequest
// @connect      zed.city
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CHECK_INTERVAL = 60 * 1000; // normal stats
    const XP_CHECK_INTERVAL = 10 * 1000; // XP percentage check
    const STORAGE_KEY = 'ZedToolsNotifier';

    const defaultConfig = {
        thresholds: { energy: 100, rad: 15, morale: 100, life: 100, booster: 300, raid: 300, xp: 80 },
        notified: { energy: false, rad: false, morale: false, life: false, reset_time: false, booster: false, raid: false, xp: false, relaxed: false, rested: false },
        enabled: { energy: true, rad: true, morale: true, life: true, reset_time: true, booster: true, raid: true, xp: true, relaxed: true, rested: true },
        uiVisible: false,
        membership: false
    };

    let config = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
    config = {
        thresholds: { ...defaultConfig.thresholds, ...(config.thresholds || {}) },
        notified: { ...defaultConfig.notified, ...(config.notified || {}) },
        enabled: { ...defaultConfig.enabled, ...(config.enabled || {}) },
        uiVisible: config.uiVisible ?? defaultConfig.uiVisible,
        isMember: config.membership ?? false
    };

    let junkTimeSeconds = 0;
    let boosterTime = 0;
    let raidTime = 0;
    let relaxedTime = 0;
    let restedTime = 0;
    let currentXP = 0;
    let nextLevelXP = 0;
    let travelingTime = 0;
    let travelingLoc = "";
    let travelingNotified = false;

    function saveConfig() { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); }

    const audio = new Audio("https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg");

    if (Notification.permission !== "granted" && Notification.permission !== "denied") {
        Notification.requestPermission();
    }

    function sendNotification(title, text) {
        if (Notification.permission === "granted") {
            new Notification(title, { body: text });
            audio.play().catch(e => console.log("[ZedTools Notifier] Audio error:", e));
        } else {
            console.log(`[ZedTools Notifier] ${title}: ${text}`);
            audio.play().catch(e => console.log("[ZedTools Notifier] Audio error:", e));
        }
    }

    function notifyUser(stat, value) {
        const msg = typeof value === 'number' ? `${stat} reached ${value}!` : value;
        console.log("[ZedTools Notifier] Notify:", msg);
        sendNotification("⚡ ZedTools Notifier", msg);
        gmNotify(msg, "warning", "Stat Alert!");
    }

    function gmNotify(message, color = "positive", caption) {
        const vueApp = window.app || window.vueApp || document.querySelector("#q-app")?._vnode?.appContext?.app;
        const $q = vueApp?.config?.globalProperties?.$q;
        if ($q && typeof $q.notify === "function") {
            $q.notify({
                message: `⚡ [ZedTools Notifier] ${message}`,
                caption: caption || "",
                color,
                position: "top-right",
                timeout: 3500,
                multiLine: true
            });
        } else {
            console.log("[ZedTools Notifier] Notify:", message);
        }
    }

    function updateCountdownUI(elId, timeSeconds) {
        const el = document.getElementById(elId);
        if (!el) return;
        if (timeSeconds <= 0) {
            el.textContent = "(Ready!)";
            el.style.color = "#00ff66";
            el.style.fontWeight = "bold";
        } else {
            const hours = Math.floor(timeSeconds / 3600);
            const minutes = Math.floor((timeSeconds % 3600) / 60);
            const seconds = timeSeconds % 60;
            const timeStr = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
            el.textContent = `(${timeStr})`;
            el.style.color = "#ffd966";
            el.style.fontWeight = "normal";
        }
    }

    function updateJunkUI() { updateCountdownUI("zc_junk_time", junkTimeSeconds); }
    function updateBoosterUI() { updateCountdownUI("zc_booster_time", boosterTime); }
    function updateRaidUI() { updateCountdownUI("zc_raid_time", raidTime); }
    function updateRelaxedUI() { updateCountdownUI("zc_relaxed_time", relaxedTime); }
    function updateRestedUI() { updateCountdownUI("zc_rested_time", restedTime); }
    function updateTravelingUI() { updateCountdownUI("zc_traveling_time", travelingTime);}


    function updateXPUI(percent) {
        const el = document.getElementById("zc_xp");
        if (!el) return;
        el.textContent = `${percent}%`;
        el.style.color = "#000";
    }

    function xhrGet(url, onload) {
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({ method: "GET", url, headers: { "Accept": "application/json" }, onload });
        } else {
            fetch(url, { headers: { "Accept": "application/json" } })
                .then(r => r.text())
                .then(responseText => onload({ status: 200, responseText }))
                .catch(e => console.error("[ZedTools Notifier] Fetch error:", e));
        }
    }

    function checkStats() {
        xhrGet("https://api.zed.city/getStats", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);

                    // Basic stats
                    const stats = { energy: data.energy, rad: data.rad, morale: data.morale, life: data.life };
                    for (const stat in stats) {
                        if (!config.enabled[stat]) continue;
                        const value = stats[stat];
                        const limit = config.thresholds[stat];
                        if (value >= limit && !config.notified[stat]) {
                            notifyUser(stat, value);
                            config.notified[stat] = true; saveConfig();
                        } else if (value < limit && config.notified[stat]) {
                            config.notified[stat] = false; saveConfig();
                        }
                    }

                    // Booster & Raid
                    boosterTime = Number(data.booster_cooldown ?? 0);
                    raidTime = Number(data.raid_cooldown ?? 0);

                    if (config.enabled.booster && ((boosterTime <= config.thresholds.booster && boosterTime > 0 && !config.notified.booster) || boosterTime === 0 && !config.notified.booster)) {
                        const msg = boosterTime === 0 ? "Booster ready!" : `Booster ready in ${Math.ceil(boosterTime/60)} min!`;
                        notifyUser("Booster", msg); config.notified.booster = true; saveConfig();
                    } else if (boosterTime > config.thresholds.booster && config.notified.booster) { config.notified.booster = false; saveConfig(); }

                    if (config.enabled.raid && ((raidTime <= config.thresholds.raid && raidTime > 0 && !config.notified.raid) || raidTime === 0 && !config.notified.raid)) {
                        const msg = raidTime === 0 ? "Raid ready!" : `Raid ready in ${Math.ceil(raidTime/60)} min!`;
                        notifyUser("Raid", msg); config.notified.raid = true; saveConfig();
                    } else if (raidTime > config.thresholds.raid && config.notified.raid) { config.notified.raid = false; saveConfig(); }

                    updateBoosterUI();
                    updateRaidUI();

                    // === Temporary Effects ===
                    const effects = data.effects || [];
                    const relaxed = effects.find(e => e.codename === "player_effect_feeling_relaxed");
                    const rested = effects.find(e => e.codename === "player_effect_recently_rested");

                    relaxedTime = relaxed ? Number(relaxed.expire) : 0;
                    restedTime = rested ? Number(rested.expire) : 0;

                    updateRelaxedUI();
                    updateRestedUI();

                } catch (e) { console.error("[ZedTools Notifier] Error parsing stats:", e); }
            }
        });

        // Junk store
        xhrGet("https://api.zed.city/getStore?store_id=junk", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.error) return;
                    junkTimeSeconds = Number(data?.limits?.reset_time ?? data?.store?.reset_time ?? 0);
                    updateJunkUI();
                    if (config.enabled.reset_time && ((junkTimeSeconds <= 300 && junkTimeSeconds > 0 && !config.notified.reset_time) || (junkTimeSeconds === 0 && !config.notified.reset_time))) {
                        const msg = junkTimeSeconds === 0 ? "Junk store has just reset!" : `Junk store reset in ${Math.ceil(junkTimeSeconds/60)} minutes!`;
                        notifyUser("Junk Store Reset", msg); config.notified.reset_time = true; saveConfig();
                    } else if (junkTimeSeconds > 300 && config.notified.reset_time) { config.notified.reset_time = false; saveConfig(); }
                } catch (e) { console.error("[ZedTools Notifier] Store parse error:", e); }
            }
        });
    }
function checkTraveling() {
    xhrGet("https://api.zed.city/getTraveling", function(response) {
        if (response.status === 200) {
            try {
                const data = JSON.parse(response.responseText);

                if (data.traveling) {
                    travelingTime = Number(data.time_left ?? 0);
                    travelingLoc = data.loc_name ?? data.codename ?? "Unknown location";

                    if (!travelingNotified && travelingTime <= 60) { // notify 1 min left
                        notifyUser("Traveling", `Arriving soon at ${travelingLoc}!`);
                        travelingNotified = true;
                    }

                } else {
                    if (travelingTime > 0) {
                        notifyUser("Traveling", `You have arrived at ${travelingLoc}!`);
                    }
                    travelingTime = 0;
                    travelingNotified = false;
                }

                updateTravelingUI();

            } catch(e) {
                console.error("[ZedTools Notifier] Traveling parse error:", e);
            }
        }
    });
}

    function checkXP() {
        xhrGet("https://api.zed.city/getStats", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);
                    currentXP = Number(data.experience);
                    nextLevelXP = Number(data.xp_end);
                    const xpPercent = Math.floor((currentXP / nextLevelXP) * 100);
                    updateXPUI(xpPercent);

                    if (config.enabled.xp && xpPercent >= config.thresholds.xp && !config.notified.xp) {
                        notifyUser("XP", `You are ${xpPercent}% towards next level!`);
                        config.notified.xp = true; saveConfig();
                    } else if (xpPercent < config.thresholds.xp && config.notified.xp) {
                        config.notified.xp = false; saveConfig();
                    }
                } catch(e){ console.error("[ZedTools Notifier] XP parse error:", e); }
            }
        });
    }

    checkStats();
    checkXP();
    checkTraveling();
    setInterval(checkStats, CHECK_INTERVAL);
    setInterval(checkXP, XP_CHECK_INTERVAL);
    setInterval(checkTraveling, CHECK_INTERVAL); // or a custom interval if you want more frequent updates


    setInterval(() => {
        if (junkTimeSeconds > 0) { junkTimeSeconds--; updateJunkUI(); }
        if (boosterTime > 0) { boosterTime--; updateBoosterUI(); }
        if (raidTime > 0) { raidTime--; updateRaidUI(); }
        if (relaxedTime > 0) { relaxedTime--; updateRelaxedUI(); }
        if (restedTime > 0) { restedTime--; updateRestedUI(); }
        if (travelingTime > 0) { travelingTime--; updateTravelingUI(); }

    }, 1000);

    // === Settings Panel ===
    const panel = document.createElement("div");
    Object.assign(panel.style, { position: "fixed", bottom: "60px", left: "20px", width: "260px", background: "rgba(20,20,20,0.95)", color: "#fff", borderRadius: "10px", fontFamily: "Arial,sans-serif", fontSize: "12px", zIndex: "9999", boxShadow: "0 0 10px rgba(0,0,0,0.5)", transition: "all 0.3s ease", opacity: config.uiVisible ? "1" : "0", transform: config.uiVisible ? "translateY(0)" : "translateY(10px)", display: config.uiVisible ? "block" : "none", backdropFilter: "blur(4px)" });

    const header = document.createElement("div");
    header.textContent = "⚙️ ZedTools Notifier";
    Object.assign(header.style, { padding: "6px", fontWeight: "bold", textAlign: "center", background: "#2c2c2c", borderRadius: "10px 10px 0 0" });
    panel.appendChild(header);

    const content = document.createElement("div");
    content.style.padding = "6px";

    const fields = [
        { id: "energy", label: "Energy" },
        { id: "rad", label: "Rad" },
        { id: "morale", label: "Morale" },
        { id: "life", label: "Life" },
        { id: "xp", label: "Experience (%)" }
    ];

    function createRow(labelText, elId, key, isNumberInput = true) {
        const row = document.createElement("div");
        Object.assign(row.style, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" });
        const label = document.createElement("label");
        label.textContent = labelText; label.style.flex = "1";
        let input;
        if (isNumberInput) {
            input = document.createElement("input");
            input.type = "number";
            input.value = config.thresholds[key];
            input.id = "zc_" + key;
            Object.assign(input.style, { width: "50px", marginRight: "4px" });
        }
        const timeEl = document.createElement("span");
        timeEl.id = elId;
        timeEl.textContent = isNumberInput ? "" : "(—)";
        timeEl.style.marginRight = "6px";
        timeEl.style.opacity = "0.8";
        timeEl.style.fontSize = "11px";
        const bell = document.createElement("span");
        bell.textContent = "🔔";
        const check = document.createElement("input");
        check.type = "checkbox";
        check.checked = config.enabled[key];
        check.id = "zc_enable_" + key;
        row.appendChild(label);
        if (isNumberInput) row.appendChild(input);
        row.appendChild(timeEl);
        row.appendChild(bell);
        row.appendChild(check);
        content.appendChild(row);
    }

    createRow("Energy", "zc_energy_time", "energy");
    createRow("Rad", "zc_rad_time", "rad");
    createRow("Morale", "zc_morale_time", "morale");
    createRow("Life", "zc_life_time", "life");
    createRow("Experience (%)", "zc_xp", "xp", true);
    createRow("Junk Reset", "zc_junk_time", "reset_time", false);
    createRow("Booster Ready", "zc_booster_time", "booster", false);
    createRow("Raid Ready", "zc_raid_time", "raid", false);
    createRow("SPA Timer", "zc_relaxed_time", "relaxed", false);
    createRow("Sleeping Timer", "zc_rested_time", "rested", false);
    createRow("Traveling", "zc_traveling_time", "traveling", false);


    // Test button
    const testButton = document.createElement("button");
    testButton.textContent = "🔊 Test Alert";
    Object.assign(testButton.style, { marginTop: "5px", width: "100%", borderRadius: "6px", border: "none", background: "#28a745", color: "white", padding: "5px 0", cursor: "pointer" });
    testButton.addEventListener("click", () => { sendNotification("ZedTools Notifier Test", "This is a test alert!"); gmNotify("Test notification sent!", "info"); });
    content.appendChild(testButton);

    // Save button
    const saveButton = document.createElement("button");
    saveButton.id = "zc_save";
    saveButton.textContent = "💾 Save";
    Object.assign(saveButton.style, { marginTop: "5px", width: "100%", borderRadius: "6px", border: "none", background: "#0078d7", color: "white", padding: "5px 0", cursor: "pointer" });
    saveButton.addEventListener("click", () => {
        fields.forEach(f => {
            if(f.id!=="xp"){
                config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 100;
            } else {
                config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 80;
            }
            config.enabled[f.id] = document.getElementById("zc_enable_" + f.id).checked;
        });
        ["reset_time","booster","raid","relaxed","rested"].forEach(k => { config.enabled[k] = document.getElementById("zc_enable_" + k).checked; });
        saveConfig();
        gmNotify("Settings saved!", "info");
    });
    content.appendChild(saveButton);

    panel.appendChild(content);
    document.body.appendChild(panel);

    let visible = config.uiVisible;
    function updatePanelVisibility() {
        if (visible) {
            panel.style.display = "block";
            setTimeout(() => { panel.style.opacity = "1"; panel.style.transform = "translateY(0)"; }, 10);
        } else {
            panel.style.opacity = "0";
            panel.style.transform = "translateY(10px)";
            setTimeout(() => { panel.style.display = "none"; }, 300);
        }
        config.uiVisible = visible;
        saveConfig();
    }

    function insertToolbarIcon() {
        const notifIcon = document.querySelector('a[href="/notifications"]');
        if (!notifIcon || document.getElementById('zcToolbarBtn')) return false;
        const iconLink = document.createElement('a'); iconLink.id = 'zcToolbarBtn'; iconLink.className = notifIcon.className; iconLink.href = 'javascript:void(0)'; iconLink.style.display = 'inline-flex'; iconLink.style.alignItems = 'center'; iconLink.style.justifyContent = 'center'; iconLink.style.height = notifIcon.offsetHeight + 'px'; iconLink.style.width = notifIcon.offsetWidth + 'px';
        iconLink.innerHTML = `<span class="q-focus-helper"></span><span class="q-btn__content text-center col items-center q-anchor--skip justify-center row" style="font-size: 1.3em;"><i class="q-icon fal fa-bullhorn" style="font-size: 1em; line-height: 1;" aria-hidden="true" role="img"></i></span>`;
        iconLink.title = "ZedTools Notifier";
        iconLink.addEventListener('click', () => { visible = !visible; updatePanelVisibility(); });
        notifIcon.parentElement.insertBefore(iconLink, notifIcon);
        const computed = window.getComputedStyle(notifIcon); iconLink.style.color = computed.color;
        iconLink.addEventListener("mouseenter", () => { iconLink.style.opacity = "0.8"; });
        iconLink.addEventListener("mouseleave", () => { iconLink.style.opacity = "1"; });
        return true;
    }

    const toolbarCheck = setInterval(() => { if (insertToolbarIcon()) clearInterval(toolbarCheck); }, 1000);

})();