// ==UserScript== // @name FUT SBC Solver v2 // @namespace https://github.com/mljpa/fut-sbc-solver-v2 // @version 0.2.24 // @description Userscript to solve EA SPORTS FC 26 SBCs with your own club // @match https://www.ea.com/*/ea-sports-fc/ultimate-team/web-app* // @match https://www.ea.com/ea-sports-fc/ultimate-team/web-app* // @run-at document-idle // @inject-into page // @grant none // @updateURL https://raw.githubusercontent.com/mljpa/fut-sbc-dist/main/fut-sbc.user.js // @downloadURL https://raw.githubusercontent.com/mljpa/fut-sbc-dist/main/fut-sbc.user.js // ==/UserScript== "use strict"; (() => { // src/ea/services.ts function getGlobal(name) { return globalThis[name]; } function waitForServices(timeoutMs = 6e4) { const start = Date.now(); return new Promise((resolve, reject) => { const tick = () => { const s = getGlobal("services"); if (s && s.Localization && s.SBC && s.Squad && s.Item && s.Club) { resolve(s); return; } if (Date.now() - start > timeoutMs) { reject(new Error("waitForServices: timed out")); return; } setTimeout(tick, 1e3); }; tick(); }); } function toPromise(obs) { return new Promise((resolve, reject) => { const o = obs; if (!o?.observe) { resolve({ data: obs, status: 200, success: true }); return; } const ctx = {}; const timer = setTimeout(() => { try { o.unobserve?.(ctx); } catch { } reject(new Error("EAObservable timeout")); }, 2e4); o.observe(ctx, (self, r) => { clearTimeout(timer); try { self.unobserve?.(ctx); } catch { } const res = r; resolve({ data: res?.response ?? res?.data, status: res?.status, error: res?.error?.code ?? res?.error, success: res?.success !== false }); }); }); } var delay = (ms) => new Promise((r) => setTimeout(r, ms)); // src/ea/vc.ts var CHILD_KEYS = [ "childViewControllers", "currentController", "gameflowControllers", "presentationController", "presentedViewController", "presentingViewController", "parentViewController" ]; function getRootViewController() { const getAppMain = getGlobal( "getAppMain" ); if (typeof getAppMain !== "function") return null; try { return getAppMain().getRootViewController?.() ?? null; } catch { return null; } } function constructorName(node) { try { return node?.constructor?.name ?? ""; } catch { return ""; } } function isInDom(vc) { try { const view = vc.getView?.(); const el2 = view?.getRootElement?.(); return !!el2 && typeof document !== "undefined" && document.contains(el2); } catch { return false; } } function findViewControllers(match, root = getRootViewController()) { const hits = []; if (!root || typeof root !== "object") return hits; const seen = /* @__PURE__ */ new Set(); const queue = [root]; while (queue.length > 0) { const node = queue.shift(); if (!node || typeof node !== "object" || seen.has(node)) continue; seen.add(node); if (Array.isArray(node)) { for (const el2 of node) queue.push(el2); continue; } const rec = node; try { if (match(rec)) hits.push(rec); } catch { } for (const key of CHILD_KEYS) { const child = rec[key]; if (child && typeof child === "object") queue.push(child); } } return hits; } // src/ea/sbc.ts var SCOPE_BY_CODE = { 0: "min", // GreaterOrEqual 1: "max", // LessOrEqual 2: "exact" }; var TIER_BY_CODE = { 1: "bronze", 2: "silver", 3: "gold" }; function parseRequirements(challenge) { const ch = challenge ?? {}; const slots = squadSlots(ch); const c = { slots, counted: [], unparsed: [] }; const reqs = Array.isArray(ch.eligibilityRequirements) ? ch.eligibilityRequirements : []; for (const req of reqs) { const text = safeBuildString(req); const kv = readKv(req); if (!kv) { c.unparsed.push(text || "unreadable requirement (no kvPairs)"); continue; } const scope = SCOPE_BY_CODE[Number(req.scope)] ?? "min"; const countField = Number(req.count ?? -1); const v0 = kv.values[0] ?? 0; const label = text || `typeKey ${kv.typeKey}`; const add = (partial) => { c.counted.push({ ...partial, label }); }; switch (kv.typeKey) { case 19: c.teamRatingMin = v0; break; case 35: c.chemistryMin = v0; break; case 3: add({ kind: "quality", value: TIER_BY_CODE[v0] ?? String(v0), count: slots || kv.values.length, scope }); break; case 17: add({ kind: "quality", value: TIER_BY_CODE[v0] ?? String(v0), count: countOrValue(countField, v0), scope }); break; case 18: add({ kind: "rarity", value: v0, count: Math.max(countField, 0), scope }); break; case 4: add({ kind: "nation", value: null, count: countOrValue(countField, v0), scope }); break; case 5: add({ kind: "league", value: null, count: countOrValue(countField, v0), scope }); break; case 6: add({ kind: "club", value: null, count: countOrValue(countField, v0), scope }); break; case 7: add({ kind: "distinctNations", value: null, count: countOrValue(countField, v0), scope }); break; case 8: add({ kind: "distinctLeagues", value: null, count: countOrValue(countField, v0), scope }); break; case 9: add({ kind: "distinctClubs", value: null, count: countOrValue(countField, v0), scope }); break; case 10: case 11: case 12: { const kind = kv.typeKey === 10 ? "nation" : kv.typeKey === 11 ? "league" : "club"; if (kv.values.length > 1) { c.unparsed.push(`${label} [OR ${kind} ids: ${kv.values.join(", ")}]`); } else { add({ kind, value: v0, count: Math.max(countField, 0), scope }); } break; } case 25: add({ kind: "group", value: v0, count: Math.max(countField, 0), scope }); break; case 26: // Players with minimum OVR of X case 27: // Players with exact OVR of X case 28: if (countField <= 0 || slots > 0 && countField >= slots) { if (kv.typeKey === 26) c.minOvrPerPlayer = v0; else if (kv.typeKey === 27) c.exactOvr = v0; else c.maxOvrPerPlayer = v0; } else { const op = kv.typeKey === 26 ? ">=" : kv.typeKey === 27 ? "==" : "<="; c.unparsed.push(`${label} [${countField} players with OVR ${op} ${v0}]`); } break; default: c.unparsed.push( text || `unmapped typeKey ${kv.typeKey} = [${kv.values.join(", ")}]` ); } } return c; } function countOrValue(count, value) { return count > 0 ? count : Math.max(value, 0); } function readKv(req) { const coll = req.kvPairs?._collection; if (!coll || typeof coll !== "object") return null; let entry; if (coll instanceof Map) { entry = [...coll.entries()][0]; } else { entry = Object.entries(coll)[0]; } if (!entry) return null; const typeKey = Number(entry[0]); if (!Number.isFinite(typeKey)) return null; const raw = entry[1]; const values = Array.isArray(raw) ? raw.map((x) => Number(x)) : [Number(raw)]; return { typeKey, values }; } function safeBuildString(req) { try { return typeof req.buildString === "function" ? String(req.buildString() ?? "") : ""; } catch { return ""; } } function liveSquadOf(ch) { const own = ch.squad; if (own) return own; try { const hits = findViewControllers( (n) => constructorName(n) === "UTSBCSquadOverviewViewController" && !!n["_squad"] && Number(n["_challenge"]?.id) === Number(ch.id) ); const vc = hits.find((h) => isInDom(h)) ?? hits[hits.length - 1]; return vc?.["_squad"] ?? void 0; } catch { return void 0; } } function squadSlots(ch) { const sq = liveSquadOf(ch); if (!sq) return 0; try { const n = sq.getNumOfRequiredPlayers?.(); if (typeof n === "number" && n > 0) return n; } catch { } try { const nb = sq.getNonBrickSlots?.(); if (Array.isArray(nb) && nb.length > 0) return nb.length; } catch { } try { const fp = sq.getFieldPlayers?.(); if (Array.isArray(fp) && fp.length > 0) return fp.length; } catch { } return 0; } async function getOpenChallenge() { const raw = findLiveChallenge() ?? await challengeFromRepository(); if (!raw) return null; const constraints = parseRequirements(raw); const slotPositions = readSlotPositions(raw); if (slotPositions.length === constraints.slots) { constraints.slotPositions = slotPositions; } return { id: Number(raw.id ?? -1), setId: Number(raw.setId ?? -1), name: String(raw.name ?? ""), slots: constraints.slots, constraints, raw }; } function readSlotPositions(ch) { const sq = liveSquadOf(ch); try { const slots = sq?.getNonBrickSlots?.() ?? []; return slots.map((s) => { const g = s.getGeneralPosition?.(); return typeof g === "number" ? g : Number(s.position?.id ?? -1); }); } catch { return []; } } function looksLikeChallenge(o) { if (!o || typeof o !== "object") return false; const c = o; return Array.isArray(c["eligibilityRequirements"]) && ("squad" in c || typeof c.isInProgress === "function"); } var CHALLENGE_VC_NAMES = /* @__PURE__ */ new Set([ "UTSBCSquadOverviewViewController", "UTSBCSquadDetailPanelViewController" ]); function findLiveChallenge() { const hits = findViewControllers( (n) => CHALLENGE_VC_NAMES.has(constructorName(n)) && looksLikeChallenge(n["_challenge"]) ); if (hits.length === 0) return null; const challengeOf = (h) => h["_challenge"]; const inProgress = hits.find((h) => { try { return challengeOf(h).isInProgress?.() === true; } catch { return false; } }); if (inProgress) return challengeOf(inProgress); const inDom = hits.find((h) => isInDom(h)); if (inDom) return challengeOf(inDom); return challengeOf(hits[hits.length - 1]); } async function challengeFromRepository() { const services = getGlobal("services"); const sbc = services?.["SBC"]; const repo = sbc?.repository; if (!repo) return null; const ahead = []; const behind = []; try { for (const set of collectionValues(repo.sets)) { const chs = set.getChallenges?.() ?? []; for (const ch of chs) { if (!looksLikeChallenge(ch)) continue; const c = ch; if (!c.squad) continue; if (safeInProgress(c)) ahead.push(c); else behind.push(c); } } } catch { } return ahead[0] ?? behind[0] ?? null; } function safeInProgress(c) { try { return c.isInProgress?.() === true; } catch { return false; } } function collectionValues(coll) { if (!coll) return []; const inner = coll._collection ?? coll; if (inner instanceof Map) return [...inner.values()]; if (Array.isArray(inner)) return inner; if (typeof inner === "object") return Object.values(inner); return []; } // src/ea/club.ts var PAGE_SIZE = 91; var MAX_PAGES = 60; async function fetchClubPlayers() { const players = []; const items = /* @__PURE__ */ new Map(); const services = getGlobal("services"); const club = services?.["Club"]; const VM = getGlobal("UTBucketedItemSearchViewModel"); if (!club?.search || typeof VM !== "function") return { players, items }; try { if (club.getStats) await toPromise(club.getStats()); } catch { } const criteria3 = new VM().searchCriteria; criteria3["count"] = PAGE_SIZE; criteria3["offset"] = 0; for (let page = 0; page < MAX_PAGES; page++) { let batch = []; let retrievedAll = false; try { const res = await toPromise(club.search(criteria3)); const data = res.data; batch = Array.isArray(data?.items) ? data.items : []; retrievedAll = data?.retrievedAll === true; } catch { break; } let added = 0; for (const raw of batch) { if (typeof raw.loans === "number" && raw.loans > -1) continue; const mapped = toSolverPlayer(raw); if (!mapped) continue; if (items.has(mapped.id)) continue; players.push(mapped); items.set(mapped.id, raw); added++; } if (retrievedAll || batch.length === 0 || added === 0) break; criteria3["offset"] = Number(criteria3["offset"] ?? 0) + batch.length; } markDuplicates(players); return { players, items }; } function rawItemToSolverPlayer(raw) { return toSolverPlayer(raw); } function toSolverPlayer(raw) { const definitionId = Number(raw.definitionId ?? 0); if (!definitionId) return null; const instanceId = Number(raw.id ?? 0); const rating = Number(raw.rating ?? raw._rating ?? 0); const untradeable = typeof raw.untradeableCount === "number" && raw.untradeableCount >= 1 || raw.untradeable === true || raw.tradable === false; return { id: instanceId || -definitionId, definitionId, rating, name: readName(raw), leagueId: Number(raw.leagueId ?? 0), nationId: Number(raw.nationId ?? raw.nation ?? 0), teamId: Number(raw.teamId ?? 0), quality: qualityFromRating(rating), rarityId: Number(raw.rareflag ?? 0), untradeable, concept: raw.concept === true, // duplicateId was 0 on every item in the 2026-09-01 dump; markDuplicates() // adds a definitionId-grouping pass on top. // TODO: trust duplicateId once // a club with real duplicates is inspected. isDuplicate: Number(raw.duplicateId ?? 0) > 0, inActiveSquad: false, // cross-checked by callers via squad.ts inAnySquad: false, inStorage: false, // TODO: verify via services.Item storage search (docs pendiente) isSpecial: isSpecial(raw), groups: readGroups(raw), positions: readPositions(raw) }; } function readGroups(raw) { const src = Array.isArray(raw.groups) ? raw.groups : []; return [...new Set(src.map((n) => Number(n)).filter((n) => Number.isFinite(n)))]; } function readPositions(raw) { const src = Array.isArray(raw.basePossiblePositions) && raw.basePossiblePositions || Array.isArray(raw.possiblePositions) && raw.possiblePositions || (typeof raw.preferredPosition === "number" ? [raw.preferredPosition] : []); return [...new Set(src.map((n) => Number(n)).filter((n) => Number.isFinite(n)))]; } function qualityFromRating(rating) { if (rating >= 75) return "gold"; if (rating >= 65) return "silver"; return "bronze"; } function readName(raw) { try { const s = raw.getStaticData?.(); if (s?.name) return String(s.name); } catch { } try { if (raw.getName) return String(raw.getName()); } catch { } return `#${raw.definitionId ?? "?"}`; } function isSpecial(raw) { try { if (raw.isEvolutions?.()) return true; } catch { } try { if (raw.isEnrolledInAcademy?.()) return true; } catch { } return Number(raw.rareflag ?? 0) > 1; } function markDuplicates(players) { const byDef = /* @__PURE__ */ new Map(); for (const p of players) { const arr = byDef.get(p.definitionId) ?? []; arr.push(p); byDef.set(p.definitionId, arr); } for (const arr of byDef.values()) { if (arr.length > 1) for (const p of arr) p.isDuplicate = true; } } // src/ea/squad.ts function squadService() { const services = getGlobal("services"); return services?.["Squad"]; } async function squadById(id) { const Squad = squadService(); if (!Squad?.requestSquadById) return null; try { const res = await toPromise( Squad.requestSquadById(id) ); const data = res.data; if (!data) return null; return data.squad ?? data ?? null; } catch { return null; } } function collectInto(squad, into) { if (!squad) return; let slots = []; try { slots = squad.getFieldPlayers?.() ?? squad.getPlayers?.() ?? []; } catch { slots = []; } for (const slot of slots) { let it; try { it = slot.getItem?.() ?? slot.item; } catch { it = slot.item; } const inst = Number(it?.id ?? 0); const def = Number(it?.definitionId ?? 0); if (inst > 0) into.instanceIds.add(inst); if (def > 0) into.defIds.add(def); } } async function getActiveSquadCards() { const out = { instanceIds: /* @__PURE__ */ new Set(), defIds: /* @__PURE__ */ new Set() }; const Squad = squadService(); if (!Squad) return out; let id = 0; try { id = Number(Squad.getActiveSquadId?.() ?? 0); } catch { id = 0; } collectInto(await squadById(id), out); return out; } async function getAllSquadCards() { const out = { instanceIds: /* @__PURE__ */ new Set(), defIds: /* @__PURE__ */ new Set() }; const Squad = squadService(); if (Squad?.requestSquadList) { try { const res = await toPromise( Squad.requestSquadList() ); const squads = res.data?.squads ?? []; for (const s of squads) { collectInto(await squadById(Number(s.id ?? 0)), out); } } catch { } } const active = await getActiveSquadCards(); for (const x of active.instanceIds) out.instanceIds.add(x); for (const x of active.defIds) out.defIds.add(x); return out; } // src/ea/tap.ts function tapElement(el2, mode2 = "touch") { try { const r = el2.getBoundingClientRect(); const clientX = Math.round(r.left + r.width / 2); const clientY = Math.round(r.top + r.height / 2); const base = { bubbles: true, cancelable: true, composed: true, clientX, clientY }; if (mode2 === "mouse") { el2.dispatchEvent(new MouseEvent("mousedown", base)); el2.dispatchEvent(new MouseEvent("mouseup", base)); return; } const pointer = (type) => { try { el2.dispatchEvent( new PointerEvent(type, { ...base, pointerId: 1, isPrimary: true, pointerType: "touch" }) ); } catch { } }; pointer("pointerdown"); try { const touch = new Touch({ identifier: 1, target: el2, clientX, clientY }); el2.dispatchEvent( new TouchEvent("touchstart", { ...base, touches: [touch], targetTouches: [touch], changedTouches: [touch] }) ); el2.dispatchEvent( new TouchEvent("touchend", { ...base, touches: [], targetTouches: [], changedTouches: [touch] }) ); } catch { el2.click(); } pointer("pointerup"); } catch { } } function findBackButton() { const buttons = Array.from( document.querySelectorAll("button.ut-navigation-button-control") ); return buttons.find((b) => { const r = b.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.top < 120 && r.left < 240; }) ?? null; } function tapBack() { const back = findBackButton(); if (back) tapElement(back, "touch"); } // src/ea/apply.ts function findLiveOverviewVC(challengeId) { const hits = findViewControllers( (n) => constructorName(n) === "UTSBCSquadOverviewViewController" && "_challenge" in n && !!n["_challenge"] && "_squad" in n && !!n["_squad"] ); if (hits.length === 0) return null; if (challengeId != null) { const byId = hits.find((h) => Number(h._challenge?.id) === challengeId); if (byId) return byId; } const inDom = hits.find((h) => isInDom(h)); return inDom ?? hits[hits.length - 1] ?? null; } async function applySolution(challenge, solution, clubItems) { const vc = findLiveOverviewVC(challenge.id); const eaChallenge = vc?._challenge ?? challenge.raw; if (!eaChallenge) { return { ok: false, reason: "El challenge no expone el objeto de EA." }; } const squad = eaChallenge.squad ?? vc?._squad; if (!squad) { return { ok: false, reason: "El challenge no tiene squad \u2014 \xBFfalta loadChallenge()?" }; } if (vc && squad !== vc._squad) vc._squad = squad; let slotIndices; try { const raw = squad.getNonBrickSlots?.() ?? []; slotIndices = raw.map((s) => { const idx = s?.index; return typeof idx === "number" ? idx : Number(s); }); } catch (err) { return { ok: false, reason: `getNonBrickSlots() fall\xF3: ${errMsg(err)}` }; } if (slotIndices.length === 0) { return { ok: false, reason: "La squad no expone slots utilizables (getNonBrickSlots vac\xEDo)." }; } let arrLen; try { arrLen = squad.getFieldPlayers?.().length ?? 0; } catch { arrLen = 0; } if (arrLen <= Math.max(...slotIndices)) arrLen = Math.max(...slotIndices) + 1; const arr = new Array(arrLen).fill(null); const players = solution.players ?? []; const conceptDefs = [ ...new Set(players.filter((p) => p.concept).map((p) => p.definitionId)) ]; const conceptItems = await fetchConceptItems(conceptDefs); for (let i = 0; i < players.length && i < slotIndices.length; i++) { const p = players[i]; const slot = slotIndices[i]; if (p.concept === true) { const real = conceptItems.get(p.definitionId); if (!real) { return { ok: false, reason: `No se encontr\xF3 la carta concept ${p.definitionId} ("${p.name}").` }; } real["concept"] = true; arr[slot] = real; } else { const clubItem = clubItems.get(p.id); if (!clubItem) { return { ok: false, reason: `Falta el UTItemEntity real del club para "${p.name}" (id ${p.id}).` }; } arr[slot] = clubItem; } } try { squad.removeAllItems?.(); squad.setPlayers?.(arr, true); } catch (err) { return { ok: false, reason: `setPlayers() fall\xF3: ${errMsg(err)}` }; } const services = getGlobal("services"); const sbc = services?.SBC; if (typeof sbc?.saveChallenge !== "function") { return { ok: false, reason: "services.SBC.saveChallenge no disponible." }; } let res; try { res = await toPromise(sbc.saveChallenge.call(sbc, eaChallenge)); } catch (err) { return { ok: false, reason: `saveChallenge() fall\xF3: ${errMsg(err)}` }; } if (res.status !== 200 || !res.success) { return { ok: false, reason: `saveChallenge devolvi\xF3 status=${res.status ?? "?"} success=${res.success}` + (res.error != null ? ` error=${String(res.error)}` : "") }; } if (vc) { try { eaChallenge.onDataChange?.notify?.({ squad }); } catch (err) { console.warn("[fut-sbc] onDataChange.notify fall\xF3", err); } try { vc._pushSquadToView?.(squad); } catch (err) { console.warn("[fut-sbc] _pushSquadToView fall\xF3", err); } } return { ok: true, teamRating: safeNum(() => squad.getRating?.()), chemistry: safeNum(() => squad.getChemistry?.()) }; } async function fetchConceptItems(defIds) { const out = /* @__PURE__ */ new Map(); const services = getGlobal("services"); const DTO = getGlobal( "UTSearchCriteriaDTO" ); const search2 = services?.Item?.searchConceptItems; if (typeof search2 !== "function" || typeof DTO !== "function") return out; for (const defId of defIds) { const c = new DTO(); c["type"] = "player"; c["defId"] = [defId]; c["isExactSearch"] = true; c["maxBuy"] = 0; c["count"] = 20; try { const res = await toPromise( search2.call(services.Item, c) ); const data = res.data; const items = Array.isArray(data) ? data : data?.items ?? []; const match = items.find( (it) => Number(it.definitionId) === defId ) ?? items[0]; if (match) out.set(defId, match); } catch { } } return out; } function repaintPitch(challengeId) { const vc = findLiveOverviewVC(challengeId); if (!vc) return; const squad = vc._challenge?.squad ?? vc._squad; if (!squad) return; if (squad !== vc._squad) vc._squad = squad; try { vc._challenge.onDataChange?.notify?.({ squad }); } catch { } try { vc._pushSquadToView?.(squad); } catch { } } function leaveChallengeView() { tapBack(); } function safeNum(fn) { try { const n = fn(); return typeof n === "number" && Number.isFinite(n) ? n : void 0; } catch { return void 0; } } function errMsg(err) { return err instanceof Error ? err.message : String(err); } // src/ea/market.ts var SEARCH_DELAY_MS = 550; var BID_DELAY_MS = 1200; var SOFT_BAN = /* @__PURE__ */ new Set([426, 429]); function itemService() { const services = getGlobal("services"); return services?.["Item"]; } function newCriteria() { const DTO = getGlobal("UTSearchCriteriaDTO"); if (typeof DTO !== "function") return null; return new DTO(); } var TIERS = [ { level: "bronze", lo: 47, hi: 64 }, { level: "silver", lo: 65, hi: 74 }, { level: "gold", lo: 75, hi: 99 } ]; var CALIBRATION_KEY = "fut-sbc-solver:concept-offsets"; var CALIBRATION_TTL_MS = 7 * 24 * 60 * 60 * 1e3; var PAGE_SIZE2 = 60; var CALIBRATION_PROBES = [ 0, 250, 500, 1e3, 1500, 2e3, 3e3, 4e3, 5e3, 6e3, 7e3, 8e3 ]; function readCalibration() { try { const raw = localStorage.getItem(CALIBRATION_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); if (!parsed.at || Date.now() - parsed.at > CALIBRATION_TTL_MS) return {}; return parsed.curves ?? {}; } catch { return {}; } } function writeCalibration(curves) { try { localStorage.setItem( CALIBRATION_KEY, JSON.stringify({ at: Date.now(), curves }) ); } catch { } } async function calibrate(Item, level, probeDelayMs) { const curve = []; for (const offset of CALIBRATION_PROBES) { const r = await ratingAt(Item, level, offset); await delay(probeDelayMs); if (r == null) break; curve.push([offset, r]); } return curve; } function guessOffset(curve, rating) { if (curve.length === 0) return 0; for (let i = 0; i < curve.length - 1; i++) { const [o1, r1] = curve[i]; const [o2, r2] = curve[i + 1]; if (rating <= r1 && rating >= r2) { if (r1 === r2) return o1; const t = (r1 - rating) / (r1 - r2); return Math.round(o1 + t * (o2 - o1)); } } const [lastO, lastR] = curve[curve.length - 1]; return rating >= curve[0][1] ? 0 : lastO; } async function ratingAt(Item, level, offset) { const c = newCriteria(); if (!c) return null; c["type"] = "player"; c["level"] = level; c["count"] = 1; c["offset"] = offset; try { const res = await toPromise( Item.searchConceptItems(c) ); const data = res.data; const items = Array.isArray(data) ? data : data?.items ?? []; const r = items[0]?.rating; return typeof r === "number" ? r : null; } catch { return null; } } var MAX_NUDGES = 3; async function offsetOfRating(Item, level, curve, target, probeDelayMs) { let offset = Math.max(0, guessOffset(curve, target)); const span = curve.length > 1 ? curve[curve.length - 1][0] - curve[0][0] : 1e3; const ratingSpan = curve.length > 1 ? Math.max(1, curve[0][1] - curve[curve.length - 1][1]) : 20; let step = Math.max(60, Math.round(span / ratingSpan)); for (let i = 0; i < MAX_NUDGES; i++) { const r = await ratingAt(Item, level, offset); await delay(probeDelayMs); if (r == null) { offset = Math.max(0, offset - step); continue; } if (r === target) { return Math.max(0, offset - PAGE_SIZE2); } offset = r > target ? offset + step : Math.max(0, offset - step); step = Math.max(60, Math.round(step / 2)); } return offset; } async function searchConceptCards(opts) { const Item = itemService(); if (!Item?.searchConceptItems || !newCriteria()) return []; const out = []; const seen = /* @__PURE__ */ new Set(); const cap = opts.limit ?? 240; const PROBE_DELAY_MS = 120; const curves = readCalibration(); let curvesDirty = false; const perRating = Math.max(8, Math.ceil(cap / (opts.ratingMax - opts.ratingMin + 1))); for (const tier2 of TIERS) { if (out.length >= cap) break; if (tier2.hi < opts.ratingMin || tier2.lo > opts.ratingMax) continue; let curve = curves[tier2.level]; if (!curve || curve.length === 0) { curve = await calibrate(Item, tier2.level, PROBE_DELAY_MS); if (curve.length === 0) continue; curves[tier2.level] = curve; curvesDirty = true; } const top = Math.min(tier2.hi, opts.ratingMax); const bottom = Math.max(tier2.lo, opts.ratingMin); for (let rating = top; rating >= bottom && out.length < cap; rating--) { const start = await offsetOfRating( Item, tier2.level, curve, rating, PROBE_DELAY_MS ); const c = newCriteria(); if (!c) break; c["type"] = "player"; c["level"] = tier2.level; if (opts.league) c["league"] = opts.league; if (opts.nation) c["nation"] = opts.nation; c["count"] = PAGE_SIZE2; c["offset"] = start; let batch = []; try { const res = await toPromise( Item.searchConceptItems(c) ); const data = res.data; batch = Array.isArray(data) ? data : data?.items ?? []; } catch { break; } if (batch.length === 0) break; let taken = 0; for (const raw of batch) { if (taken >= perRating || out.length >= cap) break; const mapped = conceptToSolverPlayer(raw); if (!mapped || mapped.rating !== rating) continue; if (seen.has(mapped.definitionId)) continue; seen.add(mapped.definitionId); out.push(mapped); taken++; } await delay(SEARCH_DELAY_MS); } } if (curvesDirty) writeCalibration(curves); return out; } function conceptToSolverPlayer(raw) { const definitionId = Number(raw.definitionId ?? 0); if (!definitionId) return null; const rating = Number(raw.rating ?? 0); const positions = Array.isArray(raw.basePossiblePositions) && raw.basePossiblePositions.map(Number) || (typeof raw.preferredPosition === "number" ? [raw.preferredPosition] : []); let name = `#${definitionId}`; try { const s = raw.getStaticData?.(); if (s?.name) name = String(s.name); } catch { } return { id: -definitionId, // synthetic — concept cards have no instance definitionId, rating, name, leagueId: Number(raw.leagueId ?? 0), nationId: Number(raw.nationId ?? raw.nation ?? 0), teamId: Number(raw.teamId ?? 0), quality: rating >= 75 ? "gold" : rating >= 65 ? "silver" : "bronze", rarityId: 0, untradeable: false, concept: true, isDuplicate: false, inActiveSquad: false, inAnySquad: false, inStorage: false, isSpecial: false, // Concept search returns base cards, so no special-group membership. Left // empty on purpose: a concept must not be offered as the answer to a // "needs a TOTW" requirement it cannot satisfy. groups: [], positions: [...new Set(positions)] }; } async function buyFodder(requests, opts) { const report = { bought: [], spent: 0, failures: [], softBanned: false }; const Item = itemService(); const ItemPile = getGlobal("ItemPile"); const clubPile = ItemPile?.["CLUB"] ?? 7; if (!Item?.searchTransferMarket || !Item.bid || !Item.move) { for (const r of requests) report.failures.push({ definitionId: r.definitionId, got: 0, want: r.count, reason: "services.Item market API unavailable" }); return report; } for (const req of requests) { let got = 0; for (let n = 0; n < req.count; n++) { if (report.softBanned) break; if (report.spent + req.maxPerCard > opts.maxSpend) { report.failures.push({ definitionId: req.definitionId, got, want: req.count, reason: `budget: ${report.spent}/${opts.maxSpend} spent` }); return report; } const listing = await findCheapestListing(req.definitionId, req.maxPerCard); if (listing.softBanned) { report.softBanned = true; break; } if (!listing.item) { report.failures.push({ definitionId: req.definitionId, got, want: req.count, reason: listing.reason ?? "no listing under maxPerCard" }); break; } await delay(BID_DELAY_MS); const bidRes = await toPromise( Item.bid(listing.item, listing.price) ); if (SOFT_BAN.has(Number(bidRes.status))) { report.softBanned = true; break; } if (!bidRes.success) { report.failures.push({ definitionId: req.definitionId, got, want: req.count, reason: `bid failed (status ${bidRes.status ?? "?"})` }); break; } try { await toPromise(Item.move(listing.item, clubPile)); } catch { } report.bought.push({ definitionId: req.definitionId, price: listing.price }); report.spent += listing.price; got++; } if (report.softBanned) break; } return report; } async function findCheapestListing(definitionId, maxBuy) { const Item = itemService(); const c = newCriteria(); if (!Item?.searchTransferMarket || !c) { return { price: 0, reason: "no market API", softBanned: false }; } c["type"] = "player"; c["defId"] = [definitionId]; c["isExactSearch"] = true; c["maxBuy"] = maxBuy; c["sortBy"] = "current"; c["count"] = 12; c["offset"] = 0; await delay(SEARCH_DELAY_MS); let items = []; let status = 200; try { const res = await toPromise( Item.searchTransferMarket(c, 1) ); status = Number(res.status ?? 200); const data = res.data; items = Array.isArray(data) ? data : data?.items ?? []; } catch { return { price: 0, reason: "market search threw", softBanned: false }; } if (SOFT_BAN.has(status)) return { price: 0, softBanned: true }; let best = null; for (const it of items) { const a = it._auction ?? it.getAuctionData?.() ?? {}; const buyNow = Number(a.buyNowPrice ?? 0); const price = buyNow > 0 ? buyNow : Number(a.currentBid ?? a.startingBid ?? 0); if (price <= 0 || price > maxBuy) continue; if (!best || price < best.price) best = { item: it, price }; } if (!best) { return { price: 0, reason: "no listing under maxBuy", softBanned: false }; } return { item: best.item, price: best.price, softBanned: false }; } // src/ea/pools.ts function itemService2() { const services = getGlobal("services"); return services?.["Item"]; } async function fetchUnassignedPlayers() { const Item = itemService2(); const players = []; const items = /* @__PURE__ */ new Map(); if (!Item?.requestUnassignedItems) return { players, items }; try { const res = await toPromise( Item.requestUnassignedItems() ); const data = res.data; const raw = Array.isArray(data) ? data : data?.items ?? []; collect(raw, players, items); } catch { } return { players, items }; } async function fetchStoragePlayers() { const Item = itemService2(); const DTO = getGlobal( "UTSearchCriteriaDTO" ); const players = []; const items = /* @__PURE__ */ new Map(); if (!Item?.searchStorageItems || typeof DTO !== "function") { return { players, items }; } for (let offset = 0; offset < 600; offset += 50) { const c = new DTO(); c["count"] = 50; c["offset"] = offset; let raw = []; try { const res = await toPromise( Item.searchStorageItems(c) ); const data = res.data; raw = Array.isArray(data) ? data : data?.items ?? []; } catch { break; } collect(raw, players, items); if (raw.length < 50) break; } return { players, items }; } function collect(raw, players, items) { for (const it of raw) { if (typeof it.loans === "number" && it.loans > -1) { continue; } const mapped = rawItemToSolverPlayer(it); if (!mapped) continue; mapped.inStorage = true; players.push(mapped); items.set(mapped.id, it); } } // src/ea/submit.ts var SOFT_BAN2 = /* @__PURE__ */ new Set([426, 429]); var SUBMIT_DELAY_MS = 1800; var CHEM_RETRY_PRE_MS = 500; var CHEM_RETRY_POST_MS = 1e3; function sbcService() { const services = getGlobal("services"); return services?.["SBC"]; } function chemistryService() { const services = getGlobal("services"); return services?.["Chemistry"]; } var HOUR_MS = 36e5; var DAY_MS = 864e5; var MAX_PER_HOUR = 90; var MAX_PER_DAY = 300; var submitTimes = []; function rateLimitReason() { const now = Date.now(); while (submitTimes.length > 0 && now - submitTimes[0] > DAY_MS) { submitTimes.shift(); } const lastHour = submitTimes.filter((t) => now - t <= HOUR_MS).length; if (lastHour >= MAX_PER_HOUR) { return `L\xEDmite de EA alcanzado: ${lastHour} env\xEDos en la \xFAltima hora (m\xE1x ${MAX_PER_HOUR}).`; } if (submitTimes.length >= MAX_PER_DAY) { return `L\xEDmite de EA alcanzado: ${submitTimes.length} env\xEDos en 24 h (m\xE1x ${MAX_PER_DAY}).`; } return null; } function noteSubmit() { submitTimes.push(Date.now()); } async function submitChallenge(challenge) { const blocked = rateLimitReason(); if (blocked) return { ok: false, reason: blocked }; const sbc = sbcService(); if (typeof sbc?.submitChallenge !== "function") { return { ok: false, reason: "services.SBC.submitChallenge no disponible." }; } const eaChallenge = liveChallengeObject(challenge); if (!eaChallenge) { return { ok: false, reason: `No se encontr\xF3 el objeto challenge de EA (id ${challenge.id}).` }; } const set = await findSet(challenge.setId); if (!set) { return { ok: false, reason: `No se encontr\xF3 el set ${challenge.setId} en services.SBC.repository.` }; } const chemEnabled = chemistryEnabled(); let res; try { res = await runSubmit(sbc, eaChallenge, set, chemEnabled); } catch (err) { return { ok: false, reason: `submitChallenge() fall\xF3: ${errMsg2(err)}` }; } if (SOFT_BAN2.has(Number(res.status))) { return { ok: false, softBanned: true, reason: `EA respondi\xF3 ${res.status} \u2014 soft-ban. No reintentar.` }; } if (isChemistryMismatch(res.error)) { const stillBlocked = rateLimitReason(); if (stillBlocked) { return { ok: false, reason: `${stillBlocked} (tras CHEMISTRY_VERSION_MISMATCH)` }; } await delay(CHEM_RETRY_PRE_MS); await resetChemistry(); await delay(CHEM_RETRY_POST_MS); try { res = await runSubmit(sbc, eaChallenge, set, chemEnabled); } catch (err) { return { ok: false, reason: `submitChallenge() fall\xF3 en el reintento de qu\xEDmica: ${errMsg2(err)}` }; } if (SOFT_BAN2.has(Number(res.status))) { return { ok: false, softBanned: true, reason: `EA respondi\xF3 ${res.status} \u2014 soft-ban. No reintentar.` }; } } const violations = readViolations(res); if (violations.length > 0) { return { ok: false, violations, reason: `EA rechaz\xF3 la squad: ${violations.join(" \xB7 ")}` }; } if (res.status != null && res.status !== 200) { return { ok: false, reason: `submitChallenge devolvi\xF3 status=${res.status}${res.error != null ? ` error=${String(res.error)}` : ""}` }; } if (!res.success) { return { ok: false, reason: `submitChallenge devolvi\xF3 success=false${res.error != null ? ` error=${String(res.error)}` : ""}` }; } return { ok: true }; } async function runSubmit(sbc, eaChallenge, set, chemEnabled) { const submit = sbc.submitChallenge; if (typeof submit !== "function") { throw new Error("services.SBC.submitChallenge no disponible."); } const obs = submit.call(sbc, eaChallenge, set, true, chemEnabled); noteSubmit(); try { return await toPromise(obs); } finally { await delay(SUBMIT_DELAY_MS); } } function chemistryEnabled() { try { return chemistryService()?.isFeatureEnabled?.() === true; } catch { return false; } } function isChemistryMismatch(error) { if (error == null) return false; const codes = getGlobal("UtasErrorCode"); const expected = codes?.["CHEMISTRY_VERSION_MISMATCH"]; if (expected == null) return false; return String(error) === String(expected); } async function resetChemistry() { const chem = chemistryService(); try { chem?.resetCustomProfiles?.(); } catch (err) { console.warn("[fut-sbc] resetCustomProfiles fall\xF3", err); } try { const obs = chem?.requestChemistryProfiles?.(); if (obs != null) await toPromise(obs); } catch (err) { console.warn("[fut-sbc] requestChemistryProfiles fall\xF3", err); } } function readViolations(res) { const buckets = []; buckets.push(res.data?.itemViolations); if (typeof res.error === "object" && res.error !== null) { buckets.push(res.error.itemViolations); } const out = []; for (const bucket of buckets) { if (!Array.isArray(bucket)) continue; for (const v of bucket) { const name = v?.name; const text = name != null ? String(name) : String(v); if (text && !out.includes(text)) out.push(text); } } return out; } function liveChallengeObject(challenge) { try { const vc = findLiveOverviewVC(challenge.id); const live = vc?._challenge; if (live && Number(live.id) === challenge.id) return live; } catch { } return challenge.raw ?? null; } var DISMISSABLE_VC = /(modal|dialog|popup|alert|reward|toast|overlay)/i; var DISMISS_METHODS = ["dismiss", "close", "hide"]; var MODAL_SELECTORS = ".ut-navigation-button-control, .view-modal-container, .ea-dialog-view"; var DISMISS_LABEL = /^(ok|continue|collect|claim)$/i; async function dismissPostSubmit() { try { const overlays = findViewControllers( (n) => DISMISSABLE_VC.test(constructorName(n)) && hasDismisser(n) && isInDom(n) ); for (const vc of overlays) { if (callDismisser(vc)) { await delay(250); break; } } } catch (err) { console.warn("[fut-sbc] dismissPostSubmit: barrido de VCs fall\xF3", err); } if (!modalPresent()) return; try { if (clickDismissButton()) await delay(250); } catch (err) { console.warn("[fut-sbc] dismissPostSubmit: click de bot\xF3n fall\xF3", err); } if (!modalPresent()) return; try { pressEscape(); await delay(250); } catch (err) { console.warn("[fut-sbc] dismissPostSubmit: Escape fall\xF3", err); } } function hasDismisser(node) { return DISMISS_METHODS.some((m) => typeof node[m] === "function"); } function callDismisser(node) { for (const m of DISMISS_METHODS) { const fn = node[m]; if (typeof fn !== "function") continue; try { fn.call(node); return true; } catch { } } return false; } function modalPresent() { try { if (typeof document === "undefined") return false; return document.querySelector(".view-modal-container, .ea-dialog-view") != null; } catch { return false; } } function clickDismissButton() { if (typeof document === "undefined") return false; const scopes = Array.from(document.querySelectorAll(MODAL_SELECTORS)); for (const scope of scopes) { const candidates = [ scope, ...Array.from(scope.querySelectorAll("button, .btn-standard, [role='button']")) ]; for (const el2 of candidates) { const label = (el2.textContent ?? "").trim(); if (!DISMISS_LABEL.test(label)) continue; el2.click?.(); return true; } } return false; } function pressEscape() { if (typeof document === "undefined") return; const init = { key: "Escape", code: "Escape", bubbles: true, cancelable: true }; Object.assign(init, { keyCode: 27, which: 27 }); document.dispatchEvent(new KeyboardEvent("keydown", init)); document.dispatchEvent(new KeyboardEvent("keyup", init)); } async function reenterChallenge(setId, challengeId) { const sbc = sbcService(); if (!sbc) return null; const set = await findSet(setId); if (!set) return null; let raw = pickChallenge(challengesOf(set), challengeId); if (!raw && typeof sbc.requestChallengesForSet === "function") { try { const res = await toPromise( sbc.requestChallengesForSet.call(sbc, set) ); const list = res.data?.challenges; if (Array.isArray(list)) raw = pickChallenge(list, challengeId); } catch (err) { console.warn("[fut-sbc] requestChallengesForSet fall\xF3", err); } } if (!raw) return null; await loadChallengeSquad(sbc, raw, challengeId); try { const open = await getOpenChallenge(); if (open && open.id === challengeId) return open; } catch (err) { console.warn("[fut-sbc] getOpenChallenge tras reenter fall\xF3", err); } if (!raw.squad) return null; const constraints = parseRequirements(raw); const positions = readSlotPositions2(raw); if (positions.length === constraints.slots) constraints.slotPositions = positions; return { id: Number(raw.id ?? challengeId), setId: Number(raw.setId ?? setId), name: String(raw.name ?? ""), slots: constraints.slots, constraints, raw }; } async function loadChallengeSquad(sbc, raw, challengeId) { const inProgress = safeInProgress2(raw); if (typeof sbc.loadChallenge === "function") { try { const res = await toPromise( sbc.loadChallenge.call(sbc, raw) ); attachSquad(raw, res.data?.squad); } catch (err) { console.warn("[fut-sbc] loadChallenge fall\xF3", err); } } if (raw.squad) return; const dao = sbc.sbcDAO; if (typeof dao?.loadChallenge === "function") { try { const res = await toPromise( dao.loadChallenge.call(dao, challengeId, inProgress) ); attachSquad(raw, res.data?.squad); } catch (err) { console.warn("[fut-sbc] sbcDAO.loadChallenge fall\xF3", err); } } } function attachSquad(raw, squad) { if (raw.squad || squad == null) return; try { raw.squad = squad; } catch { } } function pickChallenge(list, challengeId) { for (const c of list) { if (!c || typeof c !== "object") continue; if (Number(c.id) === challengeId) return c; } return null; } function readSlotPositions2(raw) { const sq = raw.squad; try { const slots = sq?.getNonBrickSlots?.() ?? []; return slots.map((s) => { const g = s.getGeneralPosition?.(); return typeof g === "number" ? g : Number(s.position?.id ?? -1); }); } catch { return []; } } async function repeatability(setId) { const set = await findSet(setId); if (!set) return null; const mode2 = String(set.repeatabilityMode ?? "").trim() || "UNKNOWN"; const repeats = safeNum2(() => Number(set.repeats)) ?? 0; const timesCompleted = safeNum2(() => Number(set.timesCompleted)) ?? 0; return { mode: mode2, repeats, timesCompleted, remaining: remainingRuns(mode2, repeats, timesCompleted) }; } function remainingRuns(mode2, repeats, timesCompleted) { if (mode2 === "NON_REPEATABLE") return timesCompleted > 0 ? 0 : 1; if (repeats > 0) return Math.max(repeats - timesCompleted, 0); if (mode2 === "UNLIMITED" || mode2 === "REFRESH") return Number.POSITIVE_INFINITY; return 0; } async function findSet(setId) { const sbc = sbcService(); if (!sbc || !Number.isFinite(setId)) return null; const direct = setFromRepository(sbc, setId); if (direct) return direct; if (typeof sbc.requestSets === "function") { try { const res = await toPromise(sbc.requestSets.call(sbc)); const sets = res.data?.sets; if (Array.isArray(sets)) { const hit = sets.find((s) => Number(s.id) === setId); if (hit) return hit; } } catch (err) { console.warn("[fut-sbc] requestSets fall\xF3", err); } } return setFromRepository(sbc, setId); } function setFromRepository(sbc, setId) { const repo = sbc.repository; if (!repo) return null; try { const byId = repo.getSetById?.(setId); if (byId) return byId; } catch { } for (const s of collectionValues2(repo.sets)) { if (Number(s.id) === setId) return s; } return null; } function challengesOf(set) { try { const chs = set.getChallenges?.(); return Array.isArray(chs) ? chs : []; } catch { return []; } } function collectionValues2(coll) { if (!coll) return []; const inner = coll._collection ?? coll; if (inner instanceof Map) return [...inner.values()]; if (Array.isArray(inner)) return inner; if (typeof inner === "object") return Object.values(inner); return []; } function safeInProgress2(raw) { try { return raw.isInProgress?.() === true; } catch { return false; } } function safeNum2(fn) { try { const n = fn(); return typeof n === "number" && Number.isFinite(n) ? n : void 0; } catch { return void 0; } } function errMsg2(err) { return err instanceof Error ? err.message : String(err); } // src/ea/daily.ts var DAILY_REFRESH_INTERVAL = 86400; function sbcService2() { const services = getGlobal("services"); return services?.["SBC"]; } async function listDailySets() { const sbc = sbcService2(); if (!sbc) return []; if (typeof sbc.requestSets === "function") { try { await toPromise(sbc.requestSets.call(sbc)); } catch (err) { console.warn("[fut-sbc] requestSets fall\xF3, uso el repositorio tal cual", err); } } const out = []; try { for (const raw of collectionValues3(sbc.repository?.sets)) { const set = raw; if (safeNum3(set.refreshInterval) !== DAILY_REFRESH_INTERVAL) continue; if (isExpired(set)) continue; const id = safeNum3(set.id); if (id == null) continue; const repeats = safeNum3(set.repeats) ?? 0; const timesCompleted = safeNum3(set.timesCompleted) ?? 0; const remaining = Math.max(repeats - timesCompleted, 0); if (remaining <= 0) continue; const entry = { id, name: String(set.name ?? ""), remaining, repeats, timesCompleted, challengeCount: Math.max(1, safeNum3(set.challengesCount) ?? 1) }; const challengeId = firstChallengeId(set); if (challengeId != null) entry.challengeId = challengeId; out.push(entry); } } catch (err) { console.warn("[fut-sbc] listDailySets: barrido de sets fall\xF3", err); return []; } out.sort(compareByCost); return out; } function compareByCost(a, b) { const ta = nameTierHint(a.name); const tb = nameTierHint(b.name); if (ta !== tb) return ta - tb; return a.id - b.id; } function nameTierHint(name) { if (/bronze/i.test(name)) return 0; if (/silver/i.test(name)) return 1; if (/gold/i.test(name)) return 2; return 3; } function isExpired(set) { if (set.notExpirable) return false; const endTime = safeNum3(set.endTime); if (endTime == null || endTime <= 0) return false; return endTime * 1e3 < Date.now(); } function firstChallengeId(set) { const first = challengesOf2(set)[0]; return first ? safeNum3(first.id) ?? void 0 : void 0; } function safeCompleted(ch) { try { return ch.isCompleted?.() === true; } catch { return false; } } function setRunComplete(setId) { const sbc = sbcService2(); const set = sbc?.repository?.getSetById?.(setId); if (!set) return false; try { if (typeof set.challengesComplete === "function") { return set.challengesComplete() === true; } } catch { } const total = safeNum3(set.challengesCount); const done = safeNum3(set.challengesCompletedCount); if (total != null && done != null) return done >= total; const challenges = challengesOf2(set); return challenges.length > 0 && challenges.every(safeCompleted); } function setRepeatsRemaining(setId) { const sbc = sbcService2(); const set = sbc?.repository?.getSetById?.(setId); if (!set) return 0; try { const n = set.getRepeatsRemaining?.(); if (typeof n === "number" && Number.isFinite(n)) return Math.max(0, n); } catch { } const repeats = safeNum3(set.repeats) ?? 0; const done = safeNum3(set.timesCompleted) ?? 0; return Math.max(0, repeats - done); } function getOpenSetId() { try { const hits = findViewControllers( (n) => constructorName(n) === "UTSBCGroupChallengeSplitViewController" ); const vc = hits.find((h) => isInDom(h)) ?? hits[hits.length - 1]; if (!vc) return null; const vm = vc["sbcViewModel"]; if (!vm) return null; let set = vm.sbcSet; if (!set && typeof vm.getSbcSet === "function") { try { set = vm.getSbcSet(); } catch { } } return set ? safeNum3(set.id) ?? null : null; } catch { return null; } } async function describeSetChallenges(setId) { const sbc = sbcService2(); if (!sbc || !Number.isFinite(setId)) return []; try { const set = await findSet2(sbc, setId); if (!set) return []; let challenges = challengesOf2(set); if (challenges.length === 0 && typeof sbc.requestChallengesForSet === "function") { for (let attempt = 0; attempt < 2 && challenges.length === 0; attempt++) { try { await toPromise( sbc.requestChallengesForSet.call(sbc, set) ); } catch (err) { console.warn("[fut-sbc] requestChallengesForSet fall\xF3", err); } challenges = challengesOf2(set); if (challenges.length === 0) await delay(400); } } const out = []; for (const ch of challenges) { const completed = safeCompleted(ch); const info = { id: safeNum3(ch.id) ?? -1, name: String(ch.name ?? ""), completed, slots: null, requirements: [] }; if (!completed) { try { if (!ch.squad || !safeInProgress3(ch)) await openInPlace(sbc, ch); if (ch.squad) { const constraints = parseRequirements(ch); info.slots = constraints.slots; const d = constraints.minOvrPerPlayer ?? constraints.exactOvr ?? constraints.teamRatingMin; if (d != null) info.demandsOvr = d; info.requirements = readRequirementStrings(ch); } } catch (err) { console.warn(`[fut-sbc] describeSetChallenges: ${info.name} fall\xF3`, err); } } out.push(info); } return out; } catch (err) { console.warn("[fut-sbc] describeSetChallenges fall\xF3", err); return []; } } function readRequirementStrings(ch) { try { const reqs = ch.eligibilityRequirements; if (!Array.isArray(reqs)) return []; return reqs.map((r) => { try { const fn = r.buildString; return typeof fn === "function" ? String(fn.call(r) ?? "") : ""; } catch { return ""; } }).filter((s) => s.length > 0); } catch { return []; } } async function openChallengeById(setId, challengeId) { const sbc = sbcService2(); if (!sbc) return null; try { const set = await findSet2(sbc, setId); if (!set) return null; let challenge = challengesOf2(set).find( (c) => safeNum3(c.id) === challengeId ); if (!challenge && typeof sbc.requestChallengesForSet === "function") { try { await toPromise( sbc.requestChallengesForSet.call(sbc, set) ); } catch { } challenge = challengesOf2(set).find( (c) => safeNum3(c.id) === challengeId ); } if (!challenge) return null; if (!challenge.squad || !safeInProgress3(challenge)) { await openInPlace(sbc, challenge); } if (!challenge.squad) return null; const constraints = parseRequirements(challenge); const slotPositions = readSlotPositions3(challenge); if (slotPositions.length === constraints.slots) { constraints.slotPositions = slotPositions; } if (constraints.slots <= 0) return null; return { id: safeNum3(challenge.id) ?? challengeId, setId: safeNum3(challenge.setId) ?? setId, name: String(challenge.name ?? ""), slots: constraints.slots, constraints, raw: challenge }; } catch (err) { console.warn("[fut-sbc] openChallengeById fall\xF3", err); return null; } } async function openSetChallenge(setId) { const sbc = sbcService2(); if (!sbc || !Number.isFinite(setId)) return null; try { const set = await findSet2(sbc, setId); if (!set) return null; const pending = () => challengesOf2(set).find((c) => !safeCompleted(c)); let challenge = pending(); if (!challenge && typeof sbc.requestChallengesForSet === "function") { for (let attempt = 0; attempt < 2 && !challenge; attempt++) { try { await toPromise( sbc.requestChallengesForSet.call(sbc, set) ); } catch (err) { console.warn("[fut-sbc] requestChallengesForSet fall\xF3", err); } challenge = pending(); if (!challenge) await delay(400); } } if (!challenge) { if (setRunComplete(setId)) return null; console.warn( `[fut-sbc] openSetChallenge: EA no expone challenges para el set ${setId} (\xBFbloqueado o expirado?).` ); return null; } if (!challenge.squad || !safeInProgress3(challenge)) { await openInPlace(sbc, challenge); } if (!challenge.squad) { console.warn( `[fut-sbc] openSetChallenge: EA no adjunt\xF3 squad al challenge del set ${setId}.` ); return null; } const constraints = parseRequirements(challenge); const slotPositions = readSlotPositions3(challenge); if (slotPositions.length === constraints.slots) { constraints.slotPositions = slotPositions; } if (constraints.slots <= 0) { console.warn( `[fut-sbc] openSetChallenge: 0 slots legibles en el set ${setId}, no hay nada que resolver.` ); return null; } return { id: safeNum3(challenge.id) ?? -1, setId: safeNum3(challenge.setId) ?? setId, name: String(challenge.name ?? ""), slots: constraints.slots, constraints, raw: challenge }; } catch (err) { console.warn(`[fut-sbc] openSetChallenge(${setId}) fall\xF3`, err); return null; } } async function openInPlace(sbc, challenge) { if (typeof sbc.loadChallenge !== "function") return; try { await toPromise(sbc.loadChallenge.call(sbc, challenge)); } catch (err) { console.warn("[fut-sbc] loadChallenge fall\xF3", err); } } function readSlotPositions3(challenge) { const sq = challenge.squad; try { const slots = sq?.getNonBrickSlots?.() ?? []; return slots.map((s) => { const g = s.getGeneralPosition?.(); return typeof g === "number" ? g : Number(s.position?.id ?? -1); }); } catch { return []; } } async function findSet2(sbc, setId) { const direct = setFromRepository2(sbc, setId); if (direct) return direct; if (typeof sbc.requestSets === "function") { try { await toPromise(sbc.requestSets.call(sbc)); } catch (err) { console.warn("[fut-sbc] requestSets fall\xF3", err); } } return setFromRepository2(sbc, setId); } function setFromRepository2(sbc, setId) { const repo = sbc.repository; if (!repo) return null; try { const byId = repo.getSetById?.(setId); if (byId) return byId; } catch { } try { for (const s of collectionValues3(repo.sets)) { if (safeNum3(s.id) === setId) return s; } } catch { } return null; } function challengesOf2(set) { try { const chs = set.getChallenges?.(); return Array.isArray(chs) ? chs : []; } catch { return []; } } function collectionValues3(coll) { if (!coll) return []; const inner = coll._collection ?? coll; if (inner instanceof Map) return [...inner.values()]; if (Array.isArray(inner)) return inner; if (typeof inner === "object") return Object.values(inner); return []; } function safeInProgress3(challenge) { try { return challenge.isInProgress?.() === true; } catch { return false; } } function safeNum3(value) { try { const n = Number(value); return Number.isFinite(n) ? n : null; } catch { return null; } } // src/ui/result-card.ts function cardShell(headingText, onClose) { const el2 = document.createElement("div"); el2.className = "card"; const head = document.createElement("div"); head.className = "card-head"; const heading = document.createElement("span"); heading.textContent = headingText; const close = document.createElement("button"); close.className = "icon-btn"; close.type = "button"; close.setAttribute("aria-label", "Cerrar"); close.textContent = "\xD7"; close.addEventListener("click", onClose); head.append(heading, close); const body = document.createElement("div"); body.className = "card-body"; el2.append(head, body); return { el: el2, body }; } function createResultCard(solution, opts, unmet = [], notes = []) { const { el: el2, body } = cardShell( unmet.length > 0 ? "Soluci\xF3n parcial" : "Soluci\xF3n", opts.onClose ); if (unmet.length > 0) { const warn = document.createElement("div"); warn.className = "warn"; const wh = document.createElement("div"); wh.className = "buy-head"; wh.textContent = "No cumple todav\xEDa"; const wl = document.createElement("ul"); for (const u of unmet) { const li = document.createElement("li"); li.textContent = u; wl.append(li); } warn.append(wh, wl); body.append(warn); } const stats = document.createElement("div"); stats.className = "stats"; stats.append( stat("Media", String(solution.teamRating)), stat("Qu\xEDmica", solution.chemistry < 0 ? "\u2014" : String(solution.chemistry)), stat("Jugadores", String(solution.players.length)) ); if (typeof solution.costCoins === "number") { stats.append(stat("Costo", formatCoins(solution.costCoins))); } if (notes.length > 0) { const info = document.createElement("div"); info.className = "buy"; const head = document.createElement("div"); head.className = "buy-head"; head.textContent = "Nota"; const list2 = document.createElement("ul"); for (const n of notes) { const li = document.createElement("li"); li.textContent = n; list2.append(li); } info.append(head, list2); body.append(info); } body.append(stats); const list = document.createElement("ul"); list.className = "player-list"; for (const p of solution.players) { const li = document.createElement("li"); const name = document.createElement("span"); name.className = "p-name"; name.textContent = p.name || `#${p.definitionId}`; if (p.concept) { const tag = document.createElement("span"); tag.className = "p-tag"; tag.textContent = "comprar"; name.append(" ", tag); } const rating = document.createElement("span"); rating.className = "p-rating"; rating.textContent = String(p.rating); li.append(name, rating); list.append(li); } body.append(list); if (solution.toBuy.length > 0) { const buyWrap = document.createElement("div"); buyWrap.className = "buy"; const buyHead = document.createElement("div"); buyHead.className = "buy-head"; buyHead.textContent = "Falta comprar"; const buyList = document.createElement("ul"); for (const b of solution.toBuy) { const li = document.createElement("li"); li.textContent = `${b.count}\xD7 rating ${b.rating} (carta ${b.definitionId})`; buyList.append(li); } buyWrap.append(buyHead, buyList); if (opts.onBuyAndApply) { const row = document.createElement("label"); row.className = "field"; const lbl = document.createElement("span"); lbl.textContent = "Tope de gasto"; const spend = document.createElement("input"); spend.type = "number"; spend.min = "0"; spend.step = "500"; spend.value = "0"; row.append(lbl, spend); const buyBtn = document.createElement("button"); buyBtn.className = "btn primary"; buyBtn.type = "button"; buyBtn.textContent = "Comprar y aplicar"; buyBtn.title = "NO verificado contra EA \u2014 us\xE1 un tope bajo"; buyBtn.addEventListener("click", () => { const cap = Math.max(0, Math.round(spend.valueAsNumber || 0)); if (cap <= 0) { spend.focus(); return; } if (window.confirm( `Comprar fodder gastando hasta ${cap} monedas y aplicar? (flujo NO verificado)` )) { opts.onBuyAndApply(solution, cap); } }); buyWrap.append(row, buyBtn); } body.append(buyWrap); } const actions = document.createElement("div"); actions.className = "card-actions"; const applyBtn = document.createElement("button"); applyBtn.className = "btn primary"; applyBtn.type = "button"; applyBtn.textContent = "Aplicar"; applyBtn.addEventListener("click", () => opts.onApply(solution)); const closeBtn = document.createElement("button"); closeBtn.className = "btn"; closeBtn.type = "button"; closeBtn.textContent = "Cerrar"; closeBtn.addEventListener("click", opts.onClose); actions.append(applyBtn, closeBtn); body.append(actions); return { el: el2, destroy() { el2.remove(); } }; } function formatCoins(coins) { if (coins >= 1e6) return `${(coins / 1e6).toFixed(1)}M`; if (coins >= 1e4) return `${Math.round(coins / 1e3)}k`; if (coins >= 1e3) return `${(coins / 1e3).toFixed(1)}k`; return String(Math.round(coins)); } function createNoticeCard(message, opts) { const { el: el2, body } = cardShell("Listo", opts.onClose); const msg = document.createElement("p"); msg.className = "err-msg"; msg.textContent = message; body.append(msg); const actions = document.createElement("div"); actions.className = "card-actions"; const closeBtn = document.createElement("button"); closeBtn.className = "btn"; closeBtn.type = "button"; closeBtn.textContent = "Cerrar"; closeBtn.addEventListener("click", opts.onClose); actions.append(closeBtn); body.append(actions); return { el: el2, destroy: () => el2.remove() }; } function createErrorCard(message, opts) { const { el: el2, body } = cardShell("Error", opts.onClose); el2.classList.add("card-error"); const msg = document.createElement("p"); msg.className = "err-msg"; msg.textContent = message; body.append(msg); const hint = document.createElement("p"); hint.className = "err-hint"; hint.textContent = "Detalle en la consola y en window.__futErr"; body.append(hint); const actions = document.createElement("div"); actions.className = "card-actions"; const closeBtn = document.createElement("button"); closeBtn.className = "btn"; closeBtn.type = "button"; closeBtn.textContent = "Cerrar"; closeBtn.addEventListener("click", opts.onClose); actions.append(closeBtn); body.append(actions); return { el: el2, destroy() { el2.remove(); } }; } function stat(label, value) { const wrap = document.createElement("div"); wrap.className = "stat"; const v = document.createElement("span"); v.className = "stat-v"; v.textContent = value; const l = document.createElement("span"); l.className = "stat-l"; l.textContent = label; wrap.append(v, l); return wrap; } // src/ui/draggable.ts var DRAG_THRESHOLD_PX = 4; function readPos(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const p = JSON.parse(raw); if (typeof p.x !== "number" || typeof p.y !== "number") return null; if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) return null; return { x: p.x, y: p.y }; } catch { return null; } } function writePos(key, p) { try { localStorage.setItem(key, JSON.stringify(p)); } catch { } } function clampToViewport(el2, p) { const r = el2.getBoundingClientRect(); const maxX = Math.max(0, window.innerWidth - Math.max(40, r.width)); const maxY = Math.max(0, window.innerHeight - Math.max(24, r.height)); return { x: Math.min(Math.max(0, p.x), maxX), y: Math.min(Math.max(0, p.y), maxY) }; } function makeDraggable(el2, handle, key) { let dragged = false; let active = false; let start = { x: 0, y: 0 }; let origin = { x: 0, y: 0 }; const applyPos = (p) => { const safe = clampToViewport(el2, p); el2.style.position = "fixed"; el2.style.left = `${safe.x}px`; el2.style.top = `${safe.y}px`; el2.style.right = "auto"; el2.style.bottom = "auto"; el2.style.transform = "none"; }; const saved = readPos(key); if (saved) applyPos(saved); handle.style.touchAction = "none"; handle.style.cursor = "grab"; const onDown = (e) => { if (e.button !== 0) return; active = true; dragged = false; start = { x: e.clientX, y: e.clientY }; const r = el2.getBoundingClientRect(); origin = { x: r.left, y: r.top }; handle.style.cursor = "grabbing"; try { handle.setPointerCapture(e.pointerId); } catch { } }; const onMove = (e) => { if (!active) return; const dx = e.clientX - start.x; const dy = e.clientY - start.y; if (!dragged && Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return; dragged = true; e.preventDefault(); e.stopPropagation(); applyPos({ x: origin.x + dx, y: origin.y + dy }); }; const onUp = (e) => { if (!active) return; active = false; handle.style.cursor = "grab"; try { handle.releasePointerCapture(e.pointerId); } catch { } if (dragged) { const r = el2.getBoundingClientRect(); writePos(key, { x: r.left, y: r.top }); e.preventDefault(); e.stopPropagation(); } }; handle.addEventListener("pointerdown", onDown); handle.addEventListener("pointermove", onMove); handle.addEventListener("pointerup", onUp); handle.addEventListener("pointercancel", onUp); const onResize = () => { const p = readPos(key); if (p) applyPos(p); }; window.addEventListener("resize", onResize); return { wasDragged: () => dragged, destroy() { handle.removeEventListener("pointerdown", onDown); handle.removeEventListener("pointermove", onMove); handle.removeEventListener("pointerup", onUp); handle.removeEventListener("pointercancel", onUp); window.removeEventListener("resize", onResize); } }; } // src/tweaks/registry.ts var STORAGE_KEY = "fut-sbc-solver:tweaks"; var CHOICES_KEY = "fut-sbc-solver:tweak-choices"; var LOG_LIMIT = 200; var registry = /* @__PURE__ */ new Map(); var applied = /* @__PURE__ */ new Map(); var state = {}; var booted = false; function log(message) { const host = globalThis; const sink = host.__futTweaks ??= []; sink.push(`[tweaks] ${message}`); if (sink.length > LOG_LIMIT) sink.splice(0, sink.length - LOG_LIMIT); } function storage() { try { return globalThis.localStorage ?? null; } catch { return null; } } var choices = {}; function readChoices() { try { const raw = storage()?.getItem(CHOICES_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return {}; const out = {}; for (const [k, v] of Object.entries(parsed)) { if (typeof v === "string") out[k] = v; } return out; } catch { return {}; } } function writeChoices() { try { storage()?.setItem(CHOICES_KEY, JSON.stringify(choices)); } catch { } } var choiceKey = (tweakId, choiceId) => `${tweakId}:${choiceId}`; function getChoice(tweakId, choiceId) { const declared = registry.get(tweakId)?.choices?.find((c) => c.id === choiceId); const fallback = declared?.defaultValue ?? ""; const stored = choices[choiceKey(tweakId, choiceId)]; if (typeof stored !== "string") return fallback; if (declared && !declared.values.some((v) => v.value === stored)) return fallback; return stored; } function setChoice(tweakId, choiceId, value) { choices = { ...choices, [choiceKey(tweakId, choiceId)]: value }; writeChoices(); log(`choice ${tweakId}.${choiceId} = ${value}`); } function ctxFor(tweak) { return { log, choice: (choiceId) => getChoice(tweak.id, choiceId) }; } function readState() { try { const raw = storage()?.getItem(STORAGE_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return {}; const out = {}; for (const [k, v] of Object.entries(parsed)) { if (typeof v === "boolean") out[k] = v; } return out; } catch { return {}; } } function writeState() { try { storage()?.setItem(STORAGE_KEY, JSON.stringify(state)); } catch { } } function registerTweak(tweak) { if (registry.has(tweak.id)) { log(`duplicado ignorado: ${tweak.id}`); return; } registry.set(tweak.id, tweak); if (booted && isEnabled(tweak.id)) applyOne(tweak); } function allTweaks() { return [...registry.values()]; } function isEnabled(id) { const tweak = registry.get(id); if (!tweak) return false; const stored = state[id]; if (typeof stored === "boolean") return stored; if (tweak.irreversible) return false; return tweak.defaultOn; } function applyOne(tweak) { if (applied.has(tweak.id)) return; try { const ctx = ctxFor(tweak); tweak.enable(ctx); applied.set(tweak.id, () => tweak.disable(ctx)); log(`ON ${tweak.id}`); } catch (e) { log(`FALL\xD3 al activar ${tweak.id}: ${String(e)}`); } } function revertOne(tweak) { const undo10 = applied.get(tweak.id); if (!undo10) return; applied.delete(tweak.id); try { undo10(); log(`OFF ${tweak.id}`); } catch (e) { log(`FALL\xD3 al desactivar ${tweak.id}: ${String(e)}`); } } function setEnabled(id, on) { const tweak = registry.get(id); if (!tweak) return; state = { ...state, [id]: on }; writeState(); if (on) applyOne(tweak); else revertOne(tweak); } function applyAllTweaks() { state = readState(); choices = readChoices(); booted = true; for (const tweak of registry.values()) { if (isEnabled(tweak.id)) applyOne(tweak); } log(`activos: ${applied.size}/${registry.size}`); } // src/tweaks/display-modes.ts function layoutRoot() { return document.querySelector(".futweb") ?? document.body; } function displayMode(id, cssClass, label, hint) { const category = "interfaz"; return { id, label, hint, category, defaultOn: false, enable() { layoutRoot().classList.add(cssClass); }, disable() { layoutRoot().classList.remove(cssClass); } }; } registerTweak( displayMode( "display.fullWidth", "full-width", "Pantalla completa", "Estira la webapp a todo el ancho en vez del recuadro central de EA." ) ); registerTweak( displayMode( "display.grid", "grid-mode", "Vista en grilla", "Muestra las listas de jugadores en columnas en vez de una fila por carta." ) ); registerTweak( displayMode( "display.compact", "compact-view", "Vista compacta", "Achica cada fila ocultando el bloque de precios. Se combina con las otras dos." ) ); // src/tweaks/patch.ts function patchMethod(target, key, factory) { if (!target) return () => void 0; const original = target[key]; if (typeof original !== "function") return () => void 0; const originalFn = original; target[key] = factory(originalFn); let restored = false; return () => { if (restored) return; restored = true; target[key] = originalFn; }; } function combine(...restores) { return () => { for (const r of restores) { try { r(); } catch { } } }; } // src/tweaks/pack-animation.ts var undo = null; registerTweak({ id: "packs.skipAnimation", label: "Saltar animaci\xF3n de packs", hint: "Abre los packs al instante. Ahorra varios segundos por vuelta en los ciclos de SBC diarios.", category: "packs", defaultOn: true, enable(ctx) { if (undo) return; const animCtor = getGlobal("UTPackAnimationViewController"); const presCtor = getGlobal("UTPresentationController"); if (!animCtor?.prototype) { ctx.log("packs.skipAnimation: falta UTPackAnimationViewController"); return; } const restoreRun = patchMethod( animCtor.prototype, "runAnimation", (original) => function(...args) { const result = original.apply(this, args); if (typeof this.animationTimeout === "number") { clearTimeout(this.animationTimeout); } const done = this.runCallback; if (typeof done === "function") { this.animationTimeout = window.setTimeout(() => done.call(this), 0); } return result; } ); const restorePresent = presCtor?.prototype ? patchMethod( presCtor.prototype, "present", (original) => function(animated, ...rest) { const isPack = typeof animCtor === "function" && this.presentedViewController instanceof animCtor; return original.apply(this, [ isPack ? false : animated, ...rest ]); } ) : () => void 0; undo = combine(restoreRun, restorePresent); }, disable() { undo?.(); undo = null; } }); // src/tweaks/auto-confirm.ts var SAFE_CONFIRMATIONS = [ "NEW_ITEMS_FULL", // "your unassigned pile is full" — informational "UNASSIGNED_ENTITLEMENT", // claiming items already owned "PLAYER_NOT_ELIGIBLE", // a card can't go in this slot "SEND_TO_CLUB", // moves an item to the club; nothing is destroyed "CONFIRM_COPY_SQUAD", // copies a squad "CLEAR_SQUAD" // empties the pitch; re-solving puts it back ]; function safeTitles(pm) { const out = /* @__PURE__ */ new Set(); const table = pm.Confirmations ?? {}; for (const key of SAFE_CONFIRMATIONS) { const title = table[key]?.title; if (typeof title === "string" && title) out.add(title); } return out; } var undo2 = null; registerTweak({ id: "popups.autoConfirmSafe", label: "Auto-confirmar pop-ups inofensivos", hint: "Acepta solo avisos sin consecuencias (pila llena, mandar al club, limpiar plantilla). Nunca auto-acepta descartar, borrar plantillas, enviar SBC ni gastar monedas.", category: "popups", defaultOn: true, enable(ctx) { if (undo2) return; const utils = getGlobal("utils"); const pm = utils?.PopupManager; if (!pm?.showConfirmation) { ctx.log("popups.autoConfirmSafe: falta utils.PopupManager.showConfirmation"); return; } const allowed = safeTitles(pm); ctx.log(`popups.autoConfirmSafe: ${allowed.size}/${SAFE_CONFIRMATIONS.length} t\xEDtulos resueltos`); undo2 = patchMethod( pm, "showConfirmation", (original) => function(...args) { const dto = args[0]; const onConfirm = args[2]; const title = typeof dto?.title === "string" ? dto.title : ""; if (allowed.has(title) && typeof onConfirm === "function") { ctx.log(`auto-confirmado: ${title}`); return onConfirm(); } return original.apply(this, args); } ); }, disable() { undo2?.(); undo2 = null; } }); // src/tweaks/reward-popup.ts var SETTLE_MS = 150; var BETWEEN_MS = 300; var undo3 = null; registerTweak({ id: "popups.autoDismissRewards", label: "Cerrar solo el aviso de recompensa", hint: "Despacha la pantalla de recompensa que aparece al terminar un SBC. Clave para que un ciclo largo corra sin vos.", category: "popups", defaultOn: true, enable(ctx) { if (undo3) return; const ctor = getGlobal("UTGameRewardsView"); if (!ctor?.prototype) { ctx.log("popups.autoDismissRewards: falta UTGameRewardsView"); return; } const queue = []; let timer = null; let draining = false; const drain = async () => { if (draining) return; draining = true; try { while (queue.length) { const el2 = queue.shift(); if (el2?.isConnected) { tapElement(el2, "mouse"); ctx.log("popups.autoDismissRewards: recompensa despachada"); await new Promise((r) => setTimeout(r, BETWEEN_MS)); } } } finally { draining = false; } }; const restore = patchMethod( ctor.prototype, "_generate", (original) => function(...args) { const result = original.apply(this, args); try { const btn = this._actionBtn?.getRootElement?.(); if (btn) queue.push(btn); if (timer !== null) clearTimeout(timer); timer = window.setTimeout(() => { timer = null; void drain(); }, SETTLE_MS); } catch { } return result; } ); undo3 = () => { restore(); if (timer !== null) clearTimeout(timer); timer = null; queue.length = 0; }; }, disable() { undo3?.(); undo3 = null; } }); // src/tweaks/back-to-packs.ts var FLOW_WINDOW_MS = 12e4; function findLiveNav() { const getAppMain = getGlobal("getAppMain"); const root = getAppMain?.()?.getRootViewController?.(); if (!root) return null; const seen = /* @__PURE__ */ new Set(); let found = null; const isMounted = (n) => { try { const view = n["getView"]?.call(n); return view?.getRootElement?.()?.isConnected === true; } catch { return false; } }; const walk = (node, depth) => { if (!node || depth > 12 || found || seen.has(node)) return; seen.add(node); const n = node; if (node.constructor?.name === "UTGameFlowNavigationController" && isMounted(n)) { found = node; return; } for (const key of ["currentController", "presentedViewController", "presentationController"]) { walk(n[key], depth + 1); } for (const key of ["childViewControllers", "gameflowControllers"]) { const arr = n[key]; if (Array.isArray(arr)) for (const c of arr) walk(c, depth + 1); } }; walk(root, 0); return found; } var undo4 = null; registerTweak({ id: "packs.backToPacksAfterOpen", label: "Volver a Packs al abrir uno", hint: "Despu\xE9s de abrir un pack te deja en la lista de packs, no en la Tienda. Abrir varios seguidos deja de ser un ida y vuelta.", category: "packs", defaultOn: true, enable(ctx) { if (undo4) return; const entityCtor = getGlobal("UTStorePurchasableArticleEntity"); const hubCtor = getGlobal("UTStoreHubViewController"); const packVcCtor = getGlobal("UTStorePackViewController"); if (!entityCtor?.prototype || !hubCtor?.prototype || typeof packVcCtor !== "function") { ctx.log("packs.backToPacksAfterOpen: falta alguno de los hooks de tienda"); return; } let openedAt = 0; const restoreOpen = patchMethod( entityCtor.prototype, "open", (original) => function(...args) { openedAt = Date.now(); return original.apply(this, args); } ); const restoreHub = patchMethod( hubCtor.prototype, "viewDidAppear", (original) => function(...args) { const result = original.apply(this, args); try { if (openedAt === 0 || Date.now() - openedAt > FLOW_WINDOW_MS) return result; openedAt = 0; const nav = findLiveNav(); if (!nav?.pushViewController) { ctx.log("packs.backToPacksAfterOpen: no encontr\xE9 el navigation controller"); return result; } setTimeout(() => { try { nav.pushViewController?.(new packVcCtor()); ctx.log("packs.backToPacksAfterOpen: de vuelta en Packs"); } catch (e) { ctx.log(`packs.backToPacksAfterOpen: fall\xF3 el push \u2014 ${String(e)}`); } }, 0); } catch (e) { ctx.log(`packs.backToPacksAfterOpen: ${String(e)}`); } return result; } ); undo4 = combine(restoreOpen, restoreHub); }, disable() { undo4?.(); undo4 = null; } }); // src/tweaks/redeem.ts var FACTOR = "factor"; var BY_RATING = "rating"; var BY_NON_DUPLICATE = "duplicate"; var MODE = "mode"; var MODE_MARK = "mark"; var MODE_CONFIRM = "confirm"; var CONFIRM_DELAY_MS = 400; function pickBest(items, factor) { const byRating2 = [...items].sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)); if (factor !== BY_NON_DUPLICATE) return byRating2[0]; const fresh = byRating2.filter((i) => !(typeof i.duplicateId === "number" && i.duplicateId > 0)); return (fresh.length > 0 ? fresh : byRating2)[0]; } function controllerOf(view) { const candidate = view.getController?.() ?? view._controller; const c = candidate; return typeof c?.markSelectedByItem === "function" ? c : null; } var undo5 = null; registerTweak({ id: "redeem.autoSelectPlayerPick", label: "Elegir solo en los Player Pick", hint: "Marca una carta cuando aparece un Player Pick, para que un ciclo largo no se frene ah\xED. Por defecto solo marca; en \xABMarcar y confirmar\xBB adem\xE1s cierra el pick solo.", category: "recompensas", defaultOn: false, irreversible: true, choices: [ { id: FACTOR, label: "Priorizar", values: [ { value: BY_RATING, label: "Mejor rating" }, { value: BY_NON_DUPLICATE, label: "Que no sea repetida" } ], defaultValue: BY_RATING }, { id: MODE, label: "Hasta d\xF3nde", values: [ { value: MODE_MARK, label: "Solo marcar" }, { value: MODE_CONFIRM, label: "Marcar y confirmar" } ], // Marking is already irreversible enough to warrant an opt-in; committing // is a second, larger step, so turning the tweak on must not start doing // it by itself. defaultValue: MODE_MARK } ], enable(ctx) { if (undo5) return; const ctor = getGlobal("UTPlayerPicksView"); if (!ctor?.prototype) { ctx.log("redeem.autoSelectPlayerPick: falta UTPlayerPicksView"); return; } undo5 = patchMethod( ctor.prototype, "setCarouselItems", (original) => function(items, ...rest) { const result = original.apply(this, [ items, ...rest ]); try { if (!Array.isArray(items) || items.length === 0) return result; const controller = controllerOf(this); const select = controller?.markSelectedByItem; if (!controller || !select) { ctx.log("redeem.autoSelectPlayerPick: no encontr\xE9 el controller, no toco nada"); return result; } if (controller.isAtMaxPicks?.() === true) return result; const best = pickBest(items, ctx.choice(FACTOR)); if (!best) return result; if (controller.isItemSelected?.(best) === true) return result; select.call(controller, best); ctx.log( `redeem.autoSelectPlayerPick: marcada ${best.rating ?? "?"} (de ${items.length}, criterio ${ctx.choice(FACTOR)})` ); if (ctx.choice(MODE) !== MODE_CONFIRM) return result; const confirm = controller.eConfirmSelection; if (typeof confirm !== "function") { ctx.log("redeem.autoSelectPlayerPick: no hay eConfirmSelection, queda sin confirmar"); return result; } setTimeout(() => { try { confirm.call(controller); ctx.log("redeem.autoSelectPlayerPick: pick CONFIRMADO"); } catch (e) { ctx.log(`redeem.autoSelectPlayerPick: fall\xF3 el confirm \u2014 ${String(e)}`); } }, CONFIRM_DELAY_MS); } catch (e) { ctx.log(`redeem.autoSelectPlayerPick: abortado, eleg\xED a mano \u2014 ${String(e)}`); } return result; } ); }, disable() { undo5?.(); undo5 = null; } }); // src/tweaks/unassigned-back.ts var undo6 = null; registerTweak({ id: "nav.unassignedAutoBack", label: "Salir solo de \xABsin asignar\xBB vac\xEDo", hint: "Cuando la pila de objetos sin asignar queda vac\xEDa, vuelve atr\xE1s sin que tengas que apretar.", category: "navegacion", defaultOn: true, enable(ctx) { if (undo6) return; const ctor = getGlobal("UTUnassignedItemsViewController"); if (!ctor?.prototype) { ctx.log("nav.unassignedAutoBack: falta UTUnassignedItemsViewController"); return; } undo6 = patchMethod( ctor.prototype, "renderView", (original) => function(...args) { const result = original.apply(this, args); try { const stillHasItems = (this.viewmodel?.length ?? 0) > 0; const services = getGlobal("services"); const picksPending = services?.User?.getUser?.()?.hasPlayerPicksPending === true; if (!stillHasItems && !picksPending) { ctx.log("nav.unassignedAutoBack: pila vac\xEDa, volviendo atr\xE1s"); tapBack(); } } catch { } return result; } ); }, disable() { undo6?.(); undo6 = null; } }); // src/ea/piles.ts var MOVE_BATCH = 50; var DISCARD_BATCH = 35; var BATCH_DELAY_MS = 800; var SOFT_BAN3 = /* @__PURE__ */ new Set([426, 429]); var CLUB_PILE_FALLBACK = 7; function defaultEnv() { return { services: getGlobal("services"), repositories: getGlobal("repositories"), itemPile: getGlobal("ItemPile") }; } function pileId(env, key) { return (env.itemPile ?? getGlobal("ItemPile"))?.[key]; } function isDuplicate(item) { return typeof item?.isDuplicate === "function" && item.isDuplicate() === true; } function isTradeable(item) { return typeof item.isTradeable === "function" ? item.isTradeable() === true : true; } function chunk(arr, size) { const out = []; for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); return out; } async function runBatches(targets, size, op) { const batches = chunk(targets, size); let done = 0; for (const [i, batch] of batches.entries()) { if (i > 0) await delay(BATCH_DELAY_MS); let res; try { res = await op(batch); } catch (e) { return { done, softBanned: false, stoppedEarly: true, failNote: String(e?.message ?? e) }; } const status = Number(res.status); if (SOFT_BAN3.has(status)) { return { done, softBanned: true, stoppedEarly: true }; } const failed = res.success === false || Number.isFinite(status) && status >= 400; if (failed) { return { done, softBanned: false, stoppedEarly: true, failStatus: Number.isFinite(status) ? status : void 0 }; } done += batch.length; } return { done, softBanned: false, stoppedEarly: false }; } function summarize(run, targeted, pastParticiple) { const failed = targeted - run.done; let reason; if (run.softBanned) { reason = `EA respondi\xF3 con un c\xF3digo de soft-ban (426/429). Fren\xE9: ${run.done} ${pastParticiple}, ${failed} sin procesar.`; } else if (run.stoppedEarly) { const why = run.failStatus != null ? ` (status ${run.failStatus})` : run.failNote ? ` (${run.failNote})` : ""; reason = `Un lote fall\xF3${why}. Fren\xE9: ${run.done} ${pastParticiple}, ${failed} sin procesar.`; } return { moved: run.done, failed, softBanned: run.softBanned, stoppedEarly: run.stoppedEarly, reason }; } var nothingToDo = (reason) => ({ moved: 0, failed: 0, softBanned: false, stoppedEarly: false, reason }); var cannotStart = (reason) => ({ moved: 0, failed: 0, softBanned: false, stoppedEarly: true, reason }); async function sendToClub(items, env = defaultEnv()) { const move = env.services?.Item?.move; if (typeof move !== "function") { return cannotStart("services.Item.move no est\xE1 disponible."); } const club = pileId(env, "CLUB") ?? CLUB_PILE_FALLBACK; const targets = items.filter((it) => it && it.pile !== club && !isDuplicate(it)); if (targets.length === 0) { return nothingToDo( "No hab\xEDa objetos para enviar al club (ya estaban en el club o eran repetidos)." ); } const run = await runBatches( targets, MOVE_BATCH, (batch) => toPromise(move(batch, club)) ); return summarize(run, targets.length, "enviados al club"); } async function parkDuplicates(items, env, dest) { const move = env.services?.Item?.move; if (typeof move !== "function") { return cannotStart("services.Item.move no est\xE1 disponible."); } const pile = pileId(env, dest.pileKey); if (pile == null) { return cannotStart( `No pude resolver ItemPile.${dest.pileKey} \u2014 no muevo nada a ciegas.` ); } const dupes = items.filter((it) => it && isDuplicate(it) && dest.keep(it)); if (dupes.length === 0) { return nothingToDo(`No hab\xEDa repetidos ${dest.emptyNote} para mover.`); } const room = pileRoom(env, pile); if (room == null) { return cannotStart( `No pude leer cu\xE1ntos espacios quedan en ${dest.placeLabel} (repositories.Item no disponible). No mov\xED nada.` ); } if (room <= 0) { return cannotStart( `Sin espacio en ${dest.placeLabel}. No mov\xED ninguno de los ${dupes.length} repetidos.` ); } const fits = dupes.slice(0, room); const shortfall = dupes.length - fits.length; const run = await runBatches( fits, MOVE_BATCH, (batch) => toPromise(move(batch, pile, true)) ); const result = summarize(run, fits.length, dest.movedLabel); if (shortfall > 0 && !run.softBanned) { const tail = `${shortfall} repetido(s) no entraron: solo hab\xEDa ${room} espacio(s) libre(s) en ${dest.placeLabel}.`; result.reason = result.reason ? `${result.reason} ${tail}` : tail; result.failed += shortfall; result.stoppedEarly = true; } return result; } async function duplicatesToTransferList(items, env = defaultEnv()) { return parkDuplicates(items, env, { pileKey: "TRANSFER", keep: isTradeable, emptyNote: "transferibles", movedLabel: "movidos a transferibles", placeLabel: "la lista de transferibles" }); } async function untradeableDuplicatesToStorage(items, env = defaultEnv()) { return parkDuplicates(items, env, { pileKey: "STORAGE", keep: (it) => !isTradeable(it), emptyNote: "intransferibles", movedLabel: "movidos al almac\xE9n de SBC", placeLabel: "el almac\xE9n de SBC" }); } async function quickSell(items, env = defaultEnv()) { const discard = env.services?.Item?.discard; if (typeof discard !== "function") { return cannotStart("services.Item.discard no est\xE1 disponible."); } const targets = items.filter((it) => Boolean(it)); if (targets.length === 0) { return nothingToDo("No hab\xEDa objetos para vender."); } const run = await runBatches( targets, DISCARD_BATCH, (batch) => toPromise(discard(batch)) ); return summarize(run, targets.length, "vendidos"); } function pileRoom(env, pile) { const repo = (env.repositories ?? getGlobal("repositories"))?.Item; const size = repo?.getPileSize?.(pile); const used = repo?.numItemsInCache?.(pile); if (typeof size !== "number" || typeof used !== "number") return null; if (!Number.isFinite(size) || !Number.isFinite(used)) return null; return Math.max(0, size - used); } // src/tweaks/unassigned-actions.ts var ROW_CLASS = "fut-bulk-actions"; var undo7 = null; registerTweak({ id: "unassigned.bulkActions", label: "Acciones masivas en \xABsin asignar\xBB", hint: "Agrega \xABAl club\xBB y \xABGuardar repetidos\xBB (transferibles a la lista, intransferibles al almacen de SBC). \xABAl club\xBB verificado en vivo; \xABGuardar repetidos\xBB todavia no.", category: "navegacion", defaultOn: false, unverified: true, enable(ctx) { if (undo7) return; const ctor = getGlobal("UTUnassignedItemsView"); if (!ctor?.prototype) { ctx.log("unassigned.bulkActions: falta UTUnassignedItemsView"); return; } undo7 = patchMethod( ctor.prototype, "renderSection", (original) => function(...args) { const result = original.apply(this, args); try { mountRow(this, ctx); } catch (e) { ctx.log(`unassigned.bulkActions: no pude montar la barra \u2014 ${String(e)}`); } return result; } ); }, disable() { undo7?.(); undo7 = null; unmountOwned("bulkActions"); } }); function getActionRow(root) { const existing = root.querySelector(`.${ROW_CLASS}`); if (existing) return existing; const row = document.createElement("div"); row.className = ROW_CLASS; row.style.cssText = "display:flex;gap:8px;margin-left:auto;padding:6px 8px;flex-wrap:wrap;"; root.prepend(row); return row; } function unmountOwned(owner) { if (typeof document === "undefined") return; document.querySelectorAll(`.${ROW_CLASS} [data-fut-owner="${owner}"]`).forEach((el2) => el2.remove()); document.querySelectorAll(`.${ROW_CLASS}`).forEach((row) => { if (row.childElementCount === 0) row.remove(); }); } function mountRow(view, ctx) { const root = view?.getRootElement?.(); if (!(root instanceof HTMLElement)) { ctx.log("unassigned.bulkActions: la vista no expone getRootElement"); return; } const row = getActionRow(root); if (row.querySelector('[data-fut-owner="bulkActions"]')) return; const toClub = makeBulkButton( "Al club", () => runBulkJob(async () => { const items = await readUnassignedItems(ctx); if (items.length === 0) { bulkToast("No hay objetos sin asignar."); return; } bulkToast(describeOutcome("Al club", await sendToClub(items))); }, ctx), ctx, view ); const dupes = makeBulkButton( "Guardar repetidos", () => runBulkJob(async () => { const items = await readUnassignedItems(ctx); if (items.length === 0) { bulkToast("No hay objetos sin asignar."); return; } const sold = await duplicatesToTransferList(items); const stored = await untradeableDuplicatesToStorage(items); bulkToast( [ describeOutcome("Transferibles", sold), describeOutcome("Almac\xE9n SBC", stored) ].join(" \xB7 ") ); }, ctx), ctx, view ); for (const b of [toClub, dupes]) { b.el.dataset["futOwner"] = "bulkActions"; row.append(b.el); } } function makeBulkButton(label, onClick, ctx, host) { const Ctrl = getGlobal("UTStandardButtonControl"); if (typeof Ctrl === "function" && typeof host?.addSubview === "function") { try { const b = new Ctrl(); b.init?.(); b.setText?.(label); const eventType = getGlobal("EventType")?.["TAP"] ?? "tap"; b.addTarget?.(b, onClick, eventType); host.addSubview(b); const el2 = b.getRootElement?.(); if (el2 instanceof HTMLElement) { el2.classList.add("primary", "mini"); return { el: el2, setLabel: (s) => b.setText?.(s) }; } } catch (e) { ctx.log( `unassigned.bulkActions: UTStandardButtonControl fall\xF3, uso