// ==UserScript==
// @name Neopets Training Helper
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Replaces training form with smart stat logic, preferences, and modern stat card UI with over-cap breakdown and color bars
// @author Fatal
// @match https://www.neopets.com/island/training.phtml?type=courses*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STATUS_URL = 'https://www.neopets.com/island/training.phtml?type=status';
const FORM_ACTION = 'process_training.phtml';
const LOCAL_KEY = 'petTrainerPreferences';
const defaultPrefs = {
strength: true,
defence: true,
agility: true,
endurance: true
};
const savePrefs = (prefs) => localStorage.setItem(LOCAL_KEY, JSON.stringify(prefs));
const loadPrefs = () => {
try {
return JSON.parse(localStorage.getItem(LOCAL_KEY)) || { ...defaultPrefs };
} catch {
return { ...defaultPrefs };
}
};
const preferences = loadPrefs();
const petStats = [];
const wipeOriginalForm = () => {
const oldForm = document.querySelector('form[action="process_training.phtml"]');
if (oldForm) {
const wrapper = document.createElement('div');
wrapper.id = 'training-form-placeholder';
oldForm.replaceWith(wrapper);
}
};
wipeOriginalForm();
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = STATUS_URL;
document.body.appendChild(iframe);
iframe.onload = () => {
const lastUsedPet = localStorage.getItem('lastUsedPet');
const doc = iframe.contentDocument || iframe.contentWindow.document;
const blocks = doc.querySelectorAll('td[bgcolor="white"][align="center"]');
blocks.forEach(block => {
const html = block.innerHTML;
const nameMatch = html.match(/cpn\/(.*?)\//);
if (!nameMatch) return;
const name = nameMatch[1];
// Get training status from header row
const headerRow = block.closest('tr').previousElementSibling;
const petNameInHeader = headerRow?.innerText?.match(/^(.+?) \(Level/)?.[1]?.trim();
const isTraining = petNameInHeader === name && headerRow?.innerText?.includes("is currently studying");
const trainingSkill = isTraining ?
headerRow.innerText.match(/is currently studying (.+?)\)?$/)?.[1] :
null;
// Check payment status
const hasTimeRemaining = html.includes('Time till course finishes');
const hasUnpaidCourse = html.includes('This course has not been paid for yet');
// Parse time remaining if paid course
const timeMatch = html.match(/Time till course finishes : <br><b>(\d+ hrs?, \d+ minutes?, \d+ seconds?)<\/b>/i);
const trainingTime = timeMatch ? timeMatch[1] : null;
// Parse codestones only for unpaid courses
let codestoneHTML = '';
let payCancelHTML = '';
if (hasUnpaidCourse && !hasTimeRemaining) {
const codestoneMatches = [...html.matchAll(/<b>(.*?) Codestone<\/b>.*?<img src="(.*?)"/g)];
codestoneHTML = codestoneMatches.map(m =>
`<div style="display: flex; align-items: center; gap: 5px; margin: 3px 0;">
<img src="${m[2]}" width="20" height="20">
<span>${m[1]} Codestone</span>
</div>`
).join('');
payCancelHTML = `
<div style="margin-top: 8px;">
<form action="process_training.phtml" method="post" style="display:inline-block; margin-right:8px;">
<input type="hidden" name="pet_name" value="${name}">
<input type="hidden" name="type" value="pay">
<input type="submit" value="Pay for Course" style="padding: 4px 8px;">
</form>
<form action="process_training.phtml" method="post" style="display:inline-block;">
<input type="hidden" name="pet_name" value="${name}">
<input type="hidden" name="type" value="cancel">
<input type="submit" value="Cancel" style="padding: 4px 8px;">
</form>
</div>
`;
}
petStats.push({
name,
level: parseInt(html.match(/Lvl : <font color="green"><b>(\d+)<\/b>/)?.[1] || 0),
str: parseInt(html.match(/Str : <b>(\d+)<\/b>/)?.[1] || 0),
def: parseInt(html.match(/Def : <b>(\d+)<\/b>/)?.[1] || 0),
mov: parseInt(html.match(/Mov : <b>(\d+)<\/b>/)?.[1] || 0),
hp: parseInt(html.match(/Hp : <b>\d+ \/ (\d+)<\/b>/)?.[1] || 0),
isTraining,
trainingSkill,
trainingTime,
trainingDetails: {
unpaid: hasUnpaidCourse && !hasTimeRemaining,
codestones: codestoneHTML,
actions: payCancelHTML
}
});
});
renderCustomForm(lastUsedPet);
const petDropdown = document.querySelector('select[name="pet_name"]');
if (lastUsedPet && petDropdown) {
petDropdown.value = lastUsedPet;
petDropdown.dispatchEvent(new Event('change'));
}
};
function renderCustomForm(lastUsedPet) {
const container = document.querySelector('#training-form-placeholder');
if (!container) return;
container.innerHTML = '';
container.style.padding = '10px';
container.style.border = '1px solid #aaa';
container.style.borderRadius = '6px';
container.style.background = '#f8f8f8';
const form = document.createElement('form');
form.method = 'post';
form.action = FORM_ACTION;
form.innerHTML = `<input type="hidden" name="type" value="start">`;
const petSelect = document.createElement('select');
petSelect.name = 'pet_name';
petSelect.style.width = '100%';
petSelect.style.marginBottom = '10px';
petSelect.innerHTML = `<option value="">🎯 Choose A Pet</option>` +
petStats.map(p => `<option value="${p.name}"${lastUsedPet && p.name === lastUsedPet ? ' selected' : ''}>${p.name}</option>`).join('');
form.appendChild(petSelect);
const conditionalUI = document.createElement('div');
conditionalUI.id = 'conditional-ui';
conditionalUI.style.display = 'none';
form.appendChild(conditionalUI);
const courseSelect = document.createElement('select');
courseSelect.name = 'course_type';
courseSelect.style.width = '100%';
courseSelect.style.marginBottom = '10px';
conditionalUI.appendChild(courseSelect);
const checkboxSection = document.createElement('div');
checkboxSection.style.margin = '10px 0';
checkboxSection.innerHTML = `<strong>🧠 Training Preferences</strong><br>`;
const stats = ['strength', 'defence', 'agility', 'endurance'];
const labels = {
strength: 'Strength',
defence: 'Defence',
agility: 'Agility',
endurance: 'Endurance (HP)'
};
stats.forEach(stat => {
const label = document.createElement('label');
label.style.marginRight = '10px';
const box = document.createElement('input');
box.type = 'checkbox';
box.checked = preferences[stat];
box.addEventListener('change', () => {
preferences[stat] = box.checked;
savePrefs(preferences);
updateSelection();
});
label.appendChild(box);
label.appendChild(document.createTextNode(' ' + labels[stat]));
checkboxSection.appendChild(label);
});
conditionalUI.appendChild(checkboxSection);
const feedback = document.createElement('div');
feedback.style.padding = '8px';
feedback.style.border = '1px solid #ccc';
feedback.style.background = '#fff';
feedback.style.borderRadius = '4px';
feedback.style.marginBottom = '10px';
conditionalUI.appendChild(feedback);
const submit = document.createElement('input');
submit.type = 'submit';
submit.id = 'start-course-button';
submit.value = 'Start Training';
submit.style.marginTop = '10px';
submit.style.padding = '6px 12px';
conditionalUI.appendChild(submit);
petSelect.addEventListener('change', () => {
const selectedPet = petSelect.value;
localStorage.setItem('lastUsedPet', selectedPet);
const showUI = !!selectedPet;
conditionalUI.style.display = showUI ? 'block' : 'none';
if (showUI) updateSelection();
});
function updateSelection() {
const selected = petStats.find(p => p.name === petSelect.value);
if (!selected) return;
const submitButton = document.querySelector('#start-course-button');
if (submitButton) {
submitButton.style.display = selected.isTraining ? 'none' : 'inline-block';
}
const level = selected.level;
const cap = level * 2;
const statList = [
{ key: 'strength', value: selected.str, label: 'Strength', course: 'Strength' },
{ key: 'defence', value: selected.def, label: 'Defence', course: 'Defence' },
{ key: 'agility', value: selected.mov, label: 'Agility', course: 'Agility' },
{ key: 'endurance', value: selected.hp, label: 'Endurance (HP)', course: 'Endurance' }
];
const overCap = statList.filter(s => s.value > cap);
const trainableStats = statList.filter(s => preferences[s.key] && s.value < cap);
const trainable = trainableStats.length > 0 ?
trainableStats.reduce((lowest, current) => current.value < lowest.value ? current : lowest) : null;
let selectedCourse = 'Level';
let reason = '';
if (overCap.length > 0) {
const overList = overCap.map(s => `${s.label}: ${s.value} / ${cap}`).join('<br>');
const highestOver = overCap.reduce((max, stat) => stat.value > max.value ? stat : max, overCap[0]);
const levelsNeeded = Math.ceil(highestOver.value / 2) - level;
const levelDurations = [
{ max: 20, hours: 2 },
{ max: 40, hours: 3 },
{ max: 80, hours: 4 },
{ max: 100, hours: 6 },
{ max: 120, hours: 8 },
{ max: 150, hours: 12 },
{ max: 200, hours: 18 },
{ max: 250, hours: 24 }
];
let estimatedTimeHours = 0;
for (let i = 1; i <= levelsNeeded; i++) {
const currentLevel = level + i - 1;
const bracket = levelDurations.find(b => currentLevel <= b.max);
estimatedTimeHours += bracket ? bracket.hours : 24;
}
const days = Math.floor(estimatedTimeHours / 24);
const hours = estimatedTimeHours % 24;
const estimatedTime = `${days} day${days !== 1 ? 's' : ''} and ${hours} hour${hours !== 1 ? 's' : ''}`;
selectedCourse = 'Level';
reason = `These stats are over the cap of ${cap}, so you must train Level to raise the limit:<br>${overList}<br><br>To unlock further training, your pet must reach level <strong>${Math.ceil(highestOver.value / 2)}</strong>.<br>Estimated time: <strong>${estimatedTime}</strong>`;
} else if (trainable) {
selectedCourse = trainable.course;
reason = `${trainable.label} is below the cap (${cap}) and is selected in your preferences.`;
} else {
selectedCourse = 'Level';
reason = `All selected stats are at or above the cap (${cap}). Training Level to raise the limit.`;
}
courseSelect.innerHTML = `
<option value="Strength"${selectedCourse === 'Strength' ? ' selected' : ''}>Strength</option>
<option value="Defence"${selectedCourse === 'Defence' ? ' selected' : ''}>Defence</option>
<option value="Agility"${selectedCourse === 'Agility' ? ' selected' : ''}>Agility</option>
<option value="Endurance"${selectedCourse === 'Endurance' ? ' selected' : ''}>Endurance</option>
<option value="Level"${selectedCourse === 'Level' ? ' selected' : ''}>Level</option>
`;
const statBars = statList.map(s => {
const percent = Math.min(s.value / cap, 1);
const isOver = s.value > cap;
const barColor = isOver ? '#e74c3c' : '#4caf50';
return `
<div style="margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; font-size: 13px; font-weight: 500;">
<span>${s.label}</span>
<span>${s.value} / ${cap}</span>
</div>
<div style="background: #eee; border-radius: 4px; overflow: hidden; height: 8px; margin-top: 2px;">
<div style="width: ${Math.min(100, percent * 100)}%; background: ${barColor}; height: 100%;"></div>
</div>
</div>`;
}).join('');
const trainingBox = selected.isTraining ? `
<div style="padding: 10px; margin-bottom: 8px; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 6px; color: #856404; font-size: 13px;">
<strong>Currently Training:</strong> ${selected.trainingSkill}<br>
${selected.trainingTime ?
`⏳ Time remaining: ${selected.trainingTime}` :
selected.trainingDetails?.codestones ?
'⚠️ This course has not been paid for yet.<br>' +
selected.trainingDetails.codestones +
selected.trainingDetails.actions :
'Course in progress'
}
</div>
` : '';
feedback.innerHTML = `
<div style="display: flex; align-items: flex-start; gap: 12px;">
<img src="https://pets.neopets.com/cpn/${selected.name}/1/4.png" width="180" style="border-radius: 8px;">
<div style="flex: 1;">
<div style="font-size: 16px; font-weight: bold; margin-bottom: 4px;">${selected.name}</div>
${trainingBox}
<div style="font-size: 13px; margin-bottom: 6px;">
<strong>Level:</strong> ${level}<br>
<strong>Str:</strong> ${selected.str},
<strong>Def:</strong> ${selected.def},
<strong>Agi:</strong> ${selected.mov},
<strong>HP:</strong> ${selected.hp}
</div>
<div style="font-weight: bold; color: green; margin-bottom: 6px;">Training: ${selectedCourse}</div>
<div style="font-size: 12px; color: #333; margin-bottom: 6px;">${reason}</div>
<div style="font-size: 12px; color: #333; background: #f9f9f9; padding: 10px; border-radius: 6px; border: 1px solid #ddd;">
${statBars}
</div>
</div>
</div>
`;
}
container.appendChild(form);
}
})();