您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Find quests selected group members have yet to complete. Requires browser versions newer than ~June 2024
// ==UserScript== // @name Quest Finder // @namespace jasper.groupironmen.questfinder // @match https://groupiron.men/* // @grant GM_addStyle // @run-at document-idle // @version 2.0 // @author JasperV // @description Find quests selected group members have yet to complete. Requires browser versions newer than ~June 2024 // @license MIT // ==/UserScript== // Possible Improvements: // - turn all snake_case into camelCase (at least be consistent bro) // - show result count // - on hover over a quest, show each player's status // - refactor code, especially into different files // - checkbox for filtering out quests that player doesn't have required skills for // - check skill requirements // - check if completed required quests // unique identifier used for css classes const id = "questfinder" //#region ENUMs const QuestStatus = { NOT_STARTED: 'Not Started', IN_PROGRESS: 'In Progress', COMPLETED: 'Completed' } /** * Represents the difficulty levels of quests * @typedef {Object} QuestDifficulty * @property {number} rank sorting order * @property {string} name The name of the difficulty level * @property {string} icon The URL to the icon. Both used to get the difficulty from the site's quest list, * and to show the icon again in the quest finder list. */ const QuestDifficulty = { NOVICE: { rank: 1, name: "Novice", icon: "/icons/3399-0.png" }, INTERMEDIATE: { rank: 2, name: "Intermediate", icon: "/icons/3400-0.png" }, EXPERIENCED: { rank: 3, name: "Experienced", icon: "/icons/3402-0.png" }, MASTER: { rank: 4, name: "Master", icon: "/icons/3403-0.png" }, GRANDMASTER: { rank: 5, name: "Grandmaster", icon: "/icons/3404-0.png" } } const SortOption = { ALPHABETICAL: 'Alphabetical', DIFFICULTY: 'Difficulty' } //#endregion //#region Models class Player { constructor(name, panelElement) { this.name = name // unique identifier this.panelElement = panelElement // reference to the player's panel element in the DOM. used for data-gathering // keep track of the player's quests by status for easy filtering this.quests = { [QuestStatus.NOT_STARTED]: new Set(), [QuestStatus.IN_PROGRESS]: new Set(), [QuestStatus.COMPLETED]: new Set() } } } /** * Represents a quest with its details and player statuses. * @typedef {Object} Quest * @property {string} name unique identifier: name of the quest * @property {string} wiki_url The URL to the quest's runescape.wiki page * @property {string, QuestStatus} playerStatuses key: player name, value: quest status for this quest */ const Quest = { name: "", wiki_url: "", difficulty: QuestDifficulty.NOVICE, } //#endregion /** * Represents the filters the user has currently selected in the UI, * which will be applied to the quest list */ const Filters = { playersNotStarted: [], // players that must not have started a quest for it to be included playersCompleted: [], // players that must have completed a quest for it to be included sort: SortOption.ALPHABETICAL, unfinished: false, countInProgressAsCompleted: false, removePlayer(player) { this.playersNotStarted = this.playersNotStarted.filter(p => p !== player) this.playersCompleted = this.playersCompleted.filter(p => p !== player) } } //#region quest management const QuestManager = { // key = quest name, value = Quest object quests: new Map(), /** * @param {string} name * @returns {Quest | null} the quest with the given name, or null if not found */ GetQuest(name) { return this.quests.get(name) || null }, /** * Register a new quest. * First use GetQuest() to ensure doesnt already exist * @param {string} name * @param {string} wiki_url * @param {QuestDifficulty} difficulty * @returns {Quest} the newly created quest */ AddQuest(name, wiki_url, difficulty) { const quest = Object.create(Quest) quest.name = name quest.wiki_url = wiki_url quest.difficulty = difficulty quest.statuses = {} this.quests.set(name, quest) return quest } } /** * Get all quests that match the given criteria * @param {Player[]} notStartedPlayers players that must not have started the quest * @param {Player[]} completedPlayers players that must have completed the quest * @param {boolean} countInProgressAsCompleted if true, in-progress quests are counted as finished * @returns {Set<Quest>} a set of matching quests */ function getMatchingQuests(notStartedPlayers, completedPlayers, countInProgressAsCompleted) { if(completedPlayers.length === 0 && notStartedPlayers.length === 0) return new Set() const notStartedQuests = (player) => countInProgressAsCompleted ? player.quests[QuestStatus.NOT_STARTED] : player.quests[QuestStatus.NOT_STARTED].union(player.quests[QuestStatus.IN_PROGRESS]) let result = null for(const player of notStartedPlayers) { const playerNotStartedQuests = notStartedQuests(player) if(result === null) result = new Set(playerNotStartedQuests) else result = result.intersection(playerNotStartedQuests) } for(const player of completedPlayers) { const playerCompletedQuests = player.quests[QuestStatus.COMPLETED] if(result === null) result = new Set(playerCompletedQuests) else result = result.intersection(playerCompletedQuests) } return result } //#endregion //#region DOM data-gathering /** * Set the completion status of each quest for a given player in the QuestManager. * From the player's panel element in the DOM. * @param {Player} player */ function UpdateQuestStatusesForPlayer(player) { const playerPanelElement = player.panelElement // get the currently active tab so we can restore it later const activeTab = playerPanelElement.querySelector('.player-panel__tab-active') let openOldActiveTabAfter = (activeTab != null) // if no tab was open, the quests tab must be closed after instead let closeTabAfter = (activeTab === null) // reset player's quest data for(const status of Object.values(QuestStatus)) player.quests[status] = new Set() // open Quests tab in UI (it's dynamically loaded by the site, so it must be clicked for us to see the data) const questsButton = playerPanelElement.querySelector('button[data-component="player-quests"]') if(questsButton != activeTab) questsButton.click() // quest tab is already active, so it can stay open else openOldActiveTabAfter = false // get all quests from list // and store in Player.quests the completion status of each const questList = playerPanelElement.querySelector('.player-quests__list') const questLinkElements = questList.getElementsByTagName('a') for(const questLinkElement of questLinkElements) { const questElement = questLinkElement.querySelector('.player-quests__quest') const questName = questElement.textContent.trim() // .replace(/\s+/g, ' ') // the site gives different CSS classes // depending on the status of the player's quest (completed, in progress or completed) let questStatus if(questElement.classList.contains('player-quests__not-started')) questStatus = QuestStatus.NOT_STARTED else if(questElement.classList.contains('player-quests__in-progress')) questStatus = QuestStatus.IN_PROGRESS else if(questElement.classList.contains('player-quests__finished')) questStatus = QuestStatus.COMPLETED // create a quest object with info about the quest // or if one already exists for this quest, get that instead let quest = QuestManager.GetQuest(questName) if(!quest) { const wiki_url = questLinkElement.getAttribute('href') const difficultyIcon = questElement.querySelector('.player-quests__difficulty-icon').getAttribute('src') const difficulty = Object.values(QuestDifficulty).find(difficulty => difficulty.icon === difficultyIcon) || QuestDifficulty.NOVICE quest = QuestManager.AddQuest(questName, wiki_url, difficulty) } // add quest to player's quest list player.quests[questStatus].add(quest) } // restore old tab if(openOldActiveTabAfter) activeTab.click() if(closeTabAfter) questsButton.click() } /** * get all players in the group from the site's DOM * and create a Player object for them * @returns {Player[]} */ function getPlayers() { const panels = document.querySelectorAll('player-panel') const players = [] panels.forEach(panelElement => { const name = panelElement.getAttribute('player-name') const player = new Player(name, panelElement) players.push(player) }) return players } //#endregion //#region Update Quest List /** * Updates the quest list UI based on the currently selected filters and players * @param {Element} quests_container the element which the list will be added to */ function updateQuestList(quests_container) { const questsSet = getMatchingQuests(Filters.playersNotStarted, Filters.playersCompleted, Filters.countInProgressAsCompleted) // helper to strip leading "A ", "An ", "The " for sorting (case-insensitive) function stripLeadingArticle(name) { return name.replace(/^(a |an |the )/i, '').trim() } // sort alphabetically let sortedQuestsArray = [...questsSet].sort((a, b) => stripLeadingArticle(a.name).localeCompare(stripLeadingArticle(b.name)) ) // apply other sort if selected if(Filters.sort === SortOption.DIFFICULTY) sortedQuestsArray = sortedQuestsArray.sort((a, b) => a.difficulty.rank - b.difficulty.rank) populateQuestList(sortedQuestsArray, quests_container) } /** * Creates a list of quests using the provided set and appends it to the container in the DOM * @param {Quest[]} quests quests to put in container * @param {HTMLElement} container container to place quests in */ function populateQuestList(quests, container) { // clear quest list while(container.firstChild) container.removeChild(container.firstChild) // show informative text when no quests match if(quests.length === 0) { const message = document.createElement("div") if(Filters.playersNotStarted.length === 0 && Filters.playersCompleted.length === 0) message.textContent = "Select players to compare" else message.textContent = "No matching quests found" container.appendChild(message) } // create a quest entry in the list for each given quest for(const quest of quests) { const link = document.createElement("a") link.target = "_blank" link.href = quest.wiki_url const element = document.createElement("div") element.classList.add(`${id}__quest__item`)//, `${id}__quest__${quest.status.toLowerCase()}`) link.appendChild(element) // Difficulty icon const difficultyImage = document.createElement("img") difficultyImage.classList.add(`${id}__quest__difficulty-image`) difficultyImage.src = quest.difficulty.icon difficultyImage.alt = quest.difficulty.name difficultyImage.title = quest.difficulty.name element.appendChild(difficultyImage) const nameSpan = document.createElement("span") nameSpan.textContent = quest.name element.appendChild(nameSpan) container.appendChild(link) } } //#endregion //#region UI Panel Creation /** * @param {Player[]} players */ function createUIPanel(players) { const container = document.createElement("div") container.id = id container.classList.add(`${id}__window`, `${id}-dark-background`, `${id}-border`) // these elements are added to body later, but created here so they can be referenced const body = document.createElement("div") const quests_container = document.createElement("div") // container for list of matching quests // header const header = createHeader(players, body, quests_container) container.appendChild(header) body.classList.add(`${id}__body`) container.appendChild(body) // title const playersTitle = document.createElement("h4") playersTitle.textContent = "Select players to compare" body.appendChild(playersTitle) // checkboxes to toggle players const players_container = createPlayerSelection(players, quests_container) body.appendChild(players_container) // title const questsTitle = document.createElement("h4") questsTitle.textContent = "Matching quests" body.appendChild(questsTitle) // sort by const sortContainer = createSortSelect(quests_container) body.appendChild(sortContainer) // exclude in-progress quests const excludeCheckbox = createExcludeInProgressCheckbox(quests_container) body.appendChild(excludeCheckbox) // list of matching quests quests_container.classList.add(`${id}__quest-list`) body.appendChild(quests_container) document.body.appendChild(container) updateQuestList(quests_container) makeDraggable(container, header) } /** * Creates the header for the quest finder UI * @param {Player[]} players * @param {Element} body body to hide when the minimise button is clicked * @param {Element} quests_container container for the list of matching quests */ function createHeader(players, body, quests_container) { const header = document.createElement("div") header.classList.add(`${id}__header`) const title = document.createElement("h1") title.classList.add(`${id}__header__title`) title.textContent = "Quest Finder" header.appendChild(title) // manual refresh button const refreshButton = document.createElement("button") refreshButton.classList.add(`${id}__header__button`, 'men-button') refreshButton.textContent = "↻" refreshButton.title = "Update quest data" refreshButton.addEventListener("click", () => { players.forEach(player => { UpdateQuestStatusesForPlayer(player) }) updateQuestList(quests_container) }) header.appendChild(refreshButton) // minimise button const minimiseButton = document.createElement("button") minimiseButton.classList.add(`${id}__header__button`, 'men-button') minimiseButton.textContent = "−" minimiseButton.addEventListener("click", () => { body.classList.toggle("hidden") if(body.classList.contains("hidden")) { minimiseButton.textContent = "+" refreshButton.classList.add("hidden") } else { minimiseButton.textContent = "−" refreshButton.classList.remove("hidden") } }) header.appendChild(minimiseButton) return header } /** * Creates a container for player selection checkboxes * @param {Player[]} players * @param {Element} quests_container container element which the quest list will be added to */ function createPlayerSelection(players, quests_container) { const players_container = document.createElement("div") players_container.classList.add(`${id}__player-list`) for (let i = 0; i < players.length; i++) { const player = players[i] const playerContainer = document.createElement("div") playerContainer.classList.add(`${id}__player`) // checkbox const checkbox = document.createElement("input") checkbox.type = "checkbox" checkbox.id = `${id}__player-checkbox__${i}` checkbox.value = player.name checkbox.checked = false playerContainer.appendChild(checkbox) // label const label = document.createElement("label") label.textContent = player.name label.htmlFor = checkbox.id playerContainer.appendChild(label) // dropdown with {finished, not started} const filterSelect = document.createElement("select") filterSelect.classList.add("hidden") playerContainer.appendChild(filterSelect) const notStartedOption = document.createElement("option") notStartedOption.value = "not_started" notStartedOption.textContent = "Not Started" filterSelect.appendChild(notStartedOption) const finishedOption = document.createElement("option") finishedOption.value = "finished" finishedOption.textContent = "Finished" filterSelect.appendChild(finishedOption) // on dropdown changed, set filter & update the quest list filterSelect.addEventListener("change", () => { Filters.removePlayer(player) if(checkbox.checked) { if(filterSelect.value === "finished") Filters.playersCompleted.push(player) else Filters.playersNotStarted.push(player) } else Filters.removePlayer(player) updateQuestList(quests_container) }) // on checkbox change, toggle status checkbox visibility checkbox.addEventListener("change", () => { if(!checkbox.checked) filterSelect.classList.add("hidden") else filterSelect.classList.remove("hidden") // trigger filterSelect change event, // to let it handle setting the filter and updating the quest list filterSelect.dispatchEvent(new Event("change")) }) players_container.appendChild(playerContainer) } return players_container } function createSortSelect(quests_container) { const sortContainer = document.createElement("div") sortContainer.classList.add(`${id}__sort`) const sortLabel = document.createElement("label") sortLabel.htmlFor = `${id}__sort-select` sortLabel.textContent = "Sort by:" sortContainer.appendChild(sortLabel) const sortSelect = document.createElement("select") sortSelect.id = `${id}__sort-select` sortSelect.classList.add(`${id}__sort-select`) sortContainer.appendChild(sortSelect) // add options based on SortOption enum for(const [key, value] of Object.entries(SortOption)) { const optionElement = document.createElement("option") optionElement.value = key optionElement.textContent = value sortSelect.appendChild(optionElement) } // update the filter and automatically refresh the quest list when the user changes selection sortSelect.addEventListener("change", () => { Filters.sort = SortOption[sortSelect.value] updateQuestList(quests_container) }) return sortContainer } /** * Creates a checkbox to exclude in-progress quests from the quest list * @param {Element} quests_container * @returns {Element} */ function createExcludeInProgressCheckbox(quests_container) { const container = document.createElement("div") container.classList.add(`${id}__exclude-in-progress`) const checkbox = document.createElement("input") checkbox.type = "checkbox" checkbox.id = `${id}__exclude-in-progress__checkbox` checkbox.checked = false checkbox.addEventListener("change", () => { Filters.countInProgressAsCompleted = checkbox.checked updateQuestList(quests_container) }) container.appendChild(checkbox) const label = document.createElement("label") label.textContent = "Consider In-Progress quests as Finished" label.htmlFor = checkbox.id container.appendChild(label) return container } /** * Makes an element draggable * @param {Element} draggedElement The element to move when dragging * @param {Element} handle The element that the user can mousedown on to drag the element */ function makeDraggable(draggedElement, handle) { let isDragging = false let offsetX = 0 let offsetY = 0 handle.addEventListener('mousedown', (e) => { isDragging = true offsetX = e.clientX - draggedElement.getBoundingClientRect().left offsetY = e.clientY - draggedElement.getBoundingClientRect().top document.body.style.userSelect = 'none' // prevent text selection while dragging }) document.addEventListener('mousemove', (e) => { if(isDragging) draggedElement.style.inset = `${e.clientY - offsetY}px auto auto ${e.clientX - offsetX}px`; }) document.addEventListener('mouseup', () => { isDragging = false document.body.style.userSelect = '' // re-enable text selection }) } function loadCSS() { GM_addStyle(` .hidden { display: none !important; } .${id}-dark-background { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAAA8BAMAAAAQ3mBLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAwUExURSQkJCUlJSYmJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPTtHbMAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIZSURBVEjHhZbZgcQgCEBJB2IH0n+RG7lBM5uPGY8ncidgz4NI8uDYk/1MWViyhRsb+xcdXgesIxSRKLsBY5fMsGJP7LxrDvvKHi6DscDQlmS4fGO5mCX3NDXeIRr8nlOWLpctm0PWgDcgYKKk9QlvV8DBmuwDBmiOS6LZ0TMtBpxYFV1h1uIGC8176a4vmGmokdrsbzhih5p/h4G6W7yZBXd4eypbVwR3Pd4H8slR4fkThqzFRXSGh8LzSzTDi1dRa3FGnt3gqDFQg++iFUZT+RFprkjm0eCpKj9THXPo/VpiMKrjUB0zWgVI1tlo9xZPqh2eD3jJGka2bthazR3ese9wWtlrBnNj8C2ND2R4BCydwexD65cYFo8sWZuJLLzesOCL4AK7VyhqmR3o8KsHWS8zdTwzRooKw2hwCJzRrgr8jhLsUcADXgwTWGo0eXFqmpyZkr9eXjyvZ2tZHfcLJD1/FphOywweFzjwoZnNh9D83WD3sZVBtpegV2X1mc41PtCrct1gTe1/4aUU/61Lt3QlFbYaA7w14qrHyDAekiWrbQ4Fnh0uc39zEOCx6drq3Frr/IA1HgWeRIBdD5vrVwaNaJeAXbRVpMLctbVdnu8xm9sFIXjDT1XEfFxgUng0Gy0iK+DHYSgNsFSAwXYvw1DiKF9UuwJJYP8S4G6/YU32As8LzFq5YgqTvsRWqEzeLDOsNiwqMNEfNgDswkZ+o/EAAAAASUVORK5CYII=) !important; } .${id}-light-background { background-color: #424343; /* background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAAA8CAYAAADi8H14AAAEbklEQVR4nOWWQXLbMAxFG49P1H23vf+JmuZlBgkMEyRAgpLcvhlENCUCn18w47ffv36+/1jk9vb28feRt9vzXIT3P99y/rx/j6FVJ0KFFmGUS9aI9ttHPCCb4Ep4cI+oRMRdAU+LN+/xYLAYJldgTGj0Zz3eyVF1hFGnjkAvcf8Yfw4ijJ5bEZXtjCizmnp6uNfKy7zloYN7jMyt5usM+6hLzNLadAXRvLes+NabA28+gic2q62S7H68PXweEVGkqFxJKuNZyKGRzn0FrPYWIYM9E735KFrgLmNXNOq1WmuGkMHVWLEtc1ePB21OBeSzuiOE/8ntomXuKpixg5m899YGV7unx0wXZJgxIQP5M3s4tIOtsNbLfQUwmYjwdAbb7o0mslgz/0XEm95e79bQI3jVzp2he0TIG6okYu7KS+910y56Pj0dEb2HV9lt7plo3/RLfjBYP7SCLiAcaS71q/Yyg6591x92ETG3mrNNFrpncAVRc6u6V4PJZ7PV4Ki5HhUdeLbJD2ewh4iMbFiezZhru1fX0WPJnYV1Os+RDDsYcQJjohJtLiYQHr17I6p1Rwl1sAWxrc0yn8GauxvRRy0ZA5+rkfzDDvaQBC0yxwNEN2hrZusINk8FkpMrIUx1sEAizOGaRbqX9StgsuSaBf1ZHayxtOamO1iTEYcZRBYrHmMFPb4a3Q62m+ohJrMGA1ubZl4ja6qwNW29MyjpYI2YxuYIQY/PhJdAWGiMSqhBLBs8EoaxRCUIt4zmGBNHoOssGwyYTAjSxT0izwg6N2ReGJslRqzoIT/RosRgxBEa+9liRWbRJjOWmCWqJ/qcsGxwz8jePYiI7T2DoUQP7ksIerwD8hPg/orobUyoNLCVS+5lYGN8XblaWnM7od50B7cM0WTNyT7fg40dTasmHk0b7IFRxAx6nR5fhYwmzAX3iOghiy0ZAR4VOUA0VuXr5bHdK7XBNVg/9Gpo7Yx75ozIrqWepvSIIDlxNaKarJn2M/APVMLSquN28Aq2UEvoK9IydcQWgy3VhnsbtWehRjSManOfZ7l69OpYSo+IKGyAOAPqEj08czGW8Gitu9vJUfFKqGXrR+htMgq1IVOfb86ots331MH2gd3IRq8O5oJcozwZ/L+RfcGjDrbcsgV2UK2h8ltIxxKz3FpiWnO7OdNkr/aKsYL7M00EesWrkXo9olrIFX3Wo8JcGJ7BiL0CGcMyz7aoMBcNxNBguIrJUVb0VpkrhAy+AlHT5DmuxJFgLKFxz2ALYu3iq4FGC3NR3dnujeRNdTBidxERu6t+NK/+DRzRCymDATFENdGcPEdYWnMZRt07Yy48GTwqJKxuaJWz6mfMhQeDxVy5jjhrkwL1CchuPIPu3ixfBltT7WcPNkissmIQ9YkdaHNnNA5/RVijdUGNbHBGhMBayaPRGlr1WafROey9DK1aWW6IJ2ZhrQTozV2Bs/V8HREtxDRNa85y5KZaHcocUclsvuERYan42mTp1ZSX6RngzR/FX4+QgWqKX+gZAAAAAElFTkSuQmCC) !important; */ } .${id}-border { border-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAAkJCQ4ODhAQEBMTExYWFhcXFxkZGRwcHB0dHR8fHyAgICIiIiUlJSYmJicnJykpKSoqKisrKy0tLS8vLzExMTQ0NDU1NTc3Nzw8PEFBQUJCQkNDQ0ZGRkhISAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANbpKagAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7CAAAOwgEVKEqAAAADhklEQVRoQ+1a7ZKiMBD0xIWQBIEsruLqvf9bej2TD+G8Ov/YV3VVjkpwJqSZkKQnOpvbhirS/PdZxIYwTkff90M/jP1+CAElNOO0Hz8PIRyKdW1fKVOJ+lYbnQXjez4dIbVpjPfGGt8aY/BpGmjqRr5F8U7Uv9vlkrsyl6aWNr8OHQDOp+slhNqgos1mEbnOWCghcojWB/taGUupX4dwvXgrAEdfhxo1W7TirJcGIS0knnloo6BsH+xLZS5F6uDrBID7d875zkMc3g5vfHzXJZVDGU9hfrQvlKnU+vAhA9R+W23xlte22sVyW1XVbofDtvrAMVkf7GtlKavK+92QAGrbVbeXSzDtTgEs+t8TAA540j/Q/ibghAGg7fYAOGBYcboIADIPPo117iNpXyiDbyNA3wJhl7QvlB7z2nsAKBKhiwbMawVQpG3SvlDQrnXqgSAxALAiKYAiEQC8MVhNBECQCABHLIb6DBSJ8JAnLLbqgSIRPBgx/NUDRSKtRZ14oEgEDxZrEZCYaxFrNdVFVDxgAegiGrSLOMv1mLuItVzvMXZ0qVAkggdT9kCRWBNNABSJ4MGpSR4oEqOLJFoZAKBIlFGE0SkAikTg5LFp4kRTJAKAMGVc7ASJ0EV9AyoWAEUiAITGtpGTBYkBkMMWRSJMNPRMjCoUiQCwl4gxPQNKVFECL0UiTTQdRazAqzwDRWLMA+kZWSpYz+C9P3gqZZgqEgNAuj558J/uDxx7f2AT6R9lFBE80AksHkyeQ/r3/YEgETx47w+eynt/8FTe+4On8u/2ByzCKbEpPS5iEU6JTVmEU0ifSjg6D0iEU54Bi3BKbMoinBKbsvhAI0bpIhZAiU1ZhFOGKYtwym/XLMIpsSmLcHQCiwcswtEJzCScEpuyCKfEpjzCwY1LZMciHAUQD1iEU2JTFuGU2JT1g1SJTVk/SJXY9OeFF7ZMQjjn48wKW+b8l/vs+36VOLFInTg9SawYp6UJhmwf7Vz+0w+zNTmtAjy0Sp1YJlascyhiYgVeqnOpXlvsc8gA10uYMWjviROprjTXQA2Rwx8SKxq5amFSiae4/5xX8SUJCrOmScQciZwdkVMnniRWRCmmZJf7r50AzAO42fsJ7JByJFapE/Ltb4kVoi4mMUT7FFUtADZe72+QxrQmCnyJV3a9NhJ6nCTdyjpAFiYt1T6oG9I+N/1nc/sFyCqLOrY9UXkAAAAASUVORK5CYII=) 32 32/32px/4px round; } #${id} h4 { margin-top: 8px; margin-bottom: 4px; color: var(--orange); font-size: 20px; font-weight: 400; } .${id}__window { position: fixed; z-index: 9999; bottom: 16px; right: 16px; width: 320px; padding: 8px; } .${id}__header { display: flex; align-items: center; } .${id}__header__title { font-size: 1em; flex-grow: 1; margin: 0; } .${id}__header__button { margin-left: 8px; } .${id}__player-list { display: flex; flex-direction: column; } .${id}__player { padding-top: 4px; padding-bottom: 4px; display: flex; justify-content: space-between; } .${id}__sort-select { margin-left: 4px; margin-bottom: 8px; } .${id}__exclude-in-progress { margin-bottom: 8px; } .${id}__quest-list { overflow: hidden; overflow-y: scroll; max-height: 50vh; } .${id}__quest__item:hover { background: rgba(255,255,255,.1); } .${id}__quest__difficulty-image { margin-right: 4px; } `) } //#endregion //#region Initialisation let QUEST_FINDER_INITIALISED = false function init() { if(QUEST_FINDER_INITIALISED) return QUEST_FINDER_INITIALISED = true const players = getPlayers() players.forEach(player => { UpdateQuestStatusesForPlayer(player) }) createUIPanel(players) loadCSS() } // wait until site is finished loading before initialising const loadingScreen = document.getElementsByTagName("loading-screen")[0] const observer = new MutationObserver(() => { // loading is done when the <loading-screen> gets style="display:none" if(loadingScreen.style.display === 'none') { // make sure it's the https://groupiron.men/group that's loaded and not another if(window.location.href.startsWith("https://groupiron.men/group")) { init() observer.disconnect() } } }) if(loadingScreen.style.display === 'none') init() else observer.observe(loadingScreen, { attributes: true }) //#endregion