跟随版本 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)
|
ab:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 4, -4)
|
||||||
end
|
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"]
|
local floatingBG = _G[b:GetName() .. "FloatingBG"]
|
||||||
if floatingBG then floatingBG:SetAlpha(0) end
|
if floatingBG then floatingBG:SetAlpha(0) end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ local S = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Tooltip scanner for training point cost & requirements
|
-- Tooltip scanner & extended craft info
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
local scanTip = nil
|
local scanTip = nil
|
||||||
|
|
||||||
function BTUI.GetCraftExtendedInfo(index)
|
function BTUI.GetCraftExtendedInfo(index)
|
||||||
local name, rank, skillType, numAvail, _, _, tpCost = GetCraftInfo(index)
|
local name, rank, skillType, v4, _, tpCost, reqLevel = GetCraftInfo(index)
|
||||||
return name, rank, skillType,
|
local canLearn = (tonumber(reqLevel) or 0) > 0
|
||||||
tonumber(numAvail) or 0,
|
tpCost = tonumber(tpCost) or 0
|
||||||
tonumber(tpCost) or 0
|
return name, rank, skillType, canLearn, tpCost
|
||||||
end
|
end
|
||||||
|
|
||||||
function BTUI.GetSkillTooltipLines(index)
|
function BTUI.GetSkillTooltipLines(index)
|
||||||
@@ -356,24 +356,26 @@ function BTUI.CreateListRow(parent, idx)
|
|||||||
self.rankFS:Hide()
|
self.rankFS:Hide()
|
||||||
end
|
end
|
||||||
|
|
||||||
local tpCost = skill.tpCost or 0
|
if skill.canLearn then
|
||||||
local canLearn = (tpCost > 0)
|
|
||||||
|
|
||||||
if canLearn then
|
|
||||||
local remaining = BTUI.GetRemainingTP()
|
|
||||||
if remaining >= tpCost 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:Show()
|
|
||||||
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
||||||
self.icon:SetVertexColor(1, 1, 1)
|
self.icon:SetVertexColor(1, 1, 1)
|
||||||
|
local tp = skill.tpCost or 0
|
||||||
|
if tp > 0 then
|
||||||
|
local remaining = BTUI.GetRemainingTP()
|
||||||
|
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(tp .. " TP")
|
||||||
|
self.tpFS:Show()
|
||||||
|
else
|
||||||
|
self.tpFS:Hide()
|
||||||
|
end
|
||||||
else
|
else
|
||||||
self.tpFS:Hide()
|
|
||||||
self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
|
self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
|
||||||
self.icon:SetVertexColor(0.5, 0.5, 0.5)
|
self.icon:SetVertexColor(0.5, 0.5, 0.5)
|
||||||
|
self.tpFS:Hide()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -400,7 +402,7 @@ function BTUI.BuildDisplayList()
|
|||||||
local catOrder = {}
|
local catOrder = {}
|
||||||
|
|
||||||
for i = 1, numCrafts do
|
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 name then
|
||||||
if skillType == "header" then
|
if skillType == "header" then
|
||||||
currentCat = name
|
currentCat = name
|
||||||
@@ -416,7 +418,6 @@ function BTUI.BuildDisplayList()
|
|||||||
table.insert(catOrder, currentCat)
|
table.insert(catOrder, currentCat)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local canLearn = (tpCost > 0)
|
|
||||||
local show = true
|
local show = true
|
||||||
if S.currentFilter == "available" then
|
if S.currentFilter == "available" then
|
||||||
show = canLearn
|
show = canLearn
|
||||||
@@ -428,8 +429,7 @@ function BTUI.BuildDisplayList()
|
|||||||
index = i,
|
index = i,
|
||||||
name = name,
|
name = name,
|
||||||
rank = rank or "",
|
rank = rank or "",
|
||||||
skillType = skillType or "none",
|
canLearn = canLearn,
|
||||||
numAvail = numAvail,
|
|
||||||
tpCost = tpCost,
|
tpCost = tpCost,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -524,13 +524,12 @@ function BTUI.UpdateDetail()
|
|||||||
return
|
return
|
||||||
end
|
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)
|
local iconTex = GetCraftIcon and GetCraftIcon(S.selectedIndex)
|
||||||
|
|
||||||
detail.icon:SetTexture(iconTex); detail.iconFrame:Show()
|
detail.icon:SetTexture(iconTex); detail.iconFrame:Show()
|
||||||
detail.nameFS:SetText(name or "")
|
detail.nameFS:SetText(name or "")
|
||||||
|
|
||||||
local canLearn = (tpCost > 0)
|
|
||||||
if canLearn then
|
if canLearn then
|
||||||
detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
|
||||||
else
|
else
|
||||||
@@ -559,6 +558,8 @@ function BTUI.UpdateDetail()
|
|||||||
local remaining = BTUI.GetRemainingTP()
|
local remaining = BTUI.GetRemainingTP()
|
||||||
local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040"
|
local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040"
|
||||||
detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")")
|
detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")")
|
||||||
|
elseif canLearn then
|
||||||
|
detail.costFS:SetText("训练点数: |cff40ff40免费|r")
|
||||||
else
|
else
|
||||||
detail.costFS:SetText("")
|
detail.costFS:SetText("")
|
||||||
end
|
end
|
||||||
@@ -974,6 +975,23 @@ function BTUI:Initialize()
|
|||||||
if this.disabled then return end
|
if this.disabled then return end
|
||||||
if S.selectedIndex and DoCraft then
|
if S.selectedIndex and DoCraft then
|
||||||
DoCraft(S.selectedIndex)
|
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
|
||||||
end)
|
end)
|
||||||
MF.trainBtn = trainBtn
|
MF.trainBtn = trainBtn
|
||||||
@@ -1062,18 +1080,25 @@ SLASH_BTDEBUG1 = "/btdebug"
|
|||||||
SlashCmdList["BTDEBUG"] = function()
|
SlashCmdList["BTDEBUG"] = function()
|
||||||
local p = "|cffff80ff[BT-Debug]|r "
|
local p = "|cffff80ff[BT-Debug]|r "
|
||||||
local numCrafts = GetNumCrafts and GetNumCrafts() or 0
|
local numCrafts = GetNumCrafts and GetNumCrafts() or 0
|
||||||
DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts)
|
DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts .. " TP: " .. BTUI.GetRemainingTP())
|
||||||
local remaining = BTUI.GetRemainingTP()
|
|
||||||
DEFAULT_CHAT_FRAME:AddMessage(p .. "Remaining TP: " .. remaining)
|
|
||||||
local shown = 0
|
local shown = 0
|
||||||
for i = 1, numCrafts do
|
for i = 1, numCrafts do
|
||||||
local v1,v2,v3,v4,v5,v6,v7 = GetCraftInfo(i)
|
local v1,v2,v3,v4,v5,v6,v7 = GetCraftInfo(i)
|
||||||
if v1 and v3 ~= "header" then
|
if v1 and v3 ~= "header" then
|
||||||
shown = shown + 1
|
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)
|
DEFAULT_CHAT_FRAME:AddMessage(p .. i .. ": " .. tostring(v1)
|
||||||
.. " " .. tostring(v2) .. " type=" .. tostring(v3)
|
.. " " .. tostring(v2) .. " v7=" .. tostring(v7)
|
||||||
.. " avail=" .. tostring(v4) .. " tp=" .. tostring(v7))
|
.. " reagents=" .. tostring(nr) .. reagentCost)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3593,6 +3593,15 @@ function CP:BuildPetPage()
|
|||||||
sY = sY - 14
|
sY = sY - 14
|
||||||
end
|
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
|
-- Resistances
|
||||||
sY = sY - 4
|
sY = sY - 4
|
||||||
sY = self:CreateStatSection(child, "抗性", sY)
|
sY = self:CreateStatSection(child, "抗性", sY)
|
||||||
@@ -3800,6 +3809,13 @@ function CP:UpdatePet()
|
|||||||
r.value:SetText(combatVals[i] or "0")
|
r.value:SetText(combatVals[i] or "0")
|
||||||
end
|
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
|
for _, r in ipairs(page.resStats) do
|
||||||
local base, bonus = 0, 0
|
local base, bonus = 0, 0
|
||||||
if UnitResistance then
|
if UnitResistance then
|
||||||
|
|||||||
62
Chat.lua
62
Chat.lua
@@ -504,21 +504,7 @@ end
|
|||||||
local function GetChannelAliasKeys(name)
|
local function GetChannelAliasKeys(name)
|
||||||
local key = ChannelKey(name)
|
local key = ChannelKey(name)
|
||||||
if key == "" then return nil end
|
if key == "" then return nil end
|
||||||
-- Check exact match first
|
|
||||||
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
|
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
|
if not gIdx then return nil end
|
||||||
return CHANNEL_ALIAS_GROUPS[gIdx]
|
return CHANNEL_ALIAS_GROUPS[gIdx]
|
||||||
end
|
end
|
||||||
@@ -583,7 +569,7 @@ local function GetJoinedChannels()
|
|||||||
-- plus individual channels that are not aliases.
|
-- plus individual channels that are not aliases.
|
||||||
-- For alias groups (like hc/hardcore/硬核), only add ONE representative
|
-- For alias groups (like hc/hardcore/硬核), only add ONE representative
|
||||||
-- so we don't create duplicate conflicting entries.
|
-- so we don't create duplicate conflicting entries.
|
||||||
local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world" }
|
local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world", "世界" }
|
||||||
local seenAliasGroups = {}
|
local seenAliasGroups = {}
|
||||||
for _, cname in ipairs(customChannels) do
|
for _, cname in ipairs(customChannels) do
|
||||||
local key = ChannelKey(cname)
|
local key = ChannelKey(cname)
|
||||||
@@ -600,20 +586,7 @@ local function GetJoinedChannels()
|
|||||||
else
|
else
|
||||||
local aliases = GetChannelAliasKeys(cname)
|
local aliases = GetChannelAliasKeys(cname)
|
||||||
if aliases then
|
if aliases then
|
||||||
-- This is an alias channel - check if any alias is already seen/added
|
|
||||||
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
|
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
|
local alreadyInJoined = false
|
||||||
if gIdx then
|
if gIdx then
|
||||||
for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do
|
for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do
|
||||||
@@ -6477,6 +6450,39 @@ function SFrames.Chat:Initialize()
|
|||||||
GuildRoster()
|
GuildRoster()
|
||||||
end
|
end
|
||||||
SFrames:RefreshClassColorCache()
|
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)
|
end)
|
||||||
|
|
||||||
-- 团队成员变化时更新职业缓存
|
-- 团队成员变化时更新职业缓存
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ local function EnsureDB()
|
|||||||
if type(SFramesDB.playerPortraitWidth) ~= "number" then SFramesDB.playerPortraitWidth = 50 end
|
if type(SFramesDB.playerPortraitWidth) ~= "number" then SFramesDB.playerPortraitWidth = 50 end
|
||||||
if type(SFramesDB.playerHealthHeight) ~= "number" then SFramesDB.playerHealthHeight = 38 end
|
if type(SFramesDB.playerHealthHeight) ~= "number" then SFramesDB.playerHealthHeight = 38 end
|
||||||
if type(SFramesDB.playerPowerHeight) ~= "number" then SFramesDB.playerPowerHeight = 9 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.playerShowClassIcon == nil then SFramesDB.playerShowClassIcon = true end
|
||||||
if SFramesDB.playerShowPortrait == nil then SFramesDB.playerShowPortrait = true end
|
if SFramesDB.playerShowPortrait == nil then SFramesDB.playerShowPortrait = true end
|
||||||
if type(SFramesDB.playerFrameAlpha) ~= "number" then SFramesDB.playerFrameAlpha = 1 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.targetPortraitWidth) ~= "number" then SFramesDB.targetPortraitWidth = 50 end
|
||||||
if type(SFramesDB.targetHealthHeight) ~= "number" then SFramesDB.targetHealthHeight = 38 end
|
if type(SFramesDB.targetHealthHeight) ~= "number" then SFramesDB.targetHealthHeight = 38 end
|
||||||
if type(SFramesDB.targetPowerHeight) ~= "number" then SFramesDB.targetPowerHeight = 9 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.targetShowClassIcon == nil then SFramesDB.targetShowClassIcon = true end
|
||||||
if SFramesDB.targetShowPortrait == nil then SFramesDB.targetShowPortrait = true end
|
if SFramesDB.targetShowPortrait == nil then SFramesDB.targetShowPortrait = true end
|
||||||
if type(SFramesDB.targetFrameAlpha) ~= "number" then SFramesDB.targetFrameAlpha = 1 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 },
|
{ "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 },
|
{ "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 },
|
{ "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 },
|
{ "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 },
|
{ "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 },
|
{ "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:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
|
||||||
closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
|
closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
|
||||||
|
|
||||||
local closeTxt = SFrames:CreateFontString(closeBtn, 12, "CENTER")
|
local closeIco = SFrames:CreateIcon(closeBtn, "close", 12)
|
||||||
closeTxt:SetAllPoints(closeBtn)
|
closeIco:SetDrawLayer("OVERLAY")
|
||||||
closeTxt:SetText("X")
|
closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
|
||||||
local _closeTxt = _A.accentLight or { 0.9, 0.5, 0.5 }
|
closeIco:SetVertexColor(1, 0.7, 0.7)
|
||||||
closeTxt:SetTextColor(_closeTxt[1], _closeTxt[2], _closeTxt[3])
|
|
||||||
|
|
||||||
closeBtn:SetScript("OnClick", function() f:Hide() end)
|
closeBtn:SetScript("OnClick", function() f:Hide() end)
|
||||||
closeBtn:SetScript("OnEnter", function()
|
closeBtn:SetScript("OnEnter", function()
|
||||||
|
|||||||
485
LootDisplay.lua
485
LootDisplay.lua
@@ -65,11 +65,18 @@ local ICON_BACKDROP = {
|
|||||||
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local ITEMS_PER_PAGE = 4
|
||||||
|
local PAGE_BAR_H = 20
|
||||||
|
|
||||||
local lootRows = {}
|
local lootRows = {}
|
||||||
local activeAlerts = {}
|
local activeAlerts = {}
|
||||||
local alertAnchor = nil
|
local alertAnchor = nil
|
||||||
local alertPool = {}
|
local alertPool = {}
|
||||||
|
|
||||||
|
local origLootFrameUpdate = nil
|
||||||
|
local ShowLootPage
|
||||||
|
local HideBagFullWarning
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Helpers
|
-- Helpers
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -224,6 +231,97 @@ local function CreateLootFrame()
|
|||||||
closeFS:SetTextColor(0.9, 0.65, 0.65, 1)
|
closeFS:SetTextColor(0.9, 0.65, 0.65, 1)
|
||||||
end)
|
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()
|
lootFrame:Hide()
|
||||||
return lootFrame
|
return lootFrame
|
||||||
end
|
end
|
||||||
@@ -302,7 +400,166 @@ local function CreateLootRow(parent, index)
|
|||||||
end
|
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 function UpdateLootFrame()
|
||||||
local db = GetDB()
|
local db = GetDB()
|
||||||
@@ -316,124 +573,18 @@ local function UpdateLootFrame()
|
|||||||
|
|
||||||
CreateLootFrame()
|
CreateLootFrame()
|
||||||
|
|
||||||
local validSlots = {}
|
lootFrame._numItems = numItems
|
||||||
for i = 1, numItems do
|
lootFrame._totalPages = math.ceil(numItems / ITEMS_PER_PAGE)
|
||||||
local texture, itemName, quantity, quality = GetLootSlotInfo(i)
|
|
||||||
if texture then
|
if not lootFrame._page or lootFrame._page > lootFrame._totalPages then
|
||||||
table.insert(validSlots, i)
|
lootFrame._page = 1
|
||||||
end
|
|
||||||
end
|
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:SetWidth(ROW_WIDTH + 14)
|
||||||
lootFrame:SetHeight(totalH)
|
|
||||||
lootFrame:SetScale(db.scale or 1.0)
|
lootFrame:SetScale(db.scale or 1.0)
|
||||||
|
|
||||||
while table.getn(lootRows) < numValid do
|
ShowLootPage()
|
||||||
local idx = table.getn(lootRows) + 1
|
|
||||||
lootRows[idx] = CreateLootRow(lootFrame, idx)
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
if not lootFrame._posApplied then
|
||||||
local hasSaved = false
|
local hasSaved = false
|
||||||
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
||||||
@@ -455,19 +606,18 @@ local function UpdateLootFrame()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function CloseLootFrame()
|
local function CloseLootFrame()
|
||||||
-- Return Blizzard buttons to LootFrame
|
if lootFrame then
|
||||||
local maxBtns = LOOTFRAME_NUMITEMS or 4
|
lootFrame._closingLoot = true
|
||||||
for i = 1, maxBtns do
|
lootFrame:Hide()
|
||||||
local blizzBtn = _G["LootButton" .. i]
|
lootFrame._closingLoot = nil
|
||||||
if blizzBtn and LootFrame then
|
end
|
||||||
blizzBtn:SetParent(LootFrame)
|
for i = 1, ITEMS_PER_PAGE do
|
||||||
blizzBtn:SetAlpha(1)
|
local nb = _G["LootButton" .. i]
|
||||||
blizzBtn:Hide()
|
if nb then nb:Hide() end
|
||||||
blizzBtn._nanamiRow = nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if lootFrame then lootFrame:Hide() end
|
|
||||||
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
|
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
|
||||||
|
HideBagFullWarning()
|
||||||
|
if LootFrame then LootFrame:Hide() end
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -750,7 +900,6 @@ function LD:Initialize()
|
|||||||
CreateLootFrame()
|
CreateLootFrame()
|
||||||
CreateAlertAnchor()
|
CreateAlertAnchor()
|
||||||
|
|
||||||
-- Apply saved positions so frames have valid coordinates for the Mover system
|
|
||||||
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
if SFrames.Movers and SFrames.Movers.ApplyPosition then
|
||||||
local applied = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
|
local applied = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
|
||||||
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
|
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
|
||||||
@@ -768,19 +917,41 @@ function LD:Initialize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
SFrames:RegisterEvent("LOOT_OPENED", function()
|
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)
|
end)
|
||||||
|
|
||||||
SFrames:RegisterEvent("LOOT_SLOT_CLEARED", function()
|
SFrames:RegisterEvent("LOOT_SLOT_CLEARED", function()
|
||||||
if GetDB().enable and lootFrame and lootFrame:IsShown() then
|
if GetDB().enable and lootFrame and lootFrame:IsShown() then
|
||||||
|
HideBagFullWarning()
|
||||||
UpdateLootFrame()
|
UpdateLootFrame()
|
||||||
end
|
end
|
||||||
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()
|
SFrames:RegisterEvent("LOOT_CLOSED", function()
|
||||||
CloseLootFrame()
|
CloseLootFrame()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
SFrames:RegisterEvent("LOOT_BIND_CONFIRM", function()
|
||||||
|
local slot = arg1
|
||||||
|
if slot then ConfirmLootSlot(slot) end
|
||||||
|
end)
|
||||||
|
|
||||||
SFrames:RegisterEvent("CHAT_MSG_LOOT", function()
|
SFrames:RegisterEvent("CHAT_MSG_LOOT", function()
|
||||||
local playerName = UnitName("player")
|
local playerName = UnitName("player")
|
||||||
if not playerName then return end
|
if not playerName then return end
|
||||||
@@ -790,27 +961,49 @@ function LD:Initialize()
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function HideBlizzardLoot()
|
-- Save the original LootFrame_Update so ShowLootPage can call it
|
||||||
if LootFrame then
|
-- to let native code set up LootButton IDs and OnClick handlers.
|
||||||
LootFrame:EnableMouse(false)
|
origLootFrameUpdate = LootFrame_Update
|
||||||
LootFrame:SetAlpha(0)
|
|
||||||
LootFrame:ClearAllPoints()
|
if LootFrame then
|
||||||
LootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
|
-- Prevent the XML-defined OnHide from calling CloseLoot()
|
||||||
local origShow = LootFrame.Show
|
LootFrame:SetScript("OnHide", function() end)
|
||||||
LootFrame.Show = function(self)
|
|
||||||
origShow(self)
|
-- Keep LootFrame shown but invisible while our UI is active
|
||||||
self:EnableMouse(false)
|
local origShow = LootFrame.Show
|
||||||
self:SetAlpha(0)
|
LootFrame.Show = function(self)
|
||||||
self:ClearAllPoints()
|
origShow(self)
|
||||||
self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
|
self:SetAlpha(0)
|
||||||
|
self:EnableMouse(false)
|
||||||
|
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
|
||||||
end
|
end
|
||||||
HideBlizzardLoot()
|
|
||||||
|
|
||||||
local lootHook = CreateFrame("Frame")
|
|
||||||
lootHook:RegisterEvent("ADDON_LOADED")
|
|
||||||
lootHook:SetScript("OnEvent", function()
|
|
||||||
if arg1 == "Blizzard_Loot" then HideBlizzardLoot() end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ MinimapButton.lua
|
|||||||
Minimap.lua
|
Minimap.lua
|
||||||
MapReveal.lua
|
MapReveal.lua
|
||||||
WorldMap.lua
|
WorldMap.lua
|
||||||
|
ZoneLevelRange.lua
|
||||||
MapIcons.lua
|
MapIcons.lua
|
||||||
Tweaks.lua
|
Tweaks.lua
|
||||||
MinimapBuffs.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)
|
-- OnUpdate: line formatting (once) + cursor tracking (every frame)
|
||||||
local orig_OnUpdate = GameTooltip:GetScript("OnUpdate")
|
local orig_OnUpdate = GameTooltip:GetScript("OnUpdate")
|
||||||
local ttFormatThrottle = 0
|
local ttFormatThrottle = 0
|
||||||
|
local ttHideGrace = 0
|
||||||
GameTooltip:SetScript("OnUpdate", function()
|
GameTooltip:SetScript("OnUpdate", function()
|
||||||
if orig_OnUpdate then orig_OnUpdate() end
|
if orig_OnUpdate then orig_OnUpdate() end
|
||||||
|
|
||||||
@@ -342,12 +343,20 @@ function SFrames.FloatingTooltip:Initialize()
|
|||||||
local hasUnit = UnitExists("mouseover")
|
local hasUnit = UnitExists("mouseover")
|
||||||
|
|
||||||
if ttHadUnit and not hasUnit then
|
if ttHadUnit and not hasUnit then
|
||||||
|
ttHideGrace = ttHideGrace + arg1
|
||||||
|
if ttHideGrace < 0.2 then
|
||||||
|
TT_ShowBar(false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
ttHideGrace = 0
|
||||||
TT_ShowBar(false)
|
TT_ShowBar(false)
|
||||||
if GameTooltip._nanamiBGFrame then
|
if GameTooltip._nanamiBGFrame then
|
||||||
GameTooltip._nanamiBGFrame:Hide()
|
GameTooltip._nanamiBGFrame:Hide()
|
||||||
end
|
end
|
||||||
this:Hide()
|
this:Hide()
|
||||||
return
|
return
|
||||||
|
else
|
||||||
|
ttHideGrace = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
if not hasUnit then
|
if not hasUnit then
|
||||||
@@ -386,6 +395,7 @@ function SFrames.FloatingTooltip:Initialize()
|
|||||||
linesFormatted = false
|
linesFormatted = false
|
||||||
ttOwner = nil
|
ttOwner = nil
|
||||||
ttHadUnit = false
|
ttHadUnit = false
|
||||||
|
ttHideGrace = 0
|
||||||
TT_ShowBar(false)
|
TT_ShowBar(false)
|
||||||
if GameTooltip._nanamiBGFrame then
|
if GameTooltip._nanamiBGFrame then
|
||||||
GameTooltip._nanamiBGFrame:Hide()
|
GameTooltip._nanamiBGFrame:Hide()
|
||||||
|
|||||||
@@ -1434,6 +1434,10 @@ function TSUI.ProfNamesMatch(a, b)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TSUI.ScanProfessions()
|
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 = {}
|
S.profList = {}
|
||||||
if not GetSpellName then return end
|
if not GetSpellName then return end
|
||||||
local seen = {}
|
local seen = {}
|
||||||
@@ -1449,6 +1453,7 @@ function TSUI.ScanProfessions()
|
|||||||
end
|
end
|
||||||
idx = idx + 1
|
idx = idx + 1
|
||||||
end
|
end
|
||||||
|
S._profScanTime = now
|
||||||
end
|
end
|
||||||
|
|
||||||
function TSUI.CreateProfTabs(parent)
|
function TSUI.CreateProfTabs(parent)
|
||||||
@@ -1549,6 +1554,32 @@ function TSUI.IsTabSwitching()
|
|||||||
return S.switchStartTime and (GetTime() - S.switchStartTime) < 1.0
|
return S.switchStartTime and (GetTime() - S.switchStartTime) < 1.0
|
||||||
end
|
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)
|
-- Hide Blizzard Frames (module methods)
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -2018,6 +2049,7 @@ function TSUI:Initialize()
|
|||||||
-- ═══ Events ══════════════════════════════════════════════════════
|
-- ═══ Events ══════════════════════════════════════════════════════
|
||||||
MF:SetScript("OnHide", function()
|
MF:SetScript("OnHide", function()
|
||||||
S.switchStartTime = nil
|
S.switchStartTime = nil
|
||||||
|
TSUI.CancelScheduledUpdate()
|
||||||
API.CloseRecipe()
|
API.CloseRecipe()
|
||||||
if S.currentMode == "tradeskill" then TSUI.CleanupBlizzardTradeSkill()
|
if S.currentMode == "tradeskill" then TSUI.CleanupBlizzardTradeSkill()
|
||||||
else TSUI.CleanupBlizzardCraft() end
|
else TSUI.CleanupBlizzardCraft() end
|
||||||
@@ -2049,7 +2081,7 @@ function TSUI:Initialize()
|
|||||||
end)
|
end)
|
||||||
elseif event == "TRADE_SKILL_UPDATE" then
|
elseif event == "TRADE_SKILL_UPDATE" then
|
||||||
if S.MainFrame:IsVisible() and S.currentMode == "tradeskill" then
|
if S.MainFrame:IsVisible() and S.currentMode == "tradeskill" then
|
||||||
TSUI.UpdateProgressBar(); TSUI.FullUpdate()
|
TSUI.ScheduleUpdate()
|
||||||
end
|
end
|
||||||
elseif event == "TRADE_SKILL_CLOSE" then
|
elseif event == "TRADE_SKILL_CLOSE" then
|
||||||
TSUI.CleanupBlizzardTradeSkill()
|
TSUI.CleanupBlizzardTradeSkill()
|
||||||
@@ -2081,7 +2113,7 @@ function TSUI:Initialize()
|
|||||||
end)
|
end)
|
||||||
elseif event == "CRAFT_UPDATE" then
|
elseif event == "CRAFT_UPDATE" then
|
||||||
if S.MainFrame:IsVisible() and S.currentMode == "craft" then
|
if S.MainFrame:IsVisible() and S.currentMode == "craft" then
|
||||||
TSUI.UpdateProgressBar(); TSUI.FullUpdate()
|
TSUI.ScheduleUpdate()
|
||||||
end
|
end
|
||||||
elseif event == "CRAFT_CLOSE" then
|
elseif event == "CRAFT_CLOSE" then
|
||||||
TSUI.CleanupBlizzardCraft()
|
TSUI.CleanupBlizzardCraft()
|
||||||
@@ -2107,14 +2139,20 @@ function TSUI.ResetAndShow()
|
|||||||
if S.MainFrame.searchBox then S.MainFrame.searchBox:SetText("") end
|
if S.MainFrame.searchBox then S.MainFrame.searchBox:SetText("") end
|
||||||
if S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end
|
if S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end
|
||||||
if S.MainFrame.listScroll then S.MainFrame.listScroll:SetVerticalScroll(0) end
|
if S.MainFrame.listScroll then S.MainFrame.listScroll:SetVerticalScroll(0) end
|
||||||
TSUI.UpdateProgressBar(); S.MainFrame:Show(); TSUI.FullUpdate()
|
TSUI.CancelScheduledUpdate()
|
||||||
TSUI.UpdateScrollbar()
|
TSUI.UpdateProgressBar(); S.MainFrame:Show()
|
||||||
TSUI.UpdateProfTabs()
|
TSUI.UpdateProfTabs()
|
||||||
|
TSUI.BuildDisplayList()
|
||||||
for _, entry in ipairs(S.displayList) do
|
for _, entry in ipairs(S.displayList) do
|
||||||
if entry.type == "recipe" then
|
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
|
||||||
end
|
end
|
||||||
|
TSUI.FullUpdate()
|
||||||
|
TSUI.UpdateScrollbar()
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
124
Tweaks.lua
124
Tweaks.lua
@@ -7,6 +7,7 @@
|
|||||||
-- 5. Dark UI - darken the entire interface
|
-- 5. Dark UI - darken the entire interface
|
||||||
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
|
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
|
||||||
-- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells
|
-- 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 {}
|
SFrames.Tweaks = SFrames.Tweaks or {}
|
||||||
@@ -17,7 +18,8 @@ SFrames.castdb = SFrames.castdb or {}
|
|||||||
local function GetTweaksCfg()
|
local function GetTweaksCfg()
|
||||||
if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then
|
if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then
|
||||||
return { autoStance = true, superWoW = true, turtleCompat = true,
|
return { autoStance = true, superWoW = true, turtleCompat = true,
|
||||||
cooldownNumbers = true, darkUI = false, worldMapWindow = false }
|
cooldownNumbers = true, darkUI = false, worldMapWindow = false,
|
||||||
|
hunterAspectGuard = true }
|
||||||
end
|
end
|
||||||
return SFramesDB.Tweaks
|
return SFramesDB.Tweaks
|
||||||
end
|
end
|
||||||
@@ -80,6 +82,7 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
local function InitAutoDismount()
|
local function InitAutoDismount()
|
||||||
local dismount = CreateFrame("Frame", "NanamiAutoDismount")
|
local dismount = CreateFrame("Frame", "NanamiAutoDismount")
|
||||||
|
local _, playerClass = UnitClass("player")
|
||||||
|
|
||||||
local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate")
|
local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate")
|
||||||
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
|
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||||
@@ -93,7 +96,7 @@ local function InitAutoDismount()
|
|||||||
"^Augmente la vitesse de (.+)%%",
|
"^Augmente la vitesse de (.+)%%",
|
||||||
"^Скорость увеличена на (.+)%%",
|
"^Скорость увеличена на (.+)%%",
|
||||||
"^이동 속도 (.+)%%만큼 증가",
|
"^이동 속도 (.+)%%만큼 증가",
|
||||||
"^速度提高(.+)%%",
|
"^速度提高(.+)%%", "^移动速度提高(.+)%%",
|
||||||
"speed based on", "Slow and steady...", "Riding",
|
"speed based on", "Slow and steady...", "Riding",
|
||||||
"Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.",
|
"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",
|
"ability_druid_treeoflife", "ability_druid_stagform",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local hunterAspectIcons = {
|
||||||
|
"ability_mount_jungletiger",
|
||||||
|
"ability_mount_packhorse",
|
||||||
|
}
|
||||||
|
|
||||||
local errorStrings = {}
|
local errorStrings = {}
|
||||||
local errorGlobals = {
|
local errorGlobals = {
|
||||||
"SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED",
|
"SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED",
|
||||||
|
"ERR_NOT_WHILE_MOUNTED",
|
||||||
"SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED",
|
"SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED",
|
||||||
"SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE",
|
"SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE",
|
||||||
"ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED",
|
"ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED",
|
||||||
@@ -134,24 +143,43 @@ local function InitAutoDismount()
|
|||||||
if not matched then return end
|
if not matched then return end
|
||||||
|
|
||||||
for i = 0, 31 do
|
for i = 0, 31 do
|
||||||
scanner:ClearLines()
|
local buff = GetPlayerBuffTexture(i)
|
||||||
scanner:SetPlayerBuff(i)
|
if buff then
|
||||||
for line = 1, scanner:NumLines() do
|
local lowerBuff = string.lower(buff)
|
||||||
local text = getfenv(0)["NanamiDismountScanTextLeft" .. line]
|
|
||||||
if text and text:GetText() then
|
local skip = false
|
||||||
for _, str in pairs(mountStrings) do
|
if playerClass == "HUNTER" then
|
||||||
if string.find(text:GetText(), str) 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
|
||||||
|
local text = getfenv(0)["NanamiDismountScanTextLeft" .. line]
|
||||||
|
if text and text:GetText() then
|
||||||
|
for _, str in pairs(mountStrings) do
|
||||||
|
if string.find(text:GetText(), str) then
|
||||||
|
CancelPlayerBuff(i)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, icon in pairs(shapeshiftIcons) do
|
||||||
|
if string.find(lowerBuff, icon) then
|
||||||
CancelPlayerBuff(i)
|
CancelPlayerBuff(i)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local buff = GetPlayerBuffTexture(i)
|
if string.find(lowerBuff, "ability_mount_") then
|
||||||
if buff then
|
|
||||||
for _, icon in pairs(shapeshiftIcons) do
|
|
||||||
if string.find(string.lower(buff), icon) then
|
|
||||||
CancelPlayerBuff(i)
|
CancelPlayerBuff(i)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -167,7 +195,7 @@ end
|
|||||||
-- Data stored in SFrames.castdb[guid] for consumption by castbar features.
|
-- Data stored in SFrames.castdb[guid] for consumption by castbar features.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
local function InitSuperWoW()
|
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
|
local castdb = SFrames.castdb
|
||||||
|
|
||||||
@@ -1031,6 +1059,63 @@ local function InitDarkUI()
|
|||||||
end)
|
end)
|
||||||
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
|
-- Module API
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -1088,6 +1173,13 @@ function Tweaks:Initialize()
|
|||||||
end
|
end
|
||||||
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
|
if cfg.darkUI then
|
||||||
local ok, err = pcall(InitDarkUI)
|
local ok, err = pcall(InitDarkUI)
|
||||||
if not ok then
|
if not ok then
|
||||||
|
|||||||
434
Units/Pet.lua
434
Units/Pet.lua
@@ -350,7 +350,9 @@ function SFrames.Pet:Initialize()
|
|||||||
self.frame = f
|
self.frame = f
|
||||||
self.frame.unit = "pet"
|
self.frame.unit = "pet"
|
||||||
f:Hide()
|
f:Hide()
|
||||||
|
|
||||||
|
self:CreateAuras()
|
||||||
|
self:CreateHappinessWarning()
|
||||||
self:CreateCastbar()
|
self:CreateCastbar()
|
||||||
|
|
||||||
SFrames:RegisterEvent("UNIT_PET", function() if arg1 == "player" then self:UpdateAll() end end)
|
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
|
if SFramesDB and SFramesDB.showPetFrame == false then
|
||||||
self.frame:Hide()
|
self.frame:Hide()
|
||||||
if self.foodPanel then self.foodPanel:Hide() end
|
if self.foodPanel then self.foodPanel:Hide() end
|
||||||
|
self:HideAuras()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -403,6 +406,7 @@ function SFrames.Pet:UpdateAll()
|
|||||||
self:UpdatePowerType()
|
self:UpdatePowerType()
|
||||||
self:UpdatePower()
|
self:UpdatePower()
|
||||||
self:UpdateHappiness()
|
self:UpdateHappiness()
|
||||||
|
self:UpdateAuras()
|
||||||
|
|
||||||
local name = UnitName("pet")
|
local name = UnitName("pet")
|
||||||
if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
|
if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
|
||||||
@@ -415,6 +419,7 @@ function SFrames.Pet:UpdateAll()
|
|||||||
else
|
else
|
||||||
self.frame:Hide()
|
self.frame:Hide()
|
||||||
if self.foodPanel then self.foodPanel:Hide() end
|
if self.foodPanel then self.foodPanel:Hide() end
|
||||||
|
self:HideAuras()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -452,6 +457,7 @@ function SFrames.Pet:UpdateHappiness()
|
|||||||
local happiness = GetPetHappiness()
|
local happiness = GetPetHappiness()
|
||||||
if not happiness then
|
if not happiness then
|
||||||
self.frame.happinessBG:Hide()
|
self.frame.happinessBG:Hide()
|
||||||
|
self:HideHappinessWarning()
|
||||||
self:UpdateFoodButton()
|
self:UpdateFoodButton()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -471,8 +477,10 @@ function SFrames.Pet:UpdateHappiness()
|
|||||||
self.frame.happiness:SetTexCoord(0, 0.1875, 0, 0.359375)
|
self.frame.happiness:SetTexCoord(0, 0.1875, 0, 0.359375)
|
||||||
self.frame.happinessBG:Show()
|
self.frame.happinessBG:Show()
|
||||||
end
|
end
|
||||||
|
self:ShowHappinessWarning(happiness)
|
||||||
else
|
else
|
||||||
self.frame.happinessBG:Hide()
|
self.frame.happinessBG:Hide()
|
||||||
|
self:HideHappinessWarning()
|
||||||
end
|
end
|
||||||
self:UpdateFoodButton()
|
self:UpdateFoodButton()
|
||||||
end
|
end
|
||||||
@@ -1018,6 +1026,430 @@ function SFrames.Pet:UpdateFoodButton()
|
|||||||
end
|
end
|
||||||
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
|
-- Pet Castbar
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1613,13 +1613,24 @@ function SFrames.Player:CastbarStart(spellName, duration)
|
|||||||
cb:SetMinMaxValues(0, cb.maxValue)
|
cb:SetMinMaxValues(0, cb.maxValue)
|
||||||
cb:SetValue(0)
|
cb:SetValue(0)
|
||||||
cb.text:SetText(spellName)
|
cb.text:SetText(spellName)
|
||||||
|
|
||||||
local texture
|
local texture
|
||||||
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
||||||
if _UnitCastingInfo then
|
if _UnitCastingInfo then
|
||||||
local _, _, _, tex = _UnitCastingInfo("player")
|
local _, _, _, tex = _UnitCastingInfo("player")
|
||||||
texture = tex
|
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")
|
||||||
|
and SFrames.GetSpellIcon then
|
||||||
|
texture = SFrames.GetSpellIcon(spellName) or texture
|
||||||
|
end
|
||||||
|
|
||||||
if texture then
|
if texture then
|
||||||
cb.icon:SetTexture(texture)
|
cb.icon:SetTexture(texture)
|
||||||
cb.icon:Show()
|
cb.icon:Show()
|
||||||
@@ -1650,13 +1661,24 @@ function SFrames.Player:CastbarChannelStart(duration, spellName)
|
|||||||
cb:SetMinMaxValues(0, cb.maxValue)
|
cb:SetMinMaxValues(0, cb.maxValue)
|
||||||
cb:SetValue(cb.maxValue)
|
cb:SetValue(cb.maxValue)
|
||||||
cb.text:SetText(spellName)
|
cb.text:SetText(spellName)
|
||||||
|
|
||||||
local texture
|
local texture
|
||||||
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
||||||
if _UnitChannelInfo then
|
if _UnitChannelInfo then
|
||||||
local _, _, _, tex = _UnitChannelInfo("player")
|
local _, _, _, tex = _UnitChannelInfo("player")
|
||||||
texture = tex
|
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")
|
||||||
|
and SFrames.GetSpellIcon then
|
||||||
|
texture = SFrames.GetSpellIcon(spellName) or texture
|
||||||
|
end
|
||||||
|
|
||||||
if texture then
|
if texture then
|
||||||
cb.icon:SetTexture(texture)
|
cb.icon:SetTexture(texture)
|
||||||
cb.icon:Show()
|
cb.icon:Show()
|
||||||
@@ -1714,6 +1736,9 @@ function SFrames.Player:CastbarOnUpdate()
|
|||||||
end
|
end
|
||||||
cb:SetValue(elapsed)
|
cb:SetValue(elapsed)
|
||||||
cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0)))
|
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
|
elseif cb.channeling then
|
||||||
local timeRemaining = cb.endTime - GetTime()
|
local timeRemaining = cb.endTime - GetTime()
|
||||||
if timeRemaining <= 0 then
|
if timeRemaining <= 0 then
|
||||||
@@ -1724,6 +1749,9 @@ function SFrames.Player:CastbarOnUpdate()
|
|||||||
end
|
end
|
||||||
cb:SetValue(timeRemaining)
|
cb:SetValue(timeRemaining)
|
||||||
cb.time:SetText(string.format("%.1f", timeRemaining))
|
cb.time:SetText(string.format("%.1f", timeRemaining))
|
||||||
|
if not cb.icon:IsShown() then
|
||||||
|
self:CastbarTryResolveIcon()
|
||||||
|
end
|
||||||
elseif cb.fadeOut then
|
elseif cb.fadeOut then
|
||||||
local alpha = cb:GetAlpha() - 0.05
|
local alpha = cb:GetAlpha() - 0.05
|
||||||
if alpha > 0 then
|
if alpha > 0 then
|
||||||
@@ -1740,3 +1768,41 @@ function SFrames.Player:CastbarOnUpdate()
|
|||||||
end
|
end
|
||||||
end
|
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
|
||||||
|
|||||||
493
Units/Target.lua
493
Units/Target.lua
@@ -1,6 +1,177 @@
|
|||||||
SFrames.Target = {}
|
SFrames.Target = {}
|
||||||
local _A = SFrames.ActiveTheme
|
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)
|
local function Clamp(value, minValue, maxValue)
|
||||||
if value < minValue then
|
if value < minValue then
|
||||||
return minValue
|
return minValue
|
||||||
@@ -467,7 +638,11 @@ function SFrames.Target:Initialize()
|
|||||||
self:CreateAuras()
|
self:CreateAuras()
|
||||||
self:CreateCastbar()
|
self:CreateCastbar()
|
||||||
self:InitializeDistanceFrame()
|
self:InitializeDistanceFrame()
|
||||||
|
self:InitCastDetection()
|
||||||
|
|
||||||
|
BuildSpellIconCache()
|
||||||
|
SFrames:RegisterEvent("SPELLS_CHANGED", BuildSpellIconCache)
|
||||||
|
|
||||||
f.unit = "target"
|
f.unit = "target"
|
||||||
f:SetScript("OnEnter", function()
|
f:SetScript("OnEnter", function()
|
||||||
GameTooltip_SetDefaultAnchor(GameTooltip, this)
|
GameTooltip_SetDefaultAnchor(GameTooltip, this)
|
||||||
@@ -492,11 +667,181 @@ function SFrames.Target:Initialize()
|
|||||||
end
|
end
|
||||||
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()
|
function SFrames.Target:OnTargetChanged()
|
||||||
|
targetCLCast = nil
|
||||||
if UnitExists("target") then
|
if UnitExists("target") then
|
||||||
self.frame:Show()
|
self.frame:Show()
|
||||||
self:UpdateAll()
|
self:UpdateAll()
|
||||||
-- Force distance update immediately
|
|
||||||
if SFrames.Target.distanceFrame then
|
if SFrames.Target.distanceFrame then
|
||||||
local dist = self:GetDistance("target")
|
local dist = self:GetDistance("target")
|
||||||
SFrames.Target.distanceFrame.text:SetText(dist or "---")
|
SFrames.Target.distanceFrame.text:SetText(dist or "---")
|
||||||
@@ -1141,49 +1486,149 @@ function SFrames.Target:CastbarOnUpdate()
|
|||||||
cb.ibg:Hide()
|
cb.ibg:Hide()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Try to read cast from Vanilla extensions (SuperWoW or TurtleWoW modern API, or ShaguTweaks)
|
local cast, texture, startTime, endTime, channel
|
||||||
local cast, nameSubtext, text, texture, startTime, endTime
|
|
||||||
|
-- 1) UnitCastingInfo / UnitChannelInfo (TurtleWoW / ShaguTweaks)
|
||||||
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
|
||||||
if _UnitCastingInfo then
|
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
|
end
|
||||||
|
|
||||||
local channel
|
if not cast then
|
||||||
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
|
||||||
if not cast and _UnitChannelInfo then
|
if _UnitChannelInfo then
|
||||||
channel, nameSubtext, text, texture, startTime, endTime = _UnitChannelInfo("target")
|
local c, _, _, tex, st, et = _UnitChannelInfo("target")
|
||||||
cast = channel
|
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
|
end
|
||||||
|
|
||||||
if cast and startTime and endTime then
|
if cast and startTime and endTime then
|
||||||
local duration = (endTime - startTime) / 1000
|
local duration = (endTime - startTime) / 1000
|
||||||
local cur = GetTime() - (startTime / 1000)
|
local cur = GetTime() - (startTime / 1000)
|
||||||
|
|
||||||
if channel then
|
if channel then
|
||||||
cur = duration + (startTime / 1000) - GetTime()
|
cur = duration + (startTime / 1000) - GetTime()
|
||||||
end
|
end
|
||||||
|
|
||||||
if cur > duration then cur = duration end
|
if cur > duration then cur = duration end
|
||||||
if cur < 0 then cur = 0 end
|
if cur < 0 then cur = 0 end
|
||||||
|
|
||||||
cb:SetMinMaxValues(0, duration)
|
cb:SetMinMaxValues(0, duration)
|
||||||
cb:SetValue(cur)
|
cb:SetValue(cur)
|
||||||
cb.text:SetText(cast)
|
cb.text:SetText(cast)
|
||||||
|
|
||||||
if texture then
|
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
|
||||||
cb.icon:SetTexture(texture)
|
texture = GetSpellIcon(cast) or texture
|
||||||
end
|
end
|
||||||
|
|
||||||
cb:SetAlpha(1)
|
cb:SetAlpha(1)
|
||||||
cb.cbbg:SetAlpha(1)
|
cb.cbbg:SetAlpha(1)
|
||||||
cb.icon:SetAlpha(1)
|
|
||||||
cb.ibg:SetAlpha(1)
|
|
||||||
|
|
||||||
cb:Show()
|
cb:Show()
|
||||||
cb.cbbg:Show()
|
cb.cbbg:Show()
|
||||||
cb.icon:Show()
|
|
||||||
cb.ibg: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
|
else
|
||||||
cb:Hide()
|
cb:Hide()
|
||||||
cb.cbbg: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