Quest Finder

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