// ==UserScript==
// @name Neopets - Grarrl Keno Auto Player
// @namespace https://scriptneo.com/
// @version 1.0.0
// @description Auto plays your Grarrl Keno. Supports Quick Pick (double click to finalize) or Random eggs (no duplicates, optional no-adjacent). Forces bet, waits for hatch, loops, persists settings, and logs prize_chart winners numbers one line per round once all 10 numbers exist.
// @author You
// @match *://*/*keno.phtml*
// @run-at document-end
// @grant GM_getValue
// @grant GM_setValue
// @downloadURL https://www.scriptneo.com/scripts/download.php?id=35
// @updateURL https://www.scriptneo.com/scripts/download.php?id=35
// ==/UserScript==
(function () {
"use strict";
const KEY_CFG = "gk_owner_cfg_v10";
const KEY_RUN = "gk_owner_run_v10";
const KEY_LOG = "gk_owner_winners_log_only_v4";
const DEFAULT_CFG = {
betAmount: 500,
pickMethod: "random", // "quickpick" or "random"
randomEggCount: 10,
randomEggMin: 1,
randomEggMax: 80,
noAdjacentNumbers: true,
hatchWaitSeconds: 10,
clickDelayMinMs: 120,
clickDelayMaxMs: 320,
quickPickFinalizeMinMs: 700,
quickPickFinalizeMaxMs: 1400,
maxRounds: 0, // 0 = unlimited
stopOnError: true,
verboseLog: true,
autoResume: false,
// winners-only log
enableWinnersLog: true,
maxLogLines: 1000,
// New: make winners logging reliable even when tab is unfocused
winnersExpectedCount: 10,
winnersWaitTimeoutMs: 15000,
winnersPollIntervalMs: 120
};
const DEFAULT_RUN = {
running: false,
rounds: 0,
lastError: "",
step: "idle", // idle | selection | waiting_results | results
stepDueAt: 0,
// Used to prevent double-logging
lastWinnersLine: ""
};
function loadObj(key, fallback) {
const v = GM_getValue(key, null);
if (!v || typeof v !== "object") return { ...fallback };
return { ...fallback, ...v };
}
function saveObj(key, obj) {
GM_setValue(key, obj);
}
function loadLogLines() {
const v = GM_getValue(KEY_LOG, null);
if (!Array.isArray(v)) return [];
return v.filter((x) => typeof x === "string");
}
function saveLogLines(lines) {
GM_setValue(KEY_LOG, lines);
}
let cfg = loadObj(KEY_CFG, DEFAULT_CFG);
let run = loadObj(KEY_RUN, DEFAULT_RUN);
function saveCfg() {
saveObj(KEY_CFG, cfg);
}
function saveRun() {
saveObj(KEY_RUN, run);
}
function log(...args) {
if (!cfg.verboseLog) return;
console.log("[GK Auto]", ...args);
}
function warn(...args) {
console.warn("[GK Auto]", ...args);
}
function errorLog(...args) {
console.error("[GK Auto]", ...args);
}
function $all(sel, root = document) {
return Array.from(root.querySelectorAll(sel));
}
function clampInt(v, min, max, fallback) {
const n = Number.parseInt(String(v), 10);
if (!Number.isFinite(n)) return fallback;
return Math.max(min, Math.min(max, n));
}
function randInt(min, max) {
const lo = Math.min(min, max);
const hi = Math.max(min, max);
return lo + Math.floor(Math.random() * (hi - lo + 1));
}
function sleep(ms) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function setStatus(text) {
const el = document.getElementById("gk_status_text");
if (el) el.textContent = text;
run.lastError = "";
saveRun();
}
function setError(text) {
const el = document.getElementById("gk_status_text");
if (el) el.textContent = text;
run.lastError = text;
saveRun();
}
function setRounds(n) {
const el = document.getElementById("gk_rounds");
if (el) el.textContent = String(n);
}
function normalize(s) {
return String(s || "").trim().toLowerCase();
}
function findClickableByLabel(label) {
const target = normalize(label);
const inputs = $all('input[type="submit"], input[type="button"], input[type="reset"]');
for (const el of inputs) {
if (normalize(el.value) === target) return el;
}
const buttons = $all("button");
for (const el of buttons) {
if (normalize(el.textContent) === target) return el;
}
const roleBtns = $all('[role="button"]');
for (const el of roleBtns) {
if (normalize(el.textContent) === target) return el;
}
return null;
}
function isSelectionScreen() {
return !!findBetInput() && !!findClickableByLabel("Hatch those Eggs!");
}
function isResultsScreen() {
return !!findClickableByLabel("Play Again!");
}
function findBetInput() {
let el = document.querySelector('input[name="bet"]');
if (el) return el;
try {
const form = document.forms && document.forms.keno ? document.forms.keno : null;
if (form && form.bet) return form.bet;
} catch (e) {
// ignore
}
el = document.querySelector('input[type="text"][maxlength="4"]');
return el || null;
}
function forceBet(amount) {
const betInput = findBetInput();
if (!betInput) return false;
const desired = String(clampInt(amount, 1, 999999, 500));
betInput.focus();
betInput.value = desired;
betInput.dispatchEvent(new Event("input", { bubbles: true }));
betInput.dispatchEvent(new Event("change", { bubbles: true }));
betInput.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "0" }));
try {
if (typeof window.show_chart === "function") window.show_chart();
} catch (e) {
// ignore
}
if (String(betInput.value || "").trim() !== desired) {
betInput.value = desired;
betInput.dispatchEvent(new Event("input", { bubbles: true }));
betInput.dispatchEvent(new Event("change", { bubbles: true }));
}
return true;
}
async function clickWithDelay(el) {
if (!el) return false;
await sleep(randInt(cfg.clickDelayMinMs, cfg.clickDelayMaxMs));
el.click();
return true;
}
function stopRunning(reason) {
run.running = false;
run.step = "idle";
run.stepDueAt = 0;
saveRun();
setStatus(reason || "Stopped.");
updateButtons();
}
function startRunning() {
run.running = true;
run.step = "selection";
run.stepDueAt = 0;
saveRun();
setStatus("Running...");
updateButtons();
tick();
}
function countMaxNoAdjacentPossible(min, max) {
const lo = Math.min(min, max);
const hi = Math.max(min, max);
const L = hi - lo + 1;
return Math.ceil(L / 2);
}
function buildUniqueRandomSetNoAdjacent(count, min, max) {
const c = clampInt(count, 1, 999, 10);
const lo = clampInt(min, 1, 1000000, 1);
const hi = clampInt(max, lo, 1000000, 80);
if (cfg.noAdjacentNumbers) {
const maxPossible = countMaxNoAdjacentPossible(lo, hi);
if (c > maxPossible) return null;
}
const picks = new Set();
const forbidden = new Set();
const MAX_ATTEMPTS = 5000;
let attempts = 0;
while (picks.size < c && attempts < MAX_ATTEMPTS) {
attempts += 1;
const n = randInt(lo, hi);
if (picks.has(n)) continue;
if (cfg.noAdjacentNumbers && forbidden.has(n)) continue;
picks.add(n);
if (cfg.noAdjacentNumbers) {
forbidden.add(n - 1);
forbidden.add(n + 1);
}
}
if (picks.size !== c) return null;
return Array.from(picks);
}
function findEggElement(n) {
const byId = document.getElementById("ch" + String(n));
if (byId) return byId;
const byName = document.querySelector('input[name="ch' + String(n) + '"]');
if (byName) return byName;
const byValue = document.querySelector('input[type="checkbox"][value="' + String(n) + '"]');
if (byValue) return byValue;
const byData = document.querySelector('[data-egg="' + String(n) + '"]');
if (byData) return byData;
return null;
}
function isEggChecked(el) {
if (!el) return false;
if ("checked" in el) return !!el.checked;
return el.getAttribute("aria-pressed") === "true" || el.classList.contains("selected");
}
async function clearEggsIfPossible() {
const clearBtn = findClickableByLabel("Clear All");
if (clearBtn) {
setStatus("Clearing eggs...");
await clickWithDelay(clearBtn);
await sleep(randInt(120, 260));
return true;
}
try {
if (typeof window.reset_eggs === "function") {
setStatus("Clearing eggs...");
window.reset_eggs();
await sleep(randInt(120, 260));
return true;
}
} catch (e) {
// ignore
}
return false;
}
async function pickEggsRandomly(count, min, max) {
const picks = buildUniqueRandomSetNoAdjacent(count, min, max);
if (!picks) {
throw new Error("Could not generate random eggs with the no-adjacent rule. Expand range or lower egg count.");
}
await clearEggsIfPossible();
setStatus("Picking eggs randomly...");
log("Picks:", picks.slice().sort((a, b) => a - b));
for (const n of picks) {
const el = findEggElement(n);
if (!el) throw new Error("Could not find egg element for #" + String(n));
if (!isEggChecked(el)) {
await sleep(randInt(90, 180));
el.click();
}
}
await sleep(randInt(120, 260));
return true;
}
async function useQuickPick() {
const qpBtn = findClickableByLabel("Quick Pick");
if (!qpBtn) throw new Error('Could not find "Quick Pick" button');
setStatus("Quick Pick starting...");
await clickWithDelay(qpBtn);
const finalizeDelay = randInt(cfg.quickPickFinalizeMinMs, cfg.quickPickFinalizeMaxMs);
setStatus("Quick Pick running...");
await sleep(finalizeDelay);
setStatus("Quick Pick finalizing...");
await clickWithDelay(qpBtn);
await sleep(randInt(150, 350));
return true;
}
function parseNumbersFromText(text) {
const matches = String(text || "").match(/\b\d+\b/g);
if (!matches) return [];
return matches.map((x) => Number.parseInt(x, 10)).filter((n) => Number.isFinite(n));
}
function extractWinnersOnlyNow() {
const winnersDiv = document.getElementById("prize_chart");
if (!winnersDiv) return [];
const winners = parseNumbersFromText(winnersDiv.textContent);
return winners;
}
function winnersToLine(winners) {
return winners.map((n) => String(n)).join(" ");
}
function appendWinnersLine(line) {
if (!cfg.enableWinnersLog) return;
const lines = loadLogLines();
lines.push(line);
const cap = clampInt(cfg.maxLogLines, 10, 50000, DEFAULT_CFG.maxLogLines);
while (lines.length > cap) lines.shift();
saveLogLines(lines);
refreshLogUI();
}
function refreshLogUI() {
const box = document.getElementById("gk_log_box");
const countEl = document.getElementById("gk_log_count");
if (!box || !countEl) return;
const lines = loadLogLines();
countEl.textContent = String(lines.length);
box.value = lines.join("\n");
box.scrollTop = box.scrollHeight;
}
function getWinnersDiv() {
return document.getElementById("prize_chart");
}
function getFullWinnersIfReady() {
const expected = clampInt(cfg.winnersExpectedCount, 1, 50, 10);
const winners = extractWinnersOnlyNow();
if (winners.length < expected) return null;
return winners.slice(0, expected);
}
async function waitForFullWinners() {
const expected = clampInt(cfg.winnersExpectedCount, 1, 50, 10);
const timeoutMs = clampInt(cfg.winnersWaitTimeoutMs, 1000, 60000, 15000);
const pollMs = clampInt(cfg.winnersPollIntervalMs, 50, 1000, 120);
const start = Date.now();
const immediate = getFullWinnersIfReady();
if (immediate) return immediate;
const prizeChart = getWinnersDiv();
let observer = null;
let done = false;
const winnerPromise = new Promise((resolve) => {
if (!prizeChart || typeof MutationObserver !== "function") {
resolve(null);
return;
}
observer = new MutationObserver(() => {
if (done) return;
const w = getFullWinnersIfReady();
if (w) {
done = true;
resolve(w);
}
});
observer.observe(prizeChart, {
childList: true,
subtree: true,
characterData: true
});
});
while (Date.now() - start < timeoutMs) {
const got = getFullWinnersIfReady();
if (got) {
done = true;
if (observer) observer.disconnect();
return got;
}
const maybeFromObs = await Promise.race([winnerPromise, sleep(pollMs)]);
if (Array.isArray(maybeFromObs) && maybeFromObs.length >= expected) {
done = true;
if (observer) observer.disconnect();
return maybeFromObs.slice(0, expected);
}
}
done = true;
if (observer) observer.disconnect();
return null;
}
async function tryLogWinnersIfNewReliable() {
if (!cfg.enableWinnersLog) return false;
const winners = await waitForFullWinners();
if (!winners || winners.length === 0) return false;
const line = winnersToLine(winners);
if (line === run.lastWinnersLine) return false;
run.lastWinnersLine = line;
saveRun();
appendWinnersLine(line);
return true;
}
async function doSelectionStep() {
if (!isSelectionScreen()) return false;
if (!forceBet(cfg.betAmount)) throw new Error("Could not find bet input");
const hatchBtn = findClickableByLabel("Hatch those Eggs!");
if (!hatchBtn) throw new Error('Could not find "Hatch those Eggs!"');
await sleep(randInt(120, 260));
if (cfg.pickMethod === "quickpick") {
await useQuickPick();
} else {
const count = clampInt(cfg.randomEggCount, 2, 10, 10);
const min = clampInt(cfg.randomEggMin, 1, 80, 1);
const max = clampInt(cfg.randomEggMax, min, 80, 80);
await pickEggsRandomly(count, min, max);
}
if (!forceBet(cfg.betAmount)) throw new Error("Could not find bet input");
run.step = "waiting_results";
run.stepDueAt = Date.now() + clampInt(cfg.hatchWaitSeconds, 1, 300, 10) * 1000;
saveRun();
setStatus("Submitting Hatch...");
await clickWithDelay(hatchBtn);
return true;
}
async function doWaitingResultsStep() {
const due = run.stepDueAt || 0;
const remainingMs = Math.max(0, due - Date.now());
if (remainingMs > 0) {
setStatus("Waiting for hatch: " + String(Math.ceil(remainingMs / 1000)) + "s");
return true;
}
run.step = "results";
run.stepDueAt = 0;
saveRun();
return true;
}
async function doResultsStep() {
if (!isResultsScreen()) return false;
setStatus("Collecting winners...");
const logged = await tryLogWinnersIfNewReliable();
if (logged) log("Logged winners:", run.lastWinnersLine);
const playAgain = findClickableByLabel("Play Again!");
if (!playAgain) throw new Error('Could not find "Play Again!"');
run.rounds = clampInt(run.rounds, 0, 999999999, 0) + 1;
saveRun();
setRounds(run.rounds);
if (cfg.maxRounds > 0 && run.rounds >= cfg.maxRounds) {
stopRunning("Max rounds reached (" + String(cfg.maxRounds) + ").");
return true;
}
// Allow next results to log even if it repeats later
run.lastWinnersLine = "";
saveRun();
run.step = "selection";
run.stepDueAt = 0;
saveRun();
setStatus('Clicking "Play Again!"...');
await clickWithDelay(playAgain);
return true;
}
let ticking = false;
async function tick() {
if (ticking) return;
ticking = true;
try {
if (!run.running) return;
if (run.step === "selection") {
const did = await doSelectionStep();
if (!did) setStatus("Waiting for selection screen...");
} else if (run.step === "waiting_results") {
await doWaitingResultsStep();
} else if (run.step === "results") {
const did = await doResultsStep();
if (!did) setStatus("Waiting for results screen...");
} else {
run.step = isSelectionScreen() ? "selection" : (isResultsScreen() ? "results" : "selection");
saveRun();
}
} catch (e) {
const msg = e && e.message ? e.message : "Unknown error";
errorLog("Tick error:", e);
setError("Error: " + msg);
if (cfg.stopOnError) {
stopRunning("Stopped due to error.");
} else {
warn("Continuing after error because stopOnError=false");
}
} finally {
ticking = false;
if (run.running) window.setTimeout(() => tick(), 250);
}
}
async function copyTextToClipboard(text) {
const t = String(text || "");
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
await navigator.clipboard.writeText(t);
return true;
}
} catch (e) {
// ignore
}
const ta = document.createElement("textarea");
ta.value = t;
ta.style.position = "fixed";
ta.style.left = "-9999px";
ta.style.top = "0";
document.body.appendChild(ta);
ta.focus();
ta.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(ta);
return ok;
}
function updateButtons() {
const startBtn = document.getElementById("gk_start");
const stopBtn = document.getElementById("gk_stop");
const onceBtn = document.getElementById("gk_once");
if (startBtn) startBtn.disabled = run.running;
if (onceBtn) onceBtn.disabled = run.running;
if (stopBtn) stopBtn.disabled = !run.running;
}
function buildUI() {
const panel = document.createElement("div");
panel.id = "gk_panel";
panel.style.position = "fixed";
panel.style.right = "12px";
panel.style.bottom = "12px";
panel.style.zIndex = "999999";
panel.style.width = "440px";
panel.style.background = "rgba(0,0,0,0.86)";
panel.style.color = "#fff";
panel.style.borderRadius = "12px";
panel.style.padding = "10px";
panel.style.font = "13px/1.35 Arial, sans-serif";
panel.style.boxShadow = "0 10px 26px rgba(0,0,0,0.35)";
panel.innerHTML = `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
<div style="font-weight:700;">Grarrl Keno Auto</div>
<button type="button" id="gk_toggle" style="cursor:pointer; border:0; border-radius:10px; padding:5px 9px;">Hide</button>
</div>
<div id="gk_body" style="margin-top:10px;">
<div style="display:flex; gap:8px; margin-bottom:8px;">
<button type="button" id="gk_start" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Start</button>
<button type="button" id="gk_stop" style="width:90px; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Stop</button>
</div>
<div style="display:flex; gap:8px; margin-bottom:10px;">
<button type="button" id="gk_once" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Run once</button>
<div style="width:90px; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.1); border-radius:12px; padding:10px;">
<div style="text-align:center;">
<div style="font-size:11px; opacity:0.85;">Rounds</div>
<div id="gk_rounds" style="font-weight:800;">0</div>
</div>
</div>
</div>
<div style="background:rgba(255,255,255,0.08); border-radius:12px; padding:10px; margin-bottom:10px;">
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Bet (NP)</label>
<input id="gk_bet" type="number" min="1" max="999999" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Pick method</label>
<select id="gk_pick" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
<option value="quickpick">Quick Pick</option>
<option value="random">Random eggs</option>
</select>
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">No adjacent numbers</label>
<select id="gk_noadj" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div id="gk_random_box" style="background:rgba(255,255,255,0.06); border-radius:10px; padding:10px; margin-bottom:8px;">
<div style="font-weight:700; margin-bottom:8px;">Random egg settings</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Egg count</label>
<input id="gk_rand_count" type="number" min="2" max="10" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Egg min</label>
<input id="gk_rand_min" type="number" min="1" max="80" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center;">
<label style="width:210px;">Egg max</label>
<input id="gk_rand_max" type="number" min="1" max="80" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Wait for hatch (sec)</label>
<input id="gk_wait" type="number" min="1" max="300" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Click delay min (ms)</label>
<input id="gk_cd_min" type="number" min="0" max="5000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Click delay max (ms)</label>
<input id="gk_cd_max" type="number" min="0" max="5000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">QuickPick finalize min (ms)</label>
<input id="gk_qp_min" type="number" min="0" max="10000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">QuickPick finalize max (ms)</label>
<input id="gk_qp_max" type="number" min="0" max="10000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Max rounds (0 = infinite)</label>
<input id="gk_max_rounds" type="number" min="0" max="1000000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Stop on error</label>
<select id="gk_stoperr" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Verbose log</label>
<select id="gk_verbose" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
<label style="width:210px;">Auto resume after refresh</label>
<select id="gk_resume" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
<option value="no">No</option>
<option value="yes">Yes</option>
</select>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<label style="width:210px;">Max log lines</label>
<input id="gk_log_max" type="number" min="10" max="50000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
</div>
<div style="display:flex; gap:8px; margin-top:10px;">
<button type="button" id="gk_save" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Save settings</button>
<button type="button" id="gk_reset" style="width:120px; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Reset</button>
</div>
</div>
<div style="background:rgba(255,255,255,0.08); border-radius:12px; padding:10px; margin-bottom:10px;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px;">
<div style="font-weight:700;">Winners log <span style="opacity:0.85;">(lines: <span id="gk_log_count">0</span>)</span></div>
<div style="display:flex; gap:8px;">
<button type="button" id="gk_log_copy" style="cursor:pointer; border:0; border-radius:10px; padding:6px 10px; font-weight:700;">Copy</button>
<button type="button" id="gk_log_clear" style="cursor:pointer; border:0; border-radius:10px; padding:6px 10px; font-weight:700;">Clear</button>
</div>
</div>
<textarea id="gk_log_box" readonly style="width:100%; height:150px; border:0; border-radius:10px; padding:10px; resize:vertical;"></textarea>
</div>
<div style="padding:10px; background:rgba(255,255,255,0.08); border-radius:12px;">
Status: <span id="gk_status_text">Ready</span>
</div>
</div>
`;
document.body.appendChild(panel);
const toggleBtn = document.getElementById("gk_toggle");
const body = document.getElementById("gk_body");
toggleBtn.addEventListener("click", () => {
const hidden = body.style.display === "none";
body.style.display = hidden ? "block" : "none";
toggleBtn.textContent = hidden ? "Hide" : "Show";
});
const betEl = document.getElementById("gk_bet");
const pickEl = document.getElementById("gk_pick");
const noAdjEl = document.getElementById("gk_noadj");
const randBox = document.getElementById("gk_random_box");
const randCountEl = document.getElementById("gk_rand_count");
const randMinEl = document.getElementById("gk_rand_min");
const randMaxEl = document.getElementById("gk_rand_max");
const waitEl = document.getElementById("gk_wait");
const cdMinEl = document.getElementById("gk_cd_min");
const cdMaxEl = document.getElementById("gk_cd_max");
const qpMinEl = document.getElementById("gk_qp_min");
const qpMaxEl = document.getElementById("gk_qp_max");
const maxRoundsEl = document.getElementById("gk_max_rounds");
const stopErrEl = document.getElementById("gk_stoperr");
const verboseEl = document.getElementById("gk_verbose");
const resumeEl = document.getElementById("gk_resume");
const logMaxEl = document.getElementById("gk_log_max");
betEl.value = String(cfg.betAmount);
pickEl.value = cfg.pickMethod === "quickpick" ? "quickpick" : "random";
noAdjEl.value = cfg.noAdjacentNumbers ? "yes" : "no";
randCountEl.value = String(cfg.randomEggCount);
randMinEl.value = String(cfg.randomEggMin);
randMaxEl.value = String(cfg.randomEggMax);
waitEl.value = String(cfg.hatchWaitSeconds);
cdMinEl.value = String(cfg.clickDelayMinMs);
cdMaxEl.value = String(cfg.clickDelayMaxMs);
qpMinEl.value = String(cfg.quickPickFinalizeMinMs);
qpMaxEl.value = String(cfg.quickPickFinalizeMaxMs);
maxRoundsEl.value = String(cfg.maxRounds);
stopErrEl.value = cfg.stopOnError ? "yes" : "no";
verboseEl.value = cfg.verboseLog ? "yes" : "no";
resumeEl.value = cfg.autoResume ? "yes" : "no";
logMaxEl.value = String(cfg.maxLogLines);
function syncRandomBoxVisibility() {
randBox.style.display = pickEl.value === "random" ? "block" : "none";
}
pickEl.addEventListener("change", syncRandomBoxVisibility);
syncRandomBoxVisibility();
setRounds(run.rounds);
refreshLogUI();
updateButtons();
document.getElementById("gk_start").addEventListener("click", () => startRunning());
document.getElementById("gk_stop").addEventListener("click", () => stopRunning("Stopped by you."));
document.getElementById("gk_once").addEventListener("click", async () => {
const wasRunning = run.running;
run.running = false;
saveRun();
try {
if (isSelectionScreen()) {
setStatus("Running once...");
await doSelectionStep();
setStatus("Submitted once. Click Start to keep looping.");
} else if (isResultsScreen()) {
setStatus("Running once...");
await doResultsStep();
setStatus("Clicked Play Again once.");
} else {
setStatus("Page not recognized for Run once.");
}
} catch (e) {
setError("Error: " + (e && e.message ? e.message : "Unknown error"));
} finally {
run.running = wasRunning;
saveRun();
updateButtons();
}
});
document.getElementById("gk_save").addEventListener("click", () => {
cfg.betAmount = clampInt(betEl.value, 1, 999999, DEFAULT_CFG.betAmount);
cfg.pickMethod = pickEl.value === "quickpick" ? "quickpick" : "random";
cfg.noAdjacentNumbers = noAdjEl.value === "yes";
cfg.randomEggCount = clampInt(randCountEl.value, 2, 10, DEFAULT_CFG.randomEggCount);
cfg.randomEggMin = clampInt(randMinEl.value, 1, 80, DEFAULT_CFG.randomEggMin);
cfg.randomEggMax = clampInt(randMaxEl.value, cfg.randomEggMin, 80, DEFAULT_CFG.randomEggMax);
cfg.hatchWaitSeconds = clampInt(waitEl.value, 1, 300, DEFAULT_CFG.hatchWaitSeconds);
cfg.clickDelayMinMs = clampInt(cdMinEl.value, 0, 5000, DEFAULT_CFG.clickDelayMinMs);
cfg.clickDelayMaxMs = clampInt(cdMaxEl.value, 0, 5000, DEFAULT_CFG.clickDelayMaxMs);
if (cfg.clickDelayMaxMs < cfg.clickDelayMinMs) {
const tmp = cfg.clickDelayMinMs;
cfg.clickDelayMinMs = cfg.clickDelayMaxMs;
cfg.clickDelayMaxMs = tmp;
}
cfg.quickPickFinalizeMinMs = clampInt(qpMinEl.value, 0, 10000, DEFAULT_CFG.quickPickFinalizeMinMs);
cfg.quickPickFinalizeMaxMs = clampInt(qpMaxEl.value, 0, 10000, DEFAULT_CFG.quickPickFinalizeMaxMs);
if (cfg.quickPickFinalizeMaxMs < cfg.quickPickFinalizeMinMs) {
const tmp2 = cfg.quickPickFinalizeMinMs;
cfg.quickPickFinalizeMinMs = cfg.quickPickFinalizeMaxMs;
cfg.quickPickFinalizeMaxMs = tmp2;
}
cfg.maxRounds = clampInt(maxRoundsEl.value, 0, 1000000, DEFAULT_CFG.maxRounds);
cfg.stopOnError = stopErrEl.value === "yes";
cfg.verboseLog = verboseEl.value === "yes";
cfg.autoResume = resumeEl.value === "yes";
cfg.maxLogLines = clampInt(logMaxEl.value, 10, 50000, DEFAULT_CFG.maxLogLines);
saveCfg();
if (isSelectionScreen()) forceBet(cfg.betAmount);
setStatus("Settings saved.");
refreshLogUI();
updateButtons();
});
document.getElementById("gk_reset").addEventListener("click", () => {
cfg = { ...DEFAULT_CFG };
saveCfg();
betEl.value = String(cfg.betAmount);
pickEl.value = cfg.pickMethod;
noAdjEl.value = cfg.noAdjacentNumbers ? "yes" : "no";
randCountEl.value = String(cfg.randomEggCount);
randMinEl.value = String(cfg.randomEggMin);
randMaxEl.value = String(cfg.randomEggMax);
waitEl.value = String(cfg.hatchWaitSeconds);
cdMinEl.value = String(cfg.clickDelayMinMs);
cdMaxEl.value = String(cfg.clickDelayMaxMs);
qpMinEl.value = String(cfg.quickPickFinalizeMinMs);
qpMaxEl.value = String(cfg.quickPickFinalizeMaxMs);
maxRoundsEl.value = String(cfg.maxRounds);
stopErrEl.value = cfg.stopOnError ? "yes" : "no";
verboseEl.value = cfg.verboseLog ? "yes" : "no";
resumeEl.value = cfg.autoResume ? "yes" : "no";
logMaxEl.value = String(cfg.maxLogLines);
syncRandomBoxVisibility();
setStatus("Settings reset.");
refreshLogUI();
});
document.getElementById("gk_log_copy").addEventListener("click", async () => {
const lines = loadLogLines().join("\n");
const ok = await copyTextToClipboard(lines);
setStatus(ok ? "Log copied to clipboard." : "Copy failed in this browser.");
});
document.getElementById("gk_log_clear").addEventListener("click", () => {
saveLogLines([]);
run.lastWinnersLine = "";
saveRun();
refreshLogUI();
setStatus("Log cleared.");
});
}
buildUI();
if (isSelectionScreen()) forceBet(cfg.betAmount);
// If page loads already showing results, try to log once fully ready
(async () => {
if (document.getElementById("prize_chart")) {
await tryLogWinnersIfNewReliable();
refreshLogUI();
}
})();
if (run.running && cfg.autoResume) {
setStatus("Auto resume active. Continuing...");
updateButtons();
tick();
} else if (run.running && !cfg.autoResume) {
stopRunning("Page refreshed. Auto resume is off, so it stopped.");
} else {
setStatus("Ready. Click Start to begin looping.");
}
})();