// ==UserScript==
// @name Neopets Lottery Quickpick
// @namespace neopets
// @version 1.3
// @description Adds a "Quickpick 20 tickets!" button that only buys as many as needed to reach 20 (with slight delay), plus a "Quickpick (fill only)" button.
// @match https://www.neopets.com/games/lottery.phtml*
// @match http://www.neopets.com/games/lottery.phtml*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---- Config ----
const MIN_DELAY_SEC = 6;
const MAX_DELAY_SEC = 10;
const DAILY_CAP = 20; // site limit per day
const K = {
ACTIVE: 'neo_lotto_active',
RUNS: 'neo_lotto_runs', // how many we've bought in the current batch
TODO: 'neo_lotto_todo', // how many we intend to buy in total this batch
GUARD: 'neo_lotto_guard',
};
if (window[K.GUARD]) return;
window[K.GUARD] = true;
// ---- Helpers ----
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
function generateTicket() {
const s = new Set();
while (s.size < 6) s.add(randInt(1, 30));
return [...s].sort((a, b) => a - b);
}
function toast(msg, ms = 2600) {
const el = document.createElement('div');
el.textContent = msg;
Object.assign(el.style, {
position: 'fixed', bottom: '12px', right: '12px',
padding: '8px 10px', background: 'rgba(0,0,0,.8)', color: '#fff',
font: '12px/1.35 monospace', borderRadius: '6px', zIndex: 2147483647,
});
document.body.appendChild(el);
setTimeout(() => el.remove(), ms);
}
function findForm() {
const one = document.querySelector('input[name="one"]');
return one ? one.closest('form') : null;
}
function fillNumbers(nums, form) {
const fields = ['one','two','three','four','five','six'];
fields.forEach((name, i) => {
const el = form.querySelector(`input[name="${name}"]`);
if (el) el.value = String(nums[i]);
});
}
function submitForm(form) {
const btn =
form.querySelector('input[type="submit"][value*="Buy a Lottery Ticket"]') ||
form.querySelector('input[type="submit"]') ||
form.querySelector('button[type="submit"]');
(btn && btn.click()) || form.submit();
}
// Count how many tickets are already listed for the next draw
function countOwnedTickets() {
// Count occurrences like: "Ticket 1 :" (body text is most robust across markup changes)
const text = document.body?.innerText || '';
const matches = text.match(/Ticket\s+\d+\s*:/g);
return (matches ? matches.length : 0);
}
function scheduleOne(form, nextRunIndex, todoTotal) {
const remaining = Math.max(0, todoTotal - (nextRunIndex - 1));
if (remaining <= 0) {
GM_setValue(K.ACTIVE, false);
return;
}
const delaySec = randInt(MIN_DELAY_SEC, MAX_DELAY_SEC);
toast(`Quickpick #${nextRunIndex}/${todoTotal} in ${delaySec}s…`);
setTimeout(() => {
const nums = generateTicket();
fillNumbers(nums, form);
// Persist progress BEFORE navigating
GM_setValue(K.RUNS, nextRunIndex);
// Stop after hitting our intended count
if (nextRunIndex >= todoTotal) GM_setValue(K.ACTIVE, false);
submitForm(form);
}, delaySec * 1000);
}
function injectButtons(form) {
if (document.getElementById('neo-quickpick20')) return;
// Find submit button to place our main button beside it
const submitBtn =
form.querySelector('input[type="submit"][value*="Buy a Lottery Ticket"]') ||
form.querySelector('input[type="submit"]') ||
form.querySelector('button[type="submit"]');
// --- Quickpick to 20 (auto-buy as needed) ---
const qp = document.createElement('button');
qp.type = 'button';
qp.id = 'neo-quickpick20';
qp.textContent = 'Quickpick 20 tickets!';
qp.style.marginLeft = '8px';
qp.style.padding = '3px 8px';
qp.style.cursor = 'pointer';
(submitBtn && submitBtn.parentNode)
? submitBtn.parentNode.insertBefore(qp, submitBtn.nextSibling)
: form.appendChild(qp);
qp.addEventListener('click', () => {
const owned = countOwnedTickets();
const need = Math.max(0, Math.min(DAILY_CAP, DAILY_CAP - owned));
if (need <= 0) {
GM_setValue(K.ACTIVE, false);
GM_setValue(K.RUNS, 0);
GM_setValue(K.TODO, 0);
toast('You already have 20 tickets — nothing to buy.');
return;
}
GM_setValue(K.RUNS, 0);
GM_setValue(K.TODO, need);
GM_setValue(K.ACTIVE, true);
toast(`Quickpick started: buying ${need} ticket(s) with 6–10s delays.`);
const newForm = findForm();
if (newForm) scheduleOne(newForm, /*nextRunIndex*/ 1, /*todoTotal*/ need);
});
// --- Fill-only button BELOW the submit row ---
const lowerBar = document.createElement('div');
lowerBar.id = 'neo-quickpick-lowerbar';
lowerBar.style.marginTop = '8px';
const fillOnly = document.createElement('button');
fillOnly.type = 'button';
fillOnly.id = 'neo-quickpick-fill';
fillOnly.textContent = 'Quickpick (fill only)';
fillOnly.style.padding = '3px 8px';
fillOnly.style.cursor = 'pointer';
fillOnly.addEventListener('click', () => {
const nums = generateTicket();
fillNumbers(nums, form);
toast(`Filled: ${nums.join(' ')}`);
});
lowerBar.appendChild(fillOnly);
form.appendChild(lowerBar);
}
// ---- Wire up page ----
const form = findForm();
if (!form) return;
injectButtons(form);
// If a batch is in progress, only run up to the intended amount
const active = !!GM_getValue(K.ACTIVE);
const runsSoFar = Number(GM_getValue(K.RUNS) || 0);
const todoTotal = Number(GM_getValue(K.TODO) || 0);
if (active && runsSoFar < todoTotal) {
scheduleOne(form, runsSoFar + 1, todoTotal);
} else if (runsSoFar >= todoTotal) {
GM_setValue(K.ACTIVE, false);
}
// ESC to stop auto-buy batch
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
GM_setValue(K.ACTIVE, false);
toast('Quickpick stopped.');
}
});
})();