您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
add utils to the HSReplay.net Battlegrounds Comps page
// ==UserScript== // @name HSReplay.net Battlegrounds Comps Utils // @namespace http://tampermonkey.net/ // @version 2025-09-14.2 // @description add utils to the HSReplay.net Battlegrounds Comps page // @author Brok3nPix3l // @match https://hsreplay.net/battlegrounds/comps/* // @icon https://www.google.com/s2/favicons?sz=64&domain=hsreplay.net // @grant GM_addValueChangeListener // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (async function () { "use strict"; if (location.href !== "https://hsreplay.net/battlegrounds/comps/") { const shouldRetrieveData = GM_getValue(location.href + "-retrieve-detailed-comp-data", false); if (!shouldRetrieveData) { console.debug("Data retrieval not requested, exiting."); return; } const guideSections = await waitElements(document, "section.guide-content", 2); console.debug("found guideSections:"); console.debug(guideSections); const guideSection = guideSections[1]; console.debug("found guideSection:"); console.debug(guideSection); const cards = guideSection.children[0]; console.debug("found cards:"); console.debug(cards); const whenToCommitCard = cards.children[3]; console.debug("found whenToCommitCard:"); console.debug(whenToCommitCard); const whenToCommitDetails = whenToCommitCard.children[1]; console.debug("found whenToCommitDetails:"); console.debug(whenToCommitDetails); const commonEnablersCard = cards.children[4]; console.debug("found commonEnablersCard:"); console.debug(commonEnablersCard); const commonEnablersDetails = commonEnablersCard.children[1]; console.debug("found commonEnablersDetails:"); console.debug(commonEnablersDetails); const commonEnablersDetailsChild = commonEnablersDetails.children[0]; console.debug("found commonEnablersDetailsChild:"); console.debug(commonEnablersDetailsChild); const commonEnablersFlattenedString = Array.from(commonEnablersDetailsChild.children).map(child => child.textContent).join(', '); GM_setValue(`${location.href}-when-to-commit`, whenToCommitDetails.textContent); console.debug(`${location.href}-when-to-commit set to: ${whenToCommitDetails.textContent}`); GM_setValue(`${location.href}-common-enablers`, commonEnablersFlattenedString); console.debug(`${location.href}-common-enablers set to: ${commonEnablersFlattenedString}`); GM_setValue(`${location.href}-retrieve-detailed-comp-data`, false); window.close(); return; } const tierColumnHeader = await waitElementWithInnerText(document.body, "div", "Tier"); console.debug("found tierColumnHeader:"); console.debug(tierColumnHeader); const titleRow = tierColumnHeader.parentElement; console.debug("found titleRow:"); console.debug(titleRow); const table = titleRow.parentElement; console.debug("found table:"); console.debug(table); const container = table.parentElement; console.debug("found container:"); console.debug(container); const tierList = table.children[1]; console.debug("found tierList:"); console.debug(tierList); const filtersContainer = document.createElement("div"); filtersContainer.style.display = "flex"; filtersContainer.style.justifyContent = "space-between"; filtersContainer.style.gap = "20px"; filtersContainer.style.flexWrap = "wrap"; filtersContainer.style.marginTop = "10px"; filtersContainer.style.marginBottom = "10px"; container.prepend(filtersContainer); const tribes = [ "beast", "demon", "dragon", "elemental", "mech", "murloc", "naga", "pirate", "quilboar", "undead", ]; const tierToCompMappings = {}; const tierElementMappings = []; const compElementMappings = []; const compToTierMappings = {}; const tribeToCompMappings = {}; Array.from(tierList.children).map((tierElement) => { const tierName = tierElement.children[0].innerText; tierElementMappings[tierName] = { element: tierElement, display: tierElement.style.display, }; Array.from(tierElement.children[1].children).map((compElement) => { const compName = compElement.children[0].children[0].children[1].children[1] .children[0].innerText; console.debug(compName); compElementMappings[compName] = { element: compElement, display: compElement.style.display, }; if (!tierToCompMappings[tierName]) { tierToCompMappings[tierName] = []; } tierToCompMappings[tierName].push(compName); compToTierMappings[compName] = tierName; const tribeName = compName.split(" ")[0].toLowerCase(); if (!tribeToCompMappings[tribeName]) { tribeToCompMappings[tribeName] = []; } tribeToCompMappings[tribeName].push(compName); }); }); console.debug("tierToCompMappings:"); console.debug(tierToCompMappings); console.debug("compToTierMappings:"); console.debug(compToTierMappings); console.debug("tribeToCompMappings:"); console.debug(tribeToCompMappings); console.debug("tierElementMappings:"); console.debug(tierElementMappings); console.debug("compElementMappings:"); console.debug(compElementMappings); const checkboxOptionsContainer = document.createElement("div"); filtersContainer.appendChild(checkboxOptionsContainer); const allOrNoneContainer = document.createElement("div"); checkboxOptionsContainer.appendChild(allOrNoneContainer); const selectAllButton = document.createElement("button"); selectAllButton.textContent = "All"; selectAllButton.onclick = () => { checkboxElements.forEach(checkboxElement => { checkboxElement.checked = true; checkboxElement.dispatchEvent(new Event("change")); }) }; allOrNoneContainer.appendChild(selectAllButton); const deselectAllButton = document.createElement("button"); deselectAllButton.textContent = "None"; deselectAllButton.onclick = () => { checkboxElements.forEach(checkboxElement => { checkboxElement.checked = false; checkboxElement.dispatchEvent(new Event("change")); }) }; allOrNoneContainer.appendChild(deselectAllButton); const checkboxMemoryOptionsContainer = document.createElement("div"); checkboxOptionsContainer.appendChild(checkboxMemoryOptionsContainer); const memoryStore = []; const memoryStoreButton = document.createElement("button"); memoryStoreButton.textContent = "Save"; memoryStoreButton.onclick = () => { memoryStore.length = 0; checkboxElements.forEach(checkboxElement => { memoryStore.push(checkboxElement.checked); }); console.debug("Saved filter state to memory:", memoryStore); }; checkboxMemoryOptionsContainer.appendChild(memoryStoreButton); const memoryRestoreButton = document.createElement("button"); memoryRestoreButton.textContent = "Restore"; memoryRestoreButton.onclick = () => { checkboxElements.forEach((checkboxElement, index) => { checkboxElement.checked = memoryStore[index] || false; checkboxElement.dispatchEvent(new Event("change")); }); }; checkboxMemoryOptionsContainer.appendChild(memoryRestoreButton); const checkboxElements = []; tribes.forEach((tribe) => { const subContainer = document.createElement("div"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = `${tribe}-checkbox`; checkbox.checked = true; const label = document.createElement("label"); label.htmlFor = `${tribe}-checkbox`; label.textContent = `${tribe.charAt(0).toUpperCase() + tribe.slice(1)}`; label.style.color = "white"; subContainer.appendChild(checkbox); subContainer.appendChild(label); filtersContainer.appendChild(subContainer); checkboxElements.push(checkbox); checkbox.addEventListener("change", function () { const comps = []; [tribe, `${tribe}s`].forEach(spelling => { comps.push(...(tribeToCompMappings[spelling] || [])); }); console.debug(`comps to update: ${comps}`); if (checkbox.checked) { console.debug(`${tribe} is now checked!`); comps.forEach((comp) => { revealComp(comp); revealTier(compToTierMappings[comp]); }); } else { console.debug(`${tribe} is now unchecked!`); comps.forEach((comp) => { hideComp(comp); if (getAllCompElementsForTier(compToTierMappings[comp]).filter( (compElementWithSameTier) => compElementWithSameTier.style.display !== "none" ).length === 0) { hideTier(compToTierMappings[comp]); } }); } }); }); let compsMissingDetailedCompDataValue = 0; Object.entries(compElementMappings).forEach(([comp, { element }]) => { const url = element.children[0].href; console.debug(`Displaying cached "detailed comp" data for: ${comp} ${url}`); let detailedCompDataElement = element.querySelector('.detailed-comp-data'); let whenToCommitDataElement; let commonEnablersDataElement; if (!detailedCompDataElement) { detailedCompDataElement = document.createElement('div'); detailedCompDataElement.className = 'detailed-comp-data'; detailedCompDataElement.style.color = 'white'; element.appendChild(detailedCompDataElement); whenToCommitDataElement = document.createElement('div'); whenToCommitDataElement.className = 'when-to-commit-data'; whenToCommitDataElement.style.color = 'white'; detailedCompDataElement.appendChild(whenToCommitDataElement); commonEnablersDataElement = document.createElement('div'); commonEnablersDataElement.className = 'common-enablers-data'; commonEnablersDataElement.style.color = 'white'; detailedCompDataElement.appendChild(commonEnablersDataElement); } else { whenToCommitDataElement = detailedCompDataElement.querySelector('.when-to-commit-data'); commonEnablersDataElement = detailedCompDataElement.querySelector('.common-enablers-data'); } const whenToCommitValue = GM_getValue(`${url}-when-to-commit`, null); const commonEnablersValue = GM_getValue(`${url}-common-enablers`, null); if (whenToCommitValue === null || commonEnablersValue === null) { compsMissingDetailedCompDataValue++; } if (whenToCommitValue === null) { whenToCommitDataElement.textContent = `When to commit: No data available. Fetch data to view`; } else { whenToCommitDataElement.textContent = `When to commit: ${whenToCommitValue}`; } if (commonEnablersValue === null) { commonEnablersDataElement.textContent = `Common enablers: No data available. Fetch data to view`; } else { commonEnablersDataElement.textContent = `Common enablers: ${commonEnablersValue}`; } GM_addValueChangeListener(`${url}-when-to-commit`, function(key, oldValue, newValue, remote) { console.debug(`Value for ${key} changed from ${oldValue} to ${newValue}`); if (newValue === null) { whenToCommitDataElement.textContent = `When to commit: Unavailable`; } else { whenToCommitDataElement.textContent = `When to commit: ${newValue}`; } }); GM_addValueChangeListener(`${url}-common-enablers`, function(key, oldValue, newValue, remote) { console.debug(`Value for ${key} changed from ${oldValue} to ${newValue}`); if (newValue === null) { commonEnablersDataElement.textContent = `Common enablers: Unavailable`; } else { commonEnablersDataElement.textContent = `Common enablers: ${newValue}`; } }); }); if (compsMissingDetailedCompDataValue > 0) { if (window.confirm(`There are ${compsMissingDetailedCompDataValue} comp(s) missing "Detailed Comp Data" data. Would you like to fetch the data now?`)) { fetchMissingDetailedCompData(); } } // const clearWhenToCommitStorageContainer = document.createElement("div"); // clearWhenToCommitStorageContainer.style.display = "flex"; // clearWhenToCommitStorageContainer.style.gap = "20px"; // clearWhenToCommitStorageContainer.style.flexWrap = "wrap"; // container.prepend(clearWhenToCommitStorageContainer); // const clearWhenToCommitButton = document.createElement("button"); // clearWhenToCommitButton.textContent = 'Clear all "When to Commit" Data'; // clearWhenToCommitButton.onclick = () => { // Object.entries(compElementMappings).forEach(([comp, { element }]) => { // const url = element.children[0].href; // GM_setValue(`${url}-when-to-commit`, null); // }); // }; // clearWhenToCommitStorageContainer.appendChild(clearWhenToCommitButton); // const clearOneRandomCompWhenToCommitButton = document.createElement("button"); // clearOneRandomCompWhenToCommitButton.textContent = 'Clear "When to Commit" Data for one Random Comp'; // clearOneRandomCompWhenToCommitButton.onclick = () => { // const randomComp = Object.keys(compElementMappings)[Math.floor(Math.random() * Object.keys(compElementMappings).length)]; // const url = compElementMappings[randomComp].element.children[0].href; // GM_setValue(`${url}-when-to-commit`, null); // }; // clearWhenToCommitStorageContainer.appendChild(clearOneRandomCompWhenToCommitButton); const whenToCommitContainer = document.createElement("div"); whenToCommitContainer.style.display = "flex"; whenToCommitContainer.style.justifyContent = "space-between"; whenToCommitContainer.style.gap = "20px"; whenToCommitContainer.style.flexWrap = "wrap"; whenToCommitContainer.style.marginTop = "10px"; whenToCommitContainer.style.marginBottom = "10px"; container.prepend(whenToCommitContainer); const showWhenToCommitDataCheckboxSubContainer = document.createElement("div"); whenToCommitContainer.appendChild(showWhenToCommitDataCheckboxSubContainer); const showWhenToCommitDataCheckbox = document.createElement("input"); showWhenToCommitDataCheckbox.type = "checkbox"; showWhenToCommitDataCheckbox.id = "show-detailed-comp-data"; showWhenToCommitDataCheckbox.checked = true; showWhenToCommitDataCheckbox.addEventListener("change", function () { const detailedCompDataElements = document.querySelectorAll('div.detailed-comp-data'); if (showWhenToCommitDataCheckbox.checked) { console.debug("Showing 'Detailed Comp' data elements"); detailedCompDataElements.forEach((dataElement) => { dataElement.style.display = "block"; }); } else { console.debug("Hiding 'Detailed Comp' data elements"); detailedCompDataElements.forEach((dataElement) => { dataElement.style.display = "none"; }); } }); showWhenToCommitDataCheckboxSubContainer.appendChild(showWhenToCommitDataCheckbox); const showWhenToCommitDataLabel = document.createElement("label"); showWhenToCommitDataLabel.htmlFor = "show-detailed-comp-data"; showWhenToCommitDataLabel.textContent = 'Show "Detailed Comp" Data'; showWhenToCommitDataLabel.style.color = "white"; showWhenToCommitDataCheckboxSubContainer.appendChild(showWhenToCommitDataLabel); const fetchMissingWhenToCommitDataButtonSubContainer = document.createElement("div"); whenToCommitContainer.appendChild(fetchMissingWhenToCommitDataButtonSubContainer); const fetchMissingWhenToCommitDataButton = document.createElement("button"); fetchMissingWhenToCommitDataButton.textContent = 'Fetch "Detailed Comp" Data for Missing Comps'; const fetchMissingWhenToCommitDataWarningMessage = `This will open a new tab for each comp without "detailed comp" data, scrape data, and then close the tabs`; fetchMissingWhenToCommitDataButton.onclick = () => window.confirm("Are you sure you want to fetch 'Detailed Comp' data for all comps missing it? " + fetchMissingWhenToCommitDataWarningMessage) && fetchMissingDetailedCompData(); fetchMissingWhenToCommitDataButtonSubContainer.appendChild(fetchMissingWhenToCommitDataButton); const fetchMissingWhenToCommitDataButtonWarningMessage = document.createElement("span"); fetchMissingWhenToCommitDataButtonWarningMessage.textContent = "⚠"; fetchMissingWhenToCommitDataButtonWarningMessage.style.color = "yellow"; fetchMissingWhenToCommitDataButtonWarningMessage.title = fetchMissingWhenToCommitDataWarningMessage; fetchMissingWhenToCommitDataButtonSubContainer.appendChild(fetchMissingWhenToCommitDataButtonWarningMessage); const invalidateAndFetchWhenToCommitDataButtonSubContainer = document.createElement("div"); whenToCommitContainer.appendChild(invalidateAndFetchWhenToCommitDataButtonSubContainer); const invalidateAndFetchWhenToCommitDataButton = document.createElement("button"); invalidateAndFetchWhenToCommitDataButton.textContent = 'Fetch new "Detailed Comp" Data for ALL Comps'; const invalidateAndFetchWhenToCommitDataWarningMessage = `This will open a new tab FOR EACH COMP (${Object.keys(compElementMappings).length}), scrape data, and then close the tabs`; invalidateAndFetchWhenToCommitDataButton.onclick = () => { window.confirm("Are you sure you want to fetch new 'Detailed Comp' data for all comps? " + invalidateAndFetchWhenToCommitDataWarningMessage) && Object.entries(compElementMappings).forEach(([comp, { element }]) => { const url = element.children[0].href; console.debug(`Fetching data for: ${comp} ${url}`); GM_setValue(`${url}-when-to-commit`, null); GM_setValue(`${url}-common-enablers`, null); GM_setValue(`${url}-retrieve-detailed-comp-data`, true); GM_openInTab(url); }); }; invalidateAndFetchWhenToCommitDataButtonSubContainer.appendChild(invalidateAndFetchWhenToCommitDataButton); const invalidateAndFetchWhenToCommitDataButtonWarningMessage = document.createElement("span"); invalidateAndFetchWhenToCommitDataButtonWarningMessage.textContent = "⚠"; invalidateAndFetchWhenToCommitDataButtonWarningMessage.style.color = "red"; invalidateAndFetchWhenToCommitDataButtonWarningMessage.title = invalidateAndFetchWhenToCommitDataWarningMessage; invalidateAndFetchWhenToCommitDataButtonSubContainer.appendChild(invalidateAndFetchWhenToCommitDataButtonWarningMessage); function fetchMissingDetailedCompData() { Object.entries(compElementMappings).forEach(([comp, { element }]) => { const url = element.children[0].href; if ((GM_getValue(`${url}-when-to-commit`, null) === null) || (GM_getValue(`${url}-common-enablers`, null) === null)) { console.debug(`Fetching data for: ${comp} ${url}`); GM_setValue(`${url}-retrieve-detailed-comp-data`, true); GM_openInTab(url); } }); } function waitElement(root, selector) { return new Promise((resolve, reject) => { new MutationObserver(check).observe(root, { childList: true, subtree: true, }); function check(changes, observer) { let element = root.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } } }); } function waitElementWithInnerText(root, selector, innerText) { return new Promise((resolve, reject) => { new MutationObserver(check).observe(root, { childList: true, subtree: true, }); function check(changes, observer) { let element = Array.from(root.querySelectorAll(selector)).find(child => child.innerText === innerText); if (element) { observer.disconnect(); resolve(element); } } }); } function waitElements(root, selector, count) { return new Promise((resolve, reject) => { new MutationObserver(check).observe(root, { childList: true, subtree: true, }); function check(changes, observer) { let elements = root.querySelectorAll(selector); if (elements.length >= count) { observer.disconnect(); resolve(elements); } } }); } function getAllCompElementsForTier(tier) { return ( tierToCompMappings[tier]?.map( (comp) => compElementMappings[comp].element ) || [] ); } function getDisplayStyleForComp(comp) { return compElementMappings[comp]?.display; } function getDisplayStyleForTier(tier) { return tierElementMappings[tier]?.display; } function revealComp(comp) { console.debug(`Revealing comp: ${comp}`); const compElement = compElementMappings[comp].element; compElement.style.display = getDisplayStyleForComp(comp); } function hideComp(comp) { console.debug(`Hiding comp: ${comp}`); const compElement = compElementMappings[comp].element; compElement.style.display = "none"; } function revealTier(tier) { console.debug(`Revealing tier: ${tier}`); const tierElement = tierElementMappings[tier].element; tierElement.style.display = getDisplayStyleForTier(tier); } function hideTier(tier) { console.debug(`Hiding tier: ${tier}`); const tierElement = tierElementMappings[tier].element; tierElement.style.display = "none"; } })();