HALO Armory Valiant Clean v2.7 (Medical Display v2)

Faction supplies tracker (medical integrated). Shows medical debts only (SFA, FA, MP). Deposits tracked internally. Reset per member kept.

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

// ==UserScript==
// @name         HALO Armory Valiant Clean v2.7 (Medical Display v2)
// @namespace    http://tampermonkey.net/
// @version      2.8
// @description  Faction supplies tracker (medical integrated). Shows medical debts only (SFA, FA, MP). Deposits tracked internally. Reset per member kept.
// @author       Nova
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
'use strict';

const factionId = "41990";

// Drug prices (kept for internal tracking if needed)
const DRUG_PRICES = {
    "xanax": 800000,
    "vicodin": 800,
    "ketamine": 2000,
    "shrooms": 2000,
    "cannabis": 4500,
    "speed": 6000,
    "pcp": 7500,
    "opium": 23000,
    "lsd": 32000,
    "ecstasy": 40000
};

// Medical prices
const MEDICAL_PRICES = {
    "small first aid kit": 4000,
    "first aid kit": 7500,
    "morphine": 10000
};

// Abbreviations for display (display-only)
const ABBR = {
  "small first aid kit": "SFA",
  "first aid kit": "FA",
  "morphine": "MP"
};

// Master list (drugs + medicals)
const ITEM_PRICES = {...DRUG_PRICES, ...MEDICAL_PRICES};

// Persistent storages
let factionKey = GM_getValue("factionAPIKey", null);
let deposits = GM_getValue("factionDeposits", {});    // structure: deposits[user][item] = { deposit: N, used: M, price: P }
let processedLogs = GM_getValue("processedLogs", {}); // processed log ids

