Added routes and submenu

This commit is contained in:
User
2026-08-18 15:07:39 -05:00
parent bb237b8fae
commit 235645cc69
9 changed files with 528 additions and 25 deletions
+262 -13
View File
@@ -1,9 +1,11 @@
-- Quest arrow that points toward the nearest active quest objective in the
-- current zone, adapted from pfQuest's route arrow.
-- Quest guidance overlays adapted from pfQuest's route.lua: a screen arrow that
-- points at the nearest active objective, and a dotted route across the map and
-- minimap that walks the current zone's objectives in nearest-first order.
QuestieOcto.Arrow = QuestieOcto.Arrow or {}
local A = QuestieOcto.Arrow
local ARROW_TEXTURE="Interface\\AddOns\\Questie-Octo\\UI\\Icons\\arrow"
local ROUTE_TEXTURE="Interface\\AddOns\\Questie-Octo\\UI\\Icons\\route"
local atan2=atan2 or math.atan2
local function Settings()
@@ -54,32 +56,53 @@ local function IsArrowRole(role)
or role=="objectiveItemSource" or role=="turnin"
end
-- Pull the current map's prepared plan and keep only objective/turn-in slots.
-- Pull the current map's prepared plan and keep only objective/turn-in slots,
-- dropping any the quest menu has hidden and recording each node's objective
-- index so focus can filter by objective.
local function CollectTargets(mapID)
local targets={}
local plan=mapID and QuestieOcto.PreparedMap and QuestieOcto.PreparedMap:Get(mapID) or nil
if not plan then return targets end
local menu=QuestieOcto.QuestMenu
for _,desc in pairs(plan) do
local x,y,questID
local node
if desc.type=="nodeSlot" then
for _,entry in pairs(desc.entries or {}) do
local node=entry.node
if node and IsArrowRole(node.role) then
x,y,questID=tonumber(desc.x),tonumber(desc.y),node.questID
break
end
local n=entry.node
if n and IsArrowRole(n.role) then node=n; break end
end
elseif desc.type=="node" and desc.node and IsArrowRole(desc.node.role) then
x,y,questID=tonumber(desc.x),tonumber(desc.y),desc.node.questID
node=desc.node
end
if x and y then table.insert(targets,{x,y,questID}) end
if node and (not menu or not menu:IsNodeHidden(node)) then
local x,y=tonumber(desc.x),tonumber(desc.y)
if x and y then table.insert(targets,{x,y,node.questID,tonumber(node.objectiveIndex)}) end
end
end
return targets
end
-- When an objective is focused, restrict to its nodes (falling back to all when
-- the focused objective has no node in this zone).
local function FocusTargets(targets)
if not QuestieOcto.QuestMenu then return targets end
local fq,fo=QuestieOcto.QuestMenu:GetFocus()
if not fq then return targets end
local filtered={}
local i=1
local total=table.getn(targets)
while i<=total do
local t=targets[i]
if tonumber(t[3])==fq and tonumber(t[4])==fo then table.insert(filtered,t) end
i=i+1
end
if table.getn(filtered)>0 then return filtered end
return targets
end
local function NearestTarget(targets,px,py)
local best,bestDist
for _,t in pairs(targets or {}) do
@@ -90,6 +113,232 @@ local function NearestTarget(targets,px,py)
return best,bestDist
end
-- Greedy nearest-neighbor ordering starting from the objective closest to the
-- player, the same heuristic pfQuest uses to approximate the shortest route.
local function BuildRoute(targets,px,py,forceFirst)
local n=table.getn(targets)
if n==0 then return {} end
local used={}
local route={}
local cx,cy=px,py
if forceFirst then
local fi=1
while fi<=n do
if targets[fi]==forceFirst then
used[fi]=true
route[1]=forceFirst
cx,cy=forceFirst[1],forceFirst[2]
break
end
fi=fi+1
end
end
local placed=table.getn(route)
while placed<n do
local bestIdx,bestDist
local j=1
while j<=n do
if not used[j] then
local dx,dy=(cx-targets[j][1])*1.5,(cy-targets[j][2])
local d=dx*dx+dy*dy
if not bestDist or d<bestDist then bestDist=d; bestIdx=j end
end
j=j+1
end
if not bestIdx then break end
used[bestIdx]=true
route[table.getn(route)+1]=targets[bestIdx]
cx,cy=targets[bestIdx][1],targets[bestIdx][2]
placed=placed+1
end
return route
end
local routeWorldDots={}
local routeMiniDots={}
local routeWorldFrame=nil
local routeMiniFrame=nil
local function EnsureRouteFrames()
if not routeWorldFrame and WorldMapButton then
routeWorldFrame=CreateFrame("Frame","QuestieOctoRouteWorld",WorldMapButton)
routeWorldFrame:SetAllPoints(WorldMapButton)
if routeWorldFrame.SetFrameLevel and WorldMapButton.GetFrameLevel then
routeWorldFrame:SetFrameLevel(WorldMapButton:GetFrameLevel()+1)
end
end
if not routeMiniFrame and Minimap then
routeMiniFrame=CreateFrame("Frame","QuestieOctoRouteMini",Minimap)
routeMiniFrame:SetAllPoints(Minimap)
end
end
local function AcquireDot(pool,parent,index)
local tex=pool[index]
if not tex then
tex=parent:CreateTexture(nil,"OVERLAY")
tex:SetWidth(5)
tex:SetHeight(5)
tex:SetTexture(ROUTE_TEXTURE)
tex:SetVertexColor(1,.8,.2,.9)
pool[index]=tex
end
return tex
end
local function HideDotsFrom(pool,index)
local i=index
local total=table.getn(pool)
while i<=total do
if pool[i] then pool[i]:Hide() end
i=i+1
end
end
local function SegmentSteps(dx,dy)
local steps=math.floor(math.sqrt(dx*1.5*dx*1.5+dy*dy))
if steps<1 then steps=1 end
return steps
end
local function DrawWorldSegment(x1,y1,x2,y2,index)
local width=WorldMapButton:GetWidth()
local height=WorldMapButton:GetHeight()
if not width or width<=0 or not height or height<=0 then return index end
local dx,dy=x2-x1,y2-y1
local steps=SegmentSteps(dx,dy)
local i=1
while i<=steps do
local x=x1+dx*i/steps
local y=y1+dy*i/steps
local tex=AcquireDot(routeWorldDots,routeWorldFrame,index)
tex:ClearAllPoints()
tex:SetPoint("CENTER",WorldMapButton,"TOPLEFT",width*(x/100),-height*(y/100))
tex:Show()
index=index+1
i=i+1
end
return index
end
local function DrawMiniSegment(proj,x1,y1,x2,y2,index)
local dx,dy=x2-x1,y2-y1
local steps=SegmentSteps(dx,dy)
local i=1
while i<=steps do
local x=x1+dx*i/steps
local y=y1+dy*i/steps
local ox=(x-proj.px)*proj.xDraw
local oy=-(y-proj.py)*proj.yDraw
local inside
if proj.square then
inside=math.abs(ox)<(proj.width/2) and math.abs(oy)<(proj.height/2)
else
inside=(ox*ox+oy*oy)<(proj.radius*proj.radius)
end
if inside then
local tex=AcquireDot(routeMiniDots,routeMiniFrame,index)
tex:ClearAllPoints()
tex:SetPoint("CENTER",Minimap,"CENTER",ox,oy)
tex:Show()
index=index+1
end
i=i+1
end
return index
end
function A:ClearRoute()
HideDotsFrom(routeWorldDots,1)
HideDotsFrom(routeMiniDots,1)
end
function A:UpdateRoute()
if (self.routeThrottle or 0)>GetTime() then return end
self.routeThrottle=GetTime()+0.1
EnsureRouteFrames()
if not Settings():Get("showQuestRoute") then
self:ClearRoute()
return
end
local px,py
if GetPlayerMapPosition then px,py=GetPlayerMapPosition("player") end
px,py=(px or 0)*100,(py or 0)*100
if px==0 and py==0 then self:ClearRoute(); return end
local mapID=CurrentMapID()
if mapID~=self.routeMapID or (self.routeRefresh or 0)<GetTime() then
self.routeMapID=mapID
self.routeTargets=CollectTargets(mapID)
self.routeRefresh=GetTime()+1
end
local targets=self.routeTargets or {}
if table.getn(targets)==0 then self:ClearRoute(); return end
-- A focused objective becomes the route's first stop.
local forceFirst,fq,fo
if QuestieOcto.QuestMenu then fq,fo=QuestieOcto.QuestMenu:GetFocus() end
if fq then
local bestD
local i=1
local total=table.getn(targets)
while i<=total do
local t=targets[i]
if tonumber(t[3])==fq and tonumber(t[4])==fo then
local dx,dy=(px-t[1])*1.5,(py-t[2])
local d=dx*dx+dy*dy
if not bestD or d<bestD then bestD=d; forceFirst=t end
end
i=i+1
end
end
-- Only reorder when the target set, the nearest objective, or the focus changes.
local nearest=NearestTarget(targets,px,py)
local sig=tostring(table.getn(targets))..":"..(nearest and (tostring(nearest[1])..":"..tostring(nearest[2])) or "")
..":"..tostring(fq)..":"..tostring(fo)
if sig~=self.routeSig then
self.routeSig=sig
self.routeOrder=BuildRoute(targets,px,py,forceFirst)
end
local order=self.routeOrder or {}
-- World-map dots sit at fixed zone coordinates, so only redraw them when the
-- route, the displayed map, or the map size actually changes.
local displayed=QuestieOcto.Map and QuestieOcto.Map.GetDisplayedMapID and QuestieOcto.Map:GetDisplayedMapID()
local worldVisible=WorldMapButton and WorldMapButton:IsVisible() and tonumber(displayed)==tonumber(mapID)
local worldSig=worldVisible and (sig.."@"..tostring(WorldMapButton:GetWidth())) or "hidden"
if worldSig~=self.routeWorldSig then
self.routeWorldSig=worldSig
local worldIndex=1
if worldVisible then
if order[1] then worldIndex=DrawWorldSegment(px,py,order[1][1],order[1][2],worldIndex) end
local i=2
while i<=table.getn(order) do
worldIndex=DrawWorldSegment(order[i-1][1],order[i-1][2],order[i][1],order[i][2],worldIndex)
i=i+1
end
end
HideDotsFrom(routeWorldDots,worldIndex)
end
local miniIndex=1
local proj=QuestieOcto.Minimap and QuestieOcto.Minimap.GetProjection and QuestieOcto.Minimap:GetProjection()
if proj then
if order[1] then miniIndex=DrawMiniSegment(proj,px,py,order[1][1],order[1][2],miniIndex) end
local i=2
while i<=table.getn(order) do
miniIndex=DrawMiniSegment(proj,order[i-1][1],order[i-1][2],order[i][1],order[i][2],miniIndex)
i=i+1
end
end
HideDotsFrom(routeMiniDots,miniIndex)
end
function A:OnUpdate()
local frame=self.frame
if not frame then return end
@@ -114,7 +363,7 @@ function A:OnUpdate()
frame.refresh=GetTime()+1
end
local best,bestDist=NearestTarget(frame.targets,px,py)
local best,bestDist=NearestTarget(FocusTargets(frame.targets),px,py)
if not best then
if frame:IsShown() then frame:Hide() end
return
@@ -200,7 +449,7 @@ function A:Initialize()
-- A hidden frame receives no OnUpdate, so drive the logic from an always-shown
-- frame that shows/hides the arrow.
local driver=CreateFrame("Frame","QuestieOctoQuestArrowDriver",UIParent)
driver:SetScript("OnUpdate",function() A:OnUpdate() end)
driver:SetScript("OnUpdate",function() A:OnUpdate(); A:UpdateRoute() end)
self.driver=driver
end