mirror of
https://github.com/rivi-s/pfQuest-turtle.git
synced 2026-11-10 20:17:35 +00:00
1284 lines
60 KiB
Lua
1284 lines
60 KiB
Lua
local original_UpdateNodes = pfMap.UpdateNodes
|
|
local original_UpdateNode = pfMap.UpdateNode
|
|
|
|
local continentPins = {}
|
|
local maxContinentPins = 2000
|
|
local continentRenderLastKey
|
|
local continentRenderLastAt = 0
|
|
|
|
local CONTINENT_DEBUG = false
|
|
local function DebugPrint(msg)
|
|
if CONTINENT_DEBUG then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffcc[ContinentPins]|r " .. msg)
|
|
end
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- WorldMapArea-style zone bounding boxes, in world coordinates.
|
|
-- Format: {width, height, left, top}
|
|
-- ============================================================================
|
|
local mapData = {
|
|
-- Eastern Kingdoms zones (instance 0)
|
|
[1429] = {3470.84, 2314.62, 1535.42, -7939.58}, -- Elwynn Forest
|
|
[1436] = {3500.00, 2333.3, 3016.67, -9400}, -- Westfall
|
|
[1433] = {2170.84, 1447.9, -1570.83, -8575}, -- Redridge Mountains
|
|
[1431] = {2700.00, 1800.03, 833.333, -9716.67}, -- Duskwood
|
|
[1434] = {6381.25, 4254.1, 2220.83, -11168.8}, -- Stranglethorn Vale
|
|
[1453] = {1737.50, 1158.34, 1722.92, -7995.83}, -- Stormwind City
|
|
[1426] = {4925.00, 3283.34, 1802.08, -3877.08}, -- Dun Morogh
|
|
[1455] = {790.63, 527.61, -713.591, -4569.24}, -- Ironforge
|
|
[1432] = {2758.33, 1839.58, -1993.75, -4487.5}, -- Loch Modan
|
|
[1437] = {4135.42, 2756.25, -389.583, -2147.92}, -- Wetlands
|
|
[1424] = {3200.00, 2133.33, 1066.67, 400}, -- Hillsbrad Foothills
|
|
[1416] = {2800.00, 1866.667, 783.333, 1500}, -- Alterac Mountains
|
|
[1417] = {3600.00, 2400.00, -866.667, -133.333}, -- Arathi Highlands
|
|
[1425] = {3850.00, 2566.67, -1575, 1466.67}, -- The Hinterlands
|
|
[1420] = {4518.75, 3012.5, 3033.33, 3837.5}, -- Tirisfal Glades
|
|
[1421] = {4200.00, 2800.00, 3450, 1666.67}, -- Silverpine Forest
|
|
[1458] = {959.38, 640.1, 873.193, 1877.94}, -- Undercity
|
|
[1422] = {4300.00, 2866.67, 416.667, 3366.67}, -- Western Plaguelands
|
|
[1423] = {4031.25, 2687.5, -2287.5, 3704.17}, -- Eastern Plaguelands
|
|
[1418] = {2487.50, 1658.34, -2079.17, -5889.58}, -- Badlands
|
|
[1427] = {2231.253, 1487.5, -322.917, -6100}, -- Searing Gorge
|
|
[1428] = {2929.163, 1952.08, -266.667, -7031.25}, -- Burning Steppes
|
|
[1435] = {2293.75, 1529.17, -2222.92, -9620.83}, -- Swamp of Sorrows
|
|
[1419] = {3350.00, 2233.30, -1241.67, -10566.7}, -- Blasted Lands
|
|
[1430] = {2500.00, 1666.63, -833.333, -9866.67}, -- Deadwind Pass
|
|
-- Kalimdor zones (instance 1)
|
|
[1438] = {5091.66, 3393.7, 3814.58, 11831.2}, -- Teldrassil
|
|
[1457] = {1058.33, 705.71, 2938.36, 10238.3}, -- Darnassus
|
|
[1439] = {6550.00, 4366.66, 2941.67, 8333.33}, -- Darkshore
|
|
[1440] = {5766.67, 3843.75, 1700, 4672.92}, -- Ashenvale
|
|
[1442] = {4883.33, 3256.25, 3245.83, 2916.67}, -- Stonetalon Mountains
|
|
[1413] = {10133.34, 6756.25, 2622.92, 1612.5}, -- The Barrens
|
|
[1411] = {5287.5, 3525, -1962.5, 1808.33}, -- Durotar
|
|
[1454] = {1402.61, 935.42, -3680.6, 2273.88}, -- Orgrimmar
|
|
[1412] = {5137.5, 3425.00, 2047.92, -272.917}, -- Mulgore
|
|
[1456] = {1043.75, 695.83, 516.667, -850}, -- Thunder Bluff
|
|
[1443] = {4495.83, 2997.91, 4233.33, 452.083}, -- Desolace
|
|
[1444] = {6950.00, 4633.33, 5441.67, -2366.67}, -- Feralas
|
|
[1441] = {4400.00, 2933.33, -433.333, -3966.67}, -- Thousand Needles
|
|
[1446] = {6900.00, 4600.00, -218.75, -5875}, -- Tanaris
|
|
[1449] = {3700.00, 2466.66, 533.333, -5966.67}, -- Un'Goro Crater
|
|
[1451] = {3483.33, 2322.92, 2537.5, -5958.33}, -- Silithus
|
|
[1445] = {5250.00, 3500.00, -975, -2033.33}, -- Dustwallow Marsh
|
|
[1452] = {7100.00, 4733.33, -316.667, 8533.33}, -- Winterspring
|
|
[1447] = {5070.84, 3381.25, -3277.08, 5341.67}, -- Azshara
|
|
[1448] = {5750.00, 3833.33, 1641.67, 7133.33}, -- Felwood
|
|
[1450] = {2308.33, 1539.59, -1381.25, 8491.67}, -- Moonglade
|
|
-- Continents
|
|
[1415] = {40500.00, 23700.00, 18500.00, 7500.00}, -- Eastern Kingdoms continent
|
|
[1414] = {36799.81, 24533.20, 17066.60, 12799.90}, -- Kalimdor continent
|
|
}
|
|
|
|
local zoneToUiMapID = {
|
|
-- Eastern Kingdoms
|
|
[12] = 1429, [40] = 1436, [44] = 1433, [10] = 1431, [33] = 1434,
|
|
[1519] = 1453, [1] = 1426, [1537] = 1455, [38] = 1432, [11] = 1437,
|
|
[267] = 1424, [36] = 1416, [45] = 1417, [47] = 1425, [85] = 1420,
|
|
[130] = 1421, [1497] = 1458, [28] = 1422, [139] = 1423, [3] = 1418,
|
|
[51] = 1427, [46] = 1428, [8] = 1435, [4] = 1419, [41] = 1430,
|
|
-- Kalimdor
|
|
[141] = 1438, [1657] = 1457, [148] = 1439, [331] = 1440, [406] = 1442,
|
|
[17] = 1413, [14] = 1411, [1637] = 1454, [215] = 1412, [1638] = 1456,
|
|
[405] = 1443, [357] = 1444, [400] = 1441, [440] = 1446, [490] = 1449,
|
|
[1377] = 1451, [15] = 1445, [618] = 1452, [16] = 1447, [361] = 1448,
|
|
[493] = 1450,
|
|
}
|
|
|
|
-- Custom zones are not represented by Blizzard's WorldMapArea data. Their
|
|
-- normalized bounds are measured from the client continent map; add future
|
|
-- custom-zone calibrations here without altering the projection code.
|
|
local customContinentTransforms = {
|
|
-- Tel'Abim: south-east of Tanaris on the Kalimdor map. Calibrated at
|
|
-- Tel'Abim 40.59 / 78.10 against Kalimdor 64.01 / 88.42, using its
|
|
-- client WorldMapArea dimensions (3227 x 2187).
|
|
[5121] = { continent = 1, left = 0.604506, top = 0.814578, width = 0.087691, height = 0.089145 },
|
|
-- Gillijim's Isle: south of Lapidis Isle on the Eastern Kingdoms map.
|
|
-- Calibrated at Gillijim's Isle 52.53 / 22.39 against Eastern Kingdoms
|
|
-- 37.47 / 87.16, using its client dimensions (2464.9438 x 1927.3799).
|
|
[408] = { continent = 2, left = 0.342729, top = 0.853392, width = 0.060863, height = 0.081324 },
|
|
-- Lapidis Isle: south-west of Stranglethorn Vale on the Eastern Kingdoms
|
|
-- map. Calibrated at Lapidis Isle 60.15 / 42.30 against Eastern Kingdoms
|
|
-- 36.40 / 82.33, using its client dimensions (2165.0662 x 2042.8711).
|
|
[409] = { continent = 2, left = 0.331845, top = 0.786839, width = 0.053458, height = 0.086197 },
|
|
-- Alah'Thalas: Eastern Kingdoms. Fitted from Warden Sira Moonwarden
|
|
-- (26.7 / 25.9 -> 50.9 / 13.0) and Marrondra
|
|
-- (35.7 / 32.5 -> 51.3 / 13.3).
|
|
[2040] = { continent = 2, left = 0.49713, top = 0.11823, width = 0.04444, height = 0.04545 },
|
|
-- Moonwhisper Coast: north-east of Kalimdor. Calibrated against Gordnak
|
|
-- (51.89 / 36.61) at Kalimdor 61.1 / 18.9, using its client
|
|
-- WorldMapArea dimensions (7856 x 5241).
|
|
[5642] = { continent = 1, left = 0.500226, top = 0.110790, width = 0.213479, height = 0.213629 },
|
|
-- Blackstone Island: east of Durotar. The Turtle client exposes this as
|
|
-- its own map, without a Blizzard WorldMapArea rectangle, so it needs a
|
|
-- calibrated continent-space transform.
|
|
[5536] = { continent = 1, left = 0.632, top = 0.484, width = 0.0672, height = 0.0679 },
|
|
-- Thalassian Highlands: a Turtle map without a WorldMapArea rectangle.
|
|
-- Calibrated from the same player position on its zone and Eastern
|
|
-- Kingdoms maps; this is safe on both clean and enhanced clients.
|
|
[5225] = { continent = 2, left = 0.489112, top = 0.107562, width = 0.076099, height = 0.086962 },
|
|
-- Gilneas: south of Silverpine Forest on the Eastern Kingdoms map.
|
|
-- Calibrated at Gilneas 45.87 / 22.69 against Eastern Kingdoms
|
|
-- 40.99 / 35.71, using its client dimensions (3666 x 2442).
|
|
[5179] = { continent = 2, left = 0.368379, top = 0.333721, width = 0.090519, height = 0.103038 },
|
|
-- Balor: west of Stormwind on the Eastern Kingdoms map. Calibrated at
|
|
-- Balor 44.81 / 35.62 against Eastern Kingdoms 35.97 / 67.25; its scale
|
|
-- comes from the client WorldMapArea dimensions (3098 x 2068).
|
|
[5561] = { continent = 2, left = 0.325423, top = 0.641419, width = 0.076494, height = 0.087257 },
|
|
-- Northwind: west of the Wetlands on the Eastern Kingdoms map. Calibrated
|
|
-- at Northwind 63.36 / 70.38 against Eastern Kingdoms 44.87 / 65.25,
|
|
-- using its client dimensions (3241 x 2157).
|
|
[5581] = { continent = 2, left = 0.397996, top = 0.588445, width = 0.080025, height = 0.091013 },
|
|
-- Grim Reaches: east of the Wetlands on the Eastern Kingdoms map.
|
|
-- Calibrated at Grim Reaches 51.52 / 57.02 against Eastern Kingdoms
|
|
-- 59.23 / 52.90, using its client dimensions (5387 x 3584).
|
|
[5602] = { continent = 2, left = 0.523772, top = 0.442772, width = 0.133012, height = 0.151224 },
|
|
}
|
|
pfMap.customContinentTransforms = customContinentTransforms
|
|
|
|
-- Standalone Turtle outdoor maps can reuse quest entities that also carry
|
|
-- coordinates in an older/base zone. Those duplicate records are useful for
|
|
-- compatibility, but Current Zone Only must not render them as local pins.
|
|
-- Most custom outdoor maps are already identified by their continent
|
|
-- transform; these additional maps use client WorldMapArea data and therefore
|
|
-- do not need a custom projection.
|
|
local boundaryAliasMaps = {}
|
|
for zoneID in pairs(customContinentTransforms) do boundaryAliasMaps[zoneID] = true end
|
|
boundaryAliasMaps[406] = true -- Stonetalon / Desolace border overlap
|
|
|
|
function pfMap:BuildBoundaryAliasKeys(map)
|
|
local aliases = {}
|
|
if not map or not boundaryAliasMaps[map] then return aliases end
|
|
local candidates, localAnchors = {}, {}
|
|
local currentNodes = self.nodes.PFQUEST and self.nodes.PFQUEST[map]
|
|
if not currentNodes then return aliases end
|
|
for _, node in pairs(currentNodes) do
|
|
for _, data in pairs(node) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
candidates[key] = true
|
|
if string.find(data.QTYPE, "_START", 1, true)
|
|
or string.find(data.QTYPE, "_END", 1, true) then
|
|
localAnchors[key] = data.questid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local duplicateKeys, sharedAnchors = {}, {}
|
|
for _, addonData in pairs(self.nodes) do
|
|
for otherZone, zoneNodes in pairs(addonData) do
|
|
if otherZone ~= map then
|
|
for _, node in pairs(zoneNodes) do
|
|
for _, data in pairs(node) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
if candidates[key] then
|
|
duplicateKeys[key] = data.questid
|
|
if localAnchors[key] then sharedAnchors[key] = true end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local anchoredQuests = {}
|
|
for key, questid in pairs(localAnchors) do
|
|
if not sharedAnchors[key] then anchoredQuests[questid] = true end
|
|
end
|
|
for key, questid in pairs(duplicateKeys) do
|
|
if not anchoredQuests[questid] then aliases[key] = true end
|
|
end
|
|
return aliases
|
|
end
|
|
|
|
function pfMap:IsBoundaryAliasNode(node, aliases)
|
|
local matched = false
|
|
for _, data in pairs(node or {}) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
if not aliases[key] then return false end
|
|
matched = true
|
|
end
|
|
end
|
|
return matched
|
|
end
|
|
|
|
-- City maps have no meaningful regional fog. Use the character's visit record
|
|
-- instead, including Alah'Thalas and the normal capital-city map IDs.
|
|
local cityVisitMaps = {
|
|
[1497] = true, [1519] = true, [1537] = true, [1637] = true,
|
|
[1638] = true, [1657] = true, [2040] = true,
|
|
}
|
|
|
|
local function GetZoneData(zoneID)
|
|
return pfDB and pfDB["zones"] and pfDB["zones"]["data"] and pfDB["zones"]["data"][zoneID]
|
|
end
|
|
|
|
-- Exploration overlays belong to the map currently shown by the client. Some
|
|
-- Turtle zones (Alah'Thalas is the important example) are stored as a child
|
|
-- rectangle inside another map, so their quest coordinates need to be lifted
|
|
-- into that parent map before they can be compared with cached overlays.
|
|
-- Direct ClassicAPI results are kept separately from the saved clean-client
|
|
-- cache. They are warmed gradually, never queried from the pin-render loop.
|
|
local directExplorationBounds = {}
|
|
local directExplorationKnown = {}
|
|
|
|
local function GetExplorationBounds(zoneID, x, y)
|
|
local exploredAreas = pfMap.exploredAreas
|
|
if not exploredAreas then return nil, x, y end
|
|
|
|
if cityVisitMaps[zoneID] and pfMap.IsMapVisited then
|
|
if pfMap:IsMapVisited(zoneID) then return nil, x, y end
|
|
return {}, x, y
|
|
end
|
|
|
|
-- Prefer the freshly updated saved state for the current map. Other maps
|
|
-- use the direct cache if it has been warmed, then the clean-client cache.
|
|
local playerMapID = pfMap.GetPlayerMapID and pfMap:GetPlayerMapID()
|
|
local explored = exploredAreas[zoneID]
|
|
if zoneID == playerMapID and explored then return explored, x, y end
|
|
if directExplorationKnown[zoneID] and directExplorationBounds[zoneID] then
|
|
return directExplorationBounds[zoneID], x, y
|
|
end
|
|
if explored then return explored, x, y end
|
|
|
|
local seen = {}
|
|
while zoneID and not seen[zoneID] do
|
|
seen[zoneID] = true
|
|
local zoneData = GetZoneData(zoneID)
|
|
if not zoneData or not zoneData[1] or not zoneData[2] or not zoneData[3] or not zoneData[4] or not zoneData[5] then
|
|
break
|
|
end
|
|
|
|
-- zoneData is { parent, width, height, centerX, centerY } in percent.
|
|
x = zoneData[4] - zoneData[2] / 2 + zoneData[2] * (x / 100)
|
|
y = zoneData[5] - zoneData[3] / 2 + zoneData[3] * (y / 100)
|
|
zoneID = zoneData[1]
|
|
|
|
explored = exploredAreas[zoneID]
|
|
if explored then return explored, x, y end
|
|
end
|
|
|
|
return nil, x, y
|
|
end
|
|
|
|
-- Continent assignments (2 = Eastern Kingdoms, 1 = Kalimdor)
|
|
local zoneContinent = {
|
|
[1] = 2, [3] = 2, [4] = 2, [8] = 2, [10] = 2, [11] = 2, [12] = 2, [28] = 2,
|
|
[33] = 2, [36] = 2, [38] = 2, [40] = 2, [41] = 2, [44] = 2, [45] = 2, [46] = 2,
|
|
[47] = 2, [51] = 2, [85] = 2, [130] = 2, [139] = 2, [267] = 2, [1497] = 2,
|
|
[1519] = 2, [1537] = 2,
|
|
[14] = 1, [15] = 1, [16] = 1, [17] = 1, [141] = 1, [148] = 1, [215] = 1,
|
|
[331] = 1, [357] = 1, [361] = 1, [400] = 1, [405] = 1, [406] = 1, [440] = 1,
|
|
[490] = 1, [493] = 1, [618] = 1, [1377] = 1, [1637] = 1, [1638] = 1, [1657] = 1,
|
|
}
|
|
|
|
local function IsSameZoneFamily(firstID, secondID)
|
|
if not firstID or not secondID then return false end
|
|
if firstID == secondID then return true end
|
|
local seen = {}
|
|
local function AddParents(zoneID)
|
|
while zoneID and zoneID ~= 0 and zoneID ~= 1 and not seen[zoneID] do
|
|
seen[zoneID] = true
|
|
local data = GetZoneData(zoneID)
|
|
zoneID = data and data[1]
|
|
end
|
|
end
|
|
AddParents(firstID)
|
|
while secondID and secondID ~= 0 and secondID ~= 1 do
|
|
if seen[secondID] then return true end
|
|
local data = GetZoneData(secondID)
|
|
secondID = data and data[1]
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function GetZoneContinent(zoneID)
|
|
local custom = customContinentTransforms[zoneID]
|
|
if custom then
|
|
return custom.continent
|
|
end
|
|
|
|
if zoneContinent[zoneID] then
|
|
return zoneContinent[zoneID]
|
|
end
|
|
|
|
local zoneData = GetZoneData(zoneID)
|
|
if zoneData and zoneData[1] then
|
|
local continent = zoneData[1]
|
|
if continent == 0 then
|
|
continent = 2
|
|
elseif continent == 1 then
|
|
continent = 1
|
|
else
|
|
return nil
|
|
end
|
|
|
|
zoneContinent[zoneID] = continent
|
|
return continent
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Warm optional ClassicAPI fog data one zone at a time. A map redraw only
|
|
-- reads this cache; it never triggers a cross-zone API query itself.
|
|
local explorationQueue, explorationQueueIndex, explorationQueueKey = {}, 1, nil
|
|
local explorationElapsed, explorationRefreshElapsed = 0, 0
|
|
local explorationChanged = false
|
|
|
|
local function QueueExplorationWarmup(continent, viewKey)
|
|
if not (pfQuestCompat and pfQuestCompat.optional and pfQuestCompat.optional.mapExploration and pfMap.GetExploredBounds and pfMap.nodes) then return false end
|
|
if explorationQueueKey == viewKey then return true end
|
|
|
|
explorationQueue, explorationQueueIndex, explorationQueueKey = {}, 1, viewKey
|
|
local queued = {}
|
|
local function QueueZone(zoneID)
|
|
local zoneContinent = GetZoneContinent(zoneID)
|
|
if (continent == 0 or zoneContinent == continent) and not directExplorationKnown[zoneID]
|
|
and not (pfMap.exploredAreas and pfMap.exploredAreas[zoneID]) and not queued[zoneID] then
|
|
queued[zoneID] = true
|
|
table.insert(explorationQueue, zoneID)
|
|
end
|
|
end
|
|
|
|
-- The node table grows as maps are visited, so seed from the known map
|
|
-- catalog first. This lets login warm both continents before either map
|
|
-- view has been opened.
|
|
for zoneID in pairs(zoneContinent) do QueueZone(zoneID) end
|
|
for zoneID in pairs(customContinentTransforms) do QueueZone(zoneID) end
|
|
for _, addonData in pairs(pfMap.nodes) do
|
|
for zoneID in pairs(addonData) do QueueZone(zoneID) end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local explorationWarmFrame = CreateFrame("Frame")
|
|
explorationWarmFrame:SetScript("OnUpdate", function()
|
|
if pfQuest_config["hideunexplored"] ~= "1" or explorationQueueIndex > table.getn(explorationQueue) then return end
|
|
explorationElapsed = explorationElapsed + (arg1 or 0)
|
|
explorationRefreshElapsed = explorationRefreshElapsed + (arg1 or 0)
|
|
if explorationElapsed < 0.05 then return end
|
|
explorationElapsed = 0
|
|
|
|
local zoneID = explorationQueue[explorationQueueIndex]
|
|
explorationQueueIndex = explorationQueueIndex + 1
|
|
local bounds = pfMap.GetExploredBounds(zoneID)
|
|
directExplorationKnown[zoneID] = true
|
|
directExplorationBounds[zoneID] = bounds
|
|
if bounds then
|
|
-- Persist the result per character so future continent views do not
|
|
-- need to warm the same zone again after a reload.
|
|
pfMap.exploredAreas[zoneID] = bounds
|
|
end
|
|
explorationChanged = true
|
|
|
|
-- Redraw in batches rather than once per API result.
|
|
if explorationChanged and (explorationRefreshElapsed >= 0.5 or explorationQueueIndex > table.getn(explorationQueue)) then
|
|
explorationChanged, explorationRefreshElapsed = false, 0
|
|
if WorldMapFrame:IsShown() then pfMap:UpdateNodes() end
|
|
end
|
|
end)
|
|
|
|
local function ZoneToWorld(x, y, zoneID)
|
|
local uiMapID = zoneToUiMapID[zoneID]
|
|
if not uiMapID then
|
|
return nil, nil
|
|
end
|
|
local data = mapData[uiMapID]
|
|
if not data then
|
|
return nil, nil
|
|
end
|
|
|
|
local worldX = data[3] - data[1] * (x / 100)
|
|
local worldY = data[4] - data[2] * (y / 100)
|
|
|
|
return worldX, worldY
|
|
end
|
|
|
|
-- continent: 1 = Kalimdor, 2 = Eastern Kingdoms
|
|
local function WorldToContinent(worldX, worldY, continent)
|
|
local contData = mapData[continent == 1 and 1414 or 1415]
|
|
if not contData then
|
|
return nil, nil
|
|
end
|
|
|
|
local x = (contData[3] - worldX) / contData[1]
|
|
local y = (contData[4] - worldY) / contData[2]
|
|
|
|
return x, y
|
|
end
|
|
|
|
local function ZoneToContinent(x, y, zoneID, continent)
|
|
local custom = customContinentTransforms[zoneID]
|
|
if custom then
|
|
if custom.continent ~= continent then
|
|
return nil, nil
|
|
end
|
|
return custom.left + custom.width * (x / 100), custom.top + custom.height * (y / 100)
|
|
end
|
|
|
|
local worldX, worldY = ZoneToWorld(x, y, zoneID)
|
|
if not worldX or not worldY then
|
|
return nil, nil
|
|
end
|
|
return WorldToContinent(worldX, worldY, continent)
|
|
end
|
|
|
|
local function NodeAnimate(self, max)
|
|
return
|
|
end
|
|
|
|
local inverseMapScale = 1.0
|
|
local function ResizeContinentNode(frame)
|
|
if not frame.icon then
|
|
frame.defsize = tonumber(pfQuest_config["continentNodeSize"]) or 12
|
|
frame.defsize = frame.defsize * inverseMapScale
|
|
else
|
|
frame.defsize = tonumber(pfQuest_config["continentUtilityNodeSize"]) or 14
|
|
frame.defsize = (frame.defsize - 2) * inverseMapScale + 2
|
|
end
|
|
frame:SetWidth(frame.defsize)
|
|
frame:SetHeight(frame.defsize)
|
|
frame.hl:SetWidth(frame.defsize)
|
|
frame.hl:SetHeight(frame.defsize)
|
|
end
|
|
|
|
local lastResize = 0
|
|
local doResize = false
|
|
|
|
local nodeResizeFrame = CreateFrame("Frame")
|
|
nodeResizeFrame:SetScript("OnUpdate", function()
|
|
lastResize = lastResize + (arg1 or 0)
|
|
if doResize and lastResize >= 1 then
|
|
lastResize = 0
|
|
doResize = false
|
|
|
|
pfMap:UpdateNodes()
|
|
|
|
local i = 1
|
|
if continentPins then
|
|
while continentPins[i] and continentPins[i]:IsShown() do
|
|
ResizeContinentNode(continentPins[i])
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
local function ResizeContinentNodes()
|
|
doResize = true
|
|
end
|
|
|
|
local function OnMapScaleChanged(frame, scale, originalfunction)
|
|
originalfunction(frame, scale)
|
|
|
|
local newInverseScale = 1.0 / WorldMapButton:GetEffectiveScale()
|
|
if (inverseMapScale ~= newInverseScale) then
|
|
inverseMapScale = newInverseScale
|
|
ResizeContinentNodes()
|
|
end
|
|
end
|
|
|
|
local originalWorldMapFrame_SetScale = WorldMapFrame.SetScale
|
|
WorldMapFrame.SetScale = function(frame, scale)
|
|
OnMapScaleChanged(frame, scale, originalWorldMapFrame_SetScale)
|
|
end
|
|
local originalWorldMapDetailFrame_SetScale = WorldMapDetailFrame.SetScale
|
|
WorldMapDetailFrame.SetScale = function(frame, scale)
|
|
OnMapScaleChanged(frame, scale, originalWorldMapDetailFrame_SetScale)
|
|
end
|
|
local originalWorldMapButton_SetScale = WorldMapButton.SetScale
|
|
WorldMapButton.SetScale = function(frame, scale)
|
|
OnMapScaleChanged(frame, scale, originalWorldMapButton_SetScale)
|
|
end
|
|
|
|
-- Share expensive native input queries across dense continent pin sets.
|
|
local interactionInputAt, interactionControl, interactionX, interactionY = 0, false, 0, 0
|
|
local function ReadContinentInteractionInput(now)
|
|
if now >= interactionInputAt then
|
|
interactionInputAt = now + 0.05
|
|
interactionControl = IsControlKeyDown()
|
|
interactionX, interactionY = GetCursorPosition()
|
|
end
|
|
return interactionControl, interactionX, interactionY
|
|
end
|
|
|
|
local function ConfigureContinentPinInteraction(pin, force)
|
|
local clickThrough = pfQuest_config["continentClickThrough"] == "1"
|
|
if not force and pin.clickThrough == clickThrough then return end
|
|
pin.clickThrough = clickThrough
|
|
|
|
if clickThrough then
|
|
pin:EnableMouse(false)
|
|
pin:RegisterForClicks()
|
|
pin:SetScript("OnEnter", nil)
|
|
pin:SetScript("OnLeave", nil)
|
|
pin:SetScript("OnClick", function()
|
|
if IsControlKeyDown() and this.node and pfMap.NodeClick then
|
|
pfMap.NodeClick()
|
|
end
|
|
end)
|
|
pin:SetScript("OnUpdate", function()
|
|
local now = GetTime()
|
|
if now < (this.interactionAt or 0) then return end
|
|
this.interactionAt = now + 0.05
|
|
if not this:IsVisible() then return end
|
|
local control, x, y = ReadContinentInteractionInput(now)
|
|
if control then
|
|
if not this.mouseEnabled then
|
|
this:EnableMouse(true)
|
|
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
this.mouseEnabled = true
|
|
end
|
|
elseif this.mouseEnabled ~= false then
|
|
this:EnableMouse(false)
|
|
this:RegisterForClicks()
|
|
this.mouseEnabled = false
|
|
end
|
|
|
|
local scale = this:GetEffectiveScale()
|
|
x, y = x / scale, y / scale
|
|
local left, right, top, bottom = this:GetLeft(), this:GetRight(), this:GetTop(), this:GetBottom()
|
|
local over = left and right and top and bottom and x >= left and x <= right and y >= bottom and y <= top
|
|
if over and not this.wasMouseOver then
|
|
if this.node then pfMap.NodeEnter() end
|
|
this.wasMouseOver = true
|
|
elseif not over and this.wasMouseOver then
|
|
this.pulse, this.mod = 1, 1
|
|
this:SetWidth(this.defsize)
|
|
this:SetHeight(this.defsize)
|
|
pfMap.NodeLeave()
|
|
this.wasMouseOver = false
|
|
end
|
|
end)
|
|
else
|
|
pin:EnableMouse(true)
|
|
pin:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
pin.mouseEnabled = true
|
|
pin:SetScript("OnUpdate", nil)
|
|
pin:SetScript("OnEnter", function()
|
|
if this.node then pfMap.NodeEnter() end
|
|
this.wasMouseOver = true
|
|
end)
|
|
pin:SetScript("OnLeave", function()
|
|
this.pulse, this.mod = 1, 1
|
|
this:SetWidth(this.defsize)
|
|
this:SetHeight(this.defsize)
|
|
pfMap.NodeLeave()
|
|
this.wasMouseOver = false
|
|
end)
|
|
pin:SetScript("OnClick", function()
|
|
if this.node and pfMap.NodeClick then pfMap.NodeClick() end
|
|
end)
|
|
end
|
|
end
|
|
|
|
local function CreateContinentPin(index)
|
|
if not continentPins[index] then
|
|
local pin = CreateFrame("Button", "pfQuestContinentPin" .. index, WorldMapButton)
|
|
pin.worldmap = true
|
|
pin:SetScript("OnHide", function()
|
|
if pfMap.ReleaseNodeTooltip then pfMap:ReleaseNodeTooltip(this) end
|
|
this.wasMouseOver = false
|
|
end)
|
|
pin.tex = pin:CreateTexture(nil, "BACKGROUND")
|
|
pin.tex:SetAllPoints(pin)
|
|
pin.pic = pin:CreateTexture(nil, "BORDER")
|
|
pin.pic:SetPoint("TOPLEFT", pin, "TOPLEFT", 1, -1)
|
|
pin.pic:SetPoint("BOTTOMRIGHT", pin, "BOTTOMRIGHT", -1, 1)
|
|
pin.hl = pin:CreateTexture(nil, "OVERLAY")
|
|
pin.hl:SetTexture(pfQuestConfig.path .. "\\img\\track")
|
|
pin.hl:SetPoint("TOPLEFT", pin, "TOPLEFT", -5, 5)
|
|
pin.hl:Hide()
|
|
pin.defalpha, pin.Animate, pin.dt = 1, NodeAnimate, 0
|
|
continentPins[index] = pin
|
|
end
|
|
ConfigureContinentPinInteraction(continentPins[index])
|
|
return continentPins[index]
|
|
end
|
|
|
|
local function HideContinentPin(pin)
|
|
if pin.wasMouseOver then
|
|
-- Programmatic hiding does not set the legacy global `this` to the
|
|
-- pin, so pfMap.NodeLeave cannot reliably choose WorldMapTooltip.
|
|
if pfMap.ReleaseNodeTooltip then
|
|
pfMap:ReleaseNodeTooltip(pin)
|
|
else
|
|
WorldMapTooltip:Hide()
|
|
pfMap.highlight = nil
|
|
end
|
|
pin.wasMouseOver = false
|
|
end
|
|
pin:Hide()
|
|
end
|
|
|
|
function pfMap:UpdateNode(frame, node, color, obj, distance)
|
|
original_UpdateNode(self, frame, node, color, obj, distance)
|
|
|
|
if obj == "minimap" then
|
|
return
|
|
end
|
|
|
|
ResizeContinentNode(frame)
|
|
end
|
|
|
|
local function GetGrayLevel(charLevel)
|
|
if charLevel <= 5 then
|
|
return 0
|
|
elseif charLevel <= 49 then
|
|
return charLevel - math.floor(charLevel / 10) - 5
|
|
elseif charLevel == 50 then
|
|
return 40
|
|
elseif charLevel <= 59 then
|
|
return charLevel - math.floor(charLevel / 5) - 1
|
|
else
|
|
return charLevel - 9
|
|
end
|
|
end
|
|
|
|
-- Where each continent sits within the combined world-view canvas.
|
|
-- Same {width, height, left, top} format as the mapData entries above,
|
|
local IDENTITY_LAYOUT = {1, 1, 0, 0}
|
|
local WORLD_VIEW_LAYOUT = {
|
|
[1] = {0.85, 0.82, -0.20, 0.06}, -- Kalimdor (left)
|
|
[2] = {0.85, 0.82, 0.35, 0.06}, -- Eastern Kingdoms (right)
|
|
}
|
|
|
|
-- Capital cities have their own map IDs and their nodes are therefore absent
|
|
-- when the player views the enclosing outdoor zone (for example Ironforge on
|
|
-- the Dun Morogh map). Project those city nodes onto that parent zone without
|
|
-- merging the source-node tables or changing the normal city-map renderer.
|
|
local cityParentMaps = {
|
|
[1519] = 12, -- Stormwind City -> Elwynn Forest
|
|
[1537] = 1, -- Ironforge -> Dun Morogh
|
|
[1497] = 85, -- Undercity -> Tirisfal Glades
|
|
[1637] = 14, -- Orgrimmar -> Durotar
|
|
[1638] = 215, -- Thunder Bluff -> Mulgore
|
|
[1657] = 141, -- Darnassus -> Teldrassil
|
|
}
|
|
|
|
local function CityToParent(x, y, cityID, parentID)
|
|
local worldX, worldY = ZoneToWorld(x, y, cityID)
|
|
local parentMapID = zoneToUiMapID[parentID]
|
|
local parent = parentMapID and mapData[parentMapID]
|
|
if not worldX or not worldY or not parent then return nil, nil end
|
|
|
|
return (parent[3] - worldX) / parent[1], (parent[4] - worldY) / parent[2]
|
|
end
|
|
|
|
local function PlaceCityPinsOnParentMap(parentID, pinCount)
|
|
local currentZoneOnly = tonumber(pfQuest_config["trackingmethod"]) == 5
|
|
local playerMapID = pfMap.GetPlayerMapID and pfMap:GetPlayerMapID() or pfMap.playerMapID
|
|
local hideUnexplored = pfQuest_config["hideunexplored"] == "1"
|
|
|
|
for cityID, cityParentID in pairs(cityParentMaps) do
|
|
if cityParentID == parentID and (not currentZoneOnly or playerMapID == cityID) then
|
|
for addon, addonData in pairs(pfMap.nodes) do
|
|
local cityNodes = addonData[cityID]
|
|
if cityNodes then
|
|
for coords, node in pairs(cityNodes) do
|
|
local _, _, strx, stry = strfind(coords, "(.*)|(.*)")
|
|
local x, y = tonumber(strx), tonumber(stry)
|
|
local explored = hideUnexplored and GetExplorationBounds(cityID, x, y)
|
|
local parentX, parentY
|
|
if x and y then
|
|
parentX, parentY = CityToParent(x, y, cityID, parentID)
|
|
end
|
|
|
|
if parentX and parentY and parentX >= 0 and parentX <= 1 and parentY >= 0 and parentY <= 1
|
|
and not (hideUnexplored and explored and not pfMap.IsExploredPosition(explored, x, y)) then
|
|
pinCount = pinCount + 1
|
|
if pinCount > maxContinentPins then return pinCount end
|
|
|
|
local pin = CreateContinentPin(pinCount)
|
|
pin.node = node
|
|
pin.sourceZone = cityID
|
|
pfMap:UpdateNode(pin, node, nil, nil, nil)
|
|
pin:ClearAllPoints()
|
|
pin:SetPoint("CENTER", WorldMapButton, "TOPLEFT", parentX * WorldMapButton:GetWidth(), -parentY * WorldMapButton:GetHeight())
|
|
|
|
if pfQuest_config["showcluster"] == "0" and pin.cluster then
|
|
HideContinentPin(pin)
|
|
elseif pfQuest_config["showspawn"] == "0" and addon == "PFQUEST" and not pin.texture then
|
|
HideContinentPin(pin)
|
|
else
|
|
pin:Show()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return pinCount
|
|
end
|
|
|
|
-- Dense item-source queries can contain hundreds of creature or object
|
|
-- spawns. Every point remains available on zone maps, but continent maps need
|
|
-- only enough representatives to show the item's geographic distribution.
|
|
-- A 1.5% map-space radius is roughly 12-16 pixels at normal World Map sizes.
|
|
local continentItemSpacing = 0.015
|
|
local function GetContinentItemKeys(node)
|
|
local keys, seen = {}, {}
|
|
for _, data in pairs(node) do
|
|
local qtype = tostring(data.QTYPE or "")
|
|
local itemSource = data.item and (not data.questid
|
|
or string.find(qtype, "ITEM_OBJECTIVE", 1, true) == 1)
|
|
if not itemSource then return nil end
|
|
|
|
local key = tostring(data.questid or "DB") .. ":" .. tostring(data.itemid or data.item)
|
|
if not seen[key] then
|
|
seen[key] = true
|
|
table.insert(keys, key)
|
|
end
|
|
end
|
|
return table.getn(keys) > 0 and keys or nil
|
|
end
|
|
|
|
local function ThinContinentItemNode(processedQuests, node, continent, x, y)
|
|
local keys = GetContinentItemKeys(node)
|
|
if not keys then return false end
|
|
|
|
-- Purple Lotus has thousands of world-drop sources. Keep its continent
|
|
-- overview sparse; zone and minimap nodes are never filtered here.
|
|
local spacing = continentItemSpacing
|
|
for _, data in pairs(node) do
|
|
if data.itemid == 8831 or data.item == "Purple Lotus" then spacing = 0.04 end
|
|
end
|
|
|
|
processedQuests.itemDensityBuckets = processedQuests.itemDensityBuckets or {}
|
|
processedQuests.itemDensityBuckets[continent] = processedQuests.itemDensityBuckets[continent] or {}
|
|
local continentBuckets = processedQuests.itemDensityBuckets[continent]
|
|
local cellX = math.floor(x / spacing)
|
|
local cellY = math.floor(y / spacing)
|
|
local allCovered = true
|
|
|
|
for _, key in pairs(keys) do
|
|
local itemBuckets = continentBuckets[key]
|
|
local covered = false
|
|
if itemBuckets then
|
|
for offsetX = -1, 1 do
|
|
for offsetY = -1, 1 do
|
|
local previous = itemBuckets[(cellX + offsetX) .. ":" .. (cellY + offsetY)]
|
|
if previous then
|
|
local dx, dy = previous.x - x, previous.y - y
|
|
if dx * dx + dy * dy < spacing * spacing then
|
|
covered = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if covered then break end
|
|
end
|
|
end
|
|
if not covered then allCovered = false end
|
|
end
|
|
|
|
if allCovered then return true end
|
|
|
|
for _, key in pairs(keys) do
|
|
local itemBuckets = continentBuckets[key] or {}
|
|
continentBuckets[key] = itemBuckets
|
|
itemBuckets[cellX .. ":" .. cellY] = { x = x, y = y }
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function PlaceContinentPins(continent, layout, pinCount, playerLevel, processedQuests, stats)
|
|
local currentZoneOnly = tonumber(pfQuest_config["trackingmethod"]) == 5
|
|
local playerMapID = pfMap.GetPlayerMapID and pfMap:GetPlayerMapID() or pfMap.playerMapID
|
|
local hideUnexplored = pfQuest_config["hideunexplored"] == "1"
|
|
processedQuests.projectedMarkers = processedQuests.projectedMarkers or {}
|
|
local currentZoneAliases = {}
|
|
if currentZoneOnly and boundaryAliasMaps[playerMapID] then
|
|
local candidates, localAnchors = {}, {}
|
|
local currentNodes = pfMap.nodes.PFQUEST and pfMap.nodes.PFQUEST[playerMapID]
|
|
if currentNodes then
|
|
for _, node in pairs(currentNodes) do
|
|
for _, data in pairs(node) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
candidates[key] = true
|
|
if string.find(data.QTYPE, "_START", 1, true)
|
|
or string.find(data.QTYPE, "_END", 1, true) then
|
|
localAnchors[key] = data.questid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local duplicateKeys, sharedAnchors = {}, {}
|
|
for _, addonData in pairs(pfMap.nodes) do
|
|
for otherZone, zoneNodes in pairs(addonData) do
|
|
if otherZone ~= playerMapID then
|
|
for _, node in pairs(zoneNodes) do
|
|
for _, data in pairs(node) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
if candidates[key] then
|
|
duplicateKeys[key] = data.questid
|
|
if localAnchors[key] then sharedAnchors[key] = true end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local anchoredQuests = {}
|
|
for key, questid in pairs(localAnchors) do
|
|
if not sharedAnchors[key] then anchoredQuests[questid] = true end
|
|
end
|
|
for key, questid in pairs(duplicateKeys) do
|
|
if not anchoredQuests[questid] then currentZoneAliases[key] = true end
|
|
end
|
|
end
|
|
for addon, addonData in pairs(pfMap.nodes) do
|
|
for zID, zoneNodes in pairs(addonData) do
|
|
stats.zonesSeen = stats.zonesSeen + 1
|
|
local zoneCont = GetZoneContinent(zID)
|
|
local sameCurrentZone = currentZoneOnly and IsSameZoneFamily(zID, playerMapID)
|
|
if zoneCont == continent and (not currentZoneOnly or sameCurrentZone) then
|
|
stats.zonesMatched = stats.zonesMatched + 1
|
|
local uiMapID = zoneToUiMapID[zID]
|
|
if customContinentTransforms[zID] or (uiMapID and mapData[uiMapID]) then
|
|
stats.zonesWithUiMapID = stats.zonesWithUiMapID + 1
|
|
for coords, node in pairs(zoneNodes) do
|
|
local skipNode = false
|
|
local questKey = nil
|
|
|
|
for title, data in pairs(node) do
|
|
local needsDeduplication = false
|
|
local isUtilityNPC = false
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local aliasKey = tostring(data.questid) .. ":" .. tostring(data.QTYPE) .. ":" .. tostring(data.spawnid)
|
|
if currentZoneAliases[aliasKey] then
|
|
skipNode = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if data.addon and string.find(data.addon, "TRACK_") then
|
|
-- avoid over populating continent maps with crap make zone only
|
|
local allowedTracks = {
|
|
"TRACK_FLIGHT", "TRACK_AUCTIONEER", "TRACK_BANKER", "TRACK_BATTLEMASTER",
|
|
"TRACK_INNKEEPER", "TRACK_MAILBOX", "TRACK_STABLEMASTER",
|
|
"TRACK_SPIRITHEALER", "TRACK_MEETINGSTONE",
|
|
}
|
|
|
|
local isAllowed = false
|
|
for _, track in pairs(allowedTracks) do
|
|
if string.find(data.addon, track) then
|
|
isAllowed = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if isAllowed then
|
|
isUtilityNPC = true
|
|
else
|
|
skipNode = true
|
|
break
|
|
end
|
|
end
|
|
|
|
-- avoid duplicate pins for zone/city pairs that overlap
|
|
if
|
|
(zID == 141 or zID == 1657) or (zID == 12 or zID == 1519) or
|
|
(zID == 1 or zID == 1537) or (zID == 14 or zID == 1637) or
|
|
(zID == 215 or zID == 1638) or (zID == 85 or zID == 1497)
|
|
then
|
|
needsDeduplication = true
|
|
|
|
if zID == 141 or zID == 1657 then
|
|
questKey = title .. "_teldrassil"
|
|
elseif zID == 12 or zID == 1519 then
|
|
questKey = title .. "_stormwind"
|
|
elseif zID == 1 or zID == 1537 then
|
|
questKey = title .. "_ironforge"
|
|
elseif zID == 14 or zID == 1637 then
|
|
questKey = title .. "_orgrimmar"
|
|
elseif zID == 215 or zID == 1638 then
|
|
questKey = title .. "_thunderbluff"
|
|
elseif zID == 85 or zID == 1497 then
|
|
questKey = title .. "_undercity"
|
|
end
|
|
end
|
|
|
|
if needsDeduplication and questKey and processedQuests[questKey] then
|
|
skipNode = true
|
|
break
|
|
end
|
|
|
|
local questLevel = tonumber(data.qlvl) or tonumber(data.lvl) or 0
|
|
local minLevel = tonumber(data.min) or 0
|
|
|
|
if not isUtilityNPC then
|
|
if pfQuest_config["showlowlevel"] == "0" then
|
|
if questLevel > 0 and questLevel <= GetGrayLevel(playerLevel) then
|
|
if not (data.texture and string.find(data.texture, "complete")) then
|
|
skipNode = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if minLevel > playerLevel + (pfQuest_config["showhighlevel"] == "1" and 3 or 0) then
|
|
if not (data.texture and string.find(data.texture, "complete")) then
|
|
skipNode = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if pfQuest_config["showlowlevel"] == "0" then
|
|
if minLevel <= 1 and questLevel <= GetGrayLevel(playerLevel) then
|
|
if not (data.texture and string.find(data.texture, "complete")) then
|
|
skipNode = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if needsDeduplication and questKey and not skipNode then
|
|
processedQuests[questKey] = true
|
|
end
|
|
end
|
|
|
|
if not skipNode then
|
|
local _, _, strx, stry = strfind(coords, "(.*)|(.*)")
|
|
local zoneX = tonumber(strx)
|
|
local zoneY = tonumber(stry)
|
|
|
|
if zoneX and zoneY then
|
|
-- Exploration overlays are cached only when the player has
|
|
-- opened that zone normally. Unknown zones stay visible;
|
|
-- hiding them would make a fresh cache look like Current
|
|
-- Zone Only until the player browsed every map once.
|
|
local explored, explorationX, explorationY = GetExplorationBounds(zID, zoneX, zoneY)
|
|
if hideUnexplored and explored and not pfMap.IsExploredPosition(explored, explorationX, explorationY) then
|
|
skipNode = true
|
|
end
|
|
local contX, contY = ZoneToContinent(zoneX, zoneY, zID, continent)
|
|
if not skipNode and contX and contY then
|
|
-- Turtle can expose one physical quest NPC through both a
|
|
-- custom-zone record and its underlying standard-zone record.
|
|
-- On the continent map those coordinates overlap. Suppress
|
|
-- only matching quest markers within one icon width when one
|
|
-- side comes from a custom map; real nearby NPC spawns remain.
|
|
local projectionKeys = {}
|
|
local duplicateProjection = false
|
|
for _, data in pairs(node) do
|
|
if data.questid and data.spawnid and data.QTYPE then
|
|
local key = tostring(data.questid) .. ":" .. tostring(data.QTYPE)
|
|
.. ":" .. tostring(data.spawnid)
|
|
table.insert(projectionKeys, key)
|
|
local previous = processedQuests.projectedMarkers[key]
|
|
if previous and (previous.custom or customContinentTransforms[zID]) then
|
|
local sameZoneFamily = previous.zone
|
|
and IsSameZoneFamily(previous.zone, zID)
|
|
local nearbyProjection = math.abs(previous.x - contX) <= 0.012
|
|
and math.abs(previous.y - contY) <= 0.012
|
|
if sameZoneFamily or nearbyProjection then
|
|
duplicateProjection = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if duplicateProjection then
|
|
skipNode = true
|
|
else
|
|
for _, key in pairs(projectionKeys) do
|
|
processedQuests.projectedMarkers[key] = {
|
|
x = contX, y = contY,
|
|
zone = zID,
|
|
custom = customContinentTransforms[zID] and true or false,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
if not skipNode and contX and contY then
|
|
if ThinContinentItemNode(processedQuests, node, continent, contX, contY) then
|
|
skipNode = true
|
|
end
|
|
end
|
|
if not skipNode and contX and contY then
|
|
stats.nodesConverted = stats.nodesConverted + 1
|
|
if contX and contY and contX >= 0 and contX <= 1 and contY >= 0 and contY <= 1 then
|
|
pinCount = pinCount + 1
|
|
if pinCount > maxContinentPins then
|
|
break
|
|
end
|
|
|
|
local worldMapX = layout[3] + layout[1] * contX
|
|
local worldMapY = layout[4] + layout[2] * contY
|
|
|
|
local pin = CreateContinentPin(pinCount)
|
|
pin.node = node
|
|
pin.sourceContinent = continent
|
|
pin.sourceZone = zID
|
|
|
|
pfMap:UpdateNode(pin, node, nil, nil, nil)
|
|
|
|
ResizeContinentNode(pin)
|
|
|
|
pin:ClearAllPoints()
|
|
pin:SetPoint(
|
|
"CENTER",
|
|
WorldMapButton,
|
|
"TOPLEFT",
|
|
worldMapX * WorldMapButton:GetWidth(),
|
|
-worldMapY * WorldMapButton:GetHeight()
|
|
)
|
|
-- Match zone-map display preferences on continent/world maps.
|
|
if pfQuest_config["showcluster"] == "0" and pin.cluster then
|
|
HideContinentPin(pin)
|
|
elseif pfQuest_config["showspawn"] == "0" and addon == "PFQUEST" and not pin.texture then
|
|
HideContinentPin(pin)
|
|
else
|
|
pin:Show()
|
|
end
|
|
|
|
if CONTINENT_DEBUG and not stats.zonesSampled[zID] then
|
|
stats.zonesSampled[zID] = true
|
|
local zoneName = pfDB and pfDB["zones"] and pfDB["zones"]["loc"] and pfDB["zones"]["loc"][zID] or "?"
|
|
DebugPrint("zone sample: continent=" .. continent .. " zID=" .. tostring(zID) .. " name=" .. tostring(zoneName) .. " contXY=" .. string.format("%.3f,%.3f", contX, contY) .. " worldMapXY=" .. string.format("%.3f,%.3f", worldMapX, worldMapY) .. " screenXY=" .. string.format("%.0f,%.0f", worldMapX * WorldMapButton:GetWidth(), worldMapY * WorldMapButton:GetHeight()))
|
|
end
|
|
elseif not stats.sampledOutOfBounds then
|
|
stats.sampledOutOfBounds = true
|
|
DebugPrint("sample out-of-bounds: continent=" .. continent .. " zID=" .. tostring(zID) .. " zoneXY=" .. tostring(zoneX) .. "," .. tostring(zoneY) .. " contXY=" .. tostring(contX) .. "," .. tostring(contY))
|
|
end
|
|
end
|
|
end
|
|
else
|
|
stats.nodesFiltered = stats.nodesFiltered + 1
|
|
end
|
|
end
|
|
if pinCount >= maxContinentPins then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if pinCount >= maxContinentPins then
|
|
break
|
|
end
|
|
end
|
|
|
|
return pinCount
|
|
end
|
|
|
|
|
|
local function StartContinentRender(continent, viewKey)
|
|
local playerLevel = UnitLevel("player")
|
|
local processedQuests = {}
|
|
local stats = { zonesSeen = 0, zonesMatched = 0, zonesWithUiMapID = 0, nodesFiltered = 0, nodesConverted = 0, sampledOutOfBounds = false, zonesSampled = {} }
|
|
|
|
local function Render()
|
|
local pinCount = 0
|
|
if continent == 0 then
|
|
pinCount = PlaceContinentPins(1, WORLD_VIEW_LAYOUT[1], pinCount, playerLevel, processedQuests, stats)
|
|
pinCount = PlaceContinentPins(2, WORLD_VIEW_LAYOUT[2], pinCount, playerLevel, processedQuests, stats)
|
|
else
|
|
pinCount = PlaceContinentPins(continent, IDENTITY_LAYOUT, pinCount, playerLevel, processedQuests, stats)
|
|
end
|
|
|
|
for i = pinCount + 1, maxContinentPins do
|
|
if continentPins[i] then HideContinentPin(continentPins[i]) end
|
|
end
|
|
|
|
continentRenderLastKey = viewKey
|
|
continentRenderLastAt = GetTime()
|
|
DebugPrint("done: zonesSeen=" .. stats.zonesSeen .. " zonesMatchedContinent=" .. stats.zonesMatched .. " zonesWithUiMapID=" .. stats.zonesWithUiMapID .. " nodesFiltered=" .. stats.nodesFiltered .. " nodesConverted=" .. stats.nodesConverted .. " pinsPlaced=" .. pinCount)
|
|
end
|
|
|
|
Render()
|
|
|
|
end
|
|
|
|
function pfMap:UpdateNodes()
|
|
local continent = GetCurrentMapContinent()
|
|
local zone = GetCurrentMapZone()
|
|
local mapName = GetMapInfo and GetMapInfo() or "?"
|
|
local viewKey = tostring(continent) .. ":" .. tostring(zone) .. ":" .. tostring(mapName)
|
|
|
|
original_UpdateNodes(self)
|
|
|
|
-- The core still refreshes tracker and route state while the map is hidden.
|
|
-- Projection frames are only needed on an open World Map; the standalone
|
|
-- quest-update driver also calls this wrapper during login and quest changes.
|
|
if not WorldMapFrame:IsShown() then return end
|
|
|
|
-- WORLD_MAP_UPDATE and the polling fallback can both request the same
|
|
-- view. Suppress duplicate redraws that arrive immediately together.
|
|
if continentRenderLastKey == viewKey and GetTime() - continentRenderLastAt < 0.5 then return end
|
|
|
|
for i = 1, maxContinentPins do
|
|
if continentPins[i] then
|
|
HideContinentPin(continentPins[i])
|
|
continentPins[i].node = nil
|
|
continentPins[i].sourceContinent = nil
|
|
end
|
|
end
|
|
|
|
DebugPrint("UpdateNodes: continent=" .. tostring(continent) .. " zone=" .. tostring(zone) .. " mapName=" .. tostring(mapName) .. " configOn=" .. tostring(pfQuest_config["continentPins"]))
|
|
|
|
-- A selected outdoor zone can contain a capital city with its own node
|
|
-- map. Render the city's projected pins here; this is independent from
|
|
-- the continent-pin preference because it is a zone-map behavior.
|
|
if zone > 0 then
|
|
-- Use the dropdown's selected map name before the optional current-
|
|
-- area API: while standing inside a capital, that API can still say
|
|
-- Ironforge even when the player is viewing Dun Morogh.
|
|
local parentID = pfMap.GetMapIDByName and pfMap:GetMapIDByName(mapName)
|
|
parentID = parentID or (pfMap.GetMapID and pfMap:GetMapID(continent, zone))
|
|
local pinCount = parentID and PlaceCityPinsOnParentMap(parentID, 0) or 0
|
|
for i = pinCount + 1, maxContinentPins do
|
|
if continentPins[i] then HideContinentPin(continentPins[i]) end
|
|
end
|
|
return
|
|
end
|
|
|
|
if pfQuest_config["continentPins"] == "0" then
|
|
DebugPrint("skip: continentPins disabled in config")
|
|
return
|
|
end
|
|
|
|
-- continent pins only apply at the top-level view of a single continent
|
|
-- (continent 1 or 2) or the combined world view (continent 0)
|
|
if continent < 0 or continent > 2 then
|
|
DebugPrint("skip: not a top-level continent view (zone=" .. tostring(zone) .. " continent=" .. tostring(continent) .. ")")
|
|
return
|
|
end
|
|
|
|
for _, pin in pairs(pfMap.pins) do
|
|
if pin then
|
|
pin:Hide()
|
|
end
|
|
end
|
|
|
|
if pfQuest_config["hideunexplored"] == "1" then
|
|
QueueExplorationWarmup(continent, viewKey)
|
|
end
|
|
StartContinentRender(continent, viewKey)
|
|
end
|
|
|
|
-- Apply projection-only settings without reloading quest data.
|
|
function pfMap:ApplyContinentSettings(changes)
|
|
if changes.size then
|
|
for i = 1, maxContinentPins do
|
|
if continentPins[i] then ResizeContinentNode(continentPins[i]) end
|
|
end
|
|
end
|
|
if changes.visibility then
|
|
continentRenderLastKey = nil
|
|
continentRenderLastAt = 0
|
|
-- Continent visibility must not hide city projections on zone maps.
|
|
if WorldMapFrame:IsShown() and GetCurrentMapZone() == 0 then
|
|
if pfQuest_config.continentPins == "0" then
|
|
for i = 1, maxContinentPins do
|
|
if continentPins[i] then HideContinentPin(continentPins[i]) end
|
|
end
|
|
else
|
|
self:UpdateNodes()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local continentPollFrame = CreateFrame("Frame")
|
|
local continentPollElapsed = 0
|
|
local lastPolledContinent, lastPolledZone
|
|
local continentRefreshPending = false
|
|
continentPollFrame:SetScript("OnUpdate", function()
|
|
if not WorldMapFrame:IsShown() then
|
|
return
|
|
end
|
|
|
|
local currentContinent = GetCurrentMapContinent()
|
|
local currentZone = GetCurrentMapZone()
|
|
|
|
if lastPolledContinent ~= currentContinent or lastPolledZone ~= currentZone then
|
|
DebugPrint("poll transition: continent " .. tostring(lastPolledContinent) .. "->" .. tostring(currentContinent) .. " zone " .. tostring(lastPolledZone) .. "->" .. tostring(currentZone) .. " mapName=" .. tostring(GetMapInfo and GetMapInfo()) .. " buttonShown=" .. tostring(WorldMapButton:IsShown()))
|
|
lastPolledContinent = currentContinent
|
|
lastPolledZone = currentZone
|
|
continentRefreshPending = true
|
|
end
|
|
|
|
-- The world map remains open while the player moves. Rebuilding every
|
|
-- pin on a timer here caused large maps to redraw several times a second.
|
|
-- A redraw is only needed after the viewed continent/zone changes.
|
|
if continentRefreshPending then
|
|
continentPollElapsed = continentPollElapsed + (arg1 or 0)
|
|
end
|
|
if continentRefreshPending and continentPollElapsed >= 0.25 then
|
|
continentPollElapsed = 0
|
|
continentRefreshPending = false
|
|
pfMap:UpdateNodes()
|
|
end
|
|
end)
|
|
|
|
local function ExtendPfQuestConfig()
|
|
for _, entry in pairs(pfQuest_defconfig) do
|
|
if entry.config == "continentPins" then
|
|
return
|
|
end
|
|
end
|
|
|
|
table.insert(pfQuest_defconfig, { text = "Continent Map", type = "header", page = "map" })
|
|
table.insert(pfQuest_defconfig, { text = "Display Continent Pins", default = "1", type = "checkbox", config = "continentPins" })
|
|
table.insert(pfQuest_defconfig, { text = "Require Ctrl+Click for Continent Pin Interaction", default = "1", type = "checkbox", config = "continentClickThrough" })
|
|
table.insert(pfQuest_defconfig, { text = "Continent Node Size", default = "12", type = "text", config = "continentNodeSize" })
|
|
table.insert(pfQuest_defconfig, { text = "Continent Utility Node Size", default = "14", type = "text", config = "continentUtilityNodeSize" })
|
|
|
|
table.insert(pfQuest_defconfig, { text = "Quest Filters", type = "header", page = "map" })
|
|
table.insert(pfQuest_defconfig, { text = "Hide Chicken Quests (CLUCK!)", default = "1", type = "checkbox", config = "hideChickenQuests", tooltip = "CLUCK! is marked repeatable. To show it, also enable Display Repeatable Quests. Level and quest requirements still apply." })
|
|
table.insert(pfQuest_defconfig, { text = "Hide Felwood Corrupted Flowers", default = "1", type = "checkbox", config = "hideFelwoodFlowers", tooltip = "Unhiding this category does not override other quest filters. Enable Display Repeatable Quests to see its repeatable quests; level and quest requirements still apply." })
|
|
table.insert(pfQuest_defconfig, { text = "Hide PvP/Battleground Quests", default = "1", type = "checkbox", config = "hidePvPQuests", tooltip = "Unhiding this category does not override other quest filters. Enable Display Repeatable Quests to see its repeatable quests; level and quest requirements still apply." })
|
|
table.insert(pfQuest_defconfig, { text = "Hide Cloth Donation Quests", default = "0", type = "checkbox", config = "hideDonationQuests", tooltip = "Unhiding this category does not override other quest filters. Enable Display Repeatable Quests to see its repeatable quests; level and quest requirements still apply." })
|
|
|
|
pfQuest_config["continentPins"] = pfQuest_config["continentPins"] or "1"
|
|
-- Existing installs defaulted to direct pin clicks. Migrate once so dense
|
|
-- island pins do not block ordinary map navigation after this update.
|
|
if pfQuest_config["continentClickThroughMigration"] ~= "1" then
|
|
pfQuest_config["continentClickThrough"] = "1"
|
|
pfQuest_config["continentClickThroughMigration"] = "1"
|
|
end
|
|
pfQuest_config["continentClickThrough"] = pfQuest_config["continentClickThrough"] or "1"
|
|
pfQuest_config["continentNodeSize"] = pfQuest_config["continentNodeSize"] or "12"
|
|
pfQuest_config["continentUtilityNodeSize"] = pfQuest_config["continentUtilityNodeSize"] or "14"
|
|
pfQuest_config["hideChickenQuests"] = pfQuest_config["hideChickenQuests"] or "1"
|
|
pfQuest_config["hideFelwoodFlowers"] = pfQuest_config["hideFelwoodFlowers"] or "1"
|
|
pfQuest_config["hidePvPQuests"] = pfQuest_config["hidePvPQuests"] or "1"
|
|
pfQuest_config["hideDonationQuests"] = pfQuest_config["hideDonationQuests"] or "0"
|
|
end
|
|
|
|
local f = CreateFrame("Frame")
|
|
f:RegisterEvent("VARIABLES_LOADED")
|
|
f:SetScript("OnEvent", function()
|
|
ExtendPfQuestConfig()
|
|
end)
|
|
if pfQuest_defconfig and pfQuest_config then ExtendPfQuestConfig() end
|
|
|
|
-- On enhanced clients begin warming while the player is in the world, before
|
|
-- they open a continent map. The small delay lets pfQuest finish populating
|
|
-- its node tables after login.
|
|
local startupWarmFrame = CreateFrame("Frame")
|
|
startupWarmFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
startupWarmFrame:SetScript("OnEvent", function()
|
|
this.warmAt = GetTime() + 3
|
|
end)
|
|
startupWarmFrame:SetScript("OnUpdate", function()
|
|
if not this.warmAt or this.warmAt > GetTime() then return end
|
|
this.warmAt = nil
|
|
if pfQuest_config["hideunexplored"] == "1" and not QueueExplorationWarmup(0, "startup") then
|
|
this.warmAt = GetTime() + 2
|
|
end
|
|
end)
|