FlightClub

Flight Club helper script for Torn

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като 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));
    }
})();