You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1425 lines
51 KiB

--[[
To get a list of nodes that count towards a gate we might be able check condition ids vs cost ids for each node
]]
local ADDON_NAME,Internal = ...
local L = Internal.L
BTWLOADOUTS_DF_TALENTS_ACTIVE = Internal.IsDragonflightPatch
BTWLOADOUTS_SPEC_TREE = L["Spec Tree"] .. " > ";
BTWLOADOUTS_CLASS_TREE = " < " .. L["Class Tree"];
--@NOTE Copying parts of the original talents code over. Dont want to use wrong mixin
local BtWLoadoutsTalentsMixin = false
Internal.OnEvent("LOADOUT_CHANGE_END", function ()
if C_Traits then
C_Traits.RollbackConfig(C_ClassTalents.GetActiveConfigID()); -- Rollback to what we were at before starting a loadout
end
end)
local function CompareSets(a, b)
if a.specID ~= b.specID then
return false
end
if a.treeID ~= b.treeID then
return false
end
if not tCompare(a.nodes, b.nodes, 10) then
return false
end
if type(a.restrictions) ~= type(b.restrictions) and not tCompare(a.restrictions, b.restrictions, 10) then
return false
end
return true
end
local function UpdateSetFilters(set)
set.filters = set.filters or {}
local specID = set.specID;
local filters = set.filters
Internal.UpdateRestrictionFilters(set)
filters.spec = specID
if specID then
filters.role, filters.class = select(5, GetSpecializationInfoByID(specID))
else
filters.role, filters.class = nil, nil
end
-- Rebuild character list
if type(filters.character) ~= "table" then
filters.character = {}
end
if set.character then
filters.character = {set.character}
else
filters.character = filters.character or {}
local characters = filters.character
table.wipe(characters)
local class = filters.class
for _,character in Internal.CharacterIterator() do
if class == Internal.GetCharacterInfo(character).class then
characters[#characters+1] = character
end
end
end
set.filters = filters
return set
end
local function RefreshSet(set)
local nodes = set.nodes or {};
local specID, specName = GetSpecializationInfo(GetSpecialization());
if specID == set.specID then
wipe(nodes);
local configID = C_ClassTalents.GetActiveConfigID();
if not configID then
return
end
local configInfo = C_Traits.GetConfigInfo(configID);
local treeID = configInfo.treeIDs[1];
local treeInfo = Internal.GetTreeInfoBySpecID(specID);
if treeInfo.ID == treeID then
local nodeIDs = C_Traits.GetTreeNodes(configInfo.treeIDs[1]);
for _,nodeID in ipairs(nodeIDs) do
local nodeInfo = C_Traits.GetNodeInfo(configID, nodeID);
if nodeInfo.isVisible then
if #nodeInfo.entryIDs > 1 then
if nodeInfo.activeEntry then
for index,entryID in ipairs(nodeInfo.entryIDs) do
if entryID == nodeInfo.activeEntry.entryID then
nodes[nodeID] = index;
break;
end
end
end
elseif nodeInfo.ranksPurchased > 0 then
nodes[nodeID] = nodeInfo.ranksPurchased;
end
end
end
set.treeID = treeID
end
end
set.nodes = nodes;
return UpdateSetFilters(set)
end
local function AddSet()
local specIndex = GetSpecialization()
if specIndex == 5 then
specIndex = 1
end
local classID = select(3, UnitClass("player"))
local specID, specName = GetSpecializationInfo(specIndex);
return Internal.AddSet("dftalents", RefreshSet({
classID = classID,
specID = specID,
name = format(L["New %s Set"], specName),
useCount = 0,
nodes = {},
}))
end
local function DeleteSet(id)
Internal.DeleteSet(BtWLoadoutsSets.dftalents, id);
if type(id) == "table" then
id = id.setID;
end
for _,set in pairs(BtWLoadoutsSets.profiles) do
if type(set) == "table" then
for index,setID in ipairs(set.dftalents) do
if setID == id then
table.remove(set.dftalents, index)
end
end
end
end
local frame = BtWLoadoutsFrame.DFTalents;
local set = frame.set;
if set.setID == id then
frame.set = nil
BtWLoadoutsFrame:Update(true);
end
end
local function GetSet(id)
if type(id) == "table" then
return id;
else
return BtWLoadoutsSets.dftalents[id]
end
end
local function GetSets(id, ...)
if id ~= nil then
return GetSet(id), GetSets(...);
end
end
-- In General, For Player, For Player Spec
local function SetIsValid(set)
set = GetSet(set);
local playerSpecID = GetSpecializationInfo(GetSpecialization());
local playerClass = select(2, UnitClass("player"));
local specClass = select(6, GetSpecializationInfoByID(set.specID));
return true, (playerClass == specClass), (playerSpecID == set.specID)
end
local function GetByName(name)
return Internal.GetSetByName(BtWLoadoutsSets.dftalents, name, SetIsValid)
end
local function IsSetActive(set)
local configID = C_ClassTalents.GetActiveConfigID();
for nodeID,value in pairs(set.nodes) do
local nodeInfo = C_Traits.GetNodeInfo(configID, nodeID);
if not nodeInfo then -- Does this mean we cant activate this set at all?
return false;
end
if not nodeInfo.isVisible then
return false;
end
if #nodeInfo.entryIDs > 1 then
if not nodeInfo.activeEntry then
return false;
end
if nodeInfo.activeEntry.entryID ~= nodeInfo.entryIDs[value] then
return false;
end
else
if nodeInfo.activeRank ~= value then
return false;
end
end
end
return true;
end
local function IsNodeEntryOnCooldown(nodeEntryID)
local entryInfo = C_Traits.GetEntryInfo(C_ClassTalents.GetActiveConfigID(), nodeEntryID);
local definitionInfo = C_Traits.GetDefinitionInfo(entryInfo.definitionID);
local spellID = definitionInfo.spellID;
if spellID then
spellID = FindSpellOverrideByID(spellID);
local start, duration = GetSpellCooldown(spellID);
if start ~= 0 then -- Talent spell on cooldown, we need to wait before switching
Internal.DirtyAfter((start + duration) - GetTime() + 1);
return true;
end
end
return false;
end
local function SetRequirements(set)
local isActive, waitForCooldown = true, false
local configID = C_ClassTalents.GetActiveConfigID();
if not configID then
return
end
local configInfo = C_Traits.GetConfigInfo(configID);
local nodeIDs = C_Traits.GetTreeNodes(configInfo.treeIDs[1]);
for _,nodeID in ipairs(nodeIDs) do
local nodeInfo = C_Traits.GetNodeInfo(configID, nodeID);
if nodeInfo.isVisible then
local value = set.nodes[nodeID];
if #nodeInfo.entryIDs > 1 then
if nodeInfo.activeEntry and nodeInfo.activeEntry.entryID and (not value or nodeInfo.entryIDs[value] ~= nodeInfo.activeEntry.entryID) then
isActive = false;
waitForCooldown = waitForCooldown or IsNodeEntryOnCooldown(nodeInfo.activeEntry.entryID);
if waitForCooldown then
break -- We dont actually need to check anything more
end
else
if nodeInfo.ranksPurchased ~= 1 or not nodeInfo.activeEntry or nodeInfo.activeEntry.entryID ~= nodeInfo.entryIDs[value] then
isActive = false;
end
end
else
if value then
if nodeInfo.ranksPurchased ~= value then
isActive = false;
end
elseif nodeInfo.ranksPurchased > 0 then
waitForCooldown = waitForCooldown or IsNodeEntryOnCooldown(nodeInfo.activeEntry.entryID);
isActive = false;
if waitForCooldown then
break -- We dont actually need to check anything more
end
end
end
end
end
return isActive, waitForCooldown
end
local function CombineSets(result, state, ...)
result = result or {};
if select('#', ...) > 0 then
local set = GetSet(select(select('#', ...), ...));
result.nodes = {}
for _,nodeID in ipairs(C_Traits.GetTreeNodes(set.treeID)) do
result.nodes[nodeID] = set.nodes[nodeID]
end
end
if state then
local isActive, waitForCooldown, anySelected = SetRequirements(result)
if not isActive then
if state.blockers then
state.blockers[Internal.GetSpellCastingBlocker()] = true
state.blockers[Internal.GetCombatBlocker()] = true
state.blockers[Internal.GetMythicPlusBlocker()] = true
state.blockers[Internal.GetJailersChainBlocker()] = true
end
state.customWait = state.customWait or (waitForCooldown and L["Waiting for talent cooldown"])
end
end
return result;
end
local function ActivateSet(set, state)
local complete = true;
local spellId = select(9, UnitCastingInfo("player"));
if spellId == 384255 then
complete = false;
elseif not IsSetActive(set) and not state.dfTalentsAttempted then
Internal.LogMessage("Activate talent tree version %d", Internal.GetTraitInfoVersion());
complete = false;
local specID = GetSpecializationInfo(GetSpecialization());
if ClassTalentFrame then
ClassTalentFrame.TalentsTab:ClearLastSelectedConfigID();
ClassTalentFrame.TalentsTab:MarkTreeDirty();
end
C_ClassTalents.UpdateLastSelectedSavedConfigID(specID, 0) -- Set active loadout to "Default Loadout"
local configID = C_ClassTalents.GetActiveConfigID();
if not configID then
return
end
local configInfo = C_Traits.GetConfigInfo(configID);
C_Traits.ResetTree(configID, configInfo.treeIDs[1]);
local done = {};
local a, b = {}, {};
local function PurchaseNode(nodeID)
if not done[nodeID] and set.nodes[nodeID] then
local nodeInfo = Internal.GetNodeInfoBySpecID(specID, nodeID);
local incomingEdges = nodeInfo.incomingEdgesBySpecID and nodeInfo.incomingEdgesBySpecID[specID] or nodeInfo.incomingEdges
if incomingEdges then
for _,sourceNode in ipairs(incomingEdges) do
PurchaseNode(sourceNode);
end
end
if nodeInfo.type == Enum.TraitNodeType.Selection then
local entryIndex = set.nodes[nodeID];
local success = C_Traits.SetSelection(configID, nodeID, nodeInfo.entryIDs[entryIndex]);
Internal.LogMessage("Set talent choice to %d for node %d (%s)", nodeInfo.entryIDs[entryIndex], nodeID, success and "true" or "false");
if not success then
b[nodeID] = true;
return;
end
else
local points = set.nodes[nodeID];
for i=1,points do
local success = C_Traits.PurchaseRank(configID, nodeID);
Internal.LogMessage("Purchase talent point %d of %d for node %d (%s)", i, points, nodeID, success and "true" or "false");
if not success then
b[nodeID] = true;
return;
end
end
end
done[nodeID] = true;
end
end
for nodeID in pairs(set.nodes) do
a[nodeID] = true;
end
local tries = 10;
while next(a) and tries > 0 do
Internal.LogMessage("Talent loop %d", 11 - tries);
wipe(b);
for nodeID in pairs(a) do
PurchaseNode(nodeID);
end
b, a = a, b;
tries = tries - 1;
end
state.dfTalentsAttempted = true
local success = C_ClassTalents.CommitConfig(configID);
Internal.LogMessage("Commit talent config (%s)", success and "true" or "false");
end
if complete then
local specID = GetSpecializationInfo(GetSpecialization());
if ClassTalentFrame then
ClassTalentFrame.TalentsTab:ClearLastSelectedConfigID();
ClassTalentFrame.TalentsTab:MarkTreeDirty();
end
C_ClassTalents.UpdateLastSelectedSavedConfigID(specID, 0) -- Set active loadout to "Default Loadout"
end
return complete
end
local function CheckErrors(errorState, set)
set = GetSet(set)
errorState.specID = errorState.specID or set.specID
if errorState.specID ~= set.specID then
return L["Incompatible Specialization"]
end
if not Internal.AreRestrictionsValidFor(set.restrictions, errorState.specID) then
return L["Incompatible Restrictions"]
end
end
function Internal.RefreshSetFromConfigID(set, configID)
local configInfo = C_Traits.GetConfigInfo(configID);
if not configInfo then
return
end
local character = Internal.GetCharacterSlug();
local treeID = configInfo.treeIDs[1];
local treeInfo = C_Traits.GetTreeInfo(configID, treeID);
local nodeIDs = C_Traits.GetTreeNodes(treeID);
local specID = GetSpecializationInfo(GetSpecialization());
local classID = select(3, UnitClass("player"))
local nodes = {};
for _,nodeID in ipairs(nodeIDs) do
local nodeInfo = C_Traits.GetNodeInfo(configID, nodeID);
if nodeInfo.isVisible then
if #nodeInfo.entryIDs > 1 then
if nodeInfo.activeEntry then
for index,entryID in ipairs(nodeInfo.entryIDs) do
if entryID == nodeInfo.activeEntry.entryID then
nodes[nodeID] = index;
break;
end
end
end
elseif nodeInfo.ranksPurchased > 0 then
nodes[nodeID] = nodeInfo.ranksPurchased;
end
end
end
set.name = configInfo.name;
set.character = character;
set.configID = configID;
set.classID = classID;
set.specID = specID;
set.treeID = treeID;
set.nodes = nodes;
UpdateSetFilters(set)
if BtWLoadoutsFrameDFTalents:IsShown() and BtWLoadoutsFrameDFTalents.set == set then
BtWLoadoutsFrameDFTalents:Update()
end
return set
end
-- Initializes the set dropdown menu for the Loadouts page
local function SetDropDownInit(self, set, index)
Internal.SetDropDownInit(self, set, index, "dftalents", BtWLoadoutsFrame.DFTalents)
end
Internal.AddLoadoutSegment({
id = "dftalents",
name = L["Talents"],
events = BTWLOADOUTS_DF_TALENTS_ACTIVE and "TRAIT_CONFIG_UPDATED" or nil,
enabled = BTWLOADOUTS_DF_TALENTS_ACTIVE,
add = AddSet,
get = GetSets,
getByName = GetByName,
isActive = IsSetActive,
combine = CombineSets,
activate = ActivateSet,
checkerrors = CheckErrors,
dropdowninit = SetDropDownInit,
export = function (set)
return {
version = 1,
name = set.name,
classID = set.classID,
specID = set.specID,
treeID = set.treeID,
nodes = set.nodes,
restrictions = set.restrictions,
}
end,
import = function (source, version, name, ...)
assert(version == 1)
local specID, classID = ...
specID = source.specID or specID
classID = source.classID or classID
return Internal.AddSet("dftalents", UpdateSetFilters({
classID = classID,
specID = specID,
treeID = source.treeID,
name = name or source.name,
useCount = 0,
nodes = source.nodes,
restrictions = source.restrictions,
}))
end,
getByValue = function (set)
return Internal.GetSetByValue(BtWLoadoutsSets.dftalents, set, CompareSets)
end,
verify = function (source, ...)
local specID, classID = ...
specID = source.specID or specID
if not specID or not GetSpecializationInfoByID(specID) then
return false, L["Invalid specialization"]
end
classID = source.classID or classID
local classFile = select(6, GetSpecializationInfoByID(specID))
if not classID or Internal.GetClassID(classFile) ~= classID then
return false, L["Invalid class"]
end
local nodes = C_Traits.GetTreeNodes(source.treeID)
if next(nodes) == nil then
return false, L["Invalid talent tree"]
end
if type(source.nodes) ~= "table" then
return false, L["Invalid nodes"]
end
if source.restrictions ~= nil and type(source.restrictions) ~= "table" then
return false, L["Missing restrictions"]
end
local nodeIDs = {}
for _,nodeID in ipairs(nodes) do
nodeIDs[nodeID] = true
end
for nodeID in pairs(source.nodes) do
if not nodeIDs[nodeID] then
return false, L["Invalid nodes"]
end
end
return true
end,
})
local DFTalentButtonMixin = CreateFromMixins(TalentButtonSpendMixin or {})
function DFTalentButtonMixin:OnLoad()
TalentButtonSpendMixin.OnLoad(self);
self.sizingAdjustment = {
{ region = "Icon", adjust = 0, },
{ region = "DisabledOverlay", adjust = 0, },
{ region = "StateBorder", adjust = 0, },
{ region = "StateBorderHover", adjust = 0, },
{ region = "IconMask", adjust = 0, },
{ region = "DisabledOverlayMask", adjust = 0, },
{ region = "Shadow", adjust = 0, },
}
end
local DFTalentButtonSplitMixin = CreateFromMixins(TalentButtonSplitSelectMixin or {});
function DFTalentButtonSplitMixin:OnLoad()
TalentButtonSplitSelectMixin.OnLoad(self);
self.sizingAdjustment = {
{ region = "Icon", adjust = 0, },
{ region = "Icon2", adjust = 0, },
{ region = "DisabledOverlay", adjust = 0, },
{ region = "IconMask", adjust = 0, },
{ region = "Icon2Mask", adjust = 0, },
{ region = "IconSplitMask", adjust = 0, },
{ region = "DisabledOverlayMask", adjust = 0, },
{ region = "Shadow", adjust = 0, },
}
end
function DFTalentButtonSplitMixin:ApplySize(width, height)
TalentButtonBasicArtMixin.ApplySize(self, width, height);
self.StateBorder:SetSize(width + 18, height + 12);
self.StateBorderHover:SetSize(width + 18, height + 12);
end
local DFTalentSelectionChoiceMixin = CreateFromMixins(TalentSelectionChoiceMixin or {});
-- function DFTalentSelectionChoiceMixin:OnLoad()
-- print(self.sizingAdjustment)
-- self.sizingAdjustment = {
-- { region = "Icon", adjust = 0, },
-- { region = "DisabledOverlay", adjust = 0, },
-- { region = "StateBorder", adjust = 10, },
-- { region = "IconMask", adjust = 0, },
-- { region = "DisabledOverlayMask", adjust = 0, },
-- { region = "Shadow", adjust = 0, },
-- }
-- end
local function GetSpecializedMixin(nodeInfo, talentType)
if nodeInfo and (nodeInfo.type == Enum.TraitNodeType.Selection) then
if FlagsUtil.IsSet(nodeInfo.flags, Enum.TraitNodeFlag.ShowMultipleIcons) then
return DFTalentButtonSplitMixin;
end
end
return DFTalentButtonMixin;
end
BtWLoadoutsDFTalentFrameBaseButtonsParentMixin = CreateFromMixins(TalentFrameBaseButtonsParentMixin or {})
BtWLoadoutsDFTalentSelectionChoiceFrameMixin = CreateFromMixins(TalentSelectionChoiceFrameMixin or {})
BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnLoad = BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnLoad or function () end
BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnShow = BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnShow or function () end
BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnHide = BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnHide or function () end
BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnEvent = BtWLoadoutsDFTalentSelectionChoiceFrameMixin.OnEvent or function () end
BtWLoadoutsDFTalentsMixin = CreateFromMixins(TalentFrameBaseMixin or CallbackRegistryMixin)
function BtWLoadoutsDFTalentsMixin:OnLoad()
CallbackRegistryMixin.OnLoad(self);
if not C_ClassTalents then
return
end
self:SetBasePanOffset(0, 0);
self.Scroll:RegisterForDrag("LeftButton");
self.RestrictionsDropDown:SetSupportedTypes("race")
self.RestrictionsDropDown:SetScript("OnChange", function ()
self:Update()
end)
self.temp = {}
self.talentButtonCollection = CreateFramePoolCollection();
self.talentDislayFramePool = CreateFramePoolCollection();
self.edgePool = CreateFramePoolCollection();
self.gatePool = CreateFramePool("FRAME", self.Scroll:GetScrollChild(), "BtWLoadoutsTalentFrameGateTemplate");
self.nodeIDToButton = {};
self.buttonsWithDirtyEdges = {};
self.treeInfoDirty = false;
self.definitionInfoCache = {};
self.dirtyDefinitionIDSet = {};
self.entryInfoCache = {};
self.dirtyEntryIDSet = {};
self.nodeInfoCache = {};
self.dirtyNodeIDSet = {};
self.condInfoCache = {};
self.dirtyCondIDSet = {};
self.panOffsetX = 0;
self.panOffsetY = 0;
self.ButtonsParent = self.Scroll:GetScrollChild():GetChildren();
self.getSpecializedMixin = GetSpecializedMixin;
end
function BtWLoadoutsDFTalentsMixin:OnShow()
if not self.initialized then
self.SpecDropDown.includeNone = false;
UIDropDownMenu_SetWidth(self.SpecDropDown, 170);
UIDropDownMenu_JustifyText(self.SpecDropDown, "LEFT");
self.SpecDropDown.GetValue = function ()
if self.set then
return self.set.specID, self.set.classID
end
end
self.SpecDropDown.SetValue = function (_, _, arg1, arg2)
CloseDropDownMenus();
local set = self.set;
if set then
local temp = self.temp;
temp[set.specID] = set.nodes;
set.specID = arg1;
set.classID = arg2;
local result = Internal.GetTreeInfoBySpecID(arg1);
set.treeID = result.ID;
set.nodes = temp[set.specID] or {};
self:Update(true);
end
end
self.initialized = true;
end
-- self:Update(true);
end
function BtWLoadoutsDFTalentsMixin:OnUpdate(...)
self:UpdateTreeCurrencyInfo();
TalentFrameBaseMixin.OnUpdate(self, ...);
end
function BtWLoadoutsDFTalentsMixin:ChangeSet(set)
self.set = set
wipe(self.temp);
self:Update(true)
end
function BtWLoadoutsDFTalentsMixin:UpdateSetName(value)
if self.set and self.set.name ~= not value then
if self.set.configID then
return
end
self.set.name = value;
self:Update(false, true);
end
end
function BtWLoadoutsDFTalentsMixin:OnButtonClick(button)
CloseDropDownMenus()
if button.isAdd then
BtWLoadoutsHelpTipFlags["TUTORIAL_NEW_SET"] = true;
self.Name:ClearFocus()
self:ChangeSet(AddSet())
C_Timer.After(0, function ()
self.Name:HighlightText()
self.Name:SetFocus()
end)
elseif button.isDelete then
local set = self.set
if set.configID then
return
end
if set.useCount > 0 then
StaticPopup_Show("BTWLOADOUTS_DELETEINUSESET", set.name, nil, {
set = set,
func = DeleteSet,
})
else
StaticPopup_Show("BTWLOADOUTS_DELETESET", set.name, nil, {
set = set,
func = DeleteSet,
})
end
elseif button.isRefresh then
local set = self.set;
if set.configID then
return
end
RefreshSet(set)
self:Update()
elseif button.isExport then
local set = self.set;
self:GetParent():SetExport(Internal.Export("dftalents", set.setID))
elseif button.isActivate then
local set = self.set;
if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then
Internal.ActivateProfile({
dftalents = {set.setID}
});
end
end
end
function BtWLoadoutsDFTalentsMixin:OnSidebarItemClick(button)
CloseDropDownMenus()
if button.isHeader then
button.collapsed[button.id] = not button.collapsed[button.id]
self:Update()
else
if IsModifiedClick("SHIFT") then
local set = GetSet(button.id);
if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then
Internal.ActivateProfile({
dftalents = {button.id}
});
end
else
self.Name:ClearFocus();
self:ChangeSet(GetSet(button.id))
end
end
end
function BtWLoadoutsDFTalentsMixin:OnSidebarItemDoubleClick(button)
CloseDropDownMenus()
if button.isHeader then
return
end
local set = GetSet(button.id);
if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then
Internal.ActivateProfile({
dftalents = {button.id}
});
end
end
function BtWLoadoutsDFTalentsMixin:OnSidebarItemDragStart(button)
CloseDropDownMenus()
if button.isHeader then
return
end
local icon = "INV_Misc_QuestionMark";
local set = GetSet(button.id);
local command = format("/btwloadouts activate dftalents %d", button.id);
if set.specID then
icon = select(4, GetSpecializationInfoByID(set.specID));
end
if command then
local macroId;
local numMacros = GetNumMacros();
for i=1,numMacros do
if GetMacroBody(i):trim() == command then
macroId = i;
break;
end
end
if not macroId then
if numMacros == MAX_ACCOUNT_MACROS then
print(L["Cannot create any more macros"]);
return;
end
if InCombatLockdown() then
print(L["Cannot create macros while in combat"]);
return;
end
macroId = CreateMacro(set.name, icon, command, false);
else
-- Rename the macro while not in combat
if not InCombatLockdown() then
icon = select(2,GetMacroInfo(macroId))
EditMacro(macroId, set.name, icon, command)
end
end
if macroId then
PickupMacro(macroId);
end
end
end
function BtWLoadoutsDFTalentsMixin:Update(updatePosition, skipUpdateTree)
self:GetParent():SetTitle(L["Talents"]);
local sidebar = BtWLoadoutsFrame.Sidebar
sidebar:SetSupportedFilters("spec", "class", "role", "character", "covenant", "race")
sidebar:SetSets(BtWLoadoutsSets.dftalents)
sidebar:SetCollapsed(BtWLoadoutsCollapsed.dftalents)
sidebar:SetCategories(BtWLoadoutsCategories.dftalents)
sidebar:SetFilters(BtWLoadoutsFilters.dftalents)
sidebar:SetSelected(self.set)
sidebar:Update()
self.set = sidebar:GetSelected()
local set = self.set
local showingNPE = BtWLoadoutsFrame:SetNPEShown(set == nil, L["Talents"], L["Create different talent layouts for the type of content you wish to do."])
self:GetParent().ExportButton:SetEnabled(true)
self:GetParent().DeleteButton:SetEnabled(true);
if not showingNPE then
local treeInfo = Internal.GetTreeInfoBySpecID(self.set.specID);
if treeInfo.ID ~= set.treeID and set.configID == nil then
print(format(L["[%s]: Talent Tree has changed and your set has been reset."], ADDON_NAME));
set.treeID = treeInfo.ID;
set.nodes = {};
end
local configID = Constants.TraitConsts.VIEW_TRAIT_CONFIG_ID; -- C_ClassTalents.GetActiveConfigID();
C_ClassTalents.InitializeViewLoadout(self.set.specID, 70);
local success = C_ClassTalents.ViewLoadout({});
Internal.UpdateTraitInfoFromConfig(self.set.specID, configID)
local treeID = set.treeID
local nodes = C_Traits.GetTreeNodes(treeID)
local oldNodes = set.nodes
set.nodes = {}
for _,nodeID in ipairs(nodes) do
set.nodes[nodeID] = oldNodes[nodeID]
end
local classID = set.classID
local specID = set.specID
if not classID then
local classInfo = Internal.GetClassInfoBySpecID(specID)
set.classID = classInfo.classID
classID = classInfo.classID
end
UpdateSetFilters(set)
sidebar:Update()
set.restrictions = set.restrictions or {}
self.RestrictionsDropDown:SetSelections(set.restrictions)
self.RestrictionsDropDown:SetLimitations()
self.RestrictionsButton:SetEnabled(true);
if not self.Name:HasFocus() then
self.Name:SetText(set.name or "");
end
local _, specName, _, icon, _, classFile = GetSpecializationInfoByID(specID);
local className = LOCALIZED_CLASS_NAMES_MALE[classFile];
local classColor = C_ClassColor.GetClassColor(classFile);
UIDropDownMenu_SetText(self.SpecDropDown, format("%s: %s", classColor:WrapTextInColorCode(className), specName));
self.talentTreeID = treeID;
self:UpdateTreeInfo(true);
local rect = {left = 65536, right = 0, top = 65536, bottom = 0}
for _,nodeID in ipairs(nodes) do
local nodeInfo = self:GetAndCacheNodeInfo(nodeID); -- /tinspect C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID(), 61086)
if nodeInfo and nodeInfo.posY > 0 then
if rect.left > nodeInfo.posX then
rect.left = nodeInfo.posX
end
if rect.right < nodeInfo.posX then
rect.right = nodeInfo.posX
end
if rect.top > nodeInfo.posY then
rect.top = nodeInfo.posY
end
if rect.bottom < nodeInfo.posY then
rect.bottom = nodeInfo.posY
end
end
end
local scroll = self.Scroll;
local scale = self.ButtonsParent:GetScale();
-- How much to shift nodes vertically, calculated in node units
local actualHeight = self.Scroll:GetHeight() / scale * 10;
local requiredHeight = (rect.bottom - rect.top + 800);
if requiredHeight < actualHeight then
self.offsetY = -rect.top + (actualHeight - requiredHeight) * 0.5 + 400;
self.Scroll:GetScrollChild():SetHeight(self.Scroll:GetHeight());
else
self.offsetY = -rect.top + 400;
self.Scroll:GetScrollChild():SetHeight(requiredHeight * 0.1 * scale);
end
if not skipUpdateTree then
self:LoadTalentTreeInternal();
end
local center = (rect.right - rect.left) * 0.5 + rect.left;
local leftSide = {left = 65536, right = 0, top = 65536, bottom = 0}
local rightSide = {left = 65536, right = 0, top = 65536, bottom = 0}
for _,nodeID in ipairs(nodes) do
local nodeInfo = self:GetAndCacheNodeInfo(nodeID); -- /tinspect C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID(), 61086)
if nodeInfo and nodeInfo.posY > 0 then
if nodeInfo.posX < center then
if leftSide.left > nodeInfo.posX then
leftSide.left = nodeInfo.posX
end
if leftSide.right < nodeInfo.posX then
leftSide.right = nodeInfo.posX
end
else
if rightSide.left > nodeInfo.posX then
rightSide.left = nodeInfo.posX
end
if rightSide.right < nodeInfo.posX then
rightSide.right = nodeInfo.posX
end
end
end
end
local halfWidth = scroll:GetWidth() * 0.5;
self.leftOffset = (((leftSide.right - leftSide.left) * 0.5 + leftSide.left) * 0.1) * scale - halfWidth;
self.centerOffset = center * 0.1 * scale - halfWidth;
self.rightOffset = (((rightSide.right - rightSide.left) * 0.5 + rightSide.left) * 0.1) * scale - halfWidth;
local scrollChild = scroll:GetScrollChild();
self.SpecTreeButton:SetPoint("RIGHT", scrollChild, "LEFT", self.leftOffset + scroll:GetWidth() - scroll.ScrollBar:GetWidth(), 0)
self.ClassTreeButton:SetPoint("LEFT", self.rightOffset, 0)
self.SpecTreeButton:SetPoint("TOP", 0, -scroll:GetVerticalScroll())
self.ClassTreeButton:SetPoint("TOP", 0, -scroll:GetVerticalScroll())
if updatePosition then
self.endScrolling = true;
self.DragHandler:Show();
end
local playerSpecIndex = GetSpecialization()
if set.configID then
UIDropDownMenu_DisableDropDown(self.SpecDropDown)
else
UIDropDownMenu_EnableDropDown(self.SpecDropDown)
end
self.Name:SetEnabled(not set.configID)
self:GetParent().DeleteButton:SetEnabled(not set.configID)
self:GetParent().RefreshButton:SetEnabled(not set.configID and ((playerSpecIndex and specID == GetSpecializationInfo(playerSpecIndex)) or (specID == nil and classID == select(3, UnitClass("player")))))
self:GetParent().ActivateButton:SetEnabled(classID == select(3, UnitClass("player")));
local helpTipBox = self:GetParent().HelpTipBox;
helpTipBox:Hide();
BtWLoadoutsHelpTipFlags["TUTORIAL_CREATE_TALENT_SET"] = true;
else
local specIndex = GetSpecialization()
if not specIndex or specIndex == 5 then
specIndex = 1
end
local specID, specName = GetSpecializationInfo(specIndex);
self.Name:SetText(format(L["New %s Set"], specName));
local _, specName, _, icon, _, classID = GetSpecializationInfoByID(specID);
local className = LOCALIZED_CLASS_NAMES_MALE[classID];
local classColor = C_ClassColor.GetClassColor(classID);
UIDropDownMenu_SetText(self.SpecDropDown, format("%s: %s", classColor:WrapTextInColorCode(className), specName));
local helpTipBox = self:GetParent().HelpTipBox;
helpTipBox:Hide();
end
end
function BtWLoadoutsDFTalentsMixin:SetSetByID(setID)
self.set = GetSet(setID)
end
function BtWLoadoutsDFTalentsMixin:OnDrag()
local scroll = self.Scroll;
if self.Scroll:GetWidth() >= 1000 then -- Lock showing both trees
local scrollX = scroll:GetHorizontalScroll();
local scrollY = scroll:GetVerticalScroll();
local targetX = self.centerOffset - 20;
if ApproximatelyEqual(scrollX, targetX, 0.1) then
scrollX = targetX;
self.DragHandler:Hide();
else
scrollX = FrameDeltaLerp(scrollX, targetX, 0.1);
end
self.SpecTreeButton:SetAlpha(0);
self.ClassTreeButton:SetAlpha(0);
scroll:SetHorizontalScroll(scrollX)
scroll:SetVerticalScroll(scrollY)
elseif self.Scroll:GetWidth() >= 620 then -- Lock showing both trees
local maxXScroll, maxYScroll = scroll:GetHorizontalScrollRange(), scroll:GetVerticalScrollRange()
local minXScroll = self.leftOffset;
local maxXScroll = self.rightOffset;
local scrollX = scroll:GetHorizontalScroll();
local scrollY = scroll:GetVerticalScroll();
if self.endScrolling then
self.DragHandler:Hide();
else
local mouseX, mouseY = GetCursorPosition()
local scale = scroll:GetScrollChild():GetEffectiveScale()
mouseX, mouseY = mouseX / scale, mouseY / scale
scrollX = min(max(self.mouseX - mouseX + self.scrollX, minXScroll), maxXScroll)
scrollY = min(max(mouseY - self.mouseY + self.scrollY, 0), maxYScroll)
end
self.SpecTreeButton:SetAlpha(0);
self.ClassTreeButton:SetAlpha(0);
scroll:SetHorizontalScroll(scrollX)
scroll:SetVerticalScroll(scrollY)
else
local maxXScroll, maxYScroll = scroll:GetHorizontalScrollRange(), scroll:GetVerticalScrollRange()
local minXScroll = self.leftOffset;
local maxXScroll = self.rightOffset;
local scrollX, scrollY
if self.endScrolling then -- Maybe check which direction the drag was going in before hand?
scrollX = scroll:GetHorizontalScroll();
scrollY = scroll:GetVerticalScroll();
if self.scrollDelta then
if self.scrollDelta < 0 then
if ApproximatelyEqual(scrollX, minXScroll, 0.1) then
scrollX = minXScroll;
self.DragHandler:Hide();
else
scrollX = FrameDeltaLerp(scrollX, minXScroll, 0.1);
end
else
if ApproximatelyEqual(scrollX, maxXScroll, 0.1) then
scrollX = maxXScroll;
self.DragHandler:Hide();
else
scrollX = FrameDeltaLerp(scrollX, maxXScroll, 0.1);
end
end
elseif ApproximatelyEqual(scrollX, minXScroll, 0.1) then
scrollX = minXScroll;
self.DragHandler:Hide();
elseif ApproximatelyEqual(scrollX, maxXScroll, 0.1) then
scrollX = maxXScroll;
self.DragHandler:Hide();
else
local halfWay = maxXScroll * 0.5;
if scrollX < maxXScroll * 0.5 then
scrollX = FrameDeltaLerp(scrollX, minXScroll, 0.1);
else
scrollX = FrameDeltaLerp(scrollX, maxXScroll, 0.1);
end
end
else
local mouseX, mouseY = GetCursorPosition()
local scale = scroll:GetScrollChild():GetEffectiveScale()
mouseX, mouseY = mouseX / scale, mouseY / scale
scrollX = self.mouseX - mouseX + self.scrollX
scrollY = mouseY - self.mouseY + self.scrollY
end
scrollX = min(max(scrollX, minXScroll), maxXScroll)
scrollY = min(max(scrollY, 0), maxYScroll)
self.SpecTreeButton:SetAlpha(math.max(1 - math.abs(scrollX - self.leftOffset) * 0.005, 0))
self.ClassTreeButton:SetAlpha(math.max(1 - math.abs(scrollX - self.rightOffset) * 0.005, 0))
scroll:SetHorizontalScroll(scrollX)
scroll:SetVerticalScroll(scrollY)
end
end
function BtWLoadoutsDFTalentsMixin:GetMaxWidth()
return 1300
end
function BtWLoadoutsDFTalentsMixin:BeginScrollDrag()
local scroll = self.Scroll;
self.scrollX, self.scrollY = scroll:GetHorizontalScroll(), scroll:GetVerticalScroll()
self.mouseX, self.mouseY = GetCursorPosition()
local scale = scroll:GetScrollChild():GetEffectiveScale()
self.mouseX, self.mouseY = self.mouseX / scale, self.mouseY / scale
self.scrollDelta = nil;
self.endScrolling = false;
self.DragHandler:Show();
end
function BtWLoadoutsDFTalentsMixin:EndScrollDrag()
self.endScrolling = true;
end
function BtWLoadoutsDFTalentsMixin:OnVerticalScroll(scroll, offset)
self.SpecTreeButton:SetPoint("TOP", 0, -offset)
self.ClassTreeButton:SetPoint("TOP", 0, -offset)
end
function BtWLoadoutsDFTalentsMixin:ScrollToClassTree()
self.scrollDelta = -1;
self.endScrolling = true;
self.DragHandler:Show();
end
function BtWLoadoutsDFTalentsMixin:ScrollToSpecTree()
self.scrollDelta = 1;
self.endScrolling = true;
self.DragHandler:Show();
end
function BtWLoadoutsDFTalentsMixin:UpdateTreeInfo(skipButtonUpdates)
self.talentTreeInfo = Internal.GetTreeInfoBySpecID(self.set.specID);
self:UpdateTreeCurrencyInfo(skipButtonUpdates);
if not skipButtonUpdates then
self:RefreshGates();
end
end
function BtWLoadoutsDFTalentsMixin:UpdateTreeCurrencyInfo(skipButtonUpdates)
local treeInfo = self:GetTreeInfo();
self.treeCurrencyInfoMap = {};
for _,currency in ipairs(treeInfo.currencies) do
if GetMaxLevelForPlayerExpansion() == 60 then
self.treeCurrencyInfoMap[currency.traitCurrencyID] = {
traitCurrencyID = currency.traitCurrencyID,
maxQuantity = currency.maxQuantity - 5,
quantity = currency.maxQuantity - 5,
spent = 0,
}
else
currency.quantity = currency.maxQuantity;
currency.spent = 0;
self.treeCurrencyInfoMap[currency.traitCurrencyID] = currency;
end
end
-- Calculate spent currencies
for _,nodeID in ipairs(treeInfo.nodes) do
-- local nodeInfo = self:GetAndCacheNodeInfo(nodeID);
-- if nodeInfo and nodeInfo.ranksPurchased > 0 then
-- for _,cost in ipairs(nodeInfo.costs) do
-- self.treeCurrencyInfoMap[cost.ID].spent = self.treeCurrencyInfoMap[cost.ID].spent + (cost.amount * nodeInfo.ranksPurchased);
-- self.treeCurrencyInfoMap[cost.ID].quantity = self.treeCurrencyInfoMap[cost.ID].quantity - (cost.amount * nodeInfo.ranksPurchased);
-- end
-- end
local value = self.set.nodes[nodeID];
if value then
local nodeInfo = Internal.GetNodeInfoBySpecID(self.set.specID, nodeID);
if #nodeInfo.entryIDs > 1 then
value = 1;
end
for _,cost in ipairs(nodeInfo.costs) do
self.treeCurrencyInfoMap[cost.ID].spent = self.treeCurrencyInfoMap[cost.ID].spent + (cost.amount * value);
self.treeCurrencyInfoMap[cost.ID].quantity = self.treeCurrencyInfoMap[cost.ID].quantity - (cost.amount * value);
end
end
end
if not skipButtonUpdates then
for condID, condInfo in pairs(self.condInfoCache) do
if condInfo.isGate then
self:MarkCondInfoCacheDirty(condID);
self:ForceCondInfoUpdate(condID);
end
end
self:RefreshGates();
end
end
function BtWLoadoutsDFTalentsMixin:GetAndCacheNodeInfo(nodeID)
if not self.set then
return
end
local function GetNodeInfoCallback()
local result = CopyTable(Internal.GetNodeInfoBySpecID(self.set.specID, nodeID), true);
result.canPurchaseRank = true;
result.canRefundRank = true;
result.isAvailable = true; -- Not Gated
result.meetsEdgeRequirements = true; -- Not Locked
local incomingEdges = result.incomingEdgesBySpecID and result.incomingEdgesBySpecID[self.set.specID] or result.incomingEdges
if incomingEdges and #incomingEdges > 0 then
result.meetsEdgeRequirements = false;
for _,ID in ipairs(incomingEdges) do
local from = self:GetAndCacheNodeInfo(ID)
if from.activeRank == from.maxRanks then
result.meetsEdgeRequirements = true;
break;
end
end
end
local tree = self:GetTreeInfo();
for _,condID in ipairs(result.conditionIDs) do
local cond = self:GetAndCacheCondInfo(condID);
if cond and cond.spentAmountRequired > 0 then
result.isAvailable = false;
result.canPurchaseRank = false;
break;
end
end
if self.set.nodes[nodeID] then
if not result.meetsEdgeRequirements or not result.isAvailable then
self.set.nodes[nodeID] = nil;
end
end
if self.set.nodes[nodeID] then
if #result.entryIDs == 1 then
if result.maxRanks < self.set.nodes[nodeID] then
self.set.nodes[nodeID] = result.maxRanks;
end
result.activeRank = self.set.nodes[nodeID];
result.currentRank = self.set.nodes[nodeID];
result.ranksPurchased = self.set.nodes[nodeID];
result.activeEntry.rank = self.set.nodes[nodeID];
else
result.activeRank = 1;
result.currentRank = 1;
result.ranksPurchased = 1;
result.activeEntry = {
entryID = result.entryIDs[self.set.nodes[nodeID]],
rank = 1,
}
end
elseif #result.entryIDs > 1 then
result.activeEntry = nil;
end
return result;
end
return GetOrCreateTableEntryByCallback(self.nodeInfoCache, nodeID, GetNodeInfoCallback);
end
function BtWLoadoutsDFTalentsMixin:GetAndCacheDefinitionInfo(definitionID)
local function GetDefinitionInfoCallback()
-- self.dirtyDefinitionIDSet[definitionID] = nil;
return C_Traits.GetDefinitionInfo(definitionID);
end
return GetOrCreateTableEntryByCallback(self.definitionInfoCache, definitionID, GetDefinitionInfoCallback);
end
function BtWLoadoutsDFTalentsMixin:GetAndCacheEntryInfo(entryID)
local function GetEntryInfoCallback()
-- self.dirtyEntryIDSet[entryID] = nil;
return C_Traits.GetEntryInfo(C_ClassTalents.GetActiveConfigID(), entryID);
end
return GetOrCreateTableEntryByCallback(self.entryInfoCache, entryID, GetEntryInfoCallback);
end
function BtWLoadoutsDFTalentsMixin:GetAndCacheCondInfo(condID)
local function GetCondInfoCallback()
local tree = self:GetTreeInfo();
for _,gate in ipairs(tree.gates) do
if gate.conditionID == condID then
local result = {
isGate = true,
isAlwaysMet = false,
condID = gate.conditionID,
spentAmountRequired = gate.spentAmountRequired,
traitCurrencyID = gate.traitCurrencyID,
}
local spent = 0;
for _,nodeID in ipairs(C_Traits.GetTreeNodes(tree.ID)) do
local nodeInfo = Internal.GetNodeInfoBySpecID(self.set.specID, nodeID);
if nodeInfo and self.set.nodes[nodeID] and not tContains(nodeInfo.conditionIDs, gate.conditionID) then
local purchased = #nodeInfo.entryIDs == 1 and self.set.nodes[nodeID] or 1;
for _,cost in ipairs(nodeInfo.costs) do
if cost.ID == gate.traitCurrencyID then
spent = spent + (cost.amount * purchased);
end
end
end
end
result.spentAmountRequired = math.max(0, result.spentAmountRequired - spent);
result.isMet = result.spentAmountRequired == 0;
return result;
end
end
end
return GetOrCreateTableEntryByCallback(self.condInfoCache, condID, GetCondInfoCallback);
end
function BtWLoadoutsDFTalentsMixin:GetNodeCost(nodeID)
local nodeInfo = self:GetAndCacheNodeInfo(nodeID)
return nodeInfo.costs;
end
function BtWLoadoutsDFTalentsMixin:AddConditionsToTooltip()
end
function BtWLoadoutsDFTalentsMixin:MarkNodeDirty(nodeID)
local nodeInfo = self:GetAndCacheNodeInfo(nodeID);
for _,edge in ipairs(nodeInfo.visibleEdges) do
self:MarkNodeDirty(edge.targetNode);
end
self:MarkNodeInfoCacheDirty(nodeID);
end
function BtWLoadoutsDFTalentsMixin:PurchaseRank(nodeID)
if self.set.configID then
return
end
local nodeInfo = self:GetAndCacheNodeInfo(nodeID);
if nodeInfo.maxRanks > nodeInfo.activeRank then
self.set.nodes[nodeID] = (self.set.nodes[nodeID] or 0) + 1;
end
-- self:MarkNodeDirty(nodeID);
-- self:RefreshButtons()
self:Update();
end
function BtWLoadoutsDFTalentsMixin:RefundRank(nodeID)
if self.set.configID then
return
end
if self.set.nodes[nodeID] then
self.set.nodes[nodeID] = (self.set.nodes[nodeID] or 0) - 1;
if self.set.nodes[nodeID] <= 0 then
self.set.nodes[nodeID] = nil;
end
-- self:MarkNodeDirty(nodeID);
self:Update();
end
end
function BtWLoadoutsDFTalentsMixin:GetSpecializedSelectionChoiceMixin(entryInfo, talentType)
return DFTalentSelectionChoiceMixin;
end
function BtWLoadoutsDFTalentsMixin:SetSelection(nodeID, entryID)
if self.set.configID then
return
end
if entryID == nil then
self.set.nodes[nodeID] = nil;
else
local nodeInfo = self:GetAndCacheNodeInfo(nodeID)
for index,nodeEntryID in ipairs(nodeInfo.entryIDs) do
if nodeEntryID == entryID then
self.set.nodes[nodeID] = index;
end
end
end
-- self:MarkNodeDirty(nodeID);
self:Update();
end
function BtWLoadoutsDFTalentsMixin:InstantiateTalentButton(nodeID, nodeInfo)
nodeInfo = nodeInfo or self:GetAndCacheNodeInfo(nodeID);
if not nodeInfo.isVisible and not self:ShouldInstantiateInvisibleButtons() then
return nil;
end
local activeEntryID = nodeInfo.activeEntry and nodeInfo.activeEntry.entryID or nil;
local entryInfo = (activeEntryID ~= nil) and self:GetAndCacheEntryInfo(activeEntryID) or nil;
local talentType = (entryInfo ~= nil) and entryInfo.type or nil;
local function InitTalentButton(newTalentButton)
newTalentButton:SetNodeID(nodeID);
end
local newTalentButton = self:AcquireTalentButton(nodeInfo, talentType, nil, nil, InitTalentButton);
if newTalentButton then
TalentButtonUtil.ApplyPosition(newTalentButton, self, nodeInfo.posX + (self.offsetX or 0), nodeInfo.posY + (self.offsetY or 0));
local frameLevel = newTalentButton:GetParent():GetFrameLevel() + self:GetFrameLevelForButton(nodeInfo);
self:SetElementFrameLevel(newTalentButton, frameLevel);
newTalentButton:Show();
end
return newTalentButton;
end
local function GetSetsForCharacter(tbl, slug)
tbl = tbl or {}
for _,set in pairs(BtWLoadoutsSets.dftalents) do
if type(set) == "table" and set.character == slug then
tbl[#tbl+1] = set
end
end
return tbl
end
-- Character deletion
Internal.OnEvent("CHARACTER_DELETE", function (event, slug)
local sets = GetSetsForCharacter({}, slug)
for _,set in ipairs(sets) do
DeleteSet(set.setID)
end
return true
end)