Torn Flight Helper

Select flight destination/type and store to session for automated flights

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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">&#8212;</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();
    }
})();