Neopets Training Helper

Replaces training form with smart stat logic, preferences, and modern stat card UI with over-cap breakdown and color bars

  1. // ==UserScript==
  2. // @name Neopets Training Helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Replaces training form with smart stat logic, preferences, and modern stat card UI with over-cap breakdown and color bars
  6. // @author Fatal
  7. // @match https://www.neopets.com/island/training.phtml?type=courses*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. const STATUS_URL = 'https://www.neopets.com/island/training.phtml?type=status';
  16. const FORM_ACTION = 'process_training.phtml';
  17. const LOCAL_KEY = 'petTrainerPreferences';
  18.  
  19. const defaultPrefs = {
  20. strength: true,
  21. defence: true,
  22. agility: true,
  23. endurance: true
  24. };
  25.  
  26. const savePrefs = (prefs) => localStorage.setItem(LOCAL_KEY, JSON.stringify(prefs));
  27. const loadPrefs = () => {
  28. try {
  29. return JSON.parse(localStorage.getItem(LOCAL_KEY)) || { ...defaultPrefs };
  30. } catch {
  31. return { ...defaultPrefs };
  32. }
  33. };
  34.  
  35. const preferences = loadPrefs();
  36. const petStats = [];
  37.  
  38. const wipeOriginalForm = () => {
  39. const oldForm = document.querySelector('form[action="process_training.phtml"]');
  40. if (oldForm) {
  41. const wrapper = document.createElement('div');
  42. wrapper.id = 'training-form-placeholder';
  43. oldForm.replaceWith(wrapper);
  44. }
  45. };
  46.  
  47. wipeOriginalForm();
  48.  
  49. const iframe = document.createElement('iframe');
  50. iframe.style.display = 'none';
  51. iframe.src = STATUS_URL;
  52. document.body.appendChild(iframe);
  53.  
  54. iframe.onload = () => {
  55. const lastUsedPet = localStorage.getItem('lastUsedPet');
  56. const doc = iframe.contentDocument || iframe.contentWindow.document;
  57. const blocks = doc.querySelectorAll('td[bgcolor="white"][align="center"]');
  58.  
  59. blocks.forEach(block => {
  60. const html = block.innerHTML;
  61. const nameMatch = html.match(/cpn\/(.*?)\//);
  62. if (!nameMatch) return;
  63. const name = nameMatch[1];
  64.  
  65. // Get training status from header row
  66. const headerRow = block.closest('tr').previousElementSibling;
  67. const petNameInHeader = headerRow?.innerText?.match(/^(.+?) \(Level/)?.[1]?.trim();
  68. const isTraining = petNameInHeader === name && headerRow?.innerText?.includes("is currently studying");
  69. const trainingSkill = isTraining ?
  70. headerRow.innerText.match(/is currently studying (.+?)\)?$/)?.[1] :
  71. null;
  72.  
  73. // Check payment status
  74. const hasTimeRemaining = html.includes('Time till course finishes');
  75. const hasUnpaidCourse = html.includes('This course has not been paid for yet');
  76.  
  77. // Parse time remaining if paid course
  78. const timeMatch = html.match(/Time till course finishes : <br><b>(\d+ hrs?, \d+ minutes?, \d+ seconds?)<\/b>/i);
  79. const trainingTime = timeMatch ? timeMatch[1] : null;
  80.  
  81. // Parse codestones only for unpaid courses
  82. let codestoneHTML = '';
  83. let payCancelHTML = '';
  84. if (hasUnpaidCourse && !hasTimeRemaining) {
  85. const codestoneMatches = [...html.matchAll(/<b>(.*?) Codestone<\/b>.*?<img src="(.*?)"/g)];
  86. codestoneHTML = codestoneMatches.map(m =>
  87. `<div style="display: flex; align-items: center; gap: 5px; margin: 3px 0;">
  88. <img src="${m[2]}" width="20" height="20">
  89. <span>${m[1]} Codestone</span>
  90. </div>`
  91. ).join('');
  92.  
  93. payCancelHTML = `
  94. <div style="margin-top: 8px;">
  95. <form action="process_training.phtml" method="post" style="display:inline-block; margin-right:8px;">
  96. <input type="hidden" name="pet_name" value="${name}">
  97. <input type="hidden" name="type" value="pay">
  98. <input type="submit" value="Pay for Course" style="padding: 4px 8px;">
  99. </form>
  100. <form action="process_training.phtml" method="post" style="display:inline-block;">
  101. <input type="hidden" name="pet_name" value="${name}">
  102. <input type="hidden" name="type" value="cancel">
  103. <input type="submit" value="Cancel" style="padding: 4px 8px;">
  104. </form>
  105. </div>
  106. `;
  107. }
  108.  
  109. petStats.push({
  110. name,
  111. level: parseInt(html.match(/Lvl : <font color="green"><b>(\d+)<\/b>/)?.[1] || 0),
  112. str: parseInt(html.match(/Str : <b>(\d+)<\/b>/)?.[1] || 0),
  113. def: parseInt(html.match(/Def : <b>(\d+)<\/b>/)?.[1] || 0),
  114. mov: parseInt(html.match(/Mov : <b>(\d+)<\/b>/)?.[1] || 0),
  115. hp: parseInt(html.match(/Hp : <b>\d+ \/ (\d+)<\/b>/)?.[1] || 0),
  116. isTraining,
  117. trainingSkill,
  118. trainingTime,
  119. trainingDetails: {
  120. unpaid: hasUnpaidCourse && !hasTimeRemaining,
  121. codestones: codestoneHTML,
  122. actions: payCancelHTML
  123. }
  124. });
  125. });
  126.  
  127. renderCustomForm(lastUsedPet);
  128.  
  129. const petDropdown = document.querySelector('select[name="pet_name"]');
  130. if (lastUsedPet && petDropdown) {
  131. petDropdown.value = lastUsedPet;
  132. petDropdown.dispatchEvent(new Event('change'));
  133. }
  134. };
  135.  
  136. function renderCustomForm(lastUsedPet) {
  137. const container = document.querySelector('#training-form-placeholder');
  138. if (!container) return;
  139.  
  140. container.innerHTML = '';
  141. container.style.padding = '10px';
  142. container.style.border = '1px solid #aaa';
  143. container.style.borderRadius = '6px';
  144. container.style.background = '#f8f8f8';
  145.  
  146. const form = document.createElement('form');
  147. form.method = 'post';
  148. form.action = FORM_ACTION;
  149.  
  150. form.innerHTML = `<input type="hidden" name="type" value="start">`;
  151.  
  152. const petSelect = document.createElement('select');
  153. petSelect.name = 'pet_name';
  154. petSelect.style.width = '100%';
  155. petSelect.style.marginBottom = '10px';
  156. petSelect.innerHTML = `<option value="">🎯 Choose A Pet</option>` +
  157. petStats.map(p => `<option value="${p.name}"${lastUsedPet && p.name === lastUsedPet ? ' selected' : ''}>${p.name}</option>`).join('');
  158. form.appendChild(petSelect);
  159.  
  160. const conditionalUI = document.createElement('div');
  161. conditionalUI.id = 'conditional-ui';
  162. conditionalUI.style.display = 'none';
  163. form.appendChild(conditionalUI);
  164.  
  165. const courseSelect = document.createElement('select');
  166. courseSelect.name = 'course_type';
  167. courseSelect.style.width = '100%';
  168. courseSelect.style.marginBottom = '10px';
  169. conditionalUI.appendChild(courseSelect);
  170.  
  171. const checkboxSection = document.createElement('div');
  172. checkboxSection.style.margin = '10px 0';
  173. checkboxSection.innerHTML = `<strong>🧠 Training Preferences</strong><br>`;
  174.  
  175. const stats = ['strength', 'defence', 'agility', 'endurance'];
  176. const labels = {
  177. strength: 'Strength',
  178. defence: 'Defence',
  179. agility: 'Agility',
  180. endurance: 'Endurance (HP)'
  181. };
  182.  
  183. stats.forEach(stat => {
  184. const label = document.createElement('label');
  185. label.style.marginRight = '10px';
  186.  
  187. const box = document.createElement('input');
  188. box.type = 'checkbox';
  189. box.checked = preferences[stat];
  190. box.addEventListener('change', () => {
  191. preferences[stat] = box.checked;
  192. savePrefs(preferences);
  193. updateSelection();
  194. });
  195.  
  196. label.appendChild(box);
  197. label.appendChild(document.createTextNode(' ' + labels[stat]));
  198. checkboxSection.appendChild(label);
  199. });
  200.  
  201. conditionalUI.appendChild(checkboxSection);
  202.  
  203. const feedback = document.createElement('div');
  204. feedback.style.padding = '8px';
  205. feedback.style.border = '1px solid #ccc';
  206. feedback.style.background = '#fff';
  207. feedback.style.borderRadius = '4px';
  208. feedback.style.marginBottom = '10px';
  209. conditionalUI.appendChild(feedback);
  210.  
  211. const submit = document.createElement('input');
  212. submit.type = 'submit';
  213. submit.id = 'start-course-button';
  214. submit.value = 'Start Training';
  215. submit.style.marginTop = '10px';
  216. submit.style.padding = '6px 12px';
  217. conditionalUI.appendChild(submit);
  218.  
  219. petSelect.addEventListener('change', () => {
  220. const selectedPet = petSelect.value;
  221. localStorage.setItem('lastUsedPet', selectedPet);
  222. const showUI = !!selectedPet;
  223. conditionalUI.style.display = showUI ? 'block' : 'none';
  224. if (showUI) updateSelection();
  225. });
  226.  
  227. function updateSelection() {
  228. const selected = petStats.find(p => p.name === petSelect.value);
  229. if (!selected) return;
  230.  
  231. const submitButton = document.querySelector('#start-course-button');
  232. if (submitButton) {
  233. submitButton.style.display = selected.isTraining ? 'none' : 'inline-block';
  234. }
  235.  
  236. const level = selected.level;
  237. const cap = level * 2;
  238.  
  239. const statList = [
  240. { key: 'strength', value: selected.str, label: 'Strength', course: 'Strength' },
  241. { key: 'defence', value: selected.def, label: 'Defence', course: 'Defence' },
  242. { key: 'agility', value: selected.mov, label: 'Agility', course: 'Agility' },
  243. { key: 'endurance', value: selected.hp, label: 'Endurance (HP)', course: 'Endurance' }
  244. ];
  245.  
  246. const overCap = statList.filter(s => s.value > cap);
  247. const trainableStats = statList.filter(s => preferences[s.key] && s.value < cap);
  248. const trainable = trainableStats.length > 0 ?
  249. trainableStats.reduce((lowest, current) => current.value < lowest.value ? current : lowest) : null;
  250.  
  251. let selectedCourse = 'Level';
  252. let reason = '';
  253.  
  254. if (overCap.length > 0) {
  255. const overList = overCap.map(s => `${s.label}: ${s.value} / ${cap}`).join('<br>');
  256. const highestOver = overCap.reduce((max, stat) => stat.value > max.value ? stat : max, overCap[0]);
  257. const levelsNeeded = Math.ceil(highestOver.value / 2) - level;
  258. const levelDurations = [
  259. { max: 20, hours: 2 },
  260. { max: 40, hours: 3 },
  261. { max: 80, hours: 4 },
  262. { max: 100, hours: 6 },
  263. { max: 120, hours: 8 },
  264. { max: 150, hours: 12 },
  265. { max: 200, hours: 18 },
  266. { max: 250, hours: 24 }
  267. ];
  268.  
  269. let estimatedTimeHours = 0;
  270. for (let i = 1; i <= levelsNeeded; i++) {
  271. const currentLevel = level + i - 1;
  272. const bracket = levelDurations.find(b => currentLevel <= b.max);
  273. estimatedTimeHours += bracket ? bracket.hours : 24;
  274. }
  275.  
  276. const days = Math.floor(estimatedTimeHours / 24);
  277. const hours = estimatedTimeHours % 24;
  278. const estimatedTime = `${days} day${days !== 1 ? 's' : ''} and ${hours} hour${hours !== 1 ? 's' : ''}`;
  279.  
  280. selectedCourse = 'Level';
  281. 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>`;
  282. } else if (trainable) {
  283. selectedCourse = trainable.course;
  284. reason = `${trainable.label} is below the cap (${cap}) and is selected in your preferences.`;
  285. } else {
  286. selectedCourse = 'Level';
  287. reason = `All selected stats are at or above the cap (${cap}). Training Level to raise the limit.`;
  288. }
  289.  
  290. courseSelect.innerHTML = `
  291. <option value="Strength"${selectedCourse === 'Strength' ? ' selected' : ''}>Strength</option>
  292. <option value="Defence"${selectedCourse === 'Defence' ? ' selected' : ''}>Defence</option>
  293. <option value="Agility"${selectedCourse === 'Agility' ? ' selected' : ''}>Agility</option>
  294. <option value="Endurance"${selectedCourse === 'Endurance' ? ' selected' : ''}>Endurance</option>
  295. <option value="Level"${selectedCourse === 'Level' ? ' selected' : ''}>Level</option>
  296. `;
  297.  
  298. const statBars = statList.map(s => {
  299. const percent = Math.min(s.value / cap, 1);
  300. const isOver = s.value > cap;
  301. const barColor = isOver ? '#e74c3c' : '#4caf50';
  302. return `
  303. <div style="margin-bottom: 8px;">
  304. <div style="display: flex; justify-content: space-between; font-size: 13px; font-weight: 500;">
  305. <span>${s.label}</span>
  306. <span>${s.value} / ${cap}</span>
  307. </div>
  308. <div style="background: #eee; border-radius: 4px; overflow: hidden; height: 8px; margin-top: 2px;">
  309. <div style="width: ${Math.min(100, percent * 100)}%; background: ${barColor}; height: 100%;"></div>
  310. </div>
  311. </div>`;
  312. }).join('');
  313.  
  314. const trainingBox = selected.isTraining ? `
  315. <div style="padding: 10px; margin-bottom: 8px; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 6px; color: #856404; font-size: 13px;">
  316. <strong>Currently Training:</strong> ${selected.trainingSkill}<br>
  317. ${selected.trainingTime ?
  318. `⏳ Time remaining: ${selected.trainingTime}` :
  319. selected.trainingDetails?.codestones ?
  320. '⚠️ This course has not been paid for yet.<br>' +
  321. selected.trainingDetails.codestones +
  322. selected.trainingDetails.actions :
  323. 'Course in progress'
  324. }
  325. </div>
  326. ` : '';
  327.  
  328. feedback.innerHTML = `
  329. <div style="display: flex; align-items: flex-start; gap: 12px;">
  330. <img src="https://pets.neopets.com/cpn/${selected.name}/1/4.png" width="180" style="border-radius: 8px;">
  331. <div style="flex: 1;">
  332. <div style="font-size: 16px; font-weight: bold; margin-bottom: 4px;">${selected.name}</div>
  333. ${trainingBox}
  334. <div style="font-size: 13px; margin-bottom: 6px;">
  335. <strong>Level:</strong> ${level}<br>
  336. <strong>Str:</strong> ${selected.str},
  337. <strong>Def:</strong> ${selected.def},
  338. <strong>Agi:</strong> ${selected.mov},
  339. <strong>HP:</strong> ${selected.hp}
  340. </div>
  341. <div style="font-weight: bold; color: green; margin-bottom: 6px;">Training: ${selectedCourse}</div>
  342. <div style="font-size: 12px; color: #333; margin-bottom: 6px;">${reason}</div>
  343. <div style="font-size: 12px; color: #333; background: #f9f9f9; padding: 10px; border-radius: 6px; border: 1px solid #ddd;">
  344. ${statBars}
  345. </div>
  346. </div>
  347. </div>
  348. `;
  349. }
  350.  
  351. container.appendChild(form);
  352. }
  353. })();