// ==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 = `
Queued 0
Searched 0
Available 0
Failed 0
You are currently SSW banned.
`;
/*** 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; });
})();