LukaNebo / Improved Production Overview

// ==UserScript==
// @name         Improved Production Overview
// @namespace    https://openuserjs.org/users/LukaNebo
// @version      1.1.0
// @description  Improved OGame's production overview: highlight earliest queue(s) and highlight empty queues.
// @author       LukaNebo
// @license      MIT
// @copyright    2024, LukaNebo (https://openuserjs.org/users/LukaNebo)
// @match        https://*.ogame.gameforge.com/game/index.php?page=ingame&component=productionqueue*
// @updateURL    https://openuserjs.org/meta/LukaNebo/Improved_Production_Overview.meta.js
// @downloadURL  https://openuserjs.org/install/LukaNebo/Improved_Production_Overview.user.js
// @grant        GM_addStyle
// ==/UserScript==





// Config
const QUEUE_HEIGHT = 46.25; // [px]
const QUEUE_WIDTH = { planet: 24.6, moon: 49.7, mechas: 19.6 }; // [%]
const BADGES_SIZE = 18; // [px]
const BADGES_MARGIN = 6; // [px]
const BUTTON_WIDTH = 38; // [px]
const TOGGLE_BUTTON_WIDTH = 28; // [px]
const COLORS = { earliest: ["#FF9600", "#804a00", "#663c00"], empty: "#99CC00", emptyRemoved: "#D43635", og: { blue: "#6F9FC8", productionOverview: { border: "rgba(20, 29, 37, 0.8)", highlight: "rgba(9, 13, 18, 0.8)", text: "rgba(168, 168, 168, 0.6)" } } };
const DISABLED_CONSTRUCION_QUEUES_INDEXES = {
    planet: {
        nonLF: { ROBOTICS_FACTORY: [1] /* disabled LF Buildings queues */, SHIPYARD: [3] /* disabled Shipyard queues */, RESEARCH_LAB: [] /* disabled Research queueu */, NANITE_FACTORY: [1, 3] /* disabled LF Buildings and Shipyard queue */ },
        LF: { LF_RESEARCH_CENTRE: [2] /* disabled LF Development queues */ },
    },
    moon: {
        nonLF: { SHIPYARD: [1] /* disabled Shipyard queues */ },
    }
};

// Calculate height for planet queues segment
let planetQueuesHeight = QUEUE_HEIGHT + 26; // [px]



// OGame queries
/* OGame clock                                           */ const OG_CLOCK_CLASS_NAME = "OGameClock"; // document.getElementsByClassName("OGameClock ")[0]
/* Main content                                          */ const OG_PRODUCTION_QUEUE_COMPONENET_ID = "productionqueuecomponent"; // document.getElementById("productionqueuecomponent") // document.getElementsByClassName("maincontent")[0]
/* Planeta and moon tab                                  */ /* */ const OG_PLANET_MOON_TABS_CLASS_NAME = "spaceObjectTab"; // document.getElementsByClassName("spaceObjectTab")
/* (Only) research queue                                 */ /* */ const OG_RESEARCH_QUEUE_CLASS_NAME = { CLASS_NAME: "planetQueues research", QUERY_SELECTOR: ".planetQueues.research" }; // document.getElementsByClassName("planetQueues research")[0] // document.querySelector(".planetQueues.research")
/* Research queue header                                 */ /* */ /* */ const OG_RESEARCH_HEADER_CLASS_NAME = "researchTitle"; // document.getElementsByClassName("researchTitle")
/* Research icon (used as empty queue check)             */ /* */ /* */ const OG_RESEARCH_ICON_CLASS_NAME = "researchIcon"; // document.getElementsByClassName("researchIcon")
/* Planet queues section                                 */ /* */ const OG_PLANET_PRODUCTION_CLASS_NAME = "planetProduction"; // document.getElementsByClassName("planetProduction")
/* Moon queues section                                   */ /* */ const OG_MOON_PRODUCTION_CLASS_NAME = "moonProduction"; // document.getElementsByClassName("moonProduction")
/* Planet AND moon queues, all 4 (or 2, for moon) types  */ /* */ /* */ const OG_PLANET_QUEUES_CLASS_NAME = "planetQueues"; // document.getElementsByClassName("planetQueues")
/* Planet/moon title/header                              */ /* */ /* */ /* */ const OG_PLANET_TITLES_CLASS_NAME = "planetTitle"; // document.getElementsByClassName("planetTitle")
/* All planet/moon queues section under planet header #1 */ /* */ /* */ /* */ const OG_QUEUE_INFOS_CLASS_NAME = "queueInfos"; // document.getElementsByClassName("queueInfos")
/* All planet/moon queues section unter planet header #2 */ /* */ /* */ /* */ /* */ const OG_QUEUES_CLASS_NAME = "queues"; // document.getElementsByClassName("queues")
/* Empty queues (same layer as "singleQueueWithTitle")   */ /* */ /* */ /* */ /* */ /* */ const OG_EMPTY_QUEUE_CLASS_NAME = "noOrder"; // document.getElementsByClassName("noOrder")
/* Single queues with box title                          */ /* */ /* */ /* */ /* */ /* */ const OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME = "singleQueueWithTitle"; // document.getElementsByClassName("singleQueueWithTitle")
/* Box titles of single queues                           */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_BOX_TITLES_CLASS_NAME = "boxTitle"; // document.getElementsByClassName("boxTitle")
/* Single queues #1 (without the box title)              */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_SINGLE_QUEUES_CLASS_NAME = "singleQueue"; // document.getElementsByClassName("singleQueue")
/* Single queues #2 inside ("singleQueue")               */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_CLASS_NAME = "production"; // document.getElementsByClassName("production")
/* Production icons                                      */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_ICONS_CLASS_NAME = "productionIcon"; // document.getElementsByClassName("productionIcon")
/* Production icon tag name                              */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_ICONS_TAG_NAME = "technology-icon"; // document.getElementsByTagName("technology-icon")
/* Attribute names for all constructions                 */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_CONSTRUCTION_ATTRIBUTE_NAMES = { nonLF: { ROBOTICS_FACTORY: "roboticsfactory", SHIPYARD: "shipyard", RESEARCH_LAB: "researchlaboratory", NANITE_FACTORY: "nanitefactory" }, LF: { LF_RESEARCH_CENTRE: /^lifeformTech(11103|12103|13103|14103)$/i /* <--- Humans: "lifeformtech11103", Rock'tal: "lifeformtech12103", Mechas: "lifeformtech13103", and Kaelesh: "lifeformtech14103". */ } };
/* All of text strings inside single queues              */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_TEXTS_CLASS_NAME = "singleQueueTexts"; // document.getElementsByClassName("singleQueueTexts")
/* Production details (names, levels, and timer)         */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_DETAILS_CLASS_NAME = "productionDetails"; // document.getElementsByClassName("productionDetails")
/* Production names                                      */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_NAMES_CLASS_NAME = "productionName"; // document.getElementsByClassName("productionName")
/* Production level (inside "Production names)           */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_LEVELS_CLASS_NAME = "productionLevel"; // document.getElementsByClassName("productionLevel")
/* Production timers (including Research)                */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_PRODUCTION_TIMERS_CLASS_NAME = "productionTimer"; // document.getElementsByClassName("productionTimer")
/* Countdown timer (including Eventbox and Research)     */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_COUNTDOWN_CLASS_NAME = "countdown"; // document.getElementsByClassName("countdown")
/* Link details (hover over the queue with mouse)        */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_LINK_DETAILS_CLASS_NAME = "linkDetails"; // document.getElementsByClassName("linkDetails")
/* Links                                                 */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_LINKS_CLASS_NAME = "link"; // document.getElementsByClassName("link")
/* No entry (when there is no construction active)       */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ const OG_NO_ENTRY = "noEntry"; // document.getElementsByClassName("noEntry");

