Files
moon-well-ui/data/addon/ForeverUI/WorldMap.lua
T
Adrestie 5842eb7a2b ForeverUI : code en anglais, commentaires resserres, code mort retire
Traduction
- Identifiants en anglais dans tout le module : addon, ecrans d'accueil,
  outils Python, banc d'essai et patcheur C#.
- Fichiers au nom francais renommes (Gabarits.lua -> Templates.lua,
  Textes_*.lua -> Texts_*.lua, deployer.py -> deploy.py,
  tools/patcheur -> tools/patcher, etc.).
- Commandes /fui et options de deploy.py en anglais (--check, --server,
  --remove-dbc).
- Cles des variables sauvegardees et de data/dbc/statistics.json
  traduites, sans reprise des anciennes cles.
- API de la micro-barre : ForeverUI.AddMicroButton(def) avec les champs
  name, atlasSet, after, tooltip, onClick, ready, et
  ForeverUI.UpdateMicro(name, isOpen).
- Commentaires reecrits en anglais, courts, sans dates ni historique ;
  les fonctions au nom peu parlant et leurs parametres sont expliques.

Code mort
- Exports, fonctions et variables jamais lus retires.
- Retires a la demande des modules : ForeverUI.PvPJauge, les exports
  FondEntete / TuilerEntete / Polices de TradeSkill, l'art legacy-up-c60,
  honorfill(half)blue et inv_misc_book_09, leurs entrees d'atlas et les
  parties d'outils correspondantes.

Verifications : liaisons des noms comparees avant et apres, banc vert,
chemins de textures inchanges, generateurs reproduisant les fichiers,
patcheur identique octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:15:52 +02:00

1528 lines
55 KiB
Lua

