Greasy Fork

Greasy Fork is available in English.

SB-AUI

Advanced UI for Starblast with extra features

当前为 2023-08-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         SB-AUI
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Advanced UI for Starblast with extra features
// @author       Halcyon
// @license      All rights reserved, this code may not be reproduced or used in any way without the express written consent of the author.
// @match        https://starblast.io/
// @icon         https://i.ibb.co/1QgnHfK/aui.png
// @grant        none
// ==/UserScript==

/**
 * CHANGELOG
 * 1.0.0 - Initial creation. Core of AUI
 * 1.1.0 - Added team evaluation (PBS, PPBS, DPS)
 * 1.2.0 - Optimizations, code cleanup and transition to a component-based system
 * 1.2.1 - More optimizations and code cleanup. Added hardElementRefresh
 * 1.2.2 - Tested component relationships (they work), added loading animations and some debugging
 * 1.2.3 - GreasyFork is begging me to update the version number
 * 1.2.4 - (Mateo) - Added user search
 * 1.2.5 - XSS prevention (username sanitization) and user search results update
 * 1.3.0 - Added prop components - Useful for building elements from a template. Uses custom syntax - ||variable||. Lists transition to prop components still WIP
 */

'use strict';

const API_LINK = "https://starblast.dankdmitron.dev/api/simstatus.json";
const CURRENT_RUNNING_VERSION = "1.3.0"

/********* STYLING ************ */

const applyCSS = (styles, element) => {
    if (!element) {
        return;
    }
    for (let key of Object.keys(styles)) {
        try {
            element.style[key] = styles[key]
        } catch (ex) {console.error(`Object.prototype.applyStyles: Cannot apply style '${key}' to ${element}`)}
    }
    return;
}

const applyBaseStyles = (element, includeFont = true) => {
    element.style.boxShadow = "black 0px 0px 0px";
    element.style.textShadow = "black 0px 0px 0px";
    if (includeFont) {
        element.style.fontFamily = `"DM Sans", sans-serif`;
    }
    element.style.fontWeight = "400";
    element.style.background = "#0b0b0b";
    element.style.border = "1px solid #1a1a1a"
    element.style.color = "#FFF";
    element.style.borderRadius = "10px";
    element.classList.add("hover-class");
}

//Remove unneeded UI elements (spotify and socials columns)
let removalQueries = ['[data-translate-base="music"]','[data-translate-base="community"]'];
for (let query of removalQueries) {
    for (let el of document.querySelectorAll(query)) {
        el.style.display = "none"
    }
}

//Setting the AUI logo
try {
    document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png";
} catch (ex) {
    try {
        setTimeout(() => {
            document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png";
        }, 500)
    } catch (ex) {
        setTimeout(() => {
            document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png";
        }, 1000)
    }
}


//Importing DM Sans and Abel
try {
    var styleElement = document.createElement('style');
    var importRule = `
        @import url('https://fonts.googleapis.com/css2?family=Abel&family=DM+Sans:wght@400;500;700&display=swap');
    `;
    styleElement.textContent = importRule;
    document.head.appendChild(styleElement);
    document.addEventListener("DOMContentLoaded", function() {
        var elementsToStyle = document.querySelectorAll("body");

        elementsToStyle.forEach(function(element) {
            element.style.fontFamily = '"DM Sans", sans-serif';
        });
    });
} catch (ex) {}

//All buttons get base styles
for (let el of document.querySelectorAll('button')) {
    applyBaseStyles(el)
}

//Scrollbar code
document.documentElement.style.scrollbarWidth = 'thin';
document.documentElement.style.msOverflowStyle = 'none';
var style = document.createElement('style');
var css = `
/* Webkit and Blink */
::-webkit-scrollbar {
    width: 0.2em;
}
::-webkit-scrollbar-thumb {
    background-color: #1a1a1a;
    border: none;
    border-radius: 0.1em;
}
::-webkit-scrollbar-track {
    background-color: transparent;
    border: none;
}
/* Firefox */
scrollbar-width: thin;
scrollbar-color: transparent transparent;
`;
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
document.querySelector("body").style.backgroundColor = "#0b0b0b";


//Overlay styles
applyCSS({
    backgroundColor: "#1a1a1a",
    background: "repeating-linear-gradient(45deg, #1a1a1a 0, #131313 1px, #0b0b0b 0, #0b0b0b 50%)",
    backgroundSize: "10px 10px",
    maxWidth: "calc(100% - 60px)",
    maxHeight: "calc(100% - 60px)",
    margin: "auto auto",
    boxSizing: "content-box",
    boxShadow: "black 0px 0px 0px",
    border: "6px solid #131313",
    outline: "54px solid #0b0b0b",
}, document.querySelector("#overlay"));


//Body styles
document.querySelector("body").style.height = "100dvh";
document.querySelector("body").style.width = "100vw";

//Play button styles
applyCSS({
    fontFamily: `"DM Sans", sans-serif`,
    letterSpacing: "4px",
    fontSize: "2.2rem",
    fontWeight: "600"
}, document.querySelector("#play"))

//Styles
applyCSS({
    background: `transparent`,
    textShadow: `black 0px 0px 0px`,
    fontFamily: `'Abel', sans-serif`,
    fontSize: `1rem`,
    letterSpacing: `0px`,
    marginTop: `5px`,
    marginLeft: `auto`,
    marginRight: `auto`,
    width: `80%`,
    borderTop: `1px solid #1a1a1a`,
    color: `gray`
},document.querySelector("#game_modes"))

//Changelog styles
document.querySelector(".changelog-new").style.fontFamily = `"DM Sans", sans-serif`;


//Tools like "modding" styles (4 buttons)
document.querySelector('.followtools').style.left = '0';
document.querySelector('.followtools').style.width = 'max-content';
document.querySelector('.followtools').style.zIndex = '500';

//Changelog to top
document.querySelector('.bottom-left').style.top = '0';
document.querySelector('.bottom-left').style.height = 'max-content';

//Name input styles
applyCSS({
    background: '#0b0b0b',
    border: '1px solid #1a1a1a',
    fontFamily: 'DM Sans',
    boxShadow: 'black 0px 0px 0px',
    borderRadius: '10px',
},document.querySelector('.inputwrapper'))