// OGame dataset queries
/* OGame clock datasets (requires OGLight!)             */ const OGL_CLOCK_TIMESTAMP_DATASETS = { DATE: "outputDate", TIME: "outputTime", TIME_UTC: "timeUtc", TIME_SERVER: "timeServer", TIME_CLIENT: "timeClient" };
/* OGame countdown dataset                              */ const OG_COUNTDOWN_DATASET = "end";

// My queries
const IPO_TIMESTAMP_DATASET = "data-ipo-timestamp";
const IPO_EARLIEST_QUEUE_CLASS_NAME = "IPO_earliestQueue";
const IPO_SOON_FINISHED_COUNTDOWN_CLASS_NAME = "IPO_earliestQueueCountdown";
const IPO_EMPTY_QUEUE_CLASS_NAME = "IPO_emptyQueue";
const IPO_TAB_BADGES_CLASS_NAME = "IPO_tabBadges";
const IPO_TAB_BADGES_EMPTY_QUEUES_CLASS_NAME = "IPO_tabBadgesEmptyQueues";
const IPO_SETTINGS_ID = "IPO_settings";
/* */ const IPO_SETTINGS_HEADER_CLASS_NAME = "IPO_settingsHeader";
/* */ const IPO_SETTINGS_BUTTON_LABEL_WRAPPER_CLASS_NAME = "IPO_settingsButtonWrapper";
/* */ /* */ const IPO_BUTTONS_CLASS_NAME = "IPO_settingButton";
/* */ /* */ const IPO_LABEL_CLASS_NAME = "IPO_settingsLabel";
/* */ /* */ const IPO_TOGGLE_BUTTONS_CLASS_NAME = "IPO_settingsToggleButton";
const IPO_REFRESH_MESSAGE_ID = "IPO_refreshMessage";



// Locales
const LOCALES = {
    en: { earliestQueue: { title: "Select the number of earliest queues." }, soonFinished: { title: "Select the time threshold below which the countdown timers of soon-to-be-finished constructions are highlighted." }, emptyQueues: { title: "Highlight construction queues when their are empty" /* without "." */ }, refreshMessage: "Refresh the page to see new settings!" },
    de: { earliestQueue: { title: "Wählen Sie die Anzahl der frühesten Warteschlangen." }, soonFinished: { title: "Wählen Sie die Zeitgrenze, unter der die Countdown-Timer von bald fertiggestellten Bauwerken hervorgehoben werden." }, emptyQueues: { title: "Markieren Sie Bauwarteschlangen, wenn sie leer sind" /* without "." */ }, refreshMessage: "Aktualisieren Sie die Seite, um die neuen Einstellungen zu sehen!" },
    fr: { earliestQueue: { title: "Sélectionnez le nombre de files d'attente les plus anciennes." }, soonFinished: { title: "Sélectionnez le seuil de temps en dessous duquel les minuteries des constructions bientôt terminées sont mises en évidence." }, emptyQueues: { title: "Mettre en surbrillance les files d'attente de construction lorsqu'elles sont vides" /* without "." */ }, refreshMessage: "Rafraîchissez la page pour voir les nouveaux paramètres !" },
    si: { earliestQueue: { title: "Izberite število najzgodnejših čakalnih vrst." }, soonFinished: { title: "Izberite časovni prag, pod katerim so označeni odštevalni časi gradnj, ki bodo kmalu končane." }, emptyQueues: { title: "Označi prazne čakalne vrste gradenj" /* without "." */ }, refreshMessage: "Osvežite stran, da vidite nove nastavitve!" },
    // __: { earliestQueue: { title: "" }, soonFinished: { title: "" }, emptyQueues: { title: "" /* without "." */ }, refreshMessage: "" },
};





