Let's get these scripts uploaded, shall we?

This commit is contained in:
Holly McFarland 2025-05-12 17:08:12 -04:00
commit 25ca635182
3 changed files with 230 additions and 0 deletions

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# monorail's neopets userscripts
Quality of life userscripts for neopets.com
## [Move Training Pets To Top](https://git.hollymcfarland.com/monorail/NeopetsUserscripts/raw/branch/main/scripts/move-training-pets-to-top.user.js)
On the status page for the Swashbuckling Academy, Mystery Island Training School, or Secret Ninja Training School\*, move any pets enrolled in a course (whether they're waiting for payment, actively training, or waiting for "course complete" confirmation) to the top of the list. Useful if your battledome pet happens to get sorted near the bottom by default, requiring you to scroll.
\*I haven't tested this script at the Secret Ninja Training School because I don't have a pet even *close* to level 250. I have reason to believe it should work there, but I would appreciate confirmation from anyone who's able to test it out.
## [Stock Highlighter](https://git.hollymcfarland.com/monorail/NeopetsUserscripts/raw/branch/main/scripts/stock-highlighter.user.js)
When checking your stock portfolio, automatically sort all stocks by current value and highlight any that are over your sell price. The sell price defaults to 60 neopoints but can be configured on the page itself. Custom sell prices are handled in real time and are automatically saved.

View File

@ -0,0 +1,70 @@
// ==UserScript==
// @name Move Training Pets To Top
// @namespace https://hollymcfarland.com
// @version 1.0
// @description At the three battledome training areas in Neopia, move pets currently in a course or waiting for input to the top of the list
// @author monorail
// @match https://www.neopets.com/pirates/academy.phtml?type=status
// @match https://www.neopets.com/island/training.phtml?type=status
// @match https://www.neopets.com/island/fight_training.phtml?type=status
// @icon https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @grant none
// ==/UserScript==
/*
Note that this has not been tested in the Secret Ninja Training School, simply
because I don't have any pets even close to level 250. But the DOM looks the
same at the Swashbuckling Academy and the Mystery Island Training School so,
I assume it'll work there too? If someone let me know I'd appreciate it haha
*/
(function() {
'use strict';
function getPairs(iter) {
/*
Given an iterable (something that can be converted to an array, anyway), return
an array of two-length arrays containing each element once, in the same order
e.g. getPairs([1, 2, 3, 4, 5, 6]) => [[1, 2], [3, 4], [5, 6]]
*/
// The function getting mapped here behaves differently depending on the parity
// of the index.
// Odd index => Empty array
// Even index => Array of array(!!) of the element at that index and the one after it
// Then because it's getting _flat_mapped, the whole thing is flattened by one level.
// Empty arrays disappear and the arrays of arrays become just two-length arrays
// https://stackoverflow.com/a/57851980/2114129
return [...iter].flatMap((_, i, a) => i % 2 ? [] : [a.slice(i, i + 2)]);
}
// Table that shows each pet
const statusTable = document.querySelector(".content > p:nth-child(6) > table:nth-child(1) > tbody:nth-child(1)");
for (let [header, pet] of getPairs(statusTable.children)) {
// Each "pet" here refers to a <tr> element representing that pet's status.
// Every pet has at exactly two <td> elements as children. The first displays
// their image and stats and the second shows any applicable status (i.e. the
// pet is waiting for payment, currently training, or waiting for "course
// complete" confirmation). This second element is empty for pets with no status
// at all.
// Any pet that has any kind of status at all should be moved to the top, and
// ideally we'd also like to otherwise preserve the order. To accomplish this,
// what we're actually going to do is take every pet that _doesn't_ have a
// status (i.e. the second <td> child is empty) and move it to the bottom. We
// do this on a copy of the list of children so we don't hit any pet more than
// once or skip any, and it's all done in the correct order.
// To move an element to the bottom, we just have to append it as a child to
// the element it's already a child of. Because Neopets uses tables for layout
// in current year, the header above each pet is actually a completely separate
// element, so those are handled at the same time.
if (pet.lastChild.childElementCount === 0) {
statusTable.appendChild(header);
statusTable.appendChild(pet);
}
}
})();

View File

@ -0,0 +1,147 @@
// ==UserScript==
// @name Stock Highlighter
// @namespace https://hollymcfarland.com
// @version 1.1
// @description Sort stocks in your portfolio and highlight ones that are ready to sell
// @author monorail
// @match https://www.neopets.com/stockmarket.phtml?type=portfolio
// @icon https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
const DEFAULT_SELL_PRICE = 60;
const SELL_PRICE_KEY = "__monorail_userscript_sell_price"; // Unlikely to collide with anything, I think
const SELL_COLOUR = "#77FF77"
function getPairs(iter) {
/*
Given an iterable (something that can be converted to an array, anyway), return
an array of two-length arrays containing each element once, in the same order
e.g. getPairs([1, 2, 3, 4, 5, 6]) => [[1, 2], [3, 4], [5, 6]]
*/
// The function getting mapped here behaves differently depending on the parity
// of the index.
// Odd index => Empty array
// Even index => Array of array(!!) of the element at that index and the one after it
// Then because it's getting _flat_mapped, the whole thing is flattened by one level.
// Empty arrays disappear and the arrays of arrays become just two-length arrays
// https://stackoverflow.com/a/57851980/2114129
return [...iter].flatMap((_, i, a) => i % 2 ? [] : [a.slice(i, i + 2)]);
}
function getPrice(stock) {
/*
Given a row from the portfolio table representing a stock, return the current value
*/
return Number(stock.children[3].innerText);
}
function stockCompare(a, b) {
/*
Sort function for comparing stocks
*/
return getPrice(b) - getPrice(a);
}
// Get the stocks from the DOM
const portfolio = document.querySelector("#postForm > table > tbody");
const stocks = getPairs(portfolio.children);
const footer = stocks.pop()[0]; // Store footer elsewhere
stocks.shift(); // Remove first element of the array, the two header rows
stocks.sort((a, b) => (stockCompare(a[0], b[0])));
// Append all stocks to the table they're already in, moving them to the bottom
// in sorted order
for (let [stock, sellStock] of stocks) {
portfolio.appendChild(stock);
portfolio.appendChild(sellStock);
// While we're iterating over all the stocks anyway, remove the bgcolor
// attribute on them. The stocks have alternating colours and moving them
// around breaks that, so we're going to remove it entirely and reimplement
// that in a more modern way.
stock.removeAttribute("bgcolor");
}
// That last step had the side effect of moving the footer to the top
// Move that to the bottom as well
portfolio.append(footer);
// Now we add a custom stylesheet. This will do two things:
// 1. Reintroduce the alternating colour pattern that we removed earlier
// 2. Add a class to highlight stocks that are above the sell price
const stylesheet = new CSSStyleSheet();
// Every fourth row, starting with the third, is given the #EEEEFF background
// colour unless:
// - There's a "bgcolor" attribute already set.
// We removed that attribute from each stock, so this avoids recolouring the footer.
// - It has the .readyToSell class
// That's going to be set on any row that's ready to sell, where we'll need to override
// the background colour. This rule is really specific, so we can't override it easily
// with specificity, we'll just have to turn it off.
//
// It's every fourth row, not second, because the hidden rows for selling stocks still
// count in this case.
stylesheet.insertRule("#postForm > table > tbody > tr:not([bgcolor]):not(.readyToSell):nth-child(4n+3) { background-color: #EEEEFF; }");
// Any stocks with a value higher than the sell price are highlighted
stylesheet.insertRule(`.readyToSell { background-color: ${SELL_COLOUR}; }`);
document.adoptedStyleSheets = [stylesheet];
// Now that we're all set up, we can start working on highlighting stocks
function highlightStocks(sellPrice) {
for (let [stock, _] of stocks) {
// There's no reason not to add a class that already exists, or not to
// remove one that doesn't exist, that's all handled for us. So we can
// simply add or remove the class based on the price and it all works out
if (getPrice(stock) >= sellPrice) {
stock.classList.add("readyToSell");
} else {
stock.classList.remove("readyToSell");
}
}
}
// We define the function and then call it right away, rather than just doing the
// work inline, so that we can update the classes dynamically.
// We also use the customized sell price if it exists, and 60 otherwise.
highlightStocks(Number(localStorage.getItem(SELL_PRICE_KEY) ?? DEFAULT_SELL_PRICE));
// These next few blocks are all just for creating and lightly styling the input for
// setting a custom sell price.
const configContainer = document.createElement("div");
configContainer.style.margin = "auto";
configContainer.style.paddingTop = "1rem";
configContainer.style.width = "fit-content";
const sellPriceLabel = document.createElement("label");
sellPriceLabel.setAttribute("for", "sellprice");
sellPriceLabel.innerText = "Sell price: ";
sellPriceLabel.style.fontWeight = "bold";
configContainer.appendChild(sellPriceLabel);
const sellPriceInput = document.createElement("input");
sellPriceInput.setAttribute("type", "number");
sellPriceInput.setAttribute("name", "sellprice");
sellPriceInput.setAttribute("min", "0");
sellPriceInput.setAttribute("max", "9999");
sellPriceInput.value = localStorage.getItem(SELL_PRICE_KEY) ?? DEFAULT_SELL_PRICE;
configContainer.appendChild(sellPriceInput);
document.getElementById("postForm").after(configContainer);
// Now that the config input exists, we simply wire it up. Changing the value should
// update the saved value, as well as rechecking the stocks for any to highlight.
sellPriceInput.addEventListener("input", function() {
localStorage.setItem(SELL_PRICE_KEY, this.value);
highlightStocks(Number(this.value));
});
})();