//Buttons for switching modes styles
const leftRight = [document.querySelector('#prevMode'),document.querySelector('#nextMode')];
for (let el of leftRight) {
    el.style.color = '#FFFFFF';
    el.style.textShadow = 'black 0px 0px 0px';
}

//Elements to apply baseStyles to
const baseStyleQueries = ['.changelog-new', '#moddingspace', "#donate", "#rankings", "#training"];
for (let query of baseStyleQueries) {
    applyBaseStyles(document.querySelector(query));
}

//Styles for button elements
const ml = ['#moddingspace', "#donate", "#rankings", "#training"]
for (let query of ml) {
    let item = document.querySelector(query);
    item.style.paddingBottom = "0.5rem";
    let icon = document.querySelector(`${query} > i`);
    icon.style.margin = "0.5rem auto 0.5rem auto";
    icon.style.paddingBottom = '0.5rem';
    icon.style.width = '80%';
    icon.style.borderBottom = '1px solid #1a1a1a';
    let span = document.querySelector(`${query} > span`);
    span.style.color = "#FFF";
    span.style.letterSpacing = '1px';
    span.style.textShadow = 'black 0px 0px 0px';
    span.style.fontWeight = '500';
}

for (let el of document.querySelectorAll('.modal')) {
    applyBaseStyles(el);
}

for (let el of document.querySelectorAll('.social i')) {
    applyBaseStyles(el, false);
}
/***********=/STYLING********** */

//Create SL INTEGRATION
var J = document.createElement("div");
J.id = "SL_INTEGRATION";
document.querySelector('#overlay').appendChild(J);

const SL_INTEGRATION = document.querySelector('#SL_INTEGRATION');
applyCSS({
    position: 'absolute',
    height: '100%',
    width: '25%',
    top: '0',
    right: '0',
    display: 'flex',
    flexDirection: 'column'
}, SL_INTEGRATION)

const templateStatusData = () => ({name: "",id: "",team_1: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []},team_2: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []},team_3: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []}})

//All variables used in the components should be declared here
window["COMPONENT_STATE_VALUES"] = {
    listingLoading: true,
    options: {
        activePanel: "listing",
        activeRegion: "europe",
        modes: {
            team: true,
            survival: false,
            deathmatch: false,
            modding: false,
            invasion: false
        }
    },
    userSearch: {
        active: false,
        loading: false,
        input: "",
        results: {},
        systemsQueried: 0,
    },
    filteredSystems: [],
    statusReportActive: false,
    statusReportLoading: false,
    statusReportData: {
        name: "",
        id: "",
        team_1: {
            hue: null,
            gems: 0,
            level: 0,
            potentialOutput: 0,
            PBS: 0,
            PPBS: 0,
            players: []
        },
        team_2: {
            hue: null,
            gems: 0,
            level: 0,
            potentialOutput: 0,
            PBS: 0,
            PPBS: 0,
            players: []
        },
        team_3: {
            hue: null,
            gems: 0,
            level: 0,
            potentialOutput: 0,
            PBS: 0,
            PPBS: 0,
            players: []
        }
    }
}


//Component class for easier maintaining. NOTE: All components must have only 1 parent element and components should be named using PascalCase 
class Component {
    /**
     * AUI HTML component. Make sure there is a wrapping parent element.
     * @param {String} ID - ID of the element 
     * @param {Function} HTML - Function that returns a template string representing innerHTML. Note that any conditions put on the wrapper element itself will never reflect upon using .refreshElement(), to reflect those changes use .hardRefreshElement()
     */
    constructor(ID, HTML) {
        this.ID = ID
        this.HTML = HTML
    }

    evaluate() {
        if (typeof this.HTML === 'function') {
            return this.HTML();
        } else {throw new Error(`Component class error: Second argument in Component instantiation is not a function ('${typeof this.HTML}'). ${typeof this.HTML === 'string' && "Hint: Put '() =>' before your template literal"}`)}
    }

    /**
     * Evaluates element with props object. The ID is not included in build elements and they cannot be refreshed
     * @param {Object} props - Props object is used to "build" elements. Its useful for displaying sets of data. A prop from the props object should be used inside the string between |||| tags like so: ||propName||
     * @returns {innerHTML}
     */
    buildElement(props = {}) {
        let processedHTML = this.evaluate();
        processedHTML = processedHTML.replace(/\|\|([^|]+)\|\|/g, (match, variableName) => {
            if (!props.hasOwnProperty(variableName)) {
                console.error(`Component class error: ${variableName} not defined in props object`);
                return match
            }
            return props[variableName] || match;
        });
        return processedHTML
    }
    
    /**
     * Evaluates the HTML 
     * @returns {innerHTML}
     */
    getElement() {
        const tempContainer = document.createElement("span");
        tempContainer.innerHTML = this.evaluate();
        tempContainer.children[0].setAttribute("id", `${this.ID}`)
        if (tempContainer.children.length > 1) {
            throw new Error(`Component class error: Components must have a parent element (Component ID: ${this.ID})`)
        }
    
        return tempContainer.innerHTML;
    }

    /**
     * Re-evaluated the HTML excluding the parent element
     */
    refreshElement() {
        //console.log(`Component refreshed: ${this.ID}`)
        try {
            let tempElement = document.createElement("span");
            tempElement.innerHTML = this.evaluate();
            document.querySelector(`#${this.ID}`).innerHTML = tempElement.children[0].innerHTML;
        } catch (ex) {console.error(`Couldn't refresh element with the ID of '${this.ID}': ` + ex)}
    }

    /**
     * Re-evaluates the HTML including the parent element
     */
    hardRefreshElement() {
        try {
            let tempElement = document.createElement("span");
            tempElement.innerHTML = this.evaluate();
            tempElement.children[0].setAttribute("id", `${this.ID}`);
            document.querySelector(`#${this.ID}`).outerHTML = tempElement.innerHTML;
        } catch (ex) {console.error(`Couldn't hardRefresh element with the ID of '${this.ID}': ` + ex)}
    }
}


