Zed City Notifier

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

Per 07-10-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

    });
})();