HALO Armory Valiant Clean v3.3 (Shared JSON Sync)

Faction supplies tracker with shared cloud sync via JSONBin.io

当前为 2025-11-04 提交的版本,查看 最新版本

// ==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或关注我们的公众号极客氢云获取最新地址