// Add style sheets
GM_addStyle(`

    /*** (NEW) OGame STYLES ***/

    .${OG_PLANET_MOON_TABS_CLASS_NAME} {
        position: relative !important;
        display: flex !important;
        justify-content: center !important;
        align-items: center !important;
    }

    .${OG_PLANET_QUEUES_CLASS_NAME}:not(.research) {
        height: ${planetQueuesHeight}px !important;
    }

    .${OG_QUEUE_INFOS_CLASS_NAME},
    .${OG_QUEUES_CLASS_NAME},
    .${OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME} {
        gap: 0.5% !important;
        height: ${QUEUE_HEIGHT}px !important;
        min-height: ${QUEUE_HEIGHT}px !important;
    }

    .${OG_PLANET_PRODUCTION_CLASS_NAME} .${OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME} {
        width: ${QUEUE_WIDTH.planet}% !important;
    }
    .${OG_MOON_PRODUCTION_CLASS_NAME} .${OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME} {
        width: ${QUEUE_WIDTH.moon}% !important;
    }

    .${OG_SINGLE_QUEUES_CLASS_NAME} {
        overflow: hidden !important;
    }
    .${OG_LINK_DETAILS_CLASS_NAME},
    .${OG_LINKS_CLASS_NAME} {
        overflow: hidden !important; /* <-------------- hides overflow content */
        text-overflow: ellipsis !important; /* <------- add ellipsis, "...", to represent cliped text */
        white-space: nowrap !important; /* <----------- prevents the text from wrapping to a new line */
        justify-content: flex-start !important; /* <--- horizontal alignment <flex> */
        align-items: center !important; /* <----------- vertical alignment <flex> */
    }

    .${OG_PRODUCTION_LEVELS_CLASS_NAME} {
        color: ${COLORS.earliest[0]} !important;
    }

    .${OG_BOX_TITLES_CLASS_NAME} {
        white-space: nowrap !important;
        overflow: hidden !important;
        max-width: 94% !important;
    }
    .${OG_BOX_TITLES_CLASS_NAME}::before,
    .${OG_BOX_TITLES_CLASS_NAME}::after {
        min-width: 0px !important;
        margin-right: 2px !important;
        margin-left: 2px !important;
    }



    /*** IPO TAB BADGES ***/

    .${IPO_TAB_BADGES_CLASS_NAME},
    .${IPO_TAB_BADGES_EMPTY_QUEUES_CLASS_NAME} {
        position: absolute;
        display: flex;
        top: -25%;
        height: ${BADGES_SIZE}px;
        box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
        justify-content: center;
        align-items: center;
        color: white;
        font-size: ${BADGES_SIZE - 7}px;
        font-weight: bold;
        text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
    }
    .${IPO_TAB_BADGES_CLASS_NAME} {
        width: ${BADGES_SIZE}px;
        border-radius: 50%;
    }
    .${IPO_TAB_BADGES_EMPTY_QUEUES_CLASS_NAME} {
        left: ${BADGES_MARGIN + 1}px;
        border-radius: 3px;
        padding: 0 6px;
    }



    /*** IPO SETTINGS ***/

    #${IPO_SETTINGS_ID} {
        margin: 14px 7px 0 7px;
        border-radius: 3px;
        border: 2px solid ${COLORS.og.productionOverview.border};
        padding: 10px 10px 8px 10px;
        background: linear-gradient(to top,  ${COLORS.og.productionOverview.border} 0%, ${COLORS.og.productionOverview.highlight} 100%);
        box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
        color: ${COLORS.og.productionOverview.text};
    }

    #${IPO_SETTINGS_ID} table {
        /*width: 100%;*/
    }
    #${IPO_SETTINGS_ID} table td {
        vertical-align: middle;
        align-items: center;
    }

    .${IPO_SETTINGS_HEADER_CLASS_NAME} {
        position: absolute;
        top: -18px;
        left: 0;
        color: ${COLORS.og.blue};
        text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
    }

    .${IPO_SETTINGS_BUTTON_LABEL_WRAPPER_CLASS_NAME} {
        display: flex;
        align-items: center;
        margin-left: 10px;
    }

    .${IPO_LABEL_CLASS_NAME} {
        margin-right: 4px;
        cursor: default;
    }

    .${IPO_BUTTONS_CLASS_NAME} {
        display: inline-flex;
        cursor: pointer;
        margin-right: 5px;
        width: ${BUTTON_WIDTH}px;
        height: 18px;
        border: 1px solid ${COLORS.og.productionOverview.text};
        border-radius: 3px;
        background-color: transparent;
        justify-content: center;
        align-items: center;
        color: white;
    }
    .${IPO_BUTTONS_CLASS_NAME}:hover {
        border-color: white;
    }

    .${IPO_TOGGLE_BUTTONS_CLASS_NAME} {
        display: inline-flex;
        cursor: pointer;
        margin-right: 5px;
        width: ${TOGGLE_BUTTON_WIDTH}px;
        height: 18px;
        border: 1px solid ${COLORS.og.productionOverview.text};
        border-radius: 3px;
    }
    .${IPO_TOGGLE_BUTTONS_CLASS_NAME}:hover {
        border-color: white;
    }
    .${IPO_TOGGLE_BUTTONS_CLASS_NAME}.active {
        background-color: ${COLORS.empty};
    }
    .${IPO_TOGGLE_BUTTONS_CLASS_NAME}.inactive {
        background-color: transparent;
    }

    #${IPO_REFRESH_MESSAGE_ID} {
        display: flex;
        margin: 4px 7px 0 7px;
        border-radius: 3px;
        border: 2px solid ${COLORS.og.productionOverview.border};
        padding: 10px;
        background: linear-gradient(to top, ${COLORS.og.productionOverview.border} 0%, ${COLORS.og.productionOverview.highlight} 100%);
        box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
        color: ${COLORS.og.productionOverview.text};
        transition: max-height 0.5s ease-out, opacity 0.5s ease-out;
        opacity: 0;
    }

`);





// Initialize local storage object with default values if it does not already exist
const IPO_HIGHLIGHT_EMPTY_QUEUES_LOCAL_STORAGE = "IPO_settings";
const DEFAULT_IPO_SETTINGS = {
    version: 1,
    n: 1,
    soonFinished: 1, // [h]
    highlightEmpty: {
        research: [true],
        planet: [true, true, false, false],
        moon: [true, false],
    },
};
let ipoSettingsCheck = localStorage.getItem(IPO_HIGHLIGHT_EMPTY_QUEUES_LOCAL_STORAGE);
if (!ipoSettingsCheck || JSON.parse(ipoSettingsCheck).version !== DEFAULT_IPO_SETTINGS.version) {
    setIpoSettings (DEFAULT_IPO_SETTINGS);
}





// Set settings from local storage
let ipoSettings = getIpoSettings();

// Get (user selected) language from cookies; if there is no locales for that languge, use default language (English)
const cookieMatch = document.cookie.match(/oglocale=([^;]+)/);
let lang = cookieMatch ? cookieMatch[1] : "en";
if (!LOCALES[lang]) {
    lang = "en";
}

// Get and assign locales from OGame page
let locales = getLocales(); //* DEBUG */ console.log("LOCALES:", LOCALES[lang], "\nlocales:", locales);

// Get current time/timestamp
const currentTimestamp = getCurrentTimestamp(); //* DEBUG */ console.debug("Current timestamp:", currentTimestamp);

// Remove <span> element from production timer while keeping production timer intact
let productionTimers = document.getElementsByClassName(OG_PRODUCTION_TIMERS_CLASS_NAME);
for (let i = 0; i < productionTimers.length; i++) {
    productionTimers[i].getElementsByTagName("span")[0].remove();
}

// Remove (planet) production names while keeping its levels intact
let planetProduction = document.getElementsByClassName(OG_PLANET_PRODUCTION_CLASS_NAME)[0];
let productionNames = planetProduction ? planetProduction.getElementsByClassName(OG_PRODUCTION_NAMES_CLASS_NAME) : undefined;
if (productionNames) {
    for (let i = 0; i < productionNames.length; i++) {
        let productionName = productionNames[i];
        for (let j = 0; j < productionName.childNodes.length; j++) {
            let node = productionName.childNodes[j];
            if (node.nodeType === Node.TEXT_NODE) {
                node.nodeValue = "";
            }
        }

        // Change styles
        productionName.style.color = COLORS.earliest[0];
    }
}

// Selector for countdown timer (inside production queue component, since first countdown timer is in eventbox)
const countdownTimers = document.getElementById(OG_PRODUCTION_QUEUE_COMPONENET_ID).getElementsByClassName(OG_COUNTDOWN_CLASS_NAME);

// Assign all countdown timers (their timestamps) into one array
const timestamps = assignTimestamps(countdownTimers); //* DEBUG */ console.debug("timestamps:", timestamps);

// Initialize an object to store earliest queues
let earliestQueues_planetOrMoon = { planet: [], moon: [] };

