Faction Target Finder

Adds a button to the top of the page that opens a live raid target from the faction list.

// ==UserScript==
// @name         Faction Target Finder
// @version      1.0.9
// @namespace    http://tampermonkey.net/
// @description  Adds a button to the top of the page that opens a live raid target from the faction list.
// @author       Omanpx [1906686], Titanic_ [2968477]
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
    'use strict';

    // Configurable
    let defaultFacIDs = [];

    const NO_API_MIN_ID = 3600000;
    const NO_API_MAX_ID = 3900000;
    // End of configurable

    let facIDs, maxLevel, apiKey, attackLink, newTab, randTarget, randFaction, ffScouterApiKey, maxStats, minStats, db;
    const DB_NAME = 'FTF_Cache';
    const STORE_NAME = 'ff_stats';
    const DB_VERSION = 1;
    const CACHE_DURATION = 10 * 24 * 60 * 60 * 1000; // 10 days

    function initDB() {
        return new Promise((resolve, reject) => {
            if (db) return resolve(db);
            const request = indexedDB.open(DB_NAME, DB_VERSION);
            request.onupgradeneeded = event => {
                const dbInstance = event.target.result;
                if (!dbInstance.objectStoreNames.contains(STORE_NAME)) {
                    dbInstance.createObjectStore(STORE_NAME, { keyPath: 'userId' });
                }
            };
            request.onsuccess = event => {
                db = event.target.result;
                console.log("[FTF] IndexedDB initialized successfully.");
                resolve(db);
            };
            request.onerror = event => {
                console.error("[FTF] IndexedDB error:", event.target.errorCode);
                reject(event.target.errorCode);
            };
        });
    }

    function getStatsFromDB(userIds) {
        return new Promise((resolve, reject) => {
            if (!db) return reject("DB not initialized");
            const transaction = db.transaction([STORE_NAME], 'readonly');
            const store = transaction.objectStore(STORE_NAME);
            const results = {};
            if (userIds.length === 0) return resolve(results);

            let processedCount = 0;
            userIds.forEach(id => {
                const request = store.get(id);
                request.onsuccess = event => {
                    const result = event.target.result;
                    if (result && (Date.now() - result.timestamp < CACHE_DURATION)) results[id] = result;
                    if (++processedCount === userIds.length) resolve(results);
                };
                request.onerror = () => {
                    if (++processedCount === userIds.length) resolve(results);
                };
            });
        });
    }

    function saveStatsToDB(statsArray) {
        return new Promise((resolve, reject) => {
            if (!db) return reject("DB not initialized");
            const transaction = db.transaction([STORE_NAME], 'readwrite');
            const store = transaction.objectStore(STORE_NAME);
            statsArray.forEach(statData => store.put({
                userId: statData.player_id,
                stats: statData,
                timestamp: Date.now()
            }));
            transaction.oncomplete = () => resolve();
            transaction.onerror = event => reject(event.target.error);
        });
    }

    function fetchFFScouterStats(targetIds) {
        return new Promise((resolve) => {
            if (!ffScouterApiKey || targetIds.length === 0) return resolve([]);
            const FFSCOUTER_TARGETS_PER_REQ = 205;
            const promises = [];

            for (let i = 0; i < targetIds.length; i += FFSCOUTER_TARGETS_PER_REQ) {
                const chunk = targetIds.slice(i, i + FFSCOUTER_TARGETS_PER_REQ);
                const endpoint = `https://ffscouter.com/api/v1/get-stats?key=${ffScouterApiKey}&targets=${chunk.join(",")}`;
                promises.push(new Promise(resolveChunk => {
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: endpoint,
                        onload: response => {
                            try {
                                const responseData = JSON.parse(response.responseText);
                                if (Array.isArray(responseData)) resolveChunk(responseData);
                                else {
                                    console.error(`[FTF] FFScouter API Error:`, responseData.error || responseData);
                                    resolveChunk([]);
                                }
                            } catch (e) {
                                console.error(`[FTF] FFScouter JSON Parse Error:`, e);
                                resolveChunk([]);
                            }
                        },
                        onerror: error => {
                            console.error("[FTF] FFScouter Fetch Error:", error);
                            resolveChunk([]);
                        }
                    });
                }));
            }
            Promise.all(promises).then(results => resolve([].concat(...results)));
        });
    }

    init();

    function parseSuffixedNumber(input) {
        if (!input) return 0;
        const s = String(input).trim().toLowerCase();
        const lastChar = s.slice(-1);
        let value = parseFloat(s);

        if (isNaN(value)) return 0;

        switch (lastChar) {
            case 'k': value *= 1e3; break;
            case 'm': value *= 1e6; break;
            case 'b': value *= 1e9; break;
            case 't': value *= 1e12; break;
            case 'q': value *= 1e15; break;
        }
        return Math.floor(value);
    }

    function init() {
        const storedFacIDs = localStorage.getItem('FTF_FACTIONS') !== null ? localStorage.getItem('FTF_FACTIONS') : defaultFacIDs.join(',');
        facIDs = storedFacIDs.split(',').map(Number).filter(id => !isNaN(id) && id > 0);

        maxLevel = localStorage.getItem('FTF_LEVEL') || 100;
        apiKey = localStorage.getItem('FTF_API') || null;
        attackLink = localStorage.getItem('FTF_PROFILE') === 'true';
        newTab = localStorage.getItem('FTF_NEWTAB') === 'true';
        randFaction = localStorage.getItem('FTF_RAND_FACTION') === 'true';
        randTarget = localStorage.getItem('FTF_RAND_TARGET') === 'true';

        ffScouterApiKey = localStorage.getItem('FTF_FF_API') || '';
        maxStats = parseSuffixedNumber(localStorage.getItem('FTF_MAX_STATS'));
        minStats = parseSuffixedNumber(localStorage.getItem('FTF_MIN_STATS'));
    }

    function promptAPIKey() {
        const key = prompt('Enter a public API key here:');
        if (key && key.trim() !== '') {
            localStorage.setItem('FTF_API', key);
            init();
        } else {
            alert('No valid API key entered! Search cancelled.');
        }
    }

    function changeSettings() {
        const newApiKey = document.querySelector('#ftf-api').value.trim();
        const newLevel = document.querySelector('#ftf-max-level').value;
        const newProfile = document.querySelector('#ftf-profile').checked;
        const newNewTab = document.querySelector('#ftf-newtab').checked;
        const newRandFaction = document.querySelector('#ftf-random-faction').checked;
        const newRandTarget = document.querySelector('#ftf-random-target').checked;
        const newFFApiKey = document.querySelector('#ftf-ff-api').value.trim();
        const newMaxStats = document.querySelector('#ftf-max-stats').value;
        const newMinStats = document.querySelector('#ftf-min-stats').value;

        localStorage.setItem('FTF_PROFILE', newProfile);
        localStorage.setItem('FTF_NEWTAB', newNewTab);
        localStorage.setItem('FTF_RAND_FACTION', newRandFaction);
        localStorage.setItem('FTF_RAND_TARGET', newRandTarget);
        localStorage.setItem('FTF_FACTIONS', facIDs.join(','));
        localStorage.setItem('FTF_API', newApiKey);
        localStorage.setItem('FTF_FF_API', newFFApiKey);
        localStorage.setItem('FTF_MAX_STATS', newMaxStats);
        localStorage.setItem('FTF_MIN_STATS', newMinStats);

        /*if (newApiKey && newApiKey.trim() !== '') localStorage.setItem('FTF_API', newApiKey);
        else {
            alert('Invalid API key entered!');
            return;
        }*/

        if (newLevel >= 0 && newLevel <= 100) localStorage.setItem('FTF_LEVEL', newLevel);
        else {
            alert('Invalid max level, please enter a value between 0 and 100!');
            return;
        }

        init();
        toggleSettings();
    }

    function findTarget() {
        if (!apiKey) {
            promptAPIKey();
            return;
        }

        initDB().then(() => {
            console.log("[FTF] Checking personal Target List first...");
            processTargetList(null, (targetID) => {
                if (!targetID) processUrls();
            });
        }).catch(err => {
            console.error("[FTF] Failed to initialize DB. Stat checking will be disabled.", err);
            processTargetList(null, (targetID) => {
                if (!targetID) processUrls();
            });
        });
    }

    async function filterAndSelectTarget(potentialTargets) {
        if (potentialTargets.length === 0) return null;

        const useStatFilter = ffScouterApiKey && (maxStats > 0 || minStats > 0);

        if (!useStatFilter) {
            const target = randTarget ? potentialTargets[Math.floor(Math.random() * potentialTargets.length)] : potentialTargets[0];
            return target.id || target;
        }

        const targetIds = potentialTargets.map(t => t.id || t);

        try {
            const cachedStats = await getStatsFromDB(targetIds);
            const idsToFetch = targetIds.filter(id => !cachedStats[id]);

            if (idsToFetch.length > 0) {
                console.log(`[FTF] Fetching stats for ${idsToFetch.length} users from FFScouter.`);
                const fetchedStats = await fetchFFScouterStats(idsToFetch);
                if (fetchedStats.length > 0) await saveStatsToDB(fetchedStats);
                fetchedStats.forEach(data => cachedStats[data.player_id] = { stats: data });
            }

            const finalTargets = potentialTargets.filter(target => {
                const id = target.id || target;
                const statInfo = cachedStats[id];

                if (!statInfo || !statInfo.stats || statInfo.stats.bs_estimate === undefined) return true;

                const estimate = statInfo.stats.bs_estimate;
                const passesMax = maxStats > 0 ? estimate <= maxStats : true;
                const passesMin = minStats > 0 ? estimate >= minStats : true;
                return passesMax && passesMin;
            });

            if (finalTargets.length === 0) return null;

            const finalTarget = randTarget ? finalTargets[Math.floor(Math.random() * finalTargets.length)] : finalTargets[0];
            return finalTarget.id || finalTarget;
        } catch (error) {
            console.error("[FTF] Error during stat filtering, skipping.", error);
            const fallbackTarget = randTarget ? potentialTargets[Math.floor(Math.random() * potentialTargets.length)] : potentialTargets[0];
            return fallbackTarget.id || fallbackTarget;
        }
    }

    function processTargetList(url, callback) {
        const apiUrl = url || `https://api.torn.com/v2/user/list?cat=Targets&timestamp=${Date.now()}&striptags=true&limit=50&sort=ASC&key=${apiKey}`;
        console.log("[FTF] Fetching target list from:", apiUrl);

        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            onload(response) {
                const data = JSON.parse(response.responseText);

                if (data.error) {
                    console.error("[FTF] Failed fetching Target List, reason:", data.error.error);
                    return callback(null);
                }

                const targets = data.list || [];
                const suitableTargets = targets.filter(user => user.level <= maxLevel && user.status.state === "Okay");

                filterAndSelectTarget(suitableTargets).then(targetId => {
                    if (targetId) {
                        console.log(`[FTF] Suitable target found: ${targetId}. Stopping search.`);
                        openTargetPage(targetId);
                        return callback(targetId);
                    }

                    const nextUrl = data._metadata?.links?.next;

                    if (nextUrl) {
                        console.log("[FTF] No suitable target on this page. Fetching next page...");
                        processTargetList(nextUrl, callback);
                    } else {
                        console.log("[FTF] No suitable target found after checking all pages.");
                        return callback(null);
                    }
                });
            },
            onerror(error) {
                console.error("[FTF] Error loading Target List URL:", error);
                return callback(null);
            }
        });
    }

    function processUrls(index = 0, checked = new Set()) {
        if (facIDs.length === 0) {
            alert("Your faction list is empty. Please add some faction IDs in the settings panel.");
            return;
        }

        if (checked.size >= facIDs.length) {
            console.log("[FTF] No players met the conditions in any faction. Using failsafe random target.");
            openRandomNoApiTarget();
            return;
        }

        if (randFaction) {
            do {
                index = Math.floor(Math.random() * facIDs.length);
            } while (checked.has(index));
        }

        checked.add(index);

        const url = `https://api.torn.com/faction/${facIDs[index]}?selections=basic&timestamp=${Date.now()}&key=${apiKey}`;
        console.log(`[FTF] Checking faction ID: ${facIDs[index]}`);

        GM_xmlhttpRequest({
            method: "GET",
            url,
            onload(response) {
                const roster = JSON.parse(response.responseText);
                const potentialTargets = checkCondition(roster);

                if (potentialTargets && potentialTargets.length > 0) {
                    filterAndSelectTarget(potentialTargets).then(targetId => {
                        if (targetId) openTargetPage(targetId);
                        else processUrls(index + 1, checked);
                    });
                } else {
                    processUrls(index + 1, checked);
                }
            },
            onerror() {
                console.log(`[FTF] Error loading URL: ${url}`);
                processUrls(index + 1, checked);
            }
        });
    }

    function checkCondition(roster) {
        if ("error" in roster) {
            console.log("[FTF] Failed fetching faction roster, reason:", roster.error.error);
            return [];
        }

        return Object.keys(roster.members).filter(userId => {
            const member = roster.members[userId];
            return member.level <= maxLevel && member.status.state === "Okay" && member.days_in_faction >= 15;
        });
    }

    function openTargetPage(targetId) {
        let profileLink;
        if (attackLink) profileLink = `https://www.torn.com/loader.php?sid=attack&user2ID=${targetId}`;
        else profileLink = `https://www.torn.com/profiles.php?XID=${targetId}`;

        if (newTab) window.open(profileLink, '_blank');
        else window.location.href = profileLink;
    }

    function openRandomNoApiTarget() {
        const randomID = Math.floor(Math.random() * (NO_API_MAX_ID - NO_API_MIN_ID + 1)) + NO_API_MIN_ID;
        openTargetPage(randomID);
    }

    function toggleContainer() {
        const container = document.querySelector('.ftf-container');
        const toggleBtn = document.querySelector('.ftf-toggle-btn');
        if (!container || !toggleBtn) return;
        const isCollapsed = container.classList.toggle('ftf-collapsed');
        localStorage.setItem('FTF_COLLAPSED', isCollapsed);
        toggleBtn.textContent = isCollapsed ? '<' : '>';
    }

    const findBtn = createButton('Find Target', 'ftf-btn', findTarget);
    const chainSaveBtn = createButton('Chain Save', 'ftf-chain-save', openRandomNoApiTarget);
    const settBtn = createButton('Settings', 'ftf-settings', toggleSettings);
    const toggleBtn = createButton('>', 'ftf-toggle-btn', toggleContainer);
    const buttonWrapper = createDiv('ftf-button-wrapper');
    buttonWrapper.append(chainSaveBtn, findBtn, settBtn);

    const container = createDiv('ftf-container');
    container.append(toggleBtn, buttonWrapper);
    document.body.appendChild(container);

    if (localStorage.getItem('FTF_COLLAPSED') === 'true') {
        container.classList.add('ftf-collapsed');
        toggleBtn.textContent = '<';
    }

    let settingsModal;
    createSettingsModal();
    function createSettingsModal() {
        const modalOverlay = createDiv('ftf-modal-overlay');
        modalOverlay.id = 'ftf-settings-modal';
        modalOverlay.style.display = 'none';

        const modalContent = createDiv('ftf-modal-content');
        const modalBody = createDiv('ftf-modal-body');

        const appendElements = (parent, ...elements) => {
            const tempDiv = document.createElement('div');
            tempDiv.classList.add('ftf-settings-row');
            elements.forEach(el => tempDiv.append(el));
            parent.append(tempDiv);
        };

        const { input: apiKeyInput, label: apiKeyLabel } = createInput('ftf-api', "API Key (Limited)", apiKey || '', "text");
        appendElements(modalBody, apiKeyLabel, apiKeyInput);

        const { input: ffApiInput, label: ffApiLabel } = createInput('ftf-ff-api', "FFScouter Key", ffScouterApiKey, "text");
        appendElements(modalBody, ffApiLabel, ffApiInput);

        const { input: maxInput, label: maxLabel } = createInput('ftf-max-level', "Max Level", maxLevel, "number");
        appendElements(modalBody, maxLabel, maxInput);

        const { input: minStatsInput, label: minStatsLabel } = createInput('ftf-min-stats', "Min Stats (k,m,b)", localStorage.getItem('FTF_MIN_STATS') || minStats, "text");
        appendElements(modalBody, minStatsLabel, minStatsInput);

        const { input: maxStatsInput, label: maxStatsLabel } = createInput('ftf-max-stats', "Max Stats (k,m,b)", localStorage.getItem('FTF_MAX_STATS') || maxStats, "text");
        appendElements(modalBody, maxStatsLabel, maxStatsInput);

        const addFactionWrapper = createDiv('ftf-settings-row');
        const addFactionLabel = document.createElement('label');
        addFactionLabel.textContent = 'Faction List';
        const addControlsDiv = createDiv('ftf-add-controls');
        const { input: addFactionInput } = createInput('ftf-add-faction-id', '', '', "number");
        addFactionInput.placeholder = 'Add ID...';

        const addFactionBtn = createButton('Add', 'ftf-add-btn', () => {
            const newId = parseInt(addFactionInput.value, 10);
            if (newId && !isNaN(newId) && newId > 0) {
                if (!facIDs.includes(newId)) {
                    facIDs.push(newId);
                    renderFactionList();
                    addFactionInput.value = '';
                } else { alert('That faction ID is already in the list.'); }
            } else { alert('Please enter a valid faction ID.'); }
        });
        addControlsDiv.append(addFactionInput, addFactionBtn);
        addFactionWrapper.append(addFactionLabel, addControlsDiv);
        modalBody.append(addFactionWrapper);

        const factionListContainer = createDiv('ftf-faction-list-container');
        factionListContainer.id = 'ftf-faction-list-container';
        const factionListRow = createDiv('ftf-settings-row');
        factionListRow.append(factionListContainer);
        modalBody.append(factionListRow);

        function renderFactionList() {
            factionListContainer.innerHTML = '';
            if (facIDs.length === 0) {
                factionListContainer.textContent = 'No factions in list.';
                return;
            }
            facIDs.forEach(id => {
                const item = document.createElement('div');
                item.className = 'ftf-faction-item';
                const nameSpan = document.createElement('span');
                nameSpan.textContent = id;
                item.appendChild(nameSpan);
                const removeBtn = createButton('✖', 'ftf-remove-btn', () => {
                    const index = facIDs.indexOf(id);
                    if (index > -1) facIDs.splice(index, 1);
                    renderFactionList();
                });
                item.appendChild(removeBtn);
                factionListContainer.appendChild(item);
            });
        }

        const { checkbox: profileCheckbox, label: profileLabel } = createCheckbox('ftf-profile', "Open directly to attack page?", attackLink);
        appendElements(modalBody, profileCheckbox, profileLabel);

        const { checkbox: tabCheckbox, label: tabLabel } = createCheckbox('ftf-newtab', "Open in new tab?", newTab);
        appendElements(modalBody, tabCheckbox, tabLabel);

        const { checkbox: randomFCheckbox, label: randomFLabel } = createCheckbox('ftf-random-faction', "Check random faction first?", randFaction);
        appendElements(modalBody, randomFCheckbox, randomFLabel);

        const { checkbox: randomTCheckbox, label: randomTLabel } = createCheckbox('ftf-random-target', "Select random target from list?", randTarget);
        appendElements(modalBody, randomTCheckbox, randomTLabel);

        const buttonContainer = createDiv('ftf-modal-buttons');
        const saveBtn = createButton('Save', 'ftf-save', changeSettings);
        const closeBtn = createButton('Close', 'ftf-close', toggleSettings);
        buttonContainer.append(saveBtn, closeBtn);

        modalContent.append(modalBody);
        modalContent.append(buttonContainer);
        modalOverlay.append(modalContent);
        document.body.appendChild(modalOverlay);
        settingsModal = modalOverlay;
        renderFactionList();
    }

    function toggleSettings() {
        if (!settingsModal) return;
        settingsModal.style.display = settingsModal.style.display === 'none' ? 'flex' : 'none';
    }

    function updateTimerColor(timerElement) {
        const wrapper = timerElement.parentNode;
        if (!wrapper || !wrapper.classList.contains('ftf-timer-wrapper')) return;

        const timeText = timerElement.textContent;
        const parts = timeText.split(':').map(Number);
        if (parts.length !== 2) return;

        const [minutes, seconds] = parts;
        const totalSeconds = (minutes * 60) + seconds;

        wrapper.classList.remove('ftf-timer-red', 'ftf-timer-yellow', 'ftf-timer-green');

        if (totalSeconds == 0) return
        else if (totalSeconds < 60) wrapper.classList.add('ftf-timer-red');
        else if (totalSeconds <= 180) wrapper.classList.add('ftf-timer-yellow');
        else wrapper.classList.add('ftf-timer-green');
    }

    const chainBarObserver = new MutationObserver((mutations, obs) => {
        const timerElement = document.querySelector('.bar-timeleft___B9RGV');
        if (timerElement) {
            timerElement.addEventListener('click', openRandomNoApiTarget);

            const timerWrapper = document.createElement('div');
            timerWrapper.className = 'ftf-timer-wrapper';
            timerElement.parentNode.insertBefore(timerWrapper, timerElement);
            timerWrapper.appendChild(timerElement);

            const timerTextObserver = new MutationObserver(() => updateTimerColor(timerElement));
            timerTextObserver.observe(timerElement, { characterData: true, childList: true, subtree: true });

            updateTimerColor(timerElement);
            obs.disconnect();
        }
    });

    chainBarObserver.observe(document.body, {
        childList: true,
        subtree: true
    });

    function addGlobalStyle(css) {
        var head, style;
        head = document.getElementsByTagName('head')[0];
        if (!head) { return; }
        style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }

    addGlobalStyle(`
        .ftf-container {
            display: grid;
            grid-template-columns: auto auto;
            gap: 0;
            position: fixed;
            top: 40%;
            right: 0;
            z-index: 9999;
            background-color: transparent;
            border-radius: 5px;
            transition: right 0.3s ease-in-out;
        }

        .ftf-button-wrapper {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }

        .ftf-container.ftf-collapsed .ftf-button-wrapper {
            display: none;
        }

        .ftf-toggle-btn {
            border: 1px solid #666;
            margin-right: 3px;
            color: #ccc;
            border-radius: 5px;
            cursor: pointer;
        }

        .ftf-toggle-btn:hover {
            background: #555;
        }

        .ftf-btn,
        .ftf-settings,
        .ftf-chain-save {
            font-size: 1em;
            padding: 5px 12px;
            cursor: pointer;
            border: 1px solid #666;
            border-radius: 5px;
            color: #ddd;
            text-shadow: 1px 1px 1px #000;
        }

        .ftf-btn {
            background: #5a5a5a;
        }

        .ftf-btn:hover {
            background: #6b6b6b;
        }

        .ftf-settings {
            background: #222;
        }

        .ftf-settings:hover {
            background: #333
        }

        .ftf-chain-save {
            background: #7a5a00;
        }

        .ftf-chain-save:hover {
            background: #8b6b00;
        }

        .ftf-modal-overlay {
            position: fixed;
            top: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 100000;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .ftf-modal-content {
            color: #ccc;
            background: #111;
            border-radius: 8px;
            max-width: 450px;
            max-height: 85vh;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .ftf-modal-body {
            padding: 15px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            overflow-y: auto;
            flex: 1;
        }

        .ftf-settings-row {
            display: grid;
            gap: 7px;
            align-items: center;
        }

        .ftf-settings-row label {
            color: orange;
        }

        #ftf-api,
        #ftf-ff-api,
        #ftf-max-level,
        #ftf-max-stats,
        #ftf-min-stats,
        #ftf-add-faction-id {
            background-color: transparent;
            border: 1px solid #444;
            color: white;
            padding: 5px;
            border-radius: 4px;
            text-align: left;
        }

        .ftf-settings-row input[type="checkbox"] {
            display: none;
        }

        .ftf-settings-row input[type="checkbox"]+label {
            position: relative;
            padding-left: 25px;
            cursor: pointer;
            color: white;
            font-size: 1em;
        }

        .ftf-settings-row input[type="checkbox"]+label:before {
            content: '';
            position: absolute;
            left: 0;
            width: 16px;
            height: 16px;
            border: 1px solid #444;
            border-radius: 5px;
        }

        .ftf-settings-row input[type="checkbox"]:checked+label:after {
            content: '✔';
            position: absolute;
            left: 4px;
            top: 1px;
            font-size: 1em;
            color: green;
        }

        #ftf-faction-list-container {
            background: #222;
            border: 1px solid #444;
            padding: 5px;
            border-radius: 4px;
            max-height: 130px;
            overflow-y: auto;
            grid-column: 1 / -1;
        }

        .ftf-faction-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 3px;
            border-bottom: 1px solid #333;
            font-size: 1em;
        }

        .ftf-faction-item:last-child {
            border-bottom: none;
        }

        .ftf-add-controls {
            display: flex;
            gap: 5px;
        }

        #ftf-add-faction-id {
            flex-grow: 1;
        }

        .ftf-modal-buttons {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            border-top: 1px solid #555;
            padding: 10px;
            background: #222;
            border-radius: 0 0 8px 8px;
        }

        .ftf-save,
        .ftf-close,
        .ftf-add-btn,
        .ftf-remove-btn {
            padding: 6px 15px;
            cursor: pointer;
            border-radius: 5px;
            text-shadow: 1px 1px 1px #000;
        }

        .ftf-save {
            color: #fff;
            background: #507b34;
            border: 1px solid #365223;
        }

        .ftf-save:hover {
            background: #5c8f3c;
        }

        .ftf-close {
            color: #ddd;
            background: #5a5a5a;
            border: 1px solid #333;
        }

        .ftf-close:hover {
            background: #6b6b6b;
        }

        .ftf-add-btn {
            padding: 6px 10px;
            color: #fff;
            background: #34687b;
            border: 1px solid #234752;
        }

        .ftf-add-btn:hover {
            background: #3c7a8f;
        }

        .ftf-remove-btn {
            padding: 2px 8px;
            font-size: 1.1em;
            color: #fff;
            background: #9d2f2f;
            border: 1px solid #712020;
        }

        .ftf-remove-btn:hover {
            background: #b13535;
        }

        .bar-stats___E_LqA {
            display: block !important;
        }

        .ftf-timer-wrapper {
            display: inline-block;
            border-radius: 8px;
            padding: 3px 5px;
            transition: all 0.3s ease;
        }

        .bar-timeleft___B9RGV {
            font-size: 60px;
            cursor: pointer;
            transition: color 0.3s;
        }

        .ftf-timer-green {
            color: #4CAF50 !important;
            background-color: rgba(76, 175, 80, 0.2) !important;
        }

        .ftf-timer-yellow {
            color: #FFC107 !important;
            background-color: rgba(255, 193, 7, 0.2) !important;
        }

        .ftf-timer-red {
            color: #F44336 !important;
            background-color: rgba(244, 67, 54, 0.3);
            animation: pulse-red 3s infinite;
        }

        @keyframes pulse-red {
            0% {
                background-color: rgba(244, 67, 54, 0.3);
            }

            50% {
                background-color: rgba(244, 67, 54, 0.6);
            }

            100% {
                background-color: rgba(244, 67, 54, 0.3);
            }
        }
    `);

    function createButton(text, className, onClick) {
        const button = document.createElement('button');
        button.className = className;
        button.textContent = text;
        button.addEventListener('click', onClick);
        return button;
    }

    function createDiv(className) {
        const div = document.createElement('div');
        div.className = className;
        return div;
    }

    function createInput(id, text, value, type) {
        const input = document.createElement('input');
        input.type = type;
        input.id = id;
        input.value = value;
        const label = document.createElement('label');
        label.htmlFor = id;
        label.textContent = text;
        return { input, label };
    }

    function createCheckbox(id, text, value) {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = id;
        checkbox.checked = value;
        const label = document.createElement('label');
        label.htmlFor = id;
        label.textContent = text;
        return { checkbox, label };
    }
})();