// ==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()
});
})();