Neopets Training Helper Plus

Training Helper Plus + Global Notification System + Grabs Stones or Coins + Complete All courses

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Neopets Training Helper Plus
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Training Helper Plus + Global Notification System + Grabs Stones or Coins + Complete All courses
// @author       Darthmagic
// @match        https://www.neopets.com/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const SCHOOLS = {
        island: { name: 'Mystery Island', processUrl: 'https://www.neopets.com/island/process_training.phtml', hpMult: 3 },
        academy: { name: 'Swashbuckling Academy', processUrl: 'https://www.neopets.com/pirates/process_academy.phtml', hpMult: 2 },
        ninja: { name: 'Secret Ninja', processUrl: 'https://www.neopets.com/island/process_fight_training.phtml', hpMult: 3 }
    };

    const NICE_SCHOOL = { ninja: 'Ninja Training', island: 'Beginner Training', academy: 'Deck Scrubber' };
    const BATTLE_STAT_CAP = 850;

    const LOCAL_ACTIVE = 'neoActiveTrainings';
    const LOCAL_COMPLETED = 'neoCompletedTrainings';
    const LOCAL_PENDING_WITHDRAW = 'neoPendingSDBWithdraw';
    const LOCAL_PAY_URL = 'neoAutoPayUrl';

    const loadActive = () => JSON.parse(localStorage.getItem(LOCAL_ACTIVE)) || {};
    const saveActive = d => localStorage.setItem(LOCAL_ACTIVE, JSON.stringify(d));
    const loadCompleted = () => { const arr = JSON.parse(localStorage.getItem(LOCAL_COMPLETED)) || []; return arr.filter(n => Date.now() - n.timestamp < 30*86400000); };
    const saveCompleted = arr => localStorage.setItem(LOCAL_COMPLETED, JSON.stringify(arr));

    function detectSchool() {
        const t = document.body.innerText;
        const u = location.href;
        if (t.includes('Secret Ninja') || u.includes('fight_training')) return 'ninja';
        if (t.includes('Swashbuckling Academy') || u.includes('academy')) return 'academy';
        return 'island';
    }

    function getSchoolInfo() { return SCHOOLS[detectSchool()]; }

    function getPetStats(container) {
        const text = (container.textContent || '').replace(/\s+/g, ' ');
        let level = 0;
        const levelMatch = text.match(/Lvl\s*:\s*(\d+)/i) || text.match(/Level\s*:\s*(\d+)/i) || text.match(/\(Level\s*(\d+)\)/i);
        if (levelMatch) level = parseInt(levelMatch[1]);

        const strMatch = text.match(/Str(?:ength)?\s*:\s*(\d+)/i);
        const str = strMatch ? parseInt(strMatch[1]) : 0;

        const defMatch = text.match(/Def(?:ence)?\s*:\s*(\d+)/i);
        const def = defMatch ? parseInt(defMatch[1]) : 0;

        let maxHp = 0;
        let hpMatch = text.match(/(?:Hp|HP|Health|Hit\s*Points?|Endurance)\s*:\s*(\d+)\s*\/\s*(\d+)/i);
        if (hpMatch) maxHp = parseInt(hpMatch[2]);
        else {
            hpMatch = text.match(/(?:Hp|HP|Health|Hit\s*Points?|Endurance)\s*:\s*(\d+)/i);
            if (hpMatch) maxHp = parseInt(hpMatch[1]);
        }
        return { level, str, def, maxHp };
    }

    function parseVisibleStatusPage() {
        const schoolKey = detectSchool();
        const schoolName = getSchoolInfo().name;
        let active = loadActive();

        document.querySelectorAll('td[bgcolor="#efefef"], td[bgcolor="#000000"]').forEach(header => {
            const txt = header.textContent || '';
            if (!txt.includes('is currently studying')) return;

            const petMatch = txt.match(/([A-Za-z][A-Za-z0-9_]+)\s*\(Level\s*\d+\)/);
            if (!petMatch) return;
            const pet = petMatch[1];
            const row = header.closest('tr');
            const statsTd = row?.nextElementSibling?.querySelector('td[bgcolor="white"]');
            if (!statsTd) return;

            const blockText = statsTd.textContent + ' ' + (row.nextElementSibling?.textContent || '');
            let endTime = null;
            if (blockText.includes('Course Finished!')) endTime = Date.now();
            else {
                const m = blockText.match(/(\d+)\s*hrs?,\s*(\d+)\s*minutes?,\s*(\d+)\s*seconds?/i);
                if (m) endTime = Date.now() + (parseInt(m[1])||0)*3600000 + (parseInt(m[2])||0)*60000 + (parseInt(m[3])||0)*1000;
            }
            if (!endTime) return;

            const skillMatch = txt.match(/studying\s+(.+?)(?:\s|$)/i);
            active[pet] = { school: schoolKey, skill: skillMatch ? skillMatch[1].trim() : 'Level', endTime, statusUrl: location.href, schoolName };
        });
        saveActive(active);
    }

    function addSmartQuickButtons() {
        const activePets = Object.keys(loadActive());
        const school = getSchoolInfo();

        document.querySelectorAll('td[bgcolor="white"]').forEach(cell => {
            cell.querySelectorAll('div[style*="margin-top:6px"]').forEach(el => el.remove());
            const text = cell.textContent || '';
            if (!text.match(/Lvl\s*:/i) && !text.match(/\(Level\s*\d+\)/i)) return;

            const stats = getPetStats(cell);
            let header = cell.closest('tr')?.previousElementSibling?.querySelector('td[bgcolor="#efefef"], td[bgcolor="#000000"]');
            let pet = null;
            if (header) {
                const headerTxt = header.textContent || '';
                const petMatch = headerTxt.match(/([A-Za-z][A-Za-z0-9_]+)\s*\(Level\s*\d+\)/);
                if (petMatch) pet = petMatch[1];
            }
            if (!pet) {
                const oldPetMatch = text.match(/([A-Za-z][A-Za-z0-9_]{2,})\s*(?:\(Level|:)/);
                if (oldPetMatch) pet = oldPetMatch[1];
            }
            if (!pet) return;

            if (activePets.includes(pet)) {
                const note = document.createElement('span');
                note.style = 'margin-left:8px;color:#e74c3c;font-weight:bold;';
                note.textContent = '[Training Elsewhere]';
                cell.appendChild(note);
                return;
            }

            const { level, str, def, maxHp } = stats;
            const hpCap = level * school.hpMult + (detectSchool() !== 'academy' ? 3 : 0);

            let recommended = null;
            if (level < 30) recommended = 'Level';
            else if (str < BATTLE_STAT_CAP) recommended = 'Strength';
            else if (def < BATTLE_STAT_CAP) recommended = 'Defence';
            else if (maxHp < hpCap) recommended = 'Endurance';
            else recommended = 'Level';

            const container = document.createElement('div');
            container.style.cssText = 'margin-top:6px;';

            const makeBtn = (course, letter) => {
                const isRec = course === recommended;
                const btn = document.createElement('a');
                btn.innerHTML = `[+ ${letter}]`;
                btn.style.cssText = `color:${isRec ? '#e74c3c' : '#27ae60'};font-weight:${isRec ? 'bold' : 'normal'};cursor:pointer;margin:0 4px;text-decoration:underline;`;
                btn.onclick = () => window.quickStart(pet, course);
                container.appendChild(btn);
            };

            makeBtn('Level', 'L');
            if (str < BATTLE_STAT_CAP) makeBtn('Strength', 'S');
            if (def < BATTLE_STAT_CAP) makeBtn('Defence', 'D');
            makeBtn('Endurance', 'E');

            if (str >= BATTLE_STAT_CAP && def >= BATTLE_STAT_CAP && maxHp >= hpCap) {
                const maxed = document.createElement('span');
                maxed.style = 'color:#e74c3c;font-size:12px;margin-left:6px;';
                maxed.textContent = '(maxed ✓)';
                container.appendChild(maxed);
            }
            cell.appendChild(container);
        });
    }

    function handleCompleteCourseButtons() {
        document.querySelectorAll('input[type="submit"][value="Complete Course!"]').forEach(button => {
            const form = button.closest('form');
            if (!form || button.dataset.enhanced) return;
            button.dataset.enhanced = 'true';

            button.addEventListener('click', async (e) => {
                e.preventDefault();
                await completeSingleCourse(form, button);
            });
        });
    }

    async function completeSingleCourse(form, button) {
        const originalText = button ? button.value : '';
        if (button) {
            button.value = 'Completing...';
            button.disabled = true;
        }

        try {
            const formData = new FormData(form);
            const response = await fetch(form.action, {
                method: 'POST',
                body: formData
            });

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            let gainMessage = 'Course completed!';
            const paragraphs = doc.querySelectorAll('p');
            for (let p of paragraphs) {
                if (p.textContent.includes('now has increased') || p.textContent.includes('Congratulations')) {
                    gainMessage = p.textContent.trim();
                    break;
                }
            }

            const resultBox = document.createElement('div');
            resultBox.style.cssText = 'margin-top:8px;padding:10px 14px;background:#e8f5e9;border:1px solid #4caf50;border-radius:6px;color:#2e7d32;font-size:13px;';
            resultBox.innerHTML = `✅ <strong>${gainMessage}</strong>`;

            const container = form.closest('td') || form.parentElement;
            if (container) {
                form.style.display = 'none';
                container.appendChild(resultBox);
            }

        } catch (err) {
            console.error('[Training Helper] Failed to complete course:', err);
            if (button) {
                button.value = 'Error';
                button.disabled = false;
            }
        }
    }

    async function autoCompleteAllCourses() {
        const buttons = Array.from(document.querySelectorAll('input[type="submit"][value="Complete Course!"]'));
        if (buttons.length === 0) return;

        const petsBeingCompleted = [];
        buttons.forEach(btn => {
            const header = btn.closest('tr')?.previousElementSibling?.querySelector('td[bgcolor="#efefef"], td[bgcolor="#000000"]');
            if (header) {
                const match = header.textContent.match(/([A-Za-z][A-Za-z0-9_]+)\s*\(Level\s*\d+\)/);
                if (match) petsBeingCompleted.push(match[1]);
            }
        });

        const banner = document.createElement('div');
        banner.style.cssText = 'position:fixed;top:15%;left:50%;transform:translate(-50%,-50%);background:#1565c0;color:white;padding:14px 24px;border-radius:8px;z-index:999999;font-size:15px;';
        banner.innerHTML = `Completing ${buttons.length} course(s)...`;
        document.body.appendChild(banner);

        for (let i = 0; i < buttons.length; i++) {
            const button = buttons[i];
            const form = button.closest('form');
            if (!form) continue;

            banner.innerHTML = `Completing course ${i + 1} of ${buttons.length}...`;
            await completeSingleCourse(form, button);
            await new Promise(resolve => setTimeout(resolve, 850));
        }

        if (petsBeingCompleted.length > 0) {
            const active = loadActive();
            petsBeingCompleted.forEach(pet => delete active[pet]);
            saveActive(active);
        }

        banner.innerHTML = `✅ All courses completed!`;
        setTimeout(() => {
            banner.remove();
            if (panel && panel.style.display === 'block') {
                showNotificationPanel();
            }
        }, 1600);
    }

    function parseRequiredTrainingItems() {
        const items = [];
        document.querySelectorAll('td[width="250"]').forEach(td => {
            if (!td.textContent.includes('This course has not been paid for yet')) return;
            td.querySelectorAll('b').forEach(b => {
                const name = b.textContent.trim();
                if (!name || name.includes('click here') || name.includes('To cancel') || name.includes('Pay') || name.includes('Cancel')) return;
                if (name.includes('Codestone') || name.includes('Dubloon')) {
                    items.push({ name, qty: 1 });
                }
            });
        });

        const merged = {};
        items.forEach(item => {
            if (merged[item.name]) merged[item.name].qty += item.qty;
            else merged[item.name] = { ...item };
        });
        return Object.values(merged);
    }

    window.quickStart = function(pet, course) {
        const school = getSchoolInfo();
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = school.processUrl;
        form.innerHTML = `
            <input type="hidden" name="type" value="start">
            <input type="hidden" name="course_type" value="${course}">
            <input type="hidden" name="pet_name" value="${pet}">
        `;
        document.body.appendChild(form);

        const banner = document.createElement('div');
        banner.style = 'position:fixed;top:20%;left:50%;transform:translate(-50%,-50%);background:#28a745;color:white;padding:20px 40px;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.4);z-index:999999;font-size:18px;text-align:center;';
        banner.innerHTML = `✅ <b>Training started!</b><br>${pet} → ${course}`;
        document.body.appendChild(banner);

        setTimeout(() => form.submit(), 550);
    };

    function waitForStatsThenRun(fn) {
        let done = false;
        const observer = new MutationObserver(() => {
            if (!done && document.querySelector('td[bgcolor="white"]')) { done = true; observer.disconnect(); fn(); }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setTimeout(() => { if (!done) { observer.disconnect(); if (document.querySelector('td[bgcolor="white"]')) fn(); } }, 2200);
    }

    function handleStatusPage() {
        const ui = document.createElement('div');
        ui.style.cssText = 'margin:8px 0;padding:10px 20px;background:linear-gradient(#e6f0ff,#d0e0ff);border:2px solid #4a90e2;border-radius:8px;text-align:center;color:#2c5aa0;font-weight:bold;font-size:15px;';
        ui.innerHTML = `🧠 Training Helper Plus v1.0 • ${getSchoolInfo().name} • By Darthy`;
        (document.querySelector('.content, #content') || document.body).prepend(ui);

        waitForStatsThenRun(() => {
            parseVisibleStatusPage();
            addSmartQuickButtons();
            addItemGrabberButton();
            handleCompleteCourseButtons();

            const finishedCount = document.querySelectorAll('input[type="submit"][value="Complete Course!"]').length;
            if (finishedCount > 0) {
                const completeAllBtn = document.createElement('button');
                completeAllBtn.style.cssText = 'margin:10px auto;display:block;background:#1565c0;color:white;border:none;padding:10px 20px;border-radius:8px;font-weight:bold;cursor:pointer;';
                completeAllBtn.textContent = `Complete All Finished Courses (${finishedCount})`;

                completeAllBtn.onclick = async () => {
                    completeAllBtn.disabled = true;
                    completeAllBtn.textContent = 'Completing...';
                    await autoCompleteAllCourses();
                    completeAllBtn.remove();
                };

                ui.appendChild(completeAllBtn);
            }

            autoPayAfterWithdraw();
        });
    }

    function addItemGrabberButton() {
        const requiredItems = parseRequiredTrainingItems();
        if (requiredItems.length === 0) return;

        document.querySelectorAll('td[width="250"]').forEach(td => {
            if (!td.textContent.includes('This course has not been paid for yet')) return;
            if (td.querySelector('.item-grabber-btn')) return;

            const payLink = td.querySelector('a[href*="type=pay"]');
            const payUrl = payLink ? payLink.href : null;

            const btnContainer = document.createElement('div');
            btnContainer.style.cssText = 'margin: 8px 0; text-align:center;';

            const btn = document.createElement('button');
            btn.className = 'item-grabber-btn';
            btn.style.cssText = 'background:#222;color:white;border:none;padding:6px 14px;border-radius:6px;font-weight:bold;cursor:pointer;font-size:12px;';
            btn.textContent = 'Item Grabber';

            btn.onclick = () => {
                if (payUrl) localStorage.setItem(LOCAL_PAY_URL, payUrl);
                localStorage.setItem(LOCAL_PENDING_WITHDRAW, JSON.stringify(requiredItems));
                btn.textContent = 'Grabbing...';
                btn.disabled = true;
                setTimeout(() => window.location.href = '/safetydeposit.phtml', 300);
            };

            btnContainer.appendChild(btn);

            const firstP = td.querySelector('p');
            if (firstP) firstP.parentNode.insertBefore(btnContainer, firstP);
            else td.appendChild(btnContainer);
        });
    }

    function autoPayAfterWithdraw() {
        const payUrl = localStorage.getItem(LOCAL_PAY_URL);
        if (!payUrl) return;
        localStorage.removeItem(LOCAL_PAY_URL);

        const banner = document.createElement('div');
        banner.style = 'position:fixed;top:30%;left:50%;transform:translate(-50%,-50%);background:#27ae60;color:white;padding:16px 28px;border-radius:10px;z-index:999999;font-size:16px;text-align:center;';
        banner.innerHTML = `✅ Items moved!<br>Starting course...`;
        document.body.appendChild(banner);

        setTimeout(() => window.location.href = payUrl, 1400);
    }

    function handleAutoSDBWithdraw() {
        if (!location.pathname.includes('safetydeposit.phtml')) return;

        const pending = JSON.parse(localStorage.getItem(LOCAL_PENDING_WITHDRAW) || '[]');
        if (pending.length === 0) return;

        const statusDiv = document.createElement('div');
        statusDiv.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#2c5aa0;color:white;padding:12px 24px;border-radius:8px;z-index:999999;font-size:15px;';
        statusDiv.textContent = 'Withdrawing items from SDB...';
        document.body.appendChild(statusDiv);

        setTimeout(() => {
            const categorySelects = document.querySelectorAll('.sdb-select');
            if (categorySelects.length > 0) {
                const isDubloon = pending.some(i => i.name.toLowerCase().includes('dubloon'));
                categorySelects[0].value = isDubloon ? '3' : '2';
                categorySelects[0].dispatchEvent(new Event('change', { bubbles: true }));
            }

            setTimeout(() => {
                pending.forEach((req, index) => {
                    setTimeout(() => {
                        const rows = document.querySelectorAll('.sdb-table tbody tr');
                        for (let row of rows) {
                            const nameEl = row.querySelector('.sdb-item-name');
                            if (nameEl && nameEl.textContent.trim() === req.name) {
                                const input = row.querySelector('.np-stepper-input');
                                if (input) {
                                    input.value = Math.min(req.qty, parseInt(input.max) || req.qty);
                                    input.dispatchEvent(new Event('input', { bubbles: true }));
                                    input.dispatchEvent(new Event('change', { bubbles: true }));
                                }
                                const checkbox = row.querySelector('.sdb-item-checkbox');
                                if (checkbox && !checkbox.checked) {
                                    checkbox.checked = true;
                                    checkbox.dispatchEvent(new Event('change', { bubbles: true }));
                                }
                                break;
                            }
                        }
                    }, index * 300);
                });

                setTimeout(() => {
                    const actionSelect = document.querySelector('.sdb-drawer .sdb-action-select') || document.querySelector('.sdb-as-native');
                    if (actionSelect) {
                        actionSelect.value = 'inventory';
                        actionSelect.dispatchEvent(new Event('change', { bubbles: true }));
                    }

                    setTimeout(() => {
                        const firstConfirm = document.querySelector('.sdb-drawer-confirm-btn');
                        if (firstConfirm) firstConfirm.click();

                        setTimeout(() => {
                            const popupConfirm = document.querySelector('#sdb__popup .popup-footer__2020 button.np-button.button-green__2020');
                            if (popupConfirm) popupConfirm.click();

                            setTimeout(() => {
                                localStorage.removeItem(LOCAL_PENDING_WITHDRAW);
                                statusDiv.textContent = 'Done! Returning...';
                                setTimeout(() => {
                                    const returnUrl = document.referrer.includes('fight_training') ||
                                                      document.referrer.includes('training.phtml') ||
                                                      document.referrer.includes('academy.phtml')
                                        ? document.referrer
                                        : '/island/fight_training.phtml?type=status';
                                    window.location.href = returnUrl;
                                }, 1400);
                            }, 1200);
                        }, 900);
                    }, 700);
                }, 1600);
            }, 1400);
        }, 900);
    }

    // Notification system - MORE COMPACT
    let panel = null, bell = null;

    function createBellAndPanel() {
        if (document.getElementById('neo-bell') && panel) return;
        const top = (document.querySelector('#header, header, .header, table[bgcolor="#000080"]')?.getBoundingClientRect().bottom + window.scrollY + 8) || 110;

        if (!document.getElementById('neo-bell')) {
            bell = document.createElement('div');
            bell.id = 'neo-bell';
            bell.style.cssText = `position:fixed;top:${top}px;right:20px;width:54px;height:54px;background:#4a90e2;color:white;border-radius:50%;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 4px 15px rgba(0,0,0,0.3);`;
            bell.textContent = '🛎️';
            document.body.appendChild(bell);
            bell.onclick = () => (panel && panel.style.display === 'block') ? panel.style.display = 'none' : showNotificationPanel();
        }

        if (!panel) {
            panel = document.createElement('div');
            // Changed width from 440px to 330px (25% narrower) + slightly less padding
            panel.style.cssText = `display:none;position:fixed;top:${top+70}px;right:20px;width:330px;max-height:75vh;overflow:auto;background:#fff;border:3px solid #4a90e2;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.3);z-index:100000;padding:12px;font-family:Verdana,sans-serif;`;
            document.body.appendChild(panel);
        }
    }

    window.showNotificationPanel = function(autoOpen = false) {
        createBellAndPanel();
        if (!panel) return;

        const active = loadActive();
        const completed = loadCompleted();
        let html = `<h3 style="margin:0 0 10px;color:#4a90e2;font-size:15px;">📬 Training Notifications <button style="float:right;font-size:20px;border:none;background:none;cursor:pointer;" onclick="panel.style.display='none'">✕</button></h3>`;

        if (completed.length) {
            html += `<h4 style="color:#e74c3c;margin:6px 0 4px;font-size:13px;">✅ Completed</h4>`;
            completed.forEach(c => {
                html += `<div onclick="window.location='${c.statusUrl}'" style="cursor:pointer;padding:8px 10px;background:#fff8f0;border:1px solid #e74c3c;border-radius:6px;margin-bottom:6px;display:flex;gap:10px;font-size:13px;">
                    <img src="https://pets.neopets.com/cpn/${c.petName}/1/4.png" width="38" style="border-radius:5px;">
                    <div style="flex:1"><strong>${c.petName}</strong><br>${c.skill} • ${NICE_SCHOOL[c.school]||c.schoolName}</div>
                </div>`;
            });
        }

        if (Object.keys(active).length) {
            html += `<h4 style="color:#27ae60;margin:8px 0 4px;font-size:13px;">⏳ Currently Training</h4>`;
            Object.keys(active).forEach(p => {
                const t = active[p];
                const minLeft = Math.max(0, Math.floor((t.endTime - Date.now()) / 60000));
                html += `<div style="padding:8px 10px;background:#f0f8ff;border:1px solid #27ae60;border-radius:6px;margin-bottom:6px;font-size:13px;"><strong>${p}</strong> • ${t.skill} • ${NICE_SCHOOL[t.school]||t.schoolName}<br><span style="color:#27ae60;font-weight:bold;">${minLeft} min left</span></div>`;
            });
        } else if (!completed.length) {
            html += `<p style="text-align:center;color:#666;padding:12px 0;font-size:13px;">No pets currently training.</p>`;
        }

        html += `<button id="clear-completed-btn" style="margin-top:10px;background:#e74c3c;color:white;border:none;padding:7px 14px;border-radius:6px;font-size:13px;">🗑️ Clear All Completed</button>`;
        panel.innerHTML = html;
        panel.style.display = 'block';

        const clearBtn = panel.querySelector('#clear-completed-btn');
        if (clearBtn) {
            clearBtn.onclick = () => {
                localStorage.removeItem(LOCAL_COMPLETED);
                showNotificationPanel();
            };
        }
    };

    function checkExpiredTrainings() {
        const active = loadActive();
        let completed = loadCompleted();
        let changed = false;

        Object.keys(active).forEach(pet => {
            if (Date.now() >= active[pet].endTime) {
                completed.unshift({ petName: pet, skill: active[pet].skill || 'Course', school: active[pet].school, schoolName: active[pet].schoolName, statusUrl: active[pet].statusUrl, timestamp: Date.now() });
                delete active[pet];
                changed = true;
            }
        });

        if (changed) {
            saveActive(active);
            saveCompleted(completed);
            showNotificationPanel(true);
        }
    }

    function init() {
        setTimeout(() => {
            createBellAndPanel();
            if (loadCompleted().length > 0) showNotificationPanel(true);
            setInterval(checkExpiredTrainings, 25000);
            checkExpiredTrainings();

            handleAutoSDBWithdraw();

            if (location.search.includes('type=status') || document.body.innerText.includes('Course Status')) {
                handleStatusPage();
            } else if (!location.search && (location.pathname.includes('training.phtml') || location.pathname.includes('academy.phtml') || location.pathname.includes('fight_training.phtml'))) {
                location.replace(location.pathname + '?type=status');
            }

            console.log('%c✅ Training Helper Plus v1.0 by Darthy • More compact notification panel', 'color:#e74c3c;font-weight:bold');
        }, 800);
    }

    if (document.readyState === 'loading') window.addEventListener('load', init);
    else init();
})();