Files
Nanami-UI/Core.lua
2026-03-20 14:04:51 +08:00

584 lines
24 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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
-- Nanami-Plates SpellDB: combat log + spell DB tracking (most accurate for debuffs)
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
local effect, rank, tex, stacks, dtype, duration, timeleft, isOwn = NanamiPlates_SpellDB:UnitDebuff(unit, index)
if timeleft and timeleft > 0 then
return timeleft, effect
end
if effect and effect ~= "" and duration and duration > 0
and NanamiPlates_Auras and NanamiPlates_Auras.timers then
local unitKey = (UnitGUID and UnitGUID(unit)) or UnitName(unit) or ""
local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect]
if not cached and UnitName(unit) then
cached = NanamiPlates_Auras.timers[UnitName(unit) .. "_" .. effect]
end
if cached and cached.startTime and cached.duration then
local remaining = cached.duration - (GetTime() - cached.startTime)
if remaining > 0 then
return remaining, effect
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 == "debugbuffs" or cmd == "db" then
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 当前所有 Buff")
for i = 0, 31 do
local buffIndex = GetPlayerBuff(i, "HELPFUL")
if buffIndex and buffIndex >= 0 then
local name = SFrames:GetBuffName(buffIndex)
local tex = GetPlayerBuffTexture(buffIndex) or "?"
local tl = GetPlayerBuffTimeLeft(buffIndex)
local timeStr = (tl and tl > 0 and tl < 99999) and string.format("%.1fs", tl) or "N/A"
local hidden = SFrames:IsBuffHidden(buffIndex) and "|cffff4444[隐藏]|r" or ""
DEFAULT_CHAT_FRAME:AddMessage(string.format(" #%d: |cffffd100%s|r %s %s", i, name or "(nil)", timeStr, hidden))
end
end
elseif cmd == "hidebuff" or cmd == "hb" then
if args == "" then
SFrames:Print("用法: /nui hidebuff <buff名称>")
else
if not SFramesDB.hiddenBuffs then SFramesDB.hiddenBuffs = {} end
SFramesDB.hiddenBuffs[args] = true
SFrames:Print("已隐藏 Buff: |cffffd100" .. args .. "|r")
end
elseif cmd == "unhidebuff" or cmd == "uhb" then
if args == "" then
SFrames:Print("用法: /nui unhidebuff <buff名称>")
else
if SFramesDB.hiddenBuffs then SFramesDB.hiddenBuffs[args] = nil end
SFrames:Print("已取消隐藏 Buff: |cffffd100" .. args .. "|r")
end
elseif cmd == "listhidden" or cmd == "lh" then
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 已隐藏的 Buff 列表:")
if SFramesDB.hiddenBuffs then
for name, _ in pairs(SFramesDB.hiddenBuffs) do
DEFAULT_CHAT_FRAME:AddMessage(" |cffffd100" .. name .. "|r (用户自定义)")
end
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