SBC Autofill with player swapping, AutoAll, and ForeverRun (639 & 638) with Retry Logic
// ==UserScript==
// @name runnan
// @namespace http://tampermonkey.net/
// @version 3.3.0
// @description SBC Autofill with player swapping, AutoAll, and ForeverRun (639 & 638) 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;
}
// Utility: Simulate internal pitch Drag and Drop
async function simulateDragAndDrop(sourceEl, destEl) {
if (!sourceEl || !destEl) return false;
const srcRect = sourceEl.getBoundingClientRect();
const dstRect = destEl.getBoundingClientRect();
const dragData = new DataTransfer();
const opts = (x, y) => ({
bubbles: true, cancelable: true, clientX: x, clientY: y
});
const startX = srcRect.left + srcRect.width/2;
const startY = srcRect.top + srcRect.height/2;
sourceEl.dispatchEvent(new PointerEvent('pointerdown', opts(startX, startY)));
sourceEl.dispatchEvent(new MouseEvent('mousedown', opts(startX, startY)));
await sleep(50);
sourceEl.dispatchEvent(new DragEvent('dragstart', { ...opts(startX, startY), dataTransfer: dragData }));
await sleep(50);
const dstX = dstRect.left + dstRect.width/2;
const dstY = dstRect.top + dstRect.height/2;
destEl.dispatchEvent(new DragEvent('dragenter', { ...opts(dstX, dstY), dataTransfer: dragData }));
destEl.dispatchEvent(new DragEvent('dragover', { ...opts(dstX, dstY), dataTransfer: dragData }));
await sleep(100);
destEl.dispatchEvent(new DragEvent('drop', { ...opts(dstX, dstY), dataTransfer: dragData }));
sourceEl.dispatchEvent(new DragEvent('dragend', { ...opts(dstX, dstY), dataTransfer: dragData }));
sourceEl.dispatchEvent(new PointerEvent('pointerup', opts(dstX, dstY)));
sourceEl.dispatchEvent(new MouseEvent('mouseup', opts(dstX, dstY)));
return true;
}
// --- Core Logic ---
// Internal optimization algorithm to re-arrange owned out-of-position players using drag and drop
async function optimizePositions() {
console.log('[Runnan] Optimizing positions (Smart Swap)...');
for (let pass = 0; pass < 3; pass++) {
let madeSwap = false;
const slotSelectors = Array.from({length: 11}, (_, i) => `.ut-squad-pitch-view.sbc > div:nth-child(${i+3})`);
for (let i = 0; i < slotSelectors.length; i++) {
const srcSlot = document.querySelector(slotSelectors[i]);
if (!srcSlot) continue;
const pedestal = srcSlot.querySelector('.ut-squad-slot-pedestal-view');
if (!pedestal) continue;
const isOwned = !srcSlot.querySelector('.small.player.item.concept') && !srcSlot.querySelector('.empty');
const isIncorrect = pedestal.classList.contains('state-incorrectly-positioned');
if (isOwned && isIncorrect) {
const possiblePositions = [
srcSlot.querySelector('.currentPosition')?.textContent,
...Array.from(srcSlot.querySelectorAll('.otherPositions')).map(el => el.textContent)
].filter(Boolean);
if (possiblePositions.length === 0) continue;
for (let j = 0; j < slotSelectors.length; j++) {
if (i === j) continue;
const dstSlot = document.querySelector(slotSelectors[j]);
if (!dstSlot) continue;
const dstLabel = dstSlot.querySelector('.label')?.textContent;
if (possiblePositions.includes(dstLabel)) {
const dstPedestal = dstSlot.querySelector('.ut-squad-slot-pedestal-view');
const dstOwned = !dstSlot.querySelector('.small.player.item.concept') && !dstSlot.querySelector('.empty');
const dstIncorrect = dstPedestal?.classList.contains('state-incorrectly-positioned');
// Swap if destination is entirely open or if its current occupant is incorrectly positioned too
if (!dstOwned || dstIncorrect) {
console.log(`[Runnan] Swapping slot ${i+1} to ${j+1} (${possiblePositions.join(',')} -> ${dstLabel})`);
const srcPlayerItem = srcSlot.querySelector('.small.player.item');
const dstPlayerItem = dstSlot.querySelector('.small.player.item') || dstSlot;
await simulateDragAndDrop(srcPlayerItem, dstPlayerItem);
await sleep(1500);
madeSwap = true;
break;
}
}
}
}
}
if (!madeSwap) {
console.log('[Runnan] No remaining internal swaps found.');
break;
}
}
}
// 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 === '639' && 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)');
}
// --- SMART SWAP: Optimize positions on pitch before replacing concept players ---
await optimizePositions();
// 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) {
continue;
}
const isConcept = playerSlot.querySelector('.small.player.item.concept');
const isEmpty = playerSlot.querySelector('.small.player.item.empty');
if (!isConcept && !isEmpty) {
// Do not attempt replacement on players that are already in the club AND natively slotted here
// If they are still out of position after internal shuffle, we still don't replace them
// because user prioritizes not buying.
continue;
}
simulateClick(playerSlot);
await sleep(800);
// If we are here, we don't have the player (Target is empty/concept).
// Try Swap Meets Requirements via EA interface / Extension interface
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';
let candidateItemBtn = null;
let candidateJ = -1;
let candidateRating = 0;
// First pass: Find duplicate players (with fut_icon icon_sbc)
for (let j = 1; ; 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`);
const isDuplicate = document.querySelector(`${listBase}:nth-child(${j}) .fut_icon.icon_sbc`);
if (!itemBtn) break;
if (fsuLocked || academyGraduate) continue;
if (ratingEl && isDuplicate) {
const rating = parseInt(ratingEl.textContent, 10);
if (rating < 84) {
candidateItemBtn = itemBtn;
candidateJ = j;
candidateRating = rating;
break;
}
}
}
// Second pass: If no duplicate found, find any qualified player
if (!candidateItemBtn) {
for (let j = 1; ; 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) {
candidateItemBtn = itemBtn;
candidateJ = j;
candidateRating = rating;
break;
}
}
}
}
if (candidateItemBtn) {
console.log(`[Runnan] Swapping with item ${candidateJ} (Rating: ${candidateRating})`);
simulateClick(candidateItemBtn);
swapped = true;
await sleep(1000);
}
}
if (swapped) continue;
// If Swap failed, we just leave the concept player / empty slot as is.
// WE DO NOT BUY DIRECTLY.
}
// 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 === '639') {
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');
}
function showSummaryPopup(picks) {
const popup = document.createElement('div');
Object.assign(popup.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#1e1e1e',
color: '#fff',
padding: '20px',
borderRadius: '8px',
zIndex: 10000,
maxHeight: '80vh',
overflowY: 'auto',
minWidth: '300px',
border: '2px solid #ffc107',
boxShadow: '0 4px 15px rgba(0,0,0,0.5)',
fontFamily: 'sans-serif'
});
let html = '<h2 style="margin-top:0; color:#ffc107; text-align:center;">AutoPick Summary</h2>';
html += '<ul style="list-style:none; padding:0;">';
picks.forEach((p, idx) => {
html += `<li style="padding: 10px 0; border-bottom: 1px solid #333; line-height: 1.4;">
<strong style="font-size: 1.1em;">#${idx + 1} - ${p.name}</strong><br>
Score (Rating): ${p.rating} | Rarity: ${p.rarity}<br>
League: ${p.league}
</li>`;
});
html += '</ul>';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
Object.assign(closeBtn.style, {
width: '100%',
padding: '10px',
marginTop: '15px',
backgroundColor: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
transition: 'transform 0.1s'
});
closeBtn.onmousedown = () => closeBtn.style.transform = 'scale(0.98)';
closeBtn.onmouseup = () => closeBtn.style.transform = 'scale(1)';
closeBtn.onclick = () => popup.remove();
popup.innerHTML = html;
popup.appendChild(closeBtn);
document.body.appendChild(popup);
}
// 3. AutoPick Function
async function sbcAutoPick() {
console.log(`[Runnan] Starting AutoPick Loop`);
stopRequested = false;
let sessionPicks = [];
while (!stopRequested) {
// Find open button
const openBtn = document.querySelector('.key-redeem-btn');
if (!openBtn || openBtn.offsetParent === null) {
console.log('[Runnan] .key-redeem-btn not found or hidden. AutoPick ended.');
break;
}
console.log('[Runnan] Opening Player Pick...');
simulateClick(openBtn);
await sleep(2000);
// Wait for player containers to load
let playerItemsLoaded = await waitForElement('.ut-companion-carousel-item-view .player.item', 5000);
if (!playerItemsLoaded) {
console.log('[Runnan] Player cards not loaded in time. Stopping.');
break;
}
await sleep(1000); // extra wait for animations
const items = document.querySelectorAll('.ut-companion-carousel-item-view');
let parsed_items = [];
for (let i = 0; i < items.length; i++) {
let el = items[i];
let playerWrapper = el.querySelector('.player.item');
if (!playerWrapper) continue;
let isRare = playerWrapper.classList.contains('rare');
let isCommon = playerWrapper.classList.contains('common');
let isSpecial = !isRare && !isCommon;
let rarityName = isSpecial ? 'Special' : (isRare ? 'Rare' : 'Common');
let nameText = el.querySelector('.name')?.textContent || 'Unknown';
let ratingText = el.querySelector('.rating')?.textContent || '0';
let rating = parseInt(ratingText, 10);
let bioRows = el.querySelectorAll('.ut-item-view--bio .ut-item-row .ut-item-row-label--left');
let leagueText = bioRows.length > 1 ? bioRows[1].textContent : '';
let priceText = el.querySelector('.fsu-PriceValue')?.textContent || el.querySelector('.currency-coins.item-slot-price')?.textContent || '0';
let price = parseInt(priceText.replace(/,/g, ''), 10);
let ownedText = el.querySelector('.fsu-cards-attr > div:nth-child(4)')?.textContent || 'NO';
let isOwned = (ownedText.trim() === 'YES');
parsed_items.push({
index: parseInt(el.getAttribute('data-index') || i, 10),
el: el,
name: nameText.trim(),
rarity: rarityName,
isSpecial: isSpecial,
rating: rating,
league: leagueText.trim(),
price: price,
isOwned: isOwned
});
}
if (parsed_items.length === 0) {
console.log('[Runnan] No valid player data extracted.');
break;
}
// Sorting logic
parsed_items.sort((a, b) => {
if (a.isSpecial !== b.isSpecial) return a.isSpecial ? -1 : 1;
if (a.isSpecial && b.isSpecial) {
if (a.isOwned !== b.isOwned) return a.isOwned ? 1 : -1;
return b.price - a.price;
}
if (a.rating !== b.rating) return b.rating - a.rating;
if (a.isOwned !== b.isOwned) return a.isOwned ? 1 : -1;
return b.price - a.price;
});
let bestPlayer = parsed_items[0];
// League Override: if highest rating <= 83
if (!bestPlayer.isSpecial && bestPlayer.rating <= 83) {
const targetLeagues = ['POR 1', 'BEL 1'];
const pref = parsed_items.find(p => targetLeagues.includes(p.league));
if (pref) {
bestPlayer = pref;
console.log(`[Runnan] Override triggered: Selected ${pref.league} player.`);
}
}
sessionPicks.push(bestPlayer);
console.log(`[Runnan] Target Player Index: ${bestPlayer.index}, Name: ${bestPlayer.name}, Rating: ${bestPlayer.rating}, Special: ${bestPlayer.isSpecial}, Price: ${bestPlayer.price}, Owned: ${bestPlayer.isOwned}`);
// Navigate array
let targetIndex = bestPlayer.index;
let attempts = 0;
while (attempts < 10 && !stopRequested) {
let currentActiveEl = document.querySelector('.ut-companion-carousel-item-view.active');
let currentIndex = currentActiveEl ? parseInt(currentActiveEl.getAttribute('data-index'), 10) : 0;
if (currentIndex === targetIndex) break; // Reached target!
if (targetIndex > currentIndex) {
let btnRight = document.querySelector('.ut-companion-carousel-item-container-view a.tapRight') || document.querySelector('a.tapRight');
if (btnRight) simulateClick(btnRight);
} else if (targetIndex < currentIndex) {
let btnLeft = document.querySelector('.ut-companion-carousel-item-container-view a.tapLeft') || document.querySelector('a.tapLeft');
if (btnLeft) simulateClick(btnLeft);
}
await sleep(600); // UI transition wait
attempts++;
}
await sleep(1000); // Add a small break before clicking select
// Validation and Fallback Setup
let verifyActiveEl = document.querySelector('.ut-companion-carousel-item-view.active');
let verifyIndex = verifyActiveEl ? parseInt(verifyActiveEl.getAttribute('data-index'), 10) : -1;
if (verifyIndex !== targetIndex) {
console.log(`[Runnan] Failed to navigate to index ${targetIndex} via arrows. Forcing direct interaction.`);
// Fallback 1: Click the player card element directly.
simulateClick(bestPlayer.el);
await sleep(800);
// Fallback 2: Try clicking the pagination dot map if it exists
let dotEl = document.querySelector(`.carousel-indicator-dot[data-index="${targetIndex}"]`);
if (dotEl) simulateClick(dotEl);
await sleep(800);
}
// Select
let selectBtn = findButtonByText(['选择', 'Select']);
if (selectBtn) {
console.log('[Runnan] Clicking Select...');
simulateClick(selectBtn);
await sleep(1500);
}
// Continue
let continueBtn = document.querySelector('button.key-continue-btn');
if (continueBtn && !continueBtn.classList.contains('disabled')) {
console.log('[Runnan] Clicking Continue...');
simulateClick(continueBtn);
await sleep(1500);
}
// Confirm
let confirmBtn = document.querySelector('button.key-confirm-btn');
if (confirmBtn && confirmBtn.offsetParent !== null && !confirmBtn.classList.contains('disabled')) {
console.log('[Runnan] Clicking Confirm...');
simulateClick(confirmBtn);
await sleep(2500);
}
// Wait before next loop iteration
await sleep(2000);
}
console.log(`[Runnan] AutoPick Loop Finished.`);
if (sessionPicks.length > 0) {
showSummaryPopup(sessionPicks);
}
}
// 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 639 Button
const btnForever639 = createBtn('Forever 639', '#007bff', async () => {
stopRequested = false;
await foreverRun('639');
});
// Forever 638 Button
const btnForever638 = createBtn('Forever 638', '#17a2b8', async () => {
stopRequested = false;
await foreverRun('638');
});
// 81+ One-click Button
const btnOneClick = createBtn('81+', '#fd7e14', async () => {
stopRequested = false;
const xStr = prompt('How many times to click?', '1');
const x = parseInt(xStr, 10);
if (isNaN(x) || x <= 0) return;
for (let i = 0; i < x; i++) {
if (stopRequested) {
console.log('[Runnan] Stopped 81+ click loop');
break;
}
console.log(`[Runnan] 81+ click ${i + 1}/${x}`);
const headers = Array.from(document.querySelectorAll('h1.tileTitle'));
const targetHeader = headers.find(h => h.textContent.includes('81+'));
if (targetHeader) {
const container = targetHeader.closest('.content-container');
if (container) {
const buttons = Array.from(container.querySelectorAll('button'));
const targetBtn = buttons.find(b => b.textContent.includes('一键完成'));
if (targetBtn) {
simulateClick(targetBtn);
await sleep(2500);
} else {
console.log('[Runnan] "一键完成" button not found');
await sleep(2500);
}
} else {
console.log('[Runnan] .content-container not found');
await sleep(2500);
}
} else {
console.log('[Runnan] "81+" header not found');
await sleep(2500);
}
}
console.log(`[Runnan] 81+ loop finished`);
});
// 80+ Job Button
const btn80PlusJob = createBtn('80+ Job', '#6f42c1', async () => {
stopRequested = false;
const xStr = prompt('How many times to do the 80+ Job?', '1');
const x = parseInt(xStr, 10);
if (isNaN(x) || x <= 0) return;
const findElByText = (textArray, selector = '*') => {
const elements = Array.from(document.querySelectorAll(selector));
for (let el of elements) {
if (el.children.length === 0 && el.textContent) {
const txt = el.textContent.trim();
if (textArray.some(t => txt.includes(t)) && el.offsetParent !== null) {
return el;
}
}
}
return null;
};
for (let i = 0; i < x; i++) {
if (stopRequested) {
console.log('[Runnan] Stopped 80+ job loop');
break;
}
console.log(`[Runnan] 80+ job iteration ${i + 1}/${x}`);
// Click SBC left tab
let sbcTab = findElByText(['SBC', 'SBCs'], '.ut-tab-bar-item span, button');
if (sbcTab) simulateClick(sbcTab.closest('.ut-tab-bar-item') || sbcTab);
await sleep(1000);
// Click "快速完成" top tab
let fastTab = findElByText(['快速完成'], '.ut-navigation-bar-view--tab, button');
if (fastTab) simulateClick(fastTab);
await sleep(1000);
// Click "80+ 球员 4选1" panel
let targetHeader = findElByText(['80+ 球员 4选1', '80+'], 'h1.tileTitle, .title, .name');
if (targetHeader) {
let container = targetHeader.closest('.content-container') || targetHeader.closest('.tile') || targetHeader;
simulateClick(container);
} else {
console.log(`[Runnan] 80+ panel not found`);
await sleep(1000);
continue;
}
await sleep(1000);
// Click "一键填充"
let fillBtn = findButtonByText(['一键填充', 'SBC squad autofill', 'SBC方案填充']);
if (fillBtn) simulateClick(fillBtn);
await sleep(1000);
// Check autofocus dialog
const confirmBtn = document.querySelector('.ea-dialog-view button.btn-standard.primary');
if (confirmBtn) {
simulateClick(confirmBtn);
await sleep(1000);
}
// Click "提交"
let submitBtn = document.querySelector('button.ut-squad-tab-button-control.actionTab.right.primary') || findButtonByText(['提交', 'Submit', 'Submit SBC', 'Submit Squad']);
if (submitBtn && !submitBtn.classList.contains('disabled')) {
simulateClick(submitBtn);
await sleep(1000);
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) {
simulateClick(continueBtn);
await sleep(1000);
}
}
} else {
console.log(`[Runnan] 提交 button not found or disabled`);
}
await sleep(1000);
}
if (stopRequested) return;
console.log('[Runnan] Going to store step');
let storeTab = findElByText(['商店', 'Store'], '.ut-tab-bar-item span, button');
if (storeTab) simulateClick(storeTab.closest('.ut-tab-bar-item') || storeTab);
await sleep(1000);
let unassignedItems = findElByText(['未分配的物品', 'Unassigned Items', '未分配物品']);
if (unassignedItems) {
let clickable = unassignedItems.closest('.tile, button') || unassignedItems;
simulateClick(clickable);
await sleep(1000);
await sbcAutoPick();
} else {
console.log('[Runnan] 未分配的物品 not found');
}
console.log(`[Runnan] 80+ Job finished`);
});
// AutoPick Button
const btnAutoPick = createBtn('Auto Pick', '#ffc107', async () => {
stopRequested = false;
await sbcAutoPick();
});
// Stop Button
const btnStop = createBtn('Stop', '#dc3545', async () => {
stopRequested = true;
console.log('[Runnan] Stop Requested');
});
container.appendChild(btnAuto);
container.appendChild(btnForever639);
container.appendChild(btnForever638);
container.appendChild(btnOneClick);
container.appendChild(btn80PlusJob);
container.appendChild(btnAutoPick);
container.appendChild(btnStop);
document.body.appendChild(container);
}
page.addEventListener('load', initUI);
// Fallback if load already fired
setTimeout(initUI, 2000);
})();