Zed City Notifier

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

Verzia zo dňa 07.10.2025. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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 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}!`);
                                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!");

    });
})();