FlightClub

Flight Club helper script for Torn

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         FlightClub
// @namespace    http://tampermonkey.net/
// @version      2025-12-07.1
// @description  Flight Club helper script for Torn
// @author       S7upidity [3567556] based off code written by Silverdark [3503183], neth [3564828]
// @match        https://www.torn.com/item.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @connect      wiki.tornflight.club/
// @license      GPL-3.0-or-later
// ==/UserScript==
 
(async function () {
    'use strict';
 
    /* -----------------------------------------
       Item → Recipient mapping (human-editable)
       ----------------------------------------- */
    const MIA_ITEMS = {
        282: "African Violet",
        617: "Banana Orchid",
        271: "Ceibo",
        277: "Cherry Blossom",
        263: "Crocus",
        260: "Dahlia",
        272: "Edelweiss",
        267: "Heather",
        264: "Orchid",
        276: "Peony",
        385: "Tribulus Omanens",
        384: "Camel",
        273: "Chamois",
        258: "Jaguar",
        215: "Kitten",
        281: "Lion",
        269: "Monkey",
        266: "Nessie",
        274: "Panda",
        268: "Red Fox",
        186: "Sheep",
        618: "Stingray",
        187: "Teddy Bear",
        261: "Wolverine",
    };
 
    const GALLO_ITEMS = {
        196: "Cannabis",
        197: "Ecstasy",
        206: "Xanax",
        203: "Shrooms",
        199: "LSD",
        204: "Speed",
        198: "Ketamine",
        205: "Vicodin",
        201: "PCP",
        870: "Love Juice",
        200: "Opium",
        394: "Brick",
        616: "Trout",
        219: "Concussion Grenade",
        246: "Fireworks",
        573: "Wrench",
        742: "Molotov Cocktail",
        220: "Grenade",
        190: "Semtex",
        229: "Stick Grenade",
        257: "Throwing Knife",
        814: "Tyrosine",
        464: "Melatonin",
        226: "Smoke Grenade",
        229: "Claymore Mine",
        611: "Snowball",
        847: "Nerve Gas",
        463: "Epinephrine",
        222: "Flash Grenade",
        840: "Nail Bomb",
        871: "Glitter Bomb",
        224: "Party Popper",
        256: "Tear Gas",
        465: "Serotonin",
        242: "HEG",
        239: "Ninja Star",
        392: "Pepper Spray"
    };
 
    const RECIPIENT_MIA = "MIA[131289]";
    const RECIPIENT_GALLO = "GalloInfligo[2133394]";
 
    /* -----------------------------------------
       Master recipient map and allowed set
       ----------------------------------------- */
    const recipientByItemId = new Map();
    Object.keys(MIA_ITEMS).forEach(id => recipientByItemId.set(String(id), RECIPIENT_MIA));
    Object.keys(GALLO_ITEMS).forEach(id => recipientByItemId.set(String(id), RECIPIENT_GALLO));
    const allowedItemIds = new Set(recipientByItemId.keys());
 
    const travelWebsiteUrl = "https://wiki.tornflight.club/";
    const dataKey_flightClub = "flightClubData";
    const supportedFlightClubDataVersion = "2.0";
    const flightClubCacheTimeInSeconds = 120;
 
    const listeners = {};
    const remainingByItemId = new Map();
 
    /* -----------------------------------------
       Event listeners
       ----------------------------------------- */
    on('flight-club-itemrow-changed', row => {
        setupFlightButton(row);
        updateStatusFlightClubItemRow(row);
    });
 
    on('flight-club-data-changed', async () => {
        const itemRows = await getItemRows();
        itemRows.forEach(updateStatusFlightClubItemRow);
    });
 
    init();
 
    /* -----------------------------------------
       Initialization functions
       ----------------------------------------- */
    function init() {
        initFlightClubData();
        initItemRowObserver();
    }
 
    async function initItemRowObserver() {
        const wrapper = await waitForElm(document.body, "#category-wrap");
 
        const observer = new MutationObserver(muts => {
            for (const m of muts) {
                const node = m.target.closest?.("li[data-item]") || null;
                if (node && allowedItemIds.has(node.dataset.item)) {
                    emit('flight-club-itemrow-changed', node);
                }
            }
        });
 
        observer.observe(wrapper, { subtree: true, attributes: true, childList: true });
    }
 
    async function getItemRows() {
        await waitForElm(document.body, "#category-wrap");
        return [...document.querySelectorAll("#category-wrap li[data-item]")]
            .filter(row => allowedItemIds.has(row.dataset.item));
    }
 
    /* -----------------------------------------
       Insert Flight Button into item rows
       ----------------------------------------- */
    function setupFlightButton(itemRow) {
        const actions = itemRow.querySelector(".actions-wrap");
        if (!actions) return;
 
        const originalSendButton = actions.children[1]; // Send button
 
        // Avoid adding button multiple times
        if (actions.querySelector(".flight-added")) return;
 
        // Create new button container
        const planeSlot = document.createElement("span");
        planeSlot.className = "flight-added";
 
        const btn = document.createElement("button");
        btn.className = "wai-btn";
        btn.style.width = "34px";
 
        btn.title = "Send to Flight Club";
 
        // Use airplane image + label
        const img = document.createElement("img");
        img.src = "https://wiki.tornflight.club/img/airplane.svg";
        img.alt = "FlightClub";
        img.style.width = "18px";
        img.style.height = "18px";
        img.style.verticalAlign = "middle";
 
        const label = document.createElement("span");
       // label.textContent = "FlightClub";
        label.style.marginLeft = "4px";
 
        btn.appendChild(img);
        btn.appendChild(label);
 
        btn.addEventListener('click', evt => {
            evt.stopPropagation();
            originalSendButton.click();
 
            const popup = evt.target.closest(".cont-wrap");
            if (!popup) return;
 
            waitForElm(popup, ".user-id-label").then(() => {
                fillSendPopup(popup);
            });
        });
 
        planeSlot.appendChild(btn);
 
        // Insert in slot 3 (index 2)
        if (actions.children.length >= 3) {
            actions.insertBefore(planeSlot, actions.children[2]);
        } else {
            actions.appendChild(planeSlot);
        }
 
        // Remove extra slot 4 if it exists
        if (actions.children.length > 3) {
            actions.removeChild(actions.children[4]);
        }
    }
 
    /* -----------------------------------------
       Autofill send popup with recipient and max amount
       ----------------------------------------- */
    function fillSendPopup(popup) {
        const itemRow = popup.closest("li[data-item]");
        if (!itemRow) return;
 
        const itemId = itemRow.dataset.item;
        const qtyHidden = popup.querySelector("input[type=hidden].amount");
        const qtyVisible = popup.querySelector("input[type=text].amount");
        const userField = popup.querySelector("input[type=text].user-id");
 
        qtyHidden.value = qtyVisible.dataset.max;
        qtyVisible.value = qtyVisible.dataset.max;
        userField.value = recipientByItemId.get(itemId) || "No ID";
    }
 
    /* -----------------------------------------
       Remaining status badge
       ----------------------------------------- */
    async function initFlightClubData() {
        let data = await GM_getValue(dataKey_flightClub, null);
        const now = Date.now();
 
        if (!data || data.version !== supportedFlightClubDataVersion || now > data.lastUpdate + flightClubCacheTimeInSeconds * 1000) {
            await GM_deleteValue(dataKey_flightClub);
 
            const raw = await fetchRemaining();
            data = { version: supportedFlightClubDataVersion, lastUpdate: now, rawRemainingByItemId: raw };
            await GM_setValue(dataKey_flightClub, data);
        }
 
        remainingByItemId.clear();
        Object.entries(data.rawRemainingByItemId).forEach(([id, val]) => remainingByItemId.set(id, val));
 
        emit('flight-club-data-changed');
    }
 
     
    /* -----------------------------------------
       Utilities
       ----------------------------------------- */
    function waitForElm(parent, selector) {
        return new Promise(resolve => {
            if (parent.querySelector(selector)) return resolve(parent.querySelector(selector));
 
            const obs = new MutationObserver(() => {
                const found = parent.querySelector(selector);
                if (found) {
                    obs.disconnect();
                    resolve(found);
                }
            });
 
            obs.observe(document.body, { childList: true, subtree: true });
        });
    }
 
    function gmFetch(url, method, headers = {}, data = '') {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({ method, url, headers, data, onload: resolve, onerror: reject });
        });
    }
 
    function on(event, cb) {
        if (!listeners[event]) listeners[event] = [];
        listeners[event].push(cb);
    }
 
    function emit(event, data) {
        (listeners[event] || []).forEach(fn => fn(data));
    }
})();