SBC Autofill with player swapping, AutoAll, and ForeverRun (423 & 422) with Retry Logic
// ==UserScript==
// @name runnan
// @namespace http://tampermonkey.net/
// @version 3.2.0
// @description SBC Autofill with player swapping, AutoAll, and ForeverRun (423 & 422) with Retry Logic
// @license MIT
// @match https://www.ea.com/ea-sports-fc/ultimate-team/web-app/*
// @match https://www.easports.com/*/ea-sports-fc/ultimate-team/web-app/*
// @match https://www.ea.com/*/ea-sports-fc/ultimate-team/web-app/*
// @run-at document-end
// ==/UserScript==
/*
* Script Usage Disclaimer
* Use at your own risk.
*/
(function () {
'use strict';
let page = unsafeWindow;
let stopRequested = false;
// Utility: sleep function
const sleep = ms => new Promise(res => setTimeout(res, ms));
// Utility: simulate click on element
function simulateClick(el) {
if (!el) {
console.log('[Runnan] Element not found for click');
return false;
}
const r = el.getBoundingClientRect();
['mousedown', 'mouseup', 'click'].forEach(t =>
el.dispatchEvent(new MouseEvent(t, {
bubbles: true, cancelable: true,
clientX: r.left + r.width / 2,
clientY: r.top + r.height / 2,
button: 0
}))
);
return true;
}
// Utility: wait for element to appear
function waitForElement(selector, timeout = 5000) {
return new Promise(resolve => {
const start = Date.now();
(function poll() {
const el = document.querySelector(selector);
if (el) return resolve(el);
if (Date.now() - start > timeout) return resolve(null);
setTimeout(poll, 200);
})();
});
}
// Utility: Find button by text
function findButtonByText(textArray) {
const buttons = Array.from(document.querySelectorAll('button, span.btn-text'));
for (let el of buttons) {
const txt = el.textContent.trim();
// Check if text matches AND element is visible
if (textArray.includes(txt) && el.offsetParent !== null) {
return el.tagName === 'BUTTON' ? el : el.closest('button');
}
}
return null;
}
// Utility: Shuffle array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// --- Core Logic ---
// 1. AutoAll Function
// 1. AutoAll Function
async function sbcAutoAll(challengeIndex = 0, sbcId = null) {
// Retry loop control
const maxRetries = 3;
let attempt = 0;
while (attempt <= maxRetries) {
if (stopRequested) return false;
attempt++;
console.log(`[Runnan] Starting AutoAll... Attempt ${attempt}/${maxRetries + 1}`);
// Step A: Autofill
const autofillBtn = findButtonByText(['SBC squad autofill', 'SBC方案填充']);
if (autofillBtn) {
console.log('[Runnan] Clicking Autofill');
simulateClick(autofillBtn);
await sleep(1000);
// Handling for input SBC URL
// Handling for input SBC URL
if (sbcId === '423' && challengeIndex === 1) {
// Check for input field (Wait up to 2 seconds)
// Use more specific selector based on the dialog structure provided
const inputSelector = '.ea-dialog-view--body input';
const input = await waitForElement(inputSelector, 2000);
if (input) {
let urlToFill = '';
const urls = [
// 'https://www.futbin.com/26/squad/100471706/sbc',
'https://www.futbin.com/26/squad/100490049/sbc',
'https://www.futbin.com/26/squad/100529570/sbc'
];
urlToFill = urls[Math.floor(Math.random() * urls.length)];
console.log(`[Runnan] Inputting URL: ${urlToFill}`);
// Robust Input Setting for Frameworks (React/Vue/etc)
input.focus();
await sleep(50);
// Simulate keydown
input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'a' }));
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, urlToFill);
input.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(50);
// Simulate keyup
input.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'a' }));
input.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(50);
input.blur();
await sleep(500);
} else {
console.log('[Runnan] Input field not found (skipped URL fill)');
}
}
const confirmBtn = await waitForElement('.ea-dialog-view button.btn-standard.primary', 3000);
if (confirmBtn) {
console.log('[Runnan] Clicking Confirm (Autofill Dialog)');
simulateClick(confirmBtn);
await sleep(5000);
// Check for submit button immediately after autofill
console.log('[Runnan] Checking for early submit...');
const submitBtnSelector = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
let submitBtn = document.querySelector(submitBtnSelector);
if (!submitBtn) {
submitBtn = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
}
if (submitBtn && !submitBtn.classList.contains('disabled')) {
console.log('[Runnan] Squad ready after autofill. Submitting early...');
simulateClick(submitBtn);
await sleep(1000);
// Check for "Precious Player" / Warning dialog
const dialog = document.querySelector('.ea-dialog-view--body');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll('button'));
const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
if (continueBtn) {
console.log('[Runnan] Warning dialog detected (Early Submit). Clicking Continue.');
simulateClick(continueBtn);
await sleep(3000);
}
} else {
await sleep(2000);
}
return true; // Early success
}
} else {
console.log('[Runnan] Confirm button not found in dialog.');
}
} else {
console.log('[Runnan] Autofill button not found (might be already filled or wrong page)');
}
// Step B: Loop Players (Random Order)
// Slots 2 to 12 correspond to nth-child indices
const slots = Array.from({ length: 11 }, (_, k) => k + 3);
const shuffledSlots = shuffleArray(slots);
for (let i of shuffledSlots) {
if (stopRequested) return false;
console.log(`[Runnan] Processing slot ${i - 2}/11`);
// Check submit button inside loop
// Check if submit button is available and enabled
const submitBtnSelectorLoop = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
let submitBtnLoop = document.querySelector(submitBtnSelectorLoop);
if (!submitBtnLoop) {
submitBtnLoop = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
}
if (submitBtnLoop && !submitBtnLoop.classList.contains('disabled')) {
console.log('[Runnan] Submit button enabled during loop. Submitting...');
simulateClick(submitBtnLoop);
await sleep(1000);
// Check for warning dialog
const dialog = document.querySelector('.ea-dialog-view--body');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll('button'));
const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
if (continueBtn) {
console.log('[Runnan] Warning dialog detected (Loop Submit). Clicking Continue.');
simulateClick(continueBtn);
await sleep(3000);
}
} else {
await sleep(2000);
}
return true; // Success, break loop
}
const playerSelector = `body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > div.ut-squad-pitch-view.sbc > div:nth-child(${i})`;
const playerSlot = document.querySelector(playerSelector);
if (!playerSlot) {
// If selector fails, it might be a different formation or already filled?
continue;
}
simulateClick(playerSlot);
await sleep(800);
// Check "Direct Purchase" button immediately
const directBuyBtn = findButtonByText(['直接购买此球员', 'Direct Purchase', 'Buy Now']);
if (!directBuyBtn) {
// console.log(`[Runnan] Slot ${i - 1}: No direct purchase option (Already owned?). Skipping.`);
// Reduced log spam
continue;
}
// If we are here, we don't have the player.
// Try Swap
let swapped = false;
// Specific selector from user REMOVED
// const swapSelector = "body > main > section > section > div.ut-navigation-container-view--content > div > div > section > div.ut-navigation-container-view--content > div > div.DetailPanel > div.fsu-substitutionBox > div:nth-child(2) > button:nth-child(3)";
// let swapBtn = document.querySelector(swapSelector);
// if (!swapBtn) {
const swapBtn = findButtonByText(['Swap Meets Requirements Players', '替换为满足需求球员', '满需求']);
// }
if (swapBtn) {
simulateClick(swapBtn);
await sleep(1000);
// Existing Swap Logic
const listBase = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > section > div.ut-navigation-container-view--content > div > div.paginated-item-list.ut-pinned-list > ul > li';
for (let j = 1; j <= 5; j++) {
const itemBtn = document.querySelector(`${listBase}:nth-child(${j}) > button`);
const ratingEl = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-view--main.ut-item-view > div > div.rating`);
const fsuLocked = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.name.fsulocked`);
const academyGraduate = document.querySelector(`${listBase}:nth-child(${j}) > div > div.entityContainer > div.small.player.item > div.ut-item-player-state-indicator-view.academy-graduate`);
if (!itemBtn) break;
if (fsuLocked || academyGraduate) continue;
if (ratingEl) {
const rating = parseInt(ratingEl.textContent, 10);
if (rating < 84) {
console.log(`[Runnan] Swapping with item ${j} (Rating: ${rating})`);
simulateClick(itemBtn);
swapped = true;
await sleep(1000);
break;
}
}
}
}
if (swapped) continue;
// If Swap failed, try Direct Purchase if cheap
// Force back to detail view if we were in swap list
if (swapBtn) {
simulateClick(playerSlot);
await sleep(800);
}
// Now check price and buy
const directBuyBtnAfter = findButtonByText(['直接购买此球员', 'Direct Purchase', 'Buy Now']);
if (directBuyBtnAfter) {
const subtext = directBuyBtnAfter.querySelector('.btn-subtext.currency-coins');
if (subtext) {
const price = parseInt(subtext.textContent.replace(/,/g, ''), 10);
if (price <= 1200) {
console.log(`[Runnan] Buying player for ${price}`);
simulateClick(directBuyBtnAfter);
await sleep(4000); // Wait longer for buy
// Check for buy confirmation modal if it exists?
const buyConfirm = await waitForElement('.dialog-body button', 500);
if (buyConfirm && (buyConfirm.textContent.includes('OK') || buyConfirm.textContent.includes('Yes') || buyConfirm.textContent.includes('确定'))) {
simulateClick(buyConfirm);
await sleep(2000);
}
} else {
console.log(`[Runnan] Price ${price} > 1200. Skipping.`);
}
}
}
}
// Step C: Submit
console.log('[Runnan] Checking submit button...');
const submitBtnSelector = 'body > main > section > section > div.ut-navigation-container-view--content > div > div > div > div.ut-draggable > button.ut-squad-tab-button-control.actionTab.right.primary';
let submitBtn = document.querySelector(submitBtnSelector);
// Fallback: find by text if selector failed
if (!submitBtn) {
submitBtn = findButtonByText(['Submit', 'Submit SBC', '提交', 'Submit Squad']);
}
if (submitBtn && !submitBtn.classList.contains('disabled')) {
console.log('[Runnan] Submitting!');
simulateClick(submitBtn);
await sleep(1000);
// Check for "Precious Player" / Warning dialog
const dialog = document.querySelector('.ea-dialog-view--body');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll('button'));
const continueBtn = buttons.find(b => b.textContent.includes('继续') || b.textContent.includes('Continue'));
if (continueBtn) {
console.log('[Runnan] Warning dialog detected. Clicking Continue.');
simulateClick(continueBtn);
await sleep(3000); // Wait for actual submission after confirmation
}
} else {
await sleep(2000); // Wait remaining time if no dialog
}
return true; // Success
} else {
console.log('[Runnan] Submit button disabled or not found.');
// If attempt < maxRetries, loop continues (retries)
if (attempt <= maxRetries) {
console.log(`[Runnan] Retrying (${attempt}/${maxRetries})...`);
await sleep(2000);
}
}
}
console.log('[Runnan] AutoAll failed after all retries.');
return false; // Failed
}
// 2. ForeverRun Function
async function foreverRun(sbcId) {
console.log(`[Runnan] Starting ForeverRun Loop for SBC ${sbcId}`);
stopRequested = false;
while (!stopRequested) {
// 2.1 Click SBC by ID
let sbcBtn = document.querySelector(`button[data-sbcid="${sbcId}"]`);
// Fallback for 311 if not found via data attribute
if (!sbcBtn && sbcId === '423') {
sbcBtn = document.querySelector('body > main > section > section > div.ut-navigation-bar-view.navbar-style-landscape.currency-purchase > div.fsu-navsbc > button:nth-child(1)');
}
if (!sbcBtn) {
console.log(`[Runnan] SBC ${sbcId} button not found. Please navigate to the SBC menu.`);
await sleep(2000);
} else {
simulateClick(sbcBtn);
await sleep(2000);
}
// 2.2 Loop 4 children
for (let i = 1; i <= 4; i++) {
if (stopRequested) break;
console.log(`[Runnan] ForeverRun: Challenge ${i}/4`);
const challengeSelector = `div.ut-sbc-challenges-view--challenges > div:nth-child(${i})`;
const challenge = await waitForElement(challengeSelector, 2000);
if (!challenge) {
// console.log(`[Runnan] Challenge ${i} not found.`);
continue;
}
simulateClick(challenge);
await sleep(1000);
// Click "Start Challenge" (开始挑战)
const startBtn = findButtonByText(['Starting Challenge', 'Start Challenge', '开始挑战', '前往挑战']);
if (startBtn) {
console.log('[Runnan] Clicking Start Challenge...');
simulateClick(startBtn);
await sleep(3000);
// Run AutoAll only if started
const success = await sbcAutoAll(i, sbcId);
if (!success) {
console.log('[Runnan] AutoAll failed. Stopping ForeverRun.');
stopRequested = true;
// Alert user?
alert('Runnan: Stopped due to repeated submit failures.');
break;
}
} else {
console.log('[Runnan] Start Challenge button not found. Skipping challenge...');
}
// Return to challenges page
const listCheck = await waitForElement('div.ut-sbc-challenges-view--challenges', 3000);
if (!listCheck) {
console.log('[Runnan] Not in challenge list. Clicking Back.');
const backBtn = document.querySelector('button.ut-navigation-button-control');
if (backBtn) simulateClick(backBtn);
await sleep(2000);
}
}
await sleep(1000);
}
console.log('[Runnan] ForeverRun Stopped');
}
// UI Initialization
function initUI() {
const container = document.createElement('div');
Object.assign(container.style, {
position: 'fixed',
bottom: '40px',
right: '40px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
zIndex: 9999
});
const createBtn = (text, color, onClick) => {
const btn = document.createElement('button');
btn.textContent = text;
Object.assign(btn.style, {
padding: '10px 20px',
background: color,
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
transition: 'transform 0.1s, opacity 0.2s',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
});
// Click animation (Scale)
btn.addEventListener('mousedown', () => btn.style.transform = 'scale(0.95)');
btn.addEventListener('mouseup', () => btn.style.transform = 'scale(1)');
btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
// Handle click with debounce/disable
btn.addEventListener('click', async (e) => {
if (btn.disabled) return;
// Visual feedback for click
btn.style.opacity = '0.7';
btn.style.transform = 'scale(0.95)';
setTimeout(() => btn.style.transform = 'scale(1)', 100);
// Disable briefly to prevent double click
btn.disabled = true;
const originalText = btn.textContent;
btn.textContent = '...';
try {
await onClick(e);
} finally {
// Re-enable after short delay or action done
setTimeout(() => {
btn.disabled = false;
btn.style.opacity = '1';
btn.textContent = originalText;
}, 500);
}
});
return btn;
};
// AutoAll Button
const btnAuto = createBtn('AutoALL', '#28a745', async () => {
stopRequested = false;
await sbcAutoAll(0, null);
});
// Forever 423 Button
const btnForever423 = createBtn('Forever 423', '#007bff', async () => {
stopRequested = false;
await foreverRun('423');
});
// Forever 422 Button
const btnForever422 = createBtn('Forever 422', '#17a2b8', async () => {
stopRequested = false;
await foreverRun('422');
});
// Stop Button
const btnStop = createBtn('Stop', '#dc3545', async () => {
stopRequested = true;
console.log('[Runnan] Stop Requested');
});
container.appendChild(btnAuto);
container.appendChild(btnForever423);
container.appendChild(btnForever422);
container.appendChild(btnStop);
document.body.appendChild(container);
}
page.addEventListener('load', initUI);
// Fallback if load already fired
setTimeout(initUI, 2000);
})();