// ==UserScript==
// @name runnan
// @namespace http://tampermonkey.net/
// @version 1.0.20
// @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 massPackRunning = false;
let massPackRounds = 10; // Number of rounds to execute
let totwRunsPerRound = 2; // Number of TOTW SBC runs per round
let slowMode = false; // Slow mode flag - doubles all wait times
let skip89SBC = false; // Skip 89 SBC and store 89+ cards instead
let customStorageRating = 89; // Custom rating threshold for storage when skip89 is enabled
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 => {
const adjustedMs = slowMode ? ms * 2 : ms;
return new Promise(res => setTimeout(res, adjustedMs));
};
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) {
const adjustedTimeout = slowMode ? timeout * 2 : 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 > adjustedTimeout) 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) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
return new Promise((resolve) => {
const item = { apiPath, method, resolve };
item._timer = setTimeout(() => {
resolve(null);
page._xhrPromiseList = page._xhrPromiseList.filter(x => x !== item);
}, adjustedTimeout);
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) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
const pollInterval = slowMode ? 1600 : 800;
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 > adjustedTimeout) return reject(new Error(`等待${name}超时`));
setTimeout(poll, pollInterval);
})();
});
}
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 adjustedTimeout = slowMode ? 20000 : 10000;
const req = waitForRequest('/purchased/items', 'GET', adjustedTimeout);
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 adjustedTimeout = slowMode ? timeout * 2 : timeout;
const modal = await waitForElement('.view-modal-container.form-modal .ut-bulk-action-popup-view', adjustedTimeout).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) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
return waitForElement(
'.ut-click-shield.showing.fsu-loading',
adjustedTimeout
);
}
function _waitForLoadingEnd(timeout = DEFAULT_WAIT_TIMEOUT) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
const checkInterval = slowMode ? 160 : 80;
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 > adjustedTimeout) {
return reject(new Error('等待 loading 隐藏超时'));
}
setTimeout(check, checkInterval);
}
check();
});
}
async function _waitFSULoading(timeout = DEFAULT_WAIT_TIMEOUT) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
const startTimeout = slowMode ? 4000 : 2000;
try {
await waitForLoadingStart(startTimeout);
} catch (e) {
return;
}
await waitForLoadingEnd(adjustedTimeout);
}
async function _clickIfExists(selector, timeout = 2000, clickDelay = 500) {
const adjustedTimeout = slowMode ? timeout * 2 : timeout;
const adjustedDelay = slowMode ? clickDelay * 2 : clickDelay;
const el = await waitForElement(selector, adjustedTimeout);
if (!el) {
throw new Error(`clickIfExists: 元素 "${selector}" ${adjustedTimeout}ms 内未找到`);
}
if (adjustedDelay > 0) {
await sleep(adjustedDelay);
}
simulateClick(el);
return true;
}
function _waitForElementGone(selector, timeout = DEFAULT_TIMEOUT) {
const adjustedTimeout = slowMode ? timeout * 2 : 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 > adjustedTimeout) 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}`);
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="1203"]', 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 = 89) {
// 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;
}
// Use custom rating when skip89SBC is enabled
const storageRating = skip89SBC ? customStorageRating : 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 >= storageRating);
const clubPlayers = items.filter(p => p.type === 'player' && p.loans === -1 && p.untradeable && !p.isDuplicate());
// Count cards by rating for storage
const storageCount = toStorage.filter(p => p.rating >= storageRating).length;
console.log('[handleUnassignedSimple] tradablePlayers', tradablePlayers.length, 'clubPlayers', clubPlayers.length,
'toStorage', toStorage.length, `(${storageRating}+:`, storageCount, ')');
// Update global storage counter
if (toStorage.length > 0) {
storageCounter89 += toStorage.length;
console.log(`[handleUnassignedSimple] 存储计数器: ${storageCounter89}`);
}
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() {
const minRating = skip89SBC ? customStorageRating : 89;
console.log(`[continuousPackOpen] 开始连续开包 - 存储${minRating}+卡片`);
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();
// 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
await handleUnassigned();
// 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() {
const minStorageRating = skip89SBC ? customStorageRating : 89;
console.log(`[massPackOpen] 开始批量开包流程 - 存储${minStorageRating}+卡片`);
storageCounter89 = 0; // Reset counter at start
let round = 0;
try {
// Continue indefinitely
while (true) {
round++;
console.log(`[massPackOpen] 第 ${round} 轮,当前存储计数: ${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();
// 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();
// Now we should be back on pack view
// Execute 89 SBC while we have enough 89+ cards (unless skip89SBC is enabled)
while (!skip89SBC && storageCounter89 >= 4) {
console.log(`[massPackOpen] 89+存储中有 ${storageCounter89} 张卡,执行89 SBC`);
const sbcSuccess = await execute89SBC();
if (!sbcSuccess) {
await navigateBackToPackView();
}
}
// Log if we're skipping 89 SBC
if (skip89SBC && storageCounter89 >= 4) {
console.log(`[massPackOpen] 跳过89 SBC - ${minStorageRating}+卡片存储数: ${storageCounter89}`);
}
// 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);
}
console.log(`[massPackOpen] 完成!共执行 ${round} 轮,最终存储计数器: ${storageCounter89}`);
} catch (error) {
console.error('[massPackOpen] 错误:', error);
stopMassPackOpen();
}
}
async function forceLoop() {
const storageRating = skip89SBC ? customStorageRating : 89;
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();
// 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();
// Now we should be back on pack view
// Execute 89 SBC if we have enough cards (unless skip89SBC is enabled)
if (!skip89SBC && storageCounter89 >= 4) {
console.log(`[forceLoop] 89+存储中有 ${storageCounter89} 张卡,执行89 SBC`);
const sbcSuccess = await execute89SBC();
if (!sbcSuccess) {
await navigateBackToPackView();
}
} else if (skip89SBC && storageCounter89 >= 4) {
console.log(`[forceLoop] 跳过89 SBC - ${storageRating}+卡片存储数: ${storageCounter89}`);
}
// 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() {
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;
}
// Use custom rating when skip89SBC is enabled
const storageMinRating = skip89SBC ? customStorageRating : 89;
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());
console.log('[handleUnassigned] tradablePlayers', tradablePlayers.length, 'clubPlayers', clubPlayers.length,
`toStorage (${storageMinRating}+):`, toStorage.length,
'toQuickSell', toQuickSell.length);
// Update global storage counter
if (toStorage.length > 0) {
storageCounter89 += toStorage.length;
console.log(`[handleUnassigned] 存储计数器: ${storageCounter89}`);
}
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
const v = prompt(`执行轮数(当前 ${massPackRounds})`, massPackRounds);
if (v === null) {
return;
}
const num = Number(v);
if (!Number.isFinite(num) || num < 1) {
alert('请输入有效的数字(至少为1)!');
return;
}
massPackRounds = num;
// Check if skip89SBC is enabled and prompt for custom rating
if (skip89SBC) {
const ratingInput = prompt(`请输入存储评分阈值(当前 ${customStorageRating})`, customStorageRating);
if (ratingInput !== null) {
const rating = Number(ratingInput);
if (Number.isFinite(rating) && rating >= 80 && rating <= 99) {
customStorageRating = rating;
}
}
}
running = true;
btn.textContent = '停止循环';
abortCtrl = new AbortController();
storageCounter89 = 0; // Reset counter
(async () => {
const signal = abortCtrl.signal;
for (let round = 1; round <= massPackRounds && running && !signal.aborted; round++) {
try {
const storageRating = skip89SBC ? customStorageRating : 89;
console.log(`[forceLoop] 第 ${round}/${massPackRounds} 轮,存储${storageRating}+卡片`);
await forceLoop();
} catch (err) {
console.log('循环中断:', err.message);
break;
}
try {
const baseDelay = slowMode ? 1600 : 800;
const randomDelay = slowMode ? 2400 : 1200;
await sleep(baseDelay + Math.random() * randomDelay);
} catch (err) {
console.log('Sleep 中断:', err.message);
break;
}
}
running = false;
abortCtrl = null;
btn.textContent = '快速开包';
console.log(`[forceLoop] 完成!共执行 ${massPackRounds} 轮,最终存储计数器: ${storageCounter89}`);
})();
}
function stopLoop() {
if (!running) return;
running = false;
btn.textContent = '快速开包';
if (abortCtrl) abortCtrl.abort();
}
function startMassPackOpen() {
if (massPackRunning) return;
// Check if skip89SBC is enabled and prompt for custom rating
if (skip89SBC) {
const ratingInput = prompt(`请输入存储评分阈值(当前 ${customStorageRating})`, customStorageRating);
if (ratingInput !== null) {
const rating = Number(ratingInput);
if (Number.isFinite(rating) && rating >= 80 && rating <= 99) {
customStorageRating = rating;
}
}
}
// 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();
} 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;
// Check if skip89SBC is enabled and prompt for custom rating
if (skip89SBC) {
const ratingInput = prompt(`请输入存储评分阈值(当前 ${customStorageRating})`, customStorageRating);
if (ratingInput !== null) {
const rating = Number(ratingInput);
if (Number.isFinite(rating) && rating >= 80 && rating <= 99) {
customStorageRating = rating;
}
}
}
continuousPackRunning = true;
btn4.textContent = '停止连续开包';
abortCtrl = new AbortController();
(async () => {
const signal = abortCtrl.signal;
if (!signal.aborted) {
try {
await continuousPackOpen();
} 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);
// Add slow mode toggle button
const btnSlowMode = document.createElement('button');
btnSlowMode.textContent = '慢速模式: 关';
Object.assign(btnSlowMode.style, {
position: 'fixed',
bottom: '110px',
right: '40px',
padding: '10px',
background: slowMode ? '#ff6347' : '#87ceeb',
borderRadius: '6px',
zIndex: 9999
});
btnSlowMode.addEventListener('click', () => {
slowMode = !slowMode;
btnSlowMode.textContent = `慢速模式: ${slowMode ? '开' : '关'}`;
btnSlowMode.style.background = slowMode ? '#ff6347' : '#87ceeb';
console.log(`[SlowMode] 慢速模式已${slowMode ? '开启' : '关闭'} - 等待时间${slowMode ? '加倍' : '正常'}`);
});
document.body.appendChild(btnSlowMode);
// Add skip 89 SBC toggle button
const btnSkip89 = document.createElement('button');
btnSkip89.textContent = '跳过89SBC: 关';
Object.assign(btnSkip89.style, {
position: 'fixed',
bottom: '110px',
right: '140px',
padding: '10px',
background: skip89SBC ? '#ff69b4' : '#98fb98',
borderRadius: '6px',
zIndex: 9999
});
btnSkip89.addEventListener('click', () => {
skip89SBC = !skip89SBC;
btnSkip89.textContent = `跳过89SBC: ${skip89SBC ? '开' : '关'}`;
btnSkip89.style.background = skip89SBC ? '#ff69b4' : '#98fb98';
console.log(`[Skip89SBC] 跳过89 SBC已${skip89SBC ? '开启' : '关闭'} - 89+卡片将${skip89SBC ? '存储到SBC仓库' : '用于89 SBC'}`);
});
document.body.appendChild(btnSkip89);
}
page.addEventListener('load', () => {
initButton();
overrideEventsPopup();
hookLoading()
});
})();