// Call functions
shrinkMechasQueues();
if (ipoSettings.n >= 1) highlightEarliestQueue(1);
if (ipoSettings.n >= 2) highlightEarliestQueue(2);
if (ipoSettings.n >= 3) highlightEarliestQueue(3);
if (ipoSettings.soonFinished !== 0) soonFinishedTimers();
if (ipoSettings.highlightEmpty.research[0] == true) highlightEmptyResearchQueue(COLORS.empty);
highlightEmptyQueues("planet");
highlightEmptyQueues("moon");
highlightDisabledConstructionQueues("planet");
highlightDisabledConstructionQueues("moon");
addTabBadges();
createSettings();










// Function to get my local storage
function getIpoSettings () {
    return JSON.parse(localStorage.getItem(IPO_HIGHLIGHT_EMPTY_QUEUES_LOCAL_STORAGE));
}

// Function to set my local storage
function setIpoSettings (ipoSettings) {
    localStorage.setItem(IPO_HIGHLIGHT_EMPTY_QUEUES_LOCAL_STORAGE, JSON.stringify(ipoSettings));
}






// Function to get current time (timestamp) from OGame clock
function getCurrentTimestamp () {

    // OGame selectior
    const ogClock = document.getElementsByClassName(OG_CLOCK_CLASS_NAME)[0]

    // Return CLIENT timestamp from OGLight if it exists (since client time is used in countdown timestamps)
    let currentTimestamp;
    let ogl_currentTimestamp = parseInt(ogClock.dataset[OGL_CLOCK_TIMESTAMP_DATASETS.TIME_CLIENT], 10); //* DEBUG */ console.debug("currentTimestamp:", currentTimestamp);
    if (ogl_currentTimestamp) {
        currentTimestamp = ogl_currentTimestamp;
    } else {
        currentTimestamp = assignCurrentTimestamp(ogClock); //* DEBUG */ console.debug("OGLight is not present! Assigning current timestamp from OGame Clock...\n\ncurrentTimestamp:", currentTimestamp);
    }
    return { client: currentTimestamp, seconds: currentTimestamp / 1000 };

}

// Function to update the <li> element with timestamp
function assignCurrentTimestamp (ogClock) {

    if (ogClock) {
        // Extract date and time from the OGame Clock
        let dateStr = ogClock.textContent.split(" ")[0];
        let timeStr = ogClock.querySelector("span").textContent;

        // Parse date and time
        let [d, m, y] = dateStr.split(".").map(Number);
        let [h, min, s] = timeStr.split(":").map(Number);

        // Create a date object ("m - 1" because months are 0-indexed)
        let date = new Date(y, m - 1, d, h, min, s);

        // Convert to timestamp [ms]
        let timestamp = date.getTime();

        // Add new attribute to OGame Clock element
        ogClock.setAttribute(IPO_TIMESTAMP_DATASET, timestamp); //* DEBUG */ console.debug("Parsed date (date):", date, "\nUnix timestamp [ms] (timestamp):", timestamp, "\n\nUpdated OGame Clock element:", ogClock);

        // Return timestamp
        return timestamp;
    }

}





// Function to get locales directly from OGame page
function getLocales () {

    // OGame selectors
    const ogBar = document.getElementById("bar");
    const ogBar_options = ogBar.getElementsByTagName("li")[5] || undefined;
    const ogTabs = document.getElementsByClassName(OG_PLANET_MOON_TABS_CLASS_NAME);
    const ogResearchHeader = document.getElementsByClassName(OG_RESEARCH_HEADER_CLASS_NAME)[0];
    const ogBoxTitles = document.getElementsByClassName(OG_BOX_TITLES_CLASS_NAME);

    // Assign locales object
    let locales = {};
    locales.options = ogBar_options.textContent || "Options";
    locales.planet = ogTabs[0].textContent || "Planets";
    locales.moon = ogTabs[1].textContent || "Moons";
    locales.researchQueue = ogResearchHeader.textContent || "Research";
    locales.planetQueues = [
        ogBoxTitles[0].textContent || "Buildings",
        ogBoxTitles[1].textContent.replace(/\n/g, "") || "Lifeform Buildings",
        ogBoxTitles[2].textContent.replace(/\n/g, "") || "Lifeform Development",
        ogBoxTitles[3].textContent || "Shipyard"
    ];
    locales.moonQueues = [
        ogBoxTitles[0].textContent || "Buildings",
        ogBoxTitles[3].textContent || "Shipyard"
    ];

    return locales;

}





// *** MECHAS QUEUES ***

// Function to change width for queues where there is extra MECHAS SHIPYARD queue
function shrinkMechasQueues () {

    // OGame selector for all queues (for all planets)
    let planetProduction = document.getElementsByClassName(OG_PLANET_PRODUCTION_CLASS_NAME)[0];
    let queues = planetProduction ? planetProduction.getElementsByClassName(OG_QUEUES_CLASS_NAME) : undefined;

    // Assign array with indexes of Mechas Shipyard queues
    let mechasQueuesIndexes = getMechasQueuesIndex(queues); //* DEBUG */ console.debug("mechasIndexes:", mechasQueuesIndexes);

    // Overwritestyles (width) for single queues for a planet with mechas queues
    mechasQueuesIndexes.forEach(index => {
        let singleQueueWithTitles = queues[index].getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME); //* DEBUG */ console.debug(singleQueueWithTitles);
        for (let i = 0; i < singleQueueWithTitles.length; i++) {
            singleQueueWithTitles[i].style.setProperty("width", QUEUE_WIDTH.mechas+"%", "important");
        }
    });

}

// Function to get indexes of queues with MECHAS SHIPYARD
function getMechasQueuesIndex (queues) {

    // Push indexes of queues where there are 5 elements (i.e. where there is extra Mechas Shipyard queue) into new array
    if (queues) {
        let mechasQueuesIndexes = [];
        for (let q = 0; q < queues.length; q++) {

            let singleQueueWithTitles = queues[q].getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME);
            if (singleQueueWithTitles.length == 5) {
                mechasQueuesIndexes.push(q);
            }

        }
        return mechasQueuesIndexes;
    } else {
        return [];
    }

}





// *** HIGHLIGHT QUEUES ***

// Function to highlight earliest construction that will be completed
function highlightEarliestQueue (n) {

    // Find smallest value in the array and its index
    let { earliestTimestamp, earliestIndex } = findEarliestTimestamp(timestamps, n);

    // Get the parent element of the countdown with the earliest timestamp and change its styles
    let earliestParentElement = countdownTimers[earliestIndex]?.closest(`.${OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME}, .${OG_PLANET_QUEUES_CLASS_NAME}`);
    if (earliestParentElement) {
        //* DEBUG */ if (n == 1) console.debug("timestamps:", timestamps, "\n\nearliestTimestamp:", earliestTimestamp, "\nearliestIndex:", earliestIndex, "\n\nearliestSingleQueueParent", earliestParentElement);

        // Highlight individual queues (planet AND moon queues)
        highlightEarliestQueue_styles (earliestParentElement, n);

        // Determine if the parent element is under planet or moon section
        let parentPlanet = earliestParentElement.closest(`.${OG_PLANET_PRODUCTION_CLASS_NAME}`);
        let parentMoon = earliestParentElement.closest(`.${OG_MOON_PRODUCTION_CLASS_NAME}`);
        if (parentPlanet) {
            earliestQueues_planetOrMoon.planet.push(n); //* DEBUG */ console.debug("#"+n+" earliest queue is under PLANET constructions");
        } else if (parentMoon) {
            earliestQueues_planetOrMoon.moon.push(n); //* DEBUG */ console.debug("#"+n+" earliest queue is under MOON constructions");
        }
    }

}

