runnan

SBC Autofill with player swapping, AutoAll, and ForeverRun (639 & 638) with Retry Logic

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         runnan
// @namespace    http://tampermonkey.net/
// @version      3.3.0
// @description  SBC Autofill with player swapping, AutoAll, and ForeverRun (639 & 638) with Retry Logic
// @license      MIT
// @match        https://www.ea.com/ea-sports-fc/ultimate-team/web-app/*
// @match        https://www.easports.com/*/ea-sports-fc/ultimate-team/web-app/*
// @match        https://www.ea.com/*/ea-sports-fc/ultimate-team/web-app/*
// @run-at       document-end
// ==/UserScript==

/*
 * Script Usage Disclaimer
 * Use at your own risk.
 */
(function () {
    'use strict';
    let page = unsafeWindow;
    let stopRequested = false;

    // Utility: sleep function
    const sleep = ms => new Promise(res => setTimeout(res, ms));

    // Utility: simulate click on element
    function simulateClick(el) {
        if (!el) {
            console.log('[Runnan] Element not found for click');
            return false;
        }
        const r = el.getBoundingClientRect();
        ['mousedown', 'mouseup', 'click'].forEach(t =>
            el.dispatchEvent(new MouseEvent(t, {
                bubbles: true, cancelable: true,
                clientX: r.left + r.width / 2,
                clientY: r.top + r.height / 2,
                button: 0
            }))
        );
        return true;
    }

    // Utility: wait for element to appear
    function waitForElement(selector, timeout = 5000) {
        return new Promise(resolve => {
            const start = Date.now();
            (function poll() {
                const el = document.querySelector(selector);
                if (el) return resolve(el);
                if (Date.now() - start > timeout) return resolve(null);
                setTimeout(poll, 200);
            })();
        });
    }

    // Utility: Find button by text
    function findButtonByText(textArray) {
        const buttons = Array.from(document.querySelectorAll('button, span.btn-text'));
        for (let el of buttons) {
            const txt = el.textContent.trim();
            // Check if text matches AND element is visible
            if (textArray.includes(txt) && el.offsetParent !== null) {
                return el.tagName === 'BUTTON' ? el : el.closest('button');
            }
        }
        return null;
    }

    // Utility: Shuffle array
    function shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        return array;
    }

    // Utility: Simulate internal pitch Drag and Drop
    async function simulateDragAndDrop(sourceEl, destEl) {
        if (!sourceEl || !destEl) return false;

        const srcRect = sourceEl.getBoundingClientRect();
        const dstRect = destEl.getBoundingClientRect();

        const dragData = new DataTransfer();

        const opts = (x, y) => ({
            bubbles: true, cancelable: true, clientX: x, clientY: y
        });

        const startX = srcRect.left + srcRect.width/2;
        const startY = srcRect.top + srcRect.height/2;
        sourceEl.dispatchEvent(new PointerEvent('pointerdown', opts(startX, startY)));
        sourceEl.dispatchEvent(new MouseEvent('mousedown', opts(startX, startY)));
        await sleep(50);

        sourceEl.dispatchEvent(new DragEvent('dragstart', { ...opts(startX, startY), dataTransfer: dragData }));
        await sleep(50);

        const dstX = dstRect.left + dstRect.width/2;
        const dstY = dstRect.top + dstRect.height/2;

        destEl.dispatchEvent(new DragEvent('dragenter', { ...opts(dstX, dstY), dataTransfer: dragData }));
        destEl.dispatchEvent(new DragEvent('dragover', { ...opts(dstX, dstY), dataTransfer: dragData }));
        await sleep(100);

        destEl.dispatchEvent(new DragEvent('drop', { ...opts(dstX, dstY), dataTransfer: dragData }));
        sourceEl.dispatchEvent(new DragEvent('dragend', { ...opts(dstX, dstY), dataTransfer: dragData }));
        sourceEl.dispatchEvent(new PointerEvent('pointerup', opts(dstX, dstY)));
        sourceEl.dispatchEvent(new MouseEvent('mouseup', opts(dstX, dstY)));

        return true;
    }

    // --- Core Logic ---

    // Internal optimization algorithm to re-arrange owned out-of-position players using drag and drop
    async function optimizePositions() {
        console.log('[Runnan] Optimizing positions (Smart Swap)...');
        for (let pass = 0; pass < 3; pass++) {
            let madeSwap = false;
            const slotSelectors = Array.from({length: 11}, (_, i) => `.ut-squad-pitch-view.sbc > div:nth-child(${i+3})`);

            for (let i = 0; i < slotSelectors.length; i++) {
                const srcSlot = document.querySelector(slotSelectors[i]);
                if (!srcSlot) continue;

                const pedestal = srcSlot.querySelector('.ut-squad-slot-pedestal-view');
                if (!pedestal) continue;

                const isOwned = !srcSlot.querySelector('.small.player.item.concept') && !srcSlot.querySelector('.empty');
                const isIncorrect = pedestal.classList.contains('state-incorrectly-positioned');

                if (isOwned && isIncorrect) {
                    const possiblePositions = [
                        srcSlot.querySelector('.currentPosition')?.textContent,
                        ...Array.from(srcSlot.querySelectorAll('.otherPositions')).map(el => el.textContent)
                    ].filter(Boolean);

                    if (possiblePositions.length === 0) continue;

                    for (let j = 0; j < slotSelectors.length; j++) {
                        if (i === j) continue;

                        const dstSlot = document.querySelector(slotSelectors[j]);
                        if (!dstSlot) continue;

                        const dstLabel = dstSlot.querySelector('.label')?.textContent;

                        if (possiblePositions.includes(dstLabel)) {
                            const dstPedestal = dstSlot.querySelector('.ut-squad-slot-pedestal-view');
                            const dstOwned = !dstSlot.querySelector('.small.player.item.concept') && !dstSlot.querySelector('.empty');
                            const dstIncorrect = dstPedestal?.classList.contains('state-incorrectly-positioned');

                            // Swap if destination is entirely open or if its current occupant is incorrectly positioned too
                            if (!dstOwned || dstIncorrect) {
                                console.log(`[Runnan] Swapping slot ${i+1} to ${j+1} (${possiblePositions.join(',')} -> ${dstLabel})`);
                                const srcPlayerItem = srcSlot.querySelector('.small.player.item');
                                const dstPlayerItem = dstSlot.querySelector('.small.player.item') || dstSlot;

                                await simulateDragAndDrop(srcPlayerItem, dstPlayerItem);
                                await sleep(1500);
                                madeSwap = true;
                                break;
                            }
                        }
                    }
                }
            }
            if (!madeSwap) {
                console.log('[Runnan] No remaining internal swaps found.');
                break;
            }
        }
    }

    // 1. AutoAll Function
    // 1. AutoAll Function
    async function sbcAutoAll(challengeIndex = 0, sbcId = null) {
        // Retry loop control
        const maxRetries = 3;
        let attempt = 0;

        while (attempt <= maxRetries) {
            if (stopRequested) return false;
            attempt++;
            console.log(`[Runnan] Starting AutoAll... Attempt ${attempt}/${maxRetries + 1}`);

            // Step A: Autofill
            const autofillBtn = findButtonByText(['SBC squad autofill', 'SBC方案填充']);
            if (autofillBtn) {
                console.log('[Runnan] Clicking Autofill');
                simulateClick(autofillBtn);
                await sleep(1000);

                // Handling for input SBC URL
                // Handling for input SBC URL
                if (sbcId === '639' && challengeIndex === 1) {
                    // Check for input field (Wait up to 2 seconds)
                    // Use more specific selector based on the dialog structure provided
                    const inputSelector = '.ea-dialog-view--body input';
                    const input = await waitForElement(inputSelector, 2000);

                    if (input) {
                        let urlToFill = '';
                        const urls = [
                            // 'https://www.futbin.com/26/squad/100471706/sbc',
                            'https://www.futbin.com/26/squad/100490049/sbc',
                            'https://www.futbin.com/26/squad/100529570/sbc'
                        ];
                        urlToFill = urls[Math.floor(Math.random() * urls.length)];

                        console.log(`[Runnan] Inputting URL: ${urlToFill}`);

                        // Robust Input Setting for Frameworks (React/Vue/etc)
                        input.focus();
                        await sleep(50);

                        // Simulate keydown
                        input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'a' }));

                        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
                        nativeInputValueSetter.call(input, urlToFill);

                        input.dispatchEvent(new Event('input', { bubbles: true }));
                        await sleep(50);

                        // Simulate keyup
                        input.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'a' }));

                        input.dispatchEvent(new Event('change', { bubbles: true }));
                        await sleep(50);
                        input.blur();

                        await sleep(500);
                    } else {
                        console.log('[Runnan] Input field not found (skipped URL fill)');
                    }
                }

                const confirmBtn = await waitForElement('.ea-dialog-view button.btn-standard.primary', 3000);
                if (confirmBtn) {
                    console.log('[Runnan] Clicking Confirm (Autofill Dialog)');
                    simulateClick(confirmBtn);
                    await sleep(5000);

                    // Check for submit button immediately after autofill
                    console.log('[Runnan] Checking for early submit...');
                    const submitBtnSelector = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
                    let submitBtn = document.querySelector(submitBtnSelector);
                    if (!submitBtn) {
                        submitBtn = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
                    }

                    if (submitBtn && !submitBtn.classList.contains('disabled')) {
                        console.log('[Runnan] Squad ready after autofill. Submitting early...');
                        simulateClick(submitBtn);
                        await sleep(1000);

                        // Check for "Precious Player" / Warning dialog
                        const dialog = document.querySelector('.ea-dialog-view--body');
                        if (dialog) {
                            const buttons = Array.from(dialog.querySelectorAll('button'));
                            const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
                            if (continueBtn) {
                                console.log('[Runnan] Warning dialog detected (Early Submit). Clicking Continue.');
                                simulateClick(continueBtn);
                                await sleep(3000);
                            }
                        } else {
                            await sleep(2000);
                        }
                        return true; // Early success
                    }
                } else {
                    console.log('[Runnan] Confirm button not found in dialog.');
                }
            } else {
                console.log('[Runnan] Autofill button not found (might be already filled or wrong page)');
            }

            // --- SMART SWAP: Optimize positions on pitch before replacing concept players ---
            await optimizePositions();

            // Step B: Loop Players (Random Order)
            // Slots 2 to 12 correspond to nth-child indices
            const slots = Array.from({ length: 11 }, (_, k) => k + 3);
            const shuffledSlots = shuffleArray(slots);

            for (let i of shuffledSlots) {
                if (stopRequested) return false;
                console.log(`[Runnan] Processing slot ${i - 2}/11`);

                // Check submit button inside loop
                // Check if submit button is available and enabled
                const submitBtnSelectorLoop = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
                let submitBtnLoop = document.querySelector(submitBtnSelectorLoop);
                if (!submitBtnLoop) {
                    submitBtnLoop = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
                }

                if (submitBtnLoop && !submitBtnLoop.classList.contains('disabled')) {
                    console.log('[Runnan] Submit button enabled during loop. Submitting...');
                    simulateClick(submitBtnLoop);
                    await sleep(1000);

                    // Check for warning dialog
                    const dialog = document.querySelector('.ea-dialog-view--body');
                    if (dialog) {
                        const buttons = Array.from(dialog.querySelectorAll('button'));
                        const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
                        if (continueBtn) {
                            console.log('[Runnan] Warning dialog detected (Loop Submit). Clicking Continue.');
                            simulateClick(continueBtn);
                            await sleep(3000);
                        }
                    } else {
                        await sleep(2000);
                    }
                    return true; // Success, break loop
                }

                const playerSelector = `body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > div.ut-squad-pitch-view.sbc > div:nth-child(${i})`;
                const playerSlot = document.querySelector(playerSelector);
                if (!playerSlot) {
                    continue;
                }

                const isConcept = playerSlot.querySelector('.small.player.item.concept');
                const isEmpty = playerSlot.querySelector('.small.player.item.empty');

                if (!isConcept && !isEmpty) {
                    // Do not attempt replacement on players that are already in the club AND natively slotted here
                    // If they are still out of position after internal shuffle, we still don't replace them
                    // because user prioritizes not buying.
                    continue;
                }

                simulateClick(playerSlot);
                await sleep(800);

                // If we are here, we don't have the player (Target is empty/concept).
                // Try Swap Meets Requirements via EA interface / Extension interface
                let swapped = false;
                // Specific selector from user REMOVED
                // const swapSelector = "body > main > section > section > div.ut-navigation-container-view--content > div > div > section > div.ut-navigation-container-view--content > div > div.DetailPanel > div.fsu-substitutionBox > div:nth-child(2) > button:nth-child(3)";
                // let swapBtn = document.querySelector(swapSelector);

                // if (!swapBtn) {
                const swapBtn = findButtonByText(['Swap Meets Requirements Players', '替换为满足需求球员', '满需求']);
                // }
                if (swapBtn) {
                    simulateClick(swapBtn);
                    await sleep(1000);

                    // Existing Swap Logic
                    const listBase = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > section > div.ut-navigation-container-view--content > div > div.paginated-item-list.ut-pinned-list > ul > li';
                    let candidateItemBtn = null;
                    let candidateJ = -1;
                    let candidateRating = 0;

                    // First pass: Find duplicate players (with fut_icon icon_sbc)
                    for (let j = 1; ; j++) {
                        const itemBtn = document.querySelector(`${listBase}:nth-child(${j}) > button`);
                        const ratingEl = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-view--main.ut-item-view > div > div.rating`);
                        const fsuLocked = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.name.fsulocked`);
                        const academyGraduate = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-player-state-indicator-view.academy-graduate`);
                        const isDuplicate = document.querySelector(`${listBase}:nth-child(${j}) .fut_icon.icon_sbc`);

                        if (!itemBtn) break;
                        if (fsuLocked || academyGraduate) continue;

                        if (ratingEl && isDuplicate) {
                            const rating = parseInt(ratingEl.textContent, 10);
                            if (rating < 84) {
                                candidateItemBtn = itemBtn;
                                candidateJ = j;
                                candidateRating = rating;
                                break;
                            }
                        }
                    }

                    // Second pass: If no duplicate found, find any qualified player
                    if (!candidateItemBtn) {
                        for (let j = 1; ; j++) {
                            const itemBtn = document.querySelector(`${listBase}:nth-child(${j}) > button`);
                            const ratingEl = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-view--main.ut-item-view > div > div.rating`);
                            const fsuLocked = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.name.fsulocked`);
                            const academyGraduate = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-player-state-indicator-view.academy-graduate`);

                            if (!itemBtn) break;
                            if (fsuLocked || academyGraduate) continue;

                            if (ratingEl) {
                                const rating = parseInt(ratingEl.textContent, 10);
                                if (rating < 84) {
                                    candidateItemBtn = itemBtn;
                                    candidateJ = j;
                                    candidateRating = rating;
                                    break;
                                }
                            }
                        }
                    }

                    if (candidateItemBtn) {
                        console.log(`[Runnan] Swapping with item ${candidateJ} (Rating: ${candidateRating})`);
                        simulateClick(candidateItemBtn);
                        swapped = true;
                        await sleep(1000);
                    }
                }

                if (swapped) continue;

                // If Swap failed, we just leave the concept player / empty slot as is.
                // WE DO NOT BUY DIRECTLY.
            }

            // Step C: Submit
            console.log('[Runnan] Checking submit button...');
            const submitBtnSelector = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
            let submitBtn = document.querySelector(submitBtnSelector);

            // Fallback: find by text if selector failed
            if (!submitBtn) {
                submitBtn = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
            }

            if (submitBtn && !submitBtn.classList.contains('disabled')) {
                console.log('[Runnan] Submitting!');
                simulateClick(submitBtn);
                await sleep(1000);

                // Check for "Precious Player" / Warning dialog
                const dialog = document.querySelector('.ea-dialog-view--body');
                if (dialog) {
                    const buttons = Array.from(dialog.querySelectorAll('button'));
                    const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
                    if (continueBtn) {
                        console.log('[Runnan] Warning dialog detected. Clicking Continue.');
                        simulateClick(continueBtn);
                        await sleep(3000); // Wait for actual submission after confirmation
                    }
                } else {
                    await sleep(2000); // Wait remaining time if no dialog
                }

                return true; // Success
            } else {
                console.log('[Runnan] Submit button disabled or not found.');
                // If attempt < maxRetries, loop continues (retries)
                if (attempt <= maxRetries) {
                    console.log(`[Runnan] Retrying (${attempt}/${maxRetries})...`);
                    await sleep(2000);
                }
            }
        }

        console.log('[Runnan] AutoAll failed after all retries.');
        return false; // Failed
    }

    // 2. ForeverRun Function
    async function foreverRun(sbcId) {
        console.log(`[Runnan] Starting ForeverRun Loop for SBC ${sbcId}`);
        stopRequested = false;

        while (!stopRequested) {
            // 2.1 Click SBC by ID
            let sbcBtn = document.querySelector(`button[data-sbcid="${sbcId}"]`);

            // Fallback for 311 if not found via data attribute
            if (!sbcBtn && sbcId === '639') {
                sbcBtn = document.querySelector('body > main > section > section > div.ut-navigation-bar-view.navbar-style-landscape.currency-purchase > div.fsu-navsbc > button:nth-child(1)');
            }

            if (!sbcBtn) {
                console.log(`[Runnan] SBC ${sbcId} button not found. Please navigate to the SBC menu.`);
                await sleep(2000);
            } else {
                simulateClick(sbcBtn);
                await sleep(2000);
            }

            // 2.2 Loop 4 children
            for (let i = 1; i <= 4; i++) {
                if (stopRequested) break;
                console.log(`[Runnan] ForeverRun: Challenge ${i}/4`);

                const challengeSelector = `div.ut-sbc-challenges-view--challenges > div:nth-child(${i})`;
                const challenge = await waitForElement(challengeSelector, 2000);

                if (!challenge) {
                    // console.log(`[Runnan] Challenge ${i} not found.`);
                    continue;
                }

                simulateClick(challenge);
                await sleep(1000);

                // Click "Start Challenge" (开始挑战)
                const startBtn = findButtonByText(['Starting Challenge', 'Start Challenge', '开始挑战', '前往挑战']);
                if (startBtn) {
                    console.log('[Runnan] Clicking Start Challenge...');
                    simulateClick(startBtn);
                    await sleep(3000);

                    // Run AutoAll only if started
                    const success = await sbcAutoAll(i, sbcId);
                    if (!success) {
                        console.log('[Runnan] AutoAll failed. Stopping ForeverRun.');
                        stopRequested = true;
                        // Alert user?
                        alert('Runnan: Stopped due to repeated submit failures.');
                        break;
                    }
                } else {
                    console.log('[Runnan] Start Challenge button not found. Skipping challenge...');
                }

                // Return to challenges page
                const listCheck = await waitForElement('div.ut-sbc-challenges-view--challenges', 3000);
                if (!listCheck) {
                    console.log('[Runnan] Not in challenge list. Clicking Back.');
                    const backBtn = document.querySelector('button.ut-navigation-button-control');
                    if (backBtn) simulateClick(backBtn);
                    await sleep(2000);
                }
            }

            await sleep(1000);
        }
        console.log('[Runnan] ForeverRun Stopped');
    }

    function showSummaryPopup(picks) {
        const popup = document.createElement('div');
        Object.assign(popup.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            backgroundColor: '#1e1e1e',
            color: '#fff',
            padding: '20px',
            borderRadius: '8px',
            zIndex: 10000,
            maxHeight: '80vh',
            overflowY: 'auto',
            minWidth: '300px',
            border: '2px solid #ffc107',
            boxShadow: '0 4px 15px rgba(0,0,0,0.5)',
            fontFamily: 'sans-serif'
        });

        let html = '<h2 style="margin-top:0; color:#ffc107; text-align:center;">AutoPick Summary</h2>';
        html += '<ul style="list-style:none; padding:0;">';

        picks.forEach((p, idx) => {
            html += `<li style="padding: 10px 0; border-bottom: 1px solid #333; line-height: 1.4;">
                <strong style="font-size: 1.1em;">#${idx + 1} - ${p.name}</strong><br>
                Score (Rating): ${p.rating} | Rarity: ${p.rarity}<br>
                League: ${p.league}
            </li>`;
        });

        html += '</ul>';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        Object.assign(closeBtn.style, {
            width: '100%',
            padding: '10px',
            marginTop: '15px',
            backgroundColor: '#dc3545',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontWeight: 'bold',
            transition: 'transform 0.1s'
        });
        closeBtn.onmousedown = () => closeBtn.style.transform = 'scale(0.98)';
        closeBtn.onmouseup = () => closeBtn.style.transform = 'scale(1)';
        closeBtn.onclick = () => popup.remove();

        popup.innerHTML = html;
        popup.appendChild(closeBtn);
        document.body.appendChild(popup);
    }

    // 3. AutoPick Function
    async function sbcAutoPick() {
        console.log(`[Runnan] Starting AutoPick Loop`);
        stopRequested = false;
        let sessionPicks = [];

        while (!stopRequested) {
            // Find open button
            const openBtn = document.querySelector('.key-redeem-btn');
            if (!openBtn || openBtn.offsetParent === null) {
                console.log('[Runnan] .key-redeem-btn not found or hidden. AutoPick ended.');
                break;
            }

            console.log('[Runnan] Opening Player Pick...');
            simulateClick(openBtn);
            await sleep(2000);

            // Wait for player containers to load
            let playerItemsLoaded = await waitForElement('.ut-companion-carousel-item-view .player.item', 5000);
            if (!playerItemsLoaded) {
                console.log('[Runnan] Player cards not loaded in time. Stopping.');
                break;
            }

            await sleep(1000); // extra wait for animations

            const items = document.querySelectorAll('.ut-companion-carousel-item-view');
            let parsed_items = [];

            for (let i = 0; i < items.length; i++) {
                let el = items[i];
                let playerWrapper = el.querySelector('.player.item');
                if (!playerWrapper) continue;

                let isRare = playerWrapper.classList.contains('rare');
                let isCommon = playerWrapper.classList.contains('common');
                let isSpecial = !isRare && !isCommon;
                let rarityName = isSpecial ? 'Special' : (isRare ? 'Rare' : 'Common');

                let nameText = el.querySelector('.name')?.textContent || 'Unknown';

                let ratingText = el.querySelector('.rating')?.textContent || '0';
                let rating = parseInt(ratingText, 10);

                let bioRows = el.querySelectorAll('.ut-item-view--bio .ut-item-row .ut-item-row-label--left');
                let leagueText = bioRows.length > 1 ? bioRows[1].textContent : '';

                let priceText = el.querySelector('.fsu-PriceValue')?.textContent || el.querySelector('.currency-coins.item-slot-price')?.textContent || '0';
                let price = parseInt(priceText.replace(/,/g, ''), 10);

                let ownedText = el.querySelector('.fsu-cards-attr > div:nth-child(4)')?.textContent || 'NO';
                let isOwned = (ownedText.trim() === 'YES');

                parsed_items.push({
                    index: parseInt(el.getAttribute('data-index') || i, 10),
                    el: el,
                    name: nameText.trim(),
                    rarity: rarityName,
                    isSpecial: isSpecial,
                    rating: rating,
                    league: leagueText.trim(),
                    price: price,
                    isOwned: isOwned
                });
            }

            if (parsed_items.length === 0) {
                console.log('[Runnan] No valid player data extracted.');
                break;
            }

            // Sorting logic
            parsed_items.sort((a, b) => {
                if (a.isSpecial !== b.isSpecial) return a.isSpecial ? -1 : 1;
                if (a.isSpecial && b.isSpecial) {
                    if (a.isOwned !== b.isOwned) return a.isOwned ? 1 : -1;
                    return b.price - a.price;
                }
                if (a.rating !== b.rating) return b.rating - a.rating;
                if (a.isOwned !== b.isOwned) return a.isOwned ? 1 : -1;
                return b.price - a.price;
            });

            let bestPlayer = parsed_items[0];

            // League Override: if highest rating <= 83
            if (!bestPlayer.isSpecial && bestPlayer.rating <= 83) {
                const targetLeagues = ['POR 1', 'BEL 1'];
                const pref = parsed_items.find(p => targetLeagues.includes(p.league));
                if (pref) {
                    bestPlayer = pref;
                    console.log(`[Runnan] Override triggered: Selected ${pref.league} player.`);
                }
            }

            sessionPicks.push(bestPlayer);

            console.log(`[Runnan] Target Player Index: ${bestPlayer.index}, Name: ${bestPlayer.name}, Rating: ${bestPlayer.rating}, Special: ${bestPlayer.isSpecial}, Price: ${bestPlayer.price}, Owned: ${bestPlayer.isOwned}`);

            // Navigate array
            let targetIndex = bestPlayer.index;
            let attempts = 0;

            while (attempts < 10 && !stopRequested) {
                let currentActiveEl = document.querySelector('.ut-companion-carousel-item-view.active');
                let currentIndex = currentActiveEl ? parseInt(currentActiveEl.getAttribute('data-index'), 10) : 0;

                if (currentIndex === targetIndex) break; // Reached target!

                if (targetIndex > currentIndex) {
                    let btnRight = document.querySelector('.ut-companion-carousel-item-container-view a.tapRight') || document.querySelector('a.tapRight');
                    if (btnRight) simulateClick(btnRight);
                } else if (targetIndex < currentIndex) {
                    let btnLeft = document.querySelector('.ut-companion-carousel-item-container-view a.tapLeft') || document.querySelector('a.tapLeft');
                    if (btnLeft) simulateClick(btnLeft);
                }

                await sleep(600); // UI transition wait
                attempts++;
            }

            await sleep(1000); // Add a small break before clicking select

            // Validation and Fallback Setup
            let verifyActiveEl = document.querySelector('.ut-companion-carousel-item-view.active');
            let verifyIndex = verifyActiveEl ? parseInt(verifyActiveEl.getAttribute('data-index'), 10) : -1;

            if (verifyIndex !== targetIndex) {
                console.log(`[Runnan] Failed to navigate to index ${targetIndex} via arrows. Forcing direct interaction.`);
                // Fallback 1: Click the player card element directly.
                simulateClick(bestPlayer.el);
                await sleep(800);

                // Fallback 2: Try clicking the pagination dot map if it exists
                let dotEl = document.querySelector(`.carousel-indicator-dot[data-index="${targetIndex}"]`);
                if (dotEl) simulateClick(dotEl);
                await sleep(800);
            }

            // Select
            let selectBtn = findButtonByText(['选择', 'Select']);
            if (selectBtn) {
                console.log('[Runnan] Clicking Select...');
                simulateClick(selectBtn);
                await sleep(1500);
            }

            // Continue
            let continueBtn = document.querySelector('button.key-continue-btn');
            if (continueBtn && !continueBtn.classList.contains('disabled')) {
                console.log('[Runnan] Clicking Continue...');
                simulateClick(continueBtn);
                await sleep(1500);
            }

            // Confirm
            let confirmBtn = document.querySelector('button.key-confirm-btn');
            if (confirmBtn && confirmBtn.offsetParent !== null && !confirmBtn.classList.contains('disabled')) {
                console.log('[Runnan] Clicking Confirm...');
                simulateClick(confirmBtn);
                await sleep(2500);
            }

            // Wait before next loop iteration
            await sleep(2000);
        }

        console.log(`[Runnan] AutoPick Loop Finished.`);

        if (sessionPicks.length > 0) {
            showSummaryPopup(sessionPicks);
        }
    }

    // UI Initialization
    function initUI() {
        const container = document.createElement('div');
        Object.assign(container.style, {
            position: 'fixed',
            bottom: '40px',
            right: '40px',
            display: 'flex',
            flexDirection: 'column',
            gap: '10px',
            zIndex: 9999
        });

        const createBtn = (text, color, onClick) => {
            const btn = document.createElement('button');
            btn.textContent = text;
            Object.assign(btn.style, {
                padding: '10px 20px',
                background: color,
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                cursor: 'pointer',
                fontWeight: 'bold',
                transition: 'transform 0.1s, opacity 0.2s',
                boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
            });

            // Click animation (Scale)
            btn.addEventListener('mousedown', () => btn.style.transform = 'scale(0.95)');
            btn.addEventListener('mouseup', () => btn.style.transform = 'scale(1)');
            btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');

            // Handle click with debounce/disable
            btn.addEventListener('click', async (e) => {
                if (btn.disabled) return;

                // Visual feedback for click
                btn.style.opacity = '0.7';
                btn.style.transform = 'scale(0.95)';
                setTimeout(() => btn.style.transform = 'scale(1)', 100);

                // Disable briefly to prevent double click
                btn.disabled = true;
                const originalText = btn.textContent;
                btn.textContent = '...';

                try {
                    await onClick(e);
                } finally {
                    // Re-enable after short delay or action done
                    setTimeout(() => {
                        btn.disabled = false;
                        btn.style.opacity = '1';
                        btn.textContent = originalText;
                    }, 500);
                }
            });

            return btn;
        };

        // AutoAll Button
        const btnAuto = createBtn('AutoALL', '#28a745', async () => {
            stopRequested = false;
            await sbcAutoAll(0, null);
        });

        // Forever 639 Button
        const btnForever639 = createBtn('Forever 639', '#007bff', async () => {
            stopRequested = false;
            await foreverRun('639');
        });

        // Forever 638 Button
        const btnForever638 = createBtn('Forever 638', '#17a2b8', async () => {
            stopRequested = false;
            await foreverRun('638');
        });

        // 81+ One-click Button
        const btnOneClick = createBtn('81+', '#fd7e14', async () => {
            stopRequested = false;
            const xStr = prompt('How many times to click?', '1');
            const x = parseInt(xStr, 10);
            if (isNaN(x) || x <= 0) return;

            for (let i = 0; i < x; i++) {
                if (stopRequested) {
                    console.log('[Runnan] Stopped 81+ click loop');
                    break;
                }
                console.log(`[Runnan] 81+ click ${i + 1}/${x}`);

                const headers = Array.from(document.querySelectorAll('h1.tileTitle'));
                const targetHeader = headers.find(h => h.textContent.includes('81+'));

                if (targetHeader) {
                    const container = targetHeader.closest('.content-container');
                    if (container) {
                        const buttons = Array.from(container.querySelectorAll('button'));
                        const targetBtn = buttons.find(b => b.textContent.includes('一键完成'));

                        if (targetBtn) {
                            simulateClick(targetBtn);
                            await sleep(2500);
                        } else {
                            console.log('[Runnan] "一键完成" button not found');
                            await sleep(2500);
                        }
                    } else {
                        console.log('[Runnan] .content-container not found');
                        await sleep(2500);
                    }
                } else {
                    console.log('[Runnan] "81+" header not found');
                    await sleep(2500);
                }
            }
            console.log(`[Runnan] 81+ loop finished`);
        });

        // 80+ Job Button
        const btn80PlusJob = createBtn('80+ Job', '#6f42c1', async () => {
            stopRequested = false;
            const xStr = prompt('How many times to do the 80+ Job?', '1');
            const x = parseInt(xStr, 10);
            if (isNaN(x) || x <= 0) return;

            const findElByText = (textArray, selector = '*') => {
                const elements = Array.from(document.querySelectorAll(selector));
                for (let el of elements) {
                    if (el.children.length === 0 && el.textContent) {
                        const txt = el.textContent.trim();
                        if (textArray.some(t => txt.includes(t)) && el.offsetParent !== null) {
                            return el;
                        }
                    }
                }
                return null;
            };

            for (let i = 0; i < x; i++) {
                if (stopRequested) {
                    console.log('[Runnan] Stopped 80+ job loop');
                    break;
                }
                console.log(`[Runnan] 80+ job iteration ${i + 1}/${x}`);

                // Click SBC left tab
                let sbcTab = findElByText(['SBC', 'SBCs'], '.ut-tab-bar-item span, button');
                if (sbcTab) simulateClick(sbcTab.closest('.ut-tab-bar-item') || sbcTab);
                await sleep(1000);

                // Click "快速完成" top tab
                let fastTab = findElByText(['快速完成'], '.ut-navigation-bar-view--tab, button');
                if (fastTab) simulateClick(fastTab);
                await sleep(1000);

                // Click "80+ 球员 4选1" panel
                let targetHeader = findElByText(['80+ 球员 4选1', '80+'], 'h1.tileTitle, .title, .name');
                if (targetHeader) {
                    let container = targetHeader.closest('.content-container') || targetHeader.closest('.tile') || targetHeader;
                    simulateClick(container);
                } else {
                    console.log(`[Runnan] 80+ panel not found`);
                    await sleep(1000);
                    continue;
                }
                await sleep(1000);

                // Click "一键填充"
                let fillBtn = findButtonByText(['一键填充', 'SBC squad autofill', 'SBC方案填充']);
                if (fillBtn) simulateClick(fillBtn);
                await sleep(1000);

                // Check autofocus dialog
                const confirmBtn = document.querySelector('.ea-dialog-view button.btn-standard.primary');
                if (confirmBtn) {
                   simulateClick(confirmBtn);
                   await sleep(1000);
                }

                // Click "提交"
                let submitBtn = document.querySelector('button.ut-squad-tab-button-control.actionTab.right.primary') || findButtonByText(['提交', 'Submit', 'Submit SBC', 'Submit Squad']);
                if (submitBtn && !submitBtn.classList.contains('disabled')) {
                    simulateClick(submitBtn);
                    await sleep(1000);

                    const dialog = document.querySelector('.ea-dialog-view--body');
                    if (dialog) {
                        const buttons = Array.from(dialog.querySelectorAll('button'));
                        const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
                        if (continueBtn) {
                            simulateClick(continueBtn);
                            await sleep(1000);
                        }
                    }
                } else {
                    console.log(`[Runnan] 提交 button not found or disabled`);
                }
                await sleep(1000);
            }

            if (stopRequested) return;

            console.log('[Runnan] Going to store step');
            let storeTab = findElByText(['商店', 'Store'], '.ut-tab-bar-item span, button');
            if (storeTab) simulateClick(storeTab.closest('.ut-tab-bar-item') || storeTab);
            await sleep(1000);

            let unassignedItems = findElByText(['未分配的物品', 'Unassigned Items', '未分配物品']);
            if (unassignedItems) {
                let clickable = unassignedItems.closest('.tile, button') || unassignedItems;
                simulateClick(clickable);
                await sleep(1000);

                await sbcAutoPick();
            } else {
                console.log('[Runnan] 未分配的物品 not found');
            }
            console.log(`[Runnan] 80+ Job finished`);
        });

        // AutoPick Button
        const btnAutoPick = createBtn('Auto Pick', '#ffc107', async () => {
            stopRequested = false;
            await sbcAutoPick();
        });

        // Stop Button
        const btnStop = createBtn('Stop', '#dc3545', async () => {
            stopRequested = true;
            console.log('[Runnan] Stop Requested');
        });

        container.appendChild(btnAuto);
        container.appendChild(btnForever639);
        container.appendChild(btnForever638);
        container.appendChild(btnOneClick);
        container.appendChild(btn80PlusJob);
        container.appendChild(btnAutoPick);
        container.appendChild(btnStop);
        document.body.appendChild(container);
    }

    page.addEventListener('load', initUI);
    // Fallback if load already fired
    setTimeout(initUI, 2000);
})();