[GC] Avatar Checklist

Avatar, Site Themes, and Relics checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco

Ajankohdalta 20.7.2025. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         [GC] Avatar Checklist
// @namespace    https://www.grundos.cafe/
// @version      3.0.0
// @description  Avatar, Site Themes, and Relics checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco
// @author       soupfaerie, supercow64, arithmancer
// @match        https://www.grundos.cafe/~Tyco*
// @match        https://www.grundos.cafe/~tyco*
// @match        https://www.grundos.cafe/~TYCO*
// @grant        none
// @license      MIT
// ==/UserScript==

const textToHTML = (text) => new DOMParser().parseFromString(text, "text/html");

/**
 * Analyse the HTML select element for a list of avatars the user has collected.
 *
 * @param {Node} node The root node (default: document)
 * @returns {string[]} the list of avatars as an array of basenames
 */
const getCollectedAvatars = (node = document) => {
  // The list of avatars is partitioned into default avatars
  // and collected secret avatars. The option with the text ---
  // (6 dashes) is the inclusive cutoff. All avatars at and below
  // the cutoff are collected secret avatars
  const allAvatars = Array.from(
    node.querySelectorAll(`[name="new_avatar"] option`)
  );
  const i = allAvatars.findIndex((e) => e.textContent.includes("---"));
  return allAvatars.slice(i).map((e) => e.value);
};

/**
 * Analyse the HTML select element for all site themes available to the user.
 *
 * @param {Node} node The root node (default: document)
 * @returns {string[]} all site themes as an array of theme names
 */
const getAllSiteThemes = (node = document) => {
  // Find all options in the site_theme select element
  const themeOptions = Array.from(node.querySelectorAll(`[name="site_theme"] option`));
  if (!themeOptions.length) return [];

  // Return the text content of each option
  return themeOptions
    .map(option => option.textContent.trim())
    .filter(themeName => themeName); // Filter out any empty theme names
};

/**
 * Returns a Promise that resolves to a list of avatars
 * the user has collected.
 *
 * @returns {string[]} list of collected avatars
 */
const getCollectedAvatarsAsync = () =>
  fetch("/neoboards/preferences/")
    .then((res) => res.text())
    .then(textToHTML)
    .then(getCollectedAvatars);

/**
 * Returns a Promise that resolves to all site themes
 * available to the user.
 *
 * @returns {string[]} all site themes as an array of theme names
 */
const getAllSiteThemesAsync = () =>
  fetch("/help/siteprefs")
    .then((res) => res.text())
    .then(textToHTML)
    .then(getAllSiteThemes);

/**
 * Analyse the HTML response to identify logged relics.
 *
 * @param {Node} node The root node (default: document)
 * @returns {string[]} the list of logged relics as an array of relic names
 */
const getLoggedRelics = (node = document) => {
  // Find all divs with id="reliclogged"
  const loggedRelics = Array.from(
    node.querySelectorAll(`[id="reliclogged"]`)
  );
  if (!loggedRelics.length) return [];

  // Extract the relic names from the logged relics
  return loggedRelics
    .map(relic => {
      const img = relic.querySelector('img');
      // Use the title attribute of the image which contains the relic name
      if (img && img.title) {
        return img.title;
      }
      // If title is not available, try to get the name from the span element
      const span = relic.querySelector('span.medfont strong');
      if (span) {
        // Extract just the relic name without any child elements
        return span.childNodes[0].textContent.trim();
      }
      return null;
    })
    .filter(name => name); // Filter out any null values
};

/**
 * Returns a Promise that resolves to a list of logged relics.
 *
 * @returns {string[]} list of logged relics as an array of relic names
 */
const getLoggedRelicsAsync = () =>
  fetch("/space/warehouse/relics/")
    .then((res) => res.text())
    .then(textToHTML)
    .then(getLoggedRelics);

/**
 * For static assets, returns the basename of the asset indicated
 * in the url.
 *
 * ```js
 * basename("https://example.com/foo/bar/baz.gif") == "baz.gif"
 * ```
 *
 * @param {string} url path to the file with slashes
 * @returns {string} the basename
 */
const basename = (url) => url.split("/").slice(-1)[0];



