您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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() }); })();