Training Helper Plus + Global Notification System + Grabs Stones or Coins + Complete All courses
// ==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();
})();