Check Xanax stock every new hour in Torn City Time for 2 mins, polling every 5s
// ==UserScript==
// @name Torn Xanax Hourly Notifier
// @namespace https://torn.com
// @version 1.0
// @description Check Xanax stock every new hour in Torn City Time for 2 mins, polling every 5s
// @author You
// @match https://www.torn.com/pharmacy.php*
// @grant GM_notification
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const ITEM_NAME = 'Xanax';
const CHECK_INTERVAL = 5000; // 5 seconds
const WINDOW_START = -20; // seconds before the new hour
const WINDOW_END = 120; // seconds after the new hour
const SOUND_ENABLED = true;
const SOUND_URL = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg';
const NOTIFY_COOLDOWN = 10; // minutes
function nowTs(){ return Date.now(); }
function findStock() {
const rows = Array.from(document.querySelectorAll('tr, div, li'))
.filter(el => el.innerText.toLowerCase().includes(ITEM_NAME.toLowerCase()));
if (!rows.length) return null;
const txt = rows[0].innerText;
const regexes = [
/stock[:\s]*([0-9,]+)/i,
/available[:\s]*([0-9,]+)/i,
/in stock[:\s]*([0-9,]+)/i,
/x([0-9]+)/i,
/\b([0-9]+)\s+left\b/i
];
for (const r of regexes) {
const m = txt.match(r);
if (m && m[1]) return parseInt(m[1].replace(/,/g,''),10);
}
const anyNum = txt.match(/\b([0-9]{1,4})\b/);
if (anyNum) return parseInt(anyNum[1],10);
return null;
}
function shouldNotify() {
const last = GM_getValue('last_notify',0);
const diff = (nowTs() - last)/60000;
return diff >= NOTIFY_COOLDOWN;
}
function markNotified() { GM_setValue('last_notify', nowTs()); }
function notifyUser(msg) {
try {
if (typeof GM_notification === 'function') {
GM_notification({title:'Torn Pharmacy', text: msg, timeout:8000});
} else if ('Notification' in window) {
if (Notification.permission === 'granted') new Notification('Torn Pharmacy', {body: msg});
else if (Notification.permission !== 'denied') Notification.requestPermission().then(p=>{
if(p==='granted') new Notification('Torn Pharmacy',{body:msg});
});
}
} catch(e){ console.warn('Notify error', e); }
if (SOUND_ENABLED){
const a = new Audio(SOUND_URL);
a.play().catch(()=>{});
}
}
// check if we are in the Torn City hourly window
function inWindow() {
// Torn City time is UTC+1
const now = new Date();
const cityHour = new Date(now.getTime() + 3600000); // UTC+1 offset
const minutes = cityHour.getMinutes();
const seconds = cityHour.getSeconds();
const totalSec = minutes*60 + seconds;
return (totalSec >= (WINDOW_START+3600)%3600) && (totalSec <= WINDOW_END);
}
function poll() {
if (!inWindow()) return; // only check in the 2+ min window
const stock = findStock();
if (stock === null) return console.log('Xanax not found');
console.log(`Xanax stock: ${stock}`);
if (stock>0 && shouldNotify()){
notifyUser(`Xanax is in stock! (${stock} available)`);
markNotified();
}
}
setInterval(poll, CHECK_INTERVAL);
})();