NeoDeck Auto-Add

Automatically adds cards from the inventory to the NeoDeck.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         NeoDeck Auto-Add
// @namespace    MoonLord
// @version      2.5
// @description  Automatically adds cards from the inventory to the NeoDeck.
// @match        https://www.neopets.com/inventory.phtml*
// @match        https://neopets.com/inventory.phtml*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Patterns that match card backs to detect collectible/trading cards
    const CARD_BACK_PATTERNS = [
        'bluetradingcardback','redtradingcardback','pinktradingcardback',
        'greentradingcardback','purpletradingcardback','blacktradingcardback',
        'goldtradingcardback','holotradingcardback'
    ];

    // Timing configuration
    const DELAY_BETWEEN_ACTIONS = 2000;
    const OPEN_ITEM_TIMEOUT = 1200;
    const MENU_INTERACTION_DELAY = 600;
    const SUCCESS_POPUP_TIMEOUT = 3500;
    const SELECT_WAIT_TIMEOUT = 8000;

    // LocalStorage keys
    const STORAGE_KEY = 'neodeck_auto_running';
    const INDEX_KEY = 'neodeck_last_index';

    let processing = false;
    let stopped = false;
    let currentIndex = 0;
    let cards = [];
    let inventoryReady = false;

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    // Save/restore persistence flags
    function persistRunning(v){ try{ localStorage.setItem(STORAGE_KEY,v?'1':'0'); }catch(e){} }
    function readPersist(){ try{return localStorage.getItem(STORAGE_KEY)==='1';}catch(e){return false;} }

    function saveIndex(i){ try{ localStorage.setItem(INDEX_KEY,String(i)); }catch(e){} }
    function loadIndex(){ try{const v=parseInt(localStorage.getItem(INDEX_KEY));return isNaN(v)?0:v;}catch(e){return 0;} }

    function debugLog(...a){ console.log('[NeoDeckAuto]',...a); }

    // Detect inventory item nodes
    function getItemNodes(){
        return Array.from(document.querySelectorAll('.lazy.item-img, .item-img, .item'));
    }

    // Extract image/background URL from an item element
    function extractBgUrl(el){
        if(!el) return '';
        const style = el.style.backgroundImage || '';
        const m = style.match(/url\((?:\"|\')?(.*?)(?:\"|\')?\)/);
        if(m) return m[1].toLowerCase();
        const dataImg = el.getAttribute('data-image')||'';
        if(dataImg) return dataImg.toLowerCase();
        const img = el.querySelector('img');
        if(img) return (img.src||img.getAttribute('data-src')||'').toLowerCase();
        return '';
    }

    // Determine whether an item is a card
    function isCard(el){
        if(!el) return false;
        const type = (el.getAttribute('data-itemtype')||'').toLowerCase();
        if(type.includes('collectable card')||type.includes('trading card')) return true;
        const url = extractBgUrl(el);
        if(url) return CARD_BACK_PATTERNS.some(p=>url.includes(p));
        return false;
    }

    // Wait for selector
    async function waitForSelector(sel,timeout=SELECT_WAIT_TIMEOUT,poll=150){
        const t0=Date.now();
        while(Date.now()-t0<timeout){
            const el=document.querySelector(sel);
            if(el) return el;
            await sleep(poll);
        }
        return null;
    }

    // Open inventory item modal
    async function openItem(el){
        try{
            if(typeof window.invView2==='function' && el.id){
                window.invView2(el.id);
            }else{
                el.click();
            }
        }catch(e){ el.click(); }
        await sleep(OPEN_ITEM_TIMEOUT);
    }

    // Select "Put in your NeoDeck" via dropdown menu
    async function chooseNeodeckViaSelect(){
        const select = await waitForSelector('#iteminfo_select_action select[name="action"], select[name="action"]', SELECT_WAIT_TIMEOUT);
        if(!select) throw new Error('select_action_not_found');

        try{ select.focus(); select.click(); }catch(e){}

        let opt=null;
        for(const o of Array.from(select.options)){
            if((o.text||'').toLowerCase().includes('neodeck')){ opt=o; break; }
        }
        if(!opt) throw new Error('neodeck_option_not_found');

        select.value = opt.value;
        select.dispatchEvent(new Event('input',{bubbles:true}));
        select.dispatchEvent(new Event('change',{bubbles:true}));

        await sleep(MENU_INTERACTION_DELAY);
        return true;
    }

    // Click the submit button in the inventory modal
    async function clickSubmit(){
        let btn=document.querySelector('.invitem-submit.button-default__2020.button-yellow__2020');
        if(!btn){
            btn=[...document.querySelectorAll('.invitem-submit')]
                .find(el => (el.textContent||'').trim().toLowerCase()==='submit');
        }
        if(!btn) throw new Error('submit_not_found');
        btn.click();
        return true;
    }

    // Wait for success confirmation popup
    async function waitForSuccessPopup(){
        const t0=Date.now();
        while(Date.now()-t0<SUCCESS_POPUP_TIMEOUT){
            const successNode = Array.from(document.querySelectorAll('div,p,span'))
                .find(el => (el.textContent||'').toLowerCase().includes('you have added'));
            if(successNode){
                const closeBtn =
                    document.querySelector('a.inv-refresh .button-red__2020') ||
                    document.querySelector('a.inv-refresh') ||
                    Array.from(document.querySelectorAll('div.button-red__2020'))
                        .find(el => (el.textContent||'').toLowerCase().includes('close and refresh'));
                return {success:true,closeBtn};
            }
            await sleep(200);
        }
        return {success:false};
    }

    // Close the success popup
    async function closeSuccess(btn){
        try{
            if(btn) btn.click();
            else window.location.reload();
        }catch(e){
            window.location.reload();
        }
        await sleep(1800);
    }

    // Wait until inventory is fully loaded
    async function waitInventoryFullyLoaded(){
        const t0=Date.now();
        while(Date.now()-t0<8000){
            const items = getItemNodes();
            if(items.length > 0){
                inventoryReady = true;
                return true;
            }
            await sleep(150);
        }
        inventoryReady = true;
        return true;
    }

    // Detect changes to inventory (fallback for slower loads)
    function attachInventoryObserver(){
        const target = document.querySelector('#inventory') || document.body;
        const obs = new MutationObserver(() => {
            const items = getItemNodes();
            if(items.length > 0){
                inventoryReady = true;
            }
        });
        obs.observe(target,{childList:true,subtree:true});
    }

    // Process a single card
    async function processCardAt(i){
        if(stopped) throw new Error('stopped');
        const el = cards[i];
        if(!el) return false;

        await openItem(el);

        let chosen=false;
        try{
            await chooseNeodeckViaSelect();
            chosen=true;
        }catch(e){}

        if(!chosen){
            const node=Array.from(document.querySelectorAll('button,a,li,span'))
                .find(n=>(n.textContent||'').toLowerCase().includes('put in your neodeck'));
            if(node){try{node.click();chosen=true;}catch(e){}}
        }

        if(!chosen) throw new Error('neodeck_action_not_chosen');

        await sleep(200);
        await clickSubmit();

        const res=await waitForSuccessPopup();
        if(res.success){
            await closeSuccess(res.closeBtn);
            return true;
        }else{
            window.location.reload();
            await sleep(1200);
            return false;
        }
    }

    // Core loop
    async function processLoop(){
        if(processing) return;
        processing=true;
        stopped=false;
        persistRunning(true);

        await waitInventoryFullyLoaded();
        cards = getItemNodes().filter(isCard);

        currentIndex = loadIndex();
        if(currentIndex >= cards.length) currentIndex=0;

        while(!stopped && currentIndex < cards.length){
            try{
                await processCardAt(currentIndex);
                currentIndex++;
                saveIndex(currentIndex);
                await sleep(DELAY_BETWEEN_ACTIONS);
                cards = getItemNodes().filter(isCard);
            }catch(e){
                if(String(e).toLowerCase().includes('stopped')) break;
                await sleep(1000);
                cards = getItemNodes().filter(isCard);
            }
        }

        processing=false;
        persistRunning(false);
        saveIndex(0);
    }

    // Create floating UI panel
    function createUI(){
        if(document.getElementById('neodeck-ui-wrapper')) return;

        const wrapper=document.createElement('div');
        wrapper.id='neodeck-ui-wrapper';
        wrapper.style.cssText='position:fixed;top:200px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px;';

        const start=document.createElement('button');
        start.textContent='▶ Start NeoDeck';
        start.style.cssText='padding:8px 12px;background:#4CAF50;color:white;border-radius:6px;border:0;cursor:pointer;font-weight:bold;';
        start.onclick=()=>{ if(!processing){ currentIndex=0; saveIndex(0); processLoop(); } };

        const stop=document.createElement('button');
        stop.textContent='⛔ Stop';
        stop.style.cssText='padding:8px 12px;background:#d9534f;color:white;border-radius:6px;border:0;cursor:pointer;font-weight:bold;';
        stop.onclick=()=>{ stopped=true; processing=false; persistRunning(false); saveIndex(0); };

        const label=document.createElement('label');
        label.style.cssText='color:#fff;background:#222;padding:6px;border-radius:6px;text-align:center;';
        label.textContent='Auto-resume on reload: '+(readPersist()?'ON':'OFF');

        wrapper.appendChild(start);
        wrapper.appendChild(stop);
        wrapper.appendChild(label);
        document.body.appendChild(wrapper);
    }

    // Auto-resume feature
    function tryAutoResume(){
        if(readPersist()){
            setTimeout(()=>{processLoop();},600);
        }
    }

    // Initialization
    function init(){
        attachInventoryObserver();
        createUI();
        tryAutoResume();
    }

    if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
    else init();

})();