// UI styles
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;
    margin-left: 6px;
    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;
  }
  .debtRow { display:flex; align-items:center; gap:8px; padding:4px 0; border-bottom:1px dashed #eee; }
  .debtText { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .smallMuted { color:#666; font-size:11px; margin-left:6px; }
`);

// Build UI
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";
});

// helpers
function stripTags(str) {
    return str ? str.replace(/<[^>]*>/g, "").trim() : "";
}

function saveDeposits() {
  try { GM_setValue("factionDeposits", deposits); } catch(e){ console.warn('saveDeposits', e); }
}
function saveProcessed() {
  try { GM_setValue("processedLogs", processedLogs); } catch(e){ console.warn('saveProcessed', e); }
}

// Loads armory logs, increments used counters always, keeps deposits internal
async function loadLogs() {
    const debtDiv = document.getElementById("debtLog");
    if (!factionKey) {
        if (debtDiv) debtDiv.innerHTML = "No API key set.";
        return;
    }
    try {
        if (debtDiv) debtDiv.innerHTML = "Fetching armory logs...";
        const url = `https://api.torn.com/faction/${factionId}?selections=armorynews&key=${factionKey}`;
        const res = await fetch(url);
        const data = await res.json();
        if (data.error) {
            if (debtDiv) debtDiv.innerHTML = "Error: " + data.error.error;
            return;
        }

        const logs = data.armorynews || {};
        // iterate logs (object keyed by id)
        Object.entries(logs).forEach(([logId, entry]) => {
            try {
              const text = (entry.news || "").toLowerCase();
              // find any item match from master list
              const matchedItem = Object.keys(ITEM_PRICES).find(item => text.includes(item));
              if (!matchedItem) return;

              // extract "User used ..." pattern
              const userMatch = (entry.news || "").match(/^(.+?) used/i);
              const rawUser = userMatch ? userMatch[1] : "Unknown";
              const user = stripTags(rawUser);

              // only process once per log id
              if (processedLogs[logId]) return;

              // ensure user deposit structure
              if (!deposits[user]) deposits[user] = {};

              // ensure item structure
              if (!deposits[user][matchedItem]) {
                deposits[user][matchedItem] = { deposit: 0, used: 0, price: ITEM_PRICES[matchedItem] || 0 };
              }

              // ALWAYS increment used counter to keep internal tally
              deposits[user][matchedItem].used = Number(deposits[user][matchedItem].used || 0) + 1;
              // ensure price stored
              deposits[user][matchedItem].price = ITEM_PRICES[matchedItem] || deposits[user][matchedItem].price || 0;

              // mark processed
              processedLogs[logId] = true;
            } catch(e) {
              console.warn('process log entry failed', e);
            }
        });

        // persist
        saveDeposits();
        saveProcessed();

        renderPanel();
    } catch (e) {
        if (debtDiv) debtDiv.innerHTML = "Request failed.";
        console.warn('loadLogs err', e);
    }
}

// compute owed for a specific deposit entry
function computeOwedForEntry(entry) {
  const dep = Number(entry.deposit || 0);
  const used = Number(entry.used || 0);
  const price = Number(entry.price || 0);
  const excess = Math.max(0, used - dep);
  return excess * price;
}

// Render panel: only show users with total medical debt > 0; display abbreviations and per-item cumulative debt
function renderPanel() {
    const debtDiv = document.getElementById("debtLog");
    if (!debtDiv) return;
    debtDiv.innerHTML = "<b>Members Supplies Debt (medical only):</b><br>";

    // gather users from deposits map
    const users = Object.keys(deposits || {});
    if (!users.length) {
      debtDiv.innerHTML += "<div class='smallMuted'>No activity yet.</div>";
      return;
    }

    let anyShown = false;

    users.sort((a,b) => a.localeCompare(b, undefined, {sensitivity:'base'})).forEach(user => {
        try {
          const userData = deposits[user] || {};
          // compute medical debts only
          const perItem = {};
          let totalForUser = 0;
          Object.keys(MEDICAL_PRICES).forEach(med => {
            const entry = userData[med] || { deposit:0, used:0, price: MEDICAL_PRICES[med] || 0 };
            const owed = computeOwedForEntry(entry);
            perItem[med] = owed;
            totalForUser += owed;
          });

          // hide users with zero total medical debt
          if (totalForUser <= 0) return;

          anyShown = true;

          // build display row: "Name | SFA: 4000 | FA: 15000 | MP: 10000"
          const parts = [];
          Object.keys(MEDICAL_PRICES).forEach(med => {
            const val = perItem[med] || 0;
            // show all medical abbrev entries even if zero? user asked display only, and hide zero-debt users. We'll show the items with their amounts (0 allowed) — but since user asked hide zero-debt users, a user will have at least one non-zero item
            const abb = ABBR[med] || med;
            parts.push(`${abb}: ${val.toLocaleString()}`);
          });

          const row = document.createElement("div");
          row.className = 'debtRow';
          const text = document.createElement("div");
          text.className = 'debtText';
          text.textContent = `${stripTags(user)} | ${parts.join(' | ')}`;

          // reset button
          const resetBtn = document.createElement("button");
          resetBtn.className = "resetBtn";
          resetBtn.textContent = "✅";
          resetBtn.title = "Reset member debts and deposit counters";
          resetBtn.addEventListener("click", (e) => {
            e.preventDefault();
            e.stopPropagation();
            // reset both deposits and used counters for this user
            delete deposits[user];
            // persist
            saveDeposits();
            renderPanel();
          });

          row.appendChild(text);
          row.appendChild(resetBtn);
          debtDiv.appendChild(row);
        } catch(e) {
          console.warn('render row failed', e);
        }
    });

    if (!anyShown) {
      debtDiv.innerHTML += "<div class='smallMuted'>No medical debts to show.</div>";
    }
}

// 45-second refresh (only when panel open)
setInterval(() => {
  if (!minimized && factionKey) loadLogs();
}, 45000);

// initial startup
if (!factionKey) {
  setTimeout(() => {
    // ask for key once shortly after load
    const key = prompt("Enter your Faction (Armory) API Key:", factionKey || "");
    if (key) {
      factionKey = key.trim();
      try { GM_setValue("factionAPIKey", factionKey); } catch(e){ console.warn('save key', e); }
      loadLogs();
    } else {
      // render with current data if any
      renderPanel();
    }
  }, 300);
} else {
  loadLogs();
  renderPanel();
}

// expose small helper to allow manual sync (optional)
window.HALO_Supplies_ForceSync = () => { if (factionKey) loadLogs(); };

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址