// Function to highlight countdown timers of construction near completion
function soonFinishedTimers () {

    //* DEBUG */ console.log("Current timestamp:", currentTimestamp.seconds);

    timestamps.forEach(entry => {
        //* DEBUG */ console.log("Entry: ", entry);

        if (entry.soonFinished) {
            let element = countdownTimers[entry.index];
            let parentResearch = element.closest(OG_RESEARCH_QUEUE_CLASS_NAME.QUERY_SELECTOR);

            // Style Research queue differently from all others
            if (parentResearch) {
                element.style.padding = "2px 6px";
                element.style.background = COLORS.earliest[0];
                element.style.color = "white";
                element.style.textShadow = "0 0 2px rgba(0, 0, 0, 0.5)";
            } else {
                let bgColor = COLORS.earliest[0] + "26"; // 15% transparency
                element.style.padding = "0px 3px";
                element.style.background = bgColor;
                element.style.boxShadow = "0 0 3px " + bgColor;
                element.style.color = COLORS.earliest[0];
                element.style.textShadow = "0 0 2px rgba(0, 0, 0, 0.5)";
            }
            element.style.borderRadius = "3px";

            // Add class name
            element.classList.add(IPO_SOON_FINISHED_COUNTDOWN_CLASS_NAME);
        }
    });

}

// Function to assign all countdown timers into one array
function assignTimestamps (countdownTimers) {

    let timestamps = [];
    for (let i = 0; i < countdownTimers.length; i++) {
        let timestamp = parseInt(countdownTimers[i].dataset[OG_COUNTDOWN_DATASET], 10);
        let timestampDifference = timestamp - currentTimestamp.seconds;
        let soonFinished = timestampDifference <= ipoSettings.soonFinished * 3600;
        timestamps.push({ index: i, timestamp: timestamp, timestampDifference: timestampDifference, soonFinished: soonFinished });
    }

    return timestamps;

}

// Function to find nth earliest timestamp and its index
function findEarliestTimestamp (timestamps, n) {

    let sortedTimestamps = [...timestamps].sort((a, b) => a.timestamp - b.timestamp);

    let rarliestTimestamp = sortedTimestamps[n-1]?.timestamp;
    let earliestIndex = sortedTimestamps[n-1]?.index;

    return { rarliestTimestamp, earliestIndex };

}

// Function to highlight individual elements (nth earliest completed construction)
function highlightEarliestQueue_styles (parentElement, n) {

    // Assign child element and change its styles (excluding Research)
    let singleQueue = parentElement.getElementsByClassName(OG_SINGLE_QUEUES_CLASS_NAME)[0];
    if (singleQueue) {
        highlightEarliestQueue_styles_frame(singleQueue, n);

        // Assign another child element and change its styles
        let boxTitle = parentElement.getElementsByClassName(OG_BOX_TITLES_CLASS_NAME)[0];
        if (boxTitle) {
            highlightEarliestQueue_styles_title(boxTitle, n)
        }
    }
    // ... else, including Research
    else {
        highlightEarliestQueue_styles_frame(parentElement, n);
    };

}

// Function to highlight and style title (nth earliest completed construction)
function highlightEarliestQueue_styles_title (title, n) {

    // Use last defined color in "COLORS.earliest" array if "n" is bigger than the number of elements in this array
    let colorIndex = Math.min(n-1, COLORS.earliest.length-1);

    // Styles
    title.style.top = "-9%";
    title.style.left = "3%";
    title.style.width = "auto";
    title.style.maxWidth = "94%";
    title.style.marginBottom = "-4%";
    title.style.borderRadius = "4px";
    title.style.backgroundColor = COLORS.earliest[colorIndex];
    title.style.lineHeight = "15px";
    title.style.color = "white";
    title.style.textShadow = "0 0 2px rgba(0, 0, 0, 0.5)";

    // Text content
    title.textContent = "#"+n+": " + title.textContent;

}

// Function to highlight frame (nth earliest completed construction)
function highlightEarliestQueue_styles_frame (frame, n) {

    // Use last defined color in "COLORS.earliest" array if "n" is bigger than the number of elements in this array
    let colorIndex = Math.min(n-1, COLORS.earliest.length-1);

    frame.id = `${IPO_EARLIEST_QUEUE_CLASS_NAME}_${n}`;
    frame.classList.add(IPO_EARLIEST_QUEUE_CLASS_NAME);
    frame.style.border = "1px solid " + COLORS.earliest[colorIndex] + "80"; // Aplha/transparency component, HEX values:  100% = "FF";  95% = "F2";  90% = "E6";  85% = "D9";  80% = "CC";  75% = "BF";  70% = "B3";  65% = "A6";  60% = "99";  55% = "8C";  50% = "80";  45% = "73";  40% = "66";  35% = "59";  30% = "4D";  25% = "40";  20% = "33";  15% = "26";  10% = "1A";  5% = "0D";  0% = "00".
    frame.style.boxShadow = "inset 0 0 3px " + COLORS.earliest[colorIndex] + "A6"; // 65% transparency
    frame.style.background = COLORS.earliest[colorIndex] + "33"; // 20% transparency

}





// *** EMPTY QUEUES ***

// Function to highlight empty planet or moon queues
function highlightEmptyQueues (type) {

    // Determine what to highlight, "planet" or "moon" queues section
    let queuesSectionClassName = type === "planet" ? OG_PLANET_PRODUCTION_CLASS_NAME : OG_MOON_PRODUCTION_CLASS_NAME;

    // Assign planet or moon queues
    let queuesSection = document.getElementsByClassName(queuesSectionClassName)[0];
    if (queuesSection) {

        // Assign queues within planet or moon section separately
        let queues = queuesSection.getElementsByClassName(OG_PLANET_QUEUES_CLASS_NAME);
        for (let p = 0; p < queues.length; p++) {

            // Assign "single" queues inside a given queue
            let singleQueues = queues[p].getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME);

            // Highlight empty construction queues (Buildings, Lifeform Buildings, Lifeform Development, and/or Shipyard)
            let ipoSettings = getIpoSettings();
            for (let i = 0; i < ipoSettings.highlightEmpty[type].length; i++) {
                highlightEmptyQueue_styles(type, i, singleQueues);
            };

        }

    }

}