let API_TIMER = setInterval(async () => {
    if (COMPONENT_STATE_VALUES.options.activePanel !== 'listing') {
        return;
    }
    if (COMPONENT_STATE_VALUES.userSearch.input) {
        return;
    }
    let raw = await(await fetch(API_LINK)).json();
    COMPONENT_STATE_VALUES.listingLoading = false;
    let allSystems = [];
    for (let item of raw) {
        if (item.modding) {
            if (!COMPONENT_STATE_VALUES.options.modes.modding) {
                continue;
            } 
        }
        if (item.location.toLowerCase() !== COMPONENT_STATE_VALUES.options.activeRegion) {
            continue;
        }
        for (let system of item.systems) {
            if (COMPONENT_STATE_VALUES.options.modes[system.mode]) {
                allSystems.push({
                    ...system,
                    IP_ADDR: item.address
                });
            }
        }
    }
    COMPONENT_STATE_VALUES.filteredSystems = allSystems.sort((a, b) => a.time - b.time);
    Listing.refreshElement();
    returnCaret();
}, 3200)

const returnCaret = () => {
        //The three lines below are necessary because of refresh resetting the caret on input
    document.querySelector('#user-search').focus()
    document.querySelector('#user-search').value = "";
    document.querySelector('#user-search').value = COMPONENT_STATE_VALUES.userSearch.input
}

let STATUS_TIMER = null;
window.statusReport = async (query) => {
    if (STATUS_TIMER) {return};
    COMPONENT_STATE_VALUES.statusReportActive = true;
    COMPONENT_STATE_VALUES.statusReportLoading = true;
    StatusReportModal.hardRefreshElement();
    STATUS_TIMER = setInterval(async () => {
        let raw = await (await fetch(`https://starblast.dankdmitron.dev/api/status/${query}`)).json();
        if (COMPONENT_STATE_VALUES.statusReportLoading) {
            COMPONENT_STATE_VALUES.statusReportLoading = false;
        }
        COMPONENT_STATE_VALUES.statusReportData = templateStatusData();
        COMPONENT_STATE_VALUES.statusReportData.name = raw.name;
        COMPONENT_STATE_VALUES.statusReportData.id = query.split('@')[0];
        try {
            for (let key of Object.keys(raw.players)) {
                let player = raw.players[key];
                COMPONENT_STATE_VALUES.statusReportData[`team_${player.friendly + 1}`].players.push({
                    name: player.player_name,
                    ecp: !!player.custom,
                    score: player.score,
                    type: player.type,
                    PBS: calculatePlayerScore(player.type, !!player.custom)
                })
                COMPONENT_STATE_VALUES.statusReportData[`team_${player.friendly + 1}`].hue = player.hue;
            }
        } catch (ex) {window.closeStatusReport()}
        for (let team of raw.mode.teams) {
            for (let num of ["team_1", "team_2", "team_3"]) {
                if (team.hue === COMPONENT_STATE_VALUES.statusReportData[num].hue) {
                    COMPONENT_STATE_VALUES.statusReportData[num].gems = team.crystals;
                    COMPONENT_STATE_VALUES.statusReportData[num].level = team.level;
                    break
                }
            }
        }
        for (let team of ["team_1", "team_2", "team_3"]) {
            let sPBS = 0, sPPBS = 0, potentialOutput = 0;
            for (let i = 0; i < COMPONENT_STATE_VALUES.statusReportData[team].players.length; i++) {
                sPBS += Number(COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.currentScore);
                sPPBS += Number(COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.potentialScore);
                potentialOutput += COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.energyOutput;
            }
            COMPONENT_STATE_VALUES.statusReportData[team].PBS = sPBS.toFixed(2);
            COMPONENT_STATE_VALUES.statusReportData[team].PPBS = sPPBS.toFixed(2);
            COMPONENT_STATE_VALUES.statusReportData[team].potentialOutput = potentialOutput;
            COMPONENT_STATE_VALUES.statusReportData[team].players = COMPONENT_STATE_VALUES.statusReportData[team].players.sort((a, b) => a.score - b.score).reverse();
        }
        StatusReportModal.refreshElement();
    }, 2500)
}

//These functions are attached to the window so listeners have access to them (Tampermonkey quirk)
window.switchActivePanel = (panel) => {
    COMPONENT_STATE_VALUES.options.activePanel = panel;
    ListingOrSettings.refreshElement();
    Settings.hardRefreshElement();
    Listing.hardRefreshElement();
}
window.switchActiveRegion = (region) => {
    COMPONENT_STATE_VALUES.options.activeRegion = region;
    Settings.refreshElement();
}
window.toggleMode = (mode) => {
    COMPONENT_STATE_VALUES.options.modes[mode] = !COMPONENT_STATE_VALUES.options.modes[mode];
    console.log(COMPONENT_STATE_VALUES.options)
    Settings.refreshElement();
}
window.closeStatusReport = () => {
    COMPONENT_STATE_VALUES.statusReportActive = false;
    clearInterval(STATUS_TIMER);
    STATUS_TIMER = null;
    StatusReportModal.hardRefreshElement();
}

let USER_QUERY_TIMER = null
window.handleSearch = () => {
    COMPONENT_STATE_VALUES.userSearch.input = document.querySelector("#user-search").value;
    clearTimeout(USER_QUERY_TIMER);
    if (!COMPONENT_STATE_VALUES.userSearch.input) {
        Listing.refreshElement();
        return COMPONENT_STATE_VALUES.userSearch.loading = false;
    }
    if (!COMPONENT_STATE_VALUES.userSearch.loading) {
        COMPONENT_STATE_VALUES.userSearch.loading = true;
        Listing.refreshElement();
        returnCaret();
    }
    USER_QUERY_TIMER = setTimeout(async () => {
        //https://starblast.dankdmitron.dev/api/status/${query}`
        COMPONENT_STATE_VALUES.userSearch.systemsQueried = 0;
        let playersList = [], mostSimilar = [];
        for (let system of COMPONENT_STATE_VALUES.filteredSystems) {
            try {
                let query = `${system.id}@${system.IP_ADDR}`
                let raw = await (await fetch(`https://starblast.dankdmitron.dev/api/status/${query}`)).json()
                for (let key of Object.keys(raw.players)) {
                    let player = raw.players[key]
                    playersList.push({name: player.player_name, query: query});
                }
                if (COMPONENT_STATE_VALUES.userSearch.systemsQueried < COMPONENT_STATE_VALUES.filteredSystems.length) {
                    COMPONENT_STATE_VALUES.userSearch.systemsQueried++;
                }
            } catch (ex) {console.log(ex)}
        }
        for (let player of playersList) {
            let similarity = calculateSimilarity(player.name).toFixed(2)
            if (similarity > 25) {
                mostSimilar.push({
                    name: player.name,
                    similarity: similarity,
                    query: player.query
                })
            }
        }
        COMPONENT_STATE_VALUES.userSearch.results = mostSimilar.sort((a, b) => a.similarity - b.similarity).reverse();
        COMPONENT_STATE_VALUES.userSearch.loading = false;
        Listing.refreshElement();
        returnCaret();
    }, 300)
}
document.querySelector('#play').addEventListener('click', () => {
    clearInterval(API_TIMER);
    clearInterval(STATUS_TIMER);
    SL_INTEGRATION.style.display = 'none';
});

