runnan

SBC Autofill with player swapping, AutoAll, and ForeverRun (423 & 422) with Retry Logic

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         runnan
// @namespace    http://tampermonkey.net/
// @version      3.2.0
// @description  SBC Autofill with player swapping, AutoAll, and ForeverRun (423 & 422) 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;
    }

    // --- Core Logic ---

    // 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 === '423' && 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)');
            }

            // 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) {
                    // If selector fails, it might be a different formation or already filled?
                    continue;
                }

                simulateClick(playerSlot);
                await sleep(800);

                // Check "Direct Purchase" button immediately
                const directBuyBtn = findButtonByText(['直接购买此球员', 'Direct Purchase', 'Buy Now']);

                if (!directBuyBtn) {
                    // console.log(`[Runnan] Slot ${i - 1}: No direct purchase option (Already owned?). Skipping.`);
                    // Reduced log spam
                    continue;
                }

                // If we are here, we don't have the player.
                // Try Swap
                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';
                    for (let j = 1; j <= 5; 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) {
                                console.log(`[Runnan] Swapping with item ${j} (Rating: ${rating})`);
                                simulateClick(itemBtn);
                                swapped = true;
                                await sleep(1000);
                                break;
                            }
                        }
                    }
                }

                if (swapped) continue;

                // If Swap failed, try Direct Purchase if cheap
                // Force back to detail view if we were in swap list
                if (swapBtn) {
                    simulateClick(playerSlot);
                    await sleep(800);
                }

                // Now check price and buy
                const directBuyBtnAfter = findButtonByText(['直接购买此球员', 'Direct Purchase', 'Buy Now']);
                if (directBuyBtnAfter) {
                    const subtext = directBuyBtnAfter.querySelector('.btn-subtext.currency-coins');
                    if (subtext) {
                        const price = parseInt(subtext.textContent.replace(/,/g, ''), 10);
                        if (price <= 1200) {
                            console.log(`[Runnan] Buying player for ${price}`);
                            simulateClick(directBuyBtnAfter);
                            await sleep(4000); // Wait longer for buy

                            // Check for buy confirmation modal if it exists?
                            const buyConfirm = await waitForElement('.dialog-body button', 500);
                            if (buyConfirm && (buyConfirm.textContent.includes('OK') || buyConfirm.textContent.includes('Yes') || buyConfirm.textContent.includes('确定'))) {
                                simulateClick(buyConfirm);
                                await sleep(2000);
                            }
                        } else {
                            console.log(`[Runnan] Price ${price} > 1200. Skipping.`);
                        }
                    }
                }
            }

            // 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 === '423') {
                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');
    }

    // 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 423 Button
        const btnForever423 = createBtn('Forever 423', '#007bff', async () => {
            stopRequested = false;
            await foreverRun('423');
        });

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

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

        container.appendChild(btnAuto);
        container.appendChild(btnForever423);
        container.appendChild(btnForever422);
        container.appendChild(btnStop);
        document.body.appendChild(container);
    }

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