Files
moon-well-ui/data/addon/ForeverUI/WorldMapZoom.lua
T
Claude 185fccb22d ForeverUI : carte et quêtes, menu du suivi, abandon, retour de carte, volet caché
Menu du clic droit sur le suivi : près du bas de l'écran, 3.3.5 retourne
la liste au-dessus du curseur, et openMenu la réancrait sur "cursor", qui
n'est pas un cadre (erreur Lua, aucun menu). Seul un cadre est réancré.

Le volet de détails refaisait SetAbandonQuest à chaque rafraîchissement :
une confirmation d'abandon ouverte depuis le suivi (« Abandonner A ? »)
abandonnait alors la quête du volet. Le marquage attend qu'elle se ferme.

Retour du volet de détails : GetCurrentMapAreaID() vaut l'identifiant de
SetMapByID plus un (WorldMapFrame_ToggleWindowSize), et zéro sur les vues
de continent, que Blizzard rend par SetMapZoom. Le retour ouvrait la zone
suivante, ou rien.

Le volet est un fils de la carte et garde son drapeau affiché carte
fermée : chaque événement de quête le reconstruisait caché, et un volet de
détails resté ouvert pouvait déplacer la carte fermée (coordonnées de la
minicarte vides, quêtes de la zone hors du suivi). Rafraîchi seulement
carte ouverte, une fois par image.

Zoom de la carte : le tri des marqueurs (une soixantaine d'enfants, trois
tables) tournait à chaque image ; dix fois par seconde suffisent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxRZNHkq453dyUMcWsyUf1
2026-10-01 23:14:54 +00:00

385 lines
12 KiB
Lua

-- World map zoom: the mouse wheel scales the map toward the cursor (x 1.25 per step, 1 to 4)
-- and a left-button drag pans the zoomed map; a click without drag keeps its meaning.
-- Camelot zooms through map art layers (mapcanvas_scrollcontainermixin.lua), which 3.3.5
-- lacks. The zoom returns to 1 when the map changes, changes mode or closes.
local W = ForeverUI.WorldMap
local Z = { z = 1, cx = 0.5, cy = 0.5, active = false, STEP = 1.25, MAX = 4, THRESHOLD = 4 }
W.zoom = Z
local CARD_W, CARD_H = 1002, 668
local TILE, COLUMNS, ROWS = 256, 4, 3
local function tile(k)
return _G["WorldMapDetailTile" .. k]
end
local function ready()
return W.built and W.canvas and WorldMapDetailFrame and WorldMapButton and WORLDMAP_SETTINGS
end
-- The view: the canvas rectangle in WorldMapDetailFrame units (WorldMapButton has the same
-- scale and corner), origin at the map's top left
local function viewSize()
local c, d = W.canvas, WorldMapDetailFrame
local k = c:GetEffectiveScale() / d:GetEffectiveScale()
return c:GetWidth() * k, c:GetHeight() * k
end
local function view()
local vw, vh = viewSize()
local x, y = Z.cx * CARD_W, Z.cy * CARD_H
return x - vw / 2, y - vh / 2, x + vw / 2, y + vh / 2
end
Z.view = view
-- Keep the center where the view stays inside the map
local function clampView()
local vw, vh = viewSize()
local halfX, halfY = vw / 2 / CARD_W, vh / 2 / CARD_H
if halfX >= 0.5 then Z.cx = 0.5 else Z.cx = math.max(halfX, math.min(1 - halfX, Z.cx)) end
if halfY >= 0.5 then Z.cy = 0.5 else Z.cy = math.max(halfY, math.min(1 - halfY, Z.cy)) end
end
-- ---------------------------------------------------------------- Tiles
-- 3.3.5 clips nothing outside a ScrollFrame and client frames are never reparented, so each
-- tile is cropped to the view: its visible part, at its place, with matching tex coords
local function cropTexture()
local d = WorldMapDetailFrame
local x0, y0, x1, y1 = view()
for rowLine = 1, ROWS do
for column = 1, COLUMNS do
local t = tile((rowLine - 1) * COLUMNS + column)
if t then
local tx, ty = (column - 1) * TILE, (rowLine - 1) * TILE
local ix0, iy0 = math.max(tx, x0), math.max(ty, y0)
local ix1 = math.min(tx + TILE, CARD_W, x1)
local iy1 = math.min(ty + TILE, CARD_H, y1)
t:ClearAllPoints()
if ix1 - ix0 < 0.01 or iy1 - iy0 < 0.01 then
t:SetPoint("TOPLEFT", d, "TOPLEFT", tx, -ty)
t:SetWidth(1)
t:SetHeight(1)
t:SetAlpha(0)
else
t:SetPoint("TOPLEFT", d, "TOPLEFT", ix0, -iy0)
t:SetWidth(ix1 - ix0)
t:SetHeight(iy1 - iy0)
t:SetTexCoord((ix0 - tx) / TILE, (ix1 - tx) / TILE, (iy0 - ty) / TILE, (iy1 - ty) / TILE)
t:SetAlpha(1)
end
end
end
end
end
-- Tile anchors from WorldMapFrame.xml (541-636): the first at the corner, each right of the
-- previous one, each row under the first tile of the row above; then the small-window crop
-- (WorldMap.lua)
local function rechainTiles()
for k = 1, COLUMNS * ROWS do
local t = tile(k)
if t then
t:ClearAllPoints()
t:SetAlpha(1)
if k == 1 then
t:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", 0, 0)
elseif (k - 1) % COLUMNS == 0 then
t:SetPoint("TOPLEFT", tile(k - COLUMNS), "BOTTOMLEFT", 0, 0)
else
t:SetPoint("TOPLEFT", tile(k - 1), "TOPRIGHT", 0, 0)
end
end
end
if W.cropTiles then W.cropTiles(true) end
end
-- --------------------------------------------------------------- Markers
-- A marker out of the view fades out and releases the mouse (invisible, it would still
-- steal hover and clicks from the quest log or the frame); back in view, it gets its alpha
-- and mouse back
local function turnOff(f)
f.foreverZoomAlpha = f:GetAlpha()
f.foreverZoomMouse = f.IsMouseEnabled and f:IsMouseEnabled() and true or false
f:SetAlpha(0)
if f.foreverZoomMouse then f:EnableMouse(false) end
end
local function restore(f)
f:SetAlpha(f.foreverZoomAlpha)
if f.foreverZoomMouse then f:EnableMouse(true) end
f.foreverZoomAlpha, f.foreverZoomMouse = nil, nil
end
-- Fade out the markers outside the view, restore those back inside
local function sortBy()
local c = W.canvas
local e = c:GetEffectiveScale()
local left, down, right, top = c:GetLeft(), c:GetBottom(), c:GetRight(), c:GetTop()
if not (left and down and right and top) then return end
left, down, right, top = left * e, down * e, right * e, top * e
for _, parent in ipairs({ WorldMapButton, WorldMapPOIFrame }) do
if parent then
for _, f in ipairs({ parent:GetChildren() }) do
if f ~= WorldMapFrameAreaFrame and f:IsShown() then
local x, y = f:GetCenter()
local fe = f:GetEffectiveScale()
local inside = not x or (x * fe >= left and x * fe <= right and y * fe >= down and y * fe <= top)
if inside then
if f.foreverZoomAlpha then restore(f) end
elseif not f.foreverZoomAlpha then
turnOff(f)
end
end
end
end
end
end
local function restoreChildren()
for _, parent in ipairs({ WorldMapButton, WorldMapPOIFrame }) do
if parent then
for _, f in ipairs({ parent:GetChildren() }) do
if f.foreverZoomAlpha then restore(f) end
end
end
end
end
-- ------------------------------------------------------------- Apply
-- Current map key: a map change resets the zoom to 1
function Z.mapKey()
return tostring(GetCurrentMapAreaID and GetCurrentMapAreaID()) .. ":" ..
tostring(GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel())
end
-- Scales of the zoomed frames: { frame, zoomed scale, normal scale }. Markers, party and
-- quest numbers are placed in WorldMapButton / WorldMapPOIFrame units, so they follow.
local function frameScales()
local base = WORLDMAP_SETTINGS.size
return {
{ WorldMapDetailFrame, base * Z.z, base },
{ WorldMapButton, base * Z.z, base },
{ WorldMapBlobFrame, base * Z.z, base },
{ WorldMapPOIFrame, Z.z, 1 },
{ WorldMapFrameAreaFrame, (Z.areaScale or 1) / Z.z, Z.areaScale or 1 },
}
end
-- The quest blob (WorldMapBlobFrame) is drawn by the engine in one piece and would overflow,
-- so it hides while zoomed; WorldMapButton's hit rect is limited to the view.
function Z.apply()
local d = WorldMapDetailFrame
if not Z.active then
Z.active = true
Z.points = {}
for i = 1, d:GetNumPoints() do
Z.points[i] = { d:GetPoint(i) }
end
Z.areaScale = WorldMapFrameAreaFrame and WorldMapFrameAreaFrame:GetScale() or 1
Z.key = Z.mapKey()
end
for _, e in ipairs(frameScales()) do
if e[1] then e[1]:SetScale(e[2]) end
end
clampView()
d:ClearAllPoints()
d:SetPoint("CENTER", W.canvas, "CENTER", (0.5 - Z.cx) * CARD_W, (Z.cy - 0.5) * CARD_H)
if WorldMapBlobFrame then
WorldMapBlobFrame:SetAlpha(0)
if WorldMapBlobFrame_CalculateHitTranslations then WorldMapBlobFrame_CalculateHitTranslations() end
end
local a = WorldMapFrameAreaFrame
if a then
a:ClearAllPoints()
a:SetPoint("TOP", W.canvas, "TOP", 0, -10)
end
local x0, y0, x1, y1 = view()
WorldMapButton:SetHitRectInsets(math.max(0, x0), math.max(0, CARD_W - x1),
math.max(0, y0), math.max(0, CARD_H - y1))
cropTexture()
sortBy()
end
-- Back to the whole map. After a mode change, the client and WorldMap.lua have just set
-- their own scales and anchors: only frames still carrying our scale are restored.
function Z.reset(isModeChange)
if not Z.active then
Z.z = 1
return
end
local zoomed = frameScales()
Z.z = 1
local restore = frameScales()
for i, e in ipairs(zoomed) do
if e[1] and math.abs(e[1]:GetScale() - e[2]) < 1e-6 then
e[1]:SetScale(restore[i][3])
end
end
Z.active = false
local d = WorldMapDetailFrame
if not isModeChange and Z.points then
d:ClearAllPoints()
for _, p in ipairs(Z.points) do
d:SetPoint(p[1], p[2], p[3], p[4], p[5])
end
end
if WorldMapBlobFrame then
WorldMapBlobFrame:SetAlpha(1)
if WorldMapBlobFrame_CalculateHitTranslations then WorldMapBlobFrame_CalculateHitTranslations() end
end
local a = WorldMapFrameAreaFrame
if a then
a:ClearAllPoints()
a:SetPoint("TOP", WorldMapButton, "TOP", 0, -10)
end
WorldMapButton:SetHitRectInsets(0, 0, 0, 0)
WorldMapButton:EnableMouse(true)
Z.pressPoint, Z.dragging = nil, false
rechainTiles()
restoreChildren()
Z.cx, Z.cy = 0.5, 0.5
end
-- ------------------------------------------------------------- Mouse wheel
-- Map point under the cursor, and the cursor offset from the canvas center in screen pixels
local function pointUnderCursor()
local x, y = GetCursorPosition()
local c = W.canvas
local cx, cy = c:GetCenter()
if not cx then return nil end
local ce = c:GetEffectiveScale()
local dx, dy = x - cx * ce, y - cy * ce
local de = WorldMapDetailFrame:GetEffectiveScale()
return Z.cx + dx / (CARD_W * de), Z.cy - dy / (CARD_H * de), dx, dy, de
end
function Z.wheel(direction)
if not ready() then return end
local old = Z.z
local new = direction > 0 and old * Z.STEP or old / Z.STEP
if new > Z.MAX then new = Z.MAX end
if new < 1.0001 then new = 1 end
if new == old then return end
if new == 1 then
Z.reset(false)
return
end
if not Z.active then Z.cx, Z.cy = 0.5, 0.5 end
-- Keep the point under the cursor in place (camelot: SetPanTarget)
local fx, fy, dx, dy, de = pointUnderCursor()
Z.z = new
if fx then
local de2 = de * new / old
Z.cx = fx - dx / (CARD_W * de2)
Z.cy = fy + dy / (CARD_H * de2)
end
Z.apply()
end
-- --------------------------------------------------------------- Watcher
local mapKey = Z.mapKey
-- Child of the map frame, so it only runs while the map is open
local watcher = CreateFrame("Frame", nil, WorldMapFrame)
watcher:SetScript("OnUpdate", function(_, elapsed)
if not (Z.active and ready()) then
Z.key = ready() and mapKey() or nil
return
end
if mapKey() ~= Z.key then
Z.reset(false)
Z.key = mapKey()
return
end
-- Drag: past THRESHOLD pixels the map follows the cursor. WorldMapButton_OnClick navigates
-- on mouse up, so WorldMapButton releases the mouse until the button is up. The client code
-- is not wrapped: it closes dropdowns and would taint their state if called from the addon.
local pressPoint = Z.pressPoint
if pressPoint then
local x, y = GetCursorPosition()
if not IsMouseButtonDown("LeftButton") then
Z.pressPoint = nil
if Z.dragging then
Z.dragging = false
WorldMapButton:EnableMouse(true)
end
else
if not Z.dragging and math.abs(x - pressPoint.x) + math.abs(y - pressPoint.y) > Z.THRESHOLD then
Z.dragging = true
WorldMapButton:EnableMouse(false)
end
if Z.dragging then
local de = WorldMapDetailFrame:GetEffectiveScale()
Z.cx = pressPoint.cx - (x - pressPoint.x) / (CARD_W * de)
Z.cy = pressPoint.cy + (y - pressPoint.y) / (CARD_H * de)
Z.apply()
return
end
end
end
-- Markers move with their units: ten passes a second are enough (Z.apply sorts at once).
-- Each pass reads every child of the map buttons (about sixty) and builds three tables.
Z.sortWait = (Z.sortWait or 0) - elapsed
if Z.sortWait <= 0 then
Z.sortWait = 0.1
sortBy()
end
end)
Z.watcher = watcher
-- ------------------------------------------------------------ Hooks
if WorldMapButton then
WorldMapButton:EnableMouseWheel(true)
WorldMapButton:SetScript("OnMouseWheel", function(_, direction)
Z.wheel(direction)
end)
WorldMapButton:HookScript("OnMouseDown", function(_, button)
if button == "LeftButton" and Z.active then
local x, y = GetCursorPosition()
Z.pressPoint = { x = x, y = y, cx = Z.cx, cy = Z.cy }
Z.dragging = false
end
end)
-- Player arrow: WorldMapButton_OnUpdate places it at the WotLK scale (position x
-- WORLDMAP_SETTINGS.size); place it again x zoom, or hide it out of the view
WorldMapButton:HookScript("OnUpdate", function()
if not Z.active then return end
local x, y = GetPlayerMapPosition("player")
if not x or (x == 0 and y == 0) then return end
local px, py = x * CARD_W, y * CARD_H
local x0, y0, x1, y1 = view()
if px < x0 or px > x1 or py < y0 or py > y1 then
if ShowWorldMapArrowFrame then ShowWorldMapArrowFrame(nil) end
elseif PositionWorldMapArrowFrame then
local s = WORLDMAP_SETTINGS.size * Z.z
PositionWorldMapArrowFrame("CENTER", "WorldMapDetailFrame", "TOPLEFT", px * s, -py * s)
end
end)
end
if WorldMapFrame then
WorldMapFrame:HookScript("OnHide", function() Z.reset(false) end)
end
-- WorldMapFrame_Update reloads the tiles (WORLD_MAP_UPDATE): crop them again while zoomed
if WorldMapFrame_Update then
hooksecurefunc("WorldMapFrame_Update", function()
if Z.active then cropTexture() end
end)
end
-- Mode or view change: back to 1, without undoing what the client has just set
for _, name in ipairs({ "WorldMap_ToggleSizeDown", "WorldMap_ToggleSizeUp", "WorldMapFrame_SetMiniMode",
"WorldMapFrame_SetQuestMapView", "WorldMapFrame_SetFullMapView" }) do
if _G[name] then
hooksecurefunc(name, function() Z.reset(true) end)
end
end