+ Reply to Thread
Results 1 to 4 of 4
  1. #1
    Junior Member Online status: Polymnie is offline Reputation: Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte
    Join Date
    May 2011
    Posts
    34

    [Plugin] DreadMeter

    Displays current dread-level so that you don't have to mouse-over the icon on your mini-map ring during combat.

    How to use
    "/dreadmeter debug" - toggles whether you want updates sent to your chat tab saying telling when your dread level has changed.
    "/dreadmeter verbose" - toggles whether the dread window displays "95% Maximum Morale" etc.

    Six-legged chitinous features
    1. Window is bare-bones and configuration is hard-coded. Sorry!
    2. Only tested down to Dread: 14.
    3. Guessing hope can be ambiguous, particularly when buffed by a Captain's Motivating Speech.

    How it works - Dread
    When you get hit with a dread effect, a pair of events soon fire. First, MaxMorale goes down. Then BaseMaxMorale goes up. The plugin waits for BaseMaxMoraleChanged, then calculates MaxMorale / BaseMaxMorale and guesses dread from the result.

    It appears that dread updates occur about every 3 seconds, but hope effects cause an immediate update.

    How it works - Hope
    Hope can often be determined because a character's normal maximum morale is an integer (integer raw morale + integer morale from vitality), but GetBaseMaxMorale returns decimal fractions. If you iterate through the possible multipliers that could turn that value back into an integer, there's often only one solution.

    When not buffed by Motivating Speech, this works over 80% of the time. However, if your max morale at Hope:0 is a multiple of 20 or 25 (8% of numbers), the result is always ambiguous. Motivating Speech clouds the picture because it has 10 possible multipliers from the Captain's legendary legacy.

    Updates
    2012/04/17 Switched to a hook that Garan suggests is safer. Also, tested down to dread 14. No user-visible changes.
    2012/04/27 Added Hope guessing.
    2012/04/29 Lowered the morale gain from "Enhanced Abilities" buff to 50.

    Code:
    import "Turbine.Gameplay"
    import "Turbine.UI.Lotro"
    
    -- configuration
    WindowLeft = Turbine.UI.Display:GetWidth() - 410
    WindowTop = 1
    WindowWidth = 170
    Font1 = Turbine.UI.Lotro.Font.Verdana16
    Font1Height = 16
    Font2 = Turbine.UI.Lotro.Font.Verdana12
    Font2Height = 12
    debug = false
    verbose = true
    HopePriority = {1, 1.01, 1.02, 1.03, 1.04, 1.05} -- high to low priority for picking between ambiguous results
    MotivatedPriority = {1.05, 1.1, 1.06, 1.065, 1.07, 1.075, 1.08, 1.085, 1.09, 1.095}  -- captain legendary weapon legacy
    InspiredPriority = {4.5} -- what are the other possible values?
    
    -- window appearance
    DreadWindow = Turbine.UI.Window()
    DreadWindow:SetPosition(WindowLeft, WindowTop)
    DreadWindow:SetSize(WindowWidth, Font1Height + 5 * Font2Height)
    DreadWindow:SetMouseVisible(false)
    DreadWindow:SetVisible(true)
    
    DreadLabel = Turbine.UI.Label()
    DreadLabel:SetParent(DreadWindow)
    DreadLabel:SetSize(WindowWidth, Font1Height)
    DreadLabel:SetFont(Font1)
    DreadLabel:SetBackColor(Turbine.UI.Color(0.1, 0, 0, 0))
    DreadLabel:SetMouseVisible(false)
    
    DreadEffectLabel = Turbine.UI.Label()
    DreadEffectLabel:SetParent(DreadWindow)
    DreadEffectLabel:SetPosition(0, 16)
    DreadEffectLabel:SetSize(WindowWidth, 5 * Font2Height)
    DreadEffectLabel:SetFont(Font2)
    DreadEffectLabel:SetBackColor(Turbine.UI.Color(0.1, 0, 0, 0))
    DreadEffectLabel:SetMouseVisible(false)
    
    -- non-configuration
    MyChar = Turbine.Gameplay.LocalPlayer:GetInstance()
    MyAttributes = MyChar:GetAttributes()
    MyClass = MyChar:GetClass()
    MyEffects = MyChar:GetEffects()
    
    DreadMorale = {}
    DreadMorale[1] = 0
    DreadMorale[0.95] = 1
    DreadMorale[0.9] = 2
    DreadMorale[0.85] = 3
    DreadMorale[0.8] = 4
    DreadMorale[0.7] = 5
    DreadMorale[0.6] = 6
    DreadMorale[0.5] = 7
    DreadMorale[0.4] = 8
    DreadMorale[0.35] = 9
    DreadMorale[0.2] = 10
    DreadMorale[0.15] = 11
    DreadMorale[0.1] = 12
    DreadMorale[0.05] = 13
    DreadMorale[0.03] = 14
    DreadMorale[0.01] = 15
    
    DreadEffects = {}
    DreadEffects[0] = ""
    DreadEffects[1] = "95% Maximum Morale\n99% Healing Received\n101% Damage Received\n99% Damage Dealt"
    DreadEffects[2] = "90% Maximum Morale\n98% Healing Received\n102% Damage Received\n95% Damage Dealt"
    DreadEffects[3] = "85% Maximum Morale\n97% Healing Received\n103% Damage Received\n90% Damage Dealt\n-1 Skill Effect Level"
    DreadEffects[4] = "80% Maximum Morale\n96% Healing Received\n104% Damage Received\n85% Damage Dealt\n-2 Skill Effect Level"
    DreadEffects[5] = "70% Maximum Morale\n92% Healing Received\n106% Damage Received\n80% Damage Dealt\n-3 Skill Effect Level"
    DreadEffects[6] = "60% Maximum Morale\n90% Healing Received\n108% Damage Received\n75% Damage Dealt\n-4 Skill Effect Level"
    DreadEffects[7] = "50% Maximum Morale\n85% Healing Received\n110% Damage Received\n70% Damage Dealt\n-5 Skill Effect Level"
    DreadEffects[8] = "40% Maximum Morale\n75% Healing Received\n115% Damage Received\n65% Damage Dealt\n-6 Skill Effect Level"
    DreadEffects[9] = "35% Maximum Morale\n65% Healing Received\n120% Damage Received\n60% Damage Dealt\n-7 Skill Effect Level"
    DreadEffects[10] = "20% Maximum Morale\n55% Healing Received\n125% Damage Received\n55% Damage Dealt\n-8 Skill Effect Level"
    DreadEffects[11] = "15% Maximum Morale\n45% Healing Received\n135% Damage Received\n50% Damage Dealt\n-9 Skill Effect Level"
    DreadEffects[12] = "10% Maximum Morale\n35% Healing Received\n145% Damage Received\n45% Damage Dealt\n-10 Skill Effect Level"
    DreadEffects[13] = "5% Maximum Morale\n25% Healing Received\n155% Damage Received\n40% Damage Dealt\n-11 Skill Effect Level"
    DreadEffects[14] = "3% Maximum Morale\n15% Healing Received\n175% Damage Received\n35% Damage Dealt\n-12 Skill Effect Level"
    DreadEffects[15] = "1% Maximum Morale\n5% Healing Received\n200% Damage Received\n30% Damage Dealt\n-13 Skill Effect Level"
    DreadEffects["?"] = "dread calculation error"
    
    HopeMorale = {}
    HopeMorale[1] = 0
    HopeMorale[1.01] = 1
    HopeMorale[1.02] = 2
    HopeMorale[1.03] = 3
    HopeMorale[1.04] = 4
    HopeMorale[1.05] = "5+"
    
    HopeEffects = {}
    HopeEffects[0] = ""
    HopeEffects[1] = "+1% Maximum Morale\n101% Damage Dealt"
    HopeEffects[2] = "+2% Maximum Morale\n102% Damage Dealt"
    HopeEffects[3] = "+3% Maximum Morale\n103% Damage Dealt"
    HopeEffects[4] = "+4% Maximum Morale\n103% Damage Dealt"
    HopeEffects["5+"] = "+5% Maximum Morale\n103% Damage Dealt"
    HopeEffects["?"] = "hope calculation error"
    
    -- raw morale by level for character with 0 vitality
    -- calculated from Lorebook info, not trustworthy
    -- The Lorebook doesn't give raw morale.  It cooks in the vitality, and assumes 3 morale per vitality even for Guardians and Wardens.
    
    -- Lore-masters, Minstrels, Rune-keepers
    RawSquishy = {75, 97, 119, 141, 163, 185, 207, 229, 251, 273, 295, 317, 339, 361, 383, 405, 427, 449, 471, 493,
    	508, 523, 538, 553, 568, 583, 598, 613, 628, 643, 658, 673, 688, 703, 718, 733, 748, 763, 778, 793,
    	808, 823, 838, 853, 868, 883, 898, 913, 928, 943, 958, 973, 988, 1003, 1018, 1033, 1048, 1063, 1078,
    	1093, 1108, 1123, 1138, 1153, 1168, 1186, 1208, 1234, 1264, 1297, 1334, 1375, 1420, 1465, 1510}
    -- Burglars, Hunters
    RawMedium = {75, 104, 133, 162, 191, 220, 249, 278, 307, 336, 365, 394, 423, 452, 481, 510, 539, 568, 597, 626, 648,
    	670, 692, 714, 736, 758, 780, 802, 824, 846, 868, 890, 912, 934, 956, 978, 1000, 1022, 1044, 1066, 1088,
    	1110, 1132, 1154, 1176, 1198, 1220, 1242, 1264, 1286, 1308, 1330, 1352, 1374, 1396, 1418, 1440, 1462,
    	1484, 1506, 1528, 1550, 1572, 1594, 1616, 1643, 1676, 1714, 1758, 1807, 1862, 1922, 1988, 2054, 2120}
    -- Captains
    RawCap = {85, 119, 153, 187, 221, 255, 289, 323, 357, 391, 410, 444, 478, 512, 546, 580, 614, 648, 682, 716, 738,
    	760, 782, 804, 826, 848, 870, 892, 914, 936, 958, 980, 1002, 1024, 1046, 1068, 1090, 1112, 1134, 1156, 1178,
    	1200, 1222, 1244, 1266, 1288, 1310, 1332, 1354, 1376, 1398, 1420, 1442, 1464, 1486, 1508, 1530, 1552, 1574,
    	1596, 1618, 1640, 1662, 1684, 1706, 1733, 1766, 1804, 1848, 1897, 1952, 2012, 2078, 2144, 2210}
    -- Champions
    RawChamp = {100, 139, 178, 217, 256, 295, 334, 373, 412, 451, 475, 514, 553, 592, 631, 670, 709, 748, 787, 826,
    	853, 880, 907, 934, 961, 988, 1015, 1042, 1069, 1096, 1123, 1150, 1177, 1204, 1231, 1258, 1285, 1312, 1339,
    	1366, 1393, 1420, 1447, 1474, 1501, 1528, 1555, 1582, 1609, 1636, 1651, 1678, 1705, 1732, 1759, 1786, 1813,
    	1840, 1867, 1894, 1921, 1948, 1975, 2002, 2029, 2062, 2102, 2149, 2203, 2263, 2330, 2404, 2485, 2566, 2647}
    -- Guardians, Wardens
    RawTank = {100, 139, 178, 217, 256, 295, 334, 373, 412, 451, 490, 529, 568, 607, 646, 685, 724, 763, 802, 841, 868,
    	895, 922, 949, 976, 1003, 1030, 1057, 1084, 1111, 1138, 1165, 1192, 1219, 1246, 1273, 1300, 1327, 1354,
    	1381, 1408, 1435, 1462, 1489, 1516, 1543, 1570, 1597, 1624, 1651, 1678, 1705, 1732, 1759, 1786, 1813, 1840,
    	1867, 1894, 1921, 1948, 1975, 2002, 2029, 2056, 2089, 2129, 2176, 2230, 2290, 2357, 2431, 2512, 2593, 2674}
    
    
    -- event handling boilerplate
    function AddCallback(object, event, callback)
        if (object[event] == nil) then
            object[event] = callback
        else
            if (type(object[event]) == "table") then
                table.insert(object[event], callback)
            else
                object[event] = {object[event], callback}
            end
        end
        return callback
    end
    
    function RemoveCallback(object, event, callback)
        if (object[event] == callback) then
            object[event] = nil
        else
            if (type(object[event]) == "table") then
                local size = table.getn(object[event])
                for i = 1, size do
                    if (object[event][i] == callback) then
                        table.remove(object[event], i)
                        break
                    end
                end
            end
        end
    end
    
    -- rounding function
    function round(number, zeroes)
    	zeroes = zeroes or 0
    	return math.floor(number * (10 ^ zeroes) + .5) / (10 ^ zeroes)
    end
    
    -- calculate minimum possible maxmorale
    -- used as a sanity check for HopeGuess
    -- I'm not sure this is worth the effort given that it's unaware of raw morale bonuses from virtues, equipment and various buffs
    function MinimumMorale(bonus)
    	mult = 3
    	raw = 0
    
    	if MyClass == Turbine.Gameplay.Class.LoreMaster
    	or MyClass == Turbine.Gameplay.Class.Minstrel
    	or MyClass == Turbine.Gameplay.Class.RuneKeeper then
    		raw = RawSquishy[MyChar:GetLevel()]
    	elseif MyClass == Turbine.Gameplay.Class.Burglar
    	or MyClass == Turbine.Gameplay.Class.Hunter then
    		raw = RawMedium[MyChar:GetLevel()]
    	elseif MyClass == Turbine.Gameplay.Class.Captain then
    		raw = RawCap[MyChar:GetLevel()]
    	elseif MyClass == Turbine.Gameplay.Class.Champion then
    		raw = RawChamp[MyChar:GetLevel()]
    	elseif MyClass == Turbine.Gameplay.Class.Guardian
    	or MyClass == Turbine.Gameplay.Class.Warden then
    		raw = RawTank[MyChar:GetLevel()]
    		mult = 5
    	end
    
    	minimum = raw + bonus + mult * MyAttributes:GetVitality()
    
    	if debug then
    		Turbine.Shell.WriteLine(string.format("%s %+d +(%s * %s) = %s minimum", raw, bonus, mult, MyAttributes:GetVitality(), minimum))
    	end
    
    	return minimum
    end
    
    -- guess hope level
    -- often works, surprisingly
    -- if MaxMorale at Hope: 0 is a multiple of 20 or 25, the result is always ambiguous (8% of numbers)
    -- if MaxMorale at Hope: 0 is a multiple of 34, 35, 51, 52, 101, 103, the result is sometimes ambiguous (an additional ~8.3% of numbers)
    function HopeGuess(max)
    	CurrentHope = "?"
    	ambiguous = false
    	InspiredSet = {1}
    	MotivatedSet = {1}
    
    	BonusMorale = 0
    	if MyChar:GetRace() == Turbine.Gameplay.Race.Elf then
    		BonusMorale = -20
    	end
    
    	for i = 1, MyEffects:GetCount() do
    		effect = MyEffects:Get(i)
    		effectName = effect:GetName()
    		if effectName == "Inspired Greatness" then
    			InspiredSet = InspiredPriority
    		elseif effectName == "Motivated" then
    			MotivatedSet = MotivatedPriority
    		elseif effectName == "Enhanced Abilities" then
    			BonusMorale = BonusMorale + 50
    		end
    	end
    
    	minimum = MinimumMorale(BonusMorale)
    
    	for i = 1, #InspiredSet do
    		for j = 1, #MotivatedSet do
    			for k = 1, #HopePriority do
    				ratio = max / (InspiredSet[i] * MotivatedSet[j] * HopePriority[k])
    				rounded = round(ratio, 3)
    				if rounded == math.floor(rounded) and rounded >= minimum then
    					if CurrentHope == "?" then
    						CurrentHope = HopeMorale[HopePriority[k]]
    					else
    						ambiguous = true
    					end
    					if debug then
    						Turbine.Shell.WriteLine(string.format("?Hope: %s (%s * %s * %s * %s = %s)", HopeMorale[HopePriority[k]], rounded, InspiredSet[i], MotivatedSet[j], HopePriority[k], max))
    					end
    				end
    			end
    		end
    	end
    
    	if ambiguous then
    		DreadLabel:SetText("Hope: "..CurrentHope.." ?")
    		DreadEffectLabel:SetText("ambiguous\n"..HopeEffects[CurrentHope])
    	else
    		DreadLabel:SetText("Hope: "..CurrentHope)
    		DreadEffectLabel:SetText(HopeEffects[CurrentHope])
    	end
    end
    
    -- calculate dread level and update display
    -- calls HopeGuess when dread = 0
    function UpdateDread(sender, args)
    	max = MyChar:GetMaxMorale()
    	base = MyChar:GetBaseMaxMorale()
    	ratio = max / base
    	current = DreadMorale[round(ratio, 3)] or "?"
    	if debug then
    		date = Turbine.Engine.GetDate()
    		output = string.format("%02d:%02d:%02d Dread: %s (%s / %s = %s)", date.Hour, date.Minute, date.Second, current, max, base, ratio)
    		Turbine.Shell.WriteLine(output)
    	end
    	if current ~= 0 then
    		DreadLabel:SetForeColor(Turbine.UI.Color(1, 0, 0))
    		DreadLabel:SetText("Dread: "..current)
    		DreadEffectLabel:SetText(DreadEffects[current])
    	else
    		DreadLabel:SetForeColor(Turbine.UI.Color(0, 1, 0))
    		HopeGuess(base)
    	end
    end
    AddCallback(MyChar, "BaseMaxMoraleChanged", UpdateDread)
    
    -- command-line processing
    CommandLine = Turbine.ShellCommand()
    CommandLine.Execute = function(sender, cmd, args)
    	if args == "debug" then
    		if debug then
    			debug = false
    			Turbine.Shell.WriteLine("DreadMeter debugging off.")
    		else
    			debug = true
    			Turbine.Shell.WriteLine("DreadMeter debugging on.")
    		end
    	elseif args == "verbose" then
    		if DreadEffectLabel:IsVisible() then
    			DreadEffectLabel:SetVisible(false)
    			Turbine.Shell.WriteLine("DreadMeter verbose mode off.")
    		else
    			DreadEffectLabel:SetVisible(true)
    			Turbine.Shell.WriteLine("DreadMeter verbose mode on.")
    		end
    	else
    		Turbine.Shell.WriteLine("Toggle debugging: /dreadmeter debug\nToggle verbosity: /dreadmeter verbose")
    	end
    	UpdateDread()
    end
    Turbine.Shell.AddCommand("DreadMeter", CommandLine)
    
    -- create a dummy window, and update until loaded, at which point set loaded=true and set the unload commands.
    local loaded = false
    tmpWindow = Turbine.UI.Window()
    tmpWindow.Update = function()
    	if Plugins["DreadMeter"] ~= nil and not loaded then
    		loaded = true
    		Plugins["DreadMeter"].Unload = function(self, sender, args)
    			RemoveCallback(MyChar, "BaseMaxMoraleChanged", UpdateDread)
    			Turbine.Shell.RemoveCommand(CommandLine)
    		end
    		tmpWindow:SetWantsUpdates(false)
    		Turbine.Shell.WriteLine("DreadMeter plugin loaded.  Hello, "..MyChar:GetName().."!")
    		UpdateDread()
    	end
    end
    tmpWindow:SetWantsUpdates(true)
    Last edited by Polymnie; Apr 29 2012 at 05:21 PM.

  2. #2
    Poster of Note Online status: Garan is offline Reputation: Garan has disabled reputation
    Join Date
    Mar 2007
    Posts
    837

    Re: [Plugin] DreadMeter

    One bit of advice, you shouldn't attach event handlers to Turbine.Gameplay.LocalPlayer, rather you should always get an instance of the local player and attach event handlers to your instance.

    AddCallback(Turbine.Gameplay.L ocalPlayer, "BaseMaxMoraleChanged", UpdateDread)

    should be:

    localPlayer=Turbine.Gameplay.L ocalPlayer:GetInstance(); -- get an instance of the local player instance
    AddCallback(localPlayer, "BaseMaxMoraleChanged", UpdateDread);

    you can then use the localPlayer variable to remove the callback in your Unload handler:

    RemoveCallback(localPlayer, "BaseMaxMoraleChanged", UpdateDread);

    We've seen cases with other objects where assigning the event handler to the global object rather than your instance can disrupt other plugins. IIRC, the other case involved someone accidentally assigning their unload event handler to Turbine.Plugin.Unload instead of their own plugin instance. I suspect the LocalPlayer object would have similar issues.
    Last edited by Garan; Apr 17 2012 at 09:43 PM. Reason: typo
    Gnashtooth - Rank 10 Warg - My breath's worse than my bite - but what d'ya want? I eat Hobbitsess fer cryin' out loud
    Garan - Captain of little note - got parked at a Fell Scrying Pool so long it dried up and blew away
    and many, many others...
    "No, no, the hamsters are for the forums. The servers run on chinchillas!"-Patience 7/20/2007

  3. #3
    Junior Member Online status: Polymnie is offline Reputation: Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte
    Join Date
    May 2011
    Posts
    34

    Re: [Plugin] DreadMeter

    Quote Originally Posted by Garan View Post
    One bit of advice, you shouldn't attach event handlers to Turbine.Gameplay.LocalPlayer, rather you should always get an instance of the local player and attach event handlers to your instance.
    Thanks, fixed. I had noticed I could do it either way, and went with the way I did on the "why not?" theory. I'll defer to your experience.

  4. #4
    Junior Member Online status: Polymnie is offline Reputation: Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte Polymnie the Neophyte
    Join Date
    May 2011
    Posts
    34

    Re: [Plugin] DreadMeter

    Added Hope guessing, which "often" works.

+ Reply to Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts