runnan

personal sbc

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         runnan
// @namespace    http://tampermonkey.net/
// @version      1.0.17
// @description  personal sbc 
// @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)
 *
 * 本脚本仅供个人学习和研究使用,不得用于任何商业或非法用途。
 * 作者对因使用本脚本造成的任何直接或间接损失、损害或法律责任不承担任何责任。
 * 使用者须自行评估风险并对其行为负责。请务必遵守目标网站的用户协议和相关法律法规。
 *
 * This script is provided “as is,” without warranty of any kind, express or implied.
 * The author shall not be liable for any damages arising out of the use of this script.
 * Use at your own risk and in compliance with the target site’s terms of service and applicable laws.
 */
(function () {
    'use strict';
    let page = unsafeWindow;
    const DEFAULT_TIMEOUT = 15000;
    let running = false;
    let minRating = 85;
    let btn;
    let btn3;
    let btn4;
    let abortCtrl = null;
    let continuousPackRunning = false;
    let storageCounter89 = 0; // Global counter for 89+ rated cards sent to storage
    let storageCounter87 = 0; // Global counter for 87-88 rated cards sent to storage
    let massPackRunning = false;
    let storage87Limit = 50; // Configurable limit for 87-88 cards
    let massPackRounds = 10; // Number of rounds to execute
    let totwRunsPerRound = 2; // Number of TOTW SBC runs per round
    function makeAbortable(fn) {
        return function (...args) {
            const p = fn.apply(this, args);
            if (!abortCtrl) return p;
            const abortP = new Promise((_, rej) => {
                abortCtrl.signal.addEventListener('abort', () => rej(new Error('Aborted')), { once: true });
            });
            return Promise.race([p, abortP]);
        };
    }
    const _sleep = ms => new Promise(res => setTimeout(res, ms));
    function simulateClick(el) {
        if (!el) {
            return new Error('no element');
        };
        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
            }))
        );
    }
    function _waitForElement(fnOrSelector, timeout = DEFAULT_TIMEOUT) {
        return new Promise(resolve => {
            const start = Date.now();
            (function poll() {
                let el = typeof fnOrSelector === 'string'
                    ? document.querySelector(fnOrSelector)
                    : fnOrSelector();
                if (el) return resolve(el);
                if (Date.now() - start > timeout) return resolve(false);
                setTimeout(poll, 200);
            })();
        });
    }

    page._xhrQueue = [];
    (function () {
        if (page._xhrHooked) return;
        page._xhrHooked = true;
        page._xhrPromiseList = [];

        const originOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function (_method, url, ...args) {
            this._xhrFlag = { method: _method.toUpperCase(), url: String(url) };
            return originOpen.apply(this, [_method, url, ...args]);
        };

        const originSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function (body) {
            this.addEventListener('load', () => {
                for (const item of page._xhrPromiseList) {
                    if (
                        this._xhrFlag &&
                        this._xhrFlag.url.includes(item.apiPath) &&
                        (!item.method || this._xhrFlag.method === item.method.toUpperCase())
                    ) {
                        try {
                            item.resolve(JSON.parse(this.responseText));
                        } catch {
                            item.resolve(null);
                        }
                        clearTimeout(item._timer);
                    }
                }
                page._xhrPromiseList = page._xhrPromiseList.filter(
                    item => !(this._xhrFlag && this._xhrFlag.url.includes(item.apiPath)
                        && (!item.method || this._xhrFlag.method === item.method.toUpperCase()))
                );
            });
            return originSend.apply(this, arguments);
        };
    })();
    function overrideEventsPopup() {
        if (!page.events || typeof page.events.popup !== 'function') return;
        const interceptMap = {
            '珍贵球员': 44408,
            '快速任务': 2,
        };
        const _orig = page.events.popup;

        page.events.popup = function (
            title, message, callback, buttonOptions,
            inputPlaceholder, inputValue, inputEnabled, extraNode
        ) {
            if (typeof title === 'string') {
                for (let key in interceptMap) {
                    if (title.includes(key)) {
                        const code = interceptMap[key];
                        return callback(code);
                    }
                }
            }
            return _orig.call(this,
                title, message, callback, buttonOptions,
                inputPlaceholder, inputValue, inputEnabled, extraNode
            );
        };

    }
    function hookLoading() {
        if (!EAClickShieldView.__hookedForLoadingEnd) {
            const oldHideShield = EAClickShieldView.prototype.hideShield;
            EAClickShieldView.prototype.hideShield = function (e) {
                oldHideShield.apply(this, arguments);
                if (!this.isShowing()) {
                    if (Array.isArray(EAClickShieldView._onLoadingEndQueue)) {
                        for (const fn of EAClickShieldView._onLoadingEndQueue) {
                            try { fn(); } catch (e) { }
                        }
                        EAClickShieldView._onLoadingEndQueue = [];
                    }
                }
            };
            EAClickShieldView._onLoadingEndQueue = [];
            EAClickShieldView.__hookedForLoadingEnd = true;
        }
    }

    function _waitForRequest(apiPath, method, timeout = 15000) {
        return new Promise((resolve) => {
            const item = { apiPath, method, resolve };
            item._timer = setTimeout(() => {
                resolve(null);
                page._xhrPromiseList = page._xhrPromiseList.filter(x => x !== item);
            }, timeout);
            page._xhrPromiseList.push(item);
        });
    }

    function _waitEALoadingEnd() {
        return new Promise(res => {
            const shield = typeof gClickShield === 'object' ? gClickShield : null;
            if (shield && !shield.isShowing()) return res();
            EAClickShieldView._onLoadingEndQueue.push(res);
        });
    }

    function _waitForController(name, timeout = DEFAULT_TIMEOUT) {
        return new Promise((resolve, reject) => {
            const start = Date.now();
            (function poll() {
                try {
                    const ctrl = getAppMain()
                        .getRootViewController()
                        .getPresentedViewController()
                        .getCurrentViewController()
                        .getCurrentController();
                    if (ctrl.constructor.name === name) return resolve(ctrl);
                } catch { }
                if (Date.now() - start > timeout) return reject(new Error(`等待${name}超时`));
                setTimeout(poll, 800);
            })();
        });
    }

    function _clickFsuRefreshIfExist(text) {
        return new Promise(async resolve => {
            const btn = document.querySelector('.ut-image-button-control.filter-btn.fsu-refresh');
            if (!btn) return resolve(0);
            const req = waitForRequest('/purchased/items', 'GET', 10000);
            simulateClick(btn);
            await waitEALoadingEnd();
            const res = await req;
            const len = res?.itemData?.length || 0;
            console.log(`[刷新] ${text} 刷新得到 ${len}`);
            resolve(len);
        });
    }
    function _findEllipsisBtnOfUntradeableDupSection() {
        // Try multiple selectors to find the ellipsis button
        // First try the specific selector for storage duplicates section
        let btn = document.querySelector('section.sectioned-item-list.storage-duplicates header button.ut-image-button-control.ellipsis-btn');
        if (btn) return btn;
        
        // Fallback to the original selector
        btn = document
            .querySelector('.sectioned-item-list:last-of-type header.ut-section-header-view.relist-section')
            ?.querySelector('.ut-image-button-control.ellipsis-btn');
        if (btn) return btn;
        
        // Try a more general selector
        const sections = document.querySelectorAll('.sectioned-item-list');
        for (let section of sections) {
            if (section.classList.contains('storage-duplicates') || 
                section.querySelector('.ut-section-header-view.relist-section')) {
                const ellipsisBtn = section.querySelector('.ut-image-button-control.ellipsis-btn');
                if (ellipsisBtn) return ellipsisBtn;
            }
        }
        
        return null;
    }
    function _waitAndClickQuickSellUntradeableBtn(timeout = 8000) {
        return new Promise(async resolve => {
            const modal = await waitForElement('.view-modal-container.form-modal .ut-bulk-action-popup-view', timeout).catch(() => null);
            if (!modal) return resolve();
            const btn = [...modal.querySelectorAll('button')].find(b => b.textContent.includes('快速出售'));
            if (btn) {
                console.log('[QuickSell] 点击快速出售按钮');
                simulateClick(btn);
                await sleep(500);
                
                // Wait for and click confirmation button
                const confirmBtn = await waitForElement(() => {
                    const buttons = document.querySelectorAll('button');
                    return Array.from(buttons).find(b => 
                        b.textContent.includes('确定') || 
                        b.textContent.includes('OK') ||
                        b.textContent.includes('Confirm')
                    );
                }, 3000);
                
                if (confirmBtn) {
                    console.log('[QuickSell] 确认快速出售');
                    simulateClick(confirmBtn);
                    await sleep(1000);
                }
            }
            await waitEALoadingEnd();
            resolve();
        });
    }
    const DEFAULT_WAIT_TIMEOUT = 10000;
    function _waitForLoadingStart(timeout = DEFAULT_WAIT_TIMEOUT) {
        return waitForElement(
            '.ut-click-shield.showing.fsu-loading',
            timeout
        );
    }

    function _waitForLoadingEnd(timeout = DEFAULT_WAIT_TIMEOUT) {
        return new Promise((resolve, reject) => {
            const start = Date.now();
            function check() {
                const el = document.querySelector('.ut-click-shield.showing.fsu-loading');
                if (!el) {
                    return resolve();
                }
                if (Date.now() - start > timeout) {
                    return reject(new Error('等待 loading 隐藏超时'));
                }
                setTimeout(check, 80);
            }
            check();
        });
    }
    async function _waitFSULoading(timeout = DEFAULT_WAIT_TIMEOUT) {
        try {
            await waitForLoadingStart(2000);
        } catch (e) {
            return;
        }
        await waitForLoadingEnd(timeout);
    }

    async function _clickIfExists(selector, timeout = 2000, clickDelay = 500) {
        const el = await waitForElement(selector, timeout);
        if (!el) {
            throw new Error(`clickIfExists: 元素 "${selector}" ${timeout}ms 内未找到`);
        }
        if (clickDelay > 0) {
            await sleep(clickDelay);
        }
        simulateClick(el);
        return true;
    }
    function _waitForElementGone(selector, timeout = DEFAULT_TIMEOUT) {
        return new Promise(resolve => {
            const start = Date.now();
            (function check() {
                const el = document.querySelector(selector);
                if (!el) return resolve(true);
                if (Date.now() - start > timeout) return resolve(false);
                setTimeout(check, 200);
            })();
        });
    }
    async function _execute89SBC() {
        console.log('[execute89SBC] 开始执行89评分SBC');
        try {
            // Click the 89 SBC button (data-sbcid="1068")
            const sbcBtn = await waitForElement('button[data-sbcid="1068"]', 5000);
            if (!sbcBtn) {
                console.log('[execute89SBC] 未找到89评分SBC按钮');
                return false;
            }
            
            simulateClick(sbcBtn);
            await waitForController('UTSBCSquadSplitViewController', 10000);
            await waitEALoadingEnd();
            
            // Click "一键填充(优先重复)" button
            const fillBtn = await waitForElement(() => 
                Array.from(document.querySelectorAll('button.btn-standard.mini.call-to-action'))
                    .find(b => b.textContent.includes('一键填充(优先重复)')),
                5000
            );
            
            if (fillBtn) {
                console.log('[execute89SBC] 点击一键填充按钮');
                simulateClick(fillBtn);
                await waitFSULoading(10000);
                await sleep(1000);
            }
            
            // Submit SBC
            const req = waitForRequest('?skipUserSquadValidation=', 'PUT');
            await clickIfExists('button.ut-squad-tab-button-control.actionTab.right.call-to-action:not(.disabled)', 5000, 500);
            const data = await req;
            
            if (!data?.grantedSetAwards?.length) {
                console.log('[execute89SBC] SBC提交失败');
                return false;
            }
            
            await waitEALoadingEnd();
            
            // Claim rewards
            const claimBtn = await waitForElement(() => {
                const buttons = document.querySelectorAll('button');
                return Array.from(buttons).find(b => 
                    b.textContent.includes('领取奖励') || 
                    b.textContent.includes('Claim')
                );
            }, 3000);
            
            if (claimBtn) {
                console.log('[execute89SBC] 点击领取奖励');
                simulateClick(claimBtn);
                await sleep(1000);
            }
            
            // Decrement storage counter (89+ cards used for this SBC)
            storageCounter89 -= 4;
            console.log(`[execute89SBC] 89+存储计数器: ${storageCounter89}, 87-88存储计数器: ${storageCounter87}`);
            
            return true;
        } catch (error) {
            console.error('[execute89SBC] 错误:', error);
            return false;
        }
    }
    
    async function _execute10x84SBC() {
        console.log('[execute10x84SBC] 开始执行10x84 SBC');
        try {
            // Click the 10x84 SBC button (data-sbcid="1185")
            const sbcBtn = await waitForElement('button[data-sbcid="1185"]', 5000);
            if (!sbcBtn) {
                console.log('[execute10x84SBC] 未找到10x84 SBC按钮');
                return false;
            }
            
            simulateClick(sbcBtn);
            
            // Try to wait for SBC view
            try {
                await waitForController('UTSBCSquadSplitViewController', 10000);
            } catch (err) {
                console.log('[execute10x84SBC] 无法进入SBC视图,可能SBC不可用或已完成');
                return false;
            }
            await waitEALoadingEnd();
            
            // Click duplicate fill button
            const dupBtn = await waitForElement(() =>
                Array.from(document.querySelectorAll('button.btn-standard.mini.call-to-action'))
                    .find(b => b.textContent.trim() === '重复球员填充阵容'),
                5000
            );
            if (dupBtn) {
                console.log('[execute10x84SBC] 点击重复球员填充阵容');
                simulateClick(dupBtn);
                await waitFSULoading(10000);
                await sleep(1000);
            }
            
            // Find and click empty slot
            const emptySlot = await waitForElement('.ut-squad-slot-view .player.item.empty', 3000);
            if (emptySlot) {
                console.log('[execute10x84SBC] 点击空位');
                const slotView = emptySlot.closest('.ut-squad-slot-view');
                if (slotView) {
                    simulateClick(slotView);
                    await sleep(800);
                    
                    // Look for eligibility search button
                    const eligibilityBtn = await waitForElement(() => {
                        const buttons = document.querySelectorAll('button[data-r="eligibilitysearch"]');
                        if (buttons.length > 0) return buttons[0];
                        
                        return Array.from(document.querySelectorAll('button')).find(b => {
                            const text = b.textContent;
                            return text && (
                                text.includes('添加 任意') || 
                                text.includes('Add Any')
                            );
                        });
                    }, 5000);
                    
                    if (eligibilityBtn) {
                        console.log('[execute10x84SBC] 执行资格搜索');
                        simulateClick(eligibilityBtn);
                        await sleep(800);
                        
                        // Wait for player list to load
                        await waitForElement('.ut-pinned-list', 3000);
                        await sleep(300);
                        
                        // Click the "范围" (Range) filter button to show all players
                        const rangeBtn = await waitForElement(() => {
                            const containers = document.querySelectorAll('.pagingContainer');
                            for (let container of containers) {
                                const divs = container.querySelectorAll('div');
                                for (let div of divs) {
                                    // Find the div with "范围" text
                                    if (div.textContent === '范围') {
                                        // Find the button that's a sibling of this div
                                        const btn = div.parentElement.querySelector('button.btn-standard.call-to-action.listfilter-btn');
                                        if (btn) return btn;
                                    }
                                }
                            }
                            return null;
                        }, 3000);
                        
                        if (rangeBtn) {
                            console.log('[execute10x84SBC] 点击范围按钮,当前状态: ' + rangeBtn.textContent);
                            simulateClick(rangeBtn);
                            await sleep(500);
                        }
                        
                        // Try to add the first player
                        let firstAddBtn = await waitForElement('.ut-image-button-control.btnAction.add', 2000);
                        
                        // If no player found and range button exists, click it again (will change to "仅仓库")
                        if (!firstAddBtn && rangeBtn) {
                            console.log('[execute10x84SBC] 没有找到可添加的球员,切换到仅仓库模式');
                            simulateClick(rangeBtn);
                            await sleep(500);
                            
                            // Try again to find the first player
                            firstAddBtn = await waitForElement('.ut-image-button-control.btnAction.add', 2000);
                        }
                        
                        if (firstAddBtn) {
                            console.log('[execute10x84SBC] 添加第一个球员');
                            simulateClick(firstAddBtn);
                            await sleep(500);
                        }
                        
                        // Click canvas once to fill remaining slots
                        const canvasSel = '.ut-squad-pitch-view--canvas';
                        await clickIfExists(canvasSel, 5000, 500);
                    }
                }
            }
            
            // Click squad completion and confirm
            await clickIfExists(() =>
                Array.from(document.querySelectorAll('button.btn-standard.mini.call-to-action'))
                    .find(b => b.textContent.includes('阵容补全')),
                5000, 500);

            await clickIfExists(() =>
                Array.from(document.querySelectorAll('button'))
                    .find(b => b.textContent.trim() === '确定'),
                5000, 500);
            await waitFSULoading();
            
            // Submit SBC
            const req = waitForRequest('?skipUserSquadValidation=', 'PUT');
            await clickIfExists('button.ut-squad-tab-button-control.actionTab.right.call-to-action:not(.disabled)', 5000, 500);
            const data = await req;
            
            if (!data?.grantedSetAwards?.length) {
                console.log('[execute10x84SBC] SBC提交失败');
                return false;
            }
            
            await waitEALoadingEnd();
            
            // Claim rewards
            const claimBtn = await waitForElement(() => {
                const buttons = document.querySelectorAll('button');
                return Array.from(buttons).find(b => 
                    b.textContent.includes('领取奖励') || 
                    b.textContent.includes('Claim')
                );
            }, 3000);
            
            if (claimBtn) {
                console.log('[execute10x84SBC] 点击领取奖励');
                simulateClick(claimBtn);
                await sleep(1000);
            }
            
            return true;
        } catch (error) {
            console.error('[execute10x84SBC] 错误:', error);
            return false;
        }
    }
    
    async function _handleUnassignedSimple(minRating = 87) {
        // Simplified version without quick sell
        let controller = await getUnassignedController();
        let times = 0;
        let items = [];
        while (items.length === 0 && times < 3) {
            items = repositories.Item.getUnassignedItems();
            times++;
            await sleep(1500)
        }
        if(items.length === 0) {
            await navigateBackToPackView();
            return;
        }
        
        const tradablePlayers = items.filter(p => p.type === 'player' && p.loans === -1 && !p.untradeable);
        const toStorage = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && p.isDuplicate() && p.rating >= minRating);
        const clubPlayers = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && !p.isDuplicate());
        
        // Count cards by rating for storage
        const storage89Plus = toStorage.filter(p => p.rating >= 89);
        const storage87to88 = toStorage.filter(p => p.rating >= 87 && p.rating <= 88);
        
        console.log('[handleUnassignedSimple] tradablePlayers', tradablePlayers.length, 'clubPlayers', clubPlayers.length, 
                    'toStorage', toStorage.length, '(89+:', storage89Plus.length, ', 87-88:', storage87to88.length, ')');
        
        // Update global storage counters
        if (storage89Plus.length > 0) {
            storageCounter89 += storage89Plus.length;
            console.log(`[handleUnassignedSimple] 89+存储计数器: ${storageCounter89}`);
        }
        if (storage87to88.length > 0) {
            storageCounter87 += storage87to88.length;
            console.log(`[handleUnassignedSimple] 87-88存储计数器: ${storageCounter87}`);
        }
        
        if (tradablePlayers.length) {
            await moveItems(tradablePlayers, ItemPile.TRANSFER, controller);
            await sleep(1000);
        }
        if (clubPlayers.length) {
            await moveItems(clubPlayers, ItemPile.CLUB, controller);
            await sleep(1000);
        }
        if (toStorage.length) {
            await moveItems(toStorage, ItemPile.STORAGE, controller);
            await sleep(2000);
        }
        
        // Navigate back to pack view after handling
        await navigateBackToPackView();
        return true;
    }
    
    async function _continuousPackOpen(useStorage87 = true) {
        const minRating = useStorage87 ? 87 : 89;
        console.log(`[continuousPackOpen] 开始连续开包 - ${useStorage87 ? '87+存储模式' : '89+存储模式'}`);
        let packCount = 0;
        
        try {
            while (true) {
                packCount++;
                console.log(`[continuousPackOpen] 开第 ${packCount} 个包`);
                
                // Open pack
                await waitForController('UTStorePackViewController', 20000);
                
                await clickIfExists(() => {
                    const btns = document.querySelectorAll('button.currency.call-to-action');
                    return Array.from(btns).reverse().find(b => {
                        const txt = b.querySelector('span.text')?.textContent.trim();
                        return txt === '打开' &&
                            b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                    });
                }, 5000, 500);
                
                const hasUnassigned = await checkAndHandleUnassignedDialog();
                if (hasUnassigned) {
                    await handleUnassigned(minRating);
                    // handleUnassigned already navigates back to pack view

                    // Open pack
                    await waitForController('UTStorePackViewController', 20000);
                    
                    await clickIfExists(() => {
                        const btns = document.querySelectorAll('button.currency.call-to-action');
                        return Array.from(btns).reverse().find(b => {
                            const txt = b.querySelector('span.text')?.textContent.trim();
                            return txt === '打开' &&
                                b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                        });
                    }, 5000, 500);
                }

                // Wait for unassigned items
                await waitForController('UTUnassignedItemsSplitViewController', 20000);
                await sleep(1000);
                
                // Handle unassigned with quick sell using chosen min rating
                await handleUnassigned(minRating);
                // Now we should be back on pack view
                
                // Small delay before next pack
                await sleep(500);
            }
        } catch (error) {
            console.log(`[continuousPackOpen] 停止 - 共开了 ${packCount} 个包`);
            console.error('[continuousPackOpen] 错误:', error);
            stopContinuousPackOpen();
        }
    }
    
    
    async function _executeTOTWSBC() {
        console.log('[executeTOTWSBC] 开始执行TOTW SBC');
        try {
            // Click the TOTW SBC button (data-sbcid="918")
            const sbcBtn = await waitForElement('button[data-sbcid="918"]', 5000);
            if (!sbcBtn) {
                console.log('[executeTOTWSBC] 未找到TOTW SBC按钮');
                return false;
            }
            
            simulateClick(sbcBtn);
            
            // Try to wait for SBC view, but handle case where SBC might not be available
            try {
                await waitForController('UTSBCSquadSplitViewController', 5000);
            } catch (err) {
                console.log('[executeTOTWSBC] 无法进入SBC视图,可能SBC不可用或已完成');
                return false;
            }
            await waitEALoadingEnd();

            // Click 阵容补全
            const completeBtn = await waitForElement(() =>
                Array.from(document.querySelectorAll('button.btn-standard.mini.call-to-action'))
                    .find(b => b.textContent.includes('阵容补全')),
                5000
            );
            if (completeBtn) {
                console.log('[executeTOTWSBC] 点击阵容补全');
                simulateClick(completeBtn);
                await sleep(500);
            }
            
            // Click 确定
            await clickIfExists(() =>
                Array.from(document.querySelectorAll('button'))
                    .find(b => b.textContent.trim() === '确定'),
                5000, 500);
            await waitFSULoading();
            
            // Submit SBC
            const req = waitForRequest('?skipUserSquadValidation=', 'PUT');
            await clickIfExists('button.ut-squad-tab-button-control.actionTab.right.call-to-action:not(.disabled)', 5000, 500);
            const data = await req;
            
            if (!data?.grantedSetAwards?.length) {
                console.log('[executeTOTWSBC] SBC提交失败');
                return false;
            }
            
            await waitEALoadingEnd();
            
            // Claim rewards
            const claimBtn = await waitForElement(() => {
                const buttons = document.querySelectorAll('button');
                return Array.from(buttons).find(b => 
                    b.textContent.includes('领取奖励') || 
                    b.textContent.includes('Claim')
                );
            }, 3000);
            
            if (claimBtn) {
                console.log('[executeTOTWSBC] 点击领取奖励');
                simulateClick(claimBtn);
                await sleep(500);
            }
            
            return true;
        } catch (error) {
            console.error('[executeTOTWSBC] 错误:', error);
            return false;
        }
    }
    
    async function _massPackOpen(useStorage87 = true) {
        console.log(`[massPackOpen] 开始批量开包流程 - ${useStorage87 ? '87+存储模式' : '89+无限循环模式'}`);
        storageCounter89 = 0; // Reset counters at start
        storageCounter87 = 0;
        let round = 0;
        const minStorageRating = useStorage87 ? 87 : 89;
        
        try {
            // Continue based on mode
            while (useStorage87 ? (storageCounter87 <= storage87Limit) : true) {
                round++;
                if (useStorage87) {
                    console.log(`[massPackOpen] 第 ${round} 轮,当前87-88计数: ${storageCounter87}, 89+计数: ${storageCounter89}`);
                } else {
                    console.log(`[massPackOpen] 第 ${round} 轮,当前89+计数: ${storageCounter89}`);
                }
                
                // Open pack
                await waitForController('UTStorePackViewController', 20000);
                
                await clickIfExists(() => {
                    const btns = document.querySelectorAll('button.currency.call-to-action');
                    return Array.from(btns).reverse().find(b => {
                        const txt = b.querySelector('span.text')?.textContent.trim();
                        return txt === '打开' &&
                            b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                    });
                }, 5000, 500);
                
                // Check for unassigned items dialog before opening pack
                const hasUnassigned = await checkAndHandleUnassignedDialog();
                if (hasUnassigned) {
                    await handleUnassigned(minStorageRating);
                    // handleUnassigned already navigates back to pack view

                    // Open pack
                    await waitForController('UTStorePackViewController', 20000);
                    
                    await clickIfExists(() => {
                        const btns = document.querySelectorAll('button.currency.call-to-action');
                        return Array.from(btns).reverse().find(b => {
                            const txt = b.querySelector('span.text')?.textContent.trim();
                            return txt === '打开' &&
                                b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                        });
                    }, 5000, 500);
                }

                // Wait for unassigned items
                await waitForController('UTUnassignedItemsSplitViewController', 20000);
                await sleep(1000);
                
                // Handle unassigned with appropriate minimum rating
                await handleUnassigned(minStorageRating);
                // Now we should be back on pack view
                
                // Check if we should stop after handling unassigned (only in 87+ mode)
                if (useStorage87 && storageCounter87 > storage87Limit) {
                    console.log(`[massPackOpen] 87-88卡片超过${storage87Limit}张 (${storageCounter87}),停止执行`);
                    break;
                }
                
                // Execute 89 SBC while we have enough 89+ cards
                while (storageCounter89 >= 4) {
                    console.log(`[massPackOpen] 89+存储中有 ${storageCounter89} 张卡,执行89 SBC`);
                    const sbcSuccess = await execute89SBC();
                    
                    if (!sbcSuccess) {
                        await navigateBackToPackView();
                    }
                    
                    // Check if we should stop (only in 87+ mode)
                    if (useStorage87 && storageCounter87 > storage87Limit) {
                        console.log(`[massPackOpen] 87-88卡片超过${storage87Limit}张 (${storageCounter87}),停止执行`);
                        break;
                    }
                }
                
                // Execute TOTW SBC configurable times
                for (let totwRun = 1; totwRun <= totwRunsPerRound; totwRun++) {
                    await sleep(1000);
                    console.log(`[massPackOpen] 执行第 ${totwRun}/${totwRunsPerRound} 次 TOTW SBC`);
                    const totwSuccess = await executeTOTWSBC();
                    
                    if (!totwSuccess) {                        
                        await navigateBackToPackView();
                    }
                }
                
                // Small delay before next round
                await sleep(1000);
            }
            
            if (useStorage87) {
                console.log(`[massPackOpen] 完成!共执行 ${round} 轮,最终89+存储计数器: ${storageCounter89}, 87-88存储计数器: ${storageCounter87}`);
            } else {
                console.log(`[massPackOpen] 完成!共执行 ${round} 轮,最终89+存储计数器: ${storageCounter89}`);
            }
        } catch (error) {
            console.error('[massPackOpen] 错误:', error);
            stopMassPackOpen();
        }
    }
    
    async function forceLoop(storageRating = 87) {
        console.log(`[forceLoop] start - 存储${storageRating}+卡片`);
        try {
            // Open pack
            await waitForController('UTStorePackViewController', 20000);
            
            await clickIfExists(() => {
                const btns = document.querySelectorAll('button.currency.call-to-action');
                return Array.from(btns).reverse().find(b => {
                    const txt = b.querySelector('span.text')?.textContent.trim();
                    return txt === '打开' &&
                        b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                });
            }, 5000, 500);
            
            const hasUnassigned = await checkAndHandleUnassignedDialog();
            if (hasUnassigned) {
                await handleUnassigned(storageRating);
                // handleUnassigned already navigates back to pack view

                // Open pack
                await waitForController('UTStorePackViewController', 20000);
                
                await clickIfExists(() => {
                    const btns = document.querySelectorAll('button.currency.call-to-action');
                    return Array.from(btns).reverse().find(b => {
                        const txt = b.querySelector('span.text')?.textContent.trim();
                        return txt === '打开' &&
                            b.closest('.ut-store-pack-details-view')?.style.display !== 'none';
                    });
                }, 5000, 500);
            }

            // Wait for unassigned items
            await waitForController('UTUnassignedItemsSplitViewController', 20000);
            await sleep(1000);

            // Handle unassigned - move cards to storage based on chosen mode, no quick sell
            await handleUnassignedSimple(storageRating);
            // Now we should be back on pack view

            // Execute 89 SBC if we have enough cards
            if (storageCounter89 >= 4) {
                console.log(`[forceLoop] 89+存储中有 ${storageCounter89} 张卡,执行89 SBC`);
                const sbcSuccess = await execute89SBC();
                
                if (!sbcSuccess) {
                    await navigateBackToPackView();
                }
            }

            // Execute 10x84 SBC
            console.log('[forceLoop] 执行10x84 SBC');
            const sbc84Success = await execute10x84SBC();
            
            if (!sbc84Success) {
                await navigateBackToPackView();
            }
            
            // Check for and click "直接发送X个至俱乐部" button
            const sendToClubBtn = await waitForElement(() => {
                const buttons = document.querySelectorAll('button.btn-standard.call-to-action.mini');
                return Array.from(buttons).find(b => 
                    b.textContent.includes('直接发送') && b.textContent.includes('至俱乐部')
                );
            }, 3000);
            
            if (sendToClubBtn) {
                console.log(`[forceLoop] 找到按钮: ${sendToClubBtn.textContent}`);
                simulateClick(sendToClubBtn);
                await sleep(1000);
            }

            console.log('[forceLoop] done');
        } catch (error) {
            console.error('[forceLoop] error:', error);
            stopLoop();
        }
    }

    function _moveItems(items, pile, controller) {
        return new Promise(resolve => {
            if (!items || !items.length) return resolve({ success: true });
            services.Item.move(items, pile, true).observe(controller, (e, t) => {
                e.unobserve(controller);
                resolve(t);
            });
        });
    }

    function getUnassignedController() {
        const ctl = getAppMain()
            .getRootViewController()
            .getPresentedViewController()
            .getCurrentViewController()
            .getCurrentController();
        if (!ctl || !ctl.childViewControllers) return null;

        return Array.from(ctl.childViewControllers).find(c =>
            c.className &&
            c.className.includes('UTUnassigned') &&
            c.className.includes('Controller')
        );
    }

    async function _handleUnassigned(minRating) {
        let controller = await getUnassignedController();
        let times = 0;
        let items = [];
        while (items.length === 0 && times < 3) {
            items = repositories.Item.getUnassignedItems();
            times++;
            await sleep(1500)
        }
        if(items.length === 0) {
            // Go back to pack view if no items
            await navigateBackToPackView();
            return;
        }
        
        // Always use 87 as minimum for storage
        const storageMinRating = minRating;
        
        const tradablePlayers = items.filter(p => p.type === 'player' && p.loans === -1 && !p.untradeable);
        const toStorage = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && p.isDuplicate() && p.rating >= storageMinRating);
        const toQuickSell = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && p.isDuplicate() && p.rating < storageMinRating);
        const clubPlayers = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && !p.isDuplicate());
        
        // Count cards by rating for storage
        const storage89Plus = toStorage.filter(p => p.rating >= 89);
        const storage87to88 = toStorage.filter(p => p.rating >= 87 && p.rating <= 88);
        
        console.log('[handleUnassigned] tradablePlayers', tradablePlayers.length, 'clubPlayers', clubPlayers.length, 
                    'toStorage', toStorage.length, '(89+:', storage89Plus.length, ', 87-88:', storage87to88.length, ')',
                    'toQuickSell', toQuickSell.length);
        
        // Update global storage counters
        if (storage89Plus.length > 0) {
            storageCounter89 += storage89Plus.length;
            console.log(`[handleUnassigned] 89+存储计数器: ${storageCounter89}`);
        }
        if (storage87to88.length > 0) {
            storageCounter87 += storage87to88.length;
            console.log(`[handleUnassigned] 87-88存储计数器: ${storageCounter87}`);
        }
        if (tradablePlayers.length) {
            await moveItems(tradablePlayers, ItemPile.TRANSFER, controller);
            await sleep(1000);
        }
        if (clubPlayers.length) {
            await moveItems(clubPlayers, ItemPile.CLUB, controller);
            await sleep(1000);
        }
        if (toStorage.length) {
            await moveItems(toStorage, ItemPile.STORAGE, controller);
            await sleep(2000); // Give more time for UI to update
        }

        // Refresh unassigned items to make sure UI is updated
        await controller.getUnassignedItems();
        await sleep(1000);

        // Try to click FSU refresh button to ensure duplicates are visible
        await clickFsuRefreshIfExist('第3次');
        await sleep(1000);
        
        // Now try to quick sell remaining untradeable duplicates
        const ellipsisBtn = await waitForElement(
            findEllipsisBtnOfUntradeableDupSection,
            5000
        );
        if (ellipsisBtn) {
            console.log('[handleUnassigned] 找到省略号按钮,准备快速出售');
            simulateClick(ellipsisBtn);
            await sleep(1000);
            await waitAndClickQuickSellUntradeableBtn();
            await waitEALoadingEnd();
            await sleep(1000);
        } else {
            console.log('[handleUnassigned] 未找到省略号按钮,可能没有需要快速出售的物品');
        }

        return true
    }
    
    async function _navigateBackToPackView() {
        try {
            // Click back button to return to pack view
            const backBtn = await waitForElement('.ut-navigation-button-control', 3000);
            if (backBtn) {
                console.log('[navigateBackToPackView] 点击返回按钮');
                simulateClick(backBtn);
                await sleep(1000);
            }
            // Wait for pack view
            await waitForController('UTStorePackViewController', 5000);
        } catch (err) {
            console.log('[navigateBackToPackView] 导航回包视图失败');
        }
    }
    
    async function _checkAndHandleUnassignedDialog() {
        // Check for unassigned items dialog
        const dialog = await waitForElement('section.ea-dialog-view.ea-dialog-view-type--message', 1000);
        
        if (dialog) {
            // Check if it's the unassigned items dialog
            const title = dialog.querySelector('.ea-dialog-view--title');
            if (title && title.textContent.includes('未分配的物品')) {
                console.log('[checkAndHandleUnassignedDialog] 发现未分配物品对话框');
                
                // Click "立即前往" button
                const goNowBtn = await waitForElement(() => {
                    const buttons = dialog.querySelectorAll('button');
                    return Array.from(buttons).find(b => 
                        b.textContent.includes('立即前往') || 
                        b.textContent.includes('Go Now')
                    );
                }, 2000);
                
                if (goNowBtn) {
                    console.log('[checkAndHandleUnassignedDialog] 点击立即前往按钮');
                    simulateClick(goNowBtn);
                    
                    // Wait for unassigned items view
                    await waitForController('UTUnassignedItemsSplitViewController', 10000);
                    await sleep(1000);
                    
                    return true;
                }
            }
        }
        
        return false;
    }




    const sleep = makeAbortable(_sleep);
    const waitForElement = makeAbortable(_waitForElement);
    const waitForRequest = makeAbortable(_waitForRequest);
    const waitEALoadingEnd = makeAbortable(_waitEALoadingEnd);
    const waitForController = makeAbortable(_waitForController);
    const clickFsuRefreshIfExist = makeAbortable(_clickFsuRefreshIfExist);
    const waitAndClickQuickSellUntradeableBtn = makeAbortable(_waitAndClickQuickSellUntradeableBtn);
    const waitForLoadingStart = makeAbortable(_waitForLoadingStart);
    const waitForLoadingEnd = makeAbortable(_waitForLoadingEnd);
    const waitFSULoading = makeAbortable(_waitFSULoading);
    const clickIfExists = makeAbortable(_clickIfExists);
    const waitForElementGone = makeAbortable(_waitForElementGone);
    const moveItems = makeAbortable(_moveItems);
    const findEllipsisBtnOfUntradeableDupSection = makeAbortable(_findEllipsisBtnOfUntradeableDupSection);
    const handleUnassigned = makeAbortable(_handleUnassigned);
    const execute89SBC = makeAbortable(_execute89SBC);
    const executeTOTWSBC = makeAbortable(_executeTOTWSBC);
    const massPackOpen = makeAbortable(_massPackOpen);
    const navigateBackToPackView = makeAbortable(_navigateBackToPackView);
    const execute10x84SBC = makeAbortable(_execute10x84SBC);
    const handleUnassignedSimple = makeAbortable(_handleUnassignedSimple);
    const continuousPackOpen = makeAbortable(_continuousPackOpen);
    const checkAndHandleUnassignedDialog = makeAbortable(_checkAndHandleUnassignedDialog);
    function startLoop() {
        if (running) return;

        // Prompt for number of rounds instead of min rating
        const v = prompt(`执行轮数(当前 ${massPackRounds})`, massPackRounds);
        if (v === null) {
            return;
        }
        const num = Number(v);
        if (!Number.isFinite(num) || num < 1) {
            alert('请输入有效的数字(至少为1)!');
            return;
        }
        massPackRounds = num;

        // Ask for storage mode
        const storageMode = confirm('选择存储模式:\n\n确定 = 89+模式(只存储89+,无限制)\n取消 = 87+模式(存储87+,有上限)');
        const forceLoopStorageRating = storageMode ? 89 : 87;

        running = true;
        btn.textContent = '停止循环';
        abortCtrl = new AbortController();
        storageCounter89 = 0; // Reset counters
        storageCounter87 = 0;

        (async () => {
            const signal = abortCtrl.signal;
            for (let round = 1; round <= massPackRounds && running && !signal.aborted; round++) {
                try {
                    console.log(`[forceLoop] 第 ${round}/${massPackRounds} 轮,存储模式: ${forceLoopStorageRating}+`);
                    await forceLoop(forceLoopStorageRating);
                } catch (err) {
                    console.log('循环中断:', err.message);
                    break;
                }
                try {
                    await sleep(800 + Math.random() * 1200);
                } catch (err) {
                    console.log('Sleep 中断:', err.message);
                    break;
                }
            }
            running = false;
            abortCtrl = null;
            btn.textContent = '快速开包';
            console.log(`[forceLoop] 完成!共执行 ${massPackRounds} 轮,最终89+存储计数器: ${storageCounter89}, 87-88存储计数器: ${storageCounter87}`);
        })();
    }

    function stopLoop() {
        if (!running) return;
        running = false;
        btn.textContent = '快速开包';
        if (abortCtrl) abortCtrl.abort();
    }
    
    
    function startMassPackOpen() {
        if (massPackRunning) return;
        
        // First ask if user wants to enable 87+ storage
        const enable87 = confirm('是否启用87+存储?\n\n是 = 存储87+卡片,达到上限后停止\n否 = 仅存储89+卡片,无限循环');
        
        let useStorage87 = enable87;
        
        if (enable87) {
            // Prompt for 87-88 cards storage limit
            const v = prompt(`87-88卡片存储上限(当前 ${storage87Limit})`, storage87Limit);
            if (v === null) {
                return;
            }
            const num = Number(v);
            if (!Number.isFinite(num) || num < 1) {
                alert('请输入有效的数字(至少为1)!');
                return;
            }
            storage87Limit = num;
        }
        
        // Prompt for TOTW runs per round
        const totwPrompt = prompt(`每轮TOTW SBC执行次数(当前 ${totwRunsPerRound})`, totwRunsPerRound);
        if (totwPrompt === null) {
            return;
        }
        const totwNum = Number(totwPrompt);
        if (!Number.isFinite(totwNum) || totwNum < 1) {
            alert('请输入有效的数字(至少为1)!');
            return;
        }
        totwRunsPerRound = totwNum;
        
        massPackRunning = true;
        btn3.textContent = '停止批量开包';
        abortCtrl = new AbortController();
        
        (async () => {
            const signal = abortCtrl.signal;
            if (!signal.aborted) {
                try {
                    await massPackOpen(useStorage87);
                } catch (err) {
                    console.log('批量开包中断:', err.message);
                }
            }
            massPackRunning = false;
            abortCtrl = null;
            btn3.textContent = '开50包';
        })();
    }
    
    function stopMassPackOpen() {
        if (!massPackRunning) return;
        massPackRunning = false;
        btn3.textContent = '开50包';
        if (abortCtrl) abortCtrl.abort();
    }
    
    function startContinuousPackOpen() {
        if (continuousPackRunning) return;
        
        // Ask user to choose storage mode
        const enable87 = confirm('选择存储模式:\n\n是 = 存储87+卡片(包含快速出售)\n否 = 仅存储89+卡片(包含快速出售)');
        
        continuousPackRunning = true;
        btn4.textContent = '停止连续开包';
        abortCtrl = new AbortController();
        
        (async () => {
            const signal = abortCtrl.signal;
            if (!signal.aborted) {
                try {
                    await continuousPackOpen(enable87);
                } catch (err) {
                    console.log('连续开包中断:', err.message);
                }
            }
            continuousPackRunning = false;
            abortCtrl = null;
            btn4.textContent = '连续开包';
        })();
    }
    
    function stopContinuousPackOpen() {
        if (!continuousPackRunning) return;
        continuousPackRunning = false;
        btn4.textContent = '连续开包';
        if (abortCtrl) abortCtrl.abort();
    }
    
    function initButton() {
        if (btn) return;
        btn = document.createElement('button'); 
        btn.textContent = '快速开包';
        Object.assign(btn.style, { 
            position: 'fixed', 
            bottom: '60px', 
            right: '40px', 
            padding: '10px', 
            background: '#ffd700', 
            borderRadius: '6px', 
            zIndex: 9999 
        });
        btn.addEventListener('click', () => running ? stopLoop() : startLoop()); 
        document.body.appendChild(btn);
        
        
        // Add second button for mass pack opening
        btn3 = document.createElement('button');
        btn3.textContent = '开50包';
        Object.assign(btn3.style, {
            position: 'fixed',
            bottom: '60px',
            right: '140px',
            padding: '10px',
            background: '#ff69b4',
            borderRadius: '6px',
            zIndex: 9999
        });
        btn3.addEventListener('click', () => massPackRunning ? stopMassPackOpen() : startMassPackOpen());
        document.body.appendChild(btn3);
        
        // Add third button for continuous pack opening
        btn4 = document.createElement('button');
        btn4.textContent = '连续开包';
        Object.assign(btn4.style, {
            position: 'fixed',
            bottom: '60px',
            right: '240px',
            padding: '10px',
            background: '#90ee90',
            borderRadius: '6px',
            zIndex: 9999
        });
        btn4.addEventListener('click', () => continuousPackRunning ? stopContinuousPackOpen() : startContinuousPackOpen());
        document.body.appendChild(btn4);
    }
    page.addEventListener('load', () => {
        initButton();
        overrideEventsPopup();
        hookLoading()
    });
})();