Quest Finder

Find quests selected group members have yet to complete. Requires browser versions newer than ~June 2024

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Advertisement:

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

Advertisement:

// ==UserScript==
// @name        Quest Finder
// @namespace   jasper.groupironmen.questfinder
// @match       https://groupiron.men/*
// @grant       GM_addStyle
// @run-at      document-idle
// @version     2.1
// @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 = QuestStatus.NOT_STARTED
        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