//WARNING: refreshSL is a SLOW function. Use it only when absolutely neccessary.
const refreshSL = () => {
    SL_INTEGRATION.innerHTML = `
        ${StatusReportModal.getElement()}
        ${TitleAndCredits.getElement()}
        ${ListingOrSettings.getElement()}
        ${Listing.getElement()}
        ${Settings.getElement()}
    `
}

//COMPONENTS GO BELOW
let ListingOrSettings = new Component("SL_OPTIONS_WRAPPER", () => `<div id="SL_OPTIONS_WRAPPER" style="height:4%; width:100%;display: flex;">
<div id="SL_LISTING_REF" onclick="window.switchActivePanel('listing')"
    style="${COMPONENT_STATE_VALUES.options.activePanel == 'listing' ? "background-color: #FFFFFF; color: #0b0b0b; fill: #0b0b0b;" : "background-color: #1a1a1a; color: #FFFFFF; fill: #FFFFFF;"}border-top-left-radius: 0.25vw; border-top-right-radius: 0.25vw;display: grid; place-items: center; font-size: 0.8vw; font-family: 'DM Sans',sans-serif;width:50%;font-weight: 500;cursor:pointer;">
    LISTING
</div>
<div id="SL_SETTINGS_REF" onclick="window.switchActivePanel('settings')"
    style="${COMPONENT_STATE_VALUES.options.activePanel == 'settings' ? "background-color: #FFFFFF; color: #0b0b0b; fill: #0b0b0b;" : "background-color: #1a1a1a; color: #FFFFFF; fill: #FFFFFF;"}border-top-left-radius: 0.25vw; border-top-right-radius: 0.25vw;display: grid; place-items: center; font-size: 0.8vw; font-family: 'DM Sans',sans-serif;width:50%;font-weight: 500;cursor:pointer;">
    SETTINGS
</div>
</div>`)

let TitleAndCredits = new Component("TitleAndCredits", () => `<div style="height:10%;width:100%;">
    <div style="font-family: 'DM Sans', sans-serif;font-weight: 600;font-size: 1.3vw;text-align:right;width:100%;color:white">
        Starblast AUI v${CURRENT_RUNNING_VERSION}
    </div>
    <div style="font-family:'Abel', sans-serif; font-weight: 300; font-size: 0.9vw; text-align: right; width: 100%; color: gray;">
        API (<a style="color:rgb(191,191,191)" href="https://starblast.dankdmitron.dev/" target="_blank"><u>Serverlist+</u></a>): <span style="color: white; font-weight: bold">dankdmitron</span>
    </div>
    <div style="font-family:'Abel', sans-serif; font-weight: 300; font-size: 0.9vw; text-align: right; width: 100%; color: gray;">
        Design and integration: <span style="color: white; font-weight: bold">Halcyon</span>
    </div>
</div>`)

let Settings = new Component("SLSettings", () => `<div id="SL_SETTINGS" style="box-sizing:border-box;padding:0.6vw;height:86%;width:100%;display: ${COMPONENT_STATE_VALUES.options.activePanel == "settings" ? "flex" : "none"}; flex-direction: column; gap: 0.2rem; overflow-y: auto;background-color:#0b0b0b;border:1px solid #1a1a1a;justify-content: flex-start;align-items:flex-end;">
<div style="text-align:right;color:white;">
    <div style="font-family:'DM Sans',sans-serif;font-size:1.5vw;margin-bottom:0.5vh;">
        REGION:
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Europe</div>
        ${
            COMPONENT_STATE_VALUES.options.activeRegion == "europe"
            ?
            `<svg xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M480-294q78 0 132-54t54-132q0-78-54-132t-132-54q-78 0-132 54t-54 132q0 78 54 132t132 54Zm0 214q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
            :
            `<svg xmlns="http://www.w3.org/2000/svg" onclick="window.switchActiveRegion('europe')" height="1vw" viewBox="0 -960 960 960"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>America</div>
        ${
            COMPONENT_STATE_VALUES.options.activeRegion == "america"
            ?
            `<svg xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M480-294q78 0 132-54t54-132q0-78-54-132t-132-54q-78 0-132 54t-54 132q0 78 54 132t132 54Zm0 214q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
            :
            `<svg xmlns="http://www.w3.org/2000/svg" onclick="window.switchActiveRegion('america')" height="1vw" viewBox="0 -960 960 960"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Asia</div>
        ${
            COMPONENT_STATE_VALUES.options.activeRegion == "asia"
            ?
            `<svg xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M480-294q78 0 132-54t54-132q0-78-54-132t-132-54q-78 0-132 54t-54 132q0 78 54 132t132 54Zm0 214q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
            :
            `<svg xmlns="http://www.w3.org/2000/svg" onclick="window.switchActiveRegion('asia')" height="1vw" viewBox="0 -960 960 960"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>`
        }
    </div>