// Function to highlight individual elements (empty queues)
function highlightEmptyQueue_styles (type, constructionIndex, queuesSelector) {

    let ipoSettings = getIpoSettings();
    if (ipoSettings.highlightEmpty[type][constructionIndex] && queuesSelector[constructionIndex].classList.contains(OG_EMPTY_QUEUE_CLASS_NAME)) {

        queuesSelector[constructionIndex].classList.add(IPO_EMPTY_QUEUE_CLASS_NAME);

        let frame = queuesSelector[constructionIndex].getElementsByClassName(OG_SINGLE_QUEUES_CLASS_NAME)[0];
        frame.style.border = "1px solid " + COLORS.empty + "40"; // 25% transparency
        frame.style.boxShadow = "inset 0 0 3px " + COLORS.empty + "59"; // 35% transparency
        frame.style.background = COLORS.empty + "1A"; // 10% transparency

        let title = queuesSelector[constructionIndex].getElementsByClassName(OG_BOX_TITLES_CLASS_NAME)[0];
        title.style.color = COLORS.empty;

        let text = queuesSelector[constructionIndex].getElementsByClassName(OG_NO_ENTRY)[0];
        text.style.color = COLORS.empty + "BF"; // 75% transparency

    }

}

// Function to highlight empty research queue
function highlightEmptyResearchQueue (color) {

    let queueCheck = document.getElementsByClassName(OG_RESEARCH_ICON_CLASS_NAME);
    if (queueCheck.length == 0) {

        let queue = document.getElementsByClassName(OG_RESEARCH_QUEUE_CLASS_NAME.CLASS_NAME)[0];
        if (queue) {
            queue.style.border = "1px solid " + color + "40";
            queue.style.boxShadow = "inset 0 0 4px " + color;
            queue.style.background = color + "1A"; // 10% transparency

            let header = document.getElementsByClassName(OG_RESEARCH_HEADER_CLASS_NAME)[0];
            header.style.color = color;

            let text = queue.getElementsByClassName(OG_NO_ENTRY)[0];
            text.style.color = color + "BF"; // 75% transparency
        }

    }
}





// *** DISABLED CONSTRUCTION QUEUES ***

// Function to check if there is any active construction that prevents other queues (like Robotics Factory that prevents Lifeform buildings, etc.)
function highlightDisabledConstructionQueues (planetType) {

    // Determine what to highlight, "planet" or "moon" queues section
    let planetProductionClassName = planetType === "planet" ? OG_PLANET_PRODUCTION_CLASS_NAME : OG_MOON_PRODUCTION_CLASS_NAME;

    // OGame selector for all queues (for all planets)
    let planetProduction = document.getElementsByClassName(planetProductionClassName)[0];
    let queues = planetProduction ? planetProduction.getElementsByClassName(OG_QUEUES_CLASS_NAME) : undefined;

    // Loop through all planets
    for (let q = 0; q < queues.length; q++) {

        // Define the queues to check (differently for planet and moon)
        const queuesToCheck = [
            { queue: queues[q] ? queues[q].getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME)[0] : undefined, constructionType: "nonLF" }
        ];
        if (planetType === "planet") queuesToCheck.push({ queue: queues[q] ? queues[q].getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME)[1] : undefined, constructionType: "LF" });

        // Loop through each queue
        queuesToCheck.forEach(({ queue, constructionType }) => {
            if (queue) {

                // OGame selecotor for element that contains construciton icon
                let techIcon = queue.getElementsByTagName(OG_PRODUCTION_ICONS_TAG_NAME)[0];
                if (techIcon && techIcon.attributes.length > 1) {

                    // Fetch name of second (!) attribute (second attribute is the name of the construction)
                    let attributeName = techIcon.attributes[1].name; //* DEBUG */ console.debug("Planet index:", q, "| Second/construction attribute name:", attributeName);

                    // Check for nonLF and LF construction types
                    let isNonLF_match = Object.values(OG_CONSTRUCTION_ATTRIBUTE_NAMES.nonLF).includes(attributeName);
                    let isLF_match = planetType === "planet" && constructionType === "LF" && OG_CONSTRUCTION_ATTRIBUTE_NAMES.LF.LF_RESEARCH_CENTRE.test(attributeName);
                    let ifLaboratory_match = planetType === "planet" && constructionType === "nonLF" && attributeName === OG_CONSTRUCTION_ATTRIBUTE_NAMES.nonLF.RESEARCH_LAB; //* DEBUG */ if (ifLaboratory_match) console.debug("--> ifLaboratory_match (attribute name: \""+OG_CONSTRUCTION_ATTRIBUTE_NAMES.nonLF.RESEARCH_LAB+"\"):", ifLaboratory_match,);

                    if (isNonLF_match || isLF_match) {

                        let disabledQueueIndexes = [];
                        if (isNonLF_match) {
                            let nonLFKey = Object.keys(OG_CONSTRUCTION_ATTRIBUTE_NAMES.nonLF).find(key => OG_CONSTRUCTION_ATTRIBUTE_NAMES.nonLF[key] === attributeName);
                            disabledQueueIndexes = disabledQueueIndexes.concat(DISABLED_CONSTRUCION_QUEUES_INDEXES[planetType].nonLF[nonLFKey] || []);
                        }
                        if (isLF_match) {
                            disabledQueueIndexes = disabledQueueIndexes.concat(DISABLED_CONSTRUCION_QUEUES_INDEXES[planetType].LF.LF_RESEARCH_CENTRE || []);
                        }

                        // Process all disabled indexes
                        disabledQueueIndexes.forEach(index => {
                            let removedEmptyQueue = queues[q]?.getElementsByClassName(OG_SINGLE_QUEUE_WITH_BOX_TITLE_CLASS_NAME)[index];
                            if (removedEmptyQueue) {
                                removeClassName(removedEmptyQueue, IPO_EMPTY_QUEUE_CLASS_NAME);
                                highlightDisabledConstructionQueues_styles(removedEmptyQueue, COLORS.emptyRemoved);
                            }
                        });

                    }

                    // Research check
                    if (ifLaboratory_match) {
                            highlightEmptyResearchQueue(COLORS.emptyRemoved);

                    }

                }

            }
        });
    }
}

// Function to remove the class name
function removeClassName (element, className) {

    if (element.classList.contains(className)) {
        element.classList.remove(className); //* DEBUG */ console.debug("--> Class \""+className+"\" removed from element. (removeClassName())");
    }

}

// Function to highlight individual elements (empty queues)
function highlightDisabledConstructionQueues_styles (element, color) {

    //* DEBUG */ console.debug("--> element (highlightDisabledConstructionQueues_styles()):", element);

    let frame = element.getElementsByClassName(OG_SINGLE_QUEUES_CLASS_NAME)[0];
    frame.style.border = "1px solid " + color + "40"; // 25% transparency
    frame.style.boxShadow = "inset 0 0 3px " + color + "59"; // 35% transparency
    frame.style.background = color + "1A"; // 10% transparency

    let title = element.getElementsByClassName(OG_BOX_TITLES_CLASS_NAME)[0];
    title.style.color = color;

    let text = element.getElementsByClassName(OG_NO_ENTRY)[0];
    text.style.color = color + "BF"; // 75% transparency

}





