├── Neopets - Active Pet Switch.user.js ├── Neopets - Battledome Set Selector.user.js ├── Neopets - Better Faster Godori.user.js ├── Neopets - Daily Puzzle Solver.user.js ├── Neopets - Direct Link Display.user.js ├── Neopets - Home Page Redirect.user.js ├── Neopets - Inventory+.user.js ├── Neopets - LoD Shame Tracker.user.js ├── Neopets - Mystery Pic Helper.user.js ├── Neopets - Negg Cave Solver.user.js ├── Neopets - NeoCola Enhancements.user.js ├── Neopets - NeoFoodClub+.user.js ├── Neopets - Quick Exploration Links.user.js ├── Neopets - Quick Inventory Deposit.user.js ├── Neopets - SDB Inventarium.user.js ├── Neopets - Trans Affirming Neopets (Compat).user.js ├── Neopets - Trans Affirming Neopets.user.js ├── README.md └── mettyneo-icon.png /Neopets - Active Pet Switch.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Active Pet Switch & Fishing Vortex Plus 3 | // @version 2.8.1 4 | // @description APS adds a button that lets you easily switch your active pet. In the classic theme, this will appear underneath your pet's image in the sidebar. In beta, it will appear next to the icons that float at the top of every page (Bookmarks, Favorites, Shop Wizard). FVP adds additional info to the fishing vortex. 5 | // @author Metamagic, with modifications by chanakin 6 | // @match *://*.neopets.com/* 7 | // @icon https://i.imgur.com/RnuqLRm.png 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // @grant GM_deleteValue 11 | // @grant GM_addValueChangeListener 12 | // @grant window.focus 13 | // @grant window.close 14 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Active%20Pet%20Switch.user.js 15 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Active%20Pet%20Switch.user.js 16 | // ==/UserScript== 17 | 18 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 19 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 20 | 21 | // Trans rights are human rights ^^ 22 | // metty says hi 23 | 24 | //=========== 25 | // APS config 26 | //=========== 27 | //collected page data times out after this many hours. default: -1 (aka never) 28 | const HOME_DATA_TIMEOUT = -1 29 | 30 | //=========== 31 | // FVP config 32 | //=========== 33 | //changes the display mode 34 | // -1 = disable display entirely. still tracks enabled info. 35 | // 0 = display table on fishing page w/o fishing info 36 | // 1 = display table on fishing page, plus fishing info (default) 37 | // 2 = display fishing info on all pages' pet table 38 | const FISHING_DISPLAY_MODE = 1 39 | //tracks and displays pet fishing levels 40 | const FISHING_LEVEL_TRACK = true 41 | //tracks time since last fishing reward 42 | const FISHING_TIME_TRACK = true 43 | 44 | //tracks fishing xp gained and displays level up chance based on /u/neo_truths' post 45 | //https://old.reddit.com/r/neopets/comments/xqylkt/ye_olde_fishing_vortex/ 46 | const FISHING_XP_TRACK = true 47 | 48 | //for my mom who keeps accidentally clicking it then getting confused 49 | const REMOVE_CAST_BUTTON = true 50 | 51 | //============== 52 | // main function 53 | //============== 54 | 55 | const url = document.location.href 56 | //check for the top left pet icon to know we're in beta 57 | let isBeta = false 58 | if ($("[class^='nav-pet-menu-icon']").length) isBeta = true 59 | 60 | let timeoutId = null 61 | 62 | if (url.includes("neopets.com/home")) { 63 | getPetData(document) //always update the data while we're here 64 | //close if flagged to 65 | let timeout = GM_getValue("stackpath-timeout") 66 | if (timeout) { 67 | GM_deleteValue("stackpath-timeout") 68 | // won't close tab after 15 second timeout 69 | if (new Date().valueOf() - timeout < 1000 * 15) { 70 | window.close() 71 | } 72 | } 73 | } 74 | 75 | if (url.includes("water/fishing.phtml")) { 76 | handleFishingVortex() 77 | } 78 | 79 | createMenuButton() //adds the button to open the menu 80 | listenForPetUpdates() //adds a listener for updates to the script's stored pet list, which then updates the menu 81 | 82 | //========= 83 | // overhead 84 | //========= 85 | function clearPetList() { 86 | GM_setValue("petlist", {}) 87 | } 88 | 89 | function updatePetList() { 90 | clearPetList() 91 | $.get("https://www.neopets.com/home/", function (data) { 92 | let doc = new DOMParser().parseFromString(data, "text/html") 93 | //sometimes stackpath blocks the page, in which case open a newtab and close it after 94 | if (doc.title !== "Welcome to Neopets!") { 95 | console.log("[APS] Home page request blocked by stackpath, opening tab manually.") 96 | GM_setValue("stackpath-timeout", new Date().valueOf()) 97 | window.open("https://www.neopets.com/home/") 98 | window.focus() 99 | } 100 | getPetData(doc) 101 | console.log("[APS] Data successfully retrieved.") 102 | }) 103 | } 104 | 105 | function timeoutForUpdateExceeded() { 106 | return new Date().valueOf() - GM_getValue("lastupdate", 0) > 1000 * 60 * 60 * HOME_DATA_TIMEOUT && HOME_DATA_TIMEOUT > 0 107 | } 108 | 109 | function usernameHasChanged() { 110 | return getUsername() !== GM_getValue("un") 111 | } 112 | 113 | function petListExists() { 114 | return GM_getValue("petlist", false) 115 | } 116 | 117 | function shouldUpdate() { 118 | //revalidates data if it times out or if username changes 119 | if (timeoutForUpdateExceeded()) { 120 | console.log("[APS] Updating data for new day.") 121 | return true 122 | } 123 | 124 | if (usernameHasChanged()) { 125 | console.log("[APS] Updating data for new user.") 126 | return true 127 | } 128 | 129 | if (!petListExists()) { 130 | console.log("[APS] Getting user pet data for first time.") 131 | return true 132 | } 133 | 134 | return false 135 | } 136 | 137 | function getPetData(doc) { 138 | GM_setValue("petlist", getPets(doc)) 139 | GM_setValue("lastupdate", new Date().valueOf()) 140 | GM_setValue("un", getUsername()) 141 | 142 | console.log("[APS] Pet data updated.") 143 | } 144 | 145 | function emptyJson(newValue) { 146 | return JSON.stringify(newValue) === JSON.stringify({}); 147 | } 148 | 149 | function setRefreshButtonVisible(visible) { 150 | Array.from($(".activetable > a")).forEach(link => { 151 | if (visible) { 152 | link.style.display = "none" 153 | } else { 154 | link.style.display = "block" 155 | } 156 | }) 157 | } 158 | 159 | function listenForPetUpdates() { 160 | GM_addValueChangeListener("petlist", function (key, oldValue, newValue, _) { 161 | //if pet list is now empty, adds loading msg 162 | if (emptyJson(newValue)) { 163 | console.log("[APS] Cleared pet tables.") 164 | addLoadDivs() 165 | setRefreshButtonVisible(false) 166 | return 167 | } 168 | 169 | console.log("[APS] Updated pet tables.") 170 | console.log(newValue) 171 | clearTimeout(timeoutId) 172 | timeoutId = null 173 | populateTables() 174 | setRefreshButtonVisible(true) 175 | }) 176 | } 177 | 178 | //================= 179 | // element creation 180 | //================= 181 | 182 | function createMenuButton() { 183 | let parentElement = getButtonLocation() 184 | 185 | if (parentElement) { 186 | let changeActivePetShortcut 187 | 188 | if (isBeta) { 189 | changeActivePetShortcut = createShortcutIcon("#", "https://images.neopets.com/themes/h5/constellations/images/mypets-icon.svg") 190 | } else { 191 | changeActivePetShortcut = document.createElement("a") 192 | changeActivePetShortcut.href = "#" 193 | changeActivePetShortcut.innerHTML = "Change Active Pet" 194 | } 195 | 196 | addCSS() 197 | 198 | changeActivePetShortcut.classList.add("openmenubutton") 199 | changeActivePetShortcut.addEventListener("click", () => { 200 | openMenu() 201 | }) 202 | 203 | parentElement.appendChild(changeActivePetShortcut); 204 | createMenu() 205 | } 206 | } 207 | 208 | function createMenu() { 209 | let menu = document.createElement("div") 210 | menu.id = "select-active-pet-menu" 211 | menu.style.display = "none" //starts not visible 212 | 213 | menu.appendChild(getTableHeader(`Select an Active Pet:
(Click anywhere else to exit)`)) 214 | 215 | let table = document.createElement("table") 216 | table.classList.add("activetable") 217 | menu.appendChild(table) 218 | document.body.appendChild(menu) 219 | 220 | // revalidates if it needs to 221 | if (shouldUpdate()) { 222 | updatePetList() 223 | return 224 | } 225 | 226 | populateTables() 227 | } 228 | 229 | function populateTables() { 230 | let petList = Object.values(GM_getValue("petlist", {})) 231 | if (petList.length < 1) { 232 | addLoadDivs() 233 | updatePetList() 234 | return 235 | } 236 | 237 | Array.from($(".activetable")).forEach(table => { 238 | table.innerHTML = "" 239 | 240 | let activePet = getActivePet() 241 | 242 | for (let i = 0; i < 4; i++) { //4 rows 243 | let row = table.insertRow() 244 | for (let j = 0; j < 5; j++) { //5 per row 245 | let index = i * 5 + j 246 | if (index < petList.length) { 247 | let img = petList[index].img.withBG //300x300 248 | let name = petList[index].name 249 | 250 | let cell = row.insertCell() 251 | cell.setAttribute("name", name) 252 | //name = "test" 253 | let d1 = document.createElement("div"), d2 = document.createElement("div") 254 | d1.innerHTML = `${name}` 255 | d1.style.width = "150px !important"; 256 | d1.style.height = "150px !important"; 257 | d2.innerHTML = name 258 | cell.appendChild(d1) 259 | cell.appendChild(d2) 260 | 261 | if (activePet === name) { 262 | cell.setAttribute("active", "") 263 | } else { 264 | cell.addEventListener("click", (event) => { 265 | event.stopPropagation(); 266 | changeActivePet(name); 267 | }) 268 | cell.style.cursor = "pointer" 269 | } 270 | 271 | if (url.includes("water/fishing.phtml") || FISHING_DISPLAY_MODE === 2) { 272 | addFishingPetDisplay(cell, name) 273 | } 274 | } 275 | } 276 | } 277 | 278 | let refresh = document.createElement("a") 279 | refresh.innerHTML = "(refresh pet list)" 280 | refresh.addEventListener("click", (event) => { 281 | event.stopPropagation() 282 | updatePetList() //updates pet list 283 | }) 284 | table.appendChild(refresh) 285 | console.log(`[APS] Table populated with ${petList.length} pets.`) 286 | }) 287 | } 288 | 289 | function addLoadDivs() { 290 | let loadMsg = '
Fetching pet data...
' 291 | Array.from($(".activetable")).forEach(table => table.innerHTML = loadMsg) 292 | 293 | // if it hasn't grabbed results after 5 seconds, display a message 294 | timeoutId = setTimeout(() => { 295 | Array.from($(".activetable")).forEach(table => { 296 | if (table.innerHTML === loadMsg) { 297 | table.innerHTML += '
(click here if its taking too long)
' 298 | } 299 | }) 300 | }, 3000) 301 | } 302 | 303 | function getTableHeader(innerHTML) { 304 | let header = document.createElement("div") 305 | header.classList.add("activetableheader") 306 | header.innerHTML = innerHTML 307 | return header 308 | } 309 | 310 | function addFishingTable(header) { 311 | let menu = document.createElement("div") 312 | menu.classList.add("vertical") 313 | $("#container__2020")[0].appendChild(menu) 314 | 315 | menu.appendChild(getTableHeader(header)) 316 | 317 | let table = document.createElement("table") 318 | table.classList.add("activetable") 319 | table.style.margin = "auto" 320 | menu.appendChild(table) 321 | } 322 | 323 | 324 | //===================== 325 | //element functionality 326 | //===================== 327 | 328 | function openMenu() { 329 | $("#select-active-pet-menu").css("display", "flex") //makes menu visible 330 | setTimeout(() => { 331 | document.body.addEventListener("click", exitClick, false) 332 | }, 50) //adds the exit click a short delay after menu is created 333 | } 334 | 335 | function exitClick(event) { 336 | event.stopPropagation() 337 | if (!(document.querySelector(".activetableheader").contains(event.target) || document.querySelector(".activetable").contains(event.target))) { 338 | $("#select-active-pet-menu")[0].style.display = "none" 339 | document.body.removeEventListener("click", exitClick, false) 340 | } 341 | } 342 | 343 | function changeActivePet(name) { 344 | let img = GM_getValue("petlist", {})[name].img.withBG 345 | 346 | Array.from($(".activetable")).forEach(table => { 347 | table.innerHTML = "" 348 | let d1 = document.createElement("div"), d2 = document.createElement("div"), d3 = document.createElement("div") 349 | d1.innerHTML = `${name}` 350 | d1.style.opacity = "0.8" 351 | d1.style.width = "150px !important" 352 | d1.style.height = "150px !important" 353 | d1.style.display = "block" 354 | d1.style.position = "relative" 355 | d3.classList.add("dot-spin") 356 | d3.style.display = "block" 357 | d3.style.position = "absolute" 358 | d3.style.top = "50%" 359 | d3.style.left = "50%" 360 | d3.style.transform = "translate(-50%, -50%)" 361 | d1.appendChild(d3) 362 | d2.id = "set-active-msg" 363 | d2.innerHTML = `Setting ${name} as active pet...` 364 | table.appendChild(d1) 365 | table.appendChild(d2) 366 | }) 367 | 368 | $.get("/process_changepet.phtml?new_active_pet=" + name, function () { 369 | console.log(`[APS] Active pet changed to ${name}.`) 370 | Array.from($(".activetable")).forEach(table => { 371 | table.querySelector(".dot-spin").style.display = "none" 372 | table.querySelector("#set-active-msg").innerHTML = `${name} is now your active pet!` 373 | }) 374 | 375 | if (url.includes("water/fishing.phtml")) { 376 | window.location.replace("fishing.phtml") 377 | } else { 378 | window.location.reload() 379 | } 380 | }) 381 | } 382 | 383 | 384 | //================== 385 | // fvp functionality 386 | //================== 387 | function isFishingResultPage() { 388 | return Array.from($("#container__2020 > p")).some((p) => { 389 | return p.innerHTML === "You reel in your line and get..." 390 | }) 391 | } 392 | 393 | function isFishingHomePage() { 394 | return $("#pageDesc").length > 0 395 | } 396 | 397 | function handleFishingVortex() { 398 | 399 | if (isFishingResultPage()) { 400 | if (FISHING_DISPLAY_MODE >= 0) { 401 | addFishingTable(`Fish With:`) 402 | } 403 | 404 | if (REMOVE_CAST_BUTTON) { 405 | removeCastButton() 406 | } 407 | 408 | if (FISHING_TIME_TRACK || FISHING_LEVEL_TRACK) { 409 | handleFishingResult() 410 | } 411 | return 412 | } 413 | 414 | if (isFishingHomePage()) { 415 | if (FISHING_DISPLAY_MODE >= 0) { 416 | addFishingTable(`Change Active Pet:`) 417 | } 418 | 419 | if (FISHING_LEVEL_TRACK) { 420 | initializePetLevel() 421 | } 422 | 423 | $(document).ready(function () { 424 | $('input[value="Reel In Your Line"]').click() 425 | }) 426 | } 427 | } 428 | 429 | function initializePetLevel() { 430 | let list = GM_getValue("fishinglist", {}) 431 | let name = getActivePet() 432 | if (!isNaN(list[name]?.lvl)) { 433 | let lvl = $("#container__2020 > p:last-of-type > b")[0].innerHTML 434 | 435 | list[name] = { 436 | lvl: lvl, 437 | xp: list[name]?.xp, 438 | lasttime: list[name]?.lasttime || null 439 | } 440 | 441 | if (list[name].xp === undefined) { 442 | list[name].xp = null 443 | } 444 | 445 | GM_setValue("fishinglist", list) 446 | console.log(`[FVP] Recorded ${name}'s fishing level. (${lvl})`) 447 | } 448 | } 449 | 450 | function removeCastButton() { 451 | $('#container__2020 > a[href="/water/fishing.phtml"]').css("display", "none") 452 | $("#container__2020 > br:last-of-type").css("display", "none") 453 | console.log("[FVP] Removed cast again button.") 454 | } 455 | 456 | function levelUp(data, lvlUpMessage) { 457 | data.lvl = lvlUpMessage[0].querySelector("b").innerHTML 458 | if (FISHING_XP_TRACK) { 459 | data.xp = 0 460 | } 461 | } 462 | 463 | function handleFishingResult() { 464 | //if theres a reward, do stuff 465 | if ($("#container__2020 > div.item-single__2020").length) { 466 | let list = GM_getValue("fishinglist", {}) 467 | let name = getActivePet() 468 | let data = list[name] || { 469 | lvl: null, 470 | xp: null, 471 | lasttime: null 472 | } 473 | 474 | if (FISHING_TIME_TRACK) { 475 | data.lasttime = new Date().valueOf() 476 | } 477 | 478 | if (FISHING_LEVEL_TRACK) { 479 | let lvlUpMessage = Array.from($("#container__2020 > p")).filter((p) => { 480 | return p.innerHTML.includes("Your pet's fishing skill increases to") 481 | }) 482 | 483 | if (lvlUpMessage.length) { 484 | levelUp(data, lvlUpMessage); 485 | } else if (FISHING_XP_TRACK && data.xp != null) { 486 | data.xp += 100 487 | } 488 | } 489 | 490 | list[name] = data 491 | console.log(`[FVP] Fishing results for ${name} recorded.`) 492 | GM_setValue("fishinglist", list) 493 | } 494 | } 495 | 496 | function addLevelDisplay(data, cell) { 497 | let ldiv = document.createElement("div") 498 | ldiv.classList.add("leveldisplay", "fishingdisplay") 499 | ldiv.innerHTML = `Lv.${data.lvl || "(?)"}` 500 | let lhover = document.createElement("div") 501 | lhover.classList.add("fishingdisplay") 502 | let chance = getLevelUpChance(data.xp, data.lvl) 503 | if (chance == null) lhover.innerHTML = "(?)% chance to lv up" 504 | else lhover.innerHTML = `${getLevelUpChance(data.xp, data.lvl)}% chance to lv up` 505 | if (data.xp == null) lhover.innerHTML += `
Exp: (?)` 506 | else lhover.innerHTML += `
Exp: ${data.xp}` 507 | ldiv.appendChild(lhover) 508 | cell.appendChild(ldiv) 509 | } 510 | 511 | function addTimeDisplay(data, cell) { 512 | let tdiv = document.createElement("div") 513 | tdiv.classList.add("timedisplay", "fishingdisplay", "circular-progress") 514 | //deals with display w/o a recorded time 515 | if (data.lasttime == null) { 516 | tdiv.innerHTML = ` 517 |
518 |

(?)hr

519 | ` 520 | tdiv.style.background = `#c4c4c4` 521 | } else { 522 | let t = getTimeSinceFished(data.lasttime) 523 | tdiv.innerHTML = ` 524 |
525 |

${t.toFixed(1)}hr