</div>
<div style="text-align:right;color:white;">
    <div style="font-family:'DM Sans',sans-serif;font-size:1.5vw;margin-bottom:0.5vh;margin-top:2vh;">
        MODE:
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Team Mode</div>
        ${
            COMPONENT_STATE_VALUES.options.modes.team
            ?
            `<svg onclick="window.toggleMode('team')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="m419-321 289-289-43-43-246 246-119-119-43 43 162 162ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z"/></svg>`
            :
            `<svg onclick="window.toggleMode('team')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Survival</div>
        ${
            COMPONENT_STATE_VALUES.options.modes.survival
            ?
            `<svg onclick="window.toggleMode('survival')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="m419-321 289-289-43-43-246 246-119-119-43 43 162 162ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z"/></svg>`
            :
            `<svg onclick="window.toggleMode('survival')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Deathmatch</div>
        ${
            COMPONENT_STATE_VALUES.options.modes.deathmatch
            ?
            `<svg onclick="window.toggleMode('deathmatch')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="m419-321 289-289-43-43-246 246-119-119-43 43 162 162ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z"/></svg>`
            :
            `<svg onclick="window.toggleMode('deathmatch')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Modded</div>
        ${
            COMPONENT_STATE_VALUES.options.modes.modding
            ?
            `<svg onclick="window.toggleMode('modding')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="m419-321 289-289-43-43-246 246-119-119-43 43 162 162ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z"/></svg>`
            :
            `<svg onclick="window.toggleMode('modding')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Z"/></svg>`
        }
    </div>
    <div style="color:gray;font-family: 'Abel',sans-serif;font-size:1vw;display:flex;gap:0.5vw;justify-content:end;align-items:center;fill:#FFFFFF;">
        <div>Invasion</div>
        ${
            COMPONENT_STATE_VALUES.options.modes.invasion
            ?
            `<svg onclick="window.toggleMode('invasion')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="m419-321 289-289-43-43-246 246-119-119-43 43 162 162ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z"/></svg>`
            :
            `<svg onclick="window.toggleMode('invasion')" xmlns="http://www.w3.org/2000/svg" height="1vw" viewBox="0 -960 960 960"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Z"/></svg>`
        }
    </div>
</div>
</div>`)

let Listing = new Component("ServerListing", () => `
    <div id="SL_LISTING" style="box-sizing:border-box;overflow-x:hidden;padding:0.6vw;height:86%;width:100%;display: ${COMPONENT_STATE_VALUES.options.activePanel == "listing" ? "flex" : "none"}; flex-direction: column; overflow-y: auto;background-color:#0b0b0b;border:1px solid #1a1a1a">
        ${
            COMPONENT_STATE_VALUES.listingLoading
            ?
            `${LoadingAnimation.getElement()}`
            :
            `
            ${
                COMPONENT_STATE_VALUES.filteredSystems.length === 0
                ?
                ""
                :
                `
                <input id="user-search" autofocus value="${COMPONENT_STATE_VALUES.userSearch.input}" oninput="window.handleSearch()" placeholder="Search user in queried servers" style="width:100%; height:2.5vh; padding:0.3vh 0 0.3vh 0; font-family: 'Abel', sans-serif; color: white; border: 1px solid #1a1a1a; outline: 0; background: #0b0b0b; text-shadow: black 0px 0px 0px; border-radius: 5px; font-size: 1.6vh;  margin-bottom: 1vh; text-indent: 0.5vw"></input>
                ${
                    COMPONENT_STATE_VALUES.userSearch.input
                    ?
                    `
                        ${
                            COMPONENT_STATE_VALUES.userSearch.loading
                            ?
                            `${LoadingAnimation.getElement()}`
                            :
                            `
                            <div style="display:flex;height:2.2vh; padding:0.3vh 0 0.3vh 0;border-bottom: 1px solid #1a1a1a; font-family: 'Abel', sans-serif; color: #444444; outline: 0; background: #0b0b0b; text-shadow: black 0px 0px 0px; font-size: 1.4vh;  margin-bottom: 0.7vh;">
                                <div style="width:33.3%">NAME</div>
                                <div style="width:33.3%">SIMILARITY</div>
                                <div style="width:33.3%">SERVER</div>
                            </div>
                            ${
                                COMPONENT_STATE_VALUES.userSearch.results.length === 0
                                ?
                                `
                                <div style="width:100%;display:flex;flex-direction:column;font-family: 'Abel',sans-serif;color:#444444;text-shadow: black 0px 0px 0px;">
                                    <div style="font-size:4.2vh;margin-top:1vh;">(⌐■_■)</div>
                                    <div style="font-size:1.9vh"><br>All clear!<br>"${COMPONENT_STATE_VALUES.userSearch.input}" yields no users with a similarity match over 25%<br>Try selecting more servers (e.g. modding, survival)</div>
                                </div>
                                `
                                :
                                `
                                ${
                                    COMPONENT_STATE_VALUES.userSearch.results.map((item, index) => PlayerQueryDisplay.buildElement({
                                            username: sanitizeUsername(item.name),
                                            similarity: item.similarity,
                                            query: item.query,
                                            similarityColor: getColorFromValue(Number(item.similarity))
                                        })
                                    ).join('')
                                }
                                <div style="width:100%;display:flex;flex-direction:column;font-family: 'Abel',sans-serif;color:#444444;text-shadow: black 0px 0px 0px;margin-top:1vh">
                                    <div style="font-size:1.9vh;text-align:center">${COMPONENT_STATE_VALUES.userSearch.results.length} results<br>${COMPONENT_STATE_VALUES.userSearch.systemsQueried} / ${COMPONENT_STATE_VALUES.filteredSystems.length} systems queried</div>
                                </div>
                                `
                            }
                            `
                        }
                    `
                    :
                    `
                    ${
                        COMPONENT_STATE_VALUES.filteredSystems.map(system => SystemDisplay.buildElement({
                            id: system.id,
                            ip: system.IP_ADDR,
                            name: system.name,
                            mode: system.mode === 'modding' ? capitalize(system.mod_id) : capitalize(system.mode),
                            time: ~~(system.time / 60),
                            players: system.players
                        })).join('')
                    }
                    `
                }
                `
            }
            `
        }
</div>`)

let SystemDisplay = new Component('SystemDisplay', () => `
<div onclick="window.statusReport('||id||@||ip||')" style="width:100%; cursor: pointer; min-height:8.5vh; margin-bottom: 0.9vh; border-radius:12px; border: 1px solid #1a1a1a; display: flex; flex-direction: column; align-items: center; justify-content: space-evenly;box-sizing:border-box;padding:0.4vh">
    <div style="font-family:'DM Sans',sans-serif;color:white;font-weight:600;font-size:1.4vw;">
        ||name||
    </div>
    <div style="width:82%;height:1px;background-color:#1a1a1a"></div>
    <div style="width:92%;display:flex;align-items:center;justify-content:space-between;color:gray;font-family:'Abel',sans-serif;font-size:0.8vw;position:relative;">
        <div>
            ||mode||
        </div>
        <div style="position:absolute;top:0;left:0;width:100%;text-align:center;">
            ||time|| min
        </div> 
        <div>
            ||players|| players
        </div>
    </div>