/**
 * Move collected avatar cards into their section's <details> element.
 *
 * The tracker page groups avatars by section. Each section is a <div>
 * directly under the #avatars container. Within each section there are one or
 * more `.avatar-grid` containers followed by a <details> element containing an
 * empty `.avatar-grid`. Collected avatars should be appended to that grid.
 *
 * @param {string[]} collectedAvatars basenames of the user's collected avatars
 */
function moveCollectedAvatars(collectedAvatars) {
  const sections = document.querySelectorAll('#avatars > div');
  // Create a set to track which collected avatars are found on the page
  const foundAvatars = new Set();

  sections.forEach((section) => {
    const foundGrid = section.querySelector('details .avatar-grid');
    if (!foundGrid) return;

    const cards = section.querySelectorAll(':scope > .avatar-grid > .avatar-card');
    cards.forEach((card) => {
      const img = card.querySelector('img');
      if (!img) return; // site theme cards currently lack images

      const avatarName = basename(img.src);
      if (collectedAvatars.includes(avatarName)) {
        card.classList.add('check');
        foundGrid.appendChild(card);
        foundAvatars.add(avatarName);
      }
    });
  });

  // Log any collected avatars that weren't found on the page
  collectedAvatars.forEach(avatar => {
    if (!foundAvatars.has(avatar)) {
      console.log(`Collected avatar not found on page: ${avatar}`);
    }
  });
}

/**
 * Move collected site theme cards into the themes section's <details> element.
 *
 * The site themes section has a <details> element containing an empty `.avatar-grid`.
 * All site themes from the site_theme select element should be appended to that grid.
 *
 * @param {string[]} collectedThemes names of all available site themes
 */
function moveCollectedSiteThemes(collectedThemes) {
  const themesSection = document.querySelector('#themes');
  if (!themesSection) return;

  const foundGrid = themesSection.querySelector('details .avatar-grid');
  if (!foundGrid) return;

  // Create a set to track which collected themes are found on the page
  const foundThemes = new Set();

  const cards = themesSection.querySelectorAll(':scope > .avatar-grid > .avatar-card');
  cards.forEach((card) => {
    const nameElement = card.querySelector('.avatar-name');
    if (!nameElement) return;

    // Get the theme name directly from the card
    const themeName = nameElement.textContent.trim();

    // Check if this theme is in the collected themes list
    if (collectedThemes.includes(themeName)) {
      card.classList.add('done');
      foundGrid.appendChild(card);
      foundThemes.add(themeName);
    }
  });

  // Log any collected themes that weren't found on the page
  collectedThemes.forEach(theme => {
    if (!foundThemes.has(theme)) {
      console.log(`Collected site theme not found on page: ${theme}`);
    }
  });
}

/**
 * Move logged relic cards into their section's <details> element.
 *
 * The relics section has a <details> element containing an empty `.avatar-grid`.
 * Logged relics should be appended to that grid and marked with the "redeemed" class.
 *
 * @param {string[]} loggedRelics names of logged relics
 */
function moveLoggedRelics(loggedRelics) {
  const relicsSection = document.querySelector('#relics');
  if (!relicsSection) return;

  const foundGrid = relicsSection.querySelector('details .avatar-grid');
  if (!foundGrid) return;

  // Create a set to track which logged relics are found on the page
  const foundRelics = new Set();

  const cards = relicsSection.querySelectorAll(':scope > .avatar-grid > .avatar-card');
  cards.forEach((card) => {
    const nameElement = card.querySelector('.avatar-name');
    if (!nameElement) return;

    // Get the relic name directly from the card
    const relicName = nameElement.textContent.trim();

    // Check if this relic is in the logged relics list
    if (loggedRelics.includes(relicName)) {
      card.classList.add('redeemed');
      foundGrid.appendChild(card);
      foundRelics.add(relicName);
    }
  });

  // Log any logged relics that weren't found on the page
  loggedRelics.forEach(relic => {
    if (!foundRelics.has(relic)) {
      console.log(`Logged relic not found on page: ${relic}`);
    }
  });
}

// Fetch avatar, site theme, and relic data and move the collected cards
Promise.all([
  getCollectedAvatarsAsync().then(moveCollectedAvatars),
  getAllSiteThemesAsync().then(moveCollectedSiteThemes),
  getLoggedRelicsAsync().then(moveLoggedRelics)
]);