Zed City Notifier

Zed City notifier with collapsible bottom-left UI, per-stat toggles, sound, and persistent custom thresholds

Устаревшая версия за 07.10.2025. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Zed City Notifier
// @namespace    http://tampermonkey.net/
// @version      2.0.2
// @description  Zed City notifier with collapsible bottom-left UI, per-stat toggles, sound, and persistent custom thresholds
// @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;
    const STORAGE_KEY = 'zedCityNotifier';

    // --- Load saved config or set defaults ---
    const defaultConfig = {
        thresholds: { energy: 100, rad: 15, morale: 100, life: 100 },
        notified: { energy: false, rad: false, morale: false, life: false },
        enabled: { energy: true, rad: true, morale: true, life: true },
        uiVisible: false
    };

    let config = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
    // Merge with defaults to avoid undefined keys
    config = {
        thresholds: { ...defaultConfig.thresholds, ...(config.thresholds || {}) },
        notified: { ...defaultConfig.notified, ...(config.notified || {}) },
        enabled: { ...defaultConfig.enabled, ...(config.enabled || {}) },
        uiVisible: config.uiVisible ?? defaultConfig.uiVisible
    };

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



    function notifyUser(stat, value) {
    const msg = `${stat} reached ${value}!`;
    // In-page toast
    gmNotify(msg, "warning", "Stat Alert!");
    // System notification
    if(Notification.permission === "granted"){
        new Notification("⚡ ZedCityNotifier", { body: msg });
    }
}


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: `⚡ [ZedCityNotifier] ${message}`,  // prepend plugin name with emoji
            caption: caption || "",                       // optional subtitle
            color,
            position: "top-right",
            timeout: 3500,
            multiLine: true,
            // minimal styling to avoid broken layout
            classes: "zc-notify",
        });
    } else {
        console.log("[ZedCityNotifier] Quasar notify not ready:", message);
    }
}



    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("[ZedCityNotifier] Audio error:", e));
        }
    }

    function checkStats() {
        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.zed.city/getStats",
            headers: { "Accept": "application/json" },
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        const stats = {
                            energy: data.energy,
                            rad: data.rad,
                            morale: data.morale,
                            life: data.life
                        };
                        console.log("[ZedCityNotifier] Stats:", stats);

                        for (const stat in stats) {
                            if (!config.enabled[stat]) continue; // skip disabled stats
                            const value = stats[stat];
                            const limit = config.thresholds[stat];
                            if (value >= limit && !config.notified[stat]) {
                                sendNotification("Zed City Alert", `${stat} reached ${value}!`);
                                 notifyUser(stat, value);
                                config.notified[stat] = true;
                                saveConfig();
                            } else if (value < limit && config.notified[stat]) {
                                config.notified[stat] = false;
                                saveConfig();
                            }
                        }
                    } catch (e) {
                        console.error("[ZedCityNotifier] Error parsing stats:", e);
                    }
                }
            },
            onerror: function(err) { console.error("[ZedCityNotifier] Request error:", err); }
        });
    }

    checkStats();
    setInterval(checkStats, CHECK_INTERVAL);

    // --- PANEL CONTAINER ---
    const panel = document.createElement("div");
    Object.assign(panel.style, {
        position: "fixed",
        bottom: "60px",
        left: "20px",
        width: "210px",
        background: "rgba(20,20,20,0.9)",
        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 = "⚙️ Zed City 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", value: config.thresholds.energy },
        { id: "rad", label: "Rad", value: config.thresholds.rad },
        { id: "morale", label: "Morale", value: config.thresholds.morale },
        { id: "life", label: "Life", value: config.thresholds.life }
    ];

    fields.forEach(f => {
        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 = f.label;
        label.style.flex = "1";

        const input = document.createElement("input");
        input.type = "number";
        input.value = f.value;
        input.id = "zc_" + f.id;
        Object.assign(input.style, {
            width: "50px",
            marginRight: "4px"
        });

        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = config.enabled[f.id];
        checkbox.id = "zc_enable_" + f.id;

        const checkboxLabel = document.createElement("span");
        checkboxLabel.textContent = "🔔";
        checkboxLabel.title = "Enable/disable notifications";

        row.appendChild(label);
        row.appendChild(input);
        row.appendChild(checkboxLabel);
        row.appendChild(checkbox);
        content.appendChild(row);
    });

    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"
    });
    content.appendChild(saveButton);
    panel.appendChild(content);
    document.body.appendChild(panel);

    // --- TOGGLE BUTTON ---
    const toggleButton = document.createElement("button");
    toggleButton.id = "zcToggleBtn";
    toggleButton.textContent = "⚙️ Notifier";
    Object.assign(toggleButton.style, {
        position: "fixed",
        bottom: "20px",
        left: "20px",
        zIndex: "9999",
        padding: "6px 12px",
        borderRadius: "8px",
        border: "none",
        background: "#2c2c2c",
        color: "#fff",
        cursor: "pointer",
        boxShadow: "0 0 5px rgba(0,0,0,0.3)"
    });
    document.body.appendChild(toggleButton);

    // --- Toggle logic ---
    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();
    }
    toggleButton.addEventListener("click", () => {
        visible = !visible;
        updatePanelVisibility();
    });

    // --- Save handler ---
    saveButton.addEventListener("click", () => {
        fields.forEach(f => {
            config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 100;
            config.enabled[f.id] = document.getElementById("zc_enable_" + f.id).checked;
        });
        saveConfig();
        //alert("Settings saved!");
        gmNotify("Settings saved!", "positive", "Your custom thresholds are now active!");

    });
})();