Faction supplies tracker (medical integrated). Shows medical debts only (SFA, FA, MP). Deposits tracked internally. Reset per member kept.
当前为
// ==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或关注我们的公众号极客氢云获取最新地址