// ==UserScript== // @name Neopets - SSW Sniper // @namespace http://tampermonkey.net/ // @version 4.3.3 // @description Batch-search the Super Shop Wizard with a professional UI: tabs, progress bar, quantity badges, failed-search log, automatic SSW cooldown handling, batch mode and single-item mode with random delay (in ms), full settings panel stored via GM_set/GM_get, pew-pew style sound alert and animated highlight for available items, and a reset-to-defaults button so you never have to edit the code by hand again. // @author You // @match *://www.neopets.com/* // @exclude https://www.neopets.com/neomessages.phtml* // @exclude https://www.neopets.com/neomail_block_check.phtml* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect neopets.com // @noframes // @downloadURL https://www.scriptneo.com/scripts/download.php?id=22 // @updateURL https://www.scriptneo.com/scripts/download.php?id=22 // ==/UserScript== (function () { 'use strict'; /*** SETTINGS DEFAULTS ***/ const SETTINGS_KEY = 'sswBatchSearchSettings'; const DEFAULT_DISCOUNT_THRESHOLD = 0.60; // 60 percent of second price const DEFAULT_SHOW_SINGLES = true; // accept when only one listing is returned const DEFAULT_BATCH_SIZE = 50; // concurrent searches per batch const DEFAULT_MAX_RETRIES_PER_ITEM = 1; // network or parse retries const DEFAULT_COOLDOWN_FALLBACK_MIN = 30; // if SSW does not provide minutes const DEFAULT_BATCH_DELAY_SECONDS = 30; // delay between batches in seconds const DEFAULT_SINGLE_DELAY_MIN_MS = 100; // min ms for single mode delay const DEFAULT_SINGLE_DELAY_MAX_MS = 2000; // default max ms for single mode delay const DEFAULT_ICON_SIZE = 50; // launcher icon size in pixels const DEFAULT_SOUND_ALERT_ENABLED = true; // play sound when a new item is available const DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED = true; // animated highlight for available item names /*** STATE (MUTABLE SETTINGS) ***/ let discountThreshold = DEFAULT_DISCOUNT_THRESHOLD; let showSingles = DEFAULT_SHOW_SINGLES; let batchSize = DEFAULT_BATCH_SIZE; let maxRetriesPerItem = DEFAULT_MAX_RETRIES_PER_ITEM; let cooldownFallbackMin = DEFAULT_COOLDOWN_FALLBACK_MIN; let batchDelaySeconds = DEFAULT_BATCH_DELAY_SECONDS; let singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS; let singleDelayMaxMs = DEFAULT_SINGLE_DELAY_MAX_MS; let iconSize = DEFAULT_ICON_SIZE; let soundAlertEnabled = DEFAULT_SOUND_ALERT_ENABLED; let fancyAvailableStyleEnabled = DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED; let searchMode = 'batch'; // 'batch' or 'single' /*** RUN STATE ***/ let isPaused = false; let banCooldownTimeout = null; let itemsQueued = 0; let itemsSearchedCount = 0; const failedItemsRaw = []; // final failures (for counts) const availableCounts = new Map(); // itemName -> count of appearances in INPUT const availableMeta = new Map(); // itemName -> { prices, linksHTML, qty } /*** PERSISTENCE ***/ function loadSettings() { try { if (typeof GM_getValue !== 'function') return; const raw = GM_getValue(SETTINGS_KEY, null); if (!raw) return; const data = JSON.parse(raw); if (typeof data.discountThreshold === 'number') { discountThreshold = data.discountThreshold; } if (typeof data.showSingles === 'boolean') { showSingles = data.showSingles; } if (typeof data.batchSize === 'number') { batchSize = data.batchSize; } if (typeof data.maxRetriesPerItem === 'number') { maxRetriesPerItem = data.maxRetriesPerItem; } if (typeof data.cooldownFallbackMin === 'number') { cooldownFallbackMin = data.cooldownFallbackMin; } if (typeof data.batchDelaySeconds === 'number') { batchDelaySeconds = data.batchDelaySeconds; } if (typeof data.singleDelayMinMs === 'number') { singleDelayMinMs = data.singleDelayMinMs; } else if (typeof data.singleDelayMinSeconds === 'number') { singleDelayMinMs = data.singleDelayMinSeconds * 1000; } if (typeof data.singleDelayMaxMs === 'number') { singleDelayMaxMs = data.singleDelayMaxMs; } else if (typeof data.singleDelayMaxSeconds === 'number') { singleDelayMaxMs = data.singleDelayMaxSeconds * 1000; } if (typeof data.iconSize === 'number') { iconSize = data.iconSize; } if (data.searchMode === 'batch' || data.searchMode === 'single') { searchMode = data.searchMode; } if (typeof data.soundAlertEnabled === 'boolean') { soundAlertEnabled = data.soundAlertEnabled; } if (typeof data.fancyAvailableStyleEnabled === 'boolean') { fancyAvailableStyleEnabled = data.fancyAvailableStyleEnabled; } } catch (e) { // ignore bad settings object } } function saveSettings() { try { if (typeof GM_setValue !== 'function') return; const data = { discountThreshold, showSingles, batchSize, maxRetriesPerItem, cooldownFallbackMin, batchDelaySeconds, singleDelayMinMs, singleDelayMaxMs, iconSize, searchMode, soundAlertEnabled, fancyAvailableStyleEnabled }; GM_setValue(SETTINGS_KEY, JSON.stringify(data)); } catch (e) { // ignore } } loadSettings(); /*** UTIL ***/ const wait = (ms) => new Promise(r => setTimeout(r, ms)); const addCommas = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); // Respect small delays const waitWithPause = async (msTotal, label = 'delay') => { if (msTotal <= 300) { await wait(msTotal); if (isPaused) await waitUntilNotPaused(label); return; } const tick = 250; const start = Date.now(); while (Date.now() - start < msTotal) { const elapsed = Date.now() - start; const remaining = msTotal - elapsed; if (remaining <= 0) break; await wait(Math.min(tick, remaining)); if (isPaused) await waitUntilNotPaused(label); } }; const groupCounts = (arr) => { const map = new Map(); for (const x of arr) map.set(x, (map.get(x) || 0) + 1); return map; }; const normalizeLines = (str) => str.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // Random delay in ms, min at least 100, max no hard cap const getRandomSingleDelayMs = () => { let minMs = singleDelayMinMs; let maxMs = singleDelayMaxMs; if (!Number.isFinite(minMs)) minMs = DEFAULT_SINGLE_DELAY_MIN_MS; if (!Number.isFinite(maxMs)) maxMs = DEFAULT_SINGLE_DELAY_MAX_MS; if (minMs < 100) minMs = 100; if (maxMs < minMs) maxMs = minMs; const low = Math.round(minMs); const high = Math.round(maxMs); if (high === low) return low; return Math.floor(Math.random() * (high - low + 1)) + low; }; /*** SOUND ALERT (pew pew style) ***/ let audioCtx = null; function playPew() { if (!soundAlertEnabled) return; try { const Ctor = window.AudioContext || window.webkitAudioContext; if (!Ctor) return; if (!audioCtx) { audioCtx = new Ctor(); } const now = audioCtx.currentTime; const osc1 = audioCtx.createOscillator(); const gain1 = audioCtx.createGain(); osc1.type = 'square'; osc1.frequency.setValueAtTime(1400, now); osc1.frequency.exponentialRampToValueAtTime(600, now + 0.15); gain1.gain.setValueAtTime(0.0001, now); gain1.gain.linearRampToValueAtTime(0.4, now + 0.01); gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.18); osc1.connect(gain1); gain1.connect(audioCtx.destination); osc1.start(now); osc1.stop(now + 0.2); const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); const start2 = now + 0.1; osc2.type = 'square'; osc2.frequency.setValueAtTime(1600, start2); osc2.frequency.exponentialRampToValueAtTime(700, start2 + 0.13); gain2.gain.setValueAtTime(0.0001, start2); gain2.gain.linearRampToValueAtTime(0.35, start2 + 0.01); gain2.gain.exponentialRampToValueAtTime(0.001, start2 + 0.16); osc2.connect(gain2); gain2.connect(audioCtx.destination); osc2.start(start2); osc2.stop(start2 + 0.18); } catch (e) { // ignore audio errors } } /*** FLOATING LAUNCHER ICON ***/ const launcher = document.createElement('img'); launcher.src = 'https://images.neopets.com/premium/shopwizard/ssw-icon.svg'; Object.assign(launcher.style, { position: 'fixed', bottom: '12px', right: '12px', width: iconSize + 'px', height: iconSize + 'px', zIndex: '2147483647', borderRadius: '12px', background: 'linear-gradient(#f6e250,#ebb233)', border: '1px solid rgba(0,0,0,0.3)', padding: '6px', boxShadow: '0 6px 18px rgba(0,0,0,0.25)', cursor: 'pointer' }); document.body.appendChild(launcher); /*** SHADOW DOM PANEL ***/ const host = document.createElement('div'); host.style.position = 'fixed'; host.style.inset = '0'; host.style.zIndex = '2147483646'; host.style.pointerEvents = 'none'; document.body.appendChild(host); const root = host.attachShadow({ mode: 'open' }); root.innerHTML = `
SSW Batch Search
LIVE
Queued 0
Searched 0
Available 0
Failed 0
Available
Failed
Log
    `; /*** SHADOW DOM REFS ***/ const $ = (sel) => root.querySelector(sel); const panel = $('#panel'); const dragbar = $('#dragbar'); const btnStart = $('#btnStart'); const btnPause = $('#btnPause'); const btnResume = $('#btnResume'); const btnClear = $('#btnClear'); const btnClose = $('#btnClose'); const btnTheme = $('#btnTheme'); const btnSettings = $('#btnSettings'); const btnMin = $('#btnMin'); const modeChip = $('#modeChip'); const itemInput = $('#itemInput'); const statQueued = $('#statQueued'); const statSearched = $('#statSearched'); const statAvailable = $('#statAvailable'); const statFailed = $('#statFailed'); const progressFill = $('#progressFill'); const availableList = $('#availableList'); const failedList = $('#failedList'); const logBox = $('#log'); const tabs = Array.from(root.querySelectorAll('.tab')); const panes = Array.from(root.querySelectorAll('.pane')); const settingsCard = $('#settingsCard'); const singleDelayMinInput = $('#singleDelayMinInput'); const singleDelayMaxInput = $('#singleDelayMaxInput'); const discountInput = $('#discountInput'); const showSinglesInput = $('#showSinglesInput'); const batchSizeInput = $('#batchSizeInput'); const batchDelayInput = $('#batchDelayInput'); const maxRetriesInput = $('#maxRetriesInput'); const cooldownInput = $('#cooldownInput'); const iconSizeInput = $('#iconSizeInput'); const soundAlertInput = $('#soundAlertInput'); const fancyStyleInput = $('#fancyStyleInput'); const singleToggle = $('#singleToggle'); const modeLabel = $('#modeLabel'); const singleToggleState = $('#singleToggleState'); const resetSettingsBtn = $('#resetSettingsBtn'); const footerBatchDelay = $('#footerBatchDelay'); const chipBatchSize = $('#chipBatchSize'); const chipDiscount = $('#chipDiscount'); const banNotice = $('#banNotice'); const banNoticeText = $('#banNoticeText'); const banNoticeClose = $('#banNoticeClose'); /*** BAN NOTICE HELPERS ***/ function showBanNotice(minutes) { if (!banNotice || !banNoticeText) return; const mins = Math.max(1, Math.round(minutes)); banNoticeText.textContent = `You are currently SSW banned for ${mins} minute${mins === 1 ? '' : 's'}. Script will resume after ban.`; banNotice.style.display = 'flex'; } function hideBanNotice() { if (!banNotice) return; banNotice.style.display = 'none'; } if (banNoticeClose) { banNoticeClose.addEventListener('click', hideBanNotice); } /*** TAB LOGIC ***/ tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.toggle('active', t === tab)); panes.forEach(p => { p.style.display = (p.dataset.pane === tab.dataset.tab) ? '' : 'none'; }); }); }); /*** THEME TOGGLE ***/ let dark = false; const applyTheme = () => { panel.classList.toggle('dark', dark); }; btnTheme.addEventListener('click', () => { dark = !dark; applyTheme(); }); /*** DRAGGABLE + MINIMIZE ***/ let dragging = false, startX = 0, startY = 0, startTop = 0, startLeft = 0; const onMouseDown = (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startTop = rect.top; startLeft = rect.left; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.top = Math.max(8, startTop + dy) + 'px'; panel.style.left = Math.max(8, startLeft + dx) + 'px'; panel.style.right = 'auto'; }; const onMouseUp = () => { dragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; dragbar.addEventListener('mousedown', onMouseDown); let minimized = false; btnMin.addEventListener('click', () => { minimized = !minimized; panel.style.maxHeight = minimized ? '48px' : '78vh'; panel.style.height = minimized ? '48px' : ''; panel.querySelector('.body').style.display = minimized ? 'none' : ''; panel.querySelector('.footer').style.display = minimized ? 'none' : ''; }); /*** OPEN/CLOSE ***/ const showPanel = () => { panel.style.display = 'block'; host.style.pointerEvents = 'auto'; }; const hidePanel = () => { panel.style.display = 'none'; host.style.pointerEvents = 'none'; }; launcher.addEventListener('click', showPanel); btnClose.addEventListener('click', hidePanel); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panel.style.display === 'block') hidePanel(); }); /*** LOG + STATS UI ***/ const log = (html, cls = 'neutral') => { const row = document.createElement('div'); row.className = `notice ${cls}`; row.innerHTML = html; logBox.appendChild(row); logBox.scrollTop = logBox.scrollHeight; }; const updateStats = () => { statQueued.textContent = String(itemsQueued); statSearched.textContent = String(itemsSearchedCount); let totalAvailableListings = 0; for (const meta of availableMeta.values()) totalAvailableListings += meta.qty || 1; statAvailable.textContent = String(totalAvailableListings); statFailed.textContent = String(failedItemsRaw.length); const total = Math.max(itemsQueued, 1); const pct = Math.min(100, Math.round((itemsSearchedCount / total) * 100)); progressFill.style.width = pct + '%'; }; /*** FLOW HELPERS ***/ const setPaused = (v) => { isPaused = v; modeChip.textContent = v ? 'PAUSED' : 'LIVE'; modeChip.style.background = v ? '#fff7e6' : ''; modeChip.style.borderColor = v ? '#f8e2b1' : ''; }; const waitUntilNotPaused = async (note) => { if (isPaused) log(`Awaiting resume ${note}`, 'warning'); while (isPaused) await wait(500); }; /*** SETTINGS UI HELPERS ***/ function clamp(val, min, max, fallback) { let n = parseFloat(val); if (!Number.isFinite(n)) n = fallback; if (n < min) n = min; if (n > max) n = max; return n; } let settingsVisible = false; const updateSettingsVisibility = () => { if (!settingsCard) return; settingsCard.style.display = settingsVisible ? '' : 'none'; btnSettings.textContent = settingsVisible ? 'Settings ?' : 'Settings ?'; }; const applySettingsUI = () => { const isSingle = (searchMode === 'single'); singleDelayMinInput.disabled = !isSingle; singleDelayMaxInput.disabled = !isSingle; if (isSingle) { singleToggle.classList.add('on'); singleToggle.setAttribute('aria-pressed', 'true'); modeLabel.textContent = 'Search one item at a time'; singleToggleState.textContent = 'On'; } else { singleToggle.classList.remove('on'); singleToggle.setAttribute('aria-pressed', 'false'); modeLabel.textContent = 'Search in batches'; singleToggleState.textContent = 'Off'; } }; const updateSettingsDisplay = () => { if (footerBatchDelay) { footerBatchDelay.textContent = `Handles SSW cooldowns automatically. ${batchDelaySeconds}s delay between batches.`; } if (chipBatchSize) { chipBatchSize.textContent = `Batch size: ${batchSize}`; } if (chipDiscount) { chipDiscount.textContent = `Discount at ${Math.round(discountThreshold * 100)} percent of #2`; } launcher.style.width = iconSize + 'px'; launcher.style.height = iconSize + 'px'; }; const syncSettingsToInputs = () => { if (!Number.isFinite(singleDelayMinMs) || singleDelayMinMs < 100) { singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS; } if (!Number.isFinite(singleDelayMaxMs) || singleDelayMaxMs < singleDelayMinMs) { singleDelayMaxMs = singleDelayMinMs; } singleDelayMinInput.value = String(singleDelayMinMs); singleDelayMaxInput.value = String(singleDelayMaxMs); discountInput.value = String(Math.round(discountThreshold * 100)); showSinglesInput.checked = !!showSingles; batchSizeInput.value = String(batchSize); batchDelayInput.value = String(batchDelaySeconds); maxRetriesInput.value = String(maxRetriesPerItem); cooldownInput.value = String(cooldownFallbackMin); iconSizeInput.value = String(iconSize); soundAlertInput.checked = !!soundAlertEnabled; fancyStyleInput.checked = !!fancyAvailableStyleEnabled; applySettingsUI(); updateSettingsDisplay(); updateSettingsVisibility(); }; function resetSettingsToDefaults() { discountThreshold = DEFAULT_DISCOUNT_THRESHOLD; showSingles = DEFAULT_SHOW_SINGLES; batchSize = DEFAULT_BATCH_SIZE; maxRetriesPerItem = DEFAULT_MAX_RETRIES_PER_ITEM; cooldownFallbackMin = DEFAULT_COOLDOWN_FALLBACK_MIN; batchDelaySeconds = DEFAULT_BATCH_DELAY_SECONDS; singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS; singleDelayMaxMs = DEFAULT_SINGLE_DELAY_MAX_MS; iconSize = DEFAULT_ICON_SIZE; soundAlertEnabled = DEFAULT_SOUND_ALERT_ENABLED; fancyAvailableStyleEnabled = DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED; searchMode = 'batch'; saveSettings(); syncSettingsToInputs(); log('Settings reset to defaults.', 'neutral'); } singleToggle.addEventListener('click', () => { searchMode = (searchMode === 'single') ? 'batch' : 'single'; applySettingsUI(); saveSettings(); }); singleDelayMinInput.addEventListener('change', () => { let n = parseFloat(singleDelayMinInput.value); if (!Number.isFinite(n)) n = DEFAULT_SINGLE_DELAY_MIN_MS; if (n < 100) n = 100; singleDelayMinMs = n; singleDelayMinInput.value = String(singleDelayMinMs); if (!Number.isFinite(singleDelayMaxMs) || singleDelayMaxMs < singleDelayMinMs) { singleDelayMaxMs = singleDelayMinMs; singleDelayMaxInput.value = String(singleDelayMaxMs); } saveSettings(); }); singleDelayMaxInput.addEventListener('change', () => { let n = parseFloat(singleDelayMaxInput.value); if (!Number.isFinite(n)) n = DEFAULT_SINGLE_DELAY_MAX_MS; if (n < singleDelayMinMs) n = singleDelayMinMs; singleDelayMaxMs = n; singleDelayMaxInput.value = String(singleDelayMaxMs); saveSettings(); }); discountInput.addEventListener('change', () => { const pct = clamp(discountInput.value, 1, 100, 60); discountInput.value = String(pct); discountThreshold = pct / 100; updateSettingsDisplay(); saveSettings(); }); showSinglesInput.addEventListener('change', () => { showSingles = !!showSinglesInput.checked; saveSettings(); }); batchSizeInput.addEventListener('change', () => { batchSize = clamp(batchSizeInput.value, 1, 200, DEFAULT_BATCH_SIZE); batchSizeInput.value = String(batchSize); updateSettingsDisplay(); saveSettings(); }); batchDelayInput.addEventListener('change', () => { batchDelaySeconds = clamp(batchDelayInput.value, 0, 300, DEFAULT_BATCH_DELAY_SECONDS); batchDelayInput.value = String(batchDelaySeconds); updateSettingsDisplay(); saveSettings(); }); maxRetriesInput.addEventListener('change', () => { maxRetriesPerItem = clamp(maxRetriesInput.value, 0, 5, DEFAULT_MAX_RETRIES_PER_ITEM); maxRetriesInput.value = String(maxRetriesPerItem); saveSettings(); }); cooldownInput.addEventListener('change', () => { cooldownFallbackMin = clamp(cooldownInput.value, 1, 120, DEFAULT_COOLDOWN_FALLBACK_MIN); cooldownInput.value = String(cooldownFallbackMin); saveSettings(); }); iconSizeInput.addEventListener('change', () => { iconSize = clamp(iconSizeInput.value, 24, 120, DEFAULT_ICON_SIZE); iconSizeInput.value = String(iconSize); updateSettingsDisplay(); saveSettings(); }); soundAlertInput.addEventListener('change', () => { soundAlertEnabled = !!soundAlertInput.checked; saveSettings(); }); fancyStyleInput.addEventListener('change', () => { fancyAvailableStyleEnabled = !!fancyStyleInput.checked; rerenderAvailableList(); saveSettings(); }); btnSettings.addEventListener('click', () => { settingsVisible = !settingsVisible; updateSettingsVisibility(); }); resetSettingsBtn.addEventListener('click', () => { if (confirm('Reset all SSW batch search settings to their defaults?')) { resetSettingsToDefaults(); } }); syncSettingsToInputs(); /*** COPY TO CLIPBOARD HELPERS ***/ function flashCopyIcon(icon) { if (!icon) return; const original = icon.textContent || '??'; icon.textContent = '?'; icon.style.opacity = '0.9'; setTimeout(() => { icon.textContent = original; icon.style.opacity = ''; }, 900); } function fallbackCopyText(text, icon) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); } catch (e) { // ignore } finally { document.body.removeChild(ta); flashCopyIcon(icon); } } function copyToClipboard(text, icon) { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text) .then(() => { flashCopyIcon(icon); }) .catch(() => { fallbackCopyText(text, icon); }); } else { fallbackCopyText(text, icon); } } /*** AVAILABLE LIST RENDER ***/ const buildLinksHTML = (item) => { const jn = `https://items.jellyneo.net/search/?name=${encodeURIComponent(item)}&name_type=3`; const wiz = `https://www.neopets.com/shops/wizard.phtml?string=${encodeURIComponent(item)}`; const tp = `https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&sort_by=newest&search_string=${encodeURIComponent(item)}`; return ` `; }; const renderAvailableLI = (name) => { const meta = availableMeta.get(name) || {}; const qty = meta.qty || 1; const li = document.createElement('li'); li.dataset.item = name; const qtyBadge = qty > 1 ? `×${qty}` : ''; const copyIcon = `copy`; const nameHTML = fancyAvailableStyleEnabled ? `${name}` : `${name}`; li.innerHTML = ` ${nameHTML}${qtyBadge}${copyIcon} ${meta.linksHTML || ''} ${meta.prices ? `: ${meta.prices.map(p => `${addCommas(p)} NP`).join(', ')}` : ''} `; return li; }; function rerenderAvailableList() { const frag = document.createDocumentFragment(); for (const name of availableMeta.keys()) { frag.appendChild(renderAvailableLI(name)); } availableList.replaceChildren(frag); } const addAvailable = (name, prices, linksHTML, qtyFromSSW = 1) => { const current = availableCounts.get(name) || 0; availableCounts.set(name, current + 1); const prev = availableMeta.get(name) || {}; const merged = { prices, linksHTML, qty: Math.max(qtyFromSSW, prev.qty || 1) }; availableMeta.set(name, merged); const existing = availableList.querySelector(`li[data-item="${CSS.escape(name)}"]`); const node = renderAvailableLI(name); if (existing) { existing.replaceWith(node); } else { availableList.appendChild(node); playPew(); } updateStats(); }; /*** FAILED LIST RENDER ***/ const refreshFailedList = () => { const counts = groupCounts(failedItemsRaw); const frag = document.createDocumentFragment(); counts.forEach((qty, name) => { const li = document.createElement('li'); li.innerHTML = qty > 1 ? `${name} ×${qty}` : `${name}`; frag.appendChild(li); }); failedList.replaceChildren(frag); updateStats(); }; /*** SSW BAN / COOLDOWN ***/ const parseCooldownMinutes = (html) => { const m = html.match(/(\d+)<\/b>/); return m ? parseInt(m[1], 10) : cooldownFallbackMin; }; const handleBan = (itemName, banHTML) => { const mins = parseCooldownMinutes(banHTML); if (!isPaused) { setPaused(true); log(`SSW ban during ${itemName}. Cooldown ${mins} minute(s).`, 'warning'); showBanNotice(mins); if (banCooldownTimeout) clearTimeout(banCooldownTimeout); banCooldownTimeout = setTimeout(() => { setPaused(false); banCooldownTimeout = null; hideBanNotice(); log('Cooldown finished. Resuming.', 'positive'); }, mins * 60 * 1000); } }; /*** FILTER ***/ const qualifiesByDiscount = (pricesArr) => { if (pricesArr.length < 2) return false; const sorted = pricesArr.slice(0).sort((a, b) => a - b); return sorted[0] <= Math.floor(sorted[1] * discountThreshold); }; /*** CORE SEARCH ***/ const searchItem = async (itemName) => { let retries = 0; while (true) { await waitUntilNotPaused(itemName); const result = await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://www.neopets.com/shops/ssw/ssw_query.php?q=${encodeURIComponent(itemName)}&priceOnly=1&context=0&partial=0&min_price=0&max_price=999999&json=1`, onload: (resp) => { try { const json = JSON.parse(resp.responseText); const banHTML = json.html || ''; if (banHTML.includes('Whoa there, too many searches!')) { handleBan(itemName, banHTML); resolve('banned'); return; } const data = json.data; if (data && Array.isArray(data.prices)) { const prices = data.prices .map(p => parseInt(p, 10)) .filter(n => Number.isFinite(n) && n >= 0); const rowCount = (data && Number.isFinite(data.rowcount)) ? data.rowcount : prices.length; const accept = (prices.length === 1 && showSingles) || (prices.length >= 2 && qualifiesByDiscount(prices)) || (prices.length <= 4); if (accept && prices.length) { const topPrices = prices.slice(0).sort((a, b) => a - b).slice(0, 4); addAvailable(itemName, topPrices, buildLinksHTML(itemName), rowCount); } else if (accept && !prices.length && rowCount > 0) { addAvailable(itemName, [], buildLinksHTML(itemName), rowCount); } } resolve('ok'); } catch (e) { log(`Parse error for ${itemName}: ${e.message}`, 'error'); resolve('error'); } }, onerror: () => { log(`Network error for ${itemName}.`, 'error'); resolve('error'); }, }); }); if (result === 'banned') { continue; } if (result === 'error') { if (retries < maxRetriesPerItem) { retries++; log(`Retrying ${itemName} (attempt ${retries + 1}/${maxRetriesPerItem + 1}).`, 'warning'); await wait(500); continue; } else { failedItemsRaw.push(itemName); refreshFailedList(); } } break; } }; /*** COMMON RESET FOR A RUN ***/ const resetRunState = (items) => { itemsQueued = items.length; itemsSearchedCount = 0; availableCounts.clear(); availableMeta.clear(); failedItemsRaw.length = 0; availableList.textContent = ''; failedList.textContent = ''; logBox.textContent = ''; updateStats(); }; /*** BATCH RUNNER ***/ const runBatches = async (items) => { resetRunState(items); log('Starting batch search in batch mode.', 'positive'); const batches = []; for (let i = 0; i < items.length; i += batchSize) { batches.push(items.slice(i, i + batchSize)); } for (let b = 0; b < batches.length; b++) { await waitUntilNotPaused(`batch ${b + 1}/${batches.length}`); log(`Searching batch ${b + 1} of ${batches.length} concurrently.`, 'neutral'); const batch = batches[b]; await Promise.all(batch.map(async (name) => { itemsSearchedCount++; updateStats(); await searchItem(name); updateStats(); })); if (b < batches.length - 1 && batchDelaySeconds > 0) { log(`Waiting ${batchDelaySeconds}s before starting the next batch.`, 'neutral'); await waitWithPause(batchDelaySeconds * 1000, 'between batches'); } } log('All batches completed.', 'positive'); }; /*** SINGLE-ITEM RUNNER (RANDOM MS DELAY) ***/ const runSingles = async (items) => { resetRunState(items); log('Starting search in single-item mode with random delays.', 'positive'); for (let i = 0; i < items.length; i++) { const name = items[i]; await waitUntilNotPaused(`item ${i + 1}/${items.length}`); itemsSearchedCount++; updateStats(); await searchItem(name); updateStats(); if (i < items.length - 1) { const delayMs = getRandomSingleDelayMs(); if (delayMs > 0) { log(`Searched ${name} waiting ${delayMs}ms before next item.`, 'neutral'); await waitWithPause(delayMs, 'between items'); } } } log('All items completed.', 'positive'); }; /*** BUTTONS ***/ btnStart.addEventListener('click', () => { const raw = normalizeLines((itemInput.value || '').trim()); if (!raw) { alert('Please enter at least one item.'); return; } const items = raw.split('\n').map(s => s.trim()).filter(Boolean); if (!items.length) { alert('Please enter at least one item.'); return; } if (settingsVisible) { settingsVisible = false; updateSettingsVisibility(); } if (searchMode === 'single') { runSingles(items); } else { runBatches(items); } }); btnPause.addEventListener('click', () => setPaused(true)); btnResume.addEventListener('click', () => setPaused(false)); btnClear.addEventListener('click', () => { itemInput.value = ''; availableList.textContent = ''; failedList.textContent = ''; logBox.textContent = ''; availableCounts.clear(); availableMeta.clear(); failedItemsRaw.length = 0; itemsQueued = 0; itemsSearchedCount = 0; updateStats(); }); /*** COPY ICON CLICK HANDLER ***/ availableList.addEventListener('click', (e) => { const icon = e.target.closest('.copy-icon'); if (!icon) return; const li = icon.closest('li'); if (!li) return; const name = li.dataset.item || ''; copyToClipboard(name, icon); }); /*** INITIAL STATE ***/ applyTheme(); launcher.addEventListener('click', () => { minimized = false; }); })();