</div>
`)

let PlayerQueryDisplay = new Component("PQD", () => `
<div style="display:flex;height:2vh;font-weight:600;padding:0.1vh 0 0.1vh 0; font-family: 'Abel', sans-serif; color: white;outline: 0; background: #0b0b0b; text-shadow: black 0px 0px 0px; border-radius: 5px; font-size: 1.6vh;  margin-bottom: 0.7vh;">
    <div style="width:33.3%;max-width:33.3%;text-overflow:ellipsis;white-space:nowrap">||username||</div>
    <div style="width:33.3%;color: ||similarityColor||">||similarity||</div>
    <div style="width:33.3%;display:flex;justify-content:center">
        <svg onclick="window.statusReport('||query||')" xmlns="http://www.w3.org/2000/svg" style="height:100%;aspect-ratio: 1 / 1; fill: white; cursor: pointer;" viewBox="0 -960 960 960"><path d="M440-220q125 0 212.5-87.5T740-520q0-125-87.5-212.5T440-820q-125 0-212.5 87.5T140-520q0 125 87.5 212.5T440-220Zm0-300Zm0 160q-83 0-147.5-44.5T200-520q28-70 92.5-115T440-680q82 0 146.5 45T680-520q-29 71-93.5 115.5T440-360Zm0-60q55 0 101-26.5t72-73.5q-26-46-72-73t-101-27q-56 0-102 27t-72 73q26 47 72 73.5T440-420Zm0-50q20 0 35-14.5t15-35.5q0-20-15-35t-35-15q-21 0-35.5 15T390-520q0 21 14.5 35.5T440-470Zm0 310q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T80-520q0-74 28.5-139.5t77-114.5q48.5-49 114-77.5T440-880q74 0 139.5 28.5T694-774q49 49 77.5 114.5T800-520q0 67-22.5 126T715-287l164 165-42 42-165-165q-48 40-107 62.5T440-160Z"/></svg>
    </div>
</div>`)

let StatusReportModal = new Component("StatusReportModal", () => `
<div style="position:fixed; top:0; left:0; width: 100%; height: 100%; display: ${COMPONENT_STATE_VALUES.statusReportActive ? "grid" : "none"}; place-items:center; z-index: 999; background: rgba(0,0,0,0.4)">
    <div style="padding:1vw;display:flex;flex-direction:column;align-items:center;background:#0b0b0b;border:1px solid #1a1a1a;border-radius:12px;${COMPONENT_STATE_VALUES.statusReportLoading ? "justify-content:center" : ""}">
        ${
            COMPONENT_STATE_VALUES.statusReportLoading
            ?
            `${LoadingAnimation.getElement()}`
            :
            `
            <div style="height:3vh; border-bottom: 1px solid #1a1a1a; width:50vw; color: white; font-family: 'DM Sans',sans-serif; font-size: 1.7vw; display: flex; justify-content: space-between; fill: white; align-items: center; padding-bottom: 2vh">
                <div>${COMPONENT_STATE_VALUES.statusReportData.name}</div>
                <div style="display:flex;gap:1vw;align-items:center;">
                    <a target="_blank" href="https://starblast.io/#${COMPONENT_STATE_VALUES.statusReportData.id}">
                        <div style="height:1.4vw;display:grid;place-items:center;font-weight:400;font-size:1vw;border:1px solid white;border-radius:0.2vw;padding:0.4vw 1vw 0.4vw 1vw;color:white;">
                            JOIN
                        </div>
                    </a>
                    <svg onclick="window.closeStatusReport()" style="cursor: pointer;" xmlns="http://www.w3.org/2000/svg" height="2.5vh" viewBox="0 -960 960 960"><path d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z"/></svg>
                </div>
            </div>
            <div style="display:flex;height:55vh;width: 100%; margin-top: 2vh;justify-content:space-between;">
                ${
                    (() => {
                        let arr = [];
                        for (let CUR_TEAM of ["team_1", "team_2", "team_3"]) {
                            arr.push(`
                            <div style="width:30%;height:100%;overflow-y: auto; display: flex; flex-direction: column; justify-content: flex-start; align-items: center">
                                <div style="margin-bottom: 1vh;border-radius: 0.25vw;font-size:1.9vw;border:1px solid hsla(${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].hue}, 100%, 50%, 1); background-color: hsla(${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].hue}, 100%, 50%, 0.25); color: hsla(${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].hue}, 100%, 50%, 1); width:80%;padding: 1vh 0 1vh 0; font-family: 'DM Sans',sans-serif; font-weight: 600; text-align: center;">
                                    ${hueToColorName(COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].hue)}
                                </div>
                                <div style="width:100%;margin: 1vh 0 1vh 0; padding-bottom:1vh;border-bottom: 1px solid #1a1a1a;">
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between">
                                        <div style="color:gray">Player count</div>
                                        <div style="color:white">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.length}</div>
                                    </div>
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between">
                                        <div style="color:gray">Gems</div>
                                        <div style="color:white">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].gems}</div>
                                    </div>
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between">
                                        <div style="color:gray">Level</div>
                                        <div style="color:white">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].level}</div>
                                    </div>
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between;font-weight:500;">
                                        <div style="color:gray">Playerbase strength score</div>
                                        <div style="color:#A4D8D8;font-weight:bold;">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].PBS}</div>
                                    </div>
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between;font-weight:500;">
                                        <div style="color:gray">Potential playerbase strength score</div>
                                        <div style="color:#F49AC2;font-weight:bold;">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].PPBS}</div>
                                    </div>
                                    <div style="font-family:'Abel',sans-serif;font-size:0.8vw;display:flex;width:100%;justify-content:space-between;font-weight:500;">
                                        <div style="color:gray">Maximum damage output (DPS)</div>
                                        <div style="color:#FDFD96;font-weight:bold;">${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].potentialOutput}</div>
                                    </div>
                                </div>
                                <div style="height:max-content;width:100%;display:flex;justify-content:space-between;color:gray;font-family:'Abel',sans-serif;font-size:0.8vw;padding-bottom:0.5vh;border-bottom:1px solid #1a1a1a;margin-bottom: 0.5vh">
                                    <div>ECP | NAME</div>
                                    <div>SCORE | SHIP</div>
                                </div>
                                ${
                                    COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.length === 0
                                    ?
                                    ""
                                    :
                                    COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.map(player => {
                                        return `
                                            <div style="height:max-content;margin-bottom:0.3vh;display:flex;width:100%;height:1vw">
                                                <div style="width:10%;height:100%;display:grid;place-items:center;">
                                                    <div style="height:60%; aspect-ratio: 1 / 1; border-radius:9999px; background: ${player.ecp ? "#37dd37" : "#Ff3931"}"></div>
                                                </div>
                                                <div style="width:90%;height:100%;display:flex;justify-content:space-between;font-family:'Abel',sans-serif;font-size:0.8vw;color:white;">
                                                    <div>${sanitizeUsername(player.name)}</div>
                                                    <div style="display:flex">${player.score}&nbsp;<img style="height:0.8vw;aspect-ratio: 1 / 1;object-fit:contain;" src="${SHIP_LINKS.find(url => url.endsWith(`/${player.type}.png`))}"/></div>
                                                </div>
                                            </div>
                                        `
                                    }).join('')
                                }
                        </div>
                        ${CUR_TEAM !== "team_3" ? `<div style="height:100%;width:1px;background-color:#1a1a1a"></div>` : ""}`)
                        }
                        return arr.join('')
                    })()
                }
            </div>
            `
        }    
            
    </div>
