Select flight destination/type and store to session for automated flights
// ==UserScript==
// @name Torn Flight Helper
// @namespace http://tampermonkey.net/
// @version 0.16
// @description Select flight destination/type and store to session for automated flights
// @match https://www.torn.com/factions.php*
// @match https://www.torn.com/item.php*
// @match https://www.torn.com/page.php?sid=travel*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const DESTINATIONS = [
{ name: 'Mexico', id: 'mexico', travelId: 2 },
{ name: 'Cayman Islands', id: 'cayman', travelId: 3 },
{ name: 'Canada', id: 'canada', travelId: 4 },
{ name: 'Hawaii', id: 'hawaii', travelId: 5 },
{ name: 'United Kingdom', id: 'uk', travelId: 6 },
{ name: 'Argentina', id: 'argentina', travelId: 7 },
{ name: 'Switzerland', id: 'switzerland', travelId: 8 },
{ name: 'Japan', id: 'japan', travelId: 9 },
{ name: 'China', id: 'china', travelId: 10 },
{ name: 'UAE', id: 'uae', travelId: 11 },
{ name: 'South Africa', id: 'south-africa', travelId: 12 },
];
const FLIGHT_TYPES = [
{ name: 'Standard', id: 'standard' },
{ name: 'Airstrip', id: 'airstrip' },
{ name: 'Private', id: 'private' },
{ name: 'Business Class', id: 'business' },
];
const ITEM_IDS = {
SMALL_FIRST_AID_KIT: 68, // < 20 min
FIRST_AID_KIT: 67, // 20–39 min
MORPHINE: 66, // 40–69 min
};
const BLOOD_BAG_IDS = {
'A+': 732,
'A-': 733,
'B+': 734,
'B-': 735,
'AB+': 736,
'AB-': 737,
'O+': 738,
'O-': 739,
};
const BLOOD_TYPES = ['none', 'A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'];
let hospitalMinutes = null;
let isAbroad = false;
const currentPage = window.location.href;
const isTravelPage = currentPage.includes('sid=travel');
const isMedPage = currentPage.includes('factions.php') || currentPage.includes('item.php');
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
}
function getApiKey() {
return sessionStorage.getItem('tfh-api-key') || '';
}
function getBloodType() {
return sessionStorage.getItem('tfh-blood-type') || 'none';
}
function getRecommendedItemId(minutes) {
if (minutes < 20) return ITEM_IDS.SMALL_FIRST_AID_KIT;
if (minutes < 40) return ITEM_IDS.FIRST_AID_KIT;
if (minutes < 70) return ITEM_IDS.MORPHINE;
const bloodType = getBloodType();
if (bloodType === 'none') {
console.log('[TFH] getRecommendedItemId: blood type set to none, no blood bag will be used');
return null;
}
const id = BLOOD_BAG_IDS[bloodType];
if (!id) {
console.warn('[TFH] getRecommendedItemId: unknown blood type:', bloodType);
return null;
}
return id;
}
function isInHospitalDOM() {
return !!document.querySelector('a[aria-label^="Hospital:"]');
}
function setAbroadMode() {
isAbroad = true;
// Hide all field rows, keep only the button and status
document.querySelectorAll('#tfh-body > div').forEach(el => el.style.display = 'none');
const btn = document.getElementById('tfh-action-btn');
if (btn) {
btn.textContent = 'Fly Home';
btn.className = 'tfh-btn tfh-btn-flight';
}
setStatus('Currently abroad');
console.log('[TFH] Abroad mode activated');
}
function saveFlightData() {
const destSelect = document.getElementById('tfh-destination');
const typeSelect = document.getElementById('tfh-flight-type');
if (!destSelect || !typeSelect) return;
const flightData = {
destination: destSelect.value,
destinationName: destSelect.options[destSelect.selectedIndex].text,
flightType: typeSelect.value,
flightTypeName: typeSelect.options[typeSelect.selectedIndex].text,
};
sessionStorage.setItem('tfh-flight-data', JSON.stringify(flightData));
setStatus(`Saved: ${flightData.destinationName} (${flightData.flightTypeName})`);
}
// Returns minutes remaining in hospital, or null on error / not in hospital.
async function fetchHospitalTime() {
const key = getApiKey();
if (!key) {
setStatus('Enter API key first');
console.log('[TFH] fetchHospitalTime: no API key set');
return null;
}
console.log('[TFH] fetchHospitalTime: calling API...');
setStatus('Checking hospital...');
try {
const res = await fetch(`https://api.torn.com/user/?selections=profile,basic&key=${key}&_=${Date.now()}`);
const data = await res.json();
console.log('[TFH] API response:', {
state: data.status?.state,
until: data.status?.until,
error: data.error,
});
if (data.error) {
setStatus(`API: ${data.error.error}`);
console.warn('[TFH] API error:', data.error);
return null;
}
if (data.status.state === 'Abroad' || data.status.state === 'Traveling') {
console.log('[TFH] Player is abroad/traveling (state:', data.status.state, ')');
setAbroadMode();
return null;
}
if (data.status.state !== 'Hospital') {
setStatus('Not in hospital');
updateMedButton(null);
console.log('[TFH] Player is not in hospital (state:', data.status.state, ')');
return null;
}
const now = Math.floor(Date.now() / 1000);
const secondsLeft = Math.max(0, data.status.until - now);
const minutes = secondsLeft / 60;
console.log('[TFH] Hospital time remaining:', {
until: data.status.until,
now,
secondsLeft,
minutes: minutes.toFixed(2),
});
if (minutes < 20) {
console.log('[TFH] Recommendation: Small Med Kit (< 20 min)');
setStatus(`${Math.ceil(minutes)}min: Small Med Kit`);
} else if (minutes < 40) {
console.log('[TFH] Recommendation: Med Kit (20–39 min)');
setStatus(`${Math.ceil(minutes)}min: Med Kit`);
} else if (minutes < 70) {
console.log('[TFH] Recommendation: Morphine (40–69 min)');
setStatus(`${Math.ceil(minutes)}min: Morphine`);
} else {
console.log('[TFH] Recommendation: Blood Bag (70+ min), blood type:', getBloodType());
setStatus(`${Math.ceil(minutes)}min: Blood Bag`);
}
hospitalMinutes = minutes;
updateMedButton(minutes);
return minutes;
} catch (e) {
setStatus('Fetch failed');
console.error('[TFH] fetchHospitalTime error:', e);
return null;
}
}
function updateMedButton(minutes) {
const btn = document.getElementById('tfh-action-btn');
if (!btn) return;
btn.textContent = (minutes !== null && minutes >= 90) ? 'Med Out 2x' : 'Med Out';
}
async function useItem() {
console.log('[TFH] Med Out clicked — fetching fresh hospital time before acting');
const minutes = await fetchHospitalTime();
if (minutes === null) {
console.log('[TFH] useItem: aborting — not in hospital or fetch failed');
return;
}
const itemId = getRecommendedItemId(minutes);
console.log('[TFH] useItem: minutes =', minutes.toFixed(2), '| itemId =', itemId, '| bloodType =', getBloodType());
if (!itemId) {
setStatus('No item to use (no blood bag selected)');
return;
}
const rfcv = getCookie('rfc_v');
if (!rfcv) {
setStatus('No CSRF token in cookies');
console.warn('[TFH] useItem: rfc_v cookie not found');
return;
}
console.log('[TFH] useItem: sending POST — itemID:', itemId, 'rfcv:', rfcv);
setStatus('Using item...');
const fetchPromise = fetch(`https://www.torn.com/item.php?rfcv=${rfcv}`, {
method: 'POST',
credentials: 'include',
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-requested-with': 'XMLHttpRequest',
},
body: `step=useItem&itemID=${itemId}&item=${itemId}`,
});
try {
const res = await fetchPromise;
const text = await res.text();
console.log('[TFH] useItem response (first 300 chars):', text.substring(0, 300));
try {
const json = JSON.parse(text);
if (json.error) {
setStatus(`Error: ${json.error}`);
console.warn('[TFH] useItem: server error:', json.error);
return;
}
} catch {
if (!res.ok) {
setStatus(`Failed (${res.status})`);
console.log('[TFH] useItem: HTTP error', res.status);
return;
}
}
// < 90 min: one item clears hospital, skip DOM check and redirect straight away
if (minutes < 90) {
console.log('[TFH] < 90 min — item used, redirecting to travel agency');
setStatus('Redirecting...');
window.location.href = 'https://www.torn.com/page.php?sid=travel';
return;
}
// 90+ min: may still be in hospital, check DOM before redirecting
setStatus('Item used! Checking hospital...');
console.log('[TFH] 90+ min — waiting for sidebar to update...');
await new Promise(resolve => setTimeout(resolve, 2000));
const stillInHospital = isInHospitalDOM();
console.log('[TFH] Still in hospital (DOM check):', stillInHospital);
if (!stillInHospital) {
setStatus('Out of hospital! Redirecting...');
console.log('[TFH] Redirecting to travel agency');
window.location.href = 'https://www.torn.com/page.php?sid=travel';
} else {
setStatus('Still in hospital');
console.log('[TFH] Still in hospital after item use');
}
} catch (e) {
setStatus('Request failed');
console.error('[TFH] useItem: fetch error:', e);
}
}
async function fly() {
const saved = sessionStorage.getItem('tfh-flight-data');
if (!saved) {
setStatus('No destination saved');
return;
}
const data = JSON.parse(saved);
const dest = DESTINATIONS.find(d => d.id === data.destination);
if (!dest) {
setStatus('Unknown destination');
return;
}
const rfcv = getCookie('rfc_v');
if (!rfcv) {
setStatus('No CSRF token (rfc_v)');
return;
}
setStatus('Flying...');
console.log('[TFH] fly: destination=', dest.name, 'id=', dest.travelId, 'type=', data.flightType);
try {
const res = await fetch(`https://www.torn.com/travelagency.php?rfcv=${rfcv}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
},
body: `step=travel&id=${dest.travelId}&type=${data.flightType}`,
});
const text = await res.text();
console.log('[TFH] fly response:', text);
setStatus('Flight booked!');
} catch (err) {
console.error('[TFH] fly error:', err);
setStatus('Error: ' + err.message);
}
}
async function flyHome() {
const rfcv = getCookie('rfc_v');
if (!rfcv) {
setStatus('No CSRF token (rfc_v)');
return;
}
setStatus('Flying home...');
console.log('[TFH] flyHome: sending backHomeAction');
try {
const formData = new FormData();
formData.append('step', 'backHomeAction');
const res = await fetch(`https://www.torn.com/travelagency.php?rfcv=${rfcv}`, {
method: 'POST',
credentials: 'include',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData,
});
const text = await res.text();
console.log('[TFH] flyHome response:', text);
setStatus('Flying home!');
} catch (err) {
console.error('[TFH] flyHome error:', err);
setStatus('Error: ' + err.message);
}
}
function createGUI() {
const container = document.createElement('div');
container.id = 'torn-flight-helper';
const medFieldsHTML = isMedPage ? `
<div>
<label>Destination</label>
<select id="tfh-destination"></select>
</div>
<div>
<label>Flight Type</label>
<select id="tfh-flight-type"></select>
</div>
<div>
<label>Blood Type</label>
<select id="tfh-blood-type"></select>
</div>
<div>
<label>API Key</label>
<input type="password" id="tfh-api-key" placeholder="Torn API key" autocomplete="off">
</div>
` : '';
const btnText = isTravelPage ? 'Fly' : 'Med Out';
const btnClass = isTravelPage ? 'tfh-btn-flight' : 'tfh-btn-med';
container.innerHTML = `
<style>
#torn-flight-helper {
position: fixed;
top: 10px;
right: 10px;
z-index: 999999;
background: #1a1a2e;
border: 1px solid #16213e;
border-radius: 8px;
padding: 12px;
font-family: Arial, sans-serif;
color: #e0e0e0;
width: 220px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
#torn-flight-helper .tfh-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
cursor: move;
}
#torn-flight-helper .tfh-title {
font-size: 13px;
font-weight: bold;
color: #e94560;
}
#torn-flight-helper .tfh-minimize {
background: none;
border: none;
color: #e0e0e0;
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
#torn-flight-helper .tfh-body { display: flex; flex-direction: column; gap: 8px; }
#torn-flight-helper label { font-size: 11px; color: #aaa; margin-bottom: 2px; display: block; }
#torn-flight-helper select,
#torn-flight-helper input[type="password"] {
width: 100%;
padding: 5px;
background: #16213e;
color: #e0e0e0;
border: 1px solid #0f3460;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
}
#torn-flight-helper .tfh-btn {
padding: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
transition: opacity 0.2s;
}
#torn-flight-helper .tfh-btn:hover { opacity: 0.85; }
#torn-flight-helper .tfh-btn-flight {
background: #0f3460;
color: #e0e0e0;
}
#torn-flight-helper .tfh-btn-med {
background: #e94560;
color: #fff;
}
#torn-flight-helper .tfh-status {
font-size: 10px;
color: #888;
text-align: center;
min-height: 14px;
}
</style>
<div class="tfh-header">
<span class="tfh-title">Flight Helper</span>
<button class="tfh-minimize" id="tfh-toggle">—</button>
</div>
<div class="tfh-body" id="tfh-body">
${medFieldsHTML}
<button class="tfh-btn ${btnClass}" id="tfh-action-btn">${btnText}</button>
<div class="tfh-status" id="tfh-status"></div>
</div>
`;
document.body.appendChild(container);
if (isMedPage) {
const destSelect = container.querySelector('#tfh-destination');
const typeSelect = container.querySelector('#tfh-flight-type');
const bloodTypeSelect = container.querySelector('#tfh-blood-type');
const apiKeyInput = container.querySelector('#tfh-api-key');
DESTINATIONS.forEach(d => {
const opt = document.createElement('option');
opt.value = d.id;
opt.textContent = d.name;
destSelect.appendChild(opt);
});
FLIGHT_TYPES.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.name;
typeSelect.appendChild(opt);
});
BLOOD_TYPES.forEach(bt => {
const opt = document.createElement('option');
opt.value = bt;
opt.textContent = bt === 'none' ? 'No Blood Bag' : bt;
bloodTypeSelect.appendChild(opt);
});
// Restore from session
const saved = sessionStorage.getItem('tfh-flight-data');
if (saved) {
const data = JSON.parse(saved);
destSelect.value = data.destination || DESTINATIONS[0].id;
typeSelect.value = data.flightType || FLIGHT_TYPES[0].id;
}
bloodTypeSelect.value = getBloodType();
const savedKey = getApiKey();
if (savedKey) {
apiKeyInput.value = savedKey;
fetchHospitalTime();
}
destSelect.addEventListener('change', saveFlightData);
typeSelect.addEventListener('change', saveFlightData);
bloodTypeSelect.addEventListener('change', () => {
sessionStorage.setItem('tfh-blood-type', bloodTypeSelect.value);
console.log('[TFH] Blood type set to:', bloodTypeSelect.value);
});
apiKeyInput.addEventListener('change', () => {
const key = apiKeyInput.value.trim();
sessionStorage.setItem('tfh-api-key', key);
if (key) fetchHospitalTime();
});
}
// Minimize toggle
const body = container.querySelector('#tfh-body');
container.querySelector('#tfh-toggle').addEventListener('click', () => {
const hidden = body.style.display === 'none';
body.style.display = hidden ? 'flex' : 'none';
container.querySelector('#tfh-toggle').textContent = hidden ? '\u2014' : '+';
});
// Drag support
makeDraggable(container, container.querySelector('.tfh-header'));
// Action button
container.querySelector('#tfh-action-btn').addEventListener('click', () => {
if (isMedPage && isAbroad) {
flyHome();
} else if (isMedPage) {
useItem();
} else if (isTravelPage) {
fly();
}
});
}
function setStatus(msg) {
const el = document.getElementById('tfh-status');
if (el) el.textContent = msg;
}
function makeDraggable(element, handle) {
let offsetX, offsetY, dragging = false;
handle.addEventListener('mousedown', (e) => {
dragging = true;
offsetX = e.clientX - element.getBoundingClientRect().left;
offsetY = e.clientY - element.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
element.style.left = (e.clientX - offsetX) + 'px';
element.style.top = (e.clientY - offsetY) + 'px';
element.style.right = 'auto';
});
document.addEventListener('mouseup', () => { dragging = false; });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createGUI);
} else {
createGUI();
}
})();