// ==UserScript==
// @name Torn Display Case Sets Tracker (API - fixed)
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Uses Torn public API (selection=display) to show flower and plushie set counts and per-item breakdown (total, av, ms). Prompt stores public API key locally.
// @author Nova
// @match https://www.torn.com/displaycase.php*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
// Required sets
const FLOWERS = [
"Dahlia", "Orchid", "African Violet", "Cherry Blossom", "Peony", "Ceibo Flower",
"Edelweiss", "Crocus", "Heather", "Tribulus Omanense", "Banana Orchid"
];
const PLUSHIES = [
"Sheep Plushie", "Teddy Bear Plushie", "Kitten Plushie", "Jaguar Plushie", "Wolverine Plushie",
"Nessie Plushie", "Red Fox Plushie", "Monkey Plushie", "Chamois Plushie", "Panda Plushie",
"Lion Plushie", "Camel Plushie", "Stingray Plushie"
];
// UI
GM_addStyle(`
#setTrackerPanel {
position: fixed;
top: 100px;
left: 20px;
width: 380px;
background: #fff;
color: #000;
font-family: monospace;
font-size: 12px;
border: 1px solid #444;
border-radius: 6px;
padding: 8px;
z-index: 2147483647;
box-shadow: 0 0 10px rgba(0,0,0,0.35);
max-height: 70vh;
overflow-y: auto;
line-height: 1.25;
}
#setTrackerPanel h4 { margin: 0 0 6px 0; font-size:13px; }
#setTrackerPanel .controls { margin-bottom:6px; }
#setTrackerPanel button { margin-right:6px; font-size:12px; padding:2px 6px; }
#setTrackerPanel ul { margin: 4px 0 8px 14px; padding:0; }
#setTrackerPanel li { margin: 2px 0; list-style: none; }
#setTrackerPanel .item-name { display:inline-block; width:180px; }
#setTrackerPanel .item-stats { display:inline-block; width:170px; text-align:right; }
`);
const panel = document.createElement('div');
panel.id = 'setTrackerPanel';
panel.innerHTML = `
<h4>Display Case Sets (API)</h4>
<div class="controls">
<button id="tc_refresh">Refresh</button>
<button id="tc_setkey">Set API Key</button>
<button id="tc_resetkey">Reset Key</button>
</div>
<div id="tc_status">Waiting for key...</div>
<div id="tc_content" style="margin-top:8px;"></div>
`;
document.body.appendChild(panel);
const statusEl = panel.querySelector('#tc_status');
const contentEl = panel.querySelector('#tc_content');
panel.querySelector('#tc_refresh').addEventListener('click', () => loadData());
panel.querySelector('#tc_setkey').addEventListener('click', () => askKey(true));
panel.querySelector('#tc_resetkey').addEventListener('click', () => {
GM_setValue('tornAPIKey', null);
apiKey = null;
statusEl.textContent = 'Key cleared. Click Set API Key.';
contentEl.innerHTML = '';
});
let apiKey = GM_getValue('tornAPIKey', null);
async function askKey(force) {
if (!apiKey || force) {
const k = prompt('Enter your Torn PUBLIC API key (public/minimal access):', apiKey || '');
if (k) {
apiKey = k.trim();
GM_setValue('tornAPIKey', apiKey);
}
}
if (apiKey) loadData();
}
// robust parser for API display data
function aggregateDisplay(data) {
const items = {};
// data.display may be array or object. also older tools use 'displaycase' or 'display'
const displayRaw = data.display || data.displaycase || data.displayCase || null;
if (!displayRaw) return items;
// If object (map) convert to values
const entries = Array.isArray(displayRaw) ? displayRaw : Object.values(displayRaw);
for (const e of entries) {
if (!e) continue;
// determine name field
const name = e.name || e.item_name || e.title || e.item || e.name_en || null;
// determine quantity
let qty = 0;
if (typeof e.quantity !== 'undefined') qty = Number(e.quantity) || 0;
else if (typeof e.qty !== 'undefined') qty = Number(e.qty) || 0;
else if (typeof e.amount !== 'undefined') qty = Number(e.amount) || 0;
else if (typeof e.q !== 'undefined') qty = Number(e.q) || 0;
else qty = 1; // many display entries are singletons
if (!name) continue;
items[name] = (items[name] || 0) + qty;
}
return items;
}
function calcSets(required, items) {
// counts for each required item
const counts = required.map(n => items[n] || 0);
const complete = counts.length ? Math.min(...counts) : 0;
const remaining = {};
const missing = {};
required.forEach((n) => {
const total = items[n] || 0;
const av = total - complete; // available after using complete full sets
remaining[n] = av;
missing[n] = av >= 1 ? 0 : (1 - av); // how many needed for next set
});
return { complete, remaining, missing };
}
function render(items) {
const flowers = calcSets(FLOWERS, items);
const plushies = calcSets(PLUSHIES, items);
let html = '';
html += `<div><strong>Flowers sets:</strong> ${flowers.complete}</div>`;
html += `<div><strong>Plushie sets:</strong> ${plushies.complete}</div>`;
html += `<hr/>`;
html += `<div><strong>Flowers breakdown</strong></div><ul>`;
FLOWERS.forEach(name => {
const total = items[name] || 0;
const av = flowers.remaining[name];
const ms = flowers.missing[name];
html += `<li><span class="item-name">${name}</span><span class="item-stats">${total} (av ${av}, ms ${ms})</span></li>`;
});
html += `</ul>`;
html += `<div><strong>Plushies breakdown</strong></div><ul>`;
PLUSHIES.forEach(name => {
const total = items[name] || 0;
const av = plushies.remaining[name];
const ms = plushies.missing[name];
html += `<li><span class="item-name">${name}</span><span class="item-stats">${total} (av ${av}, ms ${ms})</span></li>`;
});
html += `</ul>`;
contentEl.innerHTML = html;
}
async function loadData() {
contentEl.innerHTML = '';
if (!apiKey) {
statusEl.textContent = 'No API key set. Click "Set API Key".';
return;
}
statusEl.textContent = 'Fetching display via API...';
try {
// correct selection name: display
const url = `https://api.torn.com/user/?selections=display&key=${encodeURIComponent(apiKey)}`;
const res = await fetch(url);
const data = await res.json();
if (data.error) {
statusEl.textContent = `API error: ${data.error.error} (code ${data.error.code})`;
contentEl.innerHTML = '';
return;
}
// aggregate items
const items = aggregateDisplay(data);
// if nothing found, fallback: try 'displaycase' selection (some keys/version)
if (Object.keys(items).length === 0) {
// try alternative selection
// note: some older examples use 'display'. we've already used it, but try 'displaycase' as fallback
const altUrl = `https://api.torn.com/user/?selections=displaycase&key=${encodeURIComponent(apiKey)}`;
const altRes = await fetch(altUrl);
const altData = await altRes.json();
if (!altData.error) {
const altItems = aggregateDisplay(altData);
if (Object.keys(altItems).length) {
render(altItems);
statusEl.textContent = 'Loaded (fallback displaycase).';
return;
}
}
}
if (Object.keys(items).length === 0) {
statusEl.textContent = 'No display items found in API response.';
contentEl.innerHTML = 'If you can see your display case in the site but API returns nothing, your key might lack permissions. Use a public/minimal key with display permission.';
return;
}
render(items);
statusEl.textContent = 'Loaded from API.';
} catch (err) {
statusEl.textContent = 'Fetch failed. Check network / key.';
contentEl.innerHTML = `<div style="color:#900;">${err.message}</div>`;
}
}
// start
if (!apiKey) askKey(false);
else loadData();
})();