</div>`)

let LoadingAnimation = new Component("LoadingAnimation", () => `<svg style="height:4.8vh; aspect-ratio: 1 / 1" viewBox="0 0 100 100">
<g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
    <!-- left line -->
    <path d="M 21 40 V 59">
        <animateTransform
  attributeName="transform"
  attributeType="XML"
  type="rotate"
  values="0 21 59; 180 21 59"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <!-- right line -->
    <path d="M 79 40 V 59">
        <animateTransform
  attributeName="transform"
  attributeType="XML"
  type="rotate"
  values="0 79 59; -180 79 59"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <!-- top line -->
    <path d="M 50 21 V 40">
        <animate
  attributeName="d"
  values="M 50 21 V 40; M 50 59 V 40"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <!-- btm line -->
    <path d="M 50 60 V 79">
        <animate
  attributeName="d"
  values="M 50 60 V 79; M 50 98 V 79"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <!-- top box -->
    <path d="M 50 21 L 79 40 L 50 60 L 21 40 Z">
    <animate
  attributeName="stroke"
  values="rgba(255,255,255,1); rgba(100,100,100,0)"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <!-- mid box -->
    <path d="M 50 40 L 79 59 L 50 79 L 21 59 Z"/>
    <!-- btm box -->
    <path d="M 50 59 L 79 78 L 50 98 L 21 78 Z">
    <animate
  attributeName="stroke"
  values="rgba(100,100,100,0); rgba(255,255,255,1)"
  dur="2s"
  repeatCount="indefinite" />
    </path>
    <animateTransform
  attributeName="transform"
  attributeType="XML"
  type="translate"
  values="0 0; 0 -19"
  dur="2s"
  repeatCount="indefinite" />
