NeoDeck Auto-Add

Automatically adds cards from the inventory to the NeoDeck.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();

})();