local ADDON_NAME,Internal = ... local L = Internal.L local Settings = Internal.Settings local UnitClass = UnitClass; local GetClassColor = C_ClassColor.GetClassColor; local LOCALIZED_CLASS_NAMES_MALE = LOCALIZED_CLASS_NAMES_MALE; local MAX_TALENT_TIERS = MAX_TALENT_TIERS; local LearnTalent = LearnTalent; local GetTalentInfo = GetTalentInfo; local GetTalentTierInfo = GetTalentTierInfo; local GetTalentInfoByID = GetTalentInfoByID local GetTalentInfoForSpecID = Internal.GetTalentInfoForSpecID; local GetSpecialization = GetSpecialization; local GetSpecializationInfo = GetSpecializationInfo; local GetSpecializationInfoByID = GetSpecializationInfoByID; local UIDropDownMenu_SetText = UIDropDownMenu_SetText; local UIDropDownMenu_EnableDropDown = UIDropDownMenu_EnableDropDown; local UIDropDownMenu_DisableDropDown = UIDropDownMenu_DisableDropDown; local UIDropDownMenu_SetSelectedValue = UIDropDownMenu_SetSelectedValue; local format = string.format local AddSet = Internal.AddSet; local DeleteSet = Internal.DeleteSet; local HelpTipBox_Anchor = Internal.HelpTipBox_Anchor; local HelpTipBox_SetText = Internal.HelpTipBox_SetText; do -- Filter chat spam local filters = { string.gsub(ERR_LEARN_ABILITY_S, "%%s", "(.*)"), string.gsub(ERR_LEARN_SPELL_S, "%%s", "(.*)"), string.gsub(ERR_LEARN_PASSIVE_S, "%%s", "(.*)"), string.gsub(ERR_SPELL_UNLEARNED_S, "%%s", "(.*)"), } local function ChatFrame_FilterTalentChanges(self, event, msg, ...) if Settings.filterChatSpam then for _,pattern in ipairs(filters) do if string.match(msg, pattern) then return true end end end return false, msg, ... end Internal.OnEvent("LOADOUT_CHANGE_START", function () ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", ChatFrame_FilterTalentChanges) end) Internal.OnEvent("LOADOUT_CHANGE_END", function () ChatFrame_RemoveMessageEventFilter("CHAT_MSG_SYSTEM", ChatFrame_FilterTalentChanges) end) end do -- Prevent new spells from flying to the action bar local WasEventRegistered Internal.OnEvent("LOADOUT_CHANGE_START", function () WasEventRegistered = IconIntroTracker:IsEventRegistered("SPELL_PUSHED_TO_ACTIONBAR") IconIntroTracker:UnregisterEvent("SPELL_PUSHED_TO_ACTIONBAR") end) Internal.OnEvent("LOADOUT_CHANGE_END", function () if WasEventRegistered then IconIntroTracker:RegisterEvent("SPELL_PUSHED_TO_ACTIONBAR") end end) end -- Make sure talent sets dont have incorrect id, call from GetTalentSet and the UI? local function FixTalentSet(set) local temp = {} local changed = false for talentID in pairs(set.talents) do local tier, column = Internal.VerifyTalentForSpec(set.specID, talentID) if tier == nil or temp[tier] then set.talents[talentID] = nil changed = true else temp[tier] = talentID end end return changed 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 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 set.filters = filters return set end local function GetTalentSet(id) if type(id) == "table" then return id; else return BtWLoadoutsSets.talents[id]; end; end -- In General, For Player, For Player Spec local function TalentSetIsValid(set) set = GetTalentSet(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 -- Check if the talents in the table talentIDs are selected local function IsTalentSetActive(set) for talentID in pairs(set.talents) do local _, _, _, selected, _, _, _, tier = GetTalentInfoByID(talentID, 1); local tierAvailable = GetTalentTierInfo(tier, 1) -- For lower level characters just ignore tiers over their currently available if tierAvailable and not selected then return false; end end return true; end --[[ Activate a talent set return complete, dirty ]] local function ActivateTalentSet(set, state) local success, complete = true, true; local canChangeTalents = not Internal.GetRestedTomeBlocker():IsActive() for talentID in pairs(set.talents) do local selected, _, _, _, tier = select(4, GetTalentInfoByID(talentID, 1)); local available, currentColumn = GetTalentTierInfo(tier, 1) if not selected and available then if canChangeTalents or currentColumn == 0 then local slotSuccess = LearnTalent(talentID) success = slotSuccess and success complete = false Internal.LogMessage("Switching talent %d to %s (%s)", tier, GetTalentLink(talentID, 1), slotSuccess and "true" or "false") end end end return complete, false; end local function RefreshTalentSet(set) local talents = set.talents or {} local specID, specName = GetSpecializationInfo(GetSpecialization()); if specID == set.specID then wipe(talents) for tier=1,MAX_TALENT_TIERS do local _, column = GetTalentTierInfo(tier, 1); local talentID = GetTalentInfo(tier, column, 1); if talentID then talents[talentID] = true; end end end set.talents = talents return UpdateSetFilters(set) end local function AddTalentSet() local specIndex = GetSpecialization() if specIndex == 5 then specIndex = 1 end local specID, specName = GetSpecializationInfo(specIndex); return AddSet("talents", RefreshTalentSet({ specID = specID, name = format(L["New %s Set"], specName), useCount = 0, talents = {}, })) end local function TalentSetDelay(set) for talentID in pairs(set.talents) do local row = select(8, GetTalentInfoByID(talentID, 1)) local column = select(2, GetTalentTierInfo(row, 1)) local selectedTalentID, _, _, _, _, spellID = GetTalentInfo(row, column, 1) if selectedTalentID ~= talentID and 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 end return false end --[[ Check what is needed to activate this talent set return isActive, waitForCooldown, anySelected ]] local function TalentSetRequirements(set) local isActive, waitForCooldown, anySelected = true, false, false for talentID in pairs(set.talents) do local row = select(8, GetTalentInfoByID(talentID, 1)) local available, column = GetTalentTierInfo(row, 1) if available then local selectedTalentID, _, _, _, _, spellID = GetTalentInfo(row, column, 1) if selectedTalentID ~= talentID then isActive = false 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) waitForCooldown = true break -- We dont actually need to check anything more end end if column ~= 0 then anySelected = true end end end end return isActive, waitForCooldown, anySelected end local function GetTalentSetsByName(name) return Internal.GetSetsByName("talents", name) end local function GetTalentSetByName(name) return Internal.GetSetByName("talents", name, TalentSetIsValid) end local function GetTalentSets(id, ...) if id ~= nil then return GetTalentSet(id), GetTalentSets(...); end end local function GetTalentSetIfNeeded(id) if id == nil then return; end local set = Internal.GetTalentSet(id); if IsTalentSetActive(set) then return; end return set; end local talentSetsByTier = {}; local function CombineTalentSets(result, state, ...) result = result or {}; result.talents = {}; wipe(talentSetsByTier); for i=1,select('#', ...) do local set = Internal.GetTalentSet(select(i, ...)); if Internal.AreRestrictionsValidForPlayer(set.restrictions) then for talentID in pairs(set.talents) do if result.talents[talentID] == nil then local tier = select(8, GetTalentInfoByID(talentID, 1)); if (GetTalentTierInfo(tier, 1)) then if talentSetsByTier[tier] then result.talents[talentSetsByTier[tier]] = nil; end result.talents[talentID] = true; talentSetsByTier[tier] = talentID; end end end end end if state then local isActive, waitForCooldown, anySelected = TalentSetRequirements(result) if not isActive then if state.blockers then state.blockers[Internal.GetRestedTomeBlocker()] = 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 DeleteTalentSet(id) Internal.DeleteSet(BtWLoadoutsSets.talents, 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.talents) do if setID == id then table.remove(set.talents, index) end end end end local frame = BtWLoadoutsFrame.Talents; local set = frame.set; if set.setID == id then frame.set = nil;-- = select(2,next(BtWLoadoutsSets.talents)) or {}; BtWLoadoutsFrame:Update(); end end local function CheckErrors(errorState, set) set = GetTalentSet(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 Internal.FixTalentSet = FixTalentSet Internal.GetTalentSet = GetTalentSet Internal.GetTalentSets = GetTalentSets Internal.GetTalentSetIfNeeded = GetTalentSetIfNeeded Internal.GetTalentSetsByName = GetTalentSetsByName Internal.GetTalentSetByName = GetTalentSetByName Internal.TalentSetDelay = TalentSetDelay Internal.AddTalentSet = AddTalentSet Internal.RefreshTalentSet = RefreshTalentSet Internal.DeleteTalentSet = DeleteTalentSet Internal.ActivateTalentSet = ActivateTalentSet Internal.IsTalentSetActive = IsTalentSetActive Internal.CombineTalentSets = CombineTalentSets Internal.GetTalentSets = GetTalentSets -- Initializes the set dropdown menu for the Loadouts page local function SetDropDownInit(self, set, index) Internal.SetDropDownInit(self, set, index, "talents", BtWLoadoutsFrame.Talents) end local function CompareSets(a, b) if not tCompare(a.talents, b.talents, 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 Internal.AddLoadoutSegment({ id = "talents", name = L["Talents"], events = "PLAYER_TALENT_UPDATE", enabled = select(4, GetBuildInfo()) < 100000, add = AddTalentSet, get = GetTalentSets, getByName = GetTalentSetByName, combine = CombineTalentSets, isActive = IsTalentSetActive, activate = ActivateTalentSet, dropdowninit = SetDropDownInit, checkerrors = CheckErrors, export = function (set) return { version = 1, name = set.name, specID = set.specID, talents = set.talents, restrictions = set.restrictions, } end, import = function (source, version, name, ...) assert(version == 1) local specID = source.specID or ... return AddSet("talents", UpdateSetFilters({ specID = specID, name = name or source.name, useCount = 0, talents = source.talents, restrictions = source.restrictions, })) end, getByValue = function (set) return Internal.GetSetByValue(BtWLoadoutsSets.talents, set, CompareSets) end, verify = function (source, ...) local specID = source.specID or ... if not specID or not GetSpecializationInfoByID(specID) then return false, L["Invalid specialization"] end if type(source.talents) ~= "table" then return false, L["Missing talents"] end if source.restrictions ~= nil and type(source.restrictions) ~= "table" then return false, L["Missing restrictions"] end -- @TODO verify talent ids? return true end, }) BtWLoadoutsTalentsMixin = {} function BtWLoadoutsTalentsMixin:OnLoad() self.RestrictionsDropDown:SetSupportedTypes("covenant", "race") self.RestrictionsDropDown:SetScript("OnChange", function () self:Update() end) self.temp = {}; -- Stores talents for currently unselected specs incase the user switches to them self.talentIDs = {} for tier=1,MAX_TALENT_TIERS do self.talentIDs[tier] = {} end end function BtWLoadoutsTalentsMixin:OnShow() if not self.initialized then self.SpecDropDown.includeNone = false; self.SpecDropDown.includeClass = false; UIDropDownMenu_SetWidth(self.SpecDropDown, 170); UIDropDownMenu_JustifyText(self.SpecDropDown, "LEFT"); self.SpecDropDown.GetValue = function () if self.set then return self.set.specID end end self.SpecDropDown.SetValue = function (_, _, arg1) CloseDropDownMenus(); local set = self.set; if set then local temp = self.temp; -- @TODO: If we always access talents by set.talents then we can just swap tables in and out of -- the temp table instead of copying the talentIDs around -- We are going to copy the currently selected talents for the currently selected spec into -- a temporary table incase the user switches specs back local specID = set.specID; if temp[specID] then wipe(temp[specID]); else temp[specID] = {}; end for talentID in pairs(set.talents) do temp[specID][talentID] = true; end -- Clear the current talents and copy back the previously selected talents if they exist specID = arg1; set.specID = specID; wipe(set.talents); if temp[specID] then for talentID in pairs(temp[specID]) do set.talents[talentID] = true; end end self:Update() end end self.initialized = true; end end function BtWLoadoutsTalentsMixin:ChangeSet(set) self.set = set wipe(self.temp); self:Update() end function BtWLoadoutsTalentsMixin:UpdateSetName(value) if self.set and self.set.name ~= not value then self.set.name = value; self:Update(); end end function BtWLoadoutsTalentsMixin:OnButtonClick(button) CloseDropDownMenus() if button.isAdd then BtWLoadoutsHelpTipFlags["TUTORIAL_NEW_SET"] = true; self.Name:ClearFocus() self:ChangeSet(AddTalentSet()) C_Timer.After(0, function () self.Name:HighlightText() self.Name:SetFocus() end) elseif button.isDelete then local set = self.set if set.useCount > 0 then StaticPopup_Show("BTWLOADOUTS_DELETEINUSESET", set.name, nil, { set = set, func = DeleteTalentSet, }) else StaticPopup_Show("BTWLOADOUTS_DELETESET", set.name, nil, { set = set, func = DeleteTalentSet, }) end elseif button.isRefresh then local set = self.set; RefreshTalentSet(set) self:Update() elseif button.isExport then local set = self.set; self:GetParent():SetExport(Internal.Export("talents", set.setID)) elseif button.isActivate then local set = self.set; if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then Internal.ActivateProfile({ talents = {set.setID} }); end end end function BtWLoadoutsTalentsMixin: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 = GetTalentSet(button.id); if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then Internal.ActivateProfile({ talents = {button.id} }); end else self.Name:ClearFocus(); self:ChangeSet(GetTalentSet(button.id)) end end end function BtWLoadoutsTalentsMixin:OnSidebarItemDoubleClick(button) CloseDropDownMenus() if button.isHeader then return end local set = GetTalentSet(button.id); if select(6, GetSpecializationInfoByID(set.specID)) == select(2, UnitClass("player")) then Internal.ActivateProfile({ talents = {button.id} }); end end function BtWLoadoutsTalentsMixin:OnSidebarItemDragStart(button) CloseDropDownMenus() if button.isHeader then return end local icon = "INV_Misc_QuestionMark"; local set = GetTalentSet(button.id); local command = format("/btwloadouts activate talents %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 BtWLoadoutsTalentsMixin:Update() self:GetParent():SetTitle(L["Talents"]); local sidebar = BtWLoadoutsFrame.Sidebar sidebar:SetSupportedFilters("spec", "class", "role", "character", "covenant", "race") sidebar:SetSets(BtWLoadoutsSets.talents) sidebar:SetCollapsed(BtWLoadoutsCollapsed.talents) sidebar:SetCategories(BtWLoadoutsCategories.talents) sidebar:SetFilters(BtWLoadoutsFilters.talents) 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. Leave rows blank to skip the tier."]) self:GetParent().ExportButton:SetEnabled(true) self:GetParent().DeleteButton:SetEnabled(true); if not showingNPE then local specID = set.specID UpdateSetFilters(set) sidebar:Update() set.restrictions = set.restrictions or {} self.RestrictionsDropDown:SetSelections(set.restrictions) self.RestrictionsDropDown:SetLimitations() self.RestrictionsButton:SetEnabled(true); local selected = set.talents; if not self.Name:HasFocus() then self.Name:SetText(set.name or ""); end local _, specName, _, icon, _, classID = GetSpecializationInfoByID(specID); local className = LOCALIZED_CLASS_NAMES_MALE[classID]; local classColor = GetClassColor(classID); UIDropDownMenu_SetText(self.SpecDropDown, format("%s: %s", classColor:WrapTextInColorCode(className), specName)); for tier=1,MAX_TALENT_TIERS do local row = self.talentIDs[tier] wipe(row) for column=1,3 do row[column] = GetTalentInfoForSpecID(specID, tier, column) end self.rows[tier]:SetTalents(row); end local playerSpecIndex = GetSpecialization() self:GetParent().RefreshButton:SetEnabled(playerSpecIndex and specID == GetSpecializationInfo(playerSpecIndex)) self:GetParent().ActivateButton:SetEnabled(classID == select(2, 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 = GetClassColor(classID); UIDropDownMenu_SetText(self.SpecDropDown, format("%s: %s", classColor:WrapTextInColorCode(className), specName)); for tier=1,MAX_TALENT_TIERS do local row = self.talentIDs[tier] wipe(row) for column=1,3 do row[column] = GetTalentInfoForSpecID(specID, tier, column) end self.rows[tier]:SetTalents(row); end local helpTipBox = self:GetParent().HelpTipBox; helpTipBox:Hide(); end end function BtWLoadoutsTalentsMixin:SetSetByID(setID) self.set = GetTalentSet(setID) end