Eternity Tower Stats UI

Adds stats to the UI for the Eternity Tower game

当前为 2018-09-29 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          Eternity Tower Stats UI
// @icon          https://www.eternitytower.net/favicon.png
// @namespace     http://mean.cloud/
// @version       1.25
// @description   Adds stats to the UI for the Eternity Tower game
// @match         http*://*.eternitytower.net/*
// @author        [email protected]
// @copyright     2017-2018, MeanCloud
// @run-at        document-end
// @grant         GM_getValue
// @grant         GM_setValue
// ==/UserScript==


////////////////////////////////////////////////////////////////
////////////// ** SCRIPT GLOBAL INITIALIZATION ** //////////////
function startup() { ET_StatsUIMod(); }
ET_Stat_UserID = "";
ET_Stat_CombatID = "";
ET_Stat_CritChance = 0;
ET_Stat_CritDamage = 0;
ET_Stat_HealingPower = 0;
ET_Stat_DamageTaken = 0;
ET_Stat_MP = 0;
ET_JustExpanded = false;
ET_LastModal = null;
ET_LastForm = null;
////////////////////////////////////////////////////////////////


ET_StatsUIMod = function()
{
    ET.MCMF.Ready(function()
    {
        //ET.MCMF.WantDebug = true;
        
        Meteor.connection._stream.on('message', function(sMeteorRawData)
        {
            try
            {
                var oMeteorData = JSON.parse(sMeteorRawData);

                if (oMeteorData.collection == "users")
                {
                    //console.log(oMeteorData);

                    ET_Stat_UserID = oMeteorData.id;
                }

                if (oMeteorData.collection == "combat")
                    if ((oMeteorData.fields.owner === ET_Stat_UserID) &&( ET_Stat_CombatID === ""))
                        ET_Stat_CombatID = oMeteorData.id;

                if (oMeteorData.collection == "combat")
                {
                    if ((oMeteorData.fields.owner === ET_Stat_UserID) || (oMeteorData.id === ET_Stat_CombatID))
                    {
                        //console.log(oMeteorData);

                        ET_Stat_CritChance = oMeteorData.fields.stats.criticalChance;
                        ET_Stat_CritDamage = oMeteorData.fields.stats.criticalDamage;
                        ET_Stat_HealingPower = oMeteorData.fields.stats.healingPower;
                        ET_Stat_DamageTaken = oMeteorData.fields.stats.damageTaken;
                        ET_Stat_MP = oMeteorData.fields.stats.magicPower;

                        /*
                        var oHPEl = jQ("div.bg-danger").parent().parent().find("div.d-flex > i.extra-small-icon").parent();

                        if (jQ("#PET_DemonMaxHP").length === 0)
                            oHPEl.append("<span id=\"PET_DemonMaxHP\" style=\"white-space: nowrap; font-size: 9pt;\"></span>");

                        jQ("#PET_DemonMaxHP").html("&nbsp;&nbsp;(demon &lt; " + ((oMeteorData.fields.stats.healthMax * 0.2) + 1).toFixed(0) + ")");
                        */
                    }
                }
            }
            catch (err) { }
        });
    });

    ET.MCMF.Loaded(function()
    {
        ET.MCMF.StatsUIMod = { };
        
        ET.MCMF.StatsUIMod.WantAdventureLoot = (GM_getValue("PETStatusUI_WantAdventureLoot") !== false); // default true
        //GM_setValue("PETSearch_Color",     jQ("#PETSItemColor")  .is(":checked")); 
        
    	// Set background tasks
        ET_StatsUIMod_DPSShow();
    });
};

ET_StatsUIMod_GetAdventure = function(sMatchID)
{
    // oAdventure
    //    .duration
    //    .endDate
    //    .floor
    //    .icon
    //    .id
    //    .length
    //    .level
    //    .name
    //    .room
    //    .type
    //    .startDate
    var oAdventureData;
    
    jQuery.makeArray(Object.keys(Package.meteor.global.Accounts.connection._stores.adventures._getCollection()._collection._docs._map).map(key => Package.meteor.global.Accounts.connection._stores.adventures._getCollection()._collection._docs._map[key])[0].adventures).forEach(function(oAdventure)
    {
        try
        {
            if (oAdventure.id === sMatchID)
                oAdventureData = oAdventure;
        }
        catch (err) { }
    });
    
    return oAdventureData;
};

var sETStatsDB = "";
var ETStatsDB_PQ = [];
var ETStatsDB_Tower = [];

ET_StatsUIMod_ConvertItemIDToImage = function(sItemID)
{
    var s = sItemID;

    var sExt = ".svg";
    
    if (endsWith(s, "_essence")) sExt = ".png";
    if (endsWith(s, "_shield")) sExt = ".png";
    if (endsWith(s, "_scimitar")) sExt = ".png";
    if (endsWith(s, "_broad_sword")) sExt = ".png";
    if (endsWith(s, "_plate") && (s !== "opal_chest_plate")) sExt = ".png";
    if (s.indexOf("_wizard") !== -1) sExt = ".png";
    if (endsWith(s, "_battle_axe")) sExt = ".png";
    if (endsWith(s, "_long_sword")) sExt = ".png";
    if (endsWith(s, "_helm")) sExt = ".png";
    if (endsWith(s, "_knife")) sExt = ".png";
    if (endsWith(s, "_wand")) sExt = ".png";
    if (endsWith(s, "_spear")) sExt = ".png";
    if (endsWith(s, "_rapiers")) sExt = ".png";
    if (endsWith(s, "_dagger")) sExt = ".png";
    if (endsWith(s, "_log")) sExt = ".png";
    if (startsWith(s, "ore_")) { s = s.substr(4); sExt = ".png"; }
    if (startsWith(s, "poison_shard")) sExt = ".png";
    if (startsWith(s, "fire_shard")) sExt = ".png";
    if (startsWith(s, "earth_shard")) sExt = ".png";
    if (startsWith(s, "air_shard")) sExt = ".png";
    if (startsWith(s, "water_shard")) sExt = ".png";
    
    if (s.indexOf("_tome") !== -1) s = "tome";
    
    while (s.indexOf("_a") !== -1) s = s.replace("_a", "A");
    while (s.indexOf("_b") !== -1) s = s.replace("_b", "B");
    while (s.indexOf("_c") !== -1) s = s.replace("_c", "C");
    while (s.indexOf("_d") !== -1) s = s.replace("_d", "D");
    while (s.indexOf("_e") !== -1) s = s.replace("_e", "E");
    while (s.indexOf("_f") !== -1) s = s.replace("_f", "F");
    while (s.indexOf("_g") !== -1) s = s.replace("_g", "G");
    while (s.indexOf("_h") !== -1) s = s.replace("_h", "H");
    while (s.indexOf("_i") !== -1) s = s.replace("_i", "I");
    while (s.indexOf("_j") !== -1) s = s.replace("_j", "J");
    while (s.indexOf("_k") !== -1) s = s.replace("_k", "K");
    while (s.indexOf("_l") !== -1) s = s.replace("_l", "L");
    while (s.indexOf("_m") !== -1) s = s.replace("_m", "M");
    while (s.indexOf("_n") !== -1) s = s.replace("_n", "N");
    while (s.indexOf("_o") !== -1) s = s.replace("_o", "O");
    while (s.indexOf("_p") !== -1) s = s.replace("_p", "P");
    while (s.indexOf("_q") !== -1) s = s.replace("_q", "Q");
    while (s.indexOf("_r") !== -1) s = s.replace("_r", "R");
    while (s.indexOf("_s") !== -1) s = s.replace("_s", "S");
    while (s.indexOf("_t") !== -1) s = s.replace("_t", "T");
    while (s.indexOf("_u") !== -1) s = s.replace("_u", "U");
    while (s.indexOf("_v") !== -1) s = s.replace("_v", "V");
    while (s.indexOf("_w") !== -1) s = s.replace("_w", "W");
    while (s.indexOf("_x") !== -1) s = s.replace("_x", "X");
    while (s.indexOf("_y") !== -1) s = s.replace("_y", "Y");
    while (s.indexOf("_z") !== -1) s = s.replace("_z", "Z");
    s = s.split('_').join('');
    
    s = s + sExt;
    
    return s;
};

