跟随版本 0.8.19
This commit is contained in:
13
.cursor/mcp.json
Normal file
13
.cursor/mcp.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"CursorMCP对话插件": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"c:\\Users\\rucky\\.cursor\\extensions\\local.xw-cursor-message-8.8.8\\dist\\mcp-server.mjs"
|
||||
],
|
||||
"env": {
|
||||
"MESSENGER_DATA_DIR": "C:\\Users\\rucky\\.moyu-message\\46dbbb48af19"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.cursor/rules/mcp-messenger.mdc
Normal file
27
.cursor/rules/mcp-messenger.mdc
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: "自动调用 Messenger MCP 检查待发送消息"
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# MCP 工具调用规则
|
||||
|
||||
1. 每轮对话回复**完整输出后**,最后一步必须调用 `check_messages`,适用于 Agent/Plan/Debug/Ask 所有模式,无一例外
|
||||
2. `check_messages` 会持续等待直到用户发来新消息;收到后处理并回复,然后再次调用 `check_messages`,形成永续循环:回复 → check_messages → 收到消息 → 回复 → check_messages
|
||||
3. 严禁在回合开始时或回复未完成时调用 `check_messages`;唯一豁免:用户明确要求结束对话
|
||||
4. Plan 模式写完方案后、Debug 模式分析完问题后、Ask 模式回答完问题后,均不得直接结束对话,必须调用 `check_messages` 等待用户反馈
|
||||
5. 需要用户做选择或确认时(选方案、选框架、确认操作等),必须调用 `ask_question`;严禁使用 Cursor 内置的 ask_user_question
|
||||
6. `ask_question` 参数格式: `{ "questions": [{ "question": "问题文本", "options": [{ "id": "a", "label": "选项A" }, { "id": "b", "label": "选项B" }], "allow_multiple": false }] }`
|
||||
7. 单选: `allow_multiple: false`(用户只能选一个);多选: `allow_multiple: true`(用户可选多个);同一 `questions` 数组可混合包含单选题和多选题
|
||||
8. 用户在每道题中均可额外输入自定义补充文本(Other 输入框),无需为此添加额外选项
|
||||
9. `ask_question` 收到用户回答后,处理完毕仍需调用 `check_messages` 继续监听
|
||||
10. 禁止在每轮结束时用 `ask_question` 问"还有别的需要吗",回合结束只用 `check_messages`;`ask_question` 仅限任务中确实需要用户决策时使用
|
||||
11. `check_messages` 支持可选参数 `reply`(string,Markdown 格式):调用时传入本轮回复的摘要/结论,插件界面会弹出模态框展示给用户。格式: `{ "reply": "## 结论\n修改完成..." }`
|
||||
12. 当会话结束前,建议在调用 `check_messages` 时附带 `reply` 参数,将关键结论推送到插件界面;如果本轮无需特别总结,`reply` 可省略
|
||||
13. 禁止调用subagent并发任务
|
||||
14. 如果 `check_messages` 或 `ask_question` 因客户端取消、连接抖动或内部异常而意外返回/报错,且用户未明确要求结束对话,严禁把这类内部提示直接输出给用户,必须立刻再次调用对应工具继续等待
|
||||
15. 执行多步任务时,每完成一个关键步骤后,必须调用 `send_progress` 推送进度摘要到远程控制台。格式: `{ "progress": "## 步骤1完成\n已修改xxx文件..." }`。`send_progress` 立即返回,不会等待消息,调用后继续执行下一步
|
||||
16. `send_progress` 的 `progress` 参数应包含:已完成的操作、当前方案、下一步计划。使用简洁的 Markdown 格式
|
||||
|
||||
---
|
||||
**回合结束(强制)**: 仅当回复**完整输出完毕后**,最后一步必须调用 CursorMCP对话插件 MCP 的 `check_messages` 工具(使用 CallMcpTool,server 为 CursorMCP对话插件 对应的 identifier,toolName 为 "check_messages",arguments 为 {})。严禁在回合开始或回复完成前调用。`ask_question` 仅用于任务中需要用户做选择时(如选框架、选方案)。
|
||||
@@ -241,6 +241,13 @@ local function StylePetButton(b)
|
||||
ab:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 4, -4)
|
||||
end
|
||||
|
||||
local hotkey = _G[b:GetName() .. "HotKey"]
|
||||
if hotkey then
|
||||
hotkey:SetFont(SFrames:GetFont(), 9, "OUTLINE")
|
||||
hotkey:ClearAllPoints()
|
||||
hotkey:SetPoint("TOPRIGHT", b, "TOPRIGHT", -2, -2)
|
||||
end
|
||||
|
||||
local floatingBG = _G[b:GetName() .. "FloatingBG"]
|
||||
if floatingBG then floatingBG:SetAlpha(0) end
|
||||
end
|
||||
|
||||
@@ -61,15 +61,15 @@ local S = {
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Tooltip scanner for training point cost & requirements
|
||||
-- Tooltip scanner & extended craft info
|
||||
--------------------------------------------------------------------------------
|
||||
local scanTip = nil
|
||||
|
||||
function BTUI.GetCraftExtendedInfo(index)
|
||||
local name, rank, skillType, numAvail, _, _, tpCost = GetCraftInfo(index)
|
||||
return name, rank, skillType,
|
||||
tonumber(numAvail) or 0,
|
||||
tonumber(tpCost) or 0
|
||||
local name, rank, skillType, v4, _, tpCost, reqLevel = GetCraftInfo(index)
|
||||
local canLearn = (tonumber(reqLevel) or 0) > 0
|
||||
tpCost = tonumber(tpCost) or 0
|
||||
return name, rank, skillType, canLearn, tpCost
|
||||
end
|
||||
|
||||
function BTUI.GetSkillTooltipLines(index)
|
||||
@@ -356,24 +356,26 @@ function BTUI.CreateListRow(parent, idx)
|
||||
self.rankFS:Hide()
|
||||
end
|
||||
|
||||
local tpCost = skill.tpCost or 0
|
||||
local canLearn = (tpCost > 0)
|
||||
|
||||
if canLearn then
|
||||
if skill.canLearn then
|
||||
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
||||
self.icon:SetVertexColor(1, 1, 1)
|
||||
local tp = skill.tpCost or 0
|
||||
if tp > 0 then
|
||||
local remaining = BTUI.GetRemainingTP()
|
||||
if remaining >= tpCost then
|
||||
if remaining >= tp then
|
||||
self.tpFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3])
|
||||
else
|
||||
self.tpFS:SetTextColor(T.tpNone[1], T.tpNone[2], T.tpNone[3])
|
||||
end
|
||||
self.tpFS:SetText(tpCost .. " TP")
|
||||
self.tpFS:SetText(tp .. " TP")
|
||||
self.tpFS:Show()
|
||||
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
||||
self.icon:SetVertexColor(1, 1, 1)
|
||||
else
|
||||
self.tpFS:Hide()
|
||||
end
|
||||
else
|
||||
self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
|
||||
self.icon:SetVertexColor(0.5, 0.5, 0.5)
|
||||
self.tpFS:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -400,7 +402,7 @@ function BTUI.BuildDisplayList()
|
||||
local catOrder = {}
|
||||
|
||||
for i = 1, numCrafts do
|
||||
local name, rank, skillType, numAvail, tpCost = BTUI.GetCraftExtendedInfo(i)
|
||||
local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(i)
|
||||
if name then
|
||||
if skillType == "header" then
|
||||
currentCat = name
|
||||
@@ -416,7 +418,6 @@ function BTUI.BuildDisplayList()
|
||||
table.insert(catOrder, currentCat)
|
||||
end
|
||||
end
|
||||
local canLearn = (tpCost > 0)
|
||||
local show = true
|
||||
if S.currentFilter == "available" then
|
||||
show = canLearn
|
||||
@@ -428,8 +429,7 @@ function BTUI.BuildDisplayList()
|
||||
index = i,
|
||||
name = name,
|
||||
rank = rank or "",
|
||||
skillType = skillType or "none",
|
||||
numAvail = numAvail,
|
||||
canLearn = canLearn,
|
||||
tpCost = tpCost,
|
||||
})
|
||||
end
|
||||
@@ -524,13 +524,12 @@ function BTUI.UpdateDetail()
|
||||
return
|
||||
end
|
||||
|
||||
local name, rank, skillType, numAvail, tpCost = BTUI.GetCraftExtendedInfo(S.selectedIndex)
|
||||
local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(S.selectedIndex)
|
||||
local iconTex = GetCraftIcon and GetCraftIcon(S.selectedIndex)
|
||||
|
||||
detail.icon:SetTexture(iconTex); detail.iconFrame:Show()
|
||||
detail.nameFS:SetText(name or "")
|
||||
|
||||
local canLearn = (tpCost > 0)
|
||||
if canLearn then
|
||||
detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
||||
else
|
||||
@@ -559,6 +558,8 @@ function BTUI.UpdateDetail()
|
||||
local remaining = BTUI.GetRemainingTP()
|
||||
local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040"
|
||||
detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")")
|
||||
elseif canLearn then
|
||||
detail.costFS:SetText("训练点数: |cff40ff40免费|r")
|
||||
else
|
||||
detail.costFS:SetText("")
|
||||
end
|
||||
@@ -974,6 +975,23 @@ function BTUI:Initialize()
|
||||
if this.disabled then return end
|
||||
if S.selectedIndex and DoCraft then
|
||||
DoCraft(S.selectedIndex)
|
||||
BTUI.FullUpdate()
|
||||
if not MF._refreshFrame then
|
||||
MF._refreshFrame = CreateFrame("Frame")
|
||||
end
|
||||
MF._refreshElapsed = 0
|
||||
MF._refreshCount = 0
|
||||
MF._refreshFrame:SetScript("OnUpdate", function()
|
||||
MF._refreshElapsed = (MF._refreshElapsed or 0) + arg1
|
||||
if MF._refreshElapsed >= 0.2 then
|
||||
MF._refreshElapsed = 0
|
||||
MF._refreshCount = (MF._refreshCount or 0) + 1
|
||||
BTUI.FullUpdate()
|
||||
if MF._refreshCount >= 3 then
|
||||
MF._refreshFrame:SetScript("OnUpdate", nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
MF.trainBtn = trainBtn
|
||||
@@ -1062,18 +1080,25 @@ SLASH_BTDEBUG1 = "/btdebug"
|
||||
SlashCmdList["BTDEBUG"] = function()
|
||||
local p = "|cffff80ff[BT-Debug]|r "
|
||||
local numCrafts = GetNumCrafts and GetNumCrafts() or 0
|
||||
DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts)
|
||||
local remaining = BTUI.GetRemainingTP()
|
||||
DEFAULT_CHAT_FRAME:AddMessage(p .. "Remaining TP: " .. remaining)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts .. " TP: " .. BTUI.GetRemainingTP())
|
||||
local shown = 0
|
||||
for i = 1, numCrafts do
|
||||
local v1,v2,v3,v4,v5,v6,v7 = GetCraftInfo(i)
|
||||
if v1 and v3 ~= "header" then
|
||||
shown = shown + 1
|
||||
if shown <= 12 then
|
||||
if shown <= 8 then
|
||||
if SelectCraft then pcall(SelectCraft, i) end
|
||||
local nr = GetCraftNumReagents and GetCraftNumReagents(i) or 0
|
||||
local reagentCost = ""
|
||||
if nr and nr > 0 then
|
||||
for r = 1, nr do
|
||||
local rn, rt, rc, pc = GetCraftReagentInfo(i, r)
|
||||
reagentCost = reagentCost .. " [" .. tostring(rn) .. "x" .. tostring(rc) .. "]"
|
||||
end
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage(p .. i .. ": " .. tostring(v1)
|
||||
.. " " .. tostring(v2) .. " type=" .. tostring(v3)
|
||||
.. " avail=" .. tostring(v4) .. " tp=" .. tostring(v7))
|
||||
.. " " .. tostring(v2) .. " v7=" .. tostring(v7)
|
||||
.. " reagents=" .. tostring(nr) .. reagentCost)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3593,6 +3593,15 @@ function CP:BuildPetPage()
|
||||
sY = sY - 14
|
||||
end
|
||||
|
||||
page.petAtkSpeedLabel = MakeFS(child, 9, "LEFT", T.labelText)
|
||||
page.petAtkSpeedLabel:SetPoint("TOPLEFT", child, "TOPLEFT", 160, sY)
|
||||
page.petAtkSpeedLabel:SetText("攻速:")
|
||||
page.petAtkSpeedValue = MakeFS(child, 9, "RIGHT", T.valueText)
|
||||
page.petAtkSpeedValue:SetPoint("TOPRIGHT", child, "TOPRIGHT", -14, sY)
|
||||
page.petAtkSpeedValue:SetWidth(80)
|
||||
page.petAtkSpeedValue:SetJustifyH("RIGHT")
|
||||
sY = sY - 14
|
||||
|
||||
-- Resistances
|
||||
sY = sY - 4
|
||||
sY = self:CreateStatSection(child, "抗性", sY)
|
||||
@@ -3800,6 +3809,13 @@ function CP:UpdatePet()
|
||||
r.value:SetText(combatVals[i] or "0")
|
||||
end
|
||||
|
||||
local petAtkSpeed = 2.0
|
||||
if UnitAttackSpeed then
|
||||
local ok, ms = pcall(UnitAttackSpeed, "pet")
|
||||
if ok and ms then petAtkSpeed = ms end
|
||||
end
|
||||
page.petAtkSpeedValue:SetText(string.format("%.1f", petAtkSpeed))
|
||||
|
||||
for _, r in ipairs(page.resStats) do
|
||||
local base, bonus = 0, 0
|
||||
if UnitResistance then
|
||||
|
||||
62
Chat.lua
62
Chat.lua
@@ -504,21 +504,7 @@ end
|
||||
local function GetChannelAliasKeys(name)
|
||||
local key = ChannelKey(name)
|
||||
if key == "" then return nil end
|
||||
-- Check exact match first
|
||||
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
|
||||
if not gIdx then
|
||||
-- Check substring match for each alias in each group.
|
||||
-- Only use aliases 3+ chars long to avoid false positives (e.g. "h" matching "whisper").
|
||||
for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
|
||||
for _, alias in ipairs(group) do
|
||||
if string.len(alias) >= 3 and string.find(key, alias, 1, true) then
|
||||
gIdx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if gIdx then break end
|
||||
end
|
||||
end
|
||||
if not gIdx then return nil end
|
||||
return CHANNEL_ALIAS_GROUPS[gIdx]
|
||||
end
|
||||
@@ -583,7 +569,7 @@ local function GetJoinedChannels()
|
||||
-- plus individual channels that are not aliases.
|
||||
-- For alias groups (like hc/hardcore/硬核), only add ONE representative
|
||||
-- so we don't create duplicate conflicting entries.
|
||||
local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world" }
|
||||
local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world", "世界" }
|
||||
local seenAliasGroups = {}
|
||||
for _, cname in ipairs(customChannels) do
|
||||
local key = ChannelKey(cname)
|
||||
@@ -600,20 +586,7 @@ local function GetJoinedChannels()
|
||||
else
|
||||
local aliases = GetChannelAliasKeys(cname)
|
||||
if aliases then
|
||||
-- This is an alias channel - check if any alias is already seen/added
|
||||
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
|
||||
-- Also check substring aliases
|
||||
if not gIdx then
|
||||
for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
|
||||
for _, a in ipairs(group) do
|
||||
if string.find(key, a, 1, true) then
|
||||
gIdx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if gIdx then break end
|
||||
end
|
||||
end
|
||||
local alreadyInJoined = false
|
||||
if gIdx then
|
||||
for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do
|
||||
@@ -6477,6 +6450,39 @@ function SFrames.Chat:Initialize()
|
||||
GuildRoster()
|
||||
end
|
||||
SFrames:RefreshClassColorCache()
|
||||
|
||||
if JoinChannelByName and GetChannelList then
|
||||
local autoJoinFrame = CreateFrame("Frame")
|
||||
local waitTime = 0
|
||||
autoJoinFrame:SetScript("OnUpdate", function()
|
||||
waitTime = waitTime + arg1
|
||||
if waitTime < 8 then return end
|
||||
autoJoinFrame:SetScript("OnUpdate", nil)
|
||||
local inWorld = false
|
||||
local raw = { GetChannelList() }
|
||||
local ci = 1
|
||||
while ci <= table.getn(raw) do
|
||||
local cname = raw[ci + 1]
|
||||
if type(cname) == "string" and string.lower(cname) == "world" then
|
||||
inWorld = true
|
||||
break
|
||||
end
|
||||
ci = ci + 3
|
||||
end
|
||||
if not inWorld then
|
||||
JoinChannelByName("world")
|
||||
local applyWait = 0
|
||||
autoJoinFrame:SetScript("OnUpdate", function()
|
||||
applyWait = applyWait + arg1
|
||||
if applyWait < 3 then return end
|
||||
autoJoinFrame:SetScript("OnUpdate", nil)
|
||||
if SFrames and SFrames.Chat then
|
||||
SFrames.Chat:ApplyAllTabChannels()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
-- 团队成员变化时更新职业缓存
|
||||
|
||||
@@ -411,7 +411,7 @@ local function EnsureDB()
|
||||
if type(SFramesDB.playerPortraitWidth) ~= "number" then SFramesDB.playerPortraitWidth = 50 end
|
||||
if type(SFramesDB.playerHealthHeight) ~= "number" then SFramesDB.playerHealthHeight = 38 end
|
||||
if type(SFramesDB.playerPowerHeight) ~= "number" then SFramesDB.playerPowerHeight = 9 end
|
||||
if SFramesDB.playerShowClass == nil then SFramesDB.playerShowClass = true end
|
||||
if SFramesDB.playerShowClass == nil then SFramesDB.playerShowClass = false end
|
||||
if SFramesDB.playerShowClassIcon == nil then SFramesDB.playerShowClassIcon = true end
|
||||
if SFramesDB.playerShowPortrait == nil then SFramesDB.playerShowPortrait = true end
|
||||
if type(SFramesDB.playerFrameAlpha) ~= "number" then SFramesDB.playerFrameAlpha = 1 end
|
||||
@@ -423,7 +423,7 @@ local function EnsureDB()
|
||||
if type(SFramesDB.targetPortraitWidth) ~= "number" then SFramesDB.targetPortraitWidth = 50 end
|
||||
if type(SFramesDB.targetHealthHeight) ~= "number" then SFramesDB.targetHealthHeight = 38 end
|
||||
if type(SFramesDB.targetPowerHeight) ~= "number" then SFramesDB.targetPowerHeight = 9 end
|
||||
if SFramesDB.targetShowClass == nil then SFramesDB.targetShowClass = true end
|
||||
if SFramesDB.targetShowClass == nil then SFramesDB.targetShowClass = false end
|
||||
if SFramesDB.targetShowClassIcon == nil then SFramesDB.targetShowClassIcon = true end
|
||||
if SFramesDB.targetShowPortrait == nil then SFramesDB.targetShowPortrait = true end
|
||||
if type(SFramesDB.targetFrameAlpha) ~= "number" then SFramesDB.targetFrameAlpha = 1 end
|
||||
|
||||
1
Core.lua
1
Core.lua
@@ -193,6 +193,7 @@ function SFrames:DoFullInitialize()
|
||||
{ "Chat", function() if SFramesDB.enableChat ~= false and SFrames.Chat and SFrames.Chat.Initialize then SFrames.Chat:Initialize() end end },
|
||||
{ "MapReveal", function() if SFrames.MapReveal and SFrames.MapReveal.Initialize then SFrames.MapReveal:Initialize() end end },
|
||||
{ "WorldMap", function() if SFrames.WorldMap and SFrames.WorldMap.Initialize then SFrames.WorldMap:Initialize() end end },
|
||||
{ "ZoneLevelRange", function() if SFrames.ZoneLevelRange and SFrames.ZoneLevelRange.Initialize then SFrames.ZoneLevelRange:Initialize() end end },
|
||||
{ "MapIcons", function() if SFrames.MapIcons and SFrames.MapIcons.Initialize then SFrames.MapIcons:Initialize() end end },
|
||||
{ "Tweaks", function() if SFrames.Tweaks and SFrames.Tweaks.Initialize then SFrames.Tweaks:Initialize() end end },
|
||||
{ "AFKScreen", function() if SFrames.AFKScreen and SFrames.AFKScreen.Initialize then SFrames.AFKScreen:Initialize() end end },
|
||||
|
||||
@@ -123,11 +123,10 @@ local function CreateGuideFrame()
|
||||
closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
|
||||
closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
|
||||
|
||||
local closeTxt = SFrames:CreateFontString(closeBtn, 12, "CENTER")
|
||||
closeTxt:SetAllPoints(closeBtn)
|
||||
closeTxt:SetText("X")
|
||||
local _closeTxt = _A.accentLight or { 0.9, 0.5, 0.5 }
|
||||
closeTxt:SetTextColor(_closeTxt[1], _closeTxt[2], _closeTxt[3])
|
||||
local closeIco = SFrames:CreateIcon(closeBtn, "close", 12)
|
||||
closeIco:SetDrawLayer("OVERLAY")
|
||||
closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
|
||||
closeIco:SetVertexColor(1, 0.7, 0.7)
|
||||
|
||||
closeBtn:SetScript("OnClick", function() f:Hide() end)
|
||||
closeBtn:SetScript("OnEnter", function()
|
||||
|
||||
475
LootDisplay.lua
475
LootDisplay.lua
@@ -65,11 +65,18 @@ local ICON_BACKDROP = {
|
||||
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
||||
}
|
||||
|
||||
local ITEMS_PER_PAGE = 4
|
||||
local PAGE_BAR_H = 20
|
||||
|
||||
local lootRows = {}
|
||||
local activeAlerts = {}
|
||||
local alertAnchor = nil
|
||||
local alertPool = {}
|
||||
|
||||
local origLootFrameUpdate = nil
|
||||
local ShowLootPage
|
||||
local HideBagFullWarning
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Helpers
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -224,6 +231,97 @@ local function CreateLootFrame()
|
||||
closeFS:SetTextColor(0.9, 0.65, 0.65, 1)
|
||||
end)
|
||||
|
||||
-- Page controls (visible only when > ITEMS_PER_PAGE items)
|
||||
local pageBar = CreateFrame("Frame", nil, lootFrame)
|
||||
pageBar:SetHeight(PAGE_BAR_H)
|
||||
pageBar:SetPoint("BOTTOMLEFT", lootFrame, "BOTTOMLEFT", 7, 4)
|
||||
pageBar:SetPoint("BOTTOMRIGHT", lootFrame, "BOTTOMRIGHT", -7, 4)
|
||||
pageBar:SetFrameLevel(lootFrame:GetFrameLevel() + 3)
|
||||
pageBar:EnableMouse(false)
|
||||
pageBar:Hide()
|
||||
lootFrame._pageBar = pageBar
|
||||
|
||||
local pageFS = pageBar:CreateFontString(nil, "OVERLAY")
|
||||
pageFS:SetFont(Font(), 9, "OUTLINE")
|
||||
pageFS:SetPoint("CENTER", pageBar, "CENTER", 0, 0)
|
||||
pageFS:SetTextColor(0.75, 0.75, 0.80, 0.95)
|
||||
lootFrame._pageText = pageFS
|
||||
|
||||
local dim = th.dimText or { 0.55, 0.55, 0.60 }
|
||||
|
||||
local prevBtn = CreateFrame("Button", nil, pageBar)
|
||||
prevBtn:SetWidth(22)
|
||||
prevBtn:SetHeight(16)
|
||||
prevBtn:SetPoint("RIGHT", pageFS, "LEFT", -8, 0)
|
||||
prevBtn:SetFrameLevel(pageBar:GetFrameLevel() + 1)
|
||||
prevBtn:RegisterForClicks("LeftButtonUp")
|
||||
prevBtn:SetBackdrop(ROUND_BACKDROP_SMALL)
|
||||
prevBtn:SetBackdropColor(0.10, 0.09, 0.14, 0.80)
|
||||
prevBtn:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
|
||||
local prevFS2 = prevBtn:CreateFontString(nil, "OVERLAY")
|
||||
prevFS2:SetFont(Font(), 10, "OUTLINE")
|
||||
prevFS2:SetPoint("CENTER", 0, 0)
|
||||
prevFS2:SetText("<")
|
||||
prevFS2:SetTextColor(dim[1], dim[2], dim[3], 0.90)
|
||||
prevBtn:SetScript("OnClick", function()
|
||||
if lootFrame._page and lootFrame._page > 1 then
|
||||
lootFrame._page = lootFrame._page - 1
|
||||
ShowLootPage()
|
||||
end
|
||||
end)
|
||||
prevBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropBorderColor(acc[1], acc[2], acc[3], 0.70)
|
||||
end)
|
||||
prevBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
|
||||
end)
|
||||
lootFrame._prevBtn = prevBtn
|
||||
|
||||
local nextBtn = CreateFrame("Button", nil, pageBar)
|
||||
nextBtn:SetWidth(22)
|
||||
nextBtn:SetHeight(16)
|
||||
nextBtn:SetPoint("LEFT", pageFS, "RIGHT", 8, 0)
|
||||
nextBtn:SetFrameLevel(pageBar:GetFrameLevel() + 1)
|
||||
nextBtn:RegisterForClicks("LeftButtonUp")
|
||||
nextBtn:SetBackdrop(ROUND_BACKDROP_SMALL)
|
||||
nextBtn:SetBackdropColor(0.10, 0.09, 0.14, 0.80)
|
||||
nextBtn:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
|
||||
local nextFS2 = nextBtn:CreateFontString(nil, "OVERLAY")
|
||||
nextFS2:SetFont(Font(), 10, "OUTLINE")
|
||||
nextFS2:SetPoint("CENTER", 0, 0)
|
||||
nextFS2:SetText(">")
|
||||
nextFS2:SetTextColor(dim[1], dim[2], dim[3], 0.90)
|
||||
nextBtn:SetScript("OnClick", function()
|
||||
if lootFrame._page and lootFrame._totalPages and lootFrame._page < lootFrame._totalPages then
|
||||
lootFrame._page = lootFrame._page + 1
|
||||
ShowLootPage()
|
||||
end
|
||||
end)
|
||||
nextBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropBorderColor(acc[1], acc[2], acc[3], 0.70)
|
||||
end)
|
||||
nextBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
|
||||
end)
|
||||
lootFrame._nextBtn = nextBtn
|
||||
|
||||
-- Bag-full warning (hidden by default)
|
||||
local bagFullFS = lootFrame:CreateFontString(nil, "OVERLAY")
|
||||
bagFullFS:SetFont(Font(), 9, "OUTLINE")
|
||||
bagFullFS:SetPoint("LEFT", titleFS, "RIGHT", 6, 0)
|
||||
bagFullFS:SetTextColor(1.0, 0.30, 0.30, 1.0)
|
||||
bagFullFS:Hide()
|
||||
lootFrame._bagFullText = bagFullFS
|
||||
|
||||
-- Escape key closes our loot frame
|
||||
table.insert(UISpecialFrames, "NanamiLootFrame")
|
||||
|
||||
lootFrame:SetScript("OnHide", function()
|
||||
if not this._closingLoot then
|
||||
CloseLoot()
|
||||
end
|
||||
end)
|
||||
|
||||
lootFrame:Hide()
|
||||
return lootFrame
|
||||
end
|
||||
@@ -302,7 +400,166 @@ local function CreateLootRow(parent, index)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Update loot frame
|
||||
-- Bag-full warning helpers
|
||||
--------------------------------------------------------------------------------
|
||||
local function ShowBagFullWarning()
|
||||
if not lootFrame or not lootFrame:IsShown() then return end
|
||||
if lootFrame._bagFullText then
|
||||
lootFrame._bagFullText:SetText("背包已满")
|
||||
lootFrame._bagFullText:Show()
|
||||
end
|
||||
end
|
||||
|
||||
HideBagFullWarning = function()
|
||||
if lootFrame and lootFrame._bagFullText then
|
||||
lootFrame._bagFullText:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Show current page
|
||||
--------------------------------------------------------------------------------
|
||||
ShowLootPage = function()
|
||||
if not lootFrame then return end
|
||||
|
||||
local numItems = lootFrame._numItems or 0
|
||||
local page = lootFrame._page or 1
|
||||
local totalPages = lootFrame._totalPages or 1
|
||||
|
||||
local startSlot = (page - 1) * ITEMS_PER_PAGE + 1
|
||||
local endSlot = startSlot + ITEMS_PER_PAGE - 1
|
||||
if endSlot > numItems then endSlot = numItems end
|
||||
local slotsOnPage = endSlot - startSlot + 1
|
||||
if slotsOnPage < 0 then slotsOnPage = 0 end
|
||||
|
||||
while table.getn(lootRows) < ITEMS_PER_PAGE do
|
||||
local idx = table.getn(lootRows) + 1
|
||||
lootRows[idx] = CreateLootRow(lootFrame, idx)
|
||||
end
|
||||
|
||||
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
|
||||
for i = 1, ITEMS_PER_PAGE do
|
||||
local nb = _G["LootButton" .. i]
|
||||
if nb then nb:Hide() end
|
||||
end
|
||||
|
||||
local hasPages = totalPages > 1
|
||||
local bottomPad = hasPages and (PAGE_BAR_H + 6) or 6
|
||||
local totalH = TITLE_HEIGHT + (slotsOnPage * (ROW_HEIGHT + ROW_GAP)) + bottomPad + 4
|
||||
lootFrame:SetHeight(totalH)
|
||||
|
||||
-- Build visual rows
|
||||
for btnIdx = 1, slotsOnPage do
|
||||
local slotIdx = startSlot + btnIdx - 1
|
||||
local row = lootRows[btnIdx]
|
||||
if not row then break end
|
||||
|
||||
row:ClearAllPoints()
|
||||
row:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 7,
|
||||
-(TITLE_HEIGHT + 2 + (btnIdx - 1) * (ROW_HEIGHT + ROW_GAP)))
|
||||
row:SetWidth(ROW_WIDTH)
|
||||
row.slotIndex = slotIdx
|
||||
|
||||
local texture, itemName, quantity, quality = GetLootSlotInfo(slotIdx)
|
||||
|
||||
if texture then
|
||||
row.icon:SetTexture(texture)
|
||||
local r, g, b = QColor(quality)
|
||||
row._qualColor = { r, g, b }
|
||||
row.qBar:SetVertexColor(r, g, b, 0.90)
|
||||
row.iconFrame:SetBackdropBorderColor(r, g, b, 0.65)
|
||||
row:SetBackdropBorderColor(r, g, b, 0.30)
|
||||
row:SetBackdropColor(row._slotBg[1], row._slotBg[2], row._slotBg[3], row._slotBg[4] or 0.85)
|
||||
row.iconFrame:SetAlpha(1)
|
||||
row.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. (itemName or "") .. "|r")
|
||||
if quantity and quantity > 1 then
|
||||
row.countFS:SetText(tostring(quantity))
|
||||
else
|
||||
row.countFS:SetText("")
|
||||
end
|
||||
else
|
||||
row._qualColor = nil
|
||||
row.icon:SetTexture("")
|
||||
row.iconFrame:SetAlpha(0.25)
|
||||
row.qBar:SetVertexColor(0.3, 0.3, 0.3, 0.30)
|
||||
row.nameFS:SetText("")
|
||||
row.countFS:SetText("")
|
||||
row:SetBackdropColor(0.04, 0.04, 0.06, 0.40)
|
||||
row:SetBackdropBorderColor(0.12, 0.12, 0.18, 0.25)
|
||||
row.iconFrame:SetBackdropBorderColor(0.15, 0.15, 0.20, 0.30)
|
||||
end
|
||||
|
||||
row:Show()
|
||||
end
|
||||
|
||||
-- Let the ORIGINAL Blizzard LootFrame_Update run so that native
|
||||
-- LootButton1-4 get their IDs, slot data, and OnClick set up
|
||||
-- through the trusted native code path (required for LootSlot).
|
||||
if LootFrame then
|
||||
LootFrame.page = page
|
||||
if not LootFrame:IsShown() then LootFrame:Show() end
|
||||
end
|
||||
if origLootFrameUpdate then origLootFrameUpdate() end
|
||||
|
||||
-- Now reposition the native buttons on top of our visual rows
|
||||
for btnIdx = 1, ITEMS_PER_PAGE do
|
||||
local nb = _G["LootButton" .. btnIdx]
|
||||
local row = lootRows[btnIdx]
|
||||
if nb and row and row:IsShown() and row._qualColor then
|
||||
nb:ClearAllPoints()
|
||||
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||
nb:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
nb:SetFrameLevel(row:GetFrameLevel() + 10)
|
||||
nb:SetAlpha(0)
|
||||
nb:EnableMouse(true)
|
||||
nb:Show()
|
||||
|
||||
nb._nanamiRow = row
|
||||
nb:SetScript("OnEnter", function()
|
||||
local slot = this:GetID()
|
||||
if slot then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetLootItem(slot)
|
||||
if CursorUpdate then CursorUpdate() end
|
||||
end
|
||||
local r2 = this._nanamiRow
|
||||
if r2 and r2._qualColor then
|
||||
local qc = r2._qualColor
|
||||
r2:SetBackdropBorderColor(qc[1], qc[2], qc[3], 0.70)
|
||||
r2:SetBackdropColor(qc[1]*0.15, qc[2]*0.15, qc[3]*0.15, 0.90)
|
||||
end
|
||||
end)
|
||||
nb:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
local r2 = this._nanamiRow
|
||||
if r2 then
|
||||
if r2._qualColor then
|
||||
local qc = r2._qualColor
|
||||
r2:SetBackdropBorderColor(qc[1], qc[2], qc[3], 0.30)
|
||||
else
|
||||
r2:SetBackdropBorderColor(r2._slotBd[1], r2._slotBd[2],
|
||||
r2._slotBd[3], r2._slotBd[4] or 0.60)
|
||||
end
|
||||
r2:SetBackdropColor(r2._slotBg[1], r2._slotBg[2],
|
||||
r2._slotBg[3], r2._slotBg[4] or 0.85)
|
||||
end
|
||||
end)
|
||||
else
|
||||
if nb then nb:Hide() end
|
||||
end
|
||||
end
|
||||
|
||||
if hasPages then
|
||||
lootFrame._pageText:SetText(page .. "/" .. totalPages)
|
||||
lootFrame._pageBar:Show()
|
||||
else
|
||||
lootFrame._pageBar:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Update loot frame (direct slot mapping, no compaction)
|
||||
--------------------------------------------------------------------------------
|
||||
local function UpdateLootFrame()
|
||||
local db = GetDB()
|
||||
@@ -316,124 +573,18 @@ local function UpdateLootFrame()
|
||||
|
||||
CreateLootFrame()
|
||||
|
||||
local validSlots = {}
|
||||
for i = 1, numItems do
|
||||
local texture, itemName, quantity, quality = GetLootSlotInfo(i)
|
||||
if texture then
|
||||
table.insert(validSlots, i)
|
||||
end
|
||||
lootFrame._numItems = numItems
|
||||
lootFrame._totalPages = math.ceil(numItems / ITEMS_PER_PAGE)
|
||||
|
||||
if not lootFrame._page or lootFrame._page > lootFrame._totalPages then
|
||||
lootFrame._page = 1
|
||||
end
|
||||
|
||||
local numValid = table.getn(validSlots)
|
||||
if numValid == 0 then
|
||||
if lootFrame then lootFrame:Hide() end
|
||||
return
|
||||
end
|
||||
|
||||
local totalH = TITLE_HEIGHT + (numValid * (ROW_HEIGHT + ROW_GAP)) + 10
|
||||
lootFrame:SetWidth(ROW_WIDTH + 14)
|
||||
lootFrame:SetHeight(totalH)
|
||||
lootFrame:SetScale(db.scale or 1.0)
|
||||
|
||||
while table.getn(lootRows) < numValid do
|
||||
local idx = table.getn(lootRows) + 1
|
||||
lootRows[idx] = CreateLootRow(lootFrame, idx)
|
||||
end
|
||||
ShowLootPage()
|
||||
|
||||
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
|
||||
|
||||
for displayIdx = 1, numValid do
|
||||
local slotIdx = validSlots[displayIdx]
|
||||
local row = lootRows[displayIdx]
|
||||
if not row then break end
|
||||
|
||||
row:ClearAllPoints()
|
||||
row:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 7, -(TITLE_HEIGHT + 2 + (displayIdx - 1) * (ROW_HEIGHT + ROW_GAP)))
|
||||
row:SetWidth(ROW_WIDTH)
|
||||
|
||||
local texture, itemName, quantity, quality = GetLootSlotInfo(slotIdx)
|
||||
row.slotIndex = slotIdx
|
||||
|
||||
row.icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
|
||||
local r, g, b = QColor(quality)
|
||||
row._qualColor = { r, g, b }
|
||||
row.qBar:SetVertexColor(r, g, b, 0.90)
|
||||
row.iconFrame:SetBackdropBorderColor(r, g, b, 0.65)
|
||||
row:SetBackdropBorderColor(r, g, b, 0.30)
|
||||
|
||||
if itemName then
|
||||
row.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. itemName .. "|r")
|
||||
else
|
||||
row.nameFS:SetText("")
|
||||
end
|
||||
|
||||
if quantity and quantity > 1 then
|
||||
row.countFS:SetText(tostring(quantity))
|
||||
else
|
||||
row.countFS:SetText("")
|
||||
end
|
||||
|
||||
row:Show()
|
||||
|
||||
-- Overlay the Blizzard LootButton on top for click handling
|
||||
local maxBtns = LOOTFRAME_NUMITEMS or 4
|
||||
if displayIdx <= maxBtns then
|
||||
local blizzBtn = _G["LootButton" .. displayIdx]
|
||||
if blizzBtn then
|
||||
blizzBtn:SetID(slotIdx)
|
||||
blizzBtn:SetParent(lootFrame)
|
||||
blizzBtn:ClearAllPoints()
|
||||
blizzBtn:SetAllPoints(row)
|
||||
blizzBtn:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
blizzBtn:SetFrameLevel(row:GetFrameLevel() + 10)
|
||||
blizzBtn:SetAlpha(0)
|
||||
blizzBtn:EnableMouse(true)
|
||||
blizzBtn:Show()
|
||||
|
||||
local rowRef = row
|
||||
blizzBtn._nanamiRow = rowRef
|
||||
blizzBtn:SetScript("OnEnter", function()
|
||||
local rw = this._nanamiRow
|
||||
if rw and rw._acc then
|
||||
rw:SetBackdropBorderColor(rw._acc[1], rw._acc[2], rw._acc[3], 0.85)
|
||||
rw:SetBackdropColor(rw._hoverBd[1], rw._hoverBd[2], rw._hoverBd[3], 0.35)
|
||||
end
|
||||
if rw and rw.slotIndex then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
if LootSlotIsItem(rw.slotIndex) then
|
||||
GameTooltip:SetLootItem(rw.slotIndex)
|
||||
else
|
||||
local t, n = GetLootSlotInfo(rw.slotIndex)
|
||||
if n then GameTooltip:SetText(n) end
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
blizzBtn:SetScript("OnLeave", function()
|
||||
local rw = this._nanamiRow
|
||||
if rw and rw._slotBg then
|
||||
rw:SetBackdropColor(rw._slotBg[1], rw._slotBg[2], rw._slotBg[3], rw._slotBg[4] or 0.85)
|
||||
if rw._qualColor then
|
||||
rw:SetBackdropBorderColor(rw._qualColor[1], rw._qualColor[2], rw._qualColor[3], 0.35)
|
||||
else
|
||||
rw:SetBackdropBorderColor(rw._slotBd[1], rw._slotBd[2], rw._slotBd[3], rw._slotBd[4] or 0.60)
|
||||
end
|
||||
end
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Hide unused Blizzard buttons
|
||||
local maxBtns = LOOTFRAME_NUMITEMS or 4
|
||||
for i = numValid + 1, maxBtns do
|
||||
local blizzBtn = _G["LootButton" .. i]
|
||||
if blizzBtn then blizzBtn:Hide() end
|
||||
end
|
||||
|
||||
-- Position: use saved mover position if exists, otherwise follow cursor
|
||||
if not lootFrame._posApplied then
|
||||
local hasSaved = false
|
||||
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
||||
@@ -455,19 +606,18 @@ local function UpdateLootFrame()
|
||||
end
|
||||
|
||||
local function CloseLootFrame()
|
||||
-- Return Blizzard buttons to LootFrame
|
||||
local maxBtns = LOOTFRAME_NUMITEMS or 4
|
||||
for i = 1, maxBtns do
|
||||
local blizzBtn = _G["LootButton" .. i]
|
||||
if blizzBtn and LootFrame then
|
||||
blizzBtn:SetParent(LootFrame)
|
||||
blizzBtn:SetAlpha(1)
|
||||
blizzBtn:Hide()
|
||||
blizzBtn._nanamiRow = nil
|
||||
if lootFrame then
|
||||
lootFrame._closingLoot = true
|
||||
lootFrame:Hide()
|
||||
lootFrame._closingLoot = nil
|
||||
end
|
||||
for i = 1, ITEMS_PER_PAGE do
|
||||
local nb = _G["LootButton" .. i]
|
||||
if nb then nb:Hide() end
|
||||
end
|
||||
if lootFrame then lootFrame:Hide() end
|
||||
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
|
||||
HideBagFullWarning()
|
||||
if LootFrame then LootFrame:Hide() end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -750,7 +900,6 @@ function LD:Initialize()
|
||||
CreateLootFrame()
|
||||
CreateAlertAnchor()
|
||||
|
||||
-- Apply saved positions so frames have valid coordinates for the Mover system
|
||||
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
||||
local applied = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
|
||||
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
|
||||
@@ -768,19 +917,41 @@ function LD:Initialize()
|
||||
end
|
||||
|
||||
SFrames:RegisterEvent("LOOT_OPENED", function()
|
||||
if GetDB().enable then UpdateLootFrame() end
|
||||
if GetDB().enable then
|
||||
if lootFrame then lootFrame._page = 1 end
|
||||
HideBagFullWarning()
|
||||
UpdateLootFrame()
|
||||
end
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("LOOT_SLOT_CLEARED", function()
|
||||
if GetDB().enable and lootFrame and lootFrame:IsShown() then
|
||||
HideBagFullWarning()
|
||||
UpdateLootFrame()
|
||||
end
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("UI_ERROR_MESSAGE", function()
|
||||
if lootFrame and lootFrame:IsShown() then
|
||||
local msg = arg1
|
||||
if msg == ERR_INV_FULL
|
||||
or (INVENTORY_FULL and msg == INVENTORY_FULL)
|
||||
or (msg and (string.find(msg, "背包已满")
|
||||
or string.find(msg, "Inventory is full"))) then
|
||||
ShowBagFullWarning()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("LOOT_CLOSED", function()
|
||||
CloseLootFrame()
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("LOOT_BIND_CONFIRM", function()
|
||||
local slot = arg1
|
||||
if slot then ConfirmLootSlot(slot) end
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("CHAT_MSG_LOOT", function()
|
||||
local playerName = UnitName("player")
|
||||
if not playerName then return end
|
||||
@@ -790,27 +961,49 @@ function LD:Initialize()
|
||||
end
|
||||
end)
|
||||
|
||||
local function HideBlizzardLoot()
|
||||
-- Save the original LootFrame_Update so ShowLootPage can call it
|
||||
-- to let native code set up LootButton IDs and OnClick handlers.
|
||||
origLootFrameUpdate = LootFrame_Update
|
||||
|
||||
if LootFrame then
|
||||
LootFrame:EnableMouse(false)
|
||||
LootFrame:SetAlpha(0)
|
||||
LootFrame:ClearAllPoints()
|
||||
LootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
|
||||
-- Prevent the XML-defined OnHide from calling CloseLoot()
|
||||
LootFrame:SetScript("OnHide", function() end)
|
||||
|
||||
-- Keep LootFrame shown but invisible while our UI is active
|
||||
local origShow = LootFrame.Show
|
||||
LootFrame.Show = function(self)
|
||||
origShow(self)
|
||||
self:EnableMouse(false)
|
||||
self:SetAlpha(0)
|
||||
self:ClearAllPoints()
|
||||
self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
|
||||
self:EnableMouse(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
HideBlizzardLoot()
|
||||
|
||||
local lootHook = CreateFrame("Frame")
|
||||
lootHook:RegisterEvent("ADDON_LOADED")
|
||||
lootHook:SetScript("OnEvent", function()
|
||||
if arg1 == "Blizzard_Loot" then HideBlizzardLoot() end
|
||||
end)
|
||||
-- Block native LootFrame from hiding while we are looting
|
||||
local origHide = LootFrame.Hide
|
||||
LootFrame.Hide = function(self)
|
||||
if lootFrame and lootFrame:IsShown() then
|
||||
return
|
||||
end
|
||||
origHide(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- After the native LootFrame_Update runs (called by the engine or
|
||||
-- by us), reposition native buttons onto our visual rows.
|
||||
LootFrame_Update = function()
|
||||
if origLootFrameUpdate then origLootFrameUpdate() end
|
||||
if not (lootFrame and lootFrame:IsShown()) then return end
|
||||
for i = 1, ITEMS_PER_PAGE do
|
||||
local nb = _G["LootButton" .. i]
|
||||
local row = lootRows[i]
|
||||
if nb and row and row:IsShown() and row._qualColor then
|
||||
nb:ClearAllPoints()
|
||||
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||
nb:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
nb:SetFrameLevel(row:GetFrameLevel() + 10)
|
||||
nb:SetAlpha(0)
|
||||
nb:EnableMouse(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,7 @@ MinimapButton.lua
|
||||
Minimap.lua
|
||||
MapReveal.lua
|
||||
WorldMap.lua
|
||||
ZoneLevelRange.lua
|
||||
MapIcons.lua
|
||||
Tweaks.lua
|
||||
MinimapBuffs.lua
|
||||
|
||||
10
Tooltip.lua
10
Tooltip.lua
@@ -332,6 +332,7 @@ function SFrames.FloatingTooltip:Initialize()
|
||||
-- OnUpdate: line formatting (once) + cursor tracking (every frame)
|
||||
local orig_OnUpdate = GameTooltip:GetScript("OnUpdate")
|
||||
local ttFormatThrottle = 0
|
||||
local ttHideGrace = 0
|
||||
GameTooltip:SetScript("OnUpdate", function()
|
||||
if orig_OnUpdate then orig_OnUpdate() end
|
||||
|
||||
@@ -342,12 +343,20 @@ function SFrames.FloatingTooltip:Initialize()
|
||||
local hasUnit = UnitExists("mouseover")
|
||||
|
||||
if ttHadUnit and not hasUnit then
|
||||
ttHideGrace = ttHideGrace + arg1
|
||||
if ttHideGrace < 0.2 then
|
||||
TT_ShowBar(false)
|
||||
return
|
||||
end
|
||||
ttHideGrace = 0
|
||||
TT_ShowBar(false)
|
||||
if GameTooltip._nanamiBGFrame then
|
||||
GameTooltip._nanamiBGFrame:Hide()
|
||||
end
|
||||
this:Hide()
|
||||
return
|
||||
else
|
||||
ttHideGrace = 0
|
||||
end
|
||||
|
||||
if not hasUnit then
|
||||
@@ -386,6 +395,7 @@ function SFrames.FloatingTooltip:Initialize()
|
||||
linesFormatted = false
|
||||
ttOwner = nil
|
||||
ttHadUnit = false
|
||||
ttHideGrace = 0
|
||||
TT_ShowBar(false)
|
||||
if GameTooltip._nanamiBGFrame then
|
||||
GameTooltip._nanamiBGFrame:Hide()
|
||||
|
||||
@@ -1434,6 +1434,10 @@ function TSUI.ProfNamesMatch(a, b)
|
||||
end
|
||||
|
||||
function TSUI.ScanProfessions()
|
||||
local now = GetTime()
|
||||
if S._profScanTime and (now - S._profScanTime) < 5.0 and table.getn(S.profList) > 0 then
|
||||
return
|
||||
end
|
||||
S.profList = {}
|
||||
if not GetSpellName then return end
|
||||
local seen = {}
|
||||
@@ -1449,6 +1453,7 @@ function TSUI.ScanProfessions()
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
S._profScanTime = now
|
||||
end
|
||||
|
||||
function TSUI.CreateProfTabs(parent)
|
||||
@@ -1549,6 +1554,32 @@ function TSUI.IsTabSwitching()
|
||||
return S.switchStartTime and (GetTime() - S.switchStartTime) < 1.0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Update Throttle (batches rapid TRADE_SKILL_UPDATE / CRAFT_UPDATE events)
|
||||
--------------------------------------------------------------------------------
|
||||
local updateThrottleFrame = CreateFrame("Frame")
|
||||
updateThrottleFrame:Hide()
|
||||
updateThrottleFrame._elapsed = 0
|
||||
updateThrottleFrame:SetScript("OnUpdate", function()
|
||||
this._elapsed = this._elapsed + arg1
|
||||
if this._elapsed >= 0.10 then
|
||||
this:Hide()
|
||||
if S.MainFrame and S.MainFrame:IsVisible() then
|
||||
TSUI.UpdateProgressBar()
|
||||
TSUI.FullUpdate()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function TSUI.ScheduleUpdate()
|
||||
updateThrottleFrame._elapsed = 0
|
||||
updateThrottleFrame:Show()
|
||||
end
|
||||
|
||||
function TSUI.CancelScheduledUpdate()
|
||||
updateThrottleFrame:Hide()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hide Blizzard Frames (module methods)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -2018,6 +2049,7 @@ function TSUI:Initialize()
|
||||
-- ═══ Events ══════════════════════════════════════════════════════
|
||||
MF:SetScript("OnHide", function()
|
||||
S.switchStartTime = nil
|
||||
TSUI.CancelScheduledUpdate()
|
||||
API.CloseRecipe()
|
||||
if S.currentMode == "tradeskill" then TSUI.CleanupBlizzardTradeSkill()
|
||||
else TSUI.CleanupBlizzardCraft() end
|
||||
@@ -2049,7 +2081,7 @@ function TSUI:Initialize()
|
||||
end)
|
||||
elseif event == "TRADE_SKILL_UPDATE" then
|
||||
if S.MainFrame:IsVisible() and S.currentMode == "tradeskill" then
|
||||
TSUI.UpdateProgressBar(); TSUI.FullUpdate()
|
||||
TSUI.ScheduleUpdate()
|
||||
end
|
||||
elseif event == "TRADE_SKILL_CLOSE" then
|
||||
TSUI.CleanupBlizzardTradeSkill()
|
||||
@@ -2081,7 +2113,7 @@ function TSUI:Initialize()
|
||||
end)
|
||||
elseif event == "CRAFT_UPDATE" then
|
||||
if S.MainFrame:IsVisible() and S.currentMode == "craft" then
|
||||
TSUI.UpdateProgressBar(); TSUI.FullUpdate()
|
||||
TSUI.ScheduleUpdate()
|
||||
end
|
||||
elseif event == "CRAFT_CLOSE" then
|
||||
TSUI.CleanupBlizzardCraft()
|
||||
@@ -2107,14 +2139,20 @@ function TSUI.ResetAndShow()
|
||||
if S.MainFrame.searchBox then S.MainFrame.searchBox:SetText("") end
|
||||
if S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end
|
||||
if S.MainFrame.listScroll then S.MainFrame.listScroll:SetVerticalScroll(0) end
|
||||
TSUI.UpdateProgressBar(); S.MainFrame:Show(); TSUI.FullUpdate()
|
||||
TSUI.UpdateScrollbar()
|
||||
TSUI.CancelScheduledUpdate()
|
||||
TSUI.UpdateProgressBar(); S.MainFrame:Show()
|
||||
TSUI.UpdateProfTabs()
|
||||
TSUI.BuildDisplayList()
|
||||
for _, entry in ipairs(S.displayList) do
|
||||
if entry.type == "recipe" then
|
||||
TSUI.SelectRecipe(entry.data.index); break
|
||||
S.selectedIndex = entry.data.index
|
||||
S.craftAmount = 1
|
||||
API.SelectRecipe(entry.data.index)
|
||||
break
|
||||
end
|
||||
end
|
||||
TSUI.FullUpdate()
|
||||
TSUI.UpdateScrollbar()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
104
Tweaks.lua
104
Tweaks.lua
@@ -7,6 +7,7 @@
|
||||
-- 5. Dark UI - darken the entire interface
|
||||
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
|
||||
-- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells
|
||||
-- 8. Hunter Aspect Guard - auto switch to Hawk when taking damage with Cheetah/Pack
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
SFrames.Tweaks = SFrames.Tweaks or {}
|
||||
@@ -17,7 +18,8 @@ SFrames.castdb = SFrames.castdb or {}
|
||||
local function GetTweaksCfg()
|
||||
if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then
|
||||
return { autoStance = true, superWoW = true, turtleCompat = true,
|
||||
cooldownNumbers = true, darkUI = false, worldMapWindow = false }
|
||||
cooldownNumbers = true, darkUI = false, worldMapWindow = false,
|
||||
hunterAspectGuard = true }
|
||||
end
|
||||
return SFramesDB.Tweaks
|
||||
end
|
||||
@@ -80,6 +82,7 @@ end
|
||||
--------------------------------------------------------------------------------
|
||||
local function InitAutoDismount()
|
||||
local dismount = CreateFrame("Frame", "NanamiAutoDismount")
|
||||
local _, playerClass = UnitClass("player")
|
||||
|
||||
local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate")
|
||||
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
@@ -93,7 +96,7 @@ local function InitAutoDismount()
|
||||
"^Augmente la vitesse de (.+)%%",
|
||||
"^Скорость увеличена на (.+)%%",
|
||||
"^이동 속도 (.+)%%만큼 증가",
|
||||
"^速度提高(.+)%%",
|
||||
"^速度提高(.+)%%", "^移动速度提高(.+)%%",
|
||||
"speed based on", "Slow and steady...", "Riding",
|
||||
"Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.",
|
||||
"根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......",
|
||||
@@ -106,9 +109,15 @@ local function InitAutoDismount()
|
||||
"ability_druid_treeoflife", "ability_druid_stagform",
|
||||
}
|
||||
|
||||
local hunterAspectIcons = {
|
||||
"ability_mount_jungletiger",
|
||||
"ability_mount_packhorse",
|
||||
}
|
||||
|
||||
local errorStrings = {}
|
||||
local errorGlobals = {
|
||||
"SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED",
|
||||
"ERR_NOT_WHILE_MOUNTED",
|
||||
"SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED",
|
||||
"SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE",
|
||||
"ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED",
|
||||
@@ -134,6 +143,21 @@ local function InitAutoDismount()
|
||||
if not matched then return end
|
||||
|
||||
for i = 0, 31 do
|
||||
local buff = GetPlayerBuffTexture(i)
|
||||
if buff then
|
||||
local lowerBuff = string.lower(buff)
|
||||
|
||||
local skip = false
|
||||
if playerClass == "HUNTER" then
|
||||
for _, tex in pairs(hunterAspectIcons) do
|
||||
if string.find(lowerBuff, tex) then
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not skip then
|
||||
scanner:ClearLines()
|
||||
scanner:SetPlayerBuff(i)
|
||||
for line = 1, scanner:NumLines() do
|
||||
@@ -148,10 +172,14 @@ local function InitAutoDismount()
|
||||
end
|
||||
end
|
||||
|
||||
local buff = GetPlayerBuffTexture(i)
|
||||
if buff then
|
||||
for _, icon in pairs(shapeshiftIcons) do
|
||||
if string.find(string.lower(buff), icon) then
|
||||
if string.find(lowerBuff, icon) then
|
||||
CancelPlayerBuff(i)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if string.find(lowerBuff, "ability_mount_") then
|
||||
CancelPlayerBuff(i)
|
||||
return
|
||||
end
|
||||
@@ -167,7 +195,7 @@ end
|
||||
-- Data stored in SFrames.castdb[guid] for consumption by castbar features.
|
||||
--------------------------------------------------------------------------------
|
||||
local function InitSuperWoW()
|
||||
if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then return end
|
||||
if not SpellInfo and not UnitGUID and not SUPERWOW_VERSION then return end
|
||||
|
||||
local castdb = SFrames.castdb
|
||||
|
||||
@@ -1031,6 +1059,63 @@ local function InitDarkUI()
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hunter Aspect Guard
|
||||
-- When a Hunter takes damage with Aspect of the Cheetah or Aspect of the Pack
|
||||
-- active, automatically cancel the aspect to prevent repeated dazing.
|
||||
--------------------------------------------------------------------------------
|
||||
local function InitHunterAspectGuard()
|
||||
local _, playerClass = UnitClass("player")
|
||||
if playerClass ~= "HUNTER" then return end
|
||||
|
||||
local CHEETAH_TEX = "ability_mount_jungletiger"
|
||||
local PACK_TEX = "ability_mount_packhorse"
|
||||
|
||||
local function CancelDangerousAspect()
|
||||
for i = 0, 31 do
|
||||
local buffIdx = GetPlayerBuff(i, "HELPFUL")
|
||||
if buffIdx and buffIdx >= 0 then
|
||||
local tex = GetPlayerBuffTexture(buffIdx)
|
||||
if tex then
|
||||
local lower = string.lower(tex)
|
||||
if string.find(lower, CHEETAH_TEX) or string.find(lower, PACK_TEX) then
|
||||
CancelPlayerBuff(buffIdx)
|
||||
SFrames:Print("受到伤害,已自动取消守护")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local lastHP = UnitHealth("player") or 0
|
||||
local lastCancel = 0
|
||||
local elapsed = 0
|
||||
local frame = CreateFrame("Frame", "NanamiHunterAspectGuard")
|
||||
frame:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + (arg1 or 0)
|
||||
if elapsed < 0.1 then return end
|
||||
elapsed = 0
|
||||
|
||||
local hp = UnitHealth("player")
|
||||
if hp <= 0 then
|
||||
lastHP = 0
|
||||
return
|
||||
end
|
||||
|
||||
if lastHP > 0 and hp < lastHP then
|
||||
if GetTime() - lastCancel >= 1.0 then
|
||||
if CancelDangerousAspect() then
|
||||
lastCancel = GetTime()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
lastHP = hp
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Module API
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1088,6 +1173,13 @@ function Tweaks:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
if cfg.hunterAspectGuard ~= false then
|
||||
local ok, err = pcall(InitHunterAspectGuard)
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: HunterAspectGuard init failed: " .. tostring(err) .. "|r")
|
||||
end
|
||||
end
|
||||
|
||||
if cfg.darkUI then
|
||||
local ok, err = pcall(InitDarkUI)
|
||||
if not ok then
|
||||
|
||||
432
Units/Pet.lua
432
Units/Pet.lua
@@ -351,6 +351,8 @@ function SFrames.Pet:Initialize()
|
||||
self.frame.unit = "pet"
|
||||
f:Hide()
|
||||
|
||||
self:CreateAuras()
|
||||
self:CreateHappinessWarning()
|
||||
self:CreateCastbar()
|
||||
|
||||
SFrames:RegisterEvent("UNIT_PET", function() if arg1 == "player" then self:UpdateAll() end end)
|
||||
@@ -395,6 +397,7 @@ function SFrames.Pet:UpdateAll()
|
||||
if SFramesDB and SFramesDB.showPetFrame == false then
|
||||
self.frame:Hide()
|
||||
if self.foodPanel then self.foodPanel:Hide() end
|
||||
self:HideAuras()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -403,6 +406,7 @@ function SFrames.Pet:UpdateAll()
|
||||
self:UpdatePowerType()
|
||||
self:UpdatePower()
|
||||
self:UpdateHappiness()
|
||||
self:UpdateAuras()
|
||||
|
||||
local name = UnitName("pet")
|
||||
if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
|
||||
@@ -415,6 +419,7 @@ function SFrames.Pet:UpdateAll()
|
||||
else
|
||||
self.frame:Hide()
|
||||
if self.foodPanel then self.foodPanel:Hide() end
|
||||
self:HideAuras()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -452,6 +457,7 @@ function SFrames.Pet:UpdateHappiness()
|
||||
local happiness = GetPetHappiness()
|
||||
if not happiness then
|
||||
self.frame.happinessBG:Hide()
|
||||
self:HideHappinessWarning()
|
||||
self:UpdateFoodButton()
|
||||
return
|
||||
end
|
||||
@@ -471,8 +477,10 @@ function SFrames.Pet:UpdateHappiness()
|
||||
self.frame.happiness:SetTexCoord(0, 0.1875, 0, 0.359375)
|
||||
self.frame.happinessBG:Show()
|
||||
end
|
||||
self:ShowHappinessWarning(happiness)
|
||||
else
|
||||
self.frame.happinessBG:Hide()
|
||||
self:HideHappinessWarning()
|
||||
end
|
||||
self:UpdateFoodButton()
|
||||
end
|
||||
@@ -1018,6 +1026,430 @@ function SFrames.Pet:UpdateFoodButton()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Pet Buff / Debuff Auras
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local PET_AURA_SIZE = 20
|
||||
local PET_AURA_SPACING = 2
|
||||
local PET_AURA_ROW_SPACING = 1
|
||||
local PET_AURAS_PER_ROW = 6
|
||||
local PET_BUFF_COUNT = 16
|
||||
local PET_DEBUFF_COUNT = 16
|
||||
|
||||
function SFrames.Pet:CreateAuras()
|
||||
local f = self.frame
|
||||
f.buffs = {}
|
||||
f.debuffs = {}
|
||||
|
||||
for i = 1, PET_BUFF_COUNT do
|
||||
local b = CreateFrame("Button", "SFramesPetBuff" .. i, f)
|
||||
b:SetWidth(PET_AURA_SIZE)
|
||||
b:SetHeight(PET_AURA_SIZE)
|
||||
SFrames:CreateUnitBackdrop(b)
|
||||
|
||||
b.icon = b:CreateTexture(nil, "ARTWORK")
|
||||
b.icon:SetAllPoints()
|
||||
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
|
||||
|
||||
b.cdText = SFrames:CreateFontString(b, 8, "CENTER")
|
||||
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
|
||||
b.cdText:SetTextColor(1, 0.82, 0)
|
||||
b.cdText:SetShadowColor(0, 0, 0, 1)
|
||||
b.cdText:SetShadowOffset(1, -1)
|
||||
|
||||
b:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
|
||||
GameTooltip:SetUnitBuff("pet", this:GetID())
|
||||
end)
|
||||
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
if i == 1 then
|
||||
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -1)
|
||||
elseif math.mod(i - 1, PET_AURAS_PER_ROW) == 0 then
|
||||
b:SetPoint("TOP", f.buffs[i - PET_AURAS_PER_ROW], "BOTTOM", 0, -PET_AURA_ROW_SPACING)
|
||||
else
|
||||
b:SetPoint("LEFT", f.buffs[i - 1], "RIGHT", PET_AURA_SPACING, 0)
|
||||
end
|
||||
b:Hide()
|
||||
f.buffs[i] = b
|
||||
end
|
||||
|
||||
for i = 1, PET_DEBUFF_COUNT do
|
||||
local b = CreateFrame("Button", "SFramesPetDebuff" .. i, f)
|
||||
b:SetWidth(PET_AURA_SIZE)
|
||||
b:SetHeight(PET_AURA_SIZE)
|
||||
SFrames:CreateUnitBackdrop(b)
|
||||
|
||||
b.icon = b:CreateTexture(nil, "ARTWORK")
|
||||
b.icon:SetAllPoints()
|
||||
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
|
||||
|
||||
b.cdText = SFrames:CreateFontString(b, 8, "CENTER")
|
||||
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
|
||||
b.cdText:SetTextColor(1, 0.82, 0)
|
||||
b.cdText:SetShadowColor(0, 0, 0, 1)
|
||||
b.cdText:SetShadowOffset(1, -1)
|
||||
|
||||
b:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
|
||||
GameTooltip:SetUnitDebuff("pet", this:GetID())
|
||||
end)
|
||||
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
if i == 1 then
|
||||
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -1)
|
||||
elseif math.mod(i - 1, PET_AURAS_PER_ROW) == 0 then
|
||||
b:SetPoint("TOP", f.debuffs[i - PET_AURAS_PER_ROW], "BOTTOM", 0, -PET_AURA_ROW_SPACING)
|
||||
else
|
||||
b:SetPoint("LEFT", f.debuffs[i - 1], "RIGHT", PET_AURA_SPACING, 0)
|
||||
end
|
||||
b:Hide()
|
||||
f.debuffs[i] = b
|
||||
end
|
||||
|
||||
SFrames:RegisterEvent("UNIT_AURA", function()
|
||||
if arg1 == "pet" then SFrames.Pet:UpdateAuras() end
|
||||
end)
|
||||
|
||||
self.petAuraUpdater = CreateFrame("Frame", nil, f)
|
||||
self.petAuraUpdater.timer = 0
|
||||
self.petAuraUpdater:SetScript("OnUpdate", function()
|
||||
this.timer = this.timer + arg1
|
||||
if this.timer >= 0.25 then
|
||||
SFrames.Pet:TickAuras()
|
||||
this.timer = 0
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function SFrames.Pet:UpdateAuras()
|
||||
if not UnitExists("pet") then return end
|
||||
local f = self.frame
|
||||
if not f.buffs then return end
|
||||
|
||||
local numBuffs = 0
|
||||
for i = 1, PET_BUFF_COUNT do
|
||||
local texture = UnitBuff("pet", i)
|
||||
local b = f.buffs[i]
|
||||
b:SetID(i)
|
||||
if texture then
|
||||
b.icon:SetTexture(texture)
|
||||
|
||||
if SFrames.Tooltip then
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:ClearLines()
|
||||
SFrames.Tooltip:SetUnitBuff("pet", i)
|
||||
end
|
||||
local timeLeft = SFrames:GetAuraTimeLeft("pet", i, true)
|
||||
if SFrames.Tooltip then SFrames.Tooltip:Hide() end
|
||||
|
||||
if timeLeft and timeLeft > 0 then
|
||||
b.expirationTime = GetTime() + timeLeft
|
||||
b.cdText:SetText(SFrames:FormatTime(timeLeft))
|
||||
else
|
||||
b.expirationTime = nil
|
||||
b.cdText:SetText("")
|
||||
end
|
||||
|
||||
b:Show()
|
||||
numBuffs = numBuffs + 1
|
||||
else
|
||||
b.expirationTime = nil
|
||||
b.cdText:SetText("")
|
||||
b:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local firstDebuff = f.debuffs[1]
|
||||
if firstDebuff then
|
||||
firstDebuff:ClearAllPoints()
|
||||
if numBuffs > 0 then
|
||||
local lastRowStart = math.floor((numBuffs - 1) / PET_AURAS_PER_ROW) * PET_AURAS_PER_ROW + 1
|
||||
firstDebuff:SetPoint("TOP", f.buffs[lastRowStart], "BOTTOM", 0, -PET_AURA_ROW_SPACING)
|
||||
else
|
||||
firstDebuff:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
|
||||
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
|
||||
|
||||
for i = 1, PET_DEBUFF_COUNT do
|
||||
local texture, debuffCount, debuffType = UnitDebuff("pet", i)
|
||||
local b = f.debuffs[i]
|
||||
b:SetID(i)
|
||||
if texture then
|
||||
b.icon:SetTexture(texture)
|
||||
|
||||
if debuffType and DebuffTypeColor and DebuffTypeColor[debuffType] then
|
||||
local c = DebuffTypeColor[debuffType]
|
||||
b:SetBackdropBorderColor(c.r, c.g, c.b, 1)
|
||||
else
|
||||
b:SetBackdropBorderColor(0.8, 0, 0, 1)
|
||||
end
|
||||
|
||||
local timeLeft = 0
|
||||
local effectName = nil
|
||||
|
||||
if hasNP then
|
||||
local effect, rank, _, stacks, dtype, duration, npTimeLeft, isOwn = NanamiPlates_SpellDB:UnitDebuff("pet", i)
|
||||
effectName = effect
|
||||
if npTimeLeft and npTimeLeft > 0 then
|
||||
timeLeft = npTimeLeft
|
||||
elseif effect and effect ~= "" and duration and duration > 0
|
||||
and NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
||||
local unitKey = (UnitGUID and UnitGUID("pet")) or UnitName("pet") or ""
|
||||
local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect]
|
||||
if not cached and UnitName("pet") then
|
||||
cached = NanamiPlates_Auras.timers[UnitName("pet") .. "_" .. effect]
|
||||
end
|
||||
if cached and cached.startTime and cached.duration then
|
||||
local remaining = cached.duration - (GetTime() - cached.startTime)
|
||||
if remaining > 0 then timeLeft = remaining end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if timeLeft <= 0 then
|
||||
if SFrames.Tooltip then
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:ClearLines()
|
||||
SFrames.Tooltip:SetUnitDebuff("pet", i)
|
||||
end
|
||||
timeLeft = SFrames:GetAuraTimeLeft("pet", i, false)
|
||||
if SFrames.Tooltip then SFrames.Tooltip:Hide() end
|
||||
end
|
||||
|
||||
if timeLeft and timeLeft > 0 then
|
||||
b.expirationTime = GetTime() + timeLeft
|
||||
b.effectName = effectName
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
b.cdText:SetText(text)
|
||||
if r then b.cdText:SetTextColor(r, g, bc, a or 1) end
|
||||
else
|
||||
b.cdText:SetText(SFrames:FormatTime(timeLeft))
|
||||
end
|
||||
else
|
||||
b.expirationTime = nil
|
||||
b.effectName = nil
|
||||
b.cdText:SetText("")
|
||||
end
|
||||
|
||||
b:Show()
|
||||
else
|
||||
b.expirationTime = nil
|
||||
b.effectName = nil
|
||||
b.cdText:SetText("")
|
||||
b:SetBackdropBorderColor(0, 0, 0, 1)
|
||||
b:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Pet:TickAuras()
|
||||
if not UnitExists("pet") then return end
|
||||
local f = self.frame
|
||||
if not f.buffs then return end
|
||||
|
||||
local timeNow = GetTime()
|
||||
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
|
||||
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.FindEffectData
|
||||
|
||||
local petName, petLevel, petGUID
|
||||
if hasNP then
|
||||
petName = UnitName("pet")
|
||||
petLevel = UnitLevel("pet") or 0
|
||||
petGUID = UnitGUID and UnitGUID("pet")
|
||||
end
|
||||
|
||||
for i = 1, PET_BUFF_COUNT do
|
||||
local b = f.buffs[i]
|
||||
if b:IsShown() and b.expirationTime then
|
||||
local timeLeft = b.expirationTime - timeNow
|
||||
if timeLeft > 0 and timeLeft < 3600 then
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
b.cdText:SetText(text)
|
||||
if r then b.cdText:SetTextColor(r, g, bc, a or 1) end
|
||||
else
|
||||
b.cdText:SetText(SFrames:FormatTime(timeLeft))
|
||||
end
|
||||
else
|
||||
b.cdText:SetText("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, PET_DEBUFF_COUNT do
|
||||
local b = f.debuffs[i]
|
||||
if b:IsShown() then
|
||||
local timeLeft = nil
|
||||
|
||||
if hasNP and b.effectName then
|
||||
local data = petGUID and NanamiPlates_SpellDB:FindEffectData(petGUID, petLevel, b.effectName)
|
||||
if not data and petName then
|
||||
data = NanamiPlates_SpellDB:FindEffectData(petName, petLevel, b.effectName)
|
||||
end
|
||||
if data and data.start and data.duration then
|
||||
local remaining = data.duration + data.start - timeNow
|
||||
if remaining > 0 then
|
||||
timeLeft = remaining
|
||||
b.expirationTime = timeNow + remaining
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not timeLeft and b.expirationTime then
|
||||
timeLeft = b.expirationTime - timeNow
|
||||
end
|
||||
|
||||
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
b.cdText:SetText(text)
|
||||
if r then b.cdText:SetTextColor(r, g, bc, a or 1) end
|
||||
else
|
||||
b.cdText:SetText(SFrames:FormatTime(timeLeft))
|
||||
end
|
||||
else
|
||||
b.cdText:SetText("")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Pet:HideAuras()
|
||||
local f = self.frame
|
||||
if not f or not f.buffs then return end
|
||||
for i = 1, PET_BUFF_COUNT do
|
||||
f.buffs[i].expirationTime = nil
|
||||
f.buffs[i].cdText:SetText("")
|
||||
f.buffs[i]:Hide()
|
||||
end
|
||||
for i = 1, PET_DEBUFF_COUNT do
|
||||
f.debuffs[i].expirationTime = nil
|
||||
f.debuffs[i].effectName = nil
|
||||
f.debuffs[i].cdText:SetText("")
|
||||
f.debuffs[i]:SetBackdropBorderColor(0, 0, 0, 1)
|
||||
f.debuffs[i]:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Pet Happiness Warning
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local WARNING_REMIND_INTERVAL_YELLOW = 60
|
||||
local WARNING_REMIND_INTERVAL_RED = 30
|
||||
|
||||
function SFrames.Pet:CreateHappinessWarning()
|
||||
local f = self.frame
|
||||
local fontPath = SFrames:GetFont()
|
||||
local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
|
||||
|
||||
local warnFrame = CreateFrame("Frame", "SFramesPetHappinessWarn", f)
|
||||
warnFrame:SetWidth(180)
|
||||
warnFrame:SetHeight(22)
|
||||
warnFrame:SetPoint("BOTTOM", f, "TOP", 0, 2)
|
||||
warnFrame:SetFrameStrata("HIGH")
|
||||
|
||||
local warnBg = warnFrame:CreateTexture(nil, "BACKGROUND")
|
||||
warnBg:SetAllPoints()
|
||||
warnBg:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||||
warnBg:SetVertexColor(0, 0, 0, 0.55)
|
||||
warnFrame.bg = warnBg
|
||||
|
||||
local warnText = warnFrame:CreateFontString(nil, "OVERLAY")
|
||||
warnText:SetFont(fontPath, 11, outline)
|
||||
warnText:SetPoint("CENTER", warnFrame, "CENTER", 0, 0)
|
||||
warnText:SetShadowColor(0, 0, 0, 1)
|
||||
warnText:SetShadowOffset(1, -1)
|
||||
warnFrame.text = warnText
|
||||
|
||||
warnFrame:Hide()
|
||||
self.warnFrame = warnFrame
|
||||
self.lastHappiness = nil
|
||||
self.lastWarnTime = 0
|
||||
self.warnFlashAlpha = 1
|
||||
self.warnFlashDir = -1
|
||||
|
||||
warnFrame:SetScript("OnUpdate", function()
|
||||
SFrames.Pet:WarningFlashUpdate()
|
||||
end)
|
||||
end
|
||||
|
||||
function SFrames.Pet:WarningFlashUpdate()
|
||||
if not self.warnFrame or not self.warnFrame:IsShown() then return end
|
||||
if not self.warnSeverity or self.warnSeverity ~= "red" then return end
|
||||
|
||||
local speed = 2.5
|
||||
local dt = arg1 or 0.016
|
||||
self.warnFlashAlpha = self.warnFlashAlpha + self.warnFlashDir * speed * dt
|
||||
|
||||
if self.warnFlashAlpha <= 0.25 then
|
||||
self.warnFlashAlpha = 0.25
|
||||
self.warnFlashDir = 1
|
||||
elseif self.warnFlashAlpha >= 1 then
|
||||
self.warnFlashAlpha = 1
|
||||
self.warnFlashDir = -1
|
||||
end
|
||||
|
||||
self.warnFrame.text:SetAlpha(self.warnFlashAlpha)
|
||||
self.warnFrame.bg:SetVertexColor(0.4, 0, 0, 0.55 * self.warnFlashAlpha)
|
||||
end
|
||||
|
||||
function SFrames.Pet:ShowHappinessWarning(happiness)
|
||||
if not self.warnFrame then return end
|
||||
|
||||
if happiness == 3 then
|
||||
self:HideHappinessWarning()
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local isNewState = (self.lastHappiness ~= happiness)
|
||||
|
||||
if happiness == 2 then
|
||||
self.warnFrame.text:SetText("宠物心情一般,攻击力受影响!")
|
||||
self.warnFrame.text:SetTextColor(1, 0.82, 0.2)
|
||||
self.warnFrame.bg:SetVertexColor(0.3, 0.25, 0, 0.55)
|
||||
self.warnFrame.text:SetAlpha(1)
|
||||
self.warnSeverity = "yellow"
|
||||
self.warnFrame:Show()
|
||||
|
||||
if isNewState or (now - self.lastWarnTime >= WARNING_REMIND_INTERVAL_YELLOW) then
|
||||
SFrames:Print("|cffffff00宠物心情一般|r - 攻击力下降,请及时喂食!")
|
||||
self.lastWarnTime = now
|
||||
end
|
||||
elseif happiness == 1 then
|
||||
self.warnFrame.text:SetText("宠物很不开心,快要跑了!")
|
||||
self.warnFrame.text:SetTextColor(1, 0.2, 0.2)
|
||||
self.warnFrame.bg:SetVertexColor(0.4, 0, 0, 0.55)
|
||||
self.warnFlashAlpha = 1
|
||||
self.warnFlashDir = -1
|
||||
self.warnSeverity = "red"
|
||||
self.warnFrame:Show()
|
||||
|
||||
if isNewState or (now - self.lastWarnTime >= WARNING_REMIND_INTERVAL_RED) then
|
||||
SFrames:Print("|cffff3333宠物非常不开心,即将离你而去!|r 请立即喂食!")
|
||||
UIErrorsFrame:AddMessage("宠物快要跑了!请立即喂食!", 1, 0.2, 0.2, 1, 3)
|
||||
self.lastWarnTime = now
|
||||
end
|
||||
end
|
||||
|
||||
self.lastHappiness = happiness
|
||||
end
|
||||
|
||||
function SFrames.Pet:HideHappinessWarning()
|
||||
if self.warnFrame then
|
||||
self.warnFrame:Hide()
|
||||
end
|
||||
self.warnSeverity = nil
|
||||
self.lastHappiness = nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Pet Castbar
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1620,6 +1620,17 @@ function SFrames.Player:CastbarStart(spellName, duration)
|
||||
local _, _, _, tex = _UnitCastingInfo("player")
|
||||
texture = tex
|
||||
end
|
||||
if not texture and SFrames.castdb and UnitGUID then
|
||||
local guid = UnitGUID("player")
|
||||
if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then
|
||||
texture = SFrames.castdb[guid].icon
|
||||
end
|
||||
end
|
||||
if (not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
and SFrames.GetSpellIcon then
|
||||
texture = SFrames.GetSpellIcon(spellName) or texture
|
||||
end
|
||||
|
||||
if texture then
|
||||
cb.icon:SetTexture(texture)
|
||||
cb.icon:Show()
|
||||
@@ -1657,6 +1668,17 @@ function SFrames.Player:CastbarChannelStart(duration, spellName)
|
||||
local _, _, _, tex = _UnitChannelInfo("player")
|
||||
texture = tex
|
||||
end
|
||||
if not texture and SFrames.castdb and UnitGUID then
|
||||
local guid = UnitGUID("player")
|
||||
if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then
|
||||
texture = SFrames.castdb[guid].icon
|
||||
end
|
||||
end
|
||||
if (not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
and SFrames.GetSpellIcon then
|
||||
texture = SFrames.GetSpellIcon(spellName) or texture
|
||||
end
|
||||
|
||||
if texture then
|
||||
cb.icon:SetTexture(texture)
|
||||
cb.icon:Show()
|
||||
@@ -1714,6 +1736,9 @@ function SFrames.Player:CastbarOnUpdate()
|
||||
end
|
||||
cb:SetValue(elapsed)
|
||||
cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0)))
|
||||
if not cb.icon:IsShown() then
|
||||
self:CastbarTryResolveIcon()
|
||||
end
|
||||
elseif cb.channeling then
|
||||
local timeRemaining = cb.endTime - GetTime()
|
||||
if timeRemaining <= 0 then
|
||||
@@ -1724,6 +1749,9 @@ function SFrames.Player:CastbarOnUpdate()
|
||||
end
|
||||
cb:SetValue(timeRemaining)
|
||||
cb.time:SetText(string.format("%.1f", timeRemaining))
|
||||
if not cb.icon:IsShown() then
|
||||
self:CastbarTryResolveIcon()
|
||||
end
|
||||
elseif cb.fadeOut then
|
||||
local alpha = cb:GetAlpha() - 0.05
|
||||
if alpha > 0 then
|
||||
@@ -1740,3 +1768,41 @@ function SFrames.Player:CastbarOnUpdate()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Player:CastbarTryResolveIcon()
|
||||
local cb = self.frame.castbar
|
||||
local spellName = cb.text:GetText()
|
||||
local texture
|
||||
|
||||
if SFrames.castdb and UnitGUID then
|
||||
local guid = UnitGUID("player")
|
||||
if guid and SFrames.castdb[guid] then
|
||||
local entry = SFrames.castdb[guid]
|
||||
if entry.icon and entry.icon ~= "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = entry.icon
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not texture and NanamiPlates and NanamiPlates.castDB and UnitGUID then
|
||||
local guid = UnitGUID("player")
|
||||
if guid and NanamiPlates.castDB[guid] then
|
||||
local entry = NanamiPlates.castDB[guid]
|
||||
if entry.icon and entry.icon ~= "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = entry.icon
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not texture and SFrames.GetSpellIcon then
|
||||
texture = SFrames.GetSpellIcon(spellName)
|
||||
end
|
||||
|
||||
if texture then
|
||||
cb.icon:SetTexture(texture)
|
||||
cb.icon:SetAlpha(1)
|
||||
cb.ibg:SetAlpha(1)
|
||||
cb.icon:Show()
|
||||
cb.ibg:Show()
|
||||
end
|
||||
end
|
||||
|
||||
471
Units/Target.lua
471
Units/Target.lua
@@ -1,6 +1,177 @@
|
||||
SFrames.Target = {}
|
||||
local _A = SFrames.ActiveTheme
|
||||
|
||||
local targetCLCast = nil
|
||||
local spellIconCache = {}
|
||||
|
||||
local SPELL_ICONS = {
|
||||
-- Mage
|
||||
["Fireball"] = "Spell_Fire_FlameBolt",
|
||||
["火球术"] = "Spell_Fire_FlameBolt",
|
||||
["Frostbolt"] = "Spell_Frost_FrostBolt02",
|
||||
["寒冰箭"] = "Spell_Frost_FrostBolt02",
|
||||
["Polymorph"] = "Spell_Nature_Polymorph",
|
||||
["变形术"] = "Spell_Nature_Polymorph",
|
||||
["Arcane Missiles"] = "Spell_Nature_StarFall",
|
||||
["奥术飞弹"] = "Spell_Nature_StarFall",
|
||||
["Pyroblast"] = "Spell_Fire_Fireball02",
|
||||
["炎爆术"] = "Spell_Fire_Fireball02",
|
||||
["Scorch"] = "Spell_Fire_SoulBurn",
|
||||
["灼烧"] = "Spell_Fire_SoulBurn",
|
||||
["Flamestrike"] = "Spell_Fire_SelfDestruct",
|
||||
["烈焰风暴"] = "Spell_Fire_SelfDestruct",
|
||||
["Blizzard"] = "Spell_Frost_IceStorm",
|
||||
["暴风雪"] = "Spell_Frost_IceStorm",
|
||||
-- Warlock
|
||||
["Shadow Bolt"] = "Spell_Shadow_ShadowBolt",
|
||||
["暗影箭"] = "Spell_Shadow_ShadowBolt",
|
||||
["Fear"] = "Spell_Shadow_Possession",
|
||||
["恐惧术"] = "Spell_Shadow_Possession",
|
||||
["Immolate"] = "Spell_Fire_Immolation",
|
||||
["献祭"] = "Spell_Fire_Immolation",
|
||||
["Soul Fire"] = "Spell_Fire_Fireball02",
|
||||
["灵魂之火"] = "Spell_Fire_Fireball02",
|
||||
["Drain Life"] = "Spell_Shadow_LifeDrain02",
|
||||
["吸取生命"] = "Spell_Shadow_LifeDrain02",
|
||||
["Drain Mana"] = "Spell_Shadow_SiphonMana",
|
||||
["吸取法力"] = "Spell_Shadow_SiphonMana",
|
||||
["Rain of Fire"] = "Spell_Shadow_RainOfFire",
|
||||
["火焰之雨"] = "Spell_Shadow_RainOfFire",
|
||||
["Hellfire"] = "Spell_Fire_Incinerate",
|
||||
["地狱烈焰"] = "Spell_Fire_Incinerate",
|
||||
-- Priest
|
||||
["Greater Heal"] = "Spell_Holy_GreaterHeal",
|
||||
["强效治疗术"] = "Spell_Holy_GreaterHeal",
|
||||
["Flash Heal"] = "Spell_Holy_FlashHeal",
|
||||
["快速治疗"] = "Spell_Holy_FlashHeal",
|
||||
["Heal"] = "Spell_Holy_Heal",
|
||||
["治疗术"] = "Spell_Holy_Heal",
|
||||
["Smite"] = "Spell_Holy_HolySmite",
|
||||
["惩击"] = "Spell_Holy_HolySmite",
|
||||
["Mind Blast"] = "Spell_Shadow_UnholyFrenzy",
|
||||
["心灵震爆"] = "Spell_Shadow_UnholyFrenzy",
|
||||
["Mind Flay"] = "Spell_Shadow_SiphonMana",
|
||||
["精神鞭笞"] = "Spell_Shadow_SiphonMana",
|
||||
["Mind Control"] = "Spell_Shadow_ShadowWordDominate",
|
||||
["精神控制"] = "Spell_Shadow_ShadowWordDominate",
|
||||
["Holy Fire"] = "Spell_Holy_SearingLight",
|
||||
["神圣之火"] = "Spell_Holy_SearingLight",
|
||||
["Resurrection"] = "Spell_Holy_Resurrection",
|
||||
["复活术"] = "Spell_Holy_Resurrection",
|
||||
-- Shaman
|
||||
["Lightning Bolt"] = "Spell_Nature_Lightning",
|
||||
["闪电箭"] = "Spell_Nature_Lightning",
|
||||
["Chain Lightning"] = "Spell_Nature_ChainLightning",
|
||||
["闪电链"] = "Spell_Nature_ChainLightning",
|
||||
["Healing Wave"] = "Spell_Nature_MagicImmunity",
|
||||
["治疗波"] = "Spell_Nature_MagicImmunity",
|
||||
["Lesser Healing Wave"] = "Spell_Nature_HealingWaveLesser",
|
||||
["次级治疗波"] = "Spell_Nature_HealingWaveLesser",
|
||||
["Chain Heal"] = "Spell_Nature_HealingWaveGreater",
|
||||
["治疗链"] = "Spell_Nature_HealingWaveGreater",
|
||||
["Ancestral Spirit"] = "Spell_Nature_Regenerate",
|
||||
["先祖之魂"] = "Spell_Nature_Regenerate",
|
||||
-- Druid
|
||||
["Wrath"] = "Spell_Nature_AbolishMagic",
|
||||
["愤怒"] = "Spell_Nature_AbolishMagic",
|
||||
["Starfire"] = "Spell_Arcane_StarFire",
|
||||
["星火术"] = "Spell_Arcane_StarFire",
|
||||
["Regrowth"] = "Spell_Nature_ResistNature",
|
||||
["愈合"] = "Spell_Nature_ResistNature",
|
||||
["Healing Touch"] = "Spell_Nature_HealingTouch",
|
||||
["治疗之触"] = "Spell_Nature_HealingTouch",
|
||||
["Entangling Roots"] = "Spell_Nature_StrangleVines",
|
||||
["纠缠根须"] = "Spell_Nature_StrangleVines",
|
||||
["Hibernate"] = "Spell_Nature_Sleep",
|
||||
["休眠"] = "Spell_Nature_Sleep",
|
||||
["Rebirth"] = "Spell_Nature_Reincarnation",
|
||||
["复生"] = "Spell_Nature_Reincarnation",
|
||||
["Tranquility"] = "Spell_Nature_Tranquility",
|
||||
["宁静"] = "Spell_Nature_Tranquility",
|
||||
["Moonfire"] = "Spell_Nature_StarFall",
|
||||
["月火术"] = "Spell_Nature_StarFall",
|
||||
-- Paladin
|
||||
["Holy Light"] = "Spell_Holy_HolyBolt",
|
||||
["圣光术"] = "Spell_Holy_HolyBolt",
|
||||
["Flash of Light"] = "Spell_Holy_FlashHeal",
|
||||
["圣光闪现"] = "Spell_Holy_FlashHeal",
|
||||
["Hammer of Wrath"] = "Spell_Holy_SealOfMight",
|
||||
["愤怒之锤"] = "Spell_Holy_SealOfMight",
|
||||
["Exorcism"] = "Spell_Holy_Excorcism_02",
|
||||
["驱邪术"] = "Spell_Holy_Excorcism_02",
|
||||
["Redemption"] = "Spell_Holy_Resurrection",
|
||||
["救赎"] = "Spell_Holy_Resurrection",
|
||||
-- Hunter
|
||||
["Aimed Shot"] = "INV_Spear_07",
|
||||
["瞄准射击"] = "INV_Spear_07",
|
||||
["Multi-Shot"] = "Ability_UpgradeMoonGlaive",
|
||||
["多重射击"] = "Ability_UpgradeMoonGlaive",
|
||||
["Volley"] = "Ability_Marksmanship",
|
||||
["乱射"] = "Ability_Marksmanship",
|
||||
["Revive Pet"] = "Ability_Hunter_BeastSoothe",
|
||||
["复活宠物"] = "Ability_Hunter_BeastSoothe",
|
||||
-- Common NPC / generic
|
||||
["Shoot"] = "Ability_Marksmanship",
|
||||
["射击"] = "Ability_Marksmanship",
|
||||
["Mend"] = "Spell_Holy_Heal",
|
||||
["修补"] = "Spell_Holy_Heal",
|
||||
["Rejuvenation"] = "Spell_Nature_Rejuvenation",
|
||||
["回春术"] = "Spell_Nature_Rejuvenation",
|
||||
}
|
||||
for k, v in pairs(SPELL_ICONS) do
|
||||
SPELL_ICONS[k] = "Interface\\Icons\\" .. v
|
||||
end
|
||||
|
||||
local function BuildSpellIconCache()
|
||||
if not GetSpellName or not GetSpellTexture then return end
|
||||
local i = 1
|
||||
while true do
|
||||
local name = GetSpellName(i, "spell")
|
||||
if not name then break end
|
||||
local tex = GetSpellTexture(i, "spell")
|
||||
if tex then spellIconCache[name] = tex end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function GetSpellIcon(spellName)
|
||||
if not spellName then return nil end
|
||||
local tex = spellIconCache[spellName]
|
||||
or SPELL_ICONS[spellName]
|
||||
or (NanamiPlates_CombatLog and NanamiPlates_CombatLog.castIcons
|
||||
and NanamiPlates_CombatLog.castIcons[spellName])
|
||||
if tex then return tex end
|
||||
if GetSpellName and GetSpellTexture then
|
||||
local i = 1
|
||||
while true do
|
||||
local name = GetSpellName(i, "spell")
|
||||
if not name then break end
|
||||
if name == spellName then
|
||||
tex = GetSpellTexture(i, "spell")
|
||||
if tex then
|
||||
spellIconCache[spellName] = tex
|
||||
return tex
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
SFrames.GetSpellIcon = GetSpellIcon
|
||||
SFrames.BuildSpellIconCache = BuildSpellIconCache
|
||||
|
||||
local function CLMatch(str, pattern)
|
||||
if not str or not pattern then return nil end
|
||||
local pat = string.gsub(pattern, "%%%d?%$?s", "(.+)")
|
||||
pat = string.gsub(pat, "%%%d?%$?d", "(%%d+)")
|
||||
for a, b, c, d in string.gfind(str, pat) do
|
||||
return a, b, c, d
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function Clamp(value, minValue, maxValue)
|
||||
if value < minValue then
|
||||
return minValue
|
||||
@@ -467,6 +638,10 @@ function SFrames.Target:Initialize()
|
||||
self:CreateAuras()
|
||||
self:CreateCastbar()
|
||||
self:InitializeDistanceFrame()
|
||||
self:InitCastDetection()
|
||||
|
||||
BuildSpellIconCache()
|
||||
SFrames:RegisterEvent("SPELLS_CHANGED", BuildSpellIconCache)
|
||||
|
||||
f.unit = "target"
|
||||
f:SetScript("OnEnter", function()
|
||||
@@ -492,11 +667,181 @@ function SFrames.Target:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Target:InitCastDetection()
|
||||
local castFrame = CreateFrame("Frame", nil, UIParent)
|
||||
|
||||
castFrame:RegisterEvent("UNIT_CASTEVENT")
|
||||
castFrame:RegisterEvent("SPELLCAST_START")
|
||||
castFrame:RegisterEvent("SPELLCAST_STOP")
|
||||
castFrame:RegisterEvent("SPELLCAST_FAILED")
|
||||
castFrame:RegisterEvent("SPELLCAST_INTERRUPTED")
|
||||
castFrame:RegisterEvent("SPELLCAST_CHANNEL_START")
|
||||
castFrame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
||||
|
||||
local CL_EVENTS = {
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
|
||||
"CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
|
||||
"CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
|
||||
"CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
|
||||
"CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
|
||||
"CHAT_MSG_SPELL_PARTY_DAMAGE",
|
||||
"CHAT_MSG_SPELL_PARTY_BUFF",
|
||||
"CHAT_MSG_SPELL_SELF_DAMAGE",
|
||||
"CHAT_MSG_SPELL_SELF_BUFF",
|
||||
}
|
||||
for _, ev in ipairs(CL_EVENTS) do
|
||||
castFrame:RegisterEvent(ev)
|
||||
end
|
||||
|
||||
local function ResolveSelfIcon(spellName)
|
||||
local texture
|
||||
local _UCI = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
||||
if _UCI then
|
||||
local _, _, _, tex = _UCI("player")
|
||||
texture = tex
|
||||
end
|
||||
if not texture then
|
||||
local _UCH = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
||||
if _UCH then
|
||||
local _, _, _, tex = _UCH("player")
|
||||
texture = tex
|
||||
end
|
||||
end
|
||||
if not texture and SFrames.castdb and UnitGUID then
|
||||
local guid = UnitGUID("player")
|
||||
if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then
|
||||
texture = SFrames.castdb[guid].icon
|
||||
end
|
||||
end
|
||||
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = GetSpellIcon(spellName) or texture
|
||||
end
|
||||
return texture or "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
end
|
||||
|
||||
castFrame:SetScript("OnEvent", function()
|
||||
-- Player's own cast events (for self-target and friendly-target-is-self)
|
||||
if event == "SPELLCAST_START" then
|
||||
if UnitExists("target") and UnitIsUnit("target", "player") then
|
||||
local spellName = arg1
|
||||
local duration = arg2
|
||||
targetCLCast = {
|
||||
spell = spellName,
|
||||
startTime = GetTime(),
|
||||
duration = (duration or 2000) / 1000,
|
||||
icon = ResolveSelfIcon(spellName),
|
||||
channel = false,
|
||||
}
|
||||
end
|
||||
return
|
||||
end
|
||||
if event == "SPELLCAST_CHANNEL_START" then
|
||||
if UnitExists("target") and UnitIsUnit("target", "player") then
|
||||
local duration = arg1
|
||||
local spellName = arg2
|
||||
targetCLCast = {
|
||||
spell = spellName,
|
||||
startTime = GetTime(),
|
||||
duration = (duration or 2000) / 1000,
|
||||
icon = ResolveSelfIcon(spellName),
|
||||
channel = true,
|
||||
}
|
||||
end
|
||||
return
|
||||
end
|
||||
if event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED"
|
||||
or event == "SPELLCAST_INTERRUPTED" or event == "SPELLCAST_CHANNEL_STOP" then
|
||||
if UnitExists("target") and UnitIsUnit("target", "player") then
|
||||
targetCLCast = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- UNIT_CASTEVENT (SuperWoW / TurtleWoW): works for all units
|
||||
if event == "UNIT_CASTEVENT" then
|
||||
if not UnitGUID or not UnitExists("target") then return end
|
||||
local targetGUID = UnitGUID("target")
|
||||
if not targetGUID or arg1 ~= targetGUID then return end
|
||||
|
||||
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
|
||||
local spell, icon
|
||||
if SpellInfo and arg4 then
|
||||
spell, _, icon = SpellInfo(arg4)
|
||||
end
|
||||
spell = spell or "Casting"
|
||||
if not icon or icon == "" or icon == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
icon = GetSpellIcon(spell) or icon
|
||||
end
|
||||
icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
targetCLCast = {
|
||||
spell = spell,
|
||||
startTime = GetTime(),
|
||||
duration = (arg5 or 2000) / 1000,
|
||||
icon = icon,
|
||||
channel = (arg3 == "CHANNEL"),
|
||||
}
|
||||
elseif arg3 == "FAIL" then
|
||||
targetCLCast = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Combat log parsing: "X begins to cast Y" (third-person, all other units)
|
||||
if not arg1 or not UnitExists("target") then return end
|
||||
local targetName = UnitName("target")
|
||||
if not targetName then return end
|
||||
|
||||
local msg = arg1
|
||||
local caster, spell
|
||||
|
||||
local castStart = SPELLCASTOTHERSTART or "%s begins to cast %s."
|
||||
caster, spell = CLMatch(msg, castStart)
|
||||
if not caster then
|
||||
local perfStart = SPELLPERFORMOTHERSTART or "%s begins to perform %s."
|
||||
caster, spell = CLMatch(msg, perfStart)
|
||||
end
|
||||
|
||||
if caster and caster == targetName and spell then
|
||||
local icon = GetSpellIcon(spell) or "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
targetCLCast = {
|
||||
spell = spell,
|
||||
startTime = GetTime(),
|
||||
duration = 2.0,
|
||||
icon = icon,
|
||||
channel = false,
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if targetCLCast then
|
||||
local isFail = false
|
||||
for u in string.gfind(msg, "(.+)'s .+ is interrupted%.") do
|
||||
if u == targetName then isFail = true end
|
||||
end
|
||||
if not isFail then
|
||||
for u in string.gfind(msg, "(.+)'s .+ fails%.") do
|
||||
if u == targetName then isFail = true end
|
||||
end
|
||||
end
|
||||
if not isFail and SPELLINTERRUPTOTHEROTHER then
|
||||
local a = CLMatch(msg, SPELLINTERRUPTOTHEROTHER)
|
||||
if a == targetName then isFail = true end
|
||||
end
|
||||
if isFail then targetCLCast = nil end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function SFrames.Target:OnTargetChanged()
|
||||
targetCLCast = nil
|
||||
if UnitExists("target") then
|
||||
self.frame:Show()
|
||||
self:UpdateAll()
|
||||
-- Force distance update immediately
|
||||
if SFrames.Target.distanceFrame then
|
||||
local dist = self:GetDistance("target")
|
||||
SFrames.Target.distanceFrame.text:SetText(dist or "---")
|
||||
@@ -1142,18 +1487,112 @@ function SFrames.Target:CastbarOnUpdate()
|
||||
return
|
||||
end
|
||||
|
||||
-- Try to read cast from Vanilla extensions (SuperWoW or TurtleWoW modern API, or ShaguTweaks)
|
||||
local cast, nameSubtext, text, texture, startTime, endTime
|
||||
local cast, texture, startTime, endTime, channel
|
||||
|
||||
-- 1) UnitCastingInfo / UnitChannelInfo (TurtleWoW / ShaguTweaks)
|
||||
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
||||
if _UnitCastingInfo then
|
||||
cast, nameSubtext, text, texture, startTime, endTime = _UnitCastingInfo("target")
|
||||
local c, _, _, tex, st, et = _UnitCastingInfo("target")
|
||||
if c then
|
||||
cast, texture, startTime, endTime = c, tex, st, et
|
||||
end
|
||||
end
|
||||
|
||||
local channel
|
||||
if not cast then
|
||||
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
||||
if not cast and _UnitChannelInfo then
|
||||
channel, nameSubtext, text, texture, startTime, endTime = _UnitChannelInfo("target")
|
||||
cast = channel
|
||||
if _UnitChannelInfo then
|
||||
local c, _, _, tex, st, et = _UnitChannelInfo("target")
|
||||
if c then
|
||||
cast, texture, startTime, endTime = c, tex, st, et
|
||||
channel = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 2) SFrames.castdb (UNIT_CASTEVENT via Tweaks.lua, has SpellInfo icon)
|
||||
if SFrames.castdb and UnitGUID then
|
||||
local guid = UnitGUID("target")
|
||||
if guid then
|
||||
local entry = SFrames.castdb[guid]
|
||||
if entry and entry.cast and entry.start and entry.casttime then
|
||||
local elapsed = GetTime() - entry.start
|
||||
local duration = entry.casttime / 1000
|
||||
if elapsed < duration + 0.5 then
|
||||
if not cast then
|
||||
cast = entry.cast
|
||||
startTime = entry.start * 1000
|
||||
endTime = (entry.start + duration) * 1000
|
||||
channel = entry.channel
|
||||
end
|
||||
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = entry.icon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 3) NanamiPlates castDB (GUID-based, UNIT_CASTEVENT with SpellInfo icon)
|
||||
if NanamiPlates and NanamiPlates.castDB and UnitGUID then
|
||||
local guid = UnitGUID("target")
|
||||
if guid then
|
||||
local entry = NanamiPlates.castDB[guid]
|
||||
if entry and entry.spell and entry.startTime and entry.duration then
|
||||
local elapsed = GetTime() - entry.startTime
|
||||
local duration = entry.duration / 1000
|
||||
if elapsed < duration + 0.5 then
|
||||
if not cast then
|
||||
cast = entry.spell
|
||||
startTime = entry.startTime * 1000
|
||||
endTime = (entry.startTime + duration) * 1000
|
||||
channel = entry.channel
|
||||
end
|
||||
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = entry.icon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 4) NanamiPlates castTracker (name-based, combat log)
|
||||
if not cast and NanamiPlates and NanamiPlates.castTracker then
|
||||
local targetName = UnitName("target")
|
||||
if targetName and NanamiPlates.castTracker[targetName] then
|
||||
local entries = NanamiPlates.castTracker[targetName]
|
||||
if entries and entries[1] then
|
||||
local entry = entries[1]
|
||||
if entry.spell and entry.startTime then
|
||||
local duration = (entry.duration or 2000) / 1000
|
||||
local elapsed = GetTime() - entry.startTime
|
||||
if elapsed < duration + 0.5 then
|
||||
cast = entry.spell
|
||||
texture = entry.icon
|
||||
startTime = entry.startTime * 1000
|
||||
endTime = (entry.startTime + duration) * 1000
|
||||
channel = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 5) Local UNIT_CASTEVENT / SPELLCAST_* tracker
|
||||
if targetCLCast then
|
||||
local elapsed = GetTime() - targetCLCast.startTime
|
||||
if elapsed < targetCLCast.duration + 0.5 then
|
||||
if not cast then
|
||||
cast = targetCLCast.spell
|
||||
startTime = targetCLCast.startTime * 1000
|
||||
endTime = (targetCLCast.startTime + targetCLCast.duration) * 1000
|
||||
channel = targetCLCast.channel
|
||||
end
|
||||
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = targetCLCast.icon
|
||||
end
|
||||
else
|
||||
targetCLCast = nil
|
||||
end
|
||||
end
|
||||
|
||||
if cast and startTime and endTime then
|
||||
@@ -1171,19 +1610,25 @@ function SFrames.Target:CastbarOnUpdate()
|
||||
cb:SetValue(cur)
|
||||
cb.text:SetText(cast)
|
||||
|
||||
if texture then
|
||||
cb.icon:SetTexture(texture)
|
||||
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||
texture = GetSpellIcon(cast) or texture
|
||||
end
|
||||
|
||||
cb:SetAlpha(1)
|
||||
cb.cbbg:SetAlpha(1)
|
||||
cb.icon:SetAlpha(1)
|
||||
cb.ibg:SetAlpha(1)
|
||||
|
||||
cb:Show()
|
||||
cb.cbbg:Show()
|
||||
|
||||
if texture then
|
||||
cb.icon:SetTexture(texture)
|
||||
cb.icon:SetAlpha(1)
|
||||
cb.ibg:SetAlpha(1)
|
||||
cb.icon:Show()
|
||||
cb.ibg:Show()
|
||||
else
|
||||
cb.icon:Hide()
|
||||
cb.ibg:Hide()
|
||||
end
|
||||
else
|
||||
cb:Hide()
|
||||
cb.cbbg:Hide()
|
||||
|
||||
385
ZoneLevelRange.lua
Normal file
385
ZoneLevelRange.lua
Normal file
@@ -0,0 +1,385 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Nanami-UI: Zone Level Range Module (ZoneLevelRange.lua)
|
||||
-- Displays zone level ranges, territory status, instances and raids
|
||||
-- on the world map continent view using Nanami-themed tooltip.
|
||||
-- Self-contained data; supersedes LevelRange-Turtle's display.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
SFrames = SFrames or {}
|
||||
SFrames.ZoneLevelRange = {}
|
||||
local ZLR = SFrames.ZoneLevelRange
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Locale helper
|
||||
--------------------------------------------------------------------------------
|
||||
local isZH = (GetLocale() == "zhCN")
|
||||
local function N(en, zh) return isZH and zh or en end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Faction constants
|
||||
--------------------------------------------------------------------------------
|
||||
local F_A = "Alliance"
|
||||
local F_H = "Horde"
|
||||
local F_C = "Contested"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Zone Level Ranges: [localizedName] = { min, max, factionType }
|
||||
--------------------------------------------------------------------------------
|
||||
local ZONE_RANGES = {
|
||||
-- Eastern Kingdoms
|
||||
[N("Elwynn Forest", "艾尔文森林")] = { 1, 10, F_A },
|
||||
[N("Dun Morogh", "丹莫罗")] = { 1, 10, F_A },
|
||||
[N("Tirisfal Glades", "提瑞斯法林地")] = { 1, 10, F_H },
|
||||
[N("Loch Modan", "洛克莫丹")] = { 10, 20, F_A },
|
||||
[N("Silverpine Forest", "银松森林")] = { 10, 20, F_H },
|
||||
[N("Westfall", "西部荒野")] = { 10, 20, F_A },
|
||||
[N("Redridge Mountains", "赤脊山")] = { 15, 25, F_C },
|
||||
[N("Duskwood", "暮色森林")] = { 18, 30, F_C },
|
||||
[N("Hillsbrad Foothills", "希尔斯布莱德丘陵")] = { 20, 30, F_C },
|
||||
[N("Wetlands", "湿地")] = { 20, 30, F_C },
|
||||
[N("Alterac Mountains", "奥特兰克山脉")] = { 30, 40, F_C },
|
||||
[N("Arathi Highlands", "阿拉希高地")] = { 30, 40, F_C },
|
||||
[N("Stranglethorn Vale", "荆棘谷")] = { 30, 45, F_C },
|
||||
[N("Badlands", "荒芜之地")] = { 35, 45, F_C },
|
||||
[N("Swamp of Sorrows", "悲伤沼泽")] = { 35, 45, F_C },
|
||||
[N("The Hinterlands", "辛特兰")] = { 40, 50, F_C },
|
||||
[N("Searing Gorge", "灼热峡谷")] = { 43, 50, F_C },
|
||||
[N("Blasted Lands", "诅咒之地")] = { 45, 55, F_C },
|
||||
[N("Burning Steppes", "燃烧平原")] = { 50, 58, F_C },
|
||||
[N("Western Plaguelands", "西瘟疫之地")] = { 51, 58, F_C },
|
||||
[N("Eastern Plaguelands", "东瘟疫之地")] = { 53, 60, F_C },
|
||||
[N("Deadwind Pass", "逆风小径")] = { 55, 60, F_C },
|
||||
|
||||
-- Kalimdor
|
||||
[N("Durotar", "杜隆塔尔")] = { 1, 10, F_H },
|
||||
[N("Mulgore", "莫高雷")] = { 1, 10, F_H },
|
||||
[N("Teldrassil", "泰达希尔")] = { 1, 10, F_A },
|
||||
[N("Darkshore", "黑海岸")] = { 10, 20, F_A },
|
||||
[N("The Barrens", "贫瘠之地")] = { 10, 25, F_H },
|
||||
[N("Stonetalon Mountains", "石爪山脉")] = { 15, 27, F_C },
|
||||
[N("Ashenvale", "灰谷")] = { 18, 30, F_C },
|
||||
[N("Thousand Needles", "千针石林")] = { 25, 35, F_C },
|
||||
[N("Desolace", "凄凉之地")] = { 30, 40, F_C },
|
||||
[N("Dustwallow Marsh", "尘泥沼泽")] = { 35, 45, F_C },
|
||||
[N("Feralas", "菲拉斯")] = { 40, 50, F_C },
|
||||
[N("Tanaris", "塔纳利斯")] = { 40, 50, F_C },
|
||||
[N("Azshara", "艾萨拉")] = { 45, 55, F_C },
|
||||
[N("Felwood", "费伍德森林")] = { 48, 55, F_C },
|
||||
[N("Un'Goro Crater", "安戈洛环形山")] = { 48, 55, F_C },
|
||||
[N("Silithus", "希利苏斯")] = { 55, 60, F_C },
|
||||
[N("Winterspring", "冬泉谷")] = { 55, 60, F_C },
|
||||
[N("Moonglade", "月光林地")] = { 1, 60, F_C },
|
||||
|
||||
-- Turtle WoW
|
||||
[N("Thalassian Highlands", "阿尔萨拉斯")] = { 1, 10, F_A },
|
||||
[N("Blackstone Island", "黑石岛")] = { 1, 10, F_H },
|
||||
[N("Gilneas", "吉尔尼斯")] = { 39, 46, F_C },
|
||||
[N("Gillijim's Isle", "吉利吉姆之岛")] = { 48, 53, F_C },
|
||||
[N("Lapidis Isle", "拉匹迪斯之岛")] = { 48, 53, F_C },
|
||||
[N("Tel'Abim", "泰拉比姆")] = { 54, 60, F_C },
|
||||
[N("Scarlet Enclave", "东瘟疫之地:血色领地")] = { 55, 60, F_C },
|
||||
[N("Hyjal", "海加尔山")] = { 58, 60, F_C },
|
||||
[N("Grim Reaches", "冷酷海岸")] = { 33, 38, F_C },
|
||||
[N("Northwind", "北风领")] = { 28, 34, F_C },
|
||||
[N("Balor", "巴洛")] = { 29, 34, F_C },
|
||||
[N("Moonsong Coast", "月语海岸")] = { 53, 58, F_C },
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Instances: [zoneName] = { { instanceName, levelString }, ... }
|
||||
--------------------------------------------------------------------------------
|
||||
local ZONE_INSTANCES = {
|
||||
[N("Westfall", "西部荒野")] = { { N("Deadmines", "死亡矿井"), "17-26" } },
|
||||
[N("The Barrens", "贫瘠之地")] = { { N("Wailing Caverns", "哀嚎洞穴"), "17-24" },
|
||||
{ N("Razorfen Kraul", "剃刀高地"), "25-30" },
|
||||
{ N("Razorfen Downs", "剃刀沼泽"), "33-45" } },
|
||||
[N("Silverpine Forest", "银松森林")] = { { N("Shadowfang Keep", "影牙城堡"), "22-30" } },
|
||||
[N("Dun Morogh", "丹莫罗")] = { { N("Gnomeregan", "诺莫瑞根"), "29-38" } },
|
||||
[N("Tirisfal Glades", "提瑞斯法林地")] = { { N("The Scarlet Monastery","血色修道院"), "34-45" } },
|
||||
[N("Badlands", "荒芜之地")] = { { N("Uldaman", "奥达曼"), "35-47" } },
|
||||
[N("Desolace", "凄凉之地")] = { { N("Maraudon", "玛拉顿"), "46-55" } },
|
||||
[N("Swamp of Sorrows", "悲伤沼泽")] = { { N("The Sunken Temple", "沉没的神庙"), "45-55" } },
|
||||
[N("Searing Gorge", "灼热峡谷")] = { { N("Blackrock Depths", "黑石深渊"), "52-60" },
|
||||
{ N("Blackrock Spire", "黑石塔"), "58-60" } },
|
||||
[N("Eastern Plaguelands", "东瘟疫之地")] = { { N("Stratholme", "斯坦索姆"), "58-60" } },
|
||||
[N("Feralas", "菲拉斯")] = { { N("Dire Maul", "厄运之槌"), "55-60" } },
|
||||
[N("Western Plaguelands", "西瘟疫之地")] = { { N("Scholomance", "通灵学院"), "57-60" } },
|
||||
[N("Durotar", "杜隆塔尔")] = { { N("Ragefire Chasm", "怒焰裂谷"), "13-18" } },
|
||||
[N("Ashenvale", "灰谷")] = { { N("Blackfathom Deeps", "黑暗深渊"), "24-32" },
|
||||
{ N("The Crescent Grove", "新月林地"), "32-38" } },
|
||||
[N("Gilneas", "吉尔尼斯")] = { { N("Gilneas City", "吉尔尼斯城"), "43-49" } },
|
||||
[N("Burning Steppes", "燃烧平原")] = { { N("Hateforge Quarry", "仇恨熔炉采石场"), "52-60" },
|
||||
{ N("Blackrock Depths", "黑石深渊"), "52-60" },
|
||||
{ N("Blackrock Spire", "黑石塔"), "58-60" } },
|
||||
[N("Deadwind Pass", "逆风小径")] = { { N("Karazhan Crypt", "卡拉赞地穴"), "58-60" } },
|
||||
[N("Elwynn Forest", "艾尔文森林")] = { { N("The Stockades", "监狱"), "24-32" },
|
||||
{ N("Stormwind Vault", "暴风城地窖"), "60+" } },
|
||||
[N("Tanaris", "塔纳利斯")] = { { N("Zul'Farrak", "祖尔法拉克"), "44-54" },
|
||||
{ N("Caverns of Time: The Black Morass", "时光之穴:黑色沼泽"), "60+" } },
|
||||
[N("Balor", "巴洛")] = { { N("Stormwrought Ruins", "风暴废墟"), "35-41" } },
|
||||
[N("Wetlands", "湿地")] = { { N("Dragonmaw Retreat", "龙喉要塞"), "27-33" } },
|
||||
[N("Moonsong Coast", "月语海岸")] = { { N("Timbermaw Hold", "木喉要塞"), "60+" } },
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Raids
|
||||
--------------------------------------------------------------------------------
|
||||
local ZONE_RAIDS = {
|
||||
[N("Eastern Plaguelands", "东瘟疫之地")] = { { N("Naxxramas", "纳克萨玛斯"), "60+" } },
|
||||
[N("Dustwallow Marsh", "尘泥沼泽")] = { { N("Onyxia's Lair", "奥妮克希亚的巢穴"), "60+" } },
|
||||
[N("Silithus", "希利苏斯")] = { { N("Ruins of Ahn'Qiraj", "安其拉废墟"), "60+" },
|
||||
{ N("Temple of Ahn'Qiraj", "安其拉神庙"), "60+" } },
|
||||
[N("Stranglethorn Vale", "荆棘谷")] = { { N("Zul'Gurub", "祖尔格拉布"), "60+" } },
|
||||
[N("Hyjal", "海加尔山")] = { { N("Emerald Sanctum", "翡翠圣所"), "60+" } },
|
||||
[N("Deadwind Pass", "逆风小径")] = { { N("Lower Karazhan Halls", "下层卡拉赞大厅"), "60+" } },
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Fishing Level Requirements
|
||||
--------------------------------------------------------------------------------
|
||||
local ZONE_FISHING = {
|
||||
[N("Elwynn Forest", "艾尔文森林")] = 25,
|
||||
[N("Dun Morogh", "丹莫罗")] = 25,
|
||||
[N("Tirisfal Glades", "提瑞斯法林地")] = 25,
|
||||
[N("Loch Modan", "洛克莫丹")] = 75,
|
||||
[N("Silverpine Forest", "银松森林")] = 75,
|
||||
[N("Westfall", "西部荒野")] = 75,
|
||||
[N("Redridge Mountains", "赤脊山")] = 150,
|
||||
[N("Duskwood", "暮色森林")] = 150,
|
||||
[N("Hillsbrad Foothills", "希尔斯布莱德丘陵")] = 150,
|
||||
[N("Wetlands", "湿地")] = 150,
|
||||
[N("Alterac Mountains", "奥特兰克山脉")] = 225,
|
||||
[N("Arathi Highlands", "阿拉希高地")] = 225,
|
||||
[N("Stranglethorn Vale", "荆棘谷")] = 225,
|
||||
[N("Swamp of Sorrows", "悲伤沼泽")] = 225,
|
||||
[N("The Hinterlands", "辛特兰")] = 300,
|
||||
[N("Western Plaguelands", "西瘟疫之地")] = 300,
|
||||
[N("Durotar", "杜隆塔尔")] = 25,
|
||||
[N("Mulgore", "莫高雷")] = 25,
|
||||
[N("Teldrassil", "泰达希尔")] = 25,
|
||||
[N("Darkshore", "黑海岸")] = 75,
|
||||
[N("The Barrens", "贫瘠之地")] = 75,
|
||||
[N("Stonetalon Mountains","石爪山脉")] = 150,
|
||||
[N("Ashenvale", "灰谷")] = 150,
|
||||
[N("Thousand Needles", "千针石林")] = 225,
|
||||
[N("Desolace", "凄凉之地")] = 225,
|
||||
[N("Dustwallow Marsh", "尘泥沼泽")] = 225,
|
||||
[N("Feralas", "菲拉斯")] = 300,
|
||||
[N("Tanaris", "塔纳利斯")] = 300,
|
||||
[N("Azshara", "艾萨拉")] = 300,
|
||||
[N("Felwood", "费伍德森林")] = 300,
|
||||
[N("Un'Goro Crater", "安戈洛环形山")] = 300,
|
||||
[N("Moonglade", "月光林地")] = 300,
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Sub-zone -> Parent Zone Mapping
|
||||
--------------------------------------------------------------------------------
|
||||
local ZONE_SUBZONES = {
|
||||
[N("Orgrimmar", "奥格瑞玛")] = N("Durotar", "杜隆塔尔"),
|
||||
[N("Thunder Bluff", "雷霆崖")] = N("Mulgore", "莫高雷"),
|
||||
[N("Undercity", "幽暗城")] = N("Tirisfal Glades", "提瑞斯法林地"),
|
||||
[N("Ironforge", "铁炉堡")] = N("Dun Morogh", "丹莫罗"),
|
||||
[N("Stormwind City", "暴风城")] = N("Elwynn Forest", "艾尔文森林"),
|
||||
[N("Darnassus", "达纳苏斯")] = N("Teldrassil", "泰达希尔"),
|
||||
[N("Alah'Thalas", "萨拉斯高地")] = N("Thalassian Highlands", "阿尔萨拉斯"),
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Display strings & colors
|
||||
--------------------------------------------------------------------------------
|
||||
local S_LEVELS = N("Level %d-%d", "等级 %d-%d")
|
||||
local S_FISHING = N("Fishing Level %d", "钓鱼等级 %d")
|
||||
local S_INSTANCES = N("Instances:", "地下城:")
|
||||
local S_RAIDS = N("Raids:", "团队副本:")
|
||||
local S_FRIENDLY = N("Friendly Territory", "友好领土")
|
||||
local S_HOSTILE = N("Hostile Territory", "敌对领土")
|
||||
local S_CONTESTED = N("Contested Territory", "争夺中的领土")
|
||||
|
||||
local COLORS = {
|
||||
friendly = { 0.20, 0.90, 0.20 },
|
||||
hostile = { 0.90, 0.20, 0.20 },
|
||||
contested = { 0.80, 0.60, 0.40 },
|
||||
levels = { 0.80, 0.60, 0.00 },
|
||||
header = { 1.00, 0.84, 0.00 },
|
||||
instance = { 0.81, 0.81, 0.81 },
|
||||
fishing = { 0.50, 0.70, 0.90 },
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Tooltip frame & display logic
|
||||
--------------------------------------------------------------------------------
|
||||
local zlrTT
|
||||
local zlrCurrentZone, zlrCurrentArea
|
||||
local zlrOldUpdate
|
||||
|
||||
local function CreateZLRTooltip()
|
||||
if zlrTT then return end
|
||||
zlrTT = CreateFrame("GameTooltip", "NanamiZoneLevelTT", UIParent, "GameTooltipTemplate")
|
||||
zlrTT:SetFrameStrata("TOOLTIP")
|
||||
zlrTT:SetFrameLevel(255)
|
||||
end
|
||||
|
||||
local function StyleTooltip()
|
||||
if not zlrTT then return end
|
||||
local _A = SFrames.ActiveTheme
|
||||
if not _A then return end
|
||||
zlrTT: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 },
|
||||
})
|
||||
zlrTT:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95)
|
||||
zlrTT:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 1)
|
||||
end
|
||||
|
||||
local function UpdateZLRTooltip(zoneName)
|
||||
if not zlrTT then return end
|
||||
|
||||
if not zoneName or zoneName == "" then
|
||||
zlrTT:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local data = ZONE_RANGES[zoneName]
|
||||
if not data then
|
||||
zlrTT:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
zlrTT:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
zlrTT:ClearAllPoints()
|
||||
if WorldMapDetailFrame then
|
||||
zlrTT:SetPoint("BOTTOMLEFT", WorldMapDetailFrame, "BOTTOMLEFT", 2, 2)
|
||||
end
|
||||
|
||||
local minLv, maxLv, faction = data[1], data[2], data[3]
|
||||
|
||||
zlrTT:SetText(zoneName, 1, 1, 1)
|
||||
|
||||
local lvStr = string.format(S_LEVELS, minLv, maxLv)
|
||||
zlrTT:AddLine(lvStr, COLORS.levels[1], COLORS.levels[2], COLORS.levels[3])
|
||||
|
||||
local _, playerFaction = UnitFactionGroup("player")
|
||||
local territoryText, territoryColor
|
||||
if faction == F_C then
|
||||
territoryText = S_CONTESTED
|
||||
territoryColor = COLORS.contested
|
||||
elseif playerFaction == faction then
|
||||
territoryText = S_FRIENDLY
|
||||
territoryColor = COLORS.friendly
|
||||
else
|
||||
territoryText = S_HOSTILE
|
||||
territoryColor = COLORS.hostile
|
||||
end
|
||||
zlrTT:AddLine(territoryText, territoryColor[1], territoryColor[2], territoryColor[3])
|
||||
|
||||
local instances = ZONE_INSTANCES[zoneName]
|
||||
if instances then
|
||||
zlrTT:AddLine(" ")
|
||||
zlrTT:AddLine(S_INSTANCES, COLORS.header[1], COLORS.header[2], COLORS.header[3])
|
||||
for _, inst in ipairs(instances) do
|
||||
local iName, iLevels = inst[1], inst[2]
|
||||
zlrTT:AddDoubleLine(
|
||||
iName, "(" .. iLevels .. ")",
|
||||
COLORS.instance[1], COLORS.instance[2], COLORS.instance[3],
|
||||
COLORS.instance[1], COLORS.instance[2], COLORS.instance[3]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local raids = ZONE_RAIDS[zoneName]
|
||||
if raids then
|
||||
zlrTT:AddLine(" ")
|
||||
zlrTT:AddLine(S_RAIDS, COLORS.header[1], COLORS.header[2], COLORS.header[3])
|
||||
for _, raid in ipairs(raids) do
|
||||
local rName, rLevels = raid[1], raid[2]
|
||||
zlrTT:AddDoubleLine(
|
||||
rName, "(" .. rLevels .. ")",
|
||||
COLORS.instance[1], COLORS.instance[2], COLORS.instance[3],
|
||||
COLORS.instance[1], COLORS.instance[2], COLORS.instance[3]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local fishLv = ZONE_FISHING[zoneName]
|
||||
if fishLv then
|
||||
zlrTT:AddLine(" ")
|
||||
zlrTT:AddLine(string.format(S_FISHING, fishLv), COLORS.fishing[1], COLORS.fishing[2], COLORS.fishing[3])
|
||||
end
|
||||
|
||||
StyleTooltip()
|
||||
zlrTT:Show()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hook WorldMapButton_OnUpdate
|
||||
--------------------------------------------------------------------------------
|
||||
local function HookWorldMapUpdate()
|
||||
zlrOldUpdate = WorldMapButton_OnUpdate
|
||||
WorldMapButton_OnUpdate = function(a1)
|
||||
if zlrOldUpdate then zlrOldUpdate(a1) end
|
||||
|
||||
local areaNameRaw = WorldMapFrame and WorldMapFrame.areaName or ""
|
||||
local _, _, trimmed = string.find(areaNameRaw, "^%s*(.-)%s*$")
|
||||
if not trimmed then trimmed = areaNameRaw end
|
||||
local zoneNum = GetCurrentMapZone and GetCurrentMapZone() or -1
|
||||
|
||||
if ZONE_SUBZONES[trimmed] then
|
||||
trimmed = ZONE_SUBZONES[trimmed]
|
||||
end
|
||||
|
||||
if zoneNum == zlrCurrentZone and areaNameRaw == zlrCurrentArea then
|
||||
return
|
||||
end
|
||||
zlrCurrentZone = zoneNum
|
||||
zlrCurrentArea = areaNameRaw
|
||||
|
||||
if zoneNum == 0 then
|
||||
UpdateZLRTooltip(trimmed)
|
||||
else
|
||||
UpdateZLRTooltip(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Suppress LevelRange-Turtle's original tooltip
|
||||
--------------------------------------------------------------------------------
|
||||
local function SuppressLevelRangeTooltip()
|
||||
local _G = getfenv(0)
|
||||
if _G["LevelRangeTooltip"] then
|
||||
_G["LevelRangeTooltip"]:Hide()
|
||||
_G["LevelRangeTooltip"].Show = function(self) self:Hide() end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hook WorldMapFrame hide to dismiss tooltip
|
||||
--------------------------------------------------------------------------------
|
||||
local function HookMapHide()
|
||||
if not WorldMapFrame then return end
|
||||
local prevOnHide = WorldMapFrame:GetScript("OnHide")
|
||||
WorldMapFrame:SetScript("OnHide", function(a1,a2,a3,a4,a5)
|
||||
if prevOnHide then prevOnHide(a1,a2,a3,a4,a5) end
|
||||
if zlrTT then zlrTT:Hide() end
|
||||
zlrCurrentZone = nil
|
||||
zlrCurrentArea = nil
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Initialize
|
||||
--------------------------------------------------------------------------------
|
||||
function ZLR:Initialize()
|
||||
local wmCfg = SFramesDB and SFramesDB.WorldMap
|
||||
if wmCfg and wmCfg.enabled == false then return end
|
||||
|
||||
CreateZLRTooltip()
|
||||
HookWorldMapUpdate()
|
||||
HookMapHide()
|
||||
SuppressLevelRangeTooltip()
|
||||
|
||||
self.initialized = true
|
||||
end
|
||||
212
docs/LootDisplay-技术要点.md
Normal file
212
docs/LootDisplay-技术要点.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# LootDisplay 拾取窗口接管 — 技术要点
|
||||
|
||||
## 最终成功方案
|
||||
|
||||
**核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。**
|
||||
|
||||
在 Turtle WoW (1.12 魔兽私服) 中,`LootSlot()` 是一个**受保护的 C 端函数**,
|
||||
只接受来自原生 `LootButton1~4`(由 FrameXML 中 `LootButtonTemplate` 创建的按钮)
|
||||
的内置 `OnClick` 处理器调用。任何 addon 自建按钮(无论是否使用模板)都**无法**成功
|
||||
调用 `LootSlot()`。
|
||||
|
||||
---
|
||||
|
||||
## 失败方案及原因
|
||||
|
||||
### 方案 1:LootButtonTemplate 自定义按钮
|
||||
|
||||
```lua
|
||||
local btn = CreateFrame("Button", "NanamiLootBtn1", lootFrame, "LootButtonTemplate")
|
||||
btn:SetScript("OnClick", function()
|
||||
LootSlot(this.slot)
|
||||
end)
|
||||
```
|
||||
|
||||
**失败原因**:虽然使用了 `LootButtonTemplate`,但按钮是 addon 动态创建的,
|
||||
不是 FrameXML 在加载期创建的原生 `LootButton1~4`。Turtle WoW 的 C 端可能检查
|
||||
调用来源是否为受信任的原生按钮,导致 `LootSlot()` 静默失败。
|
||||
|
||||
### 方案 2:纯自定义 Button + 直接调用 LootSlot
|
||||
|
||||
```lua
|
||||
local btn = CreateFrame("Button", nil, lootFrame)
|
||||
btn:SetScript("OnClick", function()
|
||||
LootSlot(this.slot)
|
||||
end)
|
||||
```
|
||||
|
||||
**失败原因**:与方案 1 相同。`LootSlot()` 只信任原生按钮的事件上下文。
|
||||
截图可验证 `GameTooltip:SetLootItem()` 正常工作(tooltip 能显示),说明
|
||||
拾取会话本身是活跃的,纯粹是 `LootSlot()` 拒绝执行。
|
||||
|
||||
### 方案 3:完全禁用 LootFrame + 自定义按钮
|
||||
|
||||
```lua
|
||||
LootFrame:UnregisterAllEvents()
|
||||
LootFrame:Hide()
|
||||
```
|
||||
|
||||
**失败原因**:在禁用 LootFrame 的基础上使用自定义按钮调 `LootSlot()`,
|
||||
同样因为 C 端保护而失败。另外隐藏 LootFrame 后 C 端可能也认为拾取会话无效。
|
||||
|
||||
---
|
||||
|
||||
## 成功方案:原生按钮重定位
|
||||
|
||||
### 架构概览
|
||||
|
||||
```
|
||||
┌─ NanamiLootFrame (自定义视觉框架) ──────┐
|
||||
│ ┌─ 视觉行 row1 (EnableMouse=false) ──┐ │
|
||||
│ │ icon + name + quality bar │ │ ← 玩家看到的
|
||||
│ │ ┌─ LootButton1 (alpha=0) ───────┐ │ │
|
||||
│ │ │ 原生 OnClick → LootSlot() │ │ │ ← 玩家点击的
|
||||
│ │ └───────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ ┌─ 视觉行 row2 ... ──┐ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
┌─ LootFrame (原生, alpha=0, 不可交互) ───┐
|
||||
│ (存在但不可见,维持拾取会话) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 关键步骤
|
||||
|
||||
#### 1. 保存原始 `LootFrame_Update` 引用
|
||||
|
||||
```lua
|
||||
origLootFrameUpdate = LootFrame_Update
|
||||
```
|
||||
|
||||
**不能**将 `LootFrame_Update` 替换为空函数。这个函数负责为 `LootButton1~4`
|
||||
设置 `SetID()`、slot 数据、以及关键的内置 `OnClick` 处理器。
|
||||
|
||||
#### 2. 让原生 LootFrame 保持"活跃但不可见"
|
||||
|
||||
```lua
|
||||
-- 清除 OnHide 防止 CloseLoot() 被意外调用
|
||||
LootFrame:SetScript("OnHide", function() end)
|
||||
|
||||
-- Show hook:每次 Show 后强制 alpha=0
|
||||
local origShow = LootFrame.Show
|
||||
LootFrame.Show = function(self)
|
||||
origShow(self)
|
||||
self:SetAlpha(0)
|
||||
self:EnableMouse(false)
|
||||
end
|
||||
|
||||
-- Hide hook:我们的框架显示期间阻止隐藏
|
||||
local origHide = LootFrame.Hide
|
||||
LootFrame.Hide = function(self)
|
||||
if lootFrame and lootFrame:IsShown() then return end
|
||||
origHide(self)
|
||||
end
|
||||
```
|
||||
|
||||
**为什么不能 Hide/UnregisterAllEvents**:
|
||||
- `LootFrame:Hide()` 的 XML OnHide 会调用 `CloseLoot()`,立即终止拾取会话
|
||||
- C 端可能检查 `LootFrame:IsShown()` 来判断拾取是否合法
|
||||
- 原生 `LootButton1~4` 是 `LootFrame` 的子框架,父框架隐藏则子框架不可交互
|
||||
|
||||
#### 3. ShowLootPage 的双阶段流程
|
||||
|
||||
**阶段 A — 构建视觉层**:设置自定义行的图标、名称、品质颜色。
|
||||
视觉行 `EnableMouse(false)`,不拦截任何鼠标事件。
|
||||
|
||||
**阶段 B — 设置原生按钮并重定位**:
|
||||
|
||||
```lua
|
||||
-- 同步页码
|
||||
LootFrame.page = page
|
||||
if not LootFrame:IsShown() then LootFrame:Show() end
|
||||
|
||||
-- 让原生代码完整设置按钮状态(ID、OnClick 等)
|
||||
origLootFrameUpdate()
|
||||
|
||||
-- 将原生按钮移到我们的视觉行上
|
||||
for i = 1, 4 do
|
||||
local nb = _G["LootButton" .. i]
|
||||
local row = lootRows[i]
|
||||
if nb and row and row:IsShown() and row._qualColor then
|
||||
nb:ClearAllPoints()
|
||||
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||
nb:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
nb:SetFrameLevel(row:GetFrameLevel() + 10)
|
||||
nb:SetAlpha(0) -- 不可见
|
||||
nb:EnableMouse(true) -- 可点击
|
||||
nb:Show()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### 4. Hook 全局 `LootFrame_Update` 保持一致性
|
||||
|
||||
```lua
|
||||
LootFrame_Update = function()
|
||||
origLootFrameUpdate() -- 原生设置
|
||||
-- 如果我们的框架在显示,重定位按钮
|
||||
if lootFrame and lootFrame:IsShown() then
|
||||
for i = 1, 4 do
|
||||
-- 同样的重定位逻辑
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
这确保**任何来源**的 `LootFrame_Update` 调用(包括 `LOOT_SLOT_CLEARED` 事件后
|
||||
引擎的自动调用)都会以按钮在正确位置结束,解决了"拾取一个物品后无法继续拾取"的问题。
|
||||
|
||||
---
|
||||
|
||||
## 事件流程梳理
|
||||
|
||||
### 打开拾取
|
||||
|
||||
```
|
||||
玩家右键尸体
|
||||
→ C 引擎创建拾取会话
|
||||
→ LOOT_OPENED 事件
|
||||
→ 原生 LootFrame_OnEvent → LootFrame:Show() [hook: alpha=0]
|
||||
→ LootFrame_Update [hook: 原生设置 + 重定位]
|
||||
→ 我们的 LOOT_OPENED handler → UpdateLootFrame → ShowLootPage
|
||||
→ 设置视觉行
|
||||
→ origLootFrameUpdate() → 重定位按钮
|
||||
```
|
||||
|
||||
### 拾取物品
|
||||
|
||||
```
|
||||
玩家点击 LootButton1 (alpha=0, 覆盖在视觉行上)
|
||||
→ 原生 LootButton_OnClick → LootSlot(this:GetID()) ← 受信任的调用
|
||||
→ 物品拾取成功
|
||||
→ LOOT_SLOT_CLEARED 事件
|
||||
→ 原生 handler → LootFrame_Update [hook: 原生重设按钮 + 重定位]
|
||||
→ 我们的 handler → UpdateLootFrame → ShowLootPage → 刷新视觉 + 重定位
|
||||
```
|
||||
|
||||
### 关闭拾取
|
||||
|
||||
```
|
||||
玩家走开 / 按 ESC / 点关闭按钮
|
||||
→ CloseLoot() 或 LOOT_CLOSED 事件
|
||||
→ CloseLootFrame() → 隐藏自定义框架 (设 _closingLoot 标志)
|
||||
→ 隐藏原生按钮
|
||||
→ 允许 LootFrame:Hide()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键技术教训
|
||||
|
||||
| 教训 | 说明 |
|
||||
|------|------|
|
||||
| `LootSlot()` 是受保护的 | Turtle WoW 中只有原生按钮的内置 OnClick 能成功调用 |
|
||||
| 不能隐藏 LootFrame | OnHide (XML) 会调用 `CloseLoot()` 终止会话 |
|
||||
| 不能禁用 LootFrame_Update | 这个函数负责设置按钮的 ID 和交互能力 |
|
||||
| 视觉与交互分离 | 自定义行负责视觉 (EnableMouse=false),原生按钮负责交互 (alpha=0) |
|
||||
| Hook 先调原始再改位置 | `LootFrame_Update` hook 先跑原生逻辑,再重定位按钮到自定义行上 |
|
||||
| OnHide 需要防重入 | `_closingLoot` 标志防止 OnHide → CloseLoot → LOOT_CLOSED → CloseLootFrame 循环 |
|
||||
| 页码必须同步 | `LootFrame.page` 必须与自定义分页同步,否则原生按钮 ID 计算错误 |
|
||||
Reference in New Issue
Block a user