185fccb22d
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
385 lines
12 KiB
Lua
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
|