ZedTools notifier with toolbar icon, sound alerts, XP (percentage), booster, raid, junk store, and temporary effects countdowns (Relaxed / Rested)
目前為
// ==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);
})();