Neopets Auto Zapper Pro V2

Auto-zapper with persistent pet memory and lab2 cooldown sync

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Neopets Auto Zapper Pro V2
// @namespace    Neopets Auto Zapper Pro
// @version      2.1.1
// @description  Auto-zapper with persistent pet memory and lab2 cooldown sync
// @author       combined: badsk8700o / thezuki10 / nadinejun0 + modifications
// @match        https://www.neopets.com/lab.phtml
// @match        https://www.neopets.com/lab2.phtml
// @match        https://www.neopets.com/process_lab2.phtml
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ========== STORAGE (Using GM for persistence; fallback/sync with localStorage) ==========
  const STORE = {
    getPet: () => {
      const gm = GM_getValue('zapPet', null);
      if (gm) return gm;
      // fallback to older script
      try { return localStorage.getItem('lastSelectedPet') || null; } catch (e) { return null; }
    },
    setPet: (v) => {
      GM_setValue('zapPet', v);
      try { localStorage.setItem('lastSelectedPet', v); } catch (e) {}
    },
    clearPet: () => {
      GM_setValue('zapPet', null);
      try { localStorage.removeItem('lastSelectedPet'); } catch (e) {}
    },

    getCount: () => parseInt(GM_getValue('zapCount', '0'), 10),
    setCount: (n) => GM_setValue('zapCount', String(n)),
    incCount: () => STORE.setCount(STORE.getCount() + 1),

    getDate: () => GM_getValue('zapDate', null),
    setDate: (d) => GM_setValue('zapDate', d),

    getRemaining: () => parseInt(GM_getValue('zapRemaining', '0'), 10),
    setRemaining: (n) => GM_setValue('zapRemaining', String(n)),

    getTodayNST: () => {
      const now = new Date();
      const nst = new Date(now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
      return nst.toISOString().split('T')[0];
    },

    checkAndResetDay: () => {
      const today = STORE.getTodayNST();
      const stored = STORE.getDate();
      if (stored !== today) {
        STORE.setDate(today);
        STORE.setCount(0);
        return true;
      }
      return false;
    },
  };

  function log(...args) { console.log('[AutoZapper]', ...args); }
  function randDelay(min = 800, max = 1800) { return min + Math.floor(Math.random() * (max - min + 1)); }

  function getZapsFromPage() {
    const text = document.body.innerText || '';
    let match = text.match(/Zaps Left Today:\s*(\d+)\s*\/\s*(\d+)/i);
    if (match) return { remaining: parseInt(match[1], 10), total: parseInt(match[2], 10) };
    match = text.match(/(\d+)\s*\/\s*(\d+)\s*(?:zaps?|Zaps?)/i);
    if (match) return { remaining: parseInt(match[1], 10), total: parseInt(match[2], 10) };
    log('Could not find zap count; defaulting to 0');
    return { remaining: 0, total: 0 };
  }

  // ========== LAB.PHTML ==========
  function handleLabPage() {
    if (!window.location.pathname.includes('lab.phtml')) return false;
    log('On lab.phtml - going to lab2');
    setTimeout(() => {
      let form = document.querySelector('form[action="lab2.phtml"]') || document.querySelector('form[action*="lab2"]');
      if (!form) {
        const allForms = document.querySelectorAll('form');
        allForms.forEach((f) => { if (!form && (f.action && f.action.includes('lab2'))) form = f; });
      }
      if (form) form.submit();
      else setTimeout(() => { window.location.href = 'https://www.neopets.com/lab2.phtml'; }, 1000);
    }, 1500);
    return true;
  }

  // ========== UTIL: Wait for STOP cue and cooldown ==========
  // Looks for visible text "STOP - Choose Different Pet" and parses nearby countdown or waits until it's removed.
  function waitForStopCueThenSubmit(petName, form, userStoppedFlag) {
    // If user stopped via UI, do nothing
    if (userStoppedFlag && userStoppedFlag.value) {
      log('User cancelled auto-submit (stop flag). Aborting submit.');
      return;
    }

    // try to find node that contains the STOP phrase
    const findStopNode = () => Array.from(document.querySelectorAll('body *')).find(el => {
      try { return (el.innerText || '').match(/STOP\s*-\s*Choose Different Pet/i); } catch (e) { return false; }
    });

    const stopNode = findStopNode();

    if (!stopNode) {
      // no STOP cue on page. submit after small delay
      log('No STOP cue found. Submitting after random delay.');
      setTimeout(() => {
        if (!(userStoppedFlag && userStoppedFlag.value)) form.submit();
      }, randDelay());
      return;
    }

    log('STOP cue found. Attempting to parse countdown or observe DOM.');

    // Try to parse seconds from text near the node (parent or same container)
    const candidateText = (stopNode.parentElement && stopNode.parentElement.innerText) ? stopNode.parentElement.innerText : stopNode.innerText;
    let secs = null;

    // mm:ss or ss
    const mmss = candidateText.match(/(\d{1,2}):(\d{2})/);
    if (mmss) secs = parseInt(mmss[1], 10) * 60 + parseInt(mmss[2], 10);
    else {
      const sMatch = candidateText.match(/(\d{1,4})\s*seconds?/i);
      if (sMatch) secs = parseInt(sMatch[1], 10);
    }

    if (secs !== null && !Number.isNaN(secs)) {
      const ms = (secs + 1) * 1000; // small buffer
      log('Parsed countdown seconds:', secs, 'waiting', ms, 'ms before submit');
      setTimeout(() => {
        if (!(userStoppedFlag && userStoppedFlag.value)) form.submit();
      }, ms);
      return;
    }

    // If no numeric countdown, observe DOM for removal or change
    const observer = new MutationObserver((mutationsList) => {
      // Re-check existence of STOP text
      const still = findStopNode();
      if (!still) {
        observer.disconnect();
        log('STOP cue removed from page. Proceeding to submit.');
        setTimeout(() => {
          if (!(userStoppedFlag && userStoppedFlag.value)) form.submit();
        }, randDelay());
      } else {
        // optional: check again for digits as node updates
        const txt = still.innerText || still.parentElement && still.parentElement.innerText || '';
        const s = txt.match(/(\d{1,2}):(\d{2})/) || txt.match(/(\d{1,4})\s*seconds?/i);
        if (s) {
          observer.disconnect();
          let secs2;
          if (s.length === 3) secs2 = parseInt(s[1], 10) * 60 + parseInt(s[2], 10);
          else secs2 = parseInt(s[1], 10);
          const ms2 = (secs2 + 1) * 1000;
          log('Found countdown while observing. Waiting', ms2, 'ms then submit.');
          setTimeout(() => {
            if (!(userStoppedFlag && userStoppedFlag.value)) form.submit();
          }, ms2);
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true, characterData: true });
    // Safety fallback: after 40s, stop observing and submit if not user stopped
    setTimeout(() => {
      try { observer.disconnect(); } catch (e) {}
      if (!(userStoppedFlag && userStoppedFlag.value)) {
        log('Fallback timeout reached. Submitting.');
        setTimeout(() => { if (!(userStoppedFlag && userStoppedFlag.value)) form.submit(); }, randDelay());
      }
    }, 40000);
  }

  // ========== LAB2.PHTML ==========
  function handleLab2Page() {
    if (!window.location.pathname.includes('lab2.phtml')) return false;
    log('On lab2.phtml - build UI and manage pet memory');

    const isNewDay = STORE.checkAndResetDay();
    const storedPet = STORE.getPet();
    const used = STORE.getCount();
    const { remaining, total } = getZapsFromPage();

    log('Day reset?', isNewDay, '| Stored pet:', storedPet, '| Used:', used, '| Remaining:', remaining);

    const petListRoot = document.querySelector('#bxlist');
    const form = document.querySelector('form[action="process_lab2.phtml"]');
    if (!petListRoot || !form) return false;

    const slider = document.querySelector('.bx-wrapper');
    if (slider) slider.style.display = 'none';

    // small flag object to allow closures to modify stopped state
    const userStoppedFlag = { value: false };

    // Counter box (kept visually similar)
    const counterBox = document.createElement('div');
    counterBox.style.cssText = 'text-align:center;margin:16px auto;padding:12px;background:#e9ecef;border-radius:8px;max-width:600px;font-weight:bold;font-size:14px;';
    counterBox.innerHTML = `<div>Total Zaps Today: <span>${total}</span></div><div>Used: <span>${used}</span> | Remaining: <span>${remaining}</span></div>`;

    // Grid container (keeps much of original structure)
    const gridContainer = document.createElement('div');
    gridContainer.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;padding:16px;max-width:900px;margin:16px auto;';

    const petLis = Array.from(petListRoot.querySelectorAll('li'));
    const petCards = [];

    petLis.forEach(li => {
      const radio = li.querySelector('input[type="radio"]');
      const img = li.querySelector('img');
      const label = li.querySelector('b');
      if (!radio || !img) return;
      const petName = radio.value || (label && label.innerText);
      if (!petName) return;

      const card = document.createElement('div');
      card.style.cssText = 'border:3px solid #ccc;border-radius:8px;padding:12px;background:white;cursor:pointer;text-align:center;transition:all .2s;box-shadow:0 2px 4px rgba(0,0,0,0.1);';
      const imgDiv = document.createElement('div');
      imgDiv.style.cssText = 'height:120px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;';
      const clonedImg = img.cloneNode(true);
      clonedImg.style.maxWidth = '100%'; clonedImg.style.maxHeight = '100%'; clonedImg.style.objectFit = 'contain';
      imgDiv.appendChild(clonedImg);
      const nameDiv = document.createElement('div');
      nameDiv.textContent = petName;
      nameDiv.style.cssText = 'font-weight:bold;font-size:12px;word-wrap:break-word;';
      card.appendChild(imgDiv); card.appendChild(nameDiv);

      card.addEventListener('mouseenter', () => { if (!radio.checked) card.style.backgroundColor = '#f0f0f0'; });
      card.addEventListener('mouseleave', () => { if (!radio.checked) card.style.backgroundColor = 'white'; });

      card.addEventListener('click', () => {
        // user clicked: clear any auto-stop and register pet
        document.querySelectorAll('#bxlist input[type="radio"]').forEach(r => r.checked = false);
        petCards.forEach(c => { c.card.style.borderColor = '#ccc'; c.card.style.backgroundColor = 'white'; });
        radio.checked = true;
        STORE.setPet(petName);
        try { localStorage.setItem('lastSelectedPet', petName); } catch (e) {}
        STORE.setRemaining(remaining);
        card.style.borderColor = '#FFD700'; card.style.backgroundColor = '#FFFACD';
        // Auto-submit after a small delay, but respect any STOP cue
        setTimeout(() => {
          waitForStopCueThenSubmit(petName, form, userStoppedFlag);
        }, randDelay());
      });

      petCards.push({ card, petName, radio });
      gridContainer.appendChild(card);

      if (storedPet === petName) {
        radio.checked = true;
        card.style.borderColor = '#FFD700';
        card.style.backgroundColor = '#FFFACD';
      }
    });

    // Insert UI
    const zapInfo = form.querySelector('p[style*="text-align"]');
    if (zapInfo) form.insertBefore(gridContainer, zapInfo);
    else form.insertBefore(gridContainer, form.firstChild);
    form.insertBefore(counterBox, form.firstChild);

    // STOP button for UI control (clears memory and sets userStoppedFlag)
    function addStopButtonUI(container) {
      const stopBtn = document.createElement('button');
      stopBtn.textContent = '⏸️ Stop Zapping This Pet';
      stopBtn.style.cssText = 'display:block;margin:10px auto;padding:8px 16px;cursor:pointer;border-radius:6px;background:#ff9800;color:white;border:none;font-weight:bold;font-size:12px;';
      stopBtn.addEventListener('click', () => {
        STORE.clearPet();
        userStoppedFlag.value = true;
        stopBtn.remove();
        log('User cleared stored pet and set stop flag');
      });
      container.appendChild(stopBtn);
    }

    // If it's a new day and a storedPet exists, show countdown notice then submit respecting STOP cue
    if (isNewDay && storedPet && remaining > 0) {
      showNewDayCountdown(form, storedPet, counterBox, userStoppedFlag);
    } else if (storedPet && remaining > 0) {
      // same-day auto-continue with stored pet but wait for in-page STOP cue timing
      log('Same day - proceeding to auto-submit stored pet (respecting STOP cue if present)');
      // choose the pet radio visually and then wait for STOP cue before final submit
      const radio = Array.from(document.querySelectorAll('#bxlist input[type="radio"]')).find(r => r.value === storedPet);
      if (radio) radio.checked = true;
      waitForStopCueThenSubmit(storedPet, form, userStoppedFlag);
    }

    if (storedPet && !isNewDay && remaining > 0) addStopButtonUI(counterBox);

    return true;
  }

  // keep original new-day UI but wire in userStoppedFlag so stop UI works
  function showNewDayCountdown(form, petName, counterBox, userStoppedFlag) {
    const notice = document.createElement('div');
    notice.style.cssText = 'background:#fff3cd;border:3px solid #ffc107;border-radius:8px;padding:16px;margin:16px auto;text-align:center;max-width:600px;font-weight:bold;';
    const title = document.createElement('div');
    title.innerHTML = '<strong style="font-size:18px;">NEW DAY! Auto-zapping starts in...</strong>'; title.style.marginBottom = '10px';
    const countdown = document.createElement('div'); countdown.style.cssText = 'font-size:24px;color:#d9534f;margin:10px 0;'; let secs = 10; countdown.textContent = secs;
    const stopBtn = document.createElement('button');
    stopBtn.textContent = '❌ STOP - Choose Different Pet';
    stopBtn.style.cssText = 'margin-top:12px;padding:10px 16px;cursor:pointer;border-radius:6px;background:#dc3545;color:white;border:none;font-weight:bold;font-size:14px;';
    notice.appendChild(title); notice.appendChild(countdown); notice.appendChild(stopBtn);
    counterBox.insertBefore(notice, counterBox.firstChild);

    stopBtn.addEventListener('click', () => {
      userStoppedFlag.value = true;
      STORE.clearPet();
      notice.remove();
      log('User clicked STOP in NEW DAY notice; auto-start cancelled.');
    });

    const interval = setInterval(() => {
      secs--;
      countdown.textContent = secs;
      if (secs <= 0) {
        clearInterval(interval);
        notice.remove();
        // After countdown, find and use the stored pet; respect in-page STOP cue that the real site may show
        waitForStopCueThenSubmit(petName, form, userStoppedFlag);
      }
    }, 1000);
  }

  // ========== PROCESS_LAB2.PHTML ==========
  function handleResultsPage() {
    if (!window.location.pathname.includes('process_lab2.phtml')) return false;
    log('On results page - incrementing count');
    STORE.incCount();
    let remaining = STORE.getRemaining() - 1;
    remaining = Math.max(0, remaining);
    STORE.setRemaining(remaining);
    log('Results - Remaining zaps (from memory):', remaining);
    if (remaining <= 0) {
      STORE.clearPet();
      log('No zaps left - cleared stored pet');
    }

    const statusBox = document.createElement('div');
    statusBox.style.cssText = 'background:#d4edda;border:3px solid #28a745;border-radius:8px;padding:20px;text-align:center;margin:20px auto;max-width:600px;font-weight:bold;';
    const msg = document.createElement('div'); msg.style.fontSize = '16px';
    msg.innerHTML = remaining > 0 ? '✅ Zap successful! Returning to Lab...' : '🎉 All zaps used for today!';
    const countdown = document.createElement('div'); countdown.style.cssText = 'font-size:24px;color:#d9534f;margin-top:12px;'; let secs = 10; countdown.textContent = secs;
    statusBox.appendChild(msg); statusBox.appendChild(countdown);
    const centerTag = document.querySelector('center');
    if (centerTag) centerTag.insertBefore(statusBox, centerTag.firstChild); else document.body.insertBefore(statusBox, document.body.firstChild);

    let interval;
    const redirectToLab = () => {
      if (interval) clearInterval(interval);
      log('Redirecting to lab.phtml');
      window.location.replace('https://www.neopets.com/lab.phtml');
      setTimeout(() => { window.location.href = 'https://www.neopets.com/lab.phtml'; }, 800);
      setTimeout(() => { window.location.assign('https://www.neopets.com/lab.phtml'); }, 1600);
      setTimeout(() => { const link = document.createElement('a'); link.href = 'https://www.neopets.com/lab.phtml'; link.style.display = 'none'; document.body.appendChild(link); link.click(); link.remove(); }, 2400);
      setTimeout(() => { history.back(); }, 3200);
    };

    interval = setInterval(() => {
      secs--;
      countdown.textContent = secs;
      if (secs <= 0) redirectToLab();
    }, 1000);

    return true;
  }

  // ========== INIT ==========
  (async function init() {
    STORE.checkAndResetDay();
    if (handleLabPage()) return;
    if (handleLab2Page()) return;
    if (handleResultsPage()) return;
  })();
})();