This commit is contained in:
rucky
2026-03-16 13:48:46 +08:00
commit 2a55dd6dad
93 changed files with 75075 additions and 0 deletions

7
.nanami-launcher.json Normal file
View File

@@ -0,0 +1,7 @@
{
"packageId": "nanami-ui",
"packageName": "Nanami-UI",
"source": "bundled",
"folderName": "Nanami-UI",
"installedAt": "2026-03-04T11:12:15.763Z"
}

1529
AFKScreen.lua Normal file

File diff suppressed because it is too large Load Diff

1749
ActionBars.lua Normal file

File diff suppressed because it is too large Load Diff

2087
Bags/Bank.lua Normal file

File diff suppressed because it is too large Load Diff

1619
Bags/Container.lua Normal file

File diff suppressed because it is too large Load Diff

113
Bags/Core.lua Normal file
View File

@@ -0,0 +1,113 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Module Core (Bags/Core.lua)
-- Entry point, registered on PLAYER_LOGIN
--------------------------------------------------------------------------------
SFrames.Bags.Core = {}
-- Ensure default config exists
if not SFrames.Config.Bags then
SFrames.Config.Bags = {
enable = true,
columns = 10,
bagSpacing = 0,
scale = 1,
sellGrey = true,
bankColumns = 12,
bankSpacing = 0,
bankScale = 1,
}
end
local function EnsureDB()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Bags then
SFramesDB.Bags = {}
for k, v in pairs(SFrames.Config.Bags) do
SFramesDB.Bags[k] = v
end
end
-- Fill missing keys with defaults
for k, v in pairs(SFrames.Config.Bags) do
if SFramesDB.Bags[k] == nil then
SFramesDB.Bags[k] = v
end
end
end
local function HookBagFunctions()
-- Save original functions
local _OpenAllBags = OpenAllBags
local _CloseAllBags = CloseAllBags
local _ToggleBag = ToggleBag
local _ToggleBackpack = ToggleBackpack
local _ToggleKeyRing = ToggleKeyRing
SFrames.Bags._origToggleKeyRing = _ToggleKeyRing
OpenAllBags = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Open()
else
_OpenAllBags()
end
end
CloseAllBags = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Close()
else
_CloseAllBags()
end
end
ToggleBag = function(id)
if id == -2 or id == (KEYRING_CONTAINER or -2) then
_ToggleBag(id)
return
end
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Toggle()
else
_ToggleBag(id)
end
end
ToggleBackpack = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Toggle()
else
_ToggleBackpack()
end
end
-- Keyring: always use original keyring window
ToggleKeyRing = function()
_ToggleKeyRing()
end
end
function SFrames.Bags.Core:Initialize()
SFrames:Print("Debug: Bags Core Initializing...")
EnsureDB()
if not SFramesDB.Bags.enable then
SFrames:Print("Debug: Bags are disabled in config.")
return
end
SFrames:Print("Debug: Hooking functions...")
HookBagFunctions()
SFrames:Print("Debug: Init Container...")
SFrames.Bags.Container:Initialize()
SFrames:Print("Debug: Init Bank...")
if SFrames.Bags.Bank then SFrames.Bags.Bank:Initialize() end
SFrames:Print("Debug: Init Features...")
SFrames.Bags.Features:Initialize()
SFrames:Print("Debug: Bags Init Complete.")
end
-- NOTE: Initialize is called from Core.lua SFrames:Initialize() on PLAYER_LOGIN.
-- Do NOT register PLAYER_LOGIN here to avoid double initialization.

139
Bags/Features.lua Normal file
View File

@@ -0,0 +1,139 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Module Features (Bags/Features.lua)
-- Auto-sell grey + search apply (buttons are now built in Container.lua)
--------------------------------------------------------------------------------
SFrames.Bags.Features = {}
function SFrames.Bags.Features:Initialize()
self:SetupAutoSell()
self:SetupAutoOpenBags()
end
-- Auto Sell Grey Items when merchant opens
function SFrames.Bags.Features:SetupAutoSell()
local f = CreateFrame("Frame")
f:RegisterEvent("MERCHANT_SHOW")
f:SetScript("OnEvent", function()
if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.sellGrey) then return end
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot)
if link then
-- Extract item ID from link (format: item:XXXX:...)
local _, _, itemString = string.find(link, "item:(%d+)")
if itemString then
local _, _, _, _, _, _, _, _, _, _, itemSellPrice = GetItemInfo("item:" .. itemString)
-- GetItemInfo returns quality as 5th return
local _, _, itemRarity = GetItemInfo("item:" .. itemString)
-- safer: use a scan tooltip to get quality
local scanName, _, scanRarity = GetItemInfo("item:" .. itemString)
if scanRarity and scanRarity == 0 then
local _, itemCount = GetContainerItemInfo(bag, slot)
UseContainerItem(bag, slot)
if scanName then
SFrames:Print("售出: " .. scanName .. " x" .. (itemCount or 1))
end
end
end
end
end
end
end)
end
-- Auto open/close bags at Bank, Merchant, Mail, AH, Trade
function SFrames.Bags.Features:SetupAutoOpenBags()
local f = CreateFrame("Frame")
f:RegisterEvent("MERCHANT_SHOW")
f:RegisterEvent("BANKFRAME_OPENED")
f:RegisterEvent("MAIL_SHOW")
f:RegisterEvent("AUCTION_HOUSE_SHOW")
f:RegisterEvent("TRADE_SHOW")
f:RegisterEvent("MERCHANT_CLOSED")
f:RegisterEvent("BANKFRAME_CLOSED")
f:RegisterEvent("MAIL_CLOSED")
f:RegisterEvent("AUCTION_HOUSE_CLOSED")
f:RegisterEvent("TRADE_CLOSED")
local autoOpened = false
f:SetScript("OnEvent", function()
if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end
-- Shows
if event == "MERCHANT_SHOW" or event == "BANKFRAME_OPENED" or event == "MAIL_SHOW" or event == "AUCTION_HOUSE_SHOW" or event == "TRADE_SHOW" then
if SFramesBagFrame and not SFramesBagFrame:IsVisible() then
autoOpened = true
SFrames.Bags.Container:Open()
end
-- Closes
elseif event == "MERCHANT_CLOSED" or event == "BANKFRAME_CLOSED" or event == "MAIL_CLOSED" or event == "AUCTION_HOUSE_CLOSED" or event == "TRADE_CLOSED" then
if autoOpened then
SFrames.Bags.Container:Close()
autoOpened = false
end
end
end)
end
-- Search filter - called from the search EditBox OnTextChanged
function SFrames.Bags.Features:ApplySearch(query)
-- In WoW 1.12 Chinese client, text is GBK encoded.
-- using string.lower on GBK strings corrupts Chinese characters because the 2nd byte
-- of Chinese characters can hit the ASCII uppercase range (A-Z).
local safeQuery = query or ""
safeQuery = string.gsub(safeQuery, "^%s+", "")
safeQuery = string.gsub(safeQuery, "%s+$", "")
if not string.find(safeQuery, "%S") then
safeQuery = ""
end
local function ProcessSlots(prefix)
for i = 1, 250 do
local btn = _G[prefix .. i]
if not btn then break end
local icon = _G[btn:GetName() .. "IconTexture"]
if btn:IsShown() and icon then
if safeQuery == "" then
icon:SetVertexColor(1, 1, 1)
if btn.border then btn.border:SetAlpha(1) end
if btn.junkIcon then btn.junkIcon:SetAlpha(1) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end
elseif btn.bagID ~= nil and btn.slotID ~= nil then
local link = GetContainerItemLink(btn.bagID, btn.slotID)
if link then
local name = GetItemInfo(link)
if not name then
local _, _, parsedName = string.find(link, "%[(.+)%]")
name = parsedName
end
-- Exact substring match (safe for GBK)
if name and string.find(name, safeQuery, 1, true) then
icon:SetVertexColor(1, 1, 1)
if btn.border then btn.border:SetAlpha(1) end
if btn.junkIcon then btn.junkIcon:SetAlpha(1) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end
else
icon:SetVertexColor(0.45, 0.45, 0.45)
if btn.border then btn.border:SetAlpha(0.4) end
if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end
end
else
icon:SetVertexColor(0.45, 0.45, 0.45)
if btn.border then btn.border:SetAlpha(0.4) end
if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end
end
end
end
end
end
ProcessSlots("SFramesBagSlot")
end

337
Bags/Offline.lua Normal file
View File

@@ -0,0 +1,337 @@
--------------------------------------------------------------------------------
-- S-Frames: Offline DB for Bag Module (Bags/Offline.lua)
-- Tracks inventory across characters and realms
-- NOTE: This file defines SFrames.Bags first - must be loaded before other Bag files
--------------------------------------------------------------------------------
SFrames.Bags = SFrames.Bags or {}
SFrames.Bags.Offline = {}
local offlineFrame = CreateFrame("Frame")
offlineFrame:RegisterEvent("PLAYER_LOGIN")
offlineFrame:RegisterEvent("BAG_UPDATE")
offlineFrame:RegisterEvent("BANKFRAME_OPENED")
offlineFrame:RegisterEvent("BANKFRAME_CLOSED")
offlineFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
offlineFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
local realmName = ""
local playerName = ""
local isBankOpen = false
local function QueueBankRefreshScans()
local elapsed = 0
local index = 1
local points = { 0.08, 0.25, 0.55, 0.95 }
local t = CreateFrame("Frame")
t:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if index <= table.getn(points) and elapsed >= points[index] then
if isBankOpen then
SFrames.Bags.Offline:ScanBags()
end
index = index + 1
end
if index > table.getn(points) then
this:SetScript("OnUpdate", nil)
end
end)
end
local function InitDB()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB[realmName] then SFramesGlobalDB[realmName] = {} end
if not SFramesGlobalDB[realmName][playerName] then
SFramesGlobalDB[realmName][playerName] = {
bags = {},
bank = {},
bankSlots = 0,
bankBags = {},
equippedBags = {},
money = 0
}
end
end
local function IsBankBagInvSlot(slotID)
return type(slotID) == "number" and slotID > 23
end
local function IsBagItemLink(link)
if type(link) ~= "string" or link == "" then
return false
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
return equipLoc == "INVTYPE_BAG"
end
local function GetBagLinkState(link)
if type(link) ~= "string" or link == "" then
return "invalid"
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
if equipLoc == "INVTYPE_BAG" then
return "bag"
end
if equipLoc == nil or equipLoc == "" then
return "unknown"
end
return "invalid"
end
local function SafeGetInventorySlotByName(slotName)
if not GetInventorySlotInfo then return nil end
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and IsBankBagInvSlot(slotID) then
return slotID
end
return nil
end
local function ResolvePlayerBagInvSlot(index)
if type(index) ~= "number" or index < 1 or index > 4 then
return nil
end
local liveBtn = _G["CharacterBag" .. (index - 1) .. "Slot"]
if liveBtn and liveBtn.GetID then
local id = liveBtn:GetID()
if type(id) == "number" and id > 0 then
return id
end
end
if ContainerIDToInventoryID then
local ok, slotID = pcall(function() return ContainerIDToInventoryID(index) end)
if ok and type(slotID) == "number" and slotID > 0 then
return slotID
end
end
local slotNames = {
"Bag" .. (index - 1) .. "Slot",
"Bag" .. index .. "Slot",
"CharacterBag" .. (index - 1) .. "Slot",
}
for _, slotName in ipairs(slotNames) do
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and type(slotID) == "number" and slotID > 0 then
return slotID
end
end
-- Vanilla fallback.
local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
return fallback[index]
end
local function CaptureEquippedBagMeta(db)
if not db then return end
db.equippedBags = {}
for index = 1, 4 do
local invSlot = ResolvePlayerBagInvSlot(index)
local link = nil
local texture = nil
if invSlot then
link = GetInventoryItemLink("player", invSlot)
texture = GetInventoryItemTexture("player", invSlot)
end
local size = GetContainerNumSlots(index) or 0
db.equippedBags[index] = {
link = link,
texture = texture,
size = tonumber(size) or 0,
}
end
end
local function ResolveBankBagInvSlot(index)
if type(index) ~= "number" or index <= 0 then return nil end
local bagID = index + 4
local function TryBankButtonID(buttonID, isBank)
if not BankButtonIDToInvSlotID then return nil end
local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end)
if ok and IsBankBagInvSlot(slotID) then
return slotID
end
return nil
end
local function AcceptIfBag(slotID)
if not IsBankBagInvSlot(slotID) then return nil end
local link = GetInventoryItemLink("player", slotID)
if link and IsBagItemLink(link) then
return slotID
end
return nil
end
-- Guda-style primary mapping
local slot = TryBankButtonID(bagID, 1)
if slot then return slot end
slot = TryBankButtonID(index, 1)
if slot then return slot end
-- Fallbacks: only when they are confirmed bag items
slot = TryBankButtonID(bagID, nil)
slot = AcceptIfBag(slot)
if slot then return slot end
slot = TryBankButtonID(index, nil)
slot = AcceptIfBag(slot)
if slot then return slot end
if ContainerIDToInventoryID then
local ok, s = pcall(function() return ContainerIDToInventoryID(bagID) end)
if ok then
slot = AcceptIfBag(s)
if slot then return slot end
end
end
slot = AcceptIfBag(SafeGetInventorySlotByName("BankBagSlot" .. index))
if slot then return slot end
slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index))
if slot then return slot end
local liveBtn = _G["BankFrameBag" .. index] or _G["BankFrameBag" .. index .. "Slot"]
if liveBtn and liveBtn.GetID then
slot = AcceptIfBag(liveBtn:GetID())
if slot then return slot end
end
return nil
end
local function CaptureBankBagMeta(db)
if not db then return end
local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
db.bankSlots = tonumber(purchased) or 0
db.bankBags = {}
for index = 1, 7 do
local bagID = index + 4
local size = GetContainerNumSlots(bagID) or 0
local info = {
unlocked = (index <= db.bankSlots),
size = tonumber(size) or 0,
link = nil,
texture = nil,
}
if info.unlocked then
local invSlot = ResolveBankBagInvSlot(index)
if invSlot then
local link = GetInventoryItemLink("player", invSlot)
local state = GetBagLinkState(link)
if (state == "bag") or (state == "unknown" and info.size > 0) then
info.link = link
info.texture = GetInventoryItemTexture("player", invSlot)
end
end
end
db.bankBags[index] = info
end
end
local function ScanContainer(containerID, isBank)
if not SFramesGlobalDB or realmName == "" or playerName == "" then return end
local db = SFramesGlobalDB[realmName][playerName]
local targetDB = isBank and db.bank or db.bags
local size = GetContainerNumSlots(containerID)
targetDB[containerID] = { size = size, items = {} }
for slot = 1, size do
local link = GetContainerItemLink(containerID, slot)
if link then
local texture, itemCount, locked, quality = GetContainerItemInfo(containerID, slot)
local _, _, idStr = string.find(link, "item:(%d+):")
local itemID = idStr and tonumber(idStr) or nil
targetDB[containerID].items[slot] = { id = itemID,
link = link,
count = itemCount,
texture = texture,
quality = quality
}
end
end
end
function SFrames.Bags.Offline:ScanBags()
if realmName == "" or playerName == "" then return end
InitDB()
local db = SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][playerName]
if not db then return end
for bag = 0, 4 do
ScanContainer(bag, false)
end
CaptureEquippedBagMeta(db)
if isBankOpen then
ScanContainer(-1, true)
for bag = 5, 11 do
ScanContainer(bag, true)
end
CaptureBankBagMeta(db)
end
db.money = GetMoney()
end
local scanPending = false
local scanTimer = CreateFrame("Frame")
scanTimer:Hide()
local function RequestBagScan()
if scanPending then return end
scanPending = true
scanTimer.elapsed = 0
scanTimer:Show()
scanTimer:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed > 0.5 then
this:SetScript("OnUpdate", nil)
this:Hide()
scanPending = false
SFrames.Bags.Offline:ScanBags()
end
end)
end
offlineFrame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
realmName = GetRealmName() or ""
playerName = UnitName("player") or ""
InitDB()
SFrames.Bags.Offline:ScanBags()
elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then
RequestBagScan()
elseif event == "BANKFRAME_OPENED" then
isBankOpen = true
RequestBagScan()
QueueBankRefreshScans()
elseif event == "BANKFRAME_CLOSED" then
isBankOpen = false
end
end)
function SFrames.Bags.Offline:GetCharacterList()
if not SFramesGlobalDB or realmName == "" then return {} end
local chars = {}
if SFramesGlobalDB[realmName] then
for name in pairs(SFramesGlobalDB[realmName]) do
table.insert(chars, name)
end
end
return chars
end
function SFrames.Bags.Offline:GetCharacterData(name)
if not SFramesGlobalDB or realmName == "" then return nil end
return SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][name]
end
function SFrames.Bags.Offline:GetCurrentPlayerName()
return playerName
end

483
Bags/Sort.lua Normal file
View File

@@ -0,0 +1,483 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Sorting Logic (Bags/Sort.lua)
-- Manual sorting algorithm compatible with Vanilla/Turtle WoW
--------------------------------------------------------------------------------
SFrames.Bags.Sort = SFrames.Bags.Sort or {}
local isSorting = false
local sortQueue = {}
local sortTimer = CreateFrame("Frame")
local sortDelay = 0.05 -- Increased speed from 0.15 to accelerate sorting.
local timeSinceLast = 0
local activeCompleteMessage = nil
local activeCompleteUpdate = nil
local activeBagOrder = nil
local activePhase = nil
local TEXT_LOCKED = "\233\131\168\229\136\134\231\137\169\229\147\129\229\183\178\233\148\129\229\174\154\239\188\140\232\175\183\231\168\141\229\144\142\233\135\141\232\175\149\227\128\130"
local TEXT_OFFLINE_BAGS = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\232\131\140\229\140\133\227\128\130"
local TEXT_BAG_DONE = "\232\131\140\229\140\133\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local TEXT_OFFLINE_BANK = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\233\147\182\232\161\140\227\128\130"
local TEXT_BANK_DONE = "\233\147\182\232\161\140\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local itemMaxStackCache = {}
local function GetItemSortValue(link)
if not link then return 0, "" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, itemRarity, _, itemType = GetItemInfo(itemString)
if not itemName then return 0, "" end
local score = 0
local _, _, itemID = string.find(link, "item:(%d+)")
if itemID == "6948" or string.find(itemName, "Hearthstone") then
score = 1000000
else
score = score + (itemRarity or 0) * 100000
if itemType == "Weapon" then
score = score + 50000
elseif itemType == "Armor" then
score = score + 40000
elseif itemType == "Consumable" then
score = score + 30000
elseif itemType == "Trade Goods" then
score = score + 20000
elseif itemType == "Recipe" then
score = score + 10000
end
end
return score, itemName
end
local function GetItemIdentity(link)
if not link then return nil end
local _, _, itemString = string.find(link, "|H([^|]+)|h")
if not itemString then itemString = link end
return itemString
end
local function GetItemMaxStack(link)
local itemKey = GetItemIdentity(link)
if not itemKey then return 1 end
if itemMaxStackCache[itemKey] then
return itemMaxStackCache[itemKey]
end
local maxStack = 1
local res = {GetItemInfo(itemKey)}
-- In standard vanilla, maxStack is parameter 7. In some modified schema (TBC) it is parameter 8.
local v7 = tonumber(res[7])
local v8 = tonumber(res[8])
if v7 and v7 > 1 then
maxStack = v7
elseif v8 and v8 > 1 then
maxStack = v8
end
itemMaxStackCache[itemKey] = maxStack
return maxStack
end
local function GetSpecialType(link, isBag)
if not link then return "Normal" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, _, _, itemType, itemSubType = GetItemInfo(itemString)
if not itemType and not itemName then return "Normal" end
local _, _, itemID = string.find(itemString, "item:(%d+)")
if isBag then
if itemType == "Quiver" or itemType == "箭袋" then return "Ammo" end
if itemSubType == "Quiver" or itemSubType == "Ammo Pouch" or itemSubType == "箭袋" or itemSubType == "子弹袋" then return "Ammo" end
if itemSubType == "Soul Bag" or itemSubType == "灵魂袋" then return "Soul" end
if itemSubType == "Enchanting Bag" or itemSubType == "附魔材料袋" then return "Enchanting" end
if itemSubType == "Herb Bag" or itemSubType == "草药袋" then return "Herb" end
else
if itemType == "Projectile" or itemType == "弹药" then return "Ammo" end
if itemSubType == "Arrow" or itemSubType == "Bullet" or itemSubType == "" or itemSubType == "子弹" then return "Ammo" end
if itemID == "6265" or itemName == "Soul Shard" or itemName == "灵魂碎片" then return "Soul" end
end
return "Normal"
end
local function SortItemsByRule(items)
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score > b.score
elseif a.name ~= b.name then
return a.name < b.name
else
return a.count > b.count
end
end)
end
local function ResetSortState()
isSorting = false
sortQueue = {}
activeCompleteMessage = nil
activeCompleteUpdate = nil
activeBagOrder = nil
activePhase = nil
timeSinceLast = 0
sortTimer:Hide()
end
local function FinishSort()
local msg = activeCompleteMessage
local updater = activeCompleteUpdate
ResetSortState()
if msg then SFrames:Print(msg) end
if updater then updater() end
end
function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder)
sortQueue = {}
local slotPool = { Normal = {}, Ammo = {}, Soul = {}, Enchanting = {}, Herb = {} }
local allocatedStream = {}
for _, bag in ipairs(bagOrder) do
local btype = "Normal"
if bag ~= 0 and bag ~= -1 then
local invID
if bag >= 1 and bag <= 4 then
invID = ContainerIDToInventoryID(bag)
elseif bag >= 5 and bag <= 11 then
invID = bag + 34
end
if invID then
local ok, link = pcall(GetInventoryItemLink, "player", invID)
if ok and link then btype = GetSpecialType(link, true) end
end
end
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local s = { bag = bag, slot = slot, type = btype }
table.insert(allocatedStream, s)
if slotPool[btype] then
table.insert(slotPool[btype], s)
else
table.insert(slotPool.Normal, s)
end
end
end
for _, item in ipairs(items) do
item.specialType = GetSpecialType(item.link, false)
end
-- Now assign target slot to each item
for _, item in ipairs(items) do
local targetSlot = nil
if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then
targetSlot = table.remove(slotPool[item.specialType], 1)
elseif table.getn(slotPool.Normal) > 0 then
targetSlot = table.remove(slotPool.Normal, 1)
end
if targetSlot then
targetSlot.idealItem = item
end
end
-- Build virtual moves to align physical items to targetSlots
for _, target in ipairs(allocatedStream) do
if target.idealItem then
local idealItem = target.idealItem
if idealItem.bag ~= target.bag or idealItem.slot ~= target.slot then
table.insert(sortQueue, {
fromBag = idealItem.bag,
fromSlot = idealItem.slot,
toBag = target.bag,
toSlot = target.slot,
retries = 0
})
-- Reflect the virtual swap in our model so later moves stay correct.
for j = 1, table.getn(items) do
if items[j].bag == target.bag and items[j].slot == target.slot then
items[j].bag = idealItem.bag
items[j].slot = idealItem.slot
break
end
end
idealItem.bag = target.bag
idealItem.slot = target.slot
end
end
end
end
function SFrames.Bags.Sort:ScanItems(bagOrder, ignoreLocked)
local items = {}
for _, bag in ipairs(bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local link = GetContainerItemLink(bag, slot)
local _, itemCount, locked = GetContainerItemInfo(bag, slot)
if locked and not ignoreLocked then
return nil, TEXT_LOCKED
end
if link then
local score, name = GetItemSortValue(link)
table.insert(items, {
bag = bag,
slot = slot,
link = link,
score = score,
name = name,
count = itemCount or 1,
stackKey = GetItemIdentity(link),
maxStack = GetItemMaxStack(link)
})
end
end
end
return items, nil
end
function SFrames.Bags.Sort:BuildStackMergeMoves(items)
local grouped = {}
local moves = {}
for _, item in ipairs(items) do
if item and item.stackKey and item.maxStack and item.maxStack > 1 and (item.count or 0) > 0 then
if not grouped[item.stackKey] then
grouped[item.stackKey] = {
maxStack = item.maxStack,
slots = {}
}
end
if item.maxStack > grouped[item.stackKey].maxStack then
grouped[item.stackKey].maxStack = item.maxStack
end
table.insert(grouped[item.stackKey].slots, {
bag = item.bag,
slot = item.slot,
count = item.count
})
end
end
for _, group in pairs(grouped) do
local slots = group.slots
if table.getn(slots) > 1 then
table.sort(slots, function(a, b)
if a.count ~= b.count then
return a.count > b.count
elseif a.bag ~= b.bag then
return a.bag < b.bag
else
return a.slot < b.slot
end
end)
local left = 1
local right = table.getn(slots)
while left < right do
local target = slots[left]
local source = slots[right]
if target.count >= group.maxStack then
left = left + 1
elseif source.count <= 0 then
right = right - 1
else
local transfer = math.min(group.maxStack - target.count, source.count)
if transfer > 0 then
table.insert(moves, {
fromBag = source.bag,
fromSlot = source.slot,
toBag = target.bag,
toSlot = target.slot,
transferCount = transfer,
isStackMove = true
})
target.count = target.count + transfer
source.count = source.count - transfer
end
if source.count <= 0 then
right = right - 1
end
if target.count >= group.maxStack then
left = left + 1
end
end
end
end
end
return moves
end
function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked)
if not activeBagOrder then
FinishSort()
return
end
local items, err = self:ScanItems(activeBagOrder, ignoreLocked)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
SortItemsByRule(items)
self:ExecuteSimpleSort(items, activeBagOrder)
if table.getn(sortQueue) == 0 then
FinishSort()
return
end
activePhase = "sort"
sortTimer:Show()
end
function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate)
if isSorting then return end
isSorting = true
sortQueue = {}
timeSinceLast = 0
activeCompleteMessage = completeMessage
activeCompleteUpdate = completeUpdate
activeBagOrder = bagOrder
activePhase = nil
local items, err = self:ScanItems(bagOrder, false)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
local stackMoves = self:BuildStackMergeMoves(items)
if table.getn(stackMoves) > 0 then
sortQueue = stackMoves
activePhase = "stack"
sortTimer:Show()
return
end
self:StartPlacementPhase(false)
end
function SFrames.Bags.Sort:Start()
if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then
SFrames:Print(TEXT_OFFLINE_BAGS)
return
end
self:StartForBags(
{0, 1, 2, 3, 4},
TEXT_BAG_DONE,
function()
if SFrames.Bags.Container then
SFrames.Bags.Container:UpdateLayout()
end
end
)
end
function SFrames.Bags.Sort:StartBank()
if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then
SFrames:Print(TEXT_OFFLINE_BANK)
return
end
self:StartForBags(
{-1, 5, 6, 7, 8, 9, 10, 11},
TEXT_BANK_DONE,
function()
if SFrames.Bags.Bank then
SFrames.Bags.Bank:UpdateLayout()
end
end
)
end
sortTimer:SetScript("OnUpdate", function()
if not isSorting then
this:Hide()
return
end
timeSinceLast = timeSinceLast + arg1
if timeSinceLast > sortDelay then
timeSinceLast = 0
if table.getn(sortQueue) > 0 then
local move = sortQueue[1]
local _, _, lockedFrom = GetContainerItemInfo(move.fromBag, move.fromSlot)
local _, _, lockedTo = GetContainerItemInfo(move.toBag, move.toSlot)
if lockedFrom or lockedTo or CursorHasItem() then
move.retries = (move.retries or 0) + 1
if move.retries > 40 then
table.remove(sortQueue, 1)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
end
end
return
end
table.remove(sortQueue, 1)
if move.isStackMove and move.transferCount then
SplitContainerItem(move.fromBag, move.fromSlot, move.transferCount)
PickupContainerItem(move.toBag, move.toSlot)
else
PickupContainerItem(move.fromBag, move.fromSlot)
PickupContainerItem(move.toBag, move.toSlot)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
if CursorHasItem() then
PickupContainerItem(move.toBag, move.toSlot)
end
end
end
else
if activePhase == "stack" then
if activeBagOrder then
for _, bag in ipairs(activeBagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then
return
end
end
end
end
-- After consolidation, rescan inventory and run normal sorting.
SFrames.Bags.Sort:StartPlacementPhase(true)
else
if CursorHasItem() then return end
FinishSort()
end
end
end
end)
sortTimer:Hide()

7
Bindings.xml Normal file
View File

@@ -0,0 +1,7 @@
<Bindings>
<Binding name="NANAMI_TOGGLE_NAV" header="NANAMI_UI" runOnUp="false">
if SFrames and SFrames.WorldMap and SFrames.WorldMap.ToggleNav then
SFrames.WorldMap:ToggleNav()
end
</Binding>
</Bindings>

375
BookUI.lua Normal file
View File

@@ -0,0 +1,375 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Book Reading UI (BookUI.lua)
-- Replaces ItemTextFrame with Nanami-UI styled interface + page flipping
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.BookUI = {}
local BUI = SFrames.BookUI
--------------------------------------------------------------------------------
-- Theme (match QuestUI style)
--------------------------------------------------------------------------------
local T = SFrames.ActiveTheme
--------------------------------------------------------------------------------
-- Layout
--------------------------------------------------------------------------------
local FRAME_W = 340
local FRAME_H = 400
local HEADER_H = 34
local BOTTOM_H = 42
local SIDE_PAD = 14
local CONTENT_W = FRAME_W - SIDE_PAD * 2
local SCROLL_STEP = 40
local BODY_FONT_SIZE = 12
local BODY_LINE_SPACING = 2
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
local MainFrame = nil
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
end
local function CreateShadow(parent)
local s = CreateFrame("Frame", nil, parent)
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.45)
s:SetBackdropBorderColor(0, 0, 0, 0.6)
s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1))
return s
end
local function TextHeight(fs, fallback)
if not fs then return fallback or 14 end
local h = fs.GetStringHeight and fs:GetStringHeight()
if h and h > 0 then return h end
h = fs:GetHeight()
if h and h > 1 then return h end
return fallback or 14
end
local function IsTrue(v)
return v == true or v == 1
end
local function CreateScrollArea(parent, name)
local scroll = CreateFrame("ScrollFrame", name, parent)
local content = CreateFrame("Frame", name .. "Content", scroll)
content:SetWidth(CONTENT_W)
content:SetHeight(1)
scroll:SetScrollChild(content)
scroll:EnableMouseWheel(true)
scroll:SetScript("OnMouseWheel", function()
local cur = this:GetVerticalScroll()
local maxVal = this:GetVerticalScrollRange()
if arg1 > 0 then
this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP))
else
this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP))
end
end)
scroll.content = content
return scroll
end
local function CreateActionBtn(parent, text, w)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(w or 90)
btn:SetHeight(28)
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), 11, "OUTLINE")
fs:SetPoint("CENTER", 0, 0)
fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
fs:SetText(text or "")
btn.label = fs
btn.disabled = false
function btn:SetDisabled(flag)
self.disabled = flag
if flag then
self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3])
self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5)
else
self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
end
end
btn:SetScript("OnEnter", function()
if not this.disabled then
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
end
end)
btn:SetScript("OnLeave", function()
if not this.disabled then
this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
end
end)
btn:SetScript("OnMouseDown", function()
if not this.disabled then
this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
end
end)
btn:SetScript("OnMouseUp", function()
if not this.disabled then
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end
end)
return btn
end
local function HideBlizzardItemText()
if not ItemTextFrame then return end
ItemTextFrame:SetAlpha(0)
ItemTextFrame:EnableMouse(false)
ItemTextFrame:ClearAllPoints()
ItemTextFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000)
end
local function UpdateBookContent()
if not MainFrame then return end
local title = ItemTextGetItem and (ItemTextGetItem() or "") or ""
local text = ItemTextGetText and (ItemTextGetText() or "") or ""
if (not text or text == "") and ItemTextPageText and ItemTextPageText.GetText then
text = ItemTextPageText:GetText() or ""
end
if text and text ~= "" then
-- Keep paragraph gap readable: collapse 3+ consecutive newlines.
text = string.gsub(text, "\n%s*\n%s*\n+", "\n\n")
end
local pageNum = ItemTextGetPage and tonumber(ItemTextGetPage()) or 1
if not pageNum or pageNum < 1 then pageNum = 1 end
MainFrame.titleFS:SetText(title)
MainFrame.pageFS:SetText("" .. pageNum .. "")
MainFrame.bodyFS:SetText(text)
MainFrame.bodyFS:ClearAllPoints()
MainFrame.bodyFS:SetPoint("TOPLEFT", MainFrame.scroll.content, "TOPLEFT", 2, -6)
MainFrame.scroll.content:SetHeight(TextHeight(MainFrame.bodyFS, 14) + 18)
MainFrame.scroll:SetVerticalScroll(0)
local hasPrevApi = ItemTextHasPrevPage and IsTrue(ItemTextHasPrevPage()) or false
local hasNextApi = ItemTextHasNextPage and IsTrue(ItemTextHasNextPage()) or false
local canPrev = (pageNum and pageNum > 1) or hasPrevApi
MainFrame.prevBtn:SetDisabled(not canPrev)
MainFrame.nextBtn:SetDisabled(not hasNextApi)
end
local function QueueRefresh(delay, count)
if not MainFrame then return end
MainFrame._refreshDelay = delay or 0.05
MainFrame._refreshCount = count or 1
end
local function CloseBookFrame()
if MainFrame and MainFrame:IsVisible() then
MainFrame:Hide()
end
if CloseItemText then
pcall(CloseItemText)
elseif ItemTextFrame and ItemTextFrame.Hide then
pcall(ItemTextFrame.Hide, ItemTextFrame)
end
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function BUI:Initialize()
if MainFrame then return end
MainFrame = CreateFrame("Frame", "SFramesBookFrame", UIParent)
MainFrame:SetWidth(FRAME_W)
MainFrame:SetHeight(FRAME_H)
MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0)
MainFrame:SetFrameStrata("HIGH")
MainFrame:SetToplevel(true)
MainFrame:EnableMouse(true)
MainFrame:SetMovable(true)
MainFrame:RegisterForDrag("LeftButton")
MainFrame:SetScript("OnDragStart", function() this:StartMoving() end)
MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
SetRoundBackdrop(MainFrame)
CreateShadow(MainFrame)
local header = CreateFrame("Frame", nil, MainFrame)
header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0)
header:SetHeight(HEADER_H)
local titleFS = header:CreateFontString(nil, "OVERLAY")
titleFS:SetFont(GetFont(), 14, "OUTLINE")
titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0)
titleFS:SetPoint("RIGHT", header, "RIGHT", -96, 0)
titleFS:SetJustifyH("LEFT")
titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
MainFrame.titleFS = titleFS
local pageFS = header:CreateFontString(nil, "OVERLAY")
pageFS:SetFont(GetFont(), 11, "OUTLINE")
pageFS:SetPoint("RIGHT", header, "RIGHT", -30, 0)
pageFS:SetJustifyH("RIGHT")
pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
MainFrame.pageFS = pageFS
local closeBtn = CreateFrame("Button", nil, MainFrame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 2, 2)
closeBtn:SetWidth(24); closeBtn:SetHeight(24)
local headerSep = MainFrame:CreateTexture(nil, "ARTWORK")
headerSep:SetTexture("Interface\\Buttons\\WHITE8X8")
headerSep:SetHeight(1)
headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H)
headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H)
headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
local contentArea = CreateFrame("Frame", nil, MainFrame)
contentArea:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4))
contentArea:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4)
MainFrame.contentArea = contentArea
local scroll = CreateScrollArea(contentArea, "SFramesBookScroll")
scroll:SetPoint("TOPLEFT", contentArea, "TOPLEFT", 0, 0)
scroll:SetPoint("BOTTOMRIGHT", contentArea, "BOTTOMRIGHT", 0, 0)
MainFrame.scroll = scroll
local bodyFS = scroll.content:CreateFontString(nil, "OVERLAY")
bodyFS:SetFont(GetFont(), BODY_FONT_SIZE)
if bodyFS.SetSpacing then
bodyFS:SetSpacing(BODY_LINE_SPACING)
end
bodyFS:SetWidth(CONTENT_W - 4)
bodyFS:SetJustifyH("LEFT")
bodyFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
MainFrame.bodyFS = bodyFS
local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK")
bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8")
bottomSep:SetHeight(1)
bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H)
bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H)
bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
MainFrame.prevBtn = CreateActionBtn(MainFrame, "上一页", 90)
MainFrame.prevBtn:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 8)
MainFrame.prevBtn:SetScript("OnClick", function()
if this.disabled then return end
if ItemTextPrevPage then
ItemTextPrevPage()
QueueRefresh(0.05, 5)
end
end)
MainFrame.closeBtn = CreateActionBtn(MainFrame, "关闭", 90)
MainFrame.closeBtn:SetPoint("BOTTOM", MainFrame, "BOTTOM", 0, 8)
MainFrame.closeBtn:SetScript("OnClick", function()
CloseBookFrame()
end)
MainFrame.nextBtn = CreateActionBtn(MainFrame, "下一页", 90)
MainFrame.nextBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8)
MainFrame.nextBtn:SetScript("OnClick", function()
if this.disabled then return end
if ItemTextNextPage then
ItemTextNextPage()
QueueRefresh(0.05, 5)
end
end)
MainFrame:SetScript("OnHide", function()
if ItemTextFrame and ItemTextFrame:IsVisible() then
pcall(ItemTextFrame.Hide, ItemTextFrame)
end
end)
MainFrame:SetScript("OnUpdate", function()
if not this._refreshCount or this._refreshCount <= 0 then return end
this._refreshDelay = (this._refreshDelay or 0) - (arg1 or 0)
if this._refreshDelay > 0 then return end
UpdateBookContent()
this._refreshCount = this._refreshCount - 1
if this._refreshCount > 0 then
this._refreshDelay = 0.08
end
end)
MainFrame:RegisterEvent("ITEM_TEXT_BEGIN")
MainFrame:RegisterEvent("ITEM_TEXT_READY")
MainFrame:RegisterEvent("ITEM_TEXT_CLOSED")
MainFrame:SetScript("OnEvent", function()
if event == "ITEM_TEXT_BEGIN" then
HideBlizzardItemText()
UpdateBookContent()
QueueRefresh(0.05, 5)
MainFrame:Show()
elseif event == "ITEM_TEXT_READY" then
HideBlizzardItemText()
UpdateBookContent()
QueueRefresh(0.05, 5)
if not MainFrame:IsVisible() then
MainFrame:Show()
end
elseif event == "ITEM_TEXT_CLOSED" then
MainFrame._refreshCount = 0
MainFrame:Hide()
end
end)
MainFrame:Hide()
tinsert(UISpecialFrames, "SFramesBookFrame")
end
--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------
local bootstrap = CreateFrame("Frame")
bootstrap:RegisterEvent("PLAYER_LOGIN")
bootstrap:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
BUI:Initialize()
end
end)

3320
CharacterPanel.lua Normal file

File diff suppressed because it is too large Load Diff

6897
Chat.lua Normal file

File diff suppressed because it is too large Load Diff

346
ClassSkillData.lua Normal file
View File

@@ -0,0 +1,346 @@
SFrames.ClassSkillData = {
WARRIOR = {
[4] = {"冲锋", "撕裂"},
[6] = {"雷霆一击"},
[8] = {"英勇打击 2级", "断筋"},
[10] = {"撕裂 2级", "血性狂暴"},
[12] = {"压制", "盾击", "战斗怒吼 2级"},
[14] = {"挫志怒吼", "复仇"},
[16] = {"英勇打击 3级", "惩戒痛击", "盾牌格挡"},
[18] = {"雷霆一击 2级", "缴械"},
[20] = {"撕裂 3级", "反击风暴", "顺劈斩"},
[22] = {"战斗怒吼 3级", "破甲攻击 2级", "破胆怒吼"},
[24] = {"英勇打击 4级", "挫志怒吼 2级", "复仇 2级", "斩杀"},
[26] = {"冲锋 2级", "惩戒痛击 2级", "挑战怒吼"},
[28] = {"雷霆一击 3级", "压制 2级", "盾墙"},
[30] = {"撕裂 4级", "顺劈斩 2级", "猛击", "狂暴姿态"},
[32] = {"英勇打击 5级", "断筋 2级", "斩杀 2级", "战斗怒吼 4级", "盾击 2级", "狂暴之怒"},
[34] = {"挫志怒吼 3级", "复仇 3级", "破甲攻击 3级"},
[36] = {"惩戒痛击 3级", "旋风斩"},
[38] = {"雷霆一击 4级", "猛击 2级"},
[40] = {"英勇打击 6级", "撕裂 5级", "顺劈斩 3级", "斩杀 3级"},
[42] = {"战斗怒吼 5级", "拦截 2级"},
[44] = {"压制 3级", "挫志怒吼 4级", "复仇 4级"},
[46] = {"冲锋 3级", "惩戒痛击 4级", "猛击 3级", "破甲攻击 4级"},
[48] = {"英勇打击 7级", "雷霆一击 5级", "斩杀 4级"},
[50] = {"撕裂 6级", "鲁莽", "顺劈斩 4级"},
[52] = {"战斗怒吼 6级", "拦截 3级", "盾击 3级"},
[54] = {"断筋 3级", "挫志怒吼 5级", "猛击 4级", "复仇 5级"},
[56] = {"英勇打击 8级", "惩戒痛击 5级", "斩杀 5级"},
[58] = {"雷霆一击 6级", "破甲攻击 5级"},
[60] = {"撕裂 7级", "压制 4级", "顺劈斩 5级"},
},
PALADIN = {
[4] = {"力量祝福", "审判"},
[6] = {"圣光术 2级", "圣佑术", "十字军圣印"},
[8] = {"纯净术", "制裁之锤"},
[10] = {"圣疗术", "正义圣印 2级", "虔诚光环 2级", "保护祝福"},
[12] = {"力量祝福 2级", "十字军圣印 2级"},
[14] = {"圣光术 3级"},
[16] = {"正义之怒", "惩罚光环"},
[18] = {"正义圣印 3级", "圣佑术 2级"},
[20] = {"驱邪术", "圣光闪现", "虔诚光环 3级"},
[22] = {"圣光术 4级", "专注光环", "公正圣印", "力量祝福 3级", "十字军圣印 3级"},
[24] = {"超度亡灵", "救赎 2级", "智慧祝福 2级", "制裁之锤 2级", "保护祝福 2级"},
[26] = {"圣光闪现 2级", "正义圣印 4级", "拯救祝福", "惩罚光环 2级"},
[28] = {"驱邪术 2级"},
[30] = {"圣疗术 2级", "圣光术 5级", "光明圣印", "虔诚光环 4级", "神圣干涉"},
[32] = {"冰霜抗性光环", "力量祝福 4级", "十字军圣印 4级"},
[34] = {"智慧祝福 3级", "圣光闪现 3级", "正义圣印 5级", "圣盾术"},
[36] = {"驱邪术 3级", "救赎 3级", "火焰抗性光环", "惩罚光环 3级"},
[38] = {"圣光术 6级", "超度亡灵 2级", "智慧圣印", "保护祝福 3级"},
[40] = {"光明祝福", "光明圣印 2级", "虔诚光环 5级", "制裁之锤 3级"},
[42] = {"圣光闪现 4级", "正义圣印 6级", "力量祝福 5级", "十字军圣印 5级"},
[44] = {"驱邪术 4级", "智慧祝福 4级", "冰霜抗性光环 2级"},
[46] = {"圣光术 7级", "惩罚光环 4级"},
[48] = {"救赎 4级", "智慧圣印 2级", "火焰抗性光环 2级"},
[50] = {"圣疗术 3级", "圣光闪现 5级", "光明祝福 2级", "光明圣印 3级", "正义圣印 7级", "虔诚光环 6级", "圣盾术 2级"},
[52] = {"驱邪术 5级", "超度亡灵 3级", "力量祝福 6级", "十字军圣印 6级", "强效力量祝福"},
[54] = {"圣光术 8级", "智慧祝福 5级", "强效智慧祝福", "制裁之锤 4级"},
[56] = {"冰霜抗性光环 3级", "惩罚光环 5级"},
[58] = {"圣光闪现 6级", "智慧圣印 3级", "正义圣印 8级"},
[60] = {"驱邪术 6级", "救赎 5级", "光明祝福 3级", "光明圣印 4级", "强效光明祝福", "虔诚光环 7级", "火焰抗性光环 3级", "强效力量祝福 2级"},
},
HUNTER = {
[4] = {"灵猴守护", "毒蛇钉刺"},
[6] = {"猎人印记", "奥术射击"},
[8] = {"震荡射击", "猛禽一击 2级"},
[10] = {"雄鹰守护", "毒蛇钉刺 2级", "持久耐力", "自然护甲", "追踪人型生物"},
[12] = {"治疗宠物", "奥术射击 2级", "扰乱射击", "摔绊"},
[14] = {"野兽之眼", "恐吓野兽", "鹰眼术"},
[16] = {"猛禽一击 3级", "献祭陷阱", "猫鼬撕咬"},
[18] = {"雄鹰守护 2级", "毒蛇钉刺 3级", "追踪亡灵", "多重射击"},
[20] = {"治疗宠物 2级", "猎豹守护", "奥术射击 3级", "逃脱", "冰冻陷阱", "猛禽一击 4级"},
[22] = {"猎人印记 2级", "毒蝎钉刺"},
[24] = {"野兽知识", "追踪隐藏生物"},
[26] = {"毒蛇钉刺 4级", "急速射击", "追踪元素生物", "献祭陷阱 2级"},
[28] = {"治疗宠物 3级", "雄鹰守护 3级", "奥术射击 4级", "冰霜陷阱"},
[30] = {"恐吓野兽 2级", "野兽守护", "多重射击 2级", "猫鼬撕咬 2级", "假死"},
[32] = {"照明弹", "爆炸陷阱", "追踪恶魔", "猛禽一击 5级"},
[34] = {"毒蛇钉刺 5级", "逃脱 2级"},
[36] = {"治疗宠物 4级", "蝰蛇钉刺", "献祭陷阱 3级"},
[38] = {"雄鹰守护 4级"},
[40] = {"豹群守护", "猎人印记 3级", "乱射", "扰乱射击 4级", "冰冻陷阱 2级", "猛禽一击 6级", "追踪巨人"},
[42] = {"毒蛇钉刺 6级", "多重射击 3级"},
[44] = {"治疗宠物 5级", "奥术射击 6级", "爆炸陷阱 2级", "献祭陷阱 4级", "猫鼬撕咬 3级"},
[46] = {"恐吓野兽 3级", "蝰蛇钉刺 2级"},
[48] = {"雄鹰守护 5级", "猛禽一击 7级", "逃脱 3级"},
[50] = {"毒蛇钉刺 7级", "乱射 2级", "追踪龙类"},
[52] = {"治疗宠物 6级", "毒蝎钉刺 4级"},
[54] = {"多重射击 4级", "爆炸陷阱 3级", "猫鼬撕咬 4级", "猛禽一击 8级"},
[56] = {"蝰蛇钉刺 3级", "献祭陷阱 5级"},
[58] = {"猎人印记 4级", "乱射 3级", "毒蛇钉刺 8级", "雄鹰守护 6级"},
[60] = {"治疗宠物 7级", "奥术射击 8级", "扰乱射击 6级", "冰冻陷阱 3级", "摔绊 3级"},
},
ROGUE = {
[4] = {"背刺", "搜索"},
[6] = {"邪恶攻击 2级", "凿击"},
[8] = {"刺骨 2级", "闪避"},
[10] = {"切割", "疾跑", "闷棍"},
[12] = {"背刺 2级", "脚踢"},
[14] = {"绞喉", "破甲", "邪恶攻击 3级"},
[16] = {"刺骨 3级", "佯攻"},
[18] = {"凿击 2级", "伏击"},
[20] = {"割裂", "背刺 3级", "潜行 2级", "致残毒药"},
[22] = {"绞喉 2级", "邪恶攻击 4级", "扰乱", "消失"},
[24] = {"刺骨 4级", "麻痹毒药", "侦测陷阱"},
[26] = {"偷袭", "破甲 2级", "伏击 2级", "脚踢 2级"},
[28] = {"割裂 2级", "背刺 4级", "佯攻 2级", "闷棍 2级"},
[30] = {"绞喉 3级", "邪恶攻击 5级", "肾击", "致命毒药"},
[32] = {"凿击 3级", "致伤毒药"},
[34] = {"疾跑 2级"},
[36] = {"割裂 3级", "破甲 3级"},
[38] = {"绞喉 4级", "致命毒药 2级", "麻痹毒药 2级"},
[40] = {"邪恶攻击 6级", "佯攻 3级", "潜行 3级", "安全降落", "致伤毒药 2级", "消失 2级"},
[42] = {"切割 2级"},
[44] = {"割裂 4级", "背刺 6级"},
[46] = {"绞喉 5级", "破甲 4级", "致命毒药 3级"},
[48] = {"刺骨 7级", "凿击 4级", "闷棍 3级", "致伤毒药 3级"},
[50] = {"肾击 2级", "邪恶攻击 7级", "伏击 5级", "致残毒药 2级"},
[52] = {"割裂 5级", "背刺 7级", "麻痹毒药 3级"},
[54] = {"绞喉 6级", "邪恶攻击 8级", "致命毒药 4级"},
[56] = {"刺骨 8级", "破甲 5级", "致伤毒药 4级"},
[58] = {"脚踢 4级", "疾跑 3级"},
[60] = {"割裂 6级", "凿击 5级", "佯攻 4级", "背刺 8级", "潜行 4级"},
},
PRIEST = {
[4] = {"暗言术:痛", "次级治疗术 2级"},
[6] = {"真言术:盾", "惩击 2级"},
[8] = {"恢复", "渐隐术"},
[10] = {"暗言术:痛 2级", "心灵震爆", "复活术"},
[12] = {"真言术:盾 2级", "心灵之火", "真言术:韧 2级", "祛病术"},
[14] = {"恢复 2级", "心灵尖啸"},
[16] = {"治疗术", "心灵震爆 2级"},
[18] = {"真言术:盾 3级", "驱散魔法", "暗言术:痛 3级"},
[20] = {"心灵之火 2级", "束缚亡灵", "快速治疗", "安抚心灵", "渐隐术 2级", "神圣之火"},
[22] = {"惩击 4级", "心灵视界", "复活术 2级", "心灵震爆 3级"},
[24] = {"真言术:盾 4级", "真言术:韧 3级", "法力燃烧", "神圣之火 2级"},
[26] = {"恢复 4级", "暗言术:痛 4级"},
[28] = {"治疗术 3级", "心灵震爆 4级", "心灵尖啸 2级"},
[30] = {"真言术:盾 5级", "心灵之火 3级", "治疗祷言", "束缚亡灵 2级", "精神控制", "防护暗影", "渐隐术 3级"},
[32] = {"法力燃烧 2级", "恢复 5级", "快速治疗 3级"},
[34] = {"漂浮术", "暗言术:痛 5级", "心灵震爆 5级", "复活术 3级", "治疗术 4级"},
[36] = {"真言术:盾 6级", "驱散魔法 2级", "真言术:韧 4级", "心灵之火 4级", "恢复 6级", "惩击 6级"},
[38] = {"安抚心灵 2级"},
[40] = {"法力燃烧 3级", "治疗祷言 2级", "防护暗影 2级", "心灵震爆 6级", "渐隐术 4级"},
[42] = {"真言术:盾 7级", "神圣之火 5级", "心灵尖啸 3级"},
[44] = {"恢复 7级", "精神控制 2级"},
[46] = {"惩击 7级", "强效治疗术 2级", "心灵震爆 7级", "复活术 4级"},
[48] = {"真言术:盾 8级", "真言术:韧 5级", "法力燃烧 4级", "神圣之火 6级", "恢复 8级", "暗言术:痛 7级"},
[50] = {"心灵之火 5级", "治疗祷言 3级"},
[52] = {"强效治疗术 3级", "心灵震爆 8级", "安抚心灵 3级"},
[54] = {"真言术:盾 9级", "神圣之火 7级", "惩击 8级"},
[56] = {"法力燃烧 5级", "恢复 9级", "防护暗影 3级", "心灵尖啸 4级", "暗言术:痛 8级"},
[58] = {"复活术 5级", "强效治疗术 4级", "心灵震爆 9级"},
[60] = {"真言术:盾 10级", "心灵之火 6级", "真言术:韧 6级", "束缚亡灵 3级", "治疗祷言 4级", "渐隐术 6级"},
},
SHAMAN = {
[4] = {"地震术"},
[6] = {"治疗波 2级", "地缚图腾"},
[8] = {"闪电箭 2级", "石爪图腾", "地震术 2级", "闪电之盾"},
[10] = {"烈焰震击", "火舌武器", "大地之力图腾"},
[12] = {"净化术", "火焰新星图腾", "先祖之魂", "治疗波 3级"},
[14] = {"闪电箭 3级", "地震术 3级"},
[16] = {"闪电之盾 2级", "消毒术"},
[18] = {"烈焰震击 2级", "火舌武器 2级", "石爪图腾 2级", "治疗波 4级", "战栗图腾"},
[20] = {"闪电箭 4级", "冰霜震击", "幽魂之狼", "次级治疗波"},
[22] = {"火焰新星图腾 2级", "水下呼吸", "祛病术"},
[24] = {"净化术 2级", "地震术 4级", "大地之力图腾 2级", "闪电之盾 3级", "先祖之魂 2级"},
[26] = {"闪电箭 5级", "熔岩图腾", "火舌武器 3级", "视界术", "法力之泉图腾"},
[28] = {"石爪图腾 3级", "烈焰震击 3级", "火舌图腾", "水上行走", "次级治疗波 2级"},
[30] = {"星界传送", "根基图腾", "风怒武器", "治疗之泉图腾"},
[32] = {"闪电箭 6级", "火焰新星图腾 3级", "闪电之盾 4级", "治疗波 6级", "闪电链", "风怒图腾"},
[34] = {"冰霜震击 2级", "岗哨图腾"},
[36] = {"地震术 5级", "熔岩图腾 2级", "火舌武器 4级", "法力之泉图腾 2级", "次级治疗波 3级", "风墙图腾"},
[38] = {"石爪图腾 4级", "大地之力图腾 3级", "火舌图腾 2级"},
[40] = {"闪电箭 8级", "闪电链 2级", "烈焰震击 4级", "治疗波 7级", "治疗链", "治疗之泉图腾 3级", "风怒武器 2级"},
[42] = {"火焰新星图腾 4级"},
[44] = {"闪电之盾 6级", "冰霜震击 3级", "熔岩图腾 3级", "风墙图腾 2级"},
[46] = {"火舌武器 5级", "治疗链 2级"},
[48] = {"地震术 6级", "石爪图腾 5级", "火舌图腾 3级", "治疗波 8级"},
[50] = {"闪电箭 9级", "治疗之泉图腾 4级", "风怒武器 3级"},
[52] = {"烈焰震击 5级", "大地之力图腾 4级", "次级治疗波 5级"},
[54] = {"闪电箭 10级"},
[56] = {"闪电链 4级", "熔岩图腾 4级", "火舌图腾 4级", "风墙图腾 3级", "治疗波 9级", "法力之泉图腾 4级"},
[58] = {"冰霜震击 4级"},
[60] = {"风怒武器 4级", "次级治疗波 6级", "治疗之泉图腾 5级"},
},
MAGE = {
[4] = {"造水术", "寒冰箭"},
[6] = {"造食术", "火球术 2级", "火焰冲击"},
[8] = {"变形术", "奥术飞弹"},
[10] = {"霜甲术 2级", "冰霜新星"},
[12] = {"缓落术", "造食术 2级", "火球术 3级"},
[14] = {"魔爆术", "奥术智慧 2级", "火焰冲击 2级"},
[16] = {"侦测魔法", "烈焰风暴"},
[18] = {"解除次级诅咒", "魔法增效", "火球术 4级"},
[20] = {"变形术 2级", "法力护盾", "闪现术", "霜甲术 3级", "暴风雪", "唤醒"},
[22] = {"造食术 3级", "魔爆术 2级", "火焰冲击 3级", "灼烧"},
[24] = {"火球术 5级", "烈焰风暴 2级", "法术反制"},
[26] = {"寒冰箭 5级", "冰锥术"},
[28] = {"奥术智慧 3级", "法力护盾 2级", "暴风雪 2级", "灼烧 2级", "冰霜新星 2级"},
[30] = {"魔爆术 3级", "火球术 6级", "冰甲术"},
[32] = {"造食术 4级", "烈焰风暴 3级", "寒冰箭 6级"},
[34] = {"魔甲术", "冰锥术 2级", "灼烧 3级"},
[36] = {"法力护盾 3级", "火球术 7级", "暴风雪 3级", "冰霜新星 3级"},
[38] = {"魔爆术 4级", "寒冰箭 7级", "火焰冲击 5级"},
[40] = {"造食术 5级", "奥术飞弹 5级", "火球术 8级", "冰甲术 2级", "灼烧 4级"},
[42] = {"奥术智慧 4级"},
[44] = {"法力护盾 4级", "暴风雪 4级", "寒冰箭 8级"},
[46] = {"魔爆术 5级", "灼烧 5级"},
[48] = {"火球术 9级", "奥术飞弹 6级", "烈焰风暴 5级"},
[50] = {"造水术 6级", "寒冰箭 9级", "冰锥术 4级", "冰甲术 3级"},
[52] = {"法力护盾 5级", "火球术 10级", "火焰冲击 7级", "冰霜新星 4级"},
[54] = {"魔法增效 4级", "奥术飞弹 7级", "烈焰风暴 6级"},
[56] = {"奥术智慧 5级", "寒冰箭 10级", "冰锥术 5级"},
[58] = {"魔甲术 3级", "灼烧 7级"},
[60] = {"变形术 4级", "法力护盾 6级", "火球术 11级", "暴风雪 6级", "冰甲术 4级"},
},
WARLOCK = {
[2] = {"痛苦诅咒", "恐惧术"},
[4] = {"腐蚀术", "虚弱诅咒"},
[6] = {"暗影箭 3级"},
[8] = {"痛苦诅咒 2级"},
[10] = {"吸取灵魂", "献祭 2级", "恶魔皮肤 2级", "制造初级治疗石"},
[12] = {"生命分流", "生命通道", "魔息术"},
[14] = {"腐蚀术 2级", "吸取生命", "鲁莽诅咒"},
[16] = {"生命分流 2级"},
[18] = {"痛苦诅咒 3级", "灼热之痛"},
[20] = {"献祭 3级", "生命通道 2级", "暗影箭 4级", "魔甲术", "火焰之雨"},
[22] = {"吸取生命 2级", "虚弱诅咒 3级", "基尔罗格之眼"},
[24] = {"腐蚀术 3级", "吸取灵魂 2级", "吸取法力", "感知恶魔"},
[26] = {"生命分流 3级", "语言诅咒"},
[28] = {"鲁莽诅咒 2级", "痛苦诅咒 4级", "生命通道 3级", "放逐术"},
[30] = {"吸取生命 3级", "献祭 4级", "奴役恶魔", "地狱烈焰", "魔甲术 2级"},
[32] = {"虚弱诅咒 4级", "恐惧术 2级", "元素诅咒", "防护暗影结界"},
[34] = {"生命分流 4级", "吸取法力 2级", "火焰之雨 2级", "灼热之痛 3级"},
[36] = {"生命通道 4级"},
[38] = {"吸取灵魂 3级", "痛苦诅咒 5级"},
[40] = {"恐惧嚎叫", "献祭 5级", "奴役恶魔 2级"},
[42] = {"虚弱诅咒 5级", "鲁莽诅咒 3级", "死亡缠绕", "防护暗影结界 2级", "地狱烈焰 2级", "灼热之痛 4级"},
[44] = {"吸取生命 5级", "生命通道 5级", "暗影箭 7级"},
[46] = {"生命分流 5级", "火焰之雨 3级"},
[48] = {"痛苦诅咒 6级", "放逐术 2级", "灵魂之火"},
[50] = {"虚弱诅咒 6级", "死亡缠绕 2级", "恐惧嚎叫 2级", "魔甲术 4级", "吸取灵魂 4级", "吸取法力 4级", "暗影箭 8级", "灼热之痛 5级"},
[52] = {"防护暗影结界 3级", "生命通道 6级"},
[54] = {"腐蚀术 6级", "吸取生命 6级", "地狱烈焰 3级", "灵魂之火 2级"},
[56] = {"鲁莽诅咒 4级", "死亡缠绕 3级"},
[58] = {"痛苦诅咒 7级", "奴役恶魔 3级", "火焰之雨 4级", "灼热之痛 6级"},
[60] = {"厄运诅咒", "元素诅咒 3级", "魔甲术 5级", "暗影箭 9级"},
},
DRUID = {
[4] = {"月火术", "回春术"},
[6] = {"荆棘术", "愤怒 2级"},
[8] = {"纠缠根须", "治疗之触 2级"},
[10] = {"月火术 2级", "回春术 2级", "挫志咆哮", "野性印记 2级"},
[12] = {"愈合", "狂怒"},
[14] = {"荆棘术 2级", "愤怒 3级", "重击"},
[16] = {"月火术 3级", "回春术 3级", "挥击"},
[18] = {"精灵之火", "休眠", "愈合 2级"},
[20] = {"纠缠根须 2级", "星火术", "猎豹形态", "撕扯", "爪击", "治疗之触 4级", "潜行", "野性印记 3级", "复生"},
[22] = {"愤怒 4级", "撕碎", "安抚动物"},
[24] = {"荆棘术 3级", "挥击 2级", "扫击", "猛虎之怒", "解除诅咒"},
[26] = {"星火术 2级", "月火术 5级", "爪击 2级", "治疗之触 5级", "驱毒术"},
[28] = {"撕扯 2级", "挑战咆哮", "畏缩"},
[30] = {"精灵之火 2级", "星火术 3级", "愤怒 5级", "旅行形态", "撕碎 2级", "重击 2级", "野性印记 4级", "宁静", "复生 2级"},
[32] = {"挫志咆哮 3级", "挥击 3级", "毁灭", "治疗之触 6级", "凶猛撕咬"},
[34] = {"荆棘术 4级", "月火术 6级", "回春术 6级", "扫击 2级", "爪击 3级"},
[36] = {"愤怒 6级", "突袭", "狂暴回复"},
[38] = {"纠缠根须 4级", "休眠 2级", "安抚动物 2级", "撕碎 3级"},
[40] = {"星火术 4级", "飓风", "挥击 4级", "潜行 2级", "畏缩 2级", "巨熊形态", "凶猛撕咬 2级", "回春术 7级", "宁静 2级", "复生 3级", "激活"},
[42] = {"挫志咆哮 4级", "毁灭 2级"},
[44] = {"荆棘术 5级", "树皮术", "撕扯 4级", "扫击 3级", "治疗之触 8级"},
[46] = {"愤怒 7级", "重击 3级", "突袭 2级"},
[48] = {"纠缠根须 5级", "月火术 8级", "撕碎 4级"},
[50] = {"星火术 5级", "宁静 3级", "复生 4级"},
[52] = {"挫志咆哮 5级", "撕扯 5级", "畏缩 3级", "凶猛撕咬 4级", "回春术 9级"},
[54] = {"荆棘术 6级", "愤怒 8级", "月火术 9级", "挥击 5级", "扫击 4级", "爪击 4级"},
[56] = {"治疗之触 10级"},
[58] = {"纠缠根须 6级", "星火术 6级", "月火术 10级", "爪击 5级", "毁灭 4级", "回春术 10级"},
[60] = {"飓风 3级", "潜行 3级", "猛虎之怒 4级", "撕扯 6级", "宁静 4级", "复生 5级", "野性印记 7级", "愈合 9级"},
},
}
SFrames.TalentTrainerSkills = {
WARRIOR = {
[48] = {{"致死打击 2级", "致死打击"}, {"嗜血 2级", "嗜血"}, {"盾牌猛击 2级", "盾牌猛击"}},
[54] = {{"致死打击 3级", "致死打击"}, {"嗜血 3级", "嗜血"}, {"盾牌猛击 3级", "盾牌猛击"}},
[60] = {{"致死打击 4级", "致死打击"}, {"嗜血 4级", "嗜血"}, {"盾牌猛击 4级", "盾牌猛击"}},
},
PALADIN = {
[48] = {{"神圣震击 2级", "神圣震击"}},
[56] = {{"神圣震击 3级", "神圣震击"}},
},
HUNTER = {
[28] = {{"瞄准射击 2级", "瞄准射击"}},
[36] = {{"瞄准射击 3级", "瞄准射击"}},
[44] = {{"瞄准射击 4级", "瞄准射击"}},
[52] = {{"瞄准射击 5级", "瞄准射击"}},
[60] = {{"瞄准射击 6级", "瞄准射击"}},
},
ROGUE = {
[46] = {{"出血 2级", "出血"}},
[58] = {{"出血 3级", "出血"}},
},
PRIEST = {
[28] = {{"精神鞭笞 2级", "精神鞭笞"}},
[36] = {{"精神鞭笞 3级", "精神鞭笞"}},
[44] = {{"精神鞭笞 4级", "精神鞭笞"}},
[52] = {{"精神鞭笞 5级", "精神鞭笞"}},
[60] = {{"精神鞭笞 6级", "精神鞭笞"}},
},
MAGE = {
[24] = {{"炎爆术 2级", "炎爆术"}},
[30] = {{"炎爆术 3级", "炎爆术"}},
[36] = {{"炎爆术 4级", "炎爆术"}},
[42] = {{"炎爆术 5级", "炎爆术"}},
[48] = {{"炎爆术 6级", "炎爆术"}, {"冲击波 2级", "冲击波"}, {"寒冰屏障 2级", "寒冰屏障"}},
[54] = {{"炎爆术 7级", "炎爆术"}},
[56] = {{"冲击波 3级", "冲击波"}, {"寒冰屏障 3级", "寒冰屏障"}},
[60] = {{"炎爆术 8级", "炎爆术"}, {"冲击波 4级", "冲击波"}, {"寒冰屏障 4级", "寒冰屏障"}},
},
WARLOCK = {
[38] = {{"生命虹吸 2级", "生命虹吸"}},
[48] = {{"生命虹吸 3级", "生命虹吸"}},
[50] = {{"黑暗契约 2级", "黑暗契约"}},
[58] = {{"生命虹吸 4级", "生命虹吸"}},
[60] = {{"黑暗契约 3级", "黑暗契约"}},
},
DRUID = {
[30] = {{"虫群 2级", "虫群"}},
[40] = {{"虫群 3级", "虫群"}},
[50] = {{"虫群 4级", "虫群"}},
[60] = {{"虫群 5级", "虫群"}},
},
}
SFrames.ClassMountQuests = {
WARLOCK = {
[40] = "职业坐骑任务:召唤恶马",
[60] = "史诗坐骑任务:召唤恐惧战马",
},
PALADIN = {
[40] = "职业坐骑任务:召唤战马",
[60] = "史诗坐骑任务:召唤战驹",
},
}

316
Config.lua Normal file
View File

@@ -0,0 +1,316 @@
SFrames.Config = {
-- Default Settings
width = 220,
height = 50,
portraitWidth = 50,
castbarHeight = 18,
colors = {
backdrop = { r = 0.15, g = 0.10, b = 0.15, a = 0.8 }, -- Pinkish dark tint
border = { r = 1.0, g = 0.5, b = 0.8, a = 1 }, -- Cute pink border
power = {
[0] = { r = 0.0, g = 0.0, b = 1.0 }, -- Mana
[1] = { r = 1.0, g = 0.0, b = 0.0 }, -- Rage
[2] = { r = 1.0, g = 0.5, b = 0.0 }, -- Focus
[3] = { r = 1.0, g = 1.0, b = 0.0 }, -- Energy
[4] = { r = 0.0, g = 1.0, b = 1.0 }, -- Happiness
},
class = {
["WARRIOR"] = { r = 0.78, g = 0.61, b = 0.43 },
["MAGE"] = { r = 0.41, g = 0.8, b = 0.94 },
["ROGUE"] = { r = 1.0, g = 0.96, b = 0.41 },
["DRUID"] = { r = 1.0, g = 0.49, b = 0.04 },
["HUNTER"] = { r = 0.67, g = 0.83, b = 0.45 },
["SHAMAN"] = { r = 0.14, g = 0.35, b = 1.0 },
["PRIEST"] = { r = 1.0, g = 1.0, b = 1.0 },
["WARLOCK"] = { r = 0.58, g = 0.51, b = 0.79 },
["PALADIN"] = { r = 0.96, g = 0.55, b = 0.73 },
}
}
}
--------------------------------------------------------------------------------
-- Theme Engine
--------------------------------------------------------------------------------
SFrames.Theme = {}
SFrames.ActiveTheme = {}
local function HSVtoRGB(h, s, v)
if s <= 0 then return v, v, v end
h = h - math.floor(h / 360) * 360
local hh = h / 60
local i = math.floor(hh)
local f = hh - i
local p = v * (1 - s)
local q = v * (1 - s * f)
local t = v * (1 - s * (1 - f))
if i == 0 then return v, t, p
elseif i == 1 then return q, v, p
elseif i == 2 then return p, v, t
elseif i == 3 then return p, q, v
elseif i == 4 then return t, p, v
else return v, p, q end
end
local function toHexChar(n)
if n < 10 then return string.char(48 + n) end
return string.char(97 + n - 10)
end
local function RGBtoHex(r, g, b)
local rr = math.floor(r * 255 + 0.5)
local gg = math.floor(g * 255 + 0.5)
local bb = math.floor(b * 255 + 0.5)
return "ff"
.. toHexChar(math.floor(rr / 16)) .. toHexChar(rr - math.floor(rr / 16) * 16)
.. toHexChar(math.floor(gg / 16)) .. toHexChar(gg - math.floor(gg / 16) * 16)
.. toHexChar(math.floor(bb / 16)) .. toHexChar(bb - math.floor(bb / 16) * 16)
end
SFrames.Theme.Presets = {}
SFrames.Theme.Presets["pink"] = { name = "樱粉", hue = 330, satMul = 1.00 }
SFrames.Theme.Presets["frost"] = { name = "霜蓝", hue = 210, satMul = 1.00 }
SFrames.Theme.Presets["emerald"] = { name = "翠绿", hue = 140, satMul = 0.85 }
SFrames.Theme.Presets["flame"] = { name = "炎橙", hue = 25, satMul = 0.90 }
SFrames.Theme.Presets["shadow"] = { name = "暗紫", hue = 270, satMul = 0.90 }
SFrames.Theme.Presets["golden"] = { name = "金辉", hue = 45, satMul = 0.80 }
SFrames.Theme.Presets["teal"] = { name = "碧青", hue = 175, satMul = 0.85 }
SFrames.Theme.Presets["crimson"] = { name = "绯红", hue = 5, satMul = 1.00 }
SFrames.Theme.Presets["holy"] = { name = "圣光", hue = 220, satMul = 0.15 }
SFrames.Theme.PresetOrder = { "pink", "frost", "emerald", "flame", "shadow", "golden", "teal", "crimson", "holy" }
SFrames.Theme.Presets["c_warrior"] = { name = "战士", hue = 31, satMul = 0.90, swatchRGB = {0.78, 0.61, 0.43} }
SFrames.Theme.Presets["c_paladin"] = { name = "圣骑士", hue = 334, satMul = 0.50, swatchRGB = {0.96, 0.55, 0.73} }
SFrames.Theme.Presets["c_hunter"] = { name = "猎人", hue = 85, satMul = 0.55, swatchRGB = {0.67, 0.83, 0.45} }
SFrames.Theme.Presets["c_rogue"] = { name = "潜行者", hue = 56, satMul = 0.70, swatchRGB = {1.00, 0.96, 0.41} }
SFrames.Theme.Presets["c_priest"] = { name = "牧师", hue = 40, satMul = 0.06, swatchRGB = {1.00, 1.00, 1.00} }
SFrames.Theme.Presets["c_shaman"] = { name = "萨满", hue = 225, satMul = 0.95, swatchRGB = {0.14, 0.35, 1.00} }
SFrames.Theme.Presets["c_mage"] = { name = "法师", hue = 196, satMul = 0.65, swatchRGB = {0.41, 0.80, 0.94} }
SFrames.Theme.Presets["c_warlock"] = { name = "术士", hue = 255, satMul = 0.45, swatchRGB = {0.58, 0.51, 0.79} }
SFrames.Theme.Presets["c_druid"] = { name = "德鲁伊", hue = 28, satMul = 1.00, swatchRGB = {1.00, 0.49, 0.04} }
SFrames.Theme.ClassPresetOrder = { "c_warrior", "c_paladin", "c_hunter", "c_rogue", "c_priest", "c_shaman", "c_mage", "c_warlock", "c_druid" }
SFrames.Theme.ClassMap = {}
SFrames.Theme.ClassMap["WARRIOR"] = "c_warrior"
SFrames.Theme.ClassMap["PALADIN"] = "c_paladin"
SFrames.Theme.ClassMap["HUNTER"] = "c_hunter"
SFrames.Theme.ClassMap["ROGUE"] = "c_rogue"
SFrames.Theme.ClassMap["PRIEST"] = "c_priest"
SFrames.Theme.ClassMap["SHAMAN"] = "c_shaman"
SFrames.Theme.ClassMap["MAGE"] = "c_mage"
SFrames.Theme.ClassMap["WARLOCK"] = "c_warlock"
SFrames.Theme.ClassMap["DRUID"] = "c_druid"
local function GenerateTheme(H, satMul)
satMul = satMul or 1.0
local function S(s)
local v = s * satMul
if v > 1 then v = 1 end
return v
end
local function C3(s, v)
local r, g, b = HSVtoRGB(H, S(s), v)
return { r, g, b }
end
local function C4(s, v, a)
local r, g, b = HSVtoRGB(H, S(s), v)
return { r, g, b, a }
end
local t = {}
t.accent = C4(0.40, 0.80, 0.98)
t.accentDark = C3(0.45, 0.55)
t.accentLight = C3(0.30, 1.00)
t.accentHex = RGBtoHex(t.accentLight[1], t.accentLight[2], t.accentLight[3])
t.panelBg = C4(0.50, 0.12, 0.95)
t.panelBorder = C4(0.45, 0.55, 0.90)
t.headerBg = C4(0.60, 0.10, 0.98)
t.sectionBg = C4(0.43, 0.14, 0.82)
t.sectionBorder = C4(0.38, 0.45, 0.86)
t.bg = t.panelBg
t.border = t.panelBorder
t.slotBg = C4(0.20, 0.07, 0.90)
t.slotBorder = C4(0.10, 0.28, 0.80)
t.slotHover = C4(0.38, 0.40, 0.90)
t.slotSelected = C4(0.43, 0.70, 1.00)
t.buttonBg = C4(0.44, 0.18, 0.94)
t.buttonBorder = C4(0.40, 0.50, 0.90)
t.buttonHoverBg = C4(0.47, 0.30, 0.96)
t.buttonDownBg = C4(0.50, 0.14, 0.96)
t.buttonDisabledBg = C4(0.43, 0.14, 0.65)
t.buttonActiveBg = C4(0.52, 0.42, 0.98)
t.buttonActiveBorder = C4(0.42, 0.90, 1.00)
t.buttonText = C3(0.16, 0.90)
t.buttonActiveText = C3(0.08, 1.00)
t.buttonDisabledText = C4(0.14, 0.55, 0.68)
t.btnBg = t.buttonBg
t.btnBorder = t.buttonBorder
t.btnHoverBg = t.buttonHoverBg
t.btnHoverBd = C4(0.40, 0.80, 0.98)
t.btnDownBg = t.buttonDownBg
t.btnText = t.buttonText
t.btnActiveText = t.buttonActiveText
t.btnDisabledText = C3(0.14, 0.40)
t.btnHover = C4(0.47, 0.30, 0.95)
t.btnHoverBorder = t.btnHoverBd
t.tabBg = t.buttonBg
t.tabBorder = t.buttonBorder
t.tabActiveBg = C4(0.50, 0.32, 0.96)
t.tabActiveBorder = C4(0.40, 0.80, 0.98)
t.tabText = C3(0.21, 0.70)
t.tabActiveText = t.buttonActiveText
t.checkBg = t.buttonBg
t.checkBorder = t.buttonBorder
t.checkHoverBorder = C4(0.40, 0.80, 0.95)
t.checkFill = C4(0.43, 0.88, 0.98)
t.checkOn = C3(0.40, 0.80)
t.checkOff = C4(0.40, 0.25, 0.60)
t.sliderTrack = C4(0.45, 0.22, 0.90)
t.sliderFill = C4(0.35, 0.85, 0.92)
t.sliderThumb = C4(0.25, 1.00, 0.95)
t.text = C3(0.11, 0.92)
t.title = C3(0.30, 1.00)
t.gold = t.title
t.nameText = C3(0.06, 0.92)
t.dimText = C3(0.25, 0.60)
t.bodyText = C3(0.05, 0.82)
t.sectionTitle = C3(0.24, 0.90)
t.catHeader = C3(0.31, 0.80)
t.colHeader = C3(0.25, 0.80)
t.labelText = C3(0.23, 0.65)
t.valueText = t.text
t.subText = t.labelText
t.pageText = C3(0.19, 0.80)
t.objectiveText = C3(0.10, 0.90)
t.optionText = t.tabText
t.countText = t.tabText
t.trackText = C3(0.25, 0.80)
t.divider = C4(0.45, 0.55, 0.40)
t.sepColor = C4(0.44, 0.45, 0.50)
t.scrollThumb = C4(0.45, 0.55, 0.70)
t.scrollTrack = C4(0.50, 0.08, 0.50)
t.inputBg = C4(0.50, 0.08, 0.95)
t.inputBorder = C4(0.38, 0.40, 0.80)
t.searchBg = C4(0.50, 0.08, 0.80)
t.searchBorder = C4(0.38, 0.40, 0.60)
t.progressBg = C4(0.50, 0.08, 0.90)
t.progressFill = C4(0.50, 0.70, 1.00)
t.modelBg = C4(0.60, 0.08, 0.85)
t.modelBorder = C4(0.43, 0.35, 0.70)
t.emptySlot = C4(0.40, 0.25, 0.40)
t.emptySlotBg = C4(0.50, 0.08, 0.40)
t.emptySlotBd = C4(0.40, 0.25, 0.30)
t.barBg = C4(0.60, 0.10, 1.00)
t.rowNormal = C4(0.50, 0.06, 0.30)
t.rowNormalBd = C4(0.22, 0.20, 0.30)
t.raidGroup = t.sectionBg
t.raidGroupBorder = C4(0.38, 0.40, 0.70)
t.raidSlotEmpty = C4(0.50, 0.08, 0.60)
t.questSelected = C4(0.70, 0.60, 0.85)
t.questSelBorder = C4(0.47, 0.95, 1.00)
t.questSelBar = C4(0.45, 1.00, 1.00)
t.questHover = C4(0.52, 0.25, 0.50)
t.zoneHeader = t.catHeader
t.zoneBg = C4(0.50, 0.14, 0.50)
t.collapseIcon = C3(0.31, 0.70)
t.trackBar = C4(0.53, 0.95, 1.00)
t.trackGlow = C4(0.53, 0.95, 0.22)
t.rewardBg = C4(0.50, 0.10, 0.85)
t.rewardBorder = C4(0.45, 0.40, 0.70)
t.listBg = C4(0.50, 0.08, 0.80)
t.listBorder = C4(0.43, 0.35, 0.60)
t.detailBg = C4(0.50, 0.09, 0.92)
t.detailBorder = t.listBorder
t.selectedRowBg = C4(0.65, 0.35, 0.60)
t.selectedRowBorder = C4(0.50, 0.90, 0.70)
t.selectedNameText = { 1, 0.95, 1 }
t.overlayBg = C4(0.75, 0.04, 0.55)
t.accentLine = C4(0.50, 1.00, 0.90)
t.titleColor = t.title
t.nameColor = { 1, 1, 1 }
t.valueColor = t.text
t.labelColor = C3(0.28, 0.58)
t.dimColor = C3(0.29, 0.48)
t.clockColor = C3(0.18, 1.00)
t.timerColor = C3(0.27, 0.75)
t.brandColor = C4(0.37, 0.60, 0.70)
t.particleColor = C3(0.40, 1.00)
t.wbGold = { 1, 0.88, 0.55 }
t.wbBorder = { 0.95, 0.75, 0.25 }
t.passive = { 0.60, 0.60, 0.65 }
return t
end
function SFrames.Theme:Extend(extras)
local override = extras or {}
local proxy = {}
setmetatable(proxy, {
__index = function(self, k)
local v = override[k]
if v ~= nil then return v end
return SFrames.ActiveTheme[k]
end
})
return proxy
end
function SFrames.Theme:Apply(presetKey)
local key = presetKey or "pink"
local preset = self.Presets[key]
if not preset then key = "pink"; preset = self.Presets["pink"] end
local newTheme = GenerateTheme(preset.hue, preset.satMul)
local oldKeys = {}
for k, v in pairs(SFrames.ActiveTheme) do
table.insert(oldKeys, k)
end
for i = 1, table.getn(oldKeys) do
SFrames.ActiveTheme[oldKeys[i]] = nil
end
for k, v in pairs(newTheme) do
SFrames.ActiveTheme[k] = v
end
if SFrames.Config and SFrames.Config.colors then
local a = SFrames.ActiveTheme
SFrames.Config.colors.border = { r = a.accent[1], g = a.accent[2], b = a.accent[3], a = 1 }
SFrames.Config.colors.backdrop = { r = a.panelBg[1], g = a.panelBg[2], b = a.panelBg[3], a = a.panelBg[4] or 0.8 }
end
if SFrames.MinimapButton and SFrames.MinimapButton.Refresh then
SFrames.MinimapButton:Refresh()
end
end
function SFrames.Theme:GetCurrentPreset()
local dbExists = SFramesDB and true or false
local themeExists = SFramesDB and type(SFramesDB.Theme) == "table" and true or false
local savedPreset = themeExists and SFramesDB.Theme.preset or "nil"
if SFramesDB and type(SFramesDB.Theme) == "table" then
if SFramesDB.Theme.useClassTheme then
local _, class = UnitClass("player")
if class and self.ClassMap[class] then
return self.ClassMap[class]
end
end
if SFramesDB.Theme.preset and self.Presets[SFramesDB.Theme.preset] then
return SFramesDB.Theme.preset
end
end
return "pink"
end
function SFrames.Theme:GetAccentHex()
return SFrames.ActiveTheme.accentHex or "ffffb3d9"
end
SFrames.Theme.HSVtoRGB = HSVtoRGB
SFrames.Theme.RGBtoHex = RGBtoHex
SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset())
local themeInitFrame = CreateFrame("Frame")
themeInitFrame:RegisterEvent("VARIABLES_LOADED")
themeInitFrame:RegisterEvent("PLAYER_LOGIN")
themeInitFrame:SetScript("OnEvent", function()
SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset())
end)

3513
ConfigUI.lua Normal file

File diff suppressed because it is too large Load Diff

524
Core.lua Normal file
View File

@@ -0,0 +1,524 @@
-- S-Frames Core Initialize
SFrames = {}
DEFAULT_CHAT_FRAME:AddMessage("SF: Loading Core.lua...")
BINDING_HEADER_NANAMI_UI = "Nanami-UI"
BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图"
SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
SFrames.events = {}
function SFrames:GetIncomingHeals(unit)
-- Source 1: ShaguTweaks libpredict
if ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals then
local lp = ShaguTweaks.libpredict
if lp.UnitGetIncomingHealsBreakdown then
local ok, total, mine, others = pcall(function()
return lp:UnitGetIncomingHealsBreakdown(unit, UnitName("player"))
end)
if ok then
return math.max(0, tonumber(total) or 0),
math.max(0, tonumber(mine) or 0),
math.max(0, tonumber(others) or 0)
end
end
local ok, amount = pcall(function() return lp:UnitGetIncomingHeals(unit) end)
if ok then
amount = math.max(0, tonumber(amount) or 0)
return amount, 0, amount
end
end
-- Source 2: HealComm-1.0 (AceLibrary)
if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("HealComm-1.0") then
local ok, HC = pcall(function() return AceLibrary("HealComm-1.0") end)
if ok and HC and HC.getHeal then
local name = UnitName(unit)
if name then
local total = HC:getHeal(name) or 0
total = math.max(0, tonumber(total) or 0)
return total, 0, total
end
end
end
return 0, 0, 0
end
-- Event Dispatcher
SFrames.eventFrame:SetScript("OnEvent", function()
if SFrames.events[event] then
for i, func in ipairs(SFrames.events[event]) do
func(event)
end
end
end)
function SFrames:RegisterEvent(event, func)
if not self.events[event] then
self.events[event] = {}
self.eventFrame:RegisterEvent(event)
end
table.insert(self.events[event], func)
end
function SFrames:UnregisterEvent(event, func)
if self.events[event] then
for i, f in ipairs(self.events[event]) do
if f == func then
table.remove(self.events[event], i)
break
end
end
if table.getn(self.events[event]) == 0 then
self.events[event] = nil
self.eventFrame:UnregisterEvent(event)
end
end
end
-- Print Helper
function SFrames:Print(msg)
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg))
end
-- Addon Loaded Initializer
SFrames:RegisterEvent("PLAYER_LOGIN", function()
SFrames:Initialize()
end)
function SFrames:SafeInit(name, initFn)
local ok, err = pcall(initFn)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: " .. name .. " init failed: " .. tostring(err) .. "|r")
end
end
function SFrames:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.setupComplete then
if SFrames.SetupWizard and SFrames.SetupWizard.Show then
SFrames.SetupWizard:Show(function()
SFrames:DoFullInitialize()
end, "firstrun")
else
SFramesDB.setupComplete = true
self:DoFullInitialize()
end
return
end
self:DoFullInitialize()
end
function SFrames:DoFullInitialize()
self:Print("Nanami-UI 正在加载,喵呜~ =^_^=")
self:HideBlizzardFrames()
SFrames.Tooltip = CreateFrame("GameTooltip", "SFramesScanTooltip", nil, "GameTooltipTemplate")
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
-- Phase 1: Critical modules (unit frames, action bars) — must load immediately
if SFramesDB.enableUnitFrames ~= false then
if SFrames.Player and SFrames.Player.Initialize then SFrames.Player:Initialize() end
if SFrames.Pet and SFrames.Pet.Initialize then SFrames.Pet:Initialize() end
if SFrames.Target and SFrames.Target.Initialize then SFrames.Target:Initialize() end
if SFrames.ToT and SFrames.ToT.Initialize then SFrames.ToT:Initialize() end
if SFrames.Party and SFrames.Party.Initialize then SFrames.Party:Initialize() end
end
if SFrames.FloatingTooltip and SFrames.FloatingTooltip.Initialize then SFrames.FloatingTooltip:Initialize() end
if SFrames.ActionBars and SFrames.ActionBars.Initialize then
SFrames.ActionBars:Initialize()
end
self:InitSlashCommands()
-- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike
local deferred = {
{ "Raid", function() if SFramesDB.enableUnitFrames ~= false and SFrames.Raid and SFrames.Raid.Initialize then SFrames.Raid:Initialize() end end },
{ "Bags", function() if SFrames.Bags and SFrames.Bags.Core and SFrames.Bags.Core.Initialize then SFrames.Bags.Core:Initialize() end end },
{ "Focus", function() if SFrames.Focus and SFrames.Focus.Initialize then SFrames.Focus:Initialize() end end },
{ "TalentTree", function() if SFrames.TalentTree and SFrames.TalentTree.Initialize then SFrames.TalentTree:Initialize() end end },
{ "Minimap", function() if SFrames.Minimap and SFrames.Minimap.Initialize then SFrames.Minimap:Initialize() end end },
{ "MinimapBuffs",function() if SFrames.MinimapBuffs and SFrames.MinimapBuffs.Initialize then SFrames.MinimapBuffs:Initialize() end end },
{ "MinimapButton",function() if SFrames.MinimapButton and SFrames.MinimapButton.Initialize then SFrames.MinimapButton: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 },
{ "WorldMap", function() if SFrames.WorldMap and SFrames.WorldMap.Initialize then SFrames.WorldMap:Initialize() end end },
{ "MapIcons", function() if SFrames.MapIcons and SFrames.MapIcons.Initialize then SFrames.MapIcons:Initialize() end end },
{ "Tweaks", function() if SFrames.Tweaks and SFrames.Tweaks.Initialize then SFrames.Tweaks:Initialize() end end },
{ "AFKScreen", function() if SFrames.AFKScreen and SFrames.AFKScreen.Initialize then SFrames.AFKScreen:Initialize() end end },
}
local idx = 1
local batchSize = 3
local deferFrame = CreateFrame("Frame")
deferFrame:SetScript("OnUpdate", function()
if idx > table.getn(deferred) then
this:SetScript("OnUpdate", nil)
SFrames:Print("所有模块加载完成 =^_^=")
return
end
local batchEnd = idx + batchSize - 1
if batchEnd > table.getn(deferred) then batchEnd = table.getn(deferred) end
for i = idx, batchEnd do
SFrames:SafeInit(deferred[i][1], deferred[i][2])
end
idx = batchEnd + 1
end)
end
function SFrames:GetAuraTimeLeft(unit, index, isBuff)
-- If the unit is the player (e.g. you target yourself), Vanilla API CAN give us exact times
if UnitIsUnit(unit, "player") then
local texture = isBuff and UnitBuff(unit, index) or UnitDebuff(unit, index)
if texture then
local filter = isBuff and "HELPFUL" or "HARMFUL"
for i = 0, 31 do
local buffIndex = GetPlayerBuff(i, filter)
if buffIndex and buffIndex >= 0 then
if GetPlayerBuffTexture(buffIndex) == texture then
local t = GetPlayerBuffTimeLeft(buffIndex)
if t and t > 0 then return t end
end
end
end
end
end
-- Fallback to ShaguTweaks libdebuff if available (Debuffs only usually)
if ShaguTweaks and ShaguTweaks.libdebuff then
if not isBuff then
local effect, rank, texture, stacks, dtype, duration, libTimeLeft = ShaguTweaks.libdebuff:UnitDebuff(unit, index)
if libTimeLeft and libTimeLeft > 0 then
return libTimeLeft
end
end
end
return 0
end
function SFrames:FormatTime(seconds)
if not seconds then return "" end
if seconds >= 3600 then
return math.floor(seconds / 3600) .. "h"
elseif seconds >= 60 then
return math.floor(seconds / 60) .. "m"
else
return math.floor(seconds) .. "s"
end
end
function SFrames:InitSlashCommands()
DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.")
SLASH_SFRAMES1 = "/nanami"
SLASH_SFRAMES2 = "/nui"
SlashCmdList["SFRAMES"] = function(msg)
local text = msg or ""
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
local _, _, cmd, args = string.find(text, "^(%S+)%s*(.-)$")
cmd = string.lower(cmd or "")
args = args or ""
if cmd == "unlock" or cmd == "move" then
SFrames:UnlockFrames()
elseif cmd == "lock" then
SFrames:LockFrames()
elseif cmd == "chatreset" then
if SFrames.Chat and SFrames.Chat.ResetPosition then
SFrames.Chat:ResetPosition()
end
elseif cmd == "test" then
if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end
elseif cmd == "partyh" then
if SFrames.Party and SFrames.Party.SetLayout then
SFrames.Party:SetLayout("horizontal")
SFrames:Print("Party layout set to horizontal.")
end
elseif cmd == "partyv" then
if SFrames.Party and SFrames.Party.SetLayout then
SFrames.Party:SetLayout("vertical")
SFrames:Print("Party layout set to vertical.")
end
elseif cmd == "partylayout" or cmd == "playout" then
local mode = string.lower(args or "")
if mode == "h" or mode == "horizontal" then
if SFrames.Party and SFrames.Party.SetLayout then
SFrames.Party:SetLayout("horizontal")
SFrames:Print("Party layout set to horizontal.")
end
elseif mode == "v" or mode == "vertical" then
if SFrames.Party and SFrames.Party.SetLayout then
SFrames.Party:SetLayout("vertical")
SFrames:Print("Party layout set to vertical.")
end
else
local current = (SFramesDB and SFramesDB.partyLayout) or "vertical"
SFrames:Print("Usage: /nui partylayout horizontal|vertical (current: " .. current .. ")")
end
elseif cmd == "focus" then
if not SFrames.Focus then
SFrames:Print("Focus module unavailable.")
return
end
local ok, name, usedNative = SFrames.Focus:SetFromTarget()
if ok then
if usedNative then
SFrames:Print("Focus set: " .. tostring(name) .. " (native)")
else
SFrames:Print("Focus set: " .. tostring(name))
end
else
SFrames:Print("No valid target to set focus.")
end
elseif cmd == "clearfocus" or cmd == "cf" then
if not SFrames.Focus then
SFrames:Print("Focus module unavailable.")
return
end
SFrames.Focus:Clear()
SFrames:Print("Focus cleared.")
elseif cmd == "targetfocus" or cmd == "tf" then
if not SFrames.Focus then
SFrames:Print("Focus module unavailable.")
return
end
local ok = SFrames.Focus:Target()
if not ok then
SFrames:Print("Focus target not found.")
end
elseif cmd == "fcast" or cmd == "focuscast" then
if not SFrames.Focus then
SFrames:Print("Focus module unavailable.")
return
end
local ok, reason = SFrames.Focus:Cast(args)
if not ok then
if reason == "NO_SPELL" then
SFrames:Print("Usage: /nui fcast <spell name>")
elseif reason == "NO_FOCUS" then
SFrames:Print("No focus set.")
elseif reason == "FOCUS_NOT_FOUND" then
SFrames:Print("Focus not found in range/scene.")
else
SFrames:Print("Focus cast failed.")
end
end
elseif cmd == "focushelp" then
SFrames:Print("/nui focus - set current target as focus")
SFrames:Print("/nui clearfocus - clear focus")
SFrames:Print("/nui targetfocus - target focus")
SFrames:Print("/nui fcast <spell> - cast spell on focus")
SFrames:Print("/nui partyh / partyv - switch party layout")
SFrames:Print("/nui partylayout h|v - switch party layout")
SFrames:Print("/nui ui - open UI settings")
SFrames:Print("/nui bags - open bag settings")
SFrames:Print("/nui chat - open chat settings panel")
SFrames:Print("/nui chat help - chat command help")
SFrames:Print("/nui chatreset - reset chat frame position")
SFrames:Print("/nui afk - toggle AFK screen")
SFrames:Print("/nui pin - 地图标记 (clear/share)")
SFrames:Print("/nui nav - 切换导航地图")
SFrames:Print("/nui bind - 按键绑定模式(悬停按钮+按键)")
elseif cmd == "ui" or cmd == "uiconfig" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end
elseif cmd == "chat" or cmd == "chatconfig" then
if SFrames.Chat and SFrames.Chat.HandleSlash then
SFrames.Chat:HandleSlash(args)
end
elseif cmd == "bags" or cmd == "bag" or cmd == "bagconfig" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("bags") end
elseif cmd == "mapreveal" or cmd == "mr" then
if SFrames.MapReveal and SFrames.MapReveal.Toggle then
SFrames.MapReveal:Toggle()
else
SFrames:Print("MapReveal module unavailable.")
end
elseif cmd == "stats" or cmd == "stat" or cmd == "ss" then
if SFrames.StatSummary and SFrames.StatSummary.Toggle then
SFrames.StatSummary:Toggle()
else
SFrames:Print("StatSummary module unavailable.")
end
elseif cmd == "afk" then
if SFrames.AFKScreen and SFrames.AFKScreen.Toggle then
SFrames.AFKScreen:Toggle()
else
SFrames:Print("AFKScreen module unavailable.")
end
elseif cmd == "pin" or cmd == "wp" or cmd == "waypoint" then
if not SFrames.WorldMap then
SFrames:Print("WorldMap module unavailable.")
elseif args == "clear" or args == "remove" then
SFrames.WorldMap:ClearWaypoint()
SFrames:Print("地图标记已清除")
elseif args == "share" then
SFrames.WorldMap:ShareWaypoint()
else
SFrames:Print("/nui pin clear - 清除地图标记")
SFrames:Print("/nui pin share - 分享当前标记到聊天")
SFrames:Print("在世界地图中 Ctrl+左键 可放置标记")
end
elseif cmd == "nav" or cmd == "navigation" then
if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then
SFrames.WorldMap:ToggleNav()
else
SFrames:Print("WorldMap module unavailable.")
end
elseif cmd == "bind" or cmd == "keybind" then
if SFrames.ActionBars and SFrames.ActionBars.ToggleKeyBindMode then
SFrames.ActionBars:ToggleKeyBindMode()
else
SFrames:Print("ActionBars module unavailable.")
end
elseif cmd == "config" or cmd == "" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end
else
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui stats, /nui afk, /nui pin, /nui bind")
end
end
end
function SFrames:UnlockFrames()
self.isUnlocked = true
self:Print("Frames Unlocked. Drag to move.")
-- Show overlays or just let them be dragged if they are always movable
if SFramesPlayerFrame then SFramesPlayerFrame:EnableMouse(true) end
if SFramesPetFrame then SFramesPetFrame:EnableMouse(true) end
if SFramesTargetFrame then SFramesTargetFrame:EnableMouse(true) end
if SFrames.Chat and SFrames.Chat.SetUnlocked then
SFrames.Chat:SetUnlocked(true)
end
end
function SFrames:LockFrames()
self.isUnlocked = false
self:Print("Frames Locked.")
if SFrames.Chat and SFrames.Chat.SetUnlocked then
SFrames.Chat:SetUnlocked(false)
end
end
function SFrames:HideBlizzardFrames()
-- Hide Character Frame (replaced by CharacterPanel.lua)
-- Only suppress if the custom character panel is enabled
if (not SFramesDB) or (SFramesDB.charPanelEnable ~= false) then
if CharacterFrame then
CharacterFrame:UnregisterAllEvents()
CharacterFrame:Hide()
CharacterFrame.Show = function() end
end
if PaperDollFrame then PaperDollFrame:Hide() end
if ReputationFrame then ReputationFrame:Hide() end
if SkillFrame then SkillFrame:Hide() end
if HonorFrame then HonorFrame:Hide() end
end
if SFramesDB and SFramesDB.enableUnitFrames == false then
-- Keep Blizzard unit frames when Nanami frames are disabled
else
-- Hide Player Frame
if PlayerFrame then
PlayerFrame:UnregisterAllEvents()
PlayerFrame:Hide()
PlayerFrame.Show = function() end
end
-- Hide Pet Frame
if PetFrame then
PetFrame:UnregisterAllEvents()
PetFrame:Hide()
PetFrame.Show = function() end
end
-- Hide Target Frame
if TargetFrame then
TargetFrame:UnregisterAllEvents()
TargetFrame:Hide()
TargetFrame.Show = function() end
end
-- Hide Combo Frame
if ComboFrame then
ComboFrame:UnregisterAllEvents()
ComboFrame:Hide()
ComboFrame.Show = function() end
end
-- Hide Party Frames
for i = 1, 4 do
local pf = _G["PartyMemberFrame"..i]
if pf then
pf:UnregisterAllEvents()
pf:Hide()
pf.Show = function() end
end
end
end
-- Hide Native Raid Frames if SFrames raid is enabled
if (not SFramesDB) or (SFramesDB.enableRaidFrames ~= false) then
local function NeuterBlizzardRaidUI()
-- Default Classic UI (1.12)
if RaidFrame then
RaidFrame:UnregisterAllEvents()
end
-- Prevent Raid groups from updating and showing
for i = 1, NUM_RAID_GROUPS or 8 do
local rgf = _G["RaidGroupButton"..i]
if rgf then
rgf:UnregisterAllEvents()
end
end
-- Override pullout generation
RaidPullout_Update = function() end
RaidPullout_OnEvent = function() end
-- Hide individual pullout frames that might already exist
for i = 1, 40 do
local pf = _G["RaidPullout"..i]
if pf then
pf:UnregisterAllEvents()
pf:Hide()
pf.Show = function() end
end
end
-- Hide standard GroupFrames
if RaidGroupFrame_OnEvent then
RaidGroupFrame_OnEvent = function() end
end
-- Hide newer/backported Compact Raid Frames if they exist
if CompactRaidFrameManager then
CompactRaidFrameManager:UnregisterAllEvents()
CompactRaidFrameManager:Hide()
CompactRaidFrameManager.Show = function() end
end
if CompactRaidFrameContainer then
CompactRaidFrameContainer:UnregisterAllEvents()
CompactRaidFrameContainer:Hide()
CompactRaidFrameContainer.Show = function() end
end
end
NeuterBlizzardRaidUI()
-- Hook ADDON_LOADED to catch Blizzard_RaidUI loaded on demand
local raidHook = CreateFrame("Frame")
raidHook:RegisterEvent("ADDON_LOADED")
raidHook:SetScript("OnEvent", function()
if arg1 == "Blizzard_RaidUI" then
NeuterBlizzardRaidUI()
end
end)
end
end

363
DarkmoonGuide.lua Normal file
View File

@@ -0,0 +1,363 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Darkmoon Faire Buff Guide (暗月马戏团 Buff 指引)
-- Automatically shows a guide when talking to Sayge, or via /dmf command
--------------------------------------------------------------------------------
SFrames.DarkmoonGuide = SFrames.DarkmoonGuide or {}
local DG = SFrames.DarkmoonGuide
local GUIDE_WIDTH = 420
local GUIDE_HEIGHT = 520
local _A = SFrames.ActiveTheme
local SAYGE_NAMES = {
["Sayge"] = true,
["塞格"] = true,
["赛格"] = true,
["Сэйдж"] = true,
}
-- Buff data: { buff name, q1 answer, q2 answer, description, optional alt path }
local BUFF_DATA = {
{ buff = "+10% 伤害", q1 = 1, q2 = 1, star = true, tip = "DPS首选" },
{ buff = "+25 全抗性", q1 = 1, q2 = 2, star = false, tip = "PvP/坦克可选", q1_alt = 2, q2_alt = 3 },
{ buff = "+10% 护甲", q1 = 1, q2 = 3, star = false, tip = "坦克可选", q1_alt = 4, q2_alt = 3 },
{ buff = "+10% 精神", q1 = 2, q2 = 1, star = false, tip = "治疗回蓝", q1_alt = 4, q2_alt = 2 },
{ buff = "+10% 智力", q1 = 2, q2 = 2, star = false, tip = "法系/治疗", q1_alt = 4, q2_alt = 1 },
{ buff = "+10% 耐力", q1 = 3, q2 = 1, star = false, tip = "坦克/PvP" },
{ buff = "+10% 力量", q1 = 3, q2 = 2, star = false, tip = "战士/圣骑" },
{ buff = "+10% 敏捷", q1 = 3, q2 = 3, star = false, tip = "盗贼/猎人" },
}
local Q1_LABELS = {
[1] = "第一项: 我会正面迎战",
[2] = "第二项: 我会说服对方",
[3] = "第三项: 我会帮助别人",
[4] = "第四项: 我会独自思考",
}
local Q2_LABELS = {
[1] = "选第一项",
[2] = "选第二项",
[3] = "选第三项",
}
--------------------------------------------------------------------------------
-- Frame creation
--------------------------------------------------------------------------------
local function CreateGuideFrame()
if DG.frame then return DG.frame end
local f = CreateFrame("Frame", "NanamiDarkmoonGuide", UIParent)
f:SetWidth(GUIDE_WIDTH)
f:SetHeight(GUIDE_HEIGHT)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 40)
f:SetFrameStrata("FULLSCREEN_DIALOG")
f:SetFrameLevel(500)
f:SetMovable(true)
f:EnableMouse(true)
f:SetClampedToScreen(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
SFrames:CreateRoundBackdrop(f)
local _pbg = _A.panelBg or { 0.06, 0.05, 0.08, 0.96 }
local _pbd = _A.panelBorder or { 0.45, 0.25, 0.55, 1 }
f:SetBackdropColor(_pbg[1], _pbg[2], _pbg[3], _pbg[4])
f:SetBackdropBorderColor(_pbd[1], _pbd[2], _pbd[3], _pbd[4])
table.insert(UISpecialFrames, "NanamiDarkmoonGuide")
if WorldMapFrame then
local origOnHide = WorldMapFrame:GetScript("OnHide")
WorldMapFrame:SetScript("OnHide", function()
if origOnHide then origOnHide() end
if f:IsShown() then f:Hide() end
end)
end
-- Title bar
local titleBar = CreateFrame("Frame", nil, f)
titleBar:SetHeight(32)
titleBar:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8)
titleBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
titleBar:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
local _hbg = _A.headerBg or { 0.12, 0.08, 0.15, 0.9 }
local _hbd = _A.headerBorder or { 0.5, 0.3, 0.6, 0.6 }
titleBar:SetBackdropColor(_hbg[1], _hbg[2], _hbg[3], _hbg[4])
titleBar:SetBackdropBorderColor(_hbd[1], _hbd[2], _hbd[3], _hbd[4])
local titleIcon = titleBar:CreateTexture(nil, "ARTWORK")
titleIcon:SetTexture("Interface\\Icons\\INV_Misc_Orb_02")
titleIcon:SetWidth(20)
titleIcon:SetHeight(20)
titleIcon:SetPoint("LEFT", titleBar, "LEFT", 8, 0)
local titleText = SFrames:CreateFontString(titleBar, 14, "LEFT")
titleText:SetPoint("LEFT", titleIcon, "RIGHT", 6, 0)
titleText:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
titleText:SetText("暗月马戏团 Buff 指引")
-- Close button
local closeBtn = CreateFrame("Button", nil, f)
closeBtn:SetWidth(20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, -13)
closeBtn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
local _cbg = _A.closeBtnBg or { 0.3, 0.08, 0.08, 0.9 }
local _cbd = _A.closeBtnBorder or { 0.6, 0.2, 0.2, 0.8 }
local _cbgH = _A.closeBtnHoverBg or { 0.5, 0.1, 0.1, 0.95 }
local _cbdH = _A.closeBtnHoverBorder or { 0.8, 0.3, 0.3, 1 }
closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
local closeTxt = SFrames:CreateFontString(closeBtn, 12, "CENTER")
closeTxt:SetAllPoints(closeBtn)
closeTxt:SetText("X")
local _closeTxt = _A.accentLight or { 0.9, 0.5, 0.5 }
closeTxt:SetTextColor(_closeTxt[1], _closeTxt[2], _closeTxt[3])
closeBtn:SetScript("OnClick", function() f:Hide() end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_cbgH[1], _cbgH[2], _cbgH[3], _cbgH[4])
this:SetBackdropBorderColor(_cbdH[1], _cbdH[2], _cbdH[3], _cbdH[4])
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
this:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
end)
-- NPC info section
local npcSection = CreateFrame("Frame", nil, f)
npcSection:SetHeight(48)
npcSection:SetPoint("TOPLEFT", titleBar, "BOTTOMLEFT", 0, -8)
npcSection:SetPoint("TOPRIGHT", titleBar, "BOTTOMRIGHT", 0, -8)
local npcLine1 = SFrames:CreateFontString(npcSection, 11, "LEFT")
npcLine1:SetPoint("TOPLEFT", npcSection, "TOPLEFT", 4, 0)
npcLine1:SetPoint("TOPRIGHT", npcSection, "TOPRIGHT", -4, 0)
npcLine1:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
npcLine1:SetText("NPC: |cffffffffSayge (塞格)|r - 暗月马戏团占卜师")
local npcLine2 = SFrames:CreateFontString(npcSection, 10, "LEFT")
npcLine2:SetPoint("TOPLEFT", npcLine1, "BOTTOMLEFT", 0, -4)
npcLine2:SetPoint("TOPRIGHT", npcLine1, "BOTTOMRIGHT", 0, -4)
npcLine2:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
npcLine2:SetText("与赛格对话选择不同答案可获得不同2小时Buff (表中 X→Y = 第1题选X, 第2题选Y)")
-- Separator
local sep1 = f:CreateTexture(nil, "ARTWORK")
sep1:SetTexture("Interface\\Buttons\\WHITE8X8")
sep1:SetHeight(1)
sep1:SetPoint("TOPLEFT", npcSection, "BOTTOMLEFT", 0, -4)
sep1:SetPoint("TOPRIGHT", npcSection, "BOTTOMRIGHT", 0, -4)
local _sep = _A.separator or { 0.4, 0.25, 0.5, 0.5 }
sep1:SetVertexColor(_sep[1], _sep[2], _sep[3], _sep[4])
-- Column headers
local headerY = -104
local colBuff = 12
local colOpt1 = 150
local colOpt2 = 260
local colTip = 355
local hBuff = SFrames:CreateFontString(f, 11, "LEFT")
hBuff:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff, headerY)
hBuff:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
hBuff:SetText("Buff效果")
local hOpt1 = SFrames:CreateFontString(f, 11, "LEFT")
hOpt1:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt1, headerY)
hOpt1:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
hOpt1:SetText("选项1")
local hOpt2 = SFrames:CreateFontString(f, 11, "LEFT")
hOpt2:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt2, headerY)
hOpt2:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
hOpt2:SetText("选项2")
local hTip = SFrames:CreateFontString(f, 11, "LEFT")
hTip:SetPoint("TOPLEFT", f, "TOPLEFT", colTip, headerY)
hTip:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
hTip:SetText("备注")
-- Header separator
local sep2 = f:CreateTexture(nil, "ARTWORK")
sep2:SetTexture("Interface\\Buttons\\WHITE8X8")
sep2:SetHeight(1)
sep2:SetPoint("TOPLEFT", f, "TOPLEFT", 10, headerY - 14)
sep2:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, headerY - 14)
local _sep2c = _A.separator or { 0.35, 0.2, 0.45, 0.4 }
sep2:SetVertexColor(_sep2c[1], _sep2c[2], _sep2c[3], _sep2c[4])
-- Buff rows
local rowStart = headerY - 22
local rowHeight = 24
for i, data in ipairs(BUFF_DATA) do
local y = rowStart - (i - 1) * rowHeight
-- Alternate row background
if math.fmod(i, 2) == 0 then
local rowBg = f:CreateTexture(nil, "BACKGROUND")
rowBg:SetTexture("Interface\\Buttons\\WHITE8X8")
rowBg:SetHeight(rowHeight)
rowBg:SetPoint("TOPLEFT", f, "TOPLEFT", 8, y + 4)
rowBg:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, y + 4)
local _rbg = _A.sectionBg or { 0.15, 0.1, 0.2, 0.3 }
rowBg:SetVertexColor(_rbg[1], _rbg[2], _rbg[3], 0.3)
end
-- Star marker for recommended
if data.star then
local star = SFrames:CreateFontString(f, 10, "LEFT")
star:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff - 10, y)
local _stc = _A.title or { 1.0, 0.84, 0.0 }
star:SetTextColor(_stc[1], _stc[2], _stc[3])
star:SetText("*")
end
-- Buff name
local buffName = SFrames:CreateFontString(f, 11, "LEFT")
buffName:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff, y)
if data.star then
local _hl = _A.accentLight or { 0.4, 1.0, 0.4 }
buffName:SetTextColor(_hl[1], _hl[2], _hl[3])
else
buffName:SetTextColor(_A.text[1], _A.text[2], _A.text[3])
end
buffName:SetText(data.buff)
-- Option 1 path (q1→q2)
local opt1Text = SFrames:CreateFontString(f, 11, "CENTER")
opt1Text:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt1, y)
local _o1c = _A.title or { 1, 0.82, 0.5 }
opt1Text:SetTextColor(_o1c[1], _o1c[2], _o1c[3])
opt1Text:SetText(data.q1 .. "" .. data.q2)
-- Option 2 path (alt q1→q2) or "/"
local opt2Text = SFrames:CreateFontString(f, 11, "CENTER")
opt2Text:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt2, y)
if data.q1_alt then
local _o2c = _A.accentDark or { 0.7, 0.82, 1.0 }
opt2Text:SetTextColor(_o2c[1], _o2c[2], _o2c[3])
opt2Text:SetText(data.q1_alt .. "" .. data.q2_alt)
else
opt2Text:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
opt2Text:SetText("/")
end
-- Tip
local tipText = SFrames:CreateFontString(f, 10, "LEFT")
tipText:SetPoint("TOPLEFT", f, "TOPLEFT", colTip, y)
tipText:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
tipText:SetText(data.tip)
end
-- Separator before Q1 detail
local sep3 = f:CreateTexture(nil, "ARTWORK")
sep3:SetTexture("Interface\\Buttons\\WHITE8X8")
sep3:SetHeight(1)
local detailY = rowStart - table.getn(BUFF_DATA) * rowHeight - 4
sep3:SetPoint("TOPLEFT", f, "TOPLEFT", 10, detailY)
sep3:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, detailY)
local _sep3c = _A.separator or { 0.4, 0.25, 0.5, 0.5 }
sep3:SetVertexColor(_sep3c[1], _sep3c[2], _sep3c[3], _sep3c[4])
-- Q1 dialogue detail header
local q1Header = SFrames:CreateFontString(f, 11, "LEFT")
q1Header:SetPoint("TOPLEFT", f, "TOPLEFT", 12, detailY - 10)
q1Header:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
q1Header:SetText("第一题选项对照:")
-- Q1 options
local q1y = detailY - 28
for idx, label in pairs(Q1_LABELS) do
local line = SFrames:CreateFontString(f, 10, "LEFT")
line:SetPoint("TOPLEFT", f, "TOPLEFT", 18, q1y - (idx - 1) * 16)
line:SetTextColor(_A.text[1], _A.text[2], _A.text[3])
line:SetText("|cffffcc66" .. idx .. ".|r " .. label)
end
-- Q2 dialogue detail header
local q2HeaderY = q1y - 4 * 16 - 8
local q2Header = SFrames:CreateFontString(f, 11, "LEFT")
q2Header:SetPoint("TOPLEFT", f, "TOPLEFT", 12, q2HeaderY)
q2Header:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3])
q2Header:SetText("第二题: 根据上表 \"X → Y\" 中的Y选对应项 (共3项)")
-- Tip at bottom
local tipY = q2HeaderY - 24
local bottomTip = SFrames:CreateFontString(f, 10, "LEFT")
bottomTip:SetPoint("TOPLEFT", f, "TOPLEFT", 12, tipY)
bottomTip:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, tipY)
local _btc = _A.title or { 1.0, 0.84, 0.0 }
bottomTip:SetTextColor(_btc[1], _btc[2], _btc[3])
bottomTip:SetText("* 推荐: 大部分职业选 +10% 伤害 (第1题选1, 第2题选1)")
local bottomTip2 = SFrames:CreateFontString(f, 10, "LEFT")
bottomTip2:SetPoint("TOPLEFT", bottomTip, "BOTTOMLEFT", 0, -6)
bottomTip2:SetPoint("TOPRIGHT", bottomTip, "BOTTOMRIGHT", 0, -6)
bottomTip2:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
bottomTip2:SetText("输入 /dmf 可随时打开此面板 | 可拖动移动 | ESC关闭")
f:Hide()
DG.frame = f
return f
end
--------------------------------------------------------------------------------
-- Toggle
--------------------------------------------------------------------------------
function DG:Toggle()
local f = CreateGuideFrame()
if f:IsShown() then
f:Hide()
else
f:Show()
end
end
function DG:Show()
local f = CreateGuideFrame()
f:Show()
end
function DG:Hide()
if DG.frame then DG.frame:Hide() end
end
--------------------------------------------------------------------------------
-- Slash command: /dmf
--------------------------------------------------------------------------------
SLASH_DARKMOONGUIDE1 = "/dmf"
SLASH_DARKMOONGUIDE2 = "/darkmoon"
SlashCmdList["DARKMOONGUIDE"] = function()
DG:Toggle()
end
--------------------------------------------------------------------------------
-- Auto-show when talking to Sayge
--------------------------------------------------------------------------------
local detector = CreateFrame("Frame", "NanamiDarkmoonDetector", UIParent)
detector:RegisterEvent("GOSSIP_SHOW")
detector:SetScript("OnEvent", function()
local npcName = UnitName("npc")
if npcName and SAYGE_NAMES[npcName] then
DG:Show()
end
end)
DEFAULT_CHAT_FRAME:AddMessage("SF: Loading DarkmoonGuide.lua...")

2
DarkmoonMapMarker.lua Normal file
View File

@@ -0,0 +1,2 @@
-- Darkmoon Faire map markers are now integrated into WorldMap.lua (section 8b)
-- This file is intentionally left minimal to avoid duplicate code.

252
Factory.lua Normal file
View File

@@ -0,0 +1,252 @@
-- Helper function to generate ElvUI-style backdrop and shadow border
function SFrames:CreateBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
local A = SFrames.ActiveTheme
if A and A.panelBg then
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
else
frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
frame:SetBackdropBorderColor(0, 0, 0, 1)
end
end
function SFrames:CreateRoundBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
local A = SFrames.ActiveTheme
if A and A.panelBg then
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.95)
frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 0.9)
else
frame:SetBackdropColor(0.08, 0.08, 0.10, 0.95)
frame:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
end
end
function SFrames:CreateUnitBackdrop(frame)
SFrames:CreateBackdrop(frame)
frame:SetBackdropBorderColor(0, 0, 0, 1)
end
-- Generator for StatusBars
function SFrames:CreateStatusBar(parent, name)
local bar = CreateFrame("StatusBar", name, parent)
bar:SetStatusBarTexture(SFrames:GetTexture())
if (not SFramesDB or SFramesDB.smoothBars ~= false) and SmoothBar then
SmoothBar(bar)
end
return bar
end
-- Generator for FontStrings
function SFrames:CreateFontString(parent, size, justifyH)
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(SFrames:GetFont(), size or 12, SFrames.Media.fontOutline)
fs:SetJustifyH(justifyH or "CENTER")
fs:SetTextColor(1, 1, 1)
return fs
end
-- Generator for 3D Portraits
function SFrames:CreatePortrait(parent, name)
local portrait = CreateFrame("PlayerModel", name, parent)
return portrait
end
--------------------------------------------------------------------------------
-- Class Icon (circular class portraits from UI-Classes-Circles.tga)
--------------------------------------------------------------------------------
local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles"
SFrames.CLASS_ICON_TCOORDS = {
["WARRIOR"] = { 0, 0.25, 0, 0.25 },
["MAGE"] = { 0.25, 0.49609375, 0, 0.25 },
["ROGUE"] = { 0.49609375, 0.7421875, 0, 0.25 },
["DRUID"] = { 0.7421875, 0.98828125, 0, 0.25 },
["HUNTER"] = { 0, 0.25, 0.25, 0.5 },
["SHAMAN"] = { 0.25, 0.49609375, 0.25, 0.5 },
["PRIEST"] = { 0.49609375, 0.7421875, 0.25, 0.5 },
["WARLOCK"] = { 0.7421875, 0.98828125, 0.25, 0.5 },
["PALADIN"] = { 0, 0.25, 0.5, 0.75 },
}
function SFrames:CreateClassIcon(parent, size)
local sz = size or 20
local overlay = CreateFrame("Frame", nil, parent)
overlay:SetFrameLevel((parent:GetFrameLevel() or 0) + 3)
overlay:SetWidth(sz)
overlay:SetHeight(sz)
local icon = overlay:CreateTexture(nil, "OVERLAY")
icon:SetTexture(CLASS_ICON_PATH)
icon:SetAllPoints(overlay)
icon:Hide()
icon.overlay = overlay
return icon
end
function SFrames:SetClassIcon(icon, class)
if not icon then return end
local coords = self.CLASS_ICON_TCOORDS[class]
if coords then
icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
icon:Show()
if icon.overlay then icon.overlay:Show() end
else
icon:Hide()
if icon.overlay then icon.overlay:Hide() end
end
end
--------------------------------------------------------------------------------
-- UI Icon helpers (icon.tga sprite sheet)
-- SFrames.ICON_PATH and SFrames.ICON_TCOORDS are defined in IconMap.lua
--------------------------------------------------------------------------------
local ICON_SET_VALID = {
["icon"] = true, ["icon2"] = true, ["icon3"] = true, ["icon4"] = true,
["icon5"] = true, ["icon6"] = true, ["icon7"] = true, ["icon8"] = true,
}
local function GetIconPath()
local set = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet
if set and ICON_SET_VALID[set] then
return "Interface\\AddOns\\Nanami-UI\\img\\" .. set
end
return "Interface\\AddOns\\Nanami-UI\\img\\icon"
end
local ICON_TEX = GetIconPath()
local ICON_TC_FALLBACK = {
["logo"] = { 0, 0.125, 0, 0.125 },
["save"] = { 0.125, 0.25, 0, 0.125 },
["close"] = { 0.25, 0.375, 0, 0.125 },
["offline"] = { 0.375, 0.5, 0, 0.125 },
["chat"] = { 0, 0.125, 0.125, 0.25 },
["settings"] = { 0.125, 0.25, 0.125, 0.25 },
["ai"] = { 0.25, 0.375, 0.125, 0.25 },
["backpack"] = { 0.375, 0.5, 0.125, 0.25 },
["exit"] = { 0, 0.125, 0.25, 0.375 },
["party"] = { 0.125, 0.25, 0.25, 0.375 },
["loot"] = { 0.25, 0.375, 0.25, 0.375 },
["dragon"] = { 0.375, 0.5, 0.25, 0.375 },
["casting"] = { 0, 0.125, 0.375, 0.5 },
["attack"] = { 0.125, 0.25, 0.375, 0.5 },
["damage"] = { 0.25, 0.375, 0.375, 0.5 },
["latency"] = { 0.375, 0.5, 0.375, 0.5 },
["admin"] = { 0, 0.125, 0.125, 0.25 },
["bank"] = { 0.25, 0.375, 0.25, 0.375 },
["perf"] = { 0.25, 0.375, 0.375, 0.5 },
}
local ICON_TC = SFrames.ICON_TCOORDS or ICON_TC_FALLBACK
function SFrames:CreateIcon(parent, iconKey, size)
local sz = size or 16
local tex = parent:CreateTexture(nil, "ARTWORK")
tex:SetTexture(GetIconPath())
tex:SetWidth(sz)
tex:SetHeight(sz)
local coords = ICON_TC[iconKey]
if not coords and self.ICON_TCOORDS then
coords = self.ICON_TCOORDS[iconKey]
end
if coords then
tex:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
end
return tex
end
function SFrames:SetIcon(tex, iconKey)
if not tex then return end
tex:SetTexture(GetIconPath())
local coords = ICON_TC[iconKey]
if not coords and self.ICON_TCOORDS then
coords = self.ICON_TCOORDS[iconKey]
end
if coords then
tex:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
tex:Show()
else
tex:Hide()
end
end
function SFrames:CreateIconButton(parent, iconKey, iconSize, label, width, height, onClick)
local A = SFrames.ActiveTheme
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(width or 100)
btn:SetHeight(height or 24)
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
local bgC = (A and A.panelBg) or { 0.12, 0.06, 0.10, 0.9 }
local bdC = (A and A.sepColor) or { 0.45, 0.25, 0.35, 0.8 }
local hvC = (A and A.buttonHoverBg) or { 0.22, 0.12, 0.18, 0.95 }
local hvB = (A and A.accent) or { 0.70, 0.40, 0.55, 1 }
local dnC = (A and A.buttonDownBg) or { 0.08, 0.04, 0.06, 0.95 }
local txC = (A and A.buttonText) or { 0.90, 0.76, 0.84 }
local txH = (A and A.buttonActiveText) or { 1, 0.92, 0.96 }
btn:SetBackdropColor(bgC[1], bgC[2], bgC[3], bgC[4] or 0.9)
btn:SetBackdropBorderColor(bdC[1], bdC[2], bdC[3], bdC[4] or 0.8)
local iSz = iconSize or 14
local ico = self:CreateIcon(btn, iconKey, iSz)
ico:SetPoint("LEFT", btn, "LEFT", 6, 0)
btn.icon = ico
if label and label ~= "" then
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(self:GetFont(), 10, self.Media.fontOutline or "OUTLINE")
fs:SetPoint("LEFT", ico, "RIGHT", 4, 0)
fs:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
fs:SetJustifyH("LEFT")
fs:SetTextColor(txC[1], txC[2], txC[3])
fs:SetText(label)
btn.label = fs
end
btn:SetScript("OnEnter", function()
this:SetBackdropColor(hvC[1], hvC[2], hvC[3], hvC[4] or 0.95)
this:SetBackdropBorderColor(hvB[1], hvB[2], hvB[3], hvB[4] or 1)
if this.label then this.label:SetTextColor(txH[1], txH[2], txH[3]) end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(bgC[1], bgC[2], bgC[3], bgC[4] or 0.9)
this:SetBackdropBorderColor(bdC[1], bdC[2], bdC[3], bdC[4] or 0.8)
if this.label then this.label:SetTextColor(txC[1], txC[2], txC[3]) end
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(dnC[1], dnC[2], dnC[3], dnC[4] or 0.95)
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(hvC[1], hvC[2], hvC[3], hvC[4] or 0.95)
end)
if onClick then
btn:SetScript("OnClick", onClick)
end
return btn
end

1012
FlightData.lua Normal file

File diff suppressed because it is too large Load Diff

1282
FlightMap.lua Normal file

File diff suppressed because it is too large Load Diff

158
Focus.lua Normal file
View File

@@ -0,0 +1,158 @@
SFrames.Focus = {}
local function Trim(text)
if type(text) ~= "string" then return "" end
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
return text
end
function SFrames.Focus:EnsureDB()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Focus then SFramesDB.Focus = {} end
return SFramesDB.Focus
end
function SFrames.Focus:Initialize()
self:EnsureDB()
end
function SFrames.Focus:GetFocusName()
if UnitExists and UnitExists("focus") and UnitName then
local name = UnitName("focus")
if name and name ~= "" then
return name
end
end
local db = self:EnsureDB()
if db.name and db.name ~= "" then
return db.name
end
return nil
end
function SFrames.Focus:SetFromTarget()
if not UnitExists or not UnitExists("target") then
return false, "NO_TARGET"
end
local name = UnitName and UnitName("target")
if not name or name == "" then
return false, "INVALID_TARGET"
end
local db = self:EnsureDB()
db.name = name
db.level = UnitLevel and UnitLevel("target") or nil
local _, classToken = UnitClass and UnitClass("target")
db.class = classToken
local usedNative = false
if FocusUnit then
local ok = pcall(function() FocusUnit("target") end)
if not ok then
ok = pcall(function() FocusUnit() end)
end
if ok and UnitExists and UnitExists("focus") then
usedNative = true
end
end
return true, name, usedNative
end
function SFrames.Focus:Clear()
if ClearFocus then
pcall(ClearFocus)
end
local db = self:EnsureDB()
db.name = nil
db.level = nil
db.class = nil
return true
end
function SFrames.Focus:Target()
if UnitExists and UnitExists("focus") then
TargetUnit("focus")
return true, "NATIVE"
end
local name = self:GetFocusName()
if not name then
return false, "NO_FOCUS"
end
if TargetByName then
TargetByName(name, true)
else
return false, "NO_TARGETBYNAME"
end
if UnitExists and UnitExists("target") and UnitName and UnitName("target") == name then
return true, "NAME"
end
return false, "NOT_FOUND"
end
function SFrames.Focus:Cast(spellName)
local spell = Trim(spellName)
if spell == "" then
return false, "NO_SPELL"
end
if UnitExists and UnitExists("focus") then
CastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit("focus")
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
return false, "BAD_TARGET"
end
return true, "NATIVE"
end
local focusName = self:GetFocusName()
if not focusName then
return false, "NO_FOCUS"
end
local hadTarget = UnitExists and UnitExists("target")
local prevName = hadTarget and UnitName and UnitName("target") or nil
local onFocus = false
if hadTarget and prevName == focusName then
onFocus = true
elseif TargetByName then
TargetByName(focusName, true)
if UnitExists and UnitExists("target") and UnitName and UnitName("target") == focusName then
onFocus = true
end
end
if not onFocus then
return false, "FOCUS_NOT_FOUND"
end
CastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit("target")
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then
TargetLastTarget()
end
return false, "BAD_TARGET"
end
if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then
TargetLastTarget()
end
return true, "NAME"
end

346
GameMenu.lua Normal file
View File

@@ -0,0 +1,346 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Game Menu (GameMenu.lua)
-- Reskins the ESC menu (GameMenuFrame) with pink cat-paw theme
--------------------------------------------------------------------------------
SFrames.GameMenu = {}
local GM = SFrames.GameMenu
--------------------------------------------------------------------------------
-- Theme: Pink Cat-Paw
--------------------------------------------------------------------------------
local T = SFrames.ActiveTheme
local BUTTON_W = 170
local BUTTON_H = 26
local BUTTON_GAP = 5
local SIDE_PAD = 16
local HEADER_H = 32
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local bg = bgColor or T.panelBg
local bd = borderColor or T.panelBorder
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end
local function CreateShadow(parent, size)
local s = CreateFrame("Frame", nil, parent)
local sz = size or 4
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.55)
s:SetBackdropBorderColor(0, 0, 0, 0.4)
return s
end
local function HideTexture(tex)
if not tex then return end
if tex.SetTexture then tex:SetTexture(nil) end
if tex.SetAlpha then tex:SetAlpha(0) end
if tex.Hide then tex:Hide() end
end
--------------------------------------------------------------------------------
-- Button Styling
--------------------------------------------------------------------------------
local MENU_BUTTON_ICONS = {
["GameMenuButtonContinue"] = "exit",
["GameMenuButtonOptions"] = "settings",
["GameMenuButtonSoundOptions"] = "sound",
["GameMenuButtonUIOptions"] = "talent",
["GameMenuButtonKeybindings"] = "menu",
["GameMenuButtonRatings"] = "backpack",
["GameMenuButtonMacros"] = "ai",
["GameMenuButtonLogout"] = "close",
["GameMenuButtonQuit"] = "logout",
}
local function StyleMenuButton(btn)
if not btn or btn.nanamiStyled then return end
btn.nanamiStyled = true
HideTexture(btn:GetNormalTexture())
HideTexture(btn:GetPushedTexture())
HideTexture(btn:GetHighlightTexture())
HideTexture(btn:GetDisabledTexture())
local name = btn:GetName() or ""
for _, suffix in ipairs({ "Left", "Right", "Middle" }) do
local tex = _G[name .. suffix]
if tex then tex:SetAlpha(0); tex:Hide() end
end
SetRoundBackdrop(btn, T.btnBg, T.btnBorder)
btn:SetWidth(BUTTON_W)
btn:SetHeight(BUTTON_H)
local iconKey = MENU_BUTTON_ICONS[name]
local icoSize = 12
local gap = 4
local fs = btn:GetFontString()
if fs then
fs:SetFont(GetFont(), 11, "OUTLINE")
fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
fs:ClearAllPoints()
if iconKey and SFrames and SFrames.CreateIcon then
local ico = SFrames:CreateIcon(btn, iconKey, icoSize)
ico:SetDrawLayer("OVERLAY")
ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3])
fs:SetPoint("CENTER", btn, "CENTER", (icoSize + gap) / 2, 0)
ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0)
btn.nanamiIcon = ico
else
fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
end
end
local origEnter = btn:GetScript("OnEnter")
local origLeave = btn:GetScript("OnLeave")
local origDown = btn:GetScript("OnMouseDown")
local origUp = btn:GetScript("OnMouseUp")
btn:SetScript("OnEnter", function()
if origEnter then origEnter() end
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBorder[1], T.btnHoverBorder[2], T.btnHoverBorder[3], T.btnHoverBorder[4])
local txt = this:GetFontString()
if txt then txt:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end
end)
btn:SetScript("OnLeave", function()
if origLeave then origLeave() end
this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
local txt = this:GetFontString()
if txt then txt:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3]) end
end)
btn:SetScript("OnMouseDown", function()
if origDown then origDown() end
this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
if origUp then origUp() end
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end)
end
--------------------------------------------------------------------------------
-- Create Settings Button
--------------------------------------------------------------------------------
local settingsBtn
local function CreateSettingsButton(parent)
if settingsBtn then return settingsBtn end
local btn = CreateFrame("Button", "GameMenuButtonNanamiUI", parent)
btn:SetWidth(BUTTON_W)
btn:SetHeight(BUTTON_H)
SetRoundBackdrop(btn, T.btnBg, T.btnBorder)
local icoSize = 14
local gap = 4
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), 11, "OUTLINE")
fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
fs:SetPoint("CENTER", btn, "CENTER", (icoSize + gap) / 2, 0)
fs:SetText("Nanami-UI 设置")
local ico = SFrames:CreateIcon(btn, "logo", icoSize)
ico:SetDrawLayer("OVERLAY")
ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3])
ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0)
btn.nanamiIcon = ico
btn:SetScript("OnClick", function()
HideUIPanel(GameMenuFrame)
if SFrames.ConfigUI and SFrames.ConfigUI.Build then
SFrames.ConfigUI:Build("ui")
end
end)
btn:SetScript("OnEnter", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBorder[1], T.btnHoverBorder[2], T.btnHoverBorder[3], T.btnHoverBorder[4])
fs:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
ico:SetVertexColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3])
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end)
btn.nanamiStyled = true
settingsBtn = btn
return btn
end
--------------------------------------------------------------------------------
-- Known button priority order (covers vanilla + Turtle WoW extras)
--------------------------------------------------------------------------------
local BUTTON_ORDER = {
"GameMenuButtonContinue",
"__NANAMI_SETTINGS__",
"GameMenuButtonOptions",
"GameMenuButtonSoundOptions",
"GameMenuButtonUIOptions",
"GameMenuButtonKeybindings",
"GameMenuButtonRatings",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
}
--------------------------------------------------------------------------------
-- Frame Styling (called once at PLAYER_LOGIN, before first show)
--------------------------------------------------------------------------------
local styled = false
local function StyleGameMenuFrame()
if styled then return end
if not GameMenuFrame then return end
styled = true
-- Hide all default background textures and header text
local regions = { GameMenuFrame:GetRegions() }
for _, region in ipairs(regions) do
if region then
local otype = region:GetObjectType()
if otype == "Texture" then
region:SetTexture(nil)
region:SetAlpha(0)
region:Hide()
elseif otype == "FontString" then
region:SetAlpha(0)
region:Hide()
end
end
end
SetRoundBackdrop(GameMenuFrame, T.panelBg, T.panelBorder)
CreateShadow(GameMenuFrame, 5)
-- Title
local title = GameMenuFrame:CreateFontString(nil, "OVERLAY")
title:SetFont(GetFont(), 13, "OUTLINE")
title:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
title:SetPoint("TOP", GameMenuFrame, "TOP", 0, -11)
title:SetText("Nanami-UI")
-- Create settings button
local sBt = CreateSettingsButton(GameMenuFrame)
-- Build a lookup of known names for quick check
local knownSet = {}
for _, name in ipairs(BUTTON_ORDER) do
if name ~= "__NANAMI_SETTINGS__" then
knownSet[name] = true
end
end
-- Collect all child buttons that are NOT the settings button
local children = { GameMenuFrame:GetChildren() }
local unknownBtns = {}
for _, child in ipairs(children) do
if child and child:GetObjectType() == "Button" and child ~= sBt then
local cname = child:GetName()
if cname and not knownSet[cname] then
table.insert(unknownBtns, child)
end
end
end
-- Build final ordered button list from BUTTON_ORDER
local orderedBtns = {}
for _, name in ipairs(BUTTON_ORDER) do
if name == "__NANAMI_SETTINGS__" then
table.insert(orderedBtns, sBt)
else
local btn = _G[name]
if btn then
StyleMenuButton(btn)
table.insert(orderedBtns, btn)
end
end
end
-- Append unknown / Turtle WoW extra buttons before Logout & Quit
local insertBefore = table.getn(orderedBtns)
for i = table.getn(orderedBtns), 1, -1 do
local bname = orderedBtns[i]:GetName() or ""
if bname == "GameMenuButtonLogout" or bname == "GameMenuButtonQuit" then
insertBefore = i
else
break
end
end
for _, btn in ipairs(unknownBtns) do
StyleMenuButton(btn)
table.insert(orderedBtns, insertBefore, btn)
insertBefore = insertBefore + 1
end
-- Layout vertically
local numBtns = table.getn(orderedBtns)
local totalH = numBtns * BUTTON_H + (numBtns - 1) * BUTTON_GAP
local frameH = HEADER_H + SIDE_PAD + totalH + SIDE_PAD
local frameW = BUTTON_W + SIDE_PAD * 2
GameMenuFrame:SetWidth(frameW)
GameMenuFrame:SetHeight(frameH)
local startY = -(HEADER_H + SIDE_PAD)
for i, btn in ipairs(orderedBtns) do
btn:ClearAllPoints()
btn:SetPoint("TOP", GameMenuFrame, "TOP", 0, startY - (i - 1) * (BUTTON_H + BUTTON_GAP))
end
end
--------------------------------------------------------------------------------
-- Hook: style at login, BEFORE any show (avoids OnShow size-change issues)
--------------------------------------------------------------------------------
local hookFrame = CreateFrame("Frame")
hookFrame:RegisterEvent("PLAYER_LOGIN")
hookFrame:SetScript("OnEvent", function()
if GameMenuFrame then
StyleGameMenuFrame()
end
end)

1133
GearScore.lua Normal file

File diff suppressed because it is too large Load Diff

108
IconMap.lua Normal file
View File

@@ -0,0 +1,108 @@
--------------------------------------------------------------------------------
-- Nanami-UI Icon Map (icon.tga)
--
-- Texture: Interface\AddOns\Nanami-UI\img\icon
-- Size: 512x512 (8x8 grid, each cell 64x64)
-- Format: { left, right, top, bottom } tex coords
--
-- Step = 1/8 = 0.125 per cell
--
-- Layout (8 columns x 8 rows):
-- Row 1: logo | save | close | offline | quest | alliance | horde | character
-- Row 2: chat | settings | ai | backpack | mount | achieve | gold | friends
-- Row 3: exit | party | loot | dragon | profession | logout | worldmap | talent
-- Row 4: casting | attack | damage | latency | mail | questlog | spellbook | merchant
-- Row 5: star | potion | skull | heal | house | scroll | herb | key
-- Row 6: tank | auction | fishing | calendar | dungeon | lfg | charsheet | help
-- Row 7: sound | search | honor | menu | store | buff | ranged | speed
-- Row 8: energy | poison | armor | alchemy | cooking | camp | hearthstone| mining
--------------------------------------------------------------------------------
SFrames.ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\icon"
SFrames.ICON_TCOORDS = {
-- Row 1 (top = 0, bottom = 0.125)
["logo"] = { 0, 0.125, 0, 0.125 }, -- R1C1
["save"] = { 0.125, 0.25, 0, 0.125 }, -- R1C2
["close"] = { 0.25, 0.375, 0, 0.125 }, -- R1C3
["offline"] = { 0.375, 0.5, 0, 0.125 }, -- R1C4
["quest"] = { 0.5, 0.625, 0, 0.125 }, -- R1C5
["alliance"] = { 0.625, 0.75, 0, 0.125 }, -- R1C6
["horde"] = { 0.75, 0.875, 0, 0.125 }, -- R1C7
["character"] = { 0.875, 1.0, 0, 0.125 }, -- R1C8
-- Row 2 (top = 0.125, bottom = 0.25)
["chat"] = { 0, 0.125, 0.125, 0.25 }, -- R2C1
["settings"] = { 0.125, 0.25, 0.125, 0.25 }, -- R2C2
["ai"] = { 0.25, 0.375, 0.125, 0.25 }, -- R2C3
["backpack"] = { 0.375, 0.5, 0.125, 0.25 }, -- R2C4
["mount"] = { 0.5, 0.625, 0.125, 0.25 }, -- R2C5
["achieve"] = { 0.625, 0.75, 0.125, 0.25 }, -- R2C6
["gold"] = { 0.75, 0.875, 0.125, 0.25 }, -- R2C7
["friends"] = { 0.875, 1.0, 0.125, 0.25 }, -- R2C8
-- Row 3 (top = 0.25, bottom = 0.375)
["exit"] = { 0, 0.125, 0.25, 0.375 }, -- R3C1
["party"] = { 0.125, 0.25, 0.25, 0.375 }, -- R3C2
["loot"] = { 0.25, 0.375, 0.25, 0.375 }, -- R3C3
["dragon"] = { 0.375, 0.5, 0.25, 0.375 }, -- R3C4
["profession"] = { 0.5, 0.625, 0.25, 0.375 }, -- R3C5
["logout"] = { 0.625, 0.75, 0.25, 0.375 }, -- R3C6
["worldmap"] = { 0.75, 0.875, 0.25, 0.375 }, -- R3C7
["talent"] = { 0.875, 1.0, 0.25, 0.375 }, -- R3C8
-- Row 4 (top = 0.375, bottom = 0.5)
["casting"] = { 0, 0.125, 0.375, 0.5 }, -- R4C1
["attack"] = { 0.125, 0.25, 0.375, 0.5 }, -- R4C2
["damage"] = { 0.25, 0.375, 0.375, 0.5 }, -- R4C3
["latency"] = { 0.375, 0.5, 0.375, 0.5 }, -- R4C4
["mail"] = { 0.5, 0.625, 0.375, 0.5 }, -- R4C5
["questlog"] = { 0.625, 0.75, 0.375, 0.5 }, -- R4C6
["spellbook"] = { 0.75, 0.875, 0.375, 0.5 }, -- R4C7
["merchant"] = { 0.875, 1.0, 0.375, 0.5 }, -- R4C8
-- Row 5 (top = 0.5, bottom = 0.625)
["star"] = { 0, 0.125, 0.5, 0.625 }, -- R5C1
["potion"] = { 0.125, 0.25, 0.5, 0.625 }, -- R5C2
["skull"] = { 0.25, 0.375, 0.5, 0.625 }, -- R5C3
["heal"] = { 0.375, 0.5, 0.5, 0.625 }, -- R5C4
["house"] = { 0.5, 0.625, 0.5, 0.625 }, -- R5C5
["scroll"] = { 0.625, 0.75, 0.5, 0.625 }, -- R5C6
["herb"] = { 0.75, 0.875, 0.5, 0.625 }, -- R5C7
["key"] = { 0.875, 1.0, 0.5, 0.625 }, -- R5C8
-- Row 6 (top = 0.625, bottom = 0.75)
["tank"] = { 0, 0.125, 0.625, 0.75 }, -- R6C1
["auction"] = { 0.125, 0.25, 0.625, 0.75 }, -- R6C2
["fishing"] = { 0.25, 0.375, 0.625, 0.75 }, -- R6C3
["calendar"] = { 0.375, 0.5, 0.625, 0.75 }, -- R6C4
["dungeon"] = { 0.5, 0.625, 0.625, 0.75 }, -- R6C5
["lfg"] = { 0.625, 0.75, 0.625, 0.75 }, -- R6C6
["charsheet"] = { 0.75, 0.875, 0.625, 0.75 }, -- R6C7
["help"] = { 0.875, 1.0, 0.625, 0.75 }, -- R6C8
-- Row 7 (top = 0.75, bottom = 0.875)
["sound"] = { 0, 0.125, 0.75, 0.875 }, -- R7C1
["search"] = { 0.125, 0.25, 0.75, 0.875 }, -- R7C2
["honor"] = { 0.25, 0.375, 0.75, 0.875 }, -- R7C3
["menu"] = { 0.375, 0.5, 0.75, 0.875 }, -- R7C4
["store"] = { 0.5, 0.625, 0.75, 0.875 }, -- R7C5
["buff"] = { 0.625, 0.75, 0.75, 0.875 }, -- R7C6
["ranged"] = { 0.75, 0.875, 0.75, 0.875 }, -- R7C7
["speed"] = { 0.875, 1.0, 0.75, 0.875 }, -- R7C8
-- Row 8 (top = 0.875, bottom = 1.0)
["energy"] = { 0, 0.125, 0.875, 1.0 }, -- R8C1
["poison"] = { 0.125, 0.25, 0.875, 1.0 }, -- R8C2
["armor"] = { 0.25, 0.375, 0.875, 1.0 }, -- R8C3
["alchemy"] = { 0.375, 0.5, 0.875, 1.0 }, -- R8C4
["cooking"] = { 0.5, 0.625, 0.875, 1.0 }, -- R8C5
["camp"] = { 0.625, 0.75, 0.875, 1.0 }, -- R8C6
["hearthstone"] = { 0.75, 0.875, 0.875, 1.0 }, -- R8C7
["mining"] = { 0.875, 1.0, 0.875, 1.0 }, -- R8C8
-- Legacy aliases
["admin"] = { 0, 0.125, 0.125, 0.25 }, -- -> chat
["bank"] = { 0.25, 0.375, 0.25, 0.375 }, -- -> loot
["perf"] = { 0.25, 0.375, 0.375, 0.5 }, -- -> damage
}

1590
InspectPanel.lua Normal file

File diff suppressed because it is too large Load Diff

1633
Mail.lua Normal file

File diff suppressed because it is too large Load Diff

337
MapIcons.lua Normal file
View File

@@ -0,0 +1,337 @@
--------------------------------------------------------------------------------
-- Nanami-UI: MapIcons - Class-colored party/raid icons on maps
-- Shows class icon circles on World Map, Battlefield Minimap, and Minimap
-- Uses UI-Classes-Circles.tga for class-specific circular portraits
-- Zone size data: prefers pfQuest DB, falls back to built-in table
--------------------------------------------------------------------------------
SFrames.MapIcons = SFrames.MapIcons or {}
local MI = SFrames.MapIcons
local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles"
local CLASS_ICON_TCOORDS = SFrames.CLASS_ICON_TCOORDS
local CLASS_COLORS = SFrames.Config.colors.class
--------------------------------------------------------------------------------
-- Minimap zoom yard ranges: [indoor/outdoor][zoomLevel] = diameter in yards
-- indoor=0, outdoor=1
--------------------------------------------------------------------------------
local MM_ZOOM = {
[0] = { [0]=300, [1]=240, [2]=180, [3]=120, [4]=80, [5]=50 },
[1] = { [0]=466.67, [1]=400, [2]=333.33, [3]=266.67, [4]=200, [5]=133.33 },
}
--------------------------------------------------------------------------------
-- Built-in zone sizes (width, height in yards) keyed by GetMapInfo() file name
-- Fallback when pfQuest is not installed
--------------------------------------------------------------------------------
local ZONE_SIZES = {
["Ashenvale"] = { 5766.67, 3843.75 },
["Aszhara"] = { 5533.33, 3689.58 },
["Darkshore"] = { 6550.00, 4366.66 },
["Desolace"] = { 4600.00, 3066.67 },
["Durotar"] = { 4925.00, 3283.34 },
["Dustwallow"] = { 5250.00, 3500.00 },
["Felwood"] = { 5750.00, 3833.33 },
["Feralas"] = { 6950.00, 4633.33 },
["Moonglade"] = { 2308.33, 1539.59 },
["Mulgore"] = { 5250.00, 3500.00 },
["Silithus"] = { 3483.33, 2322.92 },
["StonetalonMountains"] = { 4883.33, 3256.25 },
["Tanaris"] = { 6900.00, 4600.00 },
["Teldrassil"] = { 4925.00, 3283.34 },
["Barrens"] = { 10133.34, 6756.25 },
["ThousandNeedles"] = { 4400.00, 2933.33 },
["UngoroCrater"] = { 3677.08, 2452.08 },
["Winterspring"] = { 7100.00, 4733.33 },
["Alterac"] = { 2800.00, 1866.67 },
["ArathiHighlands"] = { 3600.00, 2400.00 },
["Badlands"] = { 2487.50, 1658.34 },
["BlastedLands"] = { 3350.00, 2233.30 },
["BurningSteppes"] = { 2929.16, 1952.08 },
["DeadwindPass"] = { 2500.00, 1666.63 },
["DunMorogh"] = { 4925.00, 3283.34 },
["Duskwood"] = { 2700.00, 1800.03 },
["EasternPlaguelands"] = { 4031.25, 2687.50 },
["ElwynnForest"] = { 3470.84, 2314.62 },
["Hilsbrad"] = { 3200.00, 2133.33 },
["Hinterlands"] = { 3850.00, 2566.67 },
["LochModan"] = { 2758.33, 1839.58 },
["RedridgeMountains"] = { 2170.84, 1447.90 },
["SearingGorge"] = { 1837.50, 1225.00 },
["SilverpineForest"] = { 4200.00, 2800.00 },
["Stranglethorn"] = { 6381.25, 4254.10 },
["SwampOfSorrows"] = { 2293.75, 1529.17 },
["Tirisfal"] = { 4518.75, 3012.50 },
["WesternPlaguelands"] = { 4300.00, 2866.67 },
["Westfall"] = { 3500.00, 2333.30 },
["Wetlands"] = { 4300.00, 2866.67 },
}
--------------------------------------------------------------------------------
-- Indoor detection (CVar trick from pfQuest)
-- Returns 0 = indoor, 1 = outdoor
--------------------------------------------------------------------------------
local cachedIndoor = 1
local indoorCheckTime = 0
local function DetectIndoor()
if pfMap and pfMap.minimap_indoor then
return pfMap.minimap_indoor()
end
local ok1, zoomVal = pcall(GetCVar, "minimapZoom")
local ok2, insideVal = pcall(GetCVar, "minimapInsideZoom")
if not ok1 or not ok2 then return 1 end
local tempzoom = 0
local state = 1
if zoomVal == insideVal then
local cur = Minimap:GetZoom()
if cur >= 3 then
Minimap:SetZoom(cur - 1)
tempzoom = 1
else
Minimap:SetZoom(cur + 1)
tempzoom = -1
end
end
local ok3, zoomVal2 = pcall(GetCVar, "minimapZoom")
local ok4, insideVal2 = pcall(GetCVar, "minimapInsideZoom")
if ok3 and ok4 and zoomVal2 ~= insideVal2 then
state = 0
end
if tempzoom ~= 0 then
Minimap:SetZoom(Minimap:GetZoom() + tempzoom)
end
return state
end
--------------------------------------------------------------------------------
-- Get current zone dimensions in yards
--------------------------------------------------------------------------------
local function GetZoneYards()
if pfMap and pfMap.GetMapIDByName and pfDB and pfDB["minimap"] then
local name = GetRealZoneText and GetRealZoneText() or ""
if name ~= "" then
local id = pfMap:GetMapIDByName(name)
if id and pfDB["minimap"][id] then
return pfDB["minimap"][id][1], pfDB["minimap"][id][2]
end
end
end
if GetMapInfo then
local ok, info = pcall(GetMapInfo)
if ok and info and ZONE_SIZES[info] then
return ZONE_SIZES[info][1], ZONE_SIZES[info][2]
end
end
return nil, nil
end
--------------------------------------------------------------------------------
-- 1. World Map + Battlefield Minimap: class icon overlays
--------------------------------------------------------------------------------
local mapButtons
local function InitMapButtons()
if mapButtons then return end
mapButtons = {}
for i = 1, 4 do
mapButtons["WorldMapParty" .. i] = "party" .. i
mapButtons["BattlefieldMinimapParty" .. i] = "party" .. i
end
for i = 1, 40 do
mapButtons["WorldMapRaid" .. i] = "raid" .. i
mapButtons["BattlefieldMinimapRaid" .. i] = "raid" .. i
end
end
local mapTickTime = 0
local function UpdateMapClassIcons()
mapTickTime = mapTickTime + (arg1 or 0)
if mapTickTime < 0.15 then return end
mapTickTime = 0
if not mapButtons then InitMapButtons() end
local _G = getfenv(0)
for name, unit in pairs(mapButtons) do
local frame = _G[name]
if frame and frame:IsVisible() and UnitExists(unit) then
local defIcon = _G[name .. "Icon"]
if defIcon then defIcon:SetTexture() end
if not frame.nanamiClassTex then
frame.nanamiClassTex = frame:CreateTexture(nil, "OVERLAY")
frame.nanamiClassTex:SetTexture(CLASS_ICON_PATH)
frame.nanamiClassTex:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2)
frame.nanamiClassTex:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2)
end
local _, class = UnitClass(unit)
if class and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[class] then
local tc = CLASS_ICON_TCOORDS[class]
frame.nanamiClassTex:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
frame.nanamiClassTex:Show()
else
frame.nanamiClassTex:Hide()
end
end
end
end
--------------------------------------------------------------------------------
-- 2. Minimap: class-colored party dot overlays
--------------------------------------------------------------------------------
local MAX_PARTY = 4
local mmDots = {}
local function CreateMinimapDot(index)
local dot = CreateFrame("Frame", "NanamiMMDot" .. index, Minimap)
dot:SetWidth(10)
dot:SetHeight(10)
dot:SetFrameStrata("MEDIUM")
dot:SetFrameLevel(Minimap:GetFrameLevel() + 5)
dot.icon = dot:CreateTexture(nil, "ARTWORK")
dot.icon:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
dot.icon:SetAllPoints()
dot.icon:SetVertexColor(1, 0.82, 0, 1)
dot:Hide()
return dot
end
local mmTickTime = 0
local function UpdateMinimapDots()
mmTickTime = mmTickTime + (arg1 or 0)
if mmTickTime < 0.25 then return end
mmTickTime = 0
if not Minimap or not Minimap:IsVisible() then
for i = 1, MAX_PARTY do
if mmDots[i] then mmDots[i]:Hide() end
end
return
end
local numParty = GetNumPartyMembers and GetNumPartyMembers() or 0
if numParty == 0 then
for i = 1, MAX_PARTY do
if mmDots[i] then mmDots[i]:Hide() end
end
return
end
if WorldMapFrame and WorldMapFrame:IsVisible() then
return
end
if SetMapToCurrentZone then
pcall(SetMapToCurrentZone)
end
local px, py = GetPlayerMapPosition("player")
if not px or not py or (px == 0 and py == 0) then
for i = 1, MAX_PARTY do
if mmDots[i] then mmDots[i]:Hide() end
end
return
end
local zw, zh = GetZoneYards()
if not zw or not zh or zw == 0 or zh == 0 then
for i = 1, MAX_PARTY do
if mmDots[i] then mmDots[i]:Hide() end
end
return
end
local now = GetTime()
if now - indoorCheckTime > 3 then
indoorCheckTime = now
cachedIndoor = DetectIndoor()
end
local zoom = Minimap:GetZoom()
local mmYards = MM_ZOOM[cachedIndoor] and MM_ZOOM[cachedIndoor][zoom]
or MM_ZOOM[1][zoom] or 466.67
local mmHalfYards = mmYards / 2
local mmHalfPx = Minimap:GetWidth() / 2
local facing = 0
local doRotate = false
local okCvar, rotateVal = pcall(GetCVar, "rotateMinimap")
if okCvar and rotateVal == "1" and GetPlayerFacing then
local ok2, f = pcall(GetPlayerFacing)
if ok2 and f then
facing = f
doRotate = true
end
end
for i = 1, MAX_PARTY do
local unit = "party" .. i
if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then
local mx, my = GetPlayerMapPosition(unit)
if mx and my and (mx ~= 0 or my ~= 0) then
local dx = (mx - px) * zw
local dy = (py - my) * zh
if doRotate then
local s = math.sin(facing)
local c = math.cos(facing)
dx, dy = dx * c + dy * s, -dx * s + dy * c
end
local dist = math.sqrt(dx * dx + dy * dy)
if dist < mmHalfYards * 0.92 then
local scale = mmHalfPx / mmHalfYards
if not mmDots[i] then
mmDots[i] = CreateMinimapDot(i)
end
local dot = mmDots[i]
local _, class = UnitClass(unit)
local cc = class and CLASS_COLORS and CLASS_COLORS[class]
if cc then
dot.icon:SetVertexColor(cc.r, cc.g, cc.b, 1)
else
dot.icon:SetVertexColor(1, 0.82, 0, 1)
end
dot:ClearAllPoints()
dot:SetPoint("CENTER", Minimap, "CENTER", dx * scale, dy * scale)
dot:Show()
else
if mmDots[i] then mmDots[i]:Hide() end
end
else
if mmDots[i] then mmDots[i]:Hide() end
end
else
if mmDots[i] then mmDots[i]:Hide() end
end
end
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function MI:Initialize()
InitMapButtons()
local updater = CreateFrame("Frame", "NanamiMapIconsUpdater", UIParent)
updater._elapsed = 0
updater:SetScript("OnUpdate", function()
this._elapsed = (this._elapsed or 0) + arg1
if this._elapsed < 0.2 then return end
this._elapsed = 0
UpdateMapClassIcons()
UpdateMinimapDots()
end)
SFrames:Print("地图职业图标模块已加载")
end

287
MapReveal.lua Normal file
View File

@@ -0,0 +1,287 @@
--------------------------------------------------------------------------------
-- Nanami-UI: MapReveal -- Reveal unexplored world map areas
-- Adapted from ShaguTweaks-extras worldmap-reveal approach
-- Uses LibMapOverlayData (from !Libs) supplemented with Turtle WoW zones
--------------------------------------------------------------------------------
SFrames.MapReveal = SFrames.MapReveal or {}
local MapReveal = SFrames.MapReveal
local origWorldMapFrame_Update = nil
local overlayDBPatched = false
local errata = {
["Interface\\WorldMap\\Tirisfal\\BRIGHTWATERLAKE"] = { offsetX = { 587, 584 } },
["Interface\\WorldMap\\Silverpine\\BERENSPERIL"] = { offsetY = { 417, 415 } },
}
-- Turtle WoW new/modified zones not present in LibMapOverlayData
local TurtleWoW_Zones = {
["StonetalonMountains"] = {
"SUNROCKRETREAT:512:256:256:256", "WINDSHEARCRAG:256:256:512:256",
"MIRKFALLONLAKE:512:512:256:0", "THECHARREDVALE:256:512:256:256",
"STONETALONPEAK:256:256:256:0", "WEBWINDERPATH:256:512:512:256",
"AMANIALOR:512:256:0:0", "GRIMTOTEMPOST:512:256:512:512",
"CAMPAPARAJE:512:256:512:512", "MALAKAJIN:512:256:512:512",
"BOULDERSLIDERAVINE:256:256:512:512", "SISHIRCANYON:256:512:512:256",
"VENTURECOMPANYCAMP:256:512:256:0", "BLACKSANDOILFIELDS:512:512:0:0",
"POWDERTOWN:256:256:256:256", "BRAMBLETHORNPASS:512:512:512:256",
"BAELHARDUL:512:256:512:256", "BROKENCLIFFMINE:256:512:256:0",
"THEEARTHENRING:512:256:256:256",
},
["UpperKarazhan2f"] = {
"OUTLAND:1024:768:0:0",
},
["GrimReaches"] = {
"DUNKITHAS:512:256:256:256", "THEGRIMHOLLOW:512:512:256:256",
"LAKEKITHAS:512:256:256:256", "SLATEBEARDSFORGE:512:256:256:256",
"THEHIGHPASS:256:256:256:256", "SALGAZMINES:256:256:512:256",
"EASTRIDGEOUTPOST:512:512:256:0", "BAGGOTHSRAMPART:256:512:256:0",
"RUINSOFSTOLGAZKEEP:256:256:256:0", "GROLDANSEXCAVATION:256:512:512:0",
"ZARMGETHSTRONGHOLD:512:256:256:0", "GETHKAR:512:256:256:0",
"ZARMGETHPOINT:512:256:256:0", "SHATTERBLADEPOST:256:256:512:0",
"BRANGARSFOLLY:256:256:512:0", "BARLEYCRESTFARMSTEAD:512:512:256:0",
},
["Balor"] = {
"GULLWINGWRECKAGE:512:256:0:0", "BILGERATCOMPOUND:256:256:256:0",
"SIOUTPOST:256:512:512:256", "RUINSOFBREEZEHAVEN:512:256:256:256",
"CROAKINGPLATEAU:512:512:256:0", "LANGSTONORCHARD:256:256:256:256",
"SORROWMORELAKE:256:256:256:256", "SCURRYINGTHICKET:256:512:256:0",
"STORMWROUGHTCASTLE:512:256:256:256", "STORMREAVERSPIRE:512:512:256:256",
"WINDROCKCLIFFS:256:512:256:256", "TREACHEROUSCRAGS:512:512:256:256",
"VANDERFARMSTEAD:256:256:256:256", "GRAHANESTATE:256:256:256:256",
"STORMBREAKERPOINT:512:512:512:0",
},
["Northwind"] = {
"MERCHANTSHIGHROAD:512:512:256:256", "AMBERSHIRE:512:256:256:256",
"AMBERWOODKEEP:512:256:0:256", "CRYSTALFALLS:512:512:512:256",
"CRAWFORDWINERY:256:256:512:256", "NORTHWINDLOGGINGCAMP:256:512:256:0",
"WITCHCOVEN:256:256:256:0", "RUINSOFBIRKHAVEN:512:512:512:0",
"SHERWOODQUARRY:512:512:512:0", "BLACKROCKBREACH:512:512:512:0",
"GRIMMENLAKE:256:512:512:256", "ABBEYGARDENS:256:256:512:0",
"STILLHEARTPORT:512:512:0:0", "TOWEROFMAGILOU:512:512:0:0",
"BRISTLEWHISKERCAVERN:256:512:512:0", "NORTHRIDGEPOINT:512:512:256:0",
"CINDERFALLPASS:512:512:512:256",
},
}
local function IsTurtleWoW()
return TargetHPText and TargetHPPercText
end
local function GetOverlayDB()
return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
end
local function PatchOverlayDB()
if overlayDBPatched then return end
overlayDBPatched = true
if not IsTurtleWoW() then return end
local db = GetOverlayDB()
if not db then return end
for zone, data in pairs(TurtleWoW_Zones) do
db[zone] = data
end
end
local function GetConfig()
if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then
return { enabled = true, unexploredAlpha = 0.7 }
end
return SFramesDB.MapReveal
end
local function NextPowerOf2(n)
local p = 16
while p < n do
p = p * 2
end
return p
end
local function DoMapRevealUpdate()
local db = GetOverlayDB()
if not db then return end
local mapFileName = GetMapInfo and GetMapInfo()
if not mapFileName then mapFileName = "World" end
local zoneData = db[mapFileName]
if not zoneData then return end
local prefix = "Interface\\WorldMap\\" .. mapFileName .. "\\"
local numExploredOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0
local explored = {}
for i = 1, numExploredOverlays do
local textureName = GetMapOverlayInfo(i)
if textureName and textureName ~= "" then
explored[textureName] = true
end
end
local cfg = GetConfig()
local dimR, dimG, dimB = 0.4, 0.4, 0.4
if cfg.unexploredAlpha then
dimR = cfg.unexploredAlpha
dimG = cfg.unexploredAlpha
dimB = cfg.unexploredAlpha
end
local textureCount = 0
for idx = 1, table.getn(zoneData) do
local entry = zoneData[idx]
local _, _, name, sW, sH, sX, sY = string.find(entry, "^(%u+):(%d+):(%d+):(%d+):(%d+)$")
if not name then
_, _, name, sW, sH, sX, sY = string.find(entry, "^([^:]+):(%d+):(%d+):(%d+):(%d+)$")
end
if name then
local textureWidth = tonumber(sW)
local textureHeight = tonumber(sH)
local offsetX = tonumber(sX)
local offsetY = tonumber(sY)
local textureName = prefix .. name
local isExplored = explored[textureName]
if cfg.enabled or isExplored then
if errata[textureName] then
local e = errata[textureName]
if e.offsetX and e.offsetX[1] == offsetX then
offsetX = e.offsetX[2]
end
if e.offsetY and e.offsetY[1] == offsetY then
offsetY = e.offsetY[2]
end
end
local numTexturesHorz = math.ceil(textureWidth / 256)
local numTexturesVert = math.ceil(textureHeight / 256)
local neededTextures = textureCount + (numTexturesHorz * numTexturesVert)
if neededTextures > NUM_WORLDMAP_OVERLAYS then
for j = NUM_WORLDMAP_OVERLAYS + 1, neededTextures do
WorldMapDetailFrame:CreateTexture("WorldMapOverlay" .. j, "ARTWORK")
end
NUM_WORLDMAP_OVERLAYS = neededTextures
end
for row = 1, numTexturesVert do
local texturePixelHeight, textureFileHeight
if row < numTexturesVert then
texturePixelHeight = 256
textureFileHeight = 256
else
texturePixelHeight = math.mod(textureHeight, 256)
if texturePixelHeight == 0 then texturePixelHeight = 256 end
textureFileHeight = NextPowerOf2(texturePixelHeight)
end
for col = 1, numTexturesHorz do
if textureCount > NUM_WORLDMAP_OVERLAYS then return end
local texture = _G["WorldMapOverlay" .. (textureCount + 1)]
local texturePixelWidth, textureFileWidth
if col < numTexturesHorz then
texturePixelWidth = 256
textureFileWidth = 256
else
texturePixelWidth = math.mod(textureWidth, 256)
if texturePixelWidth == 0 then texturePixelWidth = 256 end
textureFileWidth = NextPowerOf2(texturePixelWidth)
end
texture:SetWidth(texturePixelWidth)
texture:SetHeight(texturePixelHeight)
texture:SetTexCoord(0, texturePixelWidth / textureFileWidth,
0, texturePixelHeight / textureFileHeight)
texture:ClearAllPoints()
texture:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT",
offsetX + (256 * (col - 1)),
-(offsetY + (256 * (row - 1))))
local tileIndex = ((row - 1) * numTexturesHorz) + col
texture:SetTexture(textureName .. tileIndex)
if not isExplored then
texture:SetVertexColor(dimR, dimG, dimB, 1)
else
texture:SetVertexColor(1, 1, 1, 1)
end
texture:Show()
textureCount = textureCount + 1
end
end
end
end
end
end
function MapReveal:Initialize()
local db = GetOverlayDB()
if not db then
SFrames:Print("MapReveal: LibMapOverlayData 未找到,地图揭示功能不可用。")
return
end
PatchOverlayDB()
if not origWorldMapFrame_Update and WorldMapFrame_Update then
origWorldMapFrame_Update = WorldMapFrame_Update
WorldMapFrame_Update = function()
for i = 1, NUM_WORLDMAP_OVERLAYS do
local tex = _G["WorldMapOverlay" .. i]
if tex then tex:Hide() end
end
origWorldMapFrame_Update()
local cfg = GetConfig()
if cfg.enabled then
DoMapRevealUpdate()
end
end
end
end
function MapReveal:Toggle()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.MapReveal) ~= "table" then
SFramesDB.MapReveal = { enabled = true, unexploredAlpha = 0.7 }
end
SFramesDB.MapReveal.enabled = not SFramesDB.MapReveal.enabled
if SFramesDB.MapReveal.enabled then
SFrames:Print("地图迷雾揭示: |cff00ff00已开启|r")
if not origWorldMapFrame_Update and WorldMapFrame_Update then
self:Initialize()
end
else
SFrames:Print("地图迷雾揭示: |cffff0000已关闭|r")
end
if WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
function MapReveal:SetAlpha(alpha)
if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then return end
SFramesDB.MapReveal.unexploredAlpha = alpha
if SFramesDB.MapReveal.enabled and WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
function MapReveal:Refresh()
if WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end

47
Media.lua Normal file
View File

@@ -0,0 +1,47 @@
SFrames.Media = {
-- Default ElvUI-like texture
-- Default ElvUI-like texture
-- Using the standard flat Vanilla UI status bar texture for a clean flat aesthetic
statusbar = "Interface\\TargetingFrame\\UI-StatusBar",
-- Fonts
-- We can use a default WoW font, or eventually load a custom one here.
font = "Fonts\\ARIALN.TTF",
fontOutline = "OUTLINE",
}
function SFrames:GetSharedMedia()
if LibStub then
local ok, LSM = pcall(function() return LibStub("LibSharedMedia-3.0", true) end)
if ok and LSM then return LSM end
end
return nil
end
function SFrames:GetTexture()
if SFramesDB and SFramesDB.barTexture then
local LSM = self:GetSharedMedia()
if LSM then
local path = LSM:Fetch("statusbar", SFramesDB.barTexture, true)
if path then return path end
end
end
return self.Media.statusbar
end
function SFrames:GetFont()
if SFramesDB and SFramesDB.fontName then
local LSM = self:GetSharedMedia()
if LSM then
local path = LSM:Fetch("font", SFramesDB.fontName, true)
if path then return path end
end
end
return self.Media.font
end
function SFrames:GetSharedMediaList(mediaType)
local LSM = self:GetSharedMedia()
if LSM and LSM.List then return LSM:List(mediaType) end
return nil
end

1110
Merchant.lua Normal file

File diff suppressed because it is too large Load Diff

590
Minimap.lua Normal file
View File

@@ -0,0 +1,590 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Minimap Skin (Minimap.lua)
-- Custom cat-paw pixel art frame for the minimap
--------------------------------------------------------------------------------
SFrames.Minimap = SFrames.Minimap or {}
local MM = SFrames.Minimap
local _A = SFrames.ActiveTheme
local MAP_STYLES = {
{ key = "map", tex = "Interface\\AddOns\\Nanami-UI\\img\\map", label = "猫爪", plateY = -10, textColor = {0.45, 0.32, 0.20} },
{ key = "zs", tex = "Interface\\AddOns\\Nanami-UI\\img\\zs", label = "战士", plateY = -6, textColor = {1, 1, 1} },
{ key = "qs", tex = "Interface\\AddOns\\Nanami-UI\\img\\qs", label = "圣骑士", plateY = -6, textColor = {0.22, 0.13, 0.07} },
{ key = "lr", tex = "Interface\\AddOns\\Nanami-UI\\img\\lr", label = "猎人", plateY = -6, textColor = {1, 1, 1} },
{ key = "qxz", tex = "Interface\\AddOns\\Nanami-UI\\img\\qxz", label = "潜行者", plateY = -6, textColor = {1, 1, 1} },
{ key = "ms", tex = "Interface\\AddOns\\Nanami-UI\\img\\ms", label = "牧师", plateY = -6, textColor = {0.22, 0.13, 0.07} },
{ key = "sm", tex = "Interface\\AddOns\\Nanami-UI\\img\\sm", label = "萨满", plateY = -6, textColor = {1, 1, 1} },
{ key = "fs", tex = "Interface\\AddOns\\Nanami-UI\\img\\fs", label = "法师", plateY = -6, textColor = {1, 1, 1} },
{ key = "ss", tex = "Interface\\AddOns\\Nanami-UI\\img\\ss", label = "术士", plateY = -6, textColor = {1, 1, 1} },
{ key = "dly", tex = "Interface\\AddOns\\Nanami-UI\\img\\dly", label = "德鲁伊", plateY = -6, textColor = {0.22, 0.13, 0.07} },
}
local TEX_SIZE = 512
local CIRCLE_CX = 256
local CIRCLE_CY = 256
local CIRCLE_R = 210
local PLATE_X = 103
local PLATE_Y = 29
local PLATE_W = 200
local PLATE_H = 66
local BASE_SIZE = 180
local CLASS_STYLE_MAP = {
["Warrior"] = "zs", ["WARRIOR"] = "zs", ["战士"] = "zs",
["Paladin"] = "qs", ["PALADIN"] = "qs", ["圣骑士"] = "qs",
["Hunter"] = "lr", ["HUNTER"] = "lr", ["猎人"] = "lr",
["Rogue"] = "qxz", ["ROGUE"] = "qxz", ["潜行者"] = "qxz",
["Priest"] = "ms", ["PRIEST"] = "ms", ["牧师"] = "ms",
["Shaman"] = "sm", ["SHAMAN"] = "sm", ["萨满祭司"] = "sm",
["Mage"] = "fs", ["MAGE"] = "fs", ["法师"] = "fs",
["Warlock"] = "ss", ["WARLOCK"] = "ss", ["术士"] = "ss",
["Druid"] = "dly", ["DRUID"] = "dly", ["德鲁伊"] = "dly",
}
local DEFAULTS = {
enabled = true,
scale = 1.0,
showClock = true,
showCoords = true,
mapStyle = "auto",
posX = -5,
posY = -5,
mailIconX = nil,
mailIconY = nil,
}
local container, overlayFrame, overlayTex
local zoneFs, clockFs, clockBg, coordFs
local built = false
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetDB()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end
local db = SFramesDB.Minimap
for k, v in pairs(DEFAULTS) do
if db[k] == nil then db[k] = v end
end
return db
end
local function ResolveStyleKey()
local key = GetDB().mapStyle or "auto"
if key == "auto" then
local localName, engName = UnitClass("player")
key = CLASS_STYLE_MAP[engName]
or CLASS_STYLE_MAP[localName]
or "map"
end
return key
end
local function GetCurrentStyle()
local key = ResolveStyleKey()
for _, s in ipairs(MAP_STYLES) do
if s.key == key then return s end
end
return MAP_STYLES[1]
end
local function GetMapTexture()
return GetCurrentStyle().tex
end
local function S(texPx, frameSize)
return texPx / TEX_SIZE * frameSize
end
local function FrameSize()
return math.floor(BASE_SIZE * ((GetDB().scale) or 1))
end
local function ApplyPosition()
if not container then return end
local db = GetDB()
local x = tonumber(db.posX) or -5
local y = tonumber(db.posY) or -5
container:ClearAllPoints()
container:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", x, y)
end
--------------------------------------------------------------------------------
-- Hide default Blizzard minimap chrome
-- MUST be called AFTER BuildFrame (Minimap is already reparented)
--------------------------------------------------------------------------------
local function HideDefaultElements()
local kill = {
MinimapBorder,
MinimapBorderTop,
MinimapZoomIn,
MinimapZoomOut,
MinimapToggleButton,
MiniMapWorldMapButton,
GameTimeFrame,
MinimapZoneTextButton,
MiniMapTracking,
MinimapBackdrop,
}
for _, f in ipairs(kill) do
if f then
f:Hide()
f.Show = function() end
end
end
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
local trackNames = {
"MiniMapTrackingButton", "MiniMapTrackingFrame",
"MiniMapTrackingIcon", "MiniMapTracking1", "MiniMapTracking2",
}
for _, name in ipairs(trackNames) do
local f = _G[name]
if f and f.Hide then
f:Hide()
f.Show = function() end
end
end
-- Also hide any tracking textures that are children of Minimap
if Minimap.GetChildren then
local children = { Minimap:GetChildren() }
for _, child in ipairs(children) do
local n = child.GetName and child:GetName()
if n and string.find(n, "Track") then
child:Hide()
child.Show = function() end
end
end
end
-- Move MinimapCluster off-screen instead of Hide()
-- Hide() would cascade-hide children that were still parented at load time
if MinimapCluster then
MinimapCluster:ClearAllPoints()
MinimapCluster:SetPoint("TOP", UIParent, "BOTTOM", 0, -500)
MinimapCluster:SetWidth(1)
MinimapCluster:SetHeight(1)
MinimapCluster:EnableMouse(false)
end
end
--------------------------------------------------------------------------------
-- Build
--------------------------------------------------------------------------------
local function BuildFrame()
if built then return end
built = true
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
-- Main container
container = CreateFrame("Frame", "SFramesMinimapContainer", UIParent)
container:SetWidth(fs)
container:SetHeight(fs)
container:SetFrameStrata("LOW")
container:SetFrameLevel(1)
container:EnableMouse(false)
container:SetClampedToScreen(true)
-- Reparent the actual minimap into our container
Minimap:SetParent(container)
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
Minimap:SetFrameStrata("LOW")
Minimap:SetFrameLevel(2)
Minimap:Show()
if Minimap.SetMaskTexture then
Minimap:SetMaskTexture("Textures\\MinimapMask")
end
-- Decorative overlay (map.tga with transparent circle)
overlayFrame = CreateFrame("Frame", nil, container)
overlayFrame:SetAllPoints(container)
overlayFrame:SetFrameStrata("LOW")
overlayFrame:SetFrameLevel(Minimap:GetFrameLevel() + 3)
overlayFrame:EnableMouse(false)
overlayTex = overlayFrame:CreateTexture(nil, "ARTWORK")
overlayTex:SetTexture(GetMapTexture())
overlayTex:SetAllPoints(overlayFrame)
-- Zone name on the scroll plate (horizontally centered on frame)
local style = GetCurrentStyle()
local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs)
zoneFs = overlayFrame:CreateFontString(nil, "OVERLAY")
zoneFs:SetFont(SFrames:GetFont(), 11, "")
zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy)
zoneFs:SetWidth(S(PLATE_W + 60, fs))
zoneFs:SetHeight(S(PLATE_H, fs))
zoneFs:SetJustifyH("CENTER")
zoneFs:SetJustifyV("MIDDLE")
local tc = style.textColor or {0.22, 0.13, 0.07}
zoneFs:SetTextColor(tc[1], tc[2], tc[3])
-- Clock background (semi-transparent rounded)
clockBg = CreateFrame("Frame", nil, overlayFrame)
clockBg:SetFrameLevel(overlayFrame:GetFrameLevel() + 1)
clockBg:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 10,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
local _clkBg = _A.clockBg or { 0, 0, 0, 0.55 }
local _clkBd = _A.clockBorder or { 0, 0, 0, 0.3 }
clockBg:SetBackdropColor(_clkBg[1], _clkBg[2], _clkBg[3], _clkBg[4])
clockBg:SetBackdropBorderColor(_clkBd[1], _clkBd[2], _clkBd[3], _clkBd[4])
clockBg:SetWidth(46)
clockBg:SetHeight(18)
clockBg:SetPoint("CENTER", container, "BOTTOM", 0,
S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs))
-- Clock text
clockFs = clockBg:CreateFontString(nil, "OVERLAY")
clockFs:SetFont(SFrames:GetFont(), 10, "OUTLINE")
clockFs:SetPoint("CENTER", clockBg, "CENTER", 0, 0)
clockFs:SetJustifyH("CENTER")
local _clkTxt = _A.clockText or { 0.92, 0.84, 0.72 }
clockFs:SetTextColor(_clkTxt[1], _clkTxt[2], _clkTxt[3])
-- Coordinates (inside circle, near bottom)
coordFs = overlayFrame:CreateFontString(nil, "OVERLAY")
coordFs:SetFont(SFrames:GetFont(), 9, "OUTLINE")
coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8)
coordFs:SetJustifyH("CENTER")
local _coordTxt = _A.coordText or { 0.80, 0.80, 0.80 }
coordFs:SetTextColor(_coordTxt[1], _coordTxt[2], _coordTxt[3])
MM.container = container
MM.overlayFrame = overlayFrame
end
--------------------------------------------------------------------------------
-- Interactions
--------------------------------------------------------------------------------
local function SetupScrollZoom()
Minimap:EnableMouseWheel(true)
Minimap:SetScript("OnMouseWheel", function()
if arg1 > 0 then
Minimap_ZoomIn()
else
Minimap_ZoomOut()
end
end)
end
local function SetupMouseHandler()
Minimap:SetScript("OnMouseUp", function()
if arg1 == "RightButton" then
if MiniMapTrackingDropDown then
ToggleDropDownMenu(1, nil, MiniMapTrackingDropDown, "cursor")
end
else
if Minimap_OnClick then
Minimap_OnClick(this)
end
end
end)
end
MM.ApplyPosition = ApplyPosition
--------------------------------------------------------------------------------
-- Reposition child icons
--------------------------------------------------------------------------------
local function SetupMailDragging()
if not MiniMapMailFrame then return end
MiniMapMailFrame:SetMovable(true)
MiniMapMailFrame:EnableMouse(true)
MiniMapMailFrame:RegisterForDrag("LeftButton")
MiniMapMailFrame:SetScript("OnDragStart", function()
MiniMapMailFrame:StartMoving()
end)
MiniMapMailFrame:SetScript("OnDragStop", function()
MiniMapMailFrame:StopMovingOrSizing()
local db = GetDB()
local cx, cy = MiniMapMailFrame:GetCenter()
local ux, uy = UIParent:GetCenter()
if cx and cy and ux and uy then
db.mailIconX = cx - ux
db.mailIconY = cy - uy
end
end)
end
local function RepositionIcons()
if MiniMapMailFrame then
local db = GetDB()
MiniMapMailFrame:ClearAllPoints()
if db.mailIconX and db.mailIconY then
MiniMapMailFrame:SetPoint("CENTER", UIParent, "CENTER", db.mailIconX, db.mailIconY)
else
local mapDiam = Minimap:GetWidth()
MiniMapMailFrame:SetPoint("RIGHT", Minimap, "RIGHT", 8, mapDiam * 0.12)
end
MiniMapMailFrame:SetFrameStrata("LOW")
MiniMapMailFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2)
SetupMailDragging()
end
if MiniMapBattlefieldFrame then
MiniMapBattlefieldFrame:ClearAllPoints()
MiniMapBattlefieldFrame:SetPoint("BOTTOMLEFT", Minimap, "BOTTOM", 15, -8)
MiniMapBattlefieldFrame:SetFrameStrata("LOW")
MiniMapBattlefieldFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2)
end
end
--------------------------------------------------------------------------------
-- Update helpers
--------------------------------------------------------------------------------
local function UpdateZoneText()
if not zoneFs then return end
local text = GetMinimapZoneText and GetMinimapZoneText() or ""
lastZoneText = text
zoneFs:SetText(text)
end
local function SetZoneMap()
if SetMapToCurrentZone then
pcall(SetMapToCurrentZone)
end
end
local clockTimer = 0
local coordTimer = 0
local zoneTimer = 0
local lastZoneText = ""
local function OnUpdate()
local dt = arg1 or 0
local db = GetDB()
-- Clock (every 1 s)
clockTimer = clockTimer + dt
if clockTimer >= 1 then
clockTimer = 0
if db.showClock and clockFs then
local timeStr
if date then
timeStr = date("%H:%M")
else
local h, m = GetGameTime()
timeStr = string.format("%02d:%02d", h, m)
end
clockFs:SetText(timeStr)
clockFs:Show()
if clockBg then clockBg:Show() end
elseif clockFs then
clockFs:Hide()
if clockBg then clockBg:Hide() end
end
end
-- Zone text (every 1 s) catches late API updates missed by events
zoneTimer = zoneTimer + dt
if zoneTimer >= 1 then
zoneTimer = 0
if zoneFs and GetMinimapZoneText then
local cur = GetMinimapZoneText() or ""
if cur ~= lastZoneText then
lastZoneText = cur
zoneFs:SetText(cur)
end
end
end
-- Coordinates (every 0.25 s)
coordTimer = coordTimer + dt
if coordTimer >= 0.25 then
coordTimer = 0
if db.showCoords and coordFs and GetPlayerMapPosition then
local ok, x, y = pcall(GetPlayerMapPosition, "player")
if ok and x and y and (x > 0 or y > 0) then
coordFs:SetText(string.format("%.1f, %.1f", x * 100, y * 100))
coordFs:Show()
else
coordFs:Hide()
end
elseif coordFs then
coordFs:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------
function MM:Refresh()
if not container then return end
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
container:SetWidth(fs)
container:SetHeight(fs)
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
if zoneFs then
local style = GetCurrentStyle()
local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs)
zoneFs:ClearAllPoints()
zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy)
zoneFs:SetWidth(S(PLATE_W + 60, fs))
local tc = style.textColor or {0.22, 0.13, 0.07}
zoneFs:SetTextColor(tc[1], tc[2], tc[3])
end
if clockBg then
clockBg:ClearAllPoints()
clockBg:SetPoint("CENTER", container, "BOTTOM", 0,
S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs))
end
if coordFs then
coordFs:ClearAllPoints()
coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8)
end
if overlayTex then
overlayTex:SetTexture(GetMapTexture())
end
UpdateZoneText()
RepositionIcons()
end
MM.MAP_STYLES = MAP_STYLES
--------------------------------------------------------------------------------
-- Shield: re-apply our skin after other addons (ShaguTweaks etc.) touch Minimap
--------------------------------------------------------------------------------
local shielded = false
local function ShieldMinimap()
if shielded then return end
shielded = true
-- Override any external changes to Minimap parent / position / size
if Minimap:GetParent() ~= container then
Minimap:SetParent(container)
end
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
Minimap:SetFrameStrata("LOW")
Minimap:SetFrameLevel(2)
Minimap:Show()
if Minimap.SetMaskTexture then
Minimap:SetMaskTexture("Textures\\MinimapMask")
end
HideDefaultElements()
-- Kill any border/backdrop that ShaguTweaks may have injected
local regions = { Minimap:GetRegions() }
for _, r in ipairs(regions) do
if r and r:IsObjectType("Texture") then
local tex = r.GetTexture and r:GetTexture()
if tex and type(tex) == "string" then
local low = string.lower(tex)
if string.find(low, "border") or string.find(low, "backdrop")
or string.find(low, "overlay") or string.find(low, "background") then
r:Hide()
end
end
end
end
shielded = false
end
function MM:Initialize()
if not Minimap then return end
local db = GetDB()
if db.enabled == false then return end
local ok, err = pcall(function()
-- Build first (reparents Minimap), THEN hide old chrome
BuildFrame()
HideDefaultElements()
-- Ensure Minimap is visible after reparent
Minimap:Show()
SetupScrollZoom()
SetupMouseHandler()
-- Apply position from settings
ApplyPosition()
RepositionIcons()
-- Zone text events
SFrames:RegisterEvent("ZONE_CHANGED", function()
SetZoneMap()
UpdateZoneText()
end)
SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
SetZoneMap()
UpdateZoneText()
end)
SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function()
SetZoneMap()
UpdateZoneText()
end)
-- Re-apply after other addons finish loading (ShaguTweaks etc.)
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
pcall(ShieldMinimap)
pcall(UpdateZoneText)
end)
-- Delayed zone text: GetMinimapZoneText() may be empty at PLAYER_LOGIN
local zoneDelay = CreateFrame("Frame")
local elapsed = 0
zoneDelay:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if elapsed >= 1 then
zoneDelay:SetScript("OnUpdate", nil)
pcall(SetZoneMap)
pcall(UpdateZoneText)
end
end)
-- Tick
container:SetScript("OnUpdate", OnUpdate)
-- First refresh
SetZoneMap()
UpdateZoneText()
MM:Refresh()
end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI Minimap error: " .. tostring(err) .. "|r")
end
end

638
MinimapBuffs.lua Normal file
View File

@@ -0,0 +1,638 @@
SFrames.MinimapBuffs = {}
local MB = SFrames.MinimapBuffs
local MAX_BUFFS = 32
local MAX_DEBUFFS = 16
local UPDATE_INTERVAL = 0.2
local BASE_X = -209
local BASE_Y = -26
local ROUND_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
}
local function GetDB()
if not SFramesDB or type(SFramesDB.MinimapBuffs) ~= "table" then
return {
enabled = true, iconSize = 30, iconsPerRow = 8,
spacing = 2, growDirection = "LEFT", position = "TOPRIGHT",
offsetX = 0, offsetY = 0, showTimer = true,
showDebuffs = true, debuffIconSize = 30,
}
end
return SFramesDB.MinimapBuffs
end
local function FormatBuffTime(seconds)
if not seconds or seconds <= 0 or seconds >= 99999 then
return "N/A"
end
if seconds >= 3600 then
local h = math.floor(seconds / 3600)
local m = math.floor(math.mod(seconds, 3600) / 60)
return h .. "h" .. m .. "m"
elseif seconds >= 60 then
return math.floor(seconds / 60) .. "m"
else
return math.floor(seconds) .. "s"
end
end
local DEBUFF_TYPE_COLORS = {
Magic = { r = 0.20, g = 0.60, b = 1.00 },
Curse = { r = 0.60, g = 0.00, b = 1.00 },
Disease = { r = 0.60, g = 0.40, b = 0.00 },
Poison = { r = 0.00, g = 0.60, b = 0.00 },
}
local DEBUFF_DEFAULT_COLOR = { r = 0.80, g = 0.00, b = 0.00 }
local WEAPON_ENCHANT_COLOR = { r = 0.58, g = 0.22, b = 0.82 }
local function HideBlizzardBuffs()
for i = 0, 23 do
local btn = _G["BuffButton" .. i]
if btn then
btn:Hide()
btn:UnregisterAllEvents()
btn.Show = function() end
end
end
for i = 1, 3 do
local te = _G["TempEnchant" .. i]
if te then
te:Hide()
te:UnregisterAllEvents()
te.Show = function() end
end
end
if BuffFrame then
BuffFrame:Hide()
BuffFrame:UnregisterAllEvents()
BuffFrame.Show = function() end
end
if TemporaryEnchantFrame then
TemporaryEnchantFrame:Hide()
TemporaryEnchantFrame:UnregisterAllEvents()
TemporaryEnchantFrame.Show = function() end
end
end
local function ApplySlotBackdrop(btn, isBuff)
btn:SetBackdrop(ROUND_BACKDROP)
btn:SetBackdropColor(0.06, 0.06, 0.08, 0.92)
if isBuff then
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
else
local c = DEBUFF_DEFAULT_COLOR
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
end
end
local function CreateSlot(parent, namePrefix, index, isBuff)
local db = GetDB()
local size = isBuff and (db.iconSize or 30) or (db.debuffIconSize or 30)
local btn = CreateFrame("Button", namePrefix .. index, parent)
btn:SetWidth(size)
btn:SetHeight(size)
ApplySlotBackdrop(btn, isBuff)
btn.icon = btn:CreateTexture(nil, "ARTWORK")
btn.icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
btn.icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
btn.count = SFrames:CreateFontString(btn, 10, "RIGHT")
btn.count:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -1, -1)
btn.count:SetTextColor(1, 1, 1)
btn.count:SetShadowColor(0, 0, 0, 1)
btn.count:SetShadowOffset(1, -1)
btn.timer = SFrames:CreateFontString(btn, 9, "CENTER")
btn.timer:SetPoint("BOTTOM", btn, "BOTTOM", 0, -11)
btn.timer:SetTextColor(1, 0.82, 0)
btn.timer:SetShadowColor(0, 0, 0, 1)
btn.timer:SetShadowOffset(1, -1)
btn.isBuff = isBuff
btn:EnableMouse(true)
btn:RegisterForClicks("RightButtonUp")
btn:SetScript("OnEnter", function()
if this._sfSimulated then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:AddLine(this._sfSimLabel or "Simulated", 1, 1, 1)
GameTooltip:AddLine(this._sfSimDesc or "", 0.7, 0.7, 0.7)
GameTooltip:Show()
return
end
if this._isWeaponEnchant and this._weaponSlotID then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetInventoryItem("player", this._weaponSlotID)
return
end
if this.buffIndex and this.buffIndex >= 0 then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetPlayerBuff(this.buffIndex)
end
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:SetScript("OnClick", function()
if this._sfSimulated then return end
if this._isWeaponEnchant then return end
if this.isBuff and this.buffIndex and this.buffIndex >= 0 then
CancelPlayerBuff(this.buffIndex)
end
end)
btn.buffIndex = -1
btn._sfSimulated = false
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn:Hide()
return btn
end
function MB:ApplyLayout()
if not self.buffSlots then return end
local db = GetDB()
local size = db.iconSize or 30
local spacing = db.spacing or 2
local perRow = db.iconsPerRow or 8
local growLeft = (db.growDirection == "LEFT")
for i = 1, MAX_BUFFS do
local btn = self.buffSlots[i]
btn:SetWidth(size)
btn:SetHeight(size)
btn:ClearAllPoints()
local col = math.mod(i - 1, perRow)
local row = math.floor((i - 1) / perRow)
local xDir = growLeft and -1 or 1
local xOfs = col * (size + spacing) * xDir
local yOfs = -row * (size + 14 + spacing)
local anchor = growLeft and "TOPRIGHT" or "TOPLEFT"
btn:SetPoint(anchor, self.buffContainer, anchor, xOfs, yOfs)
end
if not self.debuffSlots then return end
local dSize = db.debuffIconSize or 30
for i = 1, MAX_DEBUFFS do
local btn = self.debuffSlots[i]
btn:SetWidth(dSize)
btn:SetHeight(dSize)
btn:ClearAllPoints()
local col = math.mod(i - 1, perRow)
local row = math.floor((i - 1) / perRow)
local xDir = growLeft and -1 or 1
local xOfs = col * (dSize + spacing) * xDir
local yOfs = -row * (dSize + 14 + spacing)
local anchor = growLeft and "TOPRIGHT" or "TOPLEFT"
btn:SetPoint(anchor, self.debuffContainer, anchor, xOfs, yOfs)
end
end
local function CountVisibleRows(slots, maxSlots, perRow)
local maxVisible = 0
for i = 1, maxSlots do
if slots[i]:IsShown() then maxVisible = i end
end
if maxVisible == 0 then return 0 end
return math.floor((maxVisible - 1) / perRow) + 1
end
function MB:ApplyPosition()
if not self.buffContainer then return end
local db = GetDB()
local pos = db.position or "TOPRIGHT"
local x = BASE_X + (db.offsetX or 0)
local y = BASE_Y + (db.offsetY or 0)
self.buffContainer:ClearAllPoints()
self.buffContainer:SetPoint(pos, UIParent, pos, x, y)
self:AnchorDebuffs()
end
function MB:AnchorDebuffs()
if not self.debuffContainer or not self.buffContainer then return end
local db = GetDB()
local size = db.iconSize or 30
local spacing = db.spacing or 2
local perRow = db.iconsPerRow or 8
local rows = CountVisibleRows(self.buffSlots, MAX_BUFFS, perRow)
local rowHeight = size + 14 + spacing
local gap = 6
self.debuffContainer:ClearAllPoints()
self.debuffContainer:SetPoint("TOPRIGHT", self.buffContainer, "TOPRIGHT", 0, -(rows * rowHeight + gap))
end
local function ApplyTimerColor(btn, timeText)
if timeText == "N/A" then
btn.timer:SetTextColor(0.6, 0.6, 0.6)
return
end
local secs = nil
local _, _, hVal = string.find(timeText, "(%d+)h")
local _, _, mVal = string.find(timeText, "(%d+)m")
local _, _, sVal = string.find(timeText, "(%d+)s")
local h = tonumber(hVal)
local m = tonumber(mVal)
local s = tonumber(sVal)
if h then
secs = h * 3600 + (m or 0) * 60
elseif m then
secs = m * 60
elseif s then
secs = s
end
if secs and secs < 30 then
btn.timer:SetTextColor(1, 0.3, 0.3)
elseif secs and secs < 120 then
btn.timer:SetTextColor(1, 0.82, 0)
else
btn.timer:SetTextColor(0.8, 1, 0.8)
end
end
local function SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer)
if not showTimer then
btn.timer:Hide()
return
end
if untilCancelled == 1 or not timeLeft or timeLeft == 0 or timeLeft >= 99999 then
btn.timer:SetText("N/A")
btn.timer:SetTextColor(0.6, 0.6, 0.6)
else
btn.timer:SetText(FormatBuffTime(timeLeft))
if timeLeft < 30 then
btn.timer:SetTextColor(1, 0.3, 0.3)
elseif timeLeft < 120 then
btn.timer:SetTextColor(1, 0.82, 0)
else
btn.timer:SetTextColor(0.8, 1, 0.8)
end
end
btn.timer:Show()
end
function MB:UpdateBuffs()
if not self.buffSlots then return end
if self._simulating then return end
local db = GetDB()
local showTimer = db.showTimer ~= false
local slotIdx = 0
for i = 0, 31 do
local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL")
if buffIndex and buffIndex >= 0 then
slotIdx = slotIdx + 1
if slotIdx > MAX_BUFFS then break end
local btn = self.buffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
local apps = GetPlayerBuffApplications(buffIndex)
if apps and apps > 1 then
btn.count:SetText(apps)
btn.count:Show()
else
btn.count:SetText("")
btn.count:Hide()
end
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer)
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end
end
end
local hasMainHandEnchant, mainHandExpiration, mainHandCharges,
hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
if hasMainHandEnchant then
slotIdx = slotIdx + 1
if slotIdx <= MAX_BUFFS then
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 16)
if texture then
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
btn._isWeaponEnchant = true
btn._weaponSlotID = 16
if mainHandCharges and mainHandCharges > 0 then
btn.count:SetText(mainHandCharges)
btn.count:Show()
else
btn.count:SetText("")
btn.count:Hide()
end
local timeLeft = mainHandExpiration and (mainHandExpiration / 1000) or 0
SetTimerFromSeconds(btn, timeLeft, 0, showTimer)
btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1)
btn:Show()
end
end
end
if hasOffHandEnchant then
slotIdx = slotIdx + 1
if slotIdx <= MAX_BUFFS then
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 17)
if texture then
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
btn._isWeaponEnchant = true
btn._weaponSlotID = 17
if offHandCharges and offHandCharges > 0 then
btn.count:SetText(offHandCharges)
btn.count:Show()
else
btn.count:SetText("")
btn.count:Hide()
end
local timeLeft = offHandExpiration and (offHandExpiration / 1000) or 0
SetTimerFromSeconds(btn, timeLeft, 0, showTimer)
btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1)
btn:Show()
end
end
end
for j = slotIdx + 1, MAX_BUFFS do
local btn = self.buffSlots[j]
btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end
self:UpdateDebuffs()
self:AnchorDebuffs()
end
function MB:UpdateDebuffs()
if not self.debuffSlots then return end
if self._simulating then return end
local db = GetDB()
local showTimer = db.showTimer ~= false
if db.showDebuffs == false then
for i = 1, MAX_DEBUFFS do
self.debuffSlots[i]:Hide()
end
if self.debuffContainer then self.debuffContainer:Hide() end
return
end
if self.debuffContainer then self.debuffContainer:Show() end
local slotIdx = 0
for i = 0, 15 do
local buffIndex, untilCancelled = GetPlayerBuff(i, "HARMFUL")
if buffIndex and buffIndex >= 0 then
slotIdx = slotIdx + 1
if slotIdx > MAX_DEBUFFS then break end
local btn = self.debuffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
local apps = GetPlayerBuffApplications(buffIndex)
if apps and apps > 1 then
btn.count:SetText(apps)
btn.count:Show()
else
btn.count:SetText("")
btn.count:Hide()
end
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer)
local debuffType = nil
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetPlayerBuff(buffIndex)
local dTypeStr = SFramesScanTooltipTextRight1 and SFramesScanTooltipTextRight1:GetText()
if dTypeStr and dTypeStr ~= "" then debuffType = dTypeStr end
local c = DEBUFF_TYPE_COLORS[debuffType] or DEBUFF_DEFAULT_COLOR
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
btn:Show()
else
btn:Hide()
btn.buffIndex = -1
end
end
end
for j = slotIdx + 1, MAX_DEBUFFS do
self.debuffSlots[j]:Hide()
self.debuffSlots[j].buffIndex = -1
end
end
--------------------------------------------------------------------------------
-- Simulation (2 rows each for buff & debuff)
--------------------------------------------------------------------------------
local SIM_BUFFS = {
-- Row 1
{ tex = "Interface\\Icons\\Spell_Holy_WordFortitude", label = "Power Word: Fortitude", desc = "Stamina +54", time = "N/A" },
{ tex = "Interface\\Icons\\Spell_Shadow_AntiShadow", label = "Shadow Protection", desc = "Shadow Resistance +60", time = "N/A" },
{ tex = "Interface\\Icons\\Spell_Holy_MagicalSentry", label = "Arcane Intellect", desc = "Intellect +31", time = "42m" },
{ tex = "Interface\\Icons\\Spell_Nature_Regeneration", label = "Mark of the Wild", desc = "All stats +12", time = "38m" },
{ tex = "Interface\\Icons\\Spell_Holy_GreaterBlessingofKings", label = "Blessing of Kings", desc = "All stats +10%", time = "7m" },
{ tex = "Interface\\Icons\\Spell_Holy_PrayerOfHealing02", label = "Renew", desc = "Heals 152 over 15 sec", time = "12s" },
{ tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", label = "Divine Spirit", desc = "Spirit +40", time = "N/A" },
{ tex = "Interface\\Icons\\Spell_Fire_SealOfFire", label = "Fire Shield", desc = "Fire damage absorb", time = "25m" },
-- Row 2
{ tex = "Interface\\Icons\\Spell_Holy_PowerWordShield", label = "Power Word: Shield", desc = "Absorbs 942 damage", time = "28s" },
{ tex = "Interface\\Icons\\Spell_Nature_Lightning", label = "Lightning Shield", desc = "3 charges", time = "9m", count = 3 },
{ tex = "Interface\\Icons\\Spell_Holy_SealOfWisdom", label = "Blessing of Wisdom", desc = "MP5 +33", time = "5m" },
{ tex = "Interface\\Icons\\Spell_Nature_UndyingStrength", label = "Thorns", desc = "Nature damage on hit", time = "N/A" },
{ tex = "Interface\\Icons\\Spell_Nature_Invisibilty", label = "Innervate", desc = "Spirit +400%", time = "18s" },
{ tex = "Interface\\Icons\\Spell_Holy_PowerInfusion", label = "Power Infusion", desc = "+20% spell damage", time = "14s" },
{ tex = "Interface\\Icons\\Spell_Holy_SealOfValor", label = "Blessing of Sanctuary", desc = "Block damage reduced", time = "3m" },
{ tex = "Interface\\Icons\\Spell_Nature_EnchantArmor", label = "Nature Resistance", desc = "Nature Resistance +60", time = "1h12m" },
}
local SIM_DEBUFFS = {
-- Row 1
{ tex = "Interface\\Icons\\Spell_Shadow_CurseOfTounable", label = "Curse of Tongues", desc = "Casting 50% slower", time = "28s", dtype = "Curse" },
{ tex = "Interface\\Icons\\Spell_Shadow_UnholyStrength", label = "Weakened Soul", desc = "Cannot be shielded", time = "15s", dtype = "Magic" },
{ tex = "Interface\\Icons\\Ability_Creature_Disease_02", label = "Corrupted Blood", desc = "Inflicts disease damage", time = "N/A", dtype = "Disease" },
{ tex = "Interface\\Icons\\Spell_Nature_CorrosiveBreath", label = "Deadly Poison", desc = "Inflicts Nature damage", time = "8s", dtype = "Poison" },
{ tex = "Interface\\Icons\\Spell_Shadow_Possession", label = "Fear", desc = "Feared for 8 sec", time = "6s", dtype = "Magic" },
{ tex = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", label = "Shadow Word: Pain", desc = "Shadow damage over time", time = "24s", dtype = "Magic" },
{ tex = "Interface\\Icons\\Spell_Shadow_AbominationExplosion", label = "Mortal Strike", desc = "Healing reduced 50%", time = "5s", dtype = nil },
{ tex = "Interface\\Icons\\Spell_Frost_FrostArmor02", label = "Frostbolt", desc = "Movement slowed 40%", time = "4s", dtype = "Magic" },
-- Row 2
{ tex = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras", label = "Curse of Agony", desc = "Shadow damage over time", time = "22s", dtype = "Curse" },
{ tex = "Interface\\Icons\\Spell_Nature_Slow", label = "Crippling Poison", desc = "Movement slowed 70%", time = "10s", dtype = "Poison" },
{ tex = "Interface\\Icons\\Spell_Shadow_CurseOfMannoroth", label = "Curse of Elements", desc = "Resistance reduced 75", time = "N/A", dtype = "Curse" },
{ tex = "Interface\\Icons\\Ability_Creature_Disease_03", label = "Devouring Plague", desc = "Disease damage + heal", time = "20s", dtype = "Disease" },
}
function MB:SimulateBuffs()
if not self.buffSlots or not self.debuffSlots then return end
self._simulating = true
for i = 1, MAX_BUFFS do
local btn = self.buffSlots[i]
local sim = SIM_BUFFS[i]
if sim then
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn._sfSimLabel = sim.label
btn._sfSimDesc = sim.desc
btn.timer:SetText(sim.time)
ApplyTimerColor(btn, sim.time)
btn.timer:Show()
if sim.count and sim.count > 1 then
btn.count:SetText(sim.count)
btn.count:Show()
else
btn.count:Hide()
end
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
btn:Hide()
end
end
for i = 1, MAX_DEBUFFS do
local btn = self.debuffSlots[i]
local sim = SIM_DEBUFFS[i]
if sim then
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
btn._sfSimLabel = sim.label
btn._sfSimDesc = sim.desc
btn.timer:SetText(sim.time)
ApplyTimerColor(btn, sim.time)
btn.timer:Show()
btn.count:Hide()
local c = DEBUFF_TYPE_COLORS[sim.dtype] or DEBUFF_DEFAULT_COLOR
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
btn:Show()
else
btn:Hide()
end
end
if self.debuffContainer then self.debuffContainer:Show() end
self:AnchorDebuffs()
end
function MB:StopSimulation()
self._simulating = false
self:UpdateBuffs()
end
function MB:Refresh()
if not self.buffContainer then return end
local db = GetDB()
if db.enabled == false then
self.buffContainer:Hide()
if self.debuffContainer then self.debuffContainer:Hide() end
return
end
self:ApplyPosition()
self:ApplyLayout()
if not self._simulating then
self:UpdateBuffs()
else
self:AnchorDebuffs()
end
self.buffContainer:Show()
end
function MB:Initialize()
local db = GetDB()
if db.enabled == false then return end
HideBlizzardBuffs()
self.buffContainer = CreateFrame("Frame", "SFramesMBuffContainer", UIParent)
self.buffContainer:SetWidth(400)
self.buffContainer:SetHeight(200)
self.buffContainer:SetFrameStrata("LOW")
self.debuffContainer = CreateFrame("Frame", "SFramesMDebuffContainer", UIParent)
self.debuffContainer:SetWidth(400)
self.debuffContainer:SetHeight(100)
self.debuffContainer:SetFrameStrata("LOW")
self.buffSlots = {}
for i = 1, MAX_BUFFS do
self.buffSlots[i] = CreateSlot(self.buffContainer, "SFramesMBuff", i, true)
end
self.debuffSlots = {}
for i = 1, MAX_DEBUFFS do
self.debuffSlots[i] = CreateSlot(self.debuffContainer, "SFramesMDebuff", i, false)
end
self:ApplyPosition()
self:ApplyLayout()
self.updater = CreateFrame("Frame", nil, self.buffContainer)
self.updater.timer = 0
self.updater:SetScript("OnUpdate", function()
this.timer = this.timer + arg1
if this.timer >= UPDATE_INTERVAL then
MB:UpdateBuffs()
this.timer = 0
end
end)
self:UpdateBuffs()
end

220
MinimapButton.lua Normal file
View File

@@ -0,0 +1,220 @@
--------------------------------------------------------------------------------
-- S-Frames: Minimap quick access button (MinimapButton.lua)
-- Left Click : open /nui UI settings
-- Right Click : open /nui bag settings
-- Shift + Drag: move icon around minimap
--------------------------------------------------------------------------------
SFrames.MinimapButton = SFrames.MinimapButton or {}
local button = nil
local DEFAULT_ANGLE = 225
local function EnsureDB()
if not SFramesDB then
SFramesDB = {}
end
if type(SFramesDB.minimapButton) ~= "table" then
SFramesDB.minimapButton = {}
end
local db = SFramesDB.minimapButton
if type(db.angle) ~= "number" then
db.angle = DEFAULT_ANGLE
end
if db.hide == nil then
db.hide = false
end
return db
end
local function SafeAtan2(y, x)
if math.atan2 then
return math.atan2(y, x)
end
if x > 0 then
return math.atan(y / x)
elseif x < 0 and y >= 0 then
return math.atan(y / x) + math.pi
elseif x < 0 and y < 0 then
return math.atan(y / x) - math.pi
elseif x == 0 and y > 0 then
return math.pi / 2
elseif x == 0 and y < 0 then
return -math.pi / 2
end
return 0
end
local function GetOrbitRadius()
local w = Minimap and Minimap:GetWidth() or 140
return w / 2 + 6
end
local function UpdatePosition()
if not button or not Minimap then
return
end
local db = EnsureDB()
local radius = GetOrbitRadius()
local angleRad = math.rad(db.angle or DEFAULT_ANGLE)
local x = math.cos(angleRad) * radius
local y = math.sin(angleRad) * radius
button:ClearAllPoints()
button:SetPoint("CENTER", Minimap, "CENTER", x, y)
end
local function StartDrag()
if not button or not Minimap then
return
end
button:SetScript("OnUpdate", function()
local mx, my = GetCursorPosition()
local scale = Minimap:GetEffectiveScale() or 1
if scale == 0 then scale = 1 end
mx = mx / scale
my = my / scale
local cx, cy = Minimap:GetCenter()
if not cx or not cy then
return
end
local angle = math.deg(SafeAtan2(my - cy, mx - cx))
if angle < 0 then
angle = angle + 360
end
EnsureDB().angle = angle
UpdatePosition()
end)
end
local function StopDrag()
if button then
button:SetScript("OnUpdate", nil)
end
end
function SFrames.MinimapButton:Refresh()
local db = EnsureDB()
if not button then
return
end
if db.hide then
button:Hide()
else
button:Show()
end
if button.icon and SFrames.SetIcon then
SFrames:SetIcon(button.icon, "logo")
local A = SFrames.ActiveTheme
if A and A.accentLight then
button.icon:SetVertexColor(A.accentLight[1], A.accentLight[2], A.accentLight[3], 1)
end
end
UpdatePosition()
end
function SFrames.MinimapButton:Initialize()
if button then
self:Refresh()
return
end
if not Minimap then
return
end
EnsureDB()
button = CreateFrame("Button", "SFramesMinimapButton", Minimap)
button:SetWidth(32)
button:SetHeight(32)
button:SetFrameStrata("MEDIUM")
button:SetMovable(false)
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
button:RegisterForDrag("LeftButton")
local border = button:CreateTexture(nil, "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
border:SetWidth(56)
border:SetHeight(56)
border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
local bg = button:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
bg:SetWidth(20)
bg:SetHeight(20)
bg:SetPoint("CENTER", button, "CENTER", 0, 0)
local icon = button:CreateTexture(nil, "ARTWORK")
icon:SetWidth(20)
icon:SetHeight(20)
icon:SetPoint("CENTER", button, "CENTER", 0, 0)
local hl = button:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
hl:SetBlendMode("ADD")
hl:SetWidth(31)
hl:SetHeight(31)
hl:SetPoint("CENTER", button, "CENTER", 0, 0)
button.icon = icon
button:SetScript("OnClick", function()
if arg1 == "RightButton" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then
SFrames.ConfigUI:Build("bags")
end
else
if SFrames.ConfigUI and SFrames.ConfigUI.Build then
SFrames.ConfigUI:Build("ui")
end
end
end)
button:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:ClearLines()
local A = SFrames.ActiveTheme
local tr, tg, tb = 1, 0.82, 0.94
if A and A.accentLight then
tr, tg, tb = A.accentLight[1], A.accentLight[2], A.accentLight[3]
end
GameTooltip:AddLine("Nanami-UI", tr, tg, tb)
GameTooltip:AddLine("左键: 打开 UI 设置", 0.85, 0.85, 0.85)
GameTooltip:AddLine("右键: 打开背包设置", 0.85, 0.85, 0.85)
GameTooltip:AddLine("Shift+拖动: 移动图标", 0.6, 0.9, 0.6)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
button:SetScript("OnDragStart", function()
if not IsShiftKeyDown() then
return
end
StartDrag()
end)
button:SetScript("OnDragStop", function()
StopDrag()
end)
self:Refresh()
end

67
Nanami-UI.toc Normal file
View File

@@ -0,0 +1,67 @@
## Interface: 11200
## Title: Nanami-UI
## Notes: 现代极简猫系单位框架插件 - 喵~ (Nanami-UI)
## Author: AI Assistant
## Version: 1.0.0
## OptionalDeps: ShaguTweaks, Blizzard_CombatLog, HealComm-1.0, DruidManaLib-1.0, !Libs, pfQuest
## SavedVariablesPerCharacter: SFramesDB
## SavedVariables: SFramesGlobalDB
Bindings.xml
Core.lua
Config.lua
Media.lua
IconMap.lua
Factory.lua
Chat.lua
Whisper.lua
ConfigUI.lua
SetupWizard.lua
GameMenu.lua
MinimapButton.lua
Minimap.lua
MapReveal.lua
WorldMap.lua
MapIcons.lua
Tweaks.lua
MinimapBuffs.lua
Focus.lua
ClassSkillData.lua
Units\Player.lua
Units\Pet.lua
Units\Target.lua
Units\ToT.lua
Units\Party.lua
Units\TalentTree.lua
SellPriceDB.lua
GearScore.lua
Tooltip.lua
Units\Raid.lua
ActionBars.lua
Bags\Offline.lua
Bags\Sort.lua
Bags\Container.lua
Bags\Bank.lua
Bags\Features.lua
Bags\Core.lua
Merchant.lua
Trade.lua
Roll.lua
QuestUI.lua
BookUI.lua
QuestLogSkin.lua
TrainerUI.lua
TradeSkillDB.lua
TradeSkillUI.lua
CharacterPanel.lua
StatSummary.lua
InspectPanel.lua
SpellBookUI.lua
SocialUI.lua
FlightData.lua
FlightMap.lua
Mail.lua
PetStableSkin.lua
DarkmoonGuide.lua
DarkmoonMapMarker.lua
AFKScreen.lua

565
PetStableSkin.lua Normal file
View File

@@ -0,0 +1,565 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Pet Stable Skin (PetStableSkin.lua)
-- Skins the Blizzard PetStableFrame with Nanami-UI theme
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.PetStableSkin = {}
SFramesDB = SFramesDB or {}
local T = SFrames.ActiveTheme
local skinned = false
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function NukeTextures(frame, exceptions)
local regions = { frame:GetRegions() }
for _, r in ipairs(regions) do
if r:IsObjectType("Texture") and not r.sfKeep then
local rName = r:GetName()
local skip = false
if exceptions and rName then
for _, exc in ipairs(exceptions) do
if string.find(rName, exc) then
skip = true
break
end
end
end
if not skip then
r:SetTexture(nil)
r:SetAlpha(0)
r:Hide()
r.sfNuked = true
r:ClearAllPoints()
r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999)
end
end
end
end
local function NukeChildTextures(frame)
local children = { frame:GetChildren() }
for _, child in ipairs(children) do
local cName = child:GetName() or ""
if string.find(cName, "Inset") or string.find(cName, "Bg") then
NukeTextures(child)
if child.SetBackdrop then child:SetBackdrop(nil) end
end
end
end
local function SetRoundBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
end
local function CreateShadow(parent)
local s = CreateFrame("Frame", nil, parent)
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.45)
s:SetBackdropBorderColor(0, 0, 0, 0.6)
s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1))
return s
end
local function MarkBackdropRegions(frame)
local regions = { frame:GetRegions() }
for _, r in ipairs(regions) do
if not r.sfNuked then r.sfKeep = true end
end
end
--------------------------------------------------------------------------------
-- Slot styling (ItemButton-type pet slot buttons)
--------------------------------------------------------------------------------
local function StyleSlot(btn)
if not btn or btn.sfSkinned then return end
btn.sfSkinned = true
local bname = btn:GetName() or ""
local normalTex = _G[bname .. "NormalTexture"]
if normalTex then normalTex:SetAlpha(0) end
NukeTextures(btn, { "Icon", "Count" })
local iconTex = _G[bname .. "IconTexture"]
if iconTex then
iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
iconTex.sfKeep = true
end
local bg = btn:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
bg:SetAllPoints()
bg:SetVertexColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.8)
bg.sfKeep = true
local border = CreateFrame("Frame", nil, btn)
border:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2)
border:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2)
border:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 10,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
border:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
btn.sfBorder = border
local origEnter = btn:GetScript("OnEnter")
local origLeave = btn:GetScript("OnLeave")
btn:SetScript("OnEnter", function()
if origEnter then origEnter() end
if this.sfBorder then
this.sfBorder:SetBackdropBorderColor(
T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 1)
end
end)
btn:SetScript("OnLeave", function()
if origLeave then origLeave() end
if this.sfBorder then
this.sfBorder:SetBackdropBorderColor(
T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
end)
end
--------------------------------------------------------------------------------
-- Close button styling
--------------------------------------------------------------------------------
local function StyleCloseButton(btn)
if not btn or btn.sfSkinned then return end
btn.sfSkinned = true
btn:SetWidth(22)
btn:SetHeight(22)
btn:ClearAllPoints()
btn:SetPoint("TOPRIGHT", btn:GetParent(), "TOPRIGHT", -6, -6)
if btn.GetNormalTexture and btn:GetNormalTexture() then
btn:GetNormalTexture():SetAlpha(0)
end
if btn.GetPushedTexture and btn:GetPushedTexture() then
btn:GetPushedTexture():SetAlpha(0)
end
if btn.GetHighlightTexture and btn:GetHighlightTexture() then
btn:GetHighlightTexture():SetAlpha(0)
end
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
local cbg = { 0.5, 0.1, 0.1, 0.85 }
local cbd = { 0.6, 0.2, 0.2, 0.8 }
local cbgH = { 0.7, 0.15, 0.15, 1 }
local cbdH = { 0.9, 0.3, 0.3, 1 }
btn:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4])
btn:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4])
local xLabel = btn:CreateFontString(nil, "OVERLAY")
xLabel:SetFont(GetFont(), 11, "OUTLINE")
xLabel:SetPoint("CENTER", 0, 0)
xLabel:SetText("X")
xLabel:SetTextColor(0.9, 0.8, 0.8)
local origEnter = btn:GetScript("OnEnter")
local origLeave = btn:GetScript("OnLeave")
btn:SetScript("OnEnter", function()
if origEnter then origEnter() end
this:SetBackdropColor(cbgH[1], cbgH[2], cbgH[3], cbgH[4])
this:SetBackdropBorderColor(cbdH[1], cbdH[2], cbdH[3], cbdH[4])
end)
btn:SetScript("OnLeave", function()
if origLeave then origLeave() end
this:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4])
this:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4])
end)
end
--------------------------------------------------------------------------------
-- UIPanelButton styling (purchase button etc.)
--------------------------------------------------------------------------------
local function StyleActionButton(btn)
if not btn or btn.sfSkinned then return end
btn.sfSkinned = true
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local bg = T.btnBg
local bd = T.btnBorder
btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
btn:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4])
if btn.GetNormalTexture and btn:GetNormalTexture() then
btn:GetNormalTexture():SetTexture(nil)
end
if btn.GetPushedTexture and btn:GetPushedTexture() then
btn:GetPushedTexture():SetTexture(nil)
end
if btn.GetHighlightTexture and btn:GetHighlightTexture() then
btn:GetHighlightTexture():SetAlpha(0)
end
if btn.GetDisabledTexture and btn:GetDisabledTexture() then
btn:GetDisabledTexture():SetTexture(nil)
end
local name = btn:GetName() or ""
for _, suffix in ipairs({ "Left", "Right", "Middle" }) do
local tex = _G[name .. suffix]
if tex then tex:SetAlpha(0); tex:Hide() end
end
local fs = btn:GetFontString()
if fs then
fs:SetFont(GetFont(), 11, "OUTLINE")
fs:SetTextColor(T.text[1], T.text[2], T.text[3])
end
local hoverBg = T.btnHoverBg
local hoverBd = T.btnHoverBd
local origEnter = btn:GetScript("OnEnter")
local origLeave = btn:GetScript("OnLeave")
btn:SetScript("OnEnter", function()
if origEnter then origEnter() end
this:SetBackdropColor(hoverBg[1], hoverBg[2], hoverBg[3], hoverBg[4])
this:SetBackdropBorderColor(hoverBd[1], hoverBd[2], hoverBd[3], hoverBd[4])
end)
btn:SetScript("OnLeave", function()
if origLeave then origLeave() end
this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
this:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4])
end)
end
--------------------------------------------------------------------------------
-- Style all FontStrings on a frame with our themed font
--------------------------------------------------------------------------------
local function StyleFontStrings(frame, size, color)
local regions = { frame:GetRegions() }
local font = GetFont()
for _, r in ipairs(regions) do
if r:IsObjectType("FontString") then
r:SetFont(font, size or 11, "OUTLINE")
if color then
r:SetTextColor(color[1], color[2], color[3])
end
end
end
end
--------------------------------------------------------------------------------
-- Main skin application
--------------------------------------------------------------------------------
local function ApplySkin()
if skinned then return end
if not PetStableFrame then return end
if SFramesDB.enablePetStable == false then return end
skinned = true
local frame = PetStableFrame
local font = GetFont()
-- 1) Remove Blizzard decorative textures from main frame
NukeTextures(frame, { "Icon", "Food", "Diet", "Selected" })
-- 2) Remove textures from sub-inset frames
NukeChildTextures(frame)
-- 3) Apply themed backdrop
SetRoundBackdrop(frame)
MarkBackdropRegions(frame)
-- 4) Shadow for depth (tagged so auto-compact ignores it)
local shadow = CreateShadow(frame)
shadow.sfOverlay = true
-- 5) Free drag support with position persistence
frame:SetMovable(true)
frame:EnableMouse(true)
frame:SetClampedToScreen(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function()
this:StartMoving()
end)
frame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
local point, _, relPoint, xOfs, yOfs = this:GetPoint()
SFramesDB.petStablePos = {
point = point, relPoint = relPoint,
x = xOfs, y = yOfs,
}
end)
-- 6) Title
local titleFS = PetStableFrameTitleText
if titleFS then
titleFS:SetFont(font, 13, "OUTLINE")
titleFS:SetTextColor(T.title[1], T.title[2], T.title[3])
titleFS.sfKeep = true
end
-- 7) Close button
if PetStableFrameCloseButton then
StyleCloseButton(PetStableFrameCloseButton)
end
-- 8) Style pet info FontStrings (try various naming patterns)
local infoFS = {
"PetStablePetName", "PetStableSelectedPetName",
"PetStablePetLevel", "PetStableSelectedPetLevel",
"PetStablePetFamily", "PetStableSelectedPetFamily",
"PetStablePetLoyalty", "PetStableSelectedPetLoyalty",
}
for _, n in ipairs(infoFS) do
local fs = _G[n]
if fs and fs.SetFont then
fs:SetFont(font, 11, "OUTLINE")
fs:SetTextColor(T.text[1], T.text[2], T.text[3])
end
end
local labelFS = {
"PetStableDietLabel", "PetStableTypeLabel",
"PetStableNameLabel", "PetStableLevelLabel",
"PetStableLoyaltyLabel", "PetStableFamilyLabel",
"PetStableDietText",
}
for _, n in ipairs(labelFS) do
local fs = _G[n]
if fs and fs.SetFont then
fs:SetFont(font, 10, "OUTLINE")
fs:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
end
end
-- Also style any remaining FontStrings on the main frame
StyleFontStrings(frame, 11, T.text)
-- 9) Current pet slot
if PetStableCurrentPet then
StyleSlot(PetStableCurrentPet)
end
-- 10) Stable slots (dynamic count for Turtle WoW, check up to 20)
for i = 1, 20 do
local slot = _G["PetStableStableSlot" .. i]
if slot then
StyleSlot(slot)
end
end
-- Alternate naming pattern
for i = 1, 20 do
local slot = _G["PetStableSlot" .. i]
if slot and not slot.sfSkinned then
StyleSlot(slot)
end
end
-- 11) Purchase button
if PetStablePurchaseButton then
StyleActionButton(PetStablePurchaseButton)
end
-- 12) Model frame background (tagged so auto-compact ignores it)
if PetStableModel then
local modelBg = CreateFrame("Frame", nil, frame)
modelBg.sfOverlay = true
modelBg:SetPoint("TOPLEFT", PetStableModel, "TOPLEFT", -3, 3)
modelBg:SetPoint("BOTTOMRIGHT", PetStableModel, "BOTTOMRIGHT", 3, -3)
modelBg:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 10,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
modelBg:SetBackdropColor(
T.modelBg[1], T.modelBg[2], T.modelBg[3], T.modelBg[4] or 0.5)
modelBg:SetBackdropBorderColor(
T.modelBorder[1], T.modelBorder[2], T.modelBorder[3], T.modelBorder[4])
modelBg:SetFrameLevel(math.max(0, PetStableModel:GetFrameLevel() - 1))
end
-- 13) Food/diet icons
for i = 1, 10 do
local food = _G["PetStableFood" .. i] or _G["PetStableDietIcon" .. i]
if food and food.SetTexCoord then
food:SetTexCoord(0.08, 0.92, 0.08, 0.92)
end
end
-- 14) Any additional child frames that look like insets
local insets = {
"PetStableLeftInset", "PetStableBottomInset",
"PetStableRightInset", "PetStableTopInset",
}
for _, iname in ipairs(insets) do
local inset = _G[iname]
if inset then
NukeTextures(inset)
if inset.SetBackdrop then inset:SetBackdrop(nil) end
end
end
-- 14b) Aggressive cleanup: clear backdrops/textures on ALL non-essential children
local knownFrames = {}
if PetStableModel then knownFrames[PetStableModel] = true end
if PetStableFrameCloseButton then knownFrames[PetStableFrameCloseButton] = true end
if PetStablePurchaseButton then knownFrames[PetStablePurchaseButton] = true end
if PetStableCurrentPet then knownFrames[PetStableCurrentPet] = true end
for si = 1, 20 do
local ss = _G["PetStableStableSlot" .. si]
if ss then knownFrames[ss] = true end
local ps = _G["PetStableSlot" .. si]
if ps then knownFrames[ps] = true end
end
local allCh = { frame:GetChildren() }
for _, child in ipairs(allCh) do
if not knownFrames[child] and not child.sfSkinned
and not child.sfOverlay and not child.sfBorder then
NukeTextures(child)
if child.SetBackdrop then child:SetBackdrop(nil) end
local subCh = { child:GetChildren() }
for _, sc in ipairs(subCh) do
NukeTextures(sc)
if sc.SetBackdrop then sc:SetBackdrop(nil) end
end
end
end
-- 15) Auto-compact: measure left padding, then apply uniformly to right & bottom
local frameTop = frame:GetTop()
local frameLeft = frame:GetLeft()
local frameRight = frame:GetRight()
local frameBot = frame:GetBottom()
if frameTop and frameLeft and frameRight and frameBot then
local contentLeft = frameRight
local contentRight = frameLeft
local contentBot = frameTop
local function Scan(obj)
if not obj:IsShown() then return end
local l = obj.GetLeft and obj:GetLeft()
local r = obj.GetRight and obj:GetRight()
local b = obj.GetBottom and obj:GetBottom()
if l and l < contentLeft then contentLeft = l end
if r and r > contentRight then contentRight = r end
if b and b < contentBot then contentBot = b end
end
local children = { frame:GetChildren() }
for _, child in ipairs(children) do
if not child.sfOverlay and not child.sfBorder then
Scan(child)
end
end
local regions = { frame:GetRegions() }
for _, r in ipairs(regions) do
if not r.sfNuked and not r.sfKeep then
Scan(r)
end
end
-- Also scan named Blizzard content elements directly
local contentNames = {
"PetStableCurrentPet", "PetStablePurchaseButton",
"PetStableModel", "PetStableFrameCloseButton",
}
for i = 1, 20 do
table.insert(contentNames, "PetStableStableSlot" .. i)
table.insert(contentNames, "PetStableSlot" .. i)
end
for _, n in ipairs(contentNames) do
local obj = _G[n]
if obj and obj.IsShown and obj:IsShown() then Scan(obj) end
end
-- Scan visible FontStrings on the frame (they are content)
for _, r in ipairs(regions) do
if r:IsObjectType("FontString") and r:IsShown() and r:GetText()
and r:GetText() ~= "" then
local l = r:GetLeft()
local ri = r:GetRight()
local b = r:GetBottom()
if l and l < contentLeft then contentLeft = l end
if ri and ri > contentRight then contentRight = ri end
if b and b < contentBot then contentBot = b end
end
end
local leftPad = contentLeft - frameLeft
if leftPad < 4 then leftPad = 4 end
if leftPad > 16 then leftPad = 16 end
local newW = (contentRight - frameLeft) + leftPad
local newH = (frameTop - contentBot) + leftPad
if newW < frame:GetWidth() then
frame:SetWidth(newW)
end
if newH < frame:GetHeight() then
frame:SetHeight(newH)
end
end
end
--------------------------------------------------------------------------------
-- Restore saved position (runs every time the frame is shown)
--------------------------------------------------------------------------------
local function RestorePosition()
if not PetStableFrame then return end
if not SFramesDB.petStablePos then return end
local pos = SFramesDB.petStablePos
PetStableFrame:ClearAllPoints()
PetStableFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
end
--------------------------------------------------------------------------------
-- Event-driven skin application + position restore
--------------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PET_STABLE_SHOW")
eventFrame:SetScript("OnEvent", function()
if event == "PET_STABLE_SHOW" then
ApplySkin()
RestorePosition()
end
end)
if PetStableFrame then
local origOnShow = PetStableFrame:GetScript("OnShow")
PetStableFrame:SetScript("OnShow", function()
if origOnShow then origOnShow() end
ApplySkin()
RestorePosition()
end)
end

1308
QuestLogSkin.lua Normal file

File diff suppressed because it is too large Load Diff

1663
QuestUI.lua Normal file

File diff suppressed because it is too large Load Diff

548
Roll.lua Normal file
View File

@@ -0,0 +1,548 @@
local AddOnName = "Nanami-UI"
SFrames = SFrames or {}
-- ============================================================
-- Roll Tracker Data
-- ============================================================
local currentRolls = {} -- [itemName] = { [playerName] = { action, roll } }
local _A = SFrames.ActiveTheme
local _bg = _A and _A.panelBg or { 0.08, 0.08, 0.10, 0.95 }
local _bd = _A and _A.panelBorder or { 0.3, 0.3, 0.35, 1 }
local RollUI = CreateFrame("Frame")
RollUI:RegisterEvent("CHAT_MSG_LOOT")
RollUI:RegisterEvent("CHAT_MSG_SYSTEM")
-- ============================================================
-- String helpers (Lua 5.0 only - NO string.match!)
-- ============================================================
local function StripLinks(msg)
return string.gsub(msg, "|c%x%x%x%x%x%x%x%x|H.-|h(.-)|h|r", "%1")
end
local function GetBracketItem(text)
local _, _, name = string.find(text, "%[(.-)%]")
if name then return name end
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
text = string.gsub(text, "%.$", "")
return text
end
local function TrimStr(s)
s = string.gsub(s, "^%s+", "")
s = string.gsub(s, "%s+$", "")
return s
end
-- ============================================================
-- Parse loot roll chat messages
-- ============================================================
local function TrackRollEvent(msg)
if not msg or msg == "" then return nil end
local clean = StripLinks(msg)
local player, rawItem, rollType, rollNum
-- Patterns based on ACTUAL Turtle WoW Chinese message format:
-- "Buis选择了贪婪取向[物品名]"
-- "Buis选择了需求取向[物品名]"
local _, _, p, i
-- === PRIMARY: Turtle WoW Chinese format "选择了需求取向" / "选择了贪婪取向" ===
_, _, p, i = string.find(clean, "^(.+)选择了需求取向:(.+)$")
if p and i then player = p; rawItem = i; rollType = "Need" end
if not player then
_, _, p, i = string.find(clean, "^(.+)选择了需求取向: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Need" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+)选择了贪婪取向:(.+)$")
if p and i then player = p; rawItem = i; rollType = "Greed" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+)选择了贪婪取向: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Greed" end
end
-- Pass: "放弃了" format
if not player then
_, _, p, i = string.find(clean, "^(.+)放弃了:(.+)$")
if p and i then player = p; rawItem = i; rollType = "Pass" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+)放弃了: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Pass" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+)放弃了(.+)$")
if p and i then player = p; rawItem = i; rollType = "Pass" end
end
-- Roll (Chinese): "掷出 85 (1-100)"
if not player then
local r
_, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-(.+)$")
if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end
end
if not player then
local r
_, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-: (.+)$")
if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end
end
-- === FALLBACK: Other Chinese formats ===
if not player then
_, _, p, i = string.find(clean, "^(.+)需求了:(.+)$")
if p and i then player = p; rawItem = i; rollType = "Need" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+)贪婪了:(.+)$")
if p and i then player = p; rawItem = i; rollType = "Greed" end
end
-- === ENGLISH patterns ===
if not player then
_, _, p, i = string.find(clean, "^(.+) selected Need for: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Need" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+) selected Greed for: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Greed" end
end
if not player then
_, _, p, i = string.find(clean, "^(.+) passed on: (.+)$")
if p and i then player = p; rawItem = i; rollType = "Pass" end
end
if not player then
local r
_, _, p, r, i = string.find(clean, "^(.+) rolls (%d+) .+for: (.+)$")
if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end
end
if not player then
_, _, p, i = string.find(clean, "^(.+) passes on (.+)$")
if p and i then player = p; rawItem = i; rollType = "Pass" end
end
if player and rawItem then
player = TrimStr(player)
local itemName = GetBracketItem(rawItem)
if itemName == "" or player == "" then return nil end
if not currentRolls[itemName] then currentRolls[itemName] = {} end
if not currentRolls[itemName][player] then currentRolls[itemName][player] = {} end
if rollType == "Roll" then
currentRolls[itemName][player].roll = rollNum
else
currentRolls[itemName][player].action = rollType
end
return itemName
end
return nil
end
-- ============================================================
-- Update the tracker text on visible roll frames
-- ============================================================
local function UpdateRollTrackers()
for idx = 1, 4 do
local frame = _G["GroupLootFrame"..idx]
if frame and frame:IsVisible() and frame.rollID then
local _, itemName = GetLootRollItemInfo(frame.rollID)
if itemName and frame.sfNeedFS then
local data = currentRolls[itemName]
local needText, greedText, passText = "", "", ""
if data then
local needs, greeds, passes = {}, {}, {}
for pl, info in pairs(data) do
local hex = SFrames and SFrames:GetClassHexForName(pl)
local coloredPl = hex and ("|cff" .. hex .. pl .. "|r") or pl
local rollStr = ""
if info.roll then rollStr = "|cffaaaaaa(" .. info.roll .. ")|r" end
if info.action == "Need" then
table.insert(needs, coloredPl .. rollStr)
elseif info.action == "Greed" then
table.insert(greeds, coloredPl .. rollStr)
elseif info.action == "Pass" then
table.insert(passes, coloredPl)
end
end
if table.getn(needs) > 0 then
needText = "|cffff5555需求|r " .. table.concat(needs, " ")
end
if table.getn(greeds) > 0 then
greedText = "|cff55ff55贪婪|r " .. table.concat(greeds, " ")
end
if table.getn(passes) > 0 then
passText = "|cff888888放弃|r " .. table.concat(passes, " ")
end
end
frame.sfNeedFS:SetText(needText)
frame.sfGreedFS:SetText(greedText)
frame.sfPassFS:SetText(passText)
end
end
end
end
-- ============================================================
-- Event Handler
-- ============================================================
RollUI:SetScript("OnEvent", function()
if event == "CHAT_MSG_LOOT" or event == "CHAT_MSG_SYSTEM" then
local matched = TrackRollEvent(arg1)
if matched then
UpdateRollTrackers()
end
end
end)
-- ============================================================
-- Kill ALL Blizzard textures on the main frame only
-- ============================================================
local function NukeBlizzTextures(frame)
local regions = {frame:GetRegions()}
for _, r in ipairs(regions) do
if r:IsObjectType("Texture") and not r.sfKeep then
r:SetTexture(nil)
r:SetAlpha(0)
r:Hide()
-- Don't override Show! SetBackdrop needs it.
-- Instead mark as nuked and move off-screen
if not r.sfNuked then
r.sfNuked = true
r:ClearAllPoints()
r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999)
end
end
end
end
-- ============================================================
-- Style the GroupLootFrame
-- ============================================================
local function StyleGroupLootFrame(frame)
if frame.sfSkinned then return end
frame.sfSkinned = true
local fname = frame:GetName()
-- 1) Kill all Blizz textures on main frame
NukeBlizzTextures(frame)
-- 2) Alt-Drag
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function()
if IsAltKeyDown() then
this:StartMoving()
end
end)
frame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
end)
-- 3) Rounded backdrop
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4])
frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4])
-- Tag all backdrop regions so NukeBlizzTextures won't kill them
local bdRegions = {frame:GetRegions()}
for _, r in ipairs(bdRegions) do
r.sfKeep = true
end
-- 5) Frame size (taller for tracker area)
frame:SetWidth(300)
frame:SetHeight(100)
-- 6) Icon
local iconFrame = _G[fname.."IconFrame"]
if iconFrame then
iconFrame:ClearAllPoints()
iconFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10)
iconFrame:SetWidth(36)
iconFrame:SetHeight(36)
-- Kill IconFrame's own textures
local iRegions = {iconFrame:GetRegions()}
for _, r in ipairs(iRegions) do
if r:IsObjectType("Texture") then
local tName = r:GetName() or ""
if string.find(tName, "Icon") then
r:SetTexCoord(0.08, 0.92, 0.08, 0.92)
r:ClearAllPoints()
r:SetAllPoints(iconFrame)
r.sfKeep = true
else
r:SetTexture(nil)
r:SetAlpha(0)
r:Hide()
r.sfNuked = true
r:ClearAllPoints()
r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999)
end
end
end
-- Clean icon border
local iconBorder = frame:CreateTexture(nil, "ARTWORK")
iconBorder:SetTexture("Interface\\Buttons\\WHITE8X8")
iconBorder:SetVertexColor(0.4, 0.4, 0.45, 1)
iconBorder:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", -2, 2)
iconBorder:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", 2, -2)
iconBorder.sfKeep = true
local qualGlow = frame:CreateTexture(nil, "OVERLAY")
qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
qualGlow:SetBlendMode("ADD")
qualGlow:SetAlpha(0.8)
qualGlow:SetWidth(68)
qualGlow:SetHeight(68)
qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0)
qualGlow:Hide()
qualGlow.sfKeep = true
frame.sfQualGlow = qualGlow
end
-- 7) Item Name (shorter width to make room for buttons)
local itemNameFS = _G[fname.."Name"]
if itemNameFS then
itemNameFS:ClearAllPoints()
itemNameFS:SetPoint("LEFT", iconFrame, "RIGHT", 10, 0)
itemNameFS:SetWidth(145)
itemNameFS:SetHeight(36)
itemNameFS:SetJustifyH("LEFT")
itemNameFS:SetJustifyV("MIDDLE")
end
-- 8) Buttons: right side, vertically centered with icon
local need = _G[fname.."NeedButton"]
local greed = _G[fname.."GreedButton"]
local pass = _G[fname.."PassButton"]
-- Create our own close/pass button since Blizz keeps hiding PassButton
local closeBtn = CreateFrame("Button", nil, frame)
closeBtn:SetWidth(20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
closeBtn:SetFrameLevel(frame:GetFrameLevel() + 10)
-- Rounded backdrop matching UI
closeBtn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
local _cbg = _A and _A.closeBtnBg or { 0.5, 0.1, 0.1, 0.85 }
local _cbd = _A and _A.closeBtnBorder or { 0.6, 0.2, 0.2, 0.8 }
local _cbgH = _A and _A.closeBtnHoverBg or { 0.7, 0.15, 0.15, 1 }
local _cbdH = _A and _A.closeBtnHoverBorder or { 0.9, 0.3, 0.3, 1 }
closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
-- "X" text
local closeText = closeBtn:CreateFontString(nil, "OVERLAY")
closeText:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 11, "OUTLINE")
closeText:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeText:SetText("X")
closeText:SetTextColor(0.9, 0.8, 0.8)
closeBtn:SetScript("OnClick", function()
local parent = this:GetParent()
if parent and parent.rollID then
ConfirmLootRoll(parent.rollID, 0)
end
end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_cbgH[1], _cbgH[2], _cbgH[3], _cbgH[4])
this:SetBackdropBorderColor(_cbdH[1], _cbdH[2], _cbdH[3], _cbdH[4])
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText("放弃 / Pass")
GameTooltip:Show()
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4])
this:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4])
GameTooltip:Hide()
end)
-- Position need/greed to the left of close button
if need and greed then
greed:ClearAllPoints()
greed:SetPoint("RIGHT", closeBtn, "LEFT", -6, 0)
greed:SetWidth(22); greed:SetHeight(22)
greed:Show()
need:ClearAllPoints()
need:SetPoint("RIGHT", greed, "LEFT", -4, 0)
need:SetWidth(22); need:SetHeight(22)
need:Show()
end
-- Hide Blizz pass button
if pass then
pass:ClearAllPoints()
pass:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999)
pass:SetAlpha(0)
end
-- 9) HIDE Blizz timer completely
local blizzTimer = _G[fname.."Timer"]
if blizzTimer then
blizzTimer:SetAlpha(0)
blizzTimer:ClearAllPoints()
blizzTimer:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, 500)
end
-- 10) Our own timer bar
local myTimer = CreateFrame("StatusBar", nil, frame)
myTimer:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 8)
myTimer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 8)
myTimer:SetHeight(6)
myTimer:SetMinMaxValues(0, 1)
myTimer:SetValue(1)
myTimer:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
myTimer:SetStatusBarColor(0.9, 0.7, 0.15)
local myTimerBg = myTimer:CreateTexture(nil, "BACKGROUND")
myTimerBg:SetTexture("Interface\\Buttons\\WHITE8X8")
myTimerBg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
myTimerBg:SetAllPoints(myTimer)
frame.sfTimer = myTimer
frame.sfTimerMax = nil -- will be set on first OnUpdate
-- 11) OnUpdate: record max time on first tick, then animate
frame:SetScript("OnUpdate", function()
if this.rollID and this.sfTimer then
local timeLeft = GetLootRollTimeLeft(this.rollID)
if timeLeft and timeLeft > 0 then
if not this.sfTimerMax or this.sfTimerMax < timeLeft then
this.sfTimerMax = timeLeft
end
this.sfTimer:SetMinMaxValues(0, this.sfTimerMax)
this.sfTimer:SetValue(timeLeft)
else
this.sfTimer:SetValue(0)
end
end
end)
-- 12) Clean button textures so they don't extend beyond bounds
if need then
local nRegions = {need:GetRegions()}
for _, r in ipairs(nRegions) do
if r:IsObjectType("Texture") then
r:ClearAllPoints()
r:SetAllPoints(need)
end
end
end
if greed then
local gRegions = {greed:GetRegions()}
for _, r in ipairs(gRegions) do
if r:IsObjectType("Texture") then
r:ClearAllPoints()
r:SetAllPoints(greed)
end
end
end
-- 13) Three FontStrings for Need / Greed / Pass (vertical stack, full width)
local textWidth = 276
local needFS = frame:CreateFontString(nil, "OVERLAY")
needFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE")
needFS:SetPoint("TOPLEFT", iconFrame, "BOTTOMLEFT", 0, -4)
needFS:SetWidth(textWidth)
needFS:SetJustifyH("LEFT")
needFS:SetJustifyV("TOP")
needFS:SetTextColor(1, 1, 1)
frame.sfNeedFS = needFS
local greedFS = frame:CreateFontString(nil, "OVERLAY")
greedFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE")
greedFS:SetPoint("TOPLEFT", needFS, "BOTTOMLEFT", 0, -1)
greedFS:SetWidth(textWidth)
greedFS:SetJustifyH("LEFT")
greedFS:SetJustifyV("TOP")
greedFS:SetTextColor(1, 1, 1)
frame.sfGreedFS = greedFS
local passFS = frame:CreateFontString(nil, "OVERLAY")
passFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE")
passFS:SetPoint("TOPLEFT", greedFS, "BOTTOMLEFT", 0, -1)
passFS:SetWidth(textWidth)
passFS:SetJustifyH("LEFT")
passFS:SetJustifyV("TOP")
passFS:SetTextColor(1, 1, 1)
frame.sfPassFS = passFS
end
-- ============================================================
-- Hooks
-- ============================================================
local function RestoreBackdrop(frame)
if not frame.sfSkinned then return end
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4])
frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4])
end
local function UpdateRollQualityGlow(frame)
if frame.sfQualGlow and frame.rollID then
local _, _, _, quality = GetLootRollItemInfo(frame.rollID)
if quality and quality > 1 then
local r, g, b = GetItemQualityColor(quality)
frame.sfQualGlow:SetVertexColor(r, g, b)
frame.sfQualGlow:Show()
else
frame.sfQualGlow:Hide()
end
end
end
local Hook_GroupLootFrame_OnShow = GroupLootFrame_OnShow
function GroupLootFrame_OnShow()
if Hook_GroupLootFrame_OnShow then Hook_GroupLootFrame_OnShow() end
StyleGroupLootFrame(this)
NukeBlizzTextures(this)
RestoreBackdrop(this)
UpdateRollQualityGlow(this)
UpdateRollTrackers()
end
local Hook_GroupLootFrame_Update = GroupLootFrame_Update
function GroupLootFrame_Update()
if Hook_GroupLootFrame_Update then Hook_GroupLootFrame_Update() end
for idx = 1, 4 do
local f = _G["GroupLootFrame"..idx]
if f and f:IsVisible() and f.sfSkinned then
NukeBlizzTextures(f)
RestoreBackdrop(f)
UpdateRollQualityGlow(f)
end
end
UpdateRollTrackers()
end

3983
SellPriceDB.lua Normal file

File diff suppressed because it is too large Load Diff

1257
SetupWizard.lua Normal file

File diff suppressed because it is too large Load Diff

2331
SocialUI.lua Normal file

File diff suppressed because it is too large Load Diff

983
SpellBookUI.lua Normal file
View File

@@ -0,0 +1,983 @@
--------------------------------------------------------------------------------
-- Nanami-UI: SpellBook UI (SpellBookUI.lua)
-- Replaces the default SpellBookFrame with a modern rounded UI
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.SpellBookUI = {}
local SB = SFrames.SpellBookUI
SFramesDB = SFramesDB or {}
--------------------------------------------------------------------------------
-- Theme (aligned with CharacterPanel / SocialUI standard palette)
--------------------------------------------------------------------------------
local T = SFrames.ActiveTheme
--------------------------------------------------------------------------------
-- Layout (single table to stay under upvalue limit)
--------------------------------------------------------------------------------
local L = {
RIGHT_TAB_W = 56,
SIDE_PAD = 10,
CONTENT_W = 316,
HEADER_H = 30,
SPELL_COLS = 2,
SPELL_ROWS = 8,
SPELL_H = 38,
ICON_SIZE = 30,
PAGE_H = 26,
OPTIONS_H = 26,
TAB_H = 40,
TAB_GAP = 2,
TAB_ICON = 26,
BOOK_TAB_W = 52,
BOOK_TAB_H = 22,
}
L.SPELLS_PER_PAGE = L.SPELL_COLS * L.SPELL_ROWS
L.SPELL_W = (L.CONTENT_W - 4) / L.SPELL_COLS
L.MAIN_W = L.CONTENT_W + L.SIDE_PAD * 2
L.FRAME_W = L.MAIN_W + L.RIGHT_TAB_W + 4
L.FRAME_H = L.HEADER_H + 6 + L.SPELL_ROWS * L.SPELL_H + 6 + L.PAGE_H + 4 + L.OPTIONS_H + 10
--------------------------------------------------------------------------------
-- State (single table to stay under upvalue limit)
--------------------------------------------------------------------------------
local S = {
frame = nil,
spellButtons = {},
tabButtons = {},
bookTabs = {},
currentTab = 1,
currentPage = 1,
currentBook = "spell",
initialized = false,
filteredCache = nil,
scanTip = nil,
}
local widgetId = 0
local function NextName(p)
widgetId = widgetId + 1
return "SFramesSB" .. (p or "") .. tostring(widgetId)
end
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local bg = bgColor or T.panelBg
local bd = borderColor or T.panelBorder
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end
local function SetPixelBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
if bgColor then
frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1)
end
if borderColor then
frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
end
end
local function CreateShadow(parent)
local s = CreateFrame("Frame", nil, parent)
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.6)
s:SetBackdropBorderColor(0, 0, 0, 0.45)
return s
end
local function MakeFS(parent, size, justifyH, color)
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), size or 11, "OUTLINE")
fs:SetJustifyH(justifyH or "LEFT")
local c = color or T.nameText
fs:SetTextColor(c[1], c[2], c[3])
return fs
end
local function MakeButton(parent, text, w, h)
local btn = CreateFrame("Button", NextName("Btn"), parent)
btn:SetWidth(w or 80)
btn:SetHeight(h or 22)
SetRoundBackdrop(btn, T.btnBg, T.btnBorder)
local fs = MakeFS(btn, 10, "CENTER", T.btnText)
fs:SetPoint("CENTER", 0, 0)
fs:SetText(text or "")
btn.text = fs
btn:SetScript("OnEnter", function()
SetRoundBackdrop(this, T.btnHoverBg, T.tabActiveBorder)
this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
end)
btn:SetScript("OnLeave", function()
SetRoundBackdrop(this, T.btnBg, T.btnBorder)
this.text:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
end)
return btn
end
local function MakeSep(parent, y)
local sep = parent:CreateTexture(nil, "ARTWORK")
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
sep:SetHeight(1)
sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, y)
sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y)
sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
return sep
end
--------------------------------------------------------------------------------
-- Hide Blizzard SpellBook
--------------------------------------------------------------------------------
local function HideBlizzardSpellBook()
if not SpellBookFrame then return end
SpellBookFrame:SetAlpha(0)
SpellBookFrame:EnableMouse(false)
SpellBookFrame:ClearAllPoints()
SpellBookFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000)
if SpellBookFrame.SetScript then
SpellBookFrame:SetScript("OnShow", function() this:Hide() end)
end
end
--------------------------------------------------------------------------------
-- Spell Data Helpers
--------------------------------------------------------------------------------
local function GetBookType()
if S.currentBook == "pet" then return BOOKTYPE_PET or "pet" end
return BOOKTYPE_SPELL or "spell"
end
local function GetTabInfo()
local numTabs = GetNumSpellTabs()
local tabs = {}
for i = 1, numTabs do
local name, texture, offset, numSpells = GetSpellTabInfo(i)
if name then
table.insert(tabs, {
name = name, texture = texture,
offset = offset, numSpells = numSpells, index = i,
})
end
end
return tabs
end
local function GetCurrentTabSpells()
local tabs = GetTabInfo()
local tab = tabs[S.currentTab]
if not tab then return 0, 0 end
return tab.offset, tab.numSpells
end
local function GetFilteredSpellList()
local offset, numSpells = GetCurrentTabSpells()
local bookType = GetBookType()
if not SFramesDB.spellBookHighestOnly then
local list = {}
for i = 1, numSpells do
table.insert(list, offset + i)
end
S.filteredCache = list
return list
end
local seen = {}
local order = {}
for i = 1, numSpells do
local idx = offset + i
local name, rank = GetSpellName(idx, bookType)
if name then
if seen[name] then
for k = 1, table.getn(order) do
if order[k].name == name then
order[k].idx = idx
break
end
end
else
seen[name] = true
table.insert(order, { name = name, idx = idx })
end
end
end
local list = {}
for _, v in ipairs(order) do
table.insert(list, v.idx)
end
S.filteredCache = list
return list
end
local function GetMaxPages()
local list = S.filteredCache or GetFilteredSpellList()
return math.max(1, math.ceil(table.getn(list) / L.SPELLS_PER_PAGE))
end
--------------------------------------------------------------------------------
-- Auto-Replace Action Bar (lower rank -> highest rank)
--------------------------------------------------------------------------------
local function EnsureScanTooltip()
if S.scanTip then return end
S.scanTip = CreateFrame("GameTooltip", "SFramesSBScanTip", UIParent, "GameTooltipTemplate")
S.scanTip:SetOwner(UIParent, "ANCHOR_NONE")
S.scanTip:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 1000, 1000)
end
local function AutoReplaceActionBarSpells()
if not SFramesDB.spellBookAutoReplace then return end
if UnitAffectingCombat and UnitAffectingCombat("player") then return end
EnsureScanTooltip()
local highestByName = {}
local highestRankText = {}
local numTabs = GetNumSpellTabs()
for tab = 1, numTabs do
local _, _, offset, numSpells = GetSpellTabInfo(tab)
for i = 1, numSpells do
local idx = offset + i
local name, rank = GetSpellName(idx, "spell")
if name and not IsSpellPassive(idx, "spell") then
highestByName[name] = idx
highestRankText[name] = rank or ""
end
end
end
local tipLeft = getglobal("SFramesSBScanTipTextLeft1")
local tipRight = getglobal("SFramesSBScanTipTextRight1")
for slot = 1, 120 do
if HasAction(slot) then
S.scanTip:ClearLines()
S.scanTip:SetAction(slot)
local actionName = tipLeft and tipLeft:GetText()
local actionRank = tipRight and tipRight:GetText()
if actionName and highestByName[actionName] then
local bestRank = highestRankText[actionName]
if bestRank and bestRank ~= "" and actionRank and actionRank ~= bestRank then
PickupSpell(highestByName[actionName], "spell")
PlaceAction(slot)
ClearCursor()
end
end
end
end
end
--------------------------------------------------------------------------------
-- Slot backdrop helper: backdrop on a SEPARATE child at lower frameLevel
-- so icon / text render cleanly above it (same fix as ActionBars)
--------------------------------------------------------------------------------
local function SetSlotBg(btn, bgColor, borderColor)
if not btn.sfBg then return end
SetPixelBackdrop(btn.sfBg, bgColor or T.slotBg, borderColor or T.slotBorder)
end
local function CreateSlotBackdrop(btn)
if btn:GetBackdrop() then btn:SetBackdrop(nil) end
local level = btn:GetFrameLevel()
local bd = CreateFrame("Frame", nil, btn)
bd:SetFrameLevel(level > 0 and (level - 1) or 0)
bd:SetAllPoints(btn)
SetPixelBackdrop(bd, T.slotBg, T.slotBorder)
btn.sfBg = bd
return bd
end
--------------------------------------------------------------------------------
-- Update Spell Buttons
--------------------------------------------------------------------------------
local function UpdateSpellButtons()
if not S.frame or not S.frame:IsShown() then return end
local list = GetFilteredSpellList()
local bookType = GetBookType()
local startIdx = (S.currentPage - 1) * L.SPELLS_PER_PAGE
local totalSpells = table.getn(list)
for i = 1, L.SPELLS_PER_PAGE do
local btn = S.spellButtons[i]
if not btn then break end
local listIdx = startIdx + i
local spellIdx = list[listIdx]
if spellIdx and listIdx <= totalSpells then
local spellName, spellRank = GetSpellName(spellIdx, bookType)
local texture = GetSpellTexture(spellIdx, bookType)
btn.icon:SetTexture(texture)
btn.icon:SetAlpha(1)
btn.nameFS:SetText(spellName or "")
btn.subFS:SetText(spellRank or "")
btn.spellId = spellIdx
btn.bookType = bookType
local isPassive = IsSpellPassive(spellIdx, bookType)
if isPassive then
btn.nameFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
btn.subFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
btn.passiveBadge:Show()
else
btn.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
btn.subFS:SetTextColor(T.subText[1], T.subText[2], T.subText[3])
btn.icon:SetVertexColor(1, 1, 1)
btn.passiveBadge:Hide()
end
SetSlotBg(btn, T.slotBg, T.slotBorder)
btn:Show()
btn:Enable()
else
btn.icon:SetTexture(nil)
btn.icon:SetAlpha(0)
btn.nameFS:SetText("")
btn.subFS:SetText("")
btn.spellId = nil
btn.bookType = nil
btn.passiveBadge:Hide()
SetSlotBg(btn, T.emptySlotBg, T.emptySlotBd)
btn:Show()
btn:Disable()
end
end
local maxPages = GetMaxPages()
local f = S.frame
if f.pageText then
if maxPages <= 1 then
f.pageText:SetText("")
else
f.pageText:SetText(S.currentPage .. " / " .. maxPages)
end
end
if f.prevBtn then
if S.currentPage > 1 then
f.prevBtn:Enable(); f.prevBtn:SetAlpha(1)
else
f.prevBtn:Disable(); f.prevBtn:SetAlpha(0.4)
end
end
if f.nextBtn then
if S.currentPage < maxPages then
f.nextBtn:Enable(); f.nextBtn:SetAlpha(1)
else
f.nextBtn:Disable(); f.nextBtn:SetAlpha(0.4)
end
end
end
--------------------------------------------------------------------------------
-- Update Skill Line Tabs (right side)
--------------------------------------------------------------------------------
local function UpdateSkillLineTabs()
local tabs = GetTabInfo()
for i = 1, 8 do
local btn = S.tabButtons[i]
if not btn then break end
local tab = tabs[i]
if tab then
if tab.texture then
btn.tabIcon:SetTexture(tab.texture)
btn.tabIcon:Show()
else
btn.tabIcon:Hide()
end
btn.tabLabel:SetText(tab.name or "")
btn:Show()
if i == S.currentTab then
SetRoundBackdrop(btn, T.tabActiveBg, T.tabActiveBorder)
btn.tabIcon:SetVertexColor(1, 1, 1)
btn.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
btn.indicator:Show()
else
SetRoundBackdrop(btn, T.tabBg, T.tabBorder)
btn.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3])
btn.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
btn.indicator:Hide()
end
else
btn:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Update Book Tabs (Spell / Pet)
--------------------------------------------------------------------------------
local function UpdateBookTabs()
for i = 1, 2 do
local bt = S.bookTabs[i]
if not bt then break end
local isActive = (i == 1 and S.currentBook == "spell") or (i == 2 and S.currentBook == "pet")
if isActive then
SetRoundBackdrop(bt, T.tabActiveBg, T.tabActiveBorder)
bt.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
SetRoundBackdrop(bt, T.tabBg, T.tabBorder)
bt.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end
local petTab = S.bookTabs[2]
if petTab then
local hasPet = HasPetSpells and HasPetSpells()
if hasPet then petTab:Show() else petTab:Hide() end
end
end
local function FullRefresh()
S.filteredCache = nil
UpdateSkillLineTabs()
UpdateBookTabs()
UpdateSpellButtons()
if S.frame and S.frame.optHighest then
S.frame.optHighest:UpdateVisual()
end
if S.frame and S.frame.optReplace then
S.frame.optReplace:UpdateVisual()
end
end
--------------------------------------------------------------------------------
-- Build: Header
--------------------------------------------------------------------------------
local function BuildHeader(f)
local header = CreateFrame("Frame", nil, f)
header:SetHeight(L.HEADER_H)
header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
local function MakeBookTab(parent, label, idx, xOff)
local bt = CreateFrame("Button", NextName("BookTab"), parent)
bt:SetWidth(L.BOOK_TAB_W)
bt:SetHeight(L.BOOK_TAB_H)
bt:SetPoint("LEFT", parent, "LEFT", xOff, 0)
SetRoundBackdrop(bt, T.tabBg, T.tabBorder)
local txt = MakeFS(bt, 10, "CENTER", T.tabText)
txt:SetPoint("CENTER", 0, 0)
txt:SetText(label)
bt.text = txt
bt.bookIdx = idx
bt:SetScript("OnClick", function()
if this.bookIdx == 1 then S.currentBook = "spell" else S.currentBook = "pet" end
S.currentTab = 1
S.currentPage = 1
FullRefresh()
end)
bt:SetScript("OnEnter", function()
this.text:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
end)
bt:SetScript("OnLeave", function()
local active = (this.bookIdx == 1 and S.currentBook == "spell") or (this.bookIdx == 2 and S.currentBook == "pet")
if active then
this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
this.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end)
return bt
end
S.bookTabs[1] = MakeBookTab(header, "法术", 1, 8)
S.bookTabs[2] = MakeBookTab(header, "宠物", 2, 8 + L.BOOK_TAB_W + 4)
local titleIco = SFrames:CreateIcon(header, "spellbook", 14)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetPoint("CENTER", header, "CENTER", -28, 0)
titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3])
local title = MakeFS(header, 13, "CENTER", T.gold)
title:SetPoint("LEFT", titleIco, "RIGHT", 4, 0)
title:SetText("法术书")
f.titleFS = title
local closeBtn = CreateFrame("Button", NextName("Close"), header)
closeBtn:SetWidth(20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0)
SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
closeBtn:SetScript("OnClick", function() this:GetParent():GetParent():Hide() end)
local closeIco = SFrames:CreateIcon(closeBtn, "close", 12)
closeIco:SetDrawLayer("OVERLAY")
closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeIco:SetVertexColor(1, 0.7, 0.7)
closeBtn.nanamiIcon = closeIco
closeBtn:SetScript("OnEnter", function()
SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd)
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 1, 1) end
end)
closeBtn:SetScript("OnLeave", function()
SetRoundBackdrop(this, T.buttonDownBg, T.btnBorder)
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 0.7, 0.7) end
end)
MakeSep(f, -L.HEADER_H)
end
--------------------------------------------------------------------------------
-- Build: Right Skill Line Tabs
--------------------------------------------------------------------------------
local function BuildSkillTabs(f)
for idx = 1, 8 do
local btn = CreateFrame("Button", NextName("Tab"), f)
btn:SetWidth(L.RIGHT_TAB_W)
btn:SetHeight(L.TAB_H)
btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3,
-(L.HEADER_H + 4 + (idx - 1) * (L.TAB_H + L.TAB_GAP)))
SetRoundBackdrop(btn, T.tabBg, T.tabBorder)
btn.tabIndex = idx
local indicator = btn:CreateTexture(nil, "OVERLAY")
indicator:SetTexture("Interface\\Buttons\\WHITE8X8")
indicator:SetWidth(3)
indicator:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -4)
indicator:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 1, 4)
indicator:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4])
indicator:Hide()
btn.indicator = indicator
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetWidth(L.TAB_ICON)
icon:SetHeight(L.TAB_ICON)
icon:SetPoint("TOP", btn, "TOP", 0, -4)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
btn.tabIcon = icon
local label = MakeFS(btn, 7, "CENTER", T.tabText)
label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3)
label:SetWidth(L.RIGHT_TAB_W - 6)
btn.tabLabel = label
btn:SetScript("OnClick", function()
S.currentTab = this.tabIndex
S.currentPage = 1
FullRefresh()
end)
btn:SetScript("OnEnter", function()
if this.tabIndex ~= S.currentTab then
SetRoundBackdrop(this, T.slotHover, T.tabActiveBorder)
this.tabIcon:SetVertexColor(1, 1, 1)
this.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
end
local tabs = GetTabInfo()
local tab = tabs[this.tabIndex]
if tab then
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:AddLine(tab.name, T.gold[1], T.gold[2], T.gold[3])
GameTooltip:AddLine(tab.numSpells .. " 个法术", T.subText[1], T.subText[2], T.subText[3])
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
if this.tabIndex ~= S.currentTab then
SetRoundBackdrop(this, T.tabBg, T.tabBorder)
this.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3])
this.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end)
btn:Hide()
S.tabButtons[idx] = btn
end
local tabSep = f:CreateTexture(nil, "ARTWORK")
tabSep:SetTexture("Interface\\Buttons\\WHITE8X8")
tabSep:SetWidth(1)
tabSep:SetPoint("TOPLEFT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + 5), -(L.HEADER_H + 2))
tabSep:SetPoint("BOTTOMLEFT", f, "BOTTOMRIGHT", -(L.RIGHT_TAB_W + 5), 4)
tabSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
end
--------------------------------------------------------------------------------
-- Build: Spell Buttons Grid
-- Backdrop on a SEPARATE child frame at lower frameLevel (ActionBars fix)
--------------------------------------------------------------------------------
local function BuildSpellGrid(f)
local contentTop = -(L.HEADER_H + 6)
local contentFrame = CreateFrame("Frame", nil, f)
contentFrame:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, contentTop)
contentFrame:SetWidth(L.CONTENT_W)
contentFrame:SetHeight(L.SPELL_ROWS * L.SPELL_H + 4)
for row = 1, L.SPELL_ROWS do
for col = 1, L.SPELL_COLS do
local idx = (row - 1) * L.SPELL_COLS + col
local btn = CreateFrame("Button", NextName("Spell"), contentFrame)
btn:SetWidth(L.SPELL_W - 2)
btn:SetHeight(L.SPELL_H - 2)
local x = (col - 1) * L.SPELL_W + 1
local y = -((row - 1) * L.SPELL_H)
btn:SetPoint("TOPLEFT", contentFrame, "TOPLEFT", x, y)
CreateSlotBackdrop(btn)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetWidth(L.ICON_SIZE)
icon:SetHeight(L.ICON_SIZE)
icon:SetPoint("LEFT", btn, "LEFT", 5, 0)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
btn.icon = icon
local nameFS = MakeFS(btn, 11, "LEFT", T.nameText)
nameFS:SetPoint("TOPLEFT", icon, "TOPRIGHT", 6, -2)
nameFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
btn.nameFS = nameFS
local subFS = MakeFS(btn, 9, "LEFT", T.subText)
subFS:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 6, 2)
subFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
btn.subFS = subFS
local passiveBadge = MakeFS(btn, 7, "RIGHT", T.passive)
passiveBadge:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -4, -3)
passiveBadge:SetText("被动")
passiveBadge:Hide()
btn.passiveBadge = passiveBadge
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
btn:SetScript("OnClick", function()
if not this.spellId then return end
if arg1 == "LeftButton" then
CastSpell(this.spellId, this.bookType)
elseif arg1 == "RightButton" then
PickupSpell(this.spellId, this.bookType)
end
end)
btn:SetScript("OnDragStart", function()
if this.spellId then
PickupSpell(this.spellId, this.bookType)
end
end)
btn:SetScript("OnEnter", function()
if this.spellId then
SetSlotBg(this, T.slotHover, T.slotSelected)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetSpell(this.spellId, this.bookType)
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
if this.spellId then
SetSlotBg(this, T.slotBg, T.slotBorder)
else
SetSlotBg(this, T.emptySlotBg, T.emptySlotBd)
end
GameTooltip:Hide()
end)
S.spellButtons[idx] = btn
end
end
contentFrame:EnableMouseWheel(true)
contentFrame:SetScript("OnMouseWheel", function()
if arg1 > 0 then
if S.currentPage > 1 then
S.currentPage = S.currentPage - 1
UpdateSpellButtons()
end
else
if S.currentPage < GetMaxPages() then
S.currentPage = S.currentPage + 1
UpdateSpellButtons()
end
end
end)
return contentTop
end
--------------------------------------------------------------------------------
-- Build: Pagination
--------------------------------------------------------------------------------
local function BuildPagination(f, contentTop)
local pageY = contentTop - L.SPELL_ROWS * L.SPELL_H - 6
local prevBtn = MakeButton(f, "< 上一页", 66, L.PAGE_H)
prevBtn:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, pageY)
prevBtn:SetScript("OnClick", function()
if S.currentPage > 1 then
S.currentPage = S.currentPage - 1
UpdateSpellButtons()
end
end)
f.prevBtn = prevBtn
local nextBtn = MakeButton(f, "下一页 >", 66, L.PAGE_H)
nextBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + L.SIDE_PAD + 4), pageY)
nextBtn:SetScript("OnClick", function()
if S.currentPage < GetMaxPages() then
S.currentPage = S.currentPage + 1
UpdateSpellButtons()
end
end)
f.nextBtn = nextBtn
local pageText = MakeFS(f, 11, "CENTER", T.pageText)
pageText:SetPoint("LEFT", prevBtn, "RIGHT", 4, 0)
pageText:SetPoint("RIGHT", nextBtn, "LEFT", -4, 0)
f.pageText = pageText
return pageY
end
--------------------------------------------------------------------------------
-- Build: Options bar
--------------------------------------------------------------------------------
local function BuildOptions(f, pageY)
local optY = pageY - L.PAGE_H - 4
MakeSep(f, optY + 2)
local function MakeCheckOption(parent, label, xOff, yOff, getFunc, setFunc)
local btn = CreateFrame("Button", NextName("Opt"), parent)
btn:SetHeight(L.OPTIONS_H - 4)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", xOff, yOff)
local box = CreateFrame("Frame", nil, btn)
box:SetWidth(12)
box:SetHeight(12)
box:SetPoint("LEFT", btn, "LEFT", 0, 0)
SetPixelBackdrop(box, T.checkOff, T.tabBorder)
btn.box = box
local checkMark = MakeFS(box, 10, "CENTER", T.checkOn)
checkMark:SetPoint("CENTER", 0, 1)
checkMark:SetText("")
btn.checkMark = checkMark
local txt = MakeFS(btn, 9, "LEFT", T.optionText)
txt:SetPoint("LEFT", box, "RIGHT", 4, 0)
txt:SetText(label)
btn.label = txt
btn:SetWidth(txt:GetStringWidth() + 20)
btn.getFunc = getFunc
btn.setFunc = setFunc
function btn:UpdateVisual()
if self.getFunc() then
self.checkMark:SetText("")
SetPixelBackdrop(self.box, { T.checkOn[1]*0.3, T.checkOn[2]*0.3, T.checkOn[3]*0.3, 0.8 }, T.checkOn)
self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
self.checkMark:SetText("")
SetPixelBackdrop(self.box, T.checkOff, T.tabBorder)
self.label:SetTextColor(T.optionText[1], T.optionText[2], T.optionText[3])
end
end
btn:SetScript("OnClick", function()
this.setFunc(not this.getFunc())
this:UpdateVisual()
FullRefresh()
end)
btn:SetScript("OnEnter", function()
this.label:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
end)
btn:SetScript("OnLeave", function()
this:UpdateVisual()
end)
btn:UpdateVisual()
return btn
end
f.optHighest = MakeCheckOption(f, "只显示最高等级", L.SIDE_PAD, optY,
function() return SFramesDB.spellBookHighestOnly == true end,
function(v) SFramesDB.spellBookHighestOnly = v; S.currentPage = 1 end
)
f.optReplace = MakeCheckOption(f, "学习新等级自动替换动作条", L.SIDE_PAD + 120, optY,
function() return SFramesDB.spellBookAutoReplace == true end,
function(v) SFramesDB.spellBookAutoReplace = v end
)
end
--------------------------------------------------------------------------------
-- Build Main Frame
--------------------------------------------------------------------------------
local function BuildMainFrame()
if S.frame then return end
local f = CreateFrame("Frame", "SFramesSpellBookUI", UIParent)
f:SetWidth(L.FRAME_W)
f:SetHeight(L.FRAME_H)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
f:SetFrameStrata("HIGH")
f:SetFrameLevel(10)
SetRoundBackdrop(f)
CreateShadow(f)
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetScript("OnShow", function()
if SpellBookFrame and SpellBookFrame:IsShown() then
SpellBookFrame:Hide()
end
FullRefresh()
end)
BuildHeader(f)
BuildSkillTabs(f)
local contentTop = BuildSpellGrid(f)
local pageY = BuildPagination(f, contentTop)
BuildOptions(f, pageY)
table.insert(UISpecialFrames, "SFramesSpellBookUI")
local scale = SFramesDB.spellBookScale or 1
if scale ~= 1 then f:SetScale(scale) end
S.frame = f
f:Hide()
end
--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------
function SB:Toggle(bookType)
if not S.initialized then return end
if not S.frame then BuildMainFrame() end
if bookType then
if bookType == (BOOKTYPE_PET or "pet") then
S.currentBook = "pet"
else
S.currentBook = "spell"
end
end
if S.frame:IsShown() then
S.frame:Hide()
else
S.currentTab = 1
S.currentPage = 1
S.frame:Show()
end
end
function SB:Show(bookType)
if not S.initialized then return end
if not S.frame then BuildMainFrame() end
if bookType then
if bookType == (BOOKTYPE_PET or "pet") then
S.currentBook = "pet"
else
S.currentBook = "spell"
end
end
S.currentTab = 1
S.currentPage = 1
S.frame:Show()
end
function SB:Hide()
if S.frame and S.frame:IsShown() then
S.frame:Hide()
end
end
function SB:IsShown()
return S.frame and S.frame:IsShown()
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function SB:Initialize()
if S.initialized then return end
S.initialized = true
if SFramesDB.spellBookHighestOnly == nil then SFramesDB.spellBookHighestOnly = false end
if SFramesDB.spellBookAutoReplace == nil then SFramesDB.spellBookAutoReplace = false end
HideBlizzardSpellBook()
BuildMainFrame()
local ef = CreateFrame("Frame", nil, UIParent)
ef:RegisterEvent("SPELLS_CHANGED")
ef:RegisterEvent("LEARNED_SPELL_IN_TAB")
ef:RegisterEvent("SPELL_UPDATE_COOLDOWN")
ef:SetScript("OnEvent", function()
if event == "LEARNED_SPELL_IN_TAB" then
AutoReplaceActionBarSpells()
end
if S.frame and S.frame:IsShown() then
FullRefresh()
end
end)
end
--------------------------------------------------------------------------------
-- Hook ToggleSpellBook
--------------------------------------------------------------------------------
local origToggleSpellBook = ToggleSpellBook
ToggleSpellBook = function(bookType)
if SFramesDB and SFramesDB.enableSpellBook == false then
if origToggleSpellBook then
origToggleSpellBook(bookType)
end
return
end
SB:Toggle(bookType)
end
--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------
local bootstrap = CreateFrame("Frame", nil, UIParent)
bootstrap:RegisterEvent("PLAYER_LOGIN")
bootstrap:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
if SFramesDB.enableSpellBook == nil then
SFramesDB.enableSpellBook = true
end
if SFramesDB.enableSpellBook ~= false then
SB:Initialize()
end
end
end)

868
StatSummary.lua Normal file
View File

@@ -0,0 +1,868 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Stat Summary & Equipment List (StatSummary.lua)
--------------------------------------------------------------------------------
SFrames.StatSummary = {}
local SS = SFrames.StatSummary
local summaryFrame
--------------------------------------------------------------------------------
-- Theme (reuse CharacterPanel colors)
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend({
enchanted = { 0.30, 1, 0.30 },
noEnchant = { 1, 0.35, 0.35 },
setColor = { 1, 0.85, 0.0 },
defColor = { 0.35, 0.90, 0.25 },
physColor = { 0.90, 0.75, 0.55 },
spellColor = { 0.60, 0.80, 1.00 },
regenColor = { 0.40, 0.90, 0.70 },
statColors = {
str = { 0.78, 0.61, 0.43 },
agi = { 0.52, 1, 0.52 },
sta = { 0.75, 0.55, 0.25 },
int = { 0.41, 0.80, 0.94 },
spi = { 1, 1, 1 },
},
resistColors = {
arcane = { 0.95, 0.90, 0.40 },
fire = { 1, 0.50, 0.15 },
nature = { 0.35, 0.90, 0.25 },
frost = { 0.45, 0.70, 1.00 },
shadow = { 0.60, 0.35, 0.90 },
},
})
local PANEL_W = 220
local PANEL_H = 490
local HEADER_H = 24
local ROW_H = 14
local SECTION_GAP = 4
local SIDE_PAD = 6
local QUALITY_COLORS = {
[0] = { 0.62, 0.62, 0.62 },
[1] = { 1, 1, 1 },
[2] = { 0.12, 1, 0 },
[3] = { 0.0, 0.44, 0.87 },
[4] = { 0.64, 0.21, 0.93 },
[5] = { 1, 0.5, 0 },
}
local ENCHANTABLE_SLOTS = {
{ id = 1, name = "HeadSlot", label = "头部" },
{ id = 3, name = "ShoulderSlot", label = "肩部" },
{ id = 15, name = "BackSlot", label = "背部" },
{ id = 5, name = "ChestSlot", label = "胸部" },
{ id = 9, name = "WristSlot", label = "手腕" },
{ id = 10, name = "HandsSlot", label = "手套" },
{ id = 7, name = "LegsSlot", label = "腿部" },
{ id = 8, name = "FeetSlot", label = "脚部" },
{ id = 11, name = "Finger0Slot", label = "戒指1" },
{ id = 12, name = "Finger1Slot", label = "戒指2" },
{ id = 16, name = "MainHandSlot", label = "主手" },
{ id = 17, name = "SecondaryHandSlot",label = "副手" },
{ id = 18, name = "RangedSlot", label = "远程" },
}
local widgetId = 0
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function NN(p)
widgetId = widgetId + 1
return "SFramesSS" .. (p or "") .. widgetId
end
local function SetBD(f, bg, bd)
f: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 }
})
local b = bg or T.bg
local d = bd or T.border
f:SetBackdropColor(b[1], b[2], b[3], b[4] or 1)
f:SetBackdropBorderColor(d[1], d[2], d[3], d[4] or 1)
end
local function SetPBD(f, bg, bd)
f:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
if bg then f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) end
if bd then f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) end
end
local function FS(parent, size, jh, color)
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), size or 10, "OUTLINE")
fs:SetJustifyH(jh or "LEFT")
local c = color or T.valueText
fs:SetTextColor(c[1], c[2], c[3])
return fs
end
--------------------------------------------------------------------------------
-- Stat helpers
--------------------------------------------------------------------------------
local function GetIBL()
if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then
local ok, lib = pcall(function() return AceLibrary("ItemBonusLib-1.0") end)
if ok and lib then return lib end
end
return nil
end
local function GearB(key)
local lib = GetIBL()
if lib and lib.GetBonus then return lib:GetBonus(key) or 0 end
return 0
end
local function TryAPI(names)
for i = 1, table.getn(names) do
local fn = _G[names[i]]
if fn then
local ok, val = pcall(fn)
if ok and type(val) == "number" and val > 0 then return val end
end
end
return 0
end
local function TryAPIa(names, a1)
for i = 1, table.getn(names) do
local fn = _G[names[i]]
if fn then
local ok, val = pcall(fn, a1)
if ok and type(val) == "number" and val > 0 then return val end
end
end
return 0
end
local function GetCS()
if SFrames and SFrames.CharacterPanel and SFrames.CharacterPanel.CS then
return SFrames.CharacterPanel.CS
end
return nil
end
--------------------------------------------------------------------------------
-- Scroll frame
--------------------------------------------------------------------------------
local function MakeScroll(parent, w, h)
local holder = CreateFrame("Frame", NN("H"), parent)
holder:SetWidth(w)
holder:SetHeight(h)
local sf = CreateFrame("ScrollFrame", NN("S"), holder)
sf:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0)
sf:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -8, 0)
local child = CreateFrame("Frame", NN("C"), sf)
child:SetWidth(w - 12)
child:SetHeight(1)
sf:SetScrollChild(child)
local sb = CreateFrame("Slider", NN("B"), holder)
sb:SetWidth(5)
sb:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2)
sb:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2)
sb:SetOrientation("VERTICAL")
sb:SetMinMaxValues(0, 1)
sb:SetValue(0)
SetPBD(sb, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 })
local thumb = sb:CreateTexture(nil, "OVERLAY")
thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7)
thumb:SetWidth(5)
thumb:SetHeight(24)
sb:SetThumbTexture(thumb)
sb:SetScript("OnValueChanged", function()
sf:SetVerticalScroll(this:GetValue())
end)
holder:EnableMouseWheel(1)
holder:SetScript("OnMouseWheel", function()
local cur = sb:GetValue()
local step = 24
local lo, mx = sb:GetMinMaxValues()
if arg1 > 0 then
sb:SetValue(math.max(cur - step, 0))
else
sb:SetValue(math.min(cur + step, mx))
end
end)
holder.sf = sf
holder.child = child
holder.sb = sb
holder.UpdateH = function(_, ch)
child:SetHeight(ch)
local visH = sf:GetHeight()
local maxS = math.max(ch - visH, 0)
sb:SetMinMaxValues(0, maxS)
if maxS == 0 then sb:Hide() else sb:Show() end
sb:SetValue(math.min(sb:GetValue(), maxS))
end
return holder
end
--------------------------------------------------------------------------------
-- Enchant detection (comprehensive for vanilla 1.12 / Turtle WoW)
--
-- Vanilla WoW tooltip green text types:
-- 1. Enchant effects (what we detect)
-- 2. "装备:" / "Equip:" item innate effects
-- 3. "使用:" / "Use:" item use effects
-- 4. "击中时可能:" / "Chance on hit:" proc effects
-- 5. "(X) 套装:" active set bonuses
-- Strategy: exclude #2-5, then positive-match enchant patterns.
--
-- Covers: standard enchanting, libram/arcanum (head/legs), ZG class enchants,
-- ZG/Naxx shoulder augments, armor kits, weapon chains, scopes, spurs,
-- counterweights, and Turtle WoW custom enchants.
--------------------------------------------------------------------------------
local scanTip
local function EnsureTip()
if not scanTip then
scanTip = CreateFrame("GameTooltip", "SFramesSScanTip", nil, "GameTooltipTemplate")
end
scanTip:SetOwner(UIParent, "ANCHOR_NONE")
return scanTip
end
local PROC_ENCHANTS = {
"十字军", "Crusader",
"吸取生命", "Lifestealing",
"灼热武器", "Fiery Weapon", "火焰武器",
"寒冰", "Icy Chill",
"邪恶武器", "Unholy Weapon",
"恶魔杀手", "Demonslaying",
"无法被缴械", "Cannot be Disarmed",
}
local function IsEnchantLine(txt)
if string.find(txt, "%+%d") then return true end
if string.find(txt, "%d+%%") then return true end
for i = 1, table.getn(PROC_ENCHANTS) do
if string.find(txt, PROC_ENCHANTS[i]) then return true end
end
return false
end
local function GetEnchant(slotId)
local tip = EnsureTip()
tip:ClearLines()
tip:SetInventoryItem("player", slotId)
local n = tip:NumLines()
if not n or n < 2 then return false, nil end
for i = 2, n do
local obj = _G["SFramesSScanTipTextLeft" .. i]
if obj then
local txt = obj:GetText()
if txt and txt ~= "" then
local r, g, b = obj:GetTextColor()
if g > 0.8 and r < 0.5 and b < 0.5 then
local skip = false
if string.find(txt, "装备:") or string.find(txt, "装备:") or string.find(txt, "Equip:") then
skip = true
elseif string.find(txt, "使用:") or string.find(txt, "使用:") or string.find(txt, "Use:") then
skip = true
elseif string.find(txt, "击中时可能") or string.find(txt, "Chance on hit") then
skip = true
elseif string.find(txt, "^%(") then
skip = true
elseif string.find(txt, "套装:") or string.find(txt, "套装:") or string.find(txt, "Set:") then
skip = true
end
if not skip and IsEnchantLine(txt) then
return true, txt
end
end
end
end
end
return false, nil
end
--------------------------------------------------------------------------------
-- Set bonus detection
--------------------------------------------------------------------------------
local function GetSets()
local tip = EnsureTip()
local sets = {}
local seen = {}
local slots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 }
for si = 1, table.getn(slots) do
local sid = slots[si]
local link = GetInventoryItemLink("player", sid)
if link then
tip:ClearLines()
tip:SetOwner(UIParent, "ANCHOR_NONE")
tip:SetInventoryItem("player", sid)
local nl = tip:NumLines()
if nl then
for li = 1, nl do
local obj = _G["SFramesSScanTipTextLeft" .. li]
if obj then
local txt = obj:GetText()
if txt then
local a, b, sn, sc, sm
a, b, sn, sc, sm = string.find(txt, "^(.-)%s*%((%d+)/(%d+)%)$")
if sn and sc and sm then
sn = string.gsub(sn, "^%s+", "")
sn = string.gsub(sn, "%s+$", "")
if sn ~= "" and not seen[sn] then
seen[sn] = true
table.insert(sets, {
name = sn,
current = tonumber(sc) or 0,
max = tonumber(sm) or 0,
})
end
end
end
end
end
end
end
end
return sets
end
--------------------------------------------------------------------------------
-- Build the panel
--------------------------------------------------------------------------------
local function BuildPanel()
if summaryFrame then return summaryFrame end
local f = CreateFrame("Frame", "SFramesStatSummary", UIParent)
f:SetWidth(PANEL_W)
f:SetHeight(PANEL_H)
f:SetPoint("CENTER", UIParent, "CENTER", 200, 0)
f:SetFrameStrata("DIALOG")
f:SetFrameLevel(100)
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetClampedToScreen(true)
SetBD(f, T.bg, T.border)
f:Hide()
-- Header
local hdr = FS(f, 11, "LEFT", T.gold)
hdr:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6)
hdr:SetText("属性总览")
-- Tab buttons
local tabW = 60
f.tabs = {}
f.curTab = 1
local tNames = { "属性", "装备" }
for i = 1, 2 do
local btn = CreateFrame("Button", NN("T"), f)
btn:SetWidth(tabW)
btn:SetHeight(16)
btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(SIDE_PAD + (2 - i) * (tabW + 2) + 20), -5)
btn:SetFrameLevel(f:GetFrameLevel() + 2)
SetPBD(btn, T.tabBg, T.tabBorder)
local lbl = FS(btn, 9, "CENTER", T.tabText)
lbl:SetPoint("CENTER", btn, "CENTER", 0, 0)
lbl:SetText(tNames[i])
btn.lbl = lbl
btn.idx = i
btn:SetScript("OnClick", function() SS:SetTab(this.idx) end)
f.tabs[i] = btn
end
-- Close
local cb = CreateFrame("Button", nil, f)
cb:SetWidth(14)
cb:SetHeight(14)
cb:SetPoint("TOPRIGHT", f, "TOPRIGHT", -5, -5)
cb:SetFrameLevel(f:GetFrameLevel() + 3)
SetBD(cb, T.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }, T.btnBorder or { 0.45, 0.1, 0.1, 0.6 })
local cx = FS(cb, 8, "CENTER", { 1, 0.7, 0.7 })
cx:SetPoint("CENTER", cb, "CENTER", 0, 0)
cx:SetText("x")
cb:SetScript("OnClick", function() f:Hide() end)
-- Sep
local sep = f:CreateTexture(nil, "ARTWORK")
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
sep:SetHeight(1)
sep:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -HEADER_H)
sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -HEADER_H)
-- Content
local cH = PANEL_H - HEADER_H - 8
local cW = PANEL_W - 8
f.statsScroll = MakeScroll(f, cW, cH)
f.statsScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(HEADER_H + 2))
f.equipScroll = MakeScroll(f, cW, cH)
f.equipScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(HEADER_H + 2))
f.equipScroll:Hide()
summaryFrame = f
return f
end
--------------------------------------------------------------------------------
-- Tab switching
--------------------------------------------------------------------------------
function SS:SetTab(idx)
if not summaryFrame then return end
summaryFrame.curTab = idx
for i = 1, 2 do
local btn = summaryFrame.tabs[i]
if i == idx then
SetPBD(btn, T.tabActiveBg, T.tabActiveBorder)
btn.lbl:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
SetPBD(btn, T.tabBg, T.tabBorder)
btn.lbl:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end
if idx == 1 then
summaryFrame.statsScroll:Show()
summaryFrame.equipScroll:Hide()
local ok, err = pcall(function() SS:BuildStats() end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444SS BuildStats err: " .. tostring(err) .. "|r")
end
else
summaryFrame.statsScroll:Hide()
summaryFrame.equipScroll:Show()
local ok, err = pcall(function() SS:BuildEquip() end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444SS BuildEquip err: " .. tostring(err) .. "|r")
end
end
end
--------------------------------------------------------------------------------
-- Helpers for building rows
--------------------------------------------------------------------------------
local function HideRows(parent)
if parent._r then
for i = 1, table.getn(parent._r) do
local r = parent._r[i]
if r and r.Hide then r:Hide() end
end
end
parent._r = {}
end
local function AddHeader(p, txt, y, clr)
local f1 = FS(p, 10, "LEFT", clr or T.sectionTitle)
f1:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y)
f1:SetText(txt)
tinsert(p._r, f1)
local s = p:CreateTexture(nil, "ARTWORK")
s:SetTexture("Interface\\Buttons\\WHITE8X8")
s:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
s:SetHeight(1)
s:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y - 13)
s:SetPoint("TOPRIGHT", p, "TOPRIGHT", -4, y - 13)
tinsert(p._r, s)
return y - 16
end
local function AddRow(p, lbl, val, y, lc, vc)
local f1 = FS(p, 9, "LEFT", lc or T.labelText)
f1:SetPoint("TOPLEFT", p, "TOPLEFT", 8, y)
f1:SetText(lbl)
tinsert(p._r, f1)
local f2 = FS(p, 9, "RIGHT", vc or T.valueText)
f2:SetPoint("TOPRIGHT", p, "TOPRIGHT", -12, y)
f2:SetWidth(100)
f2:SetJustifyH("RIGHT")
f2:SetText(val)
tinsert(p._r, f2)
return y - ROW_H
end
--------------------------------------------------------------------------------
-- Stats page
--------------------------------------------------------------------------------
function SS:BuildStats()
local child = summaryFrame.statsScroll.child
HideRows(child)
local y = -4
-- Primary Stats
y = AddHeader(child, "主属性:", y)
local sn = { "力量", "敏捷", "耐力", "智力", "精神" }
local sc = { T.statColors.str, T.statColors.agi, T.statColors.sta, T.statColors.int, T.statColors.spi }
for i = 1, 5 do
local _, eff = UnitStat("player", i)
y = AddRow(child, sn[i], tostring(math.floor(eff or 0)), y, sc[i], sc[i])
end
y = y - SECTION_GAP
-- Defense
y = AddHeader(child, "防御属性:", y, T.defColor)
local hp = UnitHealthMax("player") or 0
y = AddRow(child, "生命值", tostring(hp), y, nil, T.defColor)
local dodge = 0
if GetDodgeChance then dodge = GetDodgeChance() or 0 end
if dodge == 0 then dodge = GearB("DODGE") end
y = AddRow(child, "躲闪", string.format("%.2f%%", dodge), y)
local baseA, effA = UnitArmor("player")
effA = math.floor(effA or baseA or 0)
y = AddRow(child, "护甲", tostring(effA), y)
local lvl = UnitLevel("player") or 60
local k1 = 400 + 85 * lvl
local p1 = effA / (effA + k1) * 100
if p1 > 75 then p1 = 75 end
y = AddRow(child, "物理免伤同级", string.format("%.2f%%", p1), y)
local k2 = 400 + 85 * (lvl + 3)
local p2 = effA / (effA + k2) * 100
if p2 > 75 then p2 = 75 end
y = AddRow(child, "物理免伤骷髅级", string.format("%.2f%%", p2), y)
y = y - SECTION_GAP
-- Resistances
y = AddHeader(child, "抗性:", y, T.resistColors.arcane)
local rInfo = {
{ 6, "奥术抗性", T.resistColors.arcane },
{ 2, "火焰抗性", T.resistColors.fire },
{ 3, "自然抗性", T.resistColors.nature },
{ 4, "冰霜抗性", T.resistColors.frost },
{ 5, "暗影抗性", T.resistColors.shadow },
}
for i = 1, table.getn(rInfo) do
local ri = rInfo[i]
local _, tot = UnitResistance("player", ri[1])
y = AddRow(child, ri[2], tostring(math.floor(tot or 0)), y, ri[3], ri[3])
end
y = y - SECTION_GAP
-- Physical
y = AddHeader(child, "物理:", y, T.physColor)
local mb, mp, mn = UnitAttackPower("player")
local mAP = math.floor((mb or 0) + (mp or 0) + (mn or 0))
local rb, rp, rn = UnitRangedAttackPower("player")
local rAP = math.floor((rb or 0) + (rp or 0) + (rn or 0))
y = AddRow(child, "近战/远程攻强", mAP .. "/" .. rAP, y, nil, T.physColor)
local cs = GetCS()
local mHit = cs and cs.SafeGetMeleeHit and cs.SafeGetMeleeHit() or 0
if mHit == 0 then
mHit = TryAPI({ "GetHitModifier", "GetMeleeHitModifier" })
if mHit == 0 then mHit = GearB("TOHIT") end
end
y = AddRow(child, "近战/远程命中", string.format("+%d%%/+%d%%", mHit, mHit), y)
local mCrit = cs and cs.SafeGetMeleeCrit and cs.SafeGetMeleeCrit() or 0
if mCrit == 0 then
mCrit = TryAPI({ "GetCritChance", "GetMeleeCritChance" })
if mCrit == 0 then mCrit = GearB("CRIT") end
end
local rCrit = cs and cs.SafeGetRangedCrit and cs.SafeGetRangedCrit() or 0
if rCrit == 0 then
rCrit = TryAPI({ "GetRangedCritChance" })
if rCrit == 0 then rCrit = mCrit end
end
y = AddRow(child, "近战/远程暴击", string.format("%.2f%%/%.2f%%", mCrit, rCrit), y)
y = y - SECTION_GAP
-- Spell
y = AddHeader(child, "法术:", y, T.spellColor)
local mana = UnitManaMax("player") or 0
y = AddRow(child, "法力值", tostring(mana), y, nil, T.spellColor)
local sDmg = 0
if GetSpellBonusDamage then
for s = 2, 7 do
local d = GetSpellBonusDamage(s) or 0
if d > sDmg then sDmg = d end
end
end
if sDmg == 0 then
local lib = GetIBL()
if lib and lib.GetBonus then
local baseDmg = lib:GetBonus("DMG") or 0
sDmg = baseDmg
local schools = { "FIREDMG","FROSTDMG","SHADOWDMG","ARCANEDMG","NATUREDMG","HOLYDMG" }
for _, sk in ipairs(schools) do
local sv = baseDmg + (lib:GetBonus(sk) or 0)
if sv > sDmg then sDmg = sv end
end
end
end
y = AddRow(child, "法术伤害", tostring(math.floor(sDmg)), y)
local sHeal = 0
if GetSpellBonusHealing then sHeal = GetSpellBonusHealing() or 0 end
if sHeal == 0 then sHeal = GearB("HEAL") end
y = AddRow(child, "法术治疗", tostring(math.floor(sHeal)), y)
local sCrit = cs and cs.SafeGetSpellCrit and cs.SafeGetSpellCrit() or 0
if sCrit == 0 then
sCrit = TryAPIa({ "GetSpellCritChance" }, 2)
if sCrit == 0 then sCrit = GearB("SPELLCRIT") end
end
y = AddRow(child, "法术暴击", string.format("%.2f%%", sCrit), y)
local sHit = cs and cs.SafeGetSpellHit and cs.SafeGetSpellHit() or 0
if sHit == 0 then
sHit = TryAPI({ "GetSpellHitModifier" })
if sHit == 0 then sHit = GearB("SPELLTOHIT") end
end
y = AddRow(child, "法术命中", string.format("+%d%%", sHit), y)
local sHaste = GearB("SPELLHASTE")
if sHaste > 0 then
y = AddRow(child, "急速", string.format("+%.2f%%", sHaste), y)
end
local sPen = GearB("SPELLPEN")
if sPen > 0 then
y = AddRow(child, "法术穿透", "+" .. tostring(math.floor(sPen)), y)
end
y = y - SECTION_GAP
-- Regen
y = AddHeader(child, "回复:", y, T.regenColor)
local mp5 = GearB("MANAREG")
y = AddRow(child, "装备回蓝", tostring(math.floor(mp5)) .. " MP/5s", y, nil, T.regenColor)
local _, spi = UnitStat("player", 5)
spi = math.floor(spi or 0)
local spReg = math.floor(15 + spi / 5)
y = AddRow(child, "精神回蓝", spReg .. " MP/2s", y)
y = y - SECTION_GAP
-- Set bonuses
local ok2, sets = pcall(GetSets)
if ok2 and sets and table.getn(sets) > 0 then
y = AddHeader(child, "套装:", y, T.setColor)
for i = 1, table.getn(sets) do
local s = sets[i]
y = AddRow(child, s.name, "(" .. s.current .. "/" .. s.max .. ")", y, T.setColor, T.setColor)
end
end
summaryFrame.statsScroll:UpdateH(math.abs(y) + 12)
end
--------------------------------------------------------------------------------
-- Equipment page
--------------------------------------------------------------------------------
function SS:BuildEquip()
local child = summaryFrame.equipScroll.child
HideRows(child)
local y = -4
y = AddHeader(child, "装备列表 & 附魔检查:", y, T.gold)
local totalSlots = 0
local enchCount = 0
for si = 1, table.getn(ENCHANTABLE_SLOTS) do
local slot = ENCHANTABLE_SLOTS[si]
local link = GetInventoryItemLink("player", slot.id)
if link then
totalSlots = totalSlots + 1
local _, _, rawName = string.find(link, "%[(.-)%]")
local itemName = rawName or slot.label
local quality = nil
local _, _, qH = string.find(link, "|c(%x+)|H")
local qMap = {}
qMap["ff9d9d9d"] = 0
qMap["ffffffff"] = 1
qMap["ff1eff00"] = 2
qMap["ff0070dd"] = 3
qMap["ffa335ee"] = 4
qMap["ffff8000"] = 5
if qH then quality = qMap[qH] end
local nc = (quality and QUALITY_COLORS[quality]) or T.valueText
-- Slot label
local sf = FS(child, 8, "LEFT", T.dimText)
sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y)
sf:SetText(slot.label)
sf:SetWidth(32)
tinsert(child._r, sf)
-- Item name
local nf = FS(child, 9, "LEFT", nc)
nf:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y)
nf:SetWidth(130)
if string.len(itemName) > 18 then
itemName = string.sub(itemName, 1, 16) .. ".."
end
nf:SetText(itemName)
tinsert(child._r, nf)
-- Enchant check
local hasE, eTxt = false, nil
local eOk, eR1, eR2 = pcall(GetEnchant, slot.id)
if eOk then hasE = eR1; eTxt = eR2 end
local ico = FS(child, 9, "RIGHT")
ico:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, y)
if hasE then
ico:SetTextColor(T.enchanted[1], T.enchanted[2], T.enchanted[3])
ico:SetText("*")
enchCount = enchCount + 1
else
ico:SetTextColor(T.noEnchant[1], T.noEnchant[2], T.noEnchant[3])
ico:SetText("-")
end
tinsert(child._r, ico)
y = y - ROW_H
if hasE and eTxt then
local ef = FS(child, 8, "LEFT", T.enchanted)
ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y)
ef:SetWidth(155)
ef:SetText(" " .. eTxt)
tinsert(child._r, ef)
y = y - 12
elseif not hasE then
local ef = FS(child, 8, "LEFT", T.noEnchant)
ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y)
ef:SetText(" 未附魔")
tinsert(child._r, ef)
y = y - 12
end
y = y - 2
else
local sf = FS(child, 8, "LEFT", T.dimText)
sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y)
sf:SetText(slot.label)
tinsert(child._r, sf)
local ef = FS(child, 9, "LEFT", T.dimText)
ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y)
ef:SetText("-- 未装备 --")
tinsert(child._r, ef)
y = y - ROW_H - 2
end
end
y = y - SECTION_GAP
y = AddHeader(child, "附魔统计:", y, T.gold)
local sc2 = (enchCount == totalSlots) and T.enchanted or T.noEnchant
y = AddRow(child, "已附魔/总装备", enchCount .. "/" .. totalSlots, y, nil, sc2)
if enchCount < totalSlots then
y = AddRow(child, "缺少附魔", tostring(totalSlots - enchCount) .. "", y, T.noEnchant, T.noEnchant)
end
summaryFrame.equipScroll:UpdateH(math.abs(y) + 12)
end
--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------
function SS:Toggle()
local ok, err = pcall(function()
BuildPanel()
if summaryFrame:IsShown() then
summaryFrame:Hide()
return
end
local cpf = _G["SFramesCharacterPanel"]
if cpf and cpf:IsShown() then
summaryFrame:ClearAllPoints()
summaryFrame:SetPoint("TOPLEFT", cpf, "TOPRIGHT", 2, 0)
else
summaryFrame:ClearAllPoints()
summaryFrame:SetPoint("CENTER", UIParent, "CENTER", 200, 0)
end
local scale = SFramesDB and SFramesDB.charPanelScale or 1.0
summaryFrame:SetScale(scale)
summaryFrame:Show()
SS:SetTab(summaryFrame.curTab or 1)
end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] SS:Toggle error: " .. tostring(err) .. "|r")
end
end
function SS:Show()
local ok, err = pcall(function()
BuildPanel()
local cpf = _G["SFramesCharacterPanel"]
if cpf and cpf:IsShown() then
summaryFrame:ClearAllPoints()
summaryFrame:SetPoint("TOPLEFT", cpf, "TOPRIGHT", 2, 0)
end
local scale = SFramesDB and SFramesDB.charPanelScale or 1.0
summaryFrame:SetScale(scale)
summaryFrame:Show()
SS:SetTab(summaryFrame.curTab or 1)
end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] SS:Show error: " .. tostring(err) .. "|r")
end
end
function SS:Hide()
if summaryFrame then summaryFrame:Hide() end
end
function SS:IsShown()
return summaryFrame and summaryFrame:IsShown()
end
function SS:Refresh()
if not summaryFrame or not summaryFrame:IsShown() then return end
SS:SetTab(summaryFrame.curTab or 1)
end
--------------------------------------------------------------------------------
-- Events
--------------------------------------------------------------------------------
local evf = CreateFrame("Frame", "SFramesSSEv", UIParent)
evf:RegisterEvent("UNIT_INVENTORY_CHANGED")
evf:RegisterEvent("PLAYER_AURAS_CHANGED")
evf:RegisterEvent("UNIT_ATTACK_POWER")
evf:RegisterEvent("UNIT_RESISTANCES")
evf:SetScript("OnEvent", function()
if summaryFrame and summaryFrame:IsShown() then
SS:Refresh()
end
end)
DEFAULT_CHAT_FRAME:AddMessage("SF: Loading StatSummary.lua...")

269
Theme.lua Normal file
View File

@@ -0,0 +1,269 @@
--------------------------------------------------------------------------------
-- Nanami-UI: Central Theme Engine (Theme.lua)
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.Theme = {}
SFrames.ActiveTheme = {}
local function HSVtoRGB(h, s, v)
if s <= 0 then return v, v, v end
h = h - math.floor(h / 360) * 360
local hh = h / 60
local i = math.floor(hh)
local f = hh - i
local p = v * (1 - s)
local q = v * (1 - s * f)
local t = v * (1 - s * (1 - f))
if i == 0 then return v, t, p
elseif i == 1 then return q, v, p
elseif i == 2 then return p, v, t
elseif i == 3 then return p, q, v
elseif i == 4 then return t, p, v
else return v, p, q end
end
local function toHexChar(n)
if n < 10 then return string.char(48 + n) end
return string.char(97 + n - 10)
end
local function RGBtoHex(r, g, b)
local rr = math.floor(r * 255 + 0.5)
local gg = math.floor(g * 255 + 0.5)
local bb = math.floor(b * 255 + 0.5)
return "ff"
.. toHexChar(math.floor(rr / 16)) .. toHexChar(rr - math.floor(rr / 16) * 16)
.. toHexChar(math.floor(gg / 16)) .. toHexChar(gg - math.floor(gg / 16) * 16)
.. toHexChar(math.floor(bb / 16)) .. toHexChar(bb - math.floor(bb / 16) * 16)
end
SFrames.Theme.Presets = {}
SFrames.Theme.Presets["pink"] = { name = "Pink", hue = 330, satMul = 1.00 }
SFrames.Theme.Presets["frost"] = { name = "Frost", hue = 210, satMul = 1.00 }
SFrames.Theme.Presets["emerald"] = { name = "Emerald", hue = 140, satMul = 0.85 }
SFrames.Theme.Presets["flame"] = { name = "Flame", hue = 25, satMul = 0.90 }
SFrames.Theme.Presets["shadow"] = { name = "Shadow", hue = 270, satMul = 0.90 }
SFrames.Theme.Presets["golden"] = { name = "Golden", hue = 45, satMul = 0.80 }
SFrames.Theme.Presets["teal"] = { name = "Teal", hue = 175, satMul = 0.85 }
SFrames.Theme.Presets["crimson"] = { name = "Crimson", hue = 355, satMul = 0.90 }
SFrames.Theme.Presets["holy"] = { name = "Holy", hue = 220, satMul = 0.15 }
SFrames.Theme.PresetOrder = { "pink", "frost", "emerald", "flame", "shadow", "golden", "teal", "crimson", "holy" }
SFrames.Theme.ClassMap = {}
SFrames.Theme.ClassMap["WARRIOR"] = "crimson"
SFrames.Theme.ClassMap["MAGE"] = "frost"
SFrames.Theme.ClassMap["ROGUE"] = "teal"
SFrames.Theme.ClassMap["DRUID"] = "flame"
SFrames.Theme.ClassMap["HUNTER"] = "emerald"
SFrames.Theme.ClassMap["SHAMAN"] = "frost"
SFrames.Theme.ClassMap["PRIEST"] = "holy"
SFrames.Theme.ClassMap["WARLOCK"] = "shadow"
SFrames.Theme.ClassMap["PALADIN"] = "golden"
local function GenerateTheme(H, satMul)
satMul = satMul or 1.0
local function S(s)
local v = s * satMul
if v > 1 then v = 1 end
return v
end
local function C3(s, v)
local r, g, b = HSVtoRGB(H, S(s), v)
return { r, g, b }
end
local function C4(s, v, a)
local r, g, b = HSVtoRGB(H, S(s), v)
return { r, g, b, a }
end
local t = {}
t.accent = C4(0.40, 0.80, 0.98)
t.accentDark = C3(0.45, 0.55)
t.accentLight = C3(0.30, 1.00)
t.accentHex = RGBtoHex(t.accentLight[1], t.accentLight[2], t.accentLight[3])
t.panelBg = C4(0.50, 0.12, 0.95)
t.panelBorder = C4(0.45, 0.55, 0.90)
t.headerBg = C4(0.60, 0.10, 0.98)
t.sectionBg = C4(0.43, 0.14, 0.82)
t.sectionBorder = C4(0.38, 0.45, 0.86)
t.bg = t.panelBg
t.border = t.panelBorder
t.slotBg = C4(0.20, 0.07, 0.90)
t.slotBorder = C4(0.10, 0.28, 0.80)
t.slotHover = C4(0.38, 0.40, 0.90)
t.slotSelected = C4(0.43, 0.70, 1.00)
t.buttonBg = C4(0.44, 0.18, 0.94)
t.buttonBorder = C4(0.40, 0.50, 0.90)
t.buttonHoverBg = C4(0.47, 0.30, 0.96)
t.buttonDownBg = C4(0.50, 0.14, 0.96)
t.buttonDisabledBg = C4(0.43, 0.14, 0.65)
t.buttonActiveBg = C4(0.52, 0.42, 0.98)
t.buttonActiveBorder = C4(0.42, 0.90, 1.00)
t.buttonText = C3(0.16, 0.90)
t.buttonActiveText = C3(0.08, 1.00)
t.buttonDisabledText = C4(0.14, 0.55, 0.68)
t.btnBg = t.buttonBg
t.btnBorder = t.buttonBorder
t.btnHoverBg = t.buttonHoverBg
t.btnHoverBd = C4(0.40, 0.80, 0.98)
t.btnDownBg = t.buttonDownBg
t.btnText = t.buttonText
t.btnActiveText = t.buttonActiveText
t.btnDisabledText = C3(0.14, 0.40)
t.btnHover = C4(0.47, 0.30, 0.95)
t.btnHoverBorder = t.btnHoverBd
t.tabBg = t.buttonBg
t.tabBorder = t.buttonBorder
t.tabActiveBg = C4(0.50, 0.32, 0.96)
t.tabActiveBorder = C4(0.40, 0.80, 0.98)
t.tabText = C3(0.21, 0.70)
t.tabActiveText = t.buttonActiveText
t.checkBg = t.buttonBg
t.checkBorder = t.buttonBorder
t.checkHoverBorder = C4(0.40, 0.80, 0.95)
t.checkFill = C4(0.43, 0.88, 0.98)
t.checkOn = C3(0.40, 0.80)
t.checkOff = C4(0.40, 0.25, 0.60)
t.sliderTrack = C4(0.45, 0.22, 0.90)
t.sliderFill = C4(0.35, 0.85, 0.92)
t.sliderThumb = C4(0.25, 1.00, 0.95)
t.text = C3(0.11, 0.92)
t.title = C3(0.30, 1.00)
t.gold = t.title
t.nameText = C3(0.06, 0.92)
t.dimText = C3(0.25, 0.60)
t.bodyText = C3(0.05, 0.82)
t.sectionTitle = C3(0.24, 0.90)
t.catHeader = C3(0.31, 0.80)
t.colHeader = C3(0.25, 0.80)
t.labelText = C3(0.23, 0.65)
t.valueText = t.text
t.subText = t.labelText
t.pageText = C3(0.19, 0.80)
t.objectiveText = C3(0.10, 0.90)
t.optionText = t.tabText
t.countText = t.tabText
t.trackText = C3(0.25, 0.80)
t.divider = C4(0.45, 0.55, 0.40)
t.sepColor = C4(0.44, 0.45, 0.50)
t.scrollThumb = C4(0.45, 0.55, 0.70)
t.scrollTrack = C4(0.50, 0.08, 0.50)
t.inputBg = C4(0.50, 0.08, 0.95)
t.inputBorder = C4(0.38, 0.40, 0.80)
t.searchBg = C4(0.50, 0.08, 0.80)
t.searchBorder = C4(0.38, 0.40, 0.60)
t.progressBg = C4(0.50, 0.08, 0.90)
t.progressFill = C4(0.50, 0.70, 1.00)
t.modelBg = C4(0.60, 0.08, 0.85)
t.modelBorder = C4(0.43, 0.35, 0.70)
t.emptySlot = C4(0.40, 0.25, 0.40)
t.emptySlotBg = C4(0.50, 0.08, 0.40)
t.emptySlotBd = C4(0.40, 0.25, 0.30)
t.barBg = C4(0.60, 0.10, 1.00)
t.rowNormal = C4(0.50, 0.06, 0.30)
t.rowNormalBd = C4(0.22, 0.20, 0.30)
t.raidGroup = t.sectionBg
t.raidGroupBorder = C4(0.38, 0.40, 0.70)
t.raidSlotEmpty = C4(0.50, 0.08, 0.60)
t.questSelected = C4(0.70, 0.60, 0.85)
t.questSelBorder = C4(0.47, 0.95, 1.00)
t.questSelBar = C4(0.45, 1.00, 1.00)
t.questHover = C4(0.52, 0.25, 0.50)
t.zoneHeader = t.catHeader
t.zoneBg = C4(0.50, 0.14, 0.50)
t.collapseIcon = C3(0.31, 0.70)
t.trackBar = C4(0.53, 0.95, 1.00)
t.trackGlow = C4(0.53, 0.95, 0.22)
t.rewardBg = C4(0.50, 0.10, 0.85)
t.rewardBorder = C4(0.45, 0.40, 0.70)
t.listBg = C4(0.50, 0.08, 0.80)
t.listBorder = C4(0.43, 0.35, 0.60)
t.detailBg = C4(0.50, 0.09, 0.92)
t.detailBorder = t.listBorder
t.selectedRowBg = C4(0.65, 0.35, 0.60)
t.selectedRowBorder = C4(0.50, 0.90, 0.70)
t.selectedNameText = { 1, 0.95, 1 }
t.overlayBg = C4(0.75, 0.04, 0.55)
t.accentLine = C4(0.50, 1.00, 0.90)
t.titleColor = t.title
t.nameColor = { 1, 1, 1 }
t.valueColor = t.text
t.labelColor = C3(0.28, 0.58)
t.dimColor = C3(0.29, 0.48)
t.clockColor = C3(0.18, 1.00)
t.timerColor = C3(0.27, 0.75)
t.brandColor = C4(0.37, 0.60, 0.70)
t.particleColor = C3(0.40, 1.00)
t.wbGold = { 1, 0.88, 0.55 }
t.wbBorder = { 0.95, 0.75, 0.25 }
t.passive = { 0.60, 0.60, 0.65 }
return t
end
function SFrames.Theme.Extend(self, extras)
local t = {}
local src = SFrames.ActiveTheme
if src then
for k, v in pairs(src) do
t[k] = v
end
end
if extras then
for k, v in pairs(extras) do
t[k] = v
end
end
return t
end
function SFrames.Theme.Apply(self, presetKey)
local preset = self.Presets[presetKey or "pink"]
if not preset then preset = self.Presets["pink"] end
local newTheme = GenerateTheme(preset.hue, preset.satMul)
local oldKeys = {}
for k, v in pairs(SFrames.ActiveTheme) do
table.insert(oldKeys, k)
end
for i = 1, table.getn(oldKeys) do
SFrames.ActiveTheme[oldKeys[i]] = nil
end
for k, v in pairs(newTheme) do
SFrames.ActiveTheme[k] = v
end
if SFrames.Config and SFrames.Config.colors then
local a = SFrames.ActiveTheme
SFrames.Config.colors.border = { r = a.accent[1], g = a.accent[2], b = a.accent[3], a = 1 }
SFrames.Config.colors.backdrop = { r = a.panelBg[1], g = a.panelBg[2], b = a.panelBg[3], a = a.panelBg[4] or 0.8 }
end
end
function SFrames.Theme.GetCurrentPreset(self)
if SFramesDB and SFramesDB.Theme then
if SFramesDB.Theme.useClassTheme then
local _, class = UnitClass("player")
if class and self.ClassMap[class] then
return self.ClassMap[class]
end
end
if SFramesDB.Theme.preset and self.Presets[SFramesDB.Theme.preset] then
return SFramesDB.Theme.preset
end
end
return "pink"
end
function SFrames.Theme.GetAccentHex(self)
return SFrames.ActiveTheme.accentHex or "ffffb3d9"
end
SFrames.Theme.HSVtoRGB = HSVtoRGB
SFrames.Theme.RGBtoHex = RGBtoHex
SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset())
local themeInitFrame = CreateFrame("Frame")
themeInitFrame:RegisterEvent("PLAYER_LOGIN")
themeInitFrame:SetScript("OnEvent", function()
SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset())
end)

1181
Tooltip.lua Normal file

File diff suppressed because it is too large Load Diff

1268
Trade.lua Normal file

File diff suppressed because it is too large Load Diff

9852
TradeSkillDB.lua Normal file

File diff suppressed because it is too large Load Diff

2130
TradeSkillUI.lua Normal file

File diff suppressed because it is too large Load Diff

1234
TrainerUI.lua Normal file

File diff suppressed because it is too large Load Diff

1051
Tweaks.lua Normal file

File diff suppressed because it is too large Load Diff

1176
Units/Party.lua Normal file

File diff suppressed because it is too large Load Diff

787
Units/Pet.lua Normal file
View File

@@ -0,0 +1,787 @@
SFrames.Pet = {}
local _A = SFrames.ActiveTheme
local function Clamp(value, minValue, maxValue)
if value < minValue then return minValue end
if value > maxValue then return maxValue end
return value
end
function SFrames.Pet:Initialize()
local f = CreateFrame("Button", "SFramesPetFrame", UIParent)
f:SetWidth(150)
f:SetHeight(30)
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
local pos = SFramesDB.Positions["PetFrame"]
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
else
f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 10, -55)
end
local frameScale = (SFramesDB and type(SFramesDB.petFrameScale) == "number") and SFramesDB.petFrameScale or 1
f:SetScale(Clamp(frameScale, 0.7, 1.8))
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end)
f:SetScript("OnDragStop", function()
f:StopMovingOrSizing()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
if SpellIsTargeting() then
SpellTargetUnit("pet")
elseif CursorHasItem() then
DropItemOnUnit("pet")
else
TargetUnit("pet")
end
else
ToggleDropDownMenu(1, nil, PetFrameDropDown, "SFramesPetFrame", 106, 27)
end
end)
f:SetScript("OnReceiveDrag", function()
if CursorHasItem() then
DropItemOnUnit("pet")
end
end)
f:SetScript("OnEnter", function()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit("pet")
GameTooltip:Show()
end)
f:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
SFrames:CreateUnitBackdrop(f)
-- Health Bar
f.health = SFrames:CreateStatusBar(f, "SFramesPetHealth")
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
f.health:SetHeight(18)
local hbg = CreateFrame("Frame", nil, f)
hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
f.health.bg:SetTexture(SFrames:GetTexture())
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesPetPower")
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
local pbg = CreateFrame("Frame", nil, f)
pbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
pbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(pbg)
f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
f.power.bg:SetAllPoints()
f.power.bg:SetTexture(SFrames:GetTexture())
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Texts
local fontPath = SFrames:GetFont()
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT")
f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0)
f.nameText:SetWidth(75)
f.nameText:SetHeight(12)
f.nameText:SetJustifyH("LEFT")
f.nameText:SetFont(fontPath, 10, outline)
f.nameText:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1)
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
f.healthText:SetFont(fontPath, 10, outline)
f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -1)
-- Happiness Icon (for hunters)
local hBG = CreateFrame("Frame", nil, f)
hBG:SetWidth(20)
hBG:SetHeight(20)
hBG:SetPoint("RIGHT", f, "LEFT", -2, 0)
SFrames:CreateUnitBackdrop(hBG)
f.happiness = hBG:CreateTexture(nil, "OVERLAY")
f.happiness:SetPoint("TOPLEFT", hBG, "TOPLEFT", 1, -1)
f.happiness:SetPoint("BOTTOMRIGHT", hBG, "BOTTOMRIGHT", -1, 1)
f.happiness:SetTexture("Interface\\PetPaperDollFrame\\UI-PetHappiness")
f.happinessBG = hBG
f.happinessBG:Hide()
self.frame = f
self.frame.unit = "pet"
f:Hide()
SFrames:RegisterEvent("UNIT_PET", function() if arg1 == "player" then self:UpdateAll() end end)
SFrames:RegisterEvent("PET_BAR_UPDATE", function() self:UpdateAll() end)
SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "pet" then self:UpdateHealth() end end)
SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "pet" then self:UpdateHealth() end end)
SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_FOCUS", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_MAXFOCUS", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "pet" then self:UpdatePower() end end)
SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "pet" then self:UpdatePowerType() end end)
SFrames:RegisterEvent("UNIT_HAPPINESS", function() if arg1 == "pet" then self:UpdateHappiness() end end)
SFrames:RegisterEvent("UNIT_NAME_UPDATE", function() if arg1 == "pet" then self:UpdateAll() end end)
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
self:InitFoodFeature()
self:UpdateAll()
end
function SFrames.Pet:UpdateAll()
if UnitExists("pet") then
if SFramesDB and SFramesDB.showPetFrame == false then
self.frame:Hide()
if self.foodPanel then self.foodPanel:Hide() end
return
end
self.frame:Show()
self:UpdateHealth()
self:UpdatePowerType()
self:UpdatePower()
self:UpdateHappiness()
local name = UnitName("pet")
if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
name = "宠物"
end
self.frame.nameText:SetText(name)
local r, g, b = 0.33, 0.59, 0.33
self.frame.health:SetStatusBarColor(r, g, b)
else
self.frame:Hide()
if self.foodPanel then self.foodPanel:Hide() end
end
end
function SFrames.Pet:UpdateHealth()
local hp = UnitHealth("pet")
local maxHp = UnitHealthMax("pet")
self.frame.health:SetMinMaxValues(0, maxHp)
self.frame.health:SetValue(hp)
if maxHp > 0 then
self.frame.healthText:SetText(hp .. " / " .. maxHp)
else
self.frame.healthText:SetText("")
end
end
function SFrames.Pet:UpdatePowerType()
local powerType = UnitPowerType("pet")
local color = SFrames.Config.colors.power[powerType]
if color then
self.frame.power:SetStatusBarColor(color.r, color.g, color.b)
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
end
function SFrames.Pet:UpdatePower()
local power = UnitMana("pet")
local maxPower = UnitManaMax("pet")
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
end
function SFrames.Pet:UpdateHappiness()
local happiness = GetPetHappiness()
if not happiness then
self.frame.happinessBG:Hide()
self:UpdateFoodButton()
return
end
local isHunter = false
local _, class = UnitClass("player")
if class == "HUNTER" then isHunter = true end
if isHunter then
if happiness == 1 then
self.frame.happiness:SetTexCoord(0.375, 0.5625, 0, 0.359375)
self.frame.happinessBG:Show()
elseif happiness == 2 then
self.frame.happiness:SetTexCoord(0.1875, 0.375, 0, 0.359375)
self.frame.happinessBG:Show()
elseif happiness == 3 then
self.frame.happiness:SetTexCoord(0, 0.1875, 0, 0.359375)
self.frame.happinessBG:Show()
end
else
self.frame.happinessBG:Hide()
end
self:UpdateFoodButton()
end
--------------------------------------------------------------------------------
-- Pet Food Feature (Hunter only)
-- Food button on pet frame, food selection panel, quick-feed via right-click
--------------------------------------------------------------------------------
local petFoodScanTip
local cachedFeedSpell
local function EnsureFoodScanTooltip()
if not petFoodScanTip then
petFoodScanTip = CreateFrame("GameTooltip", "NanamiPetFoodScanTip", UIParent, "GameTooltipTemplate")
petFoodScanTip:SetOwner(UIParent, "ANCHOR_NONE")
end
return petFoodScanTip
end
local function GetFeedPetSpell()
if cachedFeedSpell then return cachedFeedSpell end
for tab = 1, GetNumSpellTabs() do
local _, _, offset, numSpells = GetSpellTabInfo(tab)
for i = offset + 1, offset + numSpells do
local spellName = GetSpellName(i, BOOKTYPE_SPELL)
if spellName and (spellName == "Feed Pet" or spellName == "喂养宠物") then
cachedFeedSpell = spellName
return cachedFeedSpell
end
end
end
cachedFeedSpell = "Feed Pet"
return cachedFeedSpell
end
local REJECT_NAME_PATTERNS = {
"Potion", "potion", "药水",
"Elixir", "elixir", "药剂",
"Flask", "flask", "合剂",
"Bandage", "bandage", "绷带",
"Scroll", "scroll", "卷轴",
"Healthstone", "healthstone", "治疗石",
"Mana Gem", "法力宝石",
"Thistle Tea", "蓟花茶",
"Firewater", "火焰花水",
"Juju", "符咒",
}
local function NameIsRejected(itemName)
for i = 1, table.getn(REJECT_NAME_PATTERNS) do
if string.find(itemName, REJECT_NAME_PATTERNS[i], 1, true) then
return true
end
end
return false
end
local function IsItemPetFood(bag, slot)
local link = GetContainerItemLink(bag, slot)
if not link then return false end
local texture = GetContainerItemInfo(bag, slot)
local _, _, itemIdStr = string.find(link, "item:(%d+)")
local name, itemType, subType
if itemIdStr then
local n, _, _, _, _, t, st = GetItemInfo("item:" .. itemIdStr)
name = n
itemType = t
subType = st
end
if not name then
local _, _, parsed = string.find(link, "%[(.+)%]")
name = parsed
end
if not name then return false end
if NameIsRejected(name) then
return false
end
if itemType then
if itemType ~= "Consumable" and itemType ~= "消耗品" then
return false
end
if subType then
if string.find(subType, "Potion", 1, true) or string.find(subType, "药水", 1, true)
or string.find(subType, "Elixir", 1, true) or string.find(subType, "药剂", 1, true)
or string.find(subType, "Flask", 1, true) or string.find(subType, "合剂", 1, true)
or string.find(subType, "Bandage", 1, true) or string.find(subType, "绷带", 1, true)
or string.find(subType, "Scroll", 1, true) or string.find(subType, "卷轴", 1, true) then
return false
end
if string.find(subType, "Food", 1, true) or string.find(subType, "食物", 1, true) then
return true, name, texture
end
end
end
local tip = EnsureFoodScanTooltip()
tip:SetOwner(UIParent, "ANCHOR_NONE")
tip:SetBagItem(bag, slot)
local found = false
local rejected = false
for i = 1, tip:NumLines() do
local leftObj = _G["NanamiPetFoodScanTipTextLeft" .. i]
if leftObj then
local text = leftObj:GetText()
if text then
if string.find(text, "进食", 1, true) or string.find(text, "eating", 1, true) then
found = true
end
if string.find(text, "Well Fed", 1, true) or string.find(text, "充分进食", 1, true) then
found = true
end
if string.find(text, "Restores", 1, true) and string.find(text, "health", 1, true)
and string.find(text, "over", 1, true) then
found = true
end
if string.find(text, "恢复", 1, true) and string.find(text, "生命", 1, true)
and string.find(text, "", 1, true) then
found = true
end
if string.find(text, "Potion", 1, true) or string.find(text, "药水", 1, true)
or string.find(text, "Elixir", 1, true) or string.find(text, "药剂", 1, true)
or string.find(text, "Bandage", 1, true) or string.find(text, "绷带", 1, true) then
rejected = true
end
end
end
end
tip:Hide()
if found and not rejected then
return true, name, texture
end
if itemType and subType then
if (itemType == "Consumable" or itemType == "消耗品")
and (subType == "Food & Drink" or subType == "食物和饮料") then
return true, name, texture
end
end
return false
end
function SFrames.Pet:ScanBagsForFood()
local foods = {}
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local isFood, name, itemTex = IsItemPetFood(bag, slot)
if isFood then
local texture, itemCount = GetContainerItemInfo(bag, slot)
local link = GetContainerItemLink(bag, slot)
table.insert(foods, {
bag = bag,
slot = slot,
name = name,
link = link,
texture = texture or itemTex,
count = itemCount or 1,
})
end
end
end
return foods
end
function SFrames.Pet:FeedPet(bag, slot)
if not UnitExists("pet") then return end
local link = GetContainerItemLink(bag, slot)
local tex = GetContainerItemInfo(bag, slot)
local spell = GetFeedPetSpell()
CastSpellByName(spell)
PickupContainerItem(bag, slot)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
if itemId then
if not SFramesDB then SFramesDB = {} end
SFramesDB.lastPetFoodId = tonumber(itemId)
end
end
if tex and self.foodButton then
self.foodButton.icon:SetTexture(tex)
if not SFramesDB then SFramesDB = {} end
SFramesDB.lastPetFoodIcon = tex
end
end
function SFrames.Pet:QuickFeed()
if not UnitExists("pet") then return end
local foods = self:ScanBagsForFood()
if table.getn(foods) == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[Nanami]|r 背包中没有可喂食的食物")
return
end
local preferred = SFramesDB and SFramesDB.lastPetFoodId
if preferred then
for i = 1, table.getn(foods) do
local f = foods[i]
if f.link then
local _, _, itemId = string.find(f.link, "item:(%d+)")
if itemId and tonumber(itemId) == preferred then
self:FeedPet(f.bag, f.slot)
return
end
end
end
end
self:FeedPet(foods[1].bag, foods[1].slot)
end
--------------------------------------------------------------------------------
-- Food Button & Panel UI
--------------------------------------------------------------------------------
local FOOD_COLS = 6
local FOOD_SLOT_SIZE = 30
local FOOD_SLOT_GAP = 2
local FOOD_PAD = 8
function SFrames.Pet:CreateFoodButton()
local f = self.frame
local A = SFrames.ActiveTheme
local btn = CreateFrame("Button", "SFramesPetFoodBtn", f)
btn:SetWidth(20)
btn:SetHeight(20)
btn:SetPoint("TOP", f.happinessBG, "BOTTOM", 0, -2)
SFrames:CreateUnitBackdrop(btn)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
icon:SetTexture("Interface\\Icons\\INV_Misc_Food_14")
btn.icon = icon
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
local pet = self
btn:SetScript("OnClick", function()
if CursorHasItem() then
local curTex = pet:GetCursorItemTexture()
DropItemOnUnit("pet")
if curTex then
pet:SetFoodIcon(curTex)
end
return
end
if arg1 == "RightButton" then
pet:QuickFeed()
else
if pet.foodPanel and pet.foodPanel:IsShown() then
pet.foodPanel:Hide()
else
pet:ShowFoodPanel()
end
end
end)
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine("喂养宠物", 1, 1, 1)
GameTooltip:AddLine("左键: 选择食物", 0.7, 0.7, 0.7)
GameTooltip:AddLine("右键: 快速喂食", 0.7, 0.7, 0.7)
GameTooltip:AddLine("可拖拽背包食物到此按钮", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:SetScript("OnReceiveDrag", function()
if CursorHasItem() then
DropItemOnUnit("pet")
end
end)
self.foodButton = btn
btn:Hide()
end
function SFrames.Pet:CreateFoodPanel()
if self.foodPanel then return self.foodPanel end
local A = SFrames.ActiveTheme
local panel = CreateFrame("Frame", "SFramesPetFoodPanel", UIParent)
panel:SetFrameStrata("DIALOG")
panel:SetFrameLevel(20)
panel:SetWidth(FOOD_COLS * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP) + FOOD_PAD * 2 - FOOD_SLOT_GAP)
panel:SetHeight(80)
panel:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", -22, 4)
SFrames:CreateUnitBackdrop(panel)
panel:EnableMouse(true)
panel:Hide()
local titleBar = CreateFrame("Frame", nil, panel)
titleBar:SetPoint("TOPLEFT", panel, "TOPLEFT", 1, -1)
titleBar:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -1, -1)
titleBar:SetHeight(18)
titleBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
local hdr = A.headerBg or A.panelBg
titleBar:SetBackdropColor(hdr[1], hdr[2], hdr[3], (hdr[4] or 0.9) * 0.6)
local titleText = titleBar:CreateFontString(nil, "OVERLAY")
titleText:SetFont(SFrames:GetFont(), 10, SFrames.Media.fontOutline or "OUTLINE")
titleText:SetPoint("LEFT", titleBar, "LEFT", FOOD_PAD, 0)
titleText:SetTextColor(A.title[1], A.title[2], A.title[3])
titleText:SetText("选择食物")
panel.titleText = titleText
local hintText = panel:CreateFontString(nil, "OVERLAY")
hintText:SetFont(SFrames:GetFont(), 9, SFrames.Media.fontOutline or "OUTLINE")
hintText:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", FOOD_PAD, 4)
hintText:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -FOOD_PAD, 4)
hintText:SetJustifyH("LEFT")
local dim = A.dimText or { 0.5, 0.5, 0.5 }
hintText:SetTextColor(dim[1], dim[2], dim[3])
hintText:SetText("点击喂食 | 可拖拽食物到此面板")
panel.hintText = hintText
local emptyText = panel:CreateFontString(nil, "OVERLAY")
emptyText:SetFont(SFrames:GetFont(), 10, SFrames.Media.fontOutline or "OUTLINE")
emptyText:SetPoint("CENTER", panel, "CENTER", 0, 0)
emptyText:SetTextColor(dim[1], dim[2], dim[3])
emptyText:SetText("背包中没有可喂食的食物")
emptyText:Hide()
panel.emptyText = emptyText
local pet = self
panel:SetScript("OnReceiveDrag", function()
if CursorHasItem() then
local curTex = pet:GetCursorItemTexture()
DropItemOnUnit("pet")
if curTex then
pet:SetFoodIcon(curTex)
end
end
end)
table.insert(UISpecialFrames, "SFramesPetFoodPanel")
panel.slots = {}
self.foodPanel = panel
return panel
end
function SFrames.Pet:CreateFoodSlot(parent, index)
local A = SFrames.ActiveTheme
local slot = CreateFrame("Button", "SFramesPetFoodSlot" .. index, parent)
slot:SetWidth(FOOD_SLOT_SIZE)
slot:SetHeight(FOOD_SLOT_SIZE)
slot:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
slot:SetBackdropColor(A.slotBg[1], A.slotBg[2], A.slotBg[3], A.slotBg[4] or 0.9)
slot:SetBackdropBorderColor(0, 0, 0, 1)
local icon = slot:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", -2, 2)
slot.icon = icon
local count = slot:CreateFontString(nil, "OVERLAY")
count:SetFont(SFrames:GetFont(), 10, "OUTLINE")
count:SetPoint("BOTTOMRIGHT", -2, 2)
count:SetJustifyH("RIGHT")
count:SetTextColor(1, 1, 1)
slot.count = count
slot:RegisterForClicks("LeftButtonUp", "RightButtonUp")
local pet = self
slot:SetScript("OnClick", function()
if CursorHasItem() then
local curTex = pet:GetCursorItemTexture()
DropItemOnUnit("pet")
if curTex then
pet:SetFoodIcon(curTex)
end
return
end
if IsShiftKeyDown() and this.foodLink then
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(this.foodLink)
end
return
end
if this.foodBag and this.foodSlot then
pet:FeedPet(this.foodBag, this.foodSlot)
if pet.foodPanel then pet.foodPanel:Hide() end
end
end)
slot:SetScript("OnEnter", function()
if this.foodBag and this.foodSlot then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetBagItem(this.foodBag, this.foodSlot)
GameTooltip:AddLine(" ")
GameTooltip:AddLine("点击: 喂食宠物", 0.5, 1, 0.5)
GameTooltip:AddLine("Shift+点击: 链接到聊天", 0.7, 0.7, 0.7)
GameTooltip:Show()
end
this:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
end)
slot:SetScript("OnLeave", function()
GameTooltip:Hide()
this:SetBackdropBorderColor(0, 0, 0, 1)
end)
slot:SetScript("OnReceiveDrag", function()
if CursorHasItem() then
DropItemOnUnit("pet")
end
end)
return slot
end
function SFrames.Pet:ShowFoodPanel()
self:CreateFoodPanel()
self:RefreshFoodPanel()
self.foodPanel:Show()
end
function SFrames.Pet:RefreshFoodPanel()
local panel = self.foodPanel
if not panel then return end
local foods = self:ScanBagsForFood()
local numFoods = table.getn(foods)
for i = 1, table.getn(panel.slots) do
panel.slots[i]:Hide()
end
if numFoods == 0 then
panel:SetHeight(60)
panel.emptyText:Show()
panel.hintText:Hide()
return
end
panel.emptyText:Hide()
panel.hintText:Show()
local rows = math.ceil(numFoods / FOOD_COLS)
local panelH = FOOD_PAD + 20 + rows * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP) + 18
panel:SetHeight(panelH)
for i = 1, numFoods do
local food = foods[i]
local slot = panel.slots[i]
if not slot then
slot = self:CreateFoodSlot(panel, i)
panel.slots[i] = slot
end
local col = mod(i - 1, FOOD_COLS)
local row = math.floor((i - 1) / FOOD_COLS)
slot:ClearAllPoints()
slot:SetPoint("TOPLEFT", panel, "TOPLEFT",
FOOD_PAD + col * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP),
-(FOOD_PAD + 18 + row * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP)))
slot.icon:SetTexture(food.texture)
slot.count:SetText(food.count > 1 and tostring(food.count) or "")
slot.foodBag = food.bag
slot.foodSlot = food.slot
slot.foodName = food.name
slot.foodLink = food.link
slot:Show()
end
end
function SFrames.Pet:GetCursorItemTexture()
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local texture, count, locked = GetContainerItemInfo(bag, slot)
if locked and texture then
return texture
end
end
end
return nil
end
function SFrames.Pet:SetFoodIcon(tex)
if not tex or not self.foodButton then return end
self.foodButton.icon:SetTexture(tex)
if not SFramesDB then SFramesDB = {} end
SFramesDB.lastPetFoodIcon = tex
end
function SFrames.Pet:InitFoodFeature()
local _, playerClass = UnitClass("player")
if playerClass ~= "HUNTER" then return end
self:CreateFoodButton()
if SFramesDB and SFramesDB.lastPetFoodIcon then
self.foodButton.icon:SetTexture(SFramesDB.lastPetFoodIcon)
end
local pet = self
SFrames:RegisterEvent("BAG_UPDATE", function()
if pet.foodPanel and pet.foodPanel:IsShown() then
pet:RefreshFoodPanel()
end
end)
end
function SFrames.Pet:UpdateFoodButton()
if not self.foodButton then return end
local _, class = UnitClass("player")
if class == "HUNTER" and UnitExists("pet") then
self.foodButton:Show()
local happiness = GetPetHappiness()
if happiness and happiness == 1 then
self.foodButton.icon:SetVertexColor(1, 0.3, 0.3)
elseif happiness and happiness == 2 then
self.foodButton.icon:SetVertexColor(1, 0.8, 0.4)
else
self.foodButton.icon:SetVertexColor(1, 1, 1)
end
else
self.foodButton:Hide()
if self.foodPanel then self.foodPanel:Hide() end
end
end

1449
Units/Player.lua Normal file

File diff suppressed because it is too large Load Diff

1064
Units/Raid.lua Normal file

File diff suppressed because it is too large Load Diff

1065
Units/TalentTree.lua Normal file

File diff suppressed because it is too large Load Diff

1046
Units/Target.lua Normal file

File diff suppressed because it is too large Load Diff

85
Units/ToT.lua Normal file
View File

@@ -0,0 +1,85 @@
SFrames.ToT = {}
local _A = SFrames.ActiveTheme
function SFrames.ToT:Initialize()
local f = CreateFrame("Button", "SFramesToTFrame", UIParent)
f:SetWidth(120)
f:SetHeight(25)
f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
TargetUnit("targettarget")
end
end)
-- Health Bar
f.health = SFrames:CreateStatusBar(f, "SFramesToTHealth")
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
local hbg = CreateFrame("Frame", nil, f)
hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
f.health.bg:SetTexture(SFrames:GetTexture())
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
f.nameText = SFrames:CreateFontString(f.health, 10, "CENTER")
f.nameText:SetPoint("CENTER", f.health, "CENTER", 0, 0)
self.frame = f
f:Hide()
-- Update loop since targettarget changes don't fire precise events in Vanilla
self.updater = CreateFrame("Frame")
self.updater.timer = 0
self.updater:SetScript("OnUpdate", function()
this.timer = this.timer + arg1
if this.timer >= 0.2 then
SFrames.ToT:Update()
this.timer = 0
end
end)
end
function SFrames.ToT:Update()
if UnitExists("targettarget") then
self.frame:Show()
local hp = UnitHealth("targettarget")
local maxHp = UnitHealthMax("targettarget")
self.frame.health:SetMinMaxValues(0, maxHp)
self.frame.health:SetValue(hp)
self.frame.nameText:SetText(UnitName("targettarget"))
if UnitIsPlayer("targettarget") then
local _, class = UnitClass("targettarget")
local color = SFrames.Config.colors.class[class]
if color then
self.frame.health:SetStatusBarColor(color.r, color.g, color.b)
self.frame.nameText:SetTextColor(color.r, color.g, color.b)
else
self.frame.health:SetStatusBarColor(0, 1, 0)
self.frame.nameText:SetTextColor(1, 1, 1)
end
else
local r, g, b = 0.85, 0.77, 0.36 -- Neutral
if UnitIsEnemy("player", "targettarget") then
r, g, b = 0.78, 0.25, 0.25 -- Enemy
elseif UnitIsFriend("player", "targettarget") then
r, g, b = 0.33, 0.59, 0.33 -- Friend
end
self.frame.health:SetStatusBarColor(r, g, b)
self.frame.nameText:SetTextColor(r, g, b)
end
else
self.frame:Hide()
end
end

882
Whisper.lua Normal file
View File

@@ -0,0 +1,882 @@
local CFG_THEME = SFrames.ActiveTheme
SFrames.Whisper = SFrames.Whisper or {}
SFrames.Whisper.history = SFrames.Whisper.history or {}
SFrames.Whisper.contacts = SFrames.Whisper.contacts or {}
SFrames.Whisper.unreadCount = SFrames.Whisper.unreadCount or {}
SFrames.Whisper.activeContact = nil
local MAX_WHISPER_CONTACTS = 200
local MAX_MESSAGES_PER_CONTACT = 100
function SFrames.Whisper:SaveCache()
if not SFramesDB then SFramesDB = {} end
SFramesDB.whisperContacts = {}
SFramesDB.whisperHistory = {}
for _, contact in ipairs(self.contacts) do
table.insert(SFramesDB.whisperContacts, contact)
if self.history[contact] then
local msgs = self.history[contact]
-- Only persist the last MAX_MESSAGES_PER_CONTACT messages per contact
local start = math.max(1, table.getn(msgs) - MAX_MESSAGES_PER_CONTACT + 1)
local trimmed = {}
for i = start, table.getn(msgs) do
table.insert(trimmed, { time = msgs[i].time, text = msgs[i].text, isMe = msgs[i].isMe, translated = msgs[i].translated })
end
SFramesDB.whisperHistory[contact] = trimmed
end
end
end
function SFrames.Whisper:LoadCache()
if not SFramesDB then return end
if type(SFramesDB.whisperContacts) ~= "table" or type(SFramesDB.whisperHistory) ~= "table" then return end
self.contacts = {}
self.history = {}
for _, contact in ipairs(SFramesDB.whisperContacts) do
if type(contact) == "string" and SFramesDB.whisperHistory[contact] and table.getn(SFramesDB.whisperHistory[contact]) > 0 then
table.insert(self.contacts, contact)
self.history[contact] = SFramesDB.whisperHistory[contact]
end
end
-- Trim to max contacts (remove oldest = last in list)
while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do
local oldest = table.remove(self.contacts)
if oldest then
self.history[oldest] = nil
end
end
end
function SFrames.Whisper:RemoveContact(contact)
for i, v in ipairs(self.contacts) do
if v == contact then
table.remove(self.contacts, i)
break
end
end
self.history[contact] = nil
self.unreadCount[contact] = nil
if self.activeContact == contact then
self.activeContact = self.contacts[1]
end
self:SaveCache()
if self.frame and self.frame:IsShown() then
self:UpdateContacts()
self:UpdateMessages()
end
end
function SFrames.Whisper:ClearAllHistory()
self.history = {}
self.contacts = {}
self.unreadCount = {}
self.activeContact = nil
self:SaveCache()
if self.frame and self.frame:IsShown() then
self:UpdateContacts()
self:UpdateMessages()
end
end
local function FormatTime()
local h, m = GetGameTime()
return string.format("%02d:%02d", h, m)
end
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local bg = bgColor or CFG_THEME.panelBg
local bd = borderColor or CFG_THEME.panelBorder
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end
local function CreateShadow(parent, size)
local s = CreateFrame("Frame", nil, parent)
local sz = size or 4
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.55)
s:SetBackdropBorderColor(0, 0, 0, 0.4)
return s
end
local function EnsureBackdrop(frame)
if not frame then return end
if frame.sfCfgBackdrop then return end
SetRoundBackdrop(frame)
frame.sfCfgBackdrop = true
end
local function StyleActionButton(btn, label)
SetRoundBackdrop(btn, CFG_THEME.buttonBg, CFG_THEME.buttonBorder)
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), 12, "OUTLINE")
fs:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3])
fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
if label then fs:SetText(label) end
btn.nLabel = fs
btn:SetScript("OnEnter", function()
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
this:SetBackdropBorderColor(CFG_THEME.btnHoverBorder[1], CFG_THEME.btnHoverBorder[2], CFG_THEME.btnHoverBorder[3], CFG_THEME.btnHoverBorder[4])
if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnActiveText[1], CFG_THEME.btnActiveText[2], CFG_THEME.btnActiveText[3]) end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
this:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3]) end
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(CFG_THEME.buttonDownBg[1], CFG_THEME.buttonDownBg[2], CFG_THEME.buttonDownBg[3], CFG_THEME.buttonDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
end)
return fs
end
local function IsNotFullyChinese(text)
-- If there's any english letter, we consider it requires translation
return string.find(text, "[a-zA-Z]") ~= nil
end
local function TranslateMessage(text, callback)
if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then
callback(nil)
return
end
if not IsNotFullyChinese(text) then
callback(nil)
return
end
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|r", "")
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
_G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
if result then
callback(result)
end
end, "Nanami-UI")
else
callback(nil)
end
end
function SFrames.Whisper:AddMessage(sender, text, isMe)
if not self.history[sender] then
self.history[sender] = {}
table.insert(self.contacts, 1, sender)
else
-- Move to front
for i, v in ipairs(self.contacts) do
if v == sender then
table.remove(self.contacts, i)
break
end
end
table.insert(self.contacts, 1, sender)
end
local msgData = {
time = FormatTime(),
text = text,
isMe = isMe
}
table.insert(self.history[sender], msgData)
if not isMe then
PlaySound("TellIncoming")
if self.activeContact ~= sender or not (self.frame and self.frame:IsShown()) then
self.unreadCount[sender] = (self.unreadCount[sender] or 0) + 1
if SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then
SFrames.Chat.frame.whisperButton.hasUnread = true
if SFrames.Chat.frame.whisperButton.flashFrame then
SFrames.Chat.frame.whisperButton.flashFrame:Show()
end
end
end
TranslateMessage(text, function(translated)
if translated and translated ~= "" then
msgData.translated = "(翻译) " .. translated
if self.frame and self.frame:IsShown() and self.activeContact == sender then
self:UpdateMessages()
end
end
end)
end
-- Trim contacts if exceeding max
while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do
local oldest = table.remove(self.contacts)
if oldest then
self.history[oldest] = nil
self.unreadCount[oldest] = nil
end
end
if self.frame and self.frame:IsShown() then
self:UpdateContacts()
if self.activeContact == sender then
self:UpdateMessages()
end
end
self:SaveCache()
end
function SFrames.Whisper:SelectContact(contact)
self.activeContact = contact
self.unreadCount[contact] = 0
self:UpdateContacts()
self:UpdateMessages()
-- Clear global unread state if no more unread
local totalUnread = 0
for k, v in pairs(self.unreadCount) do
totalUnread = totalUnread + v
end
if totalUnread == 0 and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then
SFrames.Chat.frame.whisperButton.hasUnread = false
if SFrames.Chat.frame.whisperButton.flashFrame then
SFrames.Chat.frame.whisperButton.flashFrame:Hide()
end
end
if self.frame and self.frame.editBox then
self.frame.editBox:SetText("")
self.frame.editBox:SetFocus()
end
end
function SFrames.Whisper:UpdateContacts()
if not self.frame or not self.frame.contactList then return end
local scrollChild = self.frame.contactList.scrollChild
for _, child in ipairs({scrollChild:GetChildren()}) do
child:Hide()
end
local yOffset = -5
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
for i, contact in ipairs(self.contacts) do
local btn = self.contactButtons and self.contactButtons[i]
if not btn then
btn = CreateFrame("Button", nil, scrollChild)
btn:SetWidth(120)
btn:SetHeight(24)
EnsureBackdrop(btn)
local txt = btn:CreateFontString(nil, "OVERLAY")
txt:SetFont(fontPath, 12, "OUTLINE")
txt:SetPoint("LEFT", btn, "LEFT", 8, 0)
btn.txt = txt
local closeBtn = CreateFrame("Button", nil, btn)
closeBtn:SetWidth(16)
closeBtn:SetHeight(16)
closeBtn:SetPoint("RIGHT", btn, "RIGHT", -2, 0)
local closeTxt = closeBtn:CreateFontString(nil, "OVERLAY")
closeTxt:SetFont(fontPath, 10, "OUTLINE")
closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 1)
closeTxt:SetText("x")
closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
closeBtn:SetFontString(closeTxt)
closeBtn:SetScript("OnEnter", function() closeTxt:SetTextColor(1, 0.4, 0.5) end)
closeBtn:SetScript("OnLeave", function() closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
closeBtn:SetScript("OnClick", function()
SFrames.Whisper:RemoveContact(this:GetParent().contact)
end)
btn.closeBtn = closeBtn
local unreadTxt = btn:CreateFontString(nil, "OVERLAY")
unreadTxt:SetFont(fontPath, 10, "OUTLINE")
unreadTxt:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0)
unreadTxt:SetTextColor(1, 0.4, 0.4)
btn.unreadTxt = unreadTxt
btn:SetScript("OnClick", function()
SFrames.Whisper:SelectContact(this.contact)
end)
btn:SetScript("OnEnter", function()
if this.contact ~= SFrames.Whisper.activeContact then
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
end
end)
btn:SetScript("OnLeave", function()
if this.contact ~= SFrames.Whisper.activeContact then
this:SetBackdropColor(0,0,0,0)
end
end)
if not self.contactButtons then self.contactButtons = {} end
self.contactButtons[i] = btn
end
btn.contact = contact
btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset)
btn.txt:SetText(contact)
local unreads = self.unreadCount[contact] or 0
if unreads > 0 then
btn.unreadTxt:SetText("("..unreads..")")
btn.txt:SetTextColor(1, 0.8, 0.2)
else
btn.unreadTxt:SetText("")
btn.txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
if contact == self.activeContact then
btn:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
btn:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
else
btn:SetBackdropColor(0,0,0,0)
btn:SetBackdropBorderColor(0,0,0,0)
end
btn:Show()
yOffset = yOffset - 26
end
scrollChild:SetHeight(math.abs(yOffset))
self.frame.contactList:UpdateScrollChildRect()
local maxScroll = math.max(0, math.abs(yOffset) - 330)
local slider = _G["SFramesWhisperContactScrollScrollBar"]
if slider then
slider:SetMinMaxValues(0, maxScroll)
end
end
function SFrames.Whisper:UpdateMessages()
if not self.frame or not self.frame.messageScroll then return end
local scrollChild = self.frame.messageScroll.scrollChild
if self.msgLabels then
for i, fs in ipairs(self.msgLabels) do
fs:Hide()
end
end
if self.msgCopyBtns then
for i, btn in ipairs(self.msgCopyBtns) do
btn:Hide()
end
end
if not self.activeContact then return end
local messages = self.history[self.activeContact] or {}
local yOffset = -5
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
if not self.msgLabels then self.msgLabels = {} end
if not self.msgCopyBtns then self.msgCopyBtns = {} end
for i, msg in ipairs(messages) do
local fs = self.msgLabels[i]
if not fs then
fs = scrollChild:CreateFontString(nil, "OVERLAY")
fs:SetFont(fontPath, 13, "OUTLINE")
fs:SetJustifyH("LEFT")
fs:SetWidth(380)
if fs.EnableMouse then fs:EnableMouse(true) end
self.msgLabels[i] = fs
end
local btn = self.msgCopyBtns[i]
if not btn then
btn = CreateFrame("Button", nil, scrollChild)
btn:SetWidth(20)
btn:SetHeight(16)
local txt = btn:CreateFontString(nil, "OVERLAY")
txt:SetFont(fontPath, 13, "OUTLINE")
txt:SetPoint("CENTER", btn, "CENTER", 0, 0)
txt:SetText("[+]")
txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
btn:SetFontString(txt)
btn:SetScript("OnEnter", function() txt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
btn:SetScript("OnLeave", function() txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
btn:SetScript("OnClick", function()
if SFrames and SFrames.Chat and SFrames.Chat.OpenMessageContextMenu then
SFrames.Chat:OpenMessageContextMenu("whisper", this.rawText, this.senderName)
end
end)
self.msgCopyBtns[i] = btn
end
local color = msg.isMe and "|cff66ccff" or "|cffffbbee"
local nameStr = msg.isMe and "" or self.activeContact
local textStr = string.format("%s[%s] %s:|r %s", color, msg.time, nameStr, msg.text)
if msg.translated then
textStr = textStr .. "\n|cffaaaaaa" .. msg.translated .. "|r"
end
btn.rawText = msg.text
btn.senderName = nameStr
btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset)
btn:Show()
fs:SetText(textStr)
fs:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 28, yOffset)
fs:Show()
yOffset = yOffset - fs:GetHeight() - 8
end
scrollChild:SetHeight(math.abs(yOffset))
self.frame.messageScroll:UpdateScrollChildRect()
local maxScroll = math.max(0, math.abs(yOffset) - 270)
local slider = _G["SFramesWhisperMessageScrollScrollBar"]
if slider then
slider:SetMinMaxValues(0, maxScroll)
slider:SetValue(maxScroll)
end
self.frame.messageScroll:SetVerticalScroll(maxScroll)
end
function SFrames.Whisper:EnsureFrame()
if self.frame then return end
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local f = CreateFrame("Frame", "SFramesWhisperContainer", UIParent)
f:SetWidth(580)
f:SetHeight(380)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
f:SetFrameStrata("HIGH")
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
if not self.enterHooked then
self.enterHooked = true
local orig_ChatFrame_OpenChat = ChatFrame_OpenChat
if orig_ChatFrame_OpenChat then
ChatFrame_OpenChat = function(text, chatFrame)
if SFrames and SFrames.Whisper and SFrames.Whisper.frame and SFrames.Whisper.frame:IsShown() and (not text or text == "") then
SFrames.Whisper.frame.editBox:SetFocus()
return
end
orig_ChatFrame_OpenChat(text, chatFrame)
end
end
end
table.insert(UISpecialFrames, "SFramesWhisperContainer")
SetRoundBackdrop(f, CFG_THEME.panelBg, CFG_THEME.panelBorder)
CreateShadow(f, 5)
local titleIcon = SFrames:CreateIcon(f, "chat", 14)
titleIcon:SetDrawLayer("OVERLAY")
titleIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 15, -12)
titleIcon:SetVertexColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local title = f:CreateFontString(nil, "OVERLAY")
title:SetFont(fontPath, 14, "OUTLINE")
title:SetPoint("LEFT", titleIcon, "RIGHT", 4, 0)
title:SetText("私聊对话管理")
title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local close = CreateFrame("Button", nil, f)
close:SetWidth(18)
close:SetHeight(18)
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
close:SetFrameLevel(f:GetFrameLevel() + 3)
SetRoundBackdrop(close, CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }, CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 })
local closeIco = SFrames:CreateIcon(close, "close", 12)
closeIco:SetDrawLayer("OVERLAY")
closeIco:SetPoint("CENTER", close, "CENTER", 0, 0)
closeIco:SetVertexColor(1, 0.7, 0.7)
close:SetScript("OnClick", function() f:Hide() end)
close:SetScript("OnEnter", function()
local h = CFG_THEME.buttonHoverBg or { 0.55, 0.1, 0.1, 0.95 }
local hb = CFG_THEME.btnHoverBd or { 0.65, 0.15, 0.15, 0.9 }
this:SetBackdropColor(h[1], h[2], h[3], h[4] or 0.95)
this:SetBackdropBorderColor(hb[1], hb[2], hb[3], hb[4] or 0.9)
end)
close:SetScript("OnLeave", function()
local d = CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }
local db = CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 }
this:SetBackdropColor(d[1], d[2], d[3], d[4] or 0.85)
this:SetBackdropBorderColor(db[1], db[2], db[3], db[4] or 0.6)
end)
-- Contact List
local contactList = CreateFrame("ScrollFrame", "SFramesWhisperContactScroll", f, "UIPanelScrollFrameTemplate")
contactList:SetWidth(130)
contactList:SetHeight(330)
contactList:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -40)
SetRoundBackdrop(contactList, CFG_THEME.listBg, CFG_THEME.listBorder)
local contactChild = CreateFrame("Frame", nil, contactList)
contactChild:SetWidth(130)
contactChild:SetHeight(10)
contactList:SetScrollChild(contactChild)
contactList.scrollChild = contactChild
f.contactList = contactList
-- Apply Nanami-UI scrollbar styling
local contactSlider = _G["SFramesWhisperContactScrollScrollBar"]
if contactSlider then
contactSlider:SetWidth(12)
local regions = { contactSlider:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetTexture(nil)
end
end
local track = contactSlider:CreateTexture(nil, "BACKGROUND")
track:SetTexture("Interface\\Buttons\\WHITE8X8")
track:SetPoint("TOPLEFT", contactSlider, "TOPLEFT", 3, 0)
track:SetPoint("BOTTOMRIGHT", contactSlider, "BOTTOMRIGHT", -3, 0)
track:SetVertexColor(0.22, 0.12, 0.18, 0.9)
if contactSlider.SetThumbTexture then
contactSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
end
local thumb = contactSlider.GetThumbTexture and contactSlider:GetThumbTexture()
if thumb then
thumb:SetWidth(8)
thumb:SetHeight(20)
thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95)
end
local upBtn = _G["SFramesWhisperContactScrollScrollBarScrollUpButton"]
local downBtn = _G["SFramesWhisperContactScrollScrollBarScrollDownButton"]
if upBtn then upBtn:Hide() end
if downBtn then downBtn:Hide() end
contactSlider:ClearAllPoints()
contactSlider:SetPoint("TOPRIGHT", contactList, "TOPRIGHT", -2, -6)
contactSlider:SetPoint("BOTTOMRIGHT", contactList, "BOTTOMRIGHT", -2, 6)
end
-- Message List
local messageScroll = CreateFrame("ScrollFrame", "SFramesWhisperMessageScroll", f, "UIPanelScrollFrameTemplate")
messageScroll:SetWidth(405)
messageScroll:SetHeight(270)
messageScroll:SetPoint("TOPLEFT", contactList, "TOPRIGHT", 5, 0)
SetRoundBackdrop(messageScroll, CFG_THEME.listBg, CFG_THEME.listBorder)
local messageChild = CreateFrame("Frame", nil, messageScroll)
messageChild:SetWidth(405)
messageChild:SetHeight(10)
messageChild:EnableMouse(true)
-- Hyperlink 脚本仅部分帧类型支持pcall 防止不支持的客户端报错
pcall(function()
messageChild:SetScript("OnHyperlinkClick", function()
local link = arg1
if not link then return end
if IsShiftKeyDown() then
if ChatFrameEditBox and ChatFrameEditBox:IsShown() then
ChatFrameEditBox:Insert(link)
elseif SFrames.Whisper.frame and SFrames.Whisper.frame.editBox then
SFrames.Whisper.frame.editBox:Insert(link)
end
else
pcall(function() SetItemRef(link, arg2, arg3) end)
end
end)
end)
pcall(function()
messageChild:SetScript("OnHyperlinkEnter", function()
local link = arg1
if not link then return end
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
if ok then
GameTooltip:Show()
else
GameTooltip:Hide()
end
end)
end)
pcall(function()
messageChild:SetScript("OnHyperlinkLeave", function()
GameTooltip:Hide()
end)
end)
messageScroll:SetScrollChild(messageChild)
messageScroll.scrollChild = messageChild
f.messageScroll = messageScroll
local messageSlider = _G["SFramesWhisperMessageScrollScrollBar"]
if messageSlider then
messageSlider:SetWidth(12)
local regions = { messageSlider:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetTexture(nil)
end
end
local track = messageSlider:CreateTexture(nil, "BACKGROUND")
track:SetTexture("Interface\\Buttons\\WHITE8X8")
track:SetPoint("TOPLEFT", messageSlider, "TOPLEFT", 3, 0)
track:SetPoint("BOTTOMRIGHT", messageSlider, "BOTTOMRIGHT", -3, 0)
track:SetVertexColor(0.22, 0.12, 0.18, 0.9)
if messageSlider.SetThumbTexture then
messageSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
end
local thumb = messageSlider.GetThumbTexture and messageSlider:GetThumbTexture()
if thumb then
thumb:SetWidth(8)
thumb:SetHeight(20)
thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95)
end
local upBtn = _G["SFramesWhisperMessageScrollScrollBarScrollUpButton"]
local downBtn = _G["SFramesWhisperMessageScrollScrollBarScrollDownButton"]
if upBtn then upBtn:Hide() end
if downBtn then downBtn:Hide() end
messageSlider:ClearAllPoints()
messageSlider:SetPoint("TOPRIGHT", messageScroll, "TOPRIGHT", -2, -22)
messageSlider:SetPoint("BOTTOMRIGHT", messageScroll, "BOTTOMRIGHT", -2, 22)
local topBtn = CreateFrame("Button", nil, messageScroll)
topBtn:SetWidth(12)
topBtn:SetHeight(12)
topBtn:SetPoint("BOTTOM", messageSlider, "TOP", 0, 4)
local topTxt = topBtn:CreateFontString(nil, "OVERLAY")
topTxt:SetFont(fontPath, 11, "OUTLINE")
topTxt:SetPoint("CENTER", topBtn, "CENTER", 0, 1)
topTxt:SetText("")
topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
topBtn:SetFontString(topTxt)
topBtn:SetScript("OnEnter", function() topTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
topBtn:SetScript("OnLeave", function() topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
topBtn:SetScript("OnClick", function()
messageSlider:SetValue(0)
end)
local bottomBtn = CreateFrame("Button", nil, messageScroll)
bottomBtn:SetWidth(12)
bottomBtn:SetHeight(12)
bottomBtn:SetPoint("TOP", messageSlider, "BOTTOM", 0, -4)
local botTxt = bottomBtn:CreateFontString(nil, "OVERLAY")
botTxt:SetFont(fontPath, 11, "OUTLINE")
botTxt:SetPoint("CENTER", bottomBtn, "CENTER", 0, -1)
botTxt:SetText("")
botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
bottomBtn:SetFontString(botTxt)
bottomBtn:SetScript("OnEnter", function() botTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
bottomBtn:SetScript("OnLeave", function() botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
bottomBtn:SetScript("OnClick", function()
local _, maxVal = messageSlider:GetMinMaxValues()
messageSlider:SetValue(maxVal)
end)
if messageScroll.EnableMouseWheel then
messageScroll:EnableMouseWheel(true)
messageScroll:SetScript("OnMouseWheel", function()
local delta = arg1
local _, maxVal = messageSlider:GetMinMaxValues()
local val = messageSlider:GetValue() - delta * 40
if val < 0 then val = 0 end
if val > maxVal then val = maxVal end
messageSlider:SetValue(val)
end)
end
end
local contactSlider = _G["SFramesWhisperContactScrollScrollBar"]
if contactList and contactSlider and contactList.EnableMouseWheel then
contactList:EnableMouseWheel(true)
contactList:SetScript("OnMouseWheel", function()
local delta = arg1
local _, maxVal = contactSlider:GetMinMaxValues()
local val = contactSlider:GetValue() - delta * 30
if val < 0 then val = 0 end
if val > maxVal then val = maxVal end
contactSlider:SetValue(val)
end)
end
-- EditBox for replying
local editBox = CreateFrame("EditBox", "SFramesWhisperEditBox", f, "InputBoxTemplate")
editBox:SetWidth(405)
editBox:SetHeight(20)
editBox:SetPoint("TOPLEFT", messageScroll, "BOTTOMLEFT", 0, -10)
editBox:SetAutoFocus(false)
editBox:SetFont(fontPath, 13, "OUTLINE")
local translateCheck = CreateFrame("CheckButton", "SFramesWhisperTranslateCheck", f, "UICheckButtonTemplate")
translateCheck:SetWidth(24)
translateCheck:SetHeight(24)
translateCheck:SetPoint("TOPLEFT", editBox, "BOTTOMLEFT", -5, -6)
local txt = translateCheck:CreateFontString(nil, "OVERLAY")
txt:SetFont(fontPath, 11, "OUTLINE")
txt:SetPoint("LEFT", translateCheck, "RIGHT", 2, 0)
txt:SetText("发前自动翻译 (译后可修改再回车)")
txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
-- Hide the default text and reskin the button if you have defined StyleCfgCheck inside your environment
if _G["SFramesWhisperTranslateCheckText"] then
_G["SFramesWhisperTranslateCheckText"]:Hide()
end
-- Reskin checkbox
local function StyleCheck(cb)
local box = CreateFrame("Frame", nil, cb)
box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2)
box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2)
local boxLevel = (cb:GetFrameLevel() or 1) - 1
if boxLevel < 0 then boxLevel = 0 end
box:SetFrameLevel(boxLevel)
EnsureBackdrop(box)
if box.SetBackdropColor then
box:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
end
if box.SetBackdropBorderColor then
box:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
end
cb.sfBox = box
if cb.GetNormalTexture and cb:GetNormalTexture() then cb:GetNormalTexture():Hide() end
if cb.GetPushedTexture and cb:GetPushedTexture() then cb:GetPushedTexture():Hide() end
if cb.GetHighlightTexture and cb:GetHighlightTexture() then cb:GetHighlightTexture():Hide() end
if cb.SetCheckedTexture then cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8") end
local checked = cb.GetCheckedTexture and cb:GetCheckedTexture()
if checked then
checked:ClearAllPoints()
checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4])
end
end
StyleCheck(translateCheck)
f.translateCheck = translateCheck
local function SendReply()
if SFrames.Whisper.isTranslating then return end
local text = editBox:GetText()
if not text or text == "" or not SFrames.Whisper.activeContact then return end
if f.translateCheck and f.translateCheck:GetChecked() then
if text == SFrames.Whisper.lastTranslation then
SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact)
editBox:SetText("")
editBox:ClearFocus()
SFrames.Whisper.lastTranslation = nil
else
local targetLang = "en"
if not string.find(text, "[\128-\255]") then
targetLang = "zh"
end
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
SFrames.Whisper.isTranslating = true
editBox:SetText("翻译中...")
_G.STranslateAPI.Translate(text, "auto", targetLang, function(result, err, meta)
SFrames.Whisper.isTranslating = false
if result then
editBox:SetText(result)
SFrames.Whisper.lastTranslation = result
editBox:SetFocus()
else
editBox:SetText(text)
DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[私聊翻译失败]|r " .. tostring(err))
end
end, "Nanami-UI")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[Nanami-UI] STranslate插件未加载|r")
SFrames.Whisper.lastTranslation = text
end
end
else
SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact)
editBox:SetText("")
editBox:ClearFocus()
end
end
editBox:SetScript("OnEnterPressed", SendReply)
editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
f.editBox = editBox
local sendBtn = CreateFrame("Button", nil, f)
sendBtn:SetWidth(60)
sendBtn:SetHeight(24)
sendBtn:SetPoint("TOPRIGHT", editBox, "BOTTOMRIGHT", 0, -5)
StyleActionButton(sendBtn, "发送")
sendBtn:SetScript("OnClick", SendReply)
f:Hide()
self.frame = f
end
function SFrames.Whisper:Toggle()
self:EnsureFrame()
if self.frame:IsShown() then
self.frame:Hide()
else
self.frame:Show()
self:UpdateContacts()
if self.contacts[1] and not self.activeContact then
self:SelectContact(self.contacts[1])
elseif self.activeContact then
self:SelectContact(self.activeContact)
end
end
end
-- Hook Events
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("CHAT_MSG_WHISPER")
eventFrame:RegisterEvent("CHAT_MSG_WHISPER_INFORM")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
SFrames.Whisper:LoadCache()
return
end
local text = arg1
local sender = arg2
if not sender or sender == "" then return end
sender = string.gsub(sender, "-.*", "") -- remove realm name if attached
if event == "CHAT_MSG_WHISPER" then
SFrames.Whisper:AddMessage(sender, text, false)
elseif event == "CHAT_MSG_WHISPER_INFORM" then
SFrames.Whisper:AddMessage(sender, text, true)
end
end)

1941
WorldMap.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
MAGE = {
[4] = {"造水术", "寒冰箭"},
[6] = {"造食术", "火球术 2级", "火焰冲击", "魔法抑制", "造水术 2级"},
[8] = {"变形术", "奥术飞弹"},
[10] = {"霜甲术 2级", "冰霜新星"},
[12] = {"缓落术", "造食术 2级", "魔法抑制", "火球术 3级"},
[14] = {"魔爆术", "奥术智慧 2级", "奥术飞弹 3级", "火焰冲击 2级"},
[16] = {"奥术飞弹 2级", "侦测魔法", "烈焰风暴"},
[18] = {"解除次级诅咒", "魔法增效", "造水术 4级", "火球术 4级"},
[20] = {"变形术 2级", "造水术 3级", "法力护盾", "闪现术", "传送:暴风城", "传送:铁炉堡", "传送:幽暗城", "传送:奥格瑞玛", "防护火焰结界", "霜甲术 3级", "寒冰箭 4级", "暴风雪", "唤醒"},
[22] = {"造食术 3级", "魔爆术 2级", "火焰冲击 3级", "灼烧"},
[24] = {"魔法抑制 2级", "火球术 5级", "奥术飞弹 4级", "烈焰风暴 2级", "法术反制", "防护冰霜结界"},
[26] = {"寒冰箭 5级", "冰锥术"},
[28] = {"制造魔法玛瑙", "奥术智慧 3级", "法力护盾 2级", "暴风雪 2级", "灼烧 2级", "冰霜新星 2级"},
[30] = {"魔爆术 3级", "火球术 6级", "传送:达纳苏斯", "传送:雷霆崖", "防护火焰结界 2级", "冰甲术"},
[32] = {"造食术 4级", "奥术飞弹 4级", "烈焰风暴 3级", "寒冰箭 6级", "防护冰霜结界 2级"},
[34] = {"魔甲术", "冰锥术 2级", "灼烧 3级"},
[36] = {"魔法抑制 3级", "法力护盾 3级", "火球术 7级", "暴风雪 3级", "冰霜新星 3级"},
[38] = {"魔爆术 4级", "制造魔法翡翠", "寒冰箭 7级", "火焰冲击 5级"},
[40] = {"造食术 5级", "奥术飞弹 5级", "传送门:暴风城", "传送门:铁炉堡", "传送门:奥格瑞玛", "传送门:幽暗城", "火球术 8级", "冰甲术 2级", "灼烧 4级"},
[42] = {"魔法增效 3级", "奥术智慧 4级", "火球术 8级", "防护冰霜结界 3级"},
[44] = {"法力护盾 4级", "暴风雪 4级", "寒冰箭 8级"},
[46] = {"魔爆术 5级", "灼烧 5级"},
[48] = {"魔法抑制 4级", "制造魔法黄水晶", "火球术 9级", "奥术飞弹 6级", "烈焰风暴 5级"},
[50] = {"造水术 6级", "寒冰箭 9级", "冰锥术 4级", "防护火焰结界 4级", "传送门:达纳苏斯", "传送门:雷霆崖", "冰甲术 3级"},
[52] = {"法力护盾 5级", "火球术 10级", "火焰冲击 7级", "防护冰霜结界 4级", "冰霜新星 4级"},
[54] = {"魔法增效 4级", "奥术飞弹 7级", "烈焰风暴 6级"},
[56] = {"奥术智慧 5级", "寒冰箭 10级", "冰锥术 5级"},
[58] = {"魔甲术 3级", "制造魔法红宝石", "灼烧 7级"},
[60] = {"变形术 4级", "魔法抑制 5级", "法力护盾 6级", "火球术 11级", "防护火焰结界 5级", "暴风雪 6级", "冰甲术 4级"},
},
WARLOCK = {
[2] = {"痛苦诅咒", "恐惧术"},
[4] = {"腐蚀术", "虚弱诅咒"},
[6] = {"虚弱诅咒 2级", "暗影箭 3级"},
[8] = {"痛苦诅咒"},
[10] = {"吸取灵魂", "献祭 2级", "恶魔皮肤 2级", "制造初级治疗石"},
[12] = {"生命分流 2级", "生命通道", "魔息术"},
[14] = {"腐蚀术 2级", "吸取生命", "鲁莽诅咒"},
[16] = {"生命分流 2级"},
[18] = {"痛苦诅咒 2级", "制造初级灵魂石", "灼热之痛"},
[20] = {"献祭 3级", "生命通道 2级", "暗影箭 4级", "召唤仪式", "魔甲术", "火焰之雨"},
[22] = {"吸取生命 2级", "虚弱诅咒 3级", "基尔罗格之眼", "制造次级治疗石"},
[24] = {"腐蚀术 3级", "吸取灵魂 2级", "吸取法力", "感知恶魔"},
[26] = {"生命分流 3级", "语言诅咒", "侦测次级隐形"},
[28] = {"鲁莽诅咒 2级", "痛苦诅咒 3级", "生命通道 3级", "放逐术", "制造次级火焰石"},
[30] = {"吸取生命 3级", "献祭 4级", "奴役恶魔", "制造次级灵魂石", "地狱烈焰", "魔甲术 2级"},
[32] = {"虚弱诅咒 4级", "恐惧术 2级", "元素诅咒", "防护暗影结界"},
[34] = {"生命分流 4级", "吸取法力 2级", "火焰之雨 2级", "制造治疗石", "灼热之痛 3级"},
[36] = {"生命通道 4级", "制造法术石", "制造火焰石"},
[38] = {"吸取灵魂 3级", "痛苦诅咒 4级", "生命虹吸 2级", "侦测隐形"},
[40] = {"恐惧嚎叫", "献祭 5级", "制造灵魂石", "奴役恶魔 2级"},
[42] = {"虚弱诅咒 5级", "鲁莽诅咒 3级", "死亡缠绕", "防护暗影结界 2级", "地狱烈焰 2级", "灼热之痛 4级"},
[44] = {"吸取生命 5级", "生命通道 5级", "暗影诅咒", "暗影箭 7级"},
[46] = {"生命分流 5级", "制造强效治疗石", "制造强效火焰石", "火焰之雨 3级"},
[48] = {"痛苦诅咒 5级", "放逐术 2级", "灵魂之火", "制造强效法术石"},
[50] = {"虚弱诅咒 6级", "死亡缠绕 2级", "恐惧嚎叫 2级", "魔甲术 4级", "吸取灵魂 4级", "吸取法力 4级", "生命虹吸 3级", "黑暗契约 2级", "侦测强效隐形", "暗影箭 8级", "灼热之痛 5级"},
[52] = {"防护暗影结界 3级", "生命通道 6级"},
[54] = {"腐蚀术 6级", "吸取生命 6级", "地狱烈焰 3级", "灵魂之火 2级"},
[56] = {"鲁莽诅咒 4级", "暗影诅咒 2级", "死亡缠绕 3级", "生命虹吸 4级", "制造特效火焰石"},
[58] = {"痛苦诅咒 6级", "奴役恶魔 3级", "火焰之雨 4级", "制造特效治疗石", "灼热之痛 6级"},
[60] = {"厄运诅咒", "元素诅咒 3级", "魔甲术 5级", "制造特效法术石", "暗影箭 9级"},
},
DRUID = {
[4] = {"月火术", "回春术"},
[6] = {"荆棘术", "愤怒 2级"},
[8] = {"纠缠根须", "治疗之触 2级"},
[10] = {"月火术 2级", "回春术 2级", "挫志咆哮", "野性印记 2级"},
[12] = {"愈合", "狂怒"},
[14] = {"荆棘术 2级", "愤怒 3级", "重击"},
[16] = {"月火术 3级", "回春术 3级", "挥击"},
[18] = {"精灵之火", "休眠", "愈合 2级", "槌击 2级"},
[20] = {"纠缠根须 2级", "星火术", "月火术 4级", "回春术 4级", "挫志咆哮 2级", "猎豹形态", "撕扯", "爪击", "治疗之触 4级", "潜行", "野性印记 3级", "复生"},
[22] = {"月火术 4级", "回春术 4级", "愤怒 4级", "撕碎", "安抚动物"},
[24] = {"荆棘术 3级", "挥击 2级", "扫击", "猛虎之怒", "撕碎", "解除诅咒"},
[26] = {"星火术 2级", "月火术 5级", "槌击 3级", "爪击 2级", "治疗之触 5级", "驱毒术"},
[28] = {"撕扯 2级", "挑战咆哮", "畏缩"},
[30] = {"精灵之火 2级", "星火术 3级", "愤怒 5级", "旅行形态", "撕碎 2级", "重击 2级", "野性印记 4级", "宁静", "复生 2级", "虫群 2级"},
[32] = {"挫志咆哮 3级", "挥击 3级", "毁灭", "撕碎 3级", "治疗之触 6级", "追踪人型生物", "凶猛撕咬"},
[34] = {"荆棘术 4级", "月火术 6级", "回春术 6级", "槌击 4级", "扫击 2级", "爪击 3级"},
[36] = {"愤怒 6级", "突袭", "狂暴回复"},
[38] = {"纠缠根须 4级", "休眠 2级", "安抚动物 2级", "爪击 3级", "撕碎 3级"},
[40] = {"星火术 4级", "飓风", "挥击 4级", "潜行 2级", "畏缩 2级", "巨熊形态", "豹之优雅", "凶猛撕咬 2级", "回春术 7级", "宁静 2级", "复生 3级", "虫群 3级", "激活"},
[42] = {"挫志咆哮 4级", "槌击 5级", "毁灭 2级"},
[44] = {"荆棘术 5级", "树皮术", "撕扯 4级", "扫击 3级", "治疗之触 8级"},
[46] = {"愤怒 7级", "重击 3级", "突袭 2级"},
[48] = {"纠缠根须 5级", "月火术 8级", "猛虎之怒 3级", "撕碎 4级"},
[50] = {"星火术 5级", "槌击 6级", "宁静 3级", "复生 4级", "虫群 4级"},
[52] = {"挫志咆哮 5级", "撕扯 5级", "畏缩 3级", "凶猛撕咬 4级", "回春术 9级"},
[54] = {"荆棘术 6级", "愤怒 8级", "月火术 9级", "挥击 5级", "扫击 4级", "爪击 4级"},
[56] = {"凶猛撕咬 4级", "治疗之触 10级"},
[58] = {"纠缠根须 6级", "星火术 6级", "月火术 10级", "爪击 5级", "槌击 7级", "毁灭 4级", "回春术 10级"},
[60] = {"飓风 3级", "潜行 3级", "猛虎之怒 4级", "撕扯 6级", "宁静 4级", "复生 5级", "虫群 5级", "野性印记 7级", "愈合 9级"},
},

View File

@@ -0,0 +1,73 @@
--[[
TALENT-based trainer skill data for WoW Classic
Skills where Rank 1 comes from talent points, higher ranks from class trainer.
Only includes ranks that are NOT "默认开启" and have EVEN required levels.
]]
local TALENT_TRAINER_SKILLS = {
-- WARRIOR: 致死打击, 嗜血, 盾牌猛击 - NOT FOUND in provided warrior file
WARRIOR = {
-- 致死打击 (Mortal Strike), 嗜血 (Bloodthirst), 盾牌猛击 (Shield Slam)
-- These skills were not found in the warrior class data file.
},
-- ROGUE: 出血 (Hemorrhage) - Rank 1 from talent (默认开启), Ranks 2-3 from trainer
ROGUE = {
{base="出血", level=46, display="出血 2级"},
{base="出血", level=58, display="出血 3级"},
},
-- PRIEST: 精神鞭笞 (Mind Flay) - Rank 1 from talent (默认开启), Ranks 2-6 from trainer
PRIEST = {
{base="精神鞭笞", level=28, display="精神鞭笞 2级"},
{base="精神鞭笞", level=36, display="精神鞭笞 3级"},
{base="精神鞭笞", level=44, display="精神鞭笞 4级"},
{base="精神鞭笞", level=52, display="精神鞭笞 5级"},
{base="精神鞭笞", level=60, display="精神鞭笞 6级"},
},
-- HUNTER: 瞄准射击, 反击, 翼龙钉刺 - NOT FOUND in provided hunter file
HUNTER = {
-- 瞄准射击 (Aimed Shot), 反击 (Counterattack), 翼龙钉刺 (Wyvern Sting)
-- These skills were not found in the hunter class data file.
},
-- MAGE: 炎爆术 (Pyroblast) - Rank 1 from talent (默认开启), Ranks 2-8 from trainer
-- 冲击波 (Blast Wave), 寒冰屏障 (Ice Barrier) - NOT FOUND in provided mage file
MAGE = {
{base="炎爆术", level=24, display="炎爆术 2级"},
{base="炎爆术", level=30, display="炎爆术 3级"},
{base="炎爆术", level=36, display="炎爆术 4级"},
{base="炎爆术", level=42, display="炎爆术 5级"},
{base="炎爆术", level=48, display="炎爆术 6级"},
{base="炎爆术", level=54, display="炎爆术 7级"},
{base="炎爆术", level=60, display="炎爆术 8级"},
-- 冲击波 (Blast Wave), 寒冰屏障 (Ice Barrier) - not found in file
},
-- PALADIN: 神圣震击 (Holy Shock) - NOT FOUND in provided paladin file
PALADIN = {
-- 神圣震击 (Holy Shock) was not found in the paladin class data file.
},
-- WARLOCK: 暗影灼烧 (Shadowburn) - NOT FOUND; 生命虹吸, 黑暗契约 found; 灵魂之火 skipped per user
WARLOCK = {
{base="生命虹吸", level=38, display="生命虹吸 2级"},
{base="生命虹吸", level=48, display="生命虹吸 3级"},
{base="生命虹吸", level=58, display="生命虹吸 4级"},
{base="黑暗契约", level=50, display="黑暗契约 2级"},
{base="黑暗契约", level=60, display="黑暗契约 3级"},
-- 暗影灼烧 (Shadowburn) - not found in file
-- 灵魂之火 - skipped (already in regular data per user)
},
-- DRUID: 虫群 (Insect Swarm) - Rank 1 from talent (默认开启), Ranks 2-5 from trainer
DRUID = {
{base="虫群", level=30, display="虫群 2级"},
{base="虫群", level=40, display="虫群 3级"},
{base="虫群", level=50, display="虫群 4级"},
{base="虫群", level=60, display="虫群 5级"},
},
}
return TALENT_TRAINER_SKILLS

View File

@@ -0,0 +1,63 @@
PRIEST = {
[4] = {"暗言术:痛", "次级治疗术 2级"},
[6] = {"真言术:盾", "惩击 2级"},
[8] = {"恢复", "渐隐术"},
[10] = {"暗言术:痛 2级", "心灵震爆", "复活术"},
[12] = {"真言术:盾 2级", "心灵之火", "真言术:韧 2级", "惩击 3级", "祛病术"},
[14] = {"恢复 2级", "心灵尖啸", "惩击 3级"},
[16] = {"治疗术", "心灵震爆 2级"},
[18] = {"真言术:盾 3级", "驱散魔法", "星辰碎片 2级", "绝望祷言 2级", "暗言术:痛 3级"},
[20] = {"心灵之火 2级", "束缚亡灵", "回馈 2级", "恢复 3级", "快速治疗", "安抚心灵", "渐隐术 2级", "神圣之火", "虚弱之触 2级", "虚弱妖术 2级"},
[22] = {"惩击 4级", "心灵视界", "复活术 2级", "心灵震爆 3级"},
[24] = {"真言术:盾 4级", "真言术:韧 3级", "法力燃烧", "神圣之火 2级"},
[26] = {"星辰碎片 3级", "恢复 4级", "暗言术:痛 4级", "绝望祷言 3级"},
[28] = {"治疗术 3级", "心灵震爆 4级", "精神鞭笞 2级", "心灵尖啸 2级", "暗影守卫 2级"},
[30] = {"真言术:盾 5级", "心灵之火 3级", "艾露恩的赐福 3级", "回馈 2级", "治疗祷言", "束缚亡灵 2级", "虚弱之触 3级", "虚弱妖术 3级", "精神控制", "防护暗影", "渐隐术 3级"},
[32] = {"法力燃烧 2级", "恢复 5级", "驱除疾病", "快速治疗 3级"},
[34] = {"漂浮术", "星辰碎片 4级", "暗言术:痛 5级", "心灵震爆 5级", "复活术 3级", "治疗术 4级"},
[36] = {"真言术:盾 6级", "驱散魔法 2级", "真言术:韧 4级", "噬灵瘟疫 3级", "星辰碎片 5级", "心灵之火 4级", "恢复 6级", "惩击 6级", "精神鞭笞 3级", "暗影守卫 3级", "安抚心灵 2级"},
[38] = {"恢复 6级", "惩击 6级"},
[40] = {"心灵之火 4级", "艾露恩的赐福 4级", "法力燃烧 3级", "回馈 3级", "神圣之灵 2级", "治疗祷言 2级", "束缚亡灵 2级", "虚弱之触 4级", "虚弱妖术 4级", "防护暗影 2级", "心灵震爆 6级", "渐隐术 4级"},
[42] = {"真言术:盾 7级", "星辰碎片 5级", "神圣之火 5级", "心灵尖啸 3级"},
[44] = {"恢复 7级", "精神控制 2级", "心灵视界 2级"},
[46] = {"惩击 7级", "强效治疗术 2级", "心灵震爆 7级", "复活术 4级"},
[48] = {"真言术:盾 8级", "真言术:韧 5级", "法力燃烧 4级", "噬灵瘟疫 4级", "星辰碎片 6级", "神圣之火 6级", "恢复 8级", "暗言术:痛 7级"},
[50] = {"心灵之火 5级", "艾露恩的赐福 5级", "回馈 4级", "神圣之灵 3级", "治疗祷言 3级", "虚弱之触 5级", "虚弱妖术 5级", "恢复 8级", "绝望祷言 6级"},
[52] = {"强效治疗术 3级", "心灵震爆 8级", "安抚心灵 3级"},
[54] = {"真言术:盾 9级", "神圣之火 7级", "惩击 8级"},
[56] = {"法力燃烧 5级", "恢复 9级", "防护暗影 3级", "心灵尖啸 4级", "暗言术:痛 8级"},
[58] = {"复活术 5级", "强效治疗术 4级", "心灵震爆 9级"},
[60] = {"真言术:盾 10级", "心灵之火 6级", "真言术:韧 6级", "束缚亡灵 3级", "艾露恩的赐福 5级", "回馈 5级", "神圣之灵 4级", "精神祷言", "治疗祷言 4级", "虚弱之触 6级", "虚弱妖术 6级", "噬灵瘟疫 6级", "神圣之火 8级", "精神鞭笞 6级", "暗影守卫 6级", "渐隐术 6级"},
},
SHAMAN = {
[4] = {"地震术"},
[6] = {"治疗波 2级", "地缚图腾"},
[8] = {"闪电箭 2级", "石爪图腾", "地震术 2级", "闪电之盾"},
[10] = {"烈焰震击", "火舌武器", "大地之力图腾"},
[12] = {"净化术", "火焰新星图腾", "先祖之魂", "治疗波 3级"},
[14] = {"闪电箭 3级", "地震术 3级", "石肤图腾 2级"},
[16] = {"闪电之盾 2级", "石化武器 3级", "消毒术"},
[18] = {"烈焰震击 2级", "火舌武器 2级", "石爪图腾 2级", "治疗波 4级", "战栗图腾"},
[20] = {"闪电箭 4级", "灼热图腾 2级", "冰霜震击", "幽魂之狼", "次级治疗波"},
[22] = {"火焰新星图腾 2级", "水下呼吸", "祛病术", "清毒图腾"},
[24] = {"净化术 2级", "地震术 4级", "石肤图腾 3级", "石化武器 4级", "大地之力图腾 2级", "闪电之盾 3级", "抗寒图腾", "先祖之魂 2级"},
[26] = {"闪电箭 5级", "熔岩图腾", "火舌武器 3级", "视界术", "法力之泉图腾"},
[28] = {"石爪图腾 3级", "烈焰震击 3级", "冰封武器 2级", "抗火图腾", "火舌图腾", "水上行走", "次级治疗波 2级"},
[30] = {"灼热图腾 3级", "星界传送", "根基图腾", "石化武器 5级", "风怒武器", "自然抗性图腾", "治疗之泉图腾 2级"},
[32] = {"闪电箭 6级", "火焰新星图腾 3级", "闪电之盾 4级", "治疗波 6级", "闪电链", "风怒图腾"},
[34] = {"冰霜震击 2级", "石肤图腾 4级", "岗哨图腾"},
[36] = {"地震术 5级", "熔岩图腾 2级", "火舌武器 4级", "法力之泉图腾 2级", "次级治疗波 3级", "风墙图腾"},
[38] = {"石爪图腾 4级", "冰封武器 3级", "抗寒图腾 2级", "大地之力图腾 3级", "火舌图腾 2级"},
[40] = {"闪电箭 8级", "闪电链 2级", "烈焰震击 4级", "石肤图腾 5级", "治疗波 7级", "治疗链", "治疗之泉图腾 3级", "风怒武器 2级"},
[42] = {"火焰新星图腾 4级", "灼热图腾 4级", "抗火图腾 2级", "风之优雅图腾"},
[44] = {"闪电之盾 6级", "石化武器 6级", "冰霜震击 3级", "熔岩图腾 3级", "自然抗性图腾 2级", "风墙图腾 2级"},
[46] = {"火舌武器 5级", "治疗链 2级"},
[48] = {"地震术 6级", "石爪图腾 5级", "抗寒图腾 3级", "火舌图腾 3级", "治疗波 8级"},
[50] = {"闪电箭 9级", "灼热图腾 5级", "治疗之泉图腾 4级", "风怒武器 3级", "宁静之风图腾"},
[52] = {"烈焰震击 5级", "大地之力图腾 4级", "风怒图腾 3级", "次级治疗波 5级"},
[54] = {"石化武器 7级", "石肤图腾 6级", "抗寒图腾 3级"},
[56] = {"闪电箭 10级", "闪电链 4级", "熔岩图腾 4级", "冰封武器 4级", "抗火图腾 3级", "火舌图腾 4级", "风之优雅图腾 2级", "风墙图腾 3级", "治疗波 9级", "法力之泉图腾 4级"},
[58] = {"冰霜震击 4级"},
[60] = {"灼热图腾 6级", "风怒武器 4级", "自然抗性图腾 3级", "次级治疗波 6级", "治疗之泉图腾 5级"},
},

View File

@@ -0,0 +1,63 @@
WARRIOR = {
[4] = {"冲锋", "撕裂"},
[6] = {"雷霆一击"},
[8] = {"英勇打击 2级", "断筋"},
[10] = {"撕裂 2级", "血性狂暴"},
[12] = {"压制", "盾击", "战斗怒吼 2级"},
[14] = {"挫志怒吼", "复仇"},
[16] = {"英勇打击 3级", "惩戒痛击", "盾牌格挡"},
[18] = {"雷霆一击 2级", "缴械"},
[20] = {"撕裂 3级", "反击风暴", "顺劈斩"},
[22] = {"战斗怒吼 3级", "破甲攻击 2级", "破胆怒吼"},
[24] = {"英勇打击 4级", "挫志怒吼 2级", "复仇 2级", "斩杀"},
[26] = {"冲锋 2级", "惩戒痛击 2级", "挑战怒吼"},
[28] = {"雷霆一击 3级", "压制 2级", "盾墙"},
[30] = {"撕裂 4级", "顺劈斩 2级"},
[32] = {"英勇打击 5级", "断筋 2级", "斩杀 2级", "战斗怒吼 4级", "盾击 2级", "狂暴之怒"},
[34] = {"挫志怒吼 3级", "复仇 3级", "破甲攻击 3级"},
[36] = {"惩戒痛击 3级", "旋风斩"},
[38] = {"雷霆一击 4级", "猛击 2级", "拳击"},
[40] = {"英勇打击 6级", "撕裂 5级", "顺劈斩 3级", "斩杀 3级"},
[42] = {"战斗怒吼 5级", "拦截 2级"},
[44] = {"压制 3级", "挫志怒吼 4级", "复仇 4级"},
[46] = {"冲锋 3级", "惩戒痛击 4级", "猛击 3级", "破甲攻击 4级"},
[48] = {"英勇打击 7级", "雷霆一击 5级", "斩杀 4级"},
[50] = {"撕裂 6级", "鲁莽", "顺劈斩 4级"},
[52] = {"战斗怒吼 6级", "拦截 3级", "盾击 3级"},
[54] = {"断筋 3级", "挫志怒吼 5级", "猛击 4级", "复仇 5级"},
[56] = {"英勇打击 8级", "惩戒痛击 5级", "斩杀 5级"},
[58] = {"雷霆一击 6级", "拳击 2级", "破甲攻击 5级"},
[60] = {"撕裂 7级", "压制 4级", "顺劈斩 5级"},
},
PALADIN = {
[4] = {"力量祝福", "审判"},
[6] = {"圣光术 2级", "圣佑术", "十字军圣印"},
[8] = {"纯净术", "制裁之锤"},
[10] = {"圣疗术", "正义圣印 2级", "虔诚光环 2级", "保护祝福"},
[12] = {"力量祝福 2级", "十字军圣印 2级"},
[14] = {"圣光术 3级"},
[16] = {"正义之怒", "惩罚光环"},
[18] = {"正义圣印 3级", "圣佑术 2级"},
[20] = {"驱邪术", "圣光闪现", "虔诚光环 3级"},
[22] = {"圣光术 4级", "专注光环", "公正圣印", "力量祝福 3级", "十字军圣印 3级"},
[24] = {"超度亡灵", "救赎 2级", "智慧祝福 2级", "制裁之锤 2级", "保护祝福 2级"},
[26] = {"圣光闪现 2级", "正义圣印 4级", "拯救祝福", "惩罚光环 2级"},
[28] = {"驱邪术 2级"},
[30] = {"圣疗术 2级", "圣光术 5级", "光明圣印", "虔诚光环 4级", "神圣干涉"},
[32] = {"冰霜抗性光环", "力量祝福 4级", "十字军圣印 4级"},
[34] = {"智慧祝福 3级", "圣光闪现 3级", "正义圣印 5级", "圣盾术"},
[36] = {"驱邪术 3级", "救赎 3级", "火焰抗性光环", "惩罚光环 3级"},
[38] = {"圣光术 6级", "超度亡灵 2级", "智慧圣印", "保护祝福 3级"},
[40] = {"光明祝福", "光明圣印 2级", "虔诚光环 5级", "制裁之锤 3级", "暗影抗性光环 2级", "命令圣印 3级"},
[42] = {"圣光闪现 4级", "正义圣印 6级", "力量祝福 5级", "十字军圣印 5级"},
[44] = {"驱邪术 4级", "智慧祝福 4级", "冰霜抗性光环 2级"},
[46] = {"圣光术 7级", "惩罚光环 4级"},
[48] = {"救赎 4级", "智慧圣印 2级", "火焰抗性光环 2级"},
[50] = {"圣疗术 3级", "圣光闪现 5级", "光明祝福 2级", "光明圣印 3级", "正义圣印 7级", "虔诚光环 6级", "圣盾术 2级", "庇护祝福 3级", "命令圣印 4级"},
[52] = {"驱邪术 5级", "超度亡灵 3级", "愤怒之锤 2级", "暗影抗性光环 3级", "力量祝福 6级", "十字军圣印 6级", "强效力量祝福"},
[54] = {"圣光术 8级", "智慧祝福 5级", "强效智慧祝福", "制裁之锤 4级", "牺牲祝福 2级"},
[56] = {"冰霜抗性光环 3级", "惩罚光环 5级"},
[58] = {"圣光闪现 6级", "智慧圣印 3级", "正义圣印 8级"},
[60] = {"驱邪术 6级", "神圣愤怒 2级", "救赎 5级", "光明祝福 3级", "光明圣印 4级", "愤怒之锤 3级", "强效光明祝福", "强效智慧祝福 2级", "虔诚光环 7级", "火焰抗性光环 3级", "庇护祝福 4级", "命令圣印 5级", "强效力量祝福 2级", "强效拯救祝福", "强效王者祝福", "强效庇护祝福"},
},

View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
import re
def parse_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
result = {}
# Split by "需要等级" to find each skill block - the skill name is before it
parts = re.split(r'(需要等级\s+\d+)', content)
for i in range(1, len(parts), 2):
if i+1 >= len(parts):
break
level_line = parts[i] # "需要等级 12"
level_match = re.search(r'需要等级\s+(\d+)', level_line)
if not level_match:
continue
level = int(level_match.group(1))
if level % 2 != 0:
continue
after = parts[i+1]
learn_match = re.search(r'学习:\s*(.+?)(?:\n|$)', after)
if not learn_match or '默认开启' in learn_match.group(1):
continue
# Get skill name from the part before "需要等级"
before = parts[i-1]
lines = before.strip().split('\n')
skill_line = None
for line in reversed(lines):
line = line.strip()
if not line or 'javascript' in line or line.startswith('['):
continue
if re.match(r'^[\u4e00-\u9fff\s]+(等级\s+\d+)?\s*$', line) and len(line) < 50:
skill_line = line
break
if not skill_line:
continue
rank_match = re.match(r'^(.+?)\s+等级\s+(\d+)\s*$', skill_line)
if rank_match:
skill_base = rank_match.group(1).strip()
rank = int(rank_match.group(2))
display = skill_base if rank == 1 else f"{skill_base} {rank}"
else:
display = skill_line
if any(x in display for x in ['魔兽世界', '职业', '需要', '·']):
continue
if level not in result:
result[level] = []
if display not in result[level]:
result[level].append(display)
return dict(sorted(result.items()))
def format_lua(data, name):
lines = [f"{name} = {{"]
for level, skills in sorted(data.items()):
skills_str = ", ".join(f'"{s}"' for s in sorted(skills))
lines.append(f" [{level}] = {{{skills_str}}},")
lines.append("},")
return "\n".join(lines)
priest_file = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\8aaa3634-8b06-4c6a-838c-18f248f9b747.txt'
shaman_file = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\e5e3b3f9-edfa-4a99-a95e-c9f7ed954371.txt'
priest_data = parse_file(priest_file)
shaman_data = parse_file(shaman_file)
output = format_lua(priest_data, "PRIEST") + "\n\n" + format_lua(shaman_data, "SHAMAN")
outpath = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\class_trainer_skills.lua'
with open(outpath, 'w', encoding='utf-8') as f:
f.write(output)
print("Done. Output written to class_trainer_skills.lua")

BIN
img/UI-Classes-Circles.tga Normal file

Binary file not shown.

BIN
img/cat.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
img/df-gryphon-beta.tga Normal file

Binary file not shown.

BIN
img/df-gryphon.tga Normal file

Binary file not shown.

BIN
img/df-wyvern.tga Normal file

Binary file not shown.

BIN
img/dly.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/fs.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

3
img/gude-2026-03-05.log Normal file
View File

@@ -0,0 +1,3 @@
12:40:26:562 [CRITICAL] SharedConnection - Failed to open WinHTTP Session. 参数错误。
, last error code = 87

BIN
img/icon.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon2.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon3.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon4.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon5.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon6.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon7.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/icon8.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/lr.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/map.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/ms.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/qs.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/qxz.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/sm.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/ss.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
img/zs.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,177 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link type="text/css" rel="stylesheet" href="resources/sheet.css">
<style type="text/css">
.ritz .waffle a {
color: inherit;
}
.ritz .waffle .s0 {
background-color: #ffffff;
text-align: left;
color: #000000;
font-family: Arial;
font-size: 10pt;
vertical-align: bottom;
white-space: nowrap;
direction: ltr;
padding: 2px 3px 2px 3px;
}
.ritz .waffle .s1 {
background-color: #ffffff;
text-align: right;
color: #000000;
font-family: Arial;
font-size: 10pt;
vertical-align: bottom;
white-space: nowrap;
direction: ltr;
padding: 2px 3px 2px 3px;
}
</style>
<div class="ritz grid-container" dir="ltr">
<table class="waffle" cellspacing="0" cellpadding="0">
<thead>
<tr>
<th class="row-header freezebar-origin-ltr"></th>
<th id="1333869788C0" style="width:100px;" class="column-headers-background">A</th>
<th id="1333869788C1" style="width:153px;" class="column-headers-background">B</th>
<th id="1333869788C2" style="width:130px;" class="column-headers-background">C</th>
<th id="1333869788C3" style="width:136px;" class="column-headers-background">D</th>
<th id="1333869788C4" style="width:147px;" class="column-headers-background">E</th>
<th id="1333869788C5" style="width:134px;" class="column-headers-background">F</th>
<th id="1333869788C6" style="width:134px;" class="column-headers-background">G</th>
<th id="1333869788C7" style="width:126px;" class="column-headers-background">H</th>
<th id="1333869788C8" style="width:162px;" class="column-headers-background">I</th>
</tr>
</thead>
<tbody>
<tr style="height: 20px">
<th id="1333869788R0" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">1</div>
</th>
<td class="s0" dir="ltr">行 \ 列</td>
<td class="s1" dir="ltr">1</td>
<td class="s1" dir="ltr">2</td>
<td class="s1" dir="ltr">3</td>
<td class="s1" dir="ltr">4</td>
<td class="s1" dir="ltr">5</td>
<td class="s1" dir="ltr">6</td>
<td class="s1" dir="ltr">7</td>
<td class="s1" dir="ltr">8</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R1" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">2</div>
</th>
<td class="s0" dir="ltr">第 1 行</td>
<td class="s0" dir="ltr">🐾 主题logo</td>
<td class="s0" dir="ltr">💾 保存/配置文件</td>
<td class="s0" dir="ltr">❌ 关闭/取消</td>
<td class="s0" dir="ltr">🔗 断开/解除绑定</td>
<td class="s0" dir="ltr">❗ 任务/重要警告</td>
<td class="s0" dir="ltr">🦁 联盟阵营</td>
<td class="s0" dir="ltr">👹 部落阵营</td>
<td class="s0" dir="ltr">🧝 角色/玩家属性</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R2" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">3</div>
</th>
<td class="s0" dir="ltr">第 2 行</td>
<td class="s0" dir="ltr">💬 聊天/社交</td>
<td class="s0" dir="ltr">⚙️ 设置/选项</td>
<td class="s0" dir="ltr">🧠 智力/宏命令</td>
<td class="s0" dir="ltr">🎒 背包/行囊</td>
<td class="s0" dir="ltr">🐴 坐骑</td>
<td class="s0" dir="ltr">🏆 成就系统</td>
<td class="s0" dir="ltr">💰 金币/经济</td>
<td class="s0" dir="ltr">👥 好友/社交列表</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R3" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">4</div>
</th>
<td class="s0" dir="ltr">第 3 行</td>
<td class="s0" dir="ltr">🚪 离开/退出队伍</td>
<td class="s0" dir="ltr">🫂 公会/团队</td>
<td class="s0" dir="ltr">💰 战利品/拾取</td>
<td class="s0" dir="ltr">🐉 首领/地下城</td>
<td class="s0" dir="ltr">⚒️ 专业技能/制造</td>
<td class="s0" dir="ltr">⏻ 退出游戏/系统</td>
<td class="s0" dir="ltr">🗺️ 世界地图/导航</td>
<td class="s0" dir="ltr">🌳 天赋树/专精</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R4" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">5</div>
</th>
<td class="s0" dir="ltr">第 4 行</td>
<td class="s0" dir="ltr">🔥 法术/魔法</td>
<td class="s0" dir="ltr">🗡️ 攻击/近战</td>
<td class="s0" dir="ltr">📈 伤害统计/数据</td>
<td class="s0" dir="ltr">📡 网络延迟 (Ping)</td>
<td class="s0" dir="ltr">✉️ 邮件/信箱</td>
<td class="s0" dir="ltr">📜 任务日志</td>
<td class="s0" dir="ltr">📖 法术书/技能</td>
<td class="s0" dir="ltr">🏪 商店/商人</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R5" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">6</div>
</th>
<td class="s0" dir="ltr">第 5 行</td>
<td class="s0" dir="ltr">⭐ 收藏/团队标记</td>
<td class="s0" dir="ltr">🧪 治疗药水/消耗品</td>
<td class="s0" dir="ltr">☠️ 死亡/骷髅标记</td>
<td class="s0" dir="ltr"> 治疗/生命值</td>
<td class="s0" dir="ltr">🏠 房屋/要塞/旅店</td>
<td class="s0" dir="ltr">📜 笔记/文本卷轴</td>
<td class="s0" dir="ltr">🌿 草药学/自然</td>
<td class="s0" dir="ltr">🗝️ 钥匙/大秘境</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R6" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">7</div>
</th>
<td class="s0" dir="ltr">第 6 行</td>
<td class="s0" dir="ltr">🛡️ 坦克/防御</td>
<td class="s0" dir="ltr">🔨 拍卖行/竞标</td>
<td class="s0" dir="ltr">🎣 钓鱼专业</td>
<td class="s0" dir="ltr">📅 日历/游戏活动</td>
<td class="s0" dir="ltr">🏰 副本入口/传送门</td>
<td class="s0" dir="ltr">🔠 寻找组队 (LFG)</td>
<td class="s0" dir="ltr">📋 角色面板/纸娃娃</td>
<td class="s0" dir="ltr">❓ 帮助/客服/未知</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R7" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">8</div>
</th>
<td class="s0" dir="ltr">第 7 行</td>
<td class="s0" dir="ltr">🔊 声音/音量设置</td>
<td class="s0" dir="ltr">🔍 搜索/观察玩家</td>
<td class="s0" dir="ltr">🌿 荣誉/PVP/威望</td>
<td class="s0" dir="ltr">🔠 菜单/列表项</td>
<td class="s0" dir="ltr">🛒 游戏商城/购买</td>
<td class="s0" dir="ltr">💪 力量/增益 (Buff)</td>
<td class="s0" dir="ltr">🏹 远程/猎人武器</td>
<td class="s0" dir="ltr">🥾 移动速度/敏捷</td>
</tr>
<tr style="height: 20px">
<th id="1333869788R8" style="height: 20px;" class="row-headers-background">
<div class="row-header-wrapper" style="line-height: 20px">9</div>
</th>
<td class="s0" dir="ltr">第 8 行</td>
<td class="s0" dir="ltr">⚡ 能量/急速/闪电</td>
<td class="s0" dir="ltr">🧪 毒药/有害效果</td>
<td class="s0" dir="ltr">🛡️ 护甲提升/减伤</td>
<td class="s0" dir="ltr">🥣 研磨/炼金术</td>
<td class="s0" dir="ltr">🔥 营火/烹饪专业</td>
<td class="s0" dir="ltr">⛺ 营地/休息区</td>
<td class="s0" dir="ltr">🌀 炉石</td>
<td class="s0" dir="ltr">⛏️ 采矿专业</td>
</tr>
</tbody>
</table>
</div>

Binary file not shown.