// *** TAB BADGES ***

// Function to add a badge in planet/moon tabs
function addTabBadges () {

    // OGame selector
    const ogTabs = document.getElementsByClassName(OG_PLANET_MOON_TABS_CLASS_NAME);

    // Planet
    for (let i = earliestQueues_planetOrMoon.planet.length - 1; i >= 0; i--) {
        //* DEBUG */ console.debug("earliestQueues_planetOrMoon.planet[" + i + "]:", earliestQueues_planetOrMoon.planet[i]);
        createBadgeForTab(ogTabs[0], earliestQueues_planetOrMoon.planet[i]);
    }
    createBadgeForTab_emptyQueues(ogTabs[0], "planet");

    // Moon
    for (let i = earliestQueues_planetOrMoon.moon.length - 1; i >= 0; i--) {
        //* DEBUG */ console.debug("earliestQueues_planetOrMoon.moon[" + i + "]:", earliestQueues_planetOrMoon.moon[i]);
        createBadgeForTab(ogTabs[1], earliestQueues_planetOrMoon.moon[i]);
    }
    createBadgeForTab_emptyQueues(ogTabs[1], "moon");


}

// Function to create badge with number (earliest construction) to the tab
function createBadgeForTab (tab, n) {

    // Early check if tab exists at all
    if (!tab) { console.error("IPO error: OGame tabs are not found!"); return; }

    // Use last defined color in "COLORS.earliest" array if "n" is bigger than the number of elements in this array
    let colorIndex = Math.min(n-1, COLORS.earliest.length-1);

    // Create the badge element
    const badge = document.createElement("div");
    badge.className = IPO_TAB_BADGES_CLASS_NAME;
    badge.style.backgroundColor = COLORS.earliest[colorIndex];
    badge.textContent = n;

    // Adjust the position if there is already badge present
    const BADGE_SPACING = BADGES_SIZE + 4;
    let existingBadges = tab.getElementsByClassName(IPO_TAB_BADGES_CLASS_NAME);
    if (existingBadges) {
        badge.style.right = `${BADGES_MARGIN + existingBadges.length * BADGE_SPACING}px`;
    }

    // Append the badge to the tab
    tab.appendChild(badge);

}

// Function to create badge for number of empty queues
function createBadgeForTab_emptyQueues (tab, type) {

    // Early check if tab exists at all
    if (!tab) { console.error("IPO error: OGame tabs are not found!"); return; }

    // Get how many empty queeus are there, seperately on planets and moons
    let emptyQueues = type === "planet" ? document.getElementsByClassName(OG_PLANET_PRODUCTION_CLASS_NAME)[0].getElementsByClassName(IPO_EMPTY_QUEUE_CLASS_NAME).length : document.getElementsByClassName(OG_MOON_PRODUCTION_CLASS_NAME)[0].getElementsByClassName(IPO_EMPTY_QUEUE_CLASS_NAME).length;

    // Create the badge elements if there are any empty queues
    if (emptyQueues > 0) {
        const badge = document.createElement("div");
        badge.className = IPO_TAB_BADGES_EMPTY_QUEUES_CLASS_NAME;
        badge.style.backgroundColor = COLORS.empty;
        badge.textContent = emptyQueues;

        // Append the badge to the tab
        tab.appendChild(badge);
    }

}





// *** SETTINGS ***

// Function to create settings
function createSettings () {

    // Create the main <div> element
    let div = document.createElement("div");
    div.id = IPO_SETTINGS_ID;

    // Create the <table>, <tbody>, and <tr> element
    let table = document.createElement("table");
    let tbody = document.createElement("tbody");
    let tr = document.createElement("tr");



    // *** SETTINGS HEADER ***

    // Create <td> element, zero width table cell
    let settingsHeader_td = document.createElement("td");
    settingsHeader_td.style.position = "relative";

    // Create <span> element
    let settingsHeader_span = document.createElement("span");
    settingsHeader_span.className = IPO_SETTINGS_HEADER_CLASS_NAME;
    settingsHeader_span.textContent = locales.options;

    // Append elements
    settingsHeader_td.appendChild(settingsHeader_span);
    tr.appendChild(settingsHeader_td);



    // *** NUMBER OF EARLIEST QUEUES ***

    // Create <td> element
    let earliestQueue_td = document.createElement("td");
    earliestQueue_td.style.position = "relative";

    // Create <span> element
    let earliestQueue_span = document.createElement("span");
    earliestQueue_span.className = IPO_BUTTONS_CLASS_NAME;
    earliestQueue_span.dataset.value = ipoSettings.n;
    earliestQueue_span.textContent = "#" + ipoSettings.n;
    earliestQueue_span.title = LOCALES[lang].earliestQueue.title;

    // Onclick funciton
    earliestQueue_span.addEventListener("click", () => {
        toggleEarliestQueueValue(earliestQueue_span);
        showRefreshMessage();
    });

    // Append elements
    earliestQueue_td.appendChild(earliestQueue_span);
    tr.appendChild(earliestQueue_td);



    // *** HIHGLIGHT CONSTRUCTION NEAR COMPLETION ***

    // Create <td> element
    let soonFinished_td = document.createElement("td");

    // Create <span> element
    let soonFinished_span = document.createElement("span");
    soonFinished_span.className = IPO_BUTTONS_CLASS_NAME;
    soonFinished_span.dataset.value = ipoSettings.soonFinished;
    soonFinished_span.textContent = ipoSettings.soonFinished + "h";
    soonFinished_span.title = LOCALES[lang].soonFinished.title;

    // Onclick function
    soonFinished_span.addEventListener("click", () => {
        toggleSoonFinishedValue(soonFinished_span);
        showRefreshMessage();
    });

    // Append elements
    soonFinished_td.appendChild(soonFinished_span);
    tr.appendChild(soonFinished_td);



    // *** EMPTY QUEUES ***

    // *** RESEARCH (EMPTY QUEUE) ***

    // Create <td> element
    let researchEmptyQueue_td = document.createElement("td");
    researchEmptyQueue_td.style.position = "relative";

    // Create a wrapper <div> element [flex]
    let researchEmptyQueue_wrapper = document.createElement("div");
    researchEmptyQueue_wrapper.className = IPO_SETTINGS_BUTTON_LABEL_WRAPPER_CLASS_NAME;

    // Create <span> element for label
    let researchEmptyQueue_label = document.createElement("span");
    researchEmptyQueue_label.className = IPO_LABEL_CLASS_NAME;
    researchEmptyQueue_label.style.marginLeft = "10px";
    researchEmptyQueue_label.textContent = locales.researchQueue + ": ";
    researchEmptyQueue_label.title = LOCALES[lang].emptyQueues.title + " (" + locales.researchQueue + ").";

    // Create <button> element for toggle
    let researchEmptyQueue_toggleButton = document.createElement("button");
    researchEmptyQueue_toggleButton.className = IPO_TOGGLE_BUTTONS_CLASS_NAME;
    researchEmptyQueue_toggleButton.dataset.type = "research";
    researchEmptyQueue_toggleButton.title = locales.researchQueue;

    // Update color for the toggle button
    updateToggleButtonColor(researchEmptyQueue_toggleButton, "research", 0);

    // Onclick function
    researchEmptyQueue_toggleButton.addEventListener("click", () => {
        toggleQueueState(researchEmptyQueue_toggleButton, "research", 0);
        showRefreshMessage();
    });

    // Append elements
    researchEmptyQueue_wrapper.appendChild(researchEmptyQueue_label);
    researchEmptyQueue_wrapper.appendChild(researchEmptyQueue_toggleButton);
    researchEmptyQueue_td.appendChild(researchEmptyQueue_wrapper);
    tr.appendChild(researchEmptyQueue_td);



    // *** PLANETS (EMPTY QUEUES) ***

    // Create <td> element
    let planetEmptyQueues_td = document.createElement("td");

    // Create a wrapper <div> element [flex]
    let planetEmptyQueues_wrapper = document.createElement("div");
    planetEmptyQueues_wrapper.className = IPO_SETTINGS_BUTTON_LABEL_WRAPPER_CLASS_NAME;

    // Create <span> element for label and append it
    let planetsEmptyQueues_label = document.createElement("span");
    planetsEmptyQueues_label.className = IPO_LABEL_CLASS_NAME;
    planetsEmptyQueues_label.textContent = locales.planet + ": ";
    planetsEmptyQueues_label.title = LOCALES[lang].emptyQueues.title + " (" + locales.planet + ").";
    planetEmptyQueues_wrapper.appendChild(planetsEmptyQueues_label);

    // Create <button> elements (4 times)
    for (let i = 0; i < 4; i++) {
        let button = document.createElement("button");
        button.className = IPO_TOGGLE_BUTTONS_CLASS_NAME;
        button.dataset.index = i;
        button.dataset.type = "planet";
        button.title = locales.planetQueues[i];
        updateToggleButtonColor(button, "planet", i);
        button.addEventListener("click", () => {
            toggleQueueState(button, "planet", i);
            showRefreshMessage();
        });
        planetEmptyQueues_wrapper.appendChild(button);
    }

    // Append elements
    planetEmptyQueues_td.appendChild(planetEmptyQueues_wrapper);
    tr.appendChild(planetEmptyQueues_td);



    // *** MOONS (EMPTY QUEUES) ***

    // Create <td> element
    let moonEmptyQueues_td = document.createElement("td");

    // Create a wrapper <div> element [flex]
    let moonEmptyQueues_wrapper = document.createElement("div");
    moonEmptyQueues_wrapper.className = IPO_SETTINGS_BUTTON_LABEL_WRAPPER_CLASS_NAME;

    // Create <span> element for label and append it
    let moonEmptyQueues_label = document.createElement("span");
    moonEmptyQueues_label.className = IPO_LABEL_CLASS_NAME;
    moonEmptyQueues_label.textContent = locales.moon + ": ";
    moonEmptyQueues_label.title = LOCALES[lang].emptyQueues.title + " (" + locales.moon + ").";
    moonEmptyQueues_wrapper.appendChild(moonEmptyQueues_label);

    // Create <button> elements (2 times)
    for (let i = 0; i < 2; i++) {
        let button = document.createElement("button");
        button.className = IPO_TOGGLE_BUTTONS_CLASS_NAME;
        button.dataset.index = i;
        button.dataset.type = "moon";
        button.title = locales.moonQueues[i] + " (" + locales.moon + ")";
        updateToggleButtonColor(button, "moon", i);
        button.addEventListener("click", () => {
            toggleQueueState(button, "moon", i);
            showRefreshMessage();
        });
        moonEmptyQueues_wrapper.appendChild(button);
    }

    // Append elements
    moonEmptyQueues_td.appendChild(moonEmptyQueues_wrapper);
    tr.appendChild(moonEmptyQueues_td);



    // *** REFRESH MESSAGE ***

    // Add the refresh message element (hidden by default)
    let refreshMessage = document.createElement("div");
    refreshMessage.id = IPO_REFRESH_MESSAGE_ID;
    refreshMessage.textContent = LOCALES[lang].refreshMessage;



    // Append table elements to main <div>
    tbody.appendChild(tr);
    table.appendChild(tbody);
    div.appendChild(table);

    // Append main <div> element and "refresh message" to the parent OGame element
    let appendLocation = document.getElementById(OG_PRODUCTION_QUEUE_COMPONENET_ID);
    appendLocation.appendChild(div);
    appendLocation.appendChild(refreshMessage);

}

// Function to show the refresh message after any button is pressed
function showRefreshMessage () {

    let refreshMessage = document.getElementById(IPO_REFRESH_MESSAGE_ID);
    if (refreshMessage) {
        refreshMessage.style.opacity = "1";
    }

}

// Function to toggle the queue state in local storage
function toggleQueueState (button, type, index) {

    // Get local storage object
    let ipoSettings = getIpoSettings();

    // Toggle settings
    ipoSettings.highlightEmpty[type][index] = !ipoSettings.highlightEmpty[type][index];
    setIpoSettings(ipoSettings)
    updateToggleButtonColor(button, type, index);

}

// Function to update the button color based on its state
function updateToggleButtonColor (button, type, index) {

    // Get local storage object
    let ipoSettings = getIpoSettings();

    // Change styles
    if (ipoSettings.highlightEmpty[type][index]) {
        button.classList.add("active");
        button.classList.remove("inactive");
    } else {
        button.classList.add("inactive");
        button.classList.remove("active");
    }

}

// Function to toggle the value of n (nth earliest queue)
function toggleEarliestQueueValue (span) {

    // Get current settings
    let ipoSettings = getIpoSettings();

    // Advance to the next value; possible values: 0, 1, 2, and 3
    ipoSettings.n = (ipoSettings.n + 1) % 4;

    // Save new settings to local storage
    setIpoSettings(ipoSettings);

    // Display new value
    span.dataset.value = ipoSettings.n;
    span.textContent = "#" + ipoSettings.n;

}

// Function to toggle the value of "soonFinished"
function toggleSoonFinishedValue (span) {

    // Get current settings
    let ipoSettings = getIpoSettings();

    // Define possible values of "soonFinished" [hours]
    let options = [0, 1, 8, 24];

    // Advace value of the "soonFinished" to the next index
    let currentIndex = options.indexOf(ipoSettings.soonFinished);
    ipoSettings.soonFinished = options[(currentIndex + 1) % options.length];

    // Save new settings to local storage
    setIpoSettings(ipoSettings);

    // Display new value
    span.dataset.value = ipoSettings.soonFinished;
    span.textContent = ipoSettings.soonFinished + "h";

}