</g>
</svg>`)



//FUNCTIONS NOT RELEVANT TO MANIPULATING THE DOM GO BELOW
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);

const hueToColorName = (hue) => {
    const colorMap = [
      { hueRange: [0, 15], colorName: 'Red' },
      { hueRange: [15, 45], colorName: 'Orange' },
      { hueRange: [45, 75], colorName: 'Yellow' },
      { hueRange: [75, 150], colorName: 'Green' },
      { hueRange: [150, 195], colorName: 'Cyan' },
      { hueRange: [195, 285], colorName: 'Blue' },
      { hueRange: [285, 330], colorName: 'Magenta' },
      { hueRange: [330, 360], colorName: 'Red' }
    ];
    const matchedColor = colorMap.find(entry => hue >= entry.hueRange[0] && hue < entry.hueRange[1]);
    return matchedColor ? matchedColor.colorName : 'Undefined';
}



const POTENTIAL = {
    //          ECP    NON-ECP
    odyssey:   [2.5,   1.5],
    x3:        [1.7,   1  ],
    bastion:   [1.4,   0.4],
    aries:     [1  ,   0.3],
    barracuda: [1.75,  0.3],
}
const buildItem = (ecp, nonecp, eregen, potential) => ({ecp, nonecp, eregen, potential});
const SHIP_TABLE = {
    // The numbers on this table need massive improvement
    /**
     * ECP - Points when player is ecp
     * NONECP - Obvious
     * POTENTIAL - Highest points the player can achieve with current path (e.g 2.5 for an ecp playing furystar)
     * EREGEN - Obvious
    */
    //               ECP   NON-ECP    E-REGEN    POTENTIAL
    // Tier 7
    "701": buildItem(2.5 , 1.5,       150,       POTENTIAL.odyssey),
    "702": buildItem(1.7 , 1  ,       50 ,       POTENTIAL.x3),
    "703": buildItem(1.4 , 0.4,       100,       POTENTIAL.bastion),
    "704": buildItem(1   , 0.3,       175,       POTENTIAL.aries),
    // Tier 6
    "601": buildItem(1.2 , 0.5,       60 ,       POTENTIAL.odyssey),
    "602": buildItem(1.2 , 0.45,      50 ,       POTENTIAL.odyssey),
    "603": buildItem(0.9 , 0.25,      40 ,       POTENTIAL.x3),
    "604": buildItem(0.5 , 0.25,      48 ,       POTENTIAL.x3),
    "605": buildItem(0.9 , 0.25,      45 ,       POTENTIAL.x3),
    "606": buildItem(0.9 , 0.2 ,      45 ,       POTENTIAL.x3),
    "607": buildItem(1.75, 0.3 ,      0  ,       POTENTIAL.barracuda),
    "608": buildItem(0.5 , 0.2 ,      40 ,       POTENTIAL.bastion),
    // Tier 5
    "501": buildItem(1.05, 0.45,      60 ,       POTENTIAL.odyssey),
    "502": buildItem(0.75, 0.3,       40 ,       POTENTIAL.odyssey),
    "503": buildItem(0.2 , 0.1,       50 ,       POTENTIAL.x3),
    "504": buildItem(0.3 , 0.15,      45 ,       POTENTIAL.x3),
    "505": buildItem(0.1 , 0.05,      29 ,       POTENTIAL.x3),
    "506": buildItem(0.9 , 0.5,       50 ,       POTENTIAL.barracuda),
    "507": buildItem(0.15, 0.1,       35 ,       POTENTIAL.barracuda),
    // Tier 4
    "401": buildItem(0.3 , 0.05 ,     35  ,      POTENTIAL.odyssey),
    "402": buildItem(0.55, 0.25 ,     50  ,      POTENTIAL.odyssey),
    "403": buildItem(0.4 , 0.2  ,     55  ,      POTENTIAL.x3),
    "404": buildItem(0.3 , 0.15 ,     40  ,      POTENTIAL.x3),
    "405": buildItem(0.3 , 0.1  ,     30  ,      POTENTIAL.x3),
    "406": buildItem(0.05, 0    ,     25  ,      POTENTIAL.barracuda),
    // Tier 3
    "301": buildItem(0.2 , 0.07 ,     30  ,      POTENTIAL.odyssey),
    "302": buildItem(0.17, 0.05 ,     35  ,      POTENTIAL.x3),
    "303": buildItem(0.05, 0    ,     16  ,      POTENTIAL.x3),
    "304": buildItem(0.15, 0.05 ,     25  ,      POTENTIAL.barracuda),
    // Tier 2
    "201": buildItem(0.05, 0    ,     25  ,      POTENTIAL.odyssey),
    "202": buildItem(0.02, 0    ,     20  ,      POTENTIAL.x3),
    // Fly
    "101": buildItem(0   , 0    ,     10  ,      [0,0]),
}

const calculatePlayerScore = (type, ecp) => {
    return {
        currentScore: (Number(String(type).split('')[0]) / 15) + SHIP_TABLE[String(type)][ecp ? "ecp" : "nonecp"],
        potentialScore: (7 / 15) + SHIP_TABLE[String(type)]["potential"][+!ecp], // +!ecp is: First ecp is converted into a boolean and then inverted using !, then turned into a number using +, resulting in index 0 for ecp=true
        energyOutput: SHIP_TABLE[String(type)]["eregen"]
    }
}

const calculateSimilarity = (query) => {
    const referenceString = COMPONENT_STATE_VALUES.userSearch.input.toUpperCase();

    const maxLength = Math.max(query.length, referenceString.length);
    const distance = levenshteinDistance(query, referenceString);

    const similarityPercentage = ((maxLength - distance) / maxLength) * 100;
    return similarityPercentage;
}

const levenshteinDistance = (str1, str2) => {
    const matrix = [];

    for (let i = 0; i <= str1.length; i++) {
        matrix[i] = [i];
        for (let j = 1; j <= str2.length; j++) {
            if (i === 0) {
                matrix[i][j] = j;
            } else {
                const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
                matrix[i][j] = Math.min(
                    matrix[i - 1][j] + 1, // Deletion
                    matrix[i][j - 1] + 1, // Insertion
                    matrix[i - 1][j - 1] + cost // Substitution
                );
            }
        }
    }

    return matrix[str1.length][str2.length];
}

const sanitizeUsername = (username) => {
    const sanitizedUsername = username
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#x27;")
        .replace(/\//g, "&#x2F;");
    
    return sanitizedUsername;
}

const lerpColor = (color1, color2, t) => {
    const r = Math.round(color1.r * (1 - t) + color2.r * t);
    const g = Math.round(color1.g * (1 - t) + color2.g * t);
    const b = Math.round(color1.b * (1 - t) + color2.b * t);
    return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`;
}

const getColorFromValue = (value) => {
    const color1 = { r: 255, g: 57, b: 49 }; // #Ff3931
    const color2 = { r: 55, g: 221, b: 55 }; // #37dd37
    const t = value / 100; // Normalize the value
    const interpolatedColor = lerpColor(color1, color2, t);
    return interpolatedColor;
}

const SHIP_LINKS = [
    "https://i.ibb.co/6gjB0Y9/504.png",
    "https://i.ibb.co/h1BWddj/505.png",
    "https://i.ibb.co/ZG2wQtk/506.png",
    "https://i.ibb.co/ZxY43kc/507.png",
    "https://i.ibb.co/f8zzwcS/601.png",
    "https://i.ibb.co/hXgqvHQ/602.png",
    "https://i.ibb.co/HxNmSPY/603.png",
    "https://i.ibb.co/DVZrPT7/604.png",
    "https://i.ibb.co/w6jZfmK/605.png",
    "https://i.ibb.co/p4qBj2k/606.png",
    "https://i.ibb.co/4fjJcBC/607.png",
    "https://i.ibb.co/wYMGzCs/608.png",
    "https://i.ibb.co/ZNmcHfC/701.png",
    "https://i.ibb.co/JWZFqVv/702.png",
    "https://i.ibb.co/X2w682R/703.png",
    "https://i.ibb.co/RQrfMGW/704.png",
    "https://i.ibb.co/s3YVpVW/101.png",
    "https://i.ibb.co/w7GFPR5/201.png",
    "https://i.ibb.co/4JsJz8G/202.png",
    "https://i.ibb.co/Pz0xp1s/301.png",
    "https://i.ibb.co/M7PWNz7/302.png",
    "https://i.ibb.co/4ZKStWk/303.png",
    "https://i.ibb.co/df72XT8/304.png",
    "https://i.ibb.co/VM2kJgD/401.png",
    "https://i.ibb.co/8g6qgBw/402.png",
    "https://i.ibb.co/HnqK41P/403.png",
    "https://i.ibb.co/s2grnKB/404.png",
    "https://i.ibb.co/cvj9FWz/405.png",
    "https://i.ibb.co/64fsKPt/406.png",
    "https://i.ibb.co/27fLBPx/501.png",
    "https://i.ibb.co/3SfYGZX/502.png",
    "https://i.ibb.co/9pJt735/503.png"
]


//This runs the SL integration. Do not touch
refreshSL();