ET_StatsUIMod_GetDropsForTower = function(iFloor, iRoom)
{
    var sResults = "";
    
    try
    {
        if (ETStatsDB_Tower[iFloor][iRoom] !== undefined)
        {
            jQuery.makeArray(ETStatsDB_Tower[iFloor][iRoom]).forEach(function(dropInfo)
            {
                try
                {
                    //if (sResults !== "") sResults += ", ";
                    //sResults += dropInfo.name + " (" + dropInfo.percent.toString() + "%)";
                    
                    var s = ET_StatsUIMod_ConvertItemIDToImage(dropInfo.name);
                    
                    var sHover = dropInfo.name + " (" + dropInfo.percent.toString() + "%)";
                    //sResults += "<img src=\"/icons/" + s + "\" class=\"ml-1 extra-small-icon\" />";
                    sResults += "<img src=\"/icons/" + s + "\" style=\"width: 24px; height: 24px; font-size: 24px; line-height: 24px;\" alt=\"" + sHover + "\" title=\"" + sHover + "\">";
                }
                catch (err) { }
            });
        }
    }
    catch (err) { }
    
    return sResults;
};

ET_StatsUIMod_LoadETStatsDatabase = function()
{
    if (sETStatsDB !== "")
        return;

    try
    {
        sETStatsDB = "busy";
        
        jQ.get("https://news.mediacentermaster.com/ETStats/", function(data)
        {
            try
            {
                // don't hold bad data
                if (data.indexOf("Level 50-54") === -1)
                {
                    sETStatsDB = "";
                    return;
                }
                
                sETStatsDB = data;
                ETStatsDB_PQ = [];
                ETStatsDB_Tower = [];

                //console.log("ET Stats loaded!");
                //console.log(sETStatsDB);
                
                let rawPQData = ChopperBlank(sETStatsDB, "<pre>", "Tower").trim().split("Level ");
                
                //ETStatsDB_Tower
                jQuery.makeArray(rawPQData).forEach(function(rawPQChunk)
                {
                    //console.log("Raw PQ chunk: " + rawPQChunk.trim());

                    let iRangeStart = 0;
                    let iRangeEnd = 0;
                    
                    jQuery.makeArray(rawPQChunk.trim().split("\n")).forEach(function(rawPQLine, idx, arr)
                    {
                        try
                        {
                            rawPQLine = rawPQLine.trim();
                            
                            //console.log(idx.toString() + " :: " + rawPQLine);
                            
                            if (idx === 0)
                            {
                                iRangeStart = CInt(ChopperBlank(rawPQLine, "",  "-"));
                                iRangeEnd   = CInt(ChopperBlank(rawPQLine, "-", " "));
                            }
                            else if ((iRangeStart > 0) && (iRangeEnd > 0))
                            {
                                let temp_Percent = CDbl(ChopperBlank(rawPQLine, "", "%"));
                                let temp_Name    = ChopperBlank(rawPQLine, " - ", " (").trim();
                                
                                let oDataObj = { name: temp_Name, percent: temp_Percent };
                                
                                for (let i = iRangeStart; i <= iRangeEnd; i++)
                                {
                                    if (ETStatsDB_PQ[i] === undefined)
                                        ETStatsDB_PQ[i] = [];
                                    
                                    ETStatsDB_PQ[i].push(oDataObj);
                                }
                                
                                //console.log("PQ data recorded: L" + iRangeStart.toString() + "-L" + iRangeEnd.toString() + " " + JSON.stringify(oDataObj));
                            }
                        }
                        catch (err)
                        {
                            console.log("PQ line parse failure: " + JSON.stringify(err));
                        }
                    });
                });            
                            
                let rawTowerData = ChopperBlank(sETStatsDB, "Tower F0R0", "</pre>").trim().split("Tower ");
                
                //ETStatsDB_Tower
                jQuery.makeArray(rawTowerData).forEach(function(rawTowerChunk)
                {
                    let iFloor = CInt(ChopperBlank(rawTowerChunk, "F", "R"));
                    let iRoom  = CInt(ChopperBlank(rawTowerChunk, "R", " - "));
                    
                    // don't hold data for full tower floor runs or any invalid data
                    if ((iFloor === 0) || (iRoom === 0))
                        return;
                    
                    jQuery.makeArray(rawTowerChunk.trim().split("\n")).forEach(function(rawTowerLine, idx, arr)
                    {
                        try
                        {
                            rawTowerLine = rawTowerLine.trim();
                            
                            //console.log(idx.toString() + " :: " + rawTowerLine);
                            
                            if (idx === 0)
                                return;
                            
                            let temp_Percent = CDbl(ChopperBlank(rawTowerLine, "", "%"));
                            let temp_Name    = ChopperBlank(rawTowerLine, " - ", " (").trim();
                            
                            let oDataObj = { name: temp_Name, percent: temp_Percent };
                            
                            if (ETStatsDB_Tower[iFloor] === undefined)
                                ETStatsDB_Tower[iFloor] = [];
                            if (ETStatsDB_Tower[iFloor][iRoom] === undefined)
                                ETStatsDB_Tower[iFloor][iRoom] = [];
                            
                            ETStatsDB_Tower[iFloor][iRoom].push(oDataObj);
                                
                            //console.log("Tower data recorded: F" + iFloor.toString() + "R" + iRoom.toString() + " " + JSON.stringify(oDataObj));
                        }
                        catch (err)
                        {
                            console.log("Tower line parse failure: " + JSON.stringify(err));
                        }
                    });
                });
                
                let i;
                
                for (i = 0; i <= 1000; i++)
                {
                    if (ETStatsDB_PQ[i] !== undefined)
                    {
                        //console.log("PQ L" + i.toString() + " sorted!");
                        
                        ETStatsDB_PQ[i].sort(function(dropInfo_a, dropInfo_b)
                        {
                            if (dropInfo_a.percent > dropInfo_b.percent) return -1;
                            if (dropInfo_a.percent < dropInfo_b.percent) return 1;
                            return 0;
                        });
                    }
                }
                
                for (i = 0; i <= 30; i++)
                {
                    if (ETStatsDB_Tower[i] !== undefined)
                    {
                        for (let j = 0; j <= 30; j++)
                        {
                            if (ETStatsDB_Tower[i][j] !== undefined)
                            {
                                //console.log("Tower F" + i.toString() + "R" + j.toString() + " sorted!");
                                
                                ETStatsDB_Tower[i][j].sort(function(dropInfo_a, dropInfo_b)
                                {
                                    if (dropInfo_a.percent > dropInfo_b.percent) return -1;
                                    if (dropInfo_a.percent < dropInfo_b.percent) return 1;
                                    return 0;
                                });
                            }
                        }
                    }
                }
            }
            catch (err) { sETStatsDB = ""; }
        });
    }
    catch (err) { }
};

