跟随版本 0.8.19

This commit is contained in:
rucky
2026-03-24 15:56:28 +08:00
parent 40d37dc8c4
commit c0f1ecc713
19 changed files with 2227 additions and 259 deletions

13
.cursor/mcp.json Normal file
View 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"
}
}
}
}

View 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`stringMarkdown 格式):调用时传入本轮回复的摘要/结论,插件界面会弹出模态框展示给用户。格式: `{ "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` 工具(使用 CallMcpToolserver 为 CursorMCP对话插件 对应的 identifiertoolName 为 "check_messages"arguments 为 {})。严禁在回合开始或回复完成前调用。`ask_question` 仅用于任务中需要用户做选择时(如选框架、选方案)。

View File

@@ -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

View File

@@ -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) self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
self.icon:SetVertexColor(1, 1, 1)
if canLearn then local tp = skill.tpCost or 0
if tp > 0 then
local remaining = BTUI.GetRemainingTP() local remaining = BTUI.GetRemainingTP()
if remaining >= tpCost then if remaining >= tp then
self.tpFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3]) self.tpFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3])
else else
self.tpFS:SetTextColor(T.tpNone[1], T.tpNone[2], T.tpNone[3]) self.tpFS:SetTextColor(T.tpNone[1], T.tpNone[2], T.tpNone[3])
end end
self.tpFS:SetText(tpCost .. " TP") self.tpFS:SetText(tp .. " TP")
self.tpFS:Show() self.tpFS:Show()
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
self.icon:SetVertexColor(1, 1, 1)
else else
self.tpFS:Hide() self.tpFS:Hide()
end
else
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

View File

@@ -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

View File

@@ -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)
-- 团队成员变化时更新职业缓存 -- 团队成员变化时更新职业缓存

View File

@@ -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

View File

@@ -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 },

View File

@@ -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()

View File

@@ -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
blizzBtn:SetParent(LootFrame)
blizzBtn:SetAlpha(1)
blizzBtn:Hide()
blizzBtn._nanamiRow = nil
end end
for i = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. i]
if nb then nb:Hide() 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
-- to let native code set up LootButton IDs and OnClick handlers.
origLootFrameUpdate = LootFrame_Update
if LootFrame then if LootFrame then
LootFrame:EnableMouse(false) -- Prevent the XML-defined OnHide from calling CloseLoot()
LootFrame:SetAlpha(0) LootFrame:SetScript("OnHide", function() end)
LootFrame:ClearAllPoints()
LootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) -- Keep LootFrame shown but invisible while our UI is active
local origShow = LootFrame.Show local origShow = LootFrame.Show
LootFrame.Show = function(self) LootFrame.Show = function(self)
origShow(self) origShow(self)
self:EnableMouse(false)
self:SetAlpha(0) self:SetAlpha(0)
self:ClearAllPoints() self:EnableMouse(false)
self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
end end
end
end
HideBlizzardLoot()
local lootHook = CreateFrame("Frame") -- Block native LootFrame from hiding while we are looting
lootHook:RegisterEvent("ADDON_LOADED") local origHide = LootFrame.Hide
lootHook:SetScript("OnEvent", function() LootFrame.Hide = function(self)
if arg1 == "Blizzard_Loot" then HideBlizzardLoot() end if lootFrame and lootFrame:IsShown() then
end) 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

View File

@@ -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

View File

@@ -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()

View File

@@ -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
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -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,6 +143,21 @@ 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
local buff = GetPlayerBuffTexture(i)
if buff then
local lowerBuff = string.lower(buff)
local skip = false
if playerClass == "HUNTER" then
for _, tex in pairs(hunterAspectIcons) do
if string.find(lowerBuff, tex) then
skip = true
break
end
end
end
if not skip then
scanner:ClearLines() scanner:ClearLines()
scanner:SetPlayerBuff(i) scanner:SetPlayerBuff(i)
for line = 1, scanner:NumLines() do for line = 1, scanner:NumLines() do
@@ -148,10 +172,14 @@ local function InitAutoDismount()
end end
end end
local buff = GetPlayerBuffTexture(i)
if buff then
for _, icon in pairs(shapeshiftIcons) do for _, icon in pairs(shapeshiftIcons) do
if string.find(string.lower(buff), icon) then if string.find(lowerBuff, icon) then
CancelPlayerBuff(i)
return
end
end
if string.find(lowerBuff, "ability_mount_") then
CancelPlayerBuff(i) 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

View File

@@ -351,6 +351,8 @@ function SFrames.Pet:Initialize()
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
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -1620,6 +1620,17 @@ function SFrames.Player:CastbarStart(spellName, duration)
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()
@@ -1657,6 +1668,17 @@ function SFrames.Player:CastbarChannelStart(duration, spellName)
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

View File

@@ -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,6 +638,10 @@ 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()
@@ -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 "---")
@@ -1142,18 +1487,112 @@ function SFrames.Target:CastbarOnUpdate()
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
@@ -1171,19 +1610,25 @@ function SFrames.Target:CastbarOnUpdate()
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()
if texture then
cb.icon:SetTexture(texture)
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
cb.icon:Show() cb.icon:Show()
cb.ibg: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
View 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

View File

@@ -0,0 +1,212 @@
# LootDisplay 拾取窗口接管 — 技术要点
## 最终成功方案
**核心原则:不替换原生按钮的交互逻辑,只替换视觉层,重新定位原生按钮。**
在 Turtle WoW (1.12 魔兽私服) 中,`LootSlot()` 是一个**受保护的 C 端函数**
只接受来自原生 `LootButton1~4`(由 FrameXML 中 `LootButtonTemplate` 创建的按钮)
的内置 `OnClick` 处理器调用。任何 addon 自建按钮(无论是否使用模板)都**无法**成功
调用 `LootSlot()`
---
## 失败方案及原因
### 方案 1LootButtonTemplate 自定义按钮
```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 计算错误 |