你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
(我已經安裝了使用者樣式管理器,讓我安裝!)
// ==UserScript==
// @name Torn Display Case Tracker (Clean One-line Rows)
// @namespace http://tampermonkey.net/
// @version 2.5
// @description Shows flowers and plushies in one clean line: name | total | (ms X) | LOC (initials + flag). Uses Torn public API key.
// @author Nova
// @match https://www.torn.com/displaycase.php*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
const FLOWERS = {
"Dahlia": "CH 🇨🇭",
"Orchid": "HW 🇭🇹",
"African Violet": "SA 🇿🇦",
"Cherry Blossom": "JP 🇯🇵",
"Peony": "CN 🇨🇳",
"Ceibo Flower": "AR 🇦🇷",
"Edelweiss": "CH 🇨🇭",
"Crocus": "NL 🇳🇱",
"Heather": "UK 🇬🇧",
"Tribulus Omanense": "AE 🇦🇪",
"Banana Orchid": "KY 🇰🇾"
};
const PLUSHIES = {
"Sheep Plushie": "B.B 🏪",
"Teddy Bear Plushie": "B.B 🏪",
"Kitten Plushie": "B.B 🏪",
"Jaguar Plushie": "MX 🇲🇽",
"Wolverine Plushie": "CA 🇨🇦",
"Nessie Plushie": "UK 🇬🇧",
"Red Fox Plushie": "CA 🇨🇦",
"Monkey Plushie": "AF 🌍",
"Chamois Plushie": "CH 🇨🇭",
"Panda Plushie": "CN 🇨🇳",
"Lion Plushie": "SA 🇿🇦",
"Camel Plushie": "AE 🇦🇪",
"Stingray Plushie": "AU 🇦🇺"
};
GM_addStyle(`
#setTrackerPanel {
position: fixed;
top: 100px;
left: 20px;
width: 300px;
background: #0b0b0b;
color: #eaeaea;
font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 10px;
border: 1px solid #444;
border-radius: 6px;
z-index: 2147483647;
box-shadow: 0 6px 18px rgba(0,0,0,0.6);
max-height: 65vh;
overflow-y: auto;
line-height: 1.2;
}
#setTrackerHeader {
background: #121212;
padding: 5px 6px;
cursor: pointer;
font-weight: 700;
font-size: 12px;
border-bottom: 1px solid #333;
user-select: none;
}
#setTrackerContent { padding: 6px; display: none; }
#setTrackerPanel .controls { margin-bottom:6px; }
#setTrackerPanel button {
margin: 2px 3px 6px 0;
font-size: 10px;
padding: 2px 6px;
background: #171717;
color: #eaeaea;
border: 1px solid #333;
border-radius: 3px;
cursor: pointer;
}
#setTrackerPanel button:hover { background: #222; }
.group-title { font-weight:700; margin-top:6px; margin-bottom:4px; }
ul.item-list { margin:0 0 6px 0; padding:0; list-style:none; }
li.item-row {
display:flex;
align-items:center;
gap:6px;
padding:2px 0;
white-space:nowrap;
}
.item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
.item-total { flex:0 0 46px; text-align:right; color:#cfe8c6; }
.item-ms { flex:0 0 48px; text-align:right; color:#f7b3b3; }
.item-loc { flex:0 0 60px; text-align:right; color:#bcbcbc; font-size:9px; }
#tc_status { font-size:11px; color:#bdbdbd; margin-bottom:6px; }
`);
const panel = document.createElement('div');
panel.id = 'setTrackerPanel';
panel.innerHTML = `
<div id="setTrackerHeader">▶ Display Case Tracker</div>
<div id="setTrackerContent">
<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"></div>
</div>
`;
document.body.appendChild(panel);
const headerEl = panel.querySelector('#setTrackerHeader');
const contentBox = panel.querySelector('#setTrackerContent');
headerEl.addEventListener('click', () => {
const open = contentBox.style.display === 'block';
contentBox.style.display = open ? 'none' : 'block';
headerEl.textContent = (open ? '▶' : '▼') + ' Display Case Tracker';
});
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();
}
function aggregateDisplay(data) {
const items = {};
const displayRaw = data.display || data.displaycase || data.displayCase || null;
if (!displayRaw) return items;
const entries = Array.isArray(displayRaw) ? displayRaw : Object.values(displayRaw);
for (const e of entries) {
if (!e) continue;
const name = e.name || e.item_name || e.title || e.item || null;
if (!name) continue;
const qty = Number(e.quantity ?? e.qty ?? e.amount ?? 1) || 0;
items[name] = (items[name] || 0) + qty;
}
return items;
}
function compareMode(required, items) {
const names = Object.keys(required);
const counts = names.map(n => items[n] || 0);
const highest = counts.length ? Math.max(...counts) : 0;
const diff = {};
names.forEach(name => {
const total = items[name] || 0;
diff[name] = { total, missing: Math.max(0, highest - total), loc: required[name] };
});
return { highest, diff };
}
function render(items) {
const flowers = compareMode(FLOWERS, items);
const plushies = compareMode(PLUSHIES, items);
let html = '';
html += `<div class="group-title">Flowers (highest: ${flowers.highest})</div>`;
html += `<ul class="item-list">`;
Object.keys(FLOWERS).forEach(name => {
const d = flowers.diff[name];
html += `<li class="item-row">
<span class="item-name">${name}</span>
<span class="item-total">${d.total}</span>
<span class="item-ms">(ms ${d.missing})</span>
<span class="item-loc">${d.loc}</span>
</li>`;
});
html += `</ul>`;
html += `<div class="group-title">Plushies (highest: ${plushies.highest})</div>`;
html += `<ul class="item-list">`;
Object.keys(PLUSHIES).forEach(name => {
const d = plushies.diff[name];
html += `<li class="item-row">
<span class="item-name">${name}</span>
<span class="item-total">${d.total}</span>
<span class="item-ms">(ms ${d.missing})</span>
<span class="item-loc">${d.loc}</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 {
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;
}
const items = aggregateDisplay(data);
if (Object.keys(items).length === 0) {
statusEl.textContent = 'No display items found. Key may lack permission.';
return;
}
render(items);
statusEl.textContent = 'Loaded.';
} catch (err) {
statusEl.textContent = 'Fetch failed.';
contentEl.innerHTML = `<div style="color:#f88;">${err.message}</div>`;
}
}
if (!apiKey) askKey(false);
else loadData();
})();