526 | ` 527 | tdiv.style.background = `conic-gradient(${getCircleColor(t / 26.0)} ${t / 26.0 * 360}deg, #c4c4c4 0deg)` 528 | } 529 | cell.appendChild(tdiv) 530 | } 531 | 532 | function addFishingPetDisplay(cell, name) { 533 | let data = GM_getValue("fishinglist", {})[name] || { 534 | lvl: null, 535 | xp: null, 536 | lasttime: null 537 | } 538 | 539 | addLevelDisplay(data, cell); 540 | addTimeDisplay(data, cell); 541 | } 542 | 543 | function getCircleColor(p) { 544 | if (p < 0.2) { 545 | return "#e87e72" 546 | } 547 | 548 | if (p < 0.4) { 549 | return "#e8b972" 550 | } 551 | 552 | if (p < 0.6) { 553 | return "#ebed77" 554 | } 555 | 556 | if (p < 0.8) { 557 | return "#c0ed77" 558 | } 559 | 560 | if (p < 1.0) { 561 | return "#77ed7d" 562 | } 563 | 564 | return "#88f2dd" 565 | } 566 | 567 | function getTimeSinceFished(tfish) { 568 | if (tfish == null) { 569 | return null 570 | } 571 | 572 | let t = new Date().valueOf() - tfish 573 | return Math.min(Math.floor((t / (1000.0 * 60.0 * 60.0)) * 10) / 10.0, 26.0) //# of hrs w/ 1 decimal place, rounded down 574 | } 575 | 576 | function getLevelUpChance(exp, lvl) { 577 | //if we dont have the data for either, our chance is unknown 578 | if (exp == null || lvl == null) { 579 | return null 580 | } 581 | 582 | //if (1-100) <= p, we win. (eg if p = 5, we have a 5% chance) 583 | let p = Math.floor(((exp + 100) - lvl - 20) / 1.8) 584 | return Math.max(p, 0.0).toFixed(1) //can't have negative percentage, rounds to 1 decimal 585 | } 586 | 587 | 588 | //======== 589 | // getters 590 | //======== 591 | function petsSortedAlphabetically(petInfo) { 592 | return Object.keys(petInfo).sort().reduce((obj, key) => { 593 | obj[key] = petInfo[key] 594 | return obj 595 | }, {}); 596 | } 597 | 598 | function getUsername() { 599 | if (isBeta) { 600 | return $("#navprofiledropdown__2020 > div:nth-child(3) a.text-muted")[0].innerHTML 601 | } 602 | 603 | return $("#header > table > tbody > tr:nth-child(1) > td.user.medText a[href]")[0].innerHTML 604 | } 605 | 606 | function getPets(doc) { 607 | let elements = Array.from(doc.querySelectorAll(".hp-carousel-pet")) 608 | let petNames = elements.map(e => e.getAttribute("data-name")) 609 | petNames = petNames.filter((n, index) => petNames.indexOf(n) === index) 610 | 611 | let petInfo = {} 612 | 613 | petNames.forEach(name => { 614 | let e = elements.find(e => e.getAttribute("data-name") === name) 615 | let img = {noBG: e.style.backgroundImage.replace('"', "").slice(5, -2), withBG: e.getAttribute("data-petimage")} 616 | let species = e.getAttribute("data-species") 617 | let color = e.getAttribute("data-color") 618 | let gender = e.getAttribute("data-gender") 619 | let hp = `${e.getAttribute("data-health")}/${e.getAttribute("data-maxhealth")}` 620 | let level = e.getAttribute("data-level") 621 | let hunger = e.getAttribute("data-hunger") 622 | let mood = e.getAttribute("data-mood") 623 | let p2 = getPetpet(e) 624 | petInfo[name] = { 625 | name: name, 626 | species: species, 627 | color: color, 628 | img: img, 629 | gender: gender, 630 | hp: hp, 631 | level: level, 632 | hunger: hunger, 633 | mood: mood, 634 | p2: p2 635 | } 636 | }) 637 | 638 | if (petInfo === {}) return null 639 | return petsSortedAlphabetically(petInfo) 640 | } 641 | 642 | function getPetpet(e) { 643 | let petpet = null 644 | if (e.hasAttribute("data-petpet")) { 645 | let p = e.getAttribute("data-petpet").split(" ") //no, name, the, fir 646 | let name = p.slice(0, p.lastIndexOf("the")).join(" ") 647 | let species = p.slice(p.lastIndexOf("the") + 1, p.length).join(" ") 648 | let img = e.getAttribute("data-petpetimg") 649 | let p3 = null 650 | if (e.hasAttribute("data-p3")) p3 = {name: e.getAttribute("data-p3"), img: e.getAttribute("data-p3img")} 651 | petpet = {name: name, species: species, img: img, p3: p3} 652 | } 653 | return petpet 654 | } 655 | 656 | function getActivePet() { 657 | if (isBeta) { 658 | return $("#navprofiledropdown__2020 > div:nth-child(4) > a")[0].innerHTML 659 | } 660 | 661 | return $("#content > table > tbody > tr > td.sidebar > div:nth-child(1) > table > tbody > tr:nth-child(1) > td > a > b")[0].innerHTML 662 | } 663 | 664 | 665 | function getButtonLocation() { 666 | if (isBeta) var e = $(".navsub-left__2020") 667 | else e = $(".activePet") 668 | if (e.length) return e[0] 669 | else return null 670 | } 671 | 672 | 673 | //======== 674 | // css 675 | //======== 676 | 677 | function addCSS() { 678 | var theme 679 | var button 680 | var text 681 | 682 | if (isBeta) { 683 | theme = $(".nav-profile-dropdown__2020").css("background-color") 684 | button = $(".nav-profile-dropdown-text").css("color") 685 | text = $(".nav-profile-dropdown-text a.text-muted").css("color") 686 | 687 | document.head.appendChild(document.createElement("style")).innerHTML = ` 688 | .openmenubutton { 689 | height: 25px; 690 | width: 30px; 691 | 692 | } 693 | ` 694 | } else { 695 | theme = $("#content > table > tbody > tr > td.sidebar > div:nth-child(1) > table > tbody > tr:nth-child(1) > td").css("background-color") 696 | button = "gray"; 697 | text = "black"; 698 | 699 | document.head.appendChild(document.createElement("style")).innerHTML = ` 700 | .openmenubutton { 701 | margin-top: 3px; 702 | border: 1px solid black; 703 | background-color: #D3D3D3; 704 | cursor: pointer; 705 | font-size: 8pt; 706 | padding: 1px 0px 1px; 707 | width: 148px; 708 | height: 100%; 709 | display: block; 710 | font-weight: normal; 711 | text-align: center; 712 | } 713 | .openmenubutton:hover { 714 | background-color: #C0C0C0; 715 | } 716 | .openmenubutton:active { 717 | background-color: #808080; 718 | } 719 | ` 720 | } 721 | 722 | document.head.appendChild(document.createElement("style")).innerHTML = ` 723 | .shortcut { 724 | margin-left: 8px; 725 | } 726 | .vertical { 727 | display: flex; 728 | flex-direction: column; 729 | margin: auto; 730 | width: fit-content; 731 | } 732 | #select-active-pet-menu { 733 | position: fixed; 734 | z-index: 100000; 735 | background-color: #8a8a8a; 736 | flex-direction: column; 737 | left: 50%; 738 | top: 50%; 739 | transform: translate(-50%, -50%); 740 | } 741 | #select-active-pet-menu::before { 742 | width: 100vw; 743 | height: 100vh; 744 | opacity: 0.7; 745 | background-color: black; 746 | z-index: -1; 747 | content: ""; 748 | position: fixed; 749 | transform: translate(-50%, -50%); 750 | left: 50%; 751 | top: 50%; 752 | } 753 | .activetableheader { 754 | text-align: center; 755 | font-size: 16pt; 756 | cursor: default; 757 | background-color: ${theme}; 758 | color: ${text}; 759 | padding-top: 8px; 760 | padding-bottom: 8px; 761 | -webkit-user-select: none; 762 | -ms-user-select: none; 763 | user-select: none; 764 | position: relative; 765 | display: block; 766 | min-width: 300px; 767 | } 768 | .activetable > a { 769 | cursor: pointer; 770 | display: block; 771 | position: absolute; 772 | left: 50%; 773 | transform: translateX(-50%); 774 | top: 0%; 775 | padding: 4px; 776 | color: blue; 777 | text-decoration: underline; 778 | font-size: 8pt; 779 | } 780 | .activetable { 781 | padding: 20px 20px; 782 | text-align: center; 783 | background-color: #FFFFFF; 784 | border-spacing: 5px; 785 | position: relative; 786 | } 787 | .activetable td { 788 | font-weight: bold; 789 | text-align: center; 790 | background-color: #e6e6e6; 791 | border-radius: 8px; 792 | padding-top: 10px; 793 | padding-bottom: 5px; 794 | position: relative; 795 | } 796 | .activetable td img { 797 | margin: 0; 798 | padding: 0; 799 | width: 150px !important; 800 | pointer-events: none; 801 | -webkit-user-select: none; 802 | -ms-user-select: none; 803 | user-select: none; 804 | } 805 | 806 | .activetable td:hover { 807 | background-color: #c9e3b3; 808 | } 809 | .activetable td:active { 810 | background-color: #a2cf7c; 811 | } 812 | .activetable td:hover img { 813 | opacity: 0.85; 814 | } 815 | 816 | .activetable td[active] { 817 | cursor: not-allowed; 818 | background-color: #8f8f8f; 819 | color: #4a4a4a; 820 | } 821 | .activetable td[active]::after { 822 | opacity: 0.6; 823 | font-size: 16pt; 824 | text-align: center; 825 | width: 100%; 826 | height: 100%; 827 | top: 0; 828 | left: 0; 829 | cursor: not-allowed; 830 | color: black; 831 | content: "(already active)"; 832 | position: absolute; 833 | display: flex; 834 | justify-content: center; 835 | align-items: center; 836 | transform: rotate(25deg) 837 | } 838 | .activetable td[active] img { 839 | opacity: 0.3; 840 | } 841 | 842 | .leveldisplay { 843 | border-radius: 50%; 844 | width: 40px; 845 | height: 40px; 846 | border: 3px solid #c4c4c4; 847 | font-size: 8pt; 848 | position: absolute; 849 | display: flex; 850 | justify-content: center; 851 | align-items: center; 852 | background-color: rgba(255,255,255,0.95); 853 | right: 55px; 854 | top: 118px; 855 | transition: 0.5s; 856 | box-sizing: border-box; 857 | } 858 | .leveldisplay:hover { 859 | background-color: #d9d9d9; 860 | } 861 | .leveldisplay > div { 862 | justify-content: center; 863 | align-items: center; 864 | display: flex; 865 | transition: 0.2s; 866 | visibility: hidden; 867 | position: absolute; 868 | width: 120px; 869 | height: 40px; 870 | opacity: 0; 871 | top: -4px; 872 | left: -4px; 873 | background-color: white; 874 | border-radius: 4px; 875 | z-index: 100000; 876 | } 877 | .leveldisplay:hover > div { 878 | transition-delay:0.5s; 879 | opacity: 1; 880 | visibility: visible; 881 | } 882 | .timedisplay { 883 | position: absolute; 884 | top: 118px; 885 | right: 13px; 886 | display: flex; 887 | align-items: center; 888 | justify-content: center; 889 | width: 32px; 890 | height: 32px; 891 | opacity: 0.95; 892 | } 893 | ` 894 | 895 | //radial progress bar taken from https://dev.to/shubhamtiwari909/circular-progress-bar-css-1bi9 896 | document.head.appendChild(document.createElement("style")).innerHTML = ` 897 | :root { 898 | --progress-bar-width: 40px; 899 | --progress-bar-height: 40px; 900 | } 901 | .circular-progress { 902 | width: var(--progress-bar-width); 903 | height: var(--progress-bar-height); 904 | border-radius: 50%; 905 | display: flex; 906 | justify-content: center; 907 | align-items: center; 908 | } 909 | .inner-circle { 910 | position: absolute; 911 | width: 34px; 912 | height: 34px; 913 | border-radius: 50%; 914 | background-color: rgba(255,255,255,0.95); 915 | } 916 | 917 | .percentage { 918 | position: relative; 919 | font-size: 8pt !important; 920 | color: black; 921 | } 922 | ` 923 | 924 | //dot spin animation taken from https://codepen.io/nzbin/pen/GGrXbp 925 | document.head.appendChild(document.createElement("style")).innerHTML = ` 926 | .dot-spin::before { 927 | position: absolute; 928 | width: 60px; 929 | height: 60px; 930 | left: 50%; 931 | top: 50%; 932 | background-color: white; 933 | opacity: 0.5; 934 | transform: translate(-50%, -50%) translateZ(-1px); 935 | border-radius: 50%; 936 | content: ""; 937 | display: block; 938 | z-index: -1; 939 | 940 | } 941 | .dot-spin { 942 | transform-style: preserve-3d; 943 | margin: auto; 944 | position: relative; 945 | width: 10px !important; 946 | height: 10px !important; 947 | border-radius: 5px; 948 | background-color: transparent; 949 | color: transparent; 950 | box-shadow: 0 -18px 0 0 #9880ff, 12.727926px -12.727926px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.727926px 12.727926px 0 0 rgba(152, 128, 255, 0), 0 18px 0 0 rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 0 rgba(152, 128, 255, 0), -18px 0 0 0 rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 0 rgba(152, 128, 255, 0); 951 | animation: dot-spin 1.5s infinite linear; 952 | } 953 | 954 | @keyframes dot-spin { 955 | 0%, 100% { 956 | box-shadow: 0 -18px 0 0 #9880ff, 12.727926px -12.727926px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0); 957 | } 958 | 12.5% { 959 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.727926px -12.727926px 0 0 #9880ff, 18px 0 0 0 #9880ff, 12.727926px 12.727926px 0 0 #9880ff, 0 18px 0 -5px rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0); 960 | } 961 | 25% { 962 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 0 #9880ff, 12.727926px 12.727926px 0 0 #9880ff, 0 18px 0 0 #9880ff, -12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0); 963 | } 964 | 37.5% { 965 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.727926px 12.727926px 0 0 #9880ff, 0 18px 0 0 #9880ff, -12.727926px 12.727926px 0 0 #9880ff, -18px 0 0 -5px rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0); 966 | } 967 | 50% { 968 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 0 #9880ff, -12.727926px 12.727926px 0 0 #9880ff, -18px 0 0 0 #9880ff, -12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0); 969 | } 970 | 62.5% { 971 | box-shadow: 0 -18px 0 -5px rgba(152, 128, 255, 0), 12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 0 #9880ff, -18px 0 0 0 #9880ff, -12.727926px -12.727926px 0 0 #9880ff; 972 | } 973 | 75% { 974 | box-shadow: 0 -18px 0 0 #9880ff, 12.727926px -12.727926px 0 -5px rgba(152, 128, 255, 0), 18px 0 0 -5px rgba(152, 128, 255, 0), 12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 0 #9880ff, -12.727926px -12.727926px 0 0 #9880ff; 975 | } 976 | 87.5% { 977 | box-shadow: 0 -18px 0 0 #9880ff, 12.727926px -12.727926px 0 0 #9880ff, 18px 0 0 -5px rgba(152, 128, 255, 0), 12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), 0 18px 0 -5px rgba(152, 128, 255, 0), -12.727926px 12.727926px 0 -5px rgba(152, 128, 255, 0), -18px 0 0 -5px rgba(152, 128, 255, 0), -12.727926px -12.727926px 0 0 #9880ff; 978 | } 979 | } 980 | ` 981 | } 982 | 983 | function createShortcutIcon(link, iconUrl, cssClasses) { 984 | var htmlForIcon = "
" 985 | 986 | var addedIcon = document.createElement('a') 987 | addedIcon.href = link 988 | addedIcon.innerHTML = htmlForIcon 989 | 990 | return addedIcon 991 | } -------------------------------------------------------------------------------- /Neopets - Better Faster Godori.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Better Faster Godori 3 | // @version 1.3 4 | // @description Greatly enhances the speed of the game by reducing delay and clicks required to play 5 | // @author Metamagic 6 | // @match https://www.neopets.com/games/godori/godori.phtml* 7 | // @match https://www.neopets.com/games/godori/index.phtml* 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @grant GM_deleteValue 11 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Better%20Faster%20Godori.user.js 12 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Better%20Faster%20Godori.user.js 13 | // @icon https://i.imgur.com/RnuqLRm.png 14 | // @run-at document-start 15 | // ==/UserScript== 16 | 17 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 18 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 19 | 20 | // Trans rights are human rights ^^ 21 | // metty says hi 22 | 23 | //TO-DO: card count display 24 | 25 | //=============== 26 | // script options 27 | //=============== 28 | 29 | const ACTION_DELAY = "0" //the delay in ms as a string. default: 1, unmodified fast speed is 500. 30 | const CARD_SORT = "CAPTURE" //set to "SCORE" to sort hand by scoring category 31 | const COUNT_CARDS = true //make sure you don't get kicked out of the shenkuu palace for this one! 32 | const SPEEDRUN_CLOCK = true //did this one for fun 33 | //const RECORD_WINRATE = true //to know just how bad you are 34 | 35 | 36 | //=============== 37 | // main functions 38 | //=============== 39 | 40 | const CAPTURE_SETS = { 41 | "jan": 0, //shenkuu 42 | "feb": 1, //altador 43 | "mar": 2, //snow 44 | "apr": 3, //faerie 45 | "may": 4, //roo 46 | "jun": 5, //krawk 47 | "jul": 6, //tyrannia 48 | "aug": 7, //space 49 | "sep": 8, //island 50 | "oct": 9, //meridell 51 | "nov": 10, //desert 52 | "dec": 11 //haunted 53 | } 54 | const CAPTURE_SET_COLORS = { 55 | "jan": "#d47091", 56 | "feb": "#d18d3f", 57 | "mar": "#cae5ed", 58 | "apr": "#e6c5ed", 59 | "may": "#3862e0", 60 | "jun": "#46544f", 61 | "jul": "#8a7767", 62 | "aug": "#27273b", 63 | "sep": "#219c3d", 64 | "oct": "#94110f", 65 | "nov": "#dbd82c", 66 | "dec": "#520e87" 67 | } 68 | const CARD_TYPE = { 69 | "1": 0, 70 | "2": 2, 71 | "3": 2, //fuckin weird lost desert card 72 | "t": 4, //banner 73 | "y": 6, //petpet 74 | "k": 8 //pet 75 | } 76 | const SET_MINS = { 77 | "bright": 3, 78 | "animal": 5, 79 | "ribbon": 5, 80 | "junk": 10 81 | } 82 | const IMG_REGEX = /.*?\/godori\/(.{3})(.{1}).*/ 83 | 84 | 85 | //redirect 86 | let freshHand = null 87 | if(window.location.href.includes("index.phtml")) { 88 | if(document.referrer.includes("godori.phtml")) { 89 | console.log("[BFG] Referred from game page, redirecting automatically.") 90 | window.location.href = "" 91 | } 92 | } 93 | //main game page 94 | else { 95 | //disables alerts when clicking stack 96 | if (typeof unsafeWindow === "undefined") { 97 | var alrtScope = window; 98 | } else { 99 | alrtScope = unsafeWindow; 100 | } 101 | 102 | let oldAlert = alrtScope.alert 103 | alrtScope.alert = function (str) { 104 | if(!str.includes("You must select a card from your hand first")) oldAlert(str) 105 | } 106 | //runs on page load 107 | document.addEventListener('DOMContentLoaded', function() { 108 | addCSS() 109 | setGameSpeed(ACTION_DELAY) //sets delay between actions 110 | sortHand() //sorts hand at start 111 | addHandListeners() //adds click listeners to hand to auto-click stack 112 | addStackListeners() //adds click listeners to stack to auto-click a card in hand 113 | 114 | //if you have 10 cards in your hand, we're on a fresh hand 115 | freshHand = $(`#player_hand > tbody > tr > td`).filter(function() { 116 | let element = $(this) 117 | return element.css('display') != "none" 118 | }).length == 10 119 | if(freshHand) resetData() 120 | 121 | //starts systems needed to track stats 122 | startGameStats() 123 | startGameObserver() 124 | 125 | //modifies ui 126 | if(SPEEDRUN_CLOCK) speedrunDisplay() 127 | updateLabelDivs() 128 | addHighlightHover() 129 | highlightSetColor() 130 | console.log("[BFG] Page modifications applied.") 131 | 132 | //=================== 133 | // speed / efficiency 134 | //=================== 135 | 136 | function setGameSpeed(ms) { 137 | $("#fast")[0].value = ms //delay in ms between moves 138 | $("#fast")[0].click() //updates internal delay variable 139 | console.log(`[BFG] Action delay set to ${ms}ms.`) 140 | } 141 | 142 | //adds click listeners to cards in hand 143 | function addHandListeners() { 144 | let cards = Array.from($("#player_hand > tbody > tr > td > div.g_container")) 145 | for(let card of cards) { 146 | card.addEventListener("click", (event)=>{ 147 | //start speedrun timer 148 | if(SPEEDRUN_CLOCK && freshHand) { 149 | startTimer() 150 | } 151 | freshHand = false 152 | 153 | let set = card.querySelector("img").getAttribute("src").match(IMG_REGEX)[1] 154 | let stack = getSetStack(set) 155 | stack.click() 156 | sortHand() 157 | }) 158 | } 159 | } 160 | 161 | function addStackListeners() { 162 | $(".g_container").on("click", function(event) { 163 | event.stopPropagation() 164 | if(!this.querySelector("dull")) { 165 | $(".hl-selected")?.[0]?.parentElement?.click() 166 | } 167 | }) 168 | 169 | } 170 | 171 | //finds the stack to place the card in 172 | function getSetStack(set) { 173 | let stacks = Array.from($("#top_row > td.g_table_cell > div.g_container")).concat(Array.from($("#bottom_row > td.g_table_cell > div.g_container"))) 174 | let emptystack = null 175 | for(const stack of stacks) { 176 | let stackset = stack.querySelector("img.g_card")?.getAttribute("src")?.match(IMG_REGEX)?.[1] 177 | if(set == stackset) return stack 178 | else if(!stackset && !emptystack) emptystack = stack 179 | } 180 | return emptystack 181 | } 182 | 183 | //=========== 184 | // game state 185 | //=========== 186 | 187 | //resets stored data 188 | function resetData() { 189 | GM_deleteValue("cardcount") 190 | GM_deleteValue("starttime") 191 | } 192 | 193 | //records cards to show up in the stack 194 | function countCards(cards) { 195 | let cardcount = GM_getValue("cardcount", {}) 196 | for(const card of cards) { 197 | if(!(card.set in cardcount)) cardcount[card.set] = [] //initializes if none of that set 198 | if(!cardcount[card.set].includes(card.type)) cardcount[card.set].push(card.type) //records only if not already recorded 199 | } 200 | GM_setValue("cardcount", cardcount) 201 | } 202 | 203 | //starts the systems that watch for updates in game state 204 | let canAction = $("#game_status")[0].innerHTML.includes("Your move") 205 | function startGameObserver() { 206 | //watches for changes in table's stacks to track cards seen 207 | for(const stack of Array.from($("#table_cards div.g_container"))) { 208 | const stackObs = new MutationObserver(mutations => { 209 | countCards(getStackCards(stack)) 210 | }) 211 | stackObs.observe(stack, {subTree: true, childList: true, attributes: true}) 212 | } 213 | 214 | //watches for changes in status to track cards drawn 215 | let status = $("#game_status")[0] 216 | const statusObs = new MutationObserver(mutations => { 217 | //tracks cards in deck 218 | if(status.innerHTML == "You draw a card and play" || status.innerHTML.includes(" draws")) { 219 | let deck = GM_getValue("deckcount") - 1 //a card was drawn 220 | if(deck == 0) GM_deleteValue("deckcount") //resets when empty 221 | else GM_setValue("deckcount", deck) //otherwise updates 222 | if(status.innerHTML.includes(" draws")) sortHand() 223 | } 224 | 225 | //updates displays 226 | updateHandPoints() 227 | updateLabelDivs() 228 | addHighlightHover() 229 | highlightSetColor() 230 | 231 | //can highlight 232 | if(status.innerHTML.includes("Your turn")) { 233 | canAction = true 234 | addStackListeners() 235 | } 236 | //can't highlight, clears highlights 237 | else { 238 | canAction = false 239 | removeHighlights() 240 | } 241 | }) 242 | statusObs.observe(status, {subTree: true, childList: true, characterData: true}) 243 | } 244 | 245 | //counts cards on field, hand, and in deck 246 | function startGameStats() { 247 | if(COUNT_CARDS && freshHand) { 248 | //records cards in hand 249 | countCards(getHandCards()) 250 | //records cards on field 251 | for(const stack of Array.from($("#table_cards div.g_container"))) { 252 | countCards(getStackCards(stack)) 253 | } 254 | } 255 | if(!GM_getValue("deckcount")) GM_setValue("deckcount", 20) //sets card count 256 | 257 | //continues speedrun timer on a refresh 258 | if(GM_getValue("starttime")) startTimer() 259 | 260 | //records points at start of round 261 | if(GM_getValue("deckcount") == 20) { 262 | GM_setValue("startpts", [$("#user_score > span > a")[0].innerHTML.split(" ")[1], $("#comp_score > span > a")[0].innerHTML.split(" ")[1]]) 263 | } 264 | updateHandPoints() 265 | } 266 | 267 | //updates # of points earned in this round 268 | function updateHandPoints() { 269 | let startpts = GM_getValue("startpts") 270 | let user = $("#user_score > span > a")[0] 271 | let opp = $("#comp_score > span > a")[0] 272 | user.innerHTML = `${user.innerHTML.split(":")[0]}: ${user.innerHTML.split(" ")[1] - startpts[0]} (${user.innerHTML.split(" ")[1]})` 273 | opp.innerHTML = `${opp.innerHTML.split(":")[0]}: ${opp.innerHTML.split(" ")[1] - startpts[1]} (${opp.innerHTML.split(" ")[1]})` 274 | } 275 | 276 | //============= 277 | // display / ui 278 | //============= 279 | 280 | function removeHighlights() { 281 | for(let hcard of Array.from($(".highlighted"))) hcard.classList.remove("highlighted") 282 | for(let hcard of Array.from($(".dull"))) hcard.classList.remove("dull") 283 | for(let hcard of Array.from($(".hl-selected"))) hcard.classList.remove("hl-selected") 284 | } 285 | 286 | function highlightSetColor() { 287 | //fix stack z indexes 288 | let stacks = Array.from($("#table_cards .g_table_cell .g_container")) 289 | for(let stack of stacks) { 290 | let cards = Array.from(stack.querySelectorAll("img.g_card")) 291 | for(let card of cards) { 292 | card.style.zIndex = 200+(100*cards.indexOf(card)) 293 | } 294 | } 295 | 296 | //adds border 297 | for(let card of Array.from($("#player_hand .g_card, #table_cards .g_table_cell .g_card"))) { 298 | if(card.nextSibling?.classList?.contains("sethighlight") != true) { 299 | let z = parseInt(card.style.zIndex) + 1 300 | let color = CAPTURE_SET_COLORS[card.src.match(IMG_REGEX)[1]] 301 | //needed to put the highlight at the proper location 302 | let prect = card.parentElement.getBoundingClientRect() 303 | let rect = card.getBoundingClientRect() 304 | $(card).after(`
`) 305 | } 306 | } 307 | } 308 | 309 | //highlights cards of the same set 310 | function addHighlightHover() { 311 | //hovering hand 312 | $("#player_hand .g_container").mouseover(function() { 313 | if(canAction) { 314 | //get all cards on table of same set 315 | let set = this.querySelector("img.g_card").src.match(IMG_REGEX)[1] 316 | let stackcards = Array.from($("#table_cards .g_container .g_card")).filter((card) => {return card.src.includes(set)}) 317 | 318 | //adds highlight to last match 319 | if(stackcards.length > 0) { 320 | stackcards.slice(-1)[0].classList.add("highlighted") 321 | } 322 | //highlights empty stack 323 | else { 324 | $("#table_cards .g_container:not(:has(.g_card)) .g_empty_card")[0].classList.add("highlighted") 325 | } 326 | } 327 | }) 328 | $("#player_hand .g_container").mouseleave(function() { 329 | if(canAction) { 330 | for(let hcard of Array.from($(".highlighted"))) hcard.classList.remove("highlighted") 331 | } 332 | }) 333 | 334 | //hovering stack 335 | $("#table_cards .g_container").mouseover(function() { 336 | //get all cards on hand of same set 337 | if(this.querySelector(".sethighlight:hover")) { 338 | let set = this.querySelector("img.g_card").src.match(IMG_REGEX)[1] 339 | let handcards = Array.from($("#player_hand .g_container .g_card")).filter((card) => {return card.src.includes(set)}) 340 | //adds highlight 341 | if(handcards.length > 0) { 342 | let select = true 343 | for(let card of handcards) { 344 | //highlights the first card as 'selected' 345 | if(select) { 346 | card.classList.add("hl-selected") 347 | select = false 348 | } 349 | card.classList.add("highlighted") 350 | } 351 | } 352 | else { 353 | for(let hl of Array.from(this.querySelectorAll(".sethighlight"))) hl.classList.add("dull") 354 | } 355 | } 356 | //only highlights when card is hovered 357 | else removeHighlights() 358 | }) 359 | //only highlights when card is hovered 360 | $("#table_cards .g_container").mouseleave(removeHighlights) 361 | } 362 | 363 | //sorts hand based on capture set 364 | function sortHand() { 365 | let hand = $("#player_hand > tbody > tr")[0] 366 | let cards = Array.from(hand.children).filter((td)=>{return td.querySelector("div.g_container > img")}) 367 | let emptycardslots = Array.from(hand.children).filter((td) => {return !(td.querySelector("div.g_container > img"))}) 368 | hand.parentElement.parentElement.style = `width: ${cards.length * 70}px;` //removes empty spaces in hand 369 | cards.sort(compareCards) 370 | 371 | //replace unsorted hand with sorted hand 372 | hand.innerHTML = "" 373 | for(const card of cards) { 374 | hand.appendChild(card) 375 | } 376 | for(const card of emptycardslots) { 377 | card.style.display = "none" 378 | hand.appendChild(card) 379 | } 380 | 381 | //also "sorts" the opponents hand, thus centering it!) 382 | let opphand = $("#computer_hand > tbody > tr")[0] 383 | let emptyoppcards = Array.from(opphand.children).filter((td) => {return !(td.querySelector("div.g_container > img"))}) 384 | opphand.parentElement.parentElement.style = `width: ${(10-emptyoppcards.length) * 70}px;` 385 | for(const card of emptyoppcards) { 386 | card.style.display = "none" 387 | } 388 | } 389 | 390 | //adds and updates the labels 391 | function updateLabelDivs() { 392 | //deck 393 | //makes div if doesn't exist 394 | if($(".countlabel").length == 0) { 395 | let decklabel = document.createElement("div") 396 | decklabel.classList.add("countlabel") 397 | $("#stock")[0].appendChild(decklabel) 398 | //repositions deck because holy fuck why 399 | $("#stock")[0].style = "top:-70px; right:-20px; pointer-events:none; cursor:default;" 400 | } 401 | //updates label 402 | $(".countlabel")[0].innerHTML = GM_getValue("deckcount") 403 | 404 | //captures 405 | for(let div of Array.from($("#player_capture > tbody > tr > td > div")).concat(Array.from($("#computer_capture > tbody > tr > td > div")))) { 406 | //makes div if doesn't exist 407 | if(!div.querySelector(".caplabel")) { 408 | let caplabel = document.createElement("div") 409 | caplabel.classList.add("caplabel") 410 | div.appendChild(caplabel) 411 | } 412 | //updates label 413 | let label = div.querySelector(".caplabel") 414 | let cards = div.querySelectorAll("img.g_card_back").length 415 | label.innerHTML = `${cards}/${SET_MINS[div.id.split("_")[1]]}` 416 | //doesn't show label if nothing in capture category 417 | if(cards == 0) label.style.display = "none" 418 | else label.style.display = "flex" 419 | } 420 | 421 | //hands 422 | for(let hand of Array.from($("table.g_hand"))) { 423 | //makes div if it doesnt exist 424 | if(!hand.querySelector(".handlabel")) { 425 | let handlabel = document.createElement("div") 426 | handlabel.classList.add("handlabel") 427 | if(hand.id.includes("player")) handlabel.style.bottom = "-12px" 428 | else handlabel.style.top = "4px" 429 | hand.appendChild(handlabel) 430 | } 431 | let cards = Array.from(hand.querySelectorAll("td.g_card_cell")).filter((td)=>{return td.querySelector("div.g_container > img")}).length 432 | hand.querySelector(".handlabel").innerHTML = `Cards in Hand: ${cards}` 433 | } 434 | } 435 | 436 | //===================== 437 | // speedrun timer (lol) 438 | //===================== 439 | 440 | //adds speedrun display to page 441 | function speedrunDisplay() { 442 | //creates speedrun timer div 443 | let timer = document.createElement("td") 444 | timer.id = "speedruntimer" 445 | timer.innerHTML = "00:00.000" 446 | $(`#intro > tbody > tr`)[0].insertBefore(timer, $(`#intro > tbody > tr > td[align="right"]`)[0]) 447 | } 448 | 449 | function startTimer() { 450 | //gets start time and updates timer automatically 451 | let starttime = GM_getValue("starttime") || {time:Date.now()} //keeps previous start time on page refresh 452 | let id = setInterval(() => { 453 | $("#speedruntimer")[0].innerHTML = formatTime(Date.now() - starttime.time) 454 | }, 12) 455 | GM_setValue("starttime", {time: starttime.time, id:id}) 456 | console.log("[BFG] Timer started.") 457 | } 458 | 459 | function stopTimer() { 460 | clearInterval(GM_getValue("starttime").id) 461 | } 462 | 463 | 464 | //================ 465 | // cards / helpers 466 | //================ 467 | 468 | //sorts by capture set 469 | function compareCards(a, b) { 470 | return getCardValue(b.querySelector("div.g_container > img").getAttribute("src")) - getCardValue(a.querySelector("div.g_container > img").getAttribute("src")) 471 | } 472 | 473 | //gets "value" of card for sorting 474 | function getCardValue(url) { 475 | let match = url.match(IMG_REGEX) 476 | if(CARD_SORT == "SCORE") return CAPTURE_SETS[match[1]] + CARD_TYPE[match[2]]*10 477 | else return CAPTURE_SETS[match[1]]*10 + CARD_TYPE[match[2]] 478 | } 479 | 480 | //returns list of cards from one of the twelve stacks 481 | function getStackCards(stack) { 482 | let cardlist = [] 483 | for(const card of Array.from(stack.querySelectorAll("img.g_card"))) { 484 | let match = card.getAttribute("src").match(IMG_REGEX) 485 | if(!match[0].includes("back.gif")) cardlist.push( {set:match[1], type:match[2]} ) 486 | } 487 | return cardlist 488 | } 489 | 490 | //returns list of cards from hand 491 | function getHandCards() { 492 | return Array.from( //gets hand as array 493 | $(`#player_hand > tbody > tr > td`).filter(function() { 494 | let element = $(this) 495 | return element.css('display') != "none" 496 | }) 497 | ).map((td) => { //maps each card by set and type 498 | let match = td.querySelector("img.g_card").getAttribute("src").match(IMG_REGEX) 499 | return {set:match[1], type:match[2]} 500 | }) 501 | } 502 | 503 | //pads a number with 0s at the front 504 | function padNum(num, n) { 505 | let str = num.toString() 506 | while (str.length < n) str = "0" + str 507 | return str 508 | } 509 | 510 | //copied this from one of my python programs lol 511 | //formats time as xx:xx.xxxs 512 | function formatTime(t) { 513 | let ms = t //ms 514 | 515 | //find minutes 516 | let min = Math.floor(ms / 60000) 517 | ms -= min * 60000 518 | 519 | //find seconds 520 | let sec = Math.floor(ms / 1000) 521 | ms -= sec * 1000 522 | 523 | //ms is the leftover 524 | return `${padNum(min, 2)}:${padNum(sec, 2)}.${padNum(ms, 3)}` 525 | } 526 | 527 | function addCSS() { 528 | document.head.appendChild(document.createElement("style")).innerHTML = ` 529 | #speedruntimer { 530 | vertical-align: bottom; 531 | } 532 | .countlabel { 533 | position: absolute; 534 | display: flex; 535 | justify-content: center; 536 | align-items: center; 537 | bottom: -18px; 538 | left: 2px; 539 | width: 24px; 540 | height: 24px; 541 | border-radius: 50%; 542 | text-align: center; 543 | font-weight: bold; 544 | background-color: rgba(255,204,0,0.95); 545 | pointer-events: none; 546 | } 547 | .caplabel { 548 | left: 0; 549 | top: 0; 550 | width: 24px; 551 | height: 24px; 552 | border-radius: 50%; 553 | justify-content: center; 554 | align-items: center; 555 | font-weight: bold; 556 | background-color: rgba(255,204,0,0.85); 557 | display: flex; 558 | position: absolute; 559 | z-index: 10000; 560 | } 561 | .handlabel { 562 | display: block; 563 | position: absolute; 564 | width: 130px; 565 | text-align: center; 566 | font-size: 9pt; 567 | font-weight: bold; 568 | left: 50%; 569 | padding: 2px 4px; 570 | transform: translateX(-50%); 571 | background-color: rgba(255,204,0,1); 572 | border-radius: 4px; 573 | pointer-events: none; 574 | } 575 | 576 | .handlabel { 577 | transition: 0.25s; 578 | opacity: 1.0; 579 | visibility: visible; 580 | } 581 | tbody:has(td.g_card_cell:hover) ~ .handlabel, .handlabel:hover { 582 | opacity: 0; 583 | visibility: hidden; 584 | } 585 | 586 | .sethighlight { 587 | display: block; 588 | position: absolute; 589 | box-sizing: border-box; 590 | width: 60px; 591 | height: 93px; 592 | border-style: solid; 593 | border-width: 8px; 594 | } 595 | .sethighlight::before { 596 | content: ""; 597 | display: block; 598 | position: absolute; 599 | box-sizing: border-box; 600 | width: 48px; 601 | height: 81px; 602 | left: -2px; 603 | top: -2px; 604 | border-style: solid; 605 | border-width: 2px; 606 | border-color: rgba(0,0,0,0.2); 607 | } 608 | .sethighlight::after { 609 | content: ""; 610 | display: block; 611 | position: absolute; 612 | box-sizing: border-box; 613 | width: 60px; 614 | height: 93px; 615 | left: -8px; 616 | top: -8px; 617 | border-style: solid; 618 | border-width: 2px; 619 | border-color: black; 620 | } 621 | 622 | #table_cards .g_container:has(.g_empty_card.highlighted)::after { 623 | content: ""; 624 | display: block; 625 | position: absolute; 626 | box-sizing: border-box; 627 | width: 64px; 628 | height: 97px; 629 | border: solid 4px white; 630 | background-color: rgba(255,255,255,0.7); 631 | z-index: 3000; 632 | } 633 | .sethighlight.dull:hover { 634 | border-color: #969696 !important; 635 | background-color: rgba(150,150,150,0.7); 636 | z-index: 1001 !important; 637 | } 638 | .g_card.highlighted.hl-selected + .sethighlight { 639 | border-color: yellow !important; 640 | background-color: rgba(255,204,0,0.35); 641 | z-index: 1001 !important; 642 | } 643 | .sethighlight:hover, .g_card.highlighted + .sethighlight { 644 | border-color: white !important; 645 | background-color: rgba(255,255,255,0.35); 646 | z-index: 1001 !important; 647 | } 648 | .g_card:has(+ .sethighlight:hover) { 649 | z-index: 1000 !important; 650 | } 651 | 652 | .g_hand, .g_capture, .g_card_cell { 653 | position: relative; 654 | } 655 | ` 656 | } 657 | }) 658 | } -------------------------------------------------------------------------------- /Neopets - Daily Puzzle Solver.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Daily Puzzle Solver 3 | // @version 1.4.1 4 | // @description Uses TheDailyNeopets' daily puzzle answers to automatically select the correct daily puzzle answer 5 | // @author Metamagic 6 | // @icon https://i.imgur.com/RnuqLRm.png 7 | // @match https://www.neopets.com/community* 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // @grant GM_deleteValue 11 | // @grant GM_xmlhttpRequest 12 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Daily%20Puzzle%20Solver.user.js 13 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Daily%20Puzzle%20Solver.user.js 14 | // ==/UserScript== 15 | 16 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 17 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 18 | 19 | // Trans rights are human rights ^^ 20 | // metty says hi 21 | 22 | //resets stored answer on new day 23 | let date = getDate() 24 | if(date != GM_getValue("date")) GM_deleteValue("dailypuzzle") 25 | GM_setValue("date", date) 26 | 27 | //saves the answer in case user comes back 28 | let dp = GM_getValue("dailypuzzle") 29 | addStatusDisplay() 30 | 31 | //no saved answer, get from TDN 32 | if(dp == null) requestTDNPage() 33 | 34 | //saved answer, use it 35 | else setAnswer(dp) 36 | 37 | //adds the status display message 38 | function addStatusDisplay() { 39 | if($("input[name='subbyvote']").length > 0 || $("input[name='pollres']").length > 0) return 40 | 41 | let cont = document.createElement("div") 42 | cont.style = "justify-content: flex-end; display: flex; align-items: center; margin-right: 12px; " 43 | 44 | let div = document.createElement("p") 45 | div.width = "16px !important" 46 | div.height = "16px !important" 47 | div.classList.add("question") 48 | div.style.fontSize = "14px" 49 | div.style.color = "gray" 50 | div.style.textAlign = "right" 51 | div.id = "dps_status" 52 | 53 | let img = document.createElement("div") 54 | img.style.opacity = "0.5" 55 | img.innerHTML = ` 56 | 57 | ` 58 | cont.appendChild(div) 59 | cont.appendChild(img) 60 | 61 | let parent = $("#community__2020 > div.community-top__2020 > div.puzzlepoll > div.puzzlepoll-container > div.dailypoll-left-content")[0] 62 | parent.appendChild(cont) 63 | } 64 | 65 | //parses the answer from TDN's page then selects said answer 66 | function setAnswer(resp) { 67 | //right question, grab answer 68 | //note: we have to spam .trim().normalize() because of hidden ascii chars and weird spaces 69 | let s1 = cleanString(document.querySelector("div.question.sf:not(:has(div.question.sf))").innerHTML) 70 | let s2 = cleanString(resp.q) 71 | if(s1 === s2) { 72 | GM_setValue("dailypuzzle", resp) 73 | let select = $("select[name='trivia_response']")[0] 74 | //find option that matches the right answer 75 | let option = Array.from(select.children).find( 76 | (e) => cleanString(e.innerHTML).includes(cleanString(resp.a)) 77 | ) 78 | select.value = option.value 79 | $("#dps_status")[0].innerHTML = "Answer selected, thanks TDN!" 80 | $("#community__2020 > div.community-top__2020 > div.puzzlepoll > div.puzzlepoll-container > div.dailypoll-left-content > div:nth-child(2) > form > select")[0].style.backgroundColor = "#bae8bb" 81 | } 82 | //not on answer page 83 | else if(!(s1.includes("That is correct!") || s1.includes("Oops! Nice try"))){ 84 | $("#dps_status")[0].innerHTML = "Answer not posted, check back later!" 85 | GM_deleteValue("dailypuzzle") 86 | } 87 | } 88 | 89 | function cleanString(str) { 90 | return str.trim().normalize().toLowerCase().replace(/\s+/g, ' ') 91 | } 92 | 93 | //gets the daily puzzle data from TDN's page 94 | function requestTDNPage() { 95 | console.log("[DPS] Grabbing Daily Puzzle from TDN...") 96 | $("#dps_status")[0].innerHTML = "Checking TheDailyNeopets for answer..." 97 | GM_xmlhttpRequest({ 98 | method: "GET", 99 | url: "https://thedailyneopets.com/index/fca", 100 | onload: function(response) { 101 | console.log("[DPS] Response received!") 102 | let doc = new DOMParser().parseFromString(response.responseText, "text/html") 103 | //blame TDN for this absolute mess lol 104 | let question = doc.querySelector("body > table > tbody > tr:nth-child(2) > td:nth-child(3) > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div:nth-child(1) > div").innerHTML 105 | let answer = doc.querySelector("body > table > tbody > tr:nth-child(2) > td:nth-child(3) > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div > div:nth-child(2)").childNodes[2].nodeValue 106 | setAnswer({q: question, a: answer}) 107 | } 108 | }) 109 | } 110 | 111 | 112 | function getDate() { 113 | return new Date().toLocaleString("en-US", {timeZone: "PST"}).slice(0, 10).replace(",","") 114 | } -------------------------------------------------------------------------------- /Neopets - Direct Link Display.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Direct Link Display 3 | // @version 0.5 4 | // @description Gives the results from direct links a much cleaner display and tracks # of coconut shy refreshes 5 | // @author Metamagic 6 | // @match https://www.neopets.com/halloween/process_cocoshy.phtml?coconut=* 7 | // @match https://www.neopets.com/halloween/strtest/process_strtest.phtml 8 | // @match https://www.neopets.com/amfphp/json.php/WheelService.spinWheel/* 9 | // @match http://ncmall.neopets.com/games/giveaway/process_giveaway.phtml 10 | // @icon https://i.imgur.com/RnuqLRm.png 11 | // @run-at document-start 12 | // @grant GM_setValue 13 | // @grant GM_getValue 14 | // @grant GM_deleteValue 15 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Direct%20Link%20Display.user.js 16 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Direct%20Link%20Display.user.js 17 | // ==/UserScript== 18 | 19 | const reactionmap = { 20 | 1:"good", 21 | 2:"bad" 22 | } 23 | 24 | const wheelmap = { 25 | 1: "Knowledge", 26 | 2: "Excitement", 27 | 3: "Mediocrity", 28 | 4: "Misfortune", 29 | 5: "Monotony", 30 | 6: "Extravagance" 31 | } 32 | 33 | //i only added this because i won one during development lol 34 | const coconutmap = { 35 | 26800: "Angry", 36 | 26806: "Burning", 37 | 26848: "Damaged", 38 | 27162: "Golden", 39 | 26873: "Hairy", 40 | 26804: "Horned", 41 | 26805: "Infernal", 42 | 27083: "Light Brown", 43 | 26807: "Mini", 44 | 27087: "Moaning", 45 | 26809: "Monstrous", 46 | 26875: "Stitched", 47 | 27085: "One Eyed", 48 | 26874: "Painted", 49 | 27082: "Scorched", 50 | 26803: "Screaming", 51 | 26808: "Silent", 52 | 26801: "Sinister", 53 | 27060: "Sliced", 54 | 26872: "Tusked", 55 | 27084: "Wailing", 56 | 26802: "Ugly", 57 | 27086: "Vicious" 58 | } 59 | 60 | const inactiveColor = {box: "#616161", content: "#C2C2C2"} 61 | 62 | const url = window.location.href 63 | 64 | //============================ 65 | // coconut shy refresh tracker 66 | //============================ 67 | 68 | //increments coconut shy on each refresh 69 | if(url.includes("/process_cocoshy.phtml")) { 70 | //resets count on new day 71 | if(GM_getValue("day", null) != getTime().date) { 72 | GM_setValue("cscount", 1) 73 | GM_setValue("day", getTime().date) 74 | } 75 | 76 | //subsequent refreshes 77 | addEventListener("beforeunload", (e) => { 78 | //increments refresh count 79 | let count = GM_getValue("cscount", 1) //first load = count of 1 80 | if(count < 20) count++ 81 | GM_setValue("cscount", count) 82 | //updates count right then 83 | document.querySelector("#container > div.header > div.count").innerHTML = "(Count: "+count+")" 84 | }) 85 | } 86 | 87 | //runs on page load 88 | (function() { 89 | if(!document.body.innerHTML.includes("Neopets - Checking Cookies")) { 90 | addCSS() 91 | let html = document.body.innerHTML 92 | let results = createResultsBox() 93 | let contents = results.querySelector("#contents") 94 | 95 | if(url.includes("/process_cocoshy.phtml")) displayCocoShy(results, contents, html) 96 | else if(url.includes("/process_strtest.phtml")) displayStrTest(results, contents, html) 97 | else if(url.includes("/WheelService.spinWheel/")) displayWheel(results, contents, html, wheelmap[url.match(/^.*([123456]).*/)[1]]) 98 | else if(url.includes("ncmall.neopets.com/games/giveaway/process_giveaway.phtml")) displayScarab(results, contents, html) 99 | 100 | document.body.textContent = "" 101 | document.body.appendChild(results) 102 | } 103 | })() 104 | 105 | function getTime(date = new Date(), zeroTime = false) { 106 | let d = date.toLocaleString("en-US", {timeZone: "America/Los_Angeles"}).split(",") 107 | if(zeroTime) return {date: d[0].trim(), time: "12:00:00 AM NST"} 108 | else return {date: d[0].trim(), time: d[1].trim()+" NST"} 109 | } 110 | 111 | const DAY_MS = 1000*60*60*24 112 | function getFinishTime(ms=0, resetOnNewDay=true) { 113 | let d = new Date() 114 | //add time 115 | if(ms > 0) d.setTime(d.getTime() + ms) 116 | //reset on midnight next day 117 | if(getTime(d).date != getTime().date && resetOnNewDay) return getTime(d, true) 118 | return getTime(d) 119 | } 120 | 121 | //==================== 122 | // direct link display 123 | //==================== 124 | 125 | 126 | function createResultsBox() { 127 | let cont = document.createElement("div") 128 | cont.classList.add("container") 129 | cont.id = "container" 130 | 131 | let header = document.createElement("div") 132 | header.classList.add("header") 133 | let title = document.createElement("div") 134 | title.classList.add("title") 135 | title.id = "title" 136 | header.appendChild(title) 137 | cont.appendChild(header) 138 | 139 | let box = document.createElement("div") 140 | box.classList.add("box") 141 | cont.appendChild(box) 142 | 143 | let contents = document.createElement("div") 144 | contents.classList.add("contents") 145 | contents.id = "contents" 146 | box.appendChild(contents) 147 | 148 | return cont 149 | } 150 | 151 | function displayCocoShy(results, contents, html) { 152 | //i actually won a coconut while developing this script. saved the result below for testing. 153 | //html = `points=10000&totalnp=4097858&success=4&prize_id=26874&error=Ach%21+See%2C+the+game+isn%27t+rigged+after+all%21++Tell+your+friends%2C+kid%21` 154 | //out of throws 155 | if(html.includes("success=0")) { 156 | results.style.backgroundColor = inactiveColor.box 157 | contents.style.backgroundColor = inactiveColor.content 158 | } 159 | else { 160 | results.style.backgroundColor = "#4D226B" 161 | contents.style.backgroundColor = "#E4D3EB" 162 | } 163 | results.querySelector("#title").innerHTML = "Coconut Shy" 164 | 165 | let countdiv = document.createElement("div") 166 | countdiv.classList.add("count") 167 | let count = Math.min(GM_getValue("cscount", 1), 20) 168 | countdiv.innerHTML = "(Count: "+count+")" 169 | countdiv.style.fontSize = "24px" 170 | results.children[0].appendChild(countdiv) 171 | 172 | let msg = document.createElement("p") 173 | msg.innerHTML = '"'+getMessage(html, "error=")+'"' 174 | contents.appendChild(msg) 175 | 176 | if(!html.includes("success=0")) { 177 | let npsummary = document.createElement("p") 178 | npsummary.innerHTML = `NP Spent: 100 NP
NP Earned: ${html.slice(7, html.length).split("&")[0]} NP
New NP Balance: ${html.split("totalnp=")[1].split("&")[0]} NP` 179 | 180 | //coconut prize 181 | if(html.includes("prize_id=")) { 182 | document.body.style.backgroundColor = "#BEF7C1" 183 | let id = html.split("prize_id=")[1].split("&")[0] 184 | let coconut = `${coconutmap[id]} Evil Coconut` || `Unknown Coconut (Item ID: ${id})` 185 | npsummary.innerHTML += `
Prize: ${coconut}` 186 | delay(50).then(() => {window.alert("You won an evil coconut!")}) 187 | } 188 | 189 | contents.appendChild(npsummary) 190 | } 191 | console.log("[DLD] Coconut Shy displayed.") 192 | } 193 | 194 | function displayStrTest(results, contents, html) { 195 | document.body.innerHTML = getMessage(html, "msg=")+"
This display is WIP!" 196 | } 197 | 198 | function displayWheel(results, contents, html, type) { 199 | let xml = JSON.parse(html) 200 | let msg = xml.errmsg || xml.reply || null 201 | let reaction = xml.reaction || 2 202 | let spinagain = xml.spinagain || false 203 | contents.innerHTML = `${type}: ${msg}
reaction=${reaction} (${reactionmap[reaction]})
spinagain=${spinagain}
html:
${html}` 204 | //reaction 1=happy 2=bad result 205 | } 206 | 207 | function displayScarab(results, contents, html) { 208 | if(html.includes("success=0")) { 209 | results.style.backgroundColor = inactiveColor.box 210 | contents.style.backgroundColor = inactiveColor.content 211 | } 212 | else { 213 | results.style.backgroundColor = "#FAAE2A" 214 | contents.style.backgroundColor = "#FFF6D9" 215 | //7hr 7min cd 216 | GM_setValue("scarabtime", getFinishTime(1000*60*(7*60+7)).time) 217 | } 218 | results.querySelector("#title").innerHTML = "Qasalan Expellibox" 219 | 220 | let tval = GM_getValue("scarabtime", null) 221 | if(tval != null) { 222 | let time = document.createElement("div") 223 | time.classList.add("count") 224 | time.style.paddingTop = "8px" 225 | time.style.paddingBottom = "8px" 226 | time.innerHTML = `Refreshes at
${tval}` 227 | results.children[0].appendChild(time) 228 | } 229 | 230 | 231 | let msg = document.createElement("div") 232 | msg.classList.add("msg") 233 | msg.innerHTML = getMessage(html, "msg=") 234 | contents.appendChild(msg) 235 | } 236 | 237 | function getMessage(html, tag) { 238 | return decodeURIComponent(html.split(tag)[1]).replaceAll("+", " ") 239 | } 240 | 241 | function delay(ms) { 242 | return new Promise(resolve => setTimeout(resolve, ms)) 243 | } 244 | 245 | function addCSS() { 246 | document.head.appendChild(document.createElement("style")).innerHTML = ` 247 | .container { 248 | width: 400px; 249 | height: fit-content; 250 | border: 2px solid #000; 251 | } 252 | .title { 253 | width: fit-content; 254 | height: 30px; 255 | background-color: inherit; 256 | text-align: center; 257 | line-height: 100%; 258 | font-size: 30px; 259 | font-weight: bold; 260 | font-family: "Impact", Copperplate; 261 | color: #FFFFFF; 262 | -webkit-text-stroke: 1px black; 263 | padding: 8px; 264 | padding-left: 0px; 265 | } 266 | .box { 267 | box-sizing: border-box; 268 | background-color: inherit; 269 | padding: 16px; 270 | padding-top: 0px; 271 | } 272 | .contents { 273 | width: 100%; 274 | height: fit-content; 275 | display: flex; 276 | flex-direction: column; 277 | font-size: 18px; 278 | line-height: 18px; 279 | } 280 | .contents > p { 281 | margin: 8px; 282 | } 283 | .count { 284 | width: fit-content; 285 | height: 30px; 286 | background-color: inherit; 287 | text-align: center; 288 | vertical-align: top; 289 | color: #FFFFFF; 290 | padding: 8px; 291 | display: block; 292 | position: relative; 293 | padding-right: 0px; 294 | 295 | } 296 | .header { 297 | box-sizing: border-box; 298 | padding: 0px 16px; 299 | width: 100% !important; 300 | display: flex; 301 | background-color: inherit; 302 | position: relative; 303 | justify-content: space-between; 304 | z-index: 1; 305 | } 306 | ` 307 | } 308 | -------------------------------------------------------------------------------- /Neopets - Home Page Redirect.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Home Page Redirect 3 | // @version 1.1 4 | // @description Simply redirects off the new homepage, for people who keep visiting it accidentally 5 | // @author Metamagic 6 | // @match *://*.neopets.com 7 | // @match *://*.neopets.com/ 8 | // @match *://neopets.com 9 | // @match *://neopets.com/ 10 | // @match *://*.neopets.com/index.phtml 11 | // @match *://neopets.com/index.phtml 12 | // @icon https://i.imgur.com/RnuqLRm.png 13 | // @grant none 14 | // @run-at document-start 15 | // ==/UserScript== 16 | 17 | window.location.replace("https://www.neopets.com/home/") -------------------------------------------------------------------------------- /Neopets - Inventory+.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Inventory Plus 3 | // @version 1.2 4 | // @description Improves the Inventory page by allowing multiple actions to be performed without a page refresh and saves selected options for items. 5 | // @author Metamagic 6 | // @match *://*.neopets.com/inventory.phtml* 7 | // @match *://neopets.com/inventory.phtml* 8 | // @icon https://i.imgur.com/RnuqLRm.png 9 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Inventory%2B.user.js 10 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Inventory%2B.user.js 11 | // @grant GM_getValue 12 | // @grant GM_setValue 13 | // ==/UserScript== 14 | 15 | // Trans rights are human rights ^^ 16 | // metty says hi 17 | 18 | //================ 19 | // script settings 20 | //================ 21 | 22 | (function($) { 23 | 24 | //lets you bypass the refresh by clicking outside the box 25 | const DISABLE_REFRESH = true 26 | //updates inventory contents when actions are performed 27 | const UPDATE_ON_ACTION = true 28 | //remembers the last selected option for each item 29 | const REMEMBER_SELECTION = true 30 | //defaults to unstacked view. for fans of the old neopets layout. ^^ 31 | const UNSTACK_VIEW_DEFAULT = false 32 | 33 | 34 | //============ 35 | // main script 36 | //============ 37 | 38 | 39 | $("#container__2020 > div.page-title__2020 > div.inv-title-container > h1")[0].innerHTML += "+" 40 | if(DISABLE_REFRESH) $("#refreshshade__2020")[0].remove() 41 | if(UNSTACK_VIEW_DEFAULT) setStackView("unstack") 42 | 43 | 44 | //watches for item description popup 45 | const descObs = new MutationObserver((mutations) => { 46 | if(REMEMBER_SELECTION) { 47 | let selection = $("#iteminfo_select_action").find("option:selected")[0] 48 | if(selection) { 49 | if(selection.innerHTML == "Choose an Action") setRememberedSelection() 50 | } 51 | rememberSelections() 52 | } 53 | }) 54 | descObs.observe(document.getElementById("invDesc"), {childList: true, subtree: true}) 55 | 56 | 57 | //NOTE: script may not be compatible with certain selection-based capsules (eg essentials) - i don't have the nc to test this unfort 58 | //watches for item results popup 59 | let prevDisplay = 'none' 60 | const resultObs = new MutationObserver((mutations) => { 61 | //if not loading 62 | if($("#invResult div.inv-loading-static").length == 0) { 63 | //special case for gift box menu 64 | if($("#invResult div.giftgram-img").length > 0 && $("#invResult").css('display') == 'block') { 65 | //gift sent, refresh page 66 | console.log($("#invResult > div.popup-body__2020 > p")?.[0]) 67 | if($("#invResult > div.popup-body__2020 > p")?.[0]?.innerHTML?.includes("Your gift has been delivered to")) { 68 | if(UPDATE_ON_ACTION) refreshInventory() 69 | if(DISABLE_REFRESH) disableRefresh() 70 | } 71 | else if(DISABLE_REFRESH) disableRefresh(false) 72 | prevDisplay = 'block' 73 | } 74 | //now showing results 75 | else if($("#invResult").css('display') == 'block' && prevDisplay == 'none') { 76 | if(UPDATE_ON_ACTION) refreshInventory() 77 | if(DISABLE_REFRESH) disableRefresh() 78 | prevDisplay = 'block' 79 | } 80 | //no longer showing results 81 | else if($("#invResult").css('display') == 'none' && prevDisplay == 'block') { 82 | prevDisplay = 'none' 83 | } 84 | } 85 | //adds unclickable shade if loading 86 | else { 87 | document.getElementById("invpopupshade__2020").setAttribute("style", "display: block;") 88 | } 89 | }) 90 | resultObs.observe(document.getElementById("invResult"), {attributes:true}) 91 | 92 | let reload = false 93 | reloadImages() 94 | 95 | //================ 96 | // functionalities 97 | //================ 98 | 99 | function disableRefresh(addButton = true) { 100 | document.getElementById("invpopupshade__2020").setAttribute("style", "display: none;") 101 | document.getElementById("navpopupshade__2020").setAttribute("style", "display: block;") 102 | //updates the button to not refresh 103 | $("#invResult > div.popup-header__2020 > a")[0].removeAttribute("href") 104 | $("#invResult > div.popup-header__2020 > a > div")[0].setAttribute("onclick", "togglePopup__2020()") 105 | //adds refresh button 106 | if(addButton) { 107 | let a = getRefreshButton() 108 | $("#invResult > div.popup-body__2020")[0].appendChild(a) 109 | } 110 | } 111 | 112 | function getRefreshButton() { 113 | let a = document.createElement("a") 114 | a.href = "/inventory.phtml" 115 | let button = document.createElement("div") 116 | button.classList.add("button-default__2020", "button-yellow__2020", "btn-single__2020") 117 | button.innerHTML = "Refresh Page" 118 | a.appendChild(button) 119 | return a 120 | } 121 | 122 | function rememberSelections() { 123 | $("#iteminfo_select_action > select").change(() => { 124 | let option = $("#iteminfo_select_action").find("option:selected").val() 125 | let itemname = document.getElementById("invItemName").innerHTML 126 | let list = GM_getValue("invselect", {}) 127 | list[itemname] = option 128 | GM_setValue("invselect", list) 129 | console.log(`[Inv+] Selection remembered for item '${itemname}'`) 130 | }) 131 | } 132 | 133 | function setRememberedSelection() { 134 | console.log("[Inv+] Attempting to set") 135 | let itemname = document.getElementById("invItemName").innerHTML 136 | let memory = GM_getValue("invselect", {})[itemname] 137 | if(memory != undefined) { 138 | $("#iteminfo_select_action > select")[0].value = memory 139 | $("#iteminfo_select_action > div.invitem-submit")[0].classList.remove("disabledButton") 140 | console.log(`[Inv+] Selection set for item '${itemname}'`) 141 | } 142 | } 143 | 144 | //you have to do this because neopets code put a lowercase instead of an uppercase in their code. incredible. 145 | function reloadImages() { 146 | let invObs = new MutationObserver((mutations) => { 147 | if(reload && $(`#invDisplay > div.inv-display:not([style="display: none;"])`).length && $('.lazy').length) { 148 | reload = false 149 | $('.lazy').Lazy({ 150 | threshold: 50, 151 | visibleOnly: true, 152 | }) 153 | console.log("[Inv+] Item images force-loaded.") 154 | } 155 | }) 156 | invObs.observe($("#invDisplay")[0], {childList:true, subtree:true}) 157 | } 158 | 159 | function refreshInventory() { 160 | // Temporary Loading Image 161 | $('#invDisplay' + 1).html("


Loading...

"); 162 | 163 | $.get("https://www.neopets.com/inventory.phtml", ()=>{ 164 | console.log("[Inv+] Refreshed inventory contents.") 165 | $("#tabs > div.inv-tabs-container > ul.invTabs > li.inv-tab-label.invTab-selected")[0].click() //refreshes the inventory tab 166 | reload = true 167 | }) 168 | } 169 | 170 | function setStackView(type) { 171 | //if it's stacked and we want unstack 172 | if(type == "unstack" && $("#invStack > div.stack-icon-container.invfilter-active").length > 0) { 173 | document.getElementById("invStack").click() 174 | } 175 | //if it's unstacked and we want stacked 176 | else if(type == "stack" && $("#invStack > div.unstack-icon-container.invfilter-active").length > 0) { 177 | document.getElementById("invStack").click() 178 | } 179 | //simply toggles it 180 | else document.getElementById("invStack").click() 181 | } 182 | })(jQuery) -------------------------------------------------------------------------------- /Neopets - LoD Shame Tracker.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Lever of Doom Shame Tracker 3 | // @version 1.2 4 | // @description Tracks the amount of money you have lost to the Lever of Doom. 5 | // @author Metamagic 6 | // @match *neofood.club/* 7 | // @match *://www.neopets.com/space/strangelever.phtml* 8 | // @match *://neopets.com/space/strangelever.phtml* 9 | // @match *://www.neopets.com/space/leverofdoom.phtml* 10 | // @match *://neopets.com/space/leverofdoom.phtml* 11 | // @grant GM_getValue 12 | // @grant GM_setValue 13 | // @icon https://i.imgur.com/RnuqLRm.png 14 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20LoD%20Shame%20Tracker.user.js 15 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20LoD%20Shame%20Tracker.user.js 16 | // ==/UserScript== 17 | 18 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 19 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 20 | 21 | // Trans rights are human rights ^^ 22 | // metty says hi 23 | 24 | if(window.location.href.includes("leverofdoom.phtml")) { 25 | //counts first pull 26 | if(GM_getValue("firstpull", false)) { 27 | let count = GM_getValue("pullcount", 0) //first load = count of 1 28 | count++ 29 | GM_setValue("pullcount", count) 30 | GM_setValue("firstpull", false) 31 | } 32 | 33 | //displays pull count 34 | $("#container__2020 > p")[0].innerHTML += "

Lever Pulls: "+GM_getValue("pullcount", 1)+" | "+"Neopoints Lost: "+(GM_getValue("pullcount", 1)*100).toLocaleString("en-US")+"<\p>" 35 | let np = $("#npanchor")[0].innerHTML.replaceAll(",", "") 36 | 37 | //displays fake avatar popup if you have the avvie 38 | if(GM_getValue("hasAvvie")) { 39 | let div = document.createElement("div") 40 | div.style="display:block;position:relative;margin:auto;" 41 | let img = document.createElement("img") 42 | img.src = "https://i.imgur.com/2y2bO34.png" 43 | img.width = "401" 44 | img.height = "90" 45 | img.style = "display:block;position:relative;margin:auto;" 46 | div.appendChild(img) 47 | let disc = document.createElement("div") 48 | disc.innerHTML = "(you can stop pulling now)" 49 | disc.style = "display:block;position:absolute;left:50%;bottom:0px;transform:translateX(-50%);color:black;width:200px;height:20px;" 50 | div.appendChild(disc) 51 | $("#container__2020")[0].insertBefore(div, $("#container__2020 > div.page-title__2020")[0]) 52 | console.log("[LoD] Added fake event display.") 53 | } 54 | 55 | //doesnt count as refresh if any link on the page is clicked 56 | let linkClicked = false 57 | $("body").on("click", "a", function () { 58 | linkClicked = true 59 | }) 60 | 61 | //each unload (aka refresh), count a pull 62 | window.addEventListener("beforeunload", (e) => { 63 | if(!linkClicked) { 64 | //increments refresh count 65 | let count = GM_getValue("pullcount", 1) 66 | count++ 67 | GM_setValue("pullcount", count) 68 | //updates count right then 69 | $("#container__2020 > p > p:nth-child(3)")[0].innerHTML = "Lever Pulls: "+count+" | "+"Neopoints Lost: "+(count*100).toLocaleString("en-US") 70 | } 71 | }) 72 | 73 | //checks neoboards settings for if you have the avatar 74 | if(!GM_getValue("hasAvvie")) { 75 | console.log("Checking for avatar...") 76 | $.get("https://www.neopets.com/settings/neoboards/", function(data, status){ 77 | let doc = new DOMParser().parseFromString(data, "text/html") 78 | //sometimes stackpath blocks the page, in which case open a newtab and close it after 79 | if(doc.title != "Neoboard Settings") { 80 | console.log("[APS] Neoboard Settings request blocked by stackpath. :(") 81 | } 82 | //find avvie from list 83 | let avvie = Array.from(doc.querySelectorAll("#SelectAvatarPopup > div.popup-body__2020 > div:nth-child(3) > div.settings-av")).find((div) => {return div.innerHTML.includes("Lever of Doom")}) 84 | if(avvie) { 85 | GM_setValue("hasAvvie", true) 86 | console.log("[LoD] Lever of Doom avatar earned! Congrats!") 87 | } 88 | else console.log("[LoD] No avatar yet. Keep trying!") 89 | }) 90 | } 91 | } 92 | else { 93 | GM_setValue("firstpull", true) 94 | } -------------------------------------------------------------------------------- /Neopets - Mystery Pic Helper.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Mystery Pic Helper 3 | // @version 1.0 4 | // @description Adds Image Emporium quicklinks to the Mystery Pic page 5 | // @author Metamagic 6 | // @match *://*.neopets.com/games/mysterypic.phtml* 7 | // @icon https://i.imgur.com/RnuqLRm.png 8 | // @grant none 9 | // ==/UserScript== 10 | 11 | const SLOTH_ICON = `Sloth Emporium Icon` 12 | 13 | let links = document.createElement("div") 14 | links.classList.add("links") 15 | links.innerHTML = `${SLOTH_ICON}Shopkeeper Banners| 16 | ${SLOTH_ICON}Game Icons | 17 | ${SLOTH_ICON}World Images` 18 | $("#content > table > tbody > tr > td.content")[0].insertBefore(links, $("#content > table > tbody > tr > td.content > center:nth-child(5)")[0]) 19 | 20 | document.head.appendChild(document.createElement("style")).innerHTML = ` 21 | .links { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | .links > * { 27 | padding: 0px 4px; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | } 32 | .links > a > img { 33 | padding-right: 4px; 34 | } 35 | ` 36 | 37 | console.log("[MPH] Quicklinks added.") -------------------------------------------------------------------------------- /Neopets - Negg Cave Solver.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Negg Cave Solver 3 | // @version 1.1 4 | // @description Uses TDN's Negg Solver to autofill the solution to 99.9% of puzzles 5 | // @author Metamagic 6 | // @match https://www.neopets.com/shenkuu/neggcave/ 7 | // @grant GM_getValue 8 | // @grant GM_setValue 9 | // @grant GM_xmlhttpRequest 10 | // @icon https://i.imgur.com/RnuqLRm.png 11 | // @require https://thedailyneopets.com/uploads/js/negg-puzzle-solver.js 12 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Negg%20Cave%20Solver.user.js 13 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Negg%20Cave%20Solver.user.js 14 | // ==/UserScript== 15 | 16 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 17 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 18 | 19 | // Trans rights are human rights ^^ 20 | // metty says hi 21 | 22 | const PLACEMENT_DELAY = 25 //delay between tile autofills in ms, set to 0 to disable delay (default: 25) 23 | 24 | //TDN's solver returns tiles indexed from 0-8 but we need shape & color indexes to place 25 | const TILE_MAP = { 26 | //blue 27 | 0: {c:0, s:0}, //swirl 28 | 1: {c:0, s:1}, //fire 29 | 2: {c:0, s:2}, //wind 30 | //red 31 | 3: {c:1, s:0}, //swirl 32 | 4: {c:1, s:1}, //fire 33 | 5: {c:1, s:2}, //wind 34 | //yellow 35 | 6: {c:2, s:0}, //swirl 36 | 7: {c:2, s:1}, //fire 37 | 8: {c:2, s:2} //wind 38 | } 39 | 40 | //first time setup 41 | if(GM_getValue("autosolve") == null) GM_setValue("autosolve", false) 42 | 43 | //if the grid is present on the page, run this script 44 | if($("#mnc_negg_grid").length > 0) { 45 | addCSS() 46 | addNeggSolverUI() 47 | if(GM_getValue("autosolve")) $("#ncs-button")[0].click() 48 | } 49 | 50 | //adds buttons to the page that users can click and a toggle to automatically do it 51 | //also adds a status box that displays text 52 | function addNeggSolverUI() { 53 | $("#mnc_bg")[0].innerHTML += ` 54 |

55 |
Thanks to TheDailyNeopets for the lovely solver!
56 |
57 |

Auto-Solve

58 | 61 |
62 |
63 |
SOLVE!
64 | ` 65 | 66 | if(GM_getValue("autosolve")) $("#ncs-toggle > label > input")[0].checked = true 67 | $("#ncs-toggle > .switch > input")[0].addEventListener("click", toggleAutoSolve) 68 | $("#ncs-button")[0].addEventListener("click", ()=>{$("#ncs-msg")[0].innerHTML = "Finding solution..."; setTimeout(()=>{solveNeggPuzzle()}, 500);}) 69 | } 70 | 71 | //toggles setting 72 | function toggleAutoSolve() { 73 | GM_setValue("autosolve", !GM_getValue("autosolve")) 74 | console.log("[NCS] Toggled auto-solve.") 75 | } 76 | 77 | //uses TDN loop to find sol'n then fills the grid 78 | async function solveNeggPuzzle() { 79 | console.log("[NCS] Finding puzzle solution...") 80 | let source = new XMLSerializer().serializeToString(document); 81 | let solution = SolvePuzzle(source) //thanks TDN! 82 | console.log("[NCS] Solution found, autofilling grid...") 83 | $("#ncs-msg")[0].innerHTML = "Solution found, filling grid..." 84 | 85 | let i = 0 86 | function solveLoop() { 87 | setTimeout(() => { 88 | placeTile(i, solution[i]) 89 | i++ 90 | if(i < 9) solveLoop() 91 | else { 92 | console.log("[NCS] Negg Puzzle solved!") 93 | $("#ncs-msg")[0].innerHTML = "Negg Puzzle solved, thanks TheDailyNeopets!" 94 | } 95 | }, PLACEMENT_DELAY) 96 | } 97 | 98 | solveLoop() 99 | } 100 | 101 | //places a tile on the grid 102 | function placeTile(i, n) { 103 | let tile = TILE_MAP[n] 104 | clickTile(tile.s, tile.c, i) 105 | } 106 | 107 | //emulates the clicks needed to place a tile 108 | function clickTile(s, c, i) { 109 | let symbol = $(`#mnc_parch_ui_symbol_${s}`)[0] 110 | if(!symbol.classList.contains("selected")) symbol.click() 111 | let color = $(`#mnc_parch_ui_color_${c}`)[0] 112 | if(!color.classList.contains("selected")) color.click() 113 | $("#mnc_negg_grid")[0].children[i].click() 114 | } 115 | 116 | //css 117 | function addCSS() { 118 | 119 | document.head.appendChild(document.createElement("style")).innerHTML = ` 120 | /*this css is a mess please do not use this as example*/ 121 | #ncs-button { 122 | left: 464px; 123 | top: 23px; 124 | display: block; 125 | position: relative; 126 | background: linear-gradient(110.1deg, rgb(34, 126, 34) 2.9%, rgb(168, 251, 60) 90.3%); 127 | border: 4px solid black; 128 | border-radius: 100%; 129 | width: 70px; 130 | height: 70px; 131 | text-align: center; 132 | line-height: 70px; 133 | font-size: 22px; 134 | font-family: Impact; 135 | } 136 | 137 | #ncs-status { 138 | background-color: #C9B483; 139 | border: 4px solid #706449; 140 | border-radius: 25%; 141 | padding: 4px; 142 | width: 180px; 143 | height: 90px; 144 | display: flex; 145 | position: relative; 146 | justify-content: center; 147 | flex-direction: column; 148 | top: 2%; 149 | left: 73%; 150 | } 151 | #ncs-toggle { 152 | display: flex; 153 | justify-content: center; 154 | align-items: center; 155 | height: 20px; 156 | padding: 10px; 157 | text-align: center; 158 | } 159 | #ncs-msg { 160 | display: flex; 161 | justify-content: center; 162 | align-items: center; 163 | text-align: center; 164 | } 165 | #ncs-toggle > p { 166 | display: flex; 167 | justify-content: center; 168 | align-items: center; 169 | text-align: center; 170 | } 171 | 172 | /* modified from https://www.w3schools.com/howto/howto_css_switch.asp */ 173 | .switch { 174 | position: relative; 175 | display: inline-block; 176 | width: 40px; 177 | height: 23px; 178 | margin-left: 6px; 179 | font-size: 0.9em; 180 | } 181 | .switch input { 182 | opacity: 0; 183 | width: 0; 184 | height: 0; 185 | } 186 | .slider { 187 | position: absolute; 188 | cursor: pointer; 189 | top: 0; 190 | left: 0; 191 | right: 0; 192 | bottom: 0; 193 | background-color: #ccc; 194 | -webkit-transition: .4s; 195 | transition: .4s; 196 | } 197 | .slider:before { 198 | position: absolute; 199 | content: ""; 200 | height: 17px; 201 | width: 17px; 202 | left: 3px; 203 | bottom: 3px; 204 | background-color: white; 205 | -webkit-transition: .4s; 206 | transition: .4s; 207 | } 208 | input:checked + .slider { 209 | background-color: #2196F3; 210 | } 211 | input:focus + .slider { 212 | box-shadow: 0 0 1px #2196F3; 213 | } 214 | input:checked + .slider:before { 215 | -webkit-transform: translateX(17px); 216 | -ms-transform: translateX(17px); 217 | transform: translateX(17px); 218 | } 219 | .slider.round { 220 | border-radius: 34px; 221 | } 222 | .slider.round:before { 223 | border-radius: 50%; 224 | } 225 | ` 226 | } 227 | -------------------------------------------------------------------------------- /Neopets - NeoCola Enhancements.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - NeoCola Enhancements 3 | // @version 1.6 4 | // @description Improves the NeoCola machine by tracking results, improving the UI and enabling the "legal cheat". 5 | // @author Metamagic 6 | // @match https://www.neopets.com/moon/neocola2.phtml* 7 | // @match https://www.neopets.com/moon/neocola3.phtml* 8 | // @icon https://i.imgur.com/RnuqLRm.png 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // @grant GM_deleteValue 12 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20NeoCola%20Enhancements.user.js 13 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20NeoCola%20Enhancements.user.js 14 | // ==/UserScript== 15 | 16 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 17 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 18 | 19 | // Trans rights are human rights ^^ 20 | // metty says hi 21 | 22 | //TODO: display div instead of absolute, add ability to export data, resetting stored stuff and doing something w it, record avg rolls til transmog and # of transmogs 23 | //TODO: use neocola2 and emulated requests to keep a queue of requests, allowing spammed requests to still be logged and create special display for it 24 | // - item queue of most recent x prizes, color-coded hehe 25 | 26 | //============== 27 | // script config 28 | //============== 29 | 30 | // https://web.archive.org/web/20210619183531/https://old.reddit.com/r/neopets/comments/o3mq8r/neocola_tokens/ 31 | const ENABLE_LEGAL_CHEAT = true // adds another index option, granting more neopoints as defined by u/neo_truths 32 | const ALERT_ON_TRANSMOG = true // gives an alert when you earn a transmog prize to prevent accidentally refreshing past it 33 | const BETTER_RESUBMIT = true //uses neocola2 and emulated requests to keep a queue of requests, allowing spammed requests to still be logged 34 | const START_TIMEOUT = 6000 35 | const RESUBMIT_TIMEOUT = 16000 36 | const DISPLAY_STATS_ON_MAIN_PAGE = true //displays total stats on the main neocola page 37 | 38 | //============== 39 | // main function 40 | //============== 41 | 42 | const TOKEN_IDS = {"blue": 24538, "green":24539, "red":24540} 43 | const FLAVOR_NAMES = [ 44 | "Piss Water", 45 | "Can of WellCheers", 46 | "MUG Root Beer, 355mL", 47 | "Undefined Mystery Liquid", 48 | "Liquid Void", 49 | "Cursed Elixir", 50 | "Molten Plastic", 51 | "Healing Springs Bath Water", 52 | "gmuy flavor :3", 53 | "Wild Magic Surge", 54 | "Clarity", 55 | "...Don't Drink This", 56 | "Definitely Not Booze", 57 | "Butt Ooze", 58 | "Potion of Harming I", 59 | "Liquid NP", 60 | "Get Rich Quick Juice", 61 | "Diet Neocash", 62 | "Bone Hurting Juice", 63 | "Person Transmogrification Potion", 64 | "I Know Where You Live" 65 | ] 66 | const CURSED_PRIZES = ["Transmogrification Lab Background", "A Beginners Guide to Mixing Transmogrification Potions", "Evil Transmogrification Potions"] 67 | 68 | convertOldData() 69 | 70 | //selection machine 71 | if(window.location.href.includes("neocola2.phtml")) { 72 | var reqQueue = 0 73 | if(!$("#content td.content")[0].innerHTML.includes("Sorry! You don't have any NeoCola Tokens so you can't use the machine. :(")) { 74 | addSelectCSS() 75 | let count = countTokenColors() //counts tokens 76 | GM_setValue("tokencount", count) //stores token count 77 | compressTokenDisplay(count) //compresses the giant token list 78 | modifyInputs() //cleans up and autofills inputs 79 | if(BETTER_RESUBMIT) { 80 | addQueueDisplay($("#content td.content > form input[type=submit]")[0]) 81 | } 82 | } 83 | addPrizeCSS() 84 | displayAllStats() 85 | } 86 | else if(window.location.href.includes("neocola3.phtml")) { 87 | addPrizeCSS() 88 | modifyPrizePage() 89 | } 90 | 91 | function modifyPrizePage() { 92 | //records data 93 | GM_setValue("totalcount", GM_getValue("totalcount", 0) + 1) 94 | let input = GM_getValue("input") 95 | if(input) { //np stats require a known input combo to record / display 96 | //we used a token, increment it 97 | let n = GM_getValue("tokensused", 0) 98 | n += 1 99 | GM_setValue("tokensused", n) 100 | recordNP(input) //records np prize 101 | } 102 | recordItem() //records item prize 103 | 104 | //displays data 105 | if(input) $("#content td.content")[0].appendChild(displayTokenNPStats(input)) 106 | displayItemStats() 107 | $("#content td.content")[0].appendChild(displayTransmogStats()) 108 | $("#content td.content form > input[type=submit]")[0].value = "Run Awaaaaay!!!" //changes the text on the button to be a bit less misleading 109 | $("#content td.content form")[0].setAttribute("action", "neocola2.phtml") //brings to neocola2 instead of neocola1 110 | addSendButton() 111 | } 112 | 113 | 114 | //========== 115 | // selection 116 | //========== 117 | 118 | //gets the count of each token color 119 | function countTokenColors() { 120 | let colors = {"red":0, "green":0, "blue":0} 121 | let display = $("#content > table > tbody > tr > td.content > div[align='center']")[0] 122 | let tokens = Array.from(display.children).slice(6) 123 | 124 | //count number of each token color 125 | for(const token of tokens) { 126 | //find which color it matches and increment it 127 | for(const color of Object.keys(colors)) { 128 | if(token.getAttribute("src").includes(color)) { 129 | colors[color] += 1 130 | break 131 | } 132 | } 133 | } 134 | return colors 135 | } 136 | 137 | //compresses the giant list of token displays by displaying count per color 138 | function compressTokenDisplay(count) { 139 | let display = $("#content > table > tbody > tr > td.content > div[align='center']")[0] 140 | let machine = Array.from(display.children).slice(0, 6) 141 | 142 | //clear old token display 143 | display.innerHTML = "" 144 | //adds machine img back 145 | for(const e of machine) display.appendChild(e) 146 | 147 | //adds new token display 148 | let tokenDisplay = document.createElement("div") 149 | tokenDisplay.classList.add("token_display") 150 | for(const color of Object.keys(count)) makeTokenDisplay(tokenDisplay, color, count[color]) 151 | display.appendChild(tokenDisplay) 152 | } 153 | 154 | //makes one of the three token image and count displays 155 | function makeTokenDisplay(display, color, count) { 156 | if(count > 0) { 157 | display.innerHTML += `
158 |
x${count}
159 |
` 160 | if(color != "blue") display.innerHTML += " " 161 | } 162 | } 163 | 164 | //from the list of mystery brews! 165 | function getRandomFlavor() { 166 | return FLAVOR_NAMES[Math.floor(Math.random() * FLAVOR_NAMES.length)] 167 | } 168 | 169 | //handles the token inputs 170 | function modifyInputs() { 171 | //turns token display into input 172 | //updates text 173 | $("#content > table > tbody > tr > td.content > div[align='center'] > b")[0].innerHTML = "What color will you use?" 174 | 175 | //button starts inactive 176 | $("#content td.content > form input[type=submit]")[0].disabled = true 177 | 178 | for(const d of Array.from($(".token_display")[0].children)) d.classList.add("inactive") 179 | 180 | //turns imgs into inputs 181 | for(const div of Array.from($(".token_display")[0].children)) { 182 | div.style.cursor = "pointer" 183 | div.addEventListener("click", ()=>{ 184 | $(`select[name="token_id"]`).val(div.getAttribute("value")) 185 | //select token in form 186 | let tokens = Array.from($(".token_display")[0].children) 187 | //update active token 188 | for(const d of tokens) d.classList.add("inactive") 189 | div.classList.remove("inactive") 190 | //make submit button active again 191 | $("#content td.content > form input[type=submit]")[0].disabled = false 192 | }) 193 | } 194 | 195 | //hides additional space 196 | $("#content > table > tbody > tr > td.content > b")[0].remove() 197 | $("#content > table > tbody > tr > td.content > form > table > tbody > tr:nth-child(1)")[0].style.visibility = "hidden" 198 | 199 | //autofills inputs 200 | //selects neocola flavor - higher index = more np 201 | let ncf = $(`select[name="neocola_flavor"]`)[0] 202 | //legal cheat adds new option and selects it 203 | if(ENABLE_LEGAL_CHEAT) { 204 | let ncf = $(`select[name="neocola_flavor"]`)[0] 205 | ncf.innerHTML += `` 206 | ncf.value = 421 207 | console.log("[NCE] Additional flavor option added.") 208 | } 209 | //otherwise chooses final index 210 | else ncf.value = 7 211 | 212 | //selects button presses - higher index = more np 213 | $(`select[name="red_button"]`)[0].value = 3 214 | 215 | //records what inputs were selected 216 | let submitted = false 217 | $("#content td.content > form input[type=submit]")[0].addEventListener("click", (event) => { 218 | //clears last run's use count 219 | if(!submitted) { 220 | GM_deleteValue("tokensused") 221 | //selected color 222 | let id = $(`select[name="token_id"] > option:selected`)[0].getAttribute("value") 223 | let color = Object.keys(TOKEN_IDS).find((c) => {return TOKEN_IDS[c] == id}) 224 | //selected flavor 225 | let flavor = $(`select[name="neocola_flavor"] > option:selected`)[0].getAttribute("value") 226 | //# of presses 227 | let button = $(`select[name="red_button"] > option:selected`)[0].getAttribute("value") 228 | GM_setValue("input", {color: color, flavor: flavor, button: button}) 229 | console.log(`[NCE] Remembered token input selections.`) 230 | if(BETTER_RESUBMIT) console.log("[NCE] Using better token submit system.") 231 | } 232 | if(BETTER_RESUBMIT) { 233 | sendRequest(START_TIMEOUT) 234 | event.preventDefault() 235 | event.stopPropagation() 236 | } 237 | }) 238 | } 239 | 240 | //=========== 241 | // prize data 242 | //=========== 243 | 244 | //code used from stackoverflow 245 | //https://stackoverflow.com/questions/1344500/efficient-way-to-insert-a-number-into-a-sorted-array-of-numbers 246 | //god damn it this is broken 247 | function sortedInsert(arr, val) { 248 | arr.push(val); 249 | for (let i = arr.length - 1; i > 0 && arr[i] < arr[i-1]; i--) { 250 | let tmp = arr[i] 251 | arr[i] = arr[i-1] 252 | arr[i-1] = tmp 253 | } 254 | return arr 255 | } 256 | 257 | //i changed how data is stored so this makes sure data gets updated 258 | function convertOldData() { 259 | let results = GM_getValue("results") 260 | if(results) { 261 | console.log("[NCE] Converting old data...") 262 | //initializes empty objects 263 | let items = {} 264 | 265 | //we assume they used the best flavor and button 266 | if(ENABLE_LEGAL_CHEAT) var f = 421 267 | else f = 7 268 | 269 | let np = {} 270 | np[`{"color":"red","flavor":"${f}","button":"3"}`] = {list:[], avg:null, total:0, count: 0} 271 | np[`{"color":"green","flavor":"${f}","button":"3"}`] = {list:[], avg:null, total:0, count: 0} 272 | np[`{"color":"blue","flavor":"${f}","button":"3"}`] = {list:[], avg:null, total:0, count: 0} 273 | 274 | let totalcount = 0 275 | 276 | let transmog = {prob: null, n_since: null, list:[]} 277 | 278 | let i = 0 279 | for(const color of Object.keys(TOKEN_IDS)) { 280 | //items 281 | let res = results[color] 282 | for(const item of Object.keys(res.items)) { 283 | let data = res.items[item] 284 | if(!Object.keys(items).includes(item)) items[item] = data //first of item, drag data in 285 | else items[item].count += data.count //not first, add count to existing data 286 | } 287 | 288 | //np 289 | let npkey = Object.keys(np)[i] 290 | np[npkey].list = res.np.sort((a, b) => a - b) 291 | np[npkey].avg = res.avg_np 292 | np[npkey].count = res.count 293 | np[npkey].total = res.np.reduce((a, b) => a + b, 0) 294 | totalcount += res.count 295 | 296 | i++ 297 | } 298 | 299 | //transmog 300 | transmog.prob = results.prob 301 | transmog.n_since = results.n_since 302 | 303 | //saves in new data 304 | GM_setValue("item_res", items) 305 | GM_setValue("np_res", np) 306 | GM_setValue("transmog_res", transmog) 307 | GM_setValue("totalcount", totalcount) 308 | GM_deleteValue("results") 309 | console.log("[NCE] Data converted.") 310 | } 311 | } 312 | 313 | function recordNP(input) { 314 | let np = parseInt($("#content > table > tbody > tr > td.content > div[align='center'] > b:first-of-type")[0].innerHTML.replaceAll(",", "")) 315 | let np_res = GM_getValue("np_res", {}) //our previous results 316 | let npkey = JSON.stringify(input) 317 | //inserts in sorted place 318 | if(np_res[npkey]) sortedInsert(np_res[npkey].list, np) 319 | //initializes if empty 320 | else { 321 | np_res[npkey] = {} 322 | np_res[npkey].list = [np] 323 | } 324 | 325 | //update average np by summing up total earnings and adding our current earnings 326 | if(np_res[npkey].avg) np_res[npkey].avg = (np_res[npkey].avg * np_res[npkey].count + np) / (np_res[npkey].count + 1) 327 | else np_res[npkey].avg = np 328 | 329 | if(np_res[npkey].total) np_res[npkey].total += np 330 | else np_res[npkey].total = np 331 | 332 | if(np_res[npkey].count) np_res[npkey].count += 1 333 | else np_res[npkey].count = 1 334 | 335 | GM_setValue("np_res", np_res) 336 | } 337 | 338 | function recordItem() { 339 | //item prize 340 | let itemname = $("#content td.content > div[align='center'] > b:last-of-type")[0].innerHTML 341 | let item_res = GM_getValue("item_res", {}) 342 | //recorded before, increase count 343 | if(item_res[itemname]) item_res[itemname].count += 1 344 | //first of item, add data 345 | else item_res[itemname] = {count: 1, img: $("#content td.content > div[align='center'] > img:last-of-type")[0].src} 346 | GM_setValue("item_res", item_res) 347 | 348 | //inc. total count 349 | GM_setValue("totalcount", GM_getValue("totalcount", 0) + 1) 350 | 351 | //updates transmog odds based on item 352 | updateTransmogStats(itemname) 353 | } 354 | 355 | function calcProb(n) { 356 | return 1 - Math.pow((1 - 1/1000), n) 357 | } 358 | 359 | //calculates estimated probability of rolling a transmog on your next roll 360 | function updateTransmogStats(name) { 361 | let results = GM_getValue("transmog_res", {n_since: 0, prob:0.0, list:[]}) 362 | //increments rolls since 363 | results.n_since += 1 364 | 365 | //rolled transmog, record it! 366 | if(name.includes("Transmogrification")) { 367 | console.log(`[NCE] Transmogrification potion earned, congrats!`) 368 | let img = $("#content td.content > div[align='center'] > img:last-of-type")[0].src 369 | results.list.push({ 370 | name: name, 371 | img: img, 372 | n_since: results.n_since, 373 | prob: calcProb(results.n_since) 374 | }) 375 | GM_setValue("lasttransmog", img) 376 | if(ALERT_ON_TRANSMOG) window.alert("You just won a Transmogrification Potion!\n(Don't forget to take a screenshot!)") 377 | //reset the n_since 378 | results.n_since = 0 379 | results.prob = 0.0 380 | } 381 | //otherwise, update probability 382 | else { 383 | results.prob = calcProb(results.n_since) 384 | } 385 | 386 | GM_setValue("transmog_res", results) 387 | } 388 | 389 | //============== 390 | // prize display 391 | //============== 392 | 393 | //displays stats on main page 394 | function displayAllStats() { 395 | let center = $("#content > table > tbody > tr > td.content > div")[0] 396 | let div = document.createElement("div") 397 | div.id = "nce" 398 | center.insertBefore(div, center.firstChild) 399 | 400 | let statdiv = document.createElement("div") 401 | statdiv.id = "totalstats" 402 | 403 | //combos 404 | let np_div = document.createElement("div") 405 | np_div.id = "np_stats" 406 | statdiv.appendChild(np_div) 407 | 408 | for(const combo of Object.keys(GM_getValue("np_res"))) { 409 | np_div.appendChild(displayTokenNPStats(JSON.parse(combo), true)) 410 | } 411 | 412 | //transmogs 413 | let transmog_div = document.createElement("div") 414 | transmog_div.id = "transmog_stats" 415 | statdiv.appendChild(transmog_div) 416 | 417 | transmog_div.appendChild(displayTransmogStats()) 418 | //transmog_div.appendChild(displayTransmogWinnings()) 419 | 420 | div.appendChild(statdiv) 421 | let imgdiv = document.createElement("div") 422 | $("#content > table > tbody > tr > td.content > div > img").appendTo(imgdiv) 423 | div.appendChild(imgdiv) 424 | } 425 | 426 | //displays stats for the specific input combo 427 | function displayTokenNPStats(input, hideRun = false) { 428 | let color = input.color 429 | let count = GM_getValue("tokencount")?.[color] 430 | let used = GM_getValue("tokensused") 431 | const np_res = GM_getValue("np_res") 432 | if(np_res) { 433 | let res = np_res[JSON.stringify(input)] 434 | let left = count - used 435 | let countdiv = document.createElement("div") 436 | countdiv.classList.add("tokensused") 437 | countdiv.innerHTML += ` 438 | 439 |
440 | ` 441 | if(!hideRun) { 442 | countdiv.innerHTML += ` 443 | Starting Tokens: ${count.toLocaleString("en-US")} 444 |
445 | Tokens Used: ${used.toLocaleString("en-US")} 446 |
447 | Tokens Left: ${left.toLocaleString("en-US")} 448 |

449 | ` 450 | } 451 | countdiv.innerHTML += ` 452 | ${color.charAt(0).toUpperCase() + color.substr(1)} Total Stats: 453 |
454 | Tokens Used: ${(res.count).toLocaleString("en-US")} 455 |
456 | Avg. NP: ${Math.round(res.avg).toLocaleString("en-US")} NP 457 |
458 | Highest NP: ${Math.max(res.list.slice(-1)[0]).toLocaleString("en-US")} NP 459 | ` 460 | console.log(`[NCE] Token display added.`) 461 | return countdiv 462 | } 463 | } 464 | 465 | //shows # of item collected 466 | function displayItemStats() { 467 | const item_res = GM_getValue("item_res") 468 | if(item_res) { //schizochecking 469 | let nameelement = $("#content td.content > div[align='center'] > b:last-of-type")[0] 470 | let count = item_res[nameelement.innerHTML].count 471 | let cdiv = document.createElement("small") 472 | cdiv.innerHTML = `
(in case you're wondering, you've collected ${count} of these!)` 473 | nameelement.after(cdiv) 474 | } 475 | } 476 | 477 | function displayTransmogStats() { 478 | const results = GM_getValue("transmog_res") 479 | let odddiv = document.createElement("div") 480 | odddiv.classList.add("estodds") 481 | 482 | let itemname = $("#content td.content > div[align='center'] > b:last-of-type")[0].innerHTML 483 | let img = GM_getValue("lasttransmog", "//images.neopets.com/items/pot_acara_mutant.gif") 484 | 485 | //to make sure we display the right numbers since we reset them prior 486 | if(itemname.includes("Transmogrification")) { 487 | let data = results.list.slice(-1)[0] 488 | odddiv.innerHTML = ` 489 | 490 |
491 | Tokens Used: ${data.n_since.toLocaleString("en-US")} 492 |
493 | Total Odds: ${(data.prob*100.0).toLocaleString("en-US")}% 494 | ` 495 | } 496 | else { 497 | odddiv.innerHTML = ` 498 | 499 |
500 | Tokens Used: ${results.n_since.toLocaleString("en-US")} 501 |
502 | Total Odds: ${(results.prob*100.0).toLocaleString("en-US")}% 503 | ` 504 | } 505 | return odddiv 506 | } 507 | 508 | function displayTransmogWinnings() { 509 | let res = GM_getValue("transmog_res")[list] 510 | } 511 | 512 | 513 | //=================== 514 | // token use overhaul 515 | //=================== 516 | 517 | function addSendButton() { 518 | let button = document.createElement("button") 519 | button.innerHTML = "Use Another Token!" 520 | button.addEventListener("click", () => {sendRequest(RESUBMIT_TIMEOUT)}) 521 | $("#content td.content > div:first-of-type")[0].insertBefore(button, $(`#content td.content > div:first-of-type > form`)[0]) 522 | button.after(document.createElement("br"), document.createElement("br")) 523 | button.focus() 524 | console.log("[NCE] Resubmit button added.") 525 | } 526 | 527 | function sendRequest(timeout) { 528 | let input = GM_getValue("input") 529 | if(input == null) return 530 | 531 | console.log("[NCE] Attempting to use token...") 532 | reqQueue += 1 533 | $("#token_queue")[0].innerHTML = reqQueue 534 | 535 | $.ajax({ 536 | type: "POST", 537 | url: "/moon/neocola3.phtml", 538 | data: {token_id:TOKEN_IDS[input.color], neocola_flavor:input.flavor, red_button:input.button}, 539 | timeout: timeout, 540 | success: function(data) { 541 | reqQueue -= 1 542 | $("#token_queue")[0].innerHTML = reqQueue 543 | let doc = new DOMParser().parseFromString(data, "text/html") 544 | if(doc.title != "NeoCola Machine") console.log("[NCE] POST request blocked by stackpath. :(") 545 | else readResponse(doc) 546 | }, 547 | error: function(xhr, status, error) { 548 | console.log(status + error) 549 | reqQueue -= 1 550 | $("#token_queue")[0].innerHTML = reqQueue 551 | } 552 | }, {token_id:TOKEN_IDS[input.color], neocola_flavor:input.flavor, red_button:input.button}, function(data, status){ 553 | let doc = new DOMParser().parseFromString(data, "text/html") 554 | //sometimes stackpath blocks the page :/ 555 | if(doc.title != "NeoCola Machine") console.log("[NCE] POST request blocked by stackpath. :(") 556 | }) 557 | } 558 | 559 | function readResponse(doc) { 560 | console.log("[NCE] Response received, recording and updating display.") 561 | console.log(doc) 562 | //on error 563 | if(doc.querySelector(".errorMessage")) { 564 | console.log(doc.querySelector(".errorMessage").innerHTML) 565 | } 566 | else { 567 | $("#content td.content")[0].innerHTML = doc.querySelector("#content td.content").innerHTML 568 | modifyPrizePage() 569 | addQueueDisplay($("#content td.content button")[0]) 570 | } 571 | } 572 | 573 | function addQueueDisplay(before) { 574 | let d = document.createElement("div") 575 | d.id = "token_queue" 576 | d.innerHTML = reqQueue 577 | before.after(d) 578 | console.log("[NCE] Queue display added") 579 | } 580 | 581 | //========= 582 | // css 583 | //========= 584 | 585 | function addSelectCSS() { 586 | document.head.appendChild(document.createElement("style")).innerHTML = ` 587 | div.token_img.inactive::after { 588 | content: ""; 589 | width: 80px; 590 | height: 80px; 591 | display: block; 592 | position: relative; 593 | left: 0; 594 | top: 0; 595 | background-color: white; 596 | opacity: 0.3; 597 | } 598 | .token_img { 599 | display: inline-block; 600 | width: 80px; 601 | height: 80px; 602 | border: 1px solid black; 603 | position: relative; 604 | } 605 | .token_count { 606 | font-size: 10pt; 607 | font-weight: bold; 608 | display: block; 609 | position: absolute; 610 | right: 2px; 611 | bottom: 2px; 612 | } 613 | 614 | #content > table > tbody > tr > td.content > br:nth-last-of-type(2) { 615 | display: none; 616 | } 617 | #content > table > tbody > tr > td.content > br:nth-last-of-type(3) { 618 | display: none; 619 | } 620 | ` 621 | } 622 | 623 | function addPrizeCSS() { 624 | document.head.appendChild(document.createElement("style")).innerHTML = ` 625 | #nce { 626 | display: flex; 627 | position: relative; 628 | } 629 | #totalstats { 630 | display: flex; 631 | flex-direction: column; 632 | position: relative; 633 | text-align: center; 634 | padding: 6px; 635 | border: 1px solid black; 636 | background-color: white; 637 | height: auto; 638 | margin: auto; 639 | } 640 | 641 | #np_stats, #transmog_stats { 642 | position: relative; 643 | display: block; 644 | width: fit-content; 645 | height: fit-content; 646 | padding: 8px; 647 | } 648 | 649 | #totalstats > div { 650 | display: flex; 651 | position: relative; 652 | flex-direction: row; 653 | gap: 4px; 654 | } 655 | #totalstats .tokensused, #totalstats .estodds { 656 | position: relative; 657 | left: auto; 658 | top: auto; 659 | } 660 | 661 | .tokensused { 662 | display: block; 663 | position: absolute; 664 | text-align: center; 665 | padding: 6px; 666 | border: 1px solid black; 667 | left: 7px; 668 | top: 30px; 669 | background-color: white; 670 | } 671 | .estodds { 672 | display: block; 673 | position: absolute; 674 | text-align: center; 675 | padding: 6px; 676 | border: 1px solid black; 677 | left: 7px; 678 | top: 250px; 679 | background-color: white; 680 | } 681 | #content td.content { 682 | position: relative; 683 | } 684 | #token_queue { 685 | position: absolute; 686 | display: inline-block; 687 | left: 61%; 688 | font-size: 17px; 689 | } 690 | ` 691 | } -------------------------------------------------------------------------------- /Neopets - NeoFoodClub+.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - NeoFoodClub+ 3 | // @version 2.0 4 | // @description Adds some improvements to neofood.club including remembering bet status, unfocusing tabs and auto-closing tabs. 5 | // @author Metamagic 6 | // @match *neofood.club/* 7 | // @match https://www.neopets.com/pirates/foodclub.phtml?type=bet* 8 | // @match https://www.neopets.com/pirates/process_foodclub.phtml?*&type=bet* 9 | // @match https://www.neopets.com/pirates/foodclub.phtml?type=current_bets* 10 | // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js 11 | // @icon https://i.imgur.com/RnuqLRm.png 12 | // @grant GM_setValue 13 | // @grant GM_getValue 14 | // @grant GM_deleteValue 15 | // @grant GM_addValueChangeListener 16 | // @grant window.focus 17 | // @grant window.close 18 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20NeoFoodClub%2B.user.js 19 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20NeoFoodClub%2B.user.js 20 | // ==/UserScript== 21 | 22 | // Trans rights are human rights ^^ 23 | // metty says hi 24 | 25 | //=============== 26 | // script options 27 | //=============== 28 | 29 | const DEBUG = true //just leaves print statements on 30 | 31 | const AUTOCLOSETABS = true //automatically closes bet tabs 32 | const FORCEBGTAB = true //forces new tabs to be in the background 33 | const AUTOMAXBET = true //automatically fills max bet value 34 | const AUTOCOLLECTMAXBET = true //grabs the max bet from the neo food club page for convenience 35 | const AUTOCOLLECT_TIMEOUT = 120 //autocollected data times out after this many minutes (default: 2hr) 36 | const ADD_NEO_LINKS = true //adds some quick links to neopets food club pages for convenience 37 | 38 | //=============== 39 | // React classes 40 | //=============== 41 | 42 | //selectors 43 | const NFC_BET_TABLE = "#root > div:nth-child(2) > div:nth-child(4) > table" // the div 44 | const NFC_MAX_BET_INPUT = "#root > header > div > div:nth-child(3) > div > div:nth-child(1) > div:nth-child(2) > div > div:nth-child(2) > input" 45 | const NFC_ROUND_NUMBER = "#root > header > div > div:nth-child(3) > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > input" 46 | const NFC_BET_BAR = "#root > div.css-1yysssr > div.css-1s00nyj" 47 | const NFC_NO_BET_BAR = "#root > div.css-1yysssr > div:nth-child(2) > div.css-1p24gq2 > div" 48 | const NFC_APPLY_BET_VALUE = "#root > div.css-1yysssr > div.css-1s00nyj > div.chakra-stack.css-8g8ihq > div > button.chakra-button.css-148ck9i" 49 | 50 | //classes 51 | const BUTTON_CONTAINER = "css-cpjzy9" 52 | const BUTTON_CONTAINER_2 = "css-8g8ihq" 53 | const NFC_BUTTON = "css-1873r7a" 54 | const NFC_BUTTON_NOBETS = "css-igc3ti" 55 | const GREEN_BET_BUTTON = "css-13ncfhw" 56 | const GRAY_BET_BUTTON = "css-n1yvyo" 57 | const RED_BET_BUTTON = "css-1j410sj" 58 | 59 | if(DEBUG == true) debug() 60 | function debug() { 61 | console.log($(NFC_BET_TABLE)[0]) 62 | console.log($(NFC_MAX_BET_INPUT)[0]) 63 | console.log($(NFC_ROUND_NUMBER)[0]) 64 | console.log($(NFC_BET_BAR)[0]) 65 | console.log($(NFC_NO_BET_BAR)[0]) 66 | console.log($(NFC_APPLY_BET_VALUE)[0]) 67 | } 68 | 69 | 70 | //================ 71 | // food club dicts 72 | //================ 73 | 74 | const ARENA_NAMES = { 75 | 1: "Shipwreck", 76 | 2: "Lagoon", 77 | 3: "Treasure Island", 78 | 4: "Hidden Cove", 79 | 5: "Harpoon Harry's" 80 | } 81 | const ARENA_IDS = Object.fromEntries(Object.entries(ARENA_NAMES).map(a => a.reverse())) 82 | 83 | const PIRATE_NAMES = { 84 | 1: "Dan", 85 | 2: "Sproggie", 86 | 3: "Orvinn", 87 | 4: "Lucky", 88 | 5: "Edmund", 89 | 6: "Peg Leg", 90 | 7: "Bonnie", 91 | 8: "Puffo", 92 | 9: "Stuff", 93 | 10: "Squire", 94 | 11: "Crossblades", 95 | 12: "Stripey", 96 | 13: "Ned", 97 | 14: "Fairfax", 98 | 15: "Gooblah", 99 | 16: "Franchisco", 100 | 17: "Federismo", 101 | 18: "Blackbeard", 102 | 19: "Buck", 103 | 20: "Tailhook", 104 | }; 105 | const PIRATE_IDS = Object.fromEntries(Object.entries(PIRATE_NAMES).map(a => a.reverse())) 106 | 107 | //=============== 108 | // main functions 109 | //=============== 110 | 111 | //applies changes to neofood.club 112 | if(window.location.href.includes("neofood.club")) { 113 | if(GM_getValue("toprocess") == null) GM_setValue("toprocess", 0) 114 | addCSS() 115 | waitForBetTable() 116 | } 117 | //records max bet value from neopets food club page 118 | else if(window.location.href.includes("neopets.com/pirates/foodclub.phtml?type=bet") && AUTOCOLLECTMAXBET) { 119 | let maxbet = $("#content > table > tbody > tr > td.content > p:nth-child(7) > b")[0].innerHTML 120 | let date = new Date().toLocaleString("en-US", {timeZone: "PST"}) 121 | GM_setValue("maxbet", {maxbet:maxbet, date:date}) 122 | console.log("[NFC+] Max bet value recorded.") 123 | } 124 | 125 | //processing bet page - if the page loads at this url it's an error 126 | else if(window.location.href.includes("neopets.com/pirates/process_foodclub.phtml")) { 127 | if($(".errorMessage").length > 0) { 128 | let url = window.location.href 129 | let tabinfo = GM_getValue("tabinfo", {}) 130 | 131 | //if url is marked, return error and close tab 132 | if(url in tabinfo) { 133 | let error = GM_getValue("betstatus", {}) 134 | let betnum = tabinfo[url] 135 | if($(".errorMessage")[0].innerHTML.includes("you cannot place the same bet more than once!")) { 136 | error[tabinfo[url]] = "Already placed!" 137 | } 138 | else { 139 | error[tabinfo[url]] = "Invalid bet!" 140 | } 141 | GM_setValue("betstatus", error) 142 | console.log("[NFC+] Bet error status returned.") 143 | if(AUTOCLOSETABS) window.close() 144 | } 145 | } 146 | } 147 | 148 | //result page - record bets 149 | else if(window.location.href.includes("neopets.com/pirates/foodclub.phtml?type=current_bets")) { 150 | parseCurrentBets() 151 | let toProcess = GM_getValue("toprocess", 0) 152 | if(AUTOCLOSETABS && toProcess > 0) { 153 | GM_setValue("toprocess", toProcess-1) 154 | window.close() 155 | } 156 | //appends current bet count to current bets page for convenience 157 | let betCount = Array.from($("#content > table > tbody > tr > td.content > center:nth-child(6) > center:nth-child(2) > table tr[bgcolor='white']")).filter(r => r.children.length == 5).length 158 | $("#content > table > tbody > tr > td.content > center:nth-child(6) > center:nth-child(2) > table > tbody > tr:nth-child(1) > td > font")[0].innerHTML += ` (${betCount})` 159 | } 160 | 161 | //================== 162 | // program launching 163 | //================== 164 | 165 | //waits for the right conditions to apply the changes of the script - aka when to start 166 | function waitForBetTable() { 167 | //the table we want already exists, wait for it to finish populating 168 | let table = getBetTable() 169 | console.log(table) 170 | if(table) { 171 | const settableobs = new MutationObserver(mutations => { 172 | if(table.rows[1].children.length == 14) { 173 | console.log("[NFC+] Applying userscript to bet table...") 174 | handleNeoFoodMods() 175 | watchForBetTable() 176 | } 177 | }) 178 | settableobs.observe(table, {subTree: true, childList: true}); 179 | } 180 | //the table we want doesn't exist, wait for it to exist 181 | else { 182 | if(ADD_NEO_LINKS) addNeoLinks($(NFC_NO_BET_BAR)[0]) 183 | watchForBetTable() 184 | } 185 | } 186 | 187 | //watches for the addition/removal of the main bet table on the page - aka when to re-apply the script 188 | function watchForBetTable() { 189 | let page = $("#root > div")[0] 190 | const pageobs = new MutationObserver(mutations => { 191 | //if table is added, apply 192 | for(const mutation of mutations) { 193 | if(mutation.addedNodes.length > 0) { 194 | //if the bet table is added, re-apply 195 | if(mutation.addedNodes[0] == getBetTable()?.parentElement) { 196 | console.log("[NFC+] Applying userscript to bet table...") 197 | handleNeoFoodMods() 198 | break 199 | } 200 | //otherwise, check if we need to add the bet links 201 | else if($("#quicklink-cont").length == 0 && ADD_NEO_LINKS) { //if bet links don't exist 202 | addNeoLinks($(NFC_BET_BAR)[0]) 203 | } 204 | } 205 | } 206 | }) 207 | pageobs.observe(page, {subTree: true, childList: true}); 208 | } 209 | 210 | //runs the main functionality of the script 211 | function handleNeoFoodMods() { 212 | updateRound() //updates stored round #, which resets some things 213 | updateSetStatus() //updates set status if shit changes 214 | handleBetButtons() //updates place bet buttons 215 | addBetAllButton() //adds a button to place all bets at once 216 | updateMaxBet() //updates the max bet value in the header 217 | applyMaxBetValue() //presses the set all max bet button 218 | if(ADD_NEO_LINKS) addNeoLinks($(NFC_BET_BAR)[0]) //adds quick links 219 | } 220 | 221 | function clickAllBets() { 222 | for(let row of Array.from(getBetTable().children[1].getElementsByTagName("tr"))) { 223 | setTimeout(() => { 224 | let button = row.children[13].children[0] 225 | button.click() 226 | }, 500) 227 | } 228 | } 229 | 230 | function addBetAllButton() { 231 | let div = document.createElement("div") 232 | div.classList.add(BUTTON_CONTAINER) 233 | div.style.marginRight = "20px" 234 | div.color = "white" 235 | let button = document.createElement("button") 236 | button.type = "button" 237 | button.classList.add("chakra-button", NFC_BUTTON, NFC_BUTTON_NOBETS) 238 | button.style.userSelect = "auto" 239 | button.addEventListener("click", clickAllBets) 240 | button.innerHTML = "Place all bets" 241 | let reminder = document.createElement("div") 242 | reminder.classList.add(BUTTON_CONTAINER_2) 243 | reminder.style.fontSize = "9pt" 244 | reminder.style.marginTop = "2px" 245 | reminder.innerHTML = "(enable pop-ups)" 246 | 247 | 248 | div.appendChild(button) 249 | div.appendChild(reminder) 250 | $(NFC_BET_BAR)[0].appendChild(div) 251 | } 252 | 253 | function updateRound() { 254 | let resetMsg = null 255 | 256 | //if new round 257 | let prevRound = GM_getValue("round", null) 258 | let currRound = $(NFC_ROUND_NUMBER)[0].getAttribute("value") 259 | if(currRound != prevRound) { 260 | GM_setValue("round", currRound) //and update the current round 261 | resetMsg = "New round detected, cleared stored bet info." 262 | } 263 | 264 | //if new bet url 265 | let prevURL = GM_getValue("beturl", null) 266 | let currURL = window.location.href.match(/.*&b=(\w*).*?/)[1] 267 | if(prevURL != currURL) { 268 | GM_setValue("beturl", currURL) //and update the current round 269 | resetMsg = "New bet URL detected, cleared stored bet info." 270 | } 271 | 272 | //if either changed, reset stuff 273 | if(resetMsg != null) { 274 | GM_deleteValue("tabinfo") 275 | GM_deleteValue("betstatus") 276 | GM_deleteValue("placedbets") 277 | GM_setValue("toprocess", 0) 278 | console.log("[NFC+] "+ resetMsg) 279 | } 280 | } 281 | 282 | //reads the current bets page to check whether a bet was placed or not 283 | function parseCurrentBets() { 284 | let currentbets = Array.from($("#content > table > tbody > tr > td.content > center:nth-child(6) > center:nth-child(2) > table tr[bgcolor='white']")).filter(r => r.children.length == 5) 285 | let newBets = currentbets.length - GM_getValue("placedbets", []).length 286 | //only updates stored bet list if there are new bets detected (to deal with out-of-order loading) 287 | if(newBets > 0) { 288 | let betList = [] 289 | let closeTab = false 290 | //parse each row 291 | for(const row of currentbets) { 292 | //dict of 5 arenas and their betters 293 | let bets = row.children[1].innerHTML 294 | .replaceAll('"', "").replaceAll("", "").replaceAll("", "").split("
").slice(0, -1) 295 | //parses bet info 296 | let betmap = {1:null,2:null,3:null,4:null,5:null} 297 | for(const bet of bets) { 298 | let s = bet.split(":") 299 | //get arena number 300 | let arena = ARENA_IDS[s[0]] 301 | //get pirates partial name 302 | for(const piratename of Object.values(PIRATE_NAMES)) { 303 | if(s[1].includes(piratename)) { 304 | var pirate = piratename 305 | break 306 | } 307 | } 308 | //add to betmap 309 | betmap[arena] = pirate 310 | } 311 | betList.push(betmap) 312 | } 313 | //update global value 314 | GM_setValue("placedbets", betList) 315 | console.log("[NFC+] Current bets list updated.") 316 | //updates # of tabs to close 317 | GM_setValue("toprocess", GM_getValue("toprocess", 0) + newBets) 318 | } 319 | } 320 | 321 | 322 | //============== 323 | // apply max bet 324 | //============== 325 | 326 | //updates max bet value in header field 327 | function updateMaxBet() { 328 | if(AUTOCOLLECTMAXBET) { 329 | let maxbetdata = GM_getValue("maxbet", null) 330 | if(maxbetdata != null) { 331 | let currDate = new Date() 332 | let collectionDate = new Date(maxbetdata.date) 333 | let mindiff = (currDate.getTime() - collectionDate.getTime())/1000/60 334 | 335 | if(mindiff < AUTOCOLLECT_TIMEOUT) { 336 | let input = $(NFC_MAX_BET_INPUT)[0] 337 | console.log(input.value) 338 | if(input.value < maxbetdata.maxbet) { 339 | input.focus() 340 | input.value = maxbetdata.maxbet 341 | input.setAttribute("aria-valuenow", maxbetdata.maxbet) 342 | input.setAttribute("aria-valuetext", maxbetdata.maxbet) 343 | input.parentElement.value = maxbetdata.maxbet 344 | input.blur() 345 | console.log("[NFC+] Stored max bet value applied.") 346 | } 347 | //otherwise does nothing. 348 | else console.log("[NFC+] Max bet value less than current value, no update.") 349 | } 350 | //timed out 351 | else { 352 | console.log("[NFC+] Max bet value timed out, data invalidated.") 353 | } 354 | GM_deleteValue("maxbet") 355 | } 356 | } 357 | } 358 | 359 | //presses the 'set bet amounts to max' button 360 | function applyMaxBetValue() { 361 | if(AUTOMAXBET) { 362 | let button = $(NFC_APPLY_BET_VALUE)[0] 363 | if(button) { 364 | button.click() 365 | console.log("[NFC+] Bets automatically set to max value.") 366 | } 367 | } 368 | } 369 | 370 | //note: designed with dark mode in mind, too lazy to make a light mode one too. 371 | function addNeoLinks(cont) { 372 | let linkCont = document.createElement("div") 373 | linkCont.classList.add(BUTTON_CONTAINER) 374 | linkCont.style.marginRight = "20px" 375 | linkCont.style.color = "white" 376 | linkCont.id = "quicklink-cont" 377 | let linkCont2 = document.createElement("div") 378 | linkCont2.classList.add("chakra-stack", BUTTON_CONTAINER_2) 379 | 380 | let button1 = document.createElement("button") 381 | button1.type = "button" 382 | button1.classList.add("chakra-button", NFC_BUTTON, NFC_BUTTON_NOBETS) 383 | let button2 = button1.cloneNode() 384 | button1.innerHTML = "Current Bets" 385 | button1.addEventListener("click", () => { window.open('https://www.neopets.com/pirates/foodclub.phtml?type=current_bets'); GM_setValue("toprocess", 0) }) 386 | button2.innerHTML = "Collect Winnings" 387 | button2.addEventListener("click", () => { window.open('https://www.neopets.com/pirates/foodclub.phtml?type=collect') }) 388 | 389 | linkCont2.appendChild(button1) 390 | linkCont2.appendChild(button2) 391 | linkCont.appendChild(linkCont2) 392 | cont.appendChild(linkCont) 393 | 394 | } 395 | 396 | 397 | //=========== 398 | // bet button 399 | //=========== 400 | 401 | function handleBetButtons() { 402 | //updates button status 403 | updateButtonStatus() 404 | 405 | //updates existing table 406 | for(let row of Array.from(getBetTable().children[1].getElementsByTagName("tr"))) { 407 | let button = row.children[13].children[0] 408 | addButtonObserver(button.parentElement) 409 | if(!button.disabled) addButtonListener(button) 410 | } 411 | 412 | //observes updates in table to apply updates 413 | addRowObserver() 414 | //listens for outcomes after placing bets 415 | addResultsListeners() 416 | } 417 | 418 | //listens for added/removed rows 419 | function addRowObserver() { 420 | let tbody = getBetTable().children[1] 421 | const tbodyobs = new MutationObserver(mutations => { 422 | for(const mutation of mutations) { 423 | if(mutation.addedNodes.length > 0) { 424 | let button = mutation.addedNodes[0].children[13].children[0] 425 | addButtonObserver(button.parentElement) 426 | if(!button.disabled) addButtonListener(button) 427 | applyMaxBetValue() 428 | } 429 | } 430 | }) 431 | tbodyobs.observe(tbody, {childList: true}) 432 | } 433 | 434 | function addResultsListeners() { 435 | //updates successful bet statuses based on current bet page 436 | GM_addValueChangeListener("placedbets", function(key, oldValue, newValue, remote) { 437 | updateSetStatus() 438 | }) 439 | 440 | //updates bet buttons based on bet status updates 441 | GM_addValueChangeListener("betstatus", function(key, oldValue, newValue, remote) { 442 | updateButtonStatus() 443 | }) 444 | } 445 | 446 | //updates set statuses to match the current bets screen 447 | function updateSetStatus() { 448 | let placedBets = GM_getValue("placedbets", []) 449 | let betStatus = GM_getValue("betstatus", {}) 450 | for(const bet of placedBets) { 451 | let row = getRowFromCurrentBet(bet) //finds the row of the bet 452 | let betNum = row.children[0].getElementsByTagName("p")[0].innerHTML //finds the number from the row 453 | 454 | betStatus[betNum] = "Bet placed!" 455 | } 456 | GM_setValue("betstatus", betStatus) 457 | } 458 | 459 | //updates the buttons to match their bet status 460 | function updateButtonStatus() { 461 | let betStatus = GM_getValue("betstatus", {}) 462 | for(const betNum in betStatus) { 463 | let button = getBetRow(betNum).children[13].children[0] 464 | let statusMsg = betStatus[betNum] 465 | button.firstChild.data = statusMsg 466 | if(statusMsg == "Already placed!" || statusMsg == "Invalid bet!") { 467 | button.classList.add(RED_BET_BUTTON) 468 | button.classList.remove(GRAY_BET_BUTTON) 469 | button.classList.remove(GREEN_BET_BUTTON) 470 | } 471 | if(statusMsg == "Bet placed!") { 472 | button.classList.add(GRAY_BET_BUTTON) 473 | button.disabled = true 474 | } 475 | } 476 | } 477 | 478 | function addButtonObserver(buttonCell) { 479 | const rowobs = new MutationObserver(mutations => { 480 | for(const mutation of mutations) { 481 | //button updates by removing and creating a new one. apply updates to the new button. 482 | if(mutation.addedNodes.length > 0) { 483 | let newButton = mutation.addedNodes[0] 484 | if(!newButton.disabled) { 485 | addButtonListener(newButton) 486 | } 487 | } 488 | } 489 | }) 490 | rowobs.observe(buttonCell, {childList: true, subtree: true}) 491 | } 492 | 493 | function addButtonListener(button) { 494 | let betNum = button.parentElement.parentElement.getElementsByTagName("p")[0].innerHTML 495 | button.addEventListener("click", function(event){onButtonClick(event, betNum, button)}) 496 | } 497 | 498 | function onButtonClick(event, betNum, button) { 499 | //overrides behavior 500 | event.stopPropagation() 501 | //updates button 502 | button.firstChild.data = "Processing..." 503 | button.classList.remove(GREEN_BET_BUTTON) 504 | button.classList.add(GRAY_BET_BUTTON) 505 | button.disabled = true 506 | //generates link again... manually... and opens it in background 507 | let link = generate_bet_link(betNum) 508 | openBackgroundTab(link, betNum) 509 | } 510 | 511 | function generate_bet_link(betNum) { 512 | let urlString = 513 | "https://www.neopets.com/pirates/process_foodclub.phtml?"; 514 | let betData = getBetData(betNum) 515 | let bet = betData.winners 516 | for (let i = 0; i < 5; i++) { 517 | if (bet[i] != null) { 518 | urlString += `winner${i + 1}=${bet[i]}&`; 519 | } 520 | } 521 | for (let i = 0; i < 5; i++) { 522 | if (bet[i] != null) { 523 | urlString += `matches[]=${i + 1}&`; 524 | } 525 | } 526 | urlString += `bet_amount=${betData.bet_amount}&`; 527 | urlString += `total_odds=${betData.total_odds}&`; 528 | urlString += `winnings=${betData.winnings}&`; 529 | urlString += "type=bet"; 530 | return urlString; 531 | } 532 | 533 | function openBackgroundTab(url, betNum) { 534 | //opens tab 535 | if(FORCEBGTAB) { 536 | window.open(url, "_blank") 537 | window.focus() 538 | console.log("[NFC+] Forced new tab to back.") 539 | } 540 | else window.open(url) 541 | 542 | //records info abt the tab so the program knows it was pressed from here 543 | let tabinfo = GM_getValue("tabinfo", {}) 544 | tabinfo[url] = betNum 545 | GM_setValue("tabinfo", tabinfo) 546 | } 547 | 548 | 549 | //================= 550 | // helper functions 551 | //================= 552 | 553 | function getBetTable() { 554 | let t = document.querySelectorAll(NFC_BET_TABLE) 555 | if(t.length > 0) { 556 | let last = Array.from(t).slice(-1)[0] 557 | if(last.querySelector("th").innerHTML == "Bet #") return last 558 | } 559 | } 560 | 561 | function getBetData(betNum) { 562 | let betRow = getBetRow(betNum) 563 | 564 | let bet_amount = betRow.children[1].children[0].getAttribute("value") 565 | let total_odds = betRow.children[2].innerHTML.split(":")[0] 566 | let winnings = betRow.children[3].innerHTML.replace(",", "") 567 | let winners = [] 568 | for(let i=8; i<13; i++) { 569 | let pirate = betRow.children[i].innerHTML || null 570 | if(pirate != null) pirate = PIRATE_IDS[pirate] 571 | winners.push(pirate) 572 | } 573 | 574 | return {bet_amount:bet_amount, total_odds:total_odds, winnings:winnings, winners:winners} 575 | } 576 | 577 | 578 | function getBetRow(betNum) { 579 | let table = getBetTable() 580 | let tbody = table.children[1] 581 | let rows = Array.from(tbody.getElementsByTagName("tr")) 582 | let betRow = rows.filter(row=>{ 583 | let num = row.children[0].getElementsByTagName("p")[0].innerHTML 584 | return num == betNum 585 | })[0] 586 | return betRow 587 | } 588 | 589 | function getRowFromCurrentBet(bet) { 590 | let rows = Array.from(getBetTable().children[1].getElementsByTagName("tr")) 591 | let i = 0 592 | for(const row of rows) { 593 | let match = true 594 | for(let i = 0; i < 5; i++) { 595 | if((row.children[8+i].innerHTML || null) != bet[i+1]) { 596 | match = false 597 | break 598 | } 599 | } 600 | if(match) return row 601 | } 602 | return null 603 | } 604 | 605 | //============================ 606 | // css because react is stupid 607 | //============================ 608 | 609 | function addCSS() { 610 | document.head.appendChild(document.createElement("style")).innerHTML = ` 611 | /* green button */ 612 | .${GREEN_BET_BUTTON}[disabled], .${GREEN_BET_BUTTON}[aria-disabled="true"], .${GREEN_BET_BUTTON}[data-disabled] { 613 | opacity: 0.4; 614 | box-shadow: var(--chakra-shadows-none); 615 | cursor: auto; 616 | } 617 | 618 | /*grey button*/ 619 | .${GRAY_BET_BUTTON} { 620 | display: inline-flex; 621 | appearance: none; 622 | -webkit-box-align: center; 623 | align-items: center; 624 | -webkit-box-pack: center; 625 | justify-content: center; 626 | user-select: none; 627 | position: relative; 628 | white-space: nowrap; 629 | vertical-align: middle; 630 | outline: transparent solid 2px; 631 | outline-offset: 2px; 632 | width: 100%; 633 | line-height: 1.2; 634 | border-radius: var(--chakra-radii-md); 635 | font-weight: var(--chakra-fontWeights-semibold); 636 | transition-property: var(--chakra-transition-property-common); 637 | transition-duration: var(--chakra-transition-duration-normal); 638 | height: var(--chakra-sizes-8); 639 | min-width: var(--chakra-sizes-8); 640 | font-size: var(--chakra-fontSizes-sm); 641 | padding-inline-start: var(--chakra-space-3); 642 | padding-inline-end: var(--chakra-space-3); 643 | background: var(--chakra-colors-whiteAlpha-200); 644 | } 645 | .${GRAY_BET_BUTTON}:disabled, .${GRAY_BET_BUTTON}[data-hover] { 646 | background: var(--chakra-colors-whiteAlpha-300); 647 | cursor: not-allowed; 648 | } 649 | /* red button*/ 650 | .${RED_BET_BUTTON}[disabled], .${RED_BET_BUTTON}[aria-disabled="true"], .${RED_BET_BUTTON}[data-disabled] { 651 | opacity: 0.4; 652 | cursor: not-allowed; 653 | box-shadow: var(--chakra-shadows-none); 654 | } 655 | .${RED_BET_BUTTON} { 656 | display: inline-flex; 657 | appearance: none; 658 | -webkit-box-align: center; 659 | align-items: center; 660 | -webkit-box-pack: center; 661 | justify-content: center; 662 | user-select: none; 663 | position: relative; 664 | white-space: nowrap; 665 | vertical-align: middle; 666 | outline: transparent solid 2px; 667 | outline-offset: 2px; 668 | width: 100%; 669 | line-height: 1.2; 670 | border-radius: var(--chakra-radii-md); 671 | font-weight: var(--chakra-fontWeights-semibold); 672 | transition-property: var(--chakra-transition-property-common); 673 | transition-duration: var(--chakra-transition-duration-normal); 674 | height: var(--chakra-sizes-8); 675 | min-width: var(--chakra-sizes-8); 676 | font-size: var(--chakra-fontSizes-sm); 677 | padding-inline-start: var(--chakra-space-3); 678 | padding-inline-end: var(--chakra-space-3); 679 | background: var(--chakra-colors-red-200); 680 | color: var(--chakra-colors-gray-800); 681 | } 682 | ` 683 | } -------------------------------------------------------------------------------- /Neopets - Quick Exploration Links.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Quick Exploration Links 3 | // @version 1.0 4 | // @description Adds some quicklinks under the beta "Explore" button 5 | // @author Mettymagic 6 | // @match https://www.neopets.com/* 7 | // @match https://neopets.com/* 8 | // @icon https://i.imgur.com/RnuqLRm.png 9 | // @grant none 10 | // @downloadURL https://github.com/Mettymagic/np-userscripts/blob/main/Neopets%20-%20Quick%20Exploration%20Links.user.js 11 | // @updateURL https://github.com/Mettymagic/np-userscripts/blob/main/Neopets%20-%20Quick%20Exploration%20Links.user.js 12 | // @run-at document-start 13 | // ==/UserScript== 14 | 15 | //name, link, image 16 | const LINK_LIST = [ 17 | { 18 | name:"Inventory", 19 | link:"/inventory.phtml", 20 | img:"https://images.neopets.com/af13h43uw1/games/tm_3.png" 21 | }, 22 | { 23 | name:"Quickref", 24 | link:"/quickref.phtml", 25 | img:"https://images.neopets.com/themes/h5/altadorcup/images/transferlog-icon.png" 26 | }, 27 | { 28 | name:"Neopia Central", 29 | link:"/objects.phtml", 30 | img:"https://images.neopets.com/bestof/2008/neopiacentral.gif" 31 | }, 32 | { 33 | name:"Plot Hub", 34 | link:"/tvw", 35 | img:"https://images.neopets.com/neoboards/boardIcons/plot_tvw.png" 36 | }, 37 | { 38 | name:"Hospital", 39 | link:"/hospital/volunteer.phtml", 40 | img:"https://images.neopets.com/themes/h5/basic/images/health-icon.png" 41 | }, 42 | { 43 | name:"Barracks", 44 | link:"/dome/barracks.phtml", 45 | img:"https://images.neopets.com/themes/036_ddc_je4z0/events/battle_accept.png" 46 | } 47 | ] 48 | 49 | let explorePath = "body > div.nav-top__2020 > div.nav-top-grid__2020 > a.nav-explore-link__2020" 50 | document.head.appendChild(document.createElement("style")).innerHTML = ` 51 | ${explorePath}:hover .nav-explore__2020 > .nav-dropdown-arrow__2020 { 52 | transform: rotate(0deg); 53 | } 54 | ${explorePath} .nav-explore__2020 > .nav-dropdown-arrow__2020 { 55 | transform: rotate(-180deg); 56 | } 57 | 58 | ${explorePath} .nav-dropdown__2020 { 59 | display: none; 60 | position: absolute; 61 | width: 180px; 62 | left: 50%; 63 | top: calc(100%); 64 | margin-left: -90px; 65 | color: #000; 66 | max-height: calc(100vh - 100%); 67 | box-sizing: border-box; 68 | border-radius: 0 0 5px 5px; 69 | } 70 | ${explorePath}:hover .nav-dropdown__2020, .nav-dropdown__2020:hover { 71 | display: block; 72 | } 73 | 74 | ${explorePath} .nav-dropdown__2020 ul { 75 | list-style-type: none; 76 | margin: 0; 77 | padding: 0; 78 | } 79 | ${explorePath} .nav-dropdown__2020 div { 80 | display: block; 81 | height: 100%; 82 | width: 30px; 83 | min-height: 30px; 84 | background-size: contain; 85 | background-position: center; 86 | background-repeat: no-repeat; 87 | } 88 | ${explorePath} .nav-dropdown__2020 ul a:hover { 89 | background-color: rgba(255,255,255,0.15); 90 | display: block; 91 | } 92 | ${explorePath} .nav-dropdown__2020 ul li { 93 | cursor: pointer; 94 | display: grid; 95 | width: 100%; 96 | grid-template-columns: 30px 1fr; 97 | grid-gap: 10px; 98 | align-items: center; 99 | justify-content: center; 100 | padding: 5px 5px 5px 10px; 101 | box-sizing: border-box; 102 | background-color: none; 103 | } 104 | ${explorePath} .nav-dropdown__2020 ul h4 { 105 | margin: 5px 0; 106 | font-family: "Cafeteria", 'Arial Bold', sans-serif; 107 | font-size: 13pt; 108 | letter-spacing: 0.5pt; 109 | } 110 | 111 | .nav-top-grid__2020 { 112 | grid-template-columns: auto 200px 120px 155px 105px 130px 140px 35px 35px 35px !important; 113 | } 114 | ` 115 | 116 | window.addEventListener("DOMContentLoaded", function() { 117 | let bgcolor = $("#shopdropdown__2020").css("background-color") 118 | let color = $("#shopdropdown__2020").css("color") 119 | let border = $("#shopdropdown__2020").css("border") 120 | 121 | if($(explorePath).length > 0) { 122 | let explore = $(explorePath)[0] 123 | explore.classList.add("shopdropdown-button") 124 | let arrow = document.createElement("div") 125 | arrow.classList.add("nav-dropdown-arrow__2020") 126 | explore.querySelector("div").appendChild(arrow) 127 | 128 | let list = document.createElement("div") 129 | list.classList.add("nav-dropdown__2020") 130 | let html = "" 140 | list.innerHTML = html 141 | explore.appendChild(list) 142 | } 143 | 144 | document.head.appendChild(document.createElement("style")).innerHTML = ` 145 | ${explorePath} .nav-dropdown__2020 { 146 | background-color: ${bgcolor}; 147 | color: ${color}; 148 | border: ${border}; 149 | } 150 | ${explorePath} .nav-dropdown__2020 ul { 151 | background-color: ${bgcolor}; 152 | border: ${border}; 153 | } 154 | ${explorePath} .nav-dropdown__2020 ul h4 { 155 | color: ${color}; 156 | } 157 | ` 158 | }) -------------------------------------------------------------------------------- /Neopets - Quick Inventory Deposit.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Quick Inventory Deposit 3 | // @version 1.0 4 | // @description Adds a button that quickly deposits all items from your inventory into your SDB. 5 | // @author Metamagic 6 | // @match *://www.neopets.com/* 7 | // @match *://neopets.com/* 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // @icon https://i.imgur.com/RnuqLRm.png 11 | // @downloadURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Quick%20Inventory%20Deposit.user.js 12 | // @updateURL https://github.com/Mettymagic/np-userscripts/raw/main/Neopets%20-%20Quick%20Inventory%20Deposit.user.js 13 | // ==/UserScript== 14 | 15 | // You are free to modify this script for personal use but modified scripts must not be shared publicly without permission. 16 | // Feel free to contact me at @mettymagic on discord for any questions or inquiries. ^^ 17 | 18 | // Trans rights are human rights ^^ 19 | // metty says hi 20 | 21 | //============== 22 | // main function 23 | //============== 24 | 25 | //check for the top left pet icon to know we're in beta 26 | let isBeta = false 27 | if($("[class^='nav-pet-menu-icon']").length) isBeta = true 28 | 29 | addButton() //adds button to page 30 | addCSS() //adds css to page 31 | 32 | 33 | //================= 34 | // element creation 35 | //================= 36 | 37 | let clicked = false 38 | function addButton() { 39 | let button = document.createElement("div") 40 | button.classList.add("invdumpbutton") 41 | button.id = "qid-button" 42 | 43 | button.addEventListener("click", (event)=>{ 44 | if(!clicked) { 45 | clicked = true 46 | event.target.style.opacity = "0.6" 47 | event.target.style.cursor = "default" 48 | event.target.classList.add("working") 49 | $("#qid-status")[0].classList.add("working") 50 | requestQuickStock() 51 | } 52 | }) 53 | 54 | let text = document.createElement("div") 55 | text.classList.add("invdumptext") 56 | text.id = "qid-status" 57 | text.innerHTML = "Empty to SDB" 58 | if(isBeta) { 59 | let bar = $("div.navsub-right__2020")[0] 60 | bar.insertBefore(text, bar.children[0]) 61 | bar.insertBefore(button, bar.children[0]) 62 | } 63 | else { 64 | $("#content")[0].appendChild(button) 65 | $("#content")[0].appendChild(text) 66 | } 67 | console.log("[QID] Added inventory dump button.") 68 | } 69 | 70 | 71 | //======== 72 | // request 73 | //======== 74 | 75 | //gets the quick stock page and uses data from there to process 76 | function requestQuickStock() { 77 | console.log("[QID] Requesting quickstock page...") 78 | $("#qid-status")[0].innerHTML = "Counting stock..." 79 | 80 | let totalinv = 0 //# of items before dump 81 | let currn = 0 //# of items dumped so far 82 | 83 | //sends the first request 84 | $.get("https://www.neopets.com/quickstock.phtml", function(data, status){ 85 | let doc = new DOMParser().parseFromString(data, "text/html") 86 | //does nothing if stackpath's a bitch 87 | if(doc.title != "Neopets - Quickstock") { 88 | console.log("[QID] Quick Stock page request blocked by stackpath. :(") 89 | $("#qid-status")[0].style.backgroundColor = "#f2d8d3" 90 | $("#qid-status")[0].innerHTML = "ERROR: Stackpath Blocked!" 91 | $("#qid-status")[0].classList.add("done") 92 | $("#qid-button")[0].classList.add("done") 93 | return 94 | } 95 | let qs = getReqText(doc) 96 | totalinv = qs[1] //records # of items to deposit total 97 | //does nothing if nothing to dump 98 | if(totalinv < 1) { 99 | console.log("[QID] Your inventory is empty!") 100 | $("#qid-status")[0].style.backgroundColor = "#d3f5dc" 101 | $("#qid-status")[0].innerHTML = "0 Items -> SDB" 102 | $("#qid-status")[0].classList.add("done") 103 | $("#qid-button")[0].classList.add("done") 104 | return 105 | } 106 | 107 | let n = Math.min(70, qs[1]) //# of items getting deposited (max 70 at a time) 108 | let additional_reqs = Math.floor(totalinv / 70) //finds # of additional requests to make after this first one 109 | console.log(`[QID] Depositing ${totalinv} items using ${additional_reqs+1} requests.`) 110 | $("#qid-status")[0].innerHTML = `Status: ${currn} / ${totalinv}` 111 | 112 | //sends request using quickstock process link 113 | console.log(`[QID] Sending request 1 / ${additional_reqs+1}.`) 114 | $.post("/process_quickstock.phtml", qs[0], () => { 115 | console.log(`[QID] ${currn + n} / ${totalinv} items deposited.`) 116 | currn += n 117 | $("#qid-status")[0].innerHTML = `Status: ${currn} / ${totalinv}` 118 | if(additional_reqs > 0) startAdditionalReq(additional_reqs, qs, totalinv, currn, 0) //handles extra requests 119 | }) 120 | }) 121 | } 122 | 123 | //since qs has a 70 item limit, sometimes we need to send additional requests one after the other 124 | //this function is recursive and thus pretty jank. please forgive me. 125 | async function startAdditionalReq(additional_reqs, qs, totalinv, currn, i) { 126 | let promise = new Promise((res) => { 127 | //if we've done enough requests, stop the loop 128 | if(i >= additional_reqs) { 129 | $("#qid-status")[0].innerHTML = `${currn} Items -> SDB` 130 | $("#qid-status")[0].style.backgroundColor = "#d3f5dc" 131 | $("#qid-status")[0].classList.add("done") 132 | $("#qid-button")[0].classList.add("done") 133 | res(false) 134 | return 135 | } 136 | //perform a dump 137 | console.log(`[QID] Visiting quickstock ${i+2} / ${additional_reqs+1}.`) 138 | $.get("https://www.neopets.com/quickstock.phtml", function(data, status){ 139 | let doc = new DOMParser().parseFromString(data, "text/html") 140 | //stops if stackpath blocked 141 | if(doc.title != "Neopets - Quickstock") { 142 | console.log("[QID] Quick Stock page request blocked by stackpath. :(") 143 | $("#qid-status")[0].style.backgroundColor = "#f2d8d3" 144 | $("#qid-status")[0].innerHTML = "ERROR: Stackpath Blocked!" 145 | $("#qid-status")[0].classList.add("done") 146 | $("#qid-button")[0].classList.add("done") 147 | return 148 | } 149 | 150 | qs = getReqText(doc) 151 | let n = Math.min(70, qs[1]) //# of items getting deposited 152 | 153 | //sends request using quickstock process link 154 | console.log(`[QID] Sending request ${i+2} / ${additional_reqs+1}.`) 155 | $.post("/process_quickstock.phtml", qs[0], () => { 156 | console.log(`[QID] ${currn + n} / ${totalinv} items deposited.`) 157 | currn += n 158 | $("#qid-status")[0].innerHTML = `Status: ${currn} / ${totalinv}` 159 | res(currn < totalinv) 160 | }) 161 | }) 162 | }).then((val) => { //dump again 163 | if(val) startAdditionalReq(additional_reqs, qs, totalinv, currn, i+1) 164 | else { 165 | $("#qid-status")[0].innerHTML = `${currn} Items -> SDB` 166 | $("#qid-status")[0].style.backgroundColor = "#d3f5dc" 167 | $("#qid-status")[0].classList.add("done") 168 | $("#qid-button")[0].classList.add("done") 169 | } 170 | }) 171 | } 172 | 173 | //formats the data to be sent to the process post request 174 | function getReqText(doc) { 175 | let items = Array.from(doc.querySelectorAll("form > table > tbody > tr > input")) 176 | .map((input) => { 177 | let n = input.getAttribute("name").match(/id_arr\[(\d*)\]/)[1] 178 | //added an action field in case i decide to expand on this in the future 179 | return {name:input.getAttribute("name"), value: input.getAttribute("value"), actionkey: `radio_arr[${n}]`, action: "deposit"} 180 | }) 181 | if(items.length == 0) return [null, 0] //shortcut 182 | 183 | let req = "buyitem=0" 184 | for(const item of items) { 185 | //add item to request 186 | req += `&${item.name}=${item.value}` 187 | //add action to request 188 | if(item.action) req += `&${item.actionkey}=${item.action}` 189 | } 190 | return [req.replaceAll("[", "%5B").replaceAll("]", "%5D"), items.length] 191 | } 192 | 193 | 194 | //========== 195 | // css stuff 196 | //========== 197 | 198 | function addCSS() { 199 | //beta and classic need to place the elements differently 200 | if(isBeta) { 201 | document.head.appendChild(document.createElement("style")).innerHTML = ` 202 | .invdumpbutton { 203 | position: relative; 204 | margin-right: 16px; 205 | top: 0px; 206 | } 207 | .invdumptext { 208 | top: -7px; 209 | right: 176px; 210 | padding-right: 46px !important; 211 | } 212 | div.navsub-right__2020 { 213 | height: 32.422px !important; 214 | } 215 | div.navsub-right__2020 > a { 216 | position: relative; 217 | top: -8px; 218 | } 219 | ` 220 | } 221 | else { 222 | document.head.appendChild(document.createElement("style")).innerHTML = ` 223 | .invdumpbutton { 224 | top: 4px; 225 | right: 4px; 226 | position: absolute; 227 | } 228 | .invdumptext { 229 | top: -2px; 230 | left: calc(100% - 44px); 231 | padding-left: 46px !important; 232 | } 233 | #content { 234 | position: relative; 235 | } 236 | ` 237 | } 238 | 239 | document.head.appendChild(document.createElement("style")).innerHTML = ` 240 | .invdumpbutton { 241 | display: inline-block; 242 | width: 32px !important; 243 | height: 32px !important; 244 | background-image: url(https://images.neopets.com/themes/h5/altadorcup/images/quickstock-icon.png); 245 | background-size: 32px 32px; 246 | cursor: pointer; 247 | z-index: 2000; 248 | } 249 | .invdumpbutton:not(.working):hover { 250 | animation: shake 0.4s; 251 | } 252 | .invdumpbutton:hover + .invdumptext { 253 | visibility: visible; 254 | opacity: 1; 255 | } 256 | .invdumptext.working.done, .invdumpbutton.working.done { 257 | opacity: 0 !important; 258 | visibility: hidden !important; 259 | transition: opacity 4s ease-out, visibility 4s ease-out; 260 | } 261 | .invdumptext.working { 262 | visibility: visible; 263 | opacity: 1; 264 | } 265 | .invdumptext { 266 | color: black; 267 | position: absolute; 268 | display: block; 269 | background-color: white; 270 | border: solid 3px #913A15; 271 | opacity: 0; 272 | visibility: hidden; 273 | transition: visibility 0s, opacity 0.1s linear; 274 | padding: 12px 20px; 275 | border-radius: 4px; 276 | z-index: 1800; 277 | width: fit-content; 278 | white-space: nowrap; 279 | overflow: hidden; 280 | font-weight: bold; 281 | font-size: 12pt; 282 | font-family: "Cafeteria", 'Arial Bold', sans-serif; 283 | } 284 | /*adapted from https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_image_shake*/ 285 | @keyframes shake { 286 | 0% { transform: translate(2%, 2%) rotate(0deg); } 287 | 10% { transform: translate(-2%, -6%) rotate(-5deg); } 288 | 20% { transform: translate(-6%, 0px) rotate(5deg); } 289 | 30% { transform: translate(6%, 2%) rotate(0deg); } 290 | 40% { transform: translate(2%, -2%) rotate(5deg); } 291 | 50% { transform: translate(-2%, 4%) rotate(-5deg); } 292 | 60% { transform: translate(-6%, 2%) rotate(0deg); } 293 | 70% { transform: translate(6%, 2%) rotate(-5deg); } 294 | 80% { transform: translate(-2%, -2%) rotate(5deg); } 295 | 90% { transform: translate(2%, 4%) rotate(0deg); } 296 | 100% { transform: translate(2%, -4%) rotate(-5deg); } 297 | } 298 | ` 299 | } 300 | 301 | -------------------------------------------------------------------------------- /Neopets - SDB Inventarium.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - SDB Inventarium 3 | // @version 0.3 4 | // @description Allows you to view all SDB items at once, displaying additional info and allowing for more detailed search and sorts. 5 | // @author Metamagic 6 | // @match https://www.neopets.com/safetydeposit.phtml* 7 | // @match https://www.neopets.com/inventory.phtml* 8 | // @match https://www.neopets.com/quickstock.phtml* 9 | // @icon https://i.imgur.com/RnuqLRm.png 10 | // @grant GM_getValue 11 | // @grant GM_setValue 12 | // ==/UserScript== 13 | 14 | /* TO-DO: 15 | - compat with sdb dump script 16 | - display 17 | - display override on main page 18 | - item display - variable row size & detail lvls 19 | - 'back to vanilla' button 20 | - page selection / offset - variable page size 21 | - gui for moving pages 22 | - sorting system 23 | - gui for sort 24 | - search system 25 | - gui for search 26 | - record custom sdb removes 27 | */ 28 | 29 | //some items have duplicate names and thus we can't determine which of two ids an item is from its name alone 30 | const DUPLICATE_NAMES = [ 31 | "Piece of a treasure map", 32 | "Spooky Treasure Map", 33 | "Space Map", 34 | "Candy Cane Sword", 35 | "Christmas Baby Blu", 36 | "Cloud Umbrella", 37 | "Counting Babaas", 38 | "Fire Umbrella", 39 | "Kass Basher Flag", 40 | "Lost Desert Palm Tree", 41 | "Lunch Tray", 42 | "Rainbow Umbrella", 43 | "Retired Altador Cup Mystery Capsule", 44 | "Retired Dyeworks Mystery Capsule", 45 | "Retired Wonderclaw Mystery Capsule", 46 | "Shining Star Gift Box", 47 | "Snowflake Garland", 48 | "Sparkling Yellow Mystery Capsule", 49 | "The Darkest Faerie (TCG)", 50 | "Usuki Dream Car", 51 | "Usuki Dream Jetski", 52 | "Yooyuball Tattoo", 53 | "Basic Gift Box" 54 | ] 55 | let clicked = false //prevents double-inputs 56 | 57 | const url = document.location.href 58 | if(url.includes("safetydeposit.phtml")) { 59 | recordSDBPage() 60 | removeVanillaSDB() 61 | addFQCheck() 62 | } 63 | else if(url.includes("quickstock.phtml")) { 64 | depositQuickStock() 65 | } 66 | else if(url.includes("inventory.phtml")) { 67 | depositInventory() 68 | } 69 | 70 | function addFQCheck() { 71 | let row = $("#content > table > tbody > tr > td.content > form > table:nth-child(1) > tbody > tr > td:nth-child(2)")[0] 72 | let b = document.createElement("button") 73 | b.innerHTML = "Find r102-r179 Items" 74 | b.addEventListener("click", (e)=>{e.preventDefault();e.stopPropagation();getFQItems();}) 75 | row.appendChild(b) 76 | } 77 | 78 | function getFQItems() { 79 | let sdb = Array.from(Object.values(GM_getValue("sdbdata"))).filter((sdbitem) => { 80 | return sdbitem.rarity > 101 && sdbitem.rarity < 180 81 | }).sort((a, b) => { 82 | return b.count - a.count 83 | }) 84 | let count = sdb.map((a)=>{return a.count}).reduce((a, b) => {return a + b}, 0) 85 | //format list 86 | let str = `Results: (Total: ${count})` 87 | for(const item of sdb) { 88 | str += `
\t- ${item.name} x ${item.count}` 89 | } 90 | let display = document.createElement("p") 91 | display.innerHTML = str 92 | $("#content td.content")[0].insertBefore(display, $("#content > table > tbody > tr > td.content > table")[0]) 93 | console.log("Items displayed.") 94 | } 95 | 96 | //this is technically illegal btw 97 | //todo: remove this 98 | function autoNavigate() { 99 | console.log("Attempting to navigate to next page...") 100 | $("#content > table > tbody > tr > td.content > table > tbody > tr > td:nth-child(3) > a")?.[0]?.click() 101 | } 102 | 103 | //============== 104 | // parsing pages 105 | //============== 106 | 107 | function recordSDBPage() { 108 | let rows = Array.from($("#content td.content > form > table:nth-of-type(2) > tbody > tr:not(:first-child):not(:last-child)")) 109 | console.log(`[SDB] Reading data for ${rows.length} items.`) 110 | let pagedata = {} 111 | for(const tr of rows) { 112 | let rowdata = parseRowData(tr) 113 | pagedata[JSON.stringify(rowdata[0])] = rowdata[1] 114 | } 115 | getItemDBData(pagedata) 116 | } 117 | 118 | function parseRowData(tr) { 119 | let name = tr.querySelector(" td:nth-child(2) > b").childNodes[0].nodeValue 120 | let img = tr.querySelector("td:nth-child(1) > img").getAttribute("src").match(/.*?images\.neopets\.com\/items\/(.+?)\..+/)[1] 121 | let desc = tr.querySelector("td:nth-child(3) > i").innerHTML 122 | let type = tr.querySelector("td:nth-child(4) > b").innerHTML 123 | let count = parseInt(tr.querySelector("td:nth-child(5) > b").innerHTML) 124 | let id = parseInt(tr.querySelector("td:nth-child(6) > input").getAttribute("name").match(/back_to_inv\[([0-9]+)\]/)[1]) 125 | let tag = tr.querySelector("span.medText font") || null 126 | let searchhelper = tr.querySelector("td:nth-child(2) > p.search-helper") || null //for compatibility with dice's search helper script 127 | return [id,{name:name, image_id:img, desc:desc, category:type, count:count, tag: tag, searchhelper:searchhelper, id:id}] 128 | } 129 | 130 | function depositQuickStock() { 131 | let submit = $(`#content td.content > form tr:last-child input[type=submit]:nth-child(1)`)?.[0] 132 | if(submit) { 133 | submit.addEventListener("click", (event) => { 134 | //event.preventDefault() 135 | //event.stopPropagation() 136 | if(!clicked) { 137 | let selected = Array.from($(`form[name="quickstock"] tr:not(:nth-last-child(-n+2)) input:checked`)).slice(0,70) //quick stock can only do 70 at a time! 138 | let deposited = selected.filter((e) => {return e.value == "deposit"}).map((e) => {return e.parentElement.parentElement.querySelector("td:first-child").childNodes[0].nodeValue}) 139 | if(deposited.length > 0) { 140 | console.log(`[SDB] Recording deposit of ${deposited.length} items...`) 141 | recordItemListToSDB(deposited) 142 | } 143 | clicked = true 144 | } 145 | }) 146 | console.log("[SDB] Watching quick stock for deposits...") 147 | } 148 | } 149 | 150 | function depositInventory() { 151 | const resultObs = new MutationObserver((mutations) => { 152 | if($("#invResult > div.popup-body__2020 > p")[0].innerHTML.includes("into your safety deposit box!")) { 153 | //get name and img to identify item 154 | let name = $("#invResult > div.popup-body__2020 > p > b")[0].innerHTML 155 | let img = $("#invResult > div.popup-body__2020 > div > img")[0].getAttribute("src").match(/.*?images\.neopets\.com\/items\/(.+?)\..+/)[1] 156 | console.log(`[SDB] Recording ${name} deposit...`) 157 | 158 | //check if we already have info on this in sdb 159 | let res = Object.values(GM_getValue("sdbdata", {})).find((i) => {return i.name == name && i.image_id == img}) 160 | //already have info, increment count 161 | if(res != null) { 162 | res.count += 1 163 | GM_setValue("sdbdata", res) 164 | console.log(`[SDB] Item count incremented.`) 165 | } 166 | //don't have info, get info from itemdb 167 | else getItemDBDataFromPair([name, img]) 168 | } 169 | }) 170 | resultObs.observe($("#invResult")[0], {characterData: true, childList: true, subtree: true}) 171 | console.log("[SDB] Watching inventory for deposits...") 172 | } 173 | 174 | //necessary for if items are fully removed and need to be removed from the sdbdata list 175 | function removeVanillaSDB() { 176 | //remove one - simply update count 177 | $("#content td.content > form > table:nth-child(3) > tbody").on("click", "td:nth-child(6) > a", function (event) { 178 | //event.stopPropagation() 179 | //event.preventDefault() 180 | if(!clicked) { 181 | let id = parseInt(this.parentElement.querySelector("input").getAttribute("name").match(/back_to_inv\[([0-9]+)\]/)[1]) 182 | let sdb = GM_getValue("sdbdata", {}) 183 | sdb[id].count -= 1 184 | console.log(`[SDB] Removed 1 ${sdb[id].name} from SDB.`) 185 | if(sdb[id].count == 0) delete sdb[id] 186 | GM_setValue("sdbdata", sdb) 187 | clicked = true 188 | } 189 | }) 190 | //move selected items 191 | $("#content td.content > form input.submit_data")[0].addEventListener("click", (event) => { 192 | if(!clicked) { 193 | let changeditems = [] 194 | for(const input of Array.from($("input.remove_safety_deposit"))) { 195 | let count = input.value 196 | //for each input that we're removing something from, decrease the count 197 | if(count > 0) { 198 | let sdb = GM_getValue("sdbdata", {}) 199 | let id = input.getAttribute("name").match(/back_to_inv\[([0-9]+)\]/)[1] 200 | sdb[id].count -= count 201 | changeditems.push([sdb[id].name, count]) 202 | if(sdb[id].count == 0) delete sdb[id] 203 | GM_setValue("sdbdata", sdb) 204 | } 205 | } 206 | if(changeditems.length > 0) { 207 | console.log("[SDB] The following items were removed:") 208 | for(const item of changeditems) console.log(`\t- ${item[0]} (x${item[1]})`) 209 | } 210 | clicked = true 211 | } 212 | }) 213 | } 214 | 215 | //============== 216 | // sdb item data 217 | //============== 218 | 219 | function savePageData(pagedata) { 220 | //saves data 221 | let sdbdata = GM_getValue("sdbdata", {}) 222 | for(const item of Object.keys(pagedata)) { 223 | sdbdata[item] = pagedata[item] 224 | } 225 | GM_setValue("sdbdata", sdbdata) 226 | 227 | let items = Object.keys(sdbdata).length 228 | let qty = Object.values(sdbdata).reduce((a, b) => {return a + b.count}, 0) 229 | 230 | //shows item count recorded so far 231 | //todo: store these counts as a variable before page modifications 232 | if(url.includes("safetydeposit.phtml")) { 233 | let itemcounts = $("#content td.content > table > tbody > tr > td:nth-child(2)")[0].innerHTML.match(/Items:<\/b> (.+?) \| Qty:<\/b> (.+)/) 234 | console.log(`[SDB] SDB page recorded. (Items: ${items}/${itemcounts[1].replaceAll(",","")}, Qty: ${qty}/${itemcounts[2].replaceAll(",","")})`) 235 | //autoNavigate() 236 | } 237 | else console.log(`[SDB] SDB page recorded. (Items: ${items}, Qty: ${qty})`) 238 | } 239 | 240 | function recordItemListToSDB(itemnames) { 241 | let toitemdb = {} 242 | let uncertain = GM_getValue("uncertainitems", {}) 243 | let sdbdata = GM_getValue("sdbdata", {}) 244 | 245 | //handles each item to be deposited 246 | let updated = 0 247 | for(const name of itemnames) { 248 | let item = getSDBItemByName(name) 249 | if(Object.keys(item).length == 0) { //we don't have item info, let's grab it from itemdb 250 | if(toitemdb[name]) toitemdb[name] += 1 251 | else toitemdb[name] = 1 252 | } 253 | else if(item == null) { //multiple items with the same name, can't tell item without manual checking 254 | uncertain[name] = undefined 255 | console.log(`[SDB] WARNING: Can't determine item ID from item with duplicate name! (${name})`) 256 | } 257 | else { 258 | Object.values(sdbdata).find((i) => {return i.name == name}).count += 1 //increment count in sdb 259 | updated += 1 260 | } 261 | } 262 | 263 | GM_setValue("sdbdata", sdbdata) 264 | GM_setValue("uncertainitems", uncertain) 265 | 266 | if(updated > 1) console.log(`[SDB] Updated SDB item quantity for ${updated} deposited items.`) 267 | else if(updated == 1) console.log(`[SDB] Updated SDB item quantity for ${updated} deposited item.`) 268 | 269 | if(Object.keys(toitemdb).length > 0) getItemDBDataFromNames(toitemdb) //fetches data for items from db 270 | } 271 | 272 | function getSDBItemByName(name) { 273 | //can't be certain of items that share name, eg map pieces 274 | if(isDuplicateName(name)) return null 275 | return Object.values(GM_getValue("sdbdata", {})).find((i) => {return i.name == name}) || {} 276 | } 277 | 278 | function isDuplicateName(name) { 279 | return name.includes("Map Piece") || name.includes("Laboratory Map") || DUPLICATE_NAMES.includes(name) 280 | } 281 | 282 | //================ 283 | // itemdb requests 284 | //================ 285 | 286 | function parseItemDBResponse(data, itemlist, pair=false) { 287 | if(Object.keys(data).length < itemlist.length) console.log(`[SDB] WARNING: ItemDB only returned ${Object.keys(data).length} items. Make sure to submit data to itemDB using their userscript!`) 288 | 289 | let pagedata = {} 290 | for(let name of Object.keys(data)) { 291 | let key = data[name].item_id 292 | pagedata[key] = {} 293 | pagedata[key].name = data[name].name 294 | pagedata[key].image_id = data[name].image_id 295 | pagedata[key].category = data[name].category 296 | pagedata[key].desc = data[name].description 297 | pagedata[key].tag = data[name].specialType 298 | pagedata[key].rarity = data[name].rarity 299 | pagedata[key].searchhelper = null 300 | pagedata[key].estVal = data[name].estVal 301 | pagedata[key].isNC = data[name].isNC 302 | pagedata[key].isWearable = data[name].isWearable 303 | pagedata[key].isNeohome = data[name].isNeohome 304 | pagedata[key].isNoTrade = data[name].status == "no trade" 305 | pagedata[key].priceData = data[name].price 306 | pagedata[key].owlsData = data[name].owls 307 | if(pair) pagedata[key].count = 1 308 | else pagedata[key].count = itemlist[name] 309 | } 310 | console.log(`[SDB] Item data for ${Object.keys(data).length} items grabbed from ItemDB. Thanks Magnetismo Times!`) 311 | savePageData(pagedata) 312 | } 313 | 314 | function getItemDBData(pagedata) { 315 | let itemlist = Object.keys(pagedata) 316 | $.get("https://itemdb.com.br/api/v1/items/many", {item_id:itemlist}, (data, status)=>{ 317 | if(Object.keys(data).length < itemlist.length) console.log(`[SDB] WARNING: ItemDB only returned ${Object.keys(data).length} items, expected ${itemlist.length} items.`) 318 | for(const key of Object.keys(data)) { 319 | //adds data from itemdb 320 | pagedata[key].rarity = data[key].rarity 321 | pagedata[key].estVal = data[key].estVal 322 | pagedata[key].isNC = data[key].isNC 323 | pagedata[key].isWearable = data[key].isWearable 324 | pagedata[key].isNeohome = data[key].isNeohome 325 | pagedata[key].isNoTrade = data[key].status == "no trade" 326 | pagedata[key].priceData = data[key].price 327 | pagedata[key].owlsData = data[key].owls 328 | } 329 | console.log(`[SDB] Additional item data for ${Object.keys(data).length} items grabbed from ItemDB. Thanks Magnetismo Times!`) 330 | savePageData(pagedata) 331 | }) 332 | } 333 | 334 | function getItemDBDataFromNames(itemlist) { 335 | console.log(`[SDB] Fetching item data from ItemDB for ${Object.keys(itemlist).length} items.`) 336 | $.get("https://itemdb.com.br/api/v1/items/many", {name:Object.keys(itemlist)}, (data) => { 337 | parseItemDBResponse(data, itemlist) 338 | }) 339 | } 340 | 341 | function getItemDBDataFromPair(pair) { 342 | console.log(`[SDB] Fetching item data from ItemDB for ${pair[0]}.`) 343 | $.get("https://itemdb.com.br/api/v1/items/many", {name_image_id:[pair]}, (data) => { 344 | parseItemDBResponse(data, [pair[0]], true) 345 | }) 346 | } 347 | 348 | /* 349 | //back_to_inv%5B1%5D=1&back_to_inv%5B192%5D=1&obj_name=&category=0&offset=0 350 | $.post("/process_safetydeposit.phtml?checksub=scan", "back_to_inv%5B1%5D=1&back_to_inv%5B478%5D=1&obj_name=&category=0&offset=0", (data, status) => { 351 | console.log("status: "+status) 352 | }) 353 | */ -------------------------------------------------------------------------------- /Neopets - Trans Affirming Neopets (Compat).user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Trans Affirming Neopets (Userscript Compat) 3 | // @version 1.0 4 | // @description made this script to hide my own deadname so i can stream shit to my friends lol. probably not perfect. 5 | // @author You 6 | // @match *://www.neopets.com/* 7 | // @match *://neopets.com/* 8 | // @icon https://i.imgur.com/RnuqLRm.png 9 | // @grant none 10 | // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js 11 | // ==/UserScript== 12 | 13 | //make sure this script is the last of your userscripts to load! 14 | 15 | //pairs are in form of a regex statement and a string. 16 | //if you don't know code stuff, just use '/name/ig' to case-insensitive match 17 | const REPLACE_PAIRS = [ 18 | //[/xXbeli3berXx/gi, "coolername07"], 19 | //[/muffin/gi, "Your Mom"] 20 | ] 21 | replaceDeadNames() 22 | 23 | function replaceDeadNames() { 24 | let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { 25 | //filters out nodes with only whitespace 26 | acceptNode: function (node) { 27 | if(node.nodeValue.trim()) return NodeFilter.FILTER_ACCEPT 28 | else return NodeFilter.FILTER_SKIP 29 | } 30 | }, false) 31 | 32 | //walk through each node and replace 33 | let node = null 34 | while (node = walker.nextNode()) { 35 | for(const pair of REPLACE_PAIRS) { 36 | node.nodeValue = node.nodeValue.replace(pair[0], pair[1]) 37 | } 38 | } 39 | } 40 | 41 | $("#transhide")[0]?.remove() -------------------------------------------------------------------------------- /Neopets - Trans Affirming Neopets.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Neopets - Trans Affirming Neopets 3 | // @version 1.0 4 | // @description made this script to hide my own deadname so i can stream shit to my friends lol. probably not perfect. 5 | // @author You 6 | // @match *://www.neopets.com/* 7 | // @match *://neopets.com/* 8 | // @icon https://i.imgur.com/RnuqLRm.png 9 | // @grant none 10 | // @run-at document-body 11 | // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js 12 | // ==/UserScript== 13 | 14 | //pairs are in form of a regex statement and a string. 15 | //if you don't know code stuff, just use '/name/ig' to case-insensitive match 16 | const REPLACE_PAIRS = [ 17 | //[/xXbeli3berXx/gi, "coolername07"], 18 | //[/muffin/gi, "Your Mom"] 19 | ] 20 | 21 | let style = document.createElement("style") 22 | style.innerHTML = `body { visibility: hidden; }` 23 | style.id = "transhide" 24 | document.head.appendChild(style) 25 | 26 | document.addEventListener("DOMContentLoaded", replaceDeadNames) 27 | 28 | function replaceDeadNames() { 29 | let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { 30 | //filters out nodes with only whitespace 31 | acceptNode: function (node) { 32 | if(node.nodeValue.trim()) return NodeFilter.FILTER_ACCEPT 33 | else return NodeFilter.FILTER_SKIP 34 | } 35 | }, false) 36 | 37 | //walk through each node and replace 38 | let node = null 39 | while (node = walker.nextNode()) { 40 | for(const pair of REPLACE_PAIRS) { 41 | node.nodeValue = node.nodeValue.replace(pair[0], pair[1]) 42 | } 43 | } 44 | 45 | style.remove() 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## ✨ MettyNeo Userscripts ✨ 4 |

Sometimes small things on Neopets annoy me so I write userscripts to make things a bit easier and share them to the world to hopefully make everyone's life a bit easier.

5 | 6 | ___ 7 | 8 | 9 | ### Page Overhauls: 10 | 11 | 12 | 13 | * ⚔️ [Battledome Set Selector / Better Battledome ](Neopets%20-%20Battledome%20Set%20Selector.user.js) 14 | * Adds the ability to save and select up to 5 BD weapon/ability selections with the click of a button! 15 | * Sets can be set to automatically apply based on the current turn! 16 | * Disables the BD animations, making fights much more seamless! 17 | * Remembers pet / challenger selections and allows you to filter only favorited challengers! 18 | * Tracks and displays earnings and lets you copy your prizes to clipboard to share with others! 19 | * Tracks Obelisk contribution, including battles played and points earned for your team! 20 | * Note: Script automatically disables during 2P battles and after your first 10 Obelisk battles! 21 | 22 | * 🎣 [Active Pet Switch / Fishing Vortex Plus](Neopets%20-%20Active%20Pet%20Switch.user.js) 23 | * Allows you to switch your active pet from anywhere with the press of a button! 24 | * Adds additional features for the Fishing Vortex such as level display and time since last fished! 25 | - Now you can fish with all of your pets! 26 | 27 | 28 | * 🎴 [Better, Faster Godori](Neopets%20-%20Better%20Faster%20Godori.user.js) 29 | * Removes the unnecessary delay between actions, making the game way faster! 30 | * Cards in hand will automatically be assigned the relevant stack, reducing number of clicks! 31 | * You can play the highest value card in your hand by clicking on a card stack on the table! 32 | * A complete overhaul of the game's visual display! 33 | - Cards are color-coded by capture set 34 | - Hand is sorted by card type 35 | - Hovering over a card highlights cards of the same capture type 36 | - Displays number of cards in hand, deck and capture categories 37 | * Adds a timer so you can really race to those 250 hand wins! 38 | * [UNIMPLEMENTED] Tracks cards in play and displays card counts to help make better decisions! 39 | 40 | * 🥤 [NeoCola Enhancements](Neopets%20-%20NeoCola%20Enhancements.user.js) 41 | * Cleans up and vastly improves the token selection UI! 42 | * Tracks prize results, displaying your average earnings and cumulative odds for a Transmogrification Potion! 43 | * Implements the ["Legal Cheat" Easter Egg](https://web.archive.org/web/20210619183531/https://old.reddit.com/r/neopets/comments/o3mq8r/neocola_tokens/) to increase Neopoint earnings! 44 | - NOTE: The legality of this is a grey area and it is currently enabled by default. This can be easily disabled in the script's configs - use at your own discretion! 45 | * Overhauls token submission to allow for quick submissions to still be displayed and tracked! 46 | - [WIP] Submission and tracking works but no additional displays are added yet 47 | 48 | * 📦 [Inventory +](Neopets%20-%20Inventory%2B.user.js) 49 | * Allows you to perform multiple actions in an inventory without refreshing the page! 50 | - NOTE: Certain prompts, eg gifting users, may not auto-refresh at the right time. This will be fixed in a later version. 51 | * Remembers your option selection for each item, making repeated uses much quicker! 52 | 53 | ___ 54 | 55 | 56 | ### QoL Scripts: 57 | * ☠️ [Lever of Doom Shame Tracker](Neopets%20-%20LoD%20Shame%20Tracker.user.js) 58 | - Tracks the number of times the lever has been pulled and the amount of NP spent 59 | - Automatically checks to see if you have earned the avatar, allowing for spam-refreshing without worries! 60 | 61 | * 🛒 [Quick Inventory Deposit](Neopets%20-%20Quick%20Inventory%20Deposit.user.js) 62 | * Adds a button on all pages that deposits all items currently in your inventory into your Safety Deposit Box 63 | - Donates items using the quick stock page in batches of 70 64 | 65 | * ⭐ [Home Page Redirect](Neopets%20-%20Home%20Page%20Redirect.user.js) 66 | - Simply redirects from the new homepage to the actual game site's home page. 67 | 68 | * 🏳️‍🌈 [Trans Affirming Neopets](Neopets%20-%20Trans%20Affirming%20Neopets.user.js) [(+ Userscript Compat)](Neopets%20-%20Trans%20Affirming%20Neopets%20(Compat).user.js) 69 | - Lets the user replace all instances of a word with another word on all Neopets pages 70 | - Primarily intended for replacing deadnames but can be used for any word or phrase! 71 | - Userscript Compat applies the word replace after everything loads, thus replacing content added directly after the page itself loads. 72 | - Note: Requires the user to add pairs of words in the script itself! 73 | - Note: Does not currently replace instances of words added to the page after loading, eg. on pop-up prompts! 74 | 75 | * 🍗 [NeoFoodClub +](Neopets%20-%20NeoFoodClub%2B.user.js) 76 | - Adds various small improvements to the fansite [NeoFood.Club](https://neofood.club/) 77 | + Automatically applies max bet amount 78 | + Keeps track of bets that have been placed to prevent double-betting 79 | + Opens bet placement links in a new tab and automatically closes them once applied 80 | + Adds quicklinks to the Current Bets and Collect Winnings pages 81 | 82 | * 🖼 [Mystery Pic Helper](Neopets%20-%20Mystery%20Pic%20Helper.user.js) 83 | - Adds quicklinks to Jellyneo Image Emporium pages to help users determine the Mystery Pic 84 | 85 | * 🧩 [Daily Puzzle Solver](Neopets%20-%20Daily%20Puzzle%20Solver.user.js) 86 | - Automatically grabs and selects the correct daily puzzle answer from TheDailyNeopets 87 | - Permission to use pages was obtained and credit included. Thanks TDN team! 88 | 89 | * 🌎 [Quick Exploration Links](Neopets%20-%20Quick%20Exploration%20Links.user.js) 90 | - Adds a drop-down to the Explore link on beta pages 91 | - Easy to add custom links! 92 | 93 | ___ 94 | 95 | ### WIP Userscripts: 96 | 97 | * 📡 [Direct Link Display](Neopets%20-%20Direct%20Link%20Display.user.js) 98 | - Adds a visual display to daily direct links 99 | - Currently supports Qasalan Expellibox and Coconut Shy 100 | - ~~Wheel support planned~~ Neopets killed this, unfortunately... 101 | 102 | * 🗃 [SDB Inventarium](Neopets%20-%20SDB%20Inventarium.user.js) 103 | - Currently only tracks items in SDB and items going in/out of SDB 104 | + NOTE: Currently does not track items deposited via the Quick Inventory Deposit userscript 105 | - [UNIMPLEMENTED] Overhauls the SDB by displaying all items on one page 106 | - [UNIMPLEMENTED] Items can be sorted by rarity, est. price, market price, quantity, and more! 107 | + Currently allows users to sort for r102-r179 items for Faerie Festival, albeit with a primitive display 108 | - [UNIMPLEMENTED] User can save sorting options and quickly apply saved search conditions! 109 | -------------------------------------------------------------------------------- /mettyneo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mettymagic/np-userscripts/38dfedb9e6ea205596cbf1f8f53cb775f456e591/mettyneo-icon.png --------------------------------------------------------------------------------