ET_StatsUIMod_DPSShow = function()
{
	var i = 0;
	var oStatLines = null;
	var sBareDamageRange = "";
	var sBareAttackSpeed = "";
	var sBareCriticalChance = "";
	var dDamageMin = 0.0;
	var dDamageMax = 0.0;
	var sDamagePartMin = "";
	var sDamagePartMax = "";
	var dAverageDamageAvgQuality = 0.0;
	var dAverageDamageMaxQuality = 0.0;
	var sThisLine = "";
	var sThisLineText = "";
	var dAttackSpeed = 0.0;
	var dActualDPSAverageQuality = 0.0;
	var dActualDPSMaxQuality = 0.0;
	var dCriticalChance = 0.0;

    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Adventure Details
    //
    ET_StatsUIMod_LoadETStatsDatabase(); // loads on demand

    try
    {
        // Options
        if ((ET.MCMF.GetActiveTab() === "combat") /* && (ET.MCMF.GetActiveTabSection() === "adventures") */)
        {
            if (jQ("div.PETStatusUI_Menu").length === 0)
            {
                var oTemp = jQ(jQ("body div.body-content div div div div[style=\"margin-bottom: 5px;\"]").get(0));
                if (oTemp.text().trim() == "Buy adventure"); // sanity check
                {
                    oTemp = oTemp.parent();
                    
                    oTemp.before("<div class=\"PETStatusUI_Menu d-flex flex-row flex-wrap\"></div>");
                    oTemp.appendTo("div.PETStatusUI_Menu");

                    oTemp = jQ("div.PETStatusUI_Menu");
                    
                    oTemp.prepend
                    (
                        "<div>" + 
                        "<div style=\"margin-bottom: 5px;\">Options</div>" +
                        "<button class=\"buy-new-adventure btn btn-secondary PETStatusUI_btnShowHideLoot\">Loot: " + ((ET.MCMF.StatsUIMod.WantAdventureLoot) ? "Shown" : "Hidden")  + "</button>" +
                        "</div>"
                    );
                    
                    oTemp.children("div").addClass("d-flex flex-column my-1 mx-1");
                    oTemp.find("button").css("height", "40px").css("max-height", "40px").css("line-height", "24px"); // normalize the button heights
                    
                    jQ(".PETStatusUI_btnShowHideLoot").click(function()
                    {
                        ET.MCMF.StatsUIMod.WantAdventureLoot = !ET.MCMF.StatsUIMod.WantAdventureLoot;
                        GM_setValue("PETStatusUI_WantAdventureLoot", ET.MCMF.StatsUIMod.WantAdventureLoot);
                        jQ(".PETStatusUI_btnShowHideLoot").text((ET.MCMF.StatsUIMod.WantAdventureLoot) ? "Loot: Shown" : "Loot: Hidden");
                    });
                }
            }
            
            // Adventure loot lines
            if (ET.MCMF.StatsUIMod.WantAdventureLoot)
            {
                jQ("div.PETSDetails_error").remove();

                jQ("div.adventure-item-container").each(function()
                {
                    try
                    {
                        var sAdvID = jQ(this).find("button").attr("data-id");
                        
                        var oAdventureData = ET_StatsUIMod_GetAdventure(sAdvID);
                        
                        if (jQ(this).find("div.PETSDetails_success").length > 0)
                            return;

                        jQ(this).find("div.PETSDetails").remove();                
                            
                        var sLoot = ET_StatsUIMod_GetDropsForTower(oAdventureData.floor, oAdventureData.room);
                        
                        if (sLoot !== "")
                        {
                            jQ(this).append("<div class=\"d-flex PETSDetails PETSDetails_success\"><div class=\"d-flex\" style=\"margin-left: 60px;\">" + sLoot + "</div></div>\r\n");
                        }
                        else
                        {
                            jQ(jQ(this).find("div.ml-3").get(0)).append("<div class=\"PETSDetails PETSDetails_error\">Loot From F" + oAdventureData.floor.toString() + "R" + oAdventureData.room.toString() + "</div>\r\n");
                            jQ(jQ(this).find("div.mx-3").get(0)).append("<div class=\"PETSDetails PETSDetails_error\"><a target=\"_blank\" href=\"http://etstats.com/debug.html\">ETStats Loot List</a></div>\r\n");
                        }
                    }
                    catch (err) { }
                });
            }
            else
                jQ(".PETSDetails").remove();
        }
        else
        {
            jQ("div.PETStatusUI_Menu").remove();
        }
    }
    catch (err) { ET.MCMF.Log(err); }

    
    //
    ///////////////////////////////////////////////////////////////////////////////////////
    

    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Stat Descriptions
    //
    jQ(".PeteUI_TooltipStat").remove();
    // Removed: this is baked into the actual game now
    /*
    jQ("div.item-tooltip-content div i.lilIcon-attackSpeed").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;attacks per second</span>");
    jQ("div.item-tooltip-content div i.lilIcon-accuracy").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;accuracy</span>");
    jQ("div.item-tooltip-content div i.lilIcon-criticalChance").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;crit. chance</span>");
    jQ("div.item-tooltip-content div i.lilIcon-healthMax").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;max. health</span>");
    jQ("div.item-tooltip-content div i.lilIcon-defense").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;dodge (defense)</span>");
    jQ("div.item-tooltip-content div i.lilIcon-armor").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;physical armor</span>");
    jQ("div.item-tooltip-content div i.lilIcon-magicPower").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;magic power</span>");
    jQ("div.item-tooltip-content div i.lilIcon-magicArmor").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;magic armor</span>");
    jQ("div.item-tooltip-content div i.lilIcon-healingPower").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;increased healing %</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-attackSpeed").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;attacks per second</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-accuracy").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;accuracy</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-criticalChance").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;crit. chance</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-healthMax").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;max. health</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-defense").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;dodge (defense)</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-armor").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;physical armor</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-magicPower").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;magic power</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-magicArmor").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;magic armor</span>");
    jQ("form.craft-amount-form div.modal-body > div i.lilIcon-healingPower").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;increased healing %</span>");
    */
    //
    ///////////////////////////////////////////////////////////////////////////////////////

    
    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Woodcutters
    //    
    jQ("div.item-tooltip-content").each(function()
    {
        try
        {
            if (jQ(this).find("div.PeteUI_TooltipStatWC").length === 0)
            {
                if (jQ(this).html().toLowerCase().indexOf("lumber jack") !== -1)
                {
                    jQ(this).find("div i.lilIcon-attack").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;wood cut tier</span>");
                    jQ(this).find("div i.lilIcon-attackSpeed").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;attacks per second</span>");
                    jQ(this).find("div i.lilIcon-accuracy").parent().append("<span class=\"PeteUI_TooltipStat\">&nbsp;accuracy</span>");
                }
            }
        }
        catch (err) { }
    });
    //    
    ///////////////////////////////////////////////////////////////////////////////////////
        
    
    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Crafting Recipes
    //
	try
	{
		if (jQ("a.show-stats").length > 0)
		{
			ET_LastForm = jQ("a.show-stats").parent().parent();
			ET_LastModal = ET_LastForm.parent().parent().parent();

			jQ("a.show-stats")[0].click(); // auto-expand base stats for crafting ranges (needed anyway to calculate extra stats)
			ET_JustExpanded = true;
		}

		if ((ET_LastModal !== null) && (ET_LastModal.hasClass("show")) && (ET_JustExpanded === true))
		{
			var oForm = ET_LastForm;
			//oForm = jQ("form.craft-amount-form");

			if (oForm.html().indexOf("Hide Stat Range") !== -1)
			{
				oStatLines = oForm.find("div.modal-body > div");

				for (i = 1; i < oStatLines.length; i++)
				{
					sThisLine = jQ(oStatLines.get(i)).html().trim();
					sThisLineText = jQ(oStatLines.get(i)).text().trim();

					if ((sThisLine.indexOf("lilIcon-attack ") !== -1) && (sThisLine.indexOf(" - ") === -1))
						continue;

					if     ((sThisLine.indexOf("lilIcon-attack ")         !== -1) && (sBareDamageRange === "")) sBareDamageRange = sThisLineText + " [[#" + i.toString() + "]]";
					else if (sThisLine.indexOf("lilIcon-attackSpeed ")    !== -1) sBareAttackSpeed = sThisLineText + " [[#" + i.toString() + "]]";
					else if (sThisLine.indexOf("lilIcon-criticalChance ") !== -1) sBareCriticalChance = sThisLineText + " [[#" + i.toString() + "]]";
				}
                
				//console.log("******************BEGIN******************");
				//console.log("Bare DPS: " + sBareDamageRange);
				//console.log("Bare attack speed: " + sBareAttackSpeed);
				//console.log("Bare crit chance: " + sBareCriticalChance);
				//console.log("*******************END*******************");

                sBareDamageRange    = ChopperBlank(sBareDamageRange.replace(' attack', '').replace(' damage', '') + "\n", "", "\n").trim();
                sBareAttackSpeed    = ChopperBlank(sBareAttackSpeed.replace(' attack speed', '') + "\n", "", "\n").trim();
                sBareCriticalChance = ChopperBlank(sBareCriticalChance.replace(' critical chance', '') + "\n", "", "\n").trim();
                
				//console.log("******************BEGIN******************");
				//console.log("Bare DPS: " + sBareDamageRange);
				//console.log("Bare attack speed: " + sBareAttackSpeed);
				//console.log("Bare crit chance: " + sBareCriticalChance);
				//console.log("*******************END*******************");

				if (sBareDamageRange.indexOf("(") !== -1)
				{
					sDamagePartMin = ChopperBlank(sBareDamageRange, "", ") - (") + ")";
					sDamagePartMax = "(" + ChopperBlank(sBareDamageRange, ") - (", "");

					var dDamageRangeLow1 = CDbl(ChopperBlank(sDamagePartMin, "(", " - ").trim());
					var dDamageRangeLow2 = CDbl(ChopperBlank(sDamagePartMin, " - ", ")").trim());
					dDamageMin = (dDamageRangeLow1 + dDamageRangeLow2) / 2.0;
					var dDamageRangeHigh1 = CDbl(ChopperBlank(sDamagePartMax, "(", " - ").trim());
					var dDamageRangeHigh2 = CDbl(ChopperBlank(sDamagePartMax, " - ", ")").trim());
					dDamageMax = (dDamageRangeHigh1 + dDamageRangeHigh2) / 2.0;

					dAverageDamageMaxQuality = ((dDamageRangeHigh2 - dDamageRangeLow2) / 2.0) + dDamageRangeLow2;
				}
				else
				{
					sDamagePartMin = ChopperBlank(sBareDamageRange, "", " - ");
					sDamagePartMax = ChopperBlank(sBareDamageRange, " - ", "");

					dDamageMin = CDbl(sDamagePartMin);
					dDamageMax = CDbl(sDamagePartMax);

					dAverageDamageMaxQuality = ((dDamageMax - dDamageMin) / 2.0) + dDamageMin;
				}

				dAverageDamageAvgQuality = ((dDamageMax - dDamageMin) / 2.0) + dDamageMin;

				dAttackSpeed = CDbl(sBareAttackSpeed);
				dCriticalChance = CDbl(sBareCriticalChance);

				if (dCriticalChance > 0.0)
				{
					dAverageDamageAvgQuality += dAverageDamageAvgQuality * (dCriticalChance / 100.0);
					dAverageDamageMaxQuality += dAverageDamageMaxQuality * (dCriticalChance / 100.0);
				}

				dActualDPSAverageQuality = dAverageDamageAvgQuality * dAttackSpeed;
				dActualDPSMaxQuality = dAverageDamageMaxQuality * dAttackSpeed;

                if (!isNaN(dActualDPSAverageQuality))
                {
                    oForm.find("div.modal-body").append
                    (
                        "<div class=\"d-flex flex-wrap\"></div><b>Rated Damage at 50% Quality</b>\r\n" +
                        "<div class=\"d-flex flex-wrap\"><b class=\"lilIcon-attack extra-small-icon mx-1\"></b> " + dAverageDamageAvgQuality.toFixed(1) + " (per hit / base damage for abilities)</div>\r\n" +
                        "<div class=\"d-flex flex-wrap\"><b class=\"lilIcon-attackSpeed extra-small-icon mx-1\"></b> " + dActualDPSAverageQuality.toFixed(1) + " (per second / DPS)</div>\r\n" +
                        "<b>Rated Damage at 100% Quality</b>\r\n" +
                        "<div class=\"d-flex flex-wrap\"><b class=\"lilIcon-attack extra-small-icon mx-1\"></b> " + dAverageDamageMaxQuality.toFixed(1) + " (per hit / base damage for abilities)</div>\r\n" +
                        "<div class=\"d-flex flex-wrap\"><b class=\"lilIcon-attackSpeed extra-small-icon mx-1\"></b> " + dActualDPSMaxQuality.toFixed(1) + " (per second / DPS)</div>\r\n"
                    );
                }

				ET_JustExpanded = false;
			}
		}
	}
	catch (err) { console.log("ERROR: " + err); }
    //
    ///////////////////////////////////////////////////////////////////////////////////////


    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Item Hover Tooltips (Combat > Equipment & Stats, Wodcutting, viewing profiles, etc.)
    //
	try
	{
		jQ("div.item-tooltip-content").each(function()
        {
            if (jQ(this).find("div.PeteUI_TooltipExp").length !== 0)
				return;

			sBareDamageRange = "";
			sBareAttackSpeed = "";
			sBareAccuracy = "";
			sBareCriticalChance = "";
            sBareEnergyPerHit = "";
            sBareEnergyRegen = "";
            var bIsWoodcutter = false;

			oStatLines = jQ(this).find("div > div");

			for (i = 0; i < oStatLines.length; i++)
			{
				sThisLine = jQ(oStatLines.get(i)).html().trim();
				sThisLineText = jQ(oStatLines.get(i)).text().trim();

                //console.log("DEBUG::" + sThisLine);
                //console.log("DEBUG::" + sThisLineText);

				if      ((sThisLine.indexOf("lilIcon-attack ")         !== -1) && (sThisLine.indexOf(" - ") !== -1) && (sBareDamageRange === "")) sBareDamageRange = sThisLineText + " [[#" + i.toString() + "]]";
				else if ((sThisLine.indexOf("lilIcon-attack ")         !== -1) && (sThisLine.indexOf("wood cut tier") !== -1) && (sBareDamageRange === "")) { bIsWoodcutter = true; sBareDamageRange = ChopperBlank(sThisLineText, "", "wood cut tier").trim() + " - " + ChopperBlank(sThisLineText, "", "wood cut tier").trim() + " [[#" + i.toString() + "]]"; }
				else if ((sThisLine.indexOf("lilIcon-attack ")         !== -1) && (sThisLine.indexOf(" - ") === -1) && (sBareDamageRange === "")) { bIsWoodcutter = true; sBareDamageRange = sThisLineText + " - " + sThisLineText + " [[#" + i.toString() + "]]"; }
				else if  (sThisLine.indexOf("lilIcon-attackSpeed ")    !== -1) sBareAttackSpeed = sThisLineText + " [[#" + i.toString() + "]]";
				else if  (sThisLine.indexOf("lilIcon-accuracy ")       !== -1) sBareAccuracy = sThisLineText + " [[#" + i.toString() + "]]";
				else if  (sThisLine.indexOf("lilIcon-criticalChance ") !== -1) sBareCriticalChance = sThisLineText + " [[#" + i.toString() + "]]";
				else if  (sThisLine.indexOf("lilIcon-energyPerHit ")   !== -1) sBareEnergyPerHit = sThisLineText + " [[#" + i.toString() + "]]";
				else if  (sThisLine.indexOf("lilIcon-energyRegen ")    !== -1) sBareEnergyRegen = sThisLineText + " [[#" + i.toString() + "]]";
			}

            //console.log("******************BEGIN******************");
            //console.log("Bare DPS: " + sBareDamageRange);
            //console.log("Bare attack speed: " + sBareAttackSpeed);
            //console.log("Bare accuracy: " + sBareAccuracy);
            //console.log("Bare crit chance: " + sBareCriticalChance);
            //console.log("Bare energy per hit: " + sBareEnergyPerHit);
            //console.log("Bare energy regen: " + sBareEnergyRegen);
            //console.log("*******************END*******************");

            sBareDamageRange    = ChopperBlank(sBareDamageRange.replace(' damage', '').replace(' damage', '') + "\n", "", "\n").trim();
            sBareAttackSpeed    = ChopperBlank(sBareAttackSpeed.replace(' attack speed', '') + "\n", "", "\n").trim();
            sBareAccuracy       = ChopperBlank(sBareAccuracy.replace(' accuracy', '').trim() + " ", "", " ").trim();
            sBareCriticalChance = ChopperBlank(sBareCriticalChance.replace(' critical chance', '') + "\n", "", "\n").trim();
            sBareEnergyPerHit   = ChopperBlank(sBareEnergyPerHit.replace(' energy per hit', '') + "\n", "", "\n").trim();
            sBareEnergyRegen    = ChopperBlank(sBareEnergyRegen.replace(' energy regen', '') + "\n", "", "\n").trim();
            
            //console.log("******************BEGIN******************");
            //console.log("Bare DPS: " + sBareDamageRange);
            //console.log("Bare attack speed: " + sBareAttackSpeed);
            //console.log("Bare accuracy: " + sBareAccuracy);
            //console.log("Bare crit chance: " + sBareCriticalChance);
            //console.log("Bare energy per hit: " + sBareEnergyPerHit);
            //console.log("Bare energy regen: " + sBareEnergyRegen);
            //console.log("*******************END*******************");

			sDamagePartMin = ChopperBlank(sBareDamageRange, "", " - ");
			sDamagePartMax = ChopperBlank(sBareDamageRange, " - ", "");

			dDamageMin = CDbl(sDamagePartMin);
			dDamageMax = CDbl(sDamagePartMax);

			dAverageDamageAvgQuality = ((dDamageMax - dDamageMin) / 2.0) + dDamageMin;

			dAttackSpeed = CDbl(sBareAttackSpeed);
			dCriticalChance = CDbl(sBareCriticalChance);

			if (dCriticalChance > 0.0)
				dAverageDamageAvgQuality += dAverageDamageAvgQuality * (dCriticalChance / 100.0);

			dActualDPSAverageQuality = dAverageDamageAvgQuality * dAttackSpeed;

            if (!isNaN(dActualDPSAverageQuality) && !bIsWoodcutter)
            {
                if (jQ(this).find("div.PeteUI_TooltipExp").length === 0)
                {
                    jQ(jQ(this).find("div").get(0)).append
                    (
                        "<div class=\"PeteUI_TooltipExp\"><div class=\"d-flex flex-wrap\"></div><b>Rated Damage</b>\r\n" +
                        "<div class=\"d-flex flex\" style=\"white-space: nowrap;\"><b class=\"lilIcon-attack extra-small-icon mx-1\"></b> " + dAverageDamageAvgQuality.toFixed(1) + " (per hit / base damage)</div>\r\n" +
                        "<div class=\"d-flex flex\" style=\"white-space: nowrap;\"><b class=\"lilIcon-attackSpeed extra-small-icon mx-1\"></b> " + dActualDPSAverageQuality.toFixed(1) + " (per second / DPS)</div></div>\r\n"
                    );
                }
            }
            else if (CDbl(sBareEnergyPerHit) > 0.0)
            {
                if (jQ(this).find("div.PeteUI_TooltipExp").length === 0)
                {
                    dActualDPSAverageQuality = dAverageDamageAvgQuality / CDbl(sBareEnergyPerHit) * CDbl(sBareEnergyRegen);
                    
                    jQ(jQ(this).find("div").get(0)).append
                    (
                        "<div class=\"PeteUI_TooltipExp\"><div class=\"d-flex flex-wrap\"></div><b>Rated Efficiency</b>\r\n" +
                        "<div class=\"d-flex flex\" style=\"white-space: nowrap;\"><b class=\"lilIcon-mining extra-small-icon mx-1\"></b> " + dActualDPSAverageQuality.toFixed(1) + " rating</div>\r\n"
                    );
                }
            }
            else if ((dAttackSpeed > 0.0) && (CDbl(sBareAccuracy) > 0.0) && (bIsWoodcutter))
            {
                if (jQ(this).find("div.PeteUI_TooltipExp").length === 0)
                {
                    dActualDPSAverageQuality = CDbl(sBareAccuracy) * dAttackSpeed;
                    
                    var sTier = "maple logs (only)";
                    if (CInt(dAverageDamageAvgQuality) === 5) sTier = "beech logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 10) sTier = "ash logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 15) sTier = "oak logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 20) sTier = "maple logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 25) sTier = "walnut logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 30) sTier = "cherry logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 35) sTier = "mahogany logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 40) sTier = "elm logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 45) sTier = "black logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 50) sTier = "blue gum logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 55) sTier = "cedar logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 60) sTier = "denya logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 65) sTier = "gombe logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 70) sTier = "hickory logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 75) sTier = "larch logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 80) sTier = "poplar logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 85) sTier = "tali logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 90) sTier = "willow logs (and lower)";
                    else if (CInt(dAverageDamageAvgQuality) === 95) sTier = "teak logs (and lower)";
                    
                    jQ(jQ(this).find("div").get(0)).append
                    (
                        "<div class=\"PeteUI_TooltipExp\"><div class=\"d-flex flex-wrap\"></div><br /><b>Rated Efficiency</b>\r\n" +
                        "<div class=\"d-flex flex\" style=\"white-space: nowrap;\"><b class=\"lilIcon-woodcutting extra-small-icon mx-1\"></b> " + dActualDPSAverageQuality.toFixed(1) + " rating</div>\r\n" + 
                        "<div class=\"d-flex flex\" style=\"white-space: nowrap;\"><b class=\"lilIcon-woodcutter extra-small-icon mx-1\"></b> chops " + sTier + "</div>\r\n"                        
                    );
                }
            }
		});
	}
	catch (err) { console.log("ERROR: " + err); }
    //
    ///////////////////////////////////////////////////////////////////////////////////////


    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  Combat Stats page (personal)
    //
	try
	{
        if (ET.MCMF.GetActiveTab() == "combat")
        {
            if (ET.MCMF.IsNewCombatTab())
            {
                if (jQ("div.lobby-container").find("div.mb-1").html().toLowerCase().indexOf("combat levels") !== -1)
                {
                    var oCombatData = ET.MCMF.GetPlayerCombatData();
                    
                    jQ(".PeteUI_CombatStat").remove();
                    
                    dDamageMin = oCombatData.stats.attack;
                    dDamageMax = oCombatData.stats.attackMax;

                    dAverageDamageAvgQuality = ((dDamageMax - dDamageMin) / 2.0) + dDamageMin;

                    dAttackSpeed = oCombatData.stats.attackSpeed;
                    dCriticalChance = oCombatData.stats.criticalChance;

                    if (dCriticalChance > 0.0)
                        dAverageDamageAvgQuality += dAverageDamageAvgQuality * (dCriticalChance / 100.0); // note: not using ET_Stat_CritDamage!

                    dActualDPSAverageQuality = dAverageDamageAvgQuality * dAttackSpeed;
                    
                    //jQ("a.recent-battles-btn").parent().parent().before(
                    jQ("div.lobby-container").append(
                        "<div class=\"PeteUI_CombatStat\">\r\n" +
                        "  <div class=\"mb-1 text-muted\">Combat Stats</div>\r\n" + 
                        "  <div class=\"d-flex\">" +
                        
                        "    <div class=\"col\">" +
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-attack small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Weapon Damage</div>\r\n" +
                        "            <small class=\"mx-3\">" + dDamageMin.toFixed(1) + " - " + dDamageMax.toFixed(1) + " (" + dAverageDamageAvgQuality.toFixed(1) + " average)</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" +
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-accuracy small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Accuracy</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.accuracy.toFixed(1) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-defense small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Defense (Dodge)</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.defense.toFixed(1) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-health small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Health (Max)</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.healthMax.toFixed(0) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-magicPower small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Magic Power</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.magicPower.toFixed(1) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-noimage small-icon\"><img src=\"/icons/airDart.svg\" class=\"extra-small-icon\" style=\"margin-top: -16px;\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Air Dart</div>\r\n" +
                        "            <small class=\"mx-3\">" + ((1.10 * oCombatData.stats.magicPower) + 1).toFixed(0) + " armor reduction</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-noimage small-icon\"><img src=\"/icons/lightningDart.svg\" class=\"extra-small-icon\" style=\"margin-top: -16px;\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Lightning Dart</div>\r\n" +
                        "            <small class=\"mx-3\">" + ((0.90 * oCombatData.stats.magicPower) + 2).toFixed(0) + " armor reduction</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "    </div>\r\n" + 
                        
                        "    <div class=\"col\">\r\n" +
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-attackSpeed small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Attack Speed</div>\r\n" +
                        "            <small class=\"mx-3\">" + dAttackSpeed.toFixed(1) + " attacks per second (" + dActualDPSAverageQuality.toFixed(1) + " DPS)</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-criticalChance small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Critical Hit</div>\r\n" +
                        "            <small class=\"mx-3\">" + ((oCombatData.stats.criticalChance <= 0.0) ? ("none") : (oCombatData.stats.criticalChance.toFixed(1) + "% chance to deal " + oCombatData.stats.criticalDamage.toFixed(1) + "x damage")) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-armor small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Physical Armor</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.armor.toFixed(1) + " (" + ((1000-(100/(100+oCombatData.stats.armor)*1000))/10).toFixed(1) + "% physical protection)</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" +
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-noimage small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\"></div>\r\n" +
                        "            <small class=\"mx-3\"></small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-magicArmor small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Magic Armor</div>\r\n" +
                        "            <small class=\"mx-3\">" + oCombatData.stats.magicArmor.toFixed(1) + " (" + ((1000-(100/(100+oCombatData.stats.magicArmor)*1000))/10).toFixed(1) + "% magic protection)</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-noimage small-icon\"><img src=\"/icons/airBall.svg\" class=\"extra-small-icon\" style=\"margin-top: -16px;\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Air Ball</div>\r\n" +
                        "            <small class=\"mx-3\">" + ((1.60 * oCombatData.stats.magicPower) + 10).toFixed(0) + " armor reduction</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "      <div class=\"d-flex flex-wrap\">\r\n" +
                        "        <div class=\"mb-3 mr-3\">\r\n" +
                        "          <div class=\"d-flex align-items-center\">\r\n" +
                        "            <i class=\"lilIcon-healingPower small-icon\"></i>\r\n" +
                        "            <div class=\"ml-1 mb-0\">Healing Power</div>\r\n" +
                        "            <small class=\"mx-3\">" + ((oCombatData.stats.healingPower === 0.0) ? "normal (no bonus or penalty)" : ((oCombatData.stats.healingPower < 0) ? (oCombatData.stats.healingPower.toFixed(1) + "% lowered healing") : (oCombatData.stats.healingPower.toFixed(1) + "% increased healing"))) + "</small>\r\n" +
                        "          </div>\r\n" +
                        "        </div>\r\n" +
                        "      </div>\r\n" + 
                        "    </div>\r\n" +

                        "  </div>\r\n" + 
                        "</div>\r\n");
                }
            }
            else if ((!ET.MCMF.IsNewCombatTab()) && (jQ("div.stats-row").length > 0))
            {
                sBareDamageRange = CondenseSpacing(jQ(jQ("div.stats-row").find("div > div.col > div").get(0)).text().trim().replaceAll('\r', ' ').replaceAll('\n', ' '));
                //console.log("Bare damage range: " + sBareDamageRange);

                sDamagePartMin = ChopperBlank(sBareDamageRange, "", " - ");
                sDamagePartMax = ChopperBlank(sBareDamageRange, " - ", "");

                dDamageMin = CDbl(sDamagePartMin);
                dDamageMax = CDbl(sDamagePartMax);

                dAverageDamageAvgQuality = ((dDamageMax - dDamageMin) / 2.0) + dDamageMin;

                sBareAttackSpeed = jQ(jQ("div.stats-row").find("div > div.col > div").get(1)).text().trim();
                //console.log("Bare attack speed: " + sBareAttackSpeed);

                dAttackSpeed = CDbl(sBareAttackSpeed);
                dCriticalChance = CDbl(ET_Stat_CritChance);

                if (dCriticalChance > 0.0)
                    dAverageDamageAvgQuality += dAverageDamageAvgQuality * (dCriticalChance / 100.0); // note: not using ET_Stat_CritDamage!

                dActualDPSAverageQuality = dAverageDamageAvgQuality * dAttackSpeed;

                //console.log(dActualDPSAverageQuality.toFixed(1));

                if (!isNaN(dActualDPSAverageQuality))
                {
                    jQ(".PeteUI_CombatStat").remove();

                    jQ(jQ("div.stats-row").find("div > div.col > div").get(0)).append("<span class=\"PeteUI_CombatStat\">&nbsp;(" + dAverageDamageAvgQuality.toFixed(1) + " average)</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(1)).append("<span class=\"PeteUI_CombatStat\">&nbsp;attacks per second (" + dActualDPSAverageQuality.toFixed(1) + " DPS)</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(2)).append("<span class=\"PeteUI_CombatStat\">&nbsp;magic power</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(3)).append("<span class=\"PeteUI_CombatStat\">&nbsp;accuracy</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(4)).append("<span class=\"PeteUI_CombatStat\">&nbsp;health</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(5)).append("<span class=\"PeteUI_CombatStat\">&nbsp;dodge (defense)</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(6)).append("<span class=\"PeteUI_CombatStat\">&nbsp;physical armor</span>");
                    jQ(jQ("div.stats-row").find("div > div.col > div").get(7)).append("<span class=\"PeteUI_CombatStat\">&nbsp;magic armor</span>");

                    if (CDbl(ET_Stat_CritChance) > 0.0)
                        jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/criticalChance.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + ET_Stat_CritChance.toFixed(1) + " critical chance for " + ET_Stat_CritDamage + "x damage\r\n</div>\r\n");
                    else
                        jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/criticalChance.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\nno critical chance\r\n</div>\r\n");

                    if (CDbl(ET_Stat_HealingPower) !== 0.0)
                        jQ(jQ("div.stats-row").find("div > div.col").get(1)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/healingPower.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + ET_Stat_HealingPower.toFixed(1) + "% " + ((CDbl(ET_Stat_HealingPower) > 0.0) ? "increased" : "lowered") + " healing\r\n</div>\r\n");
                    else
                        jQ(jQ("div.stats-row").find("div > div.col").get(1)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/healingPower.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\nnormal healing\r\n</div>\r\n");

                    //jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/doubleEdgedSword.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + (dDamageMax * 1.5 * 1.7 * 2.5 * 5.0).toFixed(0) + " DE damage vs. 0 armor\r\n</div>\r\n");
                    //jQ(jQ("div.stats-row").find("div > div.col").get(1)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center ml-1\">\r\n(using demon curse, war cry, and berserk)\r\n</div>\r\n");
                    jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/doubleEdgedSword.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + (dDamageMax * 1.5 * 1.7 * 5.0).toFixed(0) + " DE damage vs. 0 armor\r\n</div>\r\n");
                    jQ(jQ("div.stats-row").find("div > div.col").get(1)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center ml-1\">\r\n(using L.5 war cry, berserk, and DE)\r\n</div>\r\n");
                    jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/airDart.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + ((1.10 * ET_Stat_MP) + 1).toFixed(0) + " air dart armor reduction\r\n</div>\r\n");
                    jQ(jQ("div.stats-row").find("div > div.col").get(1)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/airBall.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + ((1.60 * ET_Stat_MP) + 10).toFixed(0) + " air ball armor reduction\r\n</div>\r\n");
                    jQ(jQ("div.stats-row").find("div > div.col").get(0)).append("<div class=\"d-flex flex-row mb-1 PeteUI_CombatStat\">\r\n<div class=\"d-flex align-items-center attack-tooltip-container drop-target drop-abutted drop-abutted-left drop-abutted-top drop-element-attached-bottom drop-element-attached-left drop-target-attached-top drop-target-attached-left\">\r\n<img src=\"/icons/lightningDart.svg\" class=\"extra-small-icon\">\r\n</div>\r\n<div class=\"d-flex align-items-center ml-1\">\r\n" + ((0.90 * ET_Stat_MP) + 2).toFixed(0) + " lightning dart armor reduction\r\n</div>\r\n");
                }
            }
		}
	}
	catch (err) { console.log("ERROR: " + err); }
    //
    ///////////////////////////////////////////////////////////////////////////////////////

	setTimeout(ET_StatsUIMod_DPSShow, 1000);
};


////////////////////////////////////////////////////////////////
/////////////// ** 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;
};

Random = 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.currentTarget = target;
	shiftclickOrig.shiftKey = true;

	let shiftclick = jQ.Event("click");
    //shiftclick.type = "click"; // "mousedown" ?
    shiftclick.which = 1; // 1 = left, 2 = middle, 3 = right
	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));
    };

is_visible = (function () {
    var 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;
        var 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) {
            var 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;
    };
})();
////////////////////////////////////////////////////////////////


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

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

        CombatID: undefined,
        BattleID: undefined,

        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);

            /*
            jQ("div#ET_meancloud_bootstrap").off("ET:" + sEventName.trim()).on("ET:" + sEventName.trim(), function()
            {
                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 + ")"));
                        oThisEvent.callback();
                    }
                });
            });
            */

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

        EventTrigger: function(sEventName)
        {
            //jQ("div#ET_meancloud_bootstrap").trigger(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: " + err); }
                }
            });
        },
        
        Log: function(msg)
        {
            try
            {
                let now_time = new Date();
                let timestamp = (now_time.getMonth() + 1).toString() + "/" + now_time.getDate().toString() + "/" + (now_time.getYear() + 1900).toString() + " " + ((now_time.getHours() === 0) ? (12) : ((now_time.getHours() > 12) ? (now_time.getHours() - 12) : (now_time.getHours()))).toString() + ":" + now_time.getMinutes().toString().padStart(2, "0") + ":" + now_time.getSeconds().toString().padStart(2, "0") + ((now_time.getHours() < 12) ? ("am") : ("pm")) + " :: ";
                console.log(timestamp.toString() + msg);
            }
            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(Package.meteor.global.Accounts.connection._subscriptions).map(key => Package.meteor.global.Accounts.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 + "'");
                }
                //else if (ET.MCMF.WantDebug)
                //    window.ET.MCMF.Log("Meteor::Already 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) window.ET.MCMF.Log(err);
            }

            return oChannel;
        },
      
        IsNewCombatTab: function()
        {
            try {
                if (window.location.href.indexOf("/newCombat") !== -1) {
                    return true; }
            }
            catch (err) {
            }
            
            return false;
        },
        
        GetActiveTab: function()
        {
            let active_tab = "";
            
            /*
            try
            {
                active_tab = jQuery(jQuery("a.active").get(0)).text().trim().toLowerCase();
                
                if (active_tab.length === 0)
                    throw "Invalid active tab";
                
                if (active_tab === "mine") active_tab = "mining";
                if (active_tab === "craft") active_tab = "crafting";
                if (active_tab === "battle") active_tab = "combat";
                if (active_tab === "woodcut") active_tab = "woodcutting";
                if (active_tab === "farm") active_tab = "farming";
                if (active_tab === "inscribe") active_tab = "inscription";
                //if (active_tab === "inscription") active_tab = "inscription";
                //if (active_tab === "magic") active_tab = "magic";
                //if (active_tab === "shop") active_tab = "shop";
            }
            catch (err)
            {
            */
                if (window.location.href.indexOf("/mining") !== -1) active_tab = "mining";
                if (window.location.href.indexOf("/crafting") !== -1) active_tab = "crafting";
                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("/woodcutting") !== -1) active_tab = "woodcutting";
                if (window.location.href.indexOf("/farming") !== -1) active_tab = "farming";
                if (window.location.href.indexOf("/inscription") !== -1) active_tab = "inscription";
                if (window.location.href.indexOf("/magic") !== -1) active_tab = "magic";
                if (window.location.href.indexOf("/faq") !== -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("/achievements") !== -1) active_tab = "achievements";
                if (window.location.href.indexOf("/updates") !== -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") 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;
        },

        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) window.ET.MCMF.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) window.ET.MCMF.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) || ((time_val() - 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);
        },

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

            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 (window.ET.MCMF.FinishedLoading)
                    {
                        if (oMeteorData.collection === "battlesList")
                        {
                            window.ET.MCMF.IsDemon = false;
                            window.ET.MCMF.AbilitiesReady = false;

                            if ((oMeteorData.msg === "added") || (oMeteorData.msg === "removed"))
                            {
                                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 ((oMeteorData.collection === "battles") && (oMeteorData.msg === "added"))
                        {
                            if (oMeteorData.fields.finished)
                            {
                                window.ET.MCMF.WonLast = oMeteorData.fields.win;
                                window.ET.MCMF.TimeLastFight = time_val();

                                //if (!oMeteorData.fields.win)
                                //    window.ET.MCMF.HP = 0;

                                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")));
                            }
                        }
                    }

                    try
                    {
                        if (window.ET.MCMF.FinishedLoading)
                        {
                            if (oMeteorData.id)
                            {
                                if (oMeteorData.id.startsWith("battles-"))
                                {
                                    if (oMeteorData.msg !== "removed")
                                    {
                                        battleID = oMeteorData.id;
                                        battleData = JSON.parse(oMeteorData.fields.value);

                                        jQ.makeArray(battleData.units).forEach(function(currentPlayer)
                                        {
                                            try
                                            {
                                                if (currentPlayer.name === window.ET.MCMF.UserName)
                                                {
                                                    window.ET.MCMF.PlayerUnitData = currentPlayer;
                                                    
                                                    jQ.makeArray(currentPlayer.buffs).forEach(function(currentBuff)
                                                     {
                                                        try
                                                        {
                                                            if (currentBuff.id === "demons_heart")
                                                            {
                                                                if (currentBuff.data.active)
                                                                {
                                                                    if (!window.ET.MCMF.IsDemon)
                                                                    {
                                                                        window.ET.MCMF.IsDemon = true;

                                                                        if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering ET:combat:buffDemon -->");
                                                                        window.ET.MCMF.EventTrigger("ET:combat:buffDemon");
                                                                        //jQ("div#ET_meancloud_bootstrap").trigger("ET:combat:buffDemon");
                                                                    }
                                                                }
                                                            }
                                                        }
                                                        catch (err) { }
                                                    });

                                                    return true; // break out of forEach()
                                                }
                                            }
                                            catch (err) { }
                                        });
                                    }
                                }
                            }
                        }
                    }
                    catch (err) { }
                }
                catch (err) { }
            });
        },
        
        PlayerHP: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                return window.ET.MCMF.GetPlayerCombatData();
            
            return window.ET.MCMF.PlayerUnitData.stats.health;
        },
        
        PlayerHPMax: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                return window.ET.MCMF.GetPlayerCombatData();
            
            return window.ET.MCMF.PlayerUnitData.stats.healthMax;
        },
        
        PlayerEnergy: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                return window.ET.MCMF.GetPlayerCombatData();
            
            return window.ET.MCMF.PlayerUnitData.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");
                        //jQ("div#ET_meancloud_bootstrap").trigger("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;
        },

        GetPlayerCombatData: function()
        {
            let oCombatPlayerData;
            
            try
            {        
                window.ET.MCMF.CombatID = undefined;
            
                Meteor.connection._stores.combat._getCollection().find().fetch().forEach(function(oThisCombatUnit)
                {
                    if (oThisCombatUnit.owner === window.ET.MCMF.UserID)
                    {
                        oCombatPlayerData = oThisCombatUnit;
                        
                        window.ET.MCMF.CombatID = oCombatPlayerData._id;
                        
                        if (!window.ET.MCMF.PlayerInCombat())
                            window.ET.MCMF.PlayerUnitData = oCombatPlayerData;
                    }
                });        
            }
            catch (err) { }
            
            return oCombatPlayerData;
        },
        
        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()
        {
            return Meteor.connection._stores.adventures._getCollection().find().fetch()[0].adventures;
        },        
  
        GetChats: function()
        {
            return Meteor.connection._stores.simpleChats._getCollection().find().fetch();
        },

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

        // need a better way to check if the game has loaded basic data, but this is fine for now
        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.InitMeteorTriggers();
                window.ET.MCMF.Setup_remaining();
            }
        },

        Setup_SendDelayedInitializer: function()
        {
            try
            {
                jQ("div#ET_meancloud_bootstrap").trigger("ET:initialized");
                window.ET.MCMF.EventTrigger("ET:initialized");
                //if (window.ET.MCMF.WantDebug) window.ET.MCMF.Log("<-- triggering 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[window.ET.MCMF.UserID].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");
                    window.ET.MCMF.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);
////////////////////////////////////////////////////////////////