-- Singleton object for efficiently observing and keeping track of game state,
-- providing convenient functions and events.
local Watcher = {};
AddCallback(Turbine.Plugin, "Unload", function()
--Puts("Saving...");
-- Workaround for Turbine localization bug.
local saveData = ExportTable(Watcher.settings);
Turbine.PluginData.Save(Turbine.DataScope.Account, "Thurallor_GameInfo", saveData, function()
--Puts("Save complete.");
end);
end);
local startupTime = Turbine.Engine.GetGameTime();
local function DoDelayedCallback(_time, event, args)
if (not Watcher.delayedCallbacks) then
Watcher.delayedCallbacks = {};
end
table.insert(Watcher.delayedCallbacks, { time = _time, event = event, args = args } );
Watcher.window:SetWantsUpdates(true);
end
local function AddActorEffect(effectsObject, effect)
local categories, name, curable = effect:GetCategory(), effect:GetName(), effect:IsCurable();
-- Keep track of how many instances of this effect are currently on the character
if (not effectsObject.cache[name]) then
effectsObject.cache[name] = 0;
end
effectsObject.cache[name] = effectsObject.cache[name] + 1;
-- Add the effect to "effect list" pulldown menus
local knownEffects = Watcher.settings.knownEffects;
if (not knownEffects[name]) then
-- This is an effect we have never seen before. Learn some info about it and save for future reference.
knownEffects[name] = { categories, effect:GetIcon(), curable, effect:IsDebuff()};
end
-- Create efficient functions for bitwise category tracking
local bits = categories;
if (not Watcher.AddCategories[bits]) then
-- This is a GetCategory value we have never seen before. Create functions for adding/removing.
local AddCategories = "local categories = ...; ";
local RemoveCategories = "local categories = ...; ";
if (bits >= 2048) then
-- Unknown bits; discard them.
bits = bits % 2048;
end
if (bits >= 1226) then
-- Turbine.Gameplay.EffectCategory.Dispellable is the OR of several bits
AddCategories = AddCategories .. "categories[1226] = categories[1226] + 1; ";
RemoveCategories = RemoveCategories .. "categories[1226] = categories[1226] - 1; ";
end
if (bits >= 1024) then
AddCategories = AddCategories .. "categories[1024] = categories[1024] + 1; ";
RemoveCategories = RemoveCategories .. "categories[1024] = categories[1024] - 1; ";
bits = bits - 1024;
end
if (bits >= 512) then
AddCategories = AddCategories .. "categories[512] = categories[512] + 1; ";
RemoveCategories = RemoveCategories .. "categories[512] = categories[512] - 1; ";
bits = bits - 512;
end
if (bits >= 256) then
AddCategories = AddCategories .. "categories[256] = categories[256] + 1; ";
RemoveCategories = RemoveCategories .. "categories[256] = categories[256] - 1; ";
bits = bits - 256;
end
if (bits >= 128) then
AddCategories = AddCategories .. "categories[128] = categories[128] + 1; ";
RemoveCategories = RemoveCategories .. "categories[128] = categories[128] - 1; ";
bits = bits - 128;
end
if (bits >= 64) then
AddCategories = AddCategories .. "categories[64] = categories[64] + 1; ";
RemoveCategories = RemoveCategories .. "categories[64] = categories[64] - 1; ";
bits = bits - 64;
end
if (bits >= 32) then
AddCategories = AddCategories .. "categories[32] = categories[32] + 1; ";
RemoveCategories = RemoveCategories .. "categories[32] = categories[32] - 1; ";
bits = bits - 32;
end
if (bits >= 16) then
AddCategories = AddCategories .. "categories[16] = categories[16] + 1; ";
RemoveCategories = RemoveCategories .. "categories[16] = categories[16] - 1; ";
bits = bits - 16;
end
if (bits >= 8) then
AddCategories = AddCategories .. "categories[8] = categories[8] + 1; ";
RemoveCategories = RemoveCategories .. "categories[8] = categories[8] - 1; ";
bits = bits - 8;
end
if (bits >= 4) then
AddCategories = AddCategories .. "categories[4] = categories[4] + 1; ";
RemoveCategories = RemoveCategories .. "categories[4] = categories[4] - 1; ";
bits = bits - 4;
end
if (bits >= 2) then
AddCategories = AddCategories .. "categories[2] = categories[2] + 1; ";
RemoveCategories = RemoveCategories .. "categories[2] = categories[2] - 1; ";
bits = bits - 2;
end
if (bits >= 1) then
AddCategories = AddCategories .. "categories[1] = categories[1] + 1; ";
RemoveCategories = RemoveCategories .. "categories[1] = categories[1] - 1; ";
end
Watcher.settings.funcs.AddCategories[categories] = AddCategories;
Watcher.AddCategories[categories] = loadstring(AddCategories);
Watcher.settings.funcs.RemoveCategories[categories] = RemoveCategories;
Watcher.RemoveCategories[categories] = loadstring(RemoveCategories);
end
-- Increment/decrement bitwise category trackers
Watcher.AddCategories[categories](effectsObject.activeCategories);
if (curable) then
Watcher.AddCategories[categories](effectsObject.activeCurableCategories);
end
end
local function RemoveActorEffect(effectsObject, effect)
local categories, name, curable = effect:GetCategory(), effect:GetName(), effect:IsCurable();
-- Increment/decrement bitwise category trackers
Watcher.RemoveCategories[categories](effectsObject.activeCategories);
if (curable) then
Watcher.RemoveCategories[categories](effectsObject.activeCurableCategories);
end
-- Keep track of how many instances of this effect are currently on the character
effectsObject.cache[name] = effectsObject.cache[name] - 1;
if (effectsObject.cache[name] == 0) then
effectsObject.cache[name] = nil;
end
end
local function GetActorEffects(effectsObject)
effectsObject.cache = {};
effectsObject.activeCategories = {
[Turbine.Gameplay.EffectCategory.Dispellable] = 0;
[Turbine.Gameplay.EffectCategory.Corruption] = 0;
[Turbine.Gameplay.EffectCategory.Elemental] = 0;
[Turbine.Gameplay.EffectCategory.Tactical] = 0;
[Turbine.Gameplay.EffectCategory.Poison] = 0;
[Turbine.Gameplay.EffectCategory.Fear] = 0;
[Turbine.Gameplay.EffectCategory.Song] = 0;
[Turbine.Gameplay.EffectCategory.Cry] = 0;
[Turbine.Gameplay.EffectCategory.Wound] = 0;
[Turbine.Gameplay.EffectCategory.Physical] = 0;
[Turbine.Gameplay.EffectCategory.Disease] = 0;
[1] = 0;
};
effectsObject.activeCurableCategories = {
[Turbine.Gameplay.EffectCategory.Dispellable] = 0;
[Turbine.Gameplay.EffectCategory.Corruption] = 0;
[Turbine.Gameplay.EffectCategory.Elemental] = 0;
[Turbine.Gameplay.EffectCategory.Tactical] = 0;
[Turbine.Gameplay.EffectCategory.Poison] = 0;
[Turbine.Gameplay.EffectCategory.Fear] = 0;
[Turbine.Gameplay.EffectCategory.Song] = 0;
[Turbine.Gameplay.EffectCategory.Cry] = 0;
[Turbine.Gameplay.EffectCategory.Wound] = 0;
[Turbine.Gameplay.EffectCategory.Physical] = 0;
[Turbine.Gameplay.EffectCategory.Disease] = 0;
[1] = 0;
};
for e = 1, effectsObject:GetCount(), 1 do
local effect = effectsObject:Get(e);
AddActorEffect(effectsObject, effect);
end
end
local function GetEquipment()
local eq = Watcher.playerEquipmentObject;
eq.cache = { byName = {}; bySlot = {}; };
for slot = 1, eq:GetSize() do
local item = eq:GetItem(slot);
if (item) then
local name = item:GetName();
eq.cache.byName[name] = { item, slot };
eq.cache.bySlot[slot] = { item, name };
end
end
end
local function GetBackpack()
local bp = Watcher.playerBackpackObject;
bp.cache = { byName = {}; bySlot = {}; };
for slot = 1, bp:GetSize() do
local item = bp:GetItem(slot);
if (item) then
local name = item:GetName();
if (not bp.cache.byName[name]) then
bp.cache.byName[name] = { slot };
else
table.insert(bp.cache.byName[name], slot);
end
bp.cache.bySlot[slot] = { item, name };
end
end
return bp.cache;
end
local function ResetTimeChanged(skill, args)
DoCallbacks(self, "ResetTimeChanged", args);
end
local function StanceChanged()
local stance = Watcher.playerClassAttributes:GetStance();
if (stance ~= Watcher.stance) then
-- Lua API bug workaround: If invalid stance reported, keep the previous value.
local validStance = {
[Turbine.Gameplay.Class.Warden] = {
[Turbine.Gameplay.Attributes.WardenStance.Assailment] = true;
[Turbine.Gameplay.Attributes.WardenStance.Determination] = true;
};
[Turbine.Gameplay.Class.Hunter] = {
[Turbine.Gameplay.Attributes.HunterStance.Endurance] = true;
[Turbine.Gameplay.Attributes.HunterStance.Precision] = true;
[Turbine.Gameplay.Attributes.HunterStance.Strength] = true;
};
[Turbine.Gameplay.Class.Minstrel] = {
[Turbine.Gameplay.Attributes.MinstrelStance.Melody] = true;
[Turbine.Gameplay.Attributes.MinstrelStance.Dissonance] = true;
[Turbine.Gameplay.Attributes.MinstrelStance.Resonance] = true;
};
};
if (validStance[Watcher.playerObject:GetClass()][stance]) then
Watcher.stance = stance;
DoCallbacks(Watcher, "PlayerStanceChanged", { Stance = stance });
end
end
end
local function GetSkills(skills)
skills.cache = { names = {}; byName = {}; byIcon = {} };
local cache = skills.cache;
for c = 1, skills:GetCount(), 1 do
local skill = skills:GetItem(c);
local skillInfo = skill:GetSkillInfo();
local icon = skillInfo:GetIconImageID();
local name = skillInfo:GetName();
cache.byName[name] = skill;
cache.byIcon[icon] = skill;
table.insert(cache.names, name);
end
table.sort(cache.names);
return skills.cache;
end
local function GetGambits()
local gambits = Watcher.playerTrainedGambits;
gambits.cache = { names = {}; byName = {}; byIcon = {} };
if (Watcher.playerObject:GetClass() == Turbine.Gameplay.Class.Warden) then
for c = 1, gambits:GetCount(), 1 do
local skill = gambits:GetItem(c);
local skillInfo = skill:GetSkillInfo();
local icon = skillInfo:GetIconImageID();
local name = skillInfo:GetName();
gambits.cache.byName[name] = skill;
gambits.cache.byIcon[icon] = skill;
end
end
return gambits.cache;
end
local function GetParty()
local party = Watcher.playerPartyObject;
party.cache = { byName = {}; byNameUpper = {}; };
for c = 1, party:GetMemberCount(), 1 do
local member = party:GetMember(c);
local name = member:GetName();
party.cache.byName[name] = member;
party.cache.byNameUpper[string.upper(name)] = member;
member.num = c;
end
return party.cache;
end
local function PlayerEffectAdded(effectsObject, args)
local effect = effectsObject:Get(args.Index);
AddActorEffect(effectsObject, effect);
DoCallbacks(Watcher, "PlayerEffectAdded", effect);
end
local function PlayerEffectRemoved(effectsObject, args)
RemoveActorEffect(effectsObject, args.Effect);
DoCallbacks(Watcher, "PlayerEffectRemoved", args.Effect);
end
local function PlayerEffectsCleared(effectsObject, args)
GetActorEffects(effectsObject);
DoCallbacks(Watcher, "PlayerEffectsCleared", args);
end
local function TargetEffectAdded(effectsObject, args)
local effect = effectsObject:Get(args.Index);
AddActorEffect(effectsObject, effect);
DoCallbacks(Watcher, "TargetEffectAdded", effect);
end
function TargetEffectRemoved(effectsObject, args)
RemoveActorEffect(effectsObject, args.Effect);
DoCallbacks(Watcher, "TargetEffectRemoved", args.Effect);
end
local function TargetEffectsCleared(effectsObject, args)
GetActorEffects(effectsObject);
DoCallbacks(Watcher, "TargetEffectsCleared", args);
end
local function PartyMemberAdded(sender, args)
-- Event does not work if you're in a raid (only a single fellowship)
-- Event doesn't fire when the party is created (initial two members)
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
end
local function PartyMemberRemoved(sender, args)
-- Event does not work if you're in a raid (only a single fellowship)
-- Event doesn't fire when the party is disbanded
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
end
local function PartyAssistTargetAdded(sender, args)
-- Event does not work if you're in a raid (only a single fellowship)
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
end
local function PartyAssistTargetRemoved(sender, args)
-- Event does not work if you're in a raid (only a single fellowship)
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
end
local function PlayerPartyChanged(sender, args)
Watcher.playerPartyObject = Watcher.playerObject:GetParty();
if (Watcher.playerPartyObject) then
AddCallback(Watcher.playerPartyObject, "MemberAdded", PartyMemberAdded);
AddCallback(Watcher.playerPartyObject, "MemberRemoved", PartyMemberRemoved);
AddCallback(Watcher.playerPartyObject, "AssistTargetAdded", PartyAssistTargetAdded);
AddCallback(Watcher.playerPartyObject, "AssistTargetRemoved", PartyAssistTargetRemoved);
GetParty();
end
end
local function UpdatePlayerTarget()
Watcher.targetIsPartyMember = nil;
local target = Watcher.targetObject;
if (target and target.GetBaseMaxMorale) then
local targetName, targetMorale = target:GetName(), target:GetBaseMaxMorale();
local pet = Watcher.playerObject:GetPet();
local party = Watcher.playerPartyObject;
-- Detect if the new target is the player's self. If so, use the full LocalPlayer object
-- instead of the Actor object returned by GetTarget().
if (target:IsLocalPlayer()) then
Watcher.targetObject = Watcher.playerObject;
-- Detect if the new target is the player's pet. If so, use the full Pet object instead of the
-- Actor object returned by GetTarget().
elseif (pet and (targetName == pet:GetName()) and (targetMorale == pet:GetBaseMaxMorale())) then
Watcher.targetObject = pet;
-- Detect if the new target is a member of the player's party. If so, use the full Player object
-- instead of the Actor object returned by GetTarget().
elseif (party) then
local member = party.cache.byName[targetName];
if (member and (targetMorale == member:GetBaseMaxMorale())) then
Watcher.targetIsPartyMember = member;
Watcher.targetObject = member;
-- Also save this for Watcher.GetNextPartyMember():
Watcher.prevPartyMember = member;
end
end
end
-- Need to get the effects object now so it can start observing effects.
-- Target effects currently are too buggy to use.
--[[
if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
end
]]
DoCallbacks(Watcher, "PlayerTargetChanged", Watcher.targetObject);
end
local function TargetsTargetChanged()
Watcher.targetsTargetObject = nil;
DoCallbacks(Watcher, "TargetsTargetChanged", Watcher.targetObject);
end
local function PlayerTargetChanged()
if (Watcher.targetObject) then
RemoveCallback(Watcher.targetObject, "TargetChanged", TargetsTargetChanged);
end
Watcher.targetObject = Watcher.playerObject:GetTarget();
Watcher.targetEffectsObject = nil;
Watcher.targetsTargetObject = nil;
if (Watcher.targetObject) then
AddCallback(Watcher.targetObject, "TargetChanged", TargetsTargetChanged);
end
-- Client may not immediately have information about the target. When
-- the info arrives, we'll get a BaseMaxPowerChanged event, which seems
-- to occur near the end of the burst of events that will arrive.
if (Watcher.targetObject and Watcher.targetObject.GetBaseMaxPower and (Watcher.targetObject:GetBaseMaxPower() == 0)) then
Watcher.targetObject.BaseMaxPowerChanged = function()
Watcher.targetObject.BaseMaxPowerChanged = nil;
Watcher.targetObject = Watcher.playerObject:GetTarget();
Watcher.targetEffectsObject = nil;
UpdatePlayerTarget();
end
end
UpdatePlayerTarget();
end
local function ItemEquipped(sender, args)
--Puts("ItemEquipped: args = " .. Serialize(args));
local slot, item, name = args.Index, args.Item, args.Item:GetName();
local byName = Watcher.playerEquipmentObject.cache.byName;
if (byName[name] == nil) then
byName[name] = {};
end
table.insert(byName[name], item);
table.insert(byName[name], slot);
Watcher.playerEquipmentObject.cache.bySlot[slot] = { item, name };
DoCallbacks(Watcher, "ItemEquipped", args);
end
local function ItemUnequipped(sender, args)
--Puts("ItemUnequipped: args = " .. Serialize(args));
local slot, name = args.Index, args.Item:GetName();
Watcher.playerEquipmentObject.cache.bySlot[slot] = nil;
local byName = Watcher.playerEquipmentObject.cache.byName;
if (byName[name]) then
table.remove(byName[name], 1);
table.remove(byName[name], 1);
if (#byName[name] == 0) then
byName[name] = nil;
end
end
DoCallbacks(Watcher, "ItemUnequipped", args);
end
local function ItemAdded(sender, args)
--Puts("ItemAdded: args = " .. Serialize(args));
-- occurs out-of-combat; efficiency not important
Watcher.playerBackpackObject.cache = nil;
end
local function ItemRemoved(sender, args)
--Puts("ItemRemoved: args = " .. Serialize(args));
-- occurs out-of-combat; efficiency not important
Watcher.playerBackpackObject.cache = nil;
end
local function ItemMoved(sender, args)
-- occurs out-of-combat; efficiency not important
Watcher.playerBackpackObject.cache = nil;
DoCallbacks(Watcher, "ItemMoved", args);
end
local function BackpackSizeChanged(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
Watcher.playerBackpackObject.cache = nil;
end
local function ChatReceived(sender, args)
if (args.ChatType == Turbine.ChatType.Death) then
if (string.match(args.Message, Watcher.PlayerIncapacitatedStr[1]) or
string.match(args.Message, Watcher.PlayerIncapacitatedStr[2])) then
DoCallbacks(Watcher, "PlayerIncapacitated");
elseif (string.match(args.Message, Watcher.PlayerRevivedStr[1]) or
string.match(args.Message, Watcher.PlayerRevivedStr[2])) then
DoCallbacks(Watcher, "PlayerRevived");
end
elseif (args.ChatType == Turbine.ChatType.Advancement) then
if (string.match(args.Message, Watcher.SkillsChangedStr)) then
-- Trait line changed. Reread skills lists.
Watcher.playerTrainedSkills.cache = nil;
Watcher.playerUntrainedSkills.cache = nil;
elseif (string.match(args.Message, Watcher.TraitTreeChangedStr)) then
DoDelayedCallback(Turbine.Engine.GetGameTime() + 1.5, "TraitTreeChanged", args);
end
end
end
local function SkillAdded(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetSkills(Watcher.playerTrainedSkills);
GetSkills(Watcher.playerUntrainedSkills);
end
local function SkillRemoved(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetSkills(Watcher.playerTrainedSkills);
GetSkills(Watcher.playerUntrainedSkills);
end
local function InCombatChanged()
local inCombat = Watcher.playerObject:IsInCombat();
if (inCombat) then
collectgarbage("stop");
else
collectgarbage("restart");
end
DoCallbacks(Watcher, "InCombatChanged", { InCombat = inCombat });
end
local function LoadSettings()
--Puts("Loading...");
Turbine.PluginData.Load(Turbine.DataScope.Account, "Thurallor_GameInfo", function(loadData, args)
if (not loadData) then
return;
end
-- Workaround for Turbine localization bug.
local settings = ImportTable(loadData);
if (not settings) then
return;
end
-- Previously-saved settings override the defaults
DeepTableCopy(settings, Watcher.settings);
-- Compile loaded functions
for categories, sourceCode in pairs(settings.funcs.AddCategories) do
Watcher.AddCategories[categories] = loadstring(sourceCode);
end
for categories, sourceCode in pairs(settings.funcs.RemoveCategories) do
Watcher.RemoveCategories[categories] = loadstring(sourceCode);
end
-- Lua API bug workaround: Target effects object will not contain valid
-- data until a few ticks after plugin load.
if (Watcher.targetEffectsObject) then
Watcher.targetEffectsObject.cache = nil;
end
--Puts("Load complete.");
end);
end
function Watcher.GetTrainedSkillsInfo()
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerTrainedSkills);
end
return cache;
end
function Watcher.GetUntrainedSkillsInfo()
local cache = Watcher.playerUntrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerUntrainedSkills);
end
return cache;
end
function Watcher.GetSkillByName(name)
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerTrainedSkills);
end
local skill = cache.byName[name];
if (skill) then
return skill;
end
local cache = Watcher.playerUntrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerUntrainedSkills);
end
return cache.byName[name];
end
-- accepts a list of names (aliases for the same skill)
function Watcher.GetSkillByNames(names)
for _, name in pairs(names) do
local skill = Watcher.GetSkillByName(name);
if (skill) then
return skill;
end
end
end
function Watcher.GetSkillsInfo(trainedSkillsFirst)
local trainedSkillsCache = Watcher.GetTrainedSkillsInfo();
local untrainedSkillsCache = Watcher.GetUntrainedSkillsInfo();
if (#untrainedSkillsCache.names > 0) then
local cache = { names = {}; byName = {}; byIcon = {} };
for c in values({ trainedSkillsCache, untrainedSkillsCache }) do
for k, v in pairs(c.byIcon) do
cache.byIcon[k] = v;
end
for k, v in pairs(c.byName) do
cache.byName[k] = v;
end
for k, v in pairs(c.names) do
table.insert(cache.names, v);
end
end
if (not trainedSkillsFirst) then
table.sort(cache.names);
end
return cache;
else
return trainedSkillsCache;
end
end
function Watcher.GetGambitsInfo()
local cache = Watcher.playerTrainedGambits.cache;
if (not cache) then
cache = GetGambits();
end
for name in sorted_keys(cache.byName) do
table.insert(cache.names, name);
end
return cache;
end
function Watcher.GetKnownEffectNames()
local names = {};
for name in sorted_keys(Watcher.settings.knownEffects) do
table.insert(names, name);
end
return names;
end
-- If itemName is specified, the equipped item (if any) with the specified name is returned.
-- If itemSlot is specified, the equipped item (if any) at the specified slot is returned.
-- If neither is specified, a random equipped item (if any) is returned.
-- Returns slot, item (or nil).
function Watcher.GetEquippedItem(itemName, itemSlot)
if ((not itemSlot) and (not itemName)) then
for name, byName in pairs(Watcher.playerEquipmentObject.cache.byName) do
local item, slot = unpack(byName);
return slot, item;
end
elseif (not itemSlot) then
local byName = Watcher.playerEquipmentObject.cache.byName[itemName];
if (byName) then
local item, slot = unpack(byName);
return slot, item;
end
elseif (not itemName) then
local bySlot = Watcher.playerEquipmentObject.cache.bySlot[itemSlot];
if (bySlot) then
local item, name = unpack(bySlot);
return itemSlot, item;
end
else -- itemName and itemSlot specified
local bySlot = Watcher.playerEquipmentObject.cache.bySlot[itemSlot];
if (bySlot) then
local item, name = unpack(bySlot);
if (name == itemName) then
return itemSlot, item;
end
end
end
end
function Watcher.GetItemQuantity(itemName)
local quantity = 0;
local cache = Watcher.playerBackpackObject.cache or GetBackpack();
local slots = cache.byName[itemName];
if (slots) then
for i, s in pairs(slots) do
local item = Watcher.playerBackpackObject:GetItem(s);
if (item) then
quantity = quantity + item:GetQuantity();
end
end
end
return quantity;
end
-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillReady(arg)
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerTrainedSkills);
end
if (type(arg) == "string") then
skill = cache.byName[arg];
elseif (type(arg) == "number") then
skill = cache.byIcon[arg];
end
-- return (skill and skill:IsUsable() and (skill:GetResetTime() == -1));
return (skill and (skill:GetResetTime() == -1));
end
-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillUsable(arg)
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerTrainedSkills);
end
if (type(arg) == "string") then
skill = cache.byName[arg];
elseif (type(arg) == "number") then
skill = cache.byIcon[arg];
end
return (skill and skill:IsUsable());
end
-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillTrained(arg)
local cache, skill = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills(Watcher.playerTrainedSkills);
end
if (type(arg) == "string") then
skill = cache.byName[arg];
elseif (type(arg) == "number") then
skill = cache.byIcon[arg];
end
return (skill ~= nil);
end
local function WatchPlayerEffects()
-- Start watching player effects.
Watcher.playerEffectsObject = Watcher.playerObject:GetEffects();
AddCallback(Watcher.playerEffectsObject, "EffectAdded", PlayerEffectAdded);
AddCallback(Watcher.playerEffectsObject, "EffectRemoved", PlayerEffectRemoved);
AddCallback(Watcher.playerEffectsObject, "EffectsCleared", PlayerEffectsCleared);
GetActorEffects(Watcher.playerEffectsObject);
end
-- Returns whether effects of the specified category exist on the player.
-- If 'curable' is true, then this function returns whether there are curable effects of the specified category.
function Watcher.PlayerHasEffectCategory(category, curable)
if (not Watcher.playerEffectsObject) then
WatchPlayerEffects();
end
if (category) then
if (curable) then
return Watcher.playerEffectsObject.activeCurableCategories[category] > 0;
else
return Watcher.playerEffectsObject.activeCategories[category] > 0;
end
end
end
function Watcher.PlayerHasEffect(name)
if (not Watcher.playerEffectsObject) then
WatchPlayerEffects();
end
if (name) then
return Watcher.playerEffectsObject.cache[name];
end
end
local function WatchTargetEffects()
-- Start watching target effects.
if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
AddCallback(Watcher.targetEffectsObject, "EffectAdded", TargetEffectAdded);
AddCallback(Watcher.targetEffectsObject, "EffectRemoved", TargetEffectRemoved);
AddCallback(Watcher.targetEffectsObject, "EffectsCleared", TargetEffectsCleared);
GetActorEffects(Watcher.targetEffectsObject);
end
end
function Watcher.TargetHasEffectCategory(category)
if (not Watcher.targetEffectsObject) then
WatchTargetEffects();
end
if (Watcher.targetEffectsObject) then
return Watcher.targetEffectsObject.activeCategories[category] > 0;
end
end
function Watcher.TargetHasEffect(name)
if (not Watcher.targetEffectsObject) then
WatchTargetEffects();
end
if (Watcher.targetEffectsObject) then
return Watcher.targetEffectsObject.cache[name];
end
end
function Watcher.TargetIsPartyMember()
return Watcher.targetIsPartyMember;
end
function Watcher.GetStance()
return Watcher.stance;
end
function Watcher.GetParty()
return Watcher.playerPartyObject;
end
function Watcher.GetPartyMemberByName(name, ignoreCase)
if (Watcher.playerPartyObject) then
-- Bug: MemberAdded, MemberRemoved events don't fire for raids (only fellowships)
-- Therefore we have to reread the object every time, because we don't know if it's changed.
GetParty(); -- workaround
if (ignoreCase) then
return Watcher.playerPartyObject.cache.byNameUpper[string.upper(name)];
else
return Watcher.playerPartyObject.cache.byName[name];
end
end
end
function Watcher.GetPartyAssistTarget(num)
local party = Watcher.playerPartyObject;
if (party and (num <= party:GetAssistTargetCount())) then
return party:GetAssistTarget(num);
end
end
-- Returns party members one after the other, in round-robin fashion
-- "prevMember" argument is an optional Player object
function Watcher.GetNextPartyMember(prevMember)
if (prevMember) then
Watcher.prevPartyMember = prevMember;
else
local party = Watcher.playerPartyObject;
if (party) then
local member = Watcher.prevPartyMember or party:GetMember(1);
local num = (member and member.num) or 0;
while true do
num = num + 1;
if (num > party:GetMemberCount()) then
num = 1;
end
member = party:GetMember(num);
if (member) then
return member;
end
end
end
end
end
function Watcher.GetNextAssistTarget()
local party = Watcher.playerPartyObject;
if (party and (party:GetAssistTargetCount() > 0)) then
local member = Watcher.prevPartyMember or party:GetMember(1);
local num = (member and member.num) or 0;
while true do
num = num + 1;
if (num > party:GetMemberCount()) then
num = 1;
end
member = party:GetMember(num);
if (member and party:IsAssistTarget(member)) then
return member;
end
end
end
end
function Watcher.GetPlayerTarget()
return Watcher.targetObject;
end
function Watcher.GetTargetsTarget()
-- If target's target not previously cached, get it now.
if (not Watcher.targetsTargetObject and Watcher.targetObject and Watcher.targetObject.GetTarget) then
Watcher.targetsTargetObject = Watcher.targetObject:GetTarget();
end
return Watcher.targetsTargetObject;
end
-- Saves the selected target, assigning the specified ID.
function Watcher.SaveTarget(id)
Watcher.savedTargets[id] = Watcher.targetObject;
end
-- Returns the saved target with the given ID.
function Watcher.GetSavedTarget(id)
local target = Watcher.savedTargets[id];
if (target and target.GetName and target:GetName() and (target:GetName() ~= "")) then
return target;
end
end
function Watcher.TraitTreeChanged()
-- Trait line changed. Reread skills lists.
Watcher.playerTrainedSkills.cache = nil;
Watcher.playerUntrainedSkills.cache = nil;
end
-- In the absence of a BrawlerAttributes.GetMettle() function, we need to watch the "Battle Flow x" effects.
function Watcher.GetPlayerMettle()
for n = 1, 9 do
if (Watcher.PlayerHasEffect(Watcher.MettleEffectNames[n])) then
return n;
end
end
return 0;
end
local function Constructor()
-- Default settings
Watcher.settings = {};
Watcher.settings.knownEffects = {};
Watcher.settings.funcs = {};
Watcher.settings.funcs.AddCategories = {};
Watcher.settings.funcs.RemoveCategories = {};
-- Optimized functions generated on-the-fly
Watcher.AddCategories = {};
Watcher.RemoveCategories = {};
Watcher.savedTargets = {};
-- Object for monitoring display updates:
Watcher.window = Turbine.UI.Window();
function Watcher.window:Update()
self:SetWantsUpdates(false);
if (Watcher.delayedCallbacks) then
local notyet = {};
for k, v in pairs(Watcher.delayedCallbacks) do
if (Turbine.Engine.GetGameTime() >= v.time) then
DoCallbacks(Watcher, v.event, v.args);
else
table.insert(notyet, v);
end
end
Watcher.delayedCallbacks = nil;
if (#notyet > 0) then
Watcher.delayedCallbacks = notyet;
self:SetWantsUpdates(true);
end
end
end
-- Object for monitoring player events
Watcher.playerObject = Turbine.Gameplay.LocalPlayer:GetInstance();
AddCallback(Watcher.playerObject, "InCombatChanged", InCombatChanged);
AddCallback(Watcher.playerObject, "PartyChanged", PlayerPartyChanged);
AddCallback(Watcher.playerObject, "TargetChanged", PlayerTargetChanged);
PlayerPartyChanged();
PlayerTargetChanged();
Watcher.playerEquipmentObject = Watcher.playerObject:GetEquipment();
AddCallback(Watcher.playerEquipmentObject, "ItemEquipped", ItemEquipped);
AddCallback(Watcher.playerEquipmentObject, "ItemUnequipped", ItemUnequipped);
GetEquipment();
Watcher.playerBackpackObject = Watcher.playerObject:GetBackpack();
AddCallback(Watcher.playerBackpackObject, "ItemAdded", ItemAdded);
AddCallback(Watcher.playerBackpackObject, "ItemRemoved", ItemRemoved);
AddCallback(Watcher.playerBackpackObject, "ItemMoved", ItemMoved);
AddCallback(Watcher.playerBackpackObject, "SizeChanged", BackpackSizeChanged);
GetBackpack();
Watcher.playerTrainedSkills = Watcher.playerObject:GetTrainedSkills();
AddCallback(Watcher.playerTrainedSkills, "SkillAdded", PlayerSkillAdded);
AddCallback(Watcher.playerTrainedSkills, "SkillRemoved", PlayerSkillRemoved);
GetSkills(Watcher.playerTrainedSkills);
Watcher.playerUntrainedSkills = Watcher.playerObject:GetUntrainedSkills();
GetSkills(Watcher.playerUntrainedSkills);
Watcher.playerClassAttributes = Watcher.playerObject:GetClassAttributes();
if (Watcher.playerObject:GetClass() == Turbine.Gameplay.Class.Warden) then
Watcher.playerTrainedGambits = Watcher.playerClassAttributes:GetTrainedGambits();
else
Watcher.playerTrainedGambits = {};
end
-- Lua API bug workaround:
-- Need to cache the GetStance() result, because it returns incorrect values
-- after the user changes trait trees, until he selects a new stance.
if (Watcher.playerClassAttributes.GetStance) then
Watcher.stance = Watcher.playerClassAttributes:GetStance();
Watcher.playerClassAttributes.StanceChanged = StanceChanged;
end
-- For monitoring chat messages
AddCallback(Turbine.Chat, "Received", ChatReceived);
local language = Turbine.Engine:GetLanguage();
if ((language == Turbine.Language.EnglishGB) or (language == Turbine.Language.English)) then
Watcher.SkillsChangedStr = "^You have acquired the .* skill%.";
Watcher.TraitTreeChangedStr = "^You have acquired the Class Specialization Bonus Trait";
Watcher.PlayerIncapacitatedStr = {
" incapacitated you%.";
"You have been incapacitated";
}
Watcher.PlayerRevivedStr = {
"You have been revived";
"You succumb to your wounds";
}
Watcher.MettleEffectNames = {
"Battle Flow 1", "Battle Flow 2", "Battle Flow 3",
"Battle Flow 4", "Battle Flow 5", "Battle Flow 6",
"Battle Flow 7", "Battle Flow 8", "Battle Flow 9"
};
elseif (language == Turbine.Language.German) then
Watcher.SkillsChangedStr = "^Ihr habt Euch die Fertigkeit .* angeeignet%.";
Watcher.TraitTreeChangedStr = "^Ihr habt diese Bonus-Eigenschaft für Klassenspezialisierung erlangt";
Watcher.PlayerIncapacitatedStr = {
" hat Euch auÃer Gefecht gesetzt%.";
"Ihr wurdet durch ein Missgeschick auÃer Gefecht gesetzt%.";
}
Watcher.PlayerRevivedStr = {
"Ihr wurdet wiederbelebt%.";
"Ihr erliegt Euren Verletzungen%.";
}
Watcher.MettleEffectNames = {
"Kampffluss (Stufe 1)", "Kampffluss (Stufe 2)", "Kampffluss (Stufe 3)",
"Kampffluss (Stufe 4)", "Kampffluss (Stufe 5)", "Kampffluss (Stufe 6)",
"Kampffluss (Stufe 7)", "Kampffluss (Stufe 8)", "Kampffluss (Stufe 9)"
};
elseif (language == Turbine.Language.French) then
Watcher.SkillsChangedStr = "^Vous avez acquis la compétence .*%.";
Watcher.TraitTreeChangedStr = "^Vous avez obtenu le trait bonus de spécialisation de classe";
Watcher.PlayerIncapacitatedStr = {
" a réussi à vous mettre hors de combat%.";
"Un incident vous a réduit à l'impuissance%.";
}
Watcher.PlayerRevivedStr = {
"Vous revenez à la vie%.";
"Vous avez succombé à vos blessures%.";
}
Watcher.MettleEffectNames = {
"Flot de Bataille 1", "Flot de Bataille 2", "Flot de Bataille 3",
"Flot de Bataille 4", "Flot de Bataille 5", "Flot de Bataille 6",
"Flot de Bataille 7", "Flot de Bataille 8", "Flot de Bataille 9"
};
elseif (language == Turbine.Language.Russian) then
Watcher.SkillsChangedStr = "^ÐÑ Ð¿ÑиобÑели Ñмение <%.";
Watcher.TraitTreeChangedStr = "^ÐолÑÑен ÑÐ°Ð»Ð°Ð½Ñ ÑпеÑиализаÑии";
Watcher.PlayerIncapacitatedStr = {
" наноÑÐ¸Ñ Ð²Ð°Ð¼ ÑеÑаÑÑий ÑдаÑ%.";
-- incapacitated you.\n
"ÐÑ Ð¾ÐºÐ¾Ð½ÑаÑелÑно пали дÑÑ
ом%.";
--You have been incapacitated. You cannot retreat until the fight has ended.
--You have been incapacitated. Do you want to retreat from battle? You will automatically retreat in
"ÐонÑÑÑ-игÑок лиÑил Ð²Ð°Ñ Ð±Ð¾ÐµÐ²Ð¾Ð³Ð¾ дÑÑ
а%.";
--You have been incapacitated by a monster player. Do you want to retreat from battle? You will automatically retreat in
"ÐÑ ÑÑого неÑÑаÑÑного ÑлÑÑÐ°Ñ Ð²Ñ Ð¿Ð°Ð»Ð¸ дÑÑ
ом%.";
--You have been incapacitated by misadventure.\n
}
Watcher.PlayerRevivedStr = {
"Ðоевой дÑÑ
к вам веÑнÑлÑÑ%.";
--You have been revived.\n
"Ðоевой дÑÑ
Ð¿Ð¾ÐºÐ¸Ð´Ð°ÐµÑ Ð²Ð°Ñ%.";
--You succumb to your wounds. \n
}
Watcher.MettleEffectNames = {
"Ðоевой наÑÑÑой 1", "Ðоевой наÑÑÑой 2", "Ðоевой наÑÑÑой 3",
"Ðоевой наÑÑÑой 4", "Ðоевой наÑÑÑой 5", "Ðоевой наÑÑÑой 6",
"Ðоевой наÑÑÑой 7", "Ðоевой наÑÑÑой 8", "Ðоевой наÑÑÑой 9"
};
end
end
-- Create single instance and load saved settings (if any).
Constructor();
LoadSettings();
--t = Turbine.UI.Window();
--t:SetVisible(true);
--t:SetBackColor(Turbine.UI.Color.Red);
--t:SetMouseVisible(true);
--function t:MouseClick()
-- Puts("cache = " .. PrettyPrint(Watcher.playerEffectsObject.cache, ""));
--end
Thurallor = Thurallor or {};
Thurallor.Utils = Thurallor.Utils or {};
Thurallor.Utils.Watcher = Watcher;