Faction supplies tracker with shared cloud sync via JSONBin.io
当前为
// ==UserScript==
// @name HALO Armory Valiant Clean v3.3 (Shared JSON Sync)
// @namespace http://tampermonkey.net/
// @version 3.3.1
// @description Faction supplies tracker with shared cloud sync via JSONBin.io
// @author Nova
// @match https://www.torn.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
// ========= CONFIG =========
const factionId = "41990";
const SHARED_URL = "https://api.jsonbin.io/v3/b/69096a06ae596e708f42eeaa/latest";
const JSONBIN_KEY = "$2a$10$S3KY5Shiyn2yJXI3O5Z33.QHmmUJbmbvLQwpXsrxUTFV8Yzdhtihi"; // <--- replace this
// ===========================
const MEDICAL_PRICES = {
"small first aid kit": 4000,
"first aid kit": 7500,
"morphine": 10000
};
const ABBR = {
"small first aid kit": "SFA",
"first aid kit": "FA",
"morphine": "MP"
};
const DRUG_PRICES = {
"xanax": 800000,
"vicodin": 800,
"ketamine": 2000,
"shrooms": 2000,
"cannabis": 4500,
"speed": 6000,
"pcp": 7500,
"opium": 23000,
"lsd": 32000,
"ecstasy": 40000
};
const ITEM_PRICES = {...DRUG_PRICES, ...MEDICAL_PRICES};
// local cache (in case of connection issues)
let factionKey = GM_getValue("factionAPIKey", null);
let debts = GM_getValue("factionDebts", {});
let deposits = GM_getValue("factionDeposits", {});
let processedLogs = GM_getValue("processedLogs", {});
// === Style & UI ===
GM_addStyle(`
#armoryPanel {
position: fixed;
bottom: 0;
right: 0;
width: 28%;
height: 55%;
background: white;
color: #000;
font-family: monospace;
font-size: 12px;
border: 1px solid #444;
border-radius: 8px 8px 0 0;
overflow-y: auto;
padding: 8px;
z-index: 9999;
display: none;
}
#armoryHeader { font-weight: bold; margin-bottom: 6px; }
.resetBtn {
cursor: pointer;
background: #eee;
border: 1px solid #aaa;
border-radius: 4px;
font-size: 12px;
padding: 1px 4px;
}
.resetBtn:hover { background: #ccc; }
#bubbleBtn {
position: fixed;
bottom: 20px;
right: 20px;
background:#222;
color:#fff;
border-radius:50%;
width:36px;
height:36px;
font-size:18px;
cursor:pointer;
display:flex;
justify-content:center;
align-items:center;
z-index:10000;
}
.userBlock { margin-bottom: 8px; padding-bottom: 4px; border-bottom: 1px dashed #ddd; }
.userName { font-weight: bold; font-size: 12px; }
.userDebtLine { display: flex; justify-content: space-between; align-items: center; font-size: 11px; margin-top: 2px; }
.smallMuted { color:#666; font-size:11px; margin-left:6px; }
`);
const panel = document.createElement("div");
panel.id = "armoryPanel";
panel.innerHTML = `<div id="armoryHeader">⚕️ Faction Supplies Tracker</div><div id="debtLog">Loading...</div>`;
document.body.appendChild(panel);
const bubble = document.createElement("div");
bubble.id = "bubbleBtn";
bubble.textContent = "⚕️";
document.body.appendChild(bubble);
let minimized = true;
bubble.addEventListener("click", () => {
minimized = !minimized;
panel.style.display = minimized ? "none" : "block";
});
// === Helper functions ===
function stripTags(str){ return str ? str.replace(/<[^>]*>/g, "").trim() : ""; }
function save(v, d){ try { GM_setValue(v, d); } catch(e){} }
function computeOwed(dep, used, price){ return Math.max(0, used - dep) * price; }
// === Cloud Sync ===
async function syncFromCloud() {
try {
const res = await fetch(SHARED_URL, {
headers: { "X-Master-Key": JSONBIN_KEY }
});
const json = await res.json();
if (json?.record) {
deposits = json.record.deposits || {};
debts = json.record.debts || {};
processedLogs = json.record.processedLogs || {};
save("factionDeposits", deposits);
save("factionDebts", debts);
save("processedLogs", processedLogs);
}
} catch(e) {
console.warn("Failed to fetch shared data:", e);
}
}
async function syncToCloud() {
try {
const body = JSON.stringify({ deposits, debts, processedLogs });
await fetch(SHARED_URL, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Master-Key": JSONBIN_KEY
},
body
});
} catch(e) {
console.warn("Failed to update shared data:", e);
}
}
// === Fetch Torn Logs ===
async function loadLogs() {
const debtDiv = document.getElementById("debtLog");
if (!factionKey){ debtDiv.textContent = "No API key set."; return; }
try {
const res = await fetch(`https://api.torn.com/faction/${factionId}?selections=armorynews&key=${factionKey}`);
const data = await res.json();
if (data.error){ debtDiv.textContent = "Error: " + data.error.error; return; }
for (const [logId, entry] of Object.entries(data.armorynews || {})) {
const text = (entry.news || "").toLowerCase();
const item = Object.keys(ITEM_PRICES).find(k => text.includes(k));
if (!item || processedLogs[logId]) continue;
const match = entry.news.match(/^(.+?) used/i);
const user = stripTags(match ? match[1] : "Unknown");
if (!deposits[user]) deposits[user] = {};
if (!deposits[user][item]) deposits[user][item] = { deposit:0, used:0, price:ITEM_PRICES[item] };
deposits[user][item].used++;
processedLogs[logId] = true;
}
save("factionDeposits", deposits);
save("processedLogs", processedLogs);
renderPanel();
await syncToCloud(); // push updates
} catch(e){ debtDiv.textContent = "Failed to load logs."; }
}
function renderPanel(){
const div = document.getElementById("debtLog");
div.innerHTML = "<b>Members Supplies Debt (medical only + legacy):</b><br>";
const users = new Set([...Object.keys(deposits||{}), ...Object.keys(debts||{})]);
if (!users.size){ div.innerHTML += "<div class='smallMuted'>No activity yet.</div>"; return; }
users.forEach(u=>{
const user = stripTags(u);
const data = deposits[u] || {};
let medTotal = 0;
const medParts = [];
for (const med in MEDICAL_PRICES){
const entry = data[med] || {deposit:0, used:0, price:MEDICAL_PRICES[med]};
const owed = computeOwed(entry.deposit, entry.used, entry.price);
if (owed>0) medParts.push(`${ABBR[med]}: ${owed.toLocaleString()}`);
medTotal += owed;
}
const legacy = Number(debts[u] || 0);
const total = medTotal + legacy;
if (total <= 0) return;
const block = document.createElement("div");
block.className = "userBlock";
const name = document.createElement("div");
name.className = "userName";
name.textContent = user;
block.appendChild(name);
const debtLine = document.createElement("div");
debtLine.className = "userDebtLine";
const color = total >= 1000000 ? "#ff4d4d" : "#000";
debtLine.innerHTML = `<span style="color:${color}">${medParts.join(" | ")} | Total: ${total.toLocaleString()}</span>`;
const resetBtn = document.createElement("button");
resetBtn.className = "resetBtn";
resetBtn.textContent = "✅";
resetBtn.onclick = async e=>{
delete deposits[u];
delete debts[u];
save("factionDeposits", deposits);
save("factionDebts", debts);
renderPanel();
await syncToCloud(); // sync reset to cloud
await loadLogs(); // refresh
};
debtLine.appendChild(resetBtn);
block.appendChild(debtLine);
div.appendChild(block);
});
}
// === Main Loop ===
async function main() {
await syncFromCloud();
if (!factionKey){
const key = prompt("Enter your Faction (Armory) API Key:", "");
if (key){ factionKey = key.trim(); save("factionAPIKey", factionKey); await loadLogs(); }
else renderPanel();
} else await loadLogs();
// keep refreshing every 30s
setInterval(async ()=>{
if(!minimized && factionKey){
await syncFromCloud();
await loadLogs();
}
}, 30000);
}
main();
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址