-- World map with camelot's look (Map and Quest Log), in windowed and maximized modes.
-- It is built around the WotLK map: its frames, their names, its functions and CVars stay, so
-- addons that use WorldMapFrame keep working. Only the visible parts are added.
--
-- Hooks (WotLK WorldMapFrame.lua): WorldMap_ToggleSizeDown and WorldMapFrame_SetMiniMode for
-- the small window; WorldMap_ToggleSizeUp for the full screen (WorldMapFrame unparented over
-- the whole screen by SetupFullscreenScale, UIParent hidden, black background). The maximized
-- mode keeps that full screen and shows camelot's frame, map only, on a holder sized in UI
-- units (camelot blizzard_worldmap.lua UpdateMaximizedSize).
--
-- Scale: camelot fits the 1002 x 668 WotLK map into 697 x 465 (0.6956). WotLK places the
-- player arrow, landmarks and POI bounds with its own scales (WORLDMAP_WINDOWED_SIZE 0.573,
-- WORLDMAP_QUESTLIST_SIZE 0.691, WORLDMAP_FULLMAP_SIZE 1) in WorldMapFrame units, and the
-- engine's arrow does not follow the map scale. Writing those constants from the addon would
-- taint them, so WorldMapFrame itself is scaled (our scale / its scale) and our frames, the
-- quest pane and the red buttons take the inverse scale.
ForeverUI = ForeverUI or {}
local W = {}
ForeverUI.WorldMap = W
local L = ForeverUI.L
-- Layout in one table: the client's Lua 5.1 allows at most 60 upvalues per function.
-- Values come from camelot blizzard_worldmap.xml/.lua and its templates.
local G = {
width = 702, height = 534,
cardX = 2, mapY = -67, -- top-left corner of the map
mapW = 697, cardH = 465,
mapBottom = 2,
backgroundX1 = 2, backgroundY1 = -21, backgroundX2 = -2, backgroundY2 = 2,
trimY = -63, trimH = 3,
portraitX = -5, portraitY = 7, portraitSide = 62,
titleX1 = 58, titleX2 = -24, titleY = -1, titleH = 20, titleTextY = -5,
closeX = -2, closeY = 1, buttonSide = 24, maximizeX = -1,
-- the bar stops NAVBAR_X_OFFSET (-50) from the map's right edge,
-- which stays at 699 whether the quest pane is open or not
barX = 64 + 2, barY = -25, barRight = 2 + 697 - 50, barBottom = -67 + 9,
-- quest pane: minimizedWidth + questLogWidth (702 + 333)
paneWidth = 333,
-- SidePanelToggle: 32 x 32, BOTTOMRIGHT of the map (-2, 1)
toggleSide = 32, toggleX = -2, toggleY = 1,
filtersX = 10, filtersY = -2, filtersSide = 32,
floorW = 160, floorH = 25, floorX = 2, floorY = 0,
-- common-dropdown-textholder: 54 x 41, sliced by camelot's engine
-- (UiTextureAtlasElementSliceData.db2: 16 left, 19 right, none vertically)
floorEdgeLeft = 16, floorEdgeRight = 19, floorTextX = 8, floorTextY = -8,
coordX = 68, coordY = 2, coordW = 100, coordH = 15,
-- maximized mode
bannerH = 67, screenEdge = 30, mapRight = 3,
barXMaximized = 8 + 2,
usableMapW = 1002, usableMapH = 668,
}
G.scale = G.mapW / 1002
-- Metal border: camelot PortraitFrameTemplateMinimizable (mainline/nineslicelayouts.lua)
-- with the fixes of camelot/nineslicelayoutoverrides.lua.
local METAL = {
{ key = "topLeft", name = "ui-frame-portraitmetal-cornertopleft", point = "TOPLEFT", x = -13, y = 16 },
{ key = "topRight", name = "ui-frame-metal-cornertoprightdouble", point = "TOPRIGHT", x = 2, y = 16 },
{ key = "bottomLeft", name = "ui-frame-metal-cornerbottomleft", point = "BOTTOMLEFT", x = -13, y = -8 },
{ key = "bottomRight", name = "ui-frame-metal-cornerbottomright", point = "BOTTOMRIGHT", x = 2, y = -8 },
}
-- ButtonFrameTemplateNoPortraitMinimizable, same fixes: without the portrait,
-- both left corners move to x = -12
local METAL_MAXIMIZED = {
topLeft = { name = "ui-frame-metal-cornertopleft", x = -12, y = 16 },
bottomLeft = { name = "ui-frame-metal-cornerbottomleft", x = -12, y = -8 },
}
-- Camelot strings (GlobalStrings.db2). 3.3.5 lacks MAP_AND_QUEST_LOG, the coordinate
-- formats and WORLD: they are in Textes_<language>.lua.
local TEXT = {
title = L.WORLDMAP_TITLE, -- MAP_AND_QUEST_LOG
maximizedTitle = WORLD_MAP, -- WORLD_MAP (3.3.5 has it)
world = L.WORLDMAP_WORLD, -- WORLD
cursor = L.WORLDMAP_CURSOR_COORDS, -- WORLD_MAP_CURSOR_COORDS_INTEGER
player = L.WORLDMAP_PLAYER_COORDS, -- WORLD_MAP_PLAYER_COORDS_INTEGER
showRegion = L.WORLDMAP_FILTER_SHOW, -- WORLD_MAP_FILTER_LABEL_SHOW
}
-- Navigation bar (navigationbar.xml): two texture files, no atlas.
local NAV = {
sheet = "interface\\ForeverUI\\helpframe\\cs_helptextures",
tile = "interface\\ForeverUI\\helpframe\\cs_helptextures_tile",
shadow = "interface\\ForeverUI\\common\\shadowoverlay-left",
arrows = "interface\\ForeverUI\\buttons\\squarebuttontextures",
squareUp = "interface\\ForeverUI\\buttons\\ui-squarebutton-up",
squareDown = "interface\\ForeverUI\\buttons\\ui-squarebutton-down",
squareHover = "interface\\ForeverUI\\buttons\\ui-common-mousehilight",
background = { 0, 1, 0.1875, 0.25390625 }, -- _NavMenu-BarBG
veil = { 0, 1, 0.2578125, 0.32421875 }, -- _NavMenu-BarOverlay
buttonUp = { 0, 1, 0.0625, 0.12109375 }, -- _NavMenu-Button-Up
buttonBottom = { 0, 1, 0.125, 0.18359375 }, -- _NavMenu-Button-Down
buttonHover = { 0.00195313, 0.25195313, 0.65625, 0.92187500 },
buttonSelected = { 0.00195313, 0.25195313, 0.375, 0.640625 },
upArrow = { 0.88867188, 0.92968750, 0.29687500, 0.53125000 },
downArrow = { 0.63281250, 0.67382813, 0.75781250, 0.99218750 },
menuArrow = { 0.45312500, 0.64062500, 0.20312500, 0.01562500 },
homeUp = { 0.00781250, 0.24218750 },
homeDown = { 0.25781250, 0.49218750 },
homeHover = { 0.50781250, 0.74218750 },
homeRight = 0.703125,
buttonHeight = 30,
}
local PREFIX = "|cff66ccffForeverUI|r "
local function say(message)
DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. message)
end
-- Silences a client region: texture, alpha and visibility at once, because WotLK's
-- code shows again what is only hidden.
local function suppress(region)
if not region then
return
end
if region.SetTexture then
region:SetTexture(nil)
end
region:SetAlpha(0)
region:Hide()
end
-- Sets one button state from an atlas. SetNormalTexture only takes a path, and a 3.3.5
-- button only has the states its XML declares.
-- state: "Normal", "Pushed", "Disabled" or "Highlight"; name: atlas entry
local function applyButtonState(button, state, name)
local e = ForeverUI.AtlasEntry(name)
if not e then
return nil
end
local texture = button["Get" .. state .. "Texture"](button)
if not texture then
button["Set" .. state .. "Texture"](button, e[1])
texture = button["Get" .. state .. "Texture"](button)
end
if texture then
ForeverUI.SetAtlas(texture, name, true)
texture:ClearAllPoints()
texture:SetAllPoints(button)
end
return texture
end
local function isSmallWindow()
return WORLDMAP_SETTINGS and WORLDMAP_SETTINGS.size == WORLDMAP_WINDOWED_SIZE
end
-- ---------- Scale
-- WorldMapFrame scale in the small window: WotLK draws the map at 0.573
-- (WORLDMAP_WINDOWED_SIZE, read only); camelot needs 0.6956.
local function reducedScale()
return G.scale / (WORLDMAP_WINDOWED_SIZE or G.scale)
end
W.inverse = 1
-- ---------- Moving
-- The small window can be dragged by its title bar (camelot cannot do this; WotLK can).
-- WotLK's advanced mode (CVar advancedWorldMap, option ADVANCED_WORLD_MAP_TEXT) makes it a
-- "center" panel that the panel manager never moves (UIParent.lua:1694), movable and
-- anchored on WorldMapScreenAnchor, whose position the client saves. WorldMapTitleButton
-- drags it unless the map is locked (the default). The full screen does not move.
-- The CVar must be set before VARIABLES_LOADED, when WotLK reads it.
if GetCVar and SetCVar and GetCVar("advancedWorldMap") ~= "1" then
SetCVar("advancedWorldMap", "1")
end
-- The lock stays the client's (WORLDMAP_SETTINGS.locked, never written). When the map is
-- locked, WotLK refuses to drag, so these do the work of its WorldMapTitleButton_OnDragStart
-- / _OnDragStop after it (WorldMapFrame.lua:2063-2087). When unlocked, WotLK drags the map
-- itself and these do nothing.
local function dragStart()
if not (WORLDMAP_SETTINGS.advanced and WORLDMAP_SETTINGS.locked) then return end
if WORLDMAP_SETTINGS.selectedQuest then
WorldMapBlobFrame:DrawQuestBlob(WORLDMAP_SETTINGS.selectedQuestId, false)
end
WorldMapScreenAnchor:ClearAllPoints()
WorldMapFrame:ClearAllPoints()
WorldMapFrame:StartMoving()
end
local function dragStop()
if not (WORLDMAP_SETTINGS.advanced and WORLDMAP_SETTINGS.locked) then return end
WorldMapFrame:StopMovingOrSizing()
WorldMapBlobFrame_CalculateHitTranslations()
if WORLDMAP_SETTINGS.selectedQuest and not WORLDMAP_SETTINGS.selectedQuest.completed then
WorldMapBlobFrame:DrawQuestBlob(WORLDMAP_SETTINGS.selectedQuestId, true)
end
WorldMapScreenAnchor:StartMoving()
WorldMapScreenAnchor:SetPoint("TOPLEFT", WorldMapFrame)
WorldMapScreenAnchor:StopMovingOrSizing()
end
if WorldMapTitleButton and WorldMapTitleButton.HookScript then
WorldMapTitleButton:HookScript("OnDragStart", dragStart)
WorldMapTitleButton:HookScript("OnDragStop", dragStop)
end
-- The anchor starts at the screen's top-left corner (XML). It is moved to where camelot
-- opens its map: a "left" panel at LEFT_OFFSET 16, TOP_OFFSET -116
-- (uipanellayoutframe.lua:3-4). Like WorldMapFrame_ToggleAdvanced, it goes through
-- StartMoving so the client saves the position.
local ANCHOR = { x = 16, y = -116 }
local function placeAnchor()
local anchor = WorldMapScreenAnchor
if not anchor or (anchor.IsUserPlaced and anchor:IsUserPlaced()) then
return
end
anchor:StartMoving()
anchor:ClearAllPoints()
anchor:SetPoint("TOPLEFT", UIParent, "TOPLEFT", ANCHOR.x, ANCHOR.y)
anchor:StopMovingOrSizing()
end
-- ---------- Skin
-- Two layers, as in camelot: the background below the map (map strata, WorldMapFrame
-- level) and the frame above it (HIGH strata, like BorderFrame). Neither takes the mouse:
-- in the small window WotLK disables the mouse on WorldMapFrame, and the map must stay
-- clickable.
local function buildBackground(map)
local background = CreateFrame("Frame", "ForeverUIWorldMapBackground", map)
background:SetAllPoints(map)
-- Bg: UI-Background-Rock is a whole file, so it can tile.
local rock = background:CreateTexture(nil, "BACKGROUND")
rock:SetTexture("interface\\ForeverUI\\framegeneral\\ui-background-rock", true)
if rock.SetHorizTile then
rock:SetHorizTile(true)
rock:SetVertTile(true)
end
rock:SetPoint("TOPLEFT", background, "TOPLEFT", G.backgroundX1, G.backgroundY1)
rock:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", G.backgroundX2, G.backgroundY2)
-- OverscrollBG, on the map rectangle (the canvas, which follows the map in both modes).
-- Its textures are regions of the background, not a child frame: a child frame sits one
-- level above its parent (89), over WotLK's tiles (WorldMapDetailFrame, 88), and hides
-- them. The tile is an atlas and 3.3.5 cannot tile an atlas rectangle, so it is stretched.
local canvas = W.canvas
local tile = background:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(tile, "gamepad-mapquestlog-bgtile-2k", true)
tile:SetPoint("TOPLEFT", canvas, "TOPLEFT", 0, 0)
tile:SetPoint("BOTTOMRIGHT", canvas, "BOTTOMRIGHT", 0, 0)
background.tile = tile
local e = ForeverUI.AtlasEntry("gamepad-mapquestlog-bg-vignette")
if e then
-- four quarters, mirrored by texture coordinates (blizzard_worldmap.xml)
local quarters = {
{ "TOPLEFT", "TOPLEFT", "BOTTOMRIGHT", "CENTER", e[2], e[3], e[4], e[5] },
{ "TOPRIGHT", "TOPRIGHT", "BOTTOMLEFT", "CENTER", e[3], e[2], e[4], e[5] },
{ "BOTTOMLEFT", "BOTTOMLEFT", "TOPRIGHT", "CENTER", e[2], e[3], e[5], e[4] },
{ "BOTTOMRIGHT", "BOTTOMRIGHT", "TOPLEFT", "CENTER", e[3], e[2], e[5], e[4] },
}
for _, q in ipairs(quarters) do
local v = background:CreateTexture(nil, "BORDER")
v:SetTexture(e[1])
v:SetTexCoord(q[5], q[6], q[7], q[8])
v:SetPoint(q[1], canvas, q[2], 0, 0)
v:SetPoint(q[3], canvas, q[4], 0, 0)
end
end
return background
end
local function buildFrame(map)
local frame = CreateFrame("Frame", "ForeverUIWorldMapBorder", map)
frame:SetAllPoints(map)
frame:SetFrameStrata("HIGH")
-- trim under the title area
local trim = frame:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(trim, "_ui-frame-innertoptile", true)
trim:SetHeight(G.trimH)
trim:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, G.trimY)
trim:SetPoint("RIGHT", frame, "LEFT", G.cardX + G.mapW, 0)
frame.trim = trim
-- portrait, below the metal (PortraitContainer 400 < NineSlice 500)
local portrait = CreateFrame("Frame", nil, frame)
portrait:SetAllPoints(frame)
portrait:SetFrameLevel(frame:GetFrameLevel() + 1)
local image = portrait:CreateTexture(nil, "OVERLAY")
image:SetWidth(G.portraitSide)
image:SetHeight(G.portraitSide)
image:SetPoint("TOPLEFT", frame, "TOPLEFT", G.portraitX, G.portraitY)
local book = "interface\\ForeverUI\\questframe\\ui-questlog-bookicon"
if SetPortraitToTexture then
SetPortraitToTexture(image, book)
else
image:SetTexture(book)
end
frame.portrait = image
frame.portraitFrame = portrait
-- metal border
local metal = CreateFrame("Frame", nil, frame)
metal:SetAllPoints(frame)
metal:SetFrameLevel(frame:GetFrameLevel() + 2)
local p = {}
for _, corner in ipairs(METAL) do
local t = metal:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(t, corner.name)
t:SetPoint(corner.point, metal, corner.point, corner.x, corner.y)
p[corner.key] = t
end
frame.corners = p
local function edge(name, a1, c1, r1, a2, c2, r2)
local t = metal:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(t, name)
t:SetPoint(a1, c1, r1)
t:SetPoint(a2, c2, r2)
end
edge("_ui-frame-metal-edgetop", "TOPLEFT", p.topLeft, "TOPRIGHT", "TOPRIGHT", p.topRight, "TOPLEFT")
edge("_ui-frame-metal-edgebottom", "BOTTOMLEFT", p.bottomLeft, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "BOTTOMLEFT")
edge("!ui-frame-metal-edgeleft", "TOPLEFT", p.topLeft, "BOTTOMLEFT", "BOTTOMLEFT", p.bottomLeft, "TOPLEFT")
edge("!ui-frame-metal-edgeright", "TOPRIGHT", p.topRight, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "TOPRIGHT")
frame.metal = metal
-- title strip, above the metal (TitleContainer 510)
local banner = CreateFrame("Frame", nil, frame)
banner:SetFrameLevel(frame:GetFrameLevel() + 3)
banner:SetHeight(G.titleH)
banner:SetPoint("TOPLEFT", frame, "TOPLEFT", G.titleX1, G.titleY)
banner:SetPoint("TOPRIGHT", frame, "TOPRIGHT", G.titleX2, G.titleY)
frame.banner = banner
return frame
end
-- ---------- Navigation bar
-- camelot navigationbar.lua on WotLK data: World > continent > zone. The root button
-- shows the world view. A button's list holds its siblings: GetMapContinents for a
-- continent, GetMapZones for a zone.
local navMenu = CreateFrame("Frame", "ForeverUIWorldMapNavMenu", UIParent, "UIDropDownMenuTemplate")
navMenu:Hide()
-- Opens the shared dropdown list under anchor.
-- list: entries (text, func, checked, isTitle, ...); minimum: smallest list width, the
-- width of the dropdown that opens it (DropdownButtonMixin:RegisterMenu, SetMinimumWidth)
local function openMenu(anchor, list, minimum)
navMenu.foreverMinimum = minimum
UIDropDownMenu_Initialize(navMenu, function()
for _, entry in ipairs(list) do
local info = UIDropDownMenu_CreateInfo()
info.text = entry.text
info.func = entry.func
info.checked = entry.checked
info.notCheckable = entry.checked == nil and 1 or nil
info.keepShownOnClick = entry.keepShownOnClick
info.isTitle = entry.isTitle
info.notClickable = entry.isTitle
info.disabled = entry.disabled
UIDropDownMenu_AddButton(info)
end
end, "MENU")
ToggleDropDownMenu(1, nil, navMenu, anchor, 0, 0)
-- Always downward. 3.3.5's ToggleDropDownMenu flips the list above the button when it
-- would pass the screen bottom (long zone lists in the small window). Camelot menus
-- (Blizzard_Menu) stay below their button and are clamped to the screen, so the list is
-- moved back below and clamped while it is open.
local list1 = DropDownList1
if list1 and list1:IsShown() then
local point = list1:GetPoint(1)
if point and string.find(point, "^BOTTOM") then
list1:ClearAllPoints()
list1:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, 0)
end
if not list1.foreverClamped then
list1.foreverClamped = true
list1.foreverClampedBefore = list1.IsClampedToScreen and list1:IsClampedToScreen() or false
list1:SetClampedToScreen(true)
end
end
end
W.openMenu = openMenu
-- list closed: give back its clamping to the game's other menus
if DropDownList1 then
DropDownList1:HookScript("OnHide", function(self)
if self.foreverClamped then
self:SetClampedToScreen(self.foreverClampedBefore and true or false)
self.foreverClamped, self.foreverClampedBefore = nil, nil
end
end)
end
local function placeOnSheet(texture, coords)
texture:SetTexture(NAV.sheet)
texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
end
-- Rows of the tiled sheet have a uniform width, so they are stretched instead of tiled
-- (3.3.5 tiling would ignore the chosen row).
local function placeOnTile(texture, coords)
texture:SetTexture(NAV.tile)
texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
end
local function createNavButton(bar, index)
local b = CreateFrame("Button", "ForeverUIWorldMapNavButton" .. index, bar)
b:SetHeight(NAV.buttonHeight)
b:SetNormalTexture(NAV.tile)
placeOnTile(b:GetNormalTexture(), NAV.buttonUp)
b:SetPushedTexture(NAV.tile)
placeOnTile(b:GetPushedTexture(), NAV.buttonBottom)
b:SetDisabledTexture(NAV.tile)
placeOnTile(b:GetDisabledTexture(), NAV.buttonUp)
b:SetHighlightTexture(NAV.sheet)
placeOnSheet(b:GetHighlightTexture(), NAV.buttonHover)
b:GetHighlightTexture():SetBlendMode("ADD")
local text = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetJustifyH("LEFT")
text:SetHeight(12)
text:SetPoint("LEFT", b, "LEFT", 20, 0)
b:SetFontString(text)
b.text = text
b.arrowUp = b:CreateTexture(nil, "OVERLAY")
b.arrowUp:SetWidth(21)
b.arrowUp:SetHeight(30)
b.arrowUp:SetPoint("LEFT", b, "RIGHT", 0, 0)
placeOnSheet(b.arrowUp, NAV.upArrow)
b.arrowDown = b:CreateTexture(nil, "OVERLAY")
b.arrowDown:SetWidth(21)
b.arrowDown:SetHeight(30)
b.arrowDown:SetPoint("LEFT", b, "RIGHT", 0, 0)
placeOnSheet(b.arrowDown, NAV.downArrow)
b.arrowDown:Hide()
b.selected = b:CreateTexture(nil, "OVERLAY")
b.selected:SetAllPoints(b)
placeOnSheet(b.selected, NAV.buttonSelected)
b.selected:Hide()
b:SetScript("OnMouseDown", function(self)
if self:IsEnabled() == 1 then
self.arrowUp:Hide()
self.arrowDown:Show()
end
end)
b:SetScript("OnMouseUp", function(self)
self.arrowDown:Hide()
self.arrowUp:Show()
end)
-- MenuArrowButton: 27 x 31, RIGHT on TOPRIGHT (-2, -15)
local m = CreateFrame("Button", nil, b)
m:SetWidth(27)
m:SetHeight(31)
m:SetPoint("RIGHT", b, "TOPRIGHT", -2, -15)
m.Art = m:CreateTexture(nil, "OVERLAY")
m.Art:SetWidth(12)
m.Art:SetHeight(12)
m.Art:SetPoint("CENTER", m, "CENTER", 0, -1)
m.Art:SetTexture(NAV.arrows)
m.Art:SetTexCoord(NAV.menuArrow[1], NAV.menuArrow[2], NAV.menuArrow[3], NAV.menuArrow[4])
m:SetNormalTexture(NAV.squareUp)
m:GetNormalTexture():SetAlpha(0)
m:SetPushedTexture(NAV.squareDown)
m:GetPushedTexture():SetAlpha(0)
m:SetHighlightTexture(NAV.squareHover)
m:GetHighlightTexture():SetBlendMode("ADD")
for _, t in ipairs({ m:GetNormalTexture(), m:GetPushedTexture(), m:GetHighlightTexture() }) do
t:ClearAllPoints()
t:SetWidth(32)
t:SetHeight(32)
t:SetPoint("CENTER", m, "CENTER", 0, 0)
end
m:SetScript("OnMouseDown", function(self) self.Art:SetPoint("CENTER", self, "CENTER", -1, -2) end)
m:SetScript("OnMouseUp", function(self) self.Art:SetPoint("CENTER", self, "CENTER", 0, -1) end)
m:SetScript("OnEnter", function(self)
self:GetNormalTexture():SetAlpha(1)
self:GetPushedTexture():SetAlpha(1)
end)
m:SetScript("OnLeave", function(self)
self:GetNormalTexture():SetAlpha(0)
self:GetPushedTexture():SetAlpha(0)
end)
m:SetScript("OnClick", function(self)
local parent = self:GetParent()
if parent.listFunc then
openMenu(self, parent.listFunc())
end
end)
b.MenuArrowButton = m
b:SetScript("OnClick", function(self)
if self.myclick then
self.myclick()
end
end)
return b
end
local function buildBar(map)
local bar = CreateFrame("Frame", "ForeverUIWorldMapNavBar", map)
bar:SetPoint("TOPLEFT", map, "TOPLEFT", G.barX, G.barY)
bar:SetPoint("BOTTOMRIGHT", map, "TOPLEFT", G.barRight, G.barBottom)
local background = bar:CreateTexture(nil, "BACKGROUND")
background:SetAllPoints(bar)
placeOnTile(background, NAV.background)
-- inner border (WorldMapNavBarTemplate), BORDER layer
local function piece(name)
local t = bar:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(t, name)
return t
end
local bottomLeft = piece("ui-frame-innerbotleftcorner")
bottomLeft:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", -3, -3)
local bottomRight = piece("ui-frame-innerbotright")
bottomRight:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 3, -3)
local down = piece("_ui-frame-innerbottile")
down:SetPoint("BOTTOMLEFT", bottomLeft, "BOTTOMRIGHT")
down:SetPoint("BOTTOMRIGHT", bottomRight, "BOTTOMLEFT")
local left = piece("!ui-frame-innerlefttile")
left:SetPoint("TOPLEFT", bar, "TOPLEFT", -3, 0)
-- as written in the source: BOTTOMLEFT on the TOPLEFT of
-- InsetBorderBottomRight (blizzard_worldmaptemplates.xml:97)
left:SetPoint("BOTTOMLEFT", bottomRight, "TOPLEFT")
local right = piece("!ui-frame-innerrighttile")
right:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 3, 0)
right:SetPoint("BOTTOMRIGHT", bottomRight, "TOPRIGHT")
-- root button
local home = CreateFrame("Button", "ForeverUIWorldMapNavBarHomeButton", bar)
home:SetHeight(NAV.buttonHeight)
home:SetNormalTexture(NAV.sheet)
home:SetPushedTexture(NAV.sheet)
home:SetHighlightTexture(NAV.sheet)
home:GetHighlightTexture():SetBlendMode("ADD")
local text = home:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetJustifyH("LEFT")
text:SetHeight(12)
text:SetPoint("LEFT", home, "LEFT", 10, 0)
text:SetPoint("RIGHT", home, "RIGHT", -30, 0)
home:SetFontString(text)
home.text = text
home:SetText(TEXT.world)
local width = math.min(128, text:GetStringWidth() + 50)
local du = (width / 128) * 0.25
local d = NAV.homeRight
home:GetNormalTexture():SetTexCoord(d - du, d, NAV.homeUp[1], NAV.homeUp[2])
home:GetPushedTexture():SetTexCoord(d - du, d, NAV.homeDown[1], NAV.homeDown[2])
home:GetHighlightTexture():SetTexCoord(d - du, d + 0.01, NAV.homeHover[1], NAV.homeHover[2])
home:SetWidth(width)
home.xoffset = -15
local shadow = home:CreateTexture(nil, "OVERLAY")
shadow:SetTexture(NAV.shadow)
shadow:SetWidth(30)
shadow:SetHeight(30)
shadow:SetPoint("LEFT", home, "LEFT", 0, 0)
home:SetPoint("LEFT", bar, "LEFT", 0, 0)
-- The World button shows the map that chooses between Azeroth and Outland:
-- WORLDMAP_COSMIC_ID, like WotLK's zoom out from Azeroth (WorldMapZoomOutButton_OnClick).
-- The cosmic view is detected as WorldMapFrame_Update does: GetMapInfo() returns nil and
-- the continent is WORLDMAP_COSMIC_ID. From a dungeon WotLK only leaves through ZoomOut,
-- so SetMapZoom then ZoomOut are tried while the map keeps changing.
home.myclick = function()
PlaySound("igMainMenuOptionCheckBoxOn")
local cosmicId = WORLDMAP_COSMIC_ID or -1
local function isCosmic()
return GetMapInfo() == nil and GetCurrentMapContinent() == cosmicId
end
local function state()
return tostring(GetMapInfo()) .. ":" .. tostring(GetCurrentMapContinent()) .. ":"
.. tostring(GetCurrentMapZone()) .. ":" .. tostring(GetCurrentMapDungeonLevel())
end
for _ = 1, 6 do
if isCosmic() then break end
SetMapZoom(cosmicId)
if isCosmic() then break end
local before = state()
ZoomOut()
if state() == before then break end
end
-- the client refused: print what it reports, for diagnosis
if not isCosmic() then
say(string.format(L.WORLDMAP_DEBUG_WORLD_REFUSED,
tostring(GetMapInfo()), tostring(GetCurrentMapContinent()), tostring(GetCurrentMapZone()),
tostring(GetCurrentMapDungeonLevel()), tostring(IsZoomOutAvailable and IsZoomOutAvailable())))
end
end
home:SetScript("OnClick", function(self) self.myclick() end)
bar.home = home
-- veil, above the buttons
local veil = CreateFrame("Frame", nil, bar)
veil:SetAllPoints(bar)
local t = veil:CreateTexture(nil, "OVERLAY")
t:SetAllPoints(veil)
placeOnTile(t, NAV.veil)
bar.veil = veil
bar.buttons = {}
return bar
end
-- Hierarchy of the displayed map as 3.3.5 knows it: continent, then zone, each with
-- its click action and the list of its siblings.
local function hierarchy()
local list = {}
local continent = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local zone = GetCurrentMapZone and GetCurrentMapZone() or 0
if continent and continent > 0 then
local continents = { GetMapContinents() }
table.insert(list, {
name = continents[continent],
OnClick = function() SetMapZoom(continent) end,
listFunc = function()
local l = {}
for i, name in ipairs({ GetMapContinents() }) do
table.insert(l, { text = name, func = function() SetMapZoom(i) end })
end
return l
end,
})
if zone and zone > 0 then
local zones = { GetMapZones(continent) }
table.insert(list, {
name = zones[zone],
OnClick = function() SetMapZoom(continent, zone) end,
listFunc = function()
local l = {}
for i, name in ipairs({ GetMapZones(continent) }) do
table.insert(l, { text = name, func = function() SetMapZoom(continent, i) end })
end
return l
end,
})
end
end
return list
end
W.hierarchy = hierarchy
-- NavBar_Reset + NavBar_AddButton + NavBar_CheckLength: a button is its text + 53 wide
-- with a list, + 30 without; each one anchors right of the previous one at its xoffset
-- (-15 after the root button); the last one is disabled and shows the selection image.
local function refreshBar()
local bar = W.bar
if not bar then
return
end
for _, b in ipairs(bar.buttons) do
b:Hide()
end
local list = hierarchy()
local previous = bar.home
local level = bar:GetFrameLevel() + 1
bar.home:SetFrameLevel(level)
for i, data in ipairs(list) do
local b = bar.buttons[i]
if not b then
b = createNavButton(bar, i)
bar.buttons[i] = b
end
b:SetText(data.name or "")
if data.listFunc then
b.MenuArrowButton:Show()
b:SetWidth(b.text:GetStringWidth() + 53)
else
b.MenuArrowButton:Hide()
b:SetWidth(b.text:GetStringWidth() + 30)
end
b.myclick = data.OnClick
b.listFunc = data.listFunc
b:ClearAllPoints()
b:SetPoint("LEFT", previous, "RIGHT", previous.xoffset or 0, 0)
level = level + 1
b:SetFrameLevel(level)
b:Show()
previous = b
end
-- the last button is the current location
local last = #list
for i = 1, last do
local b = bar.buttons[i]
if i < last then
b.selected:Hide()
b:Enable()
else
b.selected:Show()
b:SetButtonState("NORMAL")
b:Disable()
end
end
-- World is the current location only on the cosmic view (no map name, continent
-- WORLDMAP_COSMIC_ID). In a dungeon 3.3.5 gives no continent or zone: the trail is
-- empty, but World must stay clickable, as on Azeroth.
local cosmicId = GetMapInfo() == nil and GetCurrentMapContinent() == (WORLDMAP_COSMIC_ID or -1)
if last == 0 and cosmicId then
bar.home:SetButtonState("NORMAL")
bar.home:Disable()
else
bar.home:Enable()
end
bar.veil:SetFrameLevel(level + 1)
end
W.refreshBar = refreshBar
-- ---------- Filters
-- camelot/blizzard_worldmaptemplates.xml. Camelot's menu lists its map filters; 3.3.5
-- has only two: quest objectives (CVar questPOI, WotLK's checkbox) and difficulty
-- colors (mapQuestDifficulty).
local function filters()
return {
{ text = TEXT.showRegion, isTitle = 1 },
{
text = SHOW_QUEST_OBJECTIVES_ON_MAP_TEXT or QUEST_OBJECTIVES,
checked = WorldMapQuestShowObjectives and WorldMapQuestShowObjectives:GetChecked() and true or false,
keepShownOnClick = 1,
func = function()
if WorldMapQuestShowObjectives then
WorldMapQuestShowObjectives:Click()
end
end,
},
{
text = MAP_QUEST_DIFFICULTY_TEXT,
checked = MAP_QUEST_DIFFICULTY == "1",
keepShownOnClick = 1,
func = function()
local value = MAP_QUEST_DIFFICULTY == "1" and "0" or "1"
SetCVar("mapQuestDifficulty", value)
MAP_QUEST_DIFFICULTY = value
if WorldMapFrame_ResetQuestColors then
WorldMapFrame_ResetQuestColors()
end
if WorldMapFrame:IsShown() and WatchFrame and WatchFrame.showObjectives then
WorldMapFrame_DisplayQuests()
end
end,
},
}
end
local function buildFilters(map, bar)
local b = CreateFrame("Button", "ForeverUIWorldMapFilterButton", map)
b:SetFrameStrata("HIGH")
b:SetWidth(G.filtersSide)
b:SetHeight(G.filtersSide)
b:SetPoint("LEFT", bar, "RIGHT", G.filtersX, G.filtersY)
local icon = b:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(icon, "common-dropdown-a-button")
icon:SetPoint("TOPLEFT", b, "TOPLEFT", 4, -6)
b.Icon = icon
b:SetHighlightTexture(ForeverUI.AtlasEntry("common-dropdown-a-button")[1])
local hover = b:GetHighlightTexture()
ForeverUI.SetAtlas(hover, "common-dropdown-a-button")
hover:ClearAllPoints()
hover:SetPoint("TOPLEFT", b, "TOPLEFT", 4, -6)
hover:SetBlendMode("ADD")
hover:SetAlpha(0.4)
b:SetScript("OnMouseDown", function(self) ForeverUI.SetAtlas(self.Icon, "common-dropdown-a-button-pressed", true) end)
b:SetScript("OnMouseUp", function(self) ForeverUI.SetAtlas(self.Icon, "common-dropdown-a-button", true) end)
b:SetScript("OnClick", function(self) openMenu(self, filters()) end)
return b
end
-- ---------- Floors
-- WowStyle1DropdownTemplate: background common-dropdown-textholder from (-8, 7) to
-- (8, -9), arrow common-dropdown-a-button at RIGHT (1, -3), GameFontHighlight text from
-- (8, -8) to the arrow. Shown only when the map has floors
-- (WorldMapFloorNavigationFrameMixin:Refresh).
-- Name of floor i as WotLK builds it: DUNGEON_FLOOR_<MAP><n>, n counted from 0 when
-- the dungeon also uses the terrain map; FLOOR_NUMBER when no name exists.
local function floorName(i)
local name = strupper(GetMapInfo() or "")
local number = i
if DungeonUsesTerrainMap and DungeonUsesTerrainMap() then
number = i - 1
end
return _G["DUNGEON_FLOOR_" .. name .. number] or string.format(FLOOR_NUMBER, i)
end
local function buildFloors(map)
local b = CreateFrame("Button", "ForeverUIWorldMapFloorButton", map)
b:SetFrameStrata("HIGH")
b:SetWidth(G.floorW)
b:SetHeight(G.floorH)
b:SetPoint("TOPLEFT", map, "TOPLEFT", G.cardX + G.floorX, G.mapY - G.floorY)
-- Background in three pieces: stretched in one piece, its 16 and 19 pixels of
-- transparent shadow grow with the width, and the visible box looks narrow and shifted
-- right. Variant c60, the one camelot shows.
local e = ForeverUI.AtlasEntry("common-dropdown-textholder-c60")
local background = CreateFrame("Frame", nil, b)
background:SetPoint("TOPLEFT", b, "TOPLEFT", -8, 7)
background:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 8, -9)
if e then
local du = (e[3] - e[2]) / e[6]
local u1, u2 = e[2] + G.floorEdgeLeft * du, e[3] - G.floorEdgeRight * du
local function piece(a, z)
local t = b:CreateTexture(nil, "BACKGROUND")
t:SetTexture(e[1])
t:SetTexCoord(a, z, e[4], e[5])
return t
end
local g = piece(e[2], u1)
g:SetWidth(G.floorEdgeLeft)
g:SetPoint("TOPLEFT", background, "TOPLEFT", 0, 0)
g:SetPoint("BOTTOMLEFT", background, "BOTTOMLEFT", 0, 0)
local d = piece(u2, e[3])
d:SetWidth(G.floorEdgeRight)
d:SetPoint("TOPRIGHT", background, "TOPRIGHT", 0, 0)
d:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", 0, 0)
local m = piece(u1, u2)
m:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0)
m:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0)
b.background = { g, m, d }
end
local arrow = b:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(arrow, "common-dropdown-a-button")
arrow:SetPoint("RIGHT", b, "RIGHT", 1, -3)
local text = b:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
text:SetJustifyH("LEFT")
text:SetHeight(10)
text:SetPoint("TOPLEFT", b, "TOPLEFT", G.floorTextX, G.floorTextY)
text:SetPoint("TOPRIGHT", arrow, "LEFT", 0, 0)
b.Text = text
b.Arrow = arrow
b:SetScript("OnMouseDown", function(self) ForeverUI.SetAtlas(self.Arrow, "common-dropdown-a-button-pressed", true) end)
b:SetScript("OnMouseUp", function(self) ForeverUI.SetAtlas(self.Arrow, "common-dropdown-a-button", true) end)
b:SetScript("OnClick", function(self)
local l = {}
local current = GetCurrentMapDungeonLevel()
for i = 1, GetNumDungeonMapLevels() do
table.insert(l, {
text = floorName(i),
checked = (i == current),
func = function() SetDungeonMapLevel(i) end,
})
end
openMenu(self, l, self:GetWidth())
end)
b:Hide()
return b
end
-- Room for the name: from the left edge (+8) to the arrow, which overhangs the button
-- by 1 on the right (RIGHT (1, -3)).
function W.floorTextWidth()
local arrow = ForeverUI.AtlasEntry("common-dropdown-a-button")
return G.floorW + 1 - (arrow and arrow[6] or 27) - G.floorTextX
end
-- wordwrap="false": a name that is too long ends with an ellipsis. Characters are
-- removed (never splitting a UTF-8 character) until the name fits.
-- fs: font string; width: room available, nil for no limit
function W.writeElided(fs, text, width)
fs:SetText(text)
if not width or fs:GetStringWidth() <= width then
return
end
local points = "..."
local isTruncated = text
while #isTruncated > 0 do
isTruncated = string.sub(isTruncated, 1, -2)
-- a UTF-8 character cut in the middle is removed whole
while #isTruncated > 0 and string.byte(isTruncated, -1) >= 128 and string.byte(isTruncated, -1) < 192 do
isTruncated = string.sub(isTruncated, 1, -2)
end
if #isTruncated > 0 and string.byte(isTruncated, -1) >= 192 then
isTruncated = string.sub(isTruncated, 1, -2)
end
fs:SetText(isTruncated .. points)
if fs:GetStringWidth() <= width then
return
end
end
fs:SetText(points)
end
local function refreshFloors()
local b = W.floors
if not b then
return
end
if GetNumDungeonMapLevels and GetNumDungeonMapLevels() > 0 then
W.writeElided(b.Text, floorName(GetCurrentMapDungeonLevel()), W.floorTextWidth())
b:Show()
else
b:Hide()
end
end
-- ---------- Coordinates
-- WorldMapCoordsPanelTemplate: two 100 x 15 rows, GameFontHighlightSmall, left-aligned.
-- The cursor row shows only while the mouse is over the map, the player row only when
-- the player is on the displayed map (3.3.5 cannot read the position on another map
-- without changing the displayed one).
local function buildCoordinates(map)
local c = CreateFrame("Frame", "ForeverUIWorldMapCoords", map)
c:SetWidth(G.coordW)
c:SetHeight(G.coordH * 2)
c:SetPoint("BOTTOMLEFT", map, "TOPLEFT", G.cardX + G.coordX, G.mapY - G.cardH + G.coordY)
local function row()
local t = c:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
t:SetJustifyH("LEFT")
t:SetWidth(G.coordW)
t:SetHeight(G.coordH)
return t
end
c.player = row()
c.player:SetPoint("BOTTOMLEFT", c, "BOTTOMLEFT", 0, 0)
c.cursor = row()
c.pending = 0
c:SetScript("OnUpdate", function(self, elapsed)
self.pending = self.pending - (elapsed or 0)
if self.pending > 0 then
return
end
self.pending = 0.05
W.updateCoordinates()
end)
return c
end
function W.updateCoordinates()
local c = W.coords
if not c then
return
end
local x, y = GetPlayerMapPosition("player")
local player = x and y and not (x == 0 and y == 0)
if player then
c.player:SetText(string.format(TEXT.player, math.floor(x * 100 + 0.5), math.floor(y * 100 + 0.5)))
c.player:Show()
else
c.player:Hide()
end
local button = WorldMapButton
local cursor = false
if button and button:IsVisible() and button:IsMouseOver() then
local cx, cy = GetCursorPosition()
local scale = button:GetEffectiveScale()
cx, cy = cx / scale, cy / scale
local left, top = button:GetLeft(), button:GetTop()
local l, h = button:GetWidth(), button:GetHeight()
if left and top and l > 0 and h > 0 then
local nx, ny = (cx - left) / l, (top - cy) / h
if nx >= 0 and nx <= 1 and ny >= 0 and ny <= 1 then
c.cursor:SetText(string.format(TEXT.cursor, math.floor(nx * 100 + 0.5), math.floor(ny * 100 + 0.5)))
cursor = true
end
end
end
c.cursor:ClearAllPoints()
if player then
c.cursor:SetPoint("BOTTOMLEFT", c.player, "TOPLEFT", 0, 0)
else
c.cursor:SetPoint("BOTTOMLEFT", c, "BOTTOMLEFT", 0, 0)
end
if cursor then c.cursor:Show() else c.cursor:Hide() end
end
-- ---------- Tiles
-- WorldMapFrame.xml:541-636: twelve 256 x 256 tiles in 4 x 3 (1024 x 768) for a usable
-- map of 1002 x 668. WotLK crops nothing: its borders cover the overflow. Camelot's
-- border does not, so the last column is cropped to 1002 - 768 = 234 and the last row
-- to 668 - 512 = 156. WorldMapFrame_Update reloads the tiles on each map change, so
-- this runs again after it.
-- active: true crops, false restores the full tiles
local TILES = { side = 256, columns = 4, rowLines = 3, usableW = 1002, usableH = 668 }
local function cropTiles(active)
local lastW = TILES.usableW - TILES.side * (TILES.columns - 1)
local lastH = TILES.usableH - TILES.side * (TILES.rowLines - 1)
for rowLine = 1, TILES.rowLines do
for column = 1, TILES.columns do
local tile = _G["WorldMapDetailTile" .. ((rowLine - 1) * TILES.columns + column)]
if tile then
local l, h = TILES.side, TILES.side
if active and column == TILES.columns then l = lastW end
if active and rowLine == TILES.rowLines then h = lastH end
tile:SetWidth(l)
tile:SetHeight(h)
tile:SetTexCoord(0, l / TILES.side, 0, h / TILES.side)
end
end
end
end
W.cropTiles = cropTiles
-- ---------- Red buttons
local function skinRedButtons(map, frame)
local close = WorldMapFrameCloseButton
if close then
close:SetFrameStrata("HIGH")
close:SetFrameLevel(frame:GetFrameLevel() + 4)
close:SetWidth(G.buttonSide)
close:SetHeight(G.buttonSide)
close:SetHitRectInsets(0, 0, 0, 0)
applyButtonState(close, "Normal", "redbutton-exit")
applyButtonState(close, "Pushed", "redbutton-exit-pressed")
applyButtonState(close, "Disabled", "redbutton-exit-disabled")
local s = applyButtonState(close, "Highlight", "redbutton-highlight")
if s then s:SetBlendMode("ADD") end
end
local grow = WorldMapFrameSizeUpButton
if grow then
grow:SetFrameStrata("HIGH")
grow:SetFrameLevel(frame:GetFrameLevel() + 4)
grow:SetWidth(G.buttonSide)
grow:SetHeight(G.buttonSide)
grow:SetHitRectInsets(0, 0, 0, 0)
applyButtonState(grow, "Normal", "redbutton-expand")
applyButtonState(grow, "Pushed", "redbutton-expand-pressed")
applyButtonState(grow, "Disabled", "redbutton-expand-disabled")
local s = applyButtonState(grow, "Highlight", "redbutton-highlight")
if s then s:SetBlendMode("ADD") end
end
end
-- ---------- Quest pane toggle
-- WorldMapSidePanelToggleTemplate: two 32 x 32 buttons at the same place, one to open
-- (QuestCollapse-Show), one to close (QuestCollapse-Hide), each on the
-- MapCornerShadow-Right shadow, hover UI-Common-MouseHilight in ADD at 48 x 48.
local function buildToggle(map)
local frame = CreateFrame("Frame", "ForeverUIWorldMapSidePanelToggle", map)
frame:SetWidth(G.toggleSide)
frame:SetHeight(G.toggleSide)
frame:SetPoint("BOTTOMRIGHT", map, "TOPLEFT",
G.cardX + G.mapW + G.toggleX, G.mapY - G.cardH + G.toggleY)
local function button(prefix)
local b = CreateFrame("Button", nil, frame)
b:SetAllPoints(frame)
local shadow = b:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(shadow, "mapcornershadow-right")
shadow:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 2, -1)
applyButtonState(b, "Normal", prefix .. "-up")
applyButtonState(b, "Pushed", prefix .. "-down")
b:SetHighlightTexture(NAV.squareHover)
local s = b:GetHighlightTexture()
s:ClearAllPoints()
s:SetWidth(48)
s:SetHeight(48)
s:SetPoint("CENTER", b, "CENTER", 0, 0)
s:SetBlendMode("ADD")
b:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
if ForeverUI.QuestLog then
ForeverUI.QuestLog.togglePane()
end
end)
return b
end
frame.open = button("questcollapse-show")
frame.close = button("questcollapse-hide")
return frame
end
local function openedPane()
local J = ForeverUI.QuestLog
return J and J.settings and J.settings().pane
end
-- ---------- Assembly
local function build()
local map = WorldMapFrame
if not map or not WorldMapDetailFrame then
say(L.WORLDMAP_DEBUG_NO_FRAME)
return false
end
if W.built then
return true
end
if not ForeverUI.SetAtlas then
say(L.WORLDMAP_DEBUG_NO_ATLAS)
return false
end
-- canvas: the map rectangle, in either mode
W.canvas = CreateFrame("Frame", "ForeverUIWorldMapCanvas", map)
W.canvas:SetPoint("TOPLEFT", map, "TOPLEFT", G.cardX, G.mapY)
W.canvas:SetWidth(G.mapW)
W.canvas:SetHeight(G.cardH)
-- holder for the maximized mode: camelot's frame, in UI units
W.maximized = CreateFrame("Frame", "ForeverUIWorldMapMaximized", map)
W.background = buildBackground(map)
W.frame = buildFrame(map)
W.bar = buildBar(map)
W.filters = buildFilters(map, W.bar)
W.floors = buildFloors(map)
W.coords = buildCoordinates(map)
W.toggle = buildToggle(map)
skinRedButtons(map, W.frame)
W.ownFrames = { W.background, W.frame, W.bar, W.filters, W.coords, W.toggle }
W.built = true
return true
end
-- Anchoring shared by both modes: everything is placed on holder (the WotLK map in the
-- small window, the maximized holder otherwise) at scale (the inverse of WorldMapFrame's,
-- back to UI units), around a canvasW x canvasH canvas.
-- maximized: true for the maximized layout (no portrait, other corners and bar offset)
local function anchorMap(holder, scale, canvasW, canvasH, maximized)
for _, f in ipairs(W.ownFrames) do
f:SetScale(scale)
end
W.floors:SetScale(scale)
W.canvas:SetScale(scale)
W.canvas:ClearAllPoints()
W.canvas:SetPoint("TOPLEFT", holder, "TOPLEFT", G.cardX, G.mapY)
W.canvas:SetWidth(canvasW)
W.canvas:SetHeight(canvasH)
W.background:ClearAllPoints()
W.background:SetAllPoints(holder)
local frame = W.frame
frame:ClearAllPoints()
frame:SetAllPoints(holder)
frame.trim:ClearAllPoints()
frame.trim:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, G.trimY)
frame.trim:SetPoint("RIGHT", frame, "LEFT", G.cardX + canvasW, 0)
if maximized then frame.portraitFrame:Hide() else frame.portraitFrame:Show() end
-- the left corners change with the portrait
for _, corner in ipairs(METAL) do
local t = frame.corners[corner.key]
local a = maximized and METAL_MAXIMIZED[corner.key] or corner
ForeverUI.SetAtlas(t, a.name)
t:ClearAllPoints()
t:SetPoint(corner.point, frame.metal, corner.point, a.x, a.y)
end
W.bar:ClearAllPoints()
W.bar:SetPoint("TOPLEFT", holder, "TOPLEFT", maximized and G.barXMaximized or G.barX, G.barY)
W.bar:SetPoint("BOTTOMRIGHT", holder, "TOPLEFT", G.cardX + canvasW - 50, G.barBottom)
W.floors:ClearAllPoints()
W.floors:SetPoint("TOPLEFT", holder, "TOPLEFT", G.cardX + G.floorX, G.mapY - G.floorY)
W.coords:ClearAllPoints()
W.coords:SetPoint("BOTTOMLEFT", holder, "TOPLEFT", G.cardX + G.coordX, G.mapY - canvasH + G.coordY)
W.toggle:ClearAllPoints()
W.toggle:SetPoint("BOTTOMRIGHT", holder, "TOPLEFT",
G.cardX + canvasW + G.toggleX, G.mapY - canvasH + G.toggleY)
-- frame levels: the background at the map's level, below WorldMapDetailFrame
W.background:SetFrameLevel(WorldMapFrame:GetFrameLevel())
W.bar:SetFrameLevel(WorldMapPOIFrame:GetFrameLevel() + 2)
W.coords:SetFrameLevel(WorldMapPOIFrame:GetFrameLevel() + 2)
W.toggle:SetFrameLevel(WorldMapPOIFrame:GetFrameLevel() + 2)
-- what WotLK shows and camelot does not have
if WorldMapQuestShowObjectives then
WorldMapQuestShowObjectives:Hide()
end
if WorldMapTrackQuest then
WorldMapTrackQuest:SetAlpha(0)
WorldMapTrackQuest:EnableMouse(false)
end
if WorldMapLevelDropDown then
WorldMapLevelDropDown:Hide()
end
-- title, in the title strip
local title = WorldMapFrameTitle
title:SetParent(frame.banner)
title:ClearAllPoints()
title:SetPoint("TOP", frame.banner, "TOP", 0, G.titleTextY)
title:SetPoint("LEFT", frame.banner, "LEFT")
title:SetPoint("RIGHT", frame.banner, "RIGHT")
title:SetText(maximized and TEXT.maximizedTitle or TEXT.title)
-- red buttons: close, and to its left maximize or minimize
local close = WorldMapFrameCloseButton
close:SetScale(scale)
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", holder, "TOPRIGHT", G.closeX, G.closeY)
local toggle = maximized and WorldMapFrameSizeDownButton or WorldMapFrameSizeUpButton
toggle:SetScale(scale)
toggle:ClearAllPoints()
toggle:SetPoint("RIGHT", close, "LEFT", G.maximizeX, 0)
for _, f in ipairs(W.ownFrames) do
f:Show()
end
cropTiles(true)
refreshBar()
refreshFloors()
end
-- Windowed mode: lays out camelot-style what WotLK just placed. Runs after
-- WorldMap_ToggleSizeDown and WorldMapFrame_SetMiniMode (which
-- WorldMapFrame_ToggleAdvanced calls on its own).
local function layoutWindowed()
if not build() or not isSmallWindow() then
return
end
local map = WorldMapFrame
if WORLDMAP_SETTINGS.advanced then
placeAnchor()
end
-- WorldMapFrame at our scale; its size and anything placed on it without its own
-- scale are then in WorldMapFrame units
local k = reducedScale()
map:SetScale(k)
W.inverse = 1 / k
local pane = openedPane()
map:SetWidth((G.width + (pane and G.paneWidth or 0)) / k)
map:SetHeight(G.height / k)
-- the map: its offsets are in its own units, already at our scale
WorldMapDetailFrame:ClearAllPoints()
WorldMapDetailFrame:SetPoint("TOPLEFT", map, "TOPLEFT", G.cardX / G.scale, G.mapY / G.scale)
WorldMapBlobFrame_CalculateHitTranslations()
-- what WotLK's small window shows and camelot does not have
suppress(WorldMapFrameMiniBorderLeft)
suppress(WorldMapFrameMiniBorderRight)
anchorMap(map, W.inverse, G.mapW, G.cardH, false)
-- WotLK's title bar (drag, opacity menu) covers the title strip
if WorldMapTitleButton then
WorldMapTitleButton:ClearAllPoints()
WorldMapTitleButton:SetPoint("TOPLEFT", map, "TOPLEFT", G.titleX1 / k, G.titleY / k)
WorldMapTitleButton:SetPoint("TOPRIGHT", map, "TOPRIGHT", (G.titleX2 - 2 * G.buttonSide) / k, G.titleY / k)
WorldMapTitleButton:SetHeight(G.titleH / k)
end
if pane then
W.toggle.close:Show()
W.toggle.open:Hide()
else
W.toggle.close:Hide()
W.toggle.open:Show()
end
if ForeverUI.QuestLog and ForeverUI.QuestLog.place then
ForeverUI.QuestLog.place()
end
end
W.layoutWindowed = layoutWindowed
-- Maximized mode. What WotLK's full screen shows and camelot does not have: its border
-- (WorldMapFrameTexture1..18, reloaded on each opening), its continent and zone menus,
-- its zoom-out button, the quest list, details and rewards on the right (the map is
-- shown alone), its floor arrows and the two checkboxes.
local SUPPRESSED_MAXIMIZED = {
"WorldMapZoneMinimapDropDown", "WorldMapZoomOutButton", "WorldMapZoneDropDown",
"WorldMapContinentDropDown", "WorldMapLevelUpButton", "WorldMapLevelDownButton",
"WorldMapQuestScrollFrame", "WorldMapQuestDetailScrollFrame", "WorldMapQuestRewardScrollFrame",
}
local function suppressFullScreen()
for i = 1, 18 do
suppress(_G["WorldMapFrameTexture" .. i])
end
for _, name in ipairs(SUPPRESSED_MAXIMIZED) do
local f = _G[name]
if f then f:Hide() end
end
end
-- Map centered in the canvas, at the scale of WotLK's view (QUESTLIST or FULLMAP);
-- WorldMapFrame carries the rest (applyMaximized).
local function placeMaximizedMap()
WorldMapDetailFrame:ClearAllPoints()
WorldMapDetailFrame:SetPoint("CENTER", W.canvas, "CENTER", 0, 0)
WorldMapBlobFrame_CalculateHitTranslations()
suppressFullScreen()
end
W.placeMaximizedMap = placeMaximizedMap
-- Maximized mode: sizes camelot's frame over WotLK's full screen and scales
-- WorldMapFrame so the map fits the canvas.
local function applyMaximized()
if not build() or isSmallWindow() then
return
end
local map = WorldMapFrame
-- UpdateMaximizedSize, in UI units
local screenW, screenH = UIParent:GetWidth(), UIParent:GetHeight()
local available = screenW - G.screenEdge
local unbounded = ((screenH - G.bannerH) * G.width) / (G.height - G.bannerH)
local width = math.min(available, unbounded)
local height = ((screenH - G.bannerH) * (width / unbounded)) + G.bannerH
width, height = math.floor(width), math.floor(height)
local canvasW = width - G.cardX - G.mapRight
local canvasH = height + G.mapY - G.mapBottom
-- the map fits the canvas, centered (camelot's canvas): at WotLK's view scale
-- (WORLDMAP_SETTINGS.size), WorldMapFrame, unparented in full screen, carries the rest
local fitScale = math.min(canvasW / G.usableMapW, canvasH / G.usableMapH)
map:SetScale(UIParent:GetEffectiveScale() * fitScale / WORLDMAP_SETTINGS.size)
-- one UI unit in the full screen scaled this way
local k = UIParent:GetEffectiveScale() / map:GetEffectiveScale()
W.inverse = k
local holder = W.maximized
holder:SetScale(k)
holder:ClearAllPoints()
holder:SetPoint("TOP", map, "TOP", 0, 0)
holder:SetWidth(width)
holder:SetHeight(height)
holder:SetFrameLevel(map:GetFrameLevel())
anchorMap(holder, k, canvasW, canvasH, true)
placeMaximizedMap()
W.toggle:Hide()
applyButtonState(WorldMapFrameSizeDownButton, "Normal", "redbutton-condense")
applyButtonState(WorldMapFrameSizeDownButton, "Pushed", "redbutton-condense-pressed")
local s = applyButtonState(WorldMapFrameSizeDownButton, "Highlight", "redbutton-highlight")
if s then s:SetBlendMode("ADD") end
WorldMapFrameSizeDownButton:SetWidth(G.buttonSide)
WorldMapFrameSizeDownButton:SetHeight(G.buttonSide)
WorldMapFrameSizeDownButton:SetHitRectInsets(0, 0, 0, 0)
WorldMapFrameSizeDownButton:SetFrameStrata("HIGH")
WorldMapFrameSizeDownButton:SetFrameLevel(W.frame:GetFrameLevel() + 4)
if ForeverUI.QuestLog and ForeverUI.QuestLog.hide then
ForeverUI.QuestLog.hide()
end
end
W.applyMaximized = applyMaximized
-- Gives back to the client what the skin took from it. Unused.
local function remove()
if not W.built then
return
end
for _, f in ipairs(W.ownFrames) do
f:Hide()
end
W.floors:Hide()
cropTiles(false)
if ForeverUI.QuestLog and ForeverUI.QuestLog.hide then
ForeverUI.QuestLog.hide()
end
WorldMapFrameTitle:SetParent(WorldMapFrame)
if WorldMapQuestShowObjectives then
WorldMapQuestShowObjectives:Show()
end
if WorldMapTrackQuest then
WorldMapTrackQuest:SetAlpha(1)
WorldMapTrackQuest:EnableMouse(true)
end
end
W.remove = remove
-- Dungeon and raid portals: clicking one opens the instance map. The icons are not
-- client landmarks (AreaPOI.dbc has none): they come from the WDM addon ("WoW Dungeon
-- Maps - HD client", in patch-enus-n.mpq) as WorldMapFrameAtlasPOIn buttons. WDM gives
-- them WotLK's landmark click (WorldMapPOI_OnClick) with mapLinkID = 0, which is true in
-- Lua, so ClickLandmark(0) goes nowhere. Their name (LibBabble-Zone) gives the instance
-- map (WorldMapInstances.lua, generated from the client DBC) and SetMapByID opens it.
-- WotLK landmarks (WorldMapFramePOIn) without a link are handled the same way. Both are
-- created on demand in WorldMapFrame_Update, where WDM may add its own after us, so they
-- are hooked again on the next frame.
local function portalMap(name)
local t = ForeverUI.InstanceMaps
return name and t and t[string.lower(name)]
end
W.portalMap = portalMap
local function openPortal(self, button)
-- a real map link: WotLK handles it
if button ~= "LeftButton" or (self.mapLinkID and self.mapLinkID ~= 0) then return end
local id = portalMap(self.name)
if id and SetMapByID then SetMapByID(id) end
end
local function hookPortals()
for _, family in ipairs({ { "WorldMapFramePOI", NUM_WORLDMAP_POIS },
{ "WorldMapFrameAtlasPOI", NUM_WORLDMAP_ATLAS_POI } }) do
for i = 1, (family[2] or 0) do
local b = _G[family[1] .. i]
if b and not b.foreverPortal then
b.foreverPortal = true
b:HookScript("OnClick", openPortal)
end
end
end
end
W.hookPortals = hookPortals
local deferredPortals = CreateFrame("Frame")
deferredPortals:Hide()
deferredPortals:SetScript("OnUpdate", function(self)
self:Hide()
hookPortals()
end)
W.deferredPortals = deferredPortals
if hooksecurefunc then
if WorldMap_ToggleSizeDown then
hooksecurefunc("WorldMap_ToggleSizeDown", layoutWindowed)
end
if WorldMapFrame_SetMiniMode then
hooksecurefunc("WorldMapFrame_SetMiniMode", layoutWindowed)
end
if WorldMap_ToggleSizeUp then
hooksecurefunc("WorldMap_ToggleSizeUp", applyMaximized)
end
-- both full-screen views place the map and its scale again
for _, name in ipairs({ "WorldMapFrame_SetQuestMapView", "WorldMapFrame_SetFullMapView" }) do
if _G[name] then
hooksecurefunc(name, function()
if W.built and not isSmallWindow() then
applyMaximized()
end
end)
end
end
-- in the small window WotLK writes the zone name in the title; camelot keeps
-- MAP_AND_QUEST_LOG
if WorldMapFrame_SetMapName then
hooksecurefunc("WorldMapFrame_SetMapName", function()
if W.built then
WorldMapFrameTitle:SetText(isSmallWindow() and TEXT.title or TEXT.maximizedTitle)
end
end)
end
-- WotLK's floor menu shows itself again on every update
if WorldMapLevelDropDown_Update then
hooksecurefunc("WorldMapLevelDropDown_Update", function()
if W.built then
WorldMapLevelDropDown:Hide()
refreshFloors()
end
end)
end
-- on each opening in the small window: the pane may have changed state while the map
-- was closed (quest log key, pane button)
if WorldMapFrame and WorldMapFrame.HookScript then
-- in full screen the scale is known only here (SetupFullscreenScale)
-- The first opening builds the skin, also when the map first opens in full screen.
WorldMapFrame:HookScript("OnShow", function()
local initial = not W.built
if isSmallWindow() then
layoutWindowed()
else
applyMaximized()
end
-- what the hooks on WotLK functions would have done if the map had been built
if initial and W.built then
cropTiles(true)
refreshBar()
refreshFloors()
end
end)
end
-- WorldMapFrame_Update reloads the displayed map's tiles
if WorldMapFrame_Update then
hooksecurefunc("WorldMapFrame_Update", function()
if W.built then
cropTiles(true)
end
hookPortals()
deferredPortals:Show()
end)
end
-- each map change rebuilds the breadcrumb
if WorldMapFrame_UpdateMap then
hooksecurefunc("WorldMapFrame_UpdateMap", function()
if W.built then
refreshBar()
refreshFloors()
end
end)
end
-- DisplayQuests shows the track checkbox again for each quest
if WorldMapFrame_DisplayQuests then
hooksecurefunc("WorldMapFrame_DisplayQuests", function()
if W.built and WorldMapTrackQuest then
WorldMapTrackQuest:SetAlpha(0)
WorldMapTrackQuest:EnableMouse(false)
end
end)
end
end
-- screen size changed: recompute the maximized mode
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("DISPLAY_SIZE_CHANGED")
watcher:RegisterEvent("UI_SCALE_CHANGED")
watcher:SetScript("OnEvent", function()
if W.built and WorldMapFrame:IsShown() and not isSmallWindow() then
applyMaximized()
end
end)
-- Prints the map's size, scale, landmarks, breadcrumb and floors to the chat,
-- for diagnosis.
ForeverUI.WorldMapDebug = function()
local map = WorldMapFrame
if not map then
say(L.WORLDMAP_DEBUG_MISSING)
return
end
local function row(text)
DEFAULT_CHAT_FRAME:AddMessage(" " .. text)
end
-- landmarks of the displayed map, and the instance map their name gives
for i = 1, (GetNumMapLandmarks and GetNumMapLandmarks() or 0) do
local name, description, icon, x, y, link = GetMapLandmarkInfo(i)
row(string.format(L.WORLDMAP_DEBUG_LANDMARK, i, tostring(name),
tostring(description), tostring(icon), tostring(link), tostring(portalMap(name))))
end
say(string.format(L.WORLDMAP_DEBUG_FRAME,
map:GetWidth(), map:GetHeight(), map:GetScale(),
isSmallWindow() and L.WORLDMAP_DEBUG_MODE_WINDOWED or L.WORLDMAP_DEBUG_MODE_FULLSCREEN,
WORLDMAP_SETTINGS.size, WORLDMAP_WINDOWED_SIZE))
local scale = WorldMapDetailFrame:GetEffectiveScale() / UIParent:GetEffectiveScale()
row(string.format(L.WORLDMAP_DEBUG_DETAIL,
WorldMapDetailFrame:GetWidth(), WorldMapDetailFrame:GetHeight(), scale,
WorldMapDetailFrame:GetWidth() * scale, WorldMapDetailFrame:GetHeight() * scale))
local names = {}
for _, d in ipairs(hierarchy()) do
table.insert(names, d.name or "?")
end
row(L.WORLDMAP_DEBUG_BREADCRUMB .. TEXT.world .. (#names > 0 and (" > " .. table.concat(names, " > ")) or ""))
row(string.format(L.WORLDMAP_DEBUG_FLOORS,
GetNumDungeonMapLevels and GetNumDungeonMapLevels() or 0,
tostring(W.built), tostring(map:GetAttribute("UIPanelLayout-area"))))
row(string.format(L.WORLDMAP_DEBUG_MOVABLE,
tostring(map:IsMovable()), tostring(GetCVar("advancedWorldMap")),
tostring(WORLDMAP_SETTINGS.locked)))
end