2619 lines
107 KiB
Lua
2619 lines
107 KiB
Lua
-- Relationships Quest And Item Browser
|
|
|
|
local RelationshipsQuestAndItemBrowser = CreateFrame("Frame", "RelationshipsQuestAndItemBrowserCore")
|
|
RelationshipsQuestAndItemBrowser:RegisterEvent("PLAYER_LOGIN")
|
|
RelationshipsQuestAndItemBrowser:RegisterEvent("ADDON_LOADED")
|
|
RelationshipsQuestAndItemBrowser:RegisterEvent("PLAYER_LOGOUT")
|
|
|
|
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
|
|
|
|
local RQL_ORIGINAL_SET_ITEM_REF = nil
|
|
local RQL_ORIGINAL_CHAT_HYPERLINK_SHOW = nil
|
|
local RQL_INDEX_READY = nil
|
|
local RQL_SEARCH_RESULTS = {}
|
|
local RQL_SELECTED_QUEST_ID = nil
|
|
local RQL_MODE = "browser"
|
|
local RQL_SEARCH_TEXT = ""
|
|
local RQL_LEVEL_FILTER = 0
|
|
local RQL_TYPE_FILTER = 0
|
|
local RQL_FACTION_FILTER = 0
|
|
local RQL_SOURCE_FILTER = 0
|
|
local RQL_MAX_RESULTS = 5000
|
|
local RQL_ROWS_VISIBLE = 16
|
|
local RQL_ROW_HEIGHT = 20
|
|
local RQL_PAGE = 1
|
|
local RQL_DEFAULT_BACKGROUND = "Light Sepia"
|
|
local RQL_SUPPRESS_SETITEMREF_ROUTE = nil
|
|
local RQL_SUPPRESS_CHATFRAME_ROUTE = nil
|
|
|
|
local function RQL_GetStartupBackground()
|
|
local saved = RelationshipsQuestAndItemBrowserDB and RelationshipsQuestAndItemBrowserDB.bgColor
|
|
if saved and saved ~= "" then return saved end
|
|
return RQL_DEFAULT_BACKGROUND
|
|
end
|
|
|
|
-- Item browser state (Item tab)
|
|
local RQL_TAB = "quests" -- "quests" | "items"
|
|
local RQL_ITEM_SEARCH_TEXT = ""
|
|
local RQL_ITEM_SOURCE_FILTER = 0
|
|
local RQL_ITEM_SEARCH_RESULTS = {}
|
|
local RQL_ITEM_INDEX_READY = nil
|
|
local RQL_ITEM_PAGE = 1
|
|
local RQL_SELECTED_ITEM_ID = nil
|
|
|
|
local RQL_LEVELS = {
|
|
"All levels", "1-9", "10-19", "20-29", "30-39", "40-49", "50-59", "60+"
|
|
}
|
|
local RQL_TYPES = {
|
|
"All types", "Normal", "Elite/Group", "Dungeon/Raid", "PvP", "Class", "Profession", "Seasonal/Event"
|
|
}
|
|
local RQL_FACTIONS = { "All factions", "Alliance", "Horde", "Neutral/Both" }
|
|
local RQL_SOURCES = { "All sources", "Vanilla", "Turtle/OctoWoW" }
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Dungeon / Raid classification
|
|
-- ---------------------------------------------------------------------------
|
|
-- Keyword-based detection is far more reliable than zone-id lookups because
|
|
-- pfQuest coord data for dungeon quests is inconsistent, and Turtle/OctoWoW
|
|
-- adds new dungeons/raids that aren't in vanilla's AreaTable at all.
|
|
--
|
|
-- A quest is considered Dungeon/Raid if its title, objective text or
|
|
-- description contains any of these keywords. Keywords are matched with
|
|
-- plain (non-pattern) string.find and are lower-case.
|
|
--
|
|
-- Sources: Wowhead Classic dungeon/raid quest lists, the Turtle WoW wiki
|
|
-- (turtle-wow.fandom.com) custom dungeon/raid pages, and OctoWoW patch notes.
|
|
local RQL_DUNGEON_KEYWORDS = {
|
|
-- Vanilla dungeons
|
|
"ragefire chasm",
|
|
"wailing caverns",
|
|
"the deadmines", "deadmines",
|
|
"shadowfang keep",
|
|
"blackfathom deeps",
|
|
"the stockade", "the stockades", "stormwind stockade",
|
|
"gnomeregan",
|
|
"razorfen kraul",
|
|
"razorfen downs",
|
|
"scarlet monastery",
|
|
"uldaman",
|
|
"zul'farrak", "zulfarrak", "zul\'farrak",
|
|
"maraudon",
|
|
"sunken temple", "temple of atal'hakkar", "atal\'hakkar", "atalhakkar",
|
|
"blackrock depths",
|
|
"lower blackrock spire", "upper blackrock spire", "blackrock spire", "lbrs", "ubrs",
|
|
"dire maul",
|
|
"stratholme",
|
|
"scholomance",
|
|
-- Vanilla raids
|
|
"molten core",
|
|
"onyxia's lair", "onyxia\'s lair", "onyxias lair", "lair of onyxia",
|
|
"blackwing lair", "nefarian",
|
|
"zul'gurub", "zul\'gurub", "zulgurub",
|
|
"ruins of ahn'qiraj", "ruins of ahn\'qiraj", "aq20",
|
|
"temple of ahn'qiraj", "temple of ahn\'qiraj", "aq40", "ahn'qiraj", "ahn\'qiraj",
|
|
"naxxramas",
|
|
-- Turtle WoW / OctoWoW dungeons & raids
|
|
"emerald sanctum",
|
|
"karazhan crypt", "karazhan halls", "lower karazhan", "upper karazhan", "karazhan",
|
|
"hateforge quarry", "hateforge",
|
|
"gilneas city", "gilneas",
|
|
"stormwind vault",
|
|
"crescent grove",
|
|
"black morass",
|
|
"caverns of time",
|
|
"tower of karazhan",
|
|
"kara10", "kara40",
|
|
}
|
|
|
|
-- ---------- helpers ----------
|
|
|
|
local function RQL_TableSize(t)
|
|
local n = 0
|
|
if type(t) == "table" then for _ in pairs(t) do n = n + 1 end end
|
|
return n
|
|
end
|
|
|
|
local function RQL_GetDB(section, key)
|
|
if RelationshipsQuestAndItemBrowserData and RelationshipsQuestAndItemBrowserData[section] then
|
|
return RelationshipsQuestAndItemBrowserData[section][key]
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function RQL_PatchTable(base, diff)
|
|
if type(diff) ~= "table" then return base end
|
|
if type(base) ~= "table" then base = {} end
|
|
for k, v in pairs(diff) do
|
|
if type(v) == "string" and v == "_" then
|
|
base[k] = nil
|
|
elseif type(v) == "table" and type(base[k]) == "table" then
|
|
RQL_PatchTable(base[k], v)
|
|
else
|
|
base[k] = v
|
|
end
|
|
end
|
|
return base
|
|
end
|
|
|
|
local function RQL_CopyTable(source)
|
|
local dest = {}
|
|
if type(source) ~= "table" then return dest end
|
|
for k, v in pairs(source) do
|
|
if type(v) == "table" then dest[k] = RQL_CopyTable(v) else dest[k] = v end
|
|
end
|
|
return dest
|
|
end
|
|
|
|
local function RQL_CleanText(text)
|
|
if not text then return "" end
|
|
text = tostring(text)
|
|
text = string.gsub(text, "%$B", "\n")
|
|
text = string.gsub(text, "%$N", UnitName("player") or "player")
|
|
text = string.gsub(text, "%$C", UnitClass("player") or "class")
|
|
text = string.gsub(text, "%$R", UnitRace("player") or "race")
|
|
text = string.gsub(text, "%$G([^:;]*):([^;]*);", "%1/%2")
|
|
text = string.gsub(text, "%$T", "")
|
|
text = string.gsub(text, "\\'", "'")
|
|
text = string.gsub(text, '\\"', '"')
|
|
return text
|
|
end
|
|
|
|
local function RQL_ParseQuestLink(link)
|
|
if not link then return nil end
|
|
local _, _, qid, qlevel, qname = string.find(link, "quest:(%d+):([%-%d]*)|h%[(.-)%]|h")
|
|
if qid then return tonumber(qid), tonumber(qlevel) or 0, qname end
|
|
_, _, qid = string.find(link, "quest:(%d+)")
|
|
if qid then return tonumber(qid), 0, nil end
|
|
return nil
|
|
end
|
|
|
|
local function RQL_ColorByLevel(questLevel)
|
|
if not questLevel or questLevel == 0 then return "|cFFFFFFFF" end
|
|
local playerLevel = UnitLevel("player") or 1
|
|
local diff = questLevel - playerLevel
|
|
if diff >= 5 then return "|cFFFF0000"
|
|
elseif diff >= 3 then return "|cFFFF8040"
|
|
elseif diff >= -2 then return "|cFFFFFF00"
|
|
elseif diff >= -4 then return "|cFF40BF40" end
|
|
return "|cFF808080"
|
|
end
|
|
|
|
local function RQL_EntryIsDeleted(v)
|
|
return type(v) == "string" and v == "_"
|
|
end
|
|
|
|
-- Detect dev/test/unused quest titles that should not appear in the browser.
|
|
local function RQL_IsDevQuestTitle(title)
|
|
if not title or title == "" then return 1 end
|
|
local low = string.lower(title)
|
|
if string.find(low, "%[dep%]", 1) then return 1 end
|
|
if string.find(low, "%[dev%]", 1) then return 1 end
|
|
if string.find(low, "%[nyi%]", 1) then return 1 end
|
|
if string.find(low, "%[phase", 1) then return 1 end
|
|
if string.find(low, "%(dev%)", 1) then return 1 end
|
|
if string.find(low, "%(test%)", 1) then return 1 end
|
|
if string.find(low, "test quest", 1, true) then return 1 end
|
|
if string.find(low, "internal", 1, true) then return 1 end
|
|
if string.find(low, "unused", 1, true) then return 1 end
|
|
if string.find(low, "deprecated", 1, true) then return 1 end
|
|
if string.find(low, "placeholder", 1, true) then return 1 end
|
|
if string.find(low, "^qa ", 1) then return 1 end
|
|
if string.find(low, "^gm ", 1) then return 1 end
|
|
if string.find(low, "do not use", 1, true) then return 1 end
|
|
if string.find(low, "debug", 1, true) then return 1 end
|
|
if title == "NYI" or title == "TEST" or title == "OLD" then return 1 end
|
|
return nil
|
|
end
|
|
|
|
local function RQL_GetQuestLocale(id)
|
|
local vanilla = RQL_GetDB("quests", "enUS")
|
|
local turtle = RQL_GetDB("quests", "enUS-turtle")
|
|
local base = nil
|
|
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
|
|
if turtle then
|
|
local tv = turtle[id]
|
|
if RQL_EntryIsDeleted(tv) then base = nil
|
|
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
|
|
end
|
|
return base
|
|
end
|
|
|
|
local function RQL_GetQuestMeta(id)
|
|
local vanilla = RQL_GetDB("quests", "data")
|
|
local turtle = RQL_GetDB("quests", "data-turtle")
|
|
local base = nil
|
|
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
|
|
if turtle then
|
|
local tv = turtle[id]
|
|
if RQL_EntryIsDeleted(tv) then base = nil
|
|
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
|
|
end
|
|
return base
|
|
end
|
|
|
|
local function RQL_GetName(section, id)
|
|
local vanilla = RQL_GetDB(section, "enUS")
|
|
local turtle = RQL_GetDB(section, "enUS-turtle")
|
|
local name = nil
|
|
if vanilla and vanilla[id] and vanilla[id] ~= "_" then name = vanilla[id] end
|
|
if turtle and turtle[id] then
|
|
if turtle[id] == "_" then name = nil else name = turtle[id] end
|
|
end
|
|
if type(name) ~= "string" then return nil end
|
|
return name
|
|
end
|
|
|
|
local function RQL_GetData(section, id)
|
|
local vanilla = RQL_GetDB(section, "data")
|
|
local turtle = RQL_GetDB(section, "data-turtle")
|
|
local base = nil
|
|
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
|
|
if turtle then
|
|
local tv = turtle[id]
|
|
if RQL_EntryIsDeleted(tv) then base = nil
|
|
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
|
|
end
|
|
return base
|
|
end
|
|
|
|
local function RQL_GetZoneName(zoneId)
|
|
if not zoneId then return "Unknown zone" end
|
|
return RQL_GetName("zones", zoneId) or ("Zone #" .. zoneId)
|
|
end
|
|
|
|
local function RQL_GetFirstCoord(section, id)
|
|
local data = RQL_GetData(section, id)
|
|
if data and type(data.coords) == "table" then
|
|
for _, coord in pairs(data.coords) do
|
|
if type(coord) == "table" and coord[1] and coord[2] and coord[3] then
|
|
return coord[1], coord[2], coord[3]
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function RQL_FormatCoord(section, id)
|
|
local x, y, zone = RQL_GetFirstCoord(section, id)
|
|
if x and y and zone then
|
|
return RQL_GetZoneName(zone) .. " " .. string.format("%.1f, %.1f", x, y)
|
|
end
|
|
return "Location unknown"
|
|
end
|
|
|
|
local function RQL_FormatEntityLines(section, ids, limit)
|
|
local out = {}
|
|
if type(ids) ~= "table" then return out end
|
|
local count = 0
|
|
local shown = 0
|
|
for _, id in pairs(ids) do
|
|
count = count + 1
|
|
if shown < (limit or 12) then
|
|
shown = shown + 1
|
|
local prefix
|
|
if section == "units" then prefix = "NPC #"
|
|
elseif section == "objects" then prefix = "Object #"
|
|
else prefix = "Item #" end
|
|
local name = RQL_GetName(section, id) or (prefix .. id)
|
|
local coord = ""
|
|
if section == "units" or section == "objects" then
|
|
coord = " |cFF808080" .. RQL_FormatCoord(section, id) .. "|r"
|
|
end
|
|
table.insert(out, " |cFFFFFFFF" .. name .. "|r|cFF808080 (" .. id .. ")|r" .. coord)
|
|
end
|
|
end
|
|
if count > shown then
|
|
table.insert(out, " |cFF808080...and " .. (count - shown) .. " more.|r")
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function RQL_GetQuestSource(id)
|
|
local turtleLocale = RQL_GetDB("quests", "enUS-turtle")
|
|
local turtleData = RQL_GetDB("quests", "data-turtle")
|
|
if (turtleLocale and type(turtleLocale[id]) == "table") or (turtleData and type(turtleData[id]) == "table") then
|
|
return "Turtle/OctoWoW"
|
|
end
|
|
return "Vanilla"
|
|
end
|
|
|
|
-- Text-based dungeon detection: check title / objective / description
|
|
-- against a curated keyword list covering every vanilla dungeon + raid
|
|
-- and every Turtle/OctoWoW custom dungeon/raid.
|
|
local function RQL_TextMentionsDungeon(searchable)
|
|
if not searchable or searchable == "" then return nil end
|
|
for _, kw in ipairs(RQL_DUNGEON_KEYWORDS) do
|
|
if string.find(searchable, kw, 1, true) then return 1 end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function RQL_GetQuestFlags(id, meta)
|
|
local flags = {}
|
|
local isElite, isDungeon, isPvp, isClass, isProfession, isEvent = nil,nil,nil,nil,nil,nil
|
|
if meta then
|
|
if meta.class then isClass = 1 end
|
|
if meta.skill then isProfession = 1 end
|
|
if meta.event then isEvent = 1 end
|
|
local function checkUnits(ids)
|
|
if type(ids) ~= "table" then return end
|
|
for _, uid in pairs(ids) do
|
|
local u = RQL_GetData("units", uid)
|
|
if u and u.rnk and tonumber(u.rnk) and tonumber(u.rnk) >= 1 then isElite = 1 end
|
|
end
|
|
end
|
|
if type(meta.start) == "table" then checkUnits(meta.start.U) end
|
|
if type(meta["end"]) == "table" then checkUnits(meta["end"].U) end
|
|
if type(meta.obj) == "table" then checkUnits(meta.obj.U) end
|
|
end
|
|
|
|
-- Text search over title + objectives + description for PvP/dungeon
|
|
local loc = RQL_GetQuestLocale(id)
|
|
local searchable = ""
|
|
if loc then
|
|
searchable = string.lower((loc.T or "") .. " " .. (loc.O or "") .. " " .. (loc.D or ""))
|
|
end
|
|
if string.find(searchable, "warsong gulch", 1, true)
|
|
or string.find(searchable, "arathi basin", 1, true)
|
|
or string.find(searchable, "alterac valley", 1, true)
|
|
or string.find(searchable, "battleground", 1, true) then
|
|
isPvp = 1
|
|
end
|
|
if RQL_TextMentionsDungeon(searchable) then
|
|
isDungeon = 1
|
|
end
|
|
|
|
-- PvP wins over Dungeon (Alterac Valley grave quests etc. are PvP)
|
|
if isPvp then isDungeon = nil end
|
|
|
|
if isDungeon then table.insert(flags, "Dungeon/Raid") end
|
|
if isElite and not isDungeon then table.insert(flags, "Elite/Group") end
|
|
if isPvp then table.insert(flags, "PvP") end
|
|
if isClass then table.insert(flags, "Class") end
|
|
if isProfession then table.insert(flags, "Profession") end
|
|
if isEvent then table.insert(flags, "Event") end
|
|
if table.getn(flags) == 0 then table.insert(flags, "Normal") end
|
|
return flags, isElite, isDungeon, isPvp, isClass, isProfession, isEvent
|
|
end
|
|
|
|
local function RQL_JoinFlags(flags)
|
|
local text = ""
|
|
for _, value in ipairs(flags) do
|
|
if text ~= "" then text = text .. ", " end
|
|
text = text .. value
|
|
end
|
|
return text
|
|
end
|
|
|
|
local function RQL_GetFaction(meta)
|
|
if not meta or not meta.race then return "Neutral/Both" end
|
|
local race = tonumber(meta.race) or 0
|
|
local ALLIANCE = 77
|
|
local HORDE = 178
|
|
if race == ALLIANCE then return "Alliance" end
|
|
if race == HORDE then return "Horde" end
|
|
if race == (ALLIANCE + HORDE) or race == 255 then return "Neutral/Both" end
|
|
return "Faction/race locked"
|
|
end
|
|
|
|
local function RQL_GetQuestData(id, linkLevel, linkName)
|
|
local locale = RQL_GetQuestLocale(id)
|
|
local meta = RQL_GetQuestMeta(id)
|
|
local data = {}
|
|
data.id = id
|
|
data.title = linkName or (locale and locale.T) or ("Quest #" .. id)
|
|
data.objectives = locale and locale.O or nil
|
|
data.description = locale and locale.D or nil
|
|
data.level = (meta and meta.lvl) or linkLevel or 0
|
|
data.min = (meta and meta.min) or 0
|
|
data.meta = meta
|
|
data.locale = locale
|
|
data.source = RQL_GetQuestSource(id)
|
|
data.flags = RQL_GetQuestFlags(id, meta)
|
|
data.faction = RQL_GetFaction(meta)
|
|
return data
|
|
end
|
|
|
|
-- ---------- indexing / filtering ----------
|
|
|
|
local function RQL_IndexQuests()
|
|
if RQL_INDEX_READY then return end
|
|
RQL_INDEX_READY = {}
|
|
local vanilla = RQL_GetDB("quests", "enUS") or {}
|
|
local turtle = RQL_GetDB("quests", "enUS-turtle") or {}
|
|
local ids = {}
|
|
for id, v in pairs(vanilla) do if type(v) == "table" then ids[id] = 1 end end
|
|
for id, v in pairs(turtle) do if type(v) == "table" then ids[id] = 1 end end
|
|
for id in pairs(ids) do
|
|
local loc = RQL_GetQuestLocale(id)
|
|
local meta = RQL_GetQuestMeta(id)
|
|
if loc and loc.T and loc.T ~= "_" then
|
|
local title = RQL_CleanText(loc.T)
|
|
if not RQL_IsDevQuestTitle(title) then
|
|
local obj = RQL_CleanText(loc.O or "")
|
|
local flags, isElite, isDungeon, isPvp, isClass, isProfession, isEvent = RQL_GetQuestFlags(id, meta)
|
|
table.insert(RQL_INDEX_READY, {
|
|
id = id, title = title,
|
|
lower = string.lower(title .. " " .. obj .. " " .. id),
|
|
level = (meta and meta.lvl) or 0,
|
|
min = (meta and meta.min) or 0,
|
|
flags = flags,
|
|
faction = RQL_GetFaction(meta),
|
|
source = RQL_GetQuestSource(id),
|
|
isElite = isElite, isDungeon = isDungeon, isPvp = isPvp,
|
|
isClass = isClass, isProfession = isProfession, isEvent = isEvent
|
|
})
|
|
end
|
|
end
|
|
end
|
|
table.sort(RQL_INDEX_READY, function(a, b)
|
|
if a.level == b.level then return a.title < b.title end
|
|
return a.level < b.level
|
|
end)
|
|
end
|
|
|
|
local function RQL_LevelMatches(level)
|
|
if RQL_LEVEL_FILTER == 0 then return 1 end
|
|
level = tonumber(level) or 0
|
|
if RQL_LEVEL_FILTER == 1 then return level >= 1 and level <= 9 end
|
|
if RQL_LEVEL_FILTER == 2 then return level >= 10 and level <= 19 end
|
|
if RQL_LEVEL_FILTER == 3 then return level >= 20 and level <= 29 end
|
|
if RQL_LEVEL_FILTER == 4 then return level >= 30 and level <= 39 end
|
|
if RQL_LEVEL_FILTER == 5 then return level >= 40 and level <= 49 end
|
|
if RQL_LEVEL_FILTER == 6 then return level >= 50 and level <= 59 end
|
|
if RQL_LEVEL_FILTER == 7 then return level >= 60 end
|
|
return 1
|
|
end
|
|
|
|
local function RQL_TypeMatches(row)
|
|
if RQL_TYPE_FILTER == 0 then return 1 end
|
|
if RQL_TYPE_FILTER == 1 then return not row.isElite and not row.isDungeon and not row.isPvp and not row.isClass and not row.isProfession and not row.isEvent end
|
|
if RQL_TYPE_FILTER == 2 then return row.isElite and not row.isDungeon end
|
|
if RQL_TYPE_FILTER == 3 then return row.isDungeon end
|
|
if RQL_TYPE_FILTER == 4 then return row.isPvp end
|
|
if RQL_TYPE_FILTER == 5 then return row.isClass end
|
|
if RQL_TYPE_FILTER == 6 then return row.isProfession end
|
|
if RQL_TYPE_FILTER == 7 then return row.isEvent end
|
|
return 1
|
|
end
|
|
|
|
local function RQL_FactionMatches(row)
|
|
if RQL_FACTION_FILTER == 0 then return 1 end
|
|
if RQL_FACTION_FILTER == 1 then return row.faction == "Alliance" or row.faction == "Neutral/Both" end
|
|
if RQL_FACTION_FILTER == 2 then return row.faction == "Horde" or row.faction == "Neutral/Both" end
|
|
if RQL_FACTION_FILTER == 3 then return row.faction == "Neutral/Both" end
|
|
return 1
|
|
end
|
|
|
|
local function RQL_SourceMatches(row)
|
|
if RQL_SOURCE_FILTER == 0 then return 1 end
|
|
if RQL_SOURCE_FILTER == 1 then return row.source == "Vanilla" end
|
|
if RQL_SOURCE_FILTER == 2 then return row.source == "Turtle/OctoWoW" end
|
|
return 1
|
|
end
|
|
|
|
local function RQL_SearchQuests()
|
|
RQL_IndexQuests()
|
|
RQL_SEARCH_RESULTS = {}
|
|
local query = string.lower(RQL_SEARCH_TEXT or "")
|
|
for _, row in ipairs(RQL_INDEX_READY) do
|
|
if (query == "" or string.find(row.lower, query, 1, true))
|
|
and RQL_LevelMatches(row.level) and RQL_TypeMatches(row)
|
|
and RQL_FactionMatches(row) and RQL_SourceMatches(row) then
|
|
table.insert(RQL_SEARCH_RESULTS, row)
|
|
if table.getn(RQL_SEARCH_RESULTS) >= RQL_MAX_RESULTS then break end
|
|
end
|
|
end
|
|
RQL_PAGE = 1
|
|
end
|
|
|
|
|
|
local RQL_UI = RQL_UI or {}
|
|
-- ---------- Item metadata / tooltip scanner ----------
|
|
-- Vanilla 1.12: GetItemInfo returns name, link, quality, minLevel, itemType,
|
|
-- itemSubType, maxStack, itemEquipLoc, itemTexture. Uncached items return nil;
|
|
-- probing via a hidden tooltip warms the client-side cache.
|
|
local RQL_ITEM_META_CACHE = {}
|
|
|
|
local function RQL_EnsureScanTip()
|
|
if not RQL_UI.scanTip then
|
|
RQL_UI.scanTip = CreateFrame("GameTooltip", "RelationshipsQuestAndItemBrowserScanTip", UIParent, "GameTooltipTemplate")
|
|
end
|
|
-- IMPORTANT: re-owner every call. In 1.12 a GameTooltip that has been
|
|
-- hidden / cleared returns 0 lines from SetHyperlink until SetOwner runs
|
|
-- again, which was the root cause of "item details never show" even after
|
|
-- the client had cached the item.
|
|
RQL_UI.scanTip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
return RQL_UI.scanTip
|
|
end
|
|
|
|
local RQL_QUALITY_HEX = {
|
|
[0] = "|cff9d9d9d", [1] = "|cffffffff", [2] = "|cff1eff00",
|
|
[3] = "|cff0070dd", [4] = "|cffa335ee", [5] = "|cffff8000",
|
|
[6] = "|cffe6cc80", [7] = "|cffe6cc80",
|
|
}
|
|
|
|
local function RQL_GetItemMeta(id)
|
|
if not id then return nil end
|
|
local cached = RQL_ITEM_META_CACHE[id]
|
|
if cached and cached.name then return cached end
|
|
-- Warm the cache via the scanning tooltip.
|
|
local tip = RQL_EnsureScanTip()
|
|
tip:ClearLines()
|
|
tip:SetHyperlink("item:" .. id .. ":0:0:0")
|
|
|
|
-- GetItemInfo return order differs across 1.12-based clients:
|
|
-- * Vanilla 1.12 (9 values):
|
|
-- name, link, quality, minLevel, iType, iSubType, stack, equipLoc, texture
|
|
-- * Turtle / OctoWoW / TBC-onward (10 values, adds iLevel):
|
|
-- name, link, quality, iLevel, minLevel, iType, iSubType, stack, equipLoc, texture
|
|
-- If we blindly unpack 10 vars on a 9-value client, every field after
|
|
-- `quality` shifts left by one and `texture` receives `equipLoc`
|
|
-- (e.g. "INVTYPE_WEAPON"), which SetTexture renders as the red "?"
|
|
-- placeholder. Detect the shape from the last argument instead.
|
|
local r1, r2, r3, r4, r5, r6, r7, r8, r9, r10 = GetItemInfo(id)
|
|
local name, link, quality = r1, r2, r3
|
|
if not name then
|
|
if not cached then RQL_ITEM_META_CACHE[id] = { pending = 1 } end
|
|
return RQL_ITEM_META_CACHE[id]
|
|
end
|
|
local function RQL_IsTexturePath(v)
|
|
if type(v) ~= "string" then return nil end
|
|
if string.find(v, "\\") then return 1 end
|
|
local lv = string.lower(v)
|
|
if string.find(lv, "^interface") then return 1 end
|
|
return nil
|
|
end
|
|
local minLevel, iType, iSubType, maxStack, equipLoc, texture
|
|
if RQL_IsTexturePath(r10) then
|
|
-- 10-value form (has iLevel we don't need)
|
|
minLevel, iType, iSubType, maxStack, equipLoc, texture = r5, r6, r7, r8, r9, r10
|
|
elseif RQL_IsTexturePath(r9) then
|
|
-- 9-value vanilla form
|
|
minLevel, iType, iSubType, maxStack, equipLoc, texture = r4, r5, r6, r7, r8, r9
|
|
else
|
|
-- Unknown shape: prefer 9-value defaults, leave texture nil so the
|
|
-- caller falls back to the question-mark placeholder.
|
|
minLevel, iType, iSubType, maxStack, equipLoc = r4, r5, r6, r7, r8
|
|
texture = nil
|
|
end
|
|
if texture and not RQL_IsTexturePath(texture) then texture = nil end
|
|
local m = {
|
|
name = name, link = link, quality = quality or 1,
|
|
minLevel = minLevel or 0, iType = iType, iSubType = iSubType,
|
|
equipLoc = equipLoc, texture = texture,
|
|
}
|
|
RQL_ITEM_META_CACHE[id] = m
|
|
return m
|
|
end
|
|
|
|
local function RQL_ScanItemTooltipLines(id)
|
|
local tip = RQL_EnsureScanTip()
|
|
tip:ClearLines()
|
|
tip:SetHyperlink("item:" .. id .. ":0:0:0")
|
|
local n = tip:NumLines() or 0
|
|
local out = {}
|
|
-- We already display the weapon/armor subtype ("Axe", "Bow", ...) in
|
|
-- the meta header line, and vanilla renders that same word on the right
|
|
-- side of the slot line AND the "Binds when picked up/equipped" line.
|
|
-- Without suppression the panel reads: "Binds when picked up Axe" and
|
|
-- "Two-Hand Bow" on every weapon, which duplicates the header.
|
|
--
|
|
-- Suppress when the right-side text matches meta.iSubType / meta.iType
|
|
-- (case-insensitive) OR any known weapon/armor subtype word, so it works
|
|
-- even when the meta cache hasn't populated yet on the first scan.
|
|
local meta = RQL_ITEM_META_CACHE[id]
|
|
local dupA = meta and meta.iSubType or nil
|
|
local dupB = meta and meta.iType or nil
|
|
local dupSet = {
|
|
["axe"]=1,["axes"]=1,["bow"]=1,["bows"]=1,["gun"]=1,["guns"]=1,
|
|
["mace"]=1,["maces"]=1,["polearm"]=1,["polearms"]=1,
|
|
["staff"]=1,["staves"]=1,["sword"]=1,["swords"]=1,
|
|
["dagger"]=1,["daggers"]=1,["fist weapon"]=1,["fist weapons"]=1,
|
|
["crossbow"]=1,["crossbows"]=1,["wand"]=1,["wands"]=1,
|
|
["thrown"]=1,["cloth"]=1,["leather"]=1,["mail"]=1,["plate"]=1,
|
|
["shield"]=1,["shields"]=1,["idol"]=1,["libram"]=1,["totem"]=1,
|
|
["miscellaneous"]=1,
|
|
}
|
|
for i = 1, n do
|
|
local lfs = _G and _G["RelationshipsQuestAndItemBrowserScanTipTextLeft" .. i]
|
|
or getglobal("RelationshipsQuestAndItemBrowserScanTipTextLeft" .. i)
|
|
local rfs = _G and _G["RelationshipsQuestAndItemBrowserScanTipTextRight" .. i]
|
|
or getglobal("RelationshipsQuestAndItemBrowserScanTipTextRight" .. i)
|
|
local ltext = lfs and lfs:GetText() or ""
|
|
local rtext = rfs and rfs:GetText() or ""
|
|
local r, g, b = 1, 1, 1
|
|
if lfs and lfs.GetTextColor then r, g, b = lfs:GetTextColor() end
|
|
local hex = string.format("|cff%02x%02x%02x", math.floor((r or 1)*255), math.floor((g or 1)*255), math.floor((b or 1)*255))
|
|
local line = hex .. (ltext or "") .. "|r"
|
|
local rtrim = rtext and string.gsub(rtext, "^%s*(.-)%s*$", "%1") or ""
|
|
local rlow = string.lower(rtrim)
|
|
local isDup = (rtrim == "")
|
|
or (dupA and rlow == string.lower(dupA))
|
|
or (dupB and rlow == string.lower(dupB))
|
|
or dupSet[rlow]
|
|
if not isDup then
|
|
local rr, gg, bb = 1, 1, 1
|
|
if rfs and rfs.GetTextColor then rr, gg, bb = rfs:GetTextColor() end
|
|
local rhex = string.format("|cff%02x%02x%02x", math.floor((rr or 1)*255), math.floor((gg or 1)*255), math.floor((bb or 1)*255))
|
|
line = line .. " " .. rhex .. rtext .. "|r"
|
|
end
|
|
table.insert(out, line)
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- ---------- Item browser (Items tab) ----------
|
|
|
|
local function RQL_GetItemSource(id)
|
|
local turtle = RQL_GetDB("items", "enUS-turtle")
|
|
if turtle and turtle[id] and turtle[id] ~= "_" then return "Turtle/OctoWoW" end
|
|
return "Vanilla"
|
|
end
|
|
|
|
-- Detect dev/test/unused item names that should be hidden from the browser.
|
|
local function RQL_IsDevItemName(name)
|
|
if not name or name == "" then return 1 end
|
|
local low = string.lower(name)
|
|
if string.find(low, "monster %-", 1) then return 1 end
|
|
if string.find(low, "qatest", 1, true) then return 1 end
|
|
if string.find(low, "^old", 1) then return 1 end
|
|
if string.find(low, "%[dep%]", 1) then return 1 end
|
|
if string.find(low, "%[dev%]", 1) then return 1 end
|
|
if string.find(low, "%[nyi%]", 1) then return 1 end
|
|
if string.find(low, "test item", 1, true) then return 1 end
|
|
if string.find(low, "unused", 1, true) then return 1 end
|
|
if string.find(low, "deprecated", 1, true) then return 1 end
|
|
if string.find(low, "placeholder", 1, true) then return 1 end
|
|
if string.find(low, "debug", 1, true) then return 1 end
|
|
return nil
|
|
end
|
|
|
|
-- Find quests that reference an item (as start / turn-in / objective).
|
|
local function RQL_ItemFindQuests(itemId)
|
|
local out = {}
|
|
local seen = {}
|
|
local function scan(dbKey)
|
|
local db = RQL_GetDB("quests", dbKey)
|
|
if type(db) ~= "table" then return end
|
|
for qid, meta in pairs(db) do
|
|
if type(meta) == "table" then
|
|
local hit
|
|
local function has(list)
|
|
if type(list) ~= "table" then return nil end
|
|
for _, v in pairs(list) do
|
|
if v == itemId then return 1 end
|
|
end
|
|
return nil
|
|
end
|
|
if type(meta.start) == "table" and has(meta.start.I) then hit = "Starts" end
|
|
if not hit and type(meta["end"]) == "table" and has(meta["end"].I) then hit = "Turn-in" end
|
|
if not hit and type(meta.obj) == "table" and has(meta.obj.I) then hit = "Objective" end
|
|
if hit and not seen[qid] then
|
|
seen[qid] = 1
|
|
table.insert(out, { id = qid, role = hit })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
scan("data")
|
|
scan("data-turtle")
|
|
return out
|
|
end
|
|
|
|
local function RQL_IndexItems()
|
|
if RQL_ITEM_INDEX_READY then return end
|
|
RQL_ITEM_INDEX_READY = {}
|
|
local vanilla = RQL_GetDB("items", "enUS") or {}
|
|
local turtle = RQL_GetDB("items", "enUS-turtle") or {}
|
|
local ids = {}
|
|
for id, v in pairs(vanilla) do if type(v) == "string" and v ~= "_" then ids[id] = 1 end end
|
|
for id, v in pairs(turtle) do
|
|
if v == "_" then ids[id] = nil
|
|
elseif type(v) == "string" then ids[id] = 1 end
|
|
end
|
|
for id in pairs(ids) do
|
|
local name = RQL_GetName("items", id)
|
|
if name and not RQL_IsDevItemName(name) then
|
|
table.insert(RQL_ITEM_INDEX_READY, {
|
|
id = id,
|
|
name = name,
|
|
lower = string.lower(name .. " " .. id),
|
|
source = RQL_GetItemSource(id),
|
|
})
|
|
end
|
|
end
|
|
table.sort(RQL_ITEM_INDEX_READY, function(a, b) return a.name < b.name end)
|
|
end
|
|
|
|
local function RQL_ItemSourceMatches(row)
|
|
if RQL_ITEM_SOURCE_FILTER == 0 then return 1 end
|
|
if RQL_ITEM_SOURCE_FILTER == 1 then return row.source == "Vanilla" end
|
|
if RQL_ITEM_SOURCE_FILTER == 2 then return row.source == "Turtle/OctoWoW" end
|
|
return 1
|
|
end
|
|
|
|
local function RQL_SearchItems()
|
|
RQL_IndexItems()
|
|
RQL_ITEM_SEARCH_RESULTS = {}
|
|
local query = string.lower(RQL_ITEM_SEARCH_TEXT or "")
|
|
for _, row in ipairs(RQL_ITEM_INDEX_READY) do
|
|
if (query == "" or string.find(row.lower, query, 1, true))
|
|
and RQL_ItemSourceMatches(row) then
|
|
table.insert(RQL_ITEM_SEARCH_RESULTS, row)
|
|
if table.getn(RQL_ITEM_SEARCH_RESULTS) >= RQL_MAX_RESULTS then break end
|
|
end
|
|
end
|
|
RQL_ITEM_PAGE = 1
|
|
end
|
|
|
|
-- ---------- UI ----------
|
|
|
|
function RQL_LinkQuestToChat(id, level, title)
|
|
if not id then return end
|
|
local meta = RQL_GetQuestMeta(id)
|
|
local loc = RQL_GetQuestLocale(id)
|
|
local lvl = tonumber(level) or (meta and tonumber(meta.lvl)) or 0
|
|
local name = title or (loc and loc.T) or ("Quest #" .. id)
|
|
local link = "|cffffff00|Hquest:" .. id .. ":" .. lvl .. "|h[" .. name .. "]|h|r"
|
|
|
|
if not ChatFrameEditBox then return end
|
|
if not ChatFrameEditBox:IsVisible() then
|
|
ChatFrameEditBox:Show()
|
|
end
|
|
ChatFrameEditBox:SetFocus()
|
|
ChatFrameEditBox:Insert(link)
|
|
end
|
|
|
|
|
|
local function RQL_TotalPages()
|
|
local results = (RQL_TAB == "items") and RQL_ITEM_SEARCH_RESULTS or RQL_SEARCH_RESULTS
|
|
local n = table.getn(results)
|
|
if n <= 0 then return 1 end
|
|
return math.floor((n - 1) / RQL_ROWS_VISIBLE) + 1
|
|
end
|
|
|
|
local function RQL_CurrentPage()
|
|
if RQL_TAB == "items" then return RQL_ITEM_PAGE end
|
|
return RQL_PAGE
|
|
end
|
|
|
|
local function RQL_SetCurrentPage(p)
|
|
if RQL_TAB == "items" then RQL_ITEM_PAGE = p else RQL_PAGE = p end
|
|
end
|
|
|
|
local function RQL_UpdateStatus()
|
|
if RQL_TAB == "items" then
|
|
if RQL_UI.itemStatus then
|
|
local total = RQL_ITEM_INDEX_READY and table.getn(RQL_ITEM_INDEX_READY) or 0
|
|
local n = table.getn(RQL_ITEM_SEARCH_RESULTS)
|
|
RQL_UI.itemStatus:SetText("Matches: " .. n .. " / " .. total .. " items")
|
|
end
|
|
else
|
|
if RQL_UI.status then
|
|
local total = RQL_INDEX_READY and table.getn(RQL_INDEX_READY) or 0
|
|
local n = table.getn(RQL_SEARCH_RESULTS)
|
|
RQL_UI.status:SetText("Matches: " .. n .. " / " .. total .. " quests")
|
|
end
|
|
end
|
|
if RQL_UI.pageLabel then
|
|
RQL_UI.pageLabel:SetText("Page " .. RQL_CurrentPage() .. " / " .. RQL_TotalPages())
|
|
end
|
|
end
|
|
|
|
local function RQL_UpdateList()
|
|
if not RQL_UI.rows then return end
|
|
local total = RQL_TotalPages()
|
|
local page = RQL_CurrentPage()
|
|
if page < 1 then page = 1 end
|
|
if page > total then page = total end
|
|
RQL_SetCurrentPage(page)
|
|
local offset = (page - 1) * RQL_ROWS_VISIBLE
|
|
local results = (RQL_TAB == "items") and RQL_ITEM_SEARCH_RESULTS or RQL_SEARCH_RESULTS
|
|
for i = 1, RQL_ROWS_VISIBLE do
|
|
local row = RQL_UI.rows[i]
|
|
local idx = i + offset
|
|
local r = results[idx]
|
|
if r then
|
|
if RQL_TAB == "items" then
|
|
local meta = RQL_GetItemMeta(r.id)
|
|
local qcol = "|cFFFFFFFF"
|
|
local lvlText = "?"
|
|
if meta and meta.name then
|
|
qcol = RQL_QUALITY_HEX[meta.quality or 1] or "|cFFFFFFFF"
|
|
local minLvl = tonumber(meta.minLevel)
|
|
lvlText = (minLvl and minLvl > 0) and tostring(minLvl) or "-"
|
|
end
|
|
row.left:SetText(qcol .. "[" .. lvlText .. "] " .. r.name .. "|r")
|
|
row.right:SetText("|cFF808080(#" .. r.id .. ", " .. r.source .. ")|r")
|
|
row.itemId = r.id
|
|
row.itemName = r.name
|
|
row.questId = nil
|
|
else
|
|
local lvlNum = tonumber(r.level)
|
|
local lvl = lvlNum and lvlNum > 0 and tostring(lvlNum) or "?"
|
|
local color = RQL_ColorByLevel(r.level)
|
|
row.left:SetText(color .. "[" .. lvl .. "]|r " .. r.title)
|
|
row.right:SetText("|cFF808080(" .. RQL_JoinFlags(r.flags) .. ", " .. r.source .. ")|r")
|
|
row.questId = r.id
|
|
row.questLevel = r.level
|
|
row.questTitle = r.title
|
|
row.itemId = nil
|
|
end
|
|
row:Show()
|
|
else
|
|
row:Hide()
|
|
row.questId = nil
|
|
row.itemId = nil
|
|
end
|
|
end
|
|
if RQL_UI.pageLabel then
|
|
RQL_UI.pageLabel:SetText("Page " .. page .. " / " .. total)
|
|
end
|
|
if RQL_UI.btnPrev then
|
|
if page <= 1 then RQL_UI.btnPrev:Disable() else RQL_UI.btnPrev:Enable() end
|
|
end
|
|
if RQL_UI.btnNext then
|
|
if page >= total then RQL_UI.btnNext:Disable() else RQL_UI.btnNext:Enable() end
|
|
end
|
|
end
|
|
|
|
-- ---------- Detail view (Quest-log style) ----------
|
|
|
|
-- Header gap (space above a section header) and body gap (space between the
|
|
-- header and its body text) are separated so titles never sit tight against
|
|
-- the previous paragraph, matching the real quest log layout.
|
|
local RQL_HEADER_GAP = 20 -- space above a section header
|
|
local RQL_BODY_GAP = 8 -- space between header and its body
|
|
local RQL_DETAIL_PAD = 18 -- inner left/right/top padding of the parchment
|
|
|
|
local function RQL_DetailSetSection(fs, label, body, prev)
|
|
if not body or body == "" then
|
|
fs.header:Hide(); fs.text:Hide()
|
|
return prev
|
|
end
|
|
fs.header:SetText(label)
|
|
fs.text:SetText(body)
|
|
fs.header:ClearAllPoints()
|
|
if prev then
|
|
fs.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
else
|
|
fs.header:SetPoint("TOPLEFT", RQL_UI.detailContent, "TOPLEFT", 0, 0)
|
|
end
|
|
fs.text:ClearAllPoints()
|
|
fs.text:SetPoint("TOPLEFT", fs.header, "BOTTOMLEFT", 0, -RQL_BODY_GAP)
|
|
fs.text:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
fs.header:Show(); fs.text:Show()
|
|
return fs.text
|
|
end
|
|
|
|
local function RQL_MakeDetailSection(parent)
|
|
-- Header: gold quest-log header font
|
|
local h = parent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
|
|
h:SetJustifyH("LEFT"); h:Hide()
|
|
-- Body: the standard quest-log body font
|
|
local b = parent:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
b:SetJustifyH("LEFT"); b:SetJustifyV("TOP"); b:SetSpacing(2); b:Hide()
|
|
return { header = h, text = b }
|
|
end
|
|
|
|
local RQL_ShowQuest -- forward declaration; defined below
|
|
local function RQL_RenderQuestDetail(id, linkLevel, linkName)
|
|
local data = RQL_GetQuestData(id, linkLevel, linkName)
|
|
|
|
-- Title
|
|
RQL_UI.dTitle:SetText(data.title)
|
|
RQL_UI.dTitle:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
|
|
-- Meta line under title (with a bit of breathing room below the title)
|
|
local color = RQL_ColorByLevel(data.level)
|
|
local lvlText = (data.level and data.level > 0) and tostring(data.level) or "?"
|
|
local minText = (data.min and data.min > 0) and tostring(data.min) or "any"
|
|
local metaLine = color .. "Level " .. lvlText .. "|r"
|
|
.. " |cFFFFD100Requires:|r " .. minText
|
|
.. " |cFFFFD100Type:|r " .. RQL_JoinFlags(data.flags)
|
|
.. " |cFFFFD100Faction:|r " .. data.faction
|
|
.. " |cFFFFD100Source:|r " .. data.source
|
|
.. " |cFF808080#" .. id .. "|r"
|
|
RQL_UI.dMeta:SetText(metaLine)
|
|
RQL_UI.dMeta:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
RQL_UI.dMeta:ClearAllPoints()
|
|
RQL_UI.dMeta:SetPoint("TOPLEFT", RQL_UI.dTitle, "BOTTOMLEFT", 0, -10)
|
|
|
|
local prev = RQL_UI.dMeta
|
|
|
|
prev = RQL_DetailSetSection(RQL_UI.dObjSection, "Objective",
|
|
data.objectives and RQL_CleanText(data.objectives) or nil, prev)
|
|
|
|
prev = RQL_DetailSetSection(RQL_UI.dDescSection, "Description",
|
|
data.description and RQL_CleanText(data.description) or nil, prev)
|
|
|
|
-- Start / End / Objective NPC + Object lists (items handled separately below)
|
|
local locBody = ""
|
|
if data.meta then
|
|
local function add(label, lines)
|
|
if table.getn(lines) == 0 then return end
|
|
if locBody ~= "" then locBody = locBody .. "\n\n" end
|
|
locBody = locBody .. "|cFFFFD100" .. label .. "|r\n\n" .. table.concat(lines, "\n")
|
|
end
|
|
if type(data.meta.start) == "table" then
|
|
add("Quest Giver (NPC)", RQL_FormatEntityLines("units", data.meta.start.U))
|
|
add("Quest Giver (Object)", RQL_FormatEntityLines("objects", data.meta.start.O))
|
|
end
|
|
if type(data.meta["end"]) == "table" then
|
|
add("Turn-in (NPC)", RQL_FormatEntityLines("units", data.meta["end"].U))
|
|
add("Turn-in (Object)", RQL_FormatEntityLines("objects", data.meta["end"].O))
|
|
end
|
|
if type(data.meta.obj) == "table" then
|
|
add("Objective NPCs", RQL_FormatEntityLines("units", data.meta.obj.U))
|
|
add("Objective Objects", RQL_FormatEntityLines("objects", data.meta.obj.O))
|
|
end
|
|
end
|
|
prev = RQL_DetailSetSection(RQL_UI.dLocSection, "Locations",
|
|
locBody ~= "" and locBody or nil, prev)
|
|
|
|
-- ---------- Clickable Items section ----------
|
|
RQL_UI.dItemButtons = RQL_UI.dItemButtons or {}
|
|
for i = 1, table.getn(RQL_UI.dItemButtons) do RQL_UI.dItemButtons[i]:Hide() end
|
|
|
|
local itemEntries = {}
|
|
local seenItem = {}
|
|
local function collectItems(label, ids)
|
|
if type(ids) ~= "table" then return end
|
|
for _, iid in pairs(ids) do
|
|
if iid and not seenItem[iid] then
|
|
seenItem[iid] = 1
|
|
local name = RQL_GetName("items", iid) or ("Item #" .. iid)
|
|
table.insert(itemEntries, { id = iid, name = name, role = label })
|
|
end
|
|
end
|
|
end
|
|
if data.meta then
|
|
if type(data.meta.start) == "table" then collectItems("Starts quest", data.meta.start.I) end
|
|
if type(data.meta["end"]) == "table" then collectItems("Turn-in", data.meta["end"].I) end
|
|
if type(data.meta.obj) == "table" then collectItems("Objective", data.meta.obj.I) end
|
|
end
|
|
|
|
if table.getn(itemEntries) > 0 then
|
|
local sec = RQL_UI.dItemSection
|
|
sec.header:SetText("Items (click to open)")
|
|
sec.header:ClearAllPoints()
|
|
sec.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
sec.header:Show()
|
|
sec.text:Hide()
|
|
|
|
local anchor = sec.header
|
|
local gap = RQL_BODY_GAP
|
|
for i = 1, table.getn(itemEntries) do
|
|
local entry = itemEntries[i]
|
|
local btn = RQL_UI.dItemButtons[i]
|
|
if not btn then
|
|
btn = CreateFrame("Button", nil, RQL_UI.detailContent)
|
|
btn:SetHeight(16)
|
|
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
|
|
fs:SetJustifyH("LEFT")
|
|
btn.fs = fs
|
|
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAllPoints(btn)
|
|
btn:SetScript("OnEnter", function()
|
|
if this.fs then this.fs:SetTextColor(1, 1, 0.4) end
|
|
end)
|
|
btn:SetScript("OnLeave", function()
|
|
if this.fs then this.fs:SetTextColor(1, 1, 1) end
|
|
end)
|
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
btn:SetScript("OnClick", function()
|
|
if this.itemId then
|
|
RQL_ShowItem(this.itemId, this.itemName)
|
|
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
|
|
end
|
|
end)
|
|
RQL_UI.dItemButtons[i] = btn
|
|
end
|
|
btn.itemId = entry.id
|
|
btn.itemName = entry.name
|
|
btn.fs:SetText(" |cFFFFFFFF" .. entry.name .. "|r |cFF808080(#" .. entry.id .. ", " .. entry.role .. ")|r |cFFAAAAFF[click to open]|r")
|
|
btn.fs:SetTextColor(1, 1, 1)
|
|
btn:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
btn:ClearAllPoints()
|
|
btn:SetPoint("TOPLEFT", anchor, (anchor == sec.header) and "BOTTOMLEFT" or "BOTTOMLEFT", 0, -gap)
|
|
btn:Show()
|
|
anchor = btn
|
|
gap = 4
|
|
end
|
|
prev = anchor
|
|
else
|
|
RQL_UI.dItemSection.header:Hide()
|
|
RQL_UI.dItemSection.text:Hide()
|
|
end
|
|
|
|
-- Prerequisites (clickable — jump straight to the required quest)
|
|
RQL_UI.dPreButtons = RQL_UI.dPreButtons or {}
|
|
-- Hide any previously-shown prereq buttons before re-rendering
|
|
for i = 1, table.getn(RQL_UI.dPreButtons) do
|
|
RQL_UI.dPreButtons[i]:Hide()
|
|
end
|
|
|
|
local preList = (data.meta and type(data.meta.pre) == "table") and data.meta.pre or nil
|
|
if preList and table.getn(preList) > 0 then
|
|
local sec = RQL_UI.dPreSection
|
|
sec.header:SetText("Prerequisites (click to open)")
|
|
sec.header:ClearAllPoints()
|
|
sec.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
sec.header:Show()
|
|
sec.text:Hide()
|
|
|
|
local anchor = sec.header
|
|
local gap = RQL_BODY_GAP
|
|
for i = 1, table.getn(preList) do
|
|
local preId = preList[i]
|
|
local preLocale = RQL_GetQuestLocale(preId)
|
|
local name = (preLocale and preLocale.T) or ("Quest #" .. preId)
|
|
|
|
local btn = RQL_UI.dPreButtons[i]
|
|
if not btn then
|
|
btn = CreateFrame("Button", nil, RQL_UI.detailContent)
|
|
btn:SetHeight(16)
|
|
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
|
|
fs:SetJustifyH("LEFT")
|
|
btn.fs = fs
|
|
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAllPoints(btn)
|
|
btn:SetScript("OnEnter", function()
|
|
if this.fs then this.fs:SetTextColor(1, 1, 0.4) end
|
|
end)
|
|
btn:SetScript("OnLeave", function()
|
|
if this.fs then this.fs:SetTextColor(1, 1, 1) end
|
|
end)
|
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
btn:SetScript("OnClick", function()
|
|
if this.preId then
|
|
RQL_ShowQuest(this.preId, 0, this.preName)
|
|
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
|
|
end
|
|
end)
|
|
RQL_UI.dPreButtons[i] = btn
|
|
end
|
|
btn.preId = preId
|
|
btn.preName = name
|
|
btn.fs:SetText(" " .. name .. " |cFF808080(#" .. preId .. ")|r |cFFAAAAFF[click to open]|r")
|
|
btn.fs:SetTextColor(1, 1, 1)
|
|
btn:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
btn:ClearAllPoints()
|
|
btn:SetPoint("TOPLEFT", anchor, (anchor == sec.header) and "BOTTOMLEFT" or "BOTTOMLEFT", 0, -gap)
|
|
btn:Show()
|
|
anchor = btn
|
|
gap = 4
|
|
end
|
|
prev = anchor
|
|
else
|
|
-- No prerequisites — hide the section entirely.
|
|
RQL_UI.dPreSection.header:Hide()
|
|
RQL_UI.dPreSection.text:Hide()
|
|
end
|
|
|
|
if not data.meta then
|
|
RQL_UI.dFooter:SetText("|cFFFF8080No bundled database entry found. Showing link-only data.|r")
|
|
RQL_UI.dFooter:ClearAllPoints()
|
|
RQL_UI.dFooter:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
RQL_UI.dFooter:SetWidth(RQL_UI.detailContent:GetWidth())
|
|
RQL_UI.dFooter:Show()
|
|
else
|
|
RQL_UI.dFooter:Hide()
|
|
end
|
|
|
|
-- Recompute scroll-child height so the ScrollFrame knows how far to scroll.
|
|
-- Without this the child stays at its initial height (1) and the wheel /
|
|
-- scrollbar have no range on longer quests.
|
|
local function _rql_h(fs)
|
|
if not fs then return 0 end
|
|
if fs.IsShown and not fs:IsShown() then return 0 end
|
|
-- GetStringHeight may be missing on some 1.12 clients / non-FontString
|
|
-- regions. Guard the call and fall back to GetHeight so a nil method
|
|
-- can't abort the whole layout (which would leave the ScrollFrame
|
|
-- with a stale/zero range and break the scrollbar).
|
|
local h
|
|
if type(fs.GetStringHeight) == "function" then
|
|
local ok, v = pcall(fs.GetStringHeight, fs)
|
|
if ok and v and v > 0 then h = v end
|
|
end
|
|
if (not h) and type(fs.GetHeight) == "function" then
|
|
local ok, v = pcall(fs.GetHeight, fs)
|
|
if ok and v then h = v end
|
|
end
|
|
return h or 0
|
|
end
|
|
local total = _rql_h(RQL_UI.dTitle) + 10 + _rql_h(RQL_UI.dMeta)
|
|
local function _rql_addSec(sec)
|
|
if sec and sec.header and sec.header:IsShown() then
|
|
total = total + RQL_HEADER_GAP + _rql_h(sec.header)
|
|
if sec.text and sec.text:IsShown() then
|
|
total = total + RQL_BODY_GAP + _rql_h(sec.text)
|
|
end
|
|
end
|
|
end
|
|
_rql_addSec(RQL_UI.dObjSection)
|
|
_rql_addSec(RQL_UI.dDescSection)
|
|
_rql_addSec(RQL_UI.dLocSection)
|
|
if RQL_UI.dPreSection and RQL_UI.dPreSection.header:IsShown() then
|
|
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dPreSection.header)
|
|
local btns = RQL_UI.dPreButtons or {}
|
|
local shown = 0
|
|
for i = 1, table.getn(btns) do
|
|
if btns[i]:IsShown() then
|
|
total = total + (shown == 0 and RQL_BODY_GAP or 4) + (btns[i]:GetHeight() or 16)
|
|
shown = shown + 1
|
|
end
|
|
end
|
|
end
|
|
if RQL_UI.dItemSection and RQL_UI.dItemSection.header:IsShown() then
|
|
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dItemSection.header)
|
|
local btns = RQL_UI.dItemButtons or {}
|
|
local shown = 0
|
|
for i = 1, table.getn(btns) do
|
|
if btns[i]:IsShown() then
|
|
total = total + (shown == 0 and RQL_BODY_GAP or 4) + (btns[i]:GetHeight() or 16)
|
|
shown = shown + 1
|
|
end
|
|
end
|
|
end
|
|
if RQL_UI.dFooter:IsShown() then
|
|
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dFooter)
|
|
end
|
|
RQL_UI.detailContent:SetHeight(total + 24)
|
|
if RQL_UI.detailScroll and RQL_UI.detailScroll.UpdateScrollChildRect then
|
|
RQL_UI.detailScroll:UpdateScrollChildRect()
|
|
-- Nudge the scroll position so the ScrollFrame recomputes its
|
|
-- vertical range immediately. Without this, the scrollbar thumb
|
|
-- occasionally stays disabled on longer quests until the user
|
|
-- resizes or reopens the frame.
|
|
local cur = RQL_UI.detailScroll:GetVerticalScroll() or 0
|
|
local maxs = RQL_UI.detailScroll:GetVerticalScrollRange() or 0
|
|
if cur > maxs then cur = maxs end
|
|
RQL_UI.detailScroll:SetVerticalScroll(cur)
|
|
end
|
|
end
|
|
|
|
-- ---------- Show/hide screens ----------
|
|
|
|
local function RQL_UpdateTabVisuals()
|
|
if RQL_UI.tabQuests and RQL_UI.tabItems then
|
|
if RQL_TAB == "items" then
|
|
RQL_UI.tabItems:LockHighlight()
|
|
RQL_UI.tabQuests:UnlockHighlight()
|
|
else
|
|
RQL_UI.tabQuests:LockHighlight()
|
|
RQL_UI.tabItems:UnlockHighlight()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function RQL_ShowBrowser()
|
|
RQL_TAB = "quests"
|
|
RQL_MODE = "browser"
|
|
RQL_UI.iRetryFor = nil
|
|
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Relationships Quest And Item Browser") end
|
|
if RQL_UI.detail then RQL_UI.detail:Hide() end
|
|
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
|
|
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
|
|
if RQL_UI.listArea then RQL_UI.listArea:Show() end
|
|
if RQL_UI.controls then RQL_UI.controls:Show() end
|
|
if RQL_UI.pager then RQL_UI.pager:Show() end
|
|
RQL_UpdateTabVisuals()
|
|
RQL_SearchQuests()
|
|
RQL_UpdateStatus()
|
|
RQL_UpdateList()
|
|
RelationshipsQuestAndItemBrowserFrame:Show()
|
|
end
|
|
|
|
local function RQL_ShowItemBrowser()
|
|
RQL_TAB = "items"
|
|
RQL_MODE = "browser"
|
|
RQL_UI.iRetryFor = nil
|
|
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Relationships Quest And Item Browser") end
|
|
if RQL_UI.detail then RQL_UI.detail:Hide() end
|
|
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
|
|
if RQL_UI.controls then RQL_UI.controls:Hide() end
|
|
if RQL_UI.itemControls then RQL_UI.itemControls:Show() end
|
|
if RQL_UI.listArea then RQL_UI.listArea:Show() end
|
|
if RQL_UI.pager then RQL_UI.pager:Show() end
|
|
RQL_UpdateTabVisuals()
|
|
RQL_SearchItems()
|
|
RQL_UpdateStatus()
|
|
RQL_UpdateList()
|
|
RelationshipsQuestAndItemBrowserFrame:Show()
|
|
end
|
|
|
|
function RQL_ShowQuest(id, level, name)
|
|
RQL_MODE = "details"
|
|
RQL_SELECTED_QUEST_ID = id
|
|
-- Cancel any pending item-detail tooltip retry from a previous screen.
|
|
RQL_UI.iRetryFor = nil
|
|
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Quest Details") end
|
|
if RQL_UI.controls then RQL_UI.controls:Hide() end
|
|
if RQL_UI.listArea then RQL_UI.listArea:Hide() end
|
|
if RQL_UI.pager then RQL_UI.pager:Hide() end
|
|
-- Hide item screens too, otherwise navigating quest -> item -> related
|
|
-- quest leaves the item detail rendered underneath the quest text.
|
|
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
|
|
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
|
|
RQL_RenderQuestDetail(id, level, name)
|
|
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
|
|
if RQL_UI.detail then RQL_UI.detail:Show() end
|
|
RelationshipsQuestAndItemBrowserFrame:Show()
|
|
end
|
|
|
|
-- Item detail renderer + shower
|
|
function RQL_ShowItem(id, name)
|
|
if not id then return end
|
|
RQL_MODE = "itemDetails"
|
|
RQL_SELECTED_ITEM_ID = id
|
|
if not RQL_UI.itemDetail then return end
|
|
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Item Details") end
|
|
if RQL_UI.controls then RQL_UI.controls:Hide() end
|
|
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
|
|
if RQL_UI.listArea then RQL_UI.listArea:Hide() end
|
|
if RQL_UI.pager then RQL_UI.pager:Hide() end
|
|
if RQL_UI.detail then RQL_UI.detail:Hide() end
|
|
|
|
local resolvedName = name or RQL_GetName("items", id) or ("Item #" .. id)
|
|
local source = RQL_GetItemSource(id)
|
|
|
|
local meta = RQL_GetItemMeta(id)
|
|
local qcol = "|cFFFFFFFF"
|
|
local lvlText = "-"
|
|
local extra = ""
|
|
if meta and meta.name then
|
|
qcol = RQL_QUALITY_HEX[meta.quality or 1] or "|cFFFFFFFF"
|
|
local ml = tonumber(meta.minLevel)
|
|
if ml and ml > 0 then lvlText = tostring(ml) end
|
|
if meta.iType then
|
|
extra = " |cFFFFD100Type:|r " .. meta.iType
|
|
if meta.iSubType and meta.iSubType ~= "" then extra = extra .. " (" .. meta.iSubType .. ")" end
|
|
end
|
|
end
|
|
RQL_UI.iTitle:SetText(qcol .. "[" .. lvlText .. "] " .. resolvedName .. "|r")
|
|
RQL_UI.iMeta:SetText("|cFFFFD100Item ID:|r " .. id .. " |cFFFFD100Source:|r " .. source .. extra)
|
|
|
|
-- Icon: use GetItemInfo texture when we have it; fall back to the
|
|
-- vanilla question-mark placeholder so the slot is never empty. Keep
|
|
-- it on top of the decorative slot art; UI-EmptySlot-Disabled is not
|
|
-- just a border and can otherwise cover the icon on 1.12 clients.
|
|
if RQL_UI.iIcon then
|
|
if meta and meta.texture then
|
|
RQL_UI.iIcon:SetTexture(meta.texture)
|
|
else
|
|
RQL_UI.iIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
|
end
|
|
RQL_UI.iIcon:Show()
|
|
end
|
|
|
|
-- Scan full tooltip and render into the description FontString.
|
|
if not RQL_UI.iDesc then
|
|
-- Use the standard quest-log body font (larger + more legible than
|
|
-- GameFontHighlightSmall) and add generous line spacing so each
|
|
-- stat / enchant / requirement row is clearly separated.
|
|
RQL_UI.iDesc = RQL_UI.itemDetailContent:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
RQL_UI.iDesc:SetJustifyH("LEFT")
|
|
RQL_UI.iDesc:SetJustifyV("TOP")
|
|
RQL_UI.iDesc:SetSpacing(7)
|
|
end
|
|
RQL_UI.iDesc:ClearAllPoints()
|
|
-- Anchor description strictly BELOW the icon + meta block. Icon is 44
|
|
-- tall at y=0, meta line sits under the title to its right, so the
|
|
-- guaranteed clearance point is icon-height + a comfortable gap.
|
|
RQL_UI.iDesc:SetPoint("TOPLEFT", RQL_UI.itemDetailContent, "TOPLEFT",
|
|
0, -(44 + RQL_HEADER_GAP + 6))
|
|
RQL_UI.iDesc:SetWidth(RQL_UI.itemDetailContent:GetWidth())
|
|
local lines = RQL_ScanItemTooltipLines(id)
|
|
local desc
|
|
if lines and table.getn(lines) > 0 then
|
|
desc = table.concat(lines, "\n")
|
|
else
|
|
-- Fall back to whatever GetItemInfo gave us so the panel is never
|
|
-- empty. An OnUpdate retry (below) will replace this once the
|
|
-- client-side item cache is warm.
|
|
local parts = {}
|
|
table.insert(parts, qcol .. resolvedName .. "|r")
|
|
if meta and meta.name then
|
|
if meta.iType then
|
|
local t = meta.iType
|
|
if meta.iSubType and meta.iSubType ~= "" then t = t .. " (" .. meta.iSubType .. ")" end
|
|
table.insert(parts, "|cFFFFFFFF" .. t .. "|r")
|
|
end
|
|
if meta.minLevel and meta.minLevel > 0 then
|
|
table.insert(parts, "|cFFFFFFFFRequires Level " .. meta.minLevel .. "|r")
|
|
end
|
|
end
|
|
table.insert(parts, "|cFF808080Loading full item data from the server... this will refresh in a moment.|r")
|
|
desc = table.concat(parts, "\n")
|
|
-- Schedule a re-scan; the OnUpdate ticker in RQL_UI.retryTicker will
|
|
-- keep probing SetHyperlink until GameTooltip returns real lines.
|
|
RQL_UI.iRetryFor = id
|
|
RQL_UI.iRetryElapsed = 0
|
|
RQL_UI.iRetryTries = 0
|
|
end
|
|
RQL_UI.iDesc:SetText(desc)
|
|
RQL_UI.iDesc:Show()
|
|
local descH = RQL_UI.iDesc:GetHeight() or 0
|
|
|
|
-- Clear previous quest buttons
|
|
for i = 1, table.getn(RQL_UI.iQuestButtons) do RQL_UI.iQuestButtons[i]:Hide() end
|
|
|
|
local relatedQuests = RQL_ItemFindQuests(id)
|
|
local sec = RQL_UI.iQuestSection
|
|
local totalHeight = 40 + 20 + RQL_HEADER_GAP + descH
|
|
if table.getn(relatedQuests) > 0 then
|
|
sec.header:SetText("Related Quests (click to open)")
|
|
sec.header:ClearAllPoints()
|
|
sec.header:SetPoint("TOPLEFT", RQL_UI.iDesc, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
sec.header:Show()
|
|
sec.text:Hide()
|
|
totalHeight = totalHeight + RQL_HEADER_GAP + 20
|
|
|
|
local anchor = sec.header
|
|
local gap = RQL_BODY_GAP
|
|
for i = 1, table.getn(relatedQuests) do
|
|
local entry = relatedQuests[i]
|
|
local loc = RQL_GetQuestLocale(entry.id)
|
|
local meta = RQL_GetQuestMeta(entry.id)
|
|
local title = (loc and loc.T) or ("Quest #" .. entry.id)
|
|
local lvl = (meta and meta.lvl) or 0
|
|
|
|
local btn = RQL_UI.iQuestButtons[i]
|
|
if not btn then
|
|
btn = CreateFrame("Button", nil, RQL_UI.itemDetailContent)
|
|
btn:SetHeight(16)
|
|
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
|
|
fs:SetJustifyH("LEFT")
|
|
btn.fs = fs
|
|
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAllPoints(btn)
|
|
btn:SetScript("OnEnter", function() if this.fs then this.fs:SetTextColor(1, 1, 0.4) end end)
|
|
btn:SetScript("OnLeave", function() if this.fs then this.fs:SetTextColor(1, 1, 1) end end)
|
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
btn:SetScript("OnClick", function()
|
|
if this.qid then RQL_ShowQuest(this.qid, this.qlvl, this.qtitle) end
|
|
end)
|
|
RQL_UI.iQuestButtons[i] = btn
|
|
end
|
|
btn.qid = entry.id
|
|
btn.qlvl = lvl
|
|
btn.qtitle = title
|
|
local lvlText = (lvl and lvl > 0) and tostring(lvl) or "?"
|
|
local color = RQL_ColorByLevel(lvl)
|
|
btn.fs:SetText(" " .. color .. "[" .. lvlText .. "]|r " .. title .. " |cFF808080(#" .. entry.id .. ", " .. entry.role .. ")|r |cFFAAAAFF[click to open]|r")
|
|
btn.fs:SetTextColor(1, 1, 1)
|
|
btn:SetWidth(RQL_UI.itemDetailContent:GetWidth())
|
|
btn:ClearAllPoints()
|
|
btn:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -gap)
|
|
btn:Show()
|
|
anchor = btn
|
|
gap = 4
|
|
totalHeight = totalHeight + gap + 16
|
|
end
|
|
else
|
|
sec.header:SetText("Related Quests")
|
|
sec.header:ClearAllPoints()
|
|
sec.header:SetPoint("TOPLEFT", RQL_UI.iDesc, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
|
|
sec.header:Show()
|
|
sec.text:SetText("|cFF808080No quests reference this item in the bundled database.|r")
|
|
sec.text:ClearAllPoints()
|
|
sec.text:SetPoint("TOPLEFT", sec.header, "BOTTOMLEFT", 0, -RQL_BODY_GAP)
|
|
sec.text:SetWidth(RQL_UI.itemDetailContent:GetWidth())
|
|
sec.text:Show()
|
|
totalHeight = totalHeight + RQL_HEADER_GAP + RQL_BODY_GAP + 40
|
|
end
|
|
|
|
RQL_UI.itemDetailContent:SetHeight(totalHeight + 24)
|
|
if RQL_UI.itemDetailScroll and RQL_UI.itemDetailScroll.UpdateScrollChildRect then
|
|
RQL_UI.itemDetailScroll:UpdateScrollChildRect()
|
|
RQL_UI.itemDetailScroll:SetVerticalScroll(0)
|
|
end
|
|
|
|
RQL_UI.itemDetail:Show()
|
|
RelationshipsQuestAndItemBrowserFrame:Show()
|
|
end
|
|
|
|
-- Dropdown helper
|
|
local function RQL_MakeDropdown(name, parent, x, y, width, labelText, items, getIndex, setIndex)
|
|
local dd = CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate")
|
|
dd:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
|
|
local label = dd:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
label:SetPoint("BOTTOMLEFT", dd, "TOPLEFT", 18, 2)
|
|
label:SetText(labelText)
|
|
UIDropDownMenu_SetWidth(width, dd)
|
|
local function init()
|
|
for i, txt in ipairs(items) do
|
|
local info = {}
|
|
info.text = txt
|
|
info.value = i - 1
|
|
info.checked = nil
|
|
info.func = function()
|
|
setIndex(this.value)
|
|
UIDropDownMenu_SetSelectedValue(dd, this.value)
|
|
UIDropDownMenu_SetText(items[this.value + 1], dd)
|
|
RQL_ShowBrowser()
|
|
end
|
|
UIDropDownMenu_AddButton(info)
|
|
end
|
|
end
|
|
UIDropDownMenu_Initialize(dd, init)
|
|
UIDropDownMenu_SetSelectedValue(dd, getIndex())
|
|
UIDropDownMenu_SetText(items[getIndex() + 1], dd)
|
|
return dd
|
|
end
|
|
|
|
local function RQL_BuildUI()
|
|
if RQL_UI.built then return end
|
|
RQL_UI.built = 1
|
|
local parent = RelationshipsQuestAndItemBrowserFrame
|
|
|
|
-- ---------- Tab bar (Quests / Items) ----------
|
|
local tabQuests = CreateFrame("Button", "RelationshipsQuestAndItemBrowserTabQuests", parent, "UIPanelButtonTemplate")
|
|
tabQuests:SetWidth(90) tabQuests:SetHeight(22)
|
|
tabQuests:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -42)
|
|
tabQuests:SetText("Quests")
|
|
tabQuests:SetScript("OnClick", function() RQL_ShowBrowser() end)
|
|
RQL_UI.tabQuests = tabQuests
|
|
|
|
local tabItems = CreateFrame("Button", "RelationshipsQuestAndItemBrowserTabItems", parent, "UIPanelButtonTemplate")
|
|
tabItems:SetWidth(90) tabItems:SetHeight(22)
|
|
tabItems:SetPoint("LEFT", tabQuests, "RIGHT", 6, 0)
|
|
tabItems:SetText("Items")
|
|
tabItems:SetScript("OnClick", function() RQL_ShowItemBrowser() end)
|
|
RQL_UI.tabItems = tabItems
|
|
|
|
-- ---------- Parchment background (quest-log style) ----------
|
|
-- The vanilla client ships Interface\QuestFrame\QuestBG as a 512x512
|
|
-- parchment. We tile a 2x2 grid so it covers the whole frame interior
|
|
-- regardless of frame size. Layered in BACKGROUND so it renders over
|
|
-- the plain dialog backdrop but under all content.
|
|
-- Darker parchment-brown background for better text contrast.
|
|
local bg = parent:CreateTexture(nil, "BACKGROUND")
|
|
-- Dark gold solid fill for the "Default" background. Chosen for good
|
|
-- contrast against white/yellow text without relying on a texture path
|
|
-- (some 1.12 clients fail to load Interface\QuestFrame\QuestBG and
|
|
-- would render fully transparent).
|
|
bg:SetTexture(0.32, 0.24, 0.10)
|
|
bg:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, -12)
|
|
bg:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -12, 12)
|
|
RQL_UI.bgParchment = bg
|
|
|
|
|
|
-- controls container
|
|
local controls = CreateFrame("Frame", nil, parent)
|
|
controls:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
|
|
controls:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -22, -84)
|
|
controls:SetHeight(132)
|
|
RQL_UI.controls = controls
|
|
|
|
local searchLabel = controls:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
searchLabel:SetPoint("TOPLEFT", controls, "TOPLEFT", 0, 0)
|
|
searchLabel:SetText("Search:")
|
|
local edit = CreateFrame("EditBox", "RelationshipsQuestAndItemBrowserSearchBox", controls, "InputBoxTemplate")
|
|
edit:SetHeight(20)
|
|
edit:SetWidth(380)
|
|
edit:SetPoint("TOPLEFT", controls, "TOPLEFT", 60, 2)
|
|
edit:SetAutoFocus(nil)
|
|
edit:SetScript("OnEnterPressed", function()
|
|
RQL_SEARCH_TEXT = this:GetText() or ""
|
|
this:ClearFocus()
|
|
RQL_ShowBrowser()
|
|
end)
|
|
edit:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
|
edit:SetScript("OnTextChanged", function()
|
|
RQL_SEARCH_TEXT = this:GetText() or ""
|
|
RQL_SearchQuests()
|
|
RQL_UpdateStatus()
|
|
RQL_UpdateList()
|
|
end)
|
|
RQL_UI.search = edit
|
|
|
|
local btnSearch = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
|
|
btnSearch:SetWidth(70) btnSearch:SetHeight(22)
|
|
btnSearch:SetPoint("LEFT", edit, "RIGHT", 8, 0)
|
|
btnSearch:SetText("Search")
|
|
btnSearch:SetScript("OnClick", function()
|
|
RQL_SEARCH_TEXT = edit:GetText() or ""
|
|
RQL_ShowBrowser()
|
|
end)
|
|
|
|
local btnReset = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
|
|
btnReset:SetWidth(70) btnReset:SetHeight(22)
|
|
btnReset:SetPoint("LEFT", btnSearch, "RIGHT", 4, 0)
|
|
btnReset:SetText("Reset")
|
|
btnReset:SetScript("OnClick", function()
|
|
RQL_SEARCH_TEXT = ""
|
|
RQL_LEVEL_FILTER = 0; RQL_TYPE_FILTER = 0
|
|
RQL_FACTION_FILTER = 0; RQL_SOURCE_FILTER = 0
|
|
edit:SetText("")
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddLevel, 0); UIDropDownMenu_SetText(RQL_LEVELS[1], RQL_UI.ddLevel)
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddType, 0); UIDropDownMenu_SetText(RQL_TYPES[1], RQL_UI.ddType)
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddFaction, 0); UIDropDownMenu_SetText(RQL_FACTIONS[1], RQL_UI.ddFaction)
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddSource, 0); UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddSource)
|
|
RQL_ShowBrowser()
|
|
end)
|
|
|
|
-- ---------- Options: background color ----------
|
|
-- Colored overlay approach: leave the parchment backdrop untouched
|
|
-- (so "Default" is truly the original look) and lay a solid-color
|
|
-- texture over it inside the border. Hidden when Default is chosen.
|
|
RQL_BG_PRESETS = {
|
|
{ name = "Parchment", r = 0.00, g = 0.00, b = 0.00, a = 0.00 },
|
|
|
|
{ name = "Light Sepia", r = 0.8428, g = 0.7448, b = 0.5880, a = 1.00 },
|
|
{ name = "Sepia", r = 0.38, g = 0.29, b = 0.20, a = 1.00 },
|
|
|
|
{ name = "Charcoal", r = 0.25, g = 0.25, b = 0.26, a = 1.00 },
|
|
{ name = "Black", r = 0.14, g = 0.14, b = 0.15, a = 1.00 },
|
|
|
|
{ name = "Midnight Blue", r = 0.13, g = 0.20, b = 0.36, a = 1.00 },
|
|
{ name = "Forest Green", r = 0.12, g = 0.28, b = 0.18, a = 1.00 },
|
|
{ name = "Deep Red", r = 0.40, g = 0.13, b = 0.13, a = 1.00 },
|
|
{ name = "Royal Purple", r = 0.31, g = 0.15, b = 0.42, a = 1.00 },
|
|
}
|
|
|
|
-- Create the overlay once, parented to the main frame, sitting just
|
|
-- inside the DialogBox border insets (11/12/12/11) and BELOW all UI
|
|
-- content by using the BACKGROUND layer with a low sublevel.
|
|
if RelationshipsQuestAndItemBrowserFrame and not RQL_UI.bgOverlay then
|
|
-- Draw on BORDER layer (above the DialogBox parchment backdrop, below
|
|
-- ARTWORK/OVERLAY content) so the selected color fully covers the
|
|
-- parchment. Earlier this sat on BACKGROUND at sublevel 0, where on
|
|
-- some 1.12 / Turtle builds it rendered UNDER the parchment bgFile
|
|
-- and blended through it, making colors look muddy / "combined".
|
|
local ov = RelationshipsQuestAndItemBrowserFrame:CreateTexture(nil, "BORDER")
|
|
ov:SetTexture(0, 0, 0, 0)
|
|
ov:SetPoint("TOPLEFT", RelationshipsQuestAndItemBrowserFrame, "TOPLEFT", 11, -12)
|
|
ov:SetPoint("BOTTOMRIGHT", RelationshipsQuestAndItemBrowserFrame, "BOTTOMRIGHT", -12, 11)
|
|
ov:Hide()
|
|
RQL_UI.bgOverlay = ov
|
|
end
|
|
|
|
function RQL_ApplyBackground(name)
|
|
local preset = RQL_BG_PRESETS[1]
|
|
for i = 1, table.getn(RQL_BG_PRESETS) do
|
|
if RQL_BG_PRESETS[i].name == name then preset = RQL_BG_PRESETS[i] break end
|
|
end
|
|
local ov = RQL_UI.bgOverlay
|
|
if ov then
|
|
if preset.a and preset.a > 0 then
|
|
ov:SetTexture(preset.r, preset.g, preset.b, preset.a)
|
|
-- Belt-and-braces: some 1.12 clients cache the previous
|
|
-- color when SetTexture is called with new r,g,b on an
|
|
-- already-solid texture, producing a blended / wrong hue.
|
|
if ov.SetVertexColor then
|
|
ov:SetVertexColor(preset.r, preset.g, preset.b, preset.a)
|
|
end
|
|
ov:Show()
|
|
else
|
|
ov:Hide()
|
|
end
|
|
end
|
|
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
|
|
RelationshipsQuestAndItemBrowserDB.bgColor = preset.name
|
|
end
|
|
|
|
local btnOptions = CreateFrame("Button", "RelationshipsQuestAndItemBrowserOptionsBtn", controls, "UIPanelButtonTemplate")
|
|
btnOptions:SetWidth(80) btnOptions:SetHeight(22)
|
|
btnOptions:SetPoint("LEFT", btnReset, "RIGHT", 4, 0)
|
|
btnOptions:SetText("Options")
|
|
|
|
-- A tiny custom menu is more reliable than UIDropDownMenu for this client.
|
|
-- Some 1.12/Turtle builds reuse dropdown button state in a way that makes
|
|
-- color clicks intermittently miss or call the wrong handler.
|
|
local optMenu = CreateFrame("Frame", "RelationshipsQuestAndItemBrowserOptionsMenu", parent)
|
|
optMenu:SetWidth(170)
|
|
optMenu:SetHeight(30 + (table.getn(RQL_BG_PRESETS) * 20))
|
|
optMenu:SetPoint("TOPLEFT", btnOptions, "BOTTOMLEFT", 0, -2)
|
|
optMenu:SetFrameStrata("DIALOG")
|
|
optMenu:SetBackdrop({
|
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
|
tile = true,
|
|
tileSize = 32,
|
|
edgeSize = 16,
|
|
insets = { left = 5, right = 5, top = 5, bottom = 5 }
|
|
})
|
|
optMenu:EnableMouse(true)
|
|
optMenu:Hide()
|
|
RQL_UI.optionsMenu = optMenu
|
|
|
|
local optTitle = optMenu:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
optTitle:SetPoint("TOPLEFT", optMenu, "TOPLEFT", 12, -10)
|
|
optTitle:SetText("Background Color")
|
|
|
|
for i = 1, table.getn(RQL_BG_PRESETS) do
|
|
local preset = RQL_BG_PRESETS[i]
|
|
local row = CreateFrame("Button", nil, optMenu)
|
|
row:SetWidth(146)
|
|
row:SetHeight(18)
|
|
row:SetPoint("TOPLEFT", optMenu, "TOPLEFT", 12, -10 - (i * 20))
|
|
row.presetName = preset.name
|
|
|
|
local swatch = row:CreateTexture(nil, "ARTWORK")
|
|
swatch:SetWidth(12)
|
|
swatch:SetHeight(12)
|
|
swatch:SetPoint("LEFT", row, "LEFT", 0, 0)
|
|
if preset.a and preset.a > 0 then
|
|
swatch:SetTexture(preset.r, preset.g, preset.b)
|
|
else
|
|
swatch:SetTexture(0.32, 0.24, 0.10)
|
|
end
|
|
|
|
local text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
text:SetPoint("LEFT", swatch, "RIGHT", 6, 0)
|
|
text:SetText(preset.name)
|
|
row.text = text
|
|
|
|
row:SetScript("OnEnter", function()
|
|
if this.text then this.text:SetTextColor(1, 1, 0.4) end
|
|
end)
|
|
row:SetScript("OnLeave", function()
|
|
if this.text then this.text:SetTextColor(1, 1, 1) end
|
|
end)
|
|
row:SetScript("OnClick", function()
|
|
if this.presetName then RQL_ApplyBackground(this.presetName) end
|
|
if RQL_UI.optionsMenu then RQL_UI.optionsMenu:Hide() end
|
|
end)
|
|
end
|
|
|
|
btnOptions:SetScript("OnClick", function()
|
|
if RQL_UI.optionsMenu and RQL_UI.optionsMenu:IsShown() then
|
|
RQL_UI.optionsMenu:Hide()
|
|
elseif RQL_UI.optionsMenu then
|
|
RQL_UI.optionsMenu:Show()
|
|
end
|
|
end)
|
|
|
|
-- Filter dropdowns — spaced to fit inside a 720-wide frame
|
|
RQL_UI.ddLevel = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDLevel", controls, -12, -72, 90, "Level", RQL_LEVELS, function() return RQL_LEVEL_FILTER end, function(v) RQL_LEVEL_FILTER = v end)
|
|
RQL_UI.ddType = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDType", controls, 128, -72, 130, "Type", RQL_TYPES, function() return RQL_TYPE_FILTER end, function(v) RQL_TYPE_FILTER = v end)
|
|
RQL_UI.ddFaction = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDFaction", controls, 298, -72, 120, "Faction", RQL_FACTIONS, function() return RQL_FACTION_FILTER end, function(v) RQL_FACTION_FILTER = v end)
|
|
RQL_UI.ddSource = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDSource", controls, 458, -72, 130, "Source", RQL_SOURCES, function() return RQL_SOURCE_FILTER end, function(v) RQL_SOURCE_FILTER = v end)
|
|
|
|
local status = controls:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
status:SetPoint("TOPLEFT", controls, "TOPLEFT", 0, -114)
|
|
status:SetText("")
|
|
RQL_UI.status = status
|
|
|
|
-- pager (bottom)
|
|
local pager = CreateFrame("Frame", nil, parent)
|
|
pager:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
|
|
pager:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 16)
|
|
pager:SetHeight(24)
|
|
RQL_UI.pager = pager
|
|
|
|
local btnPrev = CreateFrame("Button", nil, pager, "UIPanelButtonTemplate")
|
|
btnPrev:SetWidth(110) btnPrev:SetHeight(22)
|
|
btnPrev:SetPoint("LEFT", pager, "LEFT", 0, 0)
|
|
btnPrev:SetText("<< Prev Page")
|
|
btnPrev:SetScript("OnClick", function()
|
|
RQL_SetCurrentPage(RQL_CurrentPage() - 1)
|
|
RQL_UpdateList()
|
|
RQL_UpdateStatus()
|
|
end)
|
|
RQL_UI.btnPrev = btnPrev
|
|
|
|
local btnNext = CreateFrame("Button", nil, pager, "UIPanelButtonTemplate")
|
|
btnNext:SetWidth(110) btnNext:SetHeight(22)
|
|
btnNext:SetPoint("RIGHT", pager, "RIGHT", 0, 0)
|
|
btnNext:SetText("Next Page >>")
|
|
btnNext:SetScript("OnClick", function()
|
|
RQL_SetCurrentPage(RQL_CurrentPage() + 1)
|
|
RQL_UpdateList()
|
|
RQL_UpdateStatus()
|
|
end)
|
|
RQL_UI.btnNext = btnNext
|
|
|
|
local pageLabel = pager:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
|
pageLabel:SetPoint("CENTER", pager, "CENTER", 0, 0)
|
|
pageLabel:SetText("Page 1 / 1")
|
|
RQL_UI.pageLabel = pageLabel
|
|
|
|
-- list area
|
|
local listArea = CreateFrame("Frame", nil, parent)
|
|
listArea:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -224)
|
|
listArea:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
|
|
RQL_UI.listArea = listArea
|
|
|
|
RQL_UI.rows = {}
|
|
for i = 1, RQL_ROWS_VISIBLE do
|
|
local row = CreateFrame("Button", nil, listArea)
|
|
row:SetHeight(RQL_ROW_HEIGHT)
|
|
if i == 1 then
|
|
row:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0)
|
|
row:SetPoint("TOPRIGHT", listArea, "TOPRIGHT", 0, 0)
|
|
else
|
|
row:SetPoint("TOPLEFT", RQL_UI.rows[i-1], "BOTTOMLEFT", 0, 0)
|
|
row:SetPoint("TOPRIGHT", RQL_UI.rows[i-1], "BOTTOMRIGHT", 0, 0)
|
|
end
|
|
local hl = row:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAllPoints(row)
|
|
row:SetHighlightTexture(hl)
|
|
|
|
local right = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
right:SetPoint("RIGHT", row, "RIGHT", -4, 0)
|
|
right:SetJustifyH("RIGHT")
|
|
right:SetHeight(RQL_ROW_HEIGHT)
|
|
right:SetWidth(240)
|
|
right:SetNonSpaceWrap(false)
|
|
row.right = right
|
|
|
|
local left = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
left:SetPoint("LEFT", row, "LEFT", 4, 0)
|
|
left:SetPoint("RIGHT", right, "LEFT", -8, 0)
|
|
left:SetJustifyH("LEFT")
|
|
left:SetHeight(RQL_ROW_HEIGHT)
|
|
left:SetNonSpaceWrap(false)
|
|
row.left = left
|
|
|
|
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
row:SetScript("OnClick", function()
|
|
if this.itemId then
|
|
RQL_ShowItem(this.itemId, this.itemName)
|
|
return
|
|
end
|
|
if not this.questId then return end
|
|
RQL_ShowQuest(this.questId, this.questLevel, this.questTitle)
|
|
end)
|
|
RQL_UI.rows[i] = row
|
|
end
|
|
|
|
-- ---------- Detail view (quest-log style) ----------
|
|
local detail = CreateFrame("Frame", nil, parent)
|
|
detail:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
|
|
detail:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
|
|
detail:Hide()
|
|
RQL_UI.detail = detail
|
|
|
|
local dscroll = CreateFrame("ScrollFrame", "RelationshipsQuestAndItemBrowserDetailScroll", detail, "UIPanelScrollFrameTemplate")
|
|
dscroll:SetPoint("TOPLEFT", detail, "TOPLEFT", RQL_DETAIL_PAD, -RQL_DETAIL_PAD)
|
|
dscroll:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", -(RQL_DETAIL_PAD + 24), RQL_DETAIL_PAD)
|
|
RQL_UI.detailScroll = dscroll
|
|
dscroll:EnableMouseWheel(true)
|
|
dscroll:SetScript("OnMouseWheel", function()
|
|
local step = 24
|
|
local cur = this:GetVerticalScroll()
|
|
local maxs = this:GetVerticalScrollRange()
|
|
local nv = cur - (arg1 * step)
|
|
if nv < 0 then nv = 0 end
|
|
if nv > maxs then nv = maxs end
|
|
this:SetVerticalScroll(nv)
|
|
end)
|
|
|
|
local dcontent = CreateFrame("Frame", nil, dscroll)
|
|
-- Inner width: frame(720) - 2*margin(22) - 2*pad(18) - scrollbar(24) ≈ 616
|
|
dcontent:SetWidth(612) dcontent:SetHeight(1)
|
|
dscroll:SetScrollChild(dcontent)
|
|
RQL_UI.detailContent = dcontent
|
|
|
|
-- Title
|
|
local dTitle = dcontent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
|
|
dTitle:SetPoint("TOPLEFT", dcontent, "TOPLEFT", 0, 0)
|
|
dTitle:SetWidth(612)
|
|
dTitle:SetJustifyH("LEFT")
|
|
RQL_UI.dTitle = dTitle
|
|
|
|
-- Meta line
|
|
local dMeta = dcontent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
dMeta:SetPoint("TOPLEFT", dTitle, "BOTTOMLEFT", 0, -10)
|
|
dMeta:SetWidth(612)
|
|
dMeta:SetJustifyH("LEFT")
|
|
RQL_UI.dMeta = dMeta
|
|
|
|
-- Sections
|
|
RQL_UI.dObjSection = RQL_MakeDetailSection(dcontent)
|
|
RQL_UI.dDescSection = RQL_MakeDetailSection(dcontent)
|
|
RQL_UI.dLocSection = RQL_MakeDetailSection(dcontent)
|
|
RQL_UI.dPreSection = RQL_MakeDetailSection(dcontent)
|
|
RQL_UI.dItemSection = RQL_MakeDetailSection(dcontent)
|
|
|
|
local dFooter = dcontent:CreateFontString(nil, "OVERLAY", "QuestFont")
|
|
dFooter:SetWidth(612)
|
|
dFooter:SetJustifyH("LEFT")
|
|
dFooter:Hide()
|
|
RQL_UI.dFooter = dFooter
|
|
|
|
local btnBack = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
|
|
btnBack:SetWidth(90) btnBack:SetHeight(22)
|
|
btnBack:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
|
|
btnBack:SetText("Back")
|
|
btnBack:SetScript("OnClick", function()
|
|
local savedPage = RQL_PAGE
|
|
RQL_ShowBrowser()
|
|
RQL_PAGE = savedPage
|
|
RQL_UpdateList()
|
|
end)
|
|
|
|
local btnLink = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
|
|
btnLink:SetWidth(130) btnLink:SetHeight(22)
|
|
btnLink:SetPoint("LEFT", btnBack, "RIGHT", 6, 0)
|
|
btnLink:SetText("Link to Chat")
|
|
btnLink:SetScript("OnClick", function()
|
|
if RQL_SELECTED_QUEST_ID then
|
|
RQL_LinkQuestToChat(RQL_SELECTED_QUEST_ID, nil, nil)
|
|
end
|
|
end)
|
|
btnLink:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_TOP")
|
|
GameTooltip:AddLine("Insert a clickable quest link into the chat edit box.")
|
|
GameTooltip:AddLine("Tip: shift-click a quest in the list to link it directly.", 1,1,1)
|
|
GameTooltip:Show()
|
|
end)
|
|
btnLink:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
detail:SetScript("OnShow", function() btnBack:Show(); btnLink:Show() end)
|
|
detail:SetScript("OnHide", function() btnBack:Hide(); btnLink:Hide() end)
|
|
btnBack:Hide()
|
|
btnLink:Hide()
|
|
|
|
-- ---------- Item controls (separate search bar for items) ----------
|
|
local iControls = CreateFrame("Frame", nil, parent)
|
|
iControls:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
|
|
iControls:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -22, -84)
|
|
iControls:SetHeight(132)
|
|
iControls:Hide()
|
|
RQL_UI.itemControls = iControls
|
|
|
|
local iSearchLabel = iControls:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
iSearchLabel:SetPoint("TOPLEFT", iControls, "TOPLEFT", 0, 0)
|
|
iSearchLabel:SetText("Item Search:")
|
|
|
|
local iEdit = CreateFrame("EditBox", "RelationshipsQuestAndItemBrowserItemSearchBox", iControls, "InputBoxTemplate")
|
|
iEdit:SetHeight(20)
|
|
iEdit:SetWidth(380)
|
|
iEdit:SetPoint("TOPLEFT", iControls, "TOPLEFT", 78, 2)
|
|
iEdit:SetAutoFocus(nil)
|
|
iEdit:SetScript("OnEnterPressed", function()
|
|
RQL_ITEM_SEARCH_TEXT = this:GetText() or ""
|
|
this:ClearFocus()
|
|
RQL_ShowItemBrowser()
|
|
end)
|
|
iEdit:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
|
iEdit:SetScript("OnTextChanged", function()
|
|
RQL_ITEM_SEARCH_TEXT = this:GetText() or ""
|
|
RQL_SearchItems()
|
|
RQL_UpdateStatus()
|
|
RQL_UpdateList()
|
|
end)
|
|
RQL_UI.itemSearch = iEdit
|
|
|
|
local iBtnSearch = CreateFrame("Button", nil, iControls, "UIPanelButtonTemplate")
|
|
iBtnSearch:SetWidth(70) iBtnSearch:SetHeight(22)
|
|
iBtnSearch:SetPoint("LEFT", iEdit, "RIGHT", 8, 0)
|
|
iBtnSearch:SetText("Search")
|
|
iBtnSearch:SetScript("OnClick", function()
|
|
RQL_ITEM_SEARCH_TEXT = iEdit:GetText() or ""
|
|
RQL_ShowItemBrowser()
|
|
end)
|
|
|
|
local iBtnReset = CreateFrame("Button", nil, iControls, "UIPanelButtonTemplate")
|
|
iBtnReset:SetWidth(70) iBtnReset:SetHeight(22)
|
|
iBtnReset:SetPoint("LEFT", iBtnSearch, "RIGHT", 4, 0)
|
|
iBtnReset:SetText("Reset")
|
|
iBtnReset:SetScript("OnClick", function()
|
|
RQL_ITEM_SEARCH_TEXT = ""
|
|
RQL_ITEM_SOURCE_FILTER = 0
|
|
iEdit:SetText("")
|
|
if RQL_UI.ddItemSource then
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, 0)
|
|
UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddItemSource)
|
|
end
|
|
RQL_ShowItemBrowser()
|
|
end)
|
|
|
|
RQL_UI.ddItemSource = RQL_MakeDropdown(
|
|
"RelationshipsQuestAndItemBrowserDDItemSource", iControls, -12, -72, 130, "Source", RQL_SOURCES,
|
|
function() return RQL_ITEM_SOURCE_FILTER end,
|
|
function(v) RQL_ITEM_SOURCE_FILTER = v end)
|
|
|
|
-- The RQL_MakeDropdown reset flow calls RQL_ShowBrowser after selection.
|
|
-- Rewrite the item-source dropdown init so it refreshes the ITEM browser.
|
|
UIDropDownMenu_Initialize(RQL_UI.ddItemSource, function()
|
|
for i, txt in ipairs(RQL_SOURCES) do
|
|
local info = {}
|
|
info.text = txt
|
|
info.value = i - 1
|
|
info.func = function()
|
|
RQL_ITEM_SOURCE_FILTER = this.value
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, this.value)
|
|
UIDropDownMenu_SetText(RQL_SOURCES[this.value + 1], RQL_UI.ddItemSource)
|
|
RQL_ShowItemBrowser()
|
|
end
|
|
UIDropDownMenu_AddButton(info)
|
|
end
|
|
end)
|
|
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, 0)
|
|
UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddItemSource)
|
|
|
|
local iStatus = iControls:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
iStatus:SetPoint("TOPLEFT", iControls, "TOPLEFT", 0, -114)
|
|
iStatus:SetText("")
|
|
RQL_UI.itemStatus = iStatus
|
|
|
|
-- ---------- Item detail view ----------
|
|
local iDetail = CreateFrame("Frame", nil, parent)
|
|
iDetail:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
|
|
iDetail:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
|
|
iDetail:Hide()
|
|
RQL_UI.itemDetail = iDetail
|
|
|
|
local iScroll = CreateFrame("ScrollFrame", "RelationshipsQuestAndItemBrowserItemDetailScroll", iDetail, "UIPanelScrollFrameTemplate")
|
|
iScroll:SetPoint("TOPLEFT", iDetail, "TOPLEFT", RQL_DETAIL_PAD, -RQL_DETAIL_PAD)
|
|
iScroll:SetPoint("BOTTOMRIGHT", iDetail, "BOTTOMRIGHT", -(RQL_DETAIL_PAD + 24), RQL_DETAIL_PAD)
|
|
iScroll:EnableMouseWheel(true)
|
|
iScroll:SetScript("OnMouseWheel", function()
|
|
local step = 24
|
|
local cur = this:GetVerticalScroll()
|
|
local maxs = this:GetVerticalScrollRange()
|
|
local nv = cur - (arg1 * step)
|
|
if nv < 0 then nv = 0 end
|
|
if nv > maxs then nv = maxs end
|
|
this:SetVerticalScroll(nv)
|
|
end)
|
|
RQL_UI.itemDetailScroll = iScroll
|
|
|
|
local iContent = CreateFrame("Frame", nil, iScroll)
|
|
iContent:SetWidth(612) iContent:SetHeight(1)
|
|
iScroll:SetScrollChild(iContent)
|
|
RQL_UI.itemDetailContent = iContent
|
|
|
|
-- Slot art goes behind the item icon. UI-EmptySlot-Disabled is a full
|
|
-- empty-slot texture (not just a transparent border), so using OVERLAY
|
|
-- made the icon area look like a blank box on Turtle/OctoWoW.
|
|
local iIconBorder = iContent:CreateTexture(nil, "BACKGROUND")
|
|
iIconBorder:SetTexture("Interface\\Buttons\\UI-EmptySlot-Disabled")
|
|
iIconBorder:SetWidth(66) iIconBorder:SetHeight(66)
|
|
iIconBorder:SetPoint("TOPLEFT", iContent, "TOPLEFT", -11, 11)
|
|
RQL_UI.iIconBorder = iIconBorder
|
|
|
|
-- Icon (populated from GetItemInfo in RQL_ShowItem). Sits top-left.
|
|
local iIcon = iContent:CreateTexture(nil, "ARTWORK")
|
|
iIcon:SetWidth(44) iIcon:SetHeight(44)
|
|
iIcon:SetPoint("TOPLEFT", iContent, "TOPLEFT", 0, 0)
|
|
iIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
|
iIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
|
RQL_UI.iIcon = iIcon
|
|
|
|
local iTitle = iContent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
|
|
iTitle:SetPoint("TOPLEFT", iIcon, "TOPRIGHT", 14, -2)
|
|
iTitle:SetWidth(612 - 60)
|
|
iTitle:SetJustifyH("LEFT")
|
|
RQL_UI.iTitle = iTitle
|
|
|
|
local iMeta = iContent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
|
iMeta:SetPoint("TOPLEFT", iTitle, "BOTTOMLEFT", 0, -8)
|
|
iMeta:SetWidth(612 - 60)
|
|
iMeta:SetJustifyH("LEFT")
|
|
RQL_UI.iMeta = iMeta
|
|
|
|
RQL_UI.iQuestSection = RQL_MakeDetailSection(iContent)
|
|
RQL_UI.iQuestButtons = {}
|
|
|
|
local iBtnBack = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
|
|
iBtnBack:SetWidth(90) iBtnBack:SetHeight(22)
|
|
iBtnBack:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
|
|
iBtnBack:SetText("Back")
|
|
iBtnBack:SetScript("OnClick", function()
|
|
local savedPage = RQL_ITEM_PAGE
|
|
RQL_ShowItemBrowser()
|
|
RQL_ITEM_PAGE = savedPage
|
|
RQL_UpdateList()
|
|
end)
|
|
|
|
local iBtnLink = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
|
|
iBtnLink:SetWidth(130) iBtnLink:SetHeight(22)
|
|
iBtnLink:SetPoint("LEFT", iBtnBack, "RIGHT", 6, 0)
|
|
iBtnLink:SetText("Link to Chat")
|
|
iBtnLink:SetScript("OnClick", function()
|
|
if RQL_SELECTED_ITEM_ID then RQL_LinkItemToChat(RQL_SELECTED_ITEM_ID) end
|
|
end)
|
|
|
|
iDetail:SetScript("OnShow", function() iBtnBack:Show(); iBtnLink:Show() end)
|
|
iDetail:SetScript("OnHide", function() iBtnBack:Hide(); iBtnLink:Hide() end)
|
|
iBtnBack:Hide()
|
|
iBtnLink:Hide()
|
|
end
|
|
|
|
-- ---------- Item-detail async cache retry ----------
|
|
-- Vanilla 1.12 clients populate the item cache asynchronously the first
|
|
-- time an item is referenced. We keep probing GameTooltip for up to a few
|
|
-- seconds after opening an item panel and refresh the description as soon
|
|
-- as real tooltip lines come back.
|
|
function RQL_ItemDetailRetryTick(elapsed)
|
|
-- Deferred chat-link insertion: poll GetItemInfo until the client has
|
|
-- cached the real signed hyperlink, then drop it into the edit box.
|
|
if RQL_UI.iLinkPendingFor then
|
|
RQL_UI.iLinkPendingElapsed = (RQL_UI.iLinkPendingElapsed or 0) + (elapsed or 0)
|
|
if RQL_UI.iLinkPendingElapsed >= 0.2 then
|
|
RQL_UI.iLinkPendingElapsed = 0
|
|
RQL_UI.iLinkPendingTries = (RQL_UI.iLinkPendingTries or 0) + 1
|
|
local pid = RQL_UI.iLinkPendingFor
|
|
if RQL_TryInsertItemLink(pid) then
|
|
RQL_UI.iLinkPendingFor = nil
|
|
elseif RQL_UI.iLinkPendingTries >= 25 then
|
|
RQL_UI.iLinkPendingFor = nil
|
|
if DEFAULT_CHAT_FRAME then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6060RQL|r Could not fetch item link from server. Try again in a moment.")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not RQL_UI.iRetryFor then return end
|
|
RQL_UI.iRetryElapsed = (RQL_UI.iRetryElapsed or 0) + (elapsed or 0)
|
|
if RQL_UI.iRetryElapsed < 0.25 then return end
|
|
RQL_UI.iRetryElapsed = 0
|
|
RQL_UI.iRetryTries = (RQL_UI.iRetryTries or 0) + 1
|
|
local id = RQL_UI.iRetryFor
|
|
local lines = RQL_ScanItemTooltipLines(id)
|
|
if lines and table.getn(lines) > 0 then
|
|
RQL_UI.iRetryFor = nil
|
|
if RQL_UI.iDesc and RQL_UI.itemDetail and RQL_UI.itemDetail:IsShown()
|
|
and RQL_SELECTED_ITEM_ID == id then
|
|
RQL_UI.iDesc:SetText(table.concat(lines, "\n"))
|
|
if RQL_UI.itemDetailContent and RQL_UI.itemDetailScroll
|
|
and RQL_UI.itemDetailScroll.UpdateScrollChildRect then
|
|
local h = 40 + 20 + RQL_HEADER_GAP + (RQL_UI.iDesc:GetHeight() or 0) + 60
|
|
local btns = RQL_UI.iQuestButtons or {}
|
|
for i = 1, table.getn(btns) do
|
|
if btns[i]:IsShown() then h = h + 20 end
|
|
end
|
|
RQL_UI.itemDetailContent:SetHeight(h + 24)
|
|
RQL_UI.itemDetailScroll:UpdateScrollChildRect()
|
|
end
|
|
end
|
|
elseif RQL_UI.iRetryTries >= 20 then
|
|
RQL_UI.iRetryFor = nil
|
|
end
|
|
end
|
|
|
|
-- ---------- Item chat linking ----------
|
|
-- Insert a genuine client-cached item link into chat. Vanilla WoW strips
|
|
-- user-typed |Hitem:...|h strings before send -- ONLY the |cXXXX|Hitem:...
|
|
-- link that GetItemInfo returns after the client has cached the item is
|
|
-- treated as a real hyperlink. We warm the cache via SetHyperlink and
|
|
-- retry for a few seconds if needed.
|
|
local function RQL_TryInsertItemLink(id)
|
|
local name, link, quality = GetItemInfo(id)
|
|
if not name then return nil end
|
|
-- On some 1.12-based clients (Turtle WoW / OctoWoW) GetItemInfo can
|
|
-- return the bare itemString ("item:12345:0:0:0") instead of the fully
|
|
-- signed "|cXXXXXXXX|Hitem:...|h[Name]|h|r" hyperlink. If we don't
|
|
-- detect that we would drop raw text into chat that looks like
|
|
-- "item:0:0:0:12345" and is not clickable. Rebuild a proper hyperlink
|
|
-- from the pieces we do have.
|
|
if not link or not string.find(link, "|H") then
|
|
local color = RQL_QUALITY_HEX[quality or 1] or "|cffffffff"
|
|
link = color .. "|Hitem:" .. id .. ":0:0:0|h[" .. name .. "]|h|r"
|
|
end
|
|
if not ChatFrameEditBox then return 1 end
|
|
if not ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Show() end
|
|
ChatFrameEditBox:SetFocus()
|
|
ChatFrameEditBox:Insert(link)
|
|
return 1
|
|
end
|
|
|
|
function RQL_LinkItemToChat(id)
|
|
if not id then return end
|
|
-- Force the client to cache the item so GetItemInfo returns the real
|
|
-- signed link on the next tick.
|
|
RQL_EnsureScanTip()
|
|
RQL_UI.scanTip:ClearLines()
|
|
RQL_UI.scanTip:SetHyperlink("item:" .. id .. ":0:0:0")
|
|
|
|
if RQL_TryInsertItemLink(id) then return end
|
|
|
|
-- Not cached yet -- schedule delayed insertion.
|
|
RQL_UI.iLinkPendingFor = id
|
|
RQL_UI.iLinkPendingElapsed = 0
|
|
RQL_UI.iLinkPendingTries = 0
|
|
if DEFAULT_CHAT_FRAME then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99RQL|r Fetching item data from server, link will drop into chat in a moment...")
|
|
end
|
|
end
|
|
|
|
-- ---------- minimap button ----------
|
|
|
|
local function RQL_SaveMinimapPos()
|
|
if not RQL_UI.minimap then return end
|
|
RelationshipsQuestAndItemBrowserDB.minimap = RelationshipsQuestAndItemBrowserDB.minimap or {}
|
|
local x, y = RQL_UI.minimap:GetCenter()
|
|
if x and y then
|
|
RelationshipsQuestAndItemBrowserDB.minimap.x = x
|
|
RelationshipsQuestAndItemBrowserDB.minimap.y = y
|
|
end
|
|
end
|
|
|
|
local function RQL_RestoreMinimapPos()
|
|
if not RQL_UI.minimap then return end
|
|
local pos = RelationshipsQuestAndItemBrowserDB.minimap
|
|
RQL_UI.minimap:ClearAllPoints()
|
|
if pos and pos.x and pos.y then
|
|
RQL_UI.minimap:SetPoint("CENTER", UIParent, "BOTTOMLEFT", pos.x, pos.y)
|
|
else
|
|
RQL_UI.minimap:SetPoint("TOPLEFT", Minimap, "TOPLEFT", -8, -8)
|
|
end
|
|
end
|
|
|
|
local function RQL_BuildMinimapButton()
|
|
if RQL_UI.minimap then return end
|
|
local btn = CreateFrame("Button", "RelationshipsQuestAndItemBrowserMinimap", UIParent)
|
|
btn:SetFrameStrata("MEDIUM")
|
|
btn:SetWidth(32) btn:SetHeight(32)
|
|
btn:SetMovable(true)
|
|
btn:EnableMouse(true)
|
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
btn:RegisterForDrag("LeftButton")
|
|
|
|
local icon = btn:CreateTexture(nil, "BACKGROUND")
|
|
icon:SetTexture("Interface\\Icons\\INV_Misc_Book_09")
|
|
icon:SetWidth(20) icon:SetHeight(20)
|
|
icon:SetPoint("CENTER", btn, "CENTER", 0, 1)
|
|
|
|
local border = btn:CreateTexture(nil, "OVERLAY")
|
|
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
|
|
border:SetWidth(54) border:SetHeight(54)
|
|
border:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
|
|
|
|
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAllPoints(btn)
|
|
btn:SetHighlightTexture(hl)
|
|
|
|
btn:SetScript("OnClick", function()
|
|
if arg1 == "RightButton" then
|
|
RelationshipsQuestAndItemBrowserDB.minimap = nil
|
|
RQL_RestoreMinimapPos()
|
|
else
|
|
if RelationshipsQuestAndItemBrowserFrame:IsVisible() then
|
|
RelationshipsQuestAndItemBrowserFrame:Hide()
|
|
else
|
|
RQL_ShowBrowser()
|
|
end
|
|
end
|
|
end)
|
|
btn:SetScript("OnDragStart", function() this:StartMoving() end)
|
|
btn:SetScript("OnDragStop", function() this:StopMovingOrSizing(); RQL_SaveMinimapPos() end)
|
|
btn:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
|
|
GameTooltip:AddLine("Relationships Quest And Item Browser")
|
|
GameTooltip:AddLine("Left-click: open browser", 1,1,1)
|
|
GameTooltip:AddLine("Right-click: reset icon to minimap", 1,1,1)
|
|
GameTooltip:AddLine("Drag: move anywhere", 1,1,1)
|
|
GameTooltip:Show()
|
|
end)
|
|
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
RQL_UI.minimap = btn
|
|
RQL_RestoreMinimapPos()
|
|
end
|
|
|
|
-- ---------- chat link plus-button overlay ----------
|
|
--
|
|
-- Behaviour:
|
|
-- * A tiny green [+] Button is overlaid directly on top of every quest /
|
|
-- item hyperlink that is currently visible in a chat frame.
|
|
-- * The [+] tracks the link as chat scrolls: it appears when the message
|
|
-- is on screen and hides when it scrolls off. Switching chat tabs also
|
|
-- hides / shows it, since inactive tabs' frames are hidden.
|
|
-- * Clicking the [+] opens the quest / item in the RQL browser.
|
|
-- * The original chat link is untouched, so default Blizzard tooltip,
|
|
-- quest popup and shift-insert still work.
|
|
--
|
|
-- Why an overlay button instead of an inline "|Hrqlopen:..." link:
|
|
-- Some Turtle / OctoWoW builds don't dispatch clicks for unknown link
|
|
-- types AND refuse to let addons attach an OnHyperlinkShow script to
|
|
-- chat frames. A real Button widget floated over the FontString is the
|
|
-- only mechanism that reliably works on those clients.
|
|
|
|
local RQL_PLUS_BTN_SIZE = 12
|
|
local RQL_PLUS_POOL = {}
|
|
local RQL_PoolIndex = 0
|
|
local RQL_PLUS_MEASURE -- hidden FontString reused to measure prefix widths
|
|
|
|
local function RQL_GetMeasurer(refFontString)
|
|
if not RQL_PLUS_MEASURE then
|
|
RQL_PLUS_MEASURE = UIParent:CreateFontString(nil, "BACKGROUND")
|
|
RQL_PLUS_MEASURE:Hide()
|
|
end
|
|
local obj = refFontString and refFontString.GetFontObject and refFontString:GetFontObject()
|
|
if obj then
|
|
RQL_PLUS_MEASURE:SetFontObject(obj)
|
|
else
|
|
-- Fall back to font path / size directly.
|
|
local path, size, flags
|
|
if refFontString and refFontString.GetFont then path, size, flags = refFontString:GetFont() end
|
|
if path and size then RQL_PLUS_MEASURE:SetFont(path, size, flags) end
|
|
end
|
|
return RQL_PLUS_MEASURE
|
|
end
|
|
|
|
local function RQL_StripCodes(s)
|
|
if type(s) ~= "string" then return "" end
|
|
s = string.gsub(s, "|c%x%x%x%x%x%x%x%x", "")
|
|
s = string.gsub(s, "|r", "")
|
|
s = string.gsub(s, "|T[^|]*|t", "")
|
|
-- Strip hyperlink openers/closers but keep the display text between them,
|
|
-- since that text renders and consumes width in the source FontString.
|
|
s = string.gsub(s, "|H[^|]*|h", "")
|
|
s = string.gsub(s, "|h", "")
|
|
return s
|
|
end
|
|
|
|
local function RQL_PlusOnClick()
|
|
local e = this and this.rqlEntry
|
|
if not e then return end
|
|
if e.kind == "quest" then
|
|
RQL_ShowQuest(e.id, e.level or 0, e.name)
|
|
elseif e.kind == "item" then
|
|
RQL_ShowItem(e.id, e.name)
|
|
end
|
|
end
|
|
|
|
local function RQL_PlusOnEnter()
|
|
if not (this and this.rqlEntry) then return end
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
if this.rqlEntry.kind == "quest" then
|
|
GameTooltip:AddLine("Open quest in RQL browser")
|
|
else
|
|
GameTooltip:AddLine("Open item in RQL browser")
|
|
end
|
|
if this.rqlEntry.name then
|
|
GameTooltip:AddLine(this.rqlEntry.name, 1, 1, 1)
|
|
end
|
|
GameTooltip:Show()
|
|
end
|
|
|
|
local function RQL_PlusOnLeave() GameTooltip:Hide() end
|
|
|
|
local function RQL_AcquirePlusButton()
|
|
RQL_PoolIndex = RQL_PoolIndex + 1
|
|
local btn = RQL_PLUS_POOL[RQL_PoolIndex]
|
|
if not btn then
|
|
btn = CreateFrame("Button", "RQL_PlusButton" .. RQL_PoolIndex, UIParent)
|
|
btn:SetFrameStrata("HIGH")
|
|
btn:SetWidth(RQL_PLUS_BTN_SIZE)
|
|
btn:SetHeight(RQL_PLUS_BTN_SIZE)
|
|
local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
fs:SetAllPoints(btn)
|
|
fs:SetText("|cff33ff99+|r")
|
|
btn:SetFontString(fs)
|
|
local bg = btn:CreateTexture(nil, "BACKGROUND")
|
|
bg:SetAllPoints(btn)
|
|
bg:SetTexture(0, 0, 0, 0.6)
|
|
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetAllPoints(btn)
|
|
hl:SetTexture(1, 1, 1, 0.30)
|
|
btn:SetHighlightTexture(hl)
|
|
btn:SetScript("OnClick", RQL_PlusOnClick)
|
|
btn:SetScript("OnEnter", RQL_PlusOnEnter)
|
|
btn:SetScript("OnLeave", RQL_PlusOnLeave)
|
|
RQL_PLUS_POOL[RQL_PoolIndex] = btn
|
|
end
|
|
return btn
|
|
end
|
|
|
|
local function RQL_HideUnusedButtons()
|
|
local i = RQL_PoolIndex + 1
|
|
while RQL_PLUS_POOL[i] do
|
|
RQL_PLUS_POOL[i]:Hide()
|
|
RQL_PLUS_POOL[i].rqlEntry = nil
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
-- Word-wrap `plain` using the same width the FontString wraps at, and
|
|
-- return (lineCount, lastLineWidth, lineHeight). Used so the [+] follows a
|
|
-- message onto its wrapped continuation line instead of flying off the right
|
|
-- edge of the chat frame at the end of the (never-rendered) full-width line.
|
|
local function RQL_WrapMetrics(fs, plain)
|
|
local measurer = RQL_GetMeasurer(fs)
|
|
local _, fontSize = fs:GetFont()
|
|
fontSize = fontSize or 12
|
|
local spacing = 0
|
|
if fs.GetSpacing then spacing = fs:GetSpacing() or 0 end
|
|
local lineHeight = fontSize + spacing
|
|
|
|
local maxW = 0
|
|
if fs.GetWidth then maxW = fs:GetWidth() or 0 end
|
|
if maxW <= 0 then
|
|
local L, R = fs:GetLeft(), fs:GetRight()
|
|
if L and R then maxW = R - L end
|
|
end
|
|
|
|
measurer:SetText(plain or "")
|
|
local totalW = measurer:GetStringWidth() or 0
|
|
if maxW <= 0 or totalW <= maxW then
|
|
return 1, totalW, lineHeight
|
|
end
|
|
|
|
local lines = 1
|
|
local lineText = ""
|
|
local lineW = totalW
|
|
local i = 1
|
|
local len = string.len(plain)
|
|
while i <= len do
|
|
local sp = string.find(plain, " ", i, true)
|
|
local word
|
|
if sp then
|
|
word = string.sub(plain, i, sp)
|
|
i = sp + 1
|
|
else
|
|
word = string.sub(plain, i)
|
|
i = len + 1
|
|
end
|
|
local candidate = lineText .. word
|
|
measurer:SetText(candidate)
|
|
local cw = measurer:GetStringWidth() or 0
|
|
if cw > maxW and lineText ~= "" then
|
|
lines = lines + 1
|
|
lineText = word
|
|
measurer:SetText(lineText)
|
|
lineW = measurer:GetStringWidth() or 0
|
|
else
|
|
lineText = candidate
|
|
lineW = cw
|
|
end
|
|
end
|
|
return lines, lineW, lineHeight
|
|
end
|
|
|
|
-- Place a [+] button at the end of the message's LAST rendered line (which
|
|
-- may be a wrapped continuation, not the source string's true end). Multiple
|
|
-- [+] icons for a message stagger horizontally past that anchor, and wrap
|
|
-- down to a further line if they would otherwise exit the chat frame's
|
|
-- right edge, so nothing gets clipped or pushed off the frame.
|
|
local function RQL_PlaceButton(fs, anchorText, entry, frame, stagger)
|
|
if not fs or not fs.GetLeft then return end
|
|
local left = fs:GetLeft()
|
|
local top = fs:GetTop()
|
|
if not left or not top then return end
|
|
|
|
local frameLeft = frame and frame:GetLeft()
|
|
local frameRight = frame and frame:GetRight()
|
|
|
|
local plain = RQL_StripCodes(anchorText)
|
|
local lines, lastW, lineH = RQL_WrapMetrics(fs, plain)
|
|
|
|
local gap = 2
|
|
local step = RQL_PLUS_BTN_SIZE + 2
|
|
local baseX = left + lastW + gap
|
|
local baseY = top - 1 - (lines - 1) * lineH
|
|
|
|
local x = baseX + (stagger or 0) * step
|
|
local y = baseY
|
|
|
|
-- If the staggered [+] would run past the chat frame's right edge,
|
|
-- wrap it to a new line beneath the message rather than clipping it.
|
|
if frameRight and (x + RQL_PLUS_BTN_SIZE) > frameRight then
|
|
local firstRowSlots = math.floor((frameRight - baseX) / step)
|
|
if firstRowSlots < 0 then firstRowSlots = 0 end
|
|
local wrapLeft = frameLeft or left
|
|
local wrapRowSlots = math.floor((frameRight - wrapLeft) / step)
|
|
if wrapRowSlots < 1 then wrapRowSlots = 1 end
|
|
local s = stagger or 0
|
|
if s < firstRowSlots then
|
|
x = baseX + s * step
|
|
y = baseY
|
|
else
|
|
local over = s - firstRowSlots
|
|
local row = 1 + math.floor(over / wrapRowSlots)
|
|
local col = over - (row - 1) * wrapRowSlots
|
|
x = wrapLeft + col * step
|
|
y = baseY - row * step
|
|
end
|
|
end
|
|
|
|
if frameLeft and x < frameLeft then x = frameLeft end
|
|
|
|
local btn = RQL_AcquirePlusButton()
|
|
btn:ClearAllPoints()
|
|
-- Position from UIParent BOTTOMLEFT using the FontString's absolute top.
|
|
btn:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
|
|
btn.rqlEntry = entry
|
|
btn:Show()
|
|
end
|
|
|
|
local function RQL_ProcessChatFrame(frame)
|
|
if not frame or not frame.IsVisible or not frame:IsVisible() then return end
|
|
if not frame.GetRegions then return end
|
|
local regions = { frame:GetRegions() }
|
|
local n = table.getn(regions)
|
|
for i = 1, n do
|
|
local r = regions[i]
|
|
if r and r.GetObjectType and r:GetObjectType() == "FontString"
|
|
and r.IsVisible and r:IsVisible() then
|
|
local t = r:GetText()
|
|
if t and string.find(t, "|H", 1, 1) then
|
|
-- Collect every quest/item link with its span in `t`, then
|
|
-- place one [+] per link. Position rule:
|
|
-- * link is followed by more text in the same line -> put
|
|
-- the [+] after the FULL message text (last character)
|
|
-- * link is at the very end of the message -> put
|
|
-- the [+] immediately after the link's display text
|
|
local matches = {}
|
|
local pos = 1
|
|
while 1 do
|
|
local s, e, id, lvl, name = string.find(t,
|
|
"|Hquest:(%d+):(%-?%d+)|h%[([^%]]*)%]|h", pos)
|
|
if not s then break end
|
|
table.insert(matches, {
|
|
s = s, e = e,
|
|
entry = { kind = "quest", id = tonumber(id),
|
|
level = tonumber(lvl), name = name },
|
|
})
|
|
pos = e + 1
|
|
end
|
|
pos = 1
|
|
while 1 do
|
|
local s, e, id, name = string.find(t,
|
|
"|Hitem:(%d+)[^|]*|h%[([^%]]*)%]|h", pos)
|
|
if not s then break end
|
|
table.insert(matches, {
|
|
s = s, e = e,
|
|
entry = { kind = "item", id = tonumber(id), name = name },
|
|
})
|
|
pos = e + 1
|
|
end
|
|
table.sort(matches, function(a, b) return a.s < b.s end)
|
|
|
|
-- Place all [+] buttons grouped together at the end of the
|
|
-- message (after the last character / link) so multiple linked
|
|
-- quests / items get a run of plus icons like "+++" rather
|
|
-- than being spread throughout the line. `stagger` shifts each
|
|
-- subsequent button one icon-width to the right.
|
|
for j = 1, table.getn(matches) do
|
|
RQL_PlaceButton(r, t, matches[j].entry, frame, j - 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local RQL_PLUS_TIMER = 0
|
|
function RQL_PlusTick(elapsed)
|
|
RQL_PLUS_TIMER = RQL_PLUS_TIMER + (elapsed or 0)
|
|
if RQL_PLUS_TIMER < 0.03 then return end
|
|
RQL_PLUS_TIMER = 0
|
|
RQL_PoolIndex = 0
|
|
local maxFrames = NUM_CHAT_WINDOWS or 7
|
|
for i = 1, maxFrames do
|
|
local f = getglobal("ChatFrame" .. i)
|
|
if f then RQL_ProcessChatFrame(f) end
|
|
end
|
|
RQL_HideUnusedButtons()
|
|
end
|
|
|
|
local function RQL_HookChatLinks()
|
|
-- Nothing to hook up-front; the OnUpdate ticker in the event handler
|
|
-- calls RQL_PlusTick() every frame and it drives everything else.
|
|
end
|
|
-- ---------- events / slash ----------
|
|
|
|
RelationshipsQuestAndItemBrowser:SetScript("OnEvent", function()
|
|
if event == "ADDON_LOADED" and arg1 == "RelationshipsQuestAndItemBrowser" then
|
|
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
|
|
elseif event == "PLAYER_LOGOUT" then
|
|
if RQL_UI and RQL_UI.minimap then
|
|
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
|
|
RelationshipsQuestAndItemBrowserDB.minimap = RelationshipsQuestAndItemBrowserDB.minimap or {}
|
|
local cx, cy = RQL_UI.minimap:GetCenter()
|
|
if cx and cy then
|
|
RelationshipsQuestAndItemBrowserDB.minimap.x = cx
|
|
RelationshipsQuestAndItemBrowserDB.minimap.y = cy
|
|
end
|
|
end
|
|
elseif event == "PLAYER_LOGIN" then
|
|
RQL_HookChatLinks()
|
|
RQL_BuildUI()
|
|
RQL_BuildMinimapButton()
|
|
-- Item-detail async tooltip retry ticker.
|
|
RelationshipsQuestAndItemBrowser:SetScript("OnUpdate", function()
|
|
RQL_ItemDetailRetryTick(arg1)
|
|
if RQL_PlusTick then RQL_PlusTick(arg1) end
|
|
end)
|
|
if RQL_ApplyBackground then
|
|
RQL_ApplyBackground(RQL_GetStartupBackground())
|
|
end
|
|
if DEFAULT_CHAT_FRAME then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99Relationships Quest And Item Browser|r loaded. Click quest links, /rql, or use the minimap book icon.")
|
|
end
|
|
end
|
|
end)
|
|
|
|
local function RQL_SetFilterFromWord(kind, value)
|
|
value = string.lower(value or "")
|
|
if kind == "level" then
|
|
RQL_LEVEL_FILTER = 0
|
|
if value == "1-9" or value == "1" then RQL_LEVEL_FILTER = 1
|
|
elseif value == "10-19" or value == "10" then RQL_LEVEL_FILTER = 2
|
|
elseif value == "20-29" or value == "20" then RQL_LEVEL_FILTER = 3
|
|
elseif value == "30-39" or value == "30" then RQL_LEVEL_FILTER = 4
|
|
elseif value == "40-49" or value == "40" then RQL_LEVEL_FILTER = 5
|
|
elseif value == "50-59" or value == "50" then RQL_LEVEL_FILTER = 6
|
|
elseif value == "60" or value == "60+" then RQL_LEVEL_FILTER = 7 end
|
|
elseif kind == "type" then
|
|
RQL_TYPE_FILTER = 0
|
|
if value == "normal" then RQL_TYPE_FILTER = 1
|
|
elseif value == "elite" or value == "group" then RQL_TYPE_FILTER = 2
|
|
elseif value == "dungeon" or value == "raid" then RQL_TYPE_FILTER = 3
|
|
elseif value == "pvp" then RQL_TYPE_FILTER = 4
|
|
elseif value == "class" then RQL_TYPE_FILTER = 5
|
|
elseif value == "profession" or value == "skill" then RQL_TYPE_FILTER = 6
|
|
elseif value == "event" or value == "seasonal" then RQL_TYPE_FILTER = 7 end
|
|
elseif kind == "faction" then
|
|
RQL_FACTION_FILTER = 0
|
|
if value == "alliance" or value == "a" then RQL_FACTION_FILTER = 1
|
|
elseif value == "horde" or value == "h" then RQL_FACTION_FILTER = 2
|
|
elseif value == "neutral" or value == "n" or value == "both" then RQL_FACTION_FILTER = 3 end
|
|
elseif kind == "source" then
|
|
RQL_SOURCE_FILTER = 0
|
|
if value == "vanilla" then RQL_SOURCE_FILTER = 1
|
|
elseif value == "turtle" or value == "octo" or value == "octowow" then RQL_SOURCE_FILTER = 2 end
|
|
end
|
|
end
|
|
|
|
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER1 = "/rql"
|
|
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER2 = "/questbrowser"
|
|
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER3 = "/rqib"
|
|
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER4 = "/questitembrowser"
|
|
SlashCmdList["RELATIONSHIPSQUESTANDITEMBROWSER"] = function(msg)
|
|
msg = msg or ""
|
|
local _, _, cmd, rest = string.find(msg, "^%s*(%S+)%s*(.-)%s*$")
|
|
cmd = cmd and string.lower(cmd) or ""
|
|
if cmd == "" then
|
|
RQL_ShowBrowser()
|
|
elseif cmd == "close" or cmd == "hide" then
|
|
RelationshipsQuestAndItemBrowserFrame:Hide()
|
|
elseif cmd == "search" then
|
|
RQL_SEARCH_TEXT = rest or ""
|
|
if RQL_UI.search then RQL_UI.search:SetText(RQL_SEARCH_TEXT) end
|
|
RQL_ShowBrowser()
|
|
elseif cmd == "level" or cmd == "type" or cmd == "faction" or cmd == "source" then
|
|
RQL_SetFilterFromWord(cmd, rest)
|
|
RQL_ShowBrowser()
|
|
elseif cmd == "reset" then
|
|
RQL_SEARCH_TEXT = ""
|
|
RQL_LEVEL_FILTER = 0; RQL_TYPE_FILTER = 0
|
|
RQL_FACTION_FILTER = 0; RQL_SOURCE_FILTER = 0
|
|
if RQL_UI.search then RQL_UI.search:SetText("") end
|
|
RQL_ShowBrowser()
|
|
elseif cmd == "quest" and rest then
|
|
local id = tonumber(rest)
|
|
if id then RQL_ShowQuest(id, 0, nil) end
|
|
elseif cmd == "items" then
|
|
RQL_ITEM_SEARCH_TEXT = rest or ""
|
|
if RQL_UI.itemSearch then RQL_UI.itemSearch:SetText(RQL_ITEM_SEARCH_TEXT) end
|
|
RQL_ShowItemBrowser()
|
|
elseif cmd == "item" and rest then
|
|
local id = tonumber(rest)
|
|
if id then RQL_ShowItem(id, nil) end
|
|
else
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99RQL|r usage:")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql open browser")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql search <text> search")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql level 20-29 filter level")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql type dungeon filter type")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql faction alliance filter faction")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql source turtle filter source")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql quest <id> show a specific quest")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql items <text> open item browser and search")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql item <id> show a specific item")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /rql reset reset filters")
|
|
end
|
|
end
|