Neopets Auto Zapper Pro V2

Auto-zapper with persistent pet memory and lab2 cooldown sync

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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;
  })();
})();