mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-19 20:00:49 +00:00
707 lines
32 KiB
Lua
707 lines
32 KiB
Lua
-- Questie-Octo lightweight Quests browser.
|
|
--
|
|
-- Quest data/search truth comes from the bundled pfQuest database. The browser
|
|
-- intentionally avoids AceGUI TreeGroup: on Vanilla/Turtle, rebuilding a large
|
|
-- AceGUI tree is expensive enough to freeze the client. This UI recycles a
|
|
-- fixed set of native rows and scans the already-cached pfQuest title index directly.
|
|
|
|
QuestieOcto.QuestResearch = QuestieOcto.QuestResearch or {}
|
|
local R=QuestieOcto.QuestResearch
|
|
|
|
R.query=R.query or ""
|
|
R.statusFilter=R.statusFilter or "all"
|
|
R.selectedQuestID=R.selectedQuestID
|
|
R.matches=R.matches or {}
|
|
R.maxResults=1000
|
|
R.pageSize=100
|
|
R.page=R.page or 1
|
|
R.resultCount=R.resultCount or 0
|
|
R.searching=R.searching or false
|
|
R.searchGeneration=R.searchGeneration or 0
|
|
R.rowCount=18
|
|
R.rowHeight=19
|
|
|
|
local function FormatQuestText(text)
|
|
if not text or text=="" then return nil end
|
|
local player=(UnitName and UnitName("player")) or "adventurer"
|
|
local race=(UnitRace and UnitRace("player")) or ""
|
|
local class=(UnitClass and UnitClass("player")) or ""
|
|
text=string.gsub(text,"%$N",player); text=string.gsub(text,"%$n",player)
|
|
text=string.gsub(text,"%$R",race); text=string.gsub(text,"%$r",race)
|
|
text=string.gsub(text,"%$C",class); text=string.gsub(text,"%$c",class)
|
|
text=string.gsub(text,"%$B","\n"); text=string.gsub(text,"%$b","\n")
|
|
return text
|
|
end
|
|
|
|
local function QuestFlags(questID)
|
|
local completed=QuestieOcto.Completion and QuestieOcto.Completion.history and QuestieOcto.Completion.history[questID] and true or false
|
|
local active=QuestieOcto.QuestLog and QuestieOcto.QuestLog.active and QuestieOcto.QuestLog.active[questID] and true or false
|
|
local available=QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.available and QuestieOcto.AvailableQuests.available[questID] and true or false
|
|
return completed,active,available
|
|
end
|
|
|
|
local function QuestStatus(questID)
|
|
local completed,active,available=QuestFlags(questID)
|
|
if active then return "active" end
|
|
if available then return "available" end
|
|
if completed then return "completed" end
|
|
return "unavailable"
|
|
end
|
|
|
|
local function StatusMatches(id,wanted)
|
|
local completed,active,available=QuestFlags(id)
|
|
if wanted=="all" then return true end
|
|
if wanted=="completed" then return completed end
|
|
if wanted=="active" then return active end
|
|
if wanted=="available" then return available end
|
|
return false
|
|
end
|
|
|
|
local function StatusColor(status)
|
|
if status=="completed" then return "|cff35cc59" end
|
|
if status=="active" then return "|cffffff00" end
|
|
if status=="available" then return "|cff00ff00" end
|
|
return "|cff808080"
|
|
end
|
|
|
|
local function StatusLabel(status)
|
|
if status=="completed" then return "Completed" end
|
|
if status=="active" then return "Active" end
|
|
if status=="available" then return "Available" end
|
|
return "Unavailable"
|
|
end
|
|
|
|
local function AddDisplayNames(result,ids,getName)
|
|
if not ids then return end
|
|
for _,id in pairs(ids) do
|
|
local name=getName(id)
|
|
if name and name~="" then table.insert(result,name) end
|
|
end
|
|
end
|
|
|
|
local function JoinDisplayNames(ids,getName)
|
|
local result={}
|
|
AddDisplayNames(result,ids,getName)
|
|
if table.getn(result)==0 then return nil end
|
|
table.sort(result)
|
|
return table.concat(result,", ")
|
|
end
|
|
|
|
local function ObjectiveName(row)
|
|
if not row then return nil end
|
|
if row.kind=="creature" then return QuestieOcto.DatabaseAPI:GetCreatureName(row.id) end
|
|
if row.kind=="gameObject" then return QuestieOcto.DatabaseAPI:GetObjectName(row.id) end
|
|
if row.kind=="item" then return QuestieOcto.DatabaseAPI:GetItemName(row.id) end
|
|
return nil
|
|
end
|
|
|
|
local function ObjectiveText(row)
|
|
local name=ObjectiveName(row)
|
|
if not name or name=="" then return nil end
|
|
if row.kind=="creature" then return "Defeat "..name end
|
|
if row.kind=="gameObject" then return "Interact with "..name end
|
|
if row.kind=="item" then return "Collect "..name end
|
|
return name
|
|
end
|
|
|
|
local function FinalizeMatches(self,matches,total,generation)
|
|
if generation~=self.searchGeneration then return end
|
|
table.sort(matches,function(a,b)
|
|
local an=string.lower(a.title or "")
|
|
local bn=string.lower(b.title or "")
|
|
if an==bn then return a.id<b.id end
|
|
return an<bn
|
|
end)
|
|
self.matches=matches
|
|
self.resultCount=total or table.getn(matches)
|
|
self.searching=false
|
|
local found=false
|
|
if self.selectedQuestID then
|
|
for i=1,table.getn(matches) do if matches[i].id==self.selectedQuestID then found=true break end end
|
|
end
|
|
if not found then self.selectedQuestID=matches[1] and matches[1].id or nil end
|
|
self:RefreshWindow(false)
|
|
end
|
|
|
|
function R:RebuildResults()
|
|
-- The quest browser is an explicit user lookup tool. pfQuest's proven
|
|
-- browser model performs a bounded direct database search; chaining a search
|
|
-- through Questie-Octo's shared frame scheduler could leave the UI stuck on
|
|
-- "Searching..." if unrelated scheduled work was interrupted. The database
|
|
-- title index is already built before DATABASE_API_READY, so scan it directly
|
|
-- and stop after the visible result cap.
|
|
self.searchGeneration=(self.searchGeneration or 0)+1
|
|
local generation=self.searchGeneration
|
|
self.matches={}
|
|
self.resultCount=0
|
|
self.searchTruncated=false
|
|
self.searching=false
|
|
self.page=1
|
|
|
|
if not QuestieOcto.DatabaseAPI or not QuestieOcto.DatabaseAPI:IsReady() then self:RefreshWindow(false); return end
|
|
if not QuestieOcto.Completion or not QuestieOcto.Completion.ready then self:RefreshWindow(false); return end
|
|
|
|
local query=string.lower(self.query or "")
|
|
local wanted=self.statusFilter or "all"
|
|
|
|
if query=="" and wanted=="all" then
|
|
self.selectedQuestID=nil
|
|
self:RefreshWindow(false)
|
|
return
|
|
end
|
|
|
|
-- Exact numeric IDs bypass all title searching.
|
|
local exactID=(query~="" and tonumber(query)) or nil
|
|
if exactID and exactID==math.floor(exactID) and tostring(exactID)==query then
|
|
local title=QuestieOcto.DatabaseAPI:GetQuestTitle(exactID)
|
|
local raw=QuestieOcto.DatabaseAPI:GetQuestRaw(exactID)
|
|
local matches={}
|
|
if raw and title and StatusMatches(exactID,wanted) then
|
|
table.insert(matches,{id=exactID,title=title,status=QuestStatus(exactID)})
|
|
end
|
|
FinalizeMatches(self,matches,table.getn(matches),generation)
|
|
return
|
|
end
|
|
|
|
local matches={}
|
|
local total=0
|
|
|
|
local function AddMatch(id)
|
|
if not id or not StatusMatches(id,wanted) then return false end
|
|
total=total+1
|
|
if table.getn(matches)<R.maxResults then
|
|
local title=QuestieOcto.DatabaseAPI:GetQuestTitle(id) or ("Quest "..tostring(id))
|
|
table.insert(matches,{id=id,title=title,status=QuestStatus(id)})
|
|
end
|
|
if table.getn(matches)>=R.maxResults then
|
|
R.searchTruncated=true
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
if query=="" then
|
|
local source={}
|
|
if wanted=="available" then source=(QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.available) or {}
|
|
elseif wanted=="active" then source=(QuestieOcto.QuestLog and QuestieOcto.QuestLog.active) or {}
|
|
elseif wanted=="completed" then source=(QuestieOcto.Completion and QuestieOcto.Completion.history) or {}
|
|
end
|
|
for id in pairs(source) do
|
|
if AddMatch(tonumber(id)) then break end
|
|
end
|
|
else
|
|
local searchIndex=QuestieOcto.DatabaseAPI.GetQuestSearchIndex and QuestieOcto.DatabaseAPI:GetQuestSearchIndex() or {}
|
|
local ids=QuestieOcto.DatabaseAPI:GetQuestIDs()
|
|
local numericQuery=tonumber(query)
|
|
for i=1,table.getn(ids) do
|
|
local id=tonumber(ids[i])
|
|
if id then
|
|
local searchTitle=searchIndex[id] or QuestieOcto.DatabaseAPI:GetQuestSearchTitle(id) or ""
|
|
local matched=string.find(searchTitle,query,1,true) and true or false
|
|
if (not matched) and numericQuery then
|
|
matched=string.find(tostring(id),query,1,true) and true or false
|
|
end
|
|
if matched and AddMatch(id) then break end
|
|
end
|
|
end
|
|
end
|
|
|
|
FinalizeMatches(self,matches,R.searchTruncated and (R.maxResults+1) or total,generation)
|
|
end
|
|
|
|
function R:SetQuery(value)
|
|
self.query=value or ""
|
|
self:RebuildResults()
|
|
end
|
|
|
|
function R:SetStatusFilter(value)
|
|
self.statusFilter=value or "all"
|
|
self:RebuildResults()
|
|
end
|
|
|
|
function R:GetStatusText()
|
|
if not QuestieOcto.DatabaseAPI or not QuestieOcto.DatabaseAPI:IsReady() then return "Quest database is still loading." end
|
|
if not QuestieOcto.Completion or not QuestieOcto.Completion.ready then return "Completed-quest history is still loading." end
|
|
if self.searching then return "Searching quest database..." end
|
|
if (self.query or "")=="" and (self.statusFilter or "all")=="all" then return "Type a quest name/ID, or choose Available / Active / Completed." end
|
|
if self.searchTruncated or self.resultCount>self.maxResults then return "Matching quests: "..self.maxResults.."+ (showing first "..self.maxResults..")." end
|
|
return "Matching quests: "..self.resultCount.."."
|
|
end
|
|
|
|
-- Zone(s) the quest belongs to, derived from its quest giver's coordinates and
|
|
-- falling back to any resolved map for giver-less (item-start) quests. 1.12 has
|
|
-- no native zone hyperlink, so the names are shown as plain "[Zone]" text.
|
|
local function QuestZoneNames(q,id)
|
|
local mapSet={}
|
|
local function addCoords(coords)
|
|
for _,c in pairs(coords or {}) do
|
|
if type(c)=="table" and tonumber(c[3]) then mapSet[tonumber(c[3])]=true end
|
|
end
|
|
end
|
|
if q and q.starts then
|
|
for _,cid in pairs(q.starts.creature or {}) do addCoords(QuestieOcto.DatabaseAPI:GetCreatureCoords(cid)) end
|
|
for _,oid in pairs(q.starts.gameObject or {}) do addCoords(QuestieOcto.DatabaseAPI:GetObjectCoords(oid)) end
|
|
end
|
|
if not next(mapSet) then
|
|
local qm=QuestieOcto.Nodes and QuestieOcto.Nodes.questMaps and QuestieOcto.Nodes.questMaps[tonumber(id)]
|
|
for mapID in pairs(qm or {}) do mapSet[tonumber(mapID)]=true end
|
|
end
|
|
|
|
local names={}
|
|
local seen={}
|
|
for mapID in pairs(mapSet) do
|
|
local name=QuestieOcto.DatabaseAPI:GetZoneName(mapID)
|
|
if name and not seen[name] then seen[name]=true; table.insert(names,name) end
|
|
end
|
|
table.sort(names)
|
|
return names
|
|
end
|
|
|
|
function R:GetQuestDetails()
|
|
local id=self.selectedQuestID
|
|
if not id then return "Select a quest to view its details." end
|
|
local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(id)
|
|
if not q then return "Quest details are unavailable for quest ID "..tostring(id).."." end
|
|
|
|
local lines={}
|
|
table.insert(lines,"|cffffd100"..tostring(q.title or ("Quest "..id)).."|r")
|
|
if q.level and q.level>0 then
|
|
local levelLine="Level "..tostring(q.level)
|
|
if q.requiredLevel and q.requiredLevel>0 then levelLine=levelLine.." |cff888888·|r Requires "..tostring(q.requiredLevel) end
|
|
table.insert(lines,levelLine)
|
|
end
|
|
local status=QuestStatus(id)
|
|
local st=StatusColor(status)..StatusLabel(status).."|r"
|
|
if q.pvp then st=st.." |cff888888·|r |cffff4040PvP|r" end
|
|
if q.repeatable then st=st.." |cff888888·|r |cff5599ffRepeatable|r" end
|
|
if q.eventID and QuestieOcto.EventAvailability and QuestieOcto.EventAvailability:IsPresentationEvent(q.eventID) then st=st.." |cff888888·|r Event" end
|
|
table.insert(lines,st)
|
|
if status=="unavailable" and QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.GetUnavailableReason then
|
|
local reason=QuestieOcto.AvailableQuests:GetUnavailableReason(id)
|
|
if reason then table.insert(lines,"\n|cffff8040Why unavailable|r\n"..reason) end
|
|
end
|
|
|
|
local givers={}
|
|
local n=JoinDisplayNames(q.starts and q.starts.creature,function(v) return QuestieOcto.DatabaseAPI:GetCreatureName(v) end)
|
|
local o=JoinDisplayNames(q.starts and q.starts.gameObject,function(v) return QuestieOcto.DatabaseAPI:GetObjectName(v) end)
|
|
local it=JoinDisplayNames(q.starts and q.starts.item,function(v) return QuestieOcto.DatabaseAPI:GetItemName(v) end)
|
|
if n then table.insert(givers,n) end; if o then table.insert(givers,o) end; if it then table.insert(givers,it) end
|
|
if table.getn(givers)>0 then table.insert(lines,"\n|cffffd100Quest Giver|r\n"..table.concat(givers,"\n")) end
|
|
|
|
local zones=QuestZoneNames(q,id)
|
|
if table.getn(zones)>0 then
|
|
local zoneText={}
|
|
for i=1,table.getn(zones) do table.insert(zoneText,"|cffd7bea5["..zones[i].."]|r") end
|
|
table.insert(lines,"\n|cffffd100Zone|r\n"..table.concat(zoneText," "))
|
|
end
|
|
|
|
local desc=FormatQuestText(q.descriptionText)
|
|
if desc then table.insert(lines,"\n|cffffd100Description|r\n"..desc) end
|
|
local obj=FormatQuestText(q.objectiveText)
|
|
if obj then
|
|
table.insert(lines,"\n|cffffd100Objectives|r\n"..obj)
|
|
elseif q.objectiveData and table.getn(q.objectiveData)>0 then
|
|
local rows={}
|
|
for i=1,table.getn(q.objectiveData) do local text=ObjectiveText(q.objectiveData[i]); if text then table.insert(rows,"• "..text) end end
|
|
if table.getn(rows)>0 then table.insert(lines,"\n|cffffd100Objectives|r\n"..table.concat(rows,"\n")) end
|
|
end
|
|
|
|
local turnins={}
|
|
n=JoinDisplayNames(q.finishes and q.finishes.creature,function(v) return QuestieOcto.DatabaseAPI:GetCreatureName(v) end)
|
|
o=JoinDisplayNames(q.finishes and q.finishes.gameObject,function(v) return QuestieOcto.DatabaseAPI:GetObjectName(v) end)
|
|
if n then table.insert(turnins,n) end; if o then table.insert(turnins,o) end
|
|
if table.getn(turnins)>0 then table.insert(lines,"\n|cffffd100Turn In|r\n"..table.concat(turnins,"\n")) end
|
|
return table.concat(lines,"\n")
|
|
end
|
|
|
|
local ITEM_SUFFIX=":0:0:0"
|
|
|
|
local function FormatMoney(copper)
|
|
copper=math.max(0,tonumber(copper) or 0)
|
|
local gold=math.floor(copper/10000)
|
|
local silver=math.floor(math.mod(copper,10000)/100)
|
|
local coin=math.mod(copper,100)
|
|
local out={}
|
|
if gold>0 then table.insert(out,tostring(gold).."g") end
|
|
if silver>0 or gold>0 then table.insert(out,tostring(silver).."s") end
|
|
table.insert(out,tostring(coin).."c")
|
|
return table.concat(out," ")
|
|
end
|
|
|
|
local function RewardLinkText(button)
|
|
local color=button.itemColor or "|cffffffff"
|
|
local name=button.itemName or ("Item "..tostring(button.itemID or "?"))
|
|
local prefix=(tonumber(button.itemCount) or 1)>1 and (tostring(button.itemCount).."x ") or ""
|
|
return prefix..color.."|Hitem:"..tostring(button.itemID)..ITEM_SUFFIX.."|h["..name.."]|h|r"
|
|
end
|
|
|
|
local function UpdateRewardButtonText(button)
|
|
if button and button.text then button.text:SetText(RewardLinkText(button)) end
|
|
end
|
|
|
|
local function RewardButtonUpdate()
|
|
this.refreshCount=(this.refreshCount or 0)+1
|
|
if not this.itemColor and this.itemID then
|
|
GameTooltip:SetHyperlink("item:"..tostring(this.itemID)..ITEM_SUFFIX)
|
|
GameTooltip:Hide()
|
|
local name,_,quality=GetItemInfo(this.itemID)
|
|
if name and name~="" then this.itemName=name end
|
|
if quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] then
|
|
local c=ITEM_QUALITY_COLORS[quality]
|
|
local r=math.ceil((c.r or 1)*255); local g=math.ceil((c.g or 1)*255); local b=math.ceil((c.b or 1)*255)
|
|
this.itemColor="|c"..string.format("ff%02x%02x%02x",r,g,b)
|
|
end
|
|
UpdateRewardButtonText(this)
|
|
end
|
|
if (this.refreshCount or 0)>10 or this.itemColor then this:SetScript("OnUpdate",nil) end
|
|
end
|
|
|
|
local function ConfigureRewardButton(button,itemID,count)
|
|
button.itemID=tonumber(itemID)
|
|
button.itemCount=tonumber(count) or 1
|
|
button.itemName=QuestieOcto.DatabaseAPI:GetItemName(button.itemID)
|
|
button.itemColor=nil
|
|
button.refreshCount=0
|
|
UpdateRewardButtonText(button)
|
|
button:SetScript("OnUpdate",RewardButtonUpdate)
|
|
button:Show()
|
|
end
|
|
|
|
function R:RefreshDetails()
|
|
if not self.detailText then return end
|
|
self.detailText:SetText(self:GetQuestDetails())
|
|
|
|
for i=1,table.getn(self.rewardRows or {}) do
|
|
local row=self.rewardRows[i]
|
|
row:Hide(); row:SetScript("OnUpdate",nil)
|
|
end
|
|
if self.rewardHeading then self.rewardHeading:Hide() end
|
|
if self.rewardMoney then self.rewardMoney:Hide() end
|
|
if self.choiceHeading then self.choiceHeading:Hide() end
|
|
if self.rewardPanel then self.rewardPanel:Hide() end
|
|
|
|
local id=self.selectedQuestID
|
|
if self.detailScroll and self.lastDetailQuestID~=id then
|
|
self.detailScroll:SetVerticalScroll(0)
|
|
self.lastDetailQuestID=id
|
|
end
|
|
|
|
local rewardHeight=0
|
|
local rewards=id and QuestieOcto.DatabaseAPI.GetQuestRewards and QuestieOcto.DatabaseAPI:GetQuestRewards(id) or nil
|
|
if rewards and self.rewardPanel then
|
|
local money=math.max(0,tonumber(rewards.money) or 0)
|
|
local maxBonus=math.max(0,tonumber(rewards.maxLevelMoney) or 0)
|
|
local playerLevel=UnitLevel("player") or 1
|
|
local maxLevel=tonumber(MAX_PLAYER_LEVEL) or 60
|
|
if playerLevel>=maxLevel then money=money+maxBonus end
|
|
local hasItems=rewards.items and table.getn(rewards.items)>0
|
|
local hasChoices=rewards.choices and table.getn(rewards.choices)>0
|
|
|
|
if money>0 or hasItems or hasChoices then
|
|
local y=-2
|
|
self.rewardHeading:ClearAllPoints(); self.rewardHeading:SetPoint("TOPLEFT",self.rewardPanel,"TOPLEFT",0,y); self.rewardHeading:Show(); y=y-20
|
|
if money>0 then
|
|
self.rewardMoney:SetText("Money: "..FormatMoney(money)); self.rewardMoney:ClearAllPoints(); self.rewardMoney:SetPoint("TOPLEFT",self.rewardPanel,"TOPLEFT",0,y); self.rewardMoney:Show(); y=y-18
|
|
end
|
|
local rowIndex=1
|
|
for i=1,table.getn(rewards.items or {}) do
|
|
local entry=rewards.items[i]
|
|
local row=self.rewardRows[rowIndex]
|
|
if row and entry then
|
|
row:ClearAllPoints(); row:SetPoint("TOPLEFT",self.rewardPanel,"TOPLEFT",0,y); ConfigureRewardButton(row,entry[1],entry[2]); y=y-18; rowIndex=rowIndex+1
|
|
end
|
|
end
|
|
if hasChoices then
|
|
self.choiceHeading:ClearAllPoints(); self.choiceHeading:SetPoint("TOPLEFT",self.rewardPanel,"TOPLEFT",0,y-2); self.choiceHeading:Show(); y=y-22
|
|
for i=1,table.getn(rewards.choices or {}) do
|
|
local entry=rewards.choices[i]
|
|
local row=self.rewardRows[rowIndex]
|
|
if row and entry then
|
|
row:ClearAllPoints(); row:SetPoint("TOPLEFT",self.rewardPanel,"TOPLEFT",0,y); ConfigureRewardButton(row,entry[1],entry[2]); y=y-18; rowIndex=rowIndex+1
|
|
end
|
|
end
|
|
end
|
|
rewardHeight=math.max(28,-y+4)
|
|
self.rewardPanel:SetHeight(rewardHeight)
|
|
self.rewardPanel:Show()
|
|
end
|
|
end
|
|
|
|
if self.detailScroll and self.detailBG then
|
|
self.detailScroll:ClearAllPoints()
|
|
self.detailScroll:SetPoint("TOPLEFT",self.detailBG,"TOPLEFT",8,-8)
|
|
self.detailScroll:SetPoint("BOTTOMRIGHT",self.detailBG,"BOTTOMRIGHT",-27,8)
|
|
end
|
|
|
|
if self.detailContent then
|
|
-- Vanilla/Turtle's FontString API does not reliably expose GetStringHeight().
|
|
-- GetHeight() is the compatible Region measurement and is updated after SetText.
|
|
-- Anchor Rewards directly below the quest text so moving them into this shared
|
|
-- scroll child cannot leave the panel unpositioned if text measurement differs.
|
|
local baseHeight=self.detailText:GetHeight() or 0
|
|
if self.rewardPanel and rewardHeight>0 then
|
|
self.rewardPanel:ClearAllPoints()
|
|
self.rewardPanel:SetPoint("TOPLEFT",self.detailText,"BOTTOMLEFT",0,-14)
|
|
self.rewardPanel:Show()
|
|
end
|
|
local totalHeight=baseHeight+20
|
|
if rewardHeight>0 then totalHeight=baseHeight+rewardHeight+34 end
|
|
-- Keep the scroll child at least as tall as the visible pane, while allowing
|
|
-- long quest text plus rewards to extend it naturally for scrolling.
|
|
self.detailContent:SetHeight(math.max(470,totalHeight))
|
|
if self.detailScroll and self.detailScroll.UpdateScrollChildRect then self.detailScroll:UpdateScrollChildRect() end
|
|
end
|
|
end
|
|
|
|
function R:UpdatePageControls(maxPages)
|
|
local page=self.page or 1
|
|
if self.pageText then self.pageText:SetText("Page "..tostring(page).." / "..tostring(maxPages)) end
|
|
if self.prevButton then if page<=1 then self.prevButton:Disable() else self.prevButton:Enable() end end
|
|
if self.nextButton then if page>=maxPages then self.nextButton:Disable() else self.nextButton:Enable() end end
|
|
end
|
|
|
|
function R:ChangePage(delta)
|
|
local total=table.getn(self.matches or {})
|
|
local pageSize=self.pageSize or 100
|
|
local maxPages=math.max(1,math.ceil(total/pageSize))
|
|
local newPage=(self.page or 1)+(tonumber(delta) or 0)
|
|
if newPage<1 then newPage=1 end
|
|
if newPage>maxPages then newPage=maxPages end
|
|
if newPage~=(self.page or 1) then
|
|
self.page=newPage
|
|
-- Start each new page at the top of the list. Set the stored offset directly
|
|
-- so RefreshRows reads 0 even if FauxScrollFrame_SetOffset is unavailable.
|
|
if self.scroll then
|
|
self.scroll.offset=0
|
|
if FauxScrollFrame_SetOffset then FauxScrollFrame_SetOffset(self.scroll,0) end
|
|
if self.scroll.SetVerticalScroll then self.scroll:SetVerticalScroll(0) end
|
|
end
|
|
self:RefreshRows()
|
|
end
|
|
end
|
|
|
|
function R:RefreshRows()
|
|
if not self.frame or not self.frame:IsShown() then return end
|
|
if self.statusText then self.statusText:SetText(self:GetStatusText()) end
|
|
|
|
local total=table.getn(self.matches)
|
|
local pageSize=self.pageSize or 100
|
|
local maxPages=math.max(1,math.ceil(total/pageSize))
|
|
if (self.page or 1)>maxPages then self.page=maxPages end
|
|
if (self.page or 1)<1 then self.page=1 end
|
|
|
|
local pageStart=((self.page or 1)-1)*pageSize
|
|
local pageCount=total-pageStart
|
|
if pageCount<0 then pageCount=0 end
|
|
if pageCount>pageSize then pageCount=pageSize end
|
|
|
|
local offset=0
|
|
if self.scroll and FauxScrollFrame_GetOffset then offset=FauxScrollFrame_GetOffset(self.scroll) or 0 end
|
|
if self.scroll and FauxScrollFrame_Update then FauxScrollFrame_Update(self.scroll,pageCount,self.rowCount,self.rowHeight) end
|
|
|
|
for i=1,self.rowCount do
|
|
local row=self.rows[i]
|
|
local within=offset+i
|
|
local data=(within<=pageCount) and self.matches[pageStart+within] or nil
|
|
if data then
|
|
row.questID=data.id
|
|
row.text:SetText(StatusColor(data.status).."["..StatusLabel(data.status).."]|r "..data.title.." |cff777777["..data.id.."]|r")
|
|
if data.id==self.selectedQuestID then row.highlight:Show() else row.highlight:Hide() end
|
|
row:Show()
|
|
else
|
|
row.questID=nil
|
|
row:Hide()
|
|
end
|
|
end
|
|
self:UpdatePageControls(maxPages)
|
|
self:RefreshDetails()
|
|
end
|
|
|
|
function R:RefreshWindow(rebuild)
|
|
if rebuild then self:RebuildResults(); return end
|
|
self:RefreshRows()
|
|
end
|
|
|
|
local FILTER_BUTTON_WIDTH=84
|
|
|
|
-- Filter buttons share one width and sit next to each other so the row reads as
|
|
-- a connected toolbar instead of scattered auto-sized buttons.
|
|
local function MakeFilterButton(parent,label,value,anchor)
|
|
local b=CreateFrame("Button",nil,parent,"UIPanelButtonTemplate")
|
|
b:SetWidth(FILTER_BUTTON_WIDTH); b:SetHeight(22)
|
|
if anchor then
|
|
b:SetPoint("LEFT",anchor,"RIGHT",2,0)
|
|
else
|
|
b:SetPoint("TOPLEFT",parent,"TOPLEFT",24,-64)
|
|
end
|
|
b:SetText(label)
|
|
b.value=value
|
|
b:SetScript("OnClick",function() R:SetStatusFilter(this.value) end)
|
|
return b
|
|
end
|
|
|
|
function R:OpenWindowNow()
|
|
if self.frame then self.frame:Show(); self.frame:Raise(); self:RefreshRows(); return end
|
|
|
|
local f=CreateFrame("Frame","QuestieOctoQuestBrowser",UIParent)
|
|
f:SetWidth(800); f:SetHeight(590); f:SetPoint("CENTER",UIParent,"CENTER",0,20)
|
|
f:SetFrameStrata("DIALOG"); f:SetMovable(true); f:EnableMouse(true); f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart",function() this:StartMoving() end); f:SetScript("OnDragStop",function() this:StopMovingOrSizing() end)
|
|
f:SetBackdrop({bgFile="Interface\\DialogFrame\\UI-DialogBox-Background",edgeFile="Interface\\DialogFrame\\UI-DialogBox-Border",tile=true,tileSize=32,edgeSize=32,insets={left=11,right=12,top=12,bottom=11}})
|
|
f:SetBackdropColor(0.04,0.04,0.04,0.97)
|
|
self.frame=f
|
|
if UISpecialFrames then
|
|
local registered=false
|
|
for i=1,table.getn(UISpecialFrames) do if UISpecialFrames[i]=="QuestieOctoQuestBrowser" then registered=true break end end
|
|
if not registered then table.insert(UISpecialFrames,"QuestieOctoQuestBrowser") end
|
|
end
|
|
|
|
local title=f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
|
|
title:SetPoint("TOP",f,"TOP",0,-18); title:SetText("Quest Database")
|
|
local close=CreateFrame("Button",nil,f,"UIPanelCloseButton"); close:SetPoint("TOPRIGHT",f,"TOPRIGHT",-5,-5); close:SetScript("OnClick",function() R:CloseWindow() end)
|
|
|
|
local search=CreateFrame("EditBox",nil,f,"InputBoxTemplate")
|
|
search:SetWidth(285); search:SetHeight(20); search:SetPoint("TOPLEFT",f,"TOPLEFT",24,-40); search:SetAutoFocus(false); search:SetText(self.query or "")
|
|
search:SetScript("OnEnterPressed",function() this:ClearFocus(); R:SetQuery(this:GetText() or "") end)
|
|
self.searchBox=search
|
|
local searchButton=CreateFrame("Button",nil,f,"UIPanelButtonTemplate")
|
|
searchButton:SetWidth(70); searchButton:SetHeight(22); searchButton:SetPoint("LEFT",search,"RIGHT",8,0); searchButton:SetText("Search")
|
|
if QuestieOcto.GameMenu and QuestieOcto.GameMenu.SetButtonWidth then QuestieOcto.GameMenu:SetButtonWidth(searchButton,24) end
|
|
searchButton:SetScript("OnClick",function() R:SetQuery(R.searchBox:GetText() or "") end)
|
|
|
|
local allBtn=MakeFilterButton(f,"All","all")
|
|
local availBtn=MakeFilterButton(f,"Available","available",allBtn)
|
|
local activeBtn=MakeFilterButton(f,"Active","active",availBtn)
|
|
local compBtn=MakeFilterButton(f,"Completed","completed",activeBtn)
|
|
self.filterButtons={allBtn,availBtn,activeBtn,compBtn}
|
|
|
|
local status=f:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall")
|
|
status:SetPoint("TOPLEFT",f,"TOPLEFT",24,-94); status:SetWidth(355); status:SetJustifyH("LEFT")
|
|
self.statusText=status
|
|
|
|
local listBG=CreateFrame("Frame",nil,f); listBG:SetPoint("TOPLEFT",f,"TOPLEFT",20,-114); listBG:SetWidth(360); listBG:SetHeight(420)
|
|
listBG:SetBackdrop({bgFile="Interface\\Tooltips\\UI-Tooltip-Background",edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",tile=true,tileSize=16,edgeSize=12,insets={left=3,right=3,top=3,bottom=3}}); listBG:SetBackdropColor(0,0,0,0.72)
|
|
|
|
self.rows={}
|
|
for i=1,self.rowCount do
|
|
local row=CreateFrame("Button",nil,listBG)
|
|
row:SetHeight(self.rowHeight); row:SetWidth(330); row:SetPoint("TOPLEFT",listBG,"TOPLEFT",8,-8-(i-1)*self.rowHeight)
|
|
local hl=row:CreateTexture(nil,"BACKGROUND"); hl:SetAllPoints(row); hl:SetTexture(0.25,0.25,0.25,0.65); hl:Hide(); row.highlight=hl
|
|
local text=row:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); text:SetPoint("LEFT",row,"LEFT",2,0); text:SetWidth(326); text:SetJustifyH("LEFT"); row.text=text
|
|
row:SetScript("OnClick",function() if this.questID then R.selectedQuestID=this.questID; R:RefreshRows() end end)
|
|
self.rows[i]=row
|
|
end
|
|
local scroll=CreateFrame("ScrollFrame","QuestieOctoQuestBrowserScroll",listBG,"FauxScrollFrameTemplate")
|
|
scroll:SetPoint("TOPLEFT",listBG,"TOPLEFT",0,-6); scroll:SetPoint("BOTTOMRIGHT",listBG,"BOTTOMRIGHT",-24,6)
|
|
scroll:SetScript("OnVerticalScroll",function() FauxScrollFrame_OnVerticalScroll(R.rowHeight,function() R:RefreshRows() end) end)
|
|
self.scroll=scroll
|
|
|
|
-- Page navigation below the list: < Prev Page N / M Next >
|
|
local prevButton=CreateFrame("Button",nil,f)
|
|
prevButton:SetWidth(32); prevButton:SetHeight(32)
|
|
prevButton:SetPoint("TOPLEFT",listBG,"BOTTOMLEFT",4,-2)
|
|
prevButton:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
|
|
prevButton:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down")
|
|
prevButton:SetDisabledTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Disabled")
|
|
prevButton:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight","ADD")
|
|
prevButton:SetScript("OnClick",function() R:ChangePage(-1) end)
|
|
self.prevButton=prevButton
|
|
|
|
local pageText=f:CreateFontString(nil,"OVERLAY","GameFontNormal")
|
|
pageText:SetPoint("CENTER",listBG,"BOTTOM",0,-18); pageText:SetText("Page 1 / 1")
|
|
self.pageText=pageText
|
|
|
|
local nextButton=CreateFrame("Button",nil,f)
|
|
nextButton:SetWidth(32); nextButton:SetHeight(32)
|
|
nextButton:SetPoint("TOPRIGHT",listBG,"BOTTOMRIGHT",-4,-2)
|
|
nextButton:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
|
|
nextButton:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down")
|
|
nextButton:SetDisabledTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Disabled")
|
|
nextButton:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight","ADD")
|
|
nextButton:SetScript("OnClick",function() R:ChangePage(1) end)
|
|
self.nextButton=nextButton
|
|
|
|
local detailBG=CreateFrame("Frame",nil,f); detailBG:SetPoint("TOPLEFT",f,"TOPLEFT",395,-40); detailBG:SetWidth(380); detailBG:SetHeight(494)
|
|
detailBG:SetBackdrop({bgFile="Interface\\Tooltips\\UI-Tooltip-Background",edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",tile=true,tileSize=16,edgeSize=12,insets={left=3,right=3,top=3,bottom=3}}); detailBG:SetBackdropColor(0,0,0,0.72)
|
|
self.detailBG=detailBG
|
|
local detailScroll=CreateFrame("ScrollFrame","QuestieOctoQuestBrowserDetailScroll",detailBG,"UIPanelScrollFrameTemplate")
|
|
detailScroll:SetPoint("TOPLEFT",detailBG,"TOPLEFT",8,-8); detailScroll:SetPoint("BOTTOMRIGHT",detailBG,"BOTTOMRIGHT",-27,8)
|
|
local detailContent=CreateFrame("Frame",nil,detailScroll); detailContent:SetWidth(337); detailContent:SetHeight(470); detailScroll:SetScrollChild(detailContent)
|
|
self.detailScroll=detailScroll; self.detailContent=detailContent
|
|
local detail=detailContent:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); detail:SetPoint("TOPLEFT",detailContent,"TOPLEFT",0,0); detail:SetWidth(337); detail:SetJustifyH("LEFT"); detail:SetJustifyV("TOP"); detail:SetText("")
|
|
self.detailText=detail
|
|
local rewardPanel=CreateFrame("Frame",nil,detailContent); rewardPanel:SetWidth(337); rewardPanel:SetHeight(1); rewardPanel:Hide(); self.rewardPanel=rewardPanel
|
|
local rewardHeading=rewardPanel:CreateFontString(nil,"OVERLAY","GameFontNormal"); rewardHeading:SetText("Rewards"); rewardHeading:SetJustifyH("LEFT"); rewardHeading:Hide(); self.rewardHeading=rewardHeading
|
|
local rewardMoney=rewardPanel:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); rewardMoney:SetJustifyH("LEFT"); rewardMoney:Hide(); self.rewardMoney=rewardMoney
|
|
local choiceHeading=rewardPanel:CreateFontString(nil,"OVERLAY","GameFontNormalSmall"); choiceHeading:SetText("Choose one:"); choiceHeading:SetJustifyH("LEFT"); choiceHeading:Hide(); self.choiceHeading=choiceHeading
|
|
self.rewardRows={}
|
|
for i=1,10 do
|
|
local row=CreateFrame("Button",nil,rewardPanel); row:SetWidth(330); row:SetHeight(18); row:Hide()
|
|
local text=row:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); text:SetPoint("LEFT",row,"LEFT",0,0); text:SetWidth(328); text:SetJustifyH("LEFT"); row.text=text
|
|
row:SetScript("OnEnter",function() if this.itemID then GameTooltip:SetOwner(this,"ANCHOR_RIGHT"); GameTooltip:SetHyperlink("item:"..tostring(this.itemID)..ITEM_SUFFIX); GameTooltip:Show() end end)
|
|
row:SetScript("OnLeave",function() GameTooltip:Hide() end)
|
|
row:SetScript("OnClick",function()
|
|
if this.itemID and SetItemRef then
|
|
local link="item:"..tostring(this.itemID)..ITEM_SUFFIX
|
|
local color=this.itemColor or "|cffffffff"
|
|
local name=this.itemName or ("Item "..tostring(this.itemID))
|
|
local text=color.."|H"..link.."|h["..name.."]|h|r"
|
|
SetItemRef(link,text,arg1)
|
|
end
|
|
end)
|
|
self.rewardRows[i]=row
|
|
end
|
|
|
|
self:RefreshRows()
|
|
end
|
|
|
|
function R:OpenWindow()
|
|
-- AceConfig refreshes the options frame after an execute callback returns.
|
|
-- Defer the entire handoff, suppress GameMenu return, then close Options.
|
|
QuestieOcto.Scheduler:After(0.03,function()
|
|
local options=QuestieOcto.Options
|
|
if options then
|
|
options.openedFromGameMenu=false
|
|
if options.Hide then options:Hide() end
|
|
if options.configFrame and options.configFrame.frame then options.configFrame.frame:Hide() end
|
|
end
|
|
if GameMenuFrame and GameMenuFrame:IsShown() then
|
|
if HideUIPanel then HideUIPanel(GameMenuFrame) else GameMenuFrame:Hide() end
|
|
end
|
|
R:OpenWindowNow()
|
|
end,"quest-browser-open")
|
|
end
|
|
|
|
function R:CloseWindow()
|
|
-- Invalidate any stale selection/search generation. Searches are direct and
|
|
-- bounded now, so there is no background database walk to cancel.
|
|
self.searchGeneration=(self.searchGeneration or 0)+1
|
|
self.searching=false
|
|
if self.frame then self.frame:Hide() end
|
|
end
|
|
|
|
|
|
--function R:GetOptionsTab()
|
|
-- return {
|
|
-- name="Quests",type="group",order=14,
|
|
-- args={
|
|
-- heading={type="header",order=1,name="Quests"},
|
|
-- description={type="description",order=2,fontSize="medium",width="full",name="Browse/search the pfQuest quest database in a dedicated lightweight window."},
|
|
-- open={type="execute",order=3,name="Open Quests",desc="Open the quest browser.",width="double",func=function() R:OpenWindow() end},
|
|
-- },
|
|
-- }
|
|
--end
|
|
|
|
function R:OnStateChanged()
|
|
if self.frame and self.frame:IsShown() then
|
|
-- Do not rebuild full searches on every quest-state signal. Existing rows
|
|
-- simply update their current status; explicit user search/filter rebuilds.
|
|
for i=1,table.getn(self.matches) do self.matches[i].status=QuestStatus(self.matches[i].id) end
|
|
self:RefreshRows()
|
|
end
|
|
end
|
|
|
|
QuestieOcto:RegisterMessage("COMPLETION_READY",R,"OnStateChanged")
|
|
QuestieOcto:RegisterMessage("DATABASE_API_READY",R,"OnStateChanged")
|
|
QuestieOcto:RegisterMessage("AVAILABLE_QUESTS_READY",R,"OnStateChanged")
|
|
QuestieOcto:RegisterMessage("QUEST_LOG_CHANGED",R,"OnStateChanged") |