diff --git a/FieldGuide.lua b/FieldGuide.lua index d0abbca..362a8c1 100644 --- a/FieldGuide.lua +++ b/FieldGuide.lua @@ -1,17 +1,20 @@ --[[ TODO: --------------------------------------- - 1. Add Warlock/Hunter pet skills. - 2. Add tomes/spells learned through quests. - 3. (Allow player to scroll manually.) - 4. (Make it so the scroll doesn't reset back to the top after each filtering option changes.) - --------------------------------------- - - Features (in no particular order): - --------------------------------------- - 1. When clicking on weapon skill, show where the trainer is using TomTom. - 2. When clicking on any spell, show where the nearest trainer is using TomTom (maybe give player option to show nearest cheapest trainer or just nearest). - 3. (Add racials?) + 1. Add minimap icon as well. + 2. Save coordinates between sessions. + 3. When player suddenly downloads TomTom, remove all pins + 4. Add option to remove all pins somehow + 5. Add Warlock/Hunter pet skills. + 6. Add tomes/spells learned through quests. + 7. Fix TomTom for weapons. + 8. Fix TomTom for spells. + 9. Add tutorial (shift+scroll for horizontal scroll/shift+right-click for marking all of the same spells etc) + 10. (Allow player to scroll manually.) + 11. (Make it so the scroll doesn't reset back to the top after each filtering option changes.) + 12. (Add racials?) + 13. (Show where all the trainers are maybe somehow?)' + 14. Get rid of texture table, put reference in each frame: spellButton[i].texture = texture --------------------------------------- Bugs: @@ -25,6 +28,9 @@ local _, FieldGuide = ... local pairs, ipairs, select, floor = pairs, ipairs, select, math.floor local GetFactionInfoByID, IsSpellKnown, GetMoney, GetCoinTextureString = GetFactionInfoByID, IsSpellKnown, GetMoney, GetCoinTextureString +local hbd = LibStub("HereBeDragons-2.0") +local pins = LibStub("HereBeDragons-Pins-2.0") +local minimapIcon = LibStub("LibDBIcon-1.0") -- Variables. local faction = UnitFactionGroup("player") @@ -33,6 +39,8 @@ local actualClass = select(2, UnitClass("player")) local lowestLevel = 52 -- Used for figuring out which row is at the top when hiding entire rows. local currentMinLevel = 2 -- The current top row to show. local selectedClass -- The currently selected class. +local updateThrottle = 0.5 +local elapsed = 0 local emptyLevels = {} -- Holds info on if a row is empty or not. local CLASS_BACKGROUNDS = { WARRIOR = "WarriorArms", @@ -97,6 +105,30 @@ local Y_SPACING = 0 -- The spacing between all elements in y. local NBR_OF_SPELL_ROWS = 0 local NBR_OF_SPELL_COLUMNS = 0 +-- Adds a pin to the world map with the given mapId, x, y, and description. +local function addMapPin(map, x, y, desc) + local mapName = hbd:GetLocalizedMap(map) + local coordString = string.format("%.2f, %.2f", x * 100, y * 100) + if IsAddOnLoaded("TomTom") then + _G["TomTom"]:AddWaypoint(map, x, y, {title = desc}) + else + local pin = FieldGuide:getPin() + pin.map = map + pin.x, pin.y = y, x + pin.desc = desc + pin.mapName = mapName + pin.coordString = coordString + pins:AddWorldMapIconMap("FieldGuideFrame", pin, map, x, y, 3) + end + print("Your closest trainer is " .. desc .. " in " .. mapName .. " at " .. coordString .. ".") +end + +-- Removes the given pin from the world map. +local function removeMapPin(pin) + pins:RemoveWorldMapIcon("FieldGuideFrame", pin) + pin.used = false +end + -- Returns true if the player is Alliance, false otherwise. local function isAlliance() return faction == "Alliance" @@ -127,10 +159,10 @@ end local function toggleMinimapButton() FieldGuideOptions.minimapTable.hide = not FieldGuideOptions.minimapTable.hide if FieldGuideOptions.minimapTable.hide then - FieldGuide.minimapIcon:Hide("FieldGuide") + minimapIcon:Hide("FieldGuide") print("Minimap button hidden. Type /fg minimap to show it again.") else - FieldGuide.minimapIcon:Show("FieldGuide") + minimapIcon:Show("FieldGuide") end end @@ -150,7 +182,6 @@ end -- Initializes the minimap button. local function initMinimapButton() - FieldGuide.minimapIcon = LibStub("LibDBIcon-1.0") local obj = LibStub:GetLibrary("LibDataBroker-1.1"):NewDataObject("FieldGuide", { type = "launcher", text = "Field Guide", @@ -173,7 +204,7 @@ local function initMinimapButton() GameTooltip:Hide() end }) - FieldGuide.minimapIcon:Register("FieldGuide", obj, FieldGuideOptions.minimapTable) + minimapIcon:Register("FieldGuide", obj, FieldGuideOptions.minimapTable) end -- Initializes all checkboxes. @@ -209,6 +240,8 @@ local function updateFrame(texture, frame, info) texture:SetVertexColor(1, 1, 1) end frame:Hide() -- So that tooltip updates when scrolling. + frame.name = info.name + frame.rank = info.rank frame.talent = info.talent frame.spellId = info.id frame.spellCost = info.cost @@ -511,6 +544,23 @@ local function init() FieldGuideFrameHorizontalSlider:SetEnabled(false) end +-- Called whenever player clicks a pin. +function FieldGuidePin_OnClick(self, button) + removeMapPin(self) +end + +-- Called whenever player mouses over a pin. +function FieldGuidePin_OnEnter(self) + local playerX, playerY, instance = hbd:GetPlayerWorldPosition() + local destX, destY = hbd:GetWorldCoordinatesFromZone(self.x, self.y, self.map) + local distance = hbd:GetWorldDistance(instance, playerX, playerY, destX, destY) + GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT") + GameTooltip:AddLine(self.desc) + GameTooltip:AddLine(string.format("%s yards away", math.floor(distance)), 1, 1, 1) + GameTooltip:AddLine(self.mapName .. " (" .. self.coordString .. ")", 0.7, 0.7, 0.7) + GameTooltip:Show() +end + -- Called whenever player mouses over an icon. function FieldGuideSpellButton_OnEnter(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") @@ -529,25 +579,32 @@ function FieldGuideSpellButton_OnEnter(self) end) end -function FieldGuideSpellButton_OnClick(self) - local spellName = GetSpellInfo(self.spellId) - FieldGuideOptions.unwantedSpells[self.spellId] = not FieldGuideOptions.unwantedSpells[self.spellId] - if IsShiftKeyDown() then - for level, spellIndex in pairs(FieldGuide[selectedClass]) do - for spellIndex, spellInfo in ipairs(spellIndex) do - if spellInfo.name == spellName then - FieldGuideOptions.unwantedSpells[spellInfo.id] = FieldGuideOptions.unwantedSpells[self.spellId] +-- Called whenever player clicks on a spell button. +function FieldGuideSpellButton_OnClick(self, button) + if button == "RightButton" then + local spellName = GetSpellInfo(self.spellId) + FieldGuideOptions.unwantedSpells[self.spellId] = not FieldGuideOptions.unwantedSpells[self.spellId] + if IsShiftKeyDown() then + for level, spellIndex in pairs(FieldGuide[selectedClass]) do + for spellIndex, spellInfo in ipairs(spellIndex) do + if spellInfo.name == spellName then + FieldGuideOptions.unwantedSpells[spellInfo.id] = FieldGuideOptions.unwantedSpells[self.spellId] + end end end end + updateButtons() + elseif button == "LeftButton" then + addMapPin(94, 0.5, 0.562, "Whizz Fizzlebang") end - updateButtons() end +-- Called whenever player drags a spell button.1 function FieldGuideSpellButton_OnDragStart(self, button) PickupSpell(self.spellId) end +-- Called when each spell button has loaded. function FieldGuideSpellButton_OnLoad(self) self:RegisterForDrag("LeftButton") end diff --git a/FieldGuide.toc b/FieldGuide.toc index c677133..8684c7d 100644 --- a/FieldGuide.toc +++ b/FieldGuide.toc @@ -10,6 +10,8 @@ Libraries\LibStub\LibStub.lua Libraries\CallbackHandler-1.0\CallbackHandler-1.0.lua Libraries\LibDataBroker-1.1\LibDataBroker-1.1.lua Libraries\LibDBIcon-1.0\LibDBIcon-1.0.lua +Libraries\HereBeDragons-2.0\HereBeDragons-2.0.lua +Libraries\HereBeDragons-2.0\HereBeDragons-Pins-2.0.lua # Load utilites. Util.lua diff --git a/FieldGuide.xml b/FieldGuide.xml index a17d63f..59053de 100644 --- a/FieldGuide.xml +++ b/FieldGuide.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +82,7 @@ - + diff --git a/Libraries/HereBeDragons-2.0/HereBeDragons-2.0.lua b/Libraries/HereBeDragons-2.0/HereBeDragons-2.0.lua new file mode 100644 index 0000000..bbfdad5 --- /dev/null +++ b/Libraries/HereBeDragons-2.0/HereBeDragons-2.0.lua @@ -0,0 +1,534 @@ +-- HereBeDragons is a data API for the World of Warcraft mapping system + +local MAJOR, MINOR = "HereBeDragons-2.0", 9 +assert(LibStub, MAJOR .. " requires LibStub") + +local HereBeDragons, oldversion = LibStub:NewLibrary(MAJOR, MINOR) +if not HereBeDragons then return end + +local CBH = LibStub("CallbackHandler-1.0") + +HereBeDragons.eventFrame = HereBeDragons.eventFrame or CreateFrame("Frame") + +HereBeDragons.mapData = HereBeDragons.mapData or {} +HereBeDragons.worldMapData = HereBeDragons.worldMapData or {} +HereBeDragons.transforms = HereBeDragons.transforms or {} +HereBeDragons.callbacks = HereBeDragons.callbacks or CBH:New(HereBeDragons, nil, nil, false) + +local WoWClassic = select(4, GetBuildInfo()) < 20000 + +-- Data Constants +local COSMIC_MAP_ID = 946 +local WORLD_MAP_ID = 947 + +-- Lua upvalues +local PI2 = math.pi * 2 +local atan2 = math.atan2 +local pairs, ipairs = pairs, ipairs + +-- WoW API upvalues +local UnitPosition = UnitPosition +local C_Map = C_Map + +-- data table upvalues +local mapData = HereBeDragons.mapData -- table { width, height, left, top, .instance, .name, .mapType } +local worldMapData = HereBeDragons.worldMapData -- table { width, height, left, top } +local transforms = HereBeDragons.transforms + +local currentPlayerUIMapID, currentPlayerUIMapType + +-- Override instance ids for phased content +local instanceIDOverrides = { + -- Draenor + [1152] = 1116, -- Horde Garrison 1 + [1330] = 1116, -- Horde Garrison 2 + [1153] = 1116, -- Horde Garrison 3 + [1154] = 1116, -- Horde Garrison 4 (unused) + [1158] = 1116, -- Alliance Garrison 1 + [1331] = 1116, -- Alliance Garrison 2 + [1159] = 1116, -- Alliance Garrison 3 + [1160] = 1116, -- Alliance Garrison 4 (unused) + [1191] = 1116, -- Ashran PvP Zone + [1203] = 1116, -- Frostfire Finale Scenario + [1207] = 1116, -- Talador Finale Scenario + [1277] = 1116, -- Defense of Karabor Scenario (SMV) + [1402] = 1116, -- Gorgrond Finale Scenario + [1464] = 1116, -- Tanaan + [1465] = 1116, -- Tanaan + -- Legion + [1478] = 1220, -- Temple of Elune Scenario (Val'Sharah) + [1495] = 1220, -- Protection Paladin Artifact Scenario (Stormheim) + [1498] = 1220, -- Havoc Demon Hunter Artifact Scenario (Suramar) + [1502] = 1220, -- Dalaran Underbelly + [1533] = 0, -- Karazhan Artifact Scenario + [1612] = 1220, -- Feral Druid Artifact Scenario (Suramar) + [1626] = 1220, -- Suramar Withered Scenario + [1662] = 1220, -- Suramar Invasion Scenario +} + +-- gather map info, but only if this isn't an upgrade (or the upgrade version forces a re-map) +if not oldversion or oldversion < 7 then + -- wipe old data, if required, otherwise the upgrade path isn't triggered + if oldversion then + wipe(mapData) + wipe(worldMapData) + wipe(transforms) + end + + -- map transform data extracted from UIMapAssignment.db2 (see HereBeDragons-Scripts on GitHub) + -- format: instanceID, newInstanceID, minY, maxY, minX, maxX, offsetY, offsetX + local transformData = { + { 530, 0, 4800, 16000, -10133.3, -2666.67, -2400, 2400 }, + { 530, 1, -6933.33, 533.33, -16000, -8000, 10133.3, 17600 }, + { 732, 0, -3200, 533.3, -533.3, 2666.7, -611.8, 3904.3 }, + { 1064, 870, 5391, 8148, 3518, 7655, -2134.2, -2286.6 }, + { 1208, 1116, -2666, -2133, -2133, -1600, 10210, 2410 }, + { 1460, 1220, -1066.7, 2133.3, 0, 3200, -2333.9, 966.7 }, + } + + local function processTransforms() + for _, transform in pairs(transformData) do + local instanceID, newInstanceID, minY, maxY, minX, maxX, offsetY, offsetX = unpack(transform) + if not transforms[instanceID] then + transforms[instanceID] = {} + end + table.insert(transforms[instanceID], { newInstanceID = newInstanceID, minY = minY, maxY = maxY, minX = minX, maxX = maxX, offsetY = offsetY, offsetX = offsetX }) + end + end + + local function applyMapTransforms(instanceID, left, right, top, bottom) + if transforms[instanceID] then + for _, data in ipairs(transforms[instanceID]) do + if left <= data.maxX and right >= data.minX and top <= data.maxY and bottom >= data.minY then + instanceID = data.newInstanceID + left = left + data.offsetX + right = right + data.offsetX + top = top + data.offsetY + bottom = bottom + data.offsetY + break + end + end + end + return instanceID, left, right, top, bottom + end + + local vector00, vector05 = CreateVector2D(0, 0), CreateVector2D(0.5, 0.5) + -- gather the data of one map (by uiMapID) + local function processMap(id, data, parent) + if not id or not data or mapData[id] then return end + + if data.parentMapID and data.parentMapID ~= 0 then + parent = data.parentMapID + elseif not parent then + parent = 0 + end + + -- get two positions from the map, we use 0/0 and 0.5/0.5 to avoid issues on some maps where 1/1 is translated inaccurately + local instance, topLeft = C_Map.GetWorldPosFromMapPos(id, vector00) + local _, bottomRight = C_Map.GetWorldPosFromMapPos(id, vector05) + if topLeft and bottomRight then + local top, left = topLeft:GetXY() + local bottom, right = bottomRight:GetXY() + bottom = top + (bottom - top) * 2 + right = left + (right - left) * 2 + + instance, left, right, top, bottom = applyMapTransforms(instance, left, right, top, bottom) + mapData[id] = {left - right, top - bottom, left, top, instance = instance, name = data.name, mapType = data.mapType, parent = parent } + else + mapData[id] = {0, 0, 0, 0, instance = instance or -1, name = data.name, mapType = data.mapType, parent = parent } + end + end + + local function processMapChildrenRecursive(parent) + local children = C_Map.GetMapChildrenInfo(parent) + if children and #children > 0 then + for i = 1, #children do + local id = children[i].mapID + if id and not mapData[id] then + processMap(id, children[i], parent) + processMapChildrenRecursive(id) + + -- process sibling maps (in the same group) + -- in some cases these are not discovered by GetMapChildrenInfo above + local groupID = C_Map.GetMapGroupID(id) + if groupID then + local groupMembers = C_Map.GetMapGroupMembersInfo(groupID) + if groupMembers then + for k = 1, #groupMembers do + local memberId = groupMembers[k].mapID + if memberId and not mapData[memberId] then + processMap(memberId, C_Map.GetMapInfo(memberId), parent) + processMapChildrenRecursive(memberId) + end + end + end + end + end + end + end + end + + local function fixupZones() + local cosmic = C_Map.GetMapInfo(COSMIC_MAP_ID) + if cosmic then + mapData[COSMIC_MAP_ID] = {0, 0, 0, 0} + mapData[COSMIC_MAP_ID].instance = -1 + mapData[COSMIC_MAP_ID].name = cosmic.name + mapData[COSMIC_MAP_ID].mapType = cosmic.mapType + end + + -- data for the azeroth world map + if WoWClassic then + worldMapData[0] = { 44688.53, 29795.11, 32601.04, 9894.93 } + worldMapData[1] = { 44878.66, 29916.10, 8723.96, 14824.53 } + else + worldMapData[0] = { 76153.14, 50748.62, 65008.24, 23827.51 } + worldMapData[1] = { 77803.77, 51854.98, 13157.6, 28030.61 } + worldMapData[571] = { 71773.64, 50054.05, 36205.94, 12366.81 } + worldMapData[870] = { 67710.54, 45118.08, 33565.89, 38020.67 } + worldMapData[1220] = { 82758.64, 55151.28, 52943.46, 24484.72 } + worldMapData[1642] = { 77933.3, 51988.91, 44262.36, 32835.1 } + worldMapData[1643] = { 76060.47, 50696.96, 55384.8, 25774.35 } + end + end + + local function gatherMapData() + processTransforms() + + -- find all maps in well known structures + if WoWClassic then + processMap(WORLD_MAP_ID) + processMapChildrenRecursive(WORLD_MAP_ID) + else + processMapChildrenRecursive(COSMIC_MAP_ID) + end + + fixupZones() + + -- try to fill in holes in the map list + for i = 1, 2000 do + if not mapData[i] then + local mapInfo = C_Map.GetMapInfo(i) + if mapInfo and mapInfo.name then + processMap(i, mapInfo, nil) + end + end + end + end + + gatherMapData() +end + +-- Transform a set of coordinates based on the defined map transformations +local function applyCoordinateTransforms(x, y, instanceID) + if transforms[instanceID] then + for _, transformData in ipairs(transforms[instanceID]) do + if transformData.minX <= x and transformData.maxX >= x and transformData.minY <= y and transformData.maxY >= y then + instanceID = transformData.newInstanceID + x = x + transformData.offsetX + y = y + transformData.offsetY + break + end + end + end + if instanceIDOverrides[instanceID] then + instanceID = instanceIDOverrides[instanceID] + end + return x, y, instanceID +end + +local StartUpdateTimer +local function UpdateCurrentPosition() + -- retrieve current zone + local uiMapID = C_Map.GetBestMapForUnit("player") + + if uiMapID ~= currentPlayerUIMapID then + -- update upvalues and signal callback + currentPlayerUIMapID, currentPlayerUIMapType = uiMapID, mapData[uiMapID] and mapData[uiMapID].mapType or 0 + HereBeDragons.callbacks:Fire("PlayerZoneChanged", currentPlayerUIMapID, currentPlayerUIMapType) + end + + -- start a timer to update in micro dungeons since multi-level micro dungeons do not reliably fire events + if currentPlayerUIMapType == Enum.UIMapType.Micro then + StartUpdateTimer() + end +end + +-- upgradeable timer callback, don't want to keep calling the old function if the library is upgraded +HereBeDragons.UpdateCurrentPosition = UpdateCurrentPosition +local function UpdateTimerCallback() + -- signal that the timer ran + HereBeDragons.updateTimerActive = nil + + -- run update now + HereBeDragons.UpdateCurrentPosition() +end + +function StartUpdateTimer() + if not HereBeDragons.updateTimerActive then + -- prevent running multiple timers + HereBeDragons.updateTimerActive = true + + -- and queue an update + C_Timer.After(1, UpdateTimerCallback) + end +end + +local function OnEvent(frame, event, ...) + UpdateCurrentPosition() +end + +HereBeDragons.eventFrame:SetScript("OnEvent", OnEvent) +HereBeDragons.eventFrame:UnregisterAllEvents() +HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA") +HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED") +HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_INDOORS") +HereBeDragons.eventFrame:RegisterEvent("NEW_WMO_CHUNK") +HereBeDragons.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + +-- if we're loading after entering the world (ie. on demand), update position now +if IsLoggedIn() then + UpdateCurrentPosition() +end + +--- Return the localized zone name for a given uiMapID +-- @param uiMapID uiMapID of the zone +function HereBeDragons:GetLocalizedMap(uiMapID) + return mapData[uiMapID] and mapData[uiMapID].name or nil +end + +--- Get the size of the zone +-- @param uiMapID uiMapID of the zone +-- @return width, height of the zone, in yards +function HereBeDragons:GetZoneSize(uiMapID) + local data = mapData[uiMapID] + if not data then return 0, 0 end + + return data[1], data[2] +end + +--- Get a list of all map IDs +-- @return array-style table with all known/valid map IDs +function HereBeDragons:GetAllMapIDs() + local t = {} + for id in pairs(mapData) do + table.insert(t, id) + end + return t +end + +--- Convert local/point coordinates to world coordinates in yards +-- @param x X position in 0-1 point coordinates +-- @param y Y position in 0-1 point coordinates +-- @param zone uiMapID of the zone +function HereBeDragons:GetWorldCoordinatesFromZone(x, y, zone) + local data = mapData[zone] + if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil end + if not x or not y then return nil, nil, nil end + + local width, height, left, top = data[1], data[2], data[3], data[4] + x, y = left - width * x, top - height * y + + return x, y, data.instance +end + +--- Convert local/point coordinates to world coordinates in yards. The coordinates have to come from the Azeroth World Map +-- @param x X position in 0-1 point coordinates +-- @param y Y position in 0-1 point coordinates +-- @param instance Instance to use for the world coordinates +function HereBeDragons:GetWorldCoordinatesFromAzerothWorldMap(x, y, instance) + local data = worldMapData[instance] + if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil end + if not x or not y then return nil, nil, nil end + + local width, height, left, top = data[1], data[2], data[3], data[4] + x, y = left - width * x, top - height * y + + return x, y, instance +end + + +--- Convert world coordinates to local/point zone coordinates +-- @param x Global X position +-- @param y Global Y position +-- @param zone uiMapID of the zone +-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned +function HereBeDragons:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds) + local data = mapData[zone] + if not data or data[1] == 0 or data[2] == 0 then return nil, nil end + if not x or not y then return nil, nil end + + local width, height, left, top = data[1], data[2], data[3], data[4] + x, y = (left - x) / width, (top - y) / height + + -- verify the coordinates fall into the zone + if not allowOutOfBounds and (x < 0 or x > 1 or y < 0 or y > 1) then return nil, nil end + + return x, y +end + +--- Convert world coordinates to local/point zone coordinates on the azeroth world map +-- @param x Global X position +-- @param y Global Y position +-- @param instance Instance to translate coordinates from +-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned +function HereBeDragons:GetAzerothWorldMapCoordinatesFromWorld(x, y, instance, allowOutOfBounds) + local data = worldMapData[instance] + if not data or data[1] == 0 or data[2] == 0 then return nil, nil end + if not x or not y then return nil, nil end + + local width, height, left, top = data[1], data[2], data[3], data[4] + x, y = (left - x) / width, (top - y) / height + + -- verify the coordinates fall into the zone + if not allowOutOfBounds and (x < 0 or x > 1 or y < 0 or y > 1) then return nil, nil end + + return x, y +end + +-- Helper function to handle world map coordinate translation +local function TranslateAzerothWorldMapCoordinates(self, x, y, oZone, dZone, allowOutOfBounds) + if (oZone ~= WORLD_MAP_ID and not mapData[oZone]) or (dZone ~= WORLD_MAP_ID and not mapData[dZone]) then return nil, nil end + -- determine the instance we're working with + local instance = (oZone == WORLD_MAP_ID) and mapData[dZone].instance or mapData[oZone].instance + if not worldMapData[instance] then return nil, nil end + + if oZone == WORLD_MAP_ID then + x, y = self:GetWorldCoordinatesFromAzerothWorldMap(x, y, instance) + return self:GetZoneCoordinatesFromWorld(x, y, dZone, allowOutOfBounds) + else + x, y = self:GetWorldCoordinatesFromZone(x, y, oZone) + return self:GetAzerothWorldMapCoordinatesFromWorld(x, y, instance, allowOutOfBounds) + end +end + +--- Translate zone coordinates from one zone to another +-- @param x X position in 0-1 point coordinates, relative to the origin zone +-- @param y Y position in 0-1 point coordinates, relative to the origin zone +-- @param oZone Origin Zone, uiMapID +-- @param dZone Destination Zone, uiMapID +-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned +function HereBeDragons:TranslateZoneCoordinates(x, y, oZone, dZone, allowOutOfBounds) + if oZone == dZone then return x, y end + + if oZone == WORLD_MAP_ID or dZone == WORLD_MAP_ID then + return TranslateAzerothWorldMapCoordinates(self, x, y, oZone, dZone, allowOutOfBounds) + end + + local xCoord, yCoord, instance = self:GetWorldCoordinatesFromZone(x, y, oZone) + if not xCoord then return nil, nil end + + local data = mapData[dZone] + if not data or data.instance ~= instance then return nil, nil end + + return self:GetZoneCoordinatesFromWorld(xCoord, yCoord, dZone, allowOutOfBounds) +end + +--- Return the distance from an origin position to a destination position in the same instance/continent. +-- @param instanceID instance ID +-- @param oX origin X +-- @param oY origin Y +-- @param dX destination X +-- @param dY destination Y +-- @return distance, deltaX, deltaY +function HereBeDragons:GetWorldDistance(instanceID, oX, oY, dX, dY) + if not oX or not oY or not dX or not dY then return nil, nil, nil end + local deltaX, deltaY = dX - oX, dY - oY + return (deltaX * deltaX + deltaY * deltaY)^0.5, deltaX, deltaY +end + +--- Return the distance between two points on the same continent +-- @param oZone origin zone uiMapID +-- @param oX origin X, in local zone/point coordinates +-- @param oY origin Y, in local zone/point coordinates +-- @param dZone destination zone uiMapID +-- @param dX destination X, in local zone/point coordinates +-- @param dY destination Y, in local zone/point coordinates +-- @return distance, deltaX, deltaY in yards +function HereBeDragons:GetZoneDistance(oZone, oX, oY, dZone, dX, dY) + local oInstance, dInstance + oX, oY, oInstance = self:GetWorldCoordinatesFromZone(oX, oY, oZone) + if not oX then return nil, nil, nil end + + -- translate dX, dY to the origin zone + dX, dY, dInstance = self:GetWorldCoordinatesFromZone(dX, dY, dZone) + if not dX then return nil, nil, nil end + + if oInstance ~= dInstance then return nil, nil, nil end + + return self:GetWorldDistance(oInstance, oX, oY, dX, dY) +end + +--- Return the angle and distance from an origin position to a destination position in the same instance/continent. +-- @param instanceID instance ID +-- @param oX origin X +-- @param oY origin Y +-- @param dX destination X +-- @param dY destination Y +-- @return angle, distance where angle is in radians and distance in yards +function HereBeDragons:GetWorldVector(instanceID, oX, oY, dX, dY) + local distance, deltaX, deltaY = self:GetWorldDistance(instanceID, oX, oY, dX, dY) + if not distance then return nil, nil end + + -- calculate the angle from deltaY and deltaX + local angle = atan2(-deltaX, deltaY) + + -- normalize the angle + if angle > 0 then + angle = PI2 - angle + else + angle = -angle + end + + return angle, distance +end + +--- Get the current world position of the specified unit +-- The position is transformed to the current continent, if applicable +-- NOTE: The same restrictions as for the UnitPosition() API apply, +-- which means a very limited set of unit ids will actually work. +-- @param unitId Unit Id +-- @return x, y, instanceID +function HereBeDragons:GetUnitWorldPosition(unitId) + -- get the current position + local y, x, _z, instanceID = UnitPosition(unitId) + if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end + + -- return transformed coordinates + return applyCoordinateTransforms(x, y, instanceID) +end + +--- Get the current world position of the player +-- The position is transformed to the current continent, if applicable +-- @return x, y, instanceID +function HereBeDragons:GetPlayerWorldPosition() + -- get the current position + local y, x, _z, instanceID = UnitPosition("player") + if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end + + -- return transformed coordinates + return applyCoordinateTransforms(x, y, instanceID) +end + +--- Get the current zone and level of the player +-- The returned mapFile can represent a micro dungeon, if the player currently is inside one. +-- @return uiMapID, mapType +function HereBeDragons:GetPlayerZone() + return currentPlayerUIMapID, currentPlayerUIMapType +end + +--- Get the current position of the player on a zone level +-- The returned values are local point coordinates, 0-1. The mapFile can represent a micro dungeon. +-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned +-- @return x, y, uiMapID, mapType +function HereBeDragons:GetPlayerZonePosition(allowOutOfBounds) + if not currentPlayerUIMapID then return nil, nil, nil, nil end + local x, y, _instanceID = self:GetPlayerWorldPosition() + if not x or not y then return nil, nil, nil, nil end + + x, y = self:GetZoneCoordinatesFromWorld(x, y, currentPlayerUIMapID, allowOutOfBounds) + if x and y then + return x, y, currentPlayerUIMapID, currentPlayerUIMapType + end + return nil, nil, nil, nil +end diff --git a/Libraries/HereBeDragons-2.0/HereBeDragons-Pins-2.0.lua b/Libraries/HereBeDragons-2.0/HereBeDragons-Pins-2.0.lua new file mode 100644 index 0000000..6470ec9 --- /dev/null +++ b/Libraries/HereBeDragons-2.0/HereBeDragons-Pins-2.0.lua @@ -0,0 +1,752 @@ +-- HereBeDragons-Pins is a library to show pins/icons on the world map and minimap + +local MAJOR, MINOR = "HereBeDragons-Pins-2.0", 6 +assert(LibStub, MAJOR .. " requires LibStub") + +local pins, _oldversion = LibStub:NewLibrary(MAJOR, MINOR) +if not pins then return end + +local HBD = LibStub("HereBeDragons-2.0") + +pins.updateFrame = pins.updateFrame or CreateFrame("Frame") + +-- storage for minimap pins +pins.minimapPins = pins.minimapPins or {} +pins.activeMinimapPins = pins.activeMinimapPins or {} +pins.minimapPinRegistry = pins.minimapPinRegistry or {} + +-- and worldmap pins +pins.worldmapPins = pins.worldmapPins or {} +pins.worldmapPinRegistry = pins.worldmapPinRegistry or {} +pins.worldmapPinsPool = pins.worldmapPinsPool or CreateFramePool("FRAME") +pins.worldmapProvider = pins.worldmapProvider or CreateFromMixins(MapCanvasDataProviderMixin) +pins.worldmapProviderPin = pins.worldmapProviderPin or CreateFromMixins(MapCanvasPinMixin) + +-- store a reference to the active minimap object +pins.Minimap = pins.Minimap or Minimap + +-- Data Constants +local WORLD_MAP_ID = 947 + +-- upvalue lua api +local cos, sin, max = math.cos, math.sin, math.max +local type, pairs = type, pairs + +-- upvalue wow api +local GetPlayerFacing = GetPlayerFacing + +-- upvalue data tables +local minimapPins = pins.minimapPins +local activeMinimapPins = pins.activeMinimapPins +local minimapPinRegistry = pins.minimapPinRegistry + +local worldmapPins = pins.worldmapPins +local worldmapPinRegistry = pins.worldmapPinRegistry +local worldmapPinsPool = pins.worldmapPinsPool +local worldmapProvider = pins.worldmapProvider +local worldmapProviderPin = pins.worldmapProviderPin + +local minimap_size = { + indoor = { + [0] = 300, -- scale + [1] = 240, -- 1.25 + [2] = 180, -- 5/3 + [3] = 120, -- 2.5 + [4] = 80, -- 3.75 + [5] = 50, -- 6 + }, + outdoor = { + [0] = 466 + 2/3, -- scale + [1] = 400, -- 7/6 + [2] = 333 + 1/3, -- 1.4 + [3] = 266 + 2/6, -- 1.75 + [4] = 200, -- 7/3 + [5] = 133 + 1/3, -- 3.5 + }, +} + +local minimap_shapes = { + -- { upper-left, lower-left, upper-right, lower-right } + ["SQUARE"] = { false, false, false, false }, + ["CORNER-TOPLEFT"] = { true, false, false, false }, + ["CORNER-TOPRIGHT"] = { false, false, true, false }, + ["CORNER-BOTTOMLEFT"] = { false, true, false, false }, + ["CORNER-BOTTOMRIGHT"] = { false, false, false, true }, + ["SIDE-LEFT"] = { true, true, false, false }, + ["SIDE-RIGHT"] = { false, false, true, true }, + ["SIDE-TOP"] = { true, false, true, false }, + ["SIDE-BOTTOM"] = { false, true, false, true }, + ["TRICORNER-TOPLEFT"] = { true, true, true, false }, + ["TRICORNER-TOPRIGHT"] = { true, false, true, true }, + ["TRICORNER-BOTTOMLEFT"] = { true, true, false, true }, + ["TRICORNER-BOTTOMRIGHT"] = { false, true, true, true }, +} + +local tableCache = setmetatable({}, {__mode='k'}) + +local function newCachedTable() + local t = next(tableCache) + if t then + tableCache[t] = nil + else + t = {} + end + return t +end + +local function recycle(t) + tableCache[t] = true +end + +------------------------------------------------------------------------------------------- +-- Minimap pin position logic + +-- minimap rotation +local rotateMinimap = GetCVar("rotateMinimap") == "1" + +-- is the minimap indoors or outdoors +local indoors = GetCVar("minimapZoom")+0 == pins.Minimap:GetZoom() and "outdoor" or "indoor" + +local minimapPinCount, queueFullUpdate = 0, false +local minimapScale, minimapShape, mapRadius, minimapWidth, minimapHeight, mapSin, mapCos +local lastZoom, lastFacing, lastXY, lastYY + +local function drawMinimapPin(pin, data) + local xDist, yDist = lastXY - data.x, lastYY - data.y + + -- handle rotation + if rotateMinimap then + local dx, dy = xDist, yDist + xDist = dx*mapCos - dy*mapSin + yDist = dx*mapSin + dy*mapCos + end + + -- adapt delta position to the map radius + local diffX = xDist / mapRadius + local diffY = yDist / mapRadius + + -- different minimap shapes + local isRound = true + if minimapShape and not (xDist == 0 or yDist == 0) then + isRound = (xDist < 0) and 1 or 3 + if yDist < 0 then + isRound = minimapShape[isRound] + else + isRound = minimapShape[isRound + 1] + end + end + + -- calculate distance from the center of the map + local dist + if isRound then + dist = (diffX*diffX + diffY*diffY) / 0.9^2 + else + dist = max(diffX*diffX, diffY*diffY) / 0.9^2 + end + + -- if distance > 1, then adapt node position to slide on the border + if dist > 1 and data.floatOnEdge then + dist = dist^0.5 + diffX = diffX/dist + diffY = diffY/dist + end + + if dist <= 1 or data.floatOnEdge then + pin:Show() + pin:ClearAllPoints() + pin:SetPoint("CENTER", pins.Minimap, "CENTER", diffX * minimapWidth, -diffY * minimapHeight) + data.onEdge = (dist > 1) + else + pin:Hide() + data.onEdge = nil + pin.keep = nil + end +end + +local function IsParentMap(originMapId, toCheckMapId) + local parentMapID = HBD.mapData[originMapId].parent + while parentMapID and HBD.mapData[parentMapID] do + local mapType = HBD.mapData[parentMapID].mapType + if mapType ~= Enum.UIMapType.Zone and mapType ~= Enum.UIMapType.Dungeon and mapType ~= Enum.UIMapType.Micro then + return false + end + if parentMapID == toCheckMapId then + return true + end + parentMapID = HBD.mapData[parentMapID].parent + end + return false +end + +local function UpdateMinimapPins(force) + -- get the current player position + local x, y, instanceID = HBD:GetPlayerWorldPosition() + local mapID = HBD:GetPlayerZone() + + -- get data from the API for calculations + local zoom = pins.Minimap:GetZoom() + local diffZoom = zoom ~= lastZoom + + -- for rotating minimap support + local facing + if rotateMinimap then + facing = GetPlayerFacing() + else + facing = lastFacing + end + + -- check for all values to be available (starting with 7.1.0, instances don't report coordinates) + if not x or not y or (rotateMinimap and not facing) then + minimapPinCount = 0 + for pin in pairs(activeMinimapPins) do + pin:Hide() + activeMinimapPins[pin] = nil + end + return + end + + local newScale = pins.Minimap:GetScale() + if minimapScale ~= newScale then + minimapScale = newScale + force = true + end + + if x ~= lastXY or y ~= lastYY or diffZoom or facing ~= lastFacing or force then + -- minimap information + minimapShape = GetMinimapShape and minimap_shapes[GetMinimapShape() or "ROUND"] + mapRadius = minimap_size[indoors][zoom] / 2 + minimapWidth = pins.Minimap:GetWidth() / 2 + minimapHeight = pins.Minimap:GetHeight() / 2 + + -- update upvalues for icon placement + lastZoom = zoom + lastFacing = facing + lastXY, lastYY = x, y + + if rotateMinimap then + mapSin = sin(facing) + mapCos = cos(facing) + end + + for pin, data in pairs(minimapPins) do + if data.instanceID == instanceID and (not data.uiMapID or data.uiMapID == mapID or (data.showInParentZone and IsParentMap(data.uiMapID, mapID))) then + activeMinimapPins[pin] = data + data.keep = true + -- draw the pin (this may reset data.keep if outside of the map) + drawMinimapPin(pin, data) + end + end + + minimapPinCount = 0 + for pin, data in pairs(activeMinimapPins) do + if not data.keep then + pin:Hide() + activeMinimapPins[pin] = nil + else + minimapPinCount = minimapPinCount + 1 + data.keep = nil + end + end + end +end + +local function UpdateMinimapIconPosition() + + -- get the current map zoom + local zoom = pins.Minimap:GetZoom() + local diffZoom = zoom ~= lastZoom + -- if the map zoom changed, run a full update sweep + if diffZoom then + UpdateMinimapPins() + return + end + + -- we have no active minimap pins, just return early + if minimapPinCount == 0 then return end + + local x, y = HBD:GetPlayerWorldPosition() + + -- for rotating minimap support + local facing + if rotateMinimap then + facing = GetPlayerFacing() + else + facing = lastFacing + end + + -- check for all values to be available (starting with 7.1.0, instances don't report coordinates) + if not x or not y or (rotateMinimap and not facing) then + UpdateMinimapPins() + return + end + + local refresh + local newScale = pins.Minimap:GetScale() + if minimapScale ~= newScale then + minimapScale = newScale + refresh = true + end + + if x ~= lastXY or y ~= lastYY or facing ~= lastFacing or refresh then + -- update radius of the map + mapRadius = minimap_size[indoors][zoom] / 2 + -- update upvalues for icon placement + lastXY, lastYY = x, y + lastFacing = facing + + if rotateMinimap then + mapSin = sin(facing) + mapCos = cos(facing) + end + + -- iterate all nodes and check if they are still in range of our minimap display + for pin, data in pairs(activeMinimapPins) do + -- update the position of the node + drawMinimapPin(pin, data) + end + end +end + +local function UpdateMinimapZoom() + local zoom = pins.Minimap:GetZoom() + if GetCVar("minimapZoom") == GetCVar("minimapInsideZoom") then + pins.Minimap:SetZoom(zoom < 2 and zoom + 1 or zoom - 1) + end + indoors = GetCVar("minimapZoom")+0 == pins.Minimap:GetZoom() and "outdoor" or "indoor" + pins.Minimap:SetZoom(zoom) +end + +------------------------------------------------------------------------------------------- +-- WorldMap data provider + +-- setup pin pool +worldmapPinsPool.parent = WorldMapFrame:GetCanvas() +worldmapPinsPool.creationFunc = function(framePool) + local frame = CreateFrame(framePool.frameType, nil, framePool.parent) + frame:SetSize(1, 1) + return Mixin(frame, worldmapProviderPin) +end +worldmapPinsPool.resetterFunc = function(pinPool, pin) + FramePool_HideAndClearAnchors(pinPool, pin) + pin:OnReleased() + + pin.pinTemplate = nil + pin.owningMap = nil +end + +-- register pin pool with the world map +WorldMapFrame.pinPools["HereBeDragonsPinsTemplate"] = worldmapPinsPool + +-- provider base API +function worldmapProvider:RemoveAllData() + self:GetMap():RemoveAllPinsByTemplate("HereBeDragonsPinsTemplate") +end + +function worldmapProvider:RemovePinByIcon(icon) + for pin in self:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplate") do + if pin.icon == icon then + self:GetMap():RemovePin(pin) + end + end +end + +function worldmapProvider:RemovePinsByRef(ref) + for pin in self:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplate") do + if pin.icon and worldmapPinRegistry[ref][pin.icon] then + self:GetMap():RemovePin(pin) + end + end +end + +function worldmapProvider:RefreshAllData(fromOnShow) + self:RemoveAllData() + + for icon, data in pairs(worldmapPins) do + self:HandlePin(icon, data) + end +end + +function worldmapProvider:HandlePin(icon, data) + local uiMapID = self:GetMap():GetMapID() + + -- check for a valid map + if not uiMapID then return end + + local x, y + if uiMapID == WORLD_MAP_ID then + -- should this pin show on the world map? + if uiMapID ~= data.uiMapID and data.worldMapShowFlag ~= HBD_PINS_WORLDMAP_SHOW_WORLD then return end + + -- translate to the world map + x, y = HBD:GetAzerothWorldMapCoordinatesFromWorld(data.x, data.y, data.instanceID) + else + -- check that it matches the instance + if not HBD.mapData[uiMapID] or HBD.mapData[uiMapID].instance ~= data.instanceID then return end + + if uiMapID ~= data.uiMapID then + local mapType = HBD.mapData[uiMapID].mapType + if not data.uiMapID then + if mapType == Enum.UIMapType.Continent and data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_CONTINENT then + --pass + elseif mapType ~= Enum.UIMapType.Zone and mapType ~= Enum.UIMapType.Dungeon and mapType ~= Enum.UIMapType.Micro then + -- fail + return + end + else + local show = false + local parentMapID = HBD.mapData[data.uiMapID].parent + while parentMapID and HBD.mapData[parentMapID] do + if parentMapID == uiMapID then + local parentMapType = HBD.mapData[parentMapID].mapType + -- show on any parent zones if they are normal zones + if data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_PARENT and + (parentMapType == Enum.UIMapType.Zone or parentMapType == Enum.UIMapType.Dungeon or parentMapType == Enum.UIMapType.Micro) then + show = true + -- show on the continent + elseif data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_CONTINENT and + parentMapType == Enum.UIMapType.Continent then + show = true + end + break + -- worldmap is handled above already + else + parentMapID = HBD.mapData[parentMapID].parent + end + end + + if not show then return end + end + end + + -- translate coordinates + x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, uiMapID) + end + if x and y then + self:GetMap():AcquirePin("HereBeDragonsPinsTemplate", icon, x, y, data.frameLevelType) + end +end + +-- map pin base API +function worldmapProviderPin:OnLoad() + self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI") + self:SetScalingLimits(1, 1.0, 1.2) +end + +function worldmapProviderPin:OnAcquired(icon, x, y, frameLevelType) + self:UseFrameLevelType(frameLevelType or "PIN_FRAME_LEVEL_AREA_POI") + self:SetPosition(x, y) + + self.icon = icon + icon:SetParent(self) + icon:ClearAllPoints() + icon:SetPoint("CENTER", self, "CENTER") + icon:Show() +end + +function worldmapProviderPin:OnReleased() + if self.icon then + self.icon:Hide() + self.icon:SetParent(UIParent) + self.icon:ClearAllPoints() + self.icon = nil + end +end + +-- register with the world map +WorldMapFrame:AddDataProvider(worldmapProvider) + +-- map event handling +local function UpdateMinimap() + UpdateMinimapZoom() + UpdateMinimapPins() +end + +local function UpdateWorldMap() + worldmapProvider:RefreshAllData() +end + +local last_update = 0 +local function OnUpdateHandler(frame, elapsed) + last_update = last_update + elapsed + if last_update > 1 or queueFullUpdate then + UpdateMinimapPins(queueFullUpdate) + last_update = 0 + queueFullUpdate = false + else + UpdateMinimapIconPosition() + end +end +pins.updateFrame:SetScript("OnUpdate", OnUpdateHandler) + +local function OnEventHandler(frame, event, ...) + if event == "CVAR_UPDATE" then + local cvar, value = ... + if cvar == "ROTATE_MINIMAP" then + rotateMinimap = (value == "1") + queueFullUpdate = true + end + elseif event == "MINIMAP_UPDATE_ZOOM" then + UpdateMinimap() + elseif event == "PLAYER_LOGIN" then + -- recheck cvars after login + rotateMinimap = GetCVar("rotateMinimap") == "1" + elseif event == "PLAYER_ENTERING_WORLD" then + UpdateMinimap() + UpdateWorldMap() + end +end + +pins.updateFrame:SetScript("OnEvent", OnEventHandler) +pins.updateFrame:UnregisterAllEvents() +pins.updateFrame:RegisterEvent("CVAR_UPDATE") +pins.updateFrame:RegisterEvent("MINIMAP_UPDATE_ZOOM") +pins.updateFrame:RegisterEvent("PLAYER_LOGIN") +pins.updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + +HBD.RegisterCallback(pins, "PlayerZoneChanged", UpdateMinimap) + + +--- Add a icon to the minimap (x/y world coordinate version) +-- Note: This API does not let you specify a map to limit the pin to, it'll be shown on all maps these coordinates are valid for. +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +-- @param instanceID Instance ID of the map to add the icon to +-- @param x X position in world coordinates +-- @param y Y position in world coordinates +-- @param floatOnEdge flag if the icon should float on the edge of the minimap when going out of range, or hide immediately (default false) +function pins:AddMinimapIconWorld(ref, icon, instanceID, x, y, floatOnEdge) + if not ref then + error(MAJOR..": AddMinimapIconWorld: 'ref' must not be nil") + end + if type(icon) ~= "table" or not icon.SetPoint then + error(MAJOR..": AddMinimapIconWorld: 'icon' must be a frame", 2) + end + if type(instanceID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then + error(MAJOR..": AddMinimapIconWorld: 'instanceID', 'x' and 'y' must be numbers", 2) + end + + if not minimapPinRegistry[ref] then + minimapPinRegistry[ref] = {} + end + + minimapPinRegistry[ref][icon] = true + + local t = minimapPins[icon] or newCachedTable() + t.instanceID = instanceID + t.x = x + t.y = y + t.floatOnEdge = floatOnEdge + t.uiMapID = nil + t.showInParentZone = nil + + minimapPins[icon] = t + queueFullUpdate = true + + icon:SetParent(pins.Minimap) +end + +--- Add a icon to the minimap (UiMapID zone coordinate version) +-- The pin will only be shown on the map specified, or optionally its parent map if specified +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +-- @param uiMapID uiMapID of the map to place the icon on +-- @param x X position in local/point coordinates (0-1), relative to the zone +-- @param y Y position in local/point coordinates (0-1), relative to the zone +-- @param showInParentZone flag if the icon should be shown in its parent zone - ie. an icon in a microdungeon in the outdoor zone itself (default false) +-- @param floatOnEdge flag if the icon should float on the edge of the minimap when going out of range, or hide immediately (default false) +function pins:AddMinimapIconMap(ref, icon, uiMapID, x, y, showInParentZone, floatOnEdge) + if not ref then + error(MAJOR..": AddMinimapIconMap: 'ref' must not be nil") + end + if type(icon) ~= "table" or not icon.SetPoint then + error(MAJOR..": AddMinimapIconMap: 'icon' must be a frame") + end + if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then + error(MAJOR..": AddMinimapIconMap: 'uiMapID', 'x' and 'y' must be numbers") + end + + -- convert to world coordinates and use our known adding function + local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID) + if not xCoord then return end + + self:AddMinimapIconWorld(ref, icon, instanceID, xCoord, yCoord, floatOnEdge) + + -- store extra information + minimapPins[icon].uiMapID = uiMapID + minimapPins[icon].showInParentZone = showInParentZone +end + +--- Check if a floating minimap icon is on the edge of the map +-- @param icon the minimap icon +function pins:IsMinimapIconOnEdge(icon) + if not icon then return false end + local data = minimapPins[icon] + if not data then return nil end + + return data.onEdge +end + +--- Remove a minimap icon +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +function pins:RemoveMinimapIcon(ref, icon) + if not ref or not icon or not minimapPinRegistry[ref] then return end + minimapPinRegistry[ref][icon] = nil + if minimapPins[icon] then + recycle(minimapPins[icon]) + minimapPins[icon] = nil + activeMinimapPins[icon] = nil + end + icon:Hide() +end + +--- Remove all minimap icons belonging to your addon (as tracked by "ref") +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +function pins:RemoveAllMinimapIcons(ref) + if not ref or not minimapPinRegistry[ref] then return end + for icon in pairs(minimapPinRegistry[ref]) do + recycle(minimapPins[icon]) + minimapPins[icon] = nil + activeMinimapPins[icon] = nil + icon:Hide() + end + wipe(minimapPinRegistry[ref]) +end + +--- Set the minimap object to position the pins on. Needs to support the usual functions a Minimap-type object exposes. +-- @param minimapObject The new minimap object, or nil to restore the default +function pins:SetMinimapObject(minimapObject) + pins.Minimap = minimapObject or Minimap + for pin in pairs(minimapPins) do + pin:SetParent(pins.Minimap) + end + UpdateMinimapPins(true) +end + +-- world map constants +-- show worldmap pin on its parent zone map (if any) +HBD_PINS_WORLDMAP_SHOW_PARENT = 1 +-- show worldmap pin on the continent map +HBD_PINS_WORLDMAP_SHOW_CONTINENT = 2 +-- show worldmap pin on the continent and world map +HBD_PINS_WORLDMAP_SHOW_WORLD = 3 + +--- Add a icon to the world map (x/y world coordinate version) +-- Note: This API does not let you specify a map to limit the pin to, it'll be shown on all maps these coordinates are valid for. +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +-- @param instanceID Instance ID of the map to add the icon to +-- @param x X position in world coordinates +-- @param y Y position in world coordinates +-- @param showFlag Flag to control on which maps this pin will be shown +-- @param frameLevel Optional Frame Level type registered with the WorldMapFrame, defaults to PIN_FRAME_LEVEL_AREA_POI +function pins:AddWorldMapIconWorld(ref, icon, instanceID, x, y, showFlag, frameLevel) + if not ref then + error(MAJOR..": AddWorldMapIconWorld: 'ref' must not be nil") + end + if type(icon) ~= "table" or not icon.SetPoint then + error(MAJOR..": AddWorldMapIconWorld: 'icon' must be a frame", 2) + end + if type(instanceID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then + error(MAJOR..": AddWorldMapIconWorld: 'instanceID', 'x' and 'y' must be numbers", 2) + end + + if not worldmapPinRegistry[ref] then + worldmapPinRegistry[ref] = {} + end + + worldmapPinRegistry[ref][icon] = true + + local t = worldmapPins[icon] or newCachedTable() + t.instanceID = instanceID + t.x = x + t.y = y + t.uiMapID = nil + t.worldMapShowFlag = showFlag or 0 + t.frameLevelType = frameLevel + + worldmapPins[icon] = t + + worldmapProvider:HandlePin(icon, t) +end + +--- Add a icon to the world map (uiMapID zone coordinate version) +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +-- @param uiMapID uiMapID of the map to place the icon on +-- @param x X position in local/point coordinates (0-1), relative to the zone +-- @param y Y position in local/point coordinates (0-1), relative to the zone +-- @param showFlag Flag to control on which maps this pin will be shown +-- @param frameLevel Optional Frame Level type registered with the WorldMapFrame, defaults to PIN_FRAME_LEVEL_AREA_POI +function pins:AddWorldMapIconMap(ref, icon, uiMapID, x, y, showFlag, frameLevel) + if not ref then + error(MAJOR..": AddWorldMapIconMap: 'ref' must not be nil") + end + if type(icon) ~= "table" or not icon.SetPoint then + error(MAJOR..": AddWorldMapIconMap: 'icon' must be a frame") + end + if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then + error(MAJOR..": AddWorldMapIconMap: 'uiMapID', 'x' and 'y' must be numbers") + end + + -- convert to world coordinates + local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID) + if not xCoord then return end + + if not worldmapPinRegistry[ref] then + worldmapPinRegistry[ref] = {} + end + + worldmapPinRegistry[ref][icon] = true + + local t = worldmapPins[icon] or newCachedTable() + t.instanceID = instanceID + t.x = xCoord + t.y = yCoord + t.uiMapID = uiMapID + t.worldMapShowFlag = showFlag or 0 + t.frameLevelType = frameLevel + + worldmapPins[icon] = t + + worldmapProvider:HandlePin(icon, t) +end + +--- Remove a worldmap icon +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +-- @param icon Icon Frame +function pins:RemoveWorldMapIcon(ref, icon) + if not ref or not icon or not worldmapPinRegistry[ref] then return end + worldmapPinRegistry[ref][icon] = nil + if worldmapPins[icon] then + recycle(worldmapPins[icon]) + worldmapPins[icon] = nil + end + worldmapProvider:RemovePinByIcon(icon) +end + +--- Remove all worldmap icons belonging to your addon (as tracked by "ref") +-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) +function pins:RemoveAllWorldMapIcons(ref) + if not ref or not worldmapPinRegistry[ref] then return end + for icon in pairs(worldmapPinRegistry[ref]) do + recycle(worldmapPins[icon]) + worldmapPins[icon] = nil + end + worldmapProvider:RemovePinsByRef(ref) + wipe(worldmapPinRegistry[ref]) +end + +--- Return the angle and distance from the player to the specified pin +-- @param icon icon object (minimap or worldmap) +-- @return angle, distance where angle is in radians and distance in yards +function pins:GetVectorToIcon(icon) + if not icon then return nil, nil end + local data = minimapPins[icon] or worldmapPins[icon] + if not data then return nil, nil end + + local x, y, instance = HBD:GetPlayerWorldPosition() + if not x or not y or instance ~= data.instanceID then return nil end + + return HBD:GetWorldVector(instance, x, y, data.x, data.y) +end diff --git a/Media/Map/GoldGreenDot.tga b/Media/Map/GoldGreenDot.tga new file mode 100644 index 0000000..4b5a47f Binary files /dev/null and b/Media/Map/GoldGreenDot.tga differ diff --git a/Util.lua b/Util.lua index ce3c523..b930568 100644 --- a/Util.lua +++ b/Util.lua @@ -1,5 +1,21 @@ local _, FieldGuide = ... +FieldGuide.pinPool = {} + +function FieldGuide:getPin() + for _, pin in pairs(FieldGuide.pinPool) do + if not pin.used then + pin.used = true + return pin + end + end + FieldGuide.pinPool[#FieldGuide.pinPool + 1] = CreateFrame("Button", nil, nil, "FieldGuidePinTemplate") + local pin = FieldGuide.pinPool[#FieldGuide.pinPool] + pin.used = true + pin.id = #FieldGuide.pinPool + return pin +end + -- Copies the given table and returns the copy. If no table is given, this returns nil. function FieldGuide.copy(original) local copy = {} @@ -11,4 +27,5 @@ function FieldGuide.copy(original) return nil end return copy -end \ No newline at end of file +end +