Files
HailMaryGuildTools/Modules/MapOverlay/MapOverlay.lua
Torsten Brendgen fc5a8aa361 initial commit
2026-04-10 21:30:31 +02:00

1194 lines
38 KiB
Lua

local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local MapOverlay = HMGT:NewModule("MapOverlay", "AceEvent-3.0")
HMGT.MapOverlay = MapOverlay
local MEDIA_PATH = "Interface\\AddOns\\HailMaryGuildTools\\Modules\\MapOverlay\\Media\\"
local DEFAULT_ICON = MEDIA_PATH .. "DefaultIcon.png"
local DEFAULT_CATEGORY = "default"
local OBJECT_ICONS_TEXTURE = "Interface\\Minimap\\ObjectIcons"
local OBJECT_ICONS_ATLAS_TEXTURE = "Interface\\Minimap\\ObjectIconsAtlas"
local BLIP_TEXTURE_WIDTH = 256
local BLIP_TEXTURE_HEIGHT = 256
local BLIP_CELL_SIZE = 32
local ATLAS_TEXTURE_DEFAULTS = {
[OBJECT_ICONS_TEXTURE] = { width = 256, height = 256 },
[OBJECT_ICONS_ATLAS_TEXTURE] = { width = 256, height = 256 },
}
local ICON_CONFIG = HMGT.MapOverlayIconConfig or {}
local RAW_CATEGORY_DEFS = type(ICON_CONFIG.icons) == "table" and ICON_CONFIG.icons or {
{ key = DEFAULT_CATEGORY, label = "Default", texture = DEFAULT_ICON },
}
DEFAULT_CATEGORY = tostring(ICON_CONFIG.defaultKey or DEFAULT_CATEGORY):lower()
local CATEGORY_DEFS = {}
local CATEGORY_BY_KEY = {}
for _, rawDef in ipairs(RAW_CATEGORY_DEFS) do
if type(rawDef) == "table" then
local key = tostring(rawDef.key or ""):lower()
if key ~= "" and not CATEGORY_BY_KEY[key] then
local def = {
key = key,
label = tostring(rawDef.label or key),
texture = tostring(rawDef.texture or DEFAULT_ICON),
atlasIndex = rawDef.atlasIndex,
atlasSize = rawDef.atlasSize,
atlasColumns = rawDef.atlasColumns,
textureWidth = rawDef.textureWidth,
textureHeight = rawDef.textureHeight,
cell = rawDef.cell,
iconCoords = rawDef.iconCoords,
}
CATEGORY_DEFS[#CATEGORY_DEFS + 1] = def
CATEGORY_BY_KEY[key] = def
if type(rawDef.aliases) == "table" then
for _, alias in ipairs(rawDef.aliases) do
local aliasKey = tostring(alias or ""):lower()
if aliasKey ~= "" and not CATEGORY_BY_KEY[aliasKey] then
CATEGORY_BY_KEY[aliasKey] = def
end
end
end
end
end
end
if not CATEGORY_BY_KEY[DEFAULT_CATEGORY] then
local fallbackDef = {
key = DEFAULT_CATEGORY,
label = "Default",
texture = DEFAULT_ICON,
}
table.insert(CATEGORY_DEFS, 1, fallbackDef)
CATEGORY_BY_KEY[DEFAULT_CATEGORY] = fallbackDef
end
local ROUND_ICON_MASK = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
local PIN_RING_TEXTURE = "Interface\\Minimap\\POIIcons"
local PIN_RING_COLOR = { 0.96, 0.82, 0.22, 1.00 }
local PIN_SHADOW_COLOR = { 0.00, 0.00, 0.00, 0.22 }
local function EnsureRoundMask(owner, fieldName, texture)
if not owner or not fieldName or not texture then
return nil
end
if not owner.CreateMaskTexture or not texture.AddMaskTexture then
return nil
end
local mask = owner[fieldName]
if not mask then
mask = owner:CreateMaskTexture(nil, "ARTWORK")
mask:SetTexture(ROUND_ICON_MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
owner[fieldName] = mask
texture:AddMaskTexture(mask)
owner._textureMaskApplied = true
end
return mask
end
local function SetTextureMaskEnabled(owner, texture, enabled)
if not owner or not texture then
return
end
local mask = owner.textureMask
if not mask then
return
end
if enabled then
if texture.AddMaskTexture and not owner._textureMaskApplied then
texture:AddMaskTexture(mask)
owner._textureMaskApplied = true
end
mask:Show()
else
if texture.RemoveMaskTexture and owner._textureMaskApplied then
texture:RemoveMaskTexture(mask)
owner._textureMaskApplied = false
end
mask:Hide()
end
end
local function ApplyRingVisual(texture, r, g, b, a)
if not texture then
return
end
texture:SetTexture(PIN_RING_TEXTURE)
texture:SetTexCoord(0, 0.125, 0.625, 0.75)
texture:SetVertexColor(r or 1, g or 1, b or 1, a or 1)
end
local function GetIconInset(pinSize)
local size = tonumber(pinSize) or 16
local iconInset = math.max(2, math.floor((size * 0.20) + 0.5))
local maxIconInset = math.max(2, math.floor((size - 6) / 2))
if iconInset > maxIconInset then
iconInset = maxIconInset
end
return iconInset
end
local function clamp(v, minv, maxv, fallback)
v = tonumber(v)
if not v then return fallback end
if v < minv then return minv end
if v > maxv then return maxv end
return v
end
local function GetFallbackObjectIconTexCoord(atlasIndex)
local index = tonumber(atlasIndex)
if not index or index < 0 then
return nil
end
local col = math.fmod(index, 8)
local row = math.floor(index / 8)
local left = (col * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH
local right = ((col + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH
local top = (row * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT
local bottom = ((row + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT
return { left, right, top, bottom }
end
local function GetObjectIconTexCoord(iconIndex)
local index = tonumber(iconIndex)
if not index or index < 0 then
return nil
end
if type(GetObjectIconTextureCoords) == "function" then
local left, right, top, bottom = GetObjectIconTextureCoords(index)
if left and right and top and bottom then
return { left, right, top, bottom }
end
end
return GetFallbackObjectIconTexCoord(index)
end
local function ParseAtlasSize(atlasSize)
if type(atlasSize) == "string" then
local width, height = string.match(atlasSize, "^(%d+)%s*[xX]%s*(%d+)$")
return tonumber(width), tonumber(height)
end
if type(atlasSize) == "table" then
return tonumber(atlasSize[1]), tonumber(atlasSize[2])
end
return nil, nil
end
local function IsShimmedMinimapAtlas(def)
if type(def) ~= "table" then
return false
end
if tostring(def.texture or "") ~= OBJECT_ICONS_ATLAS_TEXTURE then
return false
end
local width, height = ParseAtlasSize(def.atlasSize)
local index = tonumber(def.atlasIndex)
return width == 32 and height == 32 and index and index >= 0 and index < 40 or false
end
local function GetDefinitionResolvedTexture(def)
if type(def) ~= "table" then
return DEFAULT_ICON
end
if IsShimmedMinimapAtlas(def) then
return OBJECT_ICONS_TEXTURE
end
return tostring(def.texture or DEFAULT_ICON)
end
local function GetDefinitionTextureDimensions(def)
if type(def) ~= "table" then
return BLIP_TEXTURE_WIDTH, BLIP_TEXTURE_HEIGHT
end
local texture = GetDefinitionResolvedTexture(def)
local defaults = ATLAS_TEXTURE_DEFAULTS[texture]
local width = tonumber(def.textureWidth) or (defaults and defaults.width) or BLIP_TEXTURE_WIDTH
local height = tonumber(def.textureHeight) or (defaults and defaults.height) or BLIP_TEXTURE_HEIGHT
return width, height
end
local function GetAtlasTexCoord(def)
if type(def) ~= "table" then
return nil
end
local index = tonumber(def.atlasIndex)
local cellWidth, cellHeight = ParseAtlasSize(def.atlasSize)
if not index or not cellWidth or not cellHeight or cellWidth <= 0 or cellHeight <= 0 then
return nil
end
local textureWidth, textureHeight = GetDefinitionTextureDimensions(def)
local columns = tonumber(def.atlasColumns)
if not columns or columns < 1 then
columns = math.max(1, math.floor(textureWidth / cellWidth))
end
local col = math.fmod(index, columns)
local row = math.floor(index / columns)
local left = (col * cellWidth) / textureWidth
local right = ((col + 1) * cellWidth) / textureWidth
local top = (row * cellHeight) / textureHeight
local bottom = ((row + 1) * cellHeight) / textureHeight
return { left, right, top, bottom }
end
local function GetObjectIconCellTexCoord(col, row)
col = tonumber(col)
row = tonumber(row)
if not col or not row or col < 0 or row < 0 then
return nil
end
local left = (col * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH
local right = ((col + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH
local top = (row * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT
local bottom = ((row + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT
return { left, right, top, bottom }
end
local function GetDefinitionIconCoords(def)
if type(def) ~= "table" then
return nil
end
if type(def.iconCoords) == "table" and #def.iconCoords == 4 then
return { def.iconCoords[1], def.iconCoords[2], def.iconCoords[3], def.iconCoords[4] }
end
if type(def.cell) == "table" then
return GetObjectIconCellTexCoord(def.cell[1], def.cell[2])
end
if IsShimmedMinimapAtlas(def) then
return GetObjectIconTexCoord(def.atlasIndex)
end
if def.atlasIndex ~= nil and def.atlasSize ~= nil then
return GetAtlasTexCoord(def)
end
if def.atlasIndex ~= nil then
if GetDefinitionResolvedTexture(def) == OBJECT_ICONS_TEXTURE then
return GetObjectIconTexCoord(def.atlasIndex)
end
return GetAtlasTexCoord(def)
end
return nil
end
local function GetTextureEscape(def, iconCoords, size)
local iconTexture = type(def) == "table" and GetDefinitionResolvedTexture(def) or tostring(def or "")
local iconSize = tonumber(size) or 14
if iconTexture == "" then
return ""
end
if type(iconCoords) == "table" and #iconCoords == 4 then
local textureWidth, textureHeight = GetDefinitionTextureDimensions(type(def) == "table" and def or nil)
local left = math.floor((tonumber(iconCoords[1]) or 0) * textureWidth + 0.5)
local right = math.floor((tonumber(iconCoords[2]) or 0) * textureWidth + 0.5)
local top = math.floor((tonumber(iconCoords[3]) or 0) * textureHeight + 0.5)
local bottom = math.floor((tonumber(iconCoords[4]) or 0) * textureHeight + 0.5)
return string.format(
"|T%s:%d:%d:0:0:%d:%d:%d:%d:%d:%d|t",
iconTexture,
iconSize,
iconSize,
textureWidth,
textureHeight,
left,
right,
top,
bottom
)
end
return string.format("|T%s:%d:%d:0:0|t", iconTexture, iconSize, iconSize)
end
local function BuildCategoryDropdownLabel(def)
if not def then
return "Unknown"
end
local label = tostring(def.label or def.key or "Icon")
local iconTag = GetTextureEscape(def, GetDefinitionIconCoords(def), 14)
if iconTag ~= "" then
return string.format("%s %s", iconTag, label)
end
return label
end
function MapOverlay:GetCategoryDefinitions()
return CATEGORY_DEFS
end
function MapOverlay:GetCategoryDropdownValues()
local values = {}
for _, def in ipairs(CATEGORY_DEFS) do
values[def.key] = BuildCategoryDropdownLabel(def)
end
return values
end
function MapOverlay:GetCategoryIcon(category)
local key = tostring(category or DEFAULT_CATEGORY):lower()
local def = CATEGORY_BY_KEY[key]
if def then
local texture = GetDefinitionResolvedTexture(def)
if texture and texture ~= "" then
return texture
end
end
return DEFAULT_ICON
end
function MapOverlay:GetCategoryIconCoords(category)
local key = tostring(category or DEFAULT_CATEGORY):lower()
local def = CATEGORY_BY_KEY[key]
if def then
return GetDefinitionIconCoords(def)
end
return nil
end
function MapOverlay:GetCategoryLabel(category)
local key = tostring(category or DEFAULT_CATEGORY):lower()
local def = CATEGORY_BY_KEY[key]
if def and def.label and def.label ~= "" then
return def.label
end
return tostring(category or DEFAULT_CATEGORY)
end
function MapOverlay:GetNormalizedCategory(category)
local key = tostring(category or DEFAULT_CATEGORY):lower()
if CATEGORY_BY_KEY[key] then
return key
end
return DEFAULT_CATEGORY
end
function MapOverlay:GetCategoryVisual(category)
local normalizedCategory = self:GetNormalizedCategory(category)
return self:GetCategoryIcon(normalizedCategory), self:GetCategoryIconCoords(normalizedCategory), self:GetCategoryLabel(normalizedCategory), normalizedCategory
end
function MapOverlay:UsesAtlasIcon(category)
local normalizedCategory = self:GetNormalizedCategory(category)
local def = CATEGORY_BY_KEY[normalizedCategory]
return def and (def.atlasIndex ~= nil or type(def.cell) == "table" or type(def.iconCoords) == "table") or false
end
function MapOverlay:IsPOIUserWaypoint(poi)
if not poi or not C_Map or type(C_Map.GetUserWaypoint) ~= "function" then
return false
end
local wp = C_Map.GetUserWaypoint()
if not wp then return false end
if tonumber(wp.uiMapID) ~= tonumber(poi.mapID) then return false end
local px = (tonumber(poi.x) or 0) / 100
local py = (tonumber(poi.y) or 0) / 100
local wx = tonumber(wp.position and wp.position.x) or -1
local wy = tonumber(wp.position and wp.position.y) or -1
return math.abs(px - wx) <= 0.0005 and math.abs(py - wy) <= 0.0005
end
function MapOverlay:ToggleWaypointForPOI(poi)
if not poi or not C_Map or not UiMapPoint then return end
if type(C_Map.ClearUserWaypoint) ~= "function" or type(C_Map.SetUserWaypoint) ~= "function" then
return
end
if self:IsPOIUserWaypoint(poi) then
C_Map.ClearUserWaypoint()
if C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then
C_SuperTrack.SetSuperTrackedUserWaypoint(false)
end
return
end
local mapID = tonumber(poi.mapID)
local x = (tonumber(poi.x) or 0) / 100
local y = (tonumber(poi.y) or 0) / 100
local mapPoint = UiMapPoint.CreateFromCoordinates(mapID, x, y)
if not mapPoint then return end
C_Map.SetUserWaypoint(mapPoint)
if C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then
C_SuperTrack.SetSuperTrackedUserWaypoint(true)
end
end
function MapOverlay:GetSettings()
local p = HMGT.db and HMGT.db.profile
if not p then return nil end
p.mapOverlay = p.mapOverlay or {}
p.mapOverlay.pois = p.mapOverlay.pois or {}
if p.mapOverlay.enabled == nil then p.mapOverlay.enabled = true end
p.mapOverlay.iconSize = clamp(p.mapOverlay.iconSize, 8, 48, 16)
p.mapOverlay.alpha = clamp(p.mapOverlay.alpha, 0.1, 1, 1)
if p.mapOverlay.showLabels == nil then p.mapOverlay.showLabels = true end
for _, poi in ipairs(p.mapOverlay.pois) do
if type(poi) == "table" then
local icon, iconCoords, _, normalizedCategory = self:GetCategoryVisual(poi.category)
poi.category = normalizedCategory
poi.icon = icon
poi.iconCoords = iconCoords
end
end
return p.mapOverlay
end
function MapOverlay:GetActiveMapID()
if WorldMapFrame and WorldMapFrame.GetMapID then
local mapID = WorldMapFrame:GetMapID()
if mapID then return mapID end
end
if C_Map and C_Map.GetBestMapForUnit then
return C_Map.GetBestMapForUnit("player")
end
return nil
end
function MapOverlay:GetCurrentPlayerPosition()
if not C_Map or type(C_Map.GetPlayerMapPosition) ~= "function" then
return nil, nil, nil, "api unavailable"
end
local mapID = tonumber(self:GetActiveMapID())
if not mapID and C_Map.GetBestMapForUnit then
mapID = tonumber(C_Map.GetBestMapForUnit("player"))
end
if not mapID then
return nil, nil, nil, "map unavailable"
end
local pos = C_Map.GetPlayerMapPosition(mapID, "player")
if (not pos or pos.x == nil or pos.y == nil) and C_Map.GetBestMapForUnit then
local bestMapID = tonumber(C_Map.GetBestMapForUnit("player"))
if bestMapID and bestMapID ~= mapID then
mapID = bestMapID
pos = C_Map.GetPlayerMapPosition(mapID, "player")
end
end
if not pos then
return mapID, nil, nil, "position unavailable"
end
local x, y
if type(pos.GetXY) == "function" then
x, y = pos:GetXY()
else
x, y = pos.x, pos.y
end
x = tonumber(x)
y = tonumber(y)
if not x or not y then
return mapID, nil, nil, "position unavailable"
end
return mapID, clamp(x * 100, 0, 100, 0), clamp(y * 100, 0, 100, 0)
end
function MapOverlay:AddPOI(mapID, x, y, label, icon, category)
local s = self:GetSettings()
if not s then return false, "no settings" end
mapID = tonumber(mapID)
x = tonumber(x)
y = tonumber(y)
if not mapID or not x or not y then return false, "invalid coordinates" end
x = clamp(x, 0, 100, 0)
y = clamp(y, 0, 100, 0)
label = tostring(label or ""):gsub("^%s+", ""):gsub("%s+$", "")
if label == "" then label = string.format("POI %.1f, %.1f", x, y) end
local iconCoords
icon, iconCoords, _, category = self:GetCategoryVisual(category)
s.pois[#s.pois + 1] = {
mapID = mapID,
x = x,
y = y,
label = label,
icon = icon,
iconCoords = iconCoords,
category = category,
}
self:Refresh()
self:UpdatePOIManagerFrame()
return true, #s.pois
end
function MapOverlay:UpdatePOI(index, mapID, x, y, label, icon, category)
local s = self:GetSettings()
if not s then return false, "no settings" end
index = tonumber(index)
if not index or index < 1 or index > #s.pois then return false, "invalid index" end
mapID = tonumber(mapID)
x = tonumber(x)
y = tonumber(y)
if not mapID or not x or not y then return false, "invalid coordinates" end
x = clamp(x, 0, 100, 0)
y = clamp(y, 0, 100, 0)
label = tostring(label or ""):gsub("^%s+", ""):gsub("%s+$", "")
if label == "" then label = string.format("POI %.1f, %.1f", x, y) end
local iconCoords
icon, iconCoords, _, category = self:GetCategoryVisual(category)
local poi = s.pois[index]
poi.mapID = mapID
poi.x = x
poi.y = y
poi.label = label
poi.icon = icon
poi.iconCoords = iconCoords
poi.category = category
self:Refresh()
self:UpdatePOIManagerFrame()
return true
end
function MapOverlay:RemovePOI(index)
local s = self:GetSettings()
if not s then return false end
index = tonumber(index)
if not index or index < 1 or index > #s.pois then return false end
table.remove(s.pois, index)
if self._poiManagerSelected and self._poiManagerSelected > #s.pois then
self._poiManagerSelected = #s.pois
end
if self._poiManagerSelected and self._poiManagerSelected < 1 then
self._poiManagerSelected = nil
end
self:Refresh()
self:UpdatePOIManagerFrame()
return true
end
function MapOverlay:BuildPOIListText(mapID)
local s = self:GetSettings()
if not s or type(s.pois) ~= "table" or #s.pois == 0 then
return "No POIs configured."
end
local filterMap = tonumber(mapID)
local out = {}
for i, poi in ipairs(s.pois) do
if (not filterMap) or poi.mapID == filterMap then
out[#out + 1] = string.format(
"%d) map=%d x=%.2f y=%.2f [%s] %s",
i,
tonumber(poi.mapID) or 0,
tonumber(poi.x) or 0,
tonumber(poi.y) or 0,
tostring(self:GetCategoryLabel(poi.category)),
tostring(poi.label or "POI")
)
end
end
if #out == 0 then
return "No POIs for this map."
end
return table.concat(out, "\n")
end
function MapOverlay:CreatePOIManagerFrame()
if self.poiManagerFrame then
return self.poiManagerFrame
end
local frame = CreateFrame("Frame", "HMGTMapPOIManagerFrame", UIParent, "BasicFrameTemplateWithInset")
frame:SetSize(470, 500)
frame:SetPoint("CENTER")
frame:SetFrameStrata("DIALOG")
frame:Hide()
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
frame.title:SetPoint("LEFT", frame.TitleBg, "LEFT", 8, 0)
frame.title:SetText("HMGT - POIs")
frame.subtitle = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
frame.subtitle:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -34)
frame.subtitle:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -34)
frame.subtitle:SetJustifyH("LEFT")
frame.subtitle:SetText("Klicke auf einen Eintrag, um Details unten anzuzeigen.")
local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -54)
scroll:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -32, -54)
scroll:SetHeight(250)
local content = CreateFrame("Frame", nil, scroll)
content:SetSize(1, 1)
scroll:SetScrollChild(content)
frame.scroll = scroll
frame.content = content
frame.rows = {}
frame.emptyText = content:CreateFontString(nil, "OVERLAY", "GameFontDisable")
frame.emptyText:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -4)
frame.emptyText:SetText("Keine POIs vorhanden.")
frame.detailHeader = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
frame.detailHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -320)
frame.detailHeader:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -320)
frame.detailHeader:SetJustifyH("LEFT")
frame.detailHeader:SetText("Details")
frame.detailText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
frame.detailText:SetPoint("TOPLEFT", frame.detailHeader, "BOTTOMLEFT", 0, -8)
frame.detailText:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -336)
frame.detailText:SetJustifyH("LEFT")
frame.detailText:SetJustifyV("TOP")
frame.detailText:SetText("Waehle einen POI aus der Liste.")
frame.waypointButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
frame.waypointButton:SetSize(130, 24)
frame.waypointButton:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 14, 14)
frame.waypointButton:SetText("Waypoint")
frame.waypointButton:SetScript("OnClick", function()
local s = self:GetSettings()
local idx = tonumber(self._poiManagerSelected)
local poi = s and s.pois and idx and s.pois[idx]
if not poi then return end
self:ToggleWaypointForPOI(poi)
end)
frame.removeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
frame.removeButton:SetSize(130, 24)
frame.removeButton:SetPoint("LEFT", frame.waypointButton, "RIGHT", 8, 0)
frame.removeButton:SetText("Entfernen")
frame.removeButton:SetScript("OnClick", function()
local idx = tonumber(self._poiManagerSelected)
if not idx then return end
self:RemovePOI(idx)
end)
frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
frame.closeButton:SetSize(90, 24)
frame.closeButton:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14)
frame.closeButton:SetText(CLOSE or "Close")
frame.closeButton:SetScript("OnClick", function()
frame:Hide()
end)
self.poiManagerFrame = frame
return frame
end
function MapOverlay:AcquirePOIManagerRow(index)
local frame = self.poiManagerFrame
if not frame then return nil end
local row = frame.rows[index]
if row then
return row
end
row = CreateFrame("Button", nil, frame.content)
row:SetHeight(22)
row.bg = row:CreateTexture(nil, "BACKGROUND")
row.bg:SetAllPoints(row)
row.bg:SetColorTexture(1, 1, 1, 0.05)
row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
row.highlight:SetAllPoints(row)
row.highlight:SetColorTexture(1, 1, 1, 0.12)
row.expandText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
row.expandText:SetPoint("LEFT", row, "LEFT", 6, 0)
row.expandText:SetText("+")
row.labelText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
row.labelText:SetPoint("LEFT", row.expandText, "RIGHT", 8, 0)
row.labelText:SetPoint("RIGHT", row, "RIGHT", -8, 0)
row.labelText:SetJustifyH("LEFT")
row:SetScript("OnClick", function(btn)
local idx = btn._index
if not idx then return end
if self._poiManagerSelected == idx then
self._poiManagerSelected = nil
else
self._poiManagerSelected = idx
end
self:UpdatePOIManagerFrame()
end)
frame.rows[index] = row
return row
end
function MapOverlay:UpdatePOIManagerFrame()
local frame = self.poiManagerFrame
if not frame or not frame:IsShown() then return end
local s = self:GetSettings()
local pois = (s and s.pois) or {}
local rowHeight = 22
local offsetY = -2
for i, poi in ipairs(pois) do
local row = self:AcquirePOIManagerRow(i)
row._index = i
row:ClearAllPoints()
row:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 0, offsetY)
row:SetPoint("TOPRIGHT", frame.content, "TOPRIGHT", 0, offsetY)
local selected = (self._poiManagerSelected == i)
row.expandText:SetText(selected and "-" or "+")
row.labelText:SetText(string.format("(%d) %s", i, tostring(poi.label or ("POI " .. i))))
if selected then
row.bg:SetColorTexture(0.20, 0.45, 0.80, 0.22)
else
local alpha = (i % 2 == 0) and 0.03 or 0.07
row.bg:SetColorTexture(1, 1, 1, alpha)
end
row:Show()
offsetY = offsetY - rowHeight
end
for i = #pois + 1, #frame.rows do
frame.rows[i]:Hide()
frame.rows[i]._index = nil
end
frame.content:SetHeight(math.max(1, #pois * rowHeight + 6))
frame.emptyText:SetShown(#pois == 0)
local selected = tonumber(self._poiManagerSelected)
local poi = selected and pois[selected] or nil
if poi then
frame.detailHeader:SetText(string.format("%s (%d)", tostring(poi.label or "POI"), selected))
frame.detailText:SetText(string.format(
"Map ID: %d\nX: %.2f\nY: %.2f\nIcon: %s",
tonumber(poi.mapID) or 0,
tonumber(poi.x) or 0,
tonumber(poi.y) or 0,
self:GetCategoryLabel(poi.category)
))
frame.waypointButton:Enable()
frame.removeButton:Enable()
else
frame.detailHeader:SetText("Details")
frame.detailText:SetText("Waehle einen POI aus der Liste.")
frame.waypointButton:Disable()
frame.removeButton:Disable()
end
end
function MapOverlay:ShowPOIManager()
local frame = self:CreatePOIManagerFrame()
frame:Show()
frame:Raise()
self:UpdatePOIManagerFrame()
end
function MapOverlay:HidePOIManager()
if self.poiManagerFrame then
self.poiManagerFrame:Hide()
end
end
function MapOverlay:TogglePOIManager()
local frame = self:CreatePOIManagerFrame()
if frame:IsShown() then
frame:Hide()
return
end
frame:Show()
frame:Raise()
self:UpdatePOIManagerFrame()
end
local WORLD_MAP_PIN_TEMPLATE = "HMGTMapOverlayPinTemplate"
local WorldMapPinMixin = CreateFromMixins(MapCanvasPinMixin)
local WorldMapDataProviderMixin = CreateFromMixins(MapCanvasDataProviderMixin)
_G.HMGTMapOverlayPinMixin = WorldMapPinMixin
function WorldMapPinMixin:EnsureVisuals()
if not self.shadow then
self.shadow = self:CreateTexture(nil, "BACKGROUND")
self.shadow:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1)
self.shadow:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 1, -1)
end
ApplyRingVisual(self.shadow, unpack(PIN_SHADOW_COLOR))
if not self.ring then
self.ring = self:CreateTexture(nil, "ARTWORK", nil, -2)
self.ring:SetAllPoints(self)
end
ApplyRingVisual(self.ring, unpack(PIN_RING_COLOR))
if self.innerStroke then
self.innerStroke:Hide()
end
if self.fill then
self.fill:Hide()
end
if not self.texture then
self.texture = self:CreateTexture(nil, "OVERLAY")
self.texture:SetTexCoord(0.07, 0.93, 0.07, 0.93)
end
if not self.textureMask then
EnsureRoundMask(self, "textureMask", self.texture)
end
if not self.label then
self.label = self:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
self.label:SetPoint("LEFT", self, "RIGHT", 3, 0)
self.label:SetJustifyH("LEFT")
self.label:SetText("")
end
if self.RegisterForClicks then
self:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
end
function WorldMapPinMixin:UpdateVisualLayout(pinSize, fullSizeTexture)
self:EnsureVisuals()
self.texture:ClearAllPoints()
if fullSizeTexture then
self.texture:SetAllPoints(self)
else
local iconInset = GetIconInset(pinSize)
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", iconInset, -iconInset)
self.texture:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -iconInset, iconInset)
end
if self.textureMask then
self.textureMask:ClearAllPoints()
self.textureMask:SetAllPoints(self.texture)
end
end
function WorldMapPinMixin:OnLoad()
self:EnsureVisuals()
self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI")
if self.SetScalingLimits then
self:SetScalingLimits(1, 1.0, 1.2)
end
if self.SetPassThroughButtons then
self:SetPassThroughButtons("")
end
end
function WorldMapPinMixin:OnAcquired(poi, settings, owner)
self:EnsureVisuals()
self._poi = poi
self._owner = owner
self._settings = settings
local pinSize = tonumber(settings and settings.iconSize) or 16
local usesAtlasIcon = owner and owner.UsesAtlasIcon and owner:UsesAtlasIcon(poi.category) or false
self:SetPosition((tonumber(poi.x) or 0) / 100, (tonumber(poi.y) or 0) / 100)
self:SetSize(pinSize, pinSize)
self:SetAlpha(tonumber(settings and settings.alpha) or 1)
self:UpdateVisualLayout(pinSize, usesAtlasIcon)
if usesAtlasIcon then
SetTextureMaskEnabled(self, self.texture, false)
self.shadow:Hide()
self.ring:Hide()
else
SetTextureMaskEnabled(self, self.texture, true)
self.shadow:Show()
self.ring:Show()
end
local texture, iconCoords = nil, nil
if owner and owner.GetCategoryVisual then
texture, iconCoords = owner:GetCategoryVisual(poi.category)
end
self.texture:SetTexture(texture or poi.icon or DEFAULT_ICON)
if type(iconCoords) == "table" then
self.texture:SetTexCoord(unpack(iconCoords))
elseif type(poi.iconCoords) == "table" then
self.texture:SetTexCoord(unpack(poi.iconCoords))
else
self.texture:SetTexCoord(0.07, 0.93, 0.07, 0.93)
end
self.texture:SetVertexColor(1, 1, 1, 1)
self.texture:Show()
if settings and settings.showLabels then
self.label:SetText(tostring(poi.label or "POI"))
self.label:Show()
else
self.label:SetText("")
self.label:Hide()
end
end
function WorldMapPinMixin:OnReleased()
self._poi = nil
self._owner = nil
self._settings = nil
if self.texture then
self.texture:SetTexture(nil)
end
if self.label then
self.label:SetText("")
self.label:Hide()
end
GameTooltip:Hide()
end
function WorldMapPinMixin:OnMouseEnter()
local poi = self._poi
local owner = self._owner
if not poi then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(tostring(poi.label or "POI"), 1, 1, 1)
GameTooltip:AddLine(string.format("Map: %d", tonumber(poi.mapID) or 0), 0.8, 0.8, 0.8)
GameTooltip:AddLine(string.format("X: %.2f Y: %.2f", tonumber(poi.x) or 0, tonumber(poi.y) or 0), 0.8, 0.8, 0.8)
if poi.category then
local label = owner and owner.GetCategoryLabel and owner:GetCategoryLabel(poi.category) or tostring(poi.category)
GameTooltip:AddLine("Icon: " .. tostring(label), 0.8, 0.8, 0.8)
end
GameTooltip:AddLine("Left click: toggle waypoint", 0.8, 0.8, 0.8)
HMGT:SafeShowTooltip(GameTooltip)
end
function WorldMapPinMixin:OnMouseLeave()
GameTooltip:Hide()
end
function WorldMapPinMixin:OnClick(mouseButton)
if mouseButton ~= "LeftButton" then return end
local owner = self._owner
local poi = self._poi
if not owner or not poi then return end
owner:ToggleWaypointForPOI(poi)
end
function WorldMapPinMixin:SetPassThroughButtons() end
function WorldMapDataProviderMixin:RemoveAllData()
local map = self:GetMap()
if map then
map:RemoveAllPinsByTemplate(WORLD_MAP_PIN_TEMPLATE)
end
end
function WorldMapDataProviderMixin:RefreshAllData()
self:RemoveAllData()
local map = self:GetMap()
local owner = self.owner
if not map or not owner then return end
local settings = owner:GetSettings()
if not settings or settings.enabled ~= true then
return
end
local mapID = map:GetMapID()
if not mapID then
return
end
for _, poi in ipairs(settings.pois or {}) do
if tonumber(poi.mapID) == tonumber(mapID) then
map:AcquirePin(WORLD_MAP_PIN_TEMPLATE, poi, settings, owner)
end
end
end
function MapOverlay:EnsureWorldMapPinPool()
if not WorldMapFrame then
return false
end
WorldMapFrame.pinPools = WorldMapFrame.pinPools or {}
if WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE] then
self._worldMapPinPool = WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE]
return true
end
local canvas = WorldMapFrame.GetCanvas and WorldMapFrame:GetCanvas()
if not canvas then
canvas = WorldMapFrame.ScrollContainer and WorldMapFrame.ScrollContainer.Child
end
if not canvas then
return false
end
local pool
if type(CreateUnsecuredRegionPoolInstance) == "function" then
pool = CreateUnsecuredRegionPoolInstance(WORLD_MAP_PIN_TEMPLATE)
elseif type(CreateFramePool) == "function" then
pool = CreateFramePool("BUTTON")
end
if not pool then
return false
end
pool.parent = canvas
pool.createFunc = function()
local frame = CreateFrame("Button", nil, canvas)
frame:SetSize(1, 1)
if type(Mixin) == "function" then
Mixin(frame, WorldMapPinMixin)
end
if frame.OnLoad then
frame:OnLoad()
end
return frame
end
pool.resetFunc = function(_, pin)
pin:Hide()
pin:ClearAllPoints()
if pin.OnReleased then
pin:OnReleased()
end
pin.pinTemplate = nil
pin.owningMap = nil
end
-- Compatibility with older pool field names.
pool.creationFunc = pool.createFunc
pool.resetterFunc = pool.resetFunc
WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE] = pool
self._worldMapPinPool = pool
return true
end
function MapOverlay:EnsureWorldMapProvider()
if not WorldMapFrame or type(WorldMapFrame.AddDataProvider) ~= "function" then
return false
end
if not self:EnsureWorldMapPinPool() then
return false
end
if not self._worldMapProvider then
self._worldMapProvider = CreateFromMixins(WorldMapDataProviderMixin)
self._worldMapProvider.owner = self
end
if not self._worldMapProviderRegistered then
WorldMapFrame:AddDataProvider(self._worldMapProvider)
self._worldMapProviderRegistered = true
end
return true
end
function MapOverlay:Refresh()
if not self:EnsureWorldMapProvider() then
return
end
self._worldMapProvider:RefreshAllData()
end
function MapOverlay:HookWorldMap()
if self._mapHooked then return end
if not WorldMapFrame then return end
self._mapHooked = true
self:EnsureWorldMapProvider()
WorldMapFrame:HookScript("OnShow", function() self:Refresh() end)
WorldMapFrame:HookScript("OnHide", function()
if self._worldMapProvider then
self._worldMapProvider:RemoveAllData()
end
end)
hooksecurefunc(WorldMapFrame, "SetMapID", function()
self:Refresh()
end)
end
function MapOverlay:OnEnable()
self:GetSettings()
self:RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
self:RegisterEvent("ZONE_CHANGED_NEW_AREA", "Refresh")
self:RegisterEvent("ZONE_CHANGED", "Refresh")
self:RegisterEvent("ZONE_CHANGED_INDOORS", "Refresh")
self:RegisterEvent("PLAYER_ENTERING_WORLD", "Refresh")
self:HookWorldMap()
self:Refresh()
end
function MapOverlay:OnAddonLoaded(_, addonName)
if addonName == "Blizzard_WorldMap" then
self:HookWorldMap()
self:Refresh()
end
end
function MapOverlay:OnDisable()
self:UnregisterAllEvents()
if self._worldMapProvider then
self._worldMapProvider:RemoveAllData()
end
self:HidePOIManager()
end
local function GetMapModule(self)
if self and self.MapOverlay then return self.MapOverlay end
if self and self.GetModule then
return self:GetModule("MapOverlay", true)
end
return nil
end
function HMGT:AddMapPOI(mapID, x, y, label, icon, category)
local mod = GetMapModule(self)
if not mod then return false, "module missing" end
return mod:AddPOI(mapID, x, y, label, icon, category)
end
function HMGT:RemoveMapPOI(index)
local mod = GetMapModule(self)
if not mod then return false end
return mod:RemovePOI(index)
end
function HMGT:UpdateMapPOI(index, mapID, x, y, label, icon, category)
local mod = GetMapModule(self)
if not mod then return false, "module missing" end
return mod:UpdatePOI(index, mapID, x, y, label, icon, category)
end
function HMGT:BuildMapPOIListText(mapID)
local mod = GetMapModule(self)
if not mod then return "Map module missing." end
return mod:BuildPOIListText(mapID)
end
function HMGT:GetMapPOICategoryValues()
local mod = GetMapModule(self)
if not mod then
return { [DEFAULT_CATEGORY] = "Default" }
end
return mod:GetCategoryDropdownValues()
end
function HMGT:GetCurrentMapPOIData()
local mod = GetMapModule(self)
if not mod then
return nil, nil, nil, "module missing"
end
return mod:GetCurrentPlayerPosition()
end
function HMGT:ToggleMapPOIManager()
local mod = GetMapModule(self)
if not mod then return false end
mod:TogglePOIManager()
return true
end