[GC] - Underwater Fishing Mods

Fishing rewards sorting and logging with combined results.

// ==UserScript==
// @name        [GC] - Underwater Fishing Mods
// @namespace   Grundo's Cafe
// @match       https://www.grundos.cafe/water/fishing/
// @match       https://www.grundos.cafe/help/userscripts/
// @grant       GM.getValue
// @grant     	GM.setValue
// @grant       GM.addStyle
// @grant       GM.xmlHttpRequest
// @version     1.1.0
// @license     MIT
// @author      Cupkait
// @icon        https://i.imgur.com/4Hm2e6z.png
// @description Fishing rewards sorting and logging with combined results.
// @require https://update.gf.qytechs.cn/scripts/489454/1588028/%5BGC%5D%20-%20Underwater%20Fishing%20Prizes%20Library.js
// @require https://update.gf.qytechs.cn/scripts/514423/1554918/GC%20-%20Universal%20Userscripts%20Settings.js
// ==/UserScript==

(function() {
  'use strict';

  const CACHED_SELECTORS = {
    userName: '#user-info-username',
    buttonFishAll: 'input[value="Fish with Everyone!"]',
    buttonFishOne: 'input[value="Reel in Your Line"]',
    pageContent: 'div#page_content > main',
    centerItems: '.center-items',
    resultsDiv: '.flex-column'
  };

  const DOM = {};

  function initializeDOMCache() {
    Object.entries(CACHED_SELECTORS).forEach(([key, selector]) => {
      DOM[key] = document.querySelector(selector);
    });
  }

  const SCRIPT_CATEGORY = "Underwater Fishing";
  const SETTING_ENABLE_HIGHLIGHTING = "enableHighlighting";
  const SETTING_ENABLE_LOGGING = "enableLogging";
  const SETTING_ENABLE_WEBHOOKS = "enableWebhooks";
  const SETTING_ENABLE_ANONYMIZE = "enableAnonymize";
  const SETTING_WEBHOOKS = "webHooks";

  function hashString(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash);
  }

  if (window.location.href.includes('/help/userscripts/')) {
    addCheckboxInput({
      categoryName: SCRIPT_CATEGORY,
      settingName: SETTING_ENABLE_HIGHLIGHTING,
      labelText: "Enable Highlighting",
      labelTooltip: "When enabled, notable prizes will be highlighted at the top of the fishing results page.",
      defaultSetting: true
    });

    addCheckboxInput({
      categoryName: SCRIPT_CATEGORY,
      settingName: SETTING_ENABLE_LOGGING,
      labelText: "Enable Data Logging",
      labelTooltip: "When enabled, your fishing results will be sent to the fishing data collector for statistics and analysis.",
      defaultSetting: true
    });

    addCheckboxInput({
      categoryName: SCRIPT_CATEGORY,
      settingName: SETTING_ENABLE_WEBHOOKS,
      labelText: "Enable Webhooks",
      labelTooltip: "When enabled, your fishing results will be sent to the specified webhook URLs.",
      defaultSetting: false
    });

    addCheckboxInput({
      categoryName: SCRIPT_CATEGORY,
      settingName: SETTING_ENABLE_ANONYMIZE,
      labelText: "Anonymize Data",
      labelTooltip: "When enabled, your username and pet names will be anonymized in the data collection. This does not affect webhooks.",
      defaultSetting: false
    });

    addTextInput({
      categoryName: SCRIPT_CATEGORY,
      settingName: SETTING_WEBHOOKS,
      labelText: "Webhooks",
      labelTooltip: "Separate different webhook URLs with commas. Example: https://webhook1.com, https://webhook2.com",
      defaultSetting: "",
    });
  }

  if (!window.location.href.includes('/water/fishing/')) {
    return;
  }

  const userName = document.querySelector('#user-info-username')?.textContent;
  const buttonFishAll = document.querySelector('input[value="Fish with Everyone!"]');
  const buttonFishOne = document.querySelector('input[value="Reel in Your Line"]');
  const pageContent = document.querySelector("div#page_content > main");

  async function getCurrentPetLevels() {
    try {
      const response = await fetch("https://www.grundos.cafe/quickref/");
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }

      const html = await response.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, "text/html");
      const petList = doc.querySelector("#quickref_petlist")?.children;

      if (!petList) {
        throw new Error('Pet list not found');
      }

      const petLevels = {};
      Array.from(petList).forEach(pet => {
        const petrefNameMatch = pet.querySelector("a")?.getAttribute("href")?.match(/_name=(.*?)$/);
        const fishingLevelMatch = pet.querySelectorAll("span")[12]?.textContent.match(/Fishing : (.*?)$/);

        if (petrefNameMatch && fishingLevelMatch) {
          petLevels[petrefNameMatch[1]] = fishingLevelMatch[1];
        }
      });

      sessionStorage.setItem('petLevels', JSON.stringify(petLevels));
      return petLevels;
    } catch (error) {
      console.error("Error fetching pet levels:", error);
      return null;
    }
  }

  function createStatusDisplay(message) {
    const existingDisplay = document.getElementById("displayResults");
    if (existingDisplay) {
      existingDisplay.innerHTML = message;
      return existingDisplay;
    }

    const displayResults = document.createElement("div");
    displayResults.innerHTML = message;
    displayResults.id = "displayResults";
    displayResults.style.cssText =
      "color: green; text-align: center; link-color:green; font-size:14px; font-weight:bold; margin: 10px 0;";
    pageContent?.insertAdjacentElement("beforebegin", displayResults);
    return displayResults;
  }

  function extractResultData(resultElement) {
    try {
      const pet = resultElement.querySelector('strong')?.textContent;
      const paragraphText = resultElement.querySelector('p')?.textContent;
      
      // Check if pet caught something or nothing
      let item;
      if (paragraphText?.includes('caught nothing')) {
        item = 'Nothing';
      } else {
        const itemMatch = paragraphText?.match(/ a (.*?)!/);
        item = itemMatch ? itemMatch[1] : 'Nothing';
      }
      
      const cooldown = resultElement.querySelectorAll('strong')[1]?.textContent;
      const image = resultElement.querySelectorAll('img')[1]?.src;

      const levelUpText = resultElement.querySelectorAll("p")[1]?.textContent;
      const newlevel = levelUpText && levelUpText.includes("fishing level") ?
        levelUpText.match(/ \d+/)[0] : null;

      const storedLevels = JSON.parse(sessionStorage.getItem('petLevels') || '{}');
      const oldlevel = storedLevels[pet] || null;

      if (!pet || !cooldown) {
        return null;
      }

      return { pet, item, cooldown, image, oldlevel, newlevel };
    } catch (error) {
      console.error("Error extracting result data:", error);
      return null;
    }
  }

  function extractSingleResult() {
    try {
      const resultSingle = document.querySelector('#page_content .center');
      if (!resultSingle) return null;

      // Check if this is the "cast your line again" page (no results) - before fishing
      if (resultSingle.querySelector('form input[value="Cast Your Line Again"]') && 
          !resultSingle.querySelector('img[alt]')) {
        return null; // This is not a results page
      }

      // For single pet results, extract data from the specific structure
      const paragraphs = resultSingle.querySelectorAll('p');
      
      let pet = null;
      let item = 'Nothing';
      let cooldown = null;
      let image = null;

      // Look for item image (med-image class) vs pet image (big-image class)
      const itemImgElement = resultSingle.querySelector("img.med-image[alt]");
      const petImgElement = resultSingle.querySelector("img.big-image[alt]");

      // Extract item name and image (if there's an item)
      if (itemImgElement) {
        item = itemImgElement.alt;
        image = itemImgElement.src;
      }

      // Get pet name from pet image if available
      if (petImgElement) {
        pet = petImgElement.alt;
      }

      // If no item image but has paragraphs, check for "caught nothing" case
      if (!itemImgElement && paragraphs.length > 0) {
        for (const p of paragraphs) {
          if (p.textContent.includes('caught nothing')) {
            item = 'Nothing';
            break;
          }
        }
      }

      // Extract pet name and cooldown from the cooldown paragraph
      for (const p of paragraphs) {
        const cooldownMatch = p.textContent.match(/(\w+) might be able to cast again in about.*?(\d+).*?hours?/);
        if (cooldownMatch) {
          if (!pet) pet = cooldownMatch[1]; // Only use if not already found from image
          cooldown = cooldownMatch[2];
          break;
        }
      }

      // If no cooldown found, try to get it from a strong element
      if (!cooldown) {
        cooldown = resultSingle.querySelector('strong')?.textContent;
      }

      // For "nothing" results, we need to assume a default cooldown if none is found
      // This typically happens when there's no explicit cooldown text
      if (!cooldown && pet && item === 'Nothing') {
        cooldown = '0'; // Default cooldown for nothing results
      }

      // Try alternative pet name sources if not found
      if (!pet) {
        pet = document.querySelector('.user-info-pet')?.textContent ||
              document.querySelector('#user-info-username')?.textContent;
      }

      const levelUpText = Array.from(paragraphs).find(p => p.textContent.includes("fishing level"))?.textContent;
      const newlevel = levelUpText && levelUpText.includes("fishing level") ?
        levelUpText.match(/ \d+/)[0] : null;

      const storedLevels = JSON.parse(sessionStorage.getItem('petLevels') || '{}');
      const oldlevel = storedLevels[pet] || null;

      if (!pet || !cooldown) {
        return null;
      }

      return { pet, item, cooldown, image, oldlevel, newlevel };
    } catch (error) {
      console.error("Error extracting single result:", error);
      return null;
    }
  }

async function highlightNotablePrizes(results, resultsDiv) {
  const highlightingEnabled = await GM.getValue(SETTING_ENABLE_HIGHLIGHTING, true);
  if (!highlightingEnabled || !results.length || !resultsDiv) return;

  const goodPrizes = document.createElement('div');
  let hasHighlights = false;
  
  const fragment = document.createDocumentFragment();
  
  Array.from(document.querySelectorAll(CACHED_SELECTORS.centerItems))
    .filter(element => {
      const paragraphElement = element.querySelector('p');
      if (!paragraphElement) return false;
      
      const itemMatch = paragraphElement.textContent.match(/ a (.*?)!/);
      if (!itemMatch) return false;
      
      const itemName = itemMatch[1];
      return prizes?.[itemName]?.h === true;
    })
    .forEach(element => {
      hasHighlights = true;
      element.style.borderLeft = "5px solid aquamarine";
      fragment.appendChild(element);
    });

  if (hasHighlights) {
    goodPrizes.appendChild(fragment);
    goodPrizes.classList.add("center-items");
    Object.assign(goodPrizes.style, {
      backgroundColor: "#efef404f",
      border: "2px solid black",
      marginBottom: "15px",
      padding: "10px"
    });
    goodPrizes.innerHTML = "<h3>Notable Prizes</h3>" + goodPrizes.innerHTML;
    resultsDiv.insertAdjacentElement('beforebegin', goodPrizes);
  }
}

  function createStandardEntry(result, shouldAnonymize = false) {
    const cooldownMinutes = parseInt(result.cooldown?.match(/\d+/)?.[0] || "0", 10);

    let itemId = 0;
    if (typeof result.item === 'string' && prizes?.[result.item]?.iid) {
      itemId = prizes[result.item].iid;
    }

    return {
      "petname": shouldAnonymize ? hashString(result.pet || "").toString() : (result.pet || ""),
      "currentlevel": parseInt(result.oldlevel || "0", 10),
      "newlevel": parseInt(result.newlevel || result.oldlevel || "0", 10),
      "itemid": itemId,
      "cooldown": cooldownMinutes
    };
  }

  async function submitResultsToLogger(formattedData) {
    const loggingEnabled = await GM.getValue(SETTING_ENABLE_LOGGING, true);
    if (!loggingEnabled) {
      console.log("Data logging is disabled. Skipping data submission.");
      return "Logging disabled";
    }

    try {
      const response = await fetch('https://somethinghashappened.com/gc/log.php', {
        method: 'POST',
        mode: 'no-cors',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formattedData)
      });

      try {
        const responseText = await response.json();
        console.log('Server response:', responseText);
        return responseText;
      } catch (readError) {
        return 'Request sent';
      }
    } catch (error) {
      console.error('Error submitting results:', error);
      return null;
    }
  }

  async function generatePrizeWebhooks(name, oldlevel, item, image) {
    if (!item || !prizes[item] || prizes[item].h !== true) {
      return null;
    }
  
    const webhooksString = await GM.getValue(SETTING_WEBHOOKS, "");
    if (!webhooksString.trim()) {
      return null;
    }
  
    const webHooks = webhooksString
      .split(',')
      .map(url => url.trim())
      .filter(url => url && url.startsWith('http'));
  
    const hook = {
      content: null,
      embeds: [{
        description: `${item}?\nWhat a great prize!`,
        color: 7844437,
        author: {
          name: `${userName} took ${name} fishing...`,
        },
        thumbnail: {
          url: `${image}`,
        },
      }],
      username: "Underwater Fishing Prizes",
      avatar_url: "https://i.imgur.com/4Hm2e6z.png",
      attachments: [],
    };
  
    const results = await Promise.allSettled(
      webHooks.map(webhook => sendMessage(hook, webhook))
    );
  
    const summary = results.reduce((acc, result, index) => {
      const webhookUrl = webHooks[index].substring(0, 30) + '...';
      if (result.status === 'fulfilled') {
        acc.success.push(webhookUrl);
      } else {
        acc.failed.push(webhookUrl);
      }
      return acc;
    }, { success: [], failed: [] });
  
    let statusMessage = [];
    if (summary.success.length) {
      statusMessage.push(`Sent to ${summary.success.length} webhook${summary.success.length !== 1 ? 's' : ''}`);
    }
    if (summary.failed.length) {
      statusMessage.push(`Failed ${summary.failed.length} webhook${summary.failed.length !== 1 ? 's' : ''}`);
    }
  
    return statusMessage.length ? statusMessage.join(', ') : null;
  }

  async function sendMessage(hook, webhook) {
    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        method: "POST",
        url: webhook,
        headers: {
          "Content-Type": "application/json"
        },
        data: JSON.stringify(hook),
        onload: function(response) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response.responseText);
          } else {
            reject(new Error(`Request failed with status ${response.status}`));
          }
        },
        onerror: function(error) {
          reject(error);
        }
      });
    });
  }

  async function submitResultsToWebhooks(formattedData) {
    const webhooksEnabled = await GM.getValue(SETTING_ENABLE_WEBHOOKS, false);
    if (!webhooksEnabled) {
      return "Webhooks disabled";
    }

    const results = [];

    for (const entry of formattedData.entries) {
      const result = await generatePrizeWebhooks(
        entry.petname,
        entry.currentlevel,
        Object.keys(prizes).find(key => prizes[key].iid === entry.itemid) || "Unknown Item",
        "https://i.imgur.com/4Hm2e6z.png"
      );

      if (result) {
        results.push(result);
      }
    }

    return results.length > 0 ? results.join(', ') : "No notable prizes";
  }

  async function collectFishingResults() {
    const displayResults = createStatusDisplay("Loading results... please wait.");
    const results = [];
    const originalResults = [];

    const resultsList = document.querySelectorAll('.center-items');
    const resultsDiv = document.querySelector('.flex-column');

    if (resultsList.length > 0) {
      resultsList.forEach(result => {
        const resultData = extractResultData(result);
        if (resultData) {
          results.push(resultData);
          originalResults.push({...resultData});
        }
      });

      if (results.length > 0) {
        await highlightNotablePrizes(results, resultsDiv);
      }
    } else {
      const singleResult = extractSingleResult();
      if (singleResult) {
        results.push(singleResult);
        originalResults.push({...singleResult});
      }
    }

    if (results.length > 0) {
      const anonymizeEnabled = await GM.getValue(SETTING_ENABLE_ANONYMIZE, false);
      const formattedEntries = results.map(result => createStandardEntry(result, anonymizeEnabled));

      const formattedData = {
        "username": anonymizeEnabled ? hashString(userName || "").toString() : (userName || "USERNAMEHERE"),
        "entries": formattedEntries
      };

      const loggingEnabled = await GM.getValue(SETTING_ENABLE_LOGGING, true);
      const highlightingEnabled = await GM.getValue(SETTING_ENABLE_HIGHLIGHTING, true);
      const webhooksEnabled = await GM.getValue(SETTING_ENABLE_WEBHOOKS, false);

      let statusMessage = `${results.length} pet${results.length !== 1 ? 's' : ''} participated.`;
      let statusParts = [];

      if (loggingEnabled) {
        const serverResponse = await submitResultsToLogger(formattedData);
        statusParts.push("Your fishing results have been submitted");
      } else {
        statusParts.push("Data logging is disabled");
      }

      if (webhooksEnabled) {
        const webhookPromises = originalResults.map(result =>
          generatePrizeWebhooks(
            result.pet,
            result.oldlevel,
            result.item,
            result.image
          )
        );

        const webhookResponses = await Promise.all(webhookPromises);
        const completedWebhooks = webhookResponses.filter(r => r !== null);

        if (completedWebhooks.length > 0) {
          statusParts.push("Webhooks sent for notable prizes");
        }
      }

      statusMessage = `${statusParts.join(", ")}. ${statusMessage}`;

      if (!highlightingEnabled) {
        statusMessage += " (Prize highlighting is disabled)";
      }

      displayResults.innerHTML = statusMessage;
    } else {
      displayResults.innerHTML = "No fishing results were detected.";
    }
  }

  function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  const debouncedCollectResults = debounce(collectFishingResults, 250);

  async function init() {
    initializeDOMCache();
    
    const isResultPage = document.referrer.endsWith("/water/fishing/") &&
      DOM.pageContent?.textContent.includes("You reel in your line and get");
      
    if (isResultPage) {
      debouncedCollectResults();
    }

    if (DOM.buttonFishOne || DOM.buttonFishAll) {
      const handleFishingClick = async function(event) {
        const isTargetButton = event.target === DOM.buttonFishOne || 
                              event.target === DOM.buttonFishAll;
        
        if (isTargetButton) {
          event.preventDefault();
          await getCurrentPetLevels();
          event.target.form.submit();
        }
      };

      document.addEventListener("click", handleFishingClick);
    }
  }

  init();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址