// ==UserScript== // @name Doitsburger’s FF Target Finder + Start Attack Overlay // @namespace http://tampermonkey.net/ // @version 3.7 // @description Quick target finder + START FIGHT overlay (toggleable) for attack pages // @author FFScouter // @match https://www.torn.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect ffscouter.com // @connect api.torn.com // @updateURL https://raw.githubusercontent.com/doitsburger/doits-scripts/329e49ba16b2a2a1ddba7e8ac9b2f2a26f601956/ff-target-finder/DOITSBURGER's%20FF%20TARGET%20FINDER // @downloadURL https://raw.githubusercontent.com/doitsburger/doits-scripts/329e49ba16b2a2a1ddba7e8ac9b2f2a26f601956/ff-target-finder/DOITSBURGER's%20FF%20TARGET%20FINDER // ==/UserScript== (function() { 'use strict'; // ==================== FF SCOUTER CORE ==================== // PDA API Key placeholder const PDA_API_KEY = "###PDA-APIKEY###"; // Faction medical page URL const FACTION_MEDICAL_URL = "https://www.torn.com/factions.php?step=your&type=1#/tab=armoury&start=0&sub=medical"; // Detect mobile/touch device const isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); // Default configuration (with new default positions and overlay enabled) const defaultConfig = { apiKey: '', target: { minFF: 1.50, maxFF: 3.00 }, factionlessOnly: false, inactiveOnly: true, openInNewTab: true, verifyStatus: false, hasUsedTap: false, hasUsedHold: false, buttonPosition: isMobile ? { right: 2, top: 218, isPercent: false } // mobile: 2px from right, 218px from bottom : { right: 0, top: 27, isPercent: true }, // desktop: 0% from right, 27% from top buttonVisible: true, fightOverlayEnabled: true // <-- NEW: toggle for START FIGHT overlay }; // Load or initialize configuration (migrate old format) function getConfig() { const saved = GM_getValue('ffscouter_config'); if (saved) { try { const config = JSON.parse(saved); // Migrate from old version (with easy/good) to new single target if (config.easy || config.good) { const newConfig = { apiKey: config.apiKey || '', target: { minFF: config.easy?.minFF ?? 1.50, maxFF: config.good?.maxFF ?? 3.00 }, factionlessOnly: config.factionlessOnly ?? false, inactiveOnly: config.inactiveOnly ?? true, openInNewTab: config.openInNewTab ?? true, verifyStatus: config.verifyStatus ?? false, hasUsedTap: config.hasUsedTap ?? false, hasUsedHold: config.hasUsedHold ?? false, buttonPosition: config.buttonPosition || defaultConfig.buttonPosition, buttonVisible: config.buttonVisible ?? true, fightOverlayEnabled: config.fightOverlayEnabled ?? true // migrate with default true }; saveConfig(newConfig); return newConfig; } // Ensure target property exists if (!config.target) { config.target = { minFF: 1.50, maxFF: 3.00 }; } // Ensure fightOverlayEnabled exists (for older configs) if (config.fightOverlayEnabled === undefined) { config.fightOverlayEnabled = true; } return config; } catch (e) { return defaultConfig; } } return defaultConfig; } function saveConfig(config) { GM_setValue('ffscouter_config', JSON.stringify(config)); } function getApiKey() { const config = getConfig(); if (config.apiKey && /^[a-zA-Z0-9]{16}$/.test(config.apiKey)) { return { key: config.apiKey, source: 'manual' }; } return { key: PDA_API_KEY, source: 'pda' }; } // ===== API-BASED HOSPITAL CHECK (with DOM fallback) ===== async function checkUserInHospital() { const { key } = getApiKey(); // If we have a valid API key, try the Torn API first if (key) { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.torn.com/v2/user/?selections=basic&key=${key}`, timeout: 5000, onload: resolve, onerror: reject, ontimeout: reject }); }); const data = JSON.parse(response.responseText); if (!data.error) { const state = data.status?.state; console.log('FF Scouter: User status via API:', state); return state === 'Hospital'; } else { console.warn('FF Scouter: API error, falling back to DOM check:', data.error); } } catch (e) { console.warn('FF Scouter: API request failed, falling back to DOM check:', e); } } // Fallback: DOM-based check with visibility filter const hospitalElements = document.querySelectorAll('a[href="/index.php"][aria-label^="Hospital:"]'); for (const el of hospitalElements) { if (el.offsetParent !== null) { // visible element console.log('FF Scouter: Visible hospital element found (fallback) – user is hospitalized'); return true; } } console.log('FF Scouter: No visible hospital element – user is not hospitalized'); return false; } // Torn-style CSS – updated with new glass button UI GM_addStyle(` /* ===== SETTINGS POPUP (unchanged) ===== */ .ffs-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 999999; display: flex; justify-content: center; align-items: center; font-family: Arial, Helvetica, sans-serif; backdrop-filter: blur(3px); } .ffs-popup { background: linear-gradient(180deg, #2d2d2d 0%, #1a1a1a 100%); border-radius: 8px; width: 380px; max-width: 95vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255,255,255,0.1); color: #ddd; } .ffs-popup::-webkit-scrollbar { width: 8px; } .ffs-popup::-webkit-scrollbar-track { background: #1a1a1a; } .ffs-popup::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; } .ffs-header { background: linear-gradient(180deg, #3d3d3d 0%, #2a2a2a 100%); padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; position: sticky; top: 0; z-index: 10; } .ffs-header-title { display: flex; align-items: center; gap: 10px; } .ffs-header-title svg { width: 22px; height: 22px; fill: #6ac46a; } .ffs-header h2 { margin: 0!important; color: #fff; font-size: 15px; font-weight: 600; letter-spacing: 0.5px; } .ffs-close { background: rgba(255,255,255,0.1); border: none; color: #888; font-size: 18px; cursor: pointer; padding: 0; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .ffs-close:hover { background: rgba(255,100,100,0.2); color: #f66; } .ffs-content { padding: 16px; } /* API Status Banner */ .ffs-api-banner { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; cursor: pointer; transition: all 0.2s; } .ffs-api-banner svg { width: 20px; height: 20px; flex-shrink: 0; } .ffs-api-banner-text { flex: 1; } .ffs-api-banner-title { font-size: 12px; font-weight: 600; margin-bottom: 2px; } .ffs-api-banner-sub { font-size: 10px; opacity: 0.7; } .ffs-api-banner.manual { background: linear-gradient(135deg, rgba(106, 196, 106, 0.15) 0%, rgba(80, 150, 80, 0.1) 100%); border: 1px solid rgba(106, 196, 106, 0.3); } .ffs-api-banner.manual svg { fill: #6c6; } .ffs-api-banner.manual:hover { background: linear-gradient(135deg, rgba(106, 196, 106, 0.25) 0%, rgba(80, 150, 80, 0.15) 100%); } .ffs-api-banner.auto { background: linear-gradient(135deg, rgba(106, 150, 196, 0.15) 0%, rgba(80, 120, 150, 0.1) 100%); border: 1px solid rgba(106, 150, 196, 0.3); } .ffs-api-banner.auto svg { fill: #6af; } .ffs-api-banner.auto:hover { background: linear-gradient(135deg, rgba(106, 150, 196, 0.25) 0%, rgba(80, 120, 150, 0.15) 100%); } .ffs-api-badge { background: rgba(255,255,255,0.1); padding: 3px 8px; border-radius: 10px; font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .ffs-api-banner.auto .ffs-api-badge { background: rgba(106, 170, 255, 0.2); color: #6af; } .ffs-api-banner.manual .ffs-api-badge { background: rgba(106, 196, 106, 0.2); color: #6c6; } /* Target Card (single) */ .ffs-target-card { background: rgba(0,0,0,0.2); border: 1px solid #333; border-radius: 6px; margin-bottom: 12px; overflow: hidden; } .ffs-card-header { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: rgba(255,255,255,0.03); border-bottom: 1px solid #333; } .ffs-card-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 16px; background: linear-gradient(135deg, #4a7c4a 0%, #3a5c3a 100%); } .ffs-card-title { flex: 1; } .ffs-card-title h3 { margin: 0!important; font-size: 13px; font-weight: 600; color: #eee; } .ffs-card-title span { font-size: 10px; color: #777; } .ffs-card-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; } .ffs-input-row { display: flex; align-items: center; gap: 10px; } .ffs-input-label { font-size: 11px; color: #999; width: 55px; flex-shrink: 0; } .ffs-input-group { display: flex; align-items: center; gap: 6px; flex: 1; } .ffs-input { flex: 1; padding: 8px 10px; border: 1px solid #444; border-radius: 4px; background: #252525; color: #fff; font-size: 13px; text-align: center; transition: all 0.2s; } .ffs-input:focus { outline: none; border-color: #6ac46a; background: #2a2a2a; box-shadow: 0 0 0 2px rgba(106, 196, 106, 0.1); } .ffs-input-sep { color: #555; font-size: 11px; } /* Target Count Badge */ .ffs-card-count { padding: 6px 14px; background: rgba(0,0,0,0.2); border-top: 1px solid #333; font-size: 11px; color: #888; display: flex; align-items: center; gap: 6px; } .ffs-card-count.loading { color: #666; } .ffs-card-count.error { color: #c66; } .ffs-card-count.warning { color: #c96; } .ffs-card-count.good { color: #6a6; } .ffs-count-num { font-weight: 600; color: #aaa; } .ffs-card-count.warning .ffs-count-num { color: #fc6; } .ffs-card-count.good .ffs-count-num { color: #6c6; } .ffs-card-count.error .ffs-count-num { color: #c66; } .ffs-count-spinner { width: 12px; height: 12px; border: 2px solid #444; border-top-color: #888; border-radius: 50%; animation: ffs-spin 0.8s linear infinite; } @keyframes ffs-spin { to { transform: rotate(360deg); } } /* Options Section */ .ffs-options { background: rgba(0,0,0,0.2); border: 1px solid #333; border-radius: 6px; overflow: hidden; margin-bottom: 16px; } .ffs-options-header { padding: 10px 14px; background: rgba(255,255,255,0.03); border-bottom: 1px solid #333; font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; } .ffs-option { display: flex; align-items: center; padding: 12px 14px; border-bottom: 1px solid #2a2a2a; cursor: pointer; transition: background 0.15s; } .ffs-option:last-child { border-bottom: none; } .ffs-option:hover { background: rgba(255,255,255,0.02); } .ffs-option-text { flex: 1; margin-left: 12px; } .ffs-option-title { font-size: 12px; color: #ddd; } .ffs-option-desc { font-size: 10px; color: #666; margin-top: 2px; } .ffs-option-warn { color: #c96; } /* Custom Toggle Switch */ .ffs-toggle { position: relative; width: 40px; height: 22px; flex-shrink: 0; } .ffs-toggle input { opacity: 0; width: 0; height: 0; } .ffs-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #3a3a3a; border-radius: 22px; transition: 0.2s; border: 1px solid #444; } .ffs-toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: #888; border-radius: 50%; transition: 0.2s; } .ffs-toggle input:checked + .ffs-toggle-slider { background: #4a7c4a; border-color: #5a5; } .ffs-toggle input:checked + .ffs-toggle-slider:before { transform: translateX(18px); background: #fff; } /* Footer */ .ffs-footer { display: flex; gap: 10px; padding: 16px; background: rgba(0,0,0,0.2); border-top: 1px solid #333; } .ffs-btn { flex: 1; padding: 12px 20px; border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .ffs-btn-primary { background: linear-gradient(180deg, #5a9 0%, #4a8 100%); color: #fff; box-shadow: 0 2px 8px rgba(90, 170, 150, 0.3); } .ffs-btn-primary:hover { background: linear-gradient(180deg, #6ba 0%, #5a9 100%); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(90, 170, 150, 0.4); } .ffs-btn-secondary { background: #333; color: #aaa; border: 1px solid #444; } .ffs-btn-secondary:hover { background: #3a3a3a; color: #ddd; } /* Keyboard hints */ .ffs-kbd-hints { display: ${isMobile ? 'none' : 'flex'}; justify-content: center; gap: 16px; padding: 12px; background: rgba(0,0,0,0.3); border-top: 1px solid #333; } .ffs-kbd-hint { display: flex; align-items: center; gap: 6px; font-size: 10px; color: #666; } .ffs-kbd { background: #333; padding: 3px 6px; border-radius: 3px; font-family: monospace; font-size: 10px; color: #999; border: 1px solid #444; } /* ===== TOAST ===== */ .ffs-toast { position: fixed; bottom: ${isMobile ? '80px' : '20px'}; right: 20px; left: ${isMobile ? '20px' : 'auto'}; padding: 14px 18px; border-radius: 8px; color: #fff; font-size: 13px; font-weight: 500; z-index: 9999999; animation: ffs-slide-in 0.3s ease; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); max-width: ${isMobile ? 'none' : '320px'}; } .ffs-toast-success { background: linear-gradient(135deg, #4a7c4a 0%, #3a5c3a 100%); } .ffs-toast-error { background: linear-gradient(135deg, #7c4a4a 0%, #5c3a3a 100%); } .ffs-toast-info { background: linear-gradient(135deg, #4a5c7c 0%, #3a4a5c 100%); } .ffs-toast-warning { background: linear-gradient(135deg, #7c6a4a 0%, #5c4a3a 100%); } @keyframes ffs-slide-in { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* ===== NEW GLASS BUTTON UI ===== */ .ffs-fab-container { position: fixed; z-index: 10000; display: flex; flex-direction: column; align-items: center; gap: 6px; } .ffs-fab-container.hidden { display: none !important; } .ffs-fab { width: 28px; height: 40px; border-radius: 25px; background: rgba(15,15,18,0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); box-shadow: 0 4px 12px rgba(0,0,0,0.5); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.2s, background 0.2s; outline: none; font-size: 18px; font-weight: bold; position: relative; user-select: none; -webkit-tap-highlight-color: transparent; } /* Hover effect for desktop */ .ffs-fab:hover { transform: translateY(-2px); background: rgba(25,25,28,0.9); } .ffs-fab:active { transform: scale(0.95); } .ffs-fab svg.ffs-fab-progress { position: absolute; top: -4px; left: -4px; width: calc(100% + 8px); height: calc(100% + 8px); transform: rotate(-90deg); opacity: 0; transition: opacity 0.1s; } .ffs-fab svg.ffs-fab-progress circle { fill: none; stroke: #fff; stroke-width: 3; stroke-dasharray: 160; stroke-dashoffset: 160; stroke-linecap: round; } .ffs-fab.pressing svg.ffs-fab-progress { opacity: 1; } .ffs-fab.pressing svg.ffs-fab-progress circle { animation: ffs-progress-fill 0.5s ease-out forwards; } @keyframes ffs-progress-fill { to { stroke-dashoffset: 0; } } .ffs-fab-hint { position: absolute; right: calc(100% + 12px); top: 50%; transform: translateY(-50%); background: #222; color: #ddd; padding: 10px 14px; border-radius: 8px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 15px rgba(0,0,0,0.5); opacity: 0; pointer-events: none; transition: opacity 0.3s; border: 1px solid #444; } .ffs-fab-hint::after { content: ''; position: absolute; right: -6px; top: 50%; transform: translateY(-50%); border: 6px solid transparent; border-left-color: #444; } .ffs-fab-hint.show { opacity: 1; } .ffs-fab-hint.animate { animation: ffs-hint-bounce 0.5s ease; } @keyframes ffs-hint-bounce { 0%, 100% { transform: translateY(-50%) translateX(0); } 50% { transform: translateY(-50%) translateX(-5px); } } .ffs-hint-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; } .ffs-hint-action { background: #333; padding: 2px 8px; border-radius: 4px; font-size: 10px; color: #999; min-width: 40px; text-align: center; } .ffs-hint-result { color: #ddd; } .ffs-hint-result.good { color: #fc6; } .ffs-hint-result.menu { color: #aaa; } /* ===== REPOSITION MODE ===== */ .ffs-fab-container.repositioning { cursor: grab; z-index: 9999999; } .ffs-fab-container.repositioning.dragging { cursor: grabbing; } .ffs-fab-container.repositioning .ffs-fab { animation: ffs-pulse 1.5s ease-in-out infinite; border-color: #6af; background: rgba(15,15,18,0.95); } @keyframes ffs-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(102, 170, 255, 0.5), 0 4px 15px rgba(0, 0, 0, 0.4); } 50% { box-shadow: 0 0 0 10px rgba(102, 170, 255, 0), 0 4px 15px rgba(0, 0, 0, 0.4); } } .ffs-reposition-bar { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1c1c1c; border: 1px solid #444; border-radius: 12px; padding: 12px 20px; display: flex; align-items: center; gap: 16px; z-index: 99999999; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.6); font-family: Arial, Helvetica, sans-serif; } .ffs-reposition-text { color: #ddd; font-size: 13px; } .ffs-reposition-text span { color: #6af; font-weight: 600; } .ffs-reposition-btns { display: flex; gap: 8px; } .ffs-reposition-btn { padding: 8px 16px; border: none; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .ffs-reposition-btn.confirm { background: linear-gradient(180deg, #5a9 0%, #4a8 100%); color: #fff; } .ffs-reposition-btn.confirm:hover { background: linear-gradient(180deg, #6ba 0%, #5a9 100%); } .ffs-reposition-btn.cancel { background: #333; color: #aaa; border: 1px solid #444; } .ffs-reposition-btn.cancel:hover { background: #3a3a3a; color: #ddd; } .ffs-reposition-btn.reset { background: transparent; color: #888; padding: 8px 12px; } .ffs-reposition-btn.reset:hover { color: #c66; } /* ===== TORN SETTINGS MENU TOGGLE ===== */ .ffs-torn-toggle .icon-wrapper svg { fill: #6ac46a; } `); // Toast notification function showToast(message, type = 'info', duration = 3000) { const existing = document.querySelector('.ffs-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = `ffs-toast ffs-toast-${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'ffs-slide-in 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, duration); } // API Key prompt function promptApiKey() { const config = getConfig(); const currentKey = config.apiKey || ''; const newKey = prompt( "Enter your FF Scouter API Key (16 characters):\n\nIf you don't have one: \n Get an api key from: \n https://www.torn.com/preferences.php#tab=api \n then register it in: ffscouter.com\n\nTorn PDA users: Leave empty to use automatic key", currentKey ); if (newKey === null) return; const trimmedKey = newKey.trim(); if (trimmedKey === '') { config.apiKey = ''; saveConfig(config); showToast('Using automatic API key', 'success'); return; } if (!/^[a-zA-Z0-9]{16}$/.test(trimmedKey)) { showToast('Invalid key format (must be 16 characters)', 'error'); return; } config.apiKey = trimmedKey; saveConfig(config); showToast('API key saved!', 'success'); } function getRandomElement(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function shuffleArray(arr) { const shuffled = [...arr]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } // Check target status via Torn API function checkTargetStatus(playerId, apiKey) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.torn.com/v2/user/${playerId}/basic?striptags=true&key=${apiKey}`, timeout: 5000, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.error) { resolve({ success: false, error: data.error.error || 'API error' }); return; } const state = data.profile?.status?.state; const isOkay = state === 'Okay'; resolve({ success: true, isOkay, state, status: data.profile?.status }); } catch (e) { resolve({ success: false, error: 'Parse error' }); } }, onerror: function() { resolve({ success: false, error: 'Request failed' }); }, ontimeout: function() { resolve({ success: false, error: 'Timeout' }); } }); }); } // Fetch target count for preview function fetchTargetCount(settings, inactiveOnly, factionlessOnly) { return new Promise((resolve) => { const { key } = getApiKey(); const params = new URLSearchParams({ key: key, minff: settings.minFF, maxff: settings.maxFF, inactiveonly: inactiveOnly ? 1 : 0, factionless: factionlessOnly ? 1 : 0, limit: 50 }); const url = `https://ffscouter.com/api/v1/get-targets?${params.toString()}`; GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.error) { resolve({ success: false, error: data.error }); return; } const count = data.targets?.length || 0; resolve({ success: true, count }); } catch (e) { resolve({ success: false, error: 'Parse error' }); } }, onerror: function() { resolve({ success: false, error: 'Request failed' }); }, ontimeout: function() { resolve({ success: false, error: 'Timeout' }); } }); }); } // Find a valid target (with optional status verification) async function findValidTarget(targets, apiKey, verifyStatus) { if (!verifyStatus) { return { target: getRandomElement(targets), checked: 0 }; } const shuffled = shuffleArray(targets); let checked = 0; for (const target of shuffled) { checked++; const status = await checkTargetStatus(target.player_id, apiKey); if (!status.success) { console.warn('FFS: Status check failed:', status.error); return { target, checked, verifyFailed: true }; } if (status.isOkay) { return { target, checked }; } console.log(`FFS: Target ${target.name} is ${status.state}, trying next...`); } return { target: null, checked }; } // API call function (single target type) – with hospital check async function fetchTarget() { // Check if user is in hospital (API first, then DOM fallback) const inHospital = await checkUserInHospital(); if (inHospital) { showToast('You are in hospital – redirecting to faction medical', 'warning', 3000); setTimeout(() => { window.location.href = FACTION_MEDICAL_URL; }, 800); // Adjust delay as needed (800ms = 0.8 seconds) return; } const { key, source } = getApiKey(); const config = getConfig(); const settings = config.target; const params = new URLSearchParams({ key: key, minff: settings.minFF, maxff: settings.maxFF, inactiveonly: config.inactiveOnly ? 1 : 0, factionless: config.factionlessOnly ? 1 : 0, limit: 50 }); const url = `https://ffscouter.com/api/v1/get-targets?${params.toString()}`; showToast('Finding target...', 'info'); GM_xmlhttpRequest({ method: 'GET', url: url, onload: async function(response) { try { const data = JSON.parse(response.responseText); if (data.error) { if (source === 'pda' && (data.error.includes('key') || data.error.includes('API') || data.error.includes('auth'))) { showToast('API key required - not using Torn PDA?', 'error'); setTimeout(() => promptApiKey(), 1000); } else { showToast(`Error: ${data.error}`, 'error'); } return; } if (!data.targets || data.targets.length === 0) { showToast('No targets found with current filters', 'error'); return; } const targetCount = data.targets.length; if (targetCount < 10) { showToast(`Warning: Only ${targetCount} target${targetCount === 1 ? '' : 's'} available. Consider adjusting filters.`, 'warning'); await new Promise(r => setTimeout(r, 1500)); } const verifyStatus = config.verifyStatus; if (verifyStatus) { showToast('Verifying target status...', 'info'); } const result = await findValidTarget(data.targets, key, verifyStatus); if (!result.target) { showToast(`No available targets found (checked ${result.checked})`, 'error'); return; } if (result.verifyFailed) { showToast('Status check failed, using unverified target', 'warning'); await new Promise(r => setTimeout(r, 1000)); } const target = result.target; const attackUrl = `https://www.torn.com/loader.php?sid=attack&user2ID=${target.player_id}`; let message = `${target.name} [${target.player_id}] • Lvl ${target.level} • FF ${target.fair_fight.toFixed(2)}`; if (verifyStatus && result.checked > 1) { message += ` (${result.checked} checked)`; } showToast(message, 'success'); if (config.openInNewTab) { window.open(attackUrl, '_blank'); } else { window.location.href = attackUrl; } } catch (e) { showToast('Failed to parse response', 'error'); console.error('FFS Error:', e); } }, onerror: function(error) { showToast('Request failed - check connection', 'error'); console.error('FFS Error:', error); } }); } // Reposition mode (updated with 2px padding and save fix) let isRepositioning = false; let originalPosition = null; function enterRepositionMode() { if (isRepositioning) return; isRepositioning = true; const container = document.querySelector('.ffs-fab-container'); const config = getConfig(); originalPosition = { ...config.buttonPosition }; container.classList.add('repositioning'); const bar = document.createElement('div'); bar.className = 'ffs-reposition-bar'; bar.innerHTML = `