Greasy Fork is available in English.

Eternity Tower Stats UI

Adds stats to the UI for the Eternity Tower game

اعتبارا من 21-08-2018. شاهد أحدث إصدار.

// ==UserScript==
// @name          Eternity Tower Stats UI
// @icon          https://www.eternitytower.net/favicon.png
// @namespace     http://mean.cloud/
// @version       1.23
// @description   Adds stats to the UI for the Eternity Tower game
// @match         http*://*.eternitytower.net/*
// @author        psouza4@gmail.com
// @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
    {
        jQ.get("https://news.mediacentermaster.com/ETStats/", function(data)
        {
            try
            {
                // don't hold bad data
                if (data.indexOf("Level 50-54") === -1)
                    return;
                
                sETStatsDB = data;

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

                    var iRangeStart = 0;
                    var 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))
                            {
                                var temp_Percent = CDbl(ChopperBlank(rawPQLine, "", "%"));
                                var temp_Name    = ChopperBlank(rawPQLine, " - ", " (").trim();
                                
                                var oDataObj = { name: temp_Name, percent: temp_Percent };
                                
                                for (var 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));
                        }
                    });
                });            
                            
                var rawTowerData = ChopperBlank(sETStatsDB, "Tower F0R0", "</pre>").trim().split("Tower ");
                
                //ETStatsDB_Tower
                jQuery.makeArray(rawTowerData).forEach(function(rawTowerChunk)
                {
                    var iFloor = CInt(ChopperBlank(rawTowerChunk, "F", "R"));
                    var 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;
                            
                            var temp_Percent = CDbl(ChopperBlank(rawTowerLine, "", "%"));
                            var temp_Name    = ChopperBlank(rawTowerLine, " - ", " (").trim();
                            
                            var 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));
                        }
                    });
                });
                
                var 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 (var 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) { }
        });
    }
    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) { }

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

    ///////////////////////////////////////////////////////////////////////////////////////
    //
    //  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 (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)
{
	var shiftclickOrig = jQ.Event("click");
    shiftclickOrig.which = 1; // 1 = left, 2 = middle, 3 = right
    //shiftclickOrig.type = "click"; // "mousedown" ?
    shiftclickOrig.currentTarget = target;
	shiftclickOrig.shiftKey = true;

	var 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);
		var 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);
		var 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)
{
	var sIntermediate = "";

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

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

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

	var iIndexEnd = sIntermediate.indexOf(sEnd);

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

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

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

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

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

	var iIndexEnd = sIntermediate.indexOf(sEnd);

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

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

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

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

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

	var iIndexEnd = sIntermediate.indexOf(sEnd);

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

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

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

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.03)) // MeanCloud mod framework
{
    window.ET.MCMF =
    {
        version: 1.03,
        
        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 = [];

            var 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
            {
                var now_time = new Date();
                var 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)
        {
            var oChannel;

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

                var 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;
        },
        
        GetActiveTab: function()
        {
            var active_tab = "";
            
            try
            {
                active_tab = jQuery(jQuery("a.active").get(0)).text().trim().toLowerCase();
                
                if (active_tab.length === 0)
                    throw "Invalid active tab";
            }
            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("/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()
        {
            var active_tab_section = "";
            
            try
            {
                var 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)
                {
                    var cmd = arguments[0];
                    var 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
                    {
                        var 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)
                                                {
                                                    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) { }

                try
                {
                    oMeteorData = JSON.parse(sMeteorRawData);

                    if (oMeteorData.collection === "combat")
                    {
                        if ((oMeteorData.fields.owner === window.ET.MCMF.UserID) || (oMeteorData.id === window.ET.MCMF.CombatID))
                        {
                            if (window.ET.MCMF.PlayerInCombat())
                                window.ET.MCMF.PlayerUnitData = oMeteorData.fields;
                        }
                    }
                }
                catch (err) { }
            });
        },
        
        PlayerHP: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                window.ET.MCMF.GetPlayerCombatData();
            
            return window.ET.MCMF.PlayerUnitData.stats.health;
        },
        
        PlayerHPMax: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                window.ET.MCMF.GetPlayerCombatData();
            
            return window.ET.MCMF.PlayerUnitData.stats.healthMax;
        },
        
        PlayerEnergy: function()
        {
            if (!window.ET.MCMF.PlayerInCombat())
                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()
        {
            var 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()
        {
            var 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);
////////////////////////////////////////////////////////////////