init
7
.nanami-launcher.json
Normal 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
1749
ActionBars.lua
Normal file
2087
Bags/Bank.lua
Normal file
1619
Bags/Container.lua
Normal file
113
Bags/Core.lua
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
346
ClassSkillData.lua
Normal 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
@@ -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
524
Core.lua
Normal 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
@@ -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
@@ -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
@@ -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
1282
FlightMap.lua
Normal file
158
Focus.lua
Normal 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
@@ -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
108
IconMap.lua
Normal 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
337
MapIcons.lua
Normal 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
@@ -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
@@ -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
590
Minimap.lua
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
1663
QuestUI.lua
Normal file
548
Roll.lua
Normal 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
1257
SetupWizard.lua
Normal file
2331
SocialUI.lua
Normal file
983
SpellBookUI.lua
Normal 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
@@ -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
@@ -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
9852
TradeSkillDB.lua
Normal file
2130
TradeSkillUI.lua
Normal file
1234
TrainerUI.lua
Normal file
1051
Tweaks.lua
Normal file
1176
Units/Party.lua
Normal file
787
Units/Pet.lua
Normal 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
1064
Units/Raid.lua
Normal file
1065
Units/TalentTree.lua
Normal file
1046
Units/Target.lua
Normal file
85
Units/ToT.lua
Normal 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
@@ -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
96
agent-tools/MAGE_WARLOCK_DRUID_skills.lua
Normal 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级"},
|
||||
},
|
||||
73
agent-tools/TALENT_TRAINER_SKILLS.lua
Normal 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
|
||||
63
agent-tools/class_trainer_skills.lua
Normal 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级"},
|
||||
},
|
||||
63
agent-tools/class_trainer_skills_output.lua
Normal 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级", "强效拯救祝福", "强效王者祝福", "强效庇护祝福"},
|
||||
},
|
||||
78
agent-tools/parse_skills.py
Normal 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
BIN
img/cat.tga
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
img/df-gryphon-beta.tga
Normal file
BIN
img/df-gryphon.tga
Normal file
BIN
img/df-wyvern.tga
Normal file
BIN
img/dly.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/fs.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
3
img/gude-2026-03-05.log
Normal 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
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon2.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon3.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon4.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon5.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon6.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon7.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/icon8.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/lr.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/map.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/ms.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/qs.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/qxz.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/sm.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/ss.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
img/zs.tga
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
177
img/魔兽插件图标含义对照表.html
Normal 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>
|
||||