Eternity Tower Layout Tweaks

Tweaks the layout and formatting of the Eternity Tower web game

// ==UserScript==
// @name          Eternity Tower Layout Tweaks
// @icon          https://www.eternitytower.net/favicon.png
// @namespace     http://mean.cloud/
// @version       1.15
// @description   Tweaks the layout and formatting of the Eternity Tower web game
// @match         *://eternitytower.net/*
// @match         *://www.eternitytower.net/*
// @match         http://localhost:3000/*
// @author        psouza4@gmail.com
// @copyright     2018-2023, MeanCloud
// @run-at        document-end
// @grant         unsafeWindow
// ==/UserScript==


////////////////////////////////////////////////////////////////
////////////// ** SCRIPT GLOBAL INITIALIZATION ** //////////////
ET_LayoutTweaksMod_Settings = {};
ET_LayoutTweaksMod_setupCombatListeners = false;
ET_LayoutTweaksMod_resetSpellDisplay = true;
function startup() { ET_LayoutTweaksMod(); }
////////////////////////////////////////////////////////////////


ET_LayoutTweaksMod = function()
{
    ET.MCMF.Ready(function()
    {
        setTimeout(ET_LayoutTweaksMod_Content, 1);
        setTimeout(ET_LayoutTweaksMod_PageScanner, 1);
    });

    ET.MCMF.Loaded(function()
    {
        Package.meteor.Meteor.connection._stream.on('message', function(sMeteorRawMessage)
        {
            try
            {
                let meteorMessage = JSON.parse(sMeteorRawMessage);

                // if abilities change at all (equipped/unequipped, cooldown update, level change, etc.), refresh the equipment and ability display in the combat lobby
                if ((meteorMessage.msg === "changed") && (meteorMessage.collection === "abilities"))
                    ET_LayoutTweaksMod_resetSpellDisplay = true;

                // if items change in a way that affects their .equipped status, refresh the equipment and ability display in the combat lobby
                if ((meteorMessage.msg === "changed") && (meteorMessage.collection === "items") && (sMeteorRawMessage.indexOf("\"fields\":{\"equipped\":") !== -1))
                    ET_LayoutTweaksMod_resetSpellDisplay = true;

                // if items change in a way that affects their .hidden status, refresh the equipment and ability display in the combat lobby
                if ((meteorMessage.msg === "changed") && (meteorMessage.collection === "items") && (sMeteorRawMessage.indexOf("\"fields\":{\"hid") !== -1))
                    ET_LayoutTweaksMod_resetSpellDisplay = true;
            }
            catch (err) { }
        });
    });

    ET.MCMF.EventSubscribe("ET:combatStart", function()
    {
        if (!ET_LayoutTweaksMod_setupCombatListeners)
        {
            ET_LayoutTweaksMod_setupCombatListeners = true;

            ET.MCMF.EventSubscribe("ET:combatWon", function()
            {
                ET_LayoutTweaksMod_DisableLobbyButtons();
                setTimeout(function() { ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup(true); }, 1);
            });

            ET.MCMF.EventSubscribe("ET:combatLost", function()
            {
                ET_LayoutTweaksMod_DisableLobbyButtons();
                setTimeout(function() { ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup(true); }, 1);
            });
        }
    });
};

ET_LayoutTweaksMod_HackyRedirect = function()
{
    $("#MCTempRedirect").click();
    $("#MCTempRedirect").remove();
};

ET_LayoutTweaksMod_HackyClicker = function(selector_to_click)
{
    $(selector_to_click).click();
};

/*
ET_LayoutTweaksMod_MutationObserver = undefined;

ET_LayoutTweaksMod_SetupMutationObserver = function()
{
    if (ET_LayoutTweaksMod_MutationObserver !== undefined)
    {
        ET_LayoutTweaksMod_MutationObserver.takeRecords();
        ET_LayoutTweaksMod_MutationObserver.disconnect();
    }

    ET_LayoutTweaksMod_MutationObserver = new MutationObserver(function(mutations, observer)
    {
        mutations.forEach(function(mutation)
        {
           for (let i = 0; i < mutation.addedNodes.length; i++)
           {
               try
               {
                   if (mutation.addedNodes[i].id !== undefined)
                       if (mutation.addedNodes[i].id.length === 17)
                            ET_LayoutTweaksMod_CondenseChat(jQ("div#" + mutation.addedNodes[i].id));
               }
               catch (err) { }
           }
        });
    }).observe(document.querySelector("div.direct-chat-messages div.scroll-height"), { childList: true, attributeFilter: ['div'] });
};
*/

// Do these things once
ET_LayoutTweaksMod_Content = function()
{
    let blazeWait = setInterval(function()
    {
        if (unsafeWindow.Blaze)
        {
            clearInterval(blazeWait);

            let meteorWait = setInterval(function()
            {
                if ((unsafeWindow.Router) && (unsafeWindow.Meteor))
                {
                    clearInterval(meteorWait);

                    document.PlayerAbilities = [];
                    Meteor.call('abilities.fetchLibraryExtra', function(err, data) {
                      if (err) {
                        console.log(err);
                      } else {
                        document.PlayerAbilities = jQ.makeArray(data);
                        //console.log(document.PlayerAbilities);
                      }
                    });


                    Router.onAfterAction(function()
                    {
                        //ET_LayoutTweaksMod_CombatLobby_page_rendered = false;
                    });

                    Router.onRun(function()
                    {
                        this.next();
                    });

                    Blaze._getTemplate("lobbyPage").onCreated(function()
                    {
                        ET_LayoutTweaksMod_AutoSelectFloorRoom(this, false);
                    });

                    Blaze._getTemplate("lobbyPage").onRendered(function()
                    {
                        setTimeout(ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup, 1);
                        ET_LayoutTweaksMod_AutoSelectFloorRoom(this, true);
                    });

                    Blaze._getTemplate("lobbyUnit").onRendered(function()
                    {
                        setTimeout(ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup, 1);

                        // if the lobby units are re-rendered, refresh the equipment and ability display in the combat lobby
                        ET_LayoutTweaksMod_resetSpellDisplay = true;
                    });

                    Blaze._getTemplate("overviewPage").onRendered(function()
                    {
                        ET.MCMF.Log("overviewPage rendered");
                        setTimeout(ET_LayoutTweaksMod_PageScanner_main, 1);
                    });

                    //// add new styles for chats
                    //jQ("body").append("<style type=\"text/css\">\r\nspan.direct-chat-timestamp { margin-left: 0px !important; color: #999 !important; }\r\n</style>\r\n");
                    //
                    // set up listener for re-rendered chat window
                    //Blaze._getTemplate("chatWindow").onRendered(function()
                    //{
                    //    // patch old rendered chat messages
                    //    ET_LayoutTweaksMod_CondenseAllChats();
                    //
                    //    // re-set up listener for newly-rendered chat messages
                    //    ET_LayoutTweaksMod_SetupMutationObserver();
                    //});
                    //
                    //// add new styles for our modals
                    //jQ("body").append("<style type=\"text/css\">\r\n" +
                    //
                    //    ".LTMod-modal { \r\n" +
                    //    "    display: none; /* Hidden by default */ \r\n" +
                    //    "    position: fixed; /* Stay in place */ \r\n" +
                    //    "    z-index: 1; /* Sit on top */ \r\n" +
                    //    "    left: 0; \r\n" +
                    //    "    top: 0; \r\n" +
                    //    "    width: 100%; /* Full width */ \r\n" +
                    //    "    height: 100%; /* Full height */ \r\n" +
                    //    "    overflow: auto; /* Enable scroll if needed */ \r\n" +
                    //    "    background-color: rgb(0,0,0); /* Fallback color */ \r\n" +
                    //    "    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ \r\n" +
                    //    "} \r\n" +
                    //
                    //    "</style>\r\n");

                    jQ("div.body-content").prepend("<div id=\"LayoutTweaksModal\" class=\"LTMod-modal\"></div>")

                    window.addEventListener("click", function(ev)
                    {
                        if (ev.target === document.getElementsByClassName("LTModSpellCraftingModal")[0])
                            ET_LayoutTweaksMod_CloseModals();
                        if (ev.target === document.getElementsByClassName("LTModEquipmentModal")[0])
                            ET_LayoutTweaksMod_CloseModals();
                        if (ev.target === document.getElementsByClassName("LTModAbilityModal")[0])
                            ET_LayoutTweaksMod_CloseModals();
                    });
                }
            }, 20);
        }
    }, 20);

    let htmlSafeWait = setInterval(function()
    {
        if ((jQ("a[href=\"/mining\"].nav-link").length !== 0) && (jQ("div.direct-chat-messages div.scroll-height").length !== 0) && (ET.MCMF.UserName !== undefined))
        {
            clearInterval(htmlSafeWait);

            // set up listener for newly-rendered chat messages
            //ET_LayoutTweaksMod_SetupMutationObserver();

            // set gems display to be a link to the shop
            /*
            jQ("div#navbarSupportedContent").find("img[src=\"/icons/gems.svg\"]").parent().css("cursor", "pointer").on('click', function()
            {
                // silly, but onClick events can't trigger click events, so here's a hack.. probably a better way to navigate to another template in Blaze, but this is fine
                let sDestinationURI = unsafeWindow.location.protocol + "//" + unsafeWindow.location.hostname + "/shop";
                jQ("body").append("<a href=\"" + sDestinationURI + "\" id=\"MCTempRedirect\"></a>");
                setTimeout(ET_LayoutTweaksMod_HackyRedirect, 10);
            });
            */

            /*
            // add the chat tab back to the main navigation bar
            jQ("a[href=\"/shop\"].nav-link").parent().before("<li class=\"nav-item\"><a class=\"nav-link\" href=\"/chat\">Chat</a></li>");
            */

            /*
            // remove the chat link from the personal options menu
            jQ("a[href=\"/chat\"].dropdown-item").remove();
            */

            /*
            // remove the achievements link from the personal options menu (it's already on the home page)
            jQ("a[href=\"/achievements\"].dropdown-item").remove();
            */

            /*
            // add the skills tab back to the main navigation bar
            jQ("a[href=\"/shop\"].nav-link").parent().before("<li class=\"nav-item\"><a class=\"nav-link\" href=\"/skills\">Skills</a></li>");
            */

            /*
            // add personal profile tab to the main navigation bar
            jQ("a[href=\"/skills\"].nav-link").parent().before("<li class=\"nav-item\"><a class=\"nav-link\" href=\"/profile/" + ET.MCMF.UserName + "\">Profile</a></li>");
            */

            // change the new combat tab in the main navigation bar to say 'new'
            ////jQ("a[href=\"/newCombat\"].nav-link").text("Battle (new)");

            // add the old combat tab back to the main navigation bar
            ////jQ("a[href=\"/newCombat\"].nav-link").parent().after("<li class=\"nav-item\"><a class=\"nav-link\" href=\"/combat\">Battle (old)</a></li>");

            /*
            // remove the shop link from the main navigation bar
            jQ("a[href=\"/shop\"].nav-link").remove();
            */
        }
    }, 20);
};

ET_LayoutTweaksMod_DisableLobbyButtons = function()
{
return; // doesn't seem to be working correctly -- let's leave the triggers in, but disable the functionality for now

    if ((!ET.MCMF.IsNewCombatTab()) || (ET.MCMF.GetActiveTabSection() !== "group"))
        return;

    if (jQ(".LayoutTweaksHiddenLobby").length === 0)
    {
        jQ(".lobby-container").addClass("LayoutTweaksHiddenLobby");

        if (jQ(".LayoutTweaksTempLobby").length === 0)
            jQ(".LayoutTweaksHiddenLobby").before("<div class=\"LayoutTweaksTempLobby\"><div class=\"d-flex align-items-center justify-content-center flex-wrap lobby-units-container\"><br /><br /><br /><br /><br />Lobby is loading . . .</div></div>");

        jQ(".LayoutTweaksHiddenLobby").find("button").addClass("disabled");
        jQ(".LayoutTweaksHiddenLobby").hide();

        jQ(".LayoutTweaksTempLobby").addClass("container").addClass("lobby-container").addClass("mt-2");
    }

    ET_LayoutTweaksMod_CombatLobby_page_rendered = false;
};

ET_LayoutTweaksMod_EnableLobbyButtons = function()
{
return; // doesn't seem to be working correctly -- let's leave the triggers in, but disable the functionality for now

    if ((!ET.MCMF.IsNewCombatTab()) || (ET.MCMF.GetActiveTabSection() !== "group"))
        return;

    jQ(".LayoutTweaksTempLobby").remove();
    jQ(".LayoutTweaksHiddenLobby").find("button").removeClass("disabled");
    jQ(".LayoutTweaksHiddenLobby").show();

    ET_LayoutTweaksMod_CombatLobby_page_rendered = true;
};

ET_LayoutTweaksMod_CombatLobby_page_rendered = false;
ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered = false;
ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup = function(forced)
{
return; // doesn't seem to be working correctly -- let's leave the triggers in, but disable the functionality for now

    if ((!ET.MCMF.IsNewCombatTab()) || (ET.MCMF.GetActiveTabSection() !== "group"))
        return;

    if ((!forced) && (ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered))
    {
        setTimeout(ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup, 100);
        return;
    }

    ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered = true;

    try
    {
        ET_LayoutTweaksMod_DisableLobbyButtons();

        let oLastBattle = Meteor.connection._stores.battles._getCollection().find().fetch().sort(function(battleA, battleB){ if (battleA.createdAt < battleB.createdAt) return 1; return -1; })[0];
        let iFloor = CInt(oLastBattle.floor); // throws exception if no data is loaded yet (queues retry)

        if (forced || (jQ(".lobby-container").find("button.battle-again-btn").length === 0))
        {
            ET_LayoutTweaksMod_HackyClicker("a.consumables-btn");

            setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("button.back-to-lobby-btn"); }, 250);
        }
        else
            ET_LayoutTweaksMod_EnableLobbyButtons();
    }
    catch (err)
    {
        ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered = false;
        setTimeout(function() { ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup(forced); }, 100);
    }

    // intentionally not toggling 'ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered' off here
};

ET_LayoutTweaksMod_AutoSelectFloorRoom_rendering = false;
ET_LayoutTweaksMod_AutoSelectFloorRoom = function(template, rendered)
{
return; // doesn't seem to be working correctly -- let's leave the triggers in, but disable the functionality for now

    if ((!ET.MCMF.IsNewCombatTab()) || (ET.MCMF.GetActiveTabSection() !== "group"))
        return;

    if (ET_LayoutTweaksMod_AutoSelectFloorRoom_rendering)
        return;

    ET_LayoutTweaksMod_AutoSelectFloorRoom_rendering = true;

    try
    {
        let oLastBattle = Meteor.connection._stores.battles._getCollection().find().fetch().sort(function(battleA, battleB){ if (battleA.createdAt < battleB.createdAt) return 1; return -1; })[0];
        let iFloor = CInt(oLastBattle.floor);
        let sFloor = iFloor.toFixed(0)
        let iRoom = CInt(oLastBattle.room);
        let sRoom = iRoom.toFixed(0)
        if (oLastBattle.room.toString().trim().toLowerCase() === "boss")
            sRoom = "Boss";
        let bOffs = false;

        if (oLastBattle.isExplorationRun)
        {
            if (iFloor === CInt(jQ("a.select-floor:last-child").attr("data-floor")))
                bOffs = true;
            sRoom = "All"
        }

        if (rendered)
        {
            if (jQ(".lobby-container").find("button.battle-again-btn").length === 0)
            {
                jQ("button.battle-btn").addClass("mr-3").after("<button class=\"btn btn-primary battle-again-btn d-flex justify-content-center\" style=\"width: 150px\">Battle Again</button>");
                jQ("button.battle-again-btn").on('click', function()
                {
                    template.state.set("newBattleLoading", !0);
                    jQ("button.battle-again-btn").addClass("disabled");
                    ET.MCMF.CallGameCmd("battles.findTowerBattle", sFloor, sRoom,
                        function(err, n)
                        {
                            if (err)
                            {
                                toastr.warning(err.reason);
                                template.state.set("newBattleLoading", !1);
                                jQ("button.battle-again-btn").removeClass("disabled");
                            }
                        }
                    );
                });

                ET_LayoutTweaksMod_AutoSelectFloorRoom_Backup_triggered = false;
            }
        }

        if ((jQ("btn.dropdown.room-dropdown").find("button.btn.dropdown-toggle").text() !== sRoom) || (jQ("btn.dropdown.floor-dropdown").find("button.btn.dropdown-toggle").text() !== sFloor))
        {
            if (ET.MCMF.WantDebug) ET.MCMF.Log("... selecting floor '" + sFloor + "', room '" + sRoom + "'");

            ET.MCMF.CallGameCmd("users.setUiState", "towerFloor", iFloor);
            template.state.set("usersCurrentFloor", iFloor);

            if (!bOffs)
                template.state.set("usersCurrentRoom", sRoom);
        }
    }
    catch (err) { }

    ET_LayoutTweaksMod_AutoSelectFloorRoom_rendering = false;
};

// Do these things multiple times over.. and.. over
ET_LayoutTweaksMod_PageScanner = function()
{
    ET_LayoutTweaksMod_PageScanner_main();

    setTimeout(ET_LayoutTweaksMod_PageScanner, 500);
};

ET_LayoutTweaksMod_CloseModals = function()
{
    jQ(".LTMod-modal").css("display", "none");
    jQ("div.modal").css("display", "none");
};

ET_LayoutTweaksMod_CraftSpellCast = function(ability_id)
{
    try
    {
        Meteor.call("abilities.fetchSpellCrafting", Meteor.connection._stores.abilities._getCollection().find().fetch()[0], function(err, data)
        {
            let oSpellCastCrafting = jQ.makeArray(data).filter(function(oSpellCastCraft) { if (oSpellCastCraft.abilityId === ability_id) return true; return false; })[0];

            let iMax = CInt(oSpellCastCrafting.maxToCraft);
            let iDef = iMax >= 25 ? 25 : iMax;

            jQ("h5#LTModSpellCraftingModal").parent().find("small").html("<img src=\"/icons/" + oSpellCastCrafting.icon + "\" style=\"height: 20px; width: 20px;\" /> Craft more spell casts for " + oSpellCastCrafting.name + ".");

            jQ(".LTModSpellCraftingModal").find("span.qty-max").text(iMax.toLocaleString());
            jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").val(iDef.toFixed(0));
            jQ(".LTModSpellCraftingModal").find("button.btn-secondary").attr("data-amount", iDef.toLocaleString()).text("Craft X (" + iDef.toLocaleString() + ")");
            jQ(".LTModSpellCraftingModal").find("button.btn-danger").attr("data-amount", iMax.toLocaleString()).text("Craft All (" + iMax.toLocaleString() + ")");

            jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").on('blur compositionupdate keypress keyup change', function()
            {
                let iVal = CInt(jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").val());

                iVal = (iVal > iMax) ? iMax : iVal;

                jQ(".LTModSpellCraftingModal").find("button.btn-secondary").attr("data-amount", iVal.toLocaleString()).text("Craft X (" + iVal.toLocaleString() + ")");
            });

            jQ(".LTModSpellCraftingModal").find("button.btn-primary").on('click', function()
            {
                ET.MCMF.CallGameCmd("abilities.craftSpell", ability_id, 1);
                ET_LayoutTweaksMod_CloseModals();
            });

            jQ(".LTModSpellCraftingModal").find("button.btn-secondary").on('click', function()
            {
                let iVal = CInt(jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").val());

                ET.MCMF.CallGameCmd("abilities.craftSpell", ability_id, iVal);
                ET_LayoutTweaksMod_CloseModals();
            });

            jQ(".LTModSpellCraftingModal").find("button.btn-danger").on('click', function()
            {
                let iVal = CInt(jQ(".LTModSpellCraftingModal").find("button.btn-danger").attr("data-amount"));

                ET.MCMF.CallGameCmd("abilities.craftSpell", ability_id, iVal);
                ET_LayoutTweaksMod_CloseModals();
            });

            setTimeout(function(){ jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").get(0).selectionStart = jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").get(0).selectionEnd = 10000; jQ(".LTModSpellCraftingModal").find("input.craft-amount-input").get(0).focus(); }, 1);

            jQ(".LTMod-modal").css("display", "block");
            jQ(".LTModSpellCraftingModal").css("display", "block");
        });
    }
    catch (err) { }
};

ET_LayoutTweaksMod_EquipmentModalList = function(sSlot, sSlotDesc)
{
    let oElContainer = jQ(".LTModEquipmentModal").find("div.LTModEquipmentContainer");
    let oItems = ET.MCMF.GetItems({category: "combat", slot: sSlot.trim()});

    oElContainer.html("");
    jQ("h5#LTModEquipmentModal").parent().find("small").html("Equip a new " + sSlotDesc + ".");

    oItems.forEach(function(oItem)
    {
      try {
        if (!oItem.equipped && !oItem.hidden)
        {
            let sElName = `LTModEquipThis-${oItem._id}`;

			const rarityBorderStyle = ET_LayoutTweaksMod_RarityBorder(oItem.rarityId)
			let rarityBorderCSS = ""
			
			if (rarityBorderStyle && rarityBorderStyle.trim().length > 0)
				rarityBorderCSS = " border: " + rarityBorderStyle + ";";

            oElContainer.append
            (
                "<div class=\"item-icon-container item icon-box " + sElName + "\" style=\"position: relative;" + rarityBorderCSS + "\">" +
                    "    <div class=\"item-amount text-capitalize\" style=\"position: absolute; bottom: 0px; right: 3px; font-size: 10px; text-align: center; width: 88px; max-width: 88px; text-overflow: hidden; display: inline-block; white-space: nowrap; overflow: hidden;\">" +
                    "        " + oItem.name +
                    "    </div>" +
                    "    <div class=\"item-quality\">" +
                    "        " + ((CDbl(oItem.quality) > 0.0) ? `${oItem.quality.toFixed(0)}%` : "") +
                    "    </div>" +
                    "    <img src=\"/icons/" + oItem.icon + "\" class=\"item-icon\" />" +
                "</div>"
            );

            oEl = jQ(`.${sElName}`);

            oEl.css("cursor", "pointer").on('click', function()
            {
                ET.MCMF.CallGameCmd("items.equip", oItem._id, oItem.name);
                ET_LayoutTweaksMod_CloseModals();
            });

            let sTooltipName = `LayoutTweaksTooltip-${oItem._id}`;

            if ((!oEl._tippy) && (jQ(`#${sTooltipName}`).length === 0))
            {
                jQ(`#tooltip-${sTooltipName}`).remove();
                jQ("body").append(`<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-${sTooltipName}\"></div>`);

                let sStatLines = "";
                let sExtraText = "";

                if (IsValid(oItem.stats))
                {
					let localStatsObj = Object.assign({}, oItem.stats)
									
					try {
						const rarityIdConsts = ET_LayoutsMod__ITEM_RARITIES[oItem.rarityId]
						if (rarityIdConsts && rarityIdConsts.statBonuses) {
							Object.keys(localStatsObj).forEach((statName) => {
							// disallow % bonuses to attack speed
							if (statName !== "attackSpeed") {
								// ensure that property refers to a stat that is a number and valued more than 0 (non-numeric/non-positive/non-zero all disallowed)
								if (CDbl(localStatsObj[statName]) > 0.0) {
									localStatsObj[statName] = localStatsObj[statName] * ((100.0 + rarityIdConsts.statBonuses) / 100.0)
									}
								}
							})

						}
					} catch (err) {}
					
					const rarityColorStyle = ET_LayoutTweaksMod_RarityColor(oItem.rarityId)
					if (rarityColorStyle && rarityColorStyle.trim().length > 0) {
						sStatLines = `${sStatLines}<span style="color: ${rarityColorStyle}; text-transform: capitalize;"><b>${oItem.rarityId}</b></span> <br />`;
					}
					
                    if (IsValid(localStatsObj.damage) && (CDbl(localStatsObj.damage) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-damage extra-small-icon mr-1\"></i>${localStatsObj.damage.toFixed(1)}</div>`;
                    if (IsValid(localStatsObj.energyStorage) && (CDbl(localStatsObj.energyStorage) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-energyStorage extra-small-icon mr-1\"></i>${localStatsObj.energyStorage.toFixed(1)} energy storage</div>`;

                    if (IsValid(localStatsObj.attack) && (CDbl(localStatsObj.attack) != 0.0)) {
                      if (IsValid(localStatsObj.attackMax) && (CDbl(localStatsObj.attackMax) != 0.0)) {
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attack extra-small-icon mr-1\"></i>${localStatsObj.attack.toFixed(1)} - ${localStatsObj.attackMax.toFixed(1)} damage</div>`;
                      } else {
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attack extra-small-icon mr-1\"></i>${localStatsObj.attack.toFixed(1)} damage</div>`;
                      }
                    }
                    if (IsValid(localStatsObj.attackSpeed) && (CDbl(localStatsObj.attackSpeed) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attackSpeed extra-small-icon mr-1\"></i>${localStatsObj.attackSpeed.toFixed(1)} attack speed</div>`;
                    if (IsValid(localStatsObj.healthMax) && (CDbl(localStatsObj.healthMax) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-healthMax extra-small-icon mr-1\"></i>${localStatsObj.healthMax.toFixed(1)} health</div>`;
                    if (IsValid(localStatsObj.accuracy) && (CDbl(localStatsObj.accuracy) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-accuracy extra-small-icon mr-1\"></i>${localStatsObj.accuracy.toFixed(1)} accuracy</div>`;
                    if (IsValid(localStatsObj.magicPower) && (CDbl(localStatsObj.magicPower) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-magicPower extra-small-icon mr-1\"></i>${localStatsObj.magicPower.toFixed(1)} magic power</div>`;
                    if (IsValid(localStatsObj.magicArmor) && (CDbl(localStatsObj.magicArmor) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-magicArmor extra-small-icon mr-1\"></i>${localStatsObj.magicArmor.toFixed(1)} magic armor</div>`;
                    if (IsValid(localStatsObj.healingPower) && (CDbl(localStatsObj.healingPower) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-healingPower extra-small-icon mr-1\"></i>${localStatsObj.healingPower.toFixed(1)} healing power</div>`;
                    if (IsValid(localStatsObj.defense) && (CDbl(localStatsObj.defense) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-defense extra-small-icon mr-1\"></i>${localStatsObj.defense.toFixed(1)} defense</div>`;
                    if (IsValid(localStatsObj.armor) && (CDbl(localStatsObj.armor) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-armor extra-small-icon mr-1\"></i>${localStatsObj.armor.toFixed(1)} armor</div>`;

                    if (IsValid(localStatsObj.energyRegen) && (CDbl(localStatsObj.energyRegen) != 0.0))
                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-energyRegen extra-small-icon mr-1\"></i>${localStatsObj.energyRegen.toFixed(1)} energy regen</div>`;
                }

                jQ.makeArray(oItem.enchantments).forEach(function(oThisEnchant)
                {
                    sExtraText = `${sExtraText}<div style="text-capitalize">Enchantment: ${oThisEnchant}</div>`;
                });

                jQ(`#tooltip-${sTooltipName}`).html
                (
                    "    <h3 class=\"popover-title text-capitalize\">" +
                    "        " + oItem.name + ((oItem.enhanced) ? " (E)" : "") + ((oItem?.extraStats?.level > 0) ? " (Lv." + oItem?.extraStats?.level.toFixed(0) + ")" : "") + "<br />" +
                    "    </h3>" +
                    "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                    "        " + sStatLines +
                    "        " + sExtraText +
                    "        <div><b>Left Click</b> to equip</div>" +
                    "    </div>"
                );

                tippy(`.${sElName}`,
                {
                    html: jQ(`#tooltip-${sTooltipName}`)[0],
                    performance: !0,
                    animateFill: !1,
                    distance: 5
                });
            }
        }
      } catch (err) {
        ET.MCMF.Log("Error rending details for item:");
        console.log(oItem);
        ET.MCMF.Log(err);
      }
    });

    let oCurrentItem = ET.MCMF.GetItems({category: "combat", equipped: true, slot: sSlot.trim()});
    if (IsValid(oCurrentItem) && (oCurrentItem.length > 0))
    {
        oElContainer.append("<div class=\"item-icon-container item icon-box LTModEquipThis-unequip\" style=\"position: relative; white-space: nowrap;\">[Remove]</div>");
        jQ(".LTModEquipThis-unequip").on('click', function()
        {
            ET.MCMF.CallGameCmd("items.unequip", oCurrentItem[0]._id, oCurrentItem[0].itemId);
            ET_LayoutTweaksMod_CloseModals();
        });
    }

    jQ(".LTModEquipmentModal").find("div.item-icon-container").hover
    (
        function() { jQ(this).css("border-width", "3px"); },
        function() { jQ(this).css("border-width",    ""); }
    );

    jQ(".LTMod-modal").css("display", "block");
    jQ(".LTModEquipmentModal").css("display", "block");
};

ET_LayoutTweaksMod_AbilityID = function(oAbility) {
  if (IsValid(oAbility)) {
    if (IsValid(oAbility.abilityId)) {
      return oAbility.abilityId;
    }
    if (IsValid(oAbility.id)) {
      return oAbility.id;
    }
    if (IsValid(oAbility._id)) {
      return oAbility._id;
    }
  }
  return '';
}

ET_LayoutTweaksMod_AbilityModalList = function(sSlot, bIsEmpty)
{
    let oElContainer = jQ(".LTModAbilityModal").find("div.LTModAbilityContainer");
    //let oAbilities = jQ.makeArray(Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities);
    let oAbilities = document.PlayerAbilities;

    jQ.makeArray(Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities).forEach((learntAbility) => {
      oAbilities.forEach(function(ourAbility, idx, arr) {
        if (ET_LayoutTweaksMod_AbilityID(ourAbility) === ET_LayoutTweaksMod_AbilityID(learntAbility)) {
          arr[idx] = Object.assign(ourAbility, learntAbility);
        }
      });
    });

    oElContainer.html("");
    if (sSlot === 'companion') {
      jQ("h5#LTModAbilityModal").parent().find("small").html("Equip a new <b>companion</b> ability.");
    } else {
      jQ("h5#LTModAbilityModal").parent().find("small").html("Equip a new ability or spell.");
    }

    oAbilities.sort(function(oAbility1, oAbility2)
    {
        // passives before actives
        if (oAbility1.cooldown === 0 && oAbility2.cooldown !== 0) return -1;
        if (oAbility1.cooldown !== 0 && oAbility2.cooldown === 0) return 1;

        // abilities first, then spells
        if (oAbility1.isSpell && !oAbility2.isSpell) return 1;
        if (!oAbility1.isSpell && oAbility2.isSpell) return -1;

        // sort alphabetically
        if (oAbility1.name > oAbility2.name) return 1;
        if (oAbility1.name < oAbility2.name) return -1;

        return 0;
    });

    oAbilities.forEach(function(oAbility)
    {
        if (!oAbility.equipped)
        {
            let sElName = `LTModEquipThis-${ET_LayoutTweaksMod_AbilityID(oAbility)}`;

            let sAbilityFriendlyName = oAbility.name;

			if (!oAbility.learntLevel || oAbility.learntLevel == 0)
				return;

			Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "mainHand") return true; return false;})[0];

            for (let j = 1; j <= 5; j++)
                sAbilityFriendlyName = sAbilityFriendlyName.replace(` (level ${j})`, "");

            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "defensive_stance")   sAbilityFriendlyName = sAbilityFriendlyName.replace("defensive stance", "def. stance");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "double_edged_sword") sAbilityFriendlyName = sAbilityFriendlyName.replace("doubled edged sword", "double edge");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "elemental_shield")   sAbilityFriendlyName = sAbilityFriendlyName.replace("elemental shield", "ele. shield");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "evasive_maneuvers")  sAbilityFriendlyName = sAbilityFriendlyName.replace("evasive maneuvers", "evasive");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "feeding_frenzy")     sAbilityFriendlyName = sAbilityFriendlyName.replace("feeding frenzy", "feed. frenzy");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "lightning_dart")     sAbilityFriendlyName = sAbilityFriendlyName.replace("lightning dart", "lit. dart");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "penetrating_slash")  sAbilityFriendlyName = sAbilityFriendlyName.replace("penetrating slash", "pen. slash");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "phantom_strikes")    sAbilityFriendlyName = sAbilityFriendlyName.replace("phantom strikes", "phant. strikes");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "volcanic_shield")    sAbilityFriendlyName = sAbilityFriendlyName.replace("volcanic shield", "volc. shield");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "lightning_storm")    sAbilityFriendlyName = sAbilityFriendlyName.replace("lightning storm", "light. storm");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "angels_touch")       sAbilityFriendlyName = sAbilityFriendlyName.replace("angel's touch", "angel touch");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "raise_your_glass")   sAbilityFriendlyName = sAbilityFriendlyName.replace("raise your glass", "raise glass");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "poisoned_blade")     sAbilityFriendlyName = sAbilityFriendlyName.replace("poisoned blade", "pois. blade");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "furied_defense")     sAbilityFriendlyName = sAbilityFriendlyName.replace("furied defense", "furied def.");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "frenzied_winds")     sAbilityFriendlyName = sAbilityFriendlyName.replace("frenzied winds", "frenz. winds");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "healing_shield")     sAbilityFriendlyName = sAbilityFriendlyName.replace("healing shield", "heal. shield");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "mending_spring")     sAbilityFriendlyName = sAbilityFriendlyName.replace("mending spring", "mend. spring");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "year_of_the_pig")    sAbilityFriendlyName = sAbilityFriendlyName.replace("year of the pig", "yr. of the pig");
            if (ET_LayoutTweaksMod_AbilityID(oAbility) === "skeletal_warrior")   sAbilityFriendlyName = sAbilityFriendlyName.replace("skeletal warrior", "skel. warrior");

            let sAbilityRequiresIcons = "";

            if (sSlot !== 'companion' && oAbility.slot === 'companion')
              return;
            if (sSlot === 'companion' && oAbility.slot !== 'companion')
              return;

		    if (window.ET.MCMF.IsClass() && window.ET.MCMF.WhichClass() != "tactician")
			{
				let bSlotIsActive = false;
				if (sSlot == "mainHand") bSlotIsActive = true;
				if (sSlot == "offHand") bSlotIsActive = true;
				if (sSlot == "head") bSlotIsActive = true;
				if (sSlot == "chest") bSlotIsActive = true;
				if (sSlot == "legs") bSlotIsActive = true;
				let bAbilityIsActive = !oAbility.isPassive;

				if (sSlot === 'companion')
					jQ("h5#LTModAbilityModal").parent().find("small").html("Equip a new <b>companion</b> ability.");
				else if (bSlotIsActive)
					jQ("h5#LTModAbilityModal").parent().find("small").html("Equip a new <b>active</b> ability or spell.");
				else
					jQ("h5#LTModAbilityModal").parent().find("small").html("Equip a new <b>passive</b> ability.");

				if (bSlotIsActive && !bAbilityIsActive) return;
				if (!bSlotIsActive && bAbilityIsActive) return;
			}

            if (oAbility.isPacifist || (IsValid(oAbility.requires) && (oAbility.requires.length > 0)) || (IsValid(oAbility.cantUseWith) && (oAbility.cantUseWith.length > 0)))
            {
                sAbilityRequiresIcons = "<div style=\"position: absolute; left: 2px; top: 0\" class=\"required-weapon\">";

                if (oAbility.isPacifist) {
                  sAbilityRequiresIcons += `<img src=\"/icons/pacifist.svg\" style=\"height: 20px; width: 20px;\" />`;
                }

                if (IsValid(oAbility.requires))
                {
                    jQ.makeArray(oAbility.requires).forEach(function(oThisRequiredThing)
                    {
                        if (oThisRequiredThing.type === "weaponType")
                        {
                          jQ.makeArray(oThisRequiredThing.weaponTypes).forEach((weaponTypeRequired) => {
                            sAbilityRequiresIcons += `<img src=\"/icons/${weaponTypeRequired}.svg\" style=\"height: 20px; width: 20px;\" />`;
                          });
                        }
                    });
                }

                if (IsValid(oAbility.cantUseWith))
                {
                    jQ.makeArray(oAbility.cantUseWith).forEach(function(oThisForbiddenThing)
                    {
                        if (oThisForbiddenThing.type === "weaponType")
                        {
                          jQ.makeArray(oThisForbiddenThing.weaponTypes).forEach((weaponTypeForbidden) => {
                            sAbilityRequiresIcons += `<img src="/icons/forbidden.svg" style="height: 20px; width: 20px; background: url(/icons/${weaponTypeForbidden}.svg) center center;">`;
                          });
                        }
                    });
                }

                sAbilityRequiresIcons += "</div>";
            }

            let sAbilityKindStyle = (CInt(oAbility.cooldown) === 0) ? "background-color: #f2ffe5;" : "";
            try { sAbilityKindStyle = (oAbility.isSpell && ( IsValid(oAbility.requires)) && (oAbility.requires.length  >  0)) ? "background-color: #eeddff;" : sAbilityKindStyle; } catch (err) { }
            try { sAbilityKindStyle = (oAbility.isSpell && (!IsValid(oAbility.requires)) || (oAbility.requires.length === 0)) ? "background-color: #e3f0ff;" : sAbilityKindStyle; } catch (err) { }

            oElContainer.append
            (
                "<div class=\"item-icon-container item small icon-box " + sElName + "\" style=\"position: relative; " + sAbilityKindStyle + "\">" +
                    "    <div class=\"item-amount text-capitalize\" style=\"position: absolute; bottom: 0px; right: 3px; font-size: 9px; text-align: center; width: 54px; max-width: 54px; text-overflow: hidden; display: inline-block; white-space: nowrap; overflow: hidden;\">" +
                    "        " + sAbilityFriendlyName +
                    "    </div>" +
                    "    <img src=\"/icons/" + oAbility.icon + "\" class=\"item-icon\" />" +
                    "    " + sAbilityRequiresIcons +
                "</div>"
            );

            oEl = jQ(`.${sElName}`);

            oEl.css("cursor", "pointer").on('click', function()
            {
                ET.MCMF.CallGameCmd("abilities.unequip", sSlot, function()
                {
                    ET.MCMF.CallGameCmd("abilities.equip", ET_LayoutTweaksMod_AbilityID(oAbility));
                });
                ET_LayoutTweaksMod_CloseModals();
            });

            let sTooltipName = `LayoutTweaksTooltip-${ET_LayoutTweaksMod_AbilityID(oAbility)}`;

            if ((!oEl._tippy) && (jQ(`#${sTooltipName}`).length === 0))
            {
                jQ(`#tooltip-${sTooltipName}`).remove();
                jQ("body").append(`<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-${sTooltipName}\"></div>`);

                let sStatLines = "";
                let sExtraText = "";

                jQ(`#tooltip-${sTooltipName}`).html
                (
                    "    <h3 class=\"popover-title text-capitalize\">" +
                    "        " + oAbility.name + "<br />" +
                    "    </h3>" +
                    "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                    "        <div>" + oAbility.description + "</div>" +
                    "        " + ((CInt(oAbility.cooldown) > 0) ? `<div>Cooldown <b>${oAbility.cooldown.toFixed(0).toFriendlyTime()}</b></div>` : "") +
                    "        " + ((CInt(oAbility.casts) > 0) ? `<div><b>${CInt(oAbility.casts).toLocaleString()}</b> casts remaining.</div>` : "") +
                    "        <div><b>Left Click</b> to equip</div>" +
                    "    </div>"

                );

                tippy(`.${sElName}`,
                {
                    html: jQ(`#tooltip-${sTooltipName}`)[0],
                    performance: !0,
                    animateFill: !1,
                    distance: 5
                });
            }
        }
    });

    if (!bIsEmpty)
    {
        oElContainer.append("<div class=\"item-icon-container item small icon-box LTModEquipThis-unequip\" style=\"position: relative; white-space: nowrap;\">[Remove]</div>");
        jQ(".LTModEquipThis-unequip").on('click', function()
        {
            ET.MCMF.CallGameCmd("abilities.unequip", sSlot);
            ET_LayoutTweaksMod_CloseModals();
        });
    }

    jQ(".LTModAbilityModal").find("div.item-icon-container").hover
    (
        function() { jQ(this).css("border-width", "3px"); },
        function() { jQ(this).css("border-width",    ""); }
    );

    jQ(".LTMod-modal").css("display", "block");
    jQ(".LTModAbilityModal").css("display", "block");
};

ET_LayoutTweaksMod_RarityColor = function(rarityId)
{
	let borderStyle = "";
	if (rarityId === "crude") {
		borderStyle = "#555555 !important"
	} else if (rarityId === "rough") {
		borderStyle = "#666644 !important"
	} else if (rarityId === "improved") {
		borderStyle = "#998800 !important"
	} else if (rarityId === "mastercrafted") {
		borderStyle = "#cc7700 !important"
	} else if (rarityId === "masterforged") {
		borderStyle = "#ee6622 !important"
	} else if (rarityId === "ascended") {
		borderStyle = "#ff2266 !important"
	} else if (rarityId === "ethereal") {
		borderStyle = "#FF5599 !important"
	} else if (rarityId === "perfect") {
		borderStyle = "#FF71aa !important"
	} else if (rarityId === "fine") {
		borderStyle = "#66aaaa !important"
	} else if (rarityId === "rare") {
		borderStyle = "#3388aa !important"
	} else if (rarityId === "extraordinary") {
		borderStyle = "#3366aa !important"
	} else if (rarityId === "phenomenal") {
		borderStyle = "#0055cc !important"
	} else if (rarityId === "epic") {
		borderStyle = "#0022ee !important"
	} else if (rarityId === "divine") {
		borderStyle = "#4444ff !important"
	} else if (rarityId === "incredible") {
		borderStyle = "#6141ff !important"
	} else if (rarityId === "unparalleled") {
		borderStyle = "#9151ff !important"
	} else if (rarityId === "prized") {
		borderStyle = "#883388 !important"
	} else if (rarityId === "legendary") {
		borderStyle = "#cc44cc !important"
	} else if (rarityId === "artifact") {
		borderStyle = "#44cc44 !important"
	}
	return borderStyle;
};

ET_LayoutTweaksMod_RarityBorder = function(rarityId)
{
	let borderStyle = "";
	if (rarityId === "crude") {
		borderStyle = "3px dotted #555555 !important"
	} else if (rarityId === "rough") {
		borderStyle = "3px dotted #666644 !important"
	} else if (rarityId === "improved") {
		borderStyle = "3px dashed #998800 !important"
	} else if (rarityId === "mastercrafted") {
		borderStyle = "3px dashed #cc7700 !important"
	} else if (rarityId === "masterforged") {
		borderStyle = "3px double #ee6622 !important"
	} else if (rarityId === "ascended") {
		borderStyle = "3px double #ff2266 !important"
	} else if (rarityId === "ethereal") {
		borderStyle = "3px double #FF5599 !important"
	} else if (rarityId === "perfect") {
		borderStyle = "3px double #FF71aa !important"
	} else if (rarityId === "fine") {
		borderStyle = "3px dashed #66aaaa !important"
	} else if (rarityId === "rare") {
		borderStyle = "3px dashed #3388aa !important"
	} else if (rarityId === "extraordinary") {
		borderStyle = "3px double #3366aa !important"
	} else if (rarityId === "phenomenal") {
		borderStyle = "3px double #0055cc !important"
	} else if (rarityId === "epic") {
		borderStyle = "3px double #0022ee !important"
	} else if (rarityId === "divine") {
		borderStyle = "3px double #4444ff !important"
	} else if (rarityId === "incredible") {
		borderStyle = "3px double #6141ff !important"
	} else if (rarityId === "unparalleled") {
		borderStyle = "3px double #9151ff !important"
	} else if (rarityId === "prized") {
		borderStyle = "3px double #883388 !important"
	} else if (rarityId === "legendary") {
		borderStyle = "3px double #cc44cc !important"
	} else if (rarityId === "artifact") {
		borderStyle = "3px double #44cc44 !important"
	}
	return borderStyle;
};

const ET_LayoutsMod__ITEM_RARITIES = {
    // Crafting T-1 (next - 30)
    crude: {
        rarityId: "crude",
        label: "Crude",
        color: "555555",
        statBonuses: -50.0,
        nextRarity: {
            rarityId: "rough",
            successChance: 75.0 // 75% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "crude"
        }
    },

    // Crafting T0 (next - 20)
    rough: {
        rarityId: "rough",
        label: "Rough",
        color: "666644",
        statBonuses: -20.0,
        nextRarity: {
            rarityId: "standard",
            successChance: 60.0 // 60% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "crude"
        }
    },

    // Crafting T1
    standard: {
        rarityId: "standard",
        label: "",
        color: "",
        statBonuses: 0.0,
        nextRarity: {
            rarityId: "improved",
            successChance: 45.0 // 45% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "rough"
        }
    },

    // Crafting T2 (33.333% of the last + 20/[tier-1]%)
    improved: {
        rarityId: "improved",
        label: "Improved",
        color: "998800",
        statBonuses: 20.0,
        nextRarity: {
            rarityId: "mastercrafted",
            successChance: 30.0 // 30% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "standard"
        }
    },

    // Crafting T3 (33.333% of the last + 20/[tier-1]%)
    mastercrafted: {
        rarityId: "mastercrafted",
        label: "Mastercrafted",
        color: "cc7700",
        statBonuses: 36.7,
        nextRarity: {
            rarityId: "masterforged",
            successChance: 15.0 // 15% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "improved"
        }
    },

    // Crafting T4 (33.333% of the last + 20/[tier-1]%)
    masterforged: {
        rarityId: "masterforged",
        label: "Masterforged",
        color: "ee6622",
        statBonuses: 55.6,
        nextRarity: {
            rarityId: "ascended",
            successChance: 5.0 // 5% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "mastercrafted"
        }
    },

    // Crafting T5 (33.333% of the last + 20/[tier-1]%)
    ascended: {
        rarityId: "ascended",
        label: "Ascended",
        color: "ff2266",
        statBonuses: 79.1,
        nextRarity: {
            rarityId: "ethereal",
            successChance: -5.0 // -5% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "masterforged"
        }
    },

    // Crafting T6 (33.333% of the last + 20/[tier-1]%)
    ethereal: {
        rarityId: "ethereal",
        label: "Ethereal",
        color: "FF5599",
        statBonuses: 109.4,
        nextRarity: {
            rarityId: "perfect",
            successChance: -5.0 // -5% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "ascended"
        }
    },

    // Crafting T7 (33.333% of the last + 20/[tier-1]%)
    perfect: {
        rarityId: "perfect",
        label: "Perfect",
        color: "FF71aa",
        statBonuses: 149.2,
        prevRarity: {
            rarityId: "ethereal"
        }
    },

    // Looted T1
    uncommon: {
        rarityId: "uncommon",
        label: "",
        color: "",
        statBonuses: 0.0,
        nextRarity: {
            rarityId: "fine",
            successChance: 45.0 // 45% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "uncommon"
        }
    },

    // Looted T2 (25% of the last + 25/[tier/2]%)
    fine: {
        rarityId: "fine",
        label: "Fine",
        color: "66aaaa",
        statBonuses: 25.0,
        nextRarity: {
            rarityId: "rare",
            successChance: 30.0 // 30% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "uncommon"
        }
    },

    // Looted T3 (25% of the last + 25/[tier/2]%)
    rare: {
        rarityId: "rare",
        label: "Rare",
        color: "3388aa",
        statBonuses: 47.9,
        nextRarity: {
            rarityId: "extraordinary",
            successChance: 15.0 // 1% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "fine"
        }
    },

    // Looted T4 (25% of the last + 25/[tier/2]%)
    extraordinary: {
        rarityId: "extraordinary",
        label: "Extraordinary",
        color: "3366aa",
        statBonuses: 72.4,
        nextRarity: {
            rarityId: "phenomenal",
            successChance: 5.0 // 5% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "rare"
        }
    },

    // Looted T5 (25% of the last + 25/[tier/2]%)
    phenomenal: {
        rarityId: "phenomenal",
        label: "Phenomenal",
        color: "0055cc",
        statBonuses: 100.5,
        nextRarity: {
            rarityId: "epic",
            successChance: -5.0 // -5% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "extraordinary"
        }
    },

    // Looted T6 (25% of the last + 25/[tier/2]%)
    epic: {
        rarityId: "epic",
        label: "Epic",
        color: "0022ee",
        statBonuses: 134.0,
        nextRarity: {
            rarityId: "divine",
            successChance: -10.0 // -10% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "phenomenal"
        }
    },

    // Looted T7 (25% of the last + 25/[tier/2]%)
    divine: {
        rarityId: "divine",
        label: "Divine",
        color: "4444ff",
        statBonuses: 174.6,
        nextRarity: {
            rarityId: "incredible",
            successChance: -15.0 // -15% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "epic"
        }
    },

    // Looted T8 (25% of the last + 25/[tier/2]%)
    incredible: {
        rarityId: "incredible",
        label: "Incredible",
        color: "6141ff",
        statBonuses: 224.5,
        nextRarity: {
            rarityId: "unparalleled",
            successChance: -15.0 // -15% chance (plus 1% chance per crafting skill level above recipe to craft this)
        },
        prevRarity: {
            rarityId: "divine"
        }
    },

    // Looted T9 (25% of the last + 25/[tier/2]%)
    unparalleled: {
        rarityId: "unparalleled",
        label: "Unparalleled",
        color: "9151ff",
        statBonuses: 286.2,
        prevRarity: {
            rarityId: "incredible"
        }
    },

    // Special (non-tiered): for boss drops
    prized: {
        rarityId: "prized",
        label: "Prized",
        color: "883388",
        statBonuses: 0.0
    },

    // Special (non-tiered): for legendary items
    legendary: {
        rarityId: "legendary",
        label: "Legendary",
        color: "6633ff",
        statBonuses: 0.0
    },

    // Special (non-tiered): for artifact items
    artifact: {
        rarityId: "artifact",
        label: "Artifact",
        color: "44cc44",
        statBonuses: 0.0
    }
};

ET_LayoutTweaksMod_PageScanner_main = function()
{
	
    if (ET_LayoutTweaksMod_resetSpellDisplay)
    {
        ET_LayoutTweaksMod_resetSpellDisplay = false;

        jQ("#LayoutTweaksSpellCasts2").remove();
        jQ("#LayoutTweaksSpellCasts").remove();
        jQ("#LayoutTweaksLobbyEquipment").remove();
    }

    try
    {
        // from any page, if players click a link to the new combat page and that link is called 'adventures', set the combat UI tab state to adventures ('afk')
        jQ("a[href=\"/newCombat\"]").each(function()
        {
            if (jQ(this).attr('MCModified') !== "true")
            {
                if (jQ(this).text().trim().toLowerCase() === "adventures")
                {
                    jQ(this).attr('MCModified', 'true');
                    jQ(this).on('click', function() { ET.MCMF.CallGameCmd("users.setUiState", "newCombatType", "afk"); });
                }
            }
        });

        try
        {
            if (jQ(".lobby-container").find("button.battle-btn").length > 0)
            {
                if (jQ("#LayoutTweaksSpellCasts").length === 0)
                {
                    jQ("div.lobby-units-container").after("<div id=\"LayoutTweaksLobbyEquipment\" class=\"d-flex align-items-center justify-content-center flex-wrap\" style=\"margin-top: 30px;\"></div>");
                    jQ("#LayoutTweaksLobbyEquipment").after("<div id=\"LayoutTweaksSpellCasts\" class=\"d-flex align-items-center justify-content-center flex-wrap\"></div>");
                    jQ("#LayoutTweaksSpellCasts").after("<div id=\"LayoutTweaksSpellCasts2\" class=\"d-flex align-items-center justify-content-center flex-wrap\"></div>");

                    if (jQ(".LTMod-modal").css("display") !== "block")
                    {
                        let sTemp;

                        jQ(".LTModEquipmentModal").remove();

                        sTemp =
                            "<div class=\"modal LTModEquipmentModal\" tabindex=\"-1\" role=\"dialog\" style=\"display: none;\">\r\n" +
                            "   <div class=\"modal-dialog\" role=\"document\">\r\n" +
                            "      <div class=\"modal-content\">\r\n" +
                            "         <div class=\"modal-header\">\r\n" +
                            "            <div class=\"d-flex flex-column\">\r\n" +
                            "               <h5 class=\"modal-title text-capitalize\" id=\"LTModEquipmentModal\">\r\n" +
                            "                  Equipment Select\r\n" +
                            "               </h5>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "            <div class=\"d-flex flex-column align-items-end\">\r\n" +
                            "               <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\r\n" +
                            "               <span>×</span>\r\n" +
                            "               </button>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "         </div>\r\n" +
                            "         <div class=\"modal-body\">\r\n" +
                            "            <div class=\"d-flex flex-wrap LTModEquipmentContainer\">\r\n" +
                            "               \r\n" +
                            "            </div>\r\n" +
                            "         </div>\r\n" +
                            "      </div>\r\n" +
                            "   </div>\r\n" +
                            "</div>\r\n"

                        jQ("body").append(sTemp);

                        jQ(".LTModEquipmentModal").find("button.close").on('click', function()
                        {
                            ET_LayoutTweaksMod_CloseModals();
                        });

                        jQ(".LTModAbilityModal").remove();

                        sTemp =
                            "<div class=\"modal LTModAbilityModal\" tabindex=\"-1\" role=\"dialog\" style=\"display: none;\">\r\n" +
                            "   <div class=\"modal-dialog\" role=\"document\" style=\"width: 700px; max-width: 700px;\">\r\n" +
                            "      <div class=\"modal-content\" style=\"width: 700px; max-width: 700px;\">\r\n" +
                            "         <div class=\"modal-header\">\r\n" +
                            "            <div class=\"d-flex flex-column\">\r\n" +
                            "               <h5 class=\"modal-title text-capitalize\" id=\"LTModAbilityModal\">\r\n" +
                            "                  Ability Select\r\n" +
                            "               </h5>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "            <div class=\"d-flex flex-column align-items-end\">\r\n" +
                            "               <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\r\n" +
                            "               <span>×</span>\r\n" +
                            "               </button>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "         </div>\r\n" +
                            "         <div class=\"modal-body\">\r\n" +
                            "            <div class=\"d-flex flex-wrap LTModAbilityContainer\">\r\n" +
                            "               \r\n" +
                            "            </div>\r\n" +
                            "         </div>\r\n" +
                            "      </div>\r\n" +
                            "   </div>\r\n" +
                            "</div>\r\n"

                        jQ("body").append(sTemp);

                        jQ(".LTModAbilityModal").find("button.close").on('click', function()
                        {
                            ET_LayoutTweaksMod_CloseModals();
                        });

                        jQ(".LTModSpellCraftingModal").remove();

                        sTemp =
                            "<div class=\"modal multiCraftModal LTModSpellCraftingModal\" tabindex=\"-1\" role=\"dialog\" style=\"display: none;\">\r\n" +
                            "   <div class=\"modal-dialog\" role=\"document\">\r\n" +
                            "      <div class=\"modal-content\">\r\n" +
                            "         <div class=\"modal-header\">\r\n" +
                            "            <div class=\"d-flex flex-column\">\r\n" +
                            "               <h5 class=\"modal-title text-capitalize\" id=\"LTModSpellCraftingModal\">\r\n" +
                            "                  Craft Spell Casts\r\n" +
                            "               </h5>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "            <div class=\"d-flex flex-column align-items-end\">\r\n" +
                            "               <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\r\n" +
                            "               <span>×</span>\r\n" +
                            "               </button>\r\n" +
                            "               <small></small>\r\n" +
                            "            </div>\r\n" +
                            "         </div>\r\n" +
                            "         <form class=\"craft-amount-form\">\r\n" +
                            "            <div class=\"modal-body\">\r\n" +
                            "               How many to craft?<br />\r\n" +
                            "               Max: <span class=\"qty-max\">10000</span>\r\n" +
                            "               <input class=\"form-control craft-amount-input\"><br />\r\n" +
                            "            </div>\r\n" +
                            "            <div class=\"modal-footer\">\r\n" +
                            "               <button type=\"button\" class=\"btn btn-primary craft-btn\" data-amount=\"1\">Craft 1</button>\r\n" +
                            "               <button type=\"button\" class=\"btn btn-secondary craft-btn\" data-amount=\"0\">Craft X (0)</button>\r\n" +
                            "               <button type=\"button\" class=\"btn btn-danger craft-btn\" data-amount=\"0\">Craft All (0)</button>\r\n" +
                            "            </div>\r\n" +
                            "         </form>\r\n" +
                            "      </div>\r\n" +
                            "   </div>\r\n" +
                            "</div>\r\n"

                        jQ("body").append(sTemp);

                        jQ(".LTModSpellCraftingModal").find("button.close").on('click', function()
                        {
                            ET_LayoutTweaksMod_CloseModals();
                        });
                    }

                    let oAbilitySlots = [];
                    let oAbilityTemp;

                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "mainHand") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "offHand") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "head") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "chest") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "legs") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];

					if (window.ET.MCMF.IsClass())
					{
						oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "classAbil1") return true; return false;})[0];
						oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
						oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "classAbil2") return true; return false;})[0];
						oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
						oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "classAbil3") return true; return false;})[0];
						oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];
					}

                    oAbilityTemp = Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities.filter(function(oAbility){if (oAbility.equipped && oAbility.slot === "companion") return true; return false;})[0];
                    oAbilitySlots = [...oAbilitySlots, IsValid(oAbilityTemp) ? oAbilityTemp : { equipped: false }];


                    let oEquipMainhand = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "mainHand"})[0];
                    let oEquipOffhand = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "offHand"})[0];

                    let bSpells = false;

                    for (let i = 0; i < (window.ET.MCMF.IsClass() ? 9 : 6); i++)
                    {
                        if (oAbilitySlots[i].equipped)
                        {
                            if (oAbilitySlots[i].isSpell)
                                bSpells = true;

                            let sAbilityFriendlyName = oAbilitySlots[i].name;

                            for (let j = 1; j <= 5; j++)
                            {
                                if (oAbilitySlots[i].isSpell)
                                {
                                    if (sAbilityFriendlyName.indexOf(` (level ${j})`) !== -1)
                                        sAbilityFriendlyName = ChopperBlank(sAbilityFriendlyName, "", ` (level ${j})`).trim();
                                }
                                else
                                    sAbilityFriendlyName = sAbilityFriendlyName.replace(` (level ${j})`, ` L.${j}`);
                            }

                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "defensive_stance")   sAbilityFriendlyName = sAbilityFriendlyName.replace("defensive stance", "def. stance");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "double_edged_sword") sAbilityFriendlyName = sAbilityFriendlyName.replace("doubled edged sword", "double edge");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "elemental_shield")   sAbilityFriendlyName = sAbilityFriendlyName.replace("elemental shield", "ele. shield");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "evasive_maneuvers")  sAbilityFriendlyName = sAbilityFriendlyName.replace("evasive maneuvers", "evasive");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "feeding_frenzy")     sAbilityFriendlyName = sAbilityFriendlyName.replace("feeding frenzy", "feed. frenzy");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "lightning_dart")     sAbilityFriendlyName = sAbilityFriendlyName.replace("lightning dart", "lit. dart");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "penetrating_slash")  sAbilityFriendlyName = sAbilityFriendlyName.replace("penetrating slash", "pen. slash");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "phantom_strikes")    sAbilityFriendlyName = sAbilityFriendlyName.replace("phantom strikes", "phant. strikes");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "volcanic_shield")    sAbilityFriendlyName = sAbilityFriendlyName.replace("volcanic shield", "volc. shield");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "lightning_storm")    sAbilityFriendlyName = sAbilityFriendlyName.replace("lightning storm", "light. storm");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "angels_touch")       sAbilityFriendlyName = sAbilityFriendlyName.replace("angel's touch", "angel touch");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "raise_your_glass")   sAbilityFriendlyName = sAbilityFriendlyName.replace("raise your glass", "raise glass");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "poisoned_blade")     sAbilityFriendlyName = sAbilityFriendlyName.replace("poisoned blade", "pois. blade");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "furied_defense")     sAbilityFriendlyName = sAbilityFriendlyName.replace("furied defense", "furied def.");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "frenzied_winds")     sAbilityFriendlyName = sAbilityFriendlyName.replace("frenzied winds", "frenz. winds");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "healing_shield")     sAbilityFriendlyName = sAbilityFriendlyName.replace("healing shield", "heal. shield");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "mending_spring")     sAbilityFriendlyName = sAbilityFriendlyName.replace("mending spring", "mend. spring");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "year_of_the_pig")    sAbilityFriendlyName = sAbilityFriendlyName.replace("year of the pig", "yr. of the pig");
                            if (ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]) === "skeletal_warrior")   sAbilityFriendlyName = sAbilityFriendlyName.replace("skeletal warrior", "skel. warrior");

                            let sAbilityWarningText = "";

                            if (oAbilitySlots[i].isSpell)
                            {
                                if (CInt(oAbilitySlots[i].casts) <= ((20*60)/CInt(oAbilitySlots[i].cooldown)))
                                    sAbilityWarningText = "<div><span style=\"color: red;\">⚠️ You are low on casts for this spell!</span></div>";
                            }

                            let sAbilityRequiresIcons = "";

                            if (oAbilitySlots[i].isPacifist || (IsValid(oAbilitySlots[i].requires) && (oAbilitySlots[i].requires.length > 0)) || (IsValid(oAbilitySlots[i].cantUseWith) && (oAbilitySlots[i].cantUseWith.length > 0)))
                            {
                                sAbilityRequiresIcons = "<div style=\"position: absolute; left: 2px; top: 0\" class=\"required-weapon\">";

                                if (oAbilitySlots[i].isPacifist) {
                                  sAbilityRequiresIcons += `<img src=\"/icons/pacifist.svg\" style=\"height: 20px; width: 20px;\" />`;
                                }

                                if (IsValid(oAbilitySlots[i].requires))
                                {
                                    jQ.makeArray(oAbilitySlots[i].requires).forEach(function(oThisRequiredThing)
                                    {
                                        if (oThisRequiredThing.type === "weaponType")
                                        {
                                          jQ.makeArray(oThisRequiredThing.weaponTypes).forEach((weaponTypeRequired) => {
                                              sAbilityRequiresIcons += `<img src=\"/icons/${weaponTypeRequired}.svg\" style=\"height: 20px; width: 20px;\" />`;
                                          });

                                          if (oThisRequiredThing.weaponTypes[0] === "shield")
                                          {
                                              if (!IsValid(oEquipOffhand) || (oEquipOffhand.name.indexOf("shield") === -1))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires a shield!</span></div>";
                                          }
                                          else if (oThisRequiredThing.weaponTypes[0] === "dagger")
                                          {
                                              if (!IsValid(oEquipMainhand) || ((oEquipMainhand.name.indexOf("dagger") === -1) && (oEquipMainhand.name.indexOf("rapier") === -1)))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires a dagger or rapiers!</span></div>";
                                          }
                                          else if (oThisRequiredThing.weaponTypes[0] === "rapiers")
                                          {
                                              if (!IsValid(oEquipMainhand) || (oEquipMainhand.name.indexOf("rapier") === -1))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires rapiers!</span></div>";
                                          }
                                          else if ((oThisRequiredThing.weaponTypes[0] === "staff") || (oThisRequiredThing.weaponTypes[0] === "wand"))
                                          {
                                              if (!IsValid(oEquipMainhand) || ((oEquipMainhand.name.indexOf("staff") === -1) && (oEquipMainhand.name.indexOf("wand") === -1) && (oEquipMainhand.name.indexOf("trident") === -1) && (oEquipMainhand.name.indexOf("resonator") === -1)))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires a magic staff, wand or trident!</span></div>";
                                          }
                                          else if (oThisRequiredThing.weaponTypes[0] === "axe")
                                          {
                                              if (!IsValid(oEquipMainhand) || (oEquipMainhand.name.indexOf("axe") === -1))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires an axe!</span></div>";
                                          }
                                          else if ((oThisRequiredThing.weaponTypes[0] === "spear") || (oThisRequiredThing.weaponTypes[0] === "hammer"))
                                          {
                                              if (!IsValid(oEquipMainhand) || ((oEquipMainhand.name.indexOf("spear") === -1) && (oEquipMainhand.name.indexOf("hammer") === -1)))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires a spear or hammer!</span></div>";
                                          }
                                          else if ((oThisRequiredThing.weaponTypes[0] === "bow") || (oThisRequiredThing.weaponTypes[0] === "quiver"))
                                          {
                                              if (!IsValid(oEquipMainhand) || (oEquipMainhand.name.indexOf("bow") === -1) || !IsValid(oEquipOffhand) || (oEquipOffhand.name.indexOf("quiver") === -1))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability requires a bow and quiver!</span></div>";
                                          }
                                        }
                                    });
                                }

                                if (IsValid(oAbilitySlots[i].cantUseWith))
                                {
                                    jQ.makeArray(oAbilitySlots[i].cantUseWith).forEach(function(oThisForbiddenThing)
                                    {
                                        if (oThisForbiddenThing.type === "weaponType")
                                        {
                                          jQ.makeArray(oThisForbiddenThing.weaponTypes).forEach((weaponTypeForbidden) => {
                                              sAbilityRequiresIcons += `<img src="/icons/forbidden.svg" style="height: 20px; width: 20px; background: url(/icons/${weaponTypeForbidden}.svg) center center;">`;
                                          });

                                          if (oThisForbiddenThing.weaponTypes[0] === "bow")
                                          {
                                              if (IsValid(oEquipMainhand) && (oEquipMainhand.name.indexOf("bow") !== -1))
                                                  sAbilityWarningText = "<div><span style=\"color: red;\">🛑 This ability forbids a bow!</span></div>";
                                          }
                                        }
                                    });
                                }

                                sAbilityRequiresIcons += "</div>";
                            }

                            let sAbilityKindStyle = (CInt(oAbilitySlots[i].cooldown) === 0) ? "background-color: #f2ffe5;" : "";
                            try { sAbilityKindStyle = (oAbilitySlots[i].isSpell && ( IsValid(oAbilitySlots[i].requires)) && (oAbilitySlots[i].requires.length  >  0)) ? "background-color: #eeddff;" : sAbilityKindStyle; } catch (err) { }
                            try { sAbilityKindStyle = (oAbilitySlots[i].isSpell && (!IsValid(oAbilitySlots[i].requires)) || (oAbilitySlots[i].requires.length === 0)) ? "background-color: #e3f0ff;" : sAbilityKindStyle; } catch (err) { }

                            let sAbilityWarningStyle = (sAbilityWarningText.length > 0) ? "background-color: #ffccc4; border-color: #833;" : "";
                            let sCurrentCooldownStyle = (CDbl(oAbilitySlots[i].currentCooldown) > 0.0) ? "background-color: #999; border-color: #000;" : "";
                            let sCurrentCooldownText = (CDbl(oAbilitySlots[i].currentCooldown) > 0.0) ? `<div class=\"cooldown-text\">${Math.ceil(CDbl(oAbilitySlots[i].currentCooldown)).toFixed(0)}</div>` : "";

                            if (oAbilitySlots[i].isSpell)
                            {
                                jQ("#LayoutTweaksSpellCasts").append
                                (
                                    "<div class=\"item-icon-container item icon-box LayoutTweaksSpellCastsClick" + i.toFixed(0) + "\" style=\"position: relative;" + sAbilityKindStyle + sAbilityWarningStyle + sCurrentCooldownStyle+ "\">" +
                                    "    " + sCurrentCooldownText +
                                    "    <div class=\"item-amount text-capitalize\" style=\"position: absolute; bottom: 0px; right: 3px; font-size: 10px; text-align: center; width: 88px; max-width: 88px; text-overflow: hidden; display: inline-block; white-space: nowrap; overflow: hidden;\">" +
                                    "        " + sAbilityFriendlyName +
                                    "    </div>" +
                                    "    <div class=\"item-quality\">" +
                                    "        <i class=\"lilIcon-magic\"></i> " + oAbilitySlots[i].casts.toLocaleString() +
                                    "    </div>" +
                                    "    <img src=\"/icons/" + oAbilitySlots[i].icon + "\" class=\"item-icon\" />" +
                                    "    " + sAbilityRequiresIcons +
                                    "</div>"
                                );
                            }
                            else
                            {
                                jQ("#LayoutTweaksSpellCasts").append
                                (
                                    "<div class=\"item-icon-container item icon-box LayoutTweaksSpellCastsClick" + i.toFixed(0) + "\" style=\"position: relative;" + sAbilityKindStyle + sAbilityWarningStyle + sCurrentCooldownStyle + "\">" +
                                    "    " + sCurrentCooldownText +
                                    "    <div class=\"item-amount text-capitalize\" style=\"position: absolute; bottom: 0px; right: 3px; font-size: 10px; text-align: center; width: 88px; max-width: 88px; text-overflow: hidden; display: inline-block; white-space: nowrap; overflow: hidden;\">" +
                                    "        " + sAbilityFriendlyName +
                                    "    </div>" +
                                    "    " + sAbilityRequiresIcons +
                                    "    <img src=\"/icons/" + oAbilitySlots[i].icon + "\" class=\"item-icon\" />" +
                                    "</div>"
                                );
                            }

                            oEl = jQ(`.LayoutTweaksSpellCastsClick${i}`);

                            let sTooltipName = `LayoutTweaksTooltip-${ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i])}`;

                            if ((!oEl._tippy) && (jQ(`#${sTooltipName}`).length === 0))
                            {
                                jQ(`#tooltip-${sTooltipName}`).remove();
                                jQ("body").append(`<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-${sTooltipName}\"></div>`);

                                jQ(`#tooltip-${sTooltipName}`).html
                                (
                                    "    <h3 class=\"popover-title text-capitalize\">" +
                                    "        " + oAbilitySlots[i].name + "<br />" +
                                    "    </h3>" +
                                    "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                                    "        <div>" + oAbilitySlots[i].description + "</div>" +
                                    "        " + ((CInt(oAbilitySlots[i].cooldown) > 0) ? `<div>Cooldown <b>${oAbilitySlots[i].cooldown.toFixed(0).toFriendlyTime()}</b></div>` : "") +
                                    "        " + ((CInt(oAbilitySlots[i].casts) > 0) ? `<div><b>${CInt(oAbilitySlots[i].casts).toLocaleString()}</b> casts remaining.</div>` : "") +
                                    "        " + sAbilityWarningText +
                                    "        <div><b>Left Click</b> to go to ability loadout</div>" +
                                    "    </div>"
                                );

                                tippy(`.LayoutTweaksSpellCastsClick${i}`,
                                {
                                    html: jQ(`#tooltip-${sTooltipName}`)[0],
                                    performance: !0,
                                    animateFill: !1,
                                    distance: 5
                                });
                            }
                        }
                        else
                        {
                            jQ("#LayoutTweaksSpellCasts").append
                            (
                                "<div class=\"item-icon-container item icon-box LayoutTweaksSpellCastsClick" + i.toFixed(0) + "\" style=\"position: relative\">" +
                                "    [ Empty ]" +
                                "</div>"
                            );
                        }
                    }

                    for (let i = 0; i < (window.ET.MCMF.IsClass() ? 9 : 6); i++)
                    {
                        if (oAbilitySlots[i].equipped && oAbilitySlots[i].isSpell)
							jQ("#LayoutTweaksSpellCasts2").append("<button class=\"btn-sm d-flex justify-content-center LayoutTweaksSpellCastsCraft" + i.toFixed(0) + "\" style=\"width: 96px; margin: 4px;\">Craft Casts</button>");
                        else
							jQ("#LayoutTweaksSpellCasts2").append("<div class=\"d-flex\" style=\"width: 96px; margin: 4px;\"></div>");
                    }

                    if (!bSpells)
                        jQ("#LayoutTweaksSpellCasts2").remove();

                    for (let i = 0; i < (window.ET.MCMF.IsClass() ? 9 : 6); i++)
                    {
                        jQ(".LayoutTweaksSpellCastsClick" + i.toFixed(0)).css("cursor", "pointer").on('click', function()
                        {
                            //setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("button.loadout-btn"); setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("a.edit-abilities-btn"); try { tippy.hideAllPoppers(); } catch (err) { } }, 100); }, 10);

                            if (i === 0) ET_LayoutTweaksMod_AbilityModalList('mainHand', !oAbilitySlots[i].equipped);
                            if (i === 1) ET_LayoutTweaksMod_AbilityModalList('offHand', !oAbilitySlots[i].equipped);
                            if (i === 2) ET_LayoutTweaksMod_AbilityModalList('head', !oAbilitySlots[i].equipped);
                            if (i === 3) ET_LayoutTweaksMod_AbilityModalList('chest', !oAbilitySlots[i].equipped);
                            if (i === 4) ET_LayoutTweaksMod_AbilityModalList('legs', !oAbilitySlots[i].equipped);
                            if (i === 5) ET_LayoutTweaksMod_AbilityModalList('classAbil1', !oAbilitySlots[i].equipped);
                            if (i === 6) ET_LayoutTweaksMod_AbilityModalList('classAbil2', !oAbilitySlots[i].equipped);
                            if (i === 7) ET_LayoutTweaksMod_AbilityModalList('classAbil3', !oAbilitySlots[i].equipped);
                            if (i === (window.ET.MCMF.IsClass() ? 8 : 5)) ET_LayoutTweaksMod_AbilityModalList('companion', !oAbilitySlots[i].equipped);
                        });

                        if (oAbilitySlots[i].equipped && oAbilitySlots[i].isSpell)
                        {
                            jQ(".LayoutTweaksSpellCastsCraft" + i.toFixed(0)).on('click', function()
                            {
                                ET_LayoutTweaksMod_CraftSpellCast(ET_LayoutTweaksMod_AbilityID(oAbilitySlots[i]));

                                //ET.MCMF.CallGameCmd("users.setUiState", "magicTab", "spellBook");
                                //setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("a[href=\"/magic\"]"); }, 10);
                            });
                        }
                    }

                    let oEquipSlots = [];
                    let oEquipTemp;

                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "mainHand"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];
                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "offHand"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];
                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "neck"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];
                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "head"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];
                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "chest"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];
                    oEquipTemp = ET.MCMF.GetItems({category: "combat", equipped: true, slot: "legs"})[0];
                    oEquipSlots = [...oEquipSlots, IsValid(oEquipTemp) ? oEquipTemp : { equipped: false }];

                    for (let i = 0; i < 6; i++)
                    {
                        if (oEquipSlots[i].equipped)
                        {
							const rarityBorderStyle = ET_LayoutTweaksMod_RarityBorder(oEquipSlots[i].rarityId)
							let rarityBorderCSS = ""
							
							if (rarityBorderStyle && rarityBorderStyle.trim().length > 0)
								rarityBorderCSS = "border: " + rarityBorderStyle + ";";
							
                            jQ("#LayoutTweaksLobbyEquipment").append
                            (
                                "<div class=\"item-icon-container item icon-box LayoutTweaksLobbyEquipmentClick" + i.toFixed(0) + "\" style=\"position: relative; " + rarityBorderCSS + "\">" +
                                    "    <div class=\"item-amount text-capitalize\" style=\"position: absolute; bottom: 0px; right: 3px; font-size: 10px; text-align: center; width: 88px; max-width: 88px; text-overflow: hidden; display: inline-block; white-space: nowrap; overflow: hidden;\">" +
                                    "        " + oEquipSlots[i].name +
                                    "    </div>" +
                                    "    <div class=\"item-quality\">" +
                                    "        " + ((CDbl(oEquipSlots[i].quality) > 0.0) ? `${oEquipSlots[i].quality.toFixed(0)}%` : "") +
                                    "    </div>" +
                                    "    <img src=\"/icons/" + oEquipSlots[i].icon + "\" class=\"item-icon\" />" +
                                "</div>"
                            );
                        }
                        else
                        {
                            jQ("#LayoutTweaksLobbyEquipment").append
                            (
                                "<div class=\"item-icon-container item icon-box LayoutTweaksLobbyEquipmentClick" + i.toFixed(0) + "\" style=\"position: relative\">" +
                                "    [ Empty ]" +
                                "</div>"
                            );
                        }
                    }

                    for (let i = 0; i < 6; i++)
                    {
                        if (oEquipSlots[i].equipped)
                        {
							//console.log(oEquipSlots[i])
							
                            jQ(`.LayoutTweaksLobbyEquipmentClick${i}`).css("cursor", "pointer").on('click', function()
                            {
                                //setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("button.loadout-btn"); setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("a.edit-gear-btn"); try { tippy.hideAllPoppers(); } catch (err) { } }, 100); }, 10);

                                if (oEquipSlots[i].slot === "mainHand") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "primary weapon");
                                if (oEquipSlots[i].slot === "offHand") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "off-hand weapon or shield");
                                if (oEquipSlots[i].slot === "head") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "helm or hat");
                                if (oEquipSlots[i].slot === "neck") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "amulet");
                                if (oEquipSlots[i].slot === "chest") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "chest armor or shirt");
                                if (oEquipSlots[i].slot === "legs") ET_LayoutTweaksMod_EquipmentModalList(oEquipSlots[i].slot, "leg armor or shorts");
                            });

                            oEl = jQ(`.LayoutTweaksLobbyEquipmentClick${i}`);

                            let sTooltipName = `LayoutTweaksTooltip-${oEquipSlots[i].ItemId}`;

                            if ((!oEl._tippy) && (jQ(`#${sTooltipName}`).length === 0))
                            {
                                jQ(`#tooltip-${sTooltipName}`).remove();
                                jQ("body").append(`<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-${sTooltipName}\"></div>`);

                                let sStatLines = "";
                                let sExtraText = "";

                                if (IsValid(oEquipSlots[i].stats))
                                {
									let localStatsObj = Object.assign({}, oEquipSlots[i].stats)
									
									try {
										const rarityIdConsts = ET_LayoutsMod__ITEM_RARITIES[oEquipSlots[i].rarityId]
										if (rarityIdConsts && rarityIdConsts.statBonuses) {
											Object.keys(localStatsObj).forEach((statName) => {
											// disallow % bonuses to attack speed
											if (statName !== "attackSpeed") {
												// ensure that property refers to a stat that is a number and valued more than 0 (non-numeric/non-positive/non-zero all disallowed)
												if (CDbl(localStatsObj[statName]) > 0.0) {
													localStatsObj[statName] = localStatsObj[statName] * ((100.0 + rarityIdConsts.statBonuses) / 100.0)
													}
												}
											})

										}
									} catch (err) {}
									
									const rarityColorStyle = ET_LayoutTweaksMod_RarityColor(oEquipSlots[i].rarityId)
									if (rarityColorStyle && rarityColorStyle.trim().length > 0) {
										sStatLines = `${sStatLines}<span style="color: ${rarityColorStyle}; text-transform: capitalize;"><b>${oEquipSlots[i].rarityId}</b></span> <br />`;
									}
					
                                    if (IsValid(localStatsObj.damage) && (CDbl(localStatsObj.damage) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-damage extra-small-icon mr-1\"></i>${localStatsObj.damage.toFixed(1)}</div>`;
                                    if (IsValid(localStatsObj.energyStorage) && (CDbl(localStatsObj.energyStorage) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-energyStorage extra-small-icon mr-1\"></i>${localStatsObj.energyStorage.toFixed(1)} energy storage</div>`;

                                    if (IsValid(localStatsObj.attack) && (CDbl(localStatsObj.attack) != 0.0)) {
                                      if (IsValid(localStatsObj.attackMax) && (CDbl(localStatsObj.attackMax) != 0.0)) {
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attack extra-small-icon mr-1\"></i>${localStatsObj.attack.toFixed(1)} - ${localStatsObj.attackMax.toFixed(1)} damage</div>`;
                                      } else {
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attack extra-small-icon mr-1\"></i>${localStatsObj.attack.toFixed(1)} damage</div>`;
                                      }
                                    }
                                    if (IsValid(localStatsObj.attackSpeed) && (CDbl(localStatsObj.attackSpeed) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-attackSpeed extra-small-icon mr-1\"></i>${localStatsObj.attackSpeed.toFixed(1)} attack speed</div>`;
                                    if (IsValid(localStatsObj.healthMax) && (CDbl(localStatsObj.healthMax) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-healthMax extra-small-icon mr-1\"></i>${localStatsObj.healthMax.toFixed(1)} health</div>`;
                                    if (IsValid(localStatsObj.accuracy) && (CDbl(localStatsObj.accuracy) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-accuracy extra-small-icon mr-1\"></i>${localStatsObj.accuracy.toFixed(1)} accuracy</div>`;
                                    if (IsValid(localStatsObj.magicPower) && (CDbl(localStatsObj.magicPower) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-magicPower extra-small-icon mr-1\"></i>${localStatsObj.magicPower.toFixed(1)} magic power</div>`;
                                    if (IsValid(localStatsObj.magicArmor) && (CDbl(localStatsObj.magicArmor) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-magicArmor extra-small-icon mr-1\"></i>${localStatsObj.magicArmor.toFixed(1)} magic armor</div>`;
                                    if (IsValid(localStatsObj.healingPower) && (CDbl(localStatsObj.healingPower) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-healingPower extra-small-icon mr-1\"></i>${localStatsObj.healingPower.toFixed(1)} healing power</div>`;
                                    if (IsValid(localStatsObj.defense) && (CDbl(localStatsObj.defense) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-defense extra-small-icon mr-1\"></i>${localStatsObj.defense.toFixed(1)} defense</div>`;
                                    if (IsValid(localStatsObj.armor) && (CDbl(localStatsObj.armor) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-armor extra-small-icon mr-1\"></i>${localStatsObj.armor.toFixed(1)} armor</div>`;

                                    if (IsValid(localStatsObj.energyRegen) && (CDbl(localStatsObj.energyRegen) != 0.0))
                                        sStatLines = `${sStatLines}<div class=\"d-flex align-items-center\"><i class=\"lilIcon-energyRegen extra-small-icon mr-1\"></i>${localStatsObj.energyRegen.toFixed(1)} energy regen</div>`;
                                }

                                jQ.makeArray(oEquipSlots[i].enchantments).forEach(function(oThisEnchant)
                                {
                                    sExtraText = `${sExtraText}<div style="text-capitalize">Enchantment: ${oThisEnchant}</div>`;
                                });

                                jQ(`#tooltip-${sTooltipName}`).html
                                (
                                    "    <h3 class=\"popover-title text-capitalize\">" +
                                    "        " + oEquipSlots[i].name + ((oEquipSlots[i].enhanced) ? " (E)" : "") + ((oEquipSlots[i]?.extraStats?.level > 0) ? " (Lv." + oEquipSlots[i]?.extraStats?.level.toFixed(0) + ")" : "") + "<br />" +
                                    "    </h3>" +
                                    "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                                    "        " + sStatLines +
                                    "        " + sExtraText +
                                    "        <div><b>Left Click</b> to go to equipment loadout</div>" +
                                    "    </div>"
                                );

                                tippy(`.LayoutTweaksLobbyEquipmentClick${i}`,
                                {
                                    html: jQ(`#tooltip-${sTooltipName}`)[0],
                                    performance: !0,
                                    animateFill: !1,
                                    distance: 5
                                });
                            }
                        }
                        else
                        {
                            jQ(`.LayoutTweaksLobbyEquipmentClick${i}`).css("cursor", "pointer").on('click', function()
                            {
                                if (i === 0) ET_LayoutTweaksMod_EquipmentModalList("mainHand", "primary weapon");
                                if (i === 1) ET_LayoutTweaksMod_EquipmentModalList("offHand", "off-hand weapon or shield");
                                if (i === 2) ET_LayoutTweaksMod_EquipmentModalList("neck", "amulet");
                                if (i === 3) ET_LayoutTweaksMod_EquipmentModalList("head", "helm or hat");
                                if (i === 4) ET_LayoutTweaksMod_EquipmentModalList("chest", "chest armor or shirt");
                                if (i === 5) ET_LayoutTweaksMod_EquipmentModalList("legs", "leg armor or shorts");
                            })
                        }
                    }

                    jQ("#LayoutTweaksSpellCasts").find("div.item-icon-container").hover
                    (
                        function() { jQ(this).css("border-width", "3px"); },
                        function() { jQ(this).css("border-width",    ""); }
                    );

                    jQ("#LayoutTweaksLobbyEquipment").find("div.item-icon-container").hover
                    (
                        function() { jQ(this).css("border-width", "3px"); },
                        function() { jQ(this).css("border-width",    ""); }
                    );
                }
            }
            else
            {
                jQ("#LayoutTweaksSpellCasts2").remove();
                jQ("#LayoutTweaksSpellCasts").remove();
                jQ("#LayoutTweaksLobbyEquipment").remove();
            }
        }
        catch (err) { console.log(err); }

        // fix ET bug not showing any tooltip info for woodcutters
        /*
        if (ET.MCMF.GetActiveTab() === "woodcutting")
        {
            try
            {
                let oWoodcutters = Meteor.connection._stores.woodcutting._getCollection().find({owner: Meteor.connection.userId()}).fetch()[0].woodcutters;
                let bPersonalWoodcuttingBuff = ET.MCMF.PersonalWoodcuttingBuff().active;

                let idx = -1;

                jQ("div.item-icon-container").each(function()
                {
                    if (!jQ(this).hasClass("small"))
                    {
                        idx++;
                        let oThis = jQ(this);

                        if (!oThis.hasClass("LayoutModWoodcutterFix"))
                        {
                            console.debug(jQ(this));
                            console.debug(oWoodcutters[idx]);

                            try
                            {
                                let sAccBonus = "";

                                if (bPersonalWoodcuttingBuff)
                                    sAccBonus = " +&nbsp;<span style=\"color: blue;\">" + (Math.round(oWoodcutters[idx].stats.accuracy * 2) / 10.0) + "</span>&nbsp;";

                                if (IsValid(oThis[0]._tippy))
                                    oThis[0]._tippy.destroy();

                                oThis.addClass("LayoutModWoodcutterFix").addClass("LayoutModWoodcutterIndex" + idx.toFixed(0))

                                jQ("body").append("<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-LayoutTweaksModTooltip" + idx.toFixed(0) + "\"></div>");

                                jQ("#tooltip-LayoutTweaksModTooltip" + idx.toFixed(0)).html(
                                    "    <h3 class=\"popover-title text-capitalize\">" +
                                    "        <b>" + oWoodcutters[idx].name + "</b>" +
                                    "    </h3>" +
                                    "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                                    `        <div class=\"d-flex align-items-center\"><i class=\"lilIcon-attack extra-small-icon mr-1\"></i>${oWoodcutters[idx].stats.attack.toFixed(0)}</div>` +
                                    `        <div class=\"d-flex align-items-center\"><i class=\"lilIcon-attackSpeed extra-small-icon mr-1\"></i>${oWoodcutters[idx].stats.attackSpeed}</div>` +
                                    `        <div class=\"d-flex align-items-center\"><i class=\"lilIcon-accuracy extra-small-icon mr-1\"></i>${oWoodcutters[idx].stats.accuracy}${sAccBonus}</div>` +
                                    `        <br />` +
                                    `        <b>Left Click</b>&nbsp;to activate Suicidal Fury<br />` +
                                    "    </div>"
                                );

                                tippy(".LayoutModWoodcutterIndex" + idx.toFixed(0), {
                                    html: jQ("#tooltip-LayoutTweaksModTooltip" + idx.toFixed(0))[0],
                                    performance: !0,
                                    animateFill: !1,
                                    distance: 5,
                                });

                                //oThis._tippy.popper.querySelector('.popover-content').innerHTML = sToolTipInfo;
                            }
                            catch (err) {
                            }
                        }
                    }
                });
            }
            catch (err) { }
        }
        */

        // resort skills like they are under profile.. ciepun's request.. because reasons
        if (ET.MCMF.GetActiveTab() === "skills")
        {
            try
            {
                let oSkillDivs = jQ("div.player-skills-container").find("div.my-1");

                if (!jQ(oSkillDivs.get(0)).hasClass("lilIcon-attack"))
                {
                    oSkillDivs.sort(function(divA, divB)
                    {
                        let oIconDataA = jQ(divA).find("i");
                        let oIconDataB = jQ(divB).find("i");

                        if (oIconDataA.hasClass("lilIcon-attack")) return -1;
                        if (oIconDataB.hasClass("lilIcon-attack")) return 1;

                        if (oIconDataA.hasClass("lilIcon-defense")) return -1;
                        if (oIconDataB.hasClass("lilIcon-defense")) return 1;

                        if (oIconDataA.hasClass("lilIcon-health")) return -1;
                        if (oIconDataB.hasClass("lilIcon-health")) return 1;

                        if (oIconDataA.hasClass("lilIcon-magic")) return -1;
                        if (oIconDataB.hasClass("lilIcon-magic")) return 1;

                        if (oIconDataA.hasClass("lilIcon-mining")) return -1;
                        if (oIconDataB.hasClass("lilIcon-mining")) return 1;

                        if (oIconDataA.hasClass("lilIcon-farming")) return -1;
                        if (oIconDataB.hasClass("lilIcon-farming")) return 1;

                        if (oIconDataA.hasClass("lilIcon-inscription")) return -1;
                        if (oIconDataB.hasClass("lilIcon-inscription")) return 1;

                        if (oIconDataA.hasClass("lilIcon-crafting")) return -1;
                        if (oIconDataB.hasClass("lilIcon-crafting")) return 1;

                        if (oIconDataA.hasClass("lilIcon-woodcutting")) return -1;
                        if (oIconDataB.hasClass("lilIcon-woodcutting")) return 1;

                        if (oIconDataA.hasClass("lilIcon-astronomy")) return -1;
                        if (oIconDataB.hasClass("lilIcon-astronomy")) return 1;

                        // total is always last

                        return 0;
                    });

                    oSkillDivs.detach().appendTo(jQ("div.player-skills-container"));
                }
            }
            catch (err) { }
        }

        if (ET.MCMF.GetActiveTab() === "home")
        {
            try
            {
                // remove the superfluous 'Link' text for side links
                //jQ("div.body-content#content").find("div.mr-3.hidden-md-down").find("div.text-muted.mb-1").remove();

                // remove the Shop link -- we don't need it here, too
                //jQ("div.body-content#content").find("div.mr-3.hidden-md-down").find("a[href=\"/shop\"]").remove();

                // remove the Skills link -- we don't need it here, too
                //jQ("div.body-content#content").find("div.mr-3.hidden-md-down").find("a[href=\"/skills\"]").remove();

                // fix the 'New Things' color -- it doesn't need to be red here
                //jQ("div.body-content#content").find("div.mr-3.hidden-md-down").find("a[href=\"/updates\"]").removeClass('new-update-color');

                // progress bars under farm plots aren't visible because of a height issue
                //jQ(".home-page-content").find("div.farm-space-container").find("div.icon-box").css("height", "66px");

                // adventures
                /*jQ(".home-page-content").find("div.adventure-item-container").each(function()
                {
                    let sAdvID = jQ(this).find("button.cancel-adventure-btn").attr("data-id");
                    let oAdventure = Meteor.connection._stores.adventures._getCollection().find().fetch()[0].adventures.filter(function(oThisAdventure)
                    {
                        if (oThisAdventure.id !== sAdvID) return false;
                        return true;
                    })[0];

                    if (oAdventure.endDate !== undefined)
                    {
                        if (oAdventure.startDate < new Date())
                        {
                            let iTimeLeftOnAdv = CInt(Math.floor(Math.abs(new Date(oAdventure.endDate) - (new Date())) / 1000.0));
                            let sProg = Math.floor(100 - (iTimeLeftOnAdv * 100 / oAdventure.duration)).toFixed(0);

                            jQ(jQ(this).find("div.mx-3.flex-grow").find("div").get(1)).html(iTimeLeftOnAdv.toString().toHHMMSS());
                            jQ(this).find("div.mx-3.align-items-center").html("<div style=\"width: 100%;\"><div class=\"progress\"><div class=\"progress-bar\" role=\"progressbar\" style=\"width: " + sProg + "%\" aria-valuenow=\"" + sProg + "\" aria-valuemin=\"0\" aria-valuemax=\"100\"></div></div></div>");
                        }
                        else
                            jQ(jQ(this).find("div.mx-3.flex-grow").find("div").get(1)).html(oAdventure.duration.toString().toHHMMSS());
                    }
                    else
                        jQ(jQ(this).find("div.mx-3.flex-grow").find("div").get(1)).html(oAdventure.duration.toString().toHHMMSS());
                });
                */

                /*
                jQ(".home-page-content").find("button.cancel-adventure-btn").each(function()
                {
                    let oEl = jQ(this);

                    if (oEl.attr("ModifiedByLayoutTweaks") !== "true")
                        oEl.attr("ModifiedByLayoutTweaks", "true").on('click', function() { ET.MCMF.CallGameCmd("adventures.cancelAdventure", oEl.attr("data-id")); });
                });
                */
            }
            catch (err) { ET.MCMF.Log("Error on home page!"); ET.MCMF.Log(err); }
        }

        if (ET.MCMF.IsNewCombatTab())
        {
            //jQ("div.lobby-container").find("button.btn.btn-danger.battle-boss-btn").remove();

            if (!ET_LayoutTweaksMod_CombatLobby_page_rendered)
                ET_LayoutTweaksMod_DisableLobbyButtons();
        }

        if (ET.MCMF.IsNewCombatTab())
        {
/*
            try
            {
                let oConsumables = ET.MCMF.GetItems({category: "food"});

                oConsumables = oConsumables.filter(function(oItem)
                {
                    if (oItem.hidden) return false; // don't show consumable items the player's hidden
                    return true;
                });

                if ((oConsumables.length > 0) && ((ET_LayoutTweaksMod_IsEatingAnything()) || (jQ(".MCFoodList").length !== oConsumables.length)))
                {
                    if ((jQ("a.consumables-btn").length > 0) && (jQ("a.consumables-btn").parent().parent().find("div.item-icon-container").length > 0))
                    {
                        let iCountExistingFoodBoxes = jQ("a.consumables-btn").parent().parent().find("div.item-icon-container").length;

                        // add marker
                        jQ(jQ("a.consumables-btn").parent().parent().find("div.item-icon-container").get(iCountExistingFoodBoxes - 1)).after("<div class=\"MCFoodList-temp\" style=\"position: relative\" ></div>");

                        // remove existing consumable boxes (except items being consumed)
                        jQ("a.consumables-btn").parent().parent().find("div.item-icon-container").each(function()
                        {
                            if (!(jQ(this).hasClass("food-icon-container") && jQ(this).hasClass("disabled")))
                                jQ(this).remove();
                        });

                        // add new consumable boxes
                        try
                        {
                            oConsumables.forEach(function(oThisItem)
                            {
                                try
                                {
                                    let sDivFinder = "div[data-id=\"" + oThisItem._id.toString() + "\"]";
                                    let bIsDisplayed = jQ(sDivFinder).length > 0;
                                    let bEatingThis = ET_LayoutTweaksMod_IsEatingThis(oThisItem.itemId);

                                    if (bIsDisplayed && bEatingThis)
                                    {
                                        if (jQ(sDivFinder)._tippy)
                                            jQ(sDivFinder)._tippy.destroy();
                                        jQ("#MCLTConsumable-" + oThisItem._id.toString()).remove();
                                        jQ(sDivFinder).remove();
                                    }
                                    else if (!bIsDisplayed && !bEatingThis)
                                    {
                                        jQ(jQ("a.consumables-btn").parent().parent().find(".MCFoodList-temp").get(0)).before
                                        (
                                            "<div class=\"MCFoodList item-icon-container item small icon-box\" style=\"position: relative\" data-id=\"" + oThisItem._id.toString() + "\">" +
                                            "<img src=\"/icons/" + oThisItem.icon.toString() + "\" class=\"item-icon\" />" +
                                            "<div class=\"item-amount\">" + oThisItem.amount.toLocaleString() + "</div>" +
                                            "</div>"
                                        );
                                    }

                                    // note: unfortunately, the game is messing with the HTML this section every few ticks when digesting a consumable
                                    //       this means that we will have flickering tooltips for our own DIVs and there's probably not much that can be done about it
                                }
                                catch (err) { }
                            });
                        }
                        catch (err) { }

                        // remove marker
                        jQ(".MCFoodList-temp").remove();

                        // add tooltips and on-click event trigger to consumable boxes
                        jQ("a.consumables-btn").parent().parent().find("div.item-icon-container").each(function()
                        {
                            let oEl = jQ(this);

                            try
                            {
                                let oThisConsumable = ET.MCMF.GetItems({_id: oEl.attr("data-id")})[0];

                                let sTooltipName = "MCLTConsumable-" + oThisConsumable._id.toString();

                                if ((!oEl._tippy) && (jQ("#" + sTooltipName).length === 0))
                                {
                                    jQ("#tooltip-" + sTooltipName).remove();
                                    jQ("body").append("<div class=\"item-tooltip-content my-tooltip-inner\" id=\"tooltip-" + sTooltipName + "\"></div>");

                                    jQ("#tooltip-" + sTooltipName).html
                                    (
                                        "    <h3 class=\"popover-title text-capitalize\">" +
                                        "        " + oThisConsumable.name + "<br />" +
                                        "    </h3>" +
                                        "    <div class=\"popover-content\" style=\"max-width: 400px;\">" +
                                        "        " + oThisConsumable.description + "<br />" +
                                        "        <b>Left Click</b> to eat<br />" +
                                        "    </div>"
                                    );

                                    tippy("div[data-id=\"" + oThisConsumable._id.toString() + "\"]",
                                    {
                                        html: jQ("#tooltip-" + sTooltipName)[0],
                                        performance: !0,
                                        animateFill: !1,
                                        distance: 5
                                    });

                                    jQ("div[data-id=\"" + oThisConsumable._id.toString() + "\"]").on('click', function()
                                    {
                                        let oThisLocalConsumable = ET.MCMF.GetItems({_id: jQ(this).attr("data-id")})[0];

                                        ET.MCMF.CallGameCmd("items.eat", oThisLocalConsumable._id, oThisLocalConsumable.itemId);
                                    });

                                    oThisConsumable = undefined;
                                }
                            }
                            catch (err) { }
                        });
                    }
                }
            }
            catch (err) { }
            */
        }

        // hide tomes and magic books from the inscription tab and resort the items
        if (ET.MCMF.GetActiveTab() === "inscription")
        {
            try
            {
                jQ("div.item-icon-container.item").each(function()
                {
                    let oEl = jQ(this);

                    try
                    {
                        let oThisItem = ET.MCMF.GetItems({_id: oEl.attr("data-id")})[0];

                        if (oThisItem.category === "tome") oEl.remove();
                        if (oThisItem.category === "magic_book") oEl.remove();
                    }
                    catch (err) // no category
                    {
                        oEl.remove();
                    }
                });

                var itemContainer = jQ(jQ("div.item-icon-container.item").get(0)).parent();
                var items = itemContainer.find("div.item-icon-container.item");

                items.sort(function(item_A, item_B)
                {
                    let oEl_A = jQ(item_A);
                    let oEl_B = jQ(item_B);

                    let oItem_A = ET.MCMF.GetItems({_id: oEl_A.attr("data-id")})[0];
                    let oItem_B = ET.MCMF.GetItems({_id: oEl_B.attr("data-id")})[0];

                    try
                    {
                        if ((oItem_A.category === "pigment") && (oItem_B.category !== "pigment")) return -1;
                        if ((oItem_A.category !== "pigment") && (oItem_B.category === "pigment")) return 1;
                        if ((oItem_A.category === "woodcutting") && (oItem_B.category !== "woodcutting")) return -1;
                        if ((oItem_A.category !== "woodcutting") && (oItem_B.category === "woodcutting")) return 1;
                        if ((oItem_A.category === "paper") && (oItem_B.category !== "paper")) return -1;
                        if ((oItem_A.category !== "paper") && (oItem_B.category === "paper")) return 1;
                        if ((oItem_A.category === "book") && (oItem_B.category !== "book")) return -1;
                        if ((oItem_A.category !== "book") && (oItem_B.category === "book")) return 1;
                    }
                    catch (err) // no category
                    {
                        return -1;
                    }

                    if (oItem_A.name < oItem_B.name) return -1;
                    if (oItem_A.name > oItem_B.name) return 1;

                    return 0;
                });

                items.detach().appendTo(itemContainer);

            }
            catch (err) { ET.MCMF.Log("Error on inscription tab"); ET.MCMF.Log(err); }
        }

        // change the link to your own profile (by image, not by text) to redirect to your loadout page instead
        if (ET.MCMF.IsNewCombatTab())
        {
            try
            {
                jQ("div.lobby-container").find("a[href=\"/profile/" + ET.MCMF.UserName + "\"]").attr("href", "").on('click', function()
                {
                    setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("button.loadout-btn"); }, 10);
                });
            }
            catch (err) { }
        }

        // change the link to your own profile (by image, not by text) to redirect to your loadout page instead
        if (ET.MCMF.IsNewCombatTab())
        {
            try
            {
                if (jQ(".body-content#content").find("div.item-icon-container.item").length > 0)
                {
                    if (jQ(jQ(".body-content#content").find("h4").get(0)).text().trim().toLowerCase().indexOf("gear") === 0)
                    {
                        // if we get this far, we're in the right sub-section of Battle > Loadouts

                        jQ(".body-content#content").find("div.item-icon-container.item").each(function()
                        {
                            let oEl = jQ(this);

                            try
                            {
                                let oThisItem = ET.MCMF.GetItems({_id: oEl.attr("data-id")})[0];

                                if (oThisItem._id.length < 5)
                                    throw "not an item";

                                oEl.css("cursor", "pointer").on('click', function()
                                {
                                    setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("a.edit-gear-btn"); }, 10);
                                });
                            }
                            catch (err) // no item, probably an ability (which is being given item-icon-container anyway
                            {
                                oEl.css("cursor", "pointer").on('click', function()
                                {
                                    setTimeout(function() { ET_LayoutTweaksMod_HackyClicker("a.edit-abilities-btn"); }, 10);
                                });
                            }
                        });
                    }
                }
            }
            catch (err) { }
        }

        // hide player-hidden combat items in the equipment loadout screen
        if (ET.MCMF.IsNewCombatTab())
        {
            try
            {
                if (jQ(".body-content#content").find("div.item-icon-container.item").length > 0)
                {
                    if (jQ(jQ(".body-content#content").find("h4").get(0)).text().trim().toLowerCase() === "equipped gear")
                    {
                        // if we get this far, we're in the right sub-section of Battle > Loadouts > Edit

                        jQ(".body-content#content").find("div.item-icon-container.item").each(function()
                        {
                            try
                            {
                                let oEl = jQ(this);
                                let oThisItem = ET.MCMF.GetItems({_id: oEl.attr("data-id")})[0];

                                if ((!oThisItem.equipped) && (oThisItem.hidden))
                                    oEl.remove();
                            }
                            catch (err) { }
                        });
                    }
                }
            }
            catch (err) { }
        }

        // mark out tomes the player has already learned in the ability loadout screen
        if (ET.MCMF.IsNewCombatTab())
        {
            try
            {
                if (jQ(".body-content#content").find("div.item-icon-container.item").length > 0)
                {
                    if (jQ(jQ(".body-content#content").find("h4").get(0)).text().trim().toLowerCase() === "equipped abilities")
                    {
                        // if we get this far, we're in the right sub-section of Battle > Loadouts > Edit

                        jQ(".body-content#content").find("div.item-icon-container.item").each(function()
                        {
                            try
                            {
                                let oEl = jQ(this);

                                if (!oEl.hasClass("disabled"))
                                {
                                    let oThisItem = ET.MCMF.GetItems({_id: oEl.attr("data-id")})[0];

                                    jQ.makeArray(Meteor.connection._stores.abilities._getCollection().find({owner: Meteor.connection._userId}).fetch()[0].learntAbilities).forEach(function(oThisAbility)
                                    {
                                        if (oThisItem.itemId.startsWith(oThisAbility.abilityId) || ((oThisItem.itemId === "wisdom_tome") && (oThisAbility.abilityId === "magic_wisdom")))
                                        {
                                            let iLvl = 0;

                                            if (oThisItem.itemId.indexOf("_1") !== -1) iLvl = 1;
                                            if (oThisItem.itemId.indexOf("_2") !== -1) iLvl = 2;
                                            if (oThisItem.itemId.indexOf("_3") !== -1) iLvl = 3;
                                            if (oThisItem.itemId.indexOf("_4") !== -1) iLvl = 4;
                                            if (oThisItem.itemId.indexOf("_5") !== -1) iLvl = 5;

                                            let bShouldRemove = true;

                                            if (iLvl > 0)
                                                if (iLvl > oThisAbility.level)
                                                    bShouldRemove = false;

                                            if (bShouldRemove)
                                                oEl.addClass("disabled");
                                        }
                                    });
                                }
                            }
                            catch (err) { }
                        });
                    }
                }
            }
            catch (err) { }
        }
    }
    catch (err) { }
};

ET_LayoutTweaksMod_IsEatingThis = function(item)
{
    let isEatingThis = false;

    try
    {
        jQ.makeArray(Meteor.connection._stores.combat._getCollection().find({owner:Meteor.connection._userId}).fetch()[0].buffs).forEach(function(oThisBuff)
        {
            if ((oThisBuff.data.duplicateTag === "eatingFood") && (oThisBuff.id.indexOf("food_") === 0))
            {
                if (oThisBuff.id.substr(5) === item)
                    isEatingThis = true;
            }
        });
    }
    catch (err) { }

    return isEatingThis;
};

ET_LayoutTweaksMod_IsEatingAnything = function(item)
{
    let isEating = false;

    try
    {
        jQ.makeArray(Meteor.connection._stores.combat._getCollection().find({owner:Meteor.connection._userId}).fetch()[0].buffs).forEach(function(oThisBuff)
        {
            if ((oThisBuff.data.duplicateTag === "eatingFood") && (oThisBuff.id.indexOf("food_") === 0))
                isEating = true;
        });
    }
    catch (err) { }

    return isEating;
};

/*
ET_LayoutTweaksMod_CondenseChat = function(thisNode)
{
    try
    {
        if (thisNode.find(".direct-chat-name.pull-left").length > 0)
        {
            let sHTML;

            sHTML = thisNode.find("span.direct-chat-timestamp");
            sHTML.css("margin-left", "0px").css("color", "#999 !important").prependTo(thisNode.find("div.direct-chat-info"));

            thisNode.find("span.direct-chat-name").removeClass("pull-left");

            sHTML = thisNode.find("div.direct-chat-info").html();
            sHTML = sHTML.replace("<div ", "<span ").replace("</div>", "</span>");
            thisNode.find("div.direct-chat-info").remove();
            jQ(sHTML).prependTo(thisNode.find("div.direct-chat-text"));
        }
    }
    catch (err)
    {
        ET.MCMF.Log("Exception in CondenseChat");
        console.log(err);
    }
};

ET_LayoutTweaksMod_CondenseAllChats_in_progress = false;
ET_LayoutTweaksMod_CondenseAllChats = function()
{
    if (ET_LayoutTweaksMod_CondenseAllChats_in_progress)
        return;

    ET_LayoutTweaksMod_CondenseAllChats_in_progress = true;

    jQ(".direct-chat-msg").each(function()
    {
        if ((this.id !== undefined) && (this.id.length === 17))
            ET_LayoutTweaksMod_CondenseChat(jQ(this));
    });

    ET_LayoutTweaksMod_CondenseAllChats_in_progress = false;
};
*/


////////////////////////////////////////////////////////////////
/////////////// ** common.js -- DO NOT MODIFY ** ///////////////
time_val = function()
{
    return CDbl(Math.floor(Date.now() / 1000));
};

IsValid = function(oObject)
{
    if (oObject === undefined) return false;
    if (oObject === null) return false;
    return true;
};

const CommonRandom = function(iMin, iMax)
{
    return parseInt(iMin + Math.floor(Math.random() * iMax));
};

ShiftClick = function(oEl)
{
    jQ(oEl).trigger(ShiftClickEvent());
};

ShiftClickEvent = function(target)
{
	let shiftclickOrig = jQ.Event("click");
    shiftclickOrig.which = 1; // 1 = left, 2 = middle, 3 = right
    //shiftclickOrig.type = "click"; // "mousedown" ?
	shiftclickOrig.shiftKey = true;
    shiftclickOrig.currentTarget = target;

	let shiftclick = jQ.Event("click");
    shiftclick.which = 1; // 1 = left, 2 = middle, 3 = right
    //shiftclick.type = "click"; // "mousedown" ?
	shiftclick.shiftKey = true;
    shiftclick.currentTarget = target;

	shiftclick.originalEvent = shiftclickOrig;

    //document.ET_Util_Log(shiftclick);

	return shiftclick;
};

if (!String.prototype.replaceAll)
    String.prototype.replaceAll = function(search, replace) { return ((replace === undefined) ? this.toString() : this.replace(new RegExp('[' + search + ']', 'g'), replace)); };

if (!String.prototype.startsWith)
    String.prototype.startsWith = function(search, pos) { return this.substr(((!pos) || (pos < 0)) ? 0 : +pos, search.length) === search; };

CInt = function(v)
{
	try
	{
		if (!isNaN(v)) return Math.floor(v);
		if (typeof v === 'undefined') return parseInt(0);
		if (v === null) return parseInt(0);
		let t = parseInt(v);
		if (isNaN(t)) return parseInt(0);
		return Math.floor(t);
	}
	catch (err) { }

	return parseInt(0);
};

CDbl = function(v)
{
	try
	{
		if (!isNaN(v)) return parseFloat(v);
		if (typeof v === 'undefined') return parseFloat(0.0);
		if (v === null) return parseFloat(0.0);
		let t = parseFloat(v);
		if (isNaN(t)) return parseFloat(0.0);
		return t;
	}
	catch (err) { }

	return parseFloat(0.0);
};

// dup of String.prototype.startsWith, but uses indexOf() instead of substr()
startsWith = function (haystack, needle) { return (needle === "") || (haystack.indexOf(needle) === 0); };
endsWith   = function (haystack, needle) { return (needle === "") || (haystack.substring(haystack.length - needle.length) === needle); };

Chopper = function(sText, sSearch, sEnd)
{
	let sIntermediate = "";

	if (sSearch === "")
		sIntermediate = sText.substring(0, sText.length);
	else
	{
		let iIndexStart = sText.indexOf(sSearch);
		if (iIndexStart === -1)
			return sText;

		sIntermediate = sText.substring(iIndexStart + sSearch.length);
	}

	if (sEnd === "")
		return sIntermediate;

	let iIndexEnd = sIntermediate.indexOf(sEnd);

	return (iIndexEnd === -1) ? sIntermediate : sIntermediate.substring(0, iIndexEnd);
};

ChopperBlank = function(sText, sSearch, sEnd)
{
	let sIntermediate = "";

	if (sSearch === "")
		sIntermediate = sText.substring(0, sText.length);
	else
	{
		let iIndexStart = sText.indexOf(sSearch);
		if (iIndexStart === -1)
			return "";

		sIntermediate = sText.substring(iIndexStart + sSearch.length);
	}

	if (sEnd === "")
		return sIntermediate;

	let iIndexEnd = sIntermediate.indexOf(sEnd);

	return (iIndexEnd === -1) ? "" : sIntermediate.substring(0, iIndexEnd);
};

CondenseSpacing = function(text)
{
	while (text.indexOf("  ") !== -1)
		text = text.replace("  ", " ");
	return text;
};

// pad available both ways as pad(string, width, [char]) or string.pad(width, [char])
pad = function(sText, iWidth, sChar)
{
    sChar = ((sChar !== undefined) ? sChar : ('0'));
    sText = sText.toString();
    return ((sText.length >= iWidth) ? (sText) : (new Array(iWidth - sText.length + 1).join(sChar) + sText));
};

if (!String.prototype.pad)
    String.prototype.pad = function(iWidth, sChar)
    {
        sChar = ((sChar !== undefined) ? sChar : ('0'));
        sText = sText.toString();
        return ((sText.length >= iWidth) ? (sText) : (new Array(iWidth - sText.length + 1).join(sChar) + sText));
    };

String.prototype.toHHMMSS = function () {
    let sec_num = parseInt(this, 10);
    let hours   = Math.floor(sec_num / 3600);
    let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
    let seconds = sec_num - (hours * 3600) - (minutes * 60);

    if (hours   < 10) {hours   = "0"+hours;}
    if (minutes < 10) {minutes = "0"+minutes;}
    if (seconds < 10) {seconds = "0"+seconds;}
    return hours+':'+minutes+':'+seconds;
};

String.prototype.toFriendlyTime = function () {
    let sec_num = parseInt(this, 10);
    let hours   = Math.floor(sec_num / 3600);
    let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
    let seconds = sec_num - (hours * 3600) - (minutes * 60);

    let out = '';

    if (hours > 0) out = `${out}${hours}h`;
    if (minutes > 0) out = `${out}${minutes}m`;
    if (seconds > 0) out = `${out}${seconds}s`;
    return out;
};

Number.prototype.toPercent = function () {
    try
    {
        let pct_val = parseFloat(this);
        if (pct_val >= 15.0) return pct_val.toFixed(0);
        return pct_val.toFixed(1).replace(".0", "");
    }
    catch (err) { }
    return 'NaN';
};

is_visible = (function () {
    let x = window.pageXOffset ? window.pageXOffset + window.innerWidth - 1 : 0,
        y = window.pageYOffset ? window.pageYOffset + window.innerHeight - 1 : 0,
        relative = !!((!x && !y) || !document.elementFromPoint(x, y));
    function inside(child, parent) {
        while(child){
            if (child === parent) return true;
            child = child.parentNode;
        }
        return false;
    }
    return function (elem) {
        if (
            hidden ||
            elem.offsetWidth==0 ||
            elem.offsetHeight==0 ||
            elem.style.visibility=='hidden' ||
            elem.style.display=='none' ||
            elem.style.opacity===0
        ) return false;
        let rect = elem.getBoundingClientRect();
        if (relative) {
            if (!inside(document.elementFromPoint(rect.left + elem.offsetWidth/2, rect.top + elem.offsetHeight/2),elem)) return false;
        } else if (
            !inside(document.elementFromPoint(rect.left + elem.offsetWidth/2 + window.pageXOffset, rect.top + elem.offsetHeight/2 + window.pageYOffset), elem) ||
            (
                rect.top + elem.offsetHeight/2 < 0 ||
                rect.left + elem.offsetWidth/2 < 0 ||
                rect.bottom - elem.offsetHeight/2 > (window.innerHeight || document.documentElement.clientHeight) ||
                rect.right - elem.offsetWidth/2 > (window.innerWidth || document.documentElement.clientWidth)
            )
        ) return false;
        if (window.getComputedStyle || elem.currentStyle) {
            let el = elem,
                comp = null;
            while (el) {
                if (el === document) {break;} else if(!el.parentNode) return false;
                comp = window.getComputedStyle ? window.getComputedStyle(el, null) : el.currentStyle;
                if (comp && (comp.visibility=='hidden' || comp.display == 'none' || (typeof comp.opacity !=='undefined' && comp.opacity != 1))) return false;
                el = el.parentNode;
            }
        }
        return true;
    };
})();

function sumObjectsByKey(...objs) {
	return objs.reduce((a, b) => {
		for (let k in b) {
			if (b.hasOwnProperty(k))
				a[k] = (a[k] || 0) + b[k];
		}
		return a;
	}, {});
}
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
////////////// ** common_ET.js -- DO NOT MODIFY ** /////////////
if (window.ET === undefined) window.ET = { };
if ((window.ET.MCMF === undefined) || (CDbl(window.ET.MCMF.version) < 1.08)) // MeanCloud mod framework
{
    window.ET.MCMF =
    {
        version: 1.08,

        TryingToLoad: false,
        WantDebug: false,
        WantFasterAbilityCDs: false,

        InBattle: false,
        FinishedLoading: false,
        Initialized: false,
        AbilitiesReady: false,
        InitialAbilityCheck: true,
        TimeLeftOnCD: 9999,
        TimeLastFight: 0,

        ToastMessageSuccess: function(msg)
        {
            toastr.success(msg);
        },

        ToastMessageWarning: function(msg)
        {
            toastr.warning(msg);
        },

        EventSubscribe: function(sEventName, fnCallback, sNote)
        {
            if (window.ET.MCMF.EventSubscribe_events === undefined)
                window.ET.MCMF.EventSubscribe_events = [];

            let newEvtData = {};
                newEvtData.name = ((!sEventName.startsWith("ET:")) ? (`ET:${sEventName}`) : (sEventName));
                newEvtData.callback = fnCallback;
                newEvtData.note = sNote;

            window.ET.MCMF.EventSubscribe_events.push(newEvtData);

            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Added event subscription '${sEventName}'!` + ((sNote === undefined) ? "" : ` (${sNote})`));
        },

        EventTrigger: function(sEventName)
        {
            if (window.ET.MCMF.EventSubscribe_events === undefined) return;

            window.ET.MCMF.EventSubscribe_events.forEach(function(oThisEvent)
            {
                if (sEventName === oThisEvent.name)
                {
                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`FIRING '${oThisEvent.name}'!` + ((oThisEvent.note === undefined) ? "" : ` (${oThisEvent.note})`));
                    try { oThisEvent.callback(); } catch (err) { if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("Exception:"); console.log(err); }
                }
            });
        },

        Log: function(msg)
        {
            try
            {
                let now = new Date();
                let timestamp_date = `${(now.getMonth()+1)}/${now.getDate()}`;
                let timestamp_time = `${((now.getHours()===0)?12:((now.getHours()>12)?(now.getHours()-12):(now.getHours())))}:${now.getMinutes().toString().padStart(2,"0")}:${now.getSeconds().toString().padStart(2,"0")}${((now.getHours()< 2)?"a":"p")}`;
                console.log(`%c${timestamp_date} ${timestamp_time}%c  ${msg}`, "color: #555;", "font-weight: bold;");
            }
            catch (err) { }
        },

        Time: function() // returns time in milliseconds (not seconds!)
        {
            return CInt((new Date()).getTime());
        },

        SubscribeToGameChannel: function(channel_name)
        {
            let oChannel;

            try
            {
                channel_name = channel_name.toString().trim();

                let bAlreadySubscribed = false;

                jQuery.makeArray(Object.keys(Meteor.connection._subscriptions).map(key => Meteor.connection._subscriptions[key])).forEach(function(oThisConnection)
                {
                    try
                    {
                        if (oThisConnection.name === channel_name)
                            bAlreadySubscribed = true;
                    }
                    catch (err) { }
                });

                if (!bAlreadySubscribed)
                {
                    Meteor.subscribe(channel_name);
                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Meteor::Subscribed to channel '${channel_name}'`);
                }
            }
            catch (err)
            {
                if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Meteor::Exception in SubscribeToGameChannel("${channel_name}"):`);
                if (window.ET.MCMF.WantDebug) console.log(err);
            }

            return oChannel;
        },

		IsClass: function() {
			const classData = Meteor.connection._stores.users._getCollection().find().fetch()[0].classData
			return typeof classData !== 'undefined' && typeof classData.currentClass !== 'undefined'
		},

		WhichClass: function() {
			return Meteor.connection._stores.users._getCollection().find().fetch()[0]?.classData?.currentClass
		},

        CraftingBuff: function()
        {
            let oDate, iTimeLeft;

            try
            {
                oDate = new Date(Meteor.connection._stores.state._getCollection().find({ name: "buffCrafting" }).fetch()[0].value.activeTo);
                iTimeLeft = ((oDate) > (new Date())) ? CInt(Math.floor(Math.abs(oDate - (new Date())) / 1000.0)) : 0;

                return { active: (iTimeLeft > 0), remaining: iTimeLeft, expires: oDate };
            }
            catch (err) { }

            return { active: false, remaining: 0, expires: oDate };
        },

        CombatBuff: function()
        {
            let oDate, iTimeLeft;

            try
            {
                oDate = new Date(Meteor.connection._stores.state._getCollection().find({ name: "buffCombat" }).fetch()[0].value.activeTo);
                iTimeLeft = ((oDate) > (new Date())) ? CInt(Math.floor(Math.abs(oDate - (new Date())) / 1000.0)) : 0;

                return { active: (iTimeLeft > 0), remaining: iTimeLeft, expires: oDate };
            }
            catch (err) { }

            return { active: false, remaining: 0, expires: oDate };
        },

        GatheringBuff: function()
        {
            let oDate, iTimeLeft;

            try
            {
                oDate = new Date(Meteor.connection._stores.state._getCollection().find({ name: "buffGathering" }).fetch()[0].value.activeTo);
                iTimeLeft = ((oDate) > (new Date())) ? CInt(Math.floor(Math.abs(oDate - (new Date())) / 1000.0)) : 0;

                return { active: (iTimeLeft > 0), remaining: iTimeLeft, expires: oDate };
            }
            catch (err) { }

            return { active: false, remaining: 0, expires: oDate };
        },

        PersonalCraftingBuff: function()
        {
            let oDate, iTimeLeft;

            try
            {
                oDate = new Date(Meteor.connection._stores.users._getCollection().find().fetch()[0].craftingUpgradeTo);
                iTimeLeft = ((oDate) > (new Date())) ? CInt(Math.floor(Math.abs(oDate - (new Date())) / 1000.0)) : 0;

                return { active: (iTimeLeft > 0), remaining: iTimeLeft, expires: oDate };
            }
            catch (err) { }

            return { active: false, remaining: 0, expires: oDate };
        },

        PersonalWoodcuttingBuff: function()
        {
            let oDate, iTimeLeft;

            try
            {
                oDate = new Date(Meteor.connection._stores.users._getCollection().find().fetch()[0].woodcuttingUpgradeTo);
                iTimeLeft = ((oDate) > (new Date())) ? CInt(Math.floor(Math.abs(oDate - (new Date())) / 1000.0)) : 0;

                return { active: (iTimeLeft > 0), remaining: iTimeLeft, expires: oDate };
            }
            catch (err) { }

            return { active: false, remaining: 0, expires: oDate };
        },

        // pretty much always true now since the routes changed (/combat and /newCombat are both the same page)
        IsNewCombatTab: function() {
          try {
            const routeName = Router._currentRoute.getName();
            const windowUrl = window.location.href;

            if ((routeName === 'battle') || (routeName === 'combat') || (routeName === 'newCombat') || (routeName === 'fight')) {
              return true;
            }

            if ((windowUrl.indexOf('/battle') !== -1) || (windowUrl.indexOf('/combat') !== -1) || (windowUrl.indexOf('/newCombat') !== -1) || (windowUrl.indexOf('/fight') !== -1)) {
              return true;
            }
          } catch (err) {
          }

          return false;
        },

        GetActiveTab: function()
        {
          let active_tab = "";
          let current_route = Router._currentRoute.getName();

          if ((current_route === "overview") || (current_route === "gameHome")) active_tab = "home";
          if ((current_route === "mine") || (current_route === "mining") || (current_route === "minePit")) active_tab = "mining";
          if ((current_route === "items") || (current_route === "inventory") || (current_route === "craft") || (current_route === "crafting")) active_tab = "crafting";
          if ((current_route === "battle") || (current_route === "combat") || (current_route === "newCombat") || (current_route === "fight")) active_tab = "combat";
          if ((current_route === "woodcut") || (current_route === "woodcutting") || (current_route === "lumber") || (current_route === "lumbering")) active_tab = "woodcutting";
          if ((current_route === "farm") || (current_route === "farming")) active_tab = "farming";
          if ((current_route === "inscribe") || (current_route === "inscribing") || (current_route === "inscription") || (current_route === "enchantments") || (current_route === "alchemy")) active_tab = "inscription";
          if ((current_route === "magic") || (current_route === "astro") || (current_route === "astrology") || (current_route === "astronomy") || (current_route === "spells") || (current_route === "spellBook")) active_tab = "magic";
          if ((current_route === "faq") || (current_route === "help")) active_tab = "faq";
          if (current_route === "chat") active_tab = "chat";
          if ((current_route === "skills") || (current_route === "leaderboard") || (current_route === "ranking") || (current_route === "ranks")) active_tab = "skills";
          if (current_route === "achievements") active_tab = "achievements";
          if ((current_route === "updates") || (current_route === "patchNotes") || (current_route === "changelog") || (current_route === "changes") || (current_route === "news")) active_tab = "updates";

          if (active_tab === "") {
            if (window.location.href.indexOf("/gameHome") !== -1) active_tab = "home";
            if (window.location.href.indexOf("/overview") !== -1) active_tab = "home";
            if (window.location.href.indexOf("/mine") !== -1) active_tab = "mining";
            if (window.location.href.indexOf("/mining") !== -1) active_tab = "mining";
            if (window.location.href.indexOf("/minePit") !== -1) active_tab = "mining";
            if (window.location.href.indexOf("/items") !== -1) active_tab = "crafting";
            if (window.location.href.indexOf("/inventory") !== -1) active_tab = "crafting";
            if (window.location.href.indexOf("/craft") !== -1) active_tab = "crafting";
            if (window.location.href.indexOf("/crafting") !== -1) active_tab = "crafting";
            if (window.location.href.indexOf("/battle") !== -1) active_tab = "combat";
            if (window.location.href.indexOf("/combat") !== -1) active_tab = "combat";
            if (window.location.href.indexOf("/newCombat") !== -1) active_tab = "combat";
            if (window.location.href.indexOf("/fight") !== -1) active_tab = "combat";
            if (window.location.href.indexOf("/woodcut") !== -1) active_tab = "woodcutting";
            if (window.location.href.indexOf("/woodcutting") !== -1) active_tab = "woodcutting";
            if (window.location.href.indexOf("/lumber") !== -1) active_tab = "woodcutting";
            if (window.location.href.indexOf("/lumbering") !== -1) active_tab = "woodcutting";
            if (window.location.href.indexOf("/farm") !== -1) active_tab = "farming";
            if (window.location.href.indexOf("/farming") !== -1) active_tab = "farming";
            if (window.location.href.indexOf("/inscribe") !== -1) active_tab = "inscription";
            if (window.location.href.indexOf("/inscribing") !== -1) active_tab = "inscription";
            if (window.location.href.indexOf("/inscription") !== -1) active_tab = "inscription";
            if (window.location.href.indexOf("/enchantments") !== -1) active_tab = "inscription";
            if (window.location.href.indexOf("/alchemy") !== -1) active_tab = "inscription";
            if (window.location.href.indexOf("/magic") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/astro") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/astrology") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/astronomy") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/spells") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/spellBook") !== -1) active_tab = "magic";
            if (window.location.href.indexOf("/faq") !== -1) active_tab = "faq";
            if (window.location.href.indexOf("/help") !== -1) active_tab = "faq";
            if (window.location.href.indexOf("/chat") !== -1) active_tab = "chat";
            if (window.location.href.indexOf("/skills") !== -1) active_tab = "skills";
            if (window.location.href.indexOf("/leaderboard") !== -1) active_tab = "skills";
            if (window.location.href.indexOf("/ranking") !== -1) active_tab = "skills";
            if (window.location.href.indexOf("/ranks") !== -1) active_tab = "skills";
            if (window.location.href.indexOf("/achievements") !== -1) active_tab = "achievements";
            if (window.location.href.indexOf("/updates") !== -1) active_tab = "updates";
            if (window.location.href.indexOf("/patchNotes") !== -1) active_tab = "updates";
            if (window.location.href.indexOf("/changelog") !== -1) active_tab = "updates";
            if (window.location.href.indexOf("/changes") !== -1) active_tab = "updates";
            if (window.location.href.indexOf("/news") !== -1) active_tab = "updates";
          }

            return active_tab;
        },

        GetActiveTabSection: function()
        {
            let active_tab_section = "";

            try
            {
                let active_tab = window.ET.MCMF.GetActiveTab();

                if (active_tab === "mining") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.miningTab;
                if (active_tab === "crafting") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.craftingFilter;
                if (active_tab === "combat")
                {
                    if (window.ET.MCMF.IsNewCombatTab())
                        active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.newCombatType;
                    else
                        active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.combatTab;
                }
                if (active_tab === "farming") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.farmingTab;
                if (active_tab === "inscription") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.inscriptionFilter;
                if (active_tab === "achievements") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.achievementTab;
                if (active_tab === "magic") active_tab_section = Meteor.connection._stores.users._getCollection().find().fetch()[0].uiState.magicTab;

                active_tab_section = active_tab_section.trim().toLowerCase();

                if (active_tab_section === "minepit") active_tab_section = "mine pit";
                if (active_tab_section === "personalquest") active_tab_section = "personal quest";
                if (active_tab_section === "tower") active_tab_section = "the tower";
                if (active_tab_section === "battlelog") active_tab_section = "battle log";
                if (active_tab_section === "pigment") active_tab_section = "pigments";
                if (active_tab_section === "book") active_tab_section = "books";
                if (active_tab_section === "magic_book") active_tab_section = "magic books";
                if (active_tab_section === "spellbook") active_tab_section = "spell book";

                if (active_tab_section.length === 0)
                    throw "Invalid active tab section";
            }
            catch (err)
            {
                try
                {
                    active_tab_section = jQuery(jQuery("a.active").get(1)).text().trim().toLowerCase();

                    if (active_tab_section.length === 0)
                        throw "Invalid active tab section";
                }
                catch (err) { }
            }

            return active_tab_section;
        },

        BattleSocket_UseAbility: function(abil, targ)
        {
            try
            {
                let sMsg = '';

                if (targ === undefined)
                {
                    sMsg = '["action",{"abilityId":"' + abil + '","targets":[],"caster":"' + window.ET.MCMF.UserID + '"}]';
                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Battle socket emitting: '${sMsg}'`);

                    battleSocket.emit
                    (
                        "action",
                        {
                            abilityId: abil,
                            targets: [],
                            caster: window.ET.MCMF.UserID
                        }
                    );
                }
                else
                {
                    sMsg = '["action",{"abilityId":"' + abil + '","targets":["' + targ + '"],"caster":"' + window.ET.MCMF.UserID + '"}]';
                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Battle socket emitting: '${sMsg}'`);

                    battleSocket.emit
                    (
                        "action",
                        {
                            abilityId: abil,
                            targets: [targ],
                            caster: window.ET.MCMF.UserID
                        }
                    );
                }
            }
            catch (err) { }
        },

        CallGameCmd: function()
        {
            try
            {
                if (arguments.length > 0)
                {
                    let cmd = arguments[0];
                    let fnc = function() { };

                    if (arguments.length === 1)
                    {
                        if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with no data`);
                        Package.meteor.Meteor.call(cmd, fnc);
                    }
                    else
                    {
                        let data1, data2, data3, data4;

                        if (typeof arguments[arguments.length - 1] === "function")
                        {
                            fnc = arguments[arguments.length - 1];
                            if (arguments.length >= 3) data1 = arguments[1];
                            if (arguments.length >= 4) data2 = arguments[2];
                            if (arguments.length >= 5) data3 = arguments[3];
                            if (arguments.length >= 6) data4 = arguments[4];
                        }
                        else
                        {
                            if (arguments.length >= 2) data1 = arguments[1];
                            if (arguments.length >= 3) data2 = arguments[2];
                            if (arguments.length >= 4) data3 = arguments[3];
                            if (arguments.length >= 5) data4 = arguments[4];
                        }

                        if (data1 === undefined)
                        {
                        if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with no data`);
                            Package.meteor.Meteor.call(cmd, fnc);
                        }
                        else if (data2 === undefined)
                        {
                            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with { ${JSON.stringify(data1)} }`);
                            Package.meteor.Meteor.call(cmd, data1, fnc);
                        }
                        else if (data3 === undefined)
                        {
                            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with { ${JSON.stringify(data1)}, ${JSON.stringify(data2)} }`);
                            Package.meteor.Meteor.call(cmd, data1, data2, fnc);
                        }
                        else if (data4 === undefined)
                        {
                            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with { ${JSON.stringify(data1)}, ${JSON.stringify(data2)}, ${JSON.stringify(data3)} }`);
                            Package.meteor.Meteor.call(cmd, data1, data2, data3, fnc);
                        }
                        else
                        {
                            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Calling: '${cmd}' with { ${JSON.stringify(data1)}, ${JSON.stringify(data2)}, ${JSON.stringify(data3)}, ${JSON.stringify(data4)} }`);
                            Package.meteor.Meteor.call(cmd, data1, data2, data3, data4, fnc);
                        }
                    }
                }
                else if (window.ET.MCMF.WantDebug)
                    window.ET.MCMF.Log("Meteor::Warning, CallGameCmd() with no arguments!");
            }
            catch (err)
            {
                if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("Meteor::Exception in CallGameCmd():");
                if (window.ET.MCMF.WantDebug) console.log(err);
            }
        },

        SendGameCmd: function(cmd)
        {
            try
            {
                Meteor.connection._send(cmd);
            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Meteor::Sending: ${JSON.stringify(cmd)}`);
            }
            catch (err)
            {
                if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`Meteor::Exception in SendGameCmd(${JSON.stringify(cmd)}):`);
                if (window.ET.MCMF.WantDebug) console.log(err);
            }
        },

        FasterAbilityUpdates: function()
        {
            try
            {
                window.ET.MCMF.SubscribeToGameChannel("abilities");

                if ((window.ET.MCMF.WantFasterAbilityCDs) && (window.ET.MCMF.FinishedLoading) && (!window.ET.MCMF.InBattle) && (!window.ET.MCMF.AbilitiesReady))
                    window.ET.MCMF.CallGameCmd("abilities.gameUpdate");
            }
            catch (err) { }

            setTimeout(window.ET.MCMF.FasterAbilityUpdates, 2000);
        },

        PlayerInCombat: function()
        {
            return ((window.ET.MCMF.InBattle) || ((window.ET.MCMF.Time() - window.ET.MCMF.TimeLastFight) < 3));
        },

        AbilityCDTrigger: function()
        {
            try
            {
                if ((window.ET.MCMF.FinishedLoading) && (!window.ET.MCMF.PlayerInCombat()))
                {
                    iTotalCD = 0;
                    iTotalCDTest = 0;
                    iHighestCD = 0;

                    window.ET.MCMF.GetAbilities().forEach(function(oThisAbility)
                    {
                        if (oThisAbility.equipped)
                        {
                            if (parseInt(oThisAbility.currentCooldown) > 0)
                            {
                                iTotalCD += parseInt(oThisAbility.currentCooldown);
                                if (iHighestCD < parseInt(oThisAbility.currentCooldown))
                                    iHighestCD = parseInt(oThisAbility.currentCooldown);
                            }
                        }

                        iTotalCDTest += parseInt(oThisAbility.cooldown);
                    });

                    if ((iTotalCDTest > 0) && (iTotalCD === 0))
                    {
                        if (!window.ET.MCMF.AbilitiesReady)
                        {
                            if (!window.ET.MCMF.InitialAbilityCheck)
                            {
                                if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:abilitiesReady -->");
                                window.ET.MCMF.EventTrigger("ET:abilitiesReady");
                            }
                        }

                        window.ET.MCMF.AbilitiesReady = true;
                        window.ET.MCMF.TimeLeftOnCD = 0;
                    }
                    else
                    {
                        window.ET.MCMF.AbilitiesReady = false;
                        window.ET.MCMF.TimeLeftOnCD = iHighestCD;
                    }

                    window.ET.MCMF.InitialAbilityCheck = false;
                }
                else
                {
                    window.ET.MCMF.AbilitiesReady = false;
                    window.ET.MCMF.TimeLeftOnCD = 9999;
                }
            }
            catch (err) { }

            setTimeout(window.ET.MCMF.AbilityCDTrigger, 500);
        },

        BattleFloorRoom: "0.0",
        BattleFirstFrame: undefined,
        BattleUnitList: [],
        BattleUITemplate: undefined,

        LiveBattleData: function()
        {
            try
            {
                if (window.ET.MCMF.BattleUITemplate !== undefined)
                    return window.ET.MCMF.BattleUITemplate.state.get("currentBattle");
            }
            catch (err) { }

            return undefined;
        },

        LastUnitIDList: '',
        InternalBattleTickMonitor: undefined,

		CombatStarted: function(forced)
		{
			if (!window.ET.MCMF.FinishedLoading)
			{
				setTimeout(window.ET.MCMF.CombatStarted, 100);
				return;
			}

			if (forced || (window.ET.MCMF.InternalBattleTickMonitor === undefined) || (window.ET.MCMF.BattleFirstFrame === undefined))
			{
                window.ET.MCMF.InternalBattleTickMonitor = true;

				battleSocket.on('tick', function(oAllData)
				{
					let battleData = window.ET.MCMF.LiveBattleData();

					if (battleData !== undefined)
					{
						/* if (battleData.floor !== undefined)
						{
							let currentFloorRoom = CInt(battleData.floor).toFixed(0) + "." + CInt(battleData.room).toFixed(0);

							if (window.ET.MCMF.BattleFloorRoom !== currentFloorRoom)
							{
								window.ET.MCMF.BattleFloorRoom = currentFloorRoom;
								window.ET.MCMF.BattleFirstFrame = undefined;
							}
						} */

                        let CurrentUnitIDList = '';
                        jQ.makeArray(battleData.enemies).forEach(function(oEnemyUnit)
                        {
                            CurrentUnitIDList += `${oEnemyUnit.id}|`;
                        });

                        let bNewBattleFrameReset = false;
                        jQ.makeArray(battleData.enemies).forEach(function(oEnemyUnit)
                        {
                            if (window.ET.MCMF.LastUnitIDList.indexOf(`${oEnemyUnit.id}|`) === -1)
                                bNewBattleFrameReset = true;
                        });

                        if (bNewBattleFrameReset)
                            window.ET.MCMF.BattleFirstFrame = undefined;

                        window.ET.MCMF.LastUnitIDList = CurrentUnitIDList;

						if (window.ET.MCMF.BattleFirstFrame === undefined)
						{
							window.ET.MCMF.BattleFirstFrame = battleData;

							if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:firstBattleFrame -->");
							window.ET.MCMF.EventTrigger("ET:firstBattleFrame");

                            //window.ET.MCMF.Log("new BattleFirstFrame data:");
                            //console.log(window.ET.MCMF.BattleFirstFrame);
						}

						if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:combatTick -->");
						window.ET.MCMF.EventTrigger("ET:combatTick");
					}
				});
			}
		},

        InitGameTriggers: function()
        {
            if ((Package.meteor.Meteor === undefined) || (Package.meteor.Meteor.connection === undefined) || (Package.meteor.Meteor.connection._stream === undefined) || (Template.currentBattleUi === undefined))
            {
                setTimeout(window.ET.MCMF.InitGameTriggers, 100);
                return;
            }

			window.ET.MCMF.EventSubscribe("ET:navigation", function()
			{
                window.ET.MCMF.InternalBattleTickMonitor = undefined;

                // re-trigger combat-start events when the battle socket is reconnected
				if (window.ET.MCMF.InBattle && window.ET.MCMF.IsNewCombatTab())
					window.ET.MCMF.CombatStarted(true);
			});

			Router.onRun(function()
			{
				if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:navigation -->");
				window.ET.MCMF.EventTrigger("ET:navigation");

                try
                {
                    let sCurrentRoute = Router._currentRoute.getName();

                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log(`<-- triggering ET:navigation:${sCurrentRoute} -->`);
                    window.ET.MCMF.EventTrigger(`ET:navigation:${sCurrentRoute}`);
                }
                catch (err) { }

				this.next();
			});

            // note: not a trustworthy method to get new battle unit data, since the templates will reuse and not trigger render from room to room
            Blaze._getTemplate("battleUnit").onRendered(function()
            {
                if ((this.data !== undefined) && (this.data.unit !== undefined))
                    window.ET.MCMF.BattleUnitList.push(this);
            });

            Template.currentBattleUi.onCreated(function()
            {
                window.ET.MCMF.BattleUITemplate = this;
            });

            Template.currentBattleUi.onDestroyed(function()
            {
                window.ET.MCMF.BattleUITemplate = undefined;
                window.ET.MCMF.BattleUnitList = [];
            });

            Package.meteor.Meteor.connection._stream.on('message', function(sMeteorRawData)
            {
                //if (window.ET.MCMF.CombatID === undefined)
                //    window.ET.MCMF.GetPlayerCombatData();

                try
                {
                    oMeteorData = JSON.parse(sMeteorRawData);

                    /////////////////////////////////////////////////////////////////////////////////////////////////////////
                    //
                    //  BACKUP TO RETRIEVE USER AND COMBAT IDS
                    //
                    /*
                    if (oMeteorData.collection === "users")
                        if ((window.ET.MCMF.UserID === undefined) || (window.ET.MCMF.UserID.length !== 17))
                            window.ET.MCMF.UserID = oMeteorData.id;

                    if (oMeteorData.collection === "combat")
                        if ((window.ET.MCMF.CombatID === undefined) || (window.ET.MCMF.CombatID.length !== 17))
                            if (oMeteorData.fields.owner === window.ET.MCMF.UserID)
                                window.ET.MCMF.CombatID = oMeteorData.id;
                    */
                    //
                    /////////////////////////////////////////////////////////////////////////////////////////////////////////

                    if (oMeteorData.collection === "battlesList")
                    {
                        window.ET.MCMF.AbilitiesReady = false;

                        if ((oMeteorData.msg === "added") || (oMeteorData.msg === "removed"))
                        {
                            window.ET.MCMF.InternalBattleTickMonitor = undefined;
                            window.ET.MCMF.BattleFirstFrame = undefined;
                            window.ET.MCMF.BattleUnitList = [];
                            window.ET.MCMF.InBattle = (oMeteorData.msg === "added");
                            if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:combat" + (((oMeteorData.msg === "added")) ? ("Start") : ("End")) + " -->");
                            window.ET.MCMF.EventTrigger("ET:combat" + (((oMeteorData.msg === "added")) ? ("Start") : ("End")));

                            if (window.ET.MCMF.InBattle)
                                window.ET.MCMF.CombatStarted();
                            else
                                window.ET.MCMF.BattleFloorRoom = "0.0";
                        }
                    }

                    if ((oMeteorData.collection === "battles") && (oMeteorData.msg === "added"))
                    {
                        if (oMeteorData.fields.finished)
                        {
                            window.ET.MCMF.WonLast = oMeteorData.fields.win;
                            window.ET.MCMF.TimeLastFight = window.ET.MCMF.Time();

							if (window.ET.MCMF.FinishedLoading)
							{
								if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:combat" + ((oMeteorData.fields.win) ? ("Won") : ("Lost")) + " -->");
								window.ET.MCMF.EventTrigger("ET:combat" + ((oMeteorData.fields.win) ? ("Won") : ("Lost")));
							}
                        }
						else
							window.ET.MCMF.CombatStarted();
                    }
                }
                catch (err) { }
            });
        },

        PlayerHP: function()
        {
            return window.ET.MCMF.GetPlayerCombatData().stats.health;
        },

        PlayerHPMax: function()
        {
            return window.ET.MCMF.GetPlayerCombatData().stats.healthMax;
        },

        PlayerEnergy: function()
        {
            return window.ET.MCMF.GetPlayerCombatData().stats.energy;
        },

        AbilityCDCalc: function()
        {
            iTotalCD = 0;
            iTotalCDTest = 0;
            iHighestCD = 0;

            window.ET.MCMF.GetAbilities().forEach(function(oThisAbility)
            {
                if (oThisAbility.equipped)
                {
                    if (parseInt(oThisAbility.currentCooldown) > 0)
                    {
                        iTotalCD += parseInt(oThisAbility.currentCooldown);
                        if (iHighestCD < parseInt(oThisAbility.currentCooldown))
                            iHighestCD = parseInt(oThisAbility.currentCooldown);
                    }
                }

                iTotalCDTest += parseInt(oThisAbility.cooldown);
            });

            if ((iTotalCDTest > 0) && (iTotalCD === 0))
            {
                if (!window.ET.MCMF.AbilitiesReady)
                {
                    if (!window.ET.MCMF.InitialAbilityCheck)
                    {
                        if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:abilitiesReady -->");
                        window.ET.MCMF.EventTrigger("ET:abilitiesReady");
                    }
                }

                window.ET.MCMF.AbilitiesReady = true;
                window.ET.MCMF.TimeLeftOnCD = 0;
            }
            else
            {
                window.ET.MCMF.AbilitiesReady = false;
                window.ET.MCMF.TimeLeftOnCD = iHighestCD;
            }

            window.ET.MCMF.InitialAbilityCheck = false;
        },

        GetUnitCombatData: function(sUnitID)
        {
            let oCombatPlayerData;

            try
            {
                // get recent combat data from stored 'state' data in 'BattleUITemplate' template (comes from 'battleSocket')
                if (window.ET.MCMF.LiveBattleData() !== undefined)
                {

                    jQ.makeArray(window.ET.MCMF.LiveBattleData().units).forEach(function(oCurrentUnit)
                    {
                        if (oCurrentUnit.id === sUnitID)
                            oCombatPlayerData = oCurrentUnit;
                    });
                }
            }
            catch (err) { }

            return oCombatPlayerData;
        },

        GetEnemyCombatData: function(sUnitID)
        {
            let oCombatEnemyData;

            try
            {
                // get recent combat data from stored 'state' data in 'BattleUITemplate' template (comes from 'battleSocket')
                if (window.ET.MCMF.LiveBattleData() !== undefined)
                {
                    jQ.makeArray(window.ET.MCMF.LiveBattleData().enemies).forEach(function(oCurrentUnit)
                    {
                        if (oCurrentUnit.id === sUnitID)
                            oCombatEnemyData = oCurrentUnit;
                    });
                }
            }
            catch (err) { }

            return oCombatEnemyData;
        },

        GetPlayerCombatData: function()
        {
            try
            {
                Meteor.connection._stores.combat._getCollection().find().fetch().forEach(function(oThisCombatUnit)
                {
                    if (oThisCombatUnit.owner === window.ET.MCMF.UserID)
                        window.ET.MCMF.PlayerUnitData = oThisCombatUnit;
                });

                // new: get updated combat data from stored 'state' data in 'BattleUITemplate' template (comes from 'battleSocket')
                if (window.ET.MCMF.LiveBattleData() !== undefined)
                {
                    jQ.makeArray(window.ET.MCMF.LiveBattleData().units).forEach(function(oCurrentUnit)
                    {
                        if (oCurrentUnit.id === window.ET.MCMF.UserID)
                            window.ET.MCMF.PlayerUnitData = oCurrentUnit;
                    });
                }
            }
            catch (err) { }

            return window.ET.MCMF.PlayerUnitData;
        },

        GetAbilities: function()
        {
            return Meteor.connection._stores.abilities._getCollection().find().fetch()[0].learntAbilities;
        },

        GetAdventures: function()
        {
            let oAdventureDetails = { AllAdventures: [], ShortAdventures: [], LongAdventures: [], EpicAdventures: [], PhysicalAdventures: [], MagicalAdventures: [], ActiveAdventures: [], CurrentAdventure: undefined };

            // oThisAdventure
            //    .duration     {duration in seconds} (integer)
            //    .endDate      {end date/time} (Date()) (property only exists if the adventure is ongoing)
            //    .floor        {corresponding Tower Floor} (integer)
            //    .icon         "{imageofbattle.ext}" (string)
            //    .id           "{guid}" (13-digit alphanumeric string)
            //    .length       "short" / "long" / "epic" (string)
            //    .level        {general level} (integer)
            //    .name         "{Name of Battle}" (string)
            //    .room         {corresponding Tower Room in Tower Floor} (integer)
            //    .type         "physical" / "magic" (string)
            //    .startDate    {start date/time} (Date()) (property only exists if the adventure is ongoing)
            window.ET.MCMF.GetAdventures_raw().forEach(function(oThisAdventure)
            {
                try
                {
                    oAdventureDetails.AllAdventures.push(oThisAdventure);
                    if (oThisAdventure.length  === "short")    oAdventureDetails.ShortAdventures   .push(oThisAdventure);
                    if (oThisAdventure.length  === "long")     oAdventureDetails.LongAdventures    .push(oThisAdventure);
                    if (oThisAdventure.length  === "epic")     oAdventureDetails.EpicAdventures    .push(oThisAdventure);
                    if (oThisAdventure.type    === "physical") oAdventureDetails.PhysicalAdventures.push(oThisAdventure);
                    if (oThisAdventure.type    === "magic")    oAdventureDetails.MagicalAdventures .push(oThisAdventure);
                    if (oThisAdventure.endDate !== undefined)  oAdventureDetails.ActiveAdventures  .push(oThisAdventure);
                }
                catch (err) { }
            });

            oAdventureDetails.AllAdventures.sort(function(advA, advB)
            {
                if ((advA.startDate === undefined) && (advB.startDate !== undefined)) return 1;
                if ((advA.startDate !== undefined) && (advB.startDate === undefined)) return -1;
                if ((advA.startDate !== undefined) && (advB.startDate !== undefined))
                {
                    if (advA.startDate > advB.startDate) return 1;
                    if (advA.startDate < advB.startDate) return -1;
                }
                if (advA.duration > advB.duration) return 1;
                if (advA.duration < advB.duration) return -1;
                return 0;
            });

            oAdventureDetails.ActiveAdventures.sort(function(advA, advB)
            {
                if (advA.startDate > advB.startDate) return 1;
                if (advA.startDate < advB.startDate) return -1;
                return 0;
            });

            oAdventureDetails.PhysicalAdventures.sort(function(advA, advB)
            {
                if (advA.duration > advB.duration) return 1;
                if (advA.duration < advB.duration) return -1;
                return 0;
            });

            oAdventureDetails.MagicalAdventures.sort(function(advA, advB)
            {
                if (advA.duration > advB.duration) return 1;
                if (advA.duration < advB.duration) return -1;
                return 0;
            });

            if (oAdventureDetails.ActiveAdventures.length > 0)
                oAdventureDetails.CurrentAdventure = oAdventureDetails.ActiveAdventures[0];

            return oAdventureDetails;
        },

        GetAdventures_raw: function()
        {
            try
            {
                return Meteor.connection._stores.adventures._getCollection().find().fetch()[0].adventures;
            }
            catch (err) { }

            return [];
        },

        GetChats: function()
        {
            return Meteor.connection._stores.simpleChats._getCollection().find().fetch();
        },

        GetItem: function(in__id)
        {
            let oItems = window.ET.MCMF.GetItems({_id: in__id});

            if (oItems.length > 0)
                return oItems[0];

            return {};
        },

        GetItems: function()
        {
            let oItems;

            if (arguments.length === 0)
                oItems = jQ.makeArray(Meteor.connection._stores.items._getCollection().find().fetch());
            else
                oItems = jQ.makeArray(Meteor.connection._stores.items._getCollection().find(arguments[0]).fetch());

            for (let i = 0; i < oItems.length; i++)
                oItems[i] = window.ET.MCMF.AddItemConsts(oItems[i]);

            return oItems;
        },

        AddItemConsts: function(oItem)
        {
            let oItemNew;
            let consts;

            try
            {
                consts = (IsValid(window) && IsValid(window.gameConstants)) ? window.gameConstants : (IsValid(unsafeWindow) && IsValid(unsafeWindow.gameConstants)) ? unsafeWindow.gameConstants : { };

                oItemNew = { ...(oItem) };
                oItemNew = { ...(oItemNew), ...(consts.ITEMS[oItemNew.itemId]) };

                if (IsValid(oItemNew.produces) && IsValid(consts.FARMING) && IsValid(consts.FARMING.plants))
                {
                    oItemNew.plantingDetails = consts.FARMING.plants[oItemNew.produces];

                    if (IsValid(oItemNew.plantingDetails))
                        oItemNew.required = oItemNew.plantingDetails.required;
                }
            }
            catch (err) { }

            try
            {
                oItemNew = { ...(oItemNew), ...(oItem) };
                oItemNew.stats = sumObjectsByKey(oItem.extraStats, consts.ITEMS[oItem.itemId].stats);
            }
            catch (err) { }

            try
            {
                if (typeof oItemNew['description'] === 'function')
                    oItemNew.description = oItemNew.description();
            }
            catch (err) { }

            return oItemNew;
        },

        GetSkills: function()
        {
            return Meteor.connection._stores.skills._getCollection().find().fetch();
        },

        Setup: function()
        {
            if ((!window.ET.MCMF.TryingToLoad) && (!window.ET.MCMF.FinishedLoading))
            {
                // use whatever version of jQuery available to us
                $("body").append("<div id=\"ET_meancloud_bootstrap\" style=\"visibility: hidden; display: none;\"></div>");
                window.ET.MCMF.TryingToLoad = true;
                window.ET.MCMF.Setup_Initializer();
            }
        },

        Setup_Initializer: function()
        {
            // wait for Meteor availability
            if ((Package === undefined) || (Package.meteor === undefined) || (Package.meteor.Meteor === undefined) || (Package.meteor.Meteor.connection === undefined) || (Package.meteor.Meteor.connection._stream === undefined))
            {
                setTimeout(window.ET.MCMF.Setup_Initializer, 10);
                return;
            }

            if (!window.ET.MCMF.Initialized)
            {
                window.ET.MCMF.Initialized = true;
                window.ET.MCMF.Setup_SendDelayedInitializer();
                window.ET.MCMF.InitGameTriggers();
                window.ET.MCMF.Setup_remaining();
            }
        },

        Setup_SendDelayedInitializer: function()
        {
            try
            {
                jQ("div#ET_meancloud_bootstrap").trigger("ET:initialized");
                window.ET.MCMF.EventTrigger("ET:initialized");
            }
            catch (err)
            {
                setTimeout(window.ET.MCMF.Setup_SendDelayedInitializer, 100);
            }
        },

        Setup_remaining: function()
        {
            try
            {
                if (Meteor === undefined) throw "[MCMF Setup] Not loaded yet: Meteor not initialized";
                if (Meteor.connection === undefined) throw "[MCMF Setup] Not loaded yet: Meteor not initialized";
                if (Meteor.connection._userId === undefined) throw "[MCMF Setup] Not loaded yet: Meteor not initialized";

                window.ET.MCMF.UserID = Meteor.connection._userId;
				window.ET.MCMF.UserName = [...Meteor.connection._stores.users._getCollection()._collection._docs._map.values()][0].username;
                window.ET.MCMF.GetPlayerCombatData();

                if (window.ET.MCMF.GetAbilities().length < 0) throw "[MCMF Setup] Not loaded yet: no abilities";
                if (window.ET.MCMF.GetItems().length < 0) throw "[MCMF Setup] Not loaded yet: no items";
                if (window.ET.MCMF.GetChats().length < 0) throw "[MCMF Setup] Not loaded yet: no chats";
                if (window.ET.MCMF.GetSkills().length < 0) throw "[MCMF Setup] Not loaded yet: no skills";

                // if the above is all good, then this should be no problem:

                window.ET.MCMF.AbilityCDTrigger();     // set up ability CD trigger
                window.ET.MCMF.AbilityCDCalc();
                window.ET.MCMF.FasterAbilityUpdates(); // set up faster ability updates (do not disable, this is controlled via configurable setting)

                // trigger finished-loading event
                if (!window.ET.MCMF.FinishedLoading)
                {
                    if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:loaded -->");
                    window.ET.MCMF.EventTrigger("ET:loaded");
                    window.ET.MCMF.FinishedLoading = true;
                }
            }
            catch (err) // any errors and we retry setup
            {
                if (err.toString().indexOf("[MCMF Setup]") !== -1)
                {
                    window.ET.MCMF.Log("ET MCMF setup exception:");
                    console.log(err);
                }

                setTimeout(window.ET.MCMF.Setup_remaining, 500);
            }
        },

        // Ready means the mod framework has been initialized, but Meteor is not yet available
        Ready: function(fnCallback, sNote)
        {
            if (!window.ET.MCMF.Initialized)
                window.ET.MCMF.EventSubscribe("initialized", fnCallback, sNote);
            else
                fnCallback();
        },

        // Loaded means the mod framework and Meteor are fully loaded and available
        Loaded: function(fnCallback, sNote)
        {
            if (!window.ET.MCMF.FinishedLoading)
                window.ET.MCMF.EventSubscribe("loaded", fnCallback, sNote);
            else
                fnCallback();
        },
    };

    window.ET.MCMF.Setup();
}
////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////
////////// ** CORE SCRIPT STARTUP -- DO NOT MODIFY ** //////////
function LoadJQ(callback) {
    if (window.jQ === undefined) { var script=document.createElement("script");script.setAttribute("src","//ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js");script.addEventListener('load',function() {
        var subscript=document.createElement("script");subscript.textContent="window.jQ=jQuery.noConflict(true);("+callback.toString()+")();";document.body.appendChild(subscript); },
    !1);document.body.appendChild(script); } else callback(); } LoadJQ(startup);
////////////////////////////////////////////////////////////////