Points-style PDA panel showing Torn display + abroad stock. Collapsible. Abroad stock color-coded. Flight type selection + restock-aware recommendations.
À partir de
// ==UserScript==
// @name 💫 Points Maker (Optimized + Abroad Color + Flight+Restock)
// @namespace http://tampermonkey.net/
// @version 1.2.6
// @description Points-style PDA panel showing Torn display + abroad stock. Collapsible. Abroad stock color-coded. Flight type selection + restock-aware recommendations.
// @match https://www.torn.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// CONFIG
const PANEL_ID = 'points_maker_pda';
const POLL_INTERVAL_MS = 45 * 1000;
const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
const PROM_URL = 'https://api.prombot.co.uk/api/travel';
const SPECIAL_DRUG = 'Xanax';
const MAX_ABROAD = { flowers: 50, plushies: 50, drugs: 50 };
// Flights (minutes)
const FLIGHTS = {
"Mexico: Ciudad Juárez": { standard: 26, airstrip: 18, wlt: 13, business: 8 },
"Cayman Islands: George Town": { standard: 35, airstrip: 25, wlt: 18, business: 11 },
"Canada: Toronto": { standard: 41, airstrip: 29, wlt: 20, business: 12 },
"Hawaii: Honolulu": { standard: 134, airstrip: 94, wlt: 67, business: 40 },
"United Kingdom: London": { standard: 159, airstrip: 111, wlt: 80, business: 48 },
"Argentina: Buenos Aires": { standard: 167, airstrip: 117, wlt: 83, business: 50 },
"Switzerland: Zurich": { standard: 175, airstrip: 123, wlt: 88, business: 53 },
"Japan: Tokyo": { standard: 225, airstrip: 158, wlt: 113, business: 68 },
"China: Beijing": { standard: 242, airstrip: 169, wlt: 121, business: 72 },
"United Arab Emirates: Dubai": { standard: 271, airstrip: 190, wlt: 135, business: 81 },
"South Africa: Johannesburg": { standard: 297, airstrip: 208, wlt: 149, business: 89 }
};
// Restock timers (minutes)
const RESTOCK_TIMERS = {
Camel: 22, Panda: 18, Peony: 112, Tribulus: 107, Lion: 19, Violet: 98,
Jaguar: 27, Cherry: 121, Monkey: 23, Heather: 129, Ceibo: 129, RedFox: 28,
Nessie: 23, Stingray: 22, Banana: 95, Orchid: 118, Crocus: 114, Chaimos: 21,
Dahlia: 113, Edelweiss: 100
};
// DATA: Flowers
const FLOWERS = {
"Dahlia": { short: "Dahlia", loc: "MX 🇲🇽", country: "Mexico" },
"Orchid": { short: "Orchid", loc: "HW 🏝️", country: "Hawaii" },
"African Violet": { short: "Violet", loc: "SA 🇿🇦", country: "South Africa" },
"Cherry Blossom": { short: "Cherry", loc: "JP 🇯🇵", country: "Japan" },
"Peony": { short: "Peony", loc: "CN 🇨🇳", country: "China" },
"Ceibo Flower": { short: "Ceibo", loc: "AR 🇦🇷", country: "Argentina" },
"Edelweiss": { short: "Edelweiss", loc: "CH 🇨🇭", country: "Switzerland" },
"Crocus": { short: "Crocus", loc: "CA 🇨🇦", country: "Canada" },
"Heather": { short: "Heather", loc: "UK 🇬🇧", country: "United Kingdom" },
"Tribulus Omanense": { short: "Tribulus", loc: "AE 🇦🇪", country: "UAE" },
"Banana Orchid": { short: "Banana", loc: "KY 🇰🇾", country: "Cayman Islands" }
};
// DATA: Plushies
const PLUSHIES = {
"Sheep Plushie": { short: "Sheep", loc: "B.B 🏪", country: "Torn City" },
"Teddy Bear Plushie": { short: "Teddy", loc: "B.B 🏪", country: "Torn City" },
"Kitten Plushie": { short: "Kitten", loc: "B.B 🏪", country: "Torn City" },
"Jaguar Plushie": { short: "Jaguar", loc: "MX 🇲🇽", country: "Mexico" },
"Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦", country: "Canada" },
"Nessie Plushie": { short: "Nessie", loc: "UK 🇬🇧", country: "United Kingdom" },
"Red Fox Plushie": { short: "Fox", loc: "UK 🇬🇧", country: "United Kingdom" },
"Monkey Plushie": { short: "Monkey", loc: "AR 🇦🇷", country: "Argentina" },
"Chamois Plushie": { short: "Chamois", loc: "CH 🇨🇭", country: "Switzerland" },
"Panda Plushie": { short: "Panda", loc: "CN 🇨🇳", country: "China" },
"Lion Plushie": { short: "Lion", loc: "SA 🇿🇦", country: "South Africa" },
"Camel Plushie": { short: "Camel", loc: "AE 🇦🇪", country: "UAE" },
"Stingray Plushie": { short: "Stingray", loc: "KY 🇰🇾", country: "Cayman Islands" }
};
const COUNTRY_NAME_TO_CODE = {
'JAPAN': 'JAP', 'MEXICO': 'MEX', 'CANADA': 'CAN', 'CHINA': 'CHI', 'UNITED KINGDOM': 'UNI',
'ARGENTINA': 'ARG', 'SWITZERLAND': 'SWI', 'HAWAII': 'HAW', 'UAE': 'UAE', 'CAYMAN ISLANDS': 'CAY',
'SOUTH AFRICA': 'SOU', 'S.A': 'SOU', 'SA': 'SOU', 'TORN': 'BB', 'B.B': 'BB'
};
// --- Utilities ---
function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); }
function colorForPercent(value, max) { if (!max || max===0) return '#bdbdbd'; const pct=(value/max)*100; if(pct>=75) return '#00c853'; if(pct>=40) return '#3399ff'; return '#ff1744'; }
function buildRequiredList(mapObj) {
const fullNames=Object.keys(mapObj);
const shortNames=fullNames.map(fn=>mapObj[fn].short);
const locByShort={};
const countryByShort={};
fullNames.forEach(fn=>{ const s=mapObj[fn].short; locByShort[s]=mapObj[fn].loc; countryByShort[s]=mapObj[fn].country; });
return { fullNames, shortNames, locByShort, countryByShort };
}
const flowersReq = buildRequiredList(FLOWERS);
const plushReq = buildRequiredList(PLUSHIES);
// --- UI ---
let statusEl, summaryEl, contentEl;
function buildUI() {
if(document.getElementById(PANEL_ID)) return;
const root=document.createElement('div');
root.id=PANEL_ID;
root.innerHTML=`
<div id="tc_header" class="header">▶ 💫 Points Maker</div>
<div id="tc_content_wrapper">
<div id="tc_controls" class="controls">
<button id="tc_refresh">Refresh</button>
<button id="tc_setkey">Set API Key</button>
<button id="tc_resetkey">Reset Key</button>
<select id="tc_flight_type">
<option value="standard">Standard</option>
<option value="airstrip">Airstrip</option>
<option value="wlt">WLT Benefit</option>
<option value="business">Business Class</option>
</select>
</div>
<div id="tc_status">Waiting for API key...</div>
<div id="tc_summary"></div>
<div id="tc_content"></div>
</div>
`;
document.body.appendChild(root);
const headerEl=root.querySelector('#tc_header');
const contentWrapper=root.querySelector('#tc_content_wrapper');
let collapsed=GM_getValue(`${PANEL_ID}-collapsed`,false);
function updateCollapse(){ headerEl.textContent=(collapsed?'▶ ':'▼ ')+'💫 Points Maker'; contentWrapper.style.display=collapsed?'none':'block'; }
updateCollapse();
headerEl.addEventListener('click',()=>{ collapsed=!collapsed; GM_setValue(`${PANEL_ID}-collapsed`,collapsed); updateCollapse(); });
root.querySelector('#tc_refresh').addEventListener('click',()=>refreshAll(true));
root.querySelector('#tc_setkey').addEventListener('click',()=>askKey(true));
root.querySelector('#tc_resetkey').addEventListener('click',()=>{
GM_setValue('tornAPIKey',null);
apiKey=null;
if(statusEl) statusEl.textContent='Key cleared. Click Set API Key.';
if(summaryEl) summaryEl.innerHTML='';
if(contentEl) contentEl.innerHTML='';
stopPolling();
});
const flightDropdown = document.getElementById('tc_flight_type');
if(flightDropdown) flightDropdown.addEventListener('change',()=>refreshAll(true));
statusEl=root.querySelector('#tc_status');
summaryEl=root.querySelector('#tc_summary');
contentEl=root.querySelector('#tc_content');
}
GM_addStyle(`
#${PANEL_ID} { position: fixed; top: 42px; left: 100px; width: 250px; background: #0b0b0b; color: #eaeaea; font-family: "DejaVu Sans Mono", monospace; font-size: 9px; border: 1px solid #444; border-radius: 6px; z-index: 999999; box-shadow: 0 6px 16px rgba(0,0,0,0.5); max-height: 65vh; overflow-y: auto; line-height: 1.1; }
#${PANEL_ID} .header { background: #121212; padding: 4px 6px; cursor: pointer; font-weight:700; font-size:10px; border-bottom:1px solid #333; user-select:none; display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .controls { padding:6px; display:flex; gap:6px; }
#${PANEL_ID} .controls button, #${PANEL_ID} .controls select { font-size:9px; padding:2px 6px; background:#171717; color:#eaeaea; border:1px solid #333; border-radius:3px; cursor:pointer; }
#${PANEL_ID} .controls button:hover, #${PANEL_ID} .controls select:hover { background:#222; }
#${PANEL_ID} .summary-line { font-weight:700; margin:6px; font-size:10px; color:#dfe7ff; }
#${PANEL_ID} .low-line { color:#ff4d4d; font-weight:700; margin:6px; font-size:10px; }
#${PANEL_ID} .group-title { font-weight:700; margin:6px 6px 2px 6px; font-size:9.5px; }
#${PANEL_ID} ul.item-list { margin:4px 6px 8px 12px; padding:0; list-style:none; }
#${PANEL_ID} li.item-row { display:flex; align-items:center; gap:6px; padding:2px 0; white-space:nowrap; }
#${PANEL_ID} .item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
#${PANEL_ID} .item-total { flex:0 0 36px; text-align:right; color:#cfe8c6; }
#${PANEL_ID} .item-av { flex:0 0 60px; text-align:right; color:#f7b3b3; }
#${PANEL_ID} .item-loc { flex:0 0 36px; text-align:right; color:#bcbcbc; font-size:8.5px; }
#${PANEL_ID} #tc_status { font-size:9px; color:#bdbdbd; margin:6px; }
.stock-green{ color:#00c853 !important; } .stock-orange{ color:#ff9800 !important; } .stock-red{ color:#ff1744 !important; } .stock-gray{ color:#9ea6b3 !important; }
`);
buildUI();
// -- rest of your functions: gmGetJson, fetchTornDisplayInventory, fetchDisplayViaDOM, parseYata, parseProm, aggregateFromApiResponse remain unchanged --
// --- Recommendation logic for restock + flight ---
function recommendNext(lowItems, categoryMap, selectedFlightType) {
if(!lowItems) return null;
const name = lowItems.short;
const country = lowItems.country;
const loc = lowItems.loc;
let travelTimeMin = 0;
for(const flightDest in FLIGHTS){
if(flightDest.toUpperCase().includes(country.toUpperCase())){
travelTimeMin = FLIGHTS[flightDest][selectedFlightType]||0;
break;
}
}
const restockTime = RESTOCK_TIMERS[name]||60;
const currQty = categoryMap[name]||0;
const willBeAvailable = (currQty + travelTimeMin >= restockTime);
return { name, country, loc, travelTimeMin, willBeAvailable };
}
// --- Render UI ---
async function renderUI(itemsAgg, yataRaw, promRaw){
if(!contentEl) return;
const flightTypeEl=document.getElementById('tc_flight_type');
const selectedFlightType = flightTypeEl ? flightTypeEl.value : 'standard';
// counts
const flowerTotals = countsForReq(itemsAgg, flowersReq, FLOWERS);
const plushTotals = countsForReq(itemsAgg, plushReq, PLUSHIES);
const fCalc = calcSetsAndRemainderFromCounts(flowerTotals, flowersReq.shortNames);
const pCalc = calcSetsAndRemainderFromCounts(plushTotals, plushReq.shortNames);
const totalSets = fCalc.sets + pCalc.sets;
const totalPoints = totalSets*10;
const yataMap=parseYata(yataRaw);
const promMap=parseProm(promRaw);
const flowerDisplay=fCalc.remainder;
const plushDisplay=pCalc.remainder;
if(statusEl) statusEl.textContent=`Updated: ${new Date().toLocaleTimeString()} — Sets F:${fCalc.sets} P:${pCalc.sets}`;
if(summaryEl) summaryEl.innerHTML=`<div class="summary-line">Total sets: ${totalSets} | Points: ${totalPoints}</div>`;
const lowFlower=findLowest(flowerDisplay,flowersReq.locByShort,flowersReq.countryByShort);
const lowPlush=findLowest(plushDisplay,plushReq.locByShort,plushReq.countryByShort);
const flowerRec=recommendNext(lowFlower,flowerDisplay,selectedFlightType);
const plushRec=recommendNext(lowPlush,plushDisplay,selectedFlightType);
let html='';
if(flowerRec) html+=`<div class="low-line">🛫 Low on ${escapeHtml(flowerRec.name)} — travel to ${escapeHtml(flowerRec.country)} ${escapeHtml(flowerRec.loc)} ${flowerRec.willBeAvailable?'✅ Available on arrival':'⚠ Might be depleted'} 🛬</div>`;
if(plushRec) html+=`<div class="low-line">🛫 Low on ${escapeHtml(plushRec.name)} — travel to ${escapeHtml(plushRec.country)} ${escapeHtml(plushRec.loc)} ${plushRec.willBeAvailable?'✅ Available on arrival':'⚠ Might be depleted'} 🛬</div>`;
// --- continue rendering flowers, plushies, drugs as in your previous script ---
// --- you can paste original code for rendering below this recommendation section ---
contentEl.innerHTML=html;
}
// --- rest of polling and Torn API logic remains unchanged ---
})();