------------------------------------------------------------------------------- ---------------------------------- NAMESPACE ---------------------------------- ------------------------------------------------------------------------------- local ADDON_NAME, ns = ... local L = ns.locale local Class = ns.Class local Group = ns.Group local IsInstance = ns.IsInstance local Requirement = ns.requirement.Requirement ------------------------------------------------------------------------------- ------------------------------------ NODE ------------------------------------- ------------------------------------------------------------------------------- --[[ Base class for all displayed nodes. label (string): Tooltip title for this node sublabel (string): Oneline string to display under label group (Group): Options group for this node (display, scale, alpha) fgroup (string): A category of nodes that should be focused together icon (string|number): The icon texture to display alpha (float): The default alpha value for this type scale (float): The default scale value for this type minimap (bool): Should the node be displayed on the minimap parent (int|int[]): Parent map IDs to display the node on quest (int|int[]): Quest IDs that cause this node to disappear questAny (boolean): Hide node if *any* quests are true (default *all*) questCount (boolean): Display completed quest count as rlabel questDeps (int|int[]): Quest IDs that must be true to appear requires (str|Requirement[]): Requirements to interact or unlock rewards (Reward[]): Array of rewards for this node --]] local Node = Class('Node', nil, { label = UNKNOWN, minimap = true, alpha = 1, scale = 1, icon = 'default', group = ns.groups.MISC }) function Node:Initialize(attrs) -- assign all attributes if attrs then for k, v in pairs(attrs) do self[k] = v end end -- normalize table values self.quest = ns.AsTable(self.quest) self.questDeps = ns.AsTable(self.questDeps) self.parent = ns.AsIDTable(self.parent) self.requires = ns.AsTable(self.requires, Requirement) self.group = ns.AsTable(self.group, Group) self.vignette = ns.AsTable(self.vignette) -- ensure proper group(s) is/are assigned for _, group in pairs(self.group) do if not IsInstance(group, Group) then error('group attribute must be a Group class instance: ' .. group) end end end --[[ Return the associated texture, scale and alpha value to pass to HandyNotes for this node. --]] function Node:GetDisplayInfo(mapID, minimap) local icon = ns.GetIconPath(self.icon) local scale = self.scale * self.group[1]:GetScale(mapID) -- Get scale/alpha form first (main) group local alpha = self.alpha * self.group[1]:GetAlpha(mapID) if not minimap and WorldMapFrame.isMaximized and ns:GetOpt('maximized_enlarged') then scale = scale * 1.3 -- enlarge on maximized world map end return icon, scale, alpha end --[[ Return the glow POI for this node. If the node is hovered or focused, a green glow is applyed to help highlight the node. --]] function Node:GetGlow(mapID, minimap, focused) if self.glow then local _, scale, alpha = self:GetDisplayInfo(mapID, minimap) self.glow.alpha = alpha self.glow.scale = scale if focused then self.glow.r, self.glow.g, self.glow.b = 0, 1, 0 elseif self.OnClick then self.glow.r, self.glow.g, self.glow.b = 0, 0, 1 else self.glow.r, self.glow.g, self.glow.b = 1, 1, 0 end return self.glow end end --[[ Return the "collected" status of this node. A node is collected if all associated rewards have been obtained (achievements, toys, pets, mounts). --]] function Node:IsCollected() for reward in self:IterateRewards() do if reward:IsEnabled() and reward:IsObtainable() and not reward:IsObtained() then return false end end return true end --[[ Return the "completed" state of this node. A node is completed if any or all associated quests have been completed. The behavior of any vs all is switched with the `questAny` attribute (default: all). This method can also be overridden to check for some other form of completion, such as an achievement criteria. This method is *not* called if the "Show completed" setting is enabled. --]] function Node:IsCompleted() if self.quest and self.questAny then -- Completed if *any* attached quest ids are true for i, quest in ipairs(self.quest) do if C_QuestLog.IsQuestFlaggedCompleted(quest) then return true end end elseif self.quest then -- Completed only if *all* attached quest ids are true for i, quest in ipairs(self.quest) do if not C_QuestLog.IsQuestFlaggedCompleted(quest) then return false end end return true end return false end --[[ Return true if this node should be displayed. --]] function Node:IsEnabled() -- Check prerequisites if not self:PrerequisiteCompleted() then return false end -- Check completed state if self.group == ns.groups.QUEST or not ns:GetOpt('show_completed_nodes') then if self:IsCompleted() then return false end end if self.class and self.class ~= ns.class then return false end return true end --[[ Iterate over rewards that are enabled for this character. --]] function Node:IterateRewards() local index, reward = 0, nil return function() if not (self.rewards and #self.rewards) then return end repeat index = index + 1 if index > #self.rewards then return end reward = self.rewards[index] until reward:IsEnabled() return reward end end --[[ Return the prerequisite state of this node. A node has its prerequisites met if all quests defined in the `questDeps` attribute are completed. This method can be overridden to check for other prerequisite criteria. --]] function Node:PrerequisiteCompleted() -- Prerequisite not met if any dependent quest ids are false if not self.questDeps then return true end for i, quest in ipairs(self.questDeps) do if not C_QuestLog.IsQuestFlaggedCompleted(quest) then return false end end return true end --[[ Prepare this node for display by fetching localization information for anything referenced in the text attributes of this node. This method is called when a world map containing this node is opened. --]] function Node:Prepare() -- verify chosen icon exists if type(self.icon) == 'string' and ns.icons[self.icon] == nil then error('unknown icon: ' .. self.icon) end -- initialize glow POI (if glow icon available) if not self.glow then local icon = ns.GetGlowPath(self.icon) if icon then self.glow = ns.poi.Glow({icon = icon}) end end ns.PrepareLinks(self.label) ns.PrepareLinks(self.sublabel) ns.PrepareLinks(self.location) ns.PrepareLinks(self.note) if self.requires then for i, req in ipairs(self.requires) do if IsInstance(req, Requirement) then ns.PrepareLinks(req:GetText()) else ns.PrepareLinks(req) end end end if self.rewards then for i, reward in ipairs(self.rewards) do reward:Prepare() end end end --[[ Render this node onto the given tooltip. Many features are optional depending on the attributes set on this specific node, such as setting an `rlabel` or `sublabel` value. --]] function Node:Render(tooltip, focusable) -- render the label text with NPC names resolved tooltip:SetText(ns.RenderLinks(self.label, true)) local color, text local rlabel = self.rlabel or '' if self.questCount and self.quest and #self.quest then -- set rlabel to a (completed / total) display for quest ids local count = 0 for i, quest in ipairs(self.quest) do if C_QuestLog.IsQuestFlaggedCompleted(quest) then count = count + 1 end end color = (count == #self.quest) and ns.status.Green or ns.status.Gray rlabel = rlabel .. ' ' .. color(tostring(count) .. '/' .. #self.quest) end if self.faction then rlabel = rlabel .. ' ' .. ns.GetIconLink(self.faction:lower(), 16, 1, -1) end if focusable then -- add an rlabel hint to use left-mouse to focus the node local focus = ns.GetIconLink('left_mouse', 12) .. ns.status.Gray(L['focus']) rlabel = (#rlabel > 0) and focus .. ' ' .. rlabel or focus end if self.OnClick then local click = ns.GetIconLink('left_mouse', 12) rlabel = click .. ' ' .. ns.status.Gray(self.clabel) or click end -- render top-right label text if #rlabel > 0 then local rtext = _G[tooltip:GetName() .. 'TextRight1'] rtext:SetTextColor(1, 1, 1) rtext:SetText(rlabel) rtext:Show() end -- optional text directly under sublabel/label for development notes if self.devnote and ns:GetOpt('development') then tooltip:AddLine(ns.RenderLinks(self.devnote), 1, 0, 1) end -- optional text directly under sublabel/label for development notes if self.areaPOI and ns:GetOpt('development') then tooltip:AddLine(ns.RenderLinks('Poi ID: ' .. self.areaPOI), 0.58, 0.43, 0.84) end -- optional text directly under label if self.sublabel then tooltip:AddLine(ns.RenderLinks(self.sublabel, true), 1, 1, 1) end -- display item, spell or other requirements if self.requires then for i, req in ipairs(self.requires) do if IsInstance(req, Requirement) then color = req:IsMet() and ns.color.White or ns.color.Red text = color(L['requires'] .. ' ' .. req:GetText()) else text = ns.color.Red(L['requires'] .. ' ' .. req) end tooltip:AddLine(ns.RenderLinks(text, true)) end end -- additional text for the node to describe where the object or -- rare can be found if self.location and ns:GetOpt('show_notes') then if self.requires or self.sublabel then GameTooltip_AddBlankLineToTooltip(tooltip) end tooltip:AddLine(ns.RenderLinks(self.location), 1, 1, 1, true) end -- adds text if the node spawns in a specific rotation if self.interval then if self.requires or self.sublabel or self.location then GameTooltip_AddBlankLineToTooltip(tooltip) end tooltip:AddLine(ns.RenderLinks(self.interval:GetText()), 1, 1, 1, true) end -- additional text for the node to describe how to interact with the -- object or summon the rare if self.note and ns:GetOpt('show_notes') then if self.requires or self.sublabel or self.location or self.interval then GameTooltip_AddBlankLineToTooltip(tooltip) end tooltip:AddLine(ns.RenderLinks(self.note), 1, 1, 1, true) end -- all rewards (achievements, pets, mounts, toys, quests) that can be -- collected or completed from this node if self.rewards and ns:GetOpt('show_loot') then self:RenderRewards(tooltip) end if self.spellID then local spell = Spell:CreateFromSpellID(self.spellID) self.cancelSpellDataCallback = spell:ContinueWithCancelOnSpellLoad( function() GameTooltip_AddBlankLineToTooltip(tooltip) EmbeddedItemTooltip_SetSpellWithTextureByID(tooltip.ItemTooltip, self.spellID, spell:GetSpellTexture()) self.cancelSpellDataCallback = nil end); end end function Node:RenderRewards(tooltip) local firstAchieve, firstOther = true, true for reward in self:IterateRewards() do -- Add a blank line between achievements and other rewards local isAchieve = ns.IsInstance(reward, ns.reward.Achievement) local isSpacer = ns.IsInstance(reward, ns.reward.Spacer) if isAchieve and firstAchieve then tooltip:AddLine(' ') firstAchieve = false elseif not (isAchieve or isSpacer) and firstOther then tooltip:AddLine(' ') firstOther = false end reward:Render(tooltip) end end function Node:Unrender(tooltip) if self.cancelSpellDataCallback then self.cancelSpellDataCallback() self.cancelSpellDataCallback = nil end end ------------------------------------------------------------------------------- --------------------------------- COLLECTIBLE --------------------------------- ------------------------------------------------------------------------------- local Collectible = Class('Collectible', Node) function Collectible.getters:label() if self.id then return ('{npc:%d}'):format(self.id) end if self.item then return ('{item:%d}'):format(self.item) end for reward in self:IterateRewards() do if IsInstance(reward, ns.reward.Achievement) then return GetAchievementCriteriaInfoByID(reward.id, reward.criteria[1].id) or UNKNOWN end end return UNKNOWN end function Collectible:IsCompleted() if self:IsCollected() then return true end return Node.IsCompleted(self) end ------------------------------------------------------------------------------- ------------------------------------ INTRO ------------------------------------ ------------------------------------------------------------------------------- local Intro = Class('Intro', Node, { icon = 'quest_ay', scale = 3, group = ns.groups.QUEST, minimap = false }) function Intro:Initialize(attrs) Node.Initialize(self, attrs) if self.quest then C_QuestLog.GetTitleForQuestID(self.quest[1]) -- fetch info from server end end function Intro.getters:label() if self.quest then return C_QuestLog.GetTitleForQuestID(self.quest[1]) or UNKNOWN end return UNKNOWN end ------------------------------------------------------------------------------- ------------------------------------ ITEM ------------------------------------- ------------------------------------------------------------------------------- local Item = Class('Item', Node, {icon = 454046}) function Item:Initialize(attrs) Node.Initialize(self, attrs) if not self.id then error('id required for Item nodes') end if not self.icon then self.icon = 454046 -- temp loading icon local item = _G.Item:CreateFromItemID(self.id) if not item:IsItemEmpty() then item:ContinueOnItemLoad(function() self.icon = item:GetItemIcon() end) end end end function Item:IsCompleted() if ns.PlayerHasItem(self.id) then return true end return Node.IsCompleted(self) end function Item:Render(tooltip, focusable) Node.Render(self, tooltip, focusable) GameTooltip_AddBlankLineToTooltip(tooltip) EmbeddedItemTooltip_SetItemByID(tooltip.ItemTooltip, self.id) end function Item.getters:label() return ('{item:%d}'):format(self.id) end ------------------------------------------------------------------------------- ------------------------------------- NPC ------------------------------------- ------------------------------------------------------------------------------- local NPC = Class('NPC', Node) function NPC:Initialize(attrs) Node.Initialize(self, attrs) if not self.id then error('id required for NPC nodes') end end function NPC.getters:label() return ('{npc:%d}'):format(self.id) end ------------------------------------------------------------------------------- ---------------------------------- PETBATTLE ---------------------------------- ------------------------------------------------------------------------------- local PetBattle = Class('PetBattle', NPC, { icon = 'paw_y', scale = 1.2, group = ns.groups.PETBATTLE }) ------------------------------------------------------------------------------- ------------------------------------ QUEST ------------------------------------ ------------------------------------------------------------------------------- local Quest = Class('Quest', Node, {note = AVAILABLE_QUEST, group = ns.groups.QUEST}) function Quest:Initialize(attrs) Node.Initialize(self, attrs) C_QuestLog.GetTitleForQuestID(self.quest[1]) -- fetch info from server end function Quest.getters:icon() return self.daily and 'quest_ab' or 'quest_ay' end function Quest.getters:label() return C_QuestLog.GetTitleForQuestID(self.quest[1]) or UNKNOWN end ------------------------------------------------------------------------------- ------------------------------------ RARE ------------------------------------- ------------------------------------------------------------------------------- local Rare = Class('Rare', NPC, {scale = 1.2, group = ns.groups.RARE}) function Rare.getters:icon() return self:IsCollected() and 'skull_w' or 'skull_b' end function Rare:IsEnabled() if ns:GetOpt('hide_done_rares') and self:IsCollected() then return false end return NPC.IsEnabled(self) end ------------------------------------------------------------------------------- ---------------------------------- TREASURE ----------------------------------- ------------------------------------------------------------------------------- local Treasure = Class('Treasure', Node, { icon = 'chest_gy', scale = 1.3, group = ns.groups.TREASURE }) function Treasure.getters:label() for reward in self:IterateRewards() do if IsInstance(reward, ns.reward.Achievement) then return GetAchievementCriteriaInfoByID(reward.id, reward.criteria[1].id) or UNKNOWN end end return UNKNOWN end ------------------------------------------------------------------------------- ------------------------------- Interval Class -------------------------------- ------------------------------------------------------------------------------- local Interval = Class('Interval') function Interval:Initialize(attrs) if attrs then for k, v in pairs(attrs) do self[k] = v end end local region_initial = { [1] = self.initial.us, [2] = self.initial.kr or self.initial.tw, [3] = self.initial.eu, [5] = self.initial.cn } -- https://wowpedia.fandom.com/wiki/API_GetCurrentRegion if self.id then self.SpawnTime = self.id * self.offset + (region_initial[GetCurrentRegion()] or self.initial.us) end end function Interval:Next() if not (self.id and self.initial and self.interval) then return false end local CurrentTime = GetServerTime() local SpawnTime = self.SpawnTime local NextSpawn = SpawnTime + math.ceil((CurrentTime - SpawnTime) / self.interval) * self.interval local TimeLeft = NextSpawn - CurrentTime return NextSpawn, TimeLeft end function Interval:GetText() local TimeFormat = ns:GetOpt('use_standard_time') and self.format_12hrs or self.format_24hrs local NextSpawn, TimeLeft = self:Next() local SpawnsIn = TimeLeft <= 60 and L['now'] or SecondsToTime(TimeLeft, true, true) if self.yellow and self.green then local color = ns.color.Orange if TimeLeft < self.yellow then color = ns.color.Yellow end if TimeLeft < self.green then color = ns.color.Green end SpawnsIn = color(SpawnsIn) end local text = format('%s (%s)', SpawnsIn, date(TimeFormat, NextSpawn)) if self.text then text = format(self.text, text) end ns.PrepareLinks(text) return text end ------------------------------------------------------------------------------- ns.node = { Collectible = Collectible, Intro = Intro, Item = Item, Node = Node, NPC = NPC, PetBattle = PetBattle, Quest = Quest, Rare = Rare, Treasure = Treasure } ns.Interval = Interval