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

524
Core.lua Normal file
View File

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