Zed City Notifier

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

Version vom 07.10.2025. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

    });
})();