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();
    }
})();