// ==UserScript==
// @name Neopets Main Shop Aber
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Aber for main neopets shops. Buys at realistic human speed. never banned using this. Change the settings to your own liking.
// @license MIT
// @author God
// @match https://www.neopets.com/objects.phtml?obj_type=*
// @match https://www.neopets.com/objects.phtml?type=*
// @match https://www.neopets.com/haggle.phtml*
// @grant GM_xmlhttpRequest
// @connect cdn.jsdelivr.net
// ==/UserScript==
(function() {
'use strict';
// Configuration for all delays (in milliseconds)
const delays = {
confirmation: 500, // Delay after clicking an item to wait for confirmation popup
inventoryMessage: 5000, // Delay after confirming purchase to check for inventory message
shopRedirect: 2000, // Delay before redirecting to shop after purchase or SOLD OUT (no longer used for instant redirect)
refreshMin: 2000, // Minimum delay for shop page refresh
refreshMax: 4000, // Maximum delay for shop page refresh
captcha: 500, // Delay after setting haggle offer to click CAPTCHA
waitForElementTimeout: 450, // Timeout for each attempt to wait for an element
waitForElementRetryDelay: 450, // Delay between retry attempts for waitForElement
waitForElementMaxRetries: 3, // Maximum retries for waitForElement (for #shopkeeper_makes_deal)
successMessageRetryDelay: 1000, // Delay between retries for success message detection
successMessageMaxRetries: 3 // Maximum retries for success message detection
};
// List of item names to target (updated list)
const targetItems = [
'Mythical Xweetok Hind',
'Mythical Xweetok Body',
'Mythical Xweetok Head',
];
// Shop URL to redirect to after purchase or SOLD OUT
const shopUrl = 'https://www.neopets.com/objects.phtml?type=shop&obj_type=4'; // <Change the shop ID here currently shop 4
let Tesseract;
let isScriptRunning = true; // Global flag to control script execution
let refreshTimeout = null; // To track and cancel the refresh timer
// Function to simulate a mouse click at specific coordinates on an element
function simulateClick(element, x, y) {
if (!element) return false;
try {
const rect = element.getBoundingClientRect();
const clientX = x !== undefined ? rect.left + x : rect.left + rect.width / 2; // Default to center if x not provided
const clientY = y !== undefined ? rect.top + y : rect.top + rect.height / 2; // Default to center if y not provided
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: unsafeWindow || window, // Use unsafeWindow to bypass sandboxing
clientX: clientX,
clientY: clientY
});
element.dispatchEvent(event);
return true;
} catch (error) {
return false;
}
}
// Function to get random percentage between min and max (e.g., 1.01 to 1.04)
function getRandomPercentage(min, max) {
return Math.random() * (max - min) + min;
}
// Function to get random delay between min and max milliseconds
function getRandomDelay(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Function to wait for an element to appear in the DOM with retries
function waitForElement(selector, timeout = delays.waitForElementTimeout, maxRetries = delays.waitForElementMaxRetries) {
return new Promise((resolve, reject) => {
let attempt = 0;
const tryFindElement = () => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
attempt++;
if (attempt < maxRetries) {
setTimeout(tryFindElement, delays.waitForElementRetryDelay);
} else {
reject(new Error(`Element ${selector} not found after ${maxRetries} attempts`));
}
}, timeout);
};
tryFindElement();
});
}
// Function to scroll to the bottom of the page
function scrollToBottom() {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}
// Function to create a stop/start button on the shop page
function createToggleButton() {
const button = document.createElement('button');
button.id = 'toggleScriptButton';
button.textContent = 'Stop Script';
button.style.position = 'fixed';
button.style.top = '10px';
button.style.right = '10px';
button.style.zIndex = '9999';
button.style.padding = '10px';
button.style.backgroundColor = '#ff4444';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.cursor = 'pointer';
button.style.fontWeight = 'bold';
button.addEventListener('click', () => {
isScriptRunning = !isScriptRunning;
button.textContent = isScriptRunning ? 'Stop Script' : 'Start Script';
button.style.backgroundColor = isScriptRunning ? '#ff4444' : '#44ff44';
// Clear the refresh timer if stopping the script
if (!isScriptRunning && refreshTimeout) {
clearTimeout(refreshTimeout);
refreshTimeout = null;
}
});
document.body.appendChild(button);
return button;
}
// Function to find and click target items on the shop page
function findAndClickItems() {
// Add the stop/start button on the shop page
const button = document.getElementById('toggleScriptButton') || createToggleButton();
// Scroll to the bottom of the page on shop page
scrollToBottom();
// Only proceed if the script is running
if (!isScriptRunning) {
return false;
}
const shopItems = document.querySelectorAll('.shop-item .item-img');
for (const item of shopItems) {
const itemName = item.getAttribute('data-name');
if (targetItems.includes(itemName)) {
if (simulateClick(item)) {
// Wait for the popup to appear and simulate click on confirm button
setTimeout(() => {
const confirmButton = document.evaluate(
'//button[@id="confirm-link"]',
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (confirmButton && simulateClick(confirmButton)) {
// Check for item added to inventory message, haggle success, sold out, or error page
setTimeout(() => {
const inventoryMessage = document.querySelector('p b')?.textContent?.trim();
const pageMessage = document.querySelector('p')?.textContent?.trim().toLowerCase();
// Check for successful purchase (inventory message)
if (inventoryMessage && targetItems.includes(inventoryMessage) && pageMessage.includes('has been added to your inventory')) {
window.location.href = shopUrl;
return; // Stop further execution
} else if (pageMessage.includes('the shopkeeper says \'i accept your offer of')) {
// Check for haggle success message
window.location.href = shopUrl;
return; // Stop further execution
} else if (pageMessage.includes('is sold out!')) {
// Check for "SOLD OUT!" message on shop page
window.location.href = shopUrl;
return; // Stop further execution
} else if (pageMessage.includes('too quickly') || pageMessage.includes('slow down')) {
// Check for "too quickly" error page
window.location.href = shopUrl;
return; // Stop further execution
} else {
// If none of the above conditions are met, proceed with haggle page logic
if (window.location.href.includes('haggle.phtml')) {
// Check if the URL has additional parameters indicating a true haggle page
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString().length > 0) {
handleHagglePage();
} else {
window.location.href = shopUrl;
}
} else {
window.location.href = shopUrl;
}
}
}, delays.inventoryMessage); // Dynamic delay for inventory message check
}
}, delays.confirmation); // Dynamic delay for confirmation popup
return true; // Exit after clicking one item to avoid multiple popups
} else {
return false;
}
}
}
return false;
}
// Function to retry finding a message with a delay
async function retryFindMessage(checkMessage, maxRetries = delays.successMessageMaxRetries, retryDelay = delays.successMessageRetryDelay) {
let attempt = 0;
while (attempt < maxRetries) {
const pageMessage = document.querySelector('p')?.textContent?.trim().toLowerCase();
if (pageMessage && pageMessage.includes(checkMessage)) {
return true;
}
attempt++;
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
return false;
}
// Function to handle haggling on the haggle page
async function handleHagglePage() {
// Check for SOLD OUT message first
const soldOutMessage = Array.from(document.querySelectorAll('p')).some(p => p.textContent.includes('SOLD OUT!'));
if (soldOutMessage) {
window.location.href = shopUrl;
return;
}
// Check for haggle success message with retries
const foundSuccessMessage = await retryFindMessage('the shopkeeper says \'i accept your offer of');
if (foundSuccessMessage) {
window.location.href = shopUrl;
return;
}
// Wait for the shopkeeper message element to appear
let shopkeeperDiv;
try {
shopkeeperDiv = await waitForElement('#shopkeeper_makes_deal');
} catch (error) {
// Assume successful purchase since #shopkeeper_makes_deal is not found
window.location.href = shopUrl;
return;
}
// Try to find the message within the container
const shopkeeperMessageElement = shopkeeperDiv.querySelector('b') || shopkeeperDiv.querySelector('p') || shopkeeperDiv;
const shopkeeperMessage = shopkeeperMessageElement?.textContent || '';
// Attempt to extract the minimum price with broader regex patterns
const priceMatch = shopkeeperMessage.match(/I want at least ([\d,]+) Neopoints/) ||
shopkeeperMessage.match(/I wont take less than ([\d,]+) Neopoints/) ||
shopkeeperMessage.match(/at least ([\d,]+) Neopoints/) ||
shopkeeperMessage.match(/([\d,]+) Neopoints/);
if (!priceMatch) {
window.location.href = shopUrl;
return;
}
// Parse the minimum price and remove commas
const minPrice = parseInt(priceMatch[1].replace(/,/g, ''), 10);
if (isNaN(minPrice)) {
window.location.href = shopUrl;
return;
}
// Calculate offer: 1.01 to 1.04 times the minimum price
const multiplier = getRandomPercentage(1.01, 1.04);
const offerPrice = Math.round(minPrice * multiplier);
// Set the offer in the input field
const offerInput = document.querySelector('input[name="current_offer"]');
if (offerInput) {
offerInput.value = offerPrice.toString();
} else {
window.location.href = shopUrl;
return;
}
// Load Tesseract.js if not already loaded (only for haggle page CAPTCHA)
if (!Tesseract) {
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@2/dist/tesseract.min.js';
script.onload = () => {
Tesseract = window.Tesseract;
resolve();
};
document.head.appendChild(script);
});
}
// Simulate click on the CAPTCHA image (Haggle button) targeting the Neopet using OCR
setTimeout(async () => {
const haggleButton = document.querySelector('input[type="image"]');
if (haggleButton) {
// Convert image to canvas for OCR analysis
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = 'anonymous'; // Handle CORS if needed
img.src = haggleButton.src;
await new Promise(resolve => img.onload = resolve);
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
// Use pixel data to detect dark areas (approximate Neopet position)
const { data } = context.getImageData(0, 0, canvas.width, canvas.height);
let darkestX = 0, darkestY = 0, darkestValue = 255 * 3; // RGB max sum
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const index = (y * canvas.width + x) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const brightness = r + g + b; // Sum of RGB for darkness
if (brightness < darkestValue) {
darkestValue = brightness;
darkestX = x;
darkestY = y;
}
}
}
const rect = haggleButton.getBoundingClientRect();
const clickX = darkestX * (rect.width / canvas.width);
const clickY = darkestY * (rect.height / canvas.height);
if (!simulateClick(haggleButton, clickX, clickY)) {
window.location.href = shopUrl;
}
} else {
window.location.href = shopUrl;
}
}, delays.captcha); // Dynamic delay for CAPTCHA click
}
// Main function to run the script
function main() {
const currentUrl = window.location.href;
// Handle shop pages
if (currentUrl.includes('objects.phtml')) {
if (!findAndClickItems()) {
// If no items found, set up a MutationObserver to watch for dynamic content
const observer = new MutationObserver((mutations) => {
if (findAndClickItems()) {
observer.disconnect(); // Stop observing after successful click
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Schedule a refresh if no items are found after a random delay
if (isScriptRunning) {
refreshTimeout = setTimeout(() => {
// Double-check if items appeared before refreshing
if (!findAndClickItems()) {
window.location.reload();
}
}, getRandomDelay(delays.refreshMin, delays.refreshMax)); // Dynamic refresh delay range
}
}
}
// Handle haggle page
else if (currentUrl.includes('haggle.phtml')) {
if (typeof handleHagglePage === 'function') {
handleHagglePage();
} else {
console.error('handleHagglePage is not defined, redirecting to shop page as fallback');
window.location.href = shopUrl;
}
}
}
// Run the script after the page loads
window.addEventListener('load', main);
})();