Files
Nanami-UI/QuestUI.lua
2026-03-31 18:03:23 +08:00

1693 lines
64 KiB
Lua

--------------------------------------------------------------------------------
-- Nanami-UI: Quest & NPC Interaction UI (QuestUI.lua)
-- Replaces GossipFrame and QuestFrame with Nanami-UI styled interface
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.QuestUI = {}
local QUI = SFrames.QuestUI
SFramesDB = SFramesDB or {}
--------------------------------------------------------------------------------
-- Theme: Pink Cat-Paw (matching Nanami-UI)
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend({
questAvail = { 0.9, 0.82, 0.18 },
questActive = { 0.65, 0.65, 0.65 },
questComplete = { 0.9, 0.82, 0.18 },
moneyGold = { 1, 0.84, 0.0 },
moneySilver = { 0.78, 0.78, 0.78 },
moneyCopper = { 0.71, 0.43, 0.18 },
})
local QUALITY_COLORS = {
[0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 },
[2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 },
[4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 },
}
local GOSSIP_ICONS = {
["gossip"] = "Interface\\GossipFrame\\GossipGossipIcon",
["vendor"] = "Interface\\GossipFrame\\VendorGossipIcon",
["taxi"] = "Interface\\GossipFrame\\TaxiGossipIcon",
["trainer"] = "Interface\\GossipFrame\\TrainerGossipIcon",
["banker"] = "Interface\\GossipFrame\\BankerGossipIcon",
["petition"] = "Interface\\GossipFrame\\PetitionGossipIcon",
["tabard"] = "Interface\\GossipFrame\\TabardGossipIcon",
["battlemaster"] = "Interface\\GossipFrame\\BattleMasterGossipIcon",
}
local QUEST_ICON_AVAILABLE = "Interface\\GossipFrame\\AvailableQuestIcon"
local QUEST_ICON_ACTIVE = "Interface\\GossipFrame\\ActiveQuestIcon"
local GOSSIP_TRIGGER_TEXTS = {
["TRANSMOG_TRIGGER"] = true,
}
--------------------------------------------------------------------------------
-- Layout
--------------------------------------------------------------------------------
local FRAME_W = 340
local FRAME_H = 400
local HEADER_H = 34
local BOTTOM_H = 42
local SIDE_PAD = 14
local CONTENT_W = FRAME_W - SIDE_PAD * 2
local ITEM_SIZE = 36
local ITEM_GAP = 4
local SCROLL_STEP = 40
local MAX_OPTIONS = 24
local MAX_ITEMS = 10
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
local MainFrame = nil
local currentPage = nil
local previousPage = nil
local selectedReward = 0
local pendingClose = false
local closeTimer = 0
local pages = {}
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
end
local function CreateShadow(parent)
local s = CreateFrame("Frame", nil, parent)
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.45)
s:SetBackdropBorderColor(0, 0, 0, 0.6)
s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1))
return s
end
local function MakeSep(parent, width)
local sep = parent:CreateTexture(nil, "ARTWORK")
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
sep:SetWidth(width or CONTENT_W)
sep:SetHeight(1)
sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
return sep
end
local function GetDiffColor(level)
if GetDifficultyColor then
local c = GetDifficultyColor(level)
if c then return c.r, c.g, c.b end
end
local pLvl = UnitLevel("player") or 60
local diff = (level or pLvl) - pLvl
if diff >= 5 then return 1, 0.1, 0.1
elseif diff >= 3 then return 1, 0.5, 0.25
elseif diff >= -2 then return 1, 1, 0
else return 0.25, 0.75, 0.25
end
end
local function TextHeight(fs, fallback)
if not fs then return fallback or 14 end
local h = fs.GetStringHeight and fs:GetStringHeight()
if h and h > 0 then return h end
h = fs:GetHeight()
if h and h > 1 then return h end
return fallback or 14
end
local function ForwardScrollWheel(frame)
frame:EnableMouseWheel(true)
frame:SetScript("OnMouseWheel", function()
local p = this:GetParent()
while p do
if p:GetObjectType() == "ScrollFrame" then
if p.UpdateScrollChildRect then p:UpdateScrollChildRect() end
local cur = p:GetVerticalScroll()
local maxVal = p:GetVerticalScrollRange()
if arg1 > 0 then
p:SetVerticalScroll(math.max(0, cur - SCROLL_STEP))
else
p:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP))
end
return
end
p = p:GetParent()
end
end)
end
local function FormatMoney(copper)
if not copper or copper <= 0 then return nil end
local g = math.floor(copper / 10000)
local s = math.floor(math.mod(copper, 10000) / 100)
local c = math.mod(copper, 100)
return g, s, c
end
local LINK_QUALITY_MAP = {
["9d9d9d"] = 0, ["ffffff"] = 1, ["1eff00"] = 2,
["0070dd"] = 3, ["a335ee"] = 4, ["ff8000"] = 5,
["e6cc80"] = 6,
}
local function QualityFromLink(link)
if not link then return nil end
local _, _, hex = string.find(link, "|c(%x%x%x%x%x%x%x%x)|")
if hex and string.len(hex) == 8 then
local color = string.lower(string.sub(hex, 3, 8))
return LINK_QUALITY_MAP[color]
end
return nil
end
local function GetItemQuality(itemType, index)
local name, texture, numItems, quality, isUsable = GetQuestItemInfo(itemType, index)
if quality and quality > 0 then return quality end
local link
if GetQuestItemLink then
link = GetQuestItemLink(itemType, index)
end
if link then
if GetItemInfo then
local _, _, q = GetItemInfo(link)
if q and q > 0 then return q end
end
local q = QualityFromLink(link)
if q then return q end
end
return nil
end
--------------------------------------------------------------------------------
-- Scroll Area Factory
--------------------------------------------------------------------------------
local function CreateScrollArea(parent, name)
local scroll = CreateFrame("ScrollFrame", name, parent)
local content = CreateFrame("Frame", name .. "Content", scroll)
content:SetWidth(CONTENT_W)
content:SetHeight(1)
scroll:SetScrollChild(content)
-- Hook content:SetHeight to auto-refresh scroll range
local origSetHeight = content.SetHeight
content.SetHeight = function(self, h)
origSetHeight(self, h)
if scroll.UpdateScrollChildRect then scroll:UpdateScrollChildRect() end
end
scroll:EnableMouseWheel(true)
scroll:SetScript("OnMouseWheel", function()
if this.UpdateScrollChildRect then this:UpdateScrollChildRect() end
local cur = this:GetVerticalScroll()
local maxVal = this:GetVerticalScrollRange()
if arg1 > 0 then
this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP))
else
this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP))
end
end)
scroll.content = content
return scroll
end
--------------------------------------------------------------------------------
-- Action Button Factory
--------------------------------------------------------------------------------
local function CreateActionBtn(parent, text, w)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(w or 100)
btn:SetHeight(28)
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), 11, "OUTLINE")
fs:SetPoint("CENTER", 0, 0)
fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
fs:SetText(text)
btn.label = fs
btn.disabled = false
function btn:SetDisabled(flag)
self.disabled = flag
if flag then
self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3])
self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5)
self:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.5)
else
self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
end
end
btn:SetScript("OnEnter", function()
if not this.disabled then
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
end
end)
btn:SetScript("OnLeave", function()
if not this.disabled then
this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
end
end)
btn:SetScript("OnMouseDown", function()
if not this.disabled then
this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
end
end)
btn:SetScript("OnMouseUp", function()
if not this.disabled then
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end
end)
return btn
end
--------------------------------------------------------------------------------
-- Gossip / Quest Option Button Factory
--------------------------------------------------------------------------------
local function CreateOptionButton(parent)
local btn = CreateFrame("Button", nil, parent)
btn:SetHeight(22)
btn:SetWidth(CONTENT_W)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetWidth(16)
icon:SetHeight(16)
icon:SetPoint("LEFT", btn, "LEFT", 2, 0)
btn.icon = icon
local text = btn:CreateFontString(nil, "OVERLAY")
text:SetFont(GetFont(), 12)
text:SetPoint("LEFT", icon, "RIGHT", 6, 0)
text:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
text:SetJustifyH("LEFT")
text:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
btn.text = text
btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
btn.normalR = T.nameText[1]
btn.normalG = T.nameText[2]
btn.normalB = T.nameText[3]
btn:SetScript("OnEnter", function()
this.text:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
end)
btn:SetScript("OnLeave", function()
this.text:SetTextColor(this.normalR, this.normalG, this.normalB)
end)
ForwardScrollWheel(btn)
return btn
end
--------------------------------------------------------------------------------
-- Item Slot Factory
--------------------------------------------------------------------------------
local function CreateItemSlot(parent)
local slot = CreateFrame("Button", nil, parent)
slot:SetWidth(CONTENT_W)
slot:SetHeight(ITEM_SIZE + 4)
-- Selection highlight background (covers entire row)
local selBg = slot:CreateTexture(nil, "BACKGROUND")
selBg:SetTexture("Interface\\Buttons\\WHITE8X8")
selBg:SetAllPoints(slot)
selBg:SetVertexColor(T.questSelected[1], T.questSelected[2], T.questSelected[3], T.questSelected[4] or 0.45)
selBg:Hide()
slot.selBg = selBg
-- Selection glow border around entire row
local selGlow = CreateFrame("Frame", nil, slot)
selGlow:SetPoint("TOPLEFT", slot, "TOPLEFT", -2, 2)
selGlow:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", 2, -2)
selGlow:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
selGlow:SetBackdropBorderColor(T.questSelBorder[1], T.questSelBorder[2], T.questSelBorder[3], T.questSelBorder[4] or 0.9)
selGlow:Hide()
slot.selGlow = selGlow
local iconFrame = CreateFrame("Frame", nil, slot)
iconFrame:SetWidth(ITEM_SIZE)
iconFrame:SetHeight(ITEM_SIZE)
iconFrame:SetPoint("LEFT", slot, "LEFT", 0, 0)
iconFrame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
slot.iconFrame = iconFrame
local icon = iconFrame:CreateTexture(nil, "ARTWORK")
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3)
icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3)
slot.icon = icon
local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY")
qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
qualGlow:SetBlendMode("ADD")
qualGlow:SetAlpha(0.7)
qualGlow:SetWidth(ITEM_SIZE * 1.8)
qualGlow:SetHeight(ITEM_SIZE * 1.8)
qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0)
qualGlow:Hide()
slot.qualGlow = qualGlow
local countFS = iconFrame:CreateFontString(nil, "OVERLAY")
countFS:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE")
countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2)
countFS:SetJustifyH("RIGHT")
slot.countFS = countFS
local nameFS = slot:CreateFontString(nil, "OVERLAY")
nameFS:SetFont(GetFont(), 12, "OUTLINE")
nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 8, 0)
nameFS:SetPoint("RIGHT", slot, "RIGHT", -4, 0)
nameFS:SetJustifyH("LEFT")
nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
slot.nameFS = nameFS
slot.itemType = nil
slot.itemIndex = nil
slot:SetScript("OnEnter", function()
if this.itemType and this.itemIndex then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetQuestItem(this.itemType, this.itemIndex)
GameTooltip:Show()
end
this.iconFrame:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
end)
slot:SetScript("OnLeave", function()
GameTooltip:Hide()
if this.selected then
this.iconFrame:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4])
else
local qc = QUALITY_COLORS[this._quality]
if qc and this._quality and this._quality >= 2 then
this.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
else
this.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
end
end)
function slot:ApplyItemData(name, texture, numItems, quality)
self.icon:SetTexture(texture)
self.nameFS:SetText(name or "")
if numItems and numItems > 1 then
self.countFS:SetText(numItems)
else
self.countFS:SetText("")
end
self._quality = quality
local qc = QUALITY_COLORS[quality]
if qc and quality and quality >= 2 then
self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3])
self.qualGlow:Show()
self.nameFS:SetTextColor(qc[1], qc[2], qc[3])
self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
else
self.qualGlow:Hide()
self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
end
function slot:SetItemInfo(iType, idx)
self.itemType = iType
self.itemIndex = idx
local name, texture, numItems, quality, isUsable = GetQuestItemInfo(iType, idx)
if not quality or quality <= 0 then
quality = GetItemQuality(iType, idx)
end
self:ApplyItemData(name, texture, numItems, quality or 1)
self:Show()
self:SetScript("OnUpdate", nil)
local needRetry = (not name or name == "") or (quality == nil) or (not texture or texture == "")
if needRetry then
self._retryElapsed = 0
self._retryAttempts = 0
self:SetScript("OnUpdate", function()
this._retryElapsed = (this._retryElapsed or 0) + (arg1 or 0)
if this._retryElapsed < 0.2 then return end
this._retryElapsed = 0
this._retryAttempts = (this._retryAttempts or 0) + 1
if this._retryAttempts > 15 then
this:SetScript("OnUpdate", nil)
return
end
if this.itemType and this.itemIndex then
local n, t, ni, q = GetQuestItemInfo(this.itemType, this.itemIndex)
if not q or q <= 0 then q = GetItemQuality(this.itemType, this.itemIndex) end
local nameOk = n and n ~= ""
local texOk = t and t ~= ""
local qualOk = q ~= nil
if nameOk and texOk and qualOk then
this:ApplyItemData(n, t, ni, q)
this:SetScript("OnUpdate", nil)
elseif this._retryAttempts >= 15 then
if nameOk then
this:ApplyItemData(n, t, ni, q or 1)
end
this:SetScript("OnUpdate", nil)
end
end
end)
end
end
function slot:SetSelected(flag)
self.selected = flag
if flag then
self.iconFrame:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4])
self.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4] or 0.95)
self.selBg:Show()
self.selGlow:Show()
self.nameFS:SetTextColor(T.title[1], T.title[2], T.title[3])
else
local qc = QUALITY_COLORS[self._quality]
if qc and self._quality and self._quality >= 2 then
self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
self.nameFS:SetTextColor(qc[1], qc[2], qc[3])
else
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
end
self.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
self.selBg:Hide()
self.selGlow:Hide()
end
end
function slot:Clear()
self:SetScript("OnUpdate", nil)
self.itemType = nil
self.itemIndex = nil
self._quality = nil
self.icon:SetTexture(nil)
self.nameFS:SetText("")
self.countFS:SetText("")
self.qualGlow:Hide()
self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
self.selected = false
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
self.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
self.selBg:Hide()
self.selGlow:Hide()
self:Hide()
end
ForwardScrollWheel(slot)
return slot
end
--------------------------------------------------------------------------------
-- Money Display
--------------------------------------------------------------------------------
local function CreateMoneyLine(parent)
local frame = CreateFrame("Frame", nil, parent)
frame:SetWidth(CONTENT_W)
frame:SetHeight(16)
local font = GetFont()
local gTxt = frame:CreateFontString(nil, "OVERLAY")
gTxt:SetFont(font, 12, "OUTLINE")
local gTex = frame:CreateTexture(nil, "ARTWORK")
gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
gTex:SetTexCoord(0, 0.25, 0, 1)
gTex:SetWidth(13); gTex:SetHeight(13)
local sTxt = frame:CreateFontString(nil, "OVERLAY")
sTxt:SetFont(font, 12, "OUTLINE")
local sTex = frame:CreateTexture(nil, "ARTWORK")
sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
sTex:SetTexCoord(0.25, 0.5, 0, 1)
sTex:SetWidth(13); sTex:SetHeight(13)
local cTxt = frame:CreateFontString(nil, "OVERLAY")
cTxt:SetFont(font, 12, "OUTLINE")
local cTex = frame:CreateTexture(nil, "ARTWORK")
cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons")
cTex:SetTexCoord(0.5, 0.75, 0, 1)
cTex:SetWidth(13); cTex:SetHeight(13)
frame.gTxt = gTxt; frame.gTex = gTex
frame.sTxt = sTxt; frame.sTex = sTex
frame.cTxt = cTxt; frame.cTex = cTex
function frame:SetMoney(copper)
local g, s, c = FormatMoney(copper)
if not g then self:Hide(); return end
local x = 0
if g > 0 then
self.gTxt:SetText(g)
self.gTxt:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3])
self.gTxt:ClearAllPoints()
self.gTxt:SetPoint("LEFT", self, "LEFT", x, 0)
x = x + self.gTxt:GetStringWidth() + 1
self.gTex:ClearAllPoints()
self.gTex:SetPoint("LEFT", self, "LEFT", x, 0)
x = x + 15
self.gTxt:Show(); self.gTex:Show()
else
self.gTxt:Hide(); self.gTex:Hide()
end
if s > 0 or g > 0 then
self.sTxt:SetText(s)
self.sTxt:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3])
self.sTxt:ClearAllPoints()
self.sTxt:SetPoint("LEFT", self, "LEFT", x, 0)
x = x + self.sTxt:GetStringWidth() + 1
self.sTex:ClearAllPoints()
self.sTex:SetPoint("LEFT", self, "LEFT", x, 0)
x = x + 15
self.sTxt:Show(); self.sTex:Show()
else
self.sTxt:Hide(); self.sTex:Hide()
end
if c > 0 or (g == 0 and s == 0) then
self.cTxt:SetText(c)
self.cTxt:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3])
self.cTxt:ClearAllPoints()
self.cTxt:SetPoint("LEFT", self, "LEFT", x, 0)
x = x + self.cTxt:GetStringWidth() + 1
self.cTex:ClearAllPoints()
self.cTex:SetPoint("LEFT", self, "LEFT", x, 0)
self.cTxt:Show(); self.cTex:Show()
else
self.cTxt:Hide(); self.cTex:Hide()
end
self:Show()
end
return frame
end
--------------------------------------------------------------------------------
-- Gossip data parsers
--------------------------------------------------------------------------------
local function ParseGossipAvailableQuests()
local data = { GetGossipAvailableQuests() }
local out = {}
local i = 1
while i <= table.getn(data) do
if type(data[i]) == "string" then
table.insert(out, { title = data[i], level = tonumber(data[i + 1]) or 0 })
i = i + 2
else
i = i + 1
end
end
return out
end
local function ParseGossipActiveQuests()
local data = { GetGossipActiveQuests() }
local out = {}
local i = 1
while i <= table.getn(data) do
if type(data[i]) == "string" then
local q = { title = data[i], level = tonumber(data[i + 1]) or 0 }
if i + 2 <= table.getn(data) and type(data[i + 2]) ~= "string" then
q.isComplete = data[i + 2]
i = i + 3
else
i = i + 2
end
table.insert(out, q)
else
i = i + 1
end
end
return out
end
local function ParseGossipOptions()
local data = { GetGossipOptions() }
local out = {}
local i = 1
while i <= table.getn(data) do
if i + 1 <= table.getn(data) then
table.insert(out, { title = data[i], gtype = data[i + 1] or "gossip" })
end
i = i + 2
end
return out
end
--------------------------------------------------------------------------------
-- AutoGossip logic (integrated from AutoGossip.lua)
--------------------------------------------------------------------------------
local function TryAutoGossip()
if SFramesDB and SFramesDB.autoGossip == false then return false end
if IsShiftKeyDown() then return false end
local active = { GetGossipActiveQuests() }
local avail = { GetGossipAvailableQuests() }
local opts = { GetGossipOptions() }
local nActive = table.getn(active) or 0
local nAvail = table.getn(avail) or 0
local nOpts = table.getn(opts) or 0
if nActive == 0 and nAvail == 0 and nOpts == 2 then
SelectGossipOption(1)
return true
end
return false
end
--------------------------------------------------------------------------------
-- Page Management
--------------------------------------------------------------------------------
local function HideAllPages()
for _, p in pairs(pages) do
if p and p.Hide then p:Hide() end
end
end
local function ShowPage(name)
HideAllPages()
currentPage = name
if pages[name] then
pages[name]:Show()
if pages[name].scroll then
if pages[name].scroll.UpdateScrollChildRect then
pages[name].scroll:UpdateScrollChildRect()
end
pages[name].scroll:SetVerticalScroll(0)
end
end
if MainFrame then
MainFrame.btn1:Hide()
MainFrame.btn2:Hide()
end
end
local function GoBack()
if DeclineQuest then DeclineQuest() end
end
local function CloseFrame()
currentPage = nil
MainFrame:Hide()
end
local function UpdateBottomButtons()
if not MainFrame then return end
local bL = MainFrame.btn1
local bR = MainFrame.btn2
local hasBack = previousPage and (currentPage ~= "gossip") and (currentPage ~= "greeting")
if currentPage == "gossip" or currentPage == "greeting" then
bL:Hide()
bR.label:SetText("关闭")
bR:SetDisabled(false)
bR:SetScript("OnClick", function()
if currentPage == "gossip" then
if CloseGossip then CloseGossip() end
end
currentPage = nil
MainFrame:Hide()
end)
bR:Show()
elseif currentPage == "detail" then
bL.label:SetText("接受")
bL:SetDisabled(false)
bL:SetScript("OnClick", function()
if AcceptQuest then AcceptQuest() end
end)
bL:Show()
bR.label:SetText(hasBack and "返回" or "拒绝")
bR:SetDisabled(false)
bR:SetScript("OnClick", function()
if hasBack then
GoBack()
else
if DeclineQuest then DeclineQuest() end
end
end)
bR:Show()
elseif currentPage == "progress" then
local canComplete = IsQuestCompletable and IsQuestCompletable()
bL.label:SetText("继续")
bL:SetDisabled(not canComplete)
bL:SetScript("OnClick", function()
if not this.disabled and CompleteQuest then
CompleteQuest()
end
end)
bL:Show()
bR.label:SetText(hasBack and "返回" or "关闭")
bR:SetDisabled(false)
bR:SetScript("OnClick", function()
if hasBack then
GoBack()
else
CloseFrame()
end
end)
bR:Show()
elseif currentPage == "complete" then
local numChoices = GetNumQuestChoices and GetNumQuestChoices() or 0
local canFinish = (numChoices == 0) or (selectedReward > 0)
bL.label:SetText("完成任务")
bL:SetDisabled(not canFinish)
bL:SetScript("OnClick", function()
if this.disabled then return end
if GetQuestReward then
local nc = GetNumQuestChoices and GetNumQuestChoices() or 0
if nc > 0 then
GetQuestReward(selectedReward)
else
GetQuestReward()
end
end
end)
bL:Show()
bR.label:SetText(hasBack and "返回" or "关闭")
bR:SetDisabled(false)
bR:SetScript("OnClick", function()
if hasBack then
GoBack()
else
CloseFrame()
end
end)
bR:Show()
end
end
--------------------------------------------------------------------------------
-- GOSSIP PAGE
--------------------------------------------------------------------------------
local gossipOptionBtns = {}
local gossipSectionLabels = {}
local function CreateGossipPage(parent)
local page = CreateFrame("Frame", nil, parent)
page:SetAllPoints(parent)
page:Hide()
local scroll = CreateScrollArea(page, "SFramesQuestGossipScroll")
scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
page.scroll = scroll
local content = scroll.content
local npcText = content:CreateFontString(nil, "OVERLAY")
npcText:SetFont(GetFont(), 12)
npcText:SetWidth(CONTENT_W - 4)
npcText:SetJustifyH("LEFT")
npcText:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
page.npcText = npcText
for i = 1, 3 do
local lbl = content:CreateFontString(nil, "OVERLAY")
lbl:SetFont(GetFont(), 11, "OUTLINE")
lbl:SetWidth(CONTENT_W)
lbl:SetJustifyH("LEFT")
lbl:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
gossipSectionLabels[i] = lbl
end
for i = 1, MAX_OPTIONS do
gossipOptionBtns[i] = CreateOptionButton(content)
end
page.Update = function()
local y = 6
local gossipText = GetGossipText and GetGossipText() or ""
page.npcText:SetText(gossipText)
page.npcText:ClearAllPoints()
page.npcText:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.npcText, 14) + 12
local availQuests = ParseGossipAvailableQuests()
local activeQuests = ParseGossipActiveQuests()
local gossipOpts = ParseGossipOptions()
local btnIdx = 1
for i = 1, 3 do gossipSectionLabels[i]:Hide() end
for i = 1, MAX_OPTIONS do gossipOptionBtns[i]:Hide() end
if table.getn(availQuests) > 0 then
gossipSectionLabels[1]:SetText("可接任务")
gossipSectionLabels[1]:ClearAllPoints()
gossipSectionLabels[1]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
gossipSectionLabels[1]:Show()
y = y + 16
for qi = 1, table.getn(availQuests) do
local q = availQuests[qi]
if btnIdx <= MAX_OPTIONS then
local btn = gossipOptionBtns[btnIdx]
btn.icon:SetTexture(QUEST_ICON_AVAILABLE)
local levelStr = q.level > 0 and ("[" .. q.level .. "] ") or ""
btn.text:SetText(levelStr .. (q.title or ""))
if q.level > 0 then
local r, g, b = GetDiffColor(q.level)
btn.normalR = r; btn.normalG = g; btn.normalB = b
btn.text:SetTextColor(r, g, b)
else
btn.normalR = T.questAvail[1]; btn.normalG = T.questAvail[2]; btn.normalB = T.questAvail[3]
btn.text:SetTextColor(T.questAvail[1], T.questAvail[2], T.questAvail[3])
end
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
local idx = qi
btn:SetScript("OnClick", function()
currentPage = nil
MainFrame:Hide()
SelectGossipAvailableQuest(idx)
end)
btn:Show()
y = y + 22
btnIdx = btnIdx + 1
end
end
y = y + 4
end
if table.getn(activeQuests) > 0 then
gossipSectionLabels[2]:SetText("进行中任务")
gossipSectionLabels[2]:ClearAllPoints()
gossipSectionLabels[2]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
gossipSectionLabels[2]:Show()
y = y + 16
for qi = 1, table.getn(activeQuests) do
local q = activeQuests[qi]
if btnIdx <= MAX_OPTIONS then
local btn = gossipOptionBtns[btnIdx]
btn.icon:SetTexture(QUEST_ICON_ACTIVE)
local levelStr = q.level > 0 and ("[" .. q.level .. "] ") or ""
btn.text:SetText(levelStr .. (q.title or ""))
if q.isComplete then
btn.normalR = T.questComplete[1]; btn.normalG = T.questComplete[2]; btn.normalB = T.questComplete[3]
btn.text:SetTextColor(T.questComplete[1], T.questComplete[2], T.questComplete[3])
else
btn.normalR = T.questActive[1]; btn.normalG = T.questActive[2]; btn.normalB = T.questActive[3]
btn.text:SetTextColor(T.questActive[1], T.questActive[2], T.questActive[3])
end
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
local idx = qi
btn:SetScript("OnClick", function()
currentPage = nil
MainFrame:Hide()
SelectGossipActiveQuest(idx)
end)
btn:Show()
y = y + 22
btnIdx = btnIdx + 1
end
end
y = y + 4
end
if table.getn(gossipOpts) > 0 then
gossipSectionLabels[3]:SetText("对话")
gossipSectionLabels[3]:ClearAllPoints()
gossipSectionLabels[3]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
gossipSectionLabels[3]:Show()
y = y + 16
for oi = 1, table.getn(gossipOpts) do
local opt = gossipOpts[oi]
if btnIdx <= MAX_OPTIONS then
local btn = gossipOptionBtns[btnIdx]
btn.icon:SetTexture(GOSSIP_ICONS[opt.gtype] or GOSSIP_ICONS["gossip"])
btn.text:SetText(opt.title or "")
btn.normalR = T.nameText[1]; btn.normalG = T.nameText[2]; btn.normalB = T.nameText[3]
btn.text:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
local idx = oi
btn:SetScript("OnClick", function()
currentPage = nil
MainFrame:Hide()
SelectGossipOption(idx)
end)
btn:Show()
y = y + 22
btnIdx = btnIdx + 1
end
end
end
content:SetHeight(y + 10)
end
return page
end
--------------------------------------------------------------------------------
-- GREETING PAGE (multi-quest NPC via QuestFrame)
--------------------------------------------------------------------------------
local greetingOptionBtns = {}
local greetingSectionLabels = {}
local function CreateGreetingPage(parent)
local page = CreateFrame("Frame", nil, parent)
page:SetAllPoints(parent)
page:Hide()
local scroll = CreateScrollArea(page, "SFramesQuestGreetingScroll")
scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
page.scroll = scroll
local content = scroll.content
local npcText = content:CreateFontString(nil, "OVERLAY")
npcText:SetFont(GetFont(), 12)
npcText:SetWidth(CONTENT_W - 4)
npcText:SetJustifyH("LEFT")
npcText:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
page.npcText = npcText
for i = 1, 2 do
local lbl = content:CreateFontString(nil, "OVERLAY")
lbl:SetFont(GetFont(), 11, "OUTLINE")
lbl:SetWidth(CONTENT_W)
lbl:SetJustifyH("LEFT")
lbl:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
greetingSectionLabels[i] = lbl
end
for i = 1, MAX_OPTIONS do
greetingOptionBtns[i] = CreateOptionButton(content)
end
page.Update = function()
local y = 6
local greeting = GetGreetingText and GetGreetingText() or ""
page.npcText:SetText(greeting)
page.npcText:ClearAllPoints()
page.npcText:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.npcText, 14) + 12
local numAvail = GetNumAvailableQuests and GetNumAvailableQuests() or 0
local numActive = GetNumActiveQuests and GetNumActiveQuests() or 0
local btnIdx = 1
for i = 1, 2 do greetingSectionLabels[i]:Hide() end
for i = 1, MAX_OPTIONS do greetingOptionBtns[i]:Hide() end
if numAvail > 0 then
greetingSectionLabels[1]:SetText("可接任务")
greetingSectionLabels[1]:ClearAllPoints()
greetingSectionLabels[1]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
greetingSectionLabels[1]:Show()
y = y + 16
for qi = 1, numAvail do
if btnIdx <= MAX_OPTIONS then
local title = GetAvailableTitle and GetAvailableTitle(qi) or ("Quest " .. qi)
local btn = greetingOptionBtns[btnIdx]
btn.icon:SetTexture(QUEST_ICON_AVAILABLE)
btn.text:SetText(title)
btn.normalR = T.questAvail[1]; btn.normalG = T.questAvail[2]; btn.normalB = T.questAvail[3]
btn.text:SetTextColor(T.questAvail[1], T.questAvail[2], T.questAvail[3])
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
local idx = qi
btn:SetScript("OnClick", function()
currentPage = nil
MainFrame:Hide()
SelectAvailableQuest(idx)
end)
btn:Show()
y = y + 22
btnIdx = btnIdx + 1
end
end
y = y + 4
end
if numActive > 0 then
greetingSectionLabels[2]:SetText("进行中任务")
greetingSectionLabels[2]:ClearAllPoints()
greetingSectionLabels[2]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
greetingSectionLabels[2]:Show()
y = y + 16
for qi = 1, numActive do
if btnIdx <= MAX_OPTIONS then
local title = GetActiveTitle and GetActiveTitle(qi) or ("Quest " .. qi)
local btn = greetingOptionBtns[btnIdx]
btn.icon:SetTexture(QUEST_ICON_ACTIVE)
btn.text:SetText(title)
btn.normalR = T.questActive[1]; btn.normalG = T.questActive[2]; btn.normalB = T.questActive[3]
btn.text:SetTextColor(T.questActive[1], T.questActive[2], T.questActive[3])
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
local idx = qi
btn:SetScript("OnClick", function()
currentPage = nil
MainFrame:Hide()
SelectActiveQuest(idx)
end)
btn:Show()
y = y + 22
btnIdx = btnIdx + 1
end
end
end
content:SetHeight(y + 10)
end
return page
end
--------------------------------------------------------------------------------
-- DETAIL PAGE (accept / decline quest)
--------------------------------------------------------------------------------
local detailItemSlots = {}
local function CreateDetailPage(parent)
local page = CreateFrame("Frame", nil, parent)
page:SetAllPoints(parent)
page:Hide()
local scroll = CreateScrollArea(page, "SFramesQuestDetailScroll")
scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
page.scroll = scroll
local content = scroll.content
page.titleFS = content:CreateFontString(nil, "OVERLAY")
page.titleFS:SetFont(GetFont(), 14, "OUTLINE")
page.titleFS:SetWidth(CONTENT_W - 4)
page.titleFS:SetJustifyH("LEFT")
page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
page.sepLine = MakeSep(content, CONTENT_W)
page.descFS = content:CreateFontString(nil, "OVERLAY")
page.descFS:SetFont(GetFont(), 12)
page.descFS:SetWidth(CONTENT_W - 4)
page.descFS:SetJustifyH("LEFT")
page.descFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
page.objLabel = content:CreateFontString(nil, "OVERLAY")
page.objLabel:SetFont(GetFont(), 12, "OUTLINE")
page.objLabel:SetWidth(CONTENT_W)
page.objLabel:SetJustifyH("LEFT")
page.objLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.objLabel:SetText("任务目标")
page.objFS = content:CreateFontString(nil, "OVERLAY")
page.objFS:SetFont(GetFont(), 12)
page.objFS:SetWidth(CONTENT_W - 4)
page.objFS:SetJustifyH("LEFT")
page.objFS:SetTextColor(T.objectiveText[1], T.objectiveText[2], T.objectiveText[3])
page.reqLabel = content:CreateFontString(nil, "OVERLAY")
page.reqLabel:SetFont(GetFont(), 11, "OUTLINE")
page.reqLabel:SetWidth(CONTENT_W)
page.reqLabel:SetJustifyH("LEFT")
page.reqLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.reqLabel:SetText("所需物品")
for i = 1, MAX_ITEMS do detailItemSlots[i] = CreateItemSlot(content) end
page.rewardLabel = content:CreateFontString(nil, "OVERLAY")
page.rewardLabel:SetFont(GetFont(), 11, "OUTLINE")
page.rewardLabel:SetWidth(CONTENT_W)
page.rewardLabel:SetJustifyH("LEFT")
page.rewardLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.rewardLabel:SetText("任务奖励")
page.detailRewardSlots = {}
for i = 1, MAX_ITEMS do page.detailRewardSlots[i] = CreateItemSlot(content) end
page.detailChoiceLabel = content:CreateFontString(nil, "OVERLAY")
page.detailChoiceLabel:SetFont(GetFont(), 11, "OUTLINE")
page.detailChoiceLabel:SetWidth(CONTENT_W)
page.detailChoiceLabel:SetJustifyH("LEFT")
page.detailChoiceLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.detailChoiceLabel:SetText("可选奖励")
page.detailChoiceSlots = {}
for i = 1, MAX_ITEMS do page.detailChoiceSlots[i] = CreateItemSlot(content) end
page.moneyLine = CreateMoneyLine(content)
page.Update = function()
local y = 6
page.titleFS:SetText(GetTitleText and GetTitleText() or "")
page.titleFS:ClearAllPoints()
page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.titleFS, 16) + 8
page.sepLine:ClearAllPoints()
page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
page.sepLine:Show()
y = y + 6
page.descFS:SetText(GetQuestText and GetQuestText() or "")
page.descFS:ClearAllPoints()
page.descFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.descFS, 14) + 12
local obj = GetObjectiveText and GetObjectiveText() or ""
if obj ~= "" then
page.objLabel:ClearAllPoints()
page.objLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.objLabel:Show(); y = y + 16
page.objFS:SetText(obj)
page.objFS:ClearAllPoints()
page.objFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.objFS:Show()
y = y + TextHeight(page.objFS, 14) + 10
else
page.objLabel:Hide(); page.objFS:Hide()
end
local numReq = GetNumQuestItems and GetNumQuestItems() or 0
for i = 1, MAX_ITEMS do detailItemSlots[i]:Clear() end
if numReq > 0 then
page.reqLabel:ClearAllPoints()
page.reqLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.reqLabel:Show(); y = y + 16
for i = 1, math.min(numReq, MAX_ITEMS) do
detailItemSlots[i]:ClearAllPoints()
detailItemSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
detailItemSlots[i]:SetItemInfo("required", i)
y = y + ITEM_SIZE + ITEM_GAP
end
y = y + 4
else
page.reqLabel:Hide()
end
local numRew = GetNumQuestRewards and GetNumQuestRewards() or 0
local numCho = GetNumQuestChoices and GetNumQuestChoices() or 0
local money = GetRewardMoney and GetRewardMoney() or 0
for i = 1, MAX_ITEMS do page.detailRewardSlots[i]:Clear(); page.detailChoiceSlots[i]:Clear() end
page.rewardLabel:Hide(); page.detailChoiceLabel:Hide(); page.moneyLine:Hide()
if numRew > 0 or money > 0 then
page.rewardLabel:ClearAllPoints()
page.rewardLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.rewardLabel:Show(); y = y + 16
for i = 1, math.min(numRew, MAX_ITEMS) do
page.detailRewardSlots[i]:ClearAllPoints()
page.detailRewardSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
page.detailRewardSlots[i]:SetItemInfo("reward", i)
y = y + ITEM_SIZE + ITEM_GAP
end
if money > 0 then
page.moneyLine:ClearAllPoints()
page.moneyLine:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
page.moneyLine:SetMoney(money); y = y + 20
end
y = y + 4
end
if numCho > 0 then
page.detailChoiceLabel:ClearAllPoints()
page.detailChoiceLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.detailChoiceLabel:Show(); y = y + 16
for i = 1, math.min(numCho, MAX_ITEMS) do
page.detailChoiceSlots[i]:ClearAllPoints()
page.detailChoiceSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
page.detailChoiceSlots[i]:SetItemInfo("choice", i)
y = y + ITEM_SIZE + ITEM_GAP
end
y = y + 4
end
content:SetHeight(y + 10)
end
return page
end
--------------------------------------------------------------------------------
-- PROGRESS PAGE (turn-in requirements)
--------------------------------------------------------------------------------
local progressItemSlots = {}
local function CreateProgressPage(parent)
local page = CreateFrame("Frame", nil, parent)
page:SetAllPoints(parent)
page:Hide()
local scroll = CreateScrollArea(page, "SFramesQuestProgressScroll")
scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
page.scroll = scroll
local content = scroll.content
page.titleFS = content:CreateFontString(nil, "OVERLAY")
page.titleFS:SetFont(GetFont(), 14, "OUTLINE")
page.titleFS:SetWidth(CONTENT_W - 4)
page.titleFS:SetJustifyH("LEFT")
page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
page.sepLine = MakeSep(content, CONTENT_W)
page.progressFS = content:CreateFontString(nil, "OVERLAY")
page.progressFS:SetFont(GetFont(), 12)
page.progressFS:SetWidth(CONTENT_W - 4)
page.progressFS:SetJustifyH("LEFT")
page.progressFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
page.reqLabel = content:CreateFontString(nil, "OVERLAY")
page.reqLabel:SetFont(GetFont(), 11, "OUTLINE")
page.reqLabel:SetWidth(CONTENT_W)
page.reqLabel:SetJustifyH("LEFT")
page.reqLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.reqLabel:SetText("需要的物品")
for i = 1, MAX_ITEMS do progressItemSlots[i] = CreateItemSlot(content) end
page.Update = function()
local y = 6
page.titleFS:SetText(GetTitleText and GetTitleText() or "")
page.titleFS:ClearAllPoints()
page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.titleFS, 16) + 8
page.sepLine:ClearAllPoints()
page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
page.sepLine:Show(); y = y + 6
page.progressFS:SetText(GetProgressText and GetProgressText() or "")
page.progressFS:ClearAllPoints()
page.progressFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.progressFS, 14) + 12
local numReq = GetNumQuestItems and GetNumQuestItems() or 0
for i = 1, MAX_ITEMS do progressItemSlots[i]:Clear() end
if numReq > 0 then
page.reqLabel:ClearAllPoints()
page.reqLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.reqLabel:Show(); y = y + 16
for i = 1, math.min(numReq, MAX_ITEMS) do
progressItemSlots[i]:ClearAllPoints()
progressItemSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
progressItemSlots[i]:SetItemInfo("required", i)
y = y + ITEM_SIZE + ITEM_GAP
end
else
page.reqLabel:Hide()
end
content:SetHeight(y + 10)
end
return page
end
--------------------------------------------------------------------------------
-- COMPLETE PAGE (reward selection & finish)
--------------------------------------------------------------------------------
local completeRewardSlots = {}
local completeChoiceSlots = {}
local function CreateCompletePage(parent)
local page = CreateFrame("Frame", nil, parent)
page:SetAllPoints(parent)
page:Hide()
local scroll = CreateScrollArea(page, "SFramesQuestCompleteScroll")
scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
page.scroll = scroll
local content = scroll.content
page.titleFS = content:CreateFontString(nil, "OVERLAY")
page.titleFS:SetFont(GetFont(), 14, "OUTLINE")
page.titleFS:SetWidth(CONTENT_W - 4)
page.titleFS:SetJustifyH("LEFT")
page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
page.sepLine = MakeSep(content, CONTENT_W)
page.rewardTextFS = content:CreateFontString(nil, "OVERLAY")
page.rewardTextFS:SetFont(GetFont(), 12)
page.rewardTextFS:SetWidth(CONTENT_W - 4)
page.rewardTextFS:SetJustifyH("LEFT")
page.rewardTextFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
page.rewardLabel = content:CreateFontString(nil, "OVERLAY")
page.rewardLabel:SetFont(GetFont(), 11, "OUTLINE")
page.rewardLabel:SetWidth(CONTENT_W)
page.rewardLabel:SetJustifyH("LEFT")
page.rewardLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.rewardLabel:SetText("你将获得")
for i = 1, MAX_ITEMS do completeRewardSlots[i] = CreateItemSlot(content) end
page.moneyLine = CreateMoneyLine(content)
page.choiceLabel = content:CreateFontString(nil, "OVERLAY")
page.choiceLabel:SetFont(GetFont(), 11, "OUTLINE")
page.choiceLabel:SetWidth(CONTENT_W)
page.choiceLabel:SetJustifyH("LEFT")
page.choiceLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3])
page.choiceLabel:SetText("选择一项奖励")
for i = 1, MAX_ITEMS do
local slot = CreateItemSlot(content)
local slotIdx = i
slot:SetScript("OnClick", function()
selectedReward = slotIdx
for si = 1, MAX_ITEMS do completeChoiceSlots[si]:SetSelected(si == slotIdx) end
UpdateBottomButtons()
end)
completeChoiceSlots[i] = slot
end
page.Update = function()
selectedReward = 0
local y = 6
page.titleFS:SetText(GetTitleText and GetTitleText() or "")
page.titleFS:ClearAllPoints()
page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.titleFS, 16) + 8
page.sepLine:ClearAllPoints()
page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
page.sepLine:Show(); y = y + 6
page.rewardTextFS:SetText(GetRewardText and GetRewardText() or "")
page.rewardTextFS:ClearAllPoints()
page.rewardTextFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
y = y + TextHeight(page.rewardTextFS, 14) + 12
local numRew = GetNumQuestRewards and GetNumQuestRewards() or 0
local numCho = GetNumQuestChoices and GetNumQuestChoices() or 0
local money = GetRewardMoney and GetRewardMoney() or 0
for i = 1, MAX_ITEMS do completeRewardSlots[i]:Clear(); completeChoiceSlots[i]:Clear() end
page.rewardLabel:Hide(); page.choiceLabel:Hide(); page.moneyLine:Hide()
if numRew > 0 or money > 0 then
page.rewardLabel:ClearAllPoints()
page.rewardLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.rewardLabel:Show(); y = y + 16
for i = 1, math.min(numRew, MAX_ITEMS) do
completeRewardSlots[i]:ClearAllPoints()
completeRewardSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
completeRewardSlots[i]:SetItemInfo("reward", i)
y = y + ITEM_SIZE + ITEM_GAP
end
if money > 0 then
page.moneyLine:ClearAllPoints()
page.moneyLine:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
page.moneyLine:SetMoney(money); y = y + 20
end
y = y + 4
end
if numCho > 0 then
page.choiceLabel:ClearAllPoints()
page.choiceLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y)
page.choiceLabel:Show(); y = y + 16
for i = 1, math.min(numCho, MAX_ITEMS) do
completeChoiceSlots[i]:ClearAllPoints()
completeChoiceSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y)
completeChoiceSlots[i]:SetItemInfo("choice", i)
completeChoiceSlots[i]:SetSelected(false)
y = y + ITEM_SIZE + ITEM_GAP
end
if numCho == 1 then
selectedReward = 1
completeChoiceSlots[1]:SetSelected(true)
end
end
content:SetHeight(y + 10)
end
return page
end
--------------------------------------------------------------------------------
-- INITIALIZE
--------------------------------------------------------------------------------
function QUI:Initialize()
if MainFrame then return end
MainFrame = CreateFrame("Frame", "SFramesQuestFrame", UIParent)
MainFrame:SetWidth(FRAME_W)
MainFrame:SetHeight(FRAME_H)
MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0)
MainFrame:SetFrameStrata("HIGH")
MainFrame:SetToplevel(true)
MainFrame:EnableMouse(true)
MainFrame:SetMovable(true)
MainFrame:RegisterForDrag("LeftButton")
MainFrame:SetScript("OnDragStart", function() this:StartMoving() end)
MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
SetRoundBackdrop(MainFrame)
CreateShadow(MainFrame)
local header = CreateFrame("Frame", nil, MainFrame)
header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0)
header:SetHeight(HEADER_H)
local titleIco = SFrames:CreateIcon(header, "quest", 16)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0)
titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3])
local npcNameFS = header:CreateFontString(nil, "OVERLAY")
npcNameFS:SetFont(GetFont(), 14, "OUTLINE")
npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0)
npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0)
npcNameFS:SetJustifyH("LEFT")
npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
MainFrame.npcNameFS = npcNameFS
local closeBtn = CreateFrame("Button", nil, MainFrame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 2, 2)
closeBtn:SetWidth(24); closeBtn:SetHeight(24)
local headerSep = MainFrame:CreateTexture(nil, "ARTWORK")
headerSep:SetTexture("Interface\\Buttons\\WHITE8X8")
headerSep:SetHeight(1)
headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H)
headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H)
headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
local contentArea = CreateFrame("Frame", nil, MainFrame)
contentArea:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4))
contentArea:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4)
MainFrame.contentArea = contentArea
local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK")
bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8")
bottomSep:SetHeight(1)
bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H)
bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H)
bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
MainFrame.btn1 = CreateActionBtn(MainFrame, "", 100)
MainFrame.btn1:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 8)
MainFrame.btn1:Hide()
MainFrame.btn2 = CreateActionBtn(MainFrame, "", 100)
MainFrame.btn2:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8)
MainFrame.btn2:Hide()
pages.gossip = CreateGossipPage(contentArea)
pages.greeting = CreateGreetingPage(contentArea)
pages.detail = CreateDetailPage(contentArea)
pages.progress = CreateProgressPage(contentArea)
pages.complete = CreateCompletePage(contentArea)
MainFrame:SetScript("OnHide", function()
if currentPage then
if CloseGossip then pcall(CloseGossip) end
if DeclineQuest then pcall(DeclineQuest) end
previousPage = nil
end
currentPage = nil
pendingClose = false
end)
MainFrame:SetScript("OnUpdate", function()
if pendingClose then
closeTimer = closeTimer + (arg1 or 0)
if closeTimer > 0.15 then
pendingClose = false
closeTimer = 0
previousPage = nil
if MainFrame:IsVisible() then
currentPage = nil
MainFrame:Hide()
end
end
end
-- Deferred scroll refresh: re-layout after 1 frame so FontString heights are valid
if MainFrame._deferredPage then
MainFrame._deferredTick = (MainFrame._deferredTick or 0) + 1
if MainFrame._deferredTick >= 2 then
local pg = pages[MainFrame._deferredPage]
if pg and pg.Update then pg.Update() end
if pg and pg.scroll and pg.scroll.UpdateScrollChildRect then
pg.scroll:UpdateScrollChildRect()
end
MainFrame._deferredPage = nil
MainFrame._deferredTick = nil
end
end
end)
MainFrame:Hide()
MainFrame:RegisterEvent("GOSSIP_SHOW")
MainFrame:RegisterEvent("GOSSIP_CLOSED")
MainFrame:RegisterEvent("QUEST_GREETING")
MainFrame:RegisterEvent("QUEST_DETAIL")
MainFrame:RegisterEvent("QUEST_PROGRESS")
MainFrame:RegisterEvent("QUEST_COMPLETE")
MainFrame:RegisterEvent("QUEST_FINISHED")
MainFrame:SetScript("OnEvent", function()
if event == "GOSSIP_SHOW" then
pendingClose = false
local gText = GetGossipText() or ""
if GOSSIP_TRIGGER_TEXTS[strtrim and strtrim(gText) or gText] then return end
if TryAutoGossip() then return end
previousPage = "gossip"
MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC")
ShowPage("gossip")
pages.gossip.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "gossip"; MainFrame._deferredTick = 0
elseif event == "GOSSIP_CLOSED" then
if currentPage == "gossip" then
pendingClose = true
closeTimer = 0
end
elseif event == "QUEST_GREETING" then
pendingClose = false
previousPage = "greeting"
MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC")
ShowPage("greeting")
pages.greeting.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "greeting"; MainFrame._deferredTick = 0
elseif event == "QUEST_DETAIL" then
pendingClose = false
MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC")
ShowPage("detail")
pages.detail.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "detail"; MainFrame._deferredTick = 0
elseif event == "QUEST_PROGRESS" then
pendingClose = false
MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC")
ShowPage("progress")
pages.progress.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "progress"; MainFrame._deferredTick = 0
elseif event == "QUEST_COMPLETE" then
pendingClose = false
selectedReward = 0
MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC")
ShowPage("complete")
pages.complete.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "complete"; MainFrame._deferredTick = 0
elseif event == "QUEST_FINISHED" then
if previousPage then
pendingClose = true
closeTimer = 0
else
pendingClose = false
currentPage = nil
MainFrame:Hide()
end
end
end)
if GossipFrame then
GossipFrame:UnregisterEvent("GOSSIP_SHOW")
GossipFrame:UnregisterEvent("GOSSIP_CLOSED")
end
if QuestFrame then
QuestFrame:UnregisterEvent("QUEST_GREETING")
QuestFrame:UnregisterEvent("QUEST_DETAIL")
QuestFrame:UnregisterEvent("QUEST_PROGRESS")
QuestFrame:UnregisterEvent("QUEST_COMPLETE")
QuestFrame:UnregisterEvent("QUEST_FINISHED")
end
tinsert(UISpecialFrames, "SFramesQuestFrame")
end
--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------
local bootstrap = CreateFrame("Frame")
bootstrap:RegisterEvent("PLAYER_LOGIN")
bootstrap:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
if SFramesDB.enableQuestUI == nil then
SFramesDB.enableQuestUI = true
end
if SFramesDB.enableQuestUI ~= false then
QUI:Initialize()
end
end
end)