聊天重做前缓存

This commit is contained in:
rucky
2026-04-09 09:46:47 +08:00
parent 6e18269bfd
commit e915bbd74a
39 changed files with 8501 additions and 2308 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(find . -name \"*.lua\" -exec grep -l \"database\\\\|DATABASE\\\\|Encyclopedia\\\\|encyclop\" {} \\\\;)",
"Bash(ls -lah *.lua)"
]
}
}

File diff suppressed because it is too large Load Diff

594
AuraTracker.lua Normal file
View File

@@ -0,0 +1,594 @@
SFrames.AuraTracker = SFrames.AuraTracker or {}
local AT = SFrames.AuraTracker
AT.units = AT.units or {}
AT.unitRefs = AT.unitRefs or {}
AT.durationCache = AT.durationCache or { buff = {}, debuff = {} }
AT.initialized = AT.initialized or false
local SOURCE_PRIORITY = {
player_native = 6,
superwow = 5,
nanamiplates = 4,
shagutweaks = 3,
combat_log = 2,
estimated = 1,
}
local function GetNow()
return GetTime and GetTime() or 0
end
local function IsBuffType(auraType)
return auraType == "buff"
end
local function ClampPositive(value)
value = tonumber(value) or 0
if value > 0 then
return value
end
return nil
end
local function SafeUnitGUID(unit)
if unit and UnitGUID then
local ok, guid = pcall(UnitGUID, unit)
if ok and guid and guid ~= "" then
return guid
end
end
return nil
end
local function SafeUnitName(unit)
if unit and UnitName then
local ok, name = pcall(UnitName, unit)
if ok and name and name ~= "" then
return name
end
end
return nil
end
local function GetUnitKey(unit)
local guid = SafeUnitGUID(unit)
if guid then
return guid
end
local name = SafeUnitName(unit)
if name then
return "name:" .. name
end
return unit and ("unit:" .. unit) or nil
end
local function DurationCacheKey(auraType, spellId, name, texture)
if spellId and spellId > 0 then
return auraType .. ":id:" .. tostring(spellId)
end
if name and name ~= "" then
return auraType .. ":name:" .. string.lower(name)
end
return auraType .. ":tex:" .. tostring(texture or "")
end
local function BuildStateKeys(auraType, spellId, name, texture, casterKey)
local keys = {}
if spellId and spellId > 0 then
tinsert(keys, auraType .. ":id:" .. tostring(spellId))
if casterKey and casterKey ~= "" then
tinsert(keys, auraType .. ":id:" .. tostring(spellId) .. ":caster:" .. casterKey)
end
end
if name and name ~= "" then
local lowerName = string.lower(name)
tinsert(keys, auraType .. ":name:" .. lowerName)
if casterKey and casterKey ~= "" then
tinsert(keys, auraType .. ":name:" .. lowerName .. ":caster:" .. casterKey)
end
if texture and texture ~= "" then
tinsert(keys, auraType .. ":name:" .. lowerName .. ":tex:" .. texture)
end
elseif texture and texture ~= "" then
tinsert(keys, auraType .. ":tex:" .. texture)
end
return keys
end
local function TooltipLine1()
return getglobal("SFramesScanTooltipTextLeft1")
end
local function CLMatch(msg, pattern)
if not msg or not pattern or pattern == "" then return nil end
local pat = string.gsub(pattern, "%%%d?%$?s", "(.+)")
pat = string.gsub(pat, "%%%d?%$?d", "(%%d+)")
for a, b, c in string.gfind(msg, pat) do
return a, b, c
end
return nil
end
function AT:GetUnitState(unitOrGUID)
local key = unitOrGUID
if not key then return nil end
if string.find(key, "^target") or string.find(key, "^player") or string.find(key, "^party") or string.find(key, "^raid") or string.find(key, "^pet") then
key = GetUnitKey(key)
end
if not key then return nil end
if not self.units[key] then
self.units[key] = {
guid = key,
buffs = {},
debuffs = {},
maps = { buff = {}, debuff = {} },
snapshotCount = 0,
lastSeen = 0,
name = nil,
level = 0,
}
end
return self.units[key]
end
function AT:ClearUnit(unitOrGUID)
local key = unitOrGUID
if not key then return end
if not self.units[key] then
key = GetUnitKey(unitOrGUID)
end
if not key then return end
self.units[key] = nil
for unit, guid in pairs(self.unitRefs) do
if guid == key then
self.unitRefs[unit] = nil
end
end
end
function AT:ClearCurrentTarget()
local oldGUID = self.unitRefs["target"]
self.unitRefs["target"] = nil
if oldGUID then
local state = self.units[oldGUID]
if state then
state.maps.buff = {}
state.maps.debuff = {}
end
end
end
function AT:RememberDuration(auraType, spellId, name, texture, duration)
duration = ClampPositive(duration)
if not duration then return end
self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)] = duration
end
function AT:GetRememberedDuration(auraType, spellId, name, texture)
return self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)]
end
function AT:ReadAuraName(unit, index, isBuff, auraID)
if auraID and auraID > 0 and SpellInfo then
local ok, spellName = pcall(SpellInfo, auraID)
if ok and spellName and spellName ~= "" then
return spellName
end
end
-- Tooltip scan is expensive and can crash on invalid unit/index state.
-- Only attempt if unit still exists and the aura slot is still valid.
if not SFrames.Tooltip then return nil end
if not unit or not UnitExists or not UnitExists(unit) then return nil end
local ok, text = pcall(function()
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:ClearLines()
if isBuff then
SFrames.Tooltip:SetUnitBuff(unit, index)
else
SFrames.Tooltip:SetUnitDebuff(unit, index)
end
local line = TooltipLine1()
local t = line and line.GetText and line:GetText() or nil
SFrames.Tooltip:Hide()
return t
end)
if ok and text and text ~= "" then
return text
end
return nil
end
function AT:GetPlayerAuraTime(unit, index, isBuff, texture)
if not UnitIsUnit or not UnitIsUnit(unit, "player") then
return nil, nil, nil
end
if not GetPlayerBuff or not GetPlayerBuffTexture or not GetPlayerBuffTimeLeft then
return nil, nil, nil
end
local filter = isBuff and "HELPFUL" or "HARMFUL"
for i = 0, 31 do
local buffIndex = GetPlayerBuff(i, filter)
if buffIndex and buffIndex >= 0 and GetPlayerBuffTexture(buffIndex) == texture then
local timeLeft = ClampPositive(GetPlayerBuffTimeLeft(buffIndex))
if timeLeft then
return timeLeft, nil, "player_native"
end
end
end
return nil, nil, nil
end
function AT:GetExternalTime(unit, auraType, index, state)
local isBuff = IsBuffType(auraType)
local timeLeft, duration, source
if state and state.texture then
timeLeft, duration, source = self:GetPlayerAuraTime(unit, index, isBuff, state.texture)
if timeLeft then
return timeLeft, duration, source
end
end
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
local effect, _, _, _, _, npDuration, npTimeLeft = NanamiPlates_SpellDB:UnitDebuff(unit, index)
npTimeLeft = ClampPositive(npTimeLeft)
npDuration = ClampPositive(npDuration)
if effect and effect ~= "" and (not state.name or state.name == "") then
state.name = effect
end
if npTimeLeft then
return npTimeLeft, npDuration, "nanamiplates"
end
end
if not isBuff and ShaguTweaks and ShaguTweaks.libdebuff and ShaguTweaks.libdebuff.UnitDebuff then
local _, _, _, _, _, shaguDuration, shaguTimeLeft = ShaguTweaks.libdebuff:UnitDebuff(unit, index)
shaguTimeLeft = ClampPositive(shaguTimeLeft)
shaguDuration = ClampPositive(shaguDuration)
if shaguTimeLeft then
return shaguTimeLeft, shaguDuration, "shagutweaks"
end
end
return nil, nil, nil
end
function AT:ApplyTiming(state, timeLeft, duration, source, now, allowWeaker)
timeLeft = ClampPositive(timeLeft)
if not timeLeft then return false end
duration = ClampPositive(duration) or state.duration or timeLeft
local newPriority = SOURCE_PRIORITY[source] or 0
local oldPriority = SOURCE_PRIORITY[state.source] or 0
if not allowWeaker and state.expirationTime and state.expirationTime > now and oldPriority > newPriority then
return false
end
state.duration = duration
state.expirationTime = now + timeLeft
state.appliedAt = state.expirationTime - duration
state.source = source
state.isEstimated = (source == "estimated") and 1 or nil
self:RememberDuration(state.auraType, state.spellId, state.name, state.texture, duration)
return true
end
function AT:CaptureAura(unit, auraType, index)
local isBuff = IsBuffType(auraType)
local texture, count, dispelType, auraID
local hasSuperWoW = SFrames.superwow_active and SpellInfo
if isBuff then
texture, auraID = UnitBuff(unit, index)
else
texture, count, dispelType, auraID = UnitDebuff(unit, index)
end
if not texture then
return nil
end
local spellId = (hasSuperWoW and type(auraID) == "number" and auraID > 0) and auraID or nil
-- Only call ReadAuraName when we have a spellId (fast path) or for the first 16 slots
-- to avoid spamming tooltip API on every aura slot every UNIT_AURA event.
local name
if spellId or index <= 16 then
name = self:ReadAuraName(unit, index, isBuff, spellId)
end
return {
auraType = auraType,
index = index,
spellId = spellId,
name = name,
texture = texture,
stacks = tonumber(count) or 0,
dispelType = dispelType,
casterKey = nil,
}
end
function AT:FindPreviousState(unitState, auraType, index, info)
local previous = unitState.maps[auraType][index]
if previous then
local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
local prevKeys = BuildStateKeys(auraType, previous.spellId, previous.name, previous.texture, previous.casterKey)
for _, key in ipairs(keys) do
for _, prevKey in ipairs(prevKeys) do
if key == prevKey then
return previous
end
end
end
end
local states = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
for _, key in ipairs(keys) do
local state = states[key]
if state then
return state
end
end
return nil
end
function AT:UpdateSnapshotState(unitState, unit, auraType, index, info, now)
local state = self:FindPreviousState(unitState, auraType, index, info)
if not state then
state = {
guid = unitState.guid,
auraType = auraType,
firstSeenAt = now,
}
end
state.guid = unitState.guid
state.auraType = auraType
state.spellId = info.spellId
state.name = info.name or state.name
state.texture = info.texture
state.stacks = info.stacks
state.dispelType = info.dispelType
state.casterKey = info.casterKey
state.lastSeen = now
state.index = index
local timeLeft, duration, source = self:GetExternalTime(unit, auraType, index, state)
if timeLeft then
self:ApplyTiming(state, timeLeft, duration, source, now)
elseif state.expirationTime and state.expirationTime <= now then
state.expirationTime = nil
state.duration = nil
state.appliedAt = nil
state.source = nil
end
return state
end
function AT:RebuildStateMaps(unitState, auraType, slots, now)
local active = {}
local newMap = {}
for index, state in pairs(slots) do
if state then
for _, key in ipairs(BuildStateKeys(auraType, state.spellId, state.name, state.texture, state.casterKey)) do
active[key] = state
end
newMap[index] = state
end
end
if IsBuffType(auraType) then
unitState.buffs = active
else
unitState.debuffs = active
end
unitState.maps[auraType] = newMap
unitState.lastSeen = now
end
function AT:ApplyCombatHint(auraType, auraName)
if not auraName or auraName == "" then return end
if auraType ~= "debuff" then return end
local guid = self.unitRefs["target"]
if not guid then return end
local unitState = self.units[guid]
if not unitState then return end
local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
local state = active[auraType .. ":name:" .. string.lower(auraName)]
if not state then return end
local remembered = self:GetRememberedDuration(auraType, state.spellId, state.name, state.texture)
if remembered then
self:ApplyTiming(state, remembered, remembered, "combat_log", GetNow(), true)
end
end
function AT:ClearCombatHint(auraName)
if not auraName or auraName == "" then return end
local guid = self.unitRefs["target"]
if not guid then return end
local unitState = self.units[guid]
if not unitState then return end
local lowerName = string.lower(auraName)
for _, auraType in ipairs({ "buff", "debuff" }) do
local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
local state = active[auraType .. ":name:" .. lowerName]
if state then
state.expirationTime = nil
state.duration = nil
state.appliedAt = nil
state.source = nil
end
end
end
function AT:HandleCombatMessage(msg)
if not msg or msg == "" or not UnitExists or not UnitExists("target") then return end
local targetName = SafeUnitName("target")
if not targetName then return end
local targetUnit, auraName = CLMatch(msg, AURAADDEDOTHERHARMFUL or "%s is afflicted by %s.")
if targetUnit == targetName and auraName then
self:ApplyCombatHint("debuff", auraName)
return
end
auraName, targetUnit = CLMatch(msg, AURAREMOVEDOTHER or "%s fades from %s.")
if targetUnit == targetName and auraName then
self:ClearCombatHint(auraName)
end
end
function AT:HandleAuraSnapshot(unit)
if not unit then return end
if not UnitExists or not UnitExists(unit) then
if unit == "target" then
self:ClearCurrentTarget()
end
return
end
local guid = GetUnitKey(unit)
if not guid then return end
local now = GetNow()
local unitState = self:GetUnitState(guid)
unitState.guid = guid
unitState.name = SafeUnitName(unit)
unitState.level = (UnitLevel and UnitLevel(unit)) or 0
unitState.lastSeen = now
self.unitRefs[unit] = guid
local buffSlots = {}
local debuffSlots = {}
for i = 1, 32 do
local info = self:CaptureAura(unit, "buff", i)
if info then
buffSlots[i] = self:UpdateSnapshotState(unitState, unit, "buff", i, info, now)
end
end
for i = 1, 32 do
local info = self:CaptureAura(unit, "debuff", i)
if info then
debuffSlots[i] = self:UpdateSnapshotState(unitState, unit, "debuff", i, info, now)
end
end
self:RebuildStateMaps(unitState, "buff", buffSlots, now)
self:RebuildStateMaps(unitState, "debuff", debuffSlots, now)
unitState.snapshotCount = unitState.snapshotCount + 1
end
function AT:GetAuraState(unit, auraType, index)
local unitState = self:GetUnitState(unit)
if not unitState then return nil end
local map = unitState.maps[auraType]
return map and map[index] or nil
end
function AT:GetAuraTimeLeft(unit, auraType, index)
local state = self:GetAuraState(unit, auraType, index)
if not state or not state.expirationTime then
return nil
end
local remaining = state.expirationTime - GetNow()
if remaining > 0 then
return remaining
end
return nil
end
function AT:PurgeStaleUnits()
local now = GetNow()
local activeTargetGUID = self.unitRefs["target"]
for guid, state in pairs(self.units) do
if guid ~= activeTargetGUID and state.lastSeen and (now - state.lastSeen) > 120 then
self.units[guid] = nil
end
end
end
function AT:OnEvent()
if event == "PLAYER_TARGET_CHANGED" then
if UnitExists and UnitExists("target") then
self:HandleAuraSnapshot("target")
else
self:ClearCurrentTarget()
end
self:PurgeStaleUnits()
return
end
if event == "UNIT_AURA" and arg1 == "target" then
self:HandleAuraSnapshot("target")
return
end
if event == "PLAYER_ENTERING_WORLD" then
self:ClearCurrentTarget()
self:PurgeStaleUnits()
return
end
if arg1 and string.find(event, "CHAT_MSG_SPELL") then
self:HandleCombatMessage(arg1)
end
end
function AT:Initialize()
if self.initialized then return end
self.initialized = true
local frame = CreateFrame("Frame", "SFramesAuraTracker", UIParent)
self.frame = frame
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
frame:RegisterEvent("UNIT_AURA")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
local chatEvents = {
"CHAT_MSG_SPELL_SELF_DAMAGE",
"CHAT_MSG_SPELL_SELF_BUFF",
"CHAT_MSG_SPELL_PARTY_DAMAGE",
"CHAT_MSG_SPELL_PARTY_BUFF",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
"CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
"CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
}
for _, ev in ipairs(chatEvents) do
frame:RegisterEvent(ev)
end
frame:SetScript("OnEvent", function()
AT:OnEvent()
end)
end

View File

@@ -4,4 +4,149 @@
SFrames.WorldMap:ToggleNav()
end
</Binding>
<Binding name="NANAMI_EXTRABAR1" header="NANAMI_EXTRABAR" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(1) end
</Binding>
<Binding name="NANAMI_EXTRABAR2" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(2) end
</Binding>
<Binding name="NANAMI_EXTRABAR3" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(3) end
</Binding>
<Binding name="NANAMI_EXTRABAR4" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(4) end
</Binding>
<Binding name="NANAMI_EXTRABAR5" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(5) end
</Binding>
<Binding name="NANAMI_EXTRABAR6" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(6) end
</Binding>
<Binding name="NANAMI_EXTRABAR7" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(7) end
</Binding>
<Binding name="NANAMI_EXTRABAR8" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(8) end
</Binding>
<Binding name="NANAMI_EXTRABAR9" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(9) end
</Binding>
<Binding name="NANAMI_EXTRABAR10" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(10) end
</Binding>
<Binding name="NANAMI_EXTRABAR11" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(11) end
</Binding>
<Binding name="NANAMI_EXTRABAR12" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(12) end
</Binding>
<Binding name="NANAMI_EXTRABAR13" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(13) end
</Binding>
<Binding name="NANAMI_EXTRABAR14" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(14) end
</Binding>
<Binding name="NANAMI_EXTRABAR15" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(15) end
</Binding>
<Binding name="NANAMI_EXTRABAR16" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(16) end
</Binding>
<Binding name="NANAMI_EXTRABAR17" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(17) end
</Binding>
<Binding name="NANAMI_EXTRABAR18" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(18) end
</Binding>
<Binding name="NANAMI_EXTRABAR19" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(19) end
</Binding>
<Binding name="NANAMI_EXTRABAR20" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(20) end
</Binding>
<Binding name="NANAMI_EXTRABAR21" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(21) end
</Binding>
<Binding name="NANAMI_EXTRABAR22" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(22) end
</Binding>
<Binding name="NANAMI_EXTRABAR23" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(23) end
</Binding>
<Binding name="NANAMI_EXTRABAR24" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(24) end
</Binding>
<Binding name="NANAMI_EXTRABAR25" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(25) end
</Binding>
<Binding name="NANAMI_EXTRABAR26" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(26) end
</Binding>
<Binding name="NANAMI_EXTRABAR27" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(27) end
</Binding>
<Binding name="NANAMI_EXTRABAR28" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(28) end
</Binding>
<Binding name="NANAMI_EXTRABAR29" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(29) end
</Binding>
<Binding name="NANAMI_EXTRABAR30" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(30) end
</Binding>
<Binding name="NANAMI_EXTRABAR31" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(31) end
</Binding>
<Binding name="NANAMI_EXTRABAR32" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(32) end
</Binding>
<Binding name="NANAMI_EXTRABAR33" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(33) end
</Binding>
<Binding name="NANAMI_EXTRABAR34" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(34) end
</Binding>
<Binding name="NANAMI_EXTRABAR35" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(35) end
</Binding>
<Binding name="NANAMI_EXTRABAR36" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(36) end
</Binding>
<Binding name="NANAMI_EXTRABAR37" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(37) end
</Binding>
<Binding name="NANAMI_EXTRABAR38" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(38) end
</Binding>
<Binding name="NANAMI_EXTRABAR39" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(39) end
</Binding>
<Binding name="NANAMI_EXTRABAR40" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(40) end
</Binding>
<Binding name="NANAMI_EXTRABAR41" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(41) end
</Binding>
<Binding name="NANAMI_EXTRABAR42" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(42) end
</Binding>
<Binding name="NANAMI_EXTRABAR43" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(43) end
</Binding>
<Binding name="NANAMI_EXTRABAR44" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(44) end
</Binding>
<Binding name="NANAMI_EXTRABAR45" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(45) end
</Binding>
<Binding name="NANAMI_EXTRABAR46" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(46) end
</Binding>
<Binding name="NANAMI_EXTRABAR47" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(47) end
</Binding>
<Binding name="NANAMI_EXTRABAR48" runOnUp="false">
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(48) end
</Binding>
</Bindings>

View File

@@ -338,6 +338,58 @@ local BASE_SPELL_CRIT = {
DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0,
}
--------------------------------------------------------------------------------
-- Temporary weapon enchant crit detection (sharpening stones / scopes)
-- Scans tooltip green text for crit keywords, returns crit% bonus (e.g. 2)
--------------------------------------------------------------------------------
local _tempEnchTip
local function GetTempEnchantCrit(slotId)
if not GetWeaponEnchantInfo then return 0 end
-- slotId 16=MainHand, 17=OffHand, 18=Ranged
local hasMain, _, _, hasOff
hasMain, _, _, hasOff = GetWeaponEnchantInfo()
if slotId == 16 and not hasMain then return 0 end
if slotId == 17 and not hasOff then return 0 end
if slotId == 18 and not hasMain and not hasOff then
-- ranged slot: some servers report via hasMain for ranged-only classes
-- try scanning anyway if there's a ranged weapon equipped
if not GetInventoryItemLink("player", 18) then return 0 end
end
if not _tempEnchTip then
_tempEnchTip = CreateFrame("GameTooltip", "SFramesCPTempEnchTip", UIParent, "GameTooltipTemplate")
_tempEnchTip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -300, -300)
end
local tip = _tempEnchTip
tip:SetOwner(UIParent, "ANCHOR_NONE")
tip:ClearLines()
tip:SetInventoryItem("player", slotId)
local n = tip:NumLines()
if not n or n < 2 then return 0 end
for i = 2, n do
local obj = _G["SFramesCPTempEnchTipTextLeft" .. i]
if obj then
local txt = obj:GetText()
if txt and txt ~= "" then
local r, g, b = obj:GetTextColor()
-- green text = enchant/buff line
if g > 0.8 and r < 0.5 and b < 0.5 then
-- Match patterns like: "+2% 致命一击" / "+2% Critical" / "致命一击几率提高2%"
local _, _, pct = string.find(txt, "(%d+)%%%s*致命")
if not pct then _, _, pct = string.find(txt, "致命.-(%d+)%%") end
if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*[Cc]rit") end
if not pct then _, _, pct = string.find(txt, "[Cc]rit.-(%d+)%%") end
if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*暴击") end
if not pct then _, _, pct = string.find(txt, "暴击.-(%d+)%%") end
if pct then return tonumber(pct) or 0 end
end
end
end
end
return 0
end
local function CalcMeleeCrit()
local _, class = UnitClass("player")
class = class or ""
@@ -523,8 +575,9 @@ local function FullMeleeCrit()
end
local gearCrit = GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("meleeCrit")
return baseCrit + agiCrit + gearCrit + talentCrit,
baseCrit, agiCrit, gearCrit, talentCrit
local tempCrit = GetTempEnchantCrit(16)
return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end
local function FullRangedCrit()
local _, class = UnitClass("player")
@@ -537,8 +590,9 @@ local function FullRangedCrit()
end
local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("rangedCrit")
return baseCrit + agiCrit + gearCrit + talentCrit,
baseCrit, agiCrit, gearCrit, talentCrit
local tempCrit = GetTempEnchantCrit(18)
return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end
local function FullSpellCrit()
local _, class = UnitClass("player")
@@ -659,6 +713,7 @@ CS.FullSpellHit = FullSpellHit
CS.GetTalentDetailsFor = GetTalentDetailsFor
CS.GetGearBonus = GetGearBonus
CS.GetItemBonusLib = GetItemBonusLib
CS.GetTempEnchantCrit = GetTempEnchantCrit
CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT
SFrames.CharacterPanel.CS = CS
@@ -2009,7 +2064,7 @@ function CP:BuildEquipmentPage()
local crit = CS.SafeGetMeleeCrit()
CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5)
else
local total, base, agiC, gearC, talC = CS.FullMeleeCrit()
local total, base, agiC, gearC, talC, tempC = CS.FullMeleeCrit()
CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
@@ -2021,6 +2076,9 @@ function CP:BuildEquipmentPage()
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end
end
if tempC and tempC > 0 then
CS.TipKV(" 临时附魔(磨刀石)", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
end
GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
CS.TipLine("Buff 暴击未计入)", 0.8,0.5,0.3)
@@ -2135,7 +2193,7 @@ function CP:BuildEquipmentPage()
if fromAPI then
CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5)
else
local total, base, agiC, gearC, talC = CS.FullRangedCrit()
local total, base, agiC, gearC, talC, tempC = CS.FullRangedCrit()
CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
@@ -2147,6 +2205,9 @@ function CP:BuildEquipmentPage()
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end
end
if tempC and tempC > 0 then
CS.TipKV(" 临时附魔(瞄准镜)", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
end
GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
CS.TipLine("Buff 暴击未计入)", 0.8,0.5,0.3)

611
Chat.lua
View File

@@ -18,6 +18,7 @@ local DEFAULTS = {
topPadding = 30,
bottomPadding = 8,
bgAlpha = 0.45,
hoverTransparent = true,
activeTab = 1,
editBoxPosition = "bottom",
editBoxX = 0,
@@ -739,27 +740,87 @@ local function GetTranslateFilterKeyForEvent(event)
return TRANSLATE_EVENT_FILTERS[event]
end
local function ParseHardcoreDeathMessage(text)
if type(text) ~= "string" or text == "" then return nil end
if not string.find(text, "硬核") and not string.find(text, "死亡") then
local lower = string.lower(text)
if not string.find(lower, "hc news") and not string.find(lower, "has fallen")
and not string.find(lower, "died") and not string.find(lower, "slain") then
return nil
-- ============================================================
-- HC 公会成员缓存:用于"仅通报工会成员"过滤
-- ============================================================
local HCGuildMemberCache = {}
local function RefreshHCGuildCache()
HCGuildMemberCache = {}
if not (IsInGuild and IsInGuild()) then return end
if not GetNumGuildMembers then return end
local total = GetNumGuildMembers()
for i = 1, total do
local name = GetGuildRosterInfo(i)
if type(name) == "string" and name ~= "" then
HCGuildMemberCache[name] = true
end
end
end
local function IsHCGuildMember(name)
return type(name) == "string" and HCGuildMemberCache[name] == true
end
-- 从 HC 系统消息中提取主角色名(死亡/升级均支持)
local function ParseHCCharacterName(text)
if type(text) ~= "string" then return nil end
-- 中文死亡格式: "硬核角色 NAME等级 N"
local _, _, n1 = string.find(text, "硬核角色%s+(.-)(等级")
if n1 and n1 ~= "" then return n1 end
-- 中文升级格式: "NAME 在硬核模式中已达到"
local _, _, n2 = string.find(text, "^(.-)%s+在硬核模式")
if n2 and n2 ~= "" then return n2 end
-- 英文死亡格式: "Hardcore character NAME (Level N)"
local _, _, n3 = string.find(text, "[Hh]ardcore character%s+(.-)%s+%(Level")
if n3 and n3 ~= "" then return n3 end
-- 英文升级格式: "NAME has reached level"
local _, _, n4 = string.find(text, "^(.-)%s+has reached level")
if n4 and n4 ~= "" then return n4 end
return nil
end
-- 检测HC等级里程碑消息如"达到20级"返回等级数字否则返回nil
local function ParseHardcoreLevelMessage(text)
if type(text) ~= "string" or text == "" then return nil end
local lower = string.lower(text)
-- 英文: "has reached level X" / "reached level X"
if string.find(lower, "reached level") then
local _, _, lvl = string.find(lower, "reached level%s+(%d+)")
return tonumber(lvl) or 1
end
-- 中文: "达到 X 级" / "已达到X级"
if string.find(text, "达到") then
local _, _, lvl = string.find(text, "达到%s*(%d+)%s*级")
if lvl then return tonumber(lvl) end
end
return nil
end
-- 检测HC死亡通报消息排除等级里程碑返回等级数字否则返回nil
local function ParseHardcoreDeathMessage(text)
if type(text) ~= "string" or text == "" then return nil end
-- 先排除等级里程碑消息,避免误判(里程碑消息也含"死亡"字样)
if ParseHardcoreLevelMessage(text) then return nil end
-- 初步过滤:必须含有死亡/击杀相关关键词
local lower = string.lower(text)
local hasHC = string.find(text, "硬核") or string.find(lower, "hardcore") or string.find(lower, "hc")
local hasDead = string.find(text, "死亡") or string.find(text, "击杀")
or string.find(lower, "has fallen") or string.find(lower, "slain")
or string.find(lower, "died") or string.find(lower, "hc news")
if not hasDead then return nil end
local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "")
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
-- 英文格式: Level: 17 / Level 17
local _, _, lvlStr = string.find(clean, "Level%s*:%s*(%d+)")
if lvlStr then return tonumber(lvlStr) end
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
local _, _, lvlStr2 = string.find(clean, "Level%s+(%d+)")
if lvlStr2 then return tonumber(lvlStr2) end
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
-- 中文格式: 等级 1 / 等级1 / (等级 1)
local _, _, lvlStr3 = string.find(clean, "等级%s*(%d+)")
if lvlStr3 then return tonumber(lvlStr3) end
local lower = string.lower(clean)
if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then
return 1
end
-- 兜底有死亡关键词即返回1
if hasDead then return 1 end
return nil
end
@@ -774,6 +835,68 @@ local function CleanTextForTranslation(text)
return clean
end
-- HC 死亡消息的句式精确解析:提取怪物名和地点名,翻译后原位替换,玩家名不动
-- 中文格式: "...硬核角色 [玩家](等级 N被 [怪物](等级 N击杀。这发生在 [地点]。..."
-- 英文格式: "...character [PLAYER] (Level N) has been slain by [MONSTER] (Level N)...in [ZONE]."
local function TranslateHCDeathParts(text, onDone)
local api = _G.STranslateAPI
local canTranslate = api and api.IsReady and api.IsReady() and api.ForceToChinese
-- 提取怪物名(中文句式)
local _, _, monster = string.find(text, ")被%s*(.-)(等级")
-- 提取地点名(中文句式)
local _, _, zone = string.find(text, "这发生在%s*(.-)。")
-- 英文句式兜底
if not monster then
local _, _, m = string.find(text, "slain by%s+(.-)%s+%(Level")
if m then monster = m end
end
if not zone then
local _, _, z = string.find(text, "This happened in%s+(.-)[%.%!]")
if z then zone = z end
end
-- 收集需要翻译的词(含英文字母才有翻译意义)
local targets = {}
if monster and string.find(monster, "[A-Za-z]") then
table.insert(targets, monster)
end
if zone and string.find(zone, "[A-Za-z]") and zone ~= monster then
table.insert(targets, zone)
end
if not canTranslate or table.getn(targets) == 0 then
onDone(text)
return
end
-- 并行翻译,全部完成后替换回原文
local out = text
local total = table.getn(targets)
local doneCount = 0
for i = 1, total do
local orig = targets[i]
api.ForceToChinese(orig, function(translated, err, meta)
if translated and translated ~= "" and translated ~= orig then
local esc = string.gsub(orig, "([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1")
local safeRepl = string.gsub(translated, "%%", "%%%%")
out = string.gsub(out, esc, safeRepl)
end
doneCount = doneCount + 1
if doneCount == total then
onDone(out)
end
end, "Nanami-UI-HC")
end
end
-- 等级里程碑消息:玩家名无需翻译(是玩家自选名),整条消息已由服务器本地化,直接转发
local function TranslateHCLevelParts(text, onDone)
-- 仅当存在非玩家名的英文才翻译;里程碑消息唯一英文就是玩家名,故直接原文转发
onDone(text)
end
local function ForceHide(object)
if not object then return end
object:Hide()
@@ -789,6 +912,66 @@ local function ForceInvisible(object)
if object.EnableMouse then object:EnableMouse(false) end
end
local function StripChatFrameArtwork(chatFrame)
if not chatFrame then return end
local frameID = chatFrame.GetID and chatFrame:GetID()
if frameID then
if SetChatWindowColor then
pcall(function() SetChatWindowColor(frameID, 0, 0, 0) end)
end
if SetChatWindowAlpha then
pcall(function() SetChatWindowAlpha(frameID, 0) end)
end
end
if FCF_SetWindowAlpha then
pcall(function() FCF_SetWindowAlpha(chatFrame, 0) end)
end
if chatFrame.SetBackdropColor then
chatFrame:SetBackdropColor(0, 0, 0, 0)
end
if chatFrame.SetBackdropBorderColor then
chatFrame:SetBackdropBorderColor(0, 0, 0, 0)
end
if chatFrame.SetBackdrop then
pcall(function() chatFrame:SetBackdrop(nil) end)
end
local frameName = chatFrame.GetName and chatFrame:GetName()
if type(frameName) == "string" and frameName ~= "" then
local legacyTextures = {
frameName .. "Background",
frameName .. "BackgroundLeft",
frameName .. "BackgroundMiddle",
frameName .. "BackgroundRight",
frameName .. "BottomButton",
frameName .. "ButtonFrame",
frameName .. "ButtonFrameBackground",
}
for _, texName in ipairs(legacyTextures) do
local tex = _G[texName]
if tex then
if tex.SetTexture then tex:SetTexture(nil) end
if tex.SetVertexColor then tex:SetVertexColor(0, 0, 0, 0) end
if tex.SetAlpha then tex:SetAlpha(0) end
tex:Hide()
end
end
end
local regions = { chatFrame:GetRegions() }
for _, region in ipairs(regions) do
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
if region.SetTexture then region:SetTexture(nil) end
if region.SetVertexColor then region:SetVertexColor(0, 0, 0, 0) end
if region.SetAlpha then region:SetAlpha(0) end
region:Hide()
end
end
end
local function CreateFont(parent, size, justify)
if SFrames and SFrames.CreateFontString then
return SFrames:CreateFontString(parent, size, justify)
@@ -1547,6 +1730,9 @@ local function EnsureDB()
if type(db.editBoxY) ~= "number" then db.editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY end
if db.translateEnabled == nil then db.translateEnabled = true end
if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = true end
if db.hcDeathToGuild == nil then db.hcDeathToGuild = true end
if db.hcLevelToGuild == nil then db.hcLevelToGuild = true end
if db.hcGuildMemberOnly == nil then db.hcGuildMemberOnly = false end
if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
if db.layoutVersion < 2 then
db.topPadding = DEFAULTS.topPadding
@@ -1895,6 +2081,7 @@ function SFrames.Chat:GetConfig()
topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
bgAlpha = Clamp(db.bgAlpha, 0, 1),
hoverTransparent = (db.hoverTransparent ~= false),
editBoxPosition = editBoxPosition,
editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
@@ -3321,6 +3508,19 @@ function SFrames.Chat:RefreshConfigFrame()
end
end
if self.cfgMonitorSection then
self.cfgMonitorSection:SetAlpha(1)
end
if self.cfgMonitorCb then
self.cfgMonitorCb:Enable()
end
if self.cfgMonitorDesc then
self.cfgMonitorDesc:SetTextColor(0.7, 0.7, 0.74)
end
if self.cfgMonitorReloadHint then
self.cfgMonitorReloadHint:SetTextColor(0.9, 0.75, 0.5)
end
if self.configControls then
for i = 1, table.getn(self.configControls) do
local ctrl = self.configControls[i]
@@ -3485,12 +3685,12 @@ function SFrames.Chat:EnsureConfigFrame()
transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50)
transDesc:SetWidth(520)
transDesc:SetJustifyH("LEFT")
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。")
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。聊天监控可独立启用。")
transDesc:SetTextColor(0.7, 0.7, 0.74)
local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath)
AddControl(CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
local monitorCb = CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
function() return EnsureDB().chatMonitorEnabled ~= false end,
function(checked)
EnsureDB().chatMonitorEnabled = (checked == true)
@@ -3498,15 +3698,19 @@ function SFrames.Chat:EnsureConfigFrame()
function()
SFrames.Chat:RefreshConfigFrame()
end
))
)
AddControl(monitorCb)
self.cfgMonitorCb = monitorCb
self.cfgMonitorSection = monitorSection
local monDesc = monitorSection:CreateFontString(nil, "OVERLAY")
monDesc:SetFont(fontPath, 10, "OUTLINE")
monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50)
monDesc:SetWidth(520)
monDesc:SetJustifyH("LEFT")
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用")
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、职业染色等功能。\n可独立于 AI 翻译开关使用。关闭后消息将原样通过,[+] 复制等功能不可用。")
monDesc:SetTextColor(0.7, 0.7, 0.74)
self.cfgMonitorDesc = monDesc
local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY")
reloadHint:SetFont(fontPath, 10, "OUTLINE")
@@ -3515,11 +3719,12 @@ function SFrames.Chat:EnsureConfigFrame()
reloadHint:SetJustifyH("LEFT")
reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。")
reloadHint:SetTextColor(0.9, 0.75, 0.5)
self.cfgMonitorReloadHint = reloadHint
end
local windowPage = CreatePage("window")
do
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 304, fontPath)
AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
function() return EnsureDB().width end,
function(v) EnsureDB().width = v end,
@@ -3580,13 +3785,18 @@ function SFrames.Chat:EnsureConfigFrame()
function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(appearance, "悬停显示背景", 16, -248,
function() return EnsureDB().hoverTransparent ~= false end,
function(checked) EnsureDB().hoverTransparent = (checked == true) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY")
self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8)
local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath)
local inputSection = CreateCfgSection(windowPage, "输入框", 0, -320, 584, 114, fontPath)
self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY")
self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
@@ -3618,7 +3828,7 @@ function SFrames.Chat:EnsureConfigFrame()
inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。")
inputTip:SetTextColor(0.74, 0.74, 0.8)
local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath)
local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -428, 584, 96, fontPath)
CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
SFrames.Chat:ResetPosition()
SFrames.Chat:RefreshConfigFrame()
@@ -4050,7 +4260,7 @@ function SFrames.Chat:EnsureConfigFrame()
local hcPage = CreatePage("hc")
do
local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath)
local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 382, fontPath)
local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
hcStatusText:SetFont(fontPath, 10, "OUTLINE")
@@ -4095,7 +4305,7 @@ function SFrames.Chat:EnsureConfigFrame()
deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
deathTip:SetWidth(540)
deathTip:SetJustifyH("LEFT")
deathTip:SetText("关闭那些某某在XX级死亡的系统提示。")
deathTip:SetText("关闭那些[某某在XX级死亡]的系统提示。")
deathTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1,
@@ -4104,6 +4314,48 @@ function SFrames.Chat:EnsureConfigFrame()
function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
function() SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(hcControls, "翻译死亡通报并转发到公会频道", 16, -138,
function() return EnsureDB().hcDeathToGuild ~= false end,
function(checked) EnsureDB().hcDeathToGuild = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
local guildTip = hcControls:CreateFontString(nil, "OVERLAY")
guildTip:SetFont(fontPath, 10, "OUTLINE")
guildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -164)
guildTip:SetWidth(540)
guildTip:SetJustifyH("LEFT")
guildTip:SetText("开启 AI 翻译时,将死亡黄字系统消息翻译后自动发送到公会频道。需 AI 翻译已启用。")
guildTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgCheck(hcControls, "翻译等级里程碑并转发到公会频道", 16, -204,
function() return EnsureDB().hcLevelToGuild ~= false end,
function(checked) EnsureDB().hcLevelToGuild = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
local levelGuildTip = hcControls:CreateFontString(nil, "OVERLAY")
levelGuildTip:SetFont(fontPath, 10, "OUTLINE")
levelGuildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -228)
levelGuildTip:SetWidth(540)
levelGuildTip:SetJustifyH("LEFT")
levelGuildTip:SetText("开启 AI 翻译时,将[达到X级]里程碑系统消息翻译后转发到公会频道。与死亡通报独立控制。")
levelGuildTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgCheck(hcControls, "仅通报工会成员的死亡/升级消息", 16, -270,
function() return EnsureDB().hcGuildMemberOnly == true end,
function(checked) EnsureDB().hcGuildMemberOnly = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
local guildMemberTip = hcControls:CreateFontString(nil, "OVERLAY")
guildMemberTip:SetFont(fontPath, 10, "OUTLINE")
guildMemberTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -296)
guildMemberTip:SetWidth(540)
guildMemberTip:SetJustifyH("LEFT")
guildMemberTip:SetText("勾选后,仅当死亡/升级角色为本工会成员时才转发到公会频道。默认关闭(通报所有人)。")
guildMemberTip:SetTextColor(0.8, 0.7, 0.7)
end
local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function()
@@ -4399,6 +4651,7 @@ function SFrames.Chat:CreateContainer()
})
chatShadow:SetBackdropColor(0, 0, 0, 0.55)
chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
f.chatShadow = chatShadow
local topGlow = f:CreateTexture(nil, "BACKGROUND")
topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
@@ -4674,6 +4927,9 @@ function SFrames.Chat:CreateContainer()
scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
scrollTrack:SetWidth(4)
scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9)
f.scrollUpBtn = scrollUpBtn
f.scrollDownBtn = scrollDownBtn
f.scrollTrack = scrollTrack
local resize = CreateFrame("Button", nil, f)
resize:SetWidth(16)
@@ -4709,6 +4965,67 @@ function SFrames.Chat:CreateContainer()
f.resizeHandle = resize
self.frame = f
-- ── Hover-transparent: fade background/chrome when mouse is not over chat ──
f.sfHoverAlpha = 0
f.sfHoverTarget = 0
local FADE_SPEED = 4.0 -- alpha per second
-- Apply initial transparent state immediately after first config apply
f.sfHoverInitPending = true
local function IsMouseOverChat()
if MouseIsOver(f) then return true end
if SFrames.Chat.editBackdrop and SFrames.Chat.editBackdrop:IsShown() and MouseIsOver(SFrames.Chat.editBackdrop) then return true end
if SFrames.Chat.configFrame and SFrames.Chat.configFrame:IsShown() then return true end
local editBox = ChatFrameEditBox or ChatFrame1EditBox
if editBox and editBox:IsShown() then return true end
return false
end
local hoverFrame = CreateFrame("Frame", nil, f)
hoverFrame:SetScript("OnUpdate", function()
if not (SFrames and SFrames.Chat and SFrames.Chat.frame) then return end
local cfg = SFrames.Chat:GetConfig()
-- On first tick, snap to correct state (no animation)
if f.sfHoverInitPending then
f.sfHoverInitPending = nil
if cfg.hoverTransparent then
local over = IsMouseOverChat() and 1 or 0
f.sfHoverAlpha = over
f.sfHoverTarget = over
SFrames.Chat:ApplyHoverAlpha(over)
else
f.sfHoverAlpha = 1
f.sfHoverTarget = 1
end
return
end
if not cfg.hoverTransparent then
if f.sfHoverAlpha ~= 1 then
f.sfHoverAlpha = 1
SFrames.Chat:ApplyHoverAlpha(1)
end
return
end
local target = IsMouseOverChat() and 1 or 0
f.sfHoverTarget = target
local cur = f.sfHoverAlpha or 1
if math.abs(cur - target) < 0.01 then
if cur ~= target then
f.sfHoverAlpha = target
SFrames.Chat:ApplyHoverAlpha(target)
end
return
end
local dt = arg1 or 0.016
if target > cur then
cur = math.min(cur + FADE_SPEED * dt, target)
else
cur = math.max(cur - FADE_SPEED * dt, target)
end
f.sfHoverAlpha = cur
SFrames.Chat:ApplyHoverAlpha(cur)
end)
if not self.hiddenConfigButton then
local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
hiddenConfigButton:SetWidth(74)
@@ -4749,9 +5066,45 @@ function SFrames.Chat:CreateContainer()
f:SetWidth(Clamp(db.width, 320, 900))
f:SetHeight(Clamp(db.height, 120, 460))
-- Background alpha: always show at configured bgAlpha
-- Background alpha: respect hoverTransparent on init
local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
if db.hoverTransparent ~= false then
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0)
f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0)
if f.chatShadow then
f.chatShadow:SetBackdropColor(0, 0, 0, 0)
f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0)
end
if f.innerShade then
f.innerShade:SetVertexColor(0, 0, 0, 0)
f.innerShade:Hide()
end
if f.watermark then
f.watermark:SetVertexColor(1, 0.78, 0.9, 0)
f.watermark:Hide()
end
if f.title and f.title.SetAlpha then f.title:SetAlpha(0)
elseif f.title and f.title.SetTextColor then f.title:SetTextColor(1, 0.82, 0.93, 0) end
if f.title then f.title:Hide() end
if f.titleBtn then
f.titleBtn:EnableMouse(false)
f.titleBtn:Hide()
end
if f.leftCat then
f.leftCat:SetVertexColor(1, 0.82, 0.9, 0)
f.leftCat:Hide()
end
if f.tabBar then f.tabBar:SetAlpha(0) f.tabBar:Hide() end
if f.configButton then f.configButton:SetAlpha(0) f.configButton:EnableMouse(false) f.configButton:Hide() end
if f.whisperButton then f.whisperButton:SetAlpha(0) f.whisperButton:EnableMouse(false) f.whisperButton:Hide() end
if f.scrollUpBtn then f.scrollUpBtn:SetAlpha(0) f.scrollUpBtn:EnableMouse(false) f.scrollUpBtn:Hide() end
if f.scrollDownBtn then f.scrollDownBtn:SetAlpha(0) f.scrollDownBtn:EnableMouse(false) f.scrollDownBtn:Hide() end
if f.scrollTrack then f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0) f.scrollTrack:Hide() end
if f.resizeHandle then f.resizeHandle:SetAlpha(0) f.resizeHandle:EnableMouse(false) f.resizeHandle:Hide() end
if f.hint then f.hint:Hide() end
else
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
end
end
function SFrames.Chat:HideDefaultChrome()
@@ -5225,8 +5578,15 @@ function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
end
if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(0, 0) end
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0) end
StripChatFrameArtwork(chatFrame)
if not chatFrame.sfArtworkHooked and chatFrame.HookScript then
chatFrame.sfArtworkHooked = true
chatFrame:HookScript("OnShow", function()
StripChatFrameArtwork(chatFrame)
end)
end
self:EnforceChatWindowLock(chatFrame)
if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
@@ -6324,6 +6684,122 @@ function SFrames.Chat:StyleEditBox()
end
end
-- Apply hover-transparent alpha to background, shadow, and chrome elements.
-- alpha=0 means fully transparent (mouse away), alpha=1 means fully visible (mouse over).
function SFrames.Chat:ApplyHoverAlpha(alpha)
if not self.frame then return end
local f = self.frame
local cfg = self:GetConfig()
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
local chromeVisible = alpha > 0.01
-- Main backdrop
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * alpha)
local showBorder = (cfg.showBorder ~= false)
local borderR, borderG, borderB = self:GetBorderColorRGB()
if showBorder then
f:SetBackdropBorderColor(borderR, borderG, borderB, 0.95 * alpha)
else
f:SetBackdropBorderColor(borderR, borderG, borderB, 0)
end
-- Shadow
if f.chatShadow then
f.chatShadow:SetBackdropColor(0, 0, 0, 0.55 * alpha)
f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4 * alpha)
end
-- Inner shade
if f.innerShade then
f.innerShade:SetVertexColor(0, 0, 0, 0.2 * alpha)
if chromeVisible then f.innerShade:Show() else f.innerShade:Hide() end
end
-- Watermark
if f.watermark then
f.watermark:SetVertexColor(1, 0.78, 0.9, 0.08 * alpha)
if chromeVisible then f.watermark:Show() else f.watermark:Hide() end
end
-- Title, tab bar, config button, whisper button, scroll buttons, resize handle
local chromeAlpha = alpha
if f.title and f.title.SetAlpha then
f.title:SetAlpha(chromeAlpha)
if chromeVisible then f.title:Show() else f.title:Hide() end
elseif f.title and f.title.SetTextColor then
f.title:SetTextColor(1, 0.82, 0.93, chromeAlpha)
if chromeVisible then f.title:Show() else f.title:Hide() end
end
if f.titleBtn then
f.titleBtn:EnableMouse(chromeVisible)
if chromeVisible then f.titleBtn:Show() else f.titleBtn:Hide() end
end
if f.leftCat then
f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8 * chromeAlpha)
if chromeVisible then f.leftCat:Show() else f.leftCat:Hide() end
end
if f.tabBar then
f.tabBar:SetAlpha(chromeAlpha)
if chromeVisible then f.tabBar:Show() else f.tabBar:Hide() end
end
if f.configButton then
f.configButton:SetAlpha(chromeAlpha)
f.configButton:EnableMouse(chromeVisible)
if chromeVisible then f.configButton:Show() else f.configButton:Hide() end
end
if f.whisperButton then
f.whisperButton:SetAlpha(chromeAlpha)
f.whisperButton:EnableMouse(chromeVisible)
if chromeVisible then f.whisperButton:Show() else f.whisperButton:Hide() end
end
if f.scrollUpBtn then
f.scrollUpBtn:SetAlpha(chromeAlpha)
f.scrollUpBtn:EnableMouse(chromeVisible)
if chromeVisible then f.scrollUpBtn:Show() else f.scrollUpBtn:Hide() end
end
if f.scrollDownBtn then
f.scrollDownBtn:SetAlpha(chromeAlpha)
f.scrollDownBtn:EnableMouse(chromeVisible)
if chromeVisible then f.scrollDownBtn:Show() else f.scrollDownBtn:Hide() end
end
if f.scrollTrack then
f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9 * chromeAlpha)
if chromeVisible then f.scrollTrack:Show() else f.scrollTrack:Hide() end
end
if f.resizeHandle then
f.resizeHandle:SetAlpha(chromeAlpha)
f.resizeHandle:EnableMouse(chromeVisible)
if chromeVisible then f.resizeHandle:Show() else f.resizeHandle:Hide() end
end
if f.hint then
if chromeVisible then f.hint:Show() else f.hint:Hide() end
end
-- Edit backdrop (only backdrop colors, not child alpha — preserve editbox text visibility)
if self.editBackdrop then
local editBox = ChatFrameEditBox or ChatFrame1EditBox
local editBackdropVisible = chromeVisible and editBox and editBox:IsShown()
if self.editBackdrop.SetBackdropColor then
self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96 * chromeAlpha)
end
if self.editBackdrop.SetBackdropBorderColor then
self.editBackdrop:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98 * chromeAlpha)
end
if self.editBackdrop.catIcon then
self.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9 * chromeAlpha)
end
if self.editBackdrop.topLine then
self.editBackdrop.topLine:SetVertexColor(1, 0.76, 0.9, 0.85 * chromeAlpha)
end
self.editBackdrop:EnableMouse(editBackdropVisible)
if editBackdropVisible then
self.editBackdrop:Show()
else
self.editBackdrop:Hide()
end
end
end
function SFrames.Chat:ApplyFrameBorderStyle()
if not self.frame then return end
@@ -6413,7 +6889,12 @@ function SFrames.Chat:ApplyConfig()
end
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
local hAlpha = (self.frame.sfHoverAlpha ~= nil) and self.frame.sfHoverAlpha or 1
if cfg.hoverTransparent then
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * hAlpha)
else
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
end
self:AttachChatFrame()
self:HideDefaultChrome()
@@ -6427,6 +6908,11 @@ function SFrames.Chat:ApplyConfig()
self:StartStabilizer()
self:SetUnlocked(SFrames and SFrames.isUnlocked)
self:RefreshConfigFrame()
-- Re-apply hover alpha so ApplyConfig doesn't leave chrome visible
if cfg.hoverTransparent and self.frame.sfHoverAlpha ~= nil then
self:ApplyHoverAlpha(self.frame.sfHoverAlpha)
end
end
function SFrames.Chat:Initialize()
@@ -6524,6 +7010,61 @@ function SFrames.Chat:Initialize()
end)
end
-- HC死亡系统消息AI开启时翻译并转发到公会
if not SFrames.Chat._hcDeathGuildHooked then
SFrames.Chat._hcDeathGuildHooked = true
local hcDeathEvFrame = CreateFrame("Frame", "SFramesChatHCDeathGuildEvents", UIParent)
hcDeathEvFrame:RegisterEvent("CHAT_MSG_SYSTEM")
hcDeathEvFrame:SetScript("OnEvent", function()
if not (SFrames and SFrames.Chat) then return end
local db = EnsureDB()
-- AI翻译必须开启
if db.translateEnabled == false then return end
-- 必须在公会中才能发送
if not (IsInGuild and IsInGuild()) then return end
local messageText = arg1
if type(messageText) ~= "string" or messageText == "" then return end
local isDeath = ParseHardcoreDeathMessage(messageText) ~= nil
local isLevel = (not isDeath) and (ParseHardcoreLevelMessage(messageText) ~= nil)
if not isDeath and not isLevel then return end
-- 仅通报工会成员:提取角色名并检查缓存
if db.hcGuildMemberOnly then
local charName = ParseHCCharacterName(messageText)
if not charName or not IsHCGuildMember(charName) then return end
end
-- 死亡通报:检查 hcDeathToGuild 及等级过滤
if isDeath then
if db.hcDeathToGuild == false then return end
if db.hcDeathDisable then return end
local deathLvl = ParseHardcoreDeathMessage(messageText)
if db.hcDeathLevelMin and deathLvl and deathLvl > 0 and deathLvl < db.hcDeathLevelMin then return end
end
-- 等级里程碑:检查 hcLevelToGuild
if isLevel then
if db.hcLevelToGuild == false then return end
end
local cleanText = CleanTextForTranslation(messageText)
if cleanText == "" then return end
local function doSend(finalText)
pcall(function() SendChatMessage("[HC] " .. finalText, "GUILD") end)
end
if isDeath then
TranslateHCDeathParts(cleanText, doSend)
else
TranslateHCLevelParts(cleanText, doSend)
end
end)
end
SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function()
RefreshHCGuildCache()
end)
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
if SFrames and SFrames.Chat then
SFrames.Chat:ApplyConfig()
@@ -6532,7 +7073,7 @@ function SFrames.Chat:Initialize()
end
LoadPersistentClassCache()
if IsInGuild and IsInGuild() and GuildRoster then
GuildRoster()
GuildRoster() -- 触发 GUILD_ROSTER_UPDATE → RefreshHCGuildCache
end
SFrames:RefreshClassColorCache()
@@ -6714,6 +7255,17 @@ function SFrames.Chat:Initialize()
do
local db = EnsureDB()
-- HC系统消息死亡/里程碑黄字)特殊处理:
-- AI翻译未开启时直接透传不加 [+] 注释;
-- AI开启时正常记录翻译转发由独立的 CHAT_MSG_SYSTEM 事件处理器完成。
if ParseHardcoreDeathMessage(text) or ParseHardcoreLevelMessage(text) then
if db.translateEnabled == false then
origAddMessage(self, text, r, g, b, alpha, holdTime)
return
end
end
local chanName = GetChannelNameFromChatLine(text)
if chanName and IsIgnoredChannelByDefault(chanName) then
@@ -7137,4 +7689,3 @@ end
end
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor)

View File

@@ -1,347 +0,0 @@
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 = {
[2] = {"潜行"},
[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] = "史诗坐骑任务:召唤战驹",
},
}

File diff suppressed because it is too large Load Diff

198
ConsumableDB.lua Normal file
View File

@@ -0,0 +1,198 @@
--------------------------------------------------------------------------------
-- Nanami-UI: ConsumableDB.lua
-- 食物药剂百科数据库(由导出表生成,请优先更新 Excel 后再重新生成)
-- Source: c:\Users\rucky\Downloads\WoW_Consumables_Database.xlsx
-- Stats : 6 roles / 9 categories / 101 entries
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.ConsumableDB = {
generatedAt = "2026-04-03 23:45:41",
summary = {
roleCount = 6,
categoryCount = 9,
itemCount = 101,
},
roleOrder = {
"坦克 (物理坦克)",
"坦克 (法系坦克)",
"法系输出",
"物理近战",
"物理远程",
"治疗",
},
categoryOrder = {
"合剂",
"药剂",
"攻强",
"诅咒之地buff",
"赞达拉",
"武器",
"食物",
"",
"药水",
},
groups = {
-- 1. 坦克 · 物理坦克
{
key = "tank_physical",
role = "坦克 (物理坦克)",
detail = "坦克 · 物理坦克",
color = { 0.40, 0.70, 1.00 },
items = {
{ cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
{ cat="药剂", name="坚韧药剂", effect="120生命上限", duration="1小时", id=0 },
{ cat="药剂", name="极效巨魔之血药水", effect="20生命/5秒回血", duration="1小时", id=0 },
{ cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
{ cat="药剂", name="巨人药剂", effect="25力量", duration="1小时", id=9206 },
{ cat="药剂", name="坚甲药剂", effect="450护甲", duration="1小时", id=0 },
{ cat="攻强", name="魂能之击", effect="30力量", duration="10分钟", id=0 },
{ cat="攻强", name="冬泉火酒", effect="35攻强", duration="20分钟", id=0 },
{ cat="攻强", name="魂能之力", effect="40攻强", duration="10分钟", id=0 },
{ cat="诅咒之地buff", name="肺片鸡尾酒", effect="50耐力", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="土狼肉块", effect="40力量", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="元素磨刀石", effect="2%暴击", duration="30分钟", id=0 },
{ cat="武器", name="致密磨刀石", effect="8伤害", duration="30分钟", id=0 },
{ cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
{ cat="食物", name="嫩狼肉排", effect="12耐力 / 12精神", duration="15分钟", id=0 },
{ cat="食物", name="蜘蛛肉肠", effect="12耐力 / 12精神", duration="15分钟", id=0 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
},
},
-- 2. 坦克 · 法系坦克
{
key = "tank_caster",
role = "坦克 (法系坦克)",
detail = "坦克 · 法系坦克",
color = { 1.00, 0.80, 0.20 },
items = {
{ cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
{ cat="药剂", name="坚韧药剂", effect="120生命上限", duration="1小时", id=0 },
{ cat="药剂", name="特效巨魔之血药水", effect="20生命/5秒回血", duration="1小时", id=0 },
{ cat="药剂", name="强效奥法药剂", effect="35法术伤害", duration="1小时", id=0 },
{ cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
{ cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
{ cat="药剂", name="坚甲药剂", effect="450护甲", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="肺片鸡尾酒", effect="50耐力", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="巫师之油", effect="24法术伤害", duration="30分钟", id=0 },
{ cat="武器", name="卓越巫师之油", effect="36法伤 / 1%暴击", duration="30分钟", id=0 },
{ cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
{ cat="食物", name="嫩狼肉排", effect="12耐力 / 12精神", duration="15分钟", id=0 },
{ cat="食物", name="龙息红椒", effect="攻击几率喷火", duration="10分钟", id=0 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
{ cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
{ cat="药水", name="有限无敌药水", effect="物理攻击免疫", duration="6秒", id=0 },
},
},
-- 3. 输出 · 法系输出
{
key = "caster_dps",
role = "法系输出",
detail = "输出 · 法系输出",
color = { 0.65, 0.45, 1.00 },
items = {
{ cat="合剂", name="超级能量合剂", effect="150法术伤害", duration="2小时", id=0 },
{ cat="药剂", name="强效奥法药剂", effect="35法术伤害", duration="1小时", id=0 },
{ cat="药剂", name="强效火力药剂", effect="40火焰法术伤害", duration="1小时", id=0 },
{ cat="药剂", name="暗影之力药剂", effect="40暗影法术伤害", duration="1小时", id=0 },
{ cat="药剂", name="冰霜之力药剂", effect="15冰霜法术伤害", duration="1小时", id=0 },
{ cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
{ cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="卓越巫师之油", effect="36法伤 / 1%暴击", duration="30分钟", id=0 },
{ cat="武器", name="巫师之油", effect="24法术伤害", duration="30分钟", id=0 },
{ cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
{ cat="食物", name="黑口鱼起司", effect="10法力/5秒", duration="10分钟", id=0 },
{ cat="食物", name="夜鳞鱼汤", effect="8法力/5秒", duration="10分钟", id=13931 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
},
},
-- 4. 输出 · 物理近战
{
key = "melee_dps",
role = "物理近战",
detail = "输出 · 物理近战",
color = { 1.00, 0.55, 0.25 },
items = {
{ cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
{ cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
{ cat="药剂", name="巨人药剂", effect="25力量", duration="1小时", id=9206 },
{ cat="攻强", name="魂能之击", effect="30力量", duration="10分钟", id=0 },
{ cat="攻强", name="冬泉火酒", effect="35攻强", duration="20分钟", id=0 },
{ cat="攻强", name="魂能之力", effect="40攻强", duration="10分钟", id=0 },
{ cat="诅咒之地buff", name="蝎粉", effect="25敏捷", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="土狼肉块", effect="40力量", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="元素磨刀石", effect="2%暴击", duration="30分钟", id=0 },
{ cat="武器", name="致密磨刀石", effect="8伤害", duration="30分钟", id=0 },
{ cat="食物", name="烤鱿鱼", effect="10敏捷", duration="10分钟", id=13928 },
{ cat="食物", name="沙漠肉丸子", effect="20力量", duration="15分钟", id=0 },
{ cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
{ cat="药水", name="自由行动药水", effect="免疫昏迷及移动限制", duration="30秒", id=5634 },
},
},
-- 5. 输出 · 物理远程
{
key = "ranged_dps",
role = "物理远程",
detail = "输出 · 物理远程",
color = { 0.55, 0.88, 0.42 },
items = {
{ cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
{ cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
{ cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
{ cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="蝎粉", effect="25敏捷", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="卓越法力之油", effect="14法力/5秒", duration="30分钟", id=0 },
{ cat="食物", name="烤鱿鱼", effect="10敏捷", duration="10分钟", id=13928 },
{ cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
},
},
-- 6. 治疗
{
key = "healer",
role = "治疗",
detail = "治疗",
color = { 0.42, 1.00, 0.72 },
items = {
{ cat="合剂", name="精炼智慧合剂", effect="2000法力上限", duration="2小时", id=13511 },
{ cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
{ cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
{ cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
{ cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
{ cat="武器", name="卓越法力之油", effect="14法力/5秒 / 提升治疗效果", duration="30分钟", id=0 },
{ cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
{ cat="食物", name="夜鳞鱼汤", effect="8法力/5秒", duration="10分钟", id=13931 },
{ cat="", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
{ cat="", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
{ cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
{ cat="药水", name="黑暗符文", effect="回复900-1500法力", duration="瞬发", id=0 },
{ cat="药水", name="恶魔符文", effect="回复900-1500法力", duration="瞬发", id=0 },
},
},
},
}

778
ConsumableUI.lua Normal file
View File

@@ -0,0 +1,778 @@
--------------------------------------------------------------------------------
-- Nanami-UI: ConsumableUI.lua
-- 食物药剂百科窗口
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.ConsumableUI = SFrames.ConsumableUI or {}
local CUI = SFrames.ConsumableUI
local FRAME_W = 600
local FRAME_H = 500
local HEADER_H = 34
local FILTER_H = 28
local ROLE_H = 28
local COL_H = 22
local ROW_H = 20
local MAX_ROWS = 18
local SIDE_PAD = 10
local COL_CAT_W = 86
local COL_NAME_W = 168
local COL_EFF_W = 232
local COL_DUR_W = 76
local S = {
frame = nil,
searchBox = nil,
searchText = "",
activeRole = "全部",
rows = {},
tabBtns = {},
displayList = {},
filteredCount = 0,
scrollOffset = 0,
scrollMax = 0,
summaryFS = nil,
emptyFS = nil,
scrollBar = nil,
}
local function GetTheme()
local base = {
panelBg = { 0.07, 0.07, 0.10, 0.96 },
panelBorder = { 0.35, 0.30, 0.40, 1.00 },
headerBg = { 0.10, 0.10, 0.14, 1.00 },
text = { 0.92, 0.88, 0.95 },
dimText = { 0.55, 0.50, 0.58 },
gold = { 1.00, 0.82, 0.40 },
slotBg = { 0.10, 0.10, 0.14, 0.90 },
slotBorder = { 0.30, 0.28, 0.35, 0.80 },
slotSelected = { 0.80, 0.60, 1.00 },
divider = { 0.28, 0.26, 0.32, 0.80 },
rowAlt = { 0.11, 0.10, 0.15, 0.60 },
green = { 0.40, 0.90, 0.50 },
}
if SFrames and SFrames.ActiveTheme then
for key, value in pairs(SFrames.ActiveTheme) do
base[key] = value
end
end
return base
end
local function GetFont()
return SFrames and SFrames.GetFont and SFrames:GetFont()
or "Fonts\\ARIALN.TTF"
end
local function ApplyBackdrop(frame, bg, border)
local theme = GetTheme()
local backdropBg = bg or theme.panelBg
local backdropBorder = border or theme.panelBorder
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(backdropBg[1], backdropBg[2], backdropBg[3], backdropBg[4] or 0.96)
frame:SetBackdropBorderColor(
backdropBorder[1],
backdropBorder[2],
backdropBorder[3],
backdropBorder[4] or 1.00
)
end
local function SetDivider(parent, y, rightPadding)
local theme = GetTheme()
local divider = parent:CreateTexture(nil, "ARTWORK")
divider:SetTexture("Interface\\Buttons\\WHITE8X8")
divider:SetHeight(1)
divider:SetPoint("TOPLEFT", parent, "TOPLEFT", SIDE_PAD, y)
divider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -(SIDE_PAD + (rightPadding or 0)), y)
divider:SetVertexColor(theme.divider[1], theme.divider[2], theme.divider[3], theme.divider[4] or 0.8)
end
local function Utf8DisplayWidth(value)
local width = 0
local index = 1
local len = string.len(value or "")
while index <= len do
local byte = string.byte(value, index)
if byte < 128 then
width = width + 7
index = index + 1
elseif byte < 224 then
width = width + 7
index = index + 2
elseif byte < 240 then
width = width + 11
index = index + 3
else
width = width + 11
index = index + 4
end
end
return width
end
local function GetDatabase()
local db = SFrames and SFrames.ConsumableDB
if not db then
return nil
end
if db.groups then
return db
end
return {
groups = db,
roleOrder = nil,
categoryOrder = nil,
summary = nil,
generatedAt = nil,
}
end
local function GetGroups()
local db = GetDatabase()
return db and db.groups or nil
end
local function CountAllItems(groups)
local count = 0
for _, group in ipairs(groups or {}) do
count = count + table.getn(group.items or {})
end
return count
end
local function CountAllCategories(groups)
local seen = {}
local count = 0
for _, group in ipairs(groups or {}) do
for _, item in ipairs(group.items or {}) do
if item.cat and item.cat ~= "" and not seen[item.cat] then
seen[item.cat] = true
count = count + 1
end
end
end
return count
end
local function BuildRoleList()
local db = GetDatabase()
local roles = { "全部" }
local seen = { ["全部"] = true }
if not db or not db.groups then
return roles
end
if db.roleOrder then
for _, role in ipairs(db.roleOrder) do
if role and role ~= "" and not seen[role] then
seen[role] = true
table.insert(roles, role)
end
end
end
for _, group in ipairs(db.groups) do
if group.role and group.role ~= "" and not seen[group.role] then
seen[group.role] = true
table.insert(roles, group.role)
end
end
return roles
end
local function RoleMatches(group)
if S.activeRole == "全部" then
return true
end
return group.role == S.activeRole
end
local function ItemMatches(item)
if S.searchText == "" then
return true
end
local query = string.lower(S.searchText)
return string.find(string.lower(item.name or ""), query, 1, true)
or string.find(string.lower(item.effect or ""), query, 1, true)
or string.find(string.lower(item.cat or ""), query, 1, true)
end
function CUI:RefreshSummary()
if not S.summaryFS then
return
end
local db = GetDatabase()
local groups = db and db.groups or {}
local summary = db and db.summary or nil
local roleCount = (summary and summary.roleCount) or table.getn(groups)
local categoryCount = (summary and summary.categoryCount) or CountAllCategories(groups)
local totalCount = (summary and summary.itemCount) or CountAllItems(groups)
local text = string.format("%d 定位 / %d 类别 / %d 条", roleCount, categoryCount, totalCount)
if S.activeRole ~= "全部" or S.searchText ~= "" then
text = string.format("当前筛出 %d 条,数据库共 %d 条", S.filteredCount or 0, totalCount)
end
if db and db.generatedAt then
text = text .. " · 更新于 " .. db.generatedAt
end
S.summaryFS:SetText(text)
end
function CUI:Filter()
S.displayList = {}
S.filteredCount = 0
local groups = GetGroups()
if not groups then
S.scrollOffset = 0
S.scrollMax = 0
self:RefreshSummary()
return
end
for _, group in ipairs(groups) do
if RoleMatches(group) then
local matched = {}
for _, item in ipairs(group.items or {}) do
if ItemMatches(item) then
table.insert(matched, item)
end
end
if table.getn(matched) > 0 then
table.insert(S.displayList, {
isHeader = true,
group = group,
count = table.getn(matched),
})
for _, item in ipairs(matched) do
table.insert(S.displayList, {
isHeader = false,
item = item,
})
end
S.filteredCount = S.filteredCount + table.getn(matched)
end
end
end
S.scrollOffset = 0
S.scrollMax = math.max(0, table.getn(S.displayList) - MAX_ROWS)
self:RefreshSummary()
end
local function UpdateScrollbar()
local scrollBar = S.scrollBar
if not scrollBar then
return
end
local total = table.getn(S.displayList)
if total <= MAX_ROWS then
scrollBar:Hide()
return
end
scrollBar:Show()
local trackHeight = scrollBar:GetHeight()
local thumbHeight = math.max(20, math.floor(trackHeight * MAX_ROWS / total + 0.5))
local percent = 0
if S.scrollMax > 0 then
percent = S.scrollOffset / S.scrollMax
end
local thumbY = math.floor((trackHeight - thumbHeight) * percent + 0.5)
if scrollBar.thumb then
scrollBar.thumb:SetHeight(thumbHeight)
scrollBar.thumb:SetPoint("TOP", scrollBar, "TOP", 0, -thumbY)
end
end
function CUI:Render()
local theme = GetTheme()
local total = table.getn(S.displayList)
local visibleItemIndex = 0
for i = 1, MAX_ROWS do
local row = S.rows[i]
if not row then
break
end
local displayIndex = S.scrollOffset + i
local entry = S.displayList[displayIndex]
if entry then
row:Show()
if entry.isHeader then
local headerText = entry.group.detail or entry.group.role or "未命名分组"
if entry.count and entry.count > 0 then
headerText = string.format("%s (%d)", headerText, entry.count)
end
row.catFS:SetText(headerText)
row.catFS:SetTextColor(
entry.group.color and entry.group.color[1] or theme.gold[1],
entry.group.color and entry.group.color[2] or theme.gold[2],
entry.group.color and entry.group.color[3] or theme.gold[3]
)
row.catFS:SetWidth(COL_CAT_W + COL_NAME_W + COL_EFF_W + COL_DUR_W - 10)
row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
row.nameFS:SetText("")
row.effFS:SetText("")
row.durFS:SetText("")
row:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 0,
})
row:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.85)
row.entry = nil
else
local item = entry.item
visibleItemIndex = visibleItemIndex + 1
row:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 0,
})
if math.mod(visibleItemIndex, 2) == 0 then
row:SetBackdropColor(theme.rowAlt[1], theme.rowAlt[2], theme.rowAlt[3], theme.rowAlt[4] or 0.6)
else
row:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.0)
end
row.catFS:SetWidth(COL_CAT_W - 8)
row.catFS:SetText(item.cat or "")
row.catFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
if item.id and item.id > 0 then
row.nameFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3])
else
row.nameFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3])
end
row.nameFS:SetText(item.name or "")
row.effFS:SetText(item.effect or "")
row.effFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3])
row.durFS:SetText(item.duration or "")
row.durFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
row.entry = item
end
else
row:Hide()
row.entry = nil
end
end
if S.emptyFS then
if total == 0 then
S.emptyFS:SetText("没有匹配到条目,试试更短的关键词或切回“全部”。")
S.emptyFS:Show()
else
S.emptyFS:Hide()
end
end
UpdateScrollbar()
end
local function RefreshTabs()
local theme = GetTheme()
for _, button in ipairs(S.tabBtns or {}) do
local active = (button.roleName == S.activeRole)
if active then
button:SetBackdropBorderColor(
theme.slotSelected[1],
theme.slotSelected[2],
theme.slotSelected[3],
1.00
)
button.fs:SetTextColor(
theme.slotSelected[1],
theme.slotSelected[2],
theme.slotSelected[3]
)
else
button:SetBackdropBorderColor(
theme.slotBorder[1],
theme.slotBorder[2],
theme.slotBorder[3],
theme.slotBorder[4] or 0.8
)
button.fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
end
end
end
local function BuildColumnLabel(parent, text, x, width)
local font = GetFont()
local theme = GetTheme()
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(font, 10, "OUTLINE")
fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, -4)
fs:SetWidth(width)
fs:SetJustifyH("LEFT")
fs:SetText(text)
return fs
end
function CUI:Build()
local theme = GetTheme()
local font = GetFont()
local frame = CreateFrame("Frame", "NanamiConsumableFrame", UIParent)
S.frame = frame
frame:SetWidth(FRAME_W)
frame:SetHeight(FRAME_H)
frame:SetPoint("CENTER", UIParent, "CENTER", 60, 20)
frame:SetFrameStrata("HIGH")
frame:SetToplevel(true)
frame:EnableMouse(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function() this:StartMoving() end)
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
frame:EnableMouseWheel(true)
frame:SetScript("OnMouseWheel", function()
if arg1 > 0 then
S.scrollOffset = math.max(0, S.scrollOffset - 1)
else
S.scrollOffset = math.min(S.scrollMax, S.scrollOffset + 1)
end
CUI:Render()
end)
ApplyBackdrop(frame)
tinsert(UISpecialFrames, "NanamiConsumableFrame")
local header = CreateFrame("Frame", nil, frame)
header:SetPoint("TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", 0, 0)
header:SetHeight(HEADER_H)
header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
header:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], theme.headerBg[4] or 1.0)
local titleFS = header:CreateFontString(nil, "OVERLAY")
titleFS:SetFont(font, 13, "OUTLINE")
titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD + 4, 0)
titleFS:SetText("食物药剂百科")
titleFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3])
local closeBtn = CreateFrame("Button", nil, header)
closeBtn:SetWidth(20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -7)
local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon")
closeTex:SetTexCoord(0.25, 0.375, 0, 0.125)
closeTex:SetAllPoints()
closeTex:SetVertexColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
closeBtn:SetScript("OnClick", function() frame:Hide() end)
closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end)
closeBtn:SetScript("OnLeave", function()
closeTex:SetVertexColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
end)
SetDivider(frame, -HEADER_H, 16)
local filterY = -(HEADER_H + 6)
local searchFrame = CreateFrame("Frame", nil, frame)
searchFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, filterY)
searchFrame:SetWidth(212)
searchFrame:SetHeight(FILTER_H - 4)
searchFrame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
searchFrame:SetBackdropColor(0.05, 0.05, 0.08, 0.90)
searchFrame:SetBackdropBorderColor(theme.panelBorder[1], theme.panelBorder[2], theme.panelBorder[3], 0.80)
local searchBox = CreateFrame("EditBox", "NanamiConsumableSearch", searchFrame)
searchBox:SetPoint("TOPLEFT", searchFrame, "TOPLEFT", 4, -3)
searchBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -4, 3)
searchBox:SetFont(font, 11)
searchBox:SetAutoFocus(false)
searchBox:SetMaxLetters(40)
searchBox:EnableMouse(true)
S.searchBox = searchBox
local hintFS = searchFrame:CreateFontString(nil, "OVERLAY")
hintFS:SetFont(font, 10, "OUTLINE")
hintFS:SetPoint("LEFT", searchFrame, "LEFT", 6, 0)
hintFS:SetText("搜索名称 / 效果 / 类别")
hintFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
searchBox:SetScript("OnTextChanged", function()
local text = this:GetText() or ""
S.searchText = text
if text == "" then
hintFS:Show()
else
hintFS:Hide()
end
CUI:Filter()
CUI:Render()
end)
searchBox:SetScript("OnEditFocusGained", function() hintFS:Hide() end)
searchBox:SetScript("OnEditFocusLost", function()
if (this:GetText() or "") == "" then
hintFS:Show()
end
end)
searchBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
local summaryFS = frame:CreateFontString(nil, "OVERLAY")
summaryFS:SetFont(font, 10, "OUTLINE")
summaryFS:SetPoint("LEFT", searchFrame, "RIGHT", 12, 0)
summaryFS:SetPoint("RIGHT", frame, "RIGHT", -36, filterY - 12)
summaryFS:SetJustifyH("RIGHT")
summaryFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
summaryFS:SetText("")
S.summaryFS = summaryFS
local tabX = SIDE_PAD
local tabY = filterY - FILTER_H
S.tabBtns = {}
for _, roleName in ipairs(BuildRoleList()) do
local tabW = Utf8DisplayWidth(roleName) + 14
if tabX + tabW > FRAME_W - SIDE_PAD then
tabX = SIDE_PAD
tabY = tabY - (ROLE_H - 4) - 2
end
local button = CreateFrame("Button", nil, frame)
button:SetWidth(tabW)
button:SetHeight(ROLE_H - 4)
button:SetPoint("TOPLEFT", frame, "TOPLEFT", tabX, tabY)
button:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
button:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.80)
button:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80)
local buttonFS = button:CreateFontString(nil, "OVERLAY")
buttonFS:SetFont(font, 10, "OUTLINE")
buttonFS:SetPoint("CENTER", button, "CENTER", 0, 0)
buttonFS:SetText(roleName)
buttonFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
button.fs = buttonFS
button.roleName = roleName
button:SetScript("OnClick", function()
S.activeRole = this.roleName
RefreshTabs()
CUI:Filter()
CUI:Render()
end)
button:SetScript("OnEnter", function()
if this.roleName ~= S.activeRole then
this:SetBackdropBorderColor(theme.text[1], theme.text[2], theme.text[3], 0.60)
end
end)
button:SetScript("OnLeave", function()
if this.roleName ~= S.activeRole then
this:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80)
end
end)
table.insert(S.tabBtns, button)
tabX = tabX + tabW + 3
end
RefreshTabs()
local listStartY = tabY - ROLE_H + 2
SetDivider(frame, listStartY, 16)
local colY = listStartY - 2
local colHeader = CreateFrame("Frame", nil, frame)
colHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, colY)
colHeader:SetWidth(FRAME_W - SIDE_PAD * 2 - 14)
colHeader:SetHeight(COL_H)
colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
colHeader:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.70)
BuildColumnLabel(colHeader, "类别", 4, COL_CAT_W)
BuildColumnLabel(colHeader, "名称", COL_CAT_W + 4, COL_NAME_W)
BuildColumnLabel(colHeader, "效果", COL_CAT_W + COL_NAME_W + 4, COL_EFF_W)
BuildColumnLabel(colHeader, "时长", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, COL_DUR_W)
local rowAreaY = colY - COL_H - 1
local rowAreaH = FRAME_H - (-rowAreaY) - 8
local scrollWidth = 10
local scrollBar = CreateFrame("Frame", nil, frame)
scrollBar:SetWidth(scrollWidth)
scrollBar:SetHeight(rowAreaH)
scrollBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, rowAreaY)
scrollBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
scrollBar:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.40)
local scrollThumb = scrollBar:CreateTexture(nil, "OVERLAY")
scrollThumb:SetTexture("Interface\\Buttons\\WHITE8X8")
scrollThumb:SetWidth(scrollWidth - 2)
scrollThumb:SetHeight(40)
scrollThumb:SetPoint("TOP", scrollBar, "TOP", 0, 0)
scrollThumb:SetVertexColor(theme.slotSelected[1], theme.slotSelected[2], theme.slotSelected[3], 0.60)
scrollBar.thumb = scrollThumb
S.scrollBar = scrollBar
local rowWidth = FRAME_W - SIDE_PAD * 2 - scrollWidth - 6
for i = 1, MAX_ROWS do
local rowY = rowAreaY - (i - 1) * ROW_H
local row = CreateFrame("Button", nil, frame)
row:SetWidth(rowWidth)
row:SetHeight(ROW_H)
row:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, rowY)
local catFS = row:CreateFontString(nil, "OVERLAY")
catFS:SetFont(font, 9, "OUTLINE")
catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
catFS:SetWidth(COL_CAT_W - 8)
catFS:SetJustifyH("LEFT")
row.catFS = catFS
local nameFS = row:CreateFontString(nil, "OVERLAY")
nameFS:SetFont(font, 9, "OUTLINE")
nameFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + 4, -3)
nameFS:SetWidth(COL_NAME_W - 4)
nameFS:SetJustifyH("LEFT")
row.nameFS = nameFS
local effFS = row:CreateFontString(nil, "OVERLAY")
effFS:SetFont(font, 9, "OUTLINE")
effFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + 4, -3)
effFS:SetWidth(COL_EFF_W - 4)
effFS:SetJustifyH("LEFT")
row.effFS = effFS
local durFS = row:CreateFontString(nil, "OVERLAY")
durFS:SetFont(font, 9, "OUTLINE")
durFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, -3)
durFS:SetWidth(COL_DUR_W - 2)
durFS:SetJustifyH("LEFT")
row.durFS = durFS
local highlight = row:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture("Interface\\Buttons\\WHITE8X8")
highlight:SetAllPoints()
highlight:SetVertexColor(1, 1, 1, 0.07)
highlight:SetBlendMode("ADD")
row:SetScript("OnEnter", function()
local entry = this.entry
if not entry then
return
end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
if entry.id and entry.id > 0 then
GameTooltip:SetHyperlink("item:" .. entry.id .. ":0:0:0")
GameTooltip:AddLine("Shift+点击可发送物品链接", 0.55, 0.55, 0.60)
else
GameTooltip:SetText(entry.name or "", 1, 0.82, 0.40)
if entry.effect and entry.effect ~= "" then
GameTooltip:AddLine(entry.effect, 0.80, 0.80, 0.80)
end
if entry.duration and entry.duration ~= "" then
GameTooltip:AddLine("持续时间: " .. entry.duration, 0.55, 0.55, 0.60)
end
end
GameTooltip:Show()
end)
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
row:SetScript("OnClick", function()
local entry = this.entry
if not entry then
return
end
if IsShiftKeyDown() and entry.id and entry.id > 0 then
local _, link = GetItemInfo(entry.id)
if link and ChatFrameEditBox then
ChatFrameEditBox:Show()
ChatFrameEditBox:Insert(link)
end
end
end)
row:Hide()
S.rows[i] = row
end
local emptyFS = frame:CreateFontString(nil, "OVERLAY")
emptyFS:SetFont(font, 11, "OUTLINE")
emptyFS:SetPoint("CENTER", frame, "CENTER", 0, -28)
emptyFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
emptyFS:SetText("")
emptyFS:Hide()
S.emptyFS = emptyFS
end
function CUI:Toggle()
if S.frame and S.frame:IsShown() then
S.frame:Hide()
return
end
if not S.frame then
self:Build()
end
self:Filter()
self:Render()
S.frame:Show()
end
function CUI:Hide()
if S.frame then
S.frame:Hide()
end
end

164
Core.lua
View File

@@ -32,6 +32,40 @@ do
end
end
-- 保护 ComboFrame防止 fadeInfo 为 nil 导致的报错
-- (ComboFrame.lua:46: attempt to index local 'fadeInfo' (a nil value))
if ComboFrame then
if not ComboFrame.fadeInfo then
ComboFrame.fadeInfo = {}
end
local origComboScript = ComboFrame.GetScript and ComboFrame:GetScript("OnUpdate")
if origComboScript then
ComboFrame:SetScript("OnUpdate", function(elapsed)
if ComboFrame.fadeInfo then
origComboScript(elapsed)
end
end)
end
end
if ComboFrame_Update then
local _orig_ComboUpdate = ComboFrame_Update
ComboFrame_Update = function()
if ComboFrame and not ComboFrame.fadeInfo then
ComboFrame.fadeInfo = {}
end
return _orig_ComboUpdate()
end
end
if ComboFrame_OnUpdate then
local _orig_ComboOnUpdate = ComboFrame_OnUpdate
ComboFrame_OnUpdate = function(elapsed)
if ComboFrame and not ComboFrame.fadeInfo then
ComboFrame.fadeInfo = {}
end
return _orig_ComboOnUpdate(elapsed)
end
end
local origOnUpdate = UIParent and UIParent.GetScript and UIParent:GetScript("OnUpdate")
if origOnUpdate then
UIParent:SetScript("OnUpdate", function()
@@ -43,6 +77,11 @@ end
BINDING_HEADER_NANAMI_UI = "Nanami-UI"
BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图"
BINDING_HEADER_NANAMI_EXTRABAR = "Nanami-UI 额外动作条"
for _i = 1, 48 do
_G["BINDING_NAME_NANAMI_EXTRABAR" .. _i] = "额外动作条 按钮" .. _i
end
SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
SFrames.events = {}
@@ -140,6 +179,52 @@ function SFrames:Print(msg)
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg))
end
local function IsBattlefieldMinimapVisible()
if BattlefieldMinimap and BattlefieldMinimap.IsVisible and BattlefieldMinimap:IsVisible() then
return true
end
if BattlefieldMinimapFrame and BattlefieldMinimapFrame ~= BattlefieldMinimap
and BattlefieldMinimapFrame.IsVisible and BattlefieldMinimapFrame:IsVisible() then
return true
end
return false
end
function SFrames:CaptureBattlefieldMinimapState()
return IsBattlefieldMinimapVisible()
end
function SFrames:RestoreBattlefieldMinimapState(wasVisible)
if wasVisible then
return
end
local frames = { BattlefieldMinimap, BattlefieldMinimapFrame }
local hidden = {}
for i = 1, table.getn(frames) do
local frame = frames[i]
if frame and not hidden[frame] and frame.Hide and frame.IsVisible and frame:IsVisible() then
hidden[frame] = true
pcall(frame.Hide, frame)
end
end
end
function SFrames:CallWithPreservedBattlefieldMinimap(func, a1, a2, a3, a4, a5, a6, a7, a8)
if type(func) ~= "function" then
return
end
local state = self:CaptureBattlefieldMinimapState()
local results = { pcall(func, a1, a2, a3, a4, a5, a6, a7, a8) }
self:RestoreBattlefieldMinimapState(state)
if not results[1] then
return nil, results[2]
end
return unpack(results, 2, table.getn(results))
end
-- Addon Loaded Initializer
SFrames:RegisterEvent("PLAYER_LOGIN", function()
SFrames:Initialize()
@@ -299,6 +384,10 @@ function SFrames:DoFullInitialize()
SFrames.Tooltip:SetAlpha(0)
SFrames.Tooltip:Hide()
if SFrames.AuraTracker and SFrames.AuraTracker.Initialize then
SFrames.AuraTracker:Initialize()
end
-- Phase 1: Critical modules (unit frames, action bars) — must load immediately
if SFramesDB.enableUnitFrames ~= false then
if SFramesDB.enablePlayerFrame ~= false then
@@ -319,6 +408,10 @@ function SFrames:DoFullInitialize()
SFrames.ActionBars:Initialize()
end
if SFrames.ExtraBar and SFrames.ExtraBar.Initialize then
SFrames.ExtraBar:Initialize()
end
self:InitSlashCommands()
-- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike
@@ -375,6 +468,13 @@ function SFrames:GetAuraTimeLeft(unit, index, isBuff)
end
end
if SFrames.AuraTracker and SFrames.AuraTracker.GetAuraTimeLeft then
local trackerTime = SFrames.AuraTracker:GetAuraTimeLeft(unit, isBuff and "buff" or "debuff", index)
if trackerTime and trackerTime > 0 then
return trackerTime
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)
@@ -420,6 +520,64 @@ function SFrames:FormatTime(seconds)
end
end
local POWER_RAINBOW_TEX = "Interface\\AddOns\\Nanami-UI\\img\\progress"
function SFrames:UpdateRainbowBar(bar, power, maxPower, unit)
-- 彩虹条仅适用于法力powerType 0怒气/能量等跳过
if unit and UnitPowerType(unit) ~= 0 then
if bar._rainbowActive then
if bar.rainbowTex then bar.rainbowTex:Hide() end
bar._rainbowActive = nil
end
return
end
if not (SFramesDB and SFramesDB.powerRainbow) then
if bar._rainbowActive then
if bar.rainbowTex then bar.rainbowTex:Hide() end
bar._rainbowActive = nil
end
return
end
if not bar.rainbowTex then
bar.rainbowTex = bar:CreateTexture(nil, "OVERLAY")
bar.rainbowTex:SetTexture(POWER_RAINBOW_TEX)
bar.rainbowTex:Hide()
end
if maxPower and maxPower > 0 then
local pct = power / maxPower
if pct >= 1.0 then
-- 满条:直接铺满,不依赖 GetWidth()(两锚点定尺寸的框体 GetWidth 可能返回 0
bar.rainbowTex:ClearAllPoints()
bar.rainbowTex:SetAllPoints(bar)
bar.rainbowTex:SetTexCoord(0, 1, 0, 1)
bar.rainbowTex:Show()
bar._rainbowActive = true
else
local barW = bar:GetWidth()
-- 双锚点定尺寸的框体如宠物能量条GetWidth() 可能返回 0
-- 回退到 GetRight()-GetLeft() 获取实际渲染宽度
if not barW or barW <= 0 then
local left = bar:GetLeft()
local right = bar:GetRight()
if left and right then
barW = right - left
end
end
if barW and barW > 0 then
bar.rainbowTex:ClearAllPoints()
bar.rainbowTex:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
bar.rainbowTex:SetPoint("BOTTOMRIGHT", bar, "BOTTOMLEFT", barW * pct, 0)
bar.rainbowTex:SetTexCoord(0, pct, 0, 1)
bar.rainbowTex:Show()
bar._rainbowActive = true
end
end
else
bar.rainbowTex:Hide()
bar._rainbowActive = nil
end
end
function SFrames:InitSlashCommands()
DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.")
SLASH_SFRAMES1 = "/nanami"
@@ -808,12 +966,16 @@ function SFrames:HideBlizzardFrames()
end
if ComboFrame then
ComboFrame:UnregisterAllEvents()
ComboFrame:SetScript("OnUpdate", nil)
ComboFrame:Hide()
ComboFrame.Show = function() end
ComboFrame.fadeInfo = ComboFrame.fadeInfo or {}
ComboFrame.fadeInfo = {}
if ComboFrame_Update then
ComboFrame_Update = function() end
end
if ComboFrame_OnUpdate then
ComboFrame_OnUpdate = function() end
end
end
end

792
ExtraBar.lua Normal file
View File

@@ -0,0 +1,792 @@
--------------------------------------------------------------------------------
-- Nanami-UI: ExtraBar
--
-- A fully configurable extra action bar using action slots from page 7+
-- (slots 73-120). Buttons are created from scratch since no spare Blizzard
-- ActionButton frames exist.
--
-- Supports: grid layout, per-row count, alignment, drag-and-drop, cooldown,
-- range coloring, tooltip, and Mover integration for position saving.
--------------------------------------------------------------------------------
SFrames.ExtraBar = {}
local EB = SFrames.ExtraBar
local DEFAULTS = {
enable = false,
buttonCount = 12,
perRow = 12,
buttonSize = 36,
buttonGap = 2,
align = "center",
startSlot = 73,
alpha = 1.0,
showHotkey = true,
showCount = true,
buttonRounded = false,
buttonInnerShadow = false,
}
local MAX_BUTTONS = 48
function EB:GetDB()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.ExtraBar) ~= "table" then SFramesDB.ExtraBar = {} end
local db = SFramesDB.ExtraBar
for k, v in pairs(DEFAULTS) do
if db[k] == nil then db[k] = v end
end
return db
end
--------------------------------------------------------------------------------
-- Layout helper (local LayoutGrid equivalent)
--------------------------------------------------------------------------------
local function LayoutGrid(buttons, parent, size, gap, perRow, count)
if count == 0 then return end
local numCols = math.min(perRow, count)
local numRows = math.ceil(count / perRow)
parent:SetWidth(numCols * size + math.max(numCols - 1, 0) * gap)
parent:SetHeight(numRows * size + math.max(numRows - 1, 0) * gap)
for i = 1, count do
local b = buttons[i]
if b then
b:SetWidth(size)
b:SetHeight(size)
b:ClearAllPoints()
local col = math.fmod(i - 1, perRow)
local row = math.floor((i - 1) / perRow)
b:SetPoint("TOPLEFT", parent, "TOPLEFT", col * (size + gap), -row * (size + gap))
b:Show()
end
end
for i = count + 1, MAX_BUTTONS do
if buttons[i] then buttons[i]:Hide() end
end
end
--------------------------------------------------------------------------------
-- Button visual helpers (mirrors ActionBars.lua approach)
--------------------------------------------------------------------------------
local function CreateBackdropFor(btn)
if btn.sfBackdrop then return end
local level = btn:GetFrameLevel()
local bd = CreateFrame("Frame", nil, btn)
bd:SetFrameLevel(level > 0 and (level - 1) or 0)
bd:SetAllPoints(btn)
SFrames:CreateBackdrop(bd)
btn.sfBackdrop = bd
end
local function CreateInnerShadow(btn)
if btn.sfInnerShadow then return btn.sfInnerShadow end
local shadow = {}
local thickness = 4
local top = btn:CreateTexture(nil, "OVERLAY")
top:SetTexture("Interface\\Buttons\\WHITE8X8")
top:SetHeight(thickness)
top:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)
shadow.top = top
local bot = btn:CreateTexture(nil, "OVERLAY")
bot:SetTexture("Interface\\Buttons\\WHITE8X8")
bot:SetHeight(thickness)
bot:SetGradientAlpha("VERTICAL", 0, 0, 0, 0.5, 0, 0, 0, 0)
shadow.bottom = bot
local left = btn:CreateTexture(nil, "OVERLAY")
left:SetTexture("Interface\\Buttons\\WHITE8X8")
left:SetWidth(thickness)
left:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0.5, 0, 0, 0, 0)
shadow.left = left
local right = btn:CreateTexture(nil, "OVERLAY")
right:SetTexture("Interface\\Buttons\\WHITE8X8")
right:SetWidth(thickness)
right:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0, 0, 0, 0, 0.5)
shadow.right = right
btn.sfInnerShadow = shadow
return shadow
end
local function ApplyButtonVisuals(btn, rounded, innerShadow)
local bd = btn.sfBackdrop
if not bd then return end
local inset = rounded and 3 or 2
btn.sfIconInset = inset
if rounded then
bd:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
else
bd:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
end
local A = SFrames.ActiveTheme
if A and A.panelBg then
bd:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
bd:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
else
bd:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
bd:SetBackdropBorderColor(0, 0, 0, 1)
end
local icon = btn.sfIcon
if icon then
icon:ClearAllPoints()
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
end
if btn.sfCdOverlay then
btn.sfCdOverlay:ClearAllPoints()
btn.sfCdOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
btn.sfCdOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
end
if btn.sfRangeOverlay then
btn.sfRangeOverlay:ClearAllPoints()
btn.sfRangeOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
btn.sfRangeOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
end
if innerShadow then
if not btn.sfInnerShadow then CreateInnerShadow(btn) end
local s = btn.sfInnerShadow
s.top:ClearAllPoints()
s.top:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
s.top:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset)
s.bottom:ClearAllPoints()
s.bottom:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset)
s.bottom:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
s.left:ClearAllPoints()
s.left:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
s.left:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset)
s.right:ClearAllPoints()
s.right:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset)
s.right:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
s.top:Show(); s.bottom:Show(); s.left:Show(); s.right:Show()
else
if btn.sfInnerShadow then
local s = btn.sfInnerShadow
s.top:Hide(); s.bottom:Hide(); s.left:Hide(); s.right:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Single-button refresh helpers
--------------------------------------------------------------------------------
local function RefreshButtonIcon(btn)
local slot = btn.sfActionSlot
if not slot then return end
if HasAction(slot) then
local tex = GetActionTexture(slot)
btn.sfIcon:SetTexture(tex)
btn.sfIcon:Show()
btn.sfIcon:SetVertexColor(1, 1, 1, 1)
btn:SetAlpha(1)
else
btn.sfIcon:SetTexture(nil)
btn.sfIcon:Hide()
end
end
local function RefreshButtonCooldown(btn)
local slot = btn.sfActionSlot
if not slot or not HasAction(slot) then
if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
if btn.sfCdText then btn.sfCdText:SetText("") end
btn.sfCdStart = 0
btn.sfCdDuration = 0
return
end
local start, duration, enable = GetActionCooldown(slot)
if not start or not duration or duration == 0 or enable == 0 then
if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
if btn.sfCdText then btn.sfCdText:SetText("") end
btn.sfCdStart = 0
btn.sfCdDuration = 0
return
end
btn.sfCdStart = start
btn.sfCdDuration = duration
local now = GetTime()
local remaining = (start + duration) - now
if remaining > 0 then
if btn.sfCdOverlay then btn.sfCdOverlay:Show() end
if btn.sfCdText then
if remaining >= 60 then
btn.sfCdText:SetText(math.floor(remaining / 60) .. "m")
else
btn.sfCdText:SetText(math.floor(remaining + 0.5) .. "")
end
end
else
if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
if btn.sfCdText then btn.sfCdText:SetText("") end
btn.sfCdStart = 0
btn.sfCdDuration = 0
end
end
local function RefreshButtonUsable(btn)
local slot = btn.sfActionSlot
if not slot or not HasAction(slot) then return end
local isUsable, notEnoughMana = IsUsableAction(slot)
if isUsable then
btn.sfIcon:SetVertexColor(1, 1, 1, 1)
elseif notEnoughMana then
btn.sfIcon:SetVertexColor(0.2, 0.2, 0.8, 1)
else
btn.sfIcon:SetVertexColor(0.4, 0.4, 0.4, 1)
end
end
local function RefreshButtonCount(btn)
local slot = btn.sfActionSlot
if not slot or not HasAction(slot) then
btn.sfCount:SetText("")
return
end
local count = GetActionCount(slot)
if count and count > 0 then
btn.sfCount:SetText(tostring(count))
else
btn.sfCount:SetText("")
end
end
local function RefreshButtonRange(btn)
local slot = btn.sfActionSlot
if not slot or not HasAction(slot) then
if btn.sfRangeOverlay then btn.sfRangeOverlay:Hide() end
return
end
local inRange = IsActionInRange(slot)
if not btn.sfRangeOverlay then
local inset = btn.sfIconInset or 2
local ov = btn:CreateTexture(nil, "OVERLAY")
ov:SetTexture("Interface\\Buttons\\WHITE8X8")
ov:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
ov:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
ov:SetVertexColor(1.0, 0.1, 0.1, 0.35)
ov:Hide()
btn.sfRangeOverlay = ov
end
if inRange == 0 then
btn.sfRangeOverlay:Show()
else
btn.sfRangeOverlay:Hide()
end
end
local function RefreshButtonState(btn)
local slot = btn.sfActionSlot
if not slot or not HasAction(slot) then return end
if IsCurrentAction(slot) then
if btn.sfBackdrop then
btn.sfBackdrop:SetBackdropBorderColor(1, 1, 1, 0.8)
end
else
local A = SFrames.ActiveTheme
if btn.sfBackdrop then
if A and A.panelBorder then
btn.sfBackdrop:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
else
btn.sfBackdrop:SetBackdropBorderColor(0, 0, 0, 1)
end
end
end
end
local function RefreshButtonAll(btn)
RefreshButtonIcon(btn)
RefreshButtonCooldown(btn)
RefreshButtonUsable(btn)
RefreshButtonCount(btn)
RefreshButtonState(btn)
end
--------------------------------------------------------------------------------
-- Custom action button factory
--------------------------------------------------------------------------------
local btnIndex = 0
local function CreateExtraButton(parent)
btnIndex = btnIndex + 1
local name = "SFramesExtraBarButton" .. btnIndex
local btn = CreateFrame("Button", name, parent)
btn:SetWidth(36)
btn:SetHeight(36)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
CreateBackdropFor(btn)
local icon = btn:CreateTexture(name .. "Icon", "ARTWORK")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
btn.sfIcon = icon
local cdOverlay = btn:CreateTexture(nil, "OVERLAY")
cdOverlay:SetTexture("Interface\\Buttons\\WHITE8X8")
cdOverlay:SetAllPoints(icon)
cdOverlay:SetVertexColor(0, 0, 0, 0.6)
cdOverlay:Hide()
btn.sfCdOverlay = cdOverlay
local cdText = btn:CreateFontString(nil, "OVERLAY")
cdText:SetFont(SFrames:GetFont(), 12, "OUTLINE")
cdText:SetPoint("CENTER", btn, "CENTER", 0, 0)
cdText:SetTextColor(1, 1, 0.2)
cdText:SetText("")
btn.sfCdText = cdText
btn.sfCdStart = 0
btn.sfCdDuration = 0
local font = SFrames:GetFont()
local hotkey = btn:CreateFontString(name .. "HotKey", "OVERLAY")
hotkey:SetFont(font, 9, "OUTLINE")
hotkey:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -2, -2)
hotkey:SetJustifyH("RIGHT")
btn.sfHotKey = hotkey
local count = btn:CreateFontString(name .. "Count", "OVERLAY")
count:SetFont(font, 9, "OUTLINE")
count:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
count:SetJustifyH("RIGHT")
btn.sfCount = count
-- Highlight texture
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
hl:SetBlendMode("ADD")
hl:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
hl:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
hl:SetAlpha(0.3)
-- Pushed texture overlay
local pushed = btn:CreateTexture(nil, "OVERLAY")
pushed:SetTexture("Interface\\Buttons\\WHITE8X8")
pushed:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
pushed:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
pushed:SetVertexColor(1, 1, 1, 0.15)
pushed:Hide()
btn.sfPushed = pushed
btn.sfActionSlot = nil
btn:SetScript("OnClick", function()
local slot = this.sfActionSlot
if not slot then return end
if arg1 == "LeftButton" then
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
-- Shift-click: link to chat (fallback: pickup)
pcall(PickupAction, slot)
else
UseAction(slot, 0, 1)
end
elseif arg1 == "RightButton" then
UseAction(slot, 0, 1)
end
RefreshButtonAll(this)
end)
btn:SetScript("OnDragStart", function()
local slot = this.sfActionSlot
if slot then
pcall(PickupAction, slot)
RefreshButtonAll(this)
end
end)
btn:SetScript("OnReceiveDrag", function()
local slot = this.sfActionSlot
if slot then
pcall(PlaceAction, slot)
RefreshButtonAll(this)
end
end)
btn:SetScript("OnEnter", function()
local slot = this.sfActionSlot
if slot and HasAction(slot) then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetAction(slot)
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:SetScript("OnMouseDown", function()
if this.sfPushed then this.sfPushed:Show() end
end)
btn:SetScript("OnMouseUp", function()
if this.sfPushed then this.sfPushed:Hide() end
end)
btn:Hide()
return btn
end
--------------------------------------------------------------------------------
-- Create all buttons once
--------------------------------------------------------------------------------
function EB:CreateButtons()
local db = self:GetDB()
self.holder = CreateFrame("Frame", "SFramesExtraBarHolder", UIParent)
self.holder:SetWidth(200)
self.holder:SetHeight(40)
local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ExtraBar"]
if pos and pos.point and pos.relativePoint then
self.holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
else
self.holder:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
end
self.buttons = {}
for i = 1, MAX_BUTTONS do
local btn = CreateExtraButton(self.holder)
table.insert(self.buttons, btn)
end
if SFrames.ActionBars and SFrames.ActionBars.RegisterBindButton then
for i = 1, MAX_BUTTONS do
local bname = self.buttons[i]:GetName()
SFrames.ActionBars:RegisterBindButton(bname, "NANAMI_EXTRABAR" .. i)
end
end
end
--------------------------------------------------------------------------------
-- Apply configuration
--------------------------------------------------------------------------------
function EB:ApplyConfig()
if not self.holder then return end
local db = self:GetDB()
if not db.enable then
self.holder:Hide()
return
end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
local perRow = db.perRow or 12
local size = db.buttonSize or 36
local gap = db.buttonGap or 2
local startSlot = db.startSlot or 73
-- Assign action slots
for i = 1, MAX_BUTTONS do
local btn = self.buttons[i]
btn.sfActionSlot = startSlot + i - 1
end
-- Layout
LayoutGrid(self.buttons, self.holder, size, gap, perRow, count)
-- Alignment via anchor
local positions = SFramesDB and SFramesDB.Positions
local pos = positions and positions["ExtraBar"]
if not (pos and pos.point and pos.relativePoint) then
self.holder:ClearAllPoints()
local align = db.align or "center"
if align == "left" then
self.holder:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 20, 200)
elseif align == "right" then
self.holder:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -20, 200)
else
self.holder:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
end
end
-- Alpha
local alpha = db.alpha or 1
if alpha < 0.1 then alpha = 0.1 end
if alpha > 1 then alpha = 1 end
self.holder:SetAlpha(alpha)
-- Scale (inherit from main action bars if available)
local abDb = SFrames.ActionBars and SFrames.ActionBars.GetDB and SFrames.ActionBars:GetDB()
local scale = abDb and abDb.scale or 1.0
self.holder:SetScale(scale)
-- Button visuals
local isRounded = db.buttonRounded
local isShadow = db.buttonInnerShadow
local showHK = db.showHotkey
local showCt = db.showCount
local font = SFrames:GetFont()
local fontSize = math.max(6, math.floor(size * 0.25 + 0.5))
for i = 1, count do
local btn = self.buttons[i]
ApplyButtonVisuals(btn, isRounded, isShadow)
if btn.sfHotKey then
btn.sfHotKey:SetFont(font, fontSize, "OUTLINE")
if showHK then btn.sfHotKey:Show() else btn.sfHotKey:Hide() end
end
if btn.sfCount then
btn.sfCount:SetFont(font, fontSize, "OUTLINE")
if showCt then btn.sfCount:Show() else btn.sfCount:Hide() end
end
if btn.sfCdText then
local cdFontSize = math.max(8, math.floor(size * 0.35 + 0.5))
btn.sfCdText:SetFont(font, cdFontSize, "OUTLINE")
end
RefreshButtonAll(btn)
end
self.holder:Show()
-- Update mover if in layout mode
if SFrames.Movers and SFrames.Movers:IsLayoutMode() then
SFrames.Movers:SyncMoverToFrame("ExtraBar")
end
end
--------------------------------------------------------------------------------
-- Refresh all visible buttons
--------------------------------------------------------------------------------
function EB:RefreshAll()
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
RefreshButtonAll(self.buttons[i])
end
end
function EB:RefreshCooldowns()
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
RefreshButtonCooldown(self.buttons[i])
end
end
function EB:RefreshUsable()
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
RefreshButtonUsable(self.buttons[i])
end
end
function EB:RefreshStates()
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
RefreshButtonState(self.buttons[i])
end
end
function EB:RefreshHotkeys()
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
local btn = self.buttons[i]
local cmd = "NANAMI_EXTRABAR" .. i
local hotkey = btn.sfHotKey
if hotkey then
local key1 = GetBindingKey(cmd)
if key1 then
local text = key1
if GetBindingText then
text = GetBindingText(key1, "KEY_", 1) or key1
end
hotkey:SetText(text)
else
hotkey:SetText("")
end
end
end
end
--------------------------------------------------------------------------------
-- OnUpdate poller for cooldown / range / usability
--------------------------------------------------------------------------------
function EB:SetupPoller()
local poller = CreateFrame("Frame", "SFramesExtraBarPoller", UIParent)
poller.timer = 0
self.poller = poller
poller:SetScript("OnUpdate", function()
this.timer = this.timer + arg1
if this.timer < 0.2 then return end
this.timer = 0
local db = EB:GetDB()
if not db.enable then return end
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
for i = 1, count do
local btn = EB.buttons[i]
if btn and btn:IsShown() then
RefreshButtonCooldown(btn)
RefreshButtonUsable(btn)
RefreshButtonRange(btn)
RefreshButtonState(btn)
end
end
end)
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function EB:Initialize()
local db = self:GetDB()
if not db.enable then return end
self:CreateButtons()
self:ApplyConfig()
self:SetupPoller()
-- Events
SFrames:RegisterEvent("ACTIONBAR_SLOT_CHANGED", function()
if not EB.buttons then return end
local db = EB:GetDB()
if not db.enable then return end
local slot = arg1
local startSlot = db.startSlot or 73
local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
if slot then
local idx = slot - startSlot + 1
if idx >= 1 and idx <= count then
RefreshButtonAll(EB.buttons[idx])
end
else
EB:RefreshAll()
end
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", function()
EB:RefreshCooldowns()
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_USABLE", function()
EB:RefreshUsable()
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_STATE", function()
EB:RefreshStates()
end)
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
EB:RefreshAll()
EB:RefreshHotkeys()
end)
SFrames:RegisterEvent("UPDATE_BINDINGS", function()
EB:RefreshHotkeys()
end)
-- Register mover
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("ExtraBar", self.holder, "额外动作条",
"CENTER", "UIParent", "CENTER", 0, -200)
end
end
--------------------------------------------------------------------------------
-- Late enable: called from ConfigUI when user toggles enable on
--------------------------------------------------------------------------------
function EB:Enable()
local db = self:GetDB()
db.enable = true
if not self.holder then
self:CreateButtons()
self:SetupPoller()
SFrames:RegisterEvent("ACTIONBAR_SLOT_CHANGED", function()
if not EB.buttons then return end
local db2 = EB:GetDB()
if not db2.enable then return end
local slot = arg1
local startSlot = db2.startSlot or 73
local count = math.min(db2.buttonCount or 12, MAX_BUTTONS)
if slot then
local idx = slot - startSlot + 1
if idx >= 1 and idx <= count then
RefreshButtonAll(EB.buttons[idx])
end
else
EB:RefreshAll()
end
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", function()
EB:RefreshCooldowns()
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_USABLE", function()
EB:RefreshUsable()
end)
SFrames:RegisterEvent("ACTIONBAR_UPDATE_STATE", function()
EB:RefreshStates()
end)
SFrames:RegisterEvent("UPDATE_BINDINGS", function()
EB:RefreshHotkeys()
end)
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("ExtraBar", self.holder, "额外动作条",
"CENTER", "UIParent", "CENTER", 0, -200)
end
end
self:ApplyConfig()
self:RefreshHotkeys()
end
function EB:Disable()
local db = self:GetDB()
db.enable = false
if self.holder then
self.holder:Hide()
end
if self.holder and SFrames.Movers and SFrames.Movers.IsLayoutMode
and SFrames.Movers:IsLayoutMode() then
pcall(SFrames.Movers.SyncMoverToFrame, SFrames.Movers, "ExtraBar")
end
end
function EB:RunButton(index)
if not self.buttons then return end
local db = self:GetDB()
if not db.enable then return end
if index < 1 or index > math.min(db.buttonCount or 12, MAX_BUTTONS) then return end
local btn = self.buttons[index]
if not btn or not btn:IsVisible() then return end
local slot = btn.sfActionSlot
if slot and HasAction(slot) then
UseAction(slot)
RefreshButtonAll(btn)
end
end

View File

@@ -1,19 +1,48 @@
-- 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)
function SFrames:ApplyBackdropStyle(frame, opts)
opts = opts or {}
local radius = tonumber(opts.cornerRadius) or 0
local showBorder = opts.showBorder ~= false
local useRounded = radius and radius > 0
if useRounded then
local edgeSize = math.max(8, math.min(18, math.floor(radius + 0.5)))
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = showBorder and "Interface\\Tooltips\\UI-Tooltip-Border" or nil,
tile = true, tileSize = 16, edgeSize = edgeSize,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
else
frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
frame:SetBackdropBorderColor(0, 0, 0, 1)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = showBorder and "Interface\\Buttons\\WHITE8X8" or nil,
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
end
local A = SFrames.ActiveTheme
local bgAlpha = opts.bgAlpha
if A and A.panelBg then
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgAlpha or A.panelBg[4] or 0.9)
if showBorder then
frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
else
frame:SetBackdropBorderColor(0, 0, 0, 0)
end
else
frame:SetBackdropColor(0.1, 0.1, 0.1, bgAlpha or 0.9)
if showBorder then
frame:SetBackdropBorderColor(0, 0, 0, 1)
else
frame:SetBackdropBorderColor(0, 0, 0, 0)
end
end
end
function SFrames:CreateBackdrop(frame)
self:ApplyBackdropStyle(frame)
end
function SFrames:CreateRoundBackdrop(frame)
@@ -38,6 +67,78 @@ function SFrames:CreateUnitBackdrop(frame)
frame:SetBackdropBorderColor(0, 0, 0, 1)
end
function SFrames:ApplyConfiguredUnitBackdrop(frame, prefix, isPortrait)
if not frame then return end
local db = SFramesDB or {}
local bgAlphaKey = prefix .. (isPortrait and "PortraitBgAlpha" or "BgAlpha")
self:ApplyBackdropStyle(frame, {
showBorder = false,
cornerRadius = 0,
bgAlpha = tonumber(db[bgAlphaKey]) or (isPortrait and tonumber(db[prefix .. "BgAlpha"]) or nil),
})
end
--------------------------------------------------------------------------------
-- Frame Style Preset helpers
--------------------------------------------------------------------------------
function SFrames:GetFrameStylePreset()
return (SFramesDB and SFramesDB.frameStylePreset) or "classic"
end
function SFrames:IsGradientStyle()
return self:GetFrameStylePreset() == "gradient"
end
-- Apply a gradient darkening overlay on a StatusBar.
-- Uses a separate Texture on OVERLAY layer with purple-black vertex color
-- and alpha gradient from 0 (left) to ~0.6 (right).
-- This approach does NOT touch the StatusBar texture itself,
-- so it survives SetStatusBarColor calls.
function SFrames:ApplyGradientStyle(bar)
if not bar then return end
if not bar._gradOverlay then
local ov = bar:CreateTexture(nil, "OVERLAY")
ov:SetTexture("Interface\\Buttons\\WHITE8X8")
bar._gradOverlay = ov
end
local ov = bar._gradOverlay
ov:ClearAllPoints()
ov:SetAllPoints(bar:GetStatusBarTexture())
-- Dark purple-ish tint color
ov:SetVertexColor(0.04, 0.0, 0.08, 1)
-- Alpha gradient: left fully transparent → right 65% opaque
if ov.SetGradientAlpha then
ov:SetGradientAlpha("HORIZONTAL",
1, 1, 1, 0,
1, 1, 1, 0.65)
end
ov:Show()
end
function SFrames:RemoveGradientStyle(bar)
if not bar then return end
if bar._gradOverlay then
bar._gradOverlay:Hide()
end
end
-- No-op wrappers kept for compatibility with unit files
function SFrames:ApplyBarGradient(bar)
-- Gradient is now persistent overlay; no per-color-update needed
end
function SFrames:ClearBarGradient(bar)
self:RemoveGradientStyle(bar)
end
-- Strip backdrop from a frame (used in gradient style)
function SFrames:ClearBackdrop(frame)
if not frame then return end
if frame.SetBackdrop then
frame:SetBackdrop(nil)
end
end
-- Generator for StatusBars
function SFrames:CreateStatusBar(parent, name)
local bar = CreateFrame("StatusBar", name, parent)
@@ -50,6 +151,11 @@ function SFrames:CreateStatusBar(parent, name)
return bar
end
function SFrames:ApplyStatusBarTexture(bar, settingKey, fallbackKey)
if not bar or not bar.SetStatusBarTexture then return end
bar:SetStatusBarTexture(self:ResolveBarTexture(settingKey, fallbackKey))
end
-- Generator for FontStrings
function SFrames:CreateFontString(parent, size, justifyH)
local fs = parent:CreateFontString(nil, "OVERLAY")
@@ -59,6 +165,29 @@ function SFrames:CreateFontString(parent, size, justifyH)
return fs
end
function SFrames:ApplyFontString(fs, size, fontSettingKey, fallbackFontKey, outlineSettingKey, fallbackOutlineKey)
if not fs or not fs.SetFont then return end
local fontPath = self:ResolveFont(fontSettingKey, fallbackFontKey)
local outline = self:ResolveFontOutline(outlineSettingKey, fallbackOutlineKey)
fs:SetFont(fontPath, size or 12, outline)
end
function SFrames:FormatCompactNumber(value)
local num = tonumber(value) or 0
local sign = num < 0 and "-" or ""
num = math.abs(num)
if num >= 1000000 then
return string.format("%s%.1fM", sign, num / 1000000)
elseif num >= 10000 then
return string.format("%s%.1fK", sign, num / 1000)
end
return sign .. tostring(math.floor(num + 0.5))
end
function SFrames:FormatCompactPair(currentValue, maxValue)
return self:FormatCompactNumber(currentValue) .. " / " .. self:FormatCompactNumber(maxValue)
end
-- Generator for 3D Portraits
function SFrames:CreatePortrait(parent, name)
local portrait = CreateFrame("PlayerModel", name, parent)

242
Focus.lua
View File

@@ -327,17 +327,23 @@ function SFrames.Focus:CreateFocusFrame()
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["FocusFrame"] then
local pos = SFramesDB.Positions["FocusFrame"]
-- Validate: if GetTop would be near 0 or negative, position is bad
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0)
-- After setting, check if visible on screen
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
(pos.xOfs or 250) / fScale, (pos.yOfs or 0) / fScale)
else
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
pos.xOfs or 250, pos.yOfs or 0)
end
local top = f:GetTop()
local left = f:GetLeft()
if not top or not left or top < 50 or left < 0 then
-- Bad position, reset
f:ClearAllPoints()
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
SFramesDB.Positions["FocusFrame"] = nil
end
elseif SFramesTargetFrame then
f:SetPoint("TOPLEFT", SFramesTargetFrame, "BOTTOMLEFT", 0, -75)
else
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
end
@@ -352,6 +358,11 @@ function SFrames.Focus:CreateFocusFrame()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, _, relativePoint, xOfs, yOfs = this:GetPoint()
local fScale = this:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
xOfs = (xOfs or 0) * fScale
yOfs = (yOfs or 0) * fScale
end
SFramesDB.Positions["FocusFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
@@ -546,37 +557,27 @@ function SFrames.Focus:CreateFocusFrame()
SFrames:CreateUnitBackdrop(f)
-- Portrait (right side) — EnableMouse(false) so clicks pass through to main Button
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
f.portrait = CreateFrame("PlayerModel", nil, f)
-- Portrait placeholder (hidden, focus frame does not use 3D portraits)
f.portrait = CreateFrame("Frame", nil, f)
f.portrait:SetWidth(pWidth)
f.portrait:SetHeight(totalH - 2)
f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0)
f.portrait:EnableMouse(false)
f.portrait:Hide()
local pbg = CreateFrame("Frame", nil, f)
pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0)
pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
pbg:SetFrameLevel(f:GetFrameLevel())
SFrames:CreateUnitBackdrop(pbg)
f.portraitBG = pbg
pbg:EnableMouse(false)
pbg:Hide()
if not showPortrait then
f.portrait:Hide()
pbg:Hide()
end
-- Health bar
-- Health bar (full width, no portrait)
f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth")
f.health:EnableMouse(false)
if showPortrait then
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
else
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
end
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
f.health:SetHeight(hHeight)
local hbg = CreateFrame("Frame", nil, f)
@@ -596,11 +597,7 @@ function SFrames.Focus:CreateFocusFrame()
f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower")
f.power:EnableMouse(false)
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
if showPortrait then
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
else
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
end
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
local powerbg = CreateFrame("Frame", nil, f)
powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
@@ -615,9 +612,9 @@ function SFrames.Focus:CreateFocusFrame()
f.power.bg:SetTexture(SFrames:GetTexture())
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Class icon
-- Class icon (anchored to frame top-right corner)
f.classIcon = SFrames:CreateClassIcon(f, 14)
f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)
f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", 0, 0)
f.classIcon.overlay:EnableMouse(false)
-- Texts
@@ -747,12 +744,7 @@ function SFrames.Focus:CreateCastbar()
local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar")
cb:SetHeight(cbH)
cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6)
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
if showPortrait then
cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(cbH + 6), 6)
else
cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
end
cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
local cbbg = CreateFrame("Frame", nil, self.frame)
cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
@@ -816,7 +808,6 @@ function SFrames.Focus:UpdateAll()
self.frame.power:SetMinMaxValues(0, 1)
self.frame.power:SetValue(0)
self.frame.powerText:SetText("")
if self.frame.portrait then self.frame.portrait:Hide() end
if self.frame.classIcon then self.frame.classIcon:Hide(); if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end
self.frame.raidIcon:Hide()
self:HideAuras()
@@ -830,19 +821,6 @@ function SFrames.Focus:UpdateAll()
self:UpdateRaidIcon()
self:UpdateAuras()
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
if showPortrait and self.frame.portrait then
-- Only reset portrait model on first load / focus change (not every update)
if not self.frame._lastPortraitUID or self.frame._lastPortraitUID ~= uid then
self.frame.portrait:SetUnit(uid)
self.frame.portrait:SetCamera(0)
self.frame.portrait:Hide()
self.frame.portrait:Show()
self.frame.portrait:SetPosition(-1.0, 0, 0)
self.frame._lastPortraitUID = uid
end
end
local name = UnitName(uid) or ""
local level = UnitLevel(uid)
local levelText = level
@@ -888,6 +866,7 @@ function SFrames.Focus:UpdateAll()
-- Color by class or reaction
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
if SFrames:IsGradientStyle() then useClassColor = true end
if UnitIsPlayer(uid) and useClassColor then
local _, class = UnitClass(uid)
if class and SFrames.Config.colors.class[class] then
@@ -911,6 +890,9 @@ function SFrames.Focus:UpdateAll()
self.frame.nameText:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b)
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
end
function SFrames.Focus:HideAuras()
@@ -933,7 +915,7 @@ function SFrames.Focus:UpdateHealth()
self.frame.health:SetValue(hp)
if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100)
self.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else
self.frame.healthText:SetText("")
end
@@ -950,6 +932,9 @@ function SFrames.Focus:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end
-- STUB: UpdatePower
@@ -961,10 +946,11 @@ function SFrames.Focus:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
if maxPower > 0 then
self.frame.powerText:SetText(power .. " / " .. maxPower)
self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
self.frame.powerText:SetText("")
end
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, uid)
end
-- STUB: UpdateRaidIcon
@@ -1045,8 +1031,8 @@ function SFrames.Focus:CastbarOnUpdate()
-- 1) Native UnitCastingInfo / UnitChannelInfo (if available)
if uid then
local _UCI = UnitCastingInfo or (CastingInfo and function(u) return CastingInfo(u) end)
local _UCH = UnitChannelInfo or (ChannelInfo and function(u) return ChannelInfo(u) end)
local _UCI = UnitCastingInfo or CastingInfo
local _UCH = UnitChannelInfo or ChannelInfo
if _UCI then
local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid)
if ok and cSpell and cStart then
@@ -1177,7 +1163,6 @@ function SFrames.Focus:OnFocusChanged()
local name = self:GetFocusName()
if name then
self.frame:Show()
self.frame._lastPortraitUID = nil -- Force portrait refresh on focus change
self:UpdateAll()
else
self.frame:Hide()
@@ -1206,9 +1191,28 @@ function SFrames.Focus:ApplySettings()
local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9
local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11
local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
local showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false)
local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == false)
local powerOnTop = SFramesDB and SFramesDB.focusPowerOnTop == true
local gradientStyle = SFrames:IsGradientStyle()
local defaultPowerWidth = width - 2
if defaultPowerWidth < 60 then
defaultPowerWidth = 60
end
local rawPowerWidth = tonumber(SFramesDB and SFramesDB.focusPowerWidth)
local legacyFullWidth = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or width
local maxPowerWidth = gradientStyle and width or (width - 2)
local powerWidth
if gradientStyle then
powerWidth = width
elseif not rawPowerWidth or math.abs(rawPowerWidth - legacyFullWidth) < 0.5 then
powerWidth = defaultPowerWidth
else
powerWidth = rawPowerWidth
end
powerWidth = math.floor(powerWidth + 0.5)
if powerWidth < 60 then powerWidth = 60 end
if powerWidth > maxPowerWidth then powerWidth = maxPowerWidth end
-- Main frame size & scale
f:SetWidth(width)
@@ -1222,40 +1226,75 @@ function SFrames.Focus:ApplySettings()
f:SetBackdropColor(r, g, b, bgAlpha)
end
-- Portrait
if f.portrait then
f.portrait:SetWidth(pWidth)
f.portrait:SetHeight(totalH - 2)
if showPortrait then
f.portrait:Show()
if f.portraitBG then f.portraitBG:Show() end
else
f.portrait:Hide()
if f.portraitBG then f.portraitBG:Hide() end
end
end
-- Portrait always hidden (focus frame uses class icon only)
if f.portrait then f.portrait:Hide() end
if f.portraitBG then f.portraitBG:Hide() end
-- Health bar anchors
-- Health bar anchors (always full width, no portrait)
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
if showPortrait then
f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
else
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
end
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
f.health:SetHeight(hHeight)
end
-- Power bar anchors
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
if showPortrait then
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
else
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -1 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
f.power:SetWidth(powerWidth)
f.power:SetHeight(pHeight)
end
if f.health and f.power then
local healthLevel = f:GetFrameLevel() + 2
local powerLevel = powerOnTop and (healthLevel + 1) or (healthLevel - 1)
f.health:SetFrameLevel(healthLevel)
f.power:SetFrameLevel(powerLevel)
end
if SFrames:IsGradientStyle() then
SFrames:ClearBackdrop(f)
SFrames:ClearBackdrop(f.healthBGFrame)
SFrames:ClearBackdrop(f.powerBGFrame)
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
f.health:SetHeight(hHeight)
end
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -2 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
f.power:SetWidth(powerWidth)
f.power:SetHeight(pHeight)
end
SFrames:ApplyGradientStyle(f.health)
SFrames:ApplyGradientStyle(f.power)
if f.healthBGFrame then
f.healthBGFrame:ClearAllPoints()
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
f.healthBGFrame:Hide()
end
if f.powerBGFrame then
f.powerBGFrame:ClearAllPoints()
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
f.powerBGFrame:Hide()
end
if f.health and f.health.bg then f.health.bg:Hide() end
if f.power and f.power.bg then f.power.bg:Hide() end
else
SFrames:ApplyConfiguredUnitBackdrop(f, "focus")
if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "focus") end
if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "focus") end
SFrames:RemoveGradientStyle(f.health)
SFrames:RemoveGradientStyle(f.power)
if f.healthBGFrame then f.healthBGFrame:Show() end
if f.powerBGFrame then f.powerBGFrame:Show() end
if f.health and f.health.bg then f.health.bg:Show() end
if f.power and f.power.bg then f.power.bg:Show() end
end
-- Castbar anchors
@@ -1264,11 +1303,7 @@ function SFrames.Focus:ApplySettings()
local cbH = 12
f.castbar:SetHeight(cbH)
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
if showPortrait then
f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(cbH + 6), 6)
else
f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
end
f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
if not showCastBar then
f.castbar:Hide()
if f.castbar.cbbg then f.castbar.cbbg:Hide() end
@@ -1298,8 +1333,6 @@ function SFrames.Focus:ApplySettings()
self:UpdateAuras()
end
-- Force portrait refresh
f._lastPortraitUID = nil
if self:GetFocusName() then
self:UpdateAll()
end
@@ -1336,7 +1369,6 @@ function SFrames.Focus:Initialize()
ef:RegisterEvent("UNIT_MAXRAGE")
ef:RegisterEvent("UNIT_DISPLAYPOWER")
ef:RegisterEvent("UNIT_AURA")
ef:RegisterEvent("UNIT_PORTRAIT_UPDATE")
ef:RegisterEvent("UNIT_TARGET")
ef:RegisterEvent("RAID_TARGET_UPDATE")
-- Combat log events for castbar detection (non-SuperWoW fallback)
@@ -1378,7 +1410,6 @@ function SFrames.Focus:Initialize()
if event == "PLAYER_TARGET_CHANGED" then
local focusName = focusSelf:GetFocusName()
if focusName and UnitExists("target") and UnitName("target") == focusName then
focusSelf.frame._lastPortraitUID = nil
focusSelf:UpdateAll()
-- Try to grab GUID while we have target
if UnitGUID then
@@ -1512,7 +1543,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.health:SetValue(hp)
if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100)
focusSelf.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
focusSelf.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else
focusSelf.frame.healthText:SetText("")
end
@@ -1526,7 +1557,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power)
if maxPower > 0 then
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
focusSelf.frame.powerText:SetText("")
end
@@ -1545,22 +1576,13 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power)
if maxPower > 0 then
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
focusSelf.frame.powerText:SetText("")
end
end
elseif event == "UNIT_AURA" then
focusSelf:UpdateAuras()
elseif event == "UNIT_PORTRAIT_UPDATE" then
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
if showPortrait and focusSelf.frame.portrait and evtUID then
focusSelf.frame._lastPortraitUID = nil
focusSelf.frame.portrait:SetUnit(evtUID)
focusSelf.frame.portrait:SetCamera(0)
focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
focusSelf.frame._lastPortraitUID = evtUID
end
end
end
end)
@@ -1585,8 +1607,6 @@ function SFrames.Focus:Initialize()
focusSelf.frame:Show()
end
-- Re-scan for a valid unitID every poll cycle
-- This catches cases where focus becomes target/party/raid dynamically
local uid = focusSelf:GetUnitID()
if uid then
focusSelf:UpdateHealth()
@@ -1594,31 +1614,19 @@ function SFrames.Focus:Initialize()
focusSelf:UpdatePower()
focusSelf:UpdateAuras()
focusSelf:UpdateRaidIcon()
-- Only refresh portrait when unitID changes (prevents 3D model flicker)
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
if showPortrait and focusSelf.frame.portrait then
if uid ~= ef.lastUID then
focusSelf.frame.portrait:SetUnit(uid)
focusSelf.frame.portrait:SetCamera(0)
focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
focusSelf.frame.portrait:Show()
ef.lastUID = uid
end
end
else
ef.lastUID = nil
end
end)
-- Register mover
-- Register mover (Y aligned with pet frame, X aligned with target frame)
if SFrames.Movers and SFrames.Movers.RegisterMover then
if SFramesTargetFrame then
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
"TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -10)
"TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -75,
nil, { alwaysShowInLayout = true })
else
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
"LEFT", "UIParent", "LEFT", 250, 0)
"LEFT", "UIParent", "LEFT", 250, 0,
nil, { alwaysShowInLayout = true })
end
end

View File

@@ -1364,20 +1364,6 @@ function GS:HookTooltips()
end
end
local origRef = SetItemRef
if origRef then
SetItemRef = function(link, text, button)
origRef(link, text, button)
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
pcall(function()
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
if itemStr then
ItemRefTooltip._gsScoreAdded = nil
GS:AddScoreToTooltip(ItemRefTooltip, itemStr)
end
end)
end
end
end
--------------------------------------------------------------------------------

View File

@@ -496,20 +496,31 @@ ShowLootPage = function()
row:Show()
end
-- Let the ORIGINAL Blizzard LootFrame_Update run so that native
-- LootButton1-4 get their IDs, slot data, and OnClick set up
-- through the trusted native code path (required for LootSlot).
-- Set up the native LootFrame so it stays alive (required by the
-- engine) but completely invisible. We do NOT call origLootFrameUpdate
-- because it uses a different items-per-page (3 when paginated vs our 4)
-- which mis-calculates slot indices.
if LootFrame then
LootFrame.numLootItems = numItems
LootFrame.page = page
if not LootFrame:IsShown() then LootFrame:Show() end
end
if origLootFrameUpdate then origLootFrameUpdate() end
-- Now reposition the native buttons on top of our visual rows
-- Directly configure native LootButtons with the correct slot for
-- each visual row. SetSlot is the C++ binding that the native
-- LootButton_OnClick / LootFrameItem_OnClick reads via this.slot.
for btnIdx = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. btnIdx]
local row = lootRows[btnIdx]
if nb and row and row:IsShown() and row._qualColor then
local realSlot = row.slotIndex
-- SetSlot is the native C++ method; .slot is the Lua mirror.
if nb.SetSlot then nb:SetSlot(realSlot) end
nb.slot = realSlot
local _, _, _, rq = GetLootSlotInfo(realSlot)
nb.quality = rq or 0
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
@@ -521,10 +532,10 @@ ShowLootPage = function()
nb._nanamiRow = row
nb:SetScript("OnEnter", function()
local slot = this:GetID()
if slot then
local s = this.slot
if s then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetLootItem(slot)
GameTooltip:SetLootItem(s)
if CursorUpdate then CursorUpdate() end
end
local r2 = this._nanamiRow
@@ -724,14 +735,15 @@ local function GetAlertFrame()
return CreateAlertFrame()
end
-- Layout: newest item at bottom (index 1 = oldest = top, last = newest = bottom slot 0)
local function LayoutAlerts()
CreateAlertAnchor()
for i = 1, table.getn(activeAlerts) do
local n = table.getn(activeAlerts)
for i = 1, n do
local af = activeAlerts[i]
if af._fadeState ~= "fading" then
af:ClearAllPoints()
af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (i - 1) * (ALERT_HEIGHT + ALERT_GAP))
end
-- oldest at top, newest at bottom: slot = (n - i)
af:ClearAllPoints()
af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (n - i) * (ALERT_HEIGHT + ALERT_GAP))
end
end
@@ -750,36 +762,15 @@ local function RemoveAlert(frame)
LayoutAlerts()
end
local function StartAlertFade(frame, delay)
frame._fadeState = "waiting"
frame._fadeElapsed = 0
frame._fadeDelay = delay
-- Each alert has its own independent timer; when it expires, just disappear (no float animation)
local function StartAlertTimer(frame, delay)
frame._timerElapsed = 0
frame._timerDelay = delay
frame:SetScript("OnUpdate", function()
this._fadeElapsed = (this._fadeElapsed or 0) + arg1
if this._fadeState == "waiting" then
if this._fadeElapsed >= this._fadeDelay then
this._fadeState = "fading"
this._fadeElapsed = 0
this._baseY = 0
for idx = 1, table.getn(activeAlerts) do
if activeAlerts[idx] == this then
this._baseY = (idx - 1) * (ALERT_HEIGHT + ALERT_GAP)
break
end
end
end
elseif this._fadeState == "fading" then
local p = this._fadeElapsed / ALERT_FADE_DUR
if p >= 1 then
RemoveAlert(this)
else
this:SetAlpha(1 - p)
this:ClearAllPoints()
this:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT",
0, this._baseY + p * ALERT_FLOAT)
end
this._timerElapsed = (this._timerElapsed or 0) + arg1
if this._timerElapsed >= this._timerDelay then
RemoveAlert(this)
end
end)
end
@@ -794,14 +785,15 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
local db = GetDB()
if not db.alertEnable then return end
-- Stack same item: update count and reset timer
for i = 1, table.getn(activeAlerts) do
local af = activeAlerts[i]
if af._itemName == name and af._fadeState == "waiting" then
if af._itemName == name then
af._quantity = (af._quantity or 1) + (quantity or 1)
if af._quantity > 1 then
af.countFS:SetText("x" .. af._quantity)
end
af._fadeElapsed = 0
af._timerElapsed = 0
return
end
end
@@ -815,7 +807,6 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
f._quantity = quantity or 1
f._link = link
-- Set icon texture
local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark"
f.icon:SetTexture(iconTex)
@@ -841,12 +832,12 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
f:SetAlpha(1)
f:Show()
-- New item appended to end of list = bottom position
table.insert(activeAlerts, f)
LayoutAlerts()
local hold = db.alertFadeDelay or ALERT_HOLD
local stagger = table.getn(activeAlerts) * ALERT_STAGGER
StartAlertFade(f, hold + stagger)
StartAlertTimer(f, hold)
end
--------------------------------------------------------------------------------
@@ -1000,8 +991,9 @@ function LD:Initialize()
end
end
-- After the native LootFrame_Update runs (called by the engine or
-- by us), reposition native buttons onto our visual rows.
-- Replace LootFrame_Update: run the original for engine compatibility,
-- then re-apply the correct slot on each native button based on our
-- visual rows (which use ITEMS_PER_PAGE=4, not the native 3-when-paged).
LootFrame_Update = function()
if origLootFrameUpdate then origLootFrameUpdate() end
if not (lootFrame and lootFrame:IsShown()) then return end
@@ -1009,6 +1001,11 @@ function LD:Initialize()
local nb = _G["LootButton" .. i]
local row = lootRows[i]
if nb and row and row:IsShown() and row._qualColor then
local realSlot = row.slotIndex
if realSlot then
if nb.SetSlot then nb:SetSlot(realSlot) end
nb.slot = realSlot
end
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)

442
Mail.lua
View File

@@ -45,6 +45,7 @@ local S = {
inboxRows = {},
currentTab = 1,
inboxPage = 1,
bagPage = 1,
inboxChecked = {},
collectQueue = {},
collectTimer = nil,
@@ -53,11 +54,23 @@ local S = {
isSending = false,
collectElapsed = 0,
multiSend = nil, -- active multi-send state table
codMode = false, -- send panel: 付款取信 mode toggle
}
local L = {
W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28,
BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12,
BAG_SLOT = 38, BAG_GAP = 3, BAG_PER_ROW = 8, BAG_ROWS = 8,
}
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"] = {
text = "确认支付 %s 取回此物品?",
button1 = "确认",
button2 = "取消",
OnAccept = function() end,
timeout = 0,
whileDead = true,
hideOnEscape = true,
}
--------------------------------------------------------------------------------
@@ -476,18 +489,27 @@ local function UpdateInbox()
if money and money > 0 then
row.moneyFrame:SetMoney(money)
elseif CODAmount and CODAmount > 0 then
row.codFS:SetText("COD:"); row.codFS:Show()
row.codFS:SetText("付款:"); row.codFS:Show()
row.moneyFrame:SetMoney(CODAmount)
else
row.moneyFrame:SetMoney(0)
end
row.expiryFS:SetText(FormatExpiry(daysLeft))
local canTake = (hasItem or (money and money > 0)) and (not CODAmount or CODAmount == 0)
local canTake = hasItem or (money and money > 0)
row.takeBtn:SetDisabled(not canTake)
row.takeBtn:SetScript("OnClick", function()
if row.mailIndex then
if hasItem then
TakeInboxItem(row.mailIndex)
if CODAmount and CODAmount > 0 then
local codStr = FormatMoneyString(CODAmount)
local idx = row.mailIndex
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
TakeInboxItem(idx)
end
StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
else
TakeInboxItem(row.mailIndex)
end
elseif money and money > 0 then
local idx = row.mailIndex
TakeInboxMoney(idx)
@@ -543,7 +565,7 @@ end
local function StopCollecting()
S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil
if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end
UpdateInbox()
if S.currentTab == 3 then ML:UpdateMailBag() else UpdateInbox() end
end
local function ProcessCollectQueue()
@@ -683,6 +705,8 @@ end
local function ResetSendForm()
if not S.frame then return end
ClearSendItems()
S.codMode = false
if S.frame.UpdateMoneyToggle then S.frame.UpdateMoneyToggle() end
if S.frame.toEditBox then S.frame.toEditBox:SetText("") end
if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end
if S.frame.bodyEditBox then S.frame.bodyEditBox:SetText("") end
@@ -733,8 +757,15 @@ local function DoMultiSend(recipient, subject, body, money)
local items = {}
for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end
-- No attachments: plain text / money mail
-- No attachments: plain text / money mail (付款取信 requires attachments)
if table.getn(items) == 0 then
if S.codMode and money and money > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 付款取信模式需要添加附件物品")
if S.frame and S.frame.sendBtn then
S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false)
end
return
end
if money and money > 0 then SetSendMailMoney(money) end
SendMail(recipient, subject, body or "")
return
@@ -746,6 +777,7 @@ local function DoMultiSend(recipient, subject, body, money)
subject = subject or "",
body = body or "",
money = money,
codMode = S.codMode,
total = table.getn(items),
sentCount = 0,
phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ...
@@ -803,9 +835,13 @@ local function DoMultiSend(recipient, subject, body, money)
return
end
-- Money only on first mail
if ms.sentCount == 1 and ms.money and ms.money > 0 then
SetSendMailMoney(ms.money)
-- Money or 付款取信
if ms.money and ms.money > 0 then
if ms.codMode then
SetSendMailCOD(ms.money)
elseif ms.sentCount == 1 then
SetSendMailMoney(ms.money)
end
end
-- Send this single-attachment mail
@@ -903,13 +939,18 @@ local function BuildMainFrame()
sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER)
sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
local tabInbox = CreateTabBtn(f, "收件箱", 70)
local tabInbox = CreateTabBtn(f, "收件箱", 62)
tabInbox:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6))
tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end)
f.tabInbox = tabInbox
local tabSend = CreateTabBtn(f, "发送", 70)
tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
local tabBag = CreateTabBtn(f, "邮包", 50)
tabBag:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
tabBag:SetScript("OnClick", function() S.currentTab = 3; ML:ShowMailBagPanel() end)
f.tabBag = tabBag
local tabSend = CreateTabBtn(f, "发送", 62)
tabSend:SetPoint("LEFT", tabBag, "RIGHT", 4, 0)
tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end)
f.tabSend = tabSend
@@ -1128,16 +1169,25 @@ function ML:ShowMailDetail(mailIndex)
dp.detailMoney:SetMoney(money); dp.detailMoney:Show()
end
if CODAmount and CODAmount > 0 then
dp.detailCodLabel:SetText("COD:"); dp.detailCodLabel:Show()
dp.detailCodLabel:SetText("付款取信:"); dp.detailCodLabel:Show()
dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show()
end
-- Take items button
local canTakeItem = hasItem and (not CODAmount or CODAmount == 0)
local canTakeItem = hasItem
dp.takeItemBtn:SetDisabled(not canTakeItem)
dp.takeItemBtn:SetScript("OnClick", function()
if S.detailMailIndex then
TakeInboxItem(S.detailMailIndex)
if CODAmount and CODAmount > 0 then
local codStr = FormatMoneyString(CODAmount)
local idx = S.detailMailIndex
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
TakeInboxItem(idx)
end
StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
else
TakeInboxItem(S.detailMailIndex)
end
end
end)
@@ -1205,6 +1255,294 @@ function ML:HideMailDetail()
UpdateInbox()
end
--------------------------------------------------------------------------------
-- BUILD: Mail Bag panel (grid view of all inbox items)
--------------------------------------------------------------------------------
local function BuildMailBagPanel()
local f = S.frame
local panelTop = L.HEADER + 6 + L.TAB_H + 4
local bp = CreateFrame("Frame", nil, f)
bp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop)
bp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
bp:Hide()
f.bagPanel = bp
local font = GetFont()
local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
local infoFS = bp:CreateFontString(nil, "OVERLAY")
infoFS:SetFont(font, 10, "OUTLINE")
infoFS:SetPoint("TOPLEFT", bp, "TOPLEFT", L.PAD, -2)
infoFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
f.bagInfoFS = infoFS
local codLegend = bp:CreateFontString(nil, "OVERLAY")
codLegend:SetFont(font, 9, "OUTLINE")
codLegend:SetPoint("TOPRIGHT", bp, "TOPRIGHT", -L.PAD, -2)
codLegend:SetText("|cFFFF5555■|r 付款取信")
codLegend:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
f.bagSlots = {}
local gridTop = 16
for i = 1, slotsPerPage do
local row = math.floor((i - 1) / L.BAG_PER_ROW)
local col = math.mod((i - 1), L.BAG_PER_ROW)
local sf = CreateFrame("Button", "SFramesMailBagSlot" .. i, bp)
sf:SetWidth(L.BAG_SLOT); sf:SetHeight(L.BAG_SLOT)
sf:SetPoint("TOPLEFT", bp, "TOPLEFT",
L.PAD + col * (L.BAG_SLOT + L.BAG_GAP),
-(gridTop + row * (L.BAG_SLOT + L.BAG_GAP)))
sf:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
sf:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
sf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
local ico = sf:CreateTexture(nil, "ARTWORK")
ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
ico:SetPoint("TOPLEFT", 3, -3); ico:SetPoint("BOTTOMRIGHT", -3, 3)
ico:Hide()
sf.icon = ico
local cnt = sf:CreateFontString(nil, "OVERLAY")
cnt:SetFont(font, 11, "OUTLINE")
cnt:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -2, 2)
cnt:SetJustifyH("RIGHT")
sf.countFS = cnt
local moneyFS = sf:CreateFontString(nil, "OVERLAY")
moneyFS:SetFont(font, 8, "OUTLINE")
moneyFS:SetPoint("BOTTOM", sf, "BOTTOM", 0, 2)
moneyFS:SetWidth(L.BAG_SLOT)
moneyFS:SetJustifyH("CENTER")
moneyFS:Hide()
sf.moneyFS = moneyFS
local codFS = sf:CreateFontString(nil, "OVERLAY")
codFS:SetFont(font, 7, "OUTLINE")
codFS:SetPoint("TOP", sf, "TOP", 0, -1)
codFS:SetText("|cFFFF3333付款|r")
codFS:Hide()
sf.codFS = codFS
sf.mailData = nil
sf:RegisterForClicks("LeftButtonUp")
sf:SetScript("OnClick", function()
local data = this.mailData
if not data then return end
if data.codAmount and data.codAmount > 0 then
local codStr = FormatMoneyString(data.codAmount)
local idx = data.mailIndex
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
TakeInboxItem(idx)
end
StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
elseif data.hasItem then
TakeInboxItem(data.mailIndex)
elseif data.money and data.money > 0 then
local idx = data.mailIndex
TakeInboxMoney(idx)
if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end
S.deleteElapsed = 0
S.deleteTimer:SetScript("OnUpdate", function()
S.deleteElapsed = S.deleteElapsed + arg1
if S.deleteElapsed >= 0.5 then
this:SetScript("OnUpdate", nil)
if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end
end
end)
end
end)
sf:SetScript("OnEnter", function()
this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
local data = this.mailData
if not data then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
if data.hasItem then
pcall(GameTooltip.SetInboxItem, GameTooltip, data.mailIndex)
else
GameTooltip:AddLine("金币邮件", 1, 0.84, 0)
end
GameTooltip:AddLine(" ")
if data.sender then
GameTooltip:AddLine("发件人: " .. data.sender, 0.7, 0.7, 0.7)
end
if data.subject and data.subject ~= "" then
GameTooltip:AddLine(data.subject, 0.5, 0.5, 0.5)
end
if data.codAmount and data.codAmount > 0 then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("付款取信: " .. FormatMoneyString(data.codAmount), 1, 0.3, 0.3)
GameTooltip:AddLine("|cFFFFCC00点击支付并取回|r")
elseif data.money and data.money > 0 and not data.hasItem then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("金额: " .. FormatMoneyString(data.money), 1, 0.84, 0)
GameTooltip:AddLine("|cFFFFCC00点击收取金币|r")
elseif data.hasItem then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("|cFFFFCC00点击收取物品|r")
end
GameTooltip:Show()
end)
sf:SetScript("OnLeave", function()
local data = this.mailData
if data and data.codAmount and data.codAmount > 0 then
this:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
else
this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
GameTooltip:Hide()
end)
f.bagSlots[i] = sf
end
local bsep = bp:CreateTexture(nil, "ARTWORK")
bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1)
bsep:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", 6, L.BOTTOM)
bsep:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -6, L.BOTTOM)
bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
local prev = CreateActionBtn(bp, "<", 28)
prev:SetHeight(22); prev:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", L.PAD, 12)
prev:SetScript("OnClick", function() S.bagPage = S.bagPage - 1; ML:UpdateMailBag() end)
f.bagPrevBtn = prev
local nxt = CreateActionBtn(bp, ">", 28)
nxt:SetHeight(22); nxt:SetPoint("LEFT", prev, "RIGHT", 4, 0)
nxt:SetScript("OnClick", function() S.bagPage = S.bagPage + 1; ML:UpdateMailBag() end)
f.bagNextBtn = nxt
local pageFS = bp:CreateFontString(nil, "OVERLAY")
pageFS:SetFont(font, 10, "OUTLINE")
pageFS:SetPoint("LEFT", nxt, "RIGHT", 8, 0)
pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
f.bagPageFS = pageFS
local colAll = CreateActionBtn(bp, "全部收取", 80)
colAll:SetHeight(24); colAll:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -L.PAD, 10)
colAll:SetScript("OnClick", function()
if S.isCollecting then StopCollecting() else CollectAll() end
end)
f.bagCollectAllBtn = colAll
end
--------------------------------------------------------------------------------
-- Mail Bag: Update
--------------------------------------------------------------------------------
function ML:UpdateMailBag()
if not S.frame or not S.frame:IsVisible() or S.currentTab ~= 3 then return end
local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
local numMails = GetInboxNumItems()
local entries = {}
for mi = 1, numMails do
local _, _, sender, subject, money, CODAmount, daysLeft, hasItem = GetInboxHeaderInfo(mi)
if hasItem or (money and money > 0) or (CODAmount and CODAmount > 0) then
local itemName, itemTex
if hasItem then itemName, itemTex = GetInboxItem(mi) end
table.insert(entries, {
mailIndex = mi,
hasItem = hasItem,
itemName = itemName,
itemTexture = itemTex,
money = money or 0,
codAmount = CODAmount or 0,
sender = sender or "未知",
subject = subject or "",
daysLeft = daysLeft,
})
end
end
local totalEntries = table.getn(entries)
local totalPages = math.max(1, math.ceil(totalEntries / slotsPerPage))
if S.bagPage > totalPages then S.bagPage = totalPages end
if S.bagPage < 1 then S.bagPage = 1 end
S.frame.bagInfoFS:SetText(string.format("共 %d 件可收取 (%d 封邮件)", totalEntries, numMails))
S.frame.bagPageFS:SetText(string.format("第 %d/%d 页", S.bagPage, totalPages))
S.frame.bagPrevBtn:SetDisabled(S.bagPage <= 1)
S.frame.bagNextBtn:SetDisabled(S.bagPage >= totalPages)
S.frame.bagCollectAllBtn:SetDisabled(numMails == 0 and not S.isCollecting)
if S.isCollecting then
S.frame.bagCollectAllBtn.label:SetText("收取中...")
else
S.frame.bagCollectAllBtn.label:SetText("全部收取")
end
for i = 1, slotsPerPage do
local slot = S.frame.bagSlots[i]
local ei = (S.bagPage - 1) * slotsPerPage + i
local entry = entries[ei]
if entry then
slot.mailData = entry
if entry.hasItem and entry.itemTexture then
slot.icon:SetTexture(entry.itemTexture)
elseif entry.money > 0 then
slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
else
slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01")
end
slot.icon:Show()
slot.countFS:SetText("")
slot.moneyFS:Hide()
if entry.money > 0 and not entry.hasItem then
local g = math.floor(entry.money / 10000)
if g > 0 then
slot.moneyFS:SetText("|cFFFFD700" .. g .. "g|r")
else
local sv = math.floor(math.mod(entry.money, 10000) / 100)
if sv > 0 then
slot.moneyFS:SetText("|cFFC7C7CF" .. sv .. "s|r")
else
local cv = math.mod(entry.money, 100)
slot.moneyFS:SetText("|cFFB87333" .. cv .. "c|r")
end
end
slot.moneyFS:Show()
end
if entry.codAmount > 0 then
slot.codFS:Show()
slot:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
else
slot.codFS:Hide()
slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
slot:Show()
else
slot.mailData = nil
slot.icon:Hide()
slot.countFS:SetText("")
slot.moneyFS:Hide()
slot.codFS:Hide()
slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
slot:Show()
end
end
end
--------------------------------------------------------------------------------
-- Mail Bag: Panel Switching
--------------------------------------------------------------------------------
function ML:ShowMailBagPanel()
if not S.frame then return end
S.frame.tabInbox:SetActive(false)
S.frame.tabBag:SetActive(true)
S.frame.tabSend:SetActive(false)
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
S.detailMailIndex = nil
S.frame.inboxPanel:Hide()
S.frame.sendPanel:Hide()
S.frame.bagPanel:Show()
ML:UpdateMailBag()
end
--------------------------------------------------------------------------------
-- BUILD: Send panel
--------------------------------------------------------------------------------
@@ -1555,29 +1893,56 @@ local function BuildSendPanel()
bsf:SetScrollChild(bodyEB)
f.bodyEditBox = bodyEB
-- Money row
local mLabel = sp:CreateFontString(nil, "OVERLAY")
mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
-- Money mode toggle (附加金币 / 付款取信)
local mToggle = CreateActionBtn(sp, "附加金币", 72)
mToggle:SetHeight(20); mToggle:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
f.moneyToggle = mToggle
local function UpdateMoneyToggle()
if S.codMode then
mToggle.label:SetText("|cFFFF5555付款取信|r")
mToggle:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
else
mToggle.label:SetText("附加金币")
mToggle:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
end
end
f.UpdateMoneyToggle = UpdateMoneyToggle
mToggle:SetScript("OnClick", function()
S.codMode = not S.codMode; UpdateMoneyToggle()
end)
mToggle:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOPRIGHT")
if S.codMode then
GameTooltip:AddLine("付款取信模式", 1, 0.5, 0.5)
GameTooltip:AddLine("收件人需支付指定金额才能取回附件", 0.8, 0.8, 0.8)
else
GameTooltip:AddLine("附加金币模式", 1, 0.84, 0)
GameTooltip:AddLine("随邮件附送金币给收件人", 0.8, 0.8, 0.8)
end
GameTooltip:AddLine("|cFFFFCC00点击切换模式|r")
GameTooltip:Show()
end)
mToggle:SetScript("OnLeave", function() GameTooltip:Hide() end)
UpdateMoneyToggle()
local gL = sp:CreateFontString(nil, "OVERLAY")
gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mLabel, "RIGHT", 6, 0)
gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mToggle, "RIGHT", 6, 0)
gL:SetText(""); gL:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3])
local gEB = CreateStyledEditBox(sp, 60, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
local gEB = CreateStyledEditBox(sp, 50, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
local sL = sp:CreateFontString(nil, "OVERLAY")
sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0)
sL:SetText(""); sL:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3])
local sEB = CreateStyledEditBox(sp, 40, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
local sEB = CreateStyledEditBox(sp, 36, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
local cL = sp:CreateFontString(nil, "OVERLAY")
cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0)
cL:SetText(""); cL:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3])
local cEB = CreateStyledEditBox(sp, 40, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
local cEB = CreateStyledEditBox(sp, 36, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
-- Attachments
local aLabel = sp:CreateFontString(nil, "OVERLAY")
aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mLabel, "TOPLEFT", 0, -28)
aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mToggle, "TOPLEFT", 0, -28)
aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local clrBtn = CreateActionBtn(sp, "清空", 50)
@@ -1679,7 +2044,7 @@ local function SetupEvents()
f:SetScript("OnEvent", function()
if event == "MAIL_SHOW" then
if SFramesDB and SFramesDB.enableMail == false then return end
S.currentTab = 1; S.inboxPage = 1; S.inboxChecked = {}
S.currentTab = 1; S.inboxPage = 1; S.bagPage = 1; S.inboxChecked = {}
CheckInbox(); f:Show(); ML:ShowInboxPanel()
elseif event == "MAIL_INBOX_UPDATE" then
if f:IsVisible() then
@@ -1689,10 +2054,28 @@ local function SetupEvents()
else
ML:HideMailDetail()
end
elseif S.currentTab == 3 then
ML:UpdateMailBag()
else
UpdateInbox()
end
end
-- 收件箱清空后同步小地图信件图标状态
if MiniMapMailFrame then
if HasNewMail and HasNewMail() then
MiniMapMailFrame:Show()
elseif GetInboxNumItems() == 0 then
MiniMapMailFrame:Hide()
end
end
elseif event == "UPDATE_PENDING_MAIL" then
if MiniMapMailFrame then
if HasNewMail and HasNewMail() then
MiniMapMailFrame:Show()
else
MiniMapMailFrame:Hide()
end
end
elseif event == "MAIL_CLOSED" then
if S.multiSend then AbortMultiSend("邮箱已关闭") end
f:Hide()
@@ -1721,6 +2104,7 @@ local function SetupEvents()
f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE")
f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS")
f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED")
f:RegisterEvent("UPDATE_PENDING_MAIL")
if MailFrame then
local origMailOnShow = MailFrame:GetScript("OnShow")
@@ -1728,6 +2112,7 @@ local function SetupEvents()
if origMailOnShow then origMailOnShow() end
this:ClearAllPoints()
this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
this:SetAlpha(0)
this:EnableMouse(false)
end)
for i = table.getn(UISpecialFrames), 1, -1 do
@@ -1772,6 +2157,7 @@ function ML:Initialize()
BuildMainFrame()
BuildInboxPanel()
BuildDetailPanel()
BuildMailBagPanel()
BuildSendPanel()
SetupEvents()
end
@@ -1781,19 +2167,19 @@ end
--------------------------------------------------------------------------------
function ML:ShowInboxPanel()
if not S.frame then return end
S.frame.tabInbox:SetActive(true); S.frame.tabSend:SetActive(false)
S.frame.tabInbox:SetActive(true); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(false)
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide()
S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide(); S.frame.bagPanel:Hide()
S.detailMailIndex = nil
UpdateInbox()
end
function ML:ShowSendPanel()
if not S.frame then return end
S.frame.tabInbox:SetActive(false); S.frame.tabSend:SetActive(true)
S.frame.tabInbox:SetActive(false); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(true)
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
S.detailMailIndex = nil
S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show()
S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show(); S.frame.bagPanel:Hide()
if S.frame.sendStatus then S.frame.sendStatus:SetText("") end
if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end
ML:UpdateSendPanel()

View File

@@ -127,6 +127,64 @@ local function GetZoneYards()
return nil, nil
end
local function IsMapStateProtected()
if WorldMapFrame and WorldMapFrame:IsVisible() then
return true
end
if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
return true
end
if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
return true
end
return false
end
local function SafeSetMapToCurrentZone()
if not SetMapToCurrentZone then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
end
return pcall(SetMapToCurrentZone)
end
local function SafeSetMapZoom(continent, zone)
if not SetMapZoom then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
end
return pcall(SetMapZoom, continent, zone)
end
local function WithPlayerZoneMap(func)
if type(func) ~= "function" then
return
end
if IsMapStateProtected() or not SetMapToCurrentZone then
return func()
end
local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0
SafeSetMapToCurrentZone()
local results = { func() }
if SetMapZoom then
if savedZ and savedZ > 0 and savedC and savedC > 0 then
SafeSetMapZoom(savedC, savedZ)
elseif savedC and savedC > 0 then
SafeSetMapZoom(savedC, 0)
end
end
return unpack(results)
end
--------------------------------------------------------------------------------
-- 1. World Map + Battlefield Minimap: class icon overlays
--------------------------------------------------------------------------------
@@ -224,97 +282,95 @@ local function UpdateMinimapDots()
return
end
if WorldMapFrame and WorldMapFrame:IsVisible() then
if IsMapStateProtected() then
return
end
if SetMapToCurrentZone then
pcall(SetMapToCurrentZone)
end
WithPlayerZoneMap(function()
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
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 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
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)
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 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)
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
dot.icon:SetVertexColor(1, 0.82, 0, 1)
if mmDots[i] then mmDots[i]:Hide() end
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)
end
--------------------------------------------------------------------------------

View File

@@ -34,6 +34,26 @@ local function GetOverlayDB()
return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
end
local function SafeSetMapToCurrentZone()
if not SetMapToCurrentZone then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
end
return pcall(SetMapToCurrentZone)
end
local function SafeSetMapZoom(continent, zone)
if not SetMapZoom then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
end
return pcall(SetMapZoom, continent, zone)
end
--------------------------------------------------------------------------------
-- Persistence: save/load discovered overlay data to SFramesDB
--------------------------------------------------------------------------------
@@ -428,7 +448,7 @@ local function ProcessScanZone()
end
local entry = scanQueue[scanIndex]
SetMapZoom(entry.cont, entry.zone)
SafeSetMapZoom(entry.cont, entry.zone)
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile == "" then
@@ -490,11 +510,11 @@ function MapReveal:FinishScan()
end
if savedMapZ > 0 then
SetMapZoom(savedMapC, savedMapZ)
SafeSetMapZoom(savedMapC, savedMapZ)
elseif savedMapC > 0 then
SetMapZoom(savedMapC, 0)
SafeSetMapZoom(savedMapC, 0)
else
if SetMapToCurrentZone then SetMapToCurrentZone() end
SafeSetMapToCurrentZone()
end
local cf = DEFAULT_CHAT_FRAME

View File

@@ -62,6 +62,27 @@ function SFrames:GetTexture()
return self.Media.statusbar
end
function SFrames:ResolveBarTexture(settingKey, fallbackKey)
local key
if SFramesDB and settingKey then
key = SFramesDB[settingKey]
end
if (not key or key == "") and SFramesDB and fallbackKey then
key = SFramesDB[fallbackKey]
end
if key and key ~= "" then
local builtin = self._barTextureLookup[key]
if builtin then return builtin end
local LSM = self:GetSharedMedia()
if LSM then
local path = LSM:Fetch("statusbar", key, true)
if path then return path end
end
end
return self:GetTexture()
end
function SFrames:GetFont()
-- 1. Check built-in font key
if SFramesDB and SFramesDB.fontKey then
@@ -79,6 +100,40 @@ function SFrames:GetFont()
return self.Media.font
end
function SFrames:ResolveFont(settingKey, fallbackKey)
local key
if SFramesDB and settingKey then
key = SFramesDB[settingKey]
end
if (not key or key == "") and SFramesDB and fallbackKey then
key = SFramesDB[fallbackKey]
end
if key and key ~= "" then
local builtin = self._fontLookup[key]
if builtin then return builtin end
local LSM = self:GetSharedMedia()
if LSM then
local path = LSM:Fetch("font", key, true)
if path then return path end
end
end
return self:GetFont()
end
function SFrames:ResolveFontOutline(settingKey, fallbackKey)
local outline
if SFramesDB and settingKey then
outline = SFramesDB[settingKey]
end
if (not outline or outline == "") and SFramesDB and fallbackKey then
outline = SFramesDB[fallbackKey]
end
if outline and outline ~= "" then
return outline
end
return self.Media.fontOutline or "OUTLINE"
end
function SFrames:GetSharedMediaList(mediaType)
local LSM = self:GetSharedMedia()
if LSM and LSM.List then return LSM:List(mediaType) end

View File

@@ -93,6 +93,19 @@ local function GetDB()
return db
end
local function IsMapStateProtected()
if WorldMapFrame and WorldMapFrame:IsVisible() then
return true
end
if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
return true
end
if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
return true
end
return false
end
local function ResolveStyleKey()
local key = GetDB().mapStyle or "auto"
if key == "auto" then
@@ -152,6 +165,34 @@ local function ApplyPosition()
end
end
local function HideTooltipIfOwned(frame)
if not frame or not GameTooltip or not GameTooltip:IsVisible() then
return
end
if GameTooltip.IsOwned and GameTooltip:IsOwned(frame) then
GameTooltip:Hide()
return
end
if GameTooltip.GetOwner and GameTooltip:GetOwner() == frame then
GameTooltip:Hide()
end
end
local function ForceHideMinimapFrame(frame)
if not frame then return end
HideTooltipIfOwned(frame)
if frame.EnableMouse then
frame:EnableMouse(false)
end
if frame.Hide then
frame:Hide()
end
if frame.SetAlpha then
frame:SetAlpha(0)
end
frame.Show = function() end
end
--------------------------------------------------------------------------------
-- Hide default Blizzard minimap chrome
-- MUST be called AFTER BuildFrame (Minimap is already reparented)
@@ -165,15 +206,13 @@ local function HideDefaultElements()
MinimapToggleButton,
MiniMapWorldMapButton,
GameTimeFrame,
TimeManagerClockButton,
MinimapZoneTextButton,
MiniMapTracking,
MinimapBackdrop,
}
for _, f in ipairs(kill) do
if f then
f:Hide()
f.Show = function() end
end
ForceHideMinimapFrame(f)
end
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
@@ -183,10 +222,7 @@ local function HideDefaultElements()
}
for _, name in ipairs(trackNames) do
local f = _G[name]
if f and f.Hide then
f:Hide()
f.Show = function() end
end
ForceHideMinimapFrame(f)
end
-- Also hide any tracking textures that are children of Minimap
@@ -195,8 +231,7 @@ local function HideDefaultElements()
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
ForceHideMinimapFrame(child)
end
end
end
@@ -455,8 +490,15 @@ local function UpdateZoneText()
end
local function SetZoneMap()
if IsMapStateProtected() then
return
end
if SetMapToCurrentZone then
pcall(SetMapToCurrentZone)
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
else
pcall(SetMapToCurrentZone)
end
end
end

View File

@@ -78,6 +78,43 @@ local DEBUFF_TYPE_COLORS = {
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 IsTooltipOwnedBy(frame)
if not frame or not GameTooltip or not GameTooltip:IsVisible() then
return false
end
if GameTooltip.IsOwned then
return GameTooltip:IsOwned(frame)
end
if GameTooltip.GetOwner then
return GameTooltip:GetOwner() == frame
end
return false
end
local function HideOwnedTooltip(frame)
if IsTooltipOwnedBy(frame) then
GameTooltip:Hide()
end
end
local function SetSlotTooltipKey(btn, key)
if btn._sfTooltipKey ~= key then
HideOwnedTooltip(btn)
btn._sfTooltipKey = key
end
end
local function ClearSlotState(btn)
HideOwnedTooltip(btn)
btn.buffIndex = -1
btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn._sfTooltipKey = nil
end
local function HideBlizzardBuffs()
for i = 0, 23 do
local btn = _G["BuffButton" .. i]
@@ -170,18 +207,19 @@ local function CreateSlot(parent, namePrefix, index, isBuff)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:SetScript("OnHide", function()
HideOwnedTooltip(this)
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
HideOwnedTooltip(this)
CancelPlayerBuff(this.buffIndex)
end
end)
btn.buffIndex = -1
btn._sfSimulated = false
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
ClearSlotState(btn)
btn:Hide()
return btn
end
@@ -339,9 +377,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
SetSlotTooltipKey(btn, "buff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
@@ -360,10 +401,8 @@ function MB:UpdateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
ClearSlotState(btn)
btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end
end
end
@@ -378,9 +417,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 16)
if texture then
SetSlotTooltipKey(btn, "weapon:16")
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = true
btn._weaponSlotID = 16
@@ -407,9 +449,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 17)
if texture then
SetSlotTooltipKey(btn, "weapon:17")
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = true
btn._weaponSlotID = 17
@@ -432,10 +477,8 @@ function MB:UpdateBuffs()
for j = slotIdx + 1, MAX_BUFFS do
local btn = self.buffSlots[j]
ClearSlotState(btn)
btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end
self:UpdateDebuffs()
@@ -450,6 +493,7 @@ function MB:UpdateDebuffs()
if db.showDebuffs == false then
for i = 1, MAX_DEBUFFS do
ClearSlotState(self.debuffSlots[i])
self.debuffSlots[i]:Hide()
end
if self.debuffContainer then self.debuffContainer:Hide() end
@@ -467,9 +511,14 @@ function MB:UpdateDebuffs()
local btn = self.debuffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
SetSlotTooltipKey(btn, "debuff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
local apps = GetPlayerBuffApplications(buffIndex)
if apps and apps > 1 then
@@ -495,15 +544,15 @@ function MB:UpdateDebuffs()
btn:Show()
else
ClearSlotState(btn)
btn:Hide()
btn.buffIndex = -1
end
end
end
for j = slotIdx + 1, MAX_DEBUFFS do
ClearSlotState(self.debuffSlots[j])
self.debuffSlots[j]:Hide()
self.debuffSlots[j].buffIndex = -1
end
end
@@ -556,6 +605,7 @@ function MB:SimulateBuffs()
local btn = self.buffSlots[i]
local sim = SIM_BUFFS[i]
if sim then
SetSlotTooltipKey(btn, "sim-buff:" .. tostring(i))
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
@@ -578,6 +628,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
ClearSlotState(btn)
btn:Hide()
end
end
@@ -586,11 +637,14 @@ function MB:SimulateBuffs()
local btn = self.debuffSlots[i]
local sim = SIM_DEBUFFS[i]
if sim then
SetSlotTooltipKey(btn, "sim-debuff:" .. tostring(i))
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
btn._sfSimLabel = sim.label
btn._sfSimDesc = sim.desc
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn.timer:SetText(sim.time)
ApplyTimerColor(btn, sim.time)
@@ -601,6 +655,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
btn:Show()
else
ClearSlotState(btn)
btn:Hide()
end
end

View File

@@ -207,8 +207,12 @@ local function SyncMoverToFrame(name)
local w = (frame:GetWidth() or 100) * scale
local h = (frame:GetHeight() or 50) * scale
mover:SetWidth(math.max(w, 72))
mover:SetHeight(math.max(h, 40))
local minW = 72
local minH = 40
if h > w * 3 then minW = 40 end
if w > h * 3 then minH = 24 end
mover:SetWidth(math.max(w, minW))
mover:SetHeight(math.max(h, minH))
local l = frame:GetLeft()
local b = frame:GetBottom()
@@ -256,17 +260,12 @@ local function SyncFrameToMover(name)
local pos = positions[name]
if pos then
frame:ClearAllPoints()
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local newL = frame:GetLeft() or 0
local newB = frame:GetBottom() or 0
local dL = newL * scale - moverL
local dB = newB * scale - moverB
if math.abs(dL) > 2 or math.abs(dB) > 2 then
SFrames:Print(string.format(
"|cffff6666[位置偏差]|r |cffaaddff%s|r anchor=%s ofs=(%.1f,%.1f) dL=%.1f dB=%.1f scale=%.2f",
entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale))
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
end
@@ -720,11 +719,14 @@ local function CreateControlBar()
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
local titleC = th.title or { 1, 0.88, 1 }
local ROW_Y_TOP = 12
local ROW_Y_BOT = -12
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
controlBar:SetFrameLevel(200)
controlBar:SetWidth(480)
controlBar:SetHeight(44)
controlBar:SetHeight(72)
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
controlBar:SetClampedToScreen(true)
controlBar:SetMovable(true)
@@ -739,7 +741,7 @@ local function CreateControlBar()
local title = controlBar:CreateFontString(nil, "OVERLAY")
title:SetFont(Font(), 13, "OUTLINE")
title:SetPoint("LEFT", controlBar, "LEFT", 14, 0)
title:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_TOP)
title:SetText("Nanami 布局")
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
@@ -747,17 +749,19 @@ local function CreateControlBar()
sep:SetTexture("Interface\\Buttons\\WHITE8x8")
sep:SetWidth(1)
sep:SetHeight(24)
sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0)
sep:SetPoint("LEFT", controlBar, "LEFT", 118, ROW_Y_TOP)
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
local bx = 128
-- Snap toggle
-- Snap toggle (row 1)
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
local cfg = GetLayoutCfg()
cfg.snapEnabled = not cfg.snapEnabled
if controlBar._updateSnap then controlBar._updateSnap() end
end)
snapBtn:ClearAllPoints()
snapBtn:SetPoint("LEFT", controlBar, "LEFT", bx, ROW_Y_TOP)
controlBar.snapBtn = snapBtn
local function UpdateSnapBtnText()
@@ -773,7 +777,7 @@ local function CreateControlBar()
end
controlBar._updateSnap = UpdateSnapBtnText
-- Grid toggle
-- Grid toggle (row 1)
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
local cfg = GetLayoutCfg()
cfg.showGrid = not cfg.showGrid
@@ -782,6 +786,8 @@ local function CreateControlBar()
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
end
end)
gridBtn:ClearAllPoints()
gridBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 84, ROW_Y_TOP)
controlBar.gridBtn = gridBtn
local function UpdateGridBtnText()
@@ -797,18 +803,20 @@ local function CreateControlBar()
end
controlBar._updateGrid = UpdateGridBtnText
-- Reset all
-- Reset all (row 1)
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
M:ResetAllMovers()
end)
resetBtn:ClearAllPoints()
resetBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 176, ROW_Y_TOP)
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
-- Close
-- Close (row 1)
local closeBtn = CreateFrame("Button", nil, controlBar)
closeBtn:SetWidth(60)
closeBtn:SetHeight(26)
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, ROW_Y_TOP)
closeBtn:SetBackdrop(ROUND_BACKDROP)
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
@@ -837,6 +845,64 @@ local function CreateControlBar()
end)
controlBar.closeBtn = closeBtn
-- Row separator
local rowSep = controlBar:CreateTexture(nil, "ARTWORK")
rowSep:SetTexture("Interface\\Buttons\\WHITE8x8")
rowSep:SetHeight(1)
rowSep:SetPoint("LEFT", controlBar, "LEFT", 10, 0)
rowSep:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
rowSep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.35)
-- Row 2: Preset buttons
local presetLabel = controlBar:CreateFontString(nil, "OVERLAY")
presetLabel:SetFont(Font(), 10, "OUTLINE")
presetLabel:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_BOT)
presetLabel:SetText("预设:")
presetLabel:SetTextColor(0.7, 0.68, 0.78, 1)
controlBar._presetBtns = {}
local AB = SFrames.ActionBars
local presets = AB and AB.PRESETS or {}
local px = 56
for idx = 1, 3 do
local p = presets[idx]
local pName = p and p.name or ("方案" .. idx)
local pDesc = p and p.desc or ""
local pId = idx
local pbtn = MakeControlButton(controlBar, pName, 80, px + (idx - 1) * 88, function()
if AB and AB.ApplyPreset then
AB:ApplyPreset(pId)
end
end)
pbtn:ClearAllPoints()
pbtn:SetPoint("LEFT", controlBar, "LEFT", px + (idx - 1) * 88, ROW_Y_BOT)
pbtn._text:SetFont(Font(), 9, "OUTLINE")
pbtn._presetId = pId
pbtn:SetScript("OnEnter", function()
local a2 = T().accent or { 1, 0.5, 0.8 }
this:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.95)
this._text:SetTextColor(1, 1, 1, 1)
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine(pName, 1, 0.85, 0.55)
GameTooltip:AddLine(pDesc, 0.75, 0.75, 0.85, true)
GameTooltip:Show()
end)
pbtn:SetScript("OnLeave", function()
local th2 = T()
local bd2 = th2.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
local tc2 = th2.buttonText or { 0.85, 0.82, 0.92 }
this:SetBackdropColor(th2.buttonBg and th2.buttonBg[1] or 0.16,
th2.buttonBg and th2.buttonBg[2] or 0.12,
th2.buttonBg and th2.buttonBg[3] or 0.22, 0.94)
this:SetBackdropBorderColor(bd2[1], bd2[2], bd2[3], bd2[4] or 0.90)
this._text:SetTextColor(tc2[1], tc2[2], tc2[3], 1)
GameTooltip:Hide()
end)
controlBar._presetBtns[idx] = pbtn
end
controlBar:Hide()
return controlBar
end
@@ -844,7 +910,7 @@ end
--------------------------------------------------------------------------------
-- Register mover
--------------------------------------------------------------------------------
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved, opts)
if not name or not frame then return end
registry[name] = {
@@ -856,6 +922,7 @@ function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, de
defaultX = defaultX or 0,
defaultY = defaultY or 0,
onMoved = onMoved,
alwaysShowInLayout = opts and opts.alwaysShowInLayout or false,
}
CreateMoverFrame(name, registry[name])
@@ -890,10 +957,19 @@ function M:EnterLayoutMode()
UIParent:GetWidth(), UIParent:GetHeight(),
UIParent:GetRight() or 0, UIParent:GetTop() or 0))
for name, _ in pairs(registry) do
for name, entry in pairs(registry) do
local frame = entry and entry.frame
local shouldShow = entry.alwaysShowInLayout
or (frame and frame.IsShown and frame:IsShown())
SyncMoverToFrame(name)
local mover = moverFrames[name]
if mover then mover:Show() end
if mover then
if shouldShow then
mover:Show()
else
mover:Hide()
end
end
end
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
@@ -924,6 +1000,22 @@ function M:IsLayoutMode()
return isLayoutMode
end
function M:SetMoverAlwaysShow(name, alwaysShow)
local entry = registry[name]
if entry then
entry.alwaysShowInLayout = alwaysShow
end
local mover = moverFrames[name]
if mover and isLayoutMode then
if alwaysShow then
SyncMoverToFrame(name)
mover:Show()
else
mover:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Reset movers
--------------------------------------------------------------------------------
@@ -979,13 +1071,30 @@ function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoin
local pos = positions[name]
if pos and pos.point and pos.relativePoint then
frame:ClearAllPoints()
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
return true
else
frame:ClearAllPoints()
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
if relFrame == UIParent then
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
(defaultX or 0) / fScale, (defaultY or 0) / fScale)
else
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
end
else
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
end
return false
end
end
@@ -996,3 +1105,7 @@ end
function M:GetRegistry()
return registry
end
function M:SyncMoverToFrame(name)
SyncMoverToFrame(name)
end

View File

@@ -9,6 +9,7 @@
Bindings.xml
Core.lua
Config.lua
AuraTracker.lua
Movers.lua
Media.lua
IconMap.lua
@@ -27,7 +28,6 @@ MapIcons.lua
Tweaks.lua
MinimapBuffs.lua
Focus.lua
ClassSkillData.lua
Units\Player.lua
Units\Pet.lua
Units\Target.lua
@@ -39,6 +39,7 @@ GearScore.lua
Tooltip.lua
Units\Raid.lua
ActionBars.lua
ExtraBar.lua
KeyBindManager.lua
Bags\Offline.lua
@@ -57,6 +58,8 @@ QuestLogSkin.lua
TrainerUI.lua
TradeSkillDB.lua
BeastTrainingUI.lua
ConsumableDB.lua
ConsumableUI.lua
TradeSkillUI.lua
CharacterPanel.lua
StatSummary.lua

View File

@@ -444,6 +444,10 @@ local function ApplyChoices()
SFramesDB.enableChat = c.enableChat
if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end
SFramesDB.Chat.translateEnabled = c.translateEnabled
-- 翻译关闭时,聊天监控也自动关闭
if c.translateEnabled == false then
SFramesDB.Chat.chatMonitorEnabled = false
end
SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable
SFramesDB.enableUnitFrames = c.enableUnitFrames
@@ -1316,9 +1320,14 @@ function SW:DoSkip()
self:Hide()
return
end
-- First-run: apply defaults
if not SFramesDB then SFramesDB = {} end
SFramesDB.setupComplete = true
-- First-run: apply defaults then initialize
choices = GetDefaultChoices()
local ok, err = pcall(ApplyChoices)
if not ok then
if not SFramesDB then SFramesDB = {} end
SFramesDB.setupComplete = true
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Wizard skip apply error: "..tostring(err).."|r")
end
self:Hide()
if completeCb then completeCb() end
end

View File

@@ -115,6 +115,23 @@ local function TT_DifficultyColor(unitLevel)
end
end
local function TT_GetClassificationText(unit)
if not UnitExists(unit) then return nil end
local classif = UnitClassification(unit)
if classif == "rareelite" then
return "|cffc57cff[稀有 精英]|r"
elseif classif == "rare" then
return "|cffc57cff[稀有]|r"
elseif classif == "elite" then
return "|cffffa500[精英]|r"
elseif classif == "worldboss" then
return "|cffff4040[首领]|r"
end
return nil
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
@@ -680,6 +697,29 @@ function SFrames.FloatingTooltip:FormatLines(tooltip)
GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b)
end
end
local classificationText = TT_GetClassificationText(unit)
if classificationText then
local numLines = tooltip:NumLines()
local appended = false
for i = 2, numLines do
local left = getglobal("GameTooltipTextLeft" .. i)
if left then
local txt = left:GetText()
if txt and (string.find(txt, "^Level ") or string.find(txt, "^等级 ")) then
if not string.find(txt, "%[") then
left:SetText(txt .. " " .. classificationText)
end
appended = true
break
end
end
end
if not appended then
tooltip:AddLine(classificationText)
end
end
end
--------------------------------------------------------------------------
@@ -1268,27 +1308,27 @@ function IC:HookTooltips()
---------------------------------------------------------------------------
-- SetItemRef (chat item links)
---------------------------------------------------------------------------
local orig_SetItemRef = SetItemRef
if orig_SetItemRef then
SetItemRef = function(link, text, button)
orig_SetItemRef(link, text, button)
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
if ItemRefTooltip and ItemRefTooltip.SetHyperlink then
local orig_ItemRef_SetHyperlink = ItemRefTooltip.SetHyperlink
ItemRefTooltip.SetHyperlink = function(self, link)
self._nanamiSellPriceAdded = nil
self._gsScoreAdded = nil
local r1, r2, r3, r4 = orig_ItemRef_SetHyperlink(self, link)
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then
return r1, r2, r3, r4
end
pcall(function()
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
if itemStr then
ItemRefTooltip._nanamiSellPriceAdded = nil
local itemId = IC_GetItemIdFromLink(itemStr)
local price = IC_QueryAndLearnPrice(itemStr)
if price and price > 0 and not ItemRefTooltip.hasMoney then
SetTooltipMoney(ItemRefTooltip, price)
ItemRefTooltip:Show()
end
if itemId then
ItemRefTooltip:AddLine("物品ID: " .. itemId, 0.55, 0.55, 0.70)
ItemRefTooltip:Show()
end
local moneyAlreadyShown = self.hasMoney
IC_EnhanceTooltip(self, itemStr, nil, moneyAlreadyShown)
end
end)
return r1, r2, r3, r4
end
end

View File

@@ -962,12 +962,7 @@ function TSUI.CreateReagentSlot(parent, i)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
local ok
if S.currentMode == "craft" then
local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex)
if link then
ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
else
ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex)
end
ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
else
ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex)
end
@@ -1521,6 +1516,7 @@ end
function TSUI.UpdateProfTabs()
TSUI.ScanProfessions()
local currentSkillName = API.GetSkillLineName()
local numVisible = 0
for i = 1, 10 do
local tab = S.profTabs[i]
if not tab then break end
@@ -1544,10 +1540,16 @@ function TSUI.UpdateProfTabs()
tab.glow:Hide(); tab.checked:Hide()
end
tab:Show()
numVisible = numVisible + 1
else
tab.profName = nil; tab.active = false; tab:Hide()
end
end
if S.encBtn and S.MainFrame then
S.encBtn:ClearAllPoints()
S.encBtn:SetPoint("TOPLEFT", S.MainFrame, "TOPRIGHT", 2,
-(6 + numVisible * (42 + 4) + 10))
end
end
function TSUI.IsTabSwitching()
@@ -1932,6 +1934,24 @@ function TSUI:Initialize()
end
end)
dIF:SetScript("OnLeave", function() GameTooltip:Hide() end)
dIF:SetScript("OnMouseUp", function()
if IsShiftKeyDown() and S.selectedIndex then
local link = API.GetItemLink(S.selectedIndex)
if link then
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(link)
else
if ChatFrame_OpenChat then
ChatFrame_OpenChat(link)
elseif ChatFrameEditBox then
ChatFrameEditBox:Show()
ChatFrameEditBox:SetText(link)
ChatFrameEditBox:SetFocus()
end
end
end
end
end)
local dName = det:CreateFontString(nil, "OVERLAY")
dName:SetFont(font, 13, "OUTLINE")
@@ -2128,6 +2148,52 @@ function TSUI:Initialize()
end)
TSUI.CreateProfTabs(MF)
-- ── 食物药剂百科 按钮(职业图标条底部)───────────────────────────────
do
local TAB_SZ, TAB_GAP, TAB_TOP = 42, 4, 6
local encBtn = CreateFrame("Button", nil, MF)
encBtn:SetWidth(TAB_SZ); encBtn:SetHeight(TAB_SZ)
encBtn:SetPoint("TOPLEFT", MF, "TOPRIGHT", 2, -(TAB_TOP + 10))
encBtn:SetFrameStrata("HIGH")
encBtn: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 },
})
encBtn:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
encBtn:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
local ico = encBtn:CreateTexture(nil, "ARTWORK")
ico:SetTexture("Interface\\Icons\\INV_Potion_97")
ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
ico:SetPoint("TOPLEFT", encBtn, "TOPLEFT", 4, -4)
ico:SetPoint("BOTTOMRIGHT", encBtn, "BOTTOMRIGHT", -4, 4)
local hl = encBtn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
hl:SetBlendMode("ADD"); hl:SetAlpha(0.3); hl:SetAllPoints(ico)
encBtn:SetScript("OnEnter", function()
this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText("食物药剂百科", 1, 0.82, 0.60)
GameTooltip:AddLine("查看各职业消耗品推荐列表", 0.8, 0.8, 0.8)
GameTooltip:AddLine("Shift+点击物品可插入聊天", 0.6, 0.6, 0.65)
GameTooltip:Show()
end)
encBtn:SetScript("OnLeave", function()
this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
GameTooltip:Hide()
end)
encBtn:SetScript("OnClick", function()
if SFrames.ConsumableUI then SFrames.ConsumableUI:Toggle() end
end)
encBtn:Show()
S.encBtn = encBtn
end
-- ────────────────────────────────────────────────────────────────────────
MF:Hide()
tinsert(UISpecialFrames, "SFramesTradeSkillFrame")
end

View File

@@ -61,10 +61,11 @@ local currentFilter = "all"
local displayList = {}
local rowButtons = {}
local collapsedCats = {}
local isTradeskillTrainerCached = false -- Cache to avoid repeated API calls
local isTradeskillTrainerCached = false
local function HideBlizzardTrainer()
if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
if ClassTrainerFrame:IsVisible() then
if HideUIPanel then
pcall(HideUIPanel, ClassTrainerFrame)
@@ -161,123 +162,20 @@ end
local function GetVerifiedCategory(index)
local name, _, category = GetTrainerServiceInfo(index)
if not name then return nil end
-- "used" is always reliable - player already knows this skill
if category == "used" then
return "used"
if category == "available" or category == "unavailable" or category == "used" then
return category
end
-- "unavailable" from API should be trusted - it considers:
-- - Level requirements
-- - Skill rank prerequisites (e.g., need Fireball Rank 1 before Rank 2)
-- - Profession skill requirements
if category == "unavailable" then
return "unavailable"
end
-- For "available", do extra verification only for tradeskill trainers
-- Class trainers' "available" is already accurate
if category == "available" then
-- Additional check for tradeskill trainers (use cached value)
if isTradeskillTrainerCached then
local playerLevel = UnitLevel("player") or 1
-- Check level requirement
if GetTrainerServiceLevelReq then
local ok, levelReq = pcall(GetTrainerServiceLevelReq, index)
if ok and levelReq and levelReq > 0 and playerLevel < levelReq then
return "unavailable"
end
end
-- Check skill requirement
if GetTrainerServiceSkillReq then
local ok, skillName, skillRank, hasReq = pcall(GetTrainerServiceSkillReq, index)
if ok and skillName and skillName ~= "" and not hasReq then
return "unavailable"
end
end
end
return "available"
end
-- Fallback: unknown category, treat as unavailable
return category or "unavailable"
return "unavailable"
end
local scanTip = nil
local function GetServiceTooltipInfo(index)
if not scanTip then
scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
end
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
scanTip:ClearLines()
local ok = pcall(scanTip.SetTrainerService, scanTip, index)
if not ok then return "", "" end
local infoLines = {}
local descLines = {}
local numLines = scanTip:NumLines()
local foundDesc = false
for i = 2, numLines do
local leftFS = _G["SFramesTrainerScanTipTextLeft" .. i]
local rightFS = _G["SFramesTrainerScanTipTextRight" .. i]
local leftText = leftFS and leftFS:GetText() or ""
local rightText = rightFS and rightFS:GetText() or ""
if leftText == "" and rightText == "" then
if not foundDesc and table.getn(infoLines) > 0 then
foundDesc = true
end
else
local line
if rightText ~= "" and leftText ~= "" then
line = leftText .. " " .. rightText
elseif leftText ~= "" then
line = leftText
else
line = rightText
end
local isYellow = leftFS and leftFS.GetTextColor and true
local r, g, b
if leftFS and leftFS.GetTextColor then
r, g, b = leftFS:GetTextColor()
end
local isWhiteOrYellow = r and (r > 0.9 and g > 0.75)
if not foundDesc and rightText ~= "" then
table.insert(infoLines, line)
elseif not foundDesc and not isWhiteOrYellow and string.len(leftText) < 30 then
table.insert(infoLines, line)
else
foundDesc = true
table.insert(descLines, line)
end
end
end
scanTip:Hide()
return table.concat(infoLines, "\n"), table.concat(descLines, "\n")
return "", ""
end
local function GetServiceQuality(index)
if not scanTip then
scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
end
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
scanTip:ClearLines()
local ok = pcall(scanTip.SetTrainerService, scanTip, index)
if not ok then scanTip:Hide() return nil end
local firstLine = _G["SFramesTrainerScanTipTextLeft1"]
if not firstLine or not firstLine.GetTextColor then
scanTip:Hide()
return nil
end
local r, g, b = firstLine:GetTextColor()
scanTip:Hide()
return ColorToQuality(r, g, b)
return nil
end
--------------------------------------------------------------------------------
@@ -392,10 +290,7 @@ end
local qualityCache = {}
local function GetCachedServiceQuality(index)
if qualityCache[index] ~= nil then return qualityCache[index] end
local q = GetServiceQuality(index)
qualityCache[index] = q or false
return q
return nil
end
--------------------------------------------------------------------------------
@@ -604,22 +499,8 @@ local function CreateListRow(parent, idx)
self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
end
-- Skip quality scan for tradeskill trainers (performance optimization)
if not isTradeskillTrainerCached then
local quality = GetCachedServiceQuality(svc.index)
local qc = QUALITY_COLORS[quality]
if qc and quality and quality >= 2 then
self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3])
self.qualGlow:Show()
self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
else
self.qualGlow:Hide()
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
else
self.qualGlow:Hide()
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
self.qualGlow:Hide()
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
local ok, cost = pcall(GetTrainerServiceCost, svc.index)
if ok and cost and cost > 0 then
@@ -838,13 +719,7 @@ local function UpdateDetail()
detail.icon:SetTexture(iconTex)
detail.iconFrame:Show()
local quality = GetServiceQuality(selectedIndex)
local qc = QUALITY_COLORS[quality]
if qc and quality and quality >= 2 then
detail.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
else
detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
end
detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
detail.nameFS:SetText(name or "")
@@ -873,13 +748,10 @@ local function UpdateDetail()
end
detail.reqFS:SetText(table.concat(reqParts, " "))
local spellInfo, descText = GetServiceTooltipInfo(selectedIndex)
detail.infoFS:SetText(spellInfo)
detail.descFS:SetText(descText)
detail.descDivider:Show()
local textH = detail.descFS:GetHeight() or 40
detail.descScroll:GetScrollChild():SetHeight(math.max(1, textH))
detail.infoFS:SetText("")
detail.descFS:SetText("")
detail.descDivider:Hide()
detail.descScroll:GetScrollChild():SetHeight(1)
detail.descScroll:SetVerticalScroll(0)
local canTrain = (category == "available") and cost and (GetMoney() >= cost)
@@ -1342,6 +1214,7 @@ function TUI:Initialize()
local function CleanupBlizzardTrainer()
if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end
if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end
ClassTrainerFrame:SetAlpha(0)
@@ -1361,6 +1234,7 @@ function TUI:Initialize()
if event == "TRAINER_SHOW" then
if ClassTrainerFrame then
ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
ClassTrainerFrame:SetAlpha(0)
ClassTrainerFrame:EnableMouse(false)
end

View File

@@ -1148,11 +1148,9 @@ end
-- unit under the mouse cursor without changing current target.
--
-- Strategy:
-- UseAction hook: temporarily TargetUnit(moUnit) before the real UseAction,
-- then restore previous target afterwards. This preserves the hardware
-- event callstack so the client doesn't reject the action.
-- CastSpellByName hook (SuperWoW): pass moUnit as 2nd arg directly.
-- CastSpellByName hook (no SuperWoW): same target-swap trick.
-- Temporarily try the mouseover unit first while preserving the original
-- target. If the spell cannot resolve on mouseover, stop the pending target
-- mode, restore the original target, and retry there.
--------------------------------------------------------------------------------
local mouseoverCastEnabled = false
local origUseAction = nil
@@ -1176,6 +1174,70 @@ local function GetMouseoverUnit()
return nil
end
local function CaptureTargetState()
local hadTarget = UnitExists("target")
return {
hadTarget = hadTarget,
name = hadTarget and UnitName("target") or nil,
}
end
local function RestoreTargetState(state)
if not state then return end
if state.hadTarget and state.name then
TargetLastTarget()
if not UnitExists("target") or UnitName("target") ~= state.name then
TargetByName(state.name, true)
end
else
ClearTarget()
end
end
local function ResolvePendingSpellTarget(unit)
if not (SpellIsTargeting and SpellIsTargeting()) then
return true
end
if unit then
if SpellCanTargetUnit then
if SpellCanTargetUnit(unit) then
SpellTargetUnit(unit)
end
else
SpellTargetUnit(unit)
end
end
if SpellIsTargeting and SpellIsTargeting() then
return false
end
return true
end
local function TryActionOnUnit(unit, action, cursor, onSelf)
if not unit then return false end
if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
TargetUnit(unit)
end
origUseAction(action, cursor, onSelf)
return ResolvePendingSpellTarget(unit)
end
local function TryCastSpellOnUnit(unit, spell)
if not unit then return false end
if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
TargetUnit(unit)
end
origCastSpellByName(spell)
return ResolvePendingSpellTarget(unit)
end
local function MouseoverUseAction(action, cursor, onSelf)
-- Don't interfere: picking up action, or re-entrant call
if cursor == 1 or inMouseoverAction then
@@ -1187,42 +1249,24 @@ local function MouseoverUseAction(action, cursor, onSelf)
return origUseAction(action, cursor, onSelf)
end
-- Skip if mouseover IS current target (no swap needed)
if UnitIsUnit and UnitExists("target") and UnitIsUnit(moUnit, "target") then
return origUseAction(action, cursor, onSelf)
end
local prevTarget = CaptureTargetState()
-- Remember current target state
local hadTarget = UnitExists("target")
local prevTargetName = hadTarget and UnitName("target") or nil
-- Temporarily target the mouseover unit
inMouseoverAction = true
TargetUnit(moUnit)
local castOnMouseover = TryActionOnUnit(moUnit, action, cursor, onSelf)
-- Execute the real UseAction on the now-targeted mouseover unit
origUseAction(action, cursor, onSelf)
-- Handle ground-targeted spells (Blizzard, Flamestrike, etc.)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(moUnit)
end
if SpellIsTargeting and SpellIsTargeting() then
if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
-- Restore previous target
if hadTarget and prevTargetName then
-- Target back the previous unit
TargetLastTarget()
-- Verify restoration worked
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
-- TargetLastTarget failed, try by name
TargetByName(prevTargetName, true)
RestoreTargetState(prevTarget)
if not castOnMouseover and prevTarget.hadTarget then
origUseAction(action, cursor, onSelf)
if SpellIsTargeting and SpellIsTargeting() then
if not ResolvePendingSpellTarget("target") then
SpellStopTargeting()
end
end
else
-- Had no target before, clear
ClearTarget()
end
inMouseoverAction = false
@@ -1244,43 +1288,26 @@ local function MouseoverCastSpellByName(spell, arg2)
return origCastSpellByName(spell)
end
-- SuperWoW: direct unit parameter, no target swap needed
if SUPERWOW_VERSION then
origCastSpellByName(spell, moUnit)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(moUnit)
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
return
end
local prevTarget = CaptureTargetState()
-- No SuperWoW: target-swap
local hadTarget = UnitExists("target")
local prevTargetName = hadTarget and UnitName("target") or nil
inMouseoverAction = true
local castOnMouseover = TryCastSpellOnUnit(moUnit, spell)
if not (hadTarget and UnitIsUnit and UnitIsUnit(moUnit, "target")) then
TargetUnit(moUnit)
end
origCastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit("target")
end
if SpellIsTargeting and SpellIsTargeting() then
if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
if hadTarget and prevTargetName then
TargetLastTarget()
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
TargetByName(prevTargetName, true)
RestoreTargetState(prevTarget)
if not castOnMouseover and prevTarget.hadTarget then
origCastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
if not ResolvePendingSpellTarget("target") then
SpellStopTargeting()
end
end
elseif not hadTarget then
ClearTarget()
end
inMouseoverAction = false
end
local function InitMouseoverCast()

View File

@@ -9,6 +9,9 @@ local PARTY_HORIZONTAL_GAP = 8
local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true }
-- Pre-allocated table reused every UpdateAuras call
local _partyDebuffColor = { r = 0, g = 0, b = 0 }
local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit)
end
@@ -36,6 +39,12 @@ local function Clamp(value, minValue, maxValue)
return value
end
local function SetTextureIfPresent(region, texturePath)
if region and region.SetTexture and texturePath then
region:SetTexture(texturePath)
end
end
function SFrames.Party:GetMetrics()
local db = SFramesDB or {}
@@ -57,6 +66,34 @@ function SFrames.Party:GetMetrics()
local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6)
local gradientStyle = SFrames:IsGradientStyle()
local availablePowerWidth = width - portraitWidth - 5
if availablePowerWidth < 40 then
availablePowerWidth = 40
end
local rawPowerWidth = tonumber(db.partyPowerWidth)
local legacyFullWidth = tonumber(db.partyFrameWidth) or width
local legacyPowerWidth = width - portraitWidth - 3
local defaultPowerWidth = gradientStyle and width or availablePowerWidth
local maxPowerWidth = gradientStyle and width or availablePowerWidth
local powerWidth
if gradientStyle then
-- 渐变风格:能量条始终与血条等宽(全宽)
powerWidth = width
elseif not rawPowerWidth
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
or math.abs(rawPowerWidth - legacyPowerWidth) < 0.5
or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
powerWidth = defaultPowerWidth
else
powerWidth = rawPowerWidth
end
powerWidth = Clamp(math.floor(powerWidth + 0.5), 40, maxPowerWidth)
local powerOffsetX = Clamp(math.floor((tonumber(db.partyPowerOffsetX) or 0) + 0.5), -120, 120)
local powerOffsetY = Clamp(math.floor((tonumber(db.partyPowerOffsetY) or 0) + 0.5), -80, 80)
if healthHeight + powerHeight + 3 > height then
powerHeight = height - healthHeight - 3
if powerHeight < 6 then
@@ -81,16 +118,30 @@ function SFrames.Party:GetMetrics()
local valueFont = tonumber(db.partyValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
local healthFont = tonumber(db.partyHealthFontSize) or valueFont
healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
local powerFont = tonumber(db.partyPowerFontSize) or valueFont
powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
return {
width = width,
height = height,
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.partyPowerOnTop == true,
horizontalGap = hgap,
verticalGap = vgap,
nameFont = nameFont,
valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("partyHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("partyPowerTexture", "barTexture"),
}
end
@@ -142,8 +193,8 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
if frame.power then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
end
@@ -153,14 +204,114 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
SFrames:ApplyStatusBarTexture(frame.health, "partyHealthTexture", "barTexture")
SFrames:ApplyStatusBarTexture(frame.power, "partyPowerTexture", "barTexture")
if frame.health and frame.power then
local healthLevel = frame:GetFrameLevel() + 2
local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
frame.health:SetFrameLevel(healthLevel)
frame.power:SetFrameLevel(powerLevel)
end
SFrames:ApplyConfiguredUnitBackdrop(frame, "party")
if frame.pbg then SFrames:ApplyConfiguredUnitBackdrop(frame.pbg, "party", true) end
if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "party") end
if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "party") end
SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
-- Gradient style preset
if SFrames:IsGradientStyle() then
-- Hide portrait & its backdrop
if frame.portrait then frame.portrait:Hide() end
if frame.pbg then SFrames:ClearBackdrop(frame.pbg); frame.pbg:Hide() end
-- Strip backdrops
SFrames:ClearBackdrop(frame)
SFrames:ClearBackdrop(frame.healthBGFrame)
SFrames:ClearBackdrop(frame.powerBGFrame)
-- Health bar full width
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
frame.health:SetHeight(metrics.healthHeight)
end
-- Power bar full width
if frame.power then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -2 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
end
-- Apply gradient overlays
SFrames:ApplyGradientStyle(frame.health)
SFrames:ApplyGradientStyle(frame.power)
-- Flush BG frames
if frame.healthBGFrame then
frame.healthBGFrame:ClearAllPoints()
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
end
if frame.powerBGFrame then
frame.powerBGFrame:ClearAllPoints()
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
end
-- Hide bar backgrounds (transparent)
if frame.healthBGFrame then frame.healthBGFrame:Hide() end
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
if frame.health and frame.health.bg then frame.health.bg:Hide() end
if frame.power and frame.power.bg then frame.power.bg:Hide() end
else
SFrames:RemoveGradientStyle(frame.health)
SFrames:RemoveGradientStyle(frame.power)
-- Restore bar backgrounds
if frame.healthBGFrame then frame.healthBGFrame:Show() end
if frame.powerBGFrame then frame.powerBGFrame:Show() end
if frame.health and frame.health.bg then frame.health.bg:Show() end
if frame.power and frame.power.bg then frame.power.bg:Show() end
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
if use3D then
if frame.portrait then frame.portrait:Show() end
if frame.pbg then frame.pbg:Show() end
else
-- Hide portrait area and extend health/power bars to full width
if frame.portrait then frame.portrait:Hide() end
if frame.pbg then frame.pbg:Hide() end
local fullWidth = metrics.width - 2
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
frame.health:SetHeight(metrics.healthHeight)
end
if frame.healthBGFrame then
frame.healthBGFrame:ClearAllPoints()
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
end
if frame.power then
frame.power:SetWidth(fullWidth)
end
if frame.powerBGFrame then
frame.powerBGFrame:ClearAllPoints()
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
end
end
end
if frame.nameText then
frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "partyNameFontKey", "fontKey")
end
if frame.healthText then
frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "partyHealthFontKey", "fontKey")
end
if frame.powerText then
SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "partyPowerFontKey", "fontKey")
end
end
@@ -250,12 +401,13 @@ function SFrames.Party:ApplyLayout()
end
end
local auraRowHeight = 24 -- 20px icon + 2px gap above + 2px padding
if mode == "horizontal" then
self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
self.parent:SetHeight(metrics.height)
self.parent:SetHeight(metrics.height + auraRowHeight)
else
self.parent:SetWidth(metrics.width)
self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3))
self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3) + auraRowHeight)
end
end
@@ -432,11 +584,16 @@ function SFrames.Party:Initialize()
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
f.powerText = SFrames:CreateFontString(f.power, 9, "RIGHT")
f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0)
f.nameText:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1)
f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -1)
f.powerText:SetShadowColor(0, 0, 0, 1)
f.powerText:SetShadowOffset(1, -1)
-- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
local roleOvr = CreateFrame("Frame", nil, f)
@@ -621,74 +778,79 @@ end
function SFrames.Party:CreateAuras(index)
local f = self.frames[index].frame
-- Use self.parent (plain Frame) as parent for aura buttons so they are
-- never clipped by the party Button frame's boundaries.
local auraParent = self.parent
f.buffs = {}
f.debuffs = {}
local size = 20
local spacing = 2
-- Party Buffs
for i = 1, 4 do
local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f)
local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, auraParent)
b:SetWidth(size)
b:SetHeight(size)
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitBuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
-- Anchored BELOW the frame on the left side
-- Anchored BELOW the party frame on the left side
if i == 1 then
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
else
b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0)
end
b:Hide()
f.buffs[i] = b
end
-- Debuffs (Starting right after Buffs to remain linear)
for i = 1, 4 do
local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f)
local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, auraParent)
b:SetWidth(size)
b:SetHeight(size)
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitDebuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
if i == 1 then
b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0)
else
b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0)
end
b:Hide()
f.debuffs[i] = b
end
@@ -726,6 +888,7 @@ end
function SFrames.Party:TickAuras(unit)
if self.testing then return end
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
@@ -772,7 +935,10 @@ function SFrames.Party:UpdateAll()
if inRaid and raidFramesEnabled then
for i = 1, 4 do
if self.frames[i] and self.frames[i].frame then
self.frames[i].frame:Hide()
local f = self.frames[i].frame
f:Hide()
if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
end
end
if self._globalUpdateFrame then
@@ -793,6 +959,8 @@ function SFrames.Party:UpdateAll()
hasVisible = true
else
f:Hide()
if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
end
end
if self._globalUpdateFrame then
@@ -812,11 +980,16 @@ function SFrames.Party:UpdateFrame(unit)
if not data then return end
local f = data.frame
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
f.portrait:Show()
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
if use3D then
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
f.portrait:Show()
else
f.portrait:Hide()
end
local name = UnitName(unit) or ""
local level = UnitLevel(unit)
if level == -1 then level = "??" end
@@ -841,6 +1014,10 @@ function SFrames.Party:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1)
end
end
-- Re-apply gradient after color change
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.health)
end
-- Update Leader/Master Looter
if GetPartyLeaderIndex() == data.index then
@@ -870,6 +1047,8 @@ function SFrames.Party:UpdatePortrait(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
if not use3D then return end
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
@@ -917,14 +1096,8 @@ function SFrames.Party:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther
local predOver = f.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists(unit) or not UnitIsConnected(unit) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -946,7 +1119,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end
if maxHp <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -963,7 +1136,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -971,13 +1144,19 @@ function SFrames.Party:UpdateHealPrediction(unit)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
local use3DForPred = not (SFramesDB and SFramesDB.partyPortrait3D == false)
local barWidth
if use3DForPred then
barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
else
barWidth = f:GetWidth() - 2
end
if barWidth <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1054,6 +1233,9 @@ function SFrames.Party:UpdatePowerType(unit)
else
f.power:SetStatusBarColor(0, 0, 1)
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.power)
end
end
function SFrames.Party:UpdatePower(unit)
@@ -1064,6 +1246,7 @@ function SFrames.Party:UpdatePower(unit)
if not UnitIsConnected(unit) then
f.power:SetMinMaxValues(0, 100)
f.power:SetValue(0)
if f.powerText then f.powerText:SetText("") end
return
end
@@ -1071,6 +1254,14 @@ function SFrames.Party:UpdatePower(unit)
local maxPower = UnitManaMax(unit)
f.power:SetMinMaxValues(0, maxPower)
f.power:SetValue(power)
if f.powerText then
if maxPower and maxPower > 0 then
f.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
f.powerText:SetText("")
end
end
SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
end
function SFrames.Party:UpdateRaidIcons()
@@ -1106,6 +1297,7 @@ function SFrames.Party:UpdateRaidIcon(unit)
end
function SFrames.Party:UpdateAuras(unit)
if self.testing then return end
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
@@ -1114,7 +1306,9 @@ function SFrames.Party:UpdateAuras(unit)
local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
local hasDebuff = false
local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
_partyDebuffColor.r = _A.slotBg[1]
_partyDebuffColor.g = _A.slotBg[2]
_partyDebuffColor.b = _A.slotBg[3]
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
@@ -1128,10 +1322,10 @@ function SFrames.Party:UpdateAuras(unit)
if texture then
if debuffType then
hasDebuff = true
if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
if debuffType == "Magic" then _partyDebuffColor.r = 0.2; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 1
elseif debuffType == "Curse" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0; _partyDebuffColor.b = 1
elseif debuffType == "Disease" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0.4; _partyDebuffColor.b = 0
elseif debuffType == "Poison" then _partyDebuffColor.r = 0; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 0
end
end
@@ -1174,7 +1368,7 @@ function SFrames.Party:UpdateAuras(unit)
end
if hasDebuff then
f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
f.health.bg:SetVertexColor(_partyDebuffColor.r, _partyDebuffColor.g, _partyDebuffColor.b, 1)
else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end
@@ -1254,10 +1448,38 @@ function SFrames.Party:TestMode()
f.masterIcon:Show()
end
-- Show one dummy debuff to test positioning
f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain")
f.debuffs[1]:Show()
-- Show test buffs (all 4)
local testBuffIcons = {
"Interface\\Icons\\Spell_Holy_PowerWordFortitude",
"Interface\\Icons\\Spell_Holy_Renew",
"Interface\\Icons\\Spell_Holy_GreaterHeal",
"Interface\\Icons\\Spell_Nature_Abolishmagic",
}
for j = 1, 4 do
local fakeTime = math.random(30, 300)
f.buffs[j].icon:SetTexture(testBuffIcons[j])
f.buffs[j].expirationTime = GetTime() + fakeTime
f.buffs[j].cdText:SetText(SFrames:FormatTime(fakeTime))
f.buffs[j]:Show()
end
-- Show test debuffs (all 4, different types for color test)
local testDebuffIcons = {
"Interface\\Icons\\Spell_Shadow_ShadowWordPain", -- Magic (blue)
"Interface\\Icons\\Spell_Shadow_Curse", -- Curse (purple)
"Interface\\Icons\\Ability_Rogue_FeignDeath", -- Disease (brown)
"Interface\\Icons\\Ability_Poisoning", -- Poison (green)
}
for j = 1, 4 do
local debuffTime = math.random(5, 25)
f.debuffs[j].icon:SetTexture(testDebuffIcons[j])
f.debuffs[j].expirationTime = GetTime() + debuffTime
f.debuffs[j].cdText:SetText(SFrames:FormatTime(debuffTime))
f.debuffs[j]:Show()
end
-- Magic debuff background color
f.health.bg:SetVertexColor(0.2, 0.6, 1, 1)
-- Test pet
if f.petFrame then
f.petFrame:Show()
@@ -1268,9 +1490,18 @@ function SFrames.Party:TestMode()
end
end
else
self:UpdateAll()
for i = 1, 4 do
self.frames[i].frame.debuffs[1]:Hide()
local f = self.frames[i].frame
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
for j = 1, 4 do
f.buffs[j].expirationTime = nil
f.buffs[j].cdText:SetText("")
f.buffs[j]:Hide()
f.debuffs[j].expirationTime = nil
f.debuffs[j].cdText:SetText("")
f.debuffs[j]:Hide()
end
end
self:UpdateAll()
end
end

View File

@@ -225,16 +225,22 @@ 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))
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
local pos = SFramesDB.Positions["PetFrame"]
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
f:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
else
f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 0, -75)
end
f:SetMovable(true)
f:EnableMouse(true)
@@ -245,6 +251,11 @@ function SFrames.Pet:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
xOfs = (xOfs or 0) * fScale
yOfs = (yOfs or 0) * fScale
end
SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
@@ -294,6 +305,8 @@ function SFrames.Pet:Initialize()
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
f.healthBGFrame = hbg
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
f.health.bg:SetTexture(SFrames:GetTexture())
@@ -328,6 +341,7 @@ function SFrames.Pet:Initialize()
pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
pbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(pbg)
f.powerBGFrame = pbg
f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
f.power.bg:SetAllPoints()
@@ -394,11 +408,13 @@ function SFrames.Pet:Initialize()
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
self:InitFoodFeature()
self:ApplyConfig()
self:UpdateAll()
if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物",
"TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 10, -55)
"TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 0, -75,
nil, { alwaysShowInLayout = true })
end
if StaticPopup_Show then
@@ -413,6 +429,90 @@ function SFrames.Pet:Initialize()
end
end
function SFrames.Pet:ApplyConfig()
if not self.frame then return end
local f = self.frame
-- Apply bar textures
SFrames:ApplyStatusBarTexture(f.health, "petHealthTexture", "barTexture")
SFrames:ApplyStatusBarTexture(f.power, "petPowerTexture", "barTexture")
local healthTex = SFrames:ResolveBarTexture("petHealthTexture", "barTexture")
local powerTex = SFrames:ResolveBarTexture("petPowerTexture", "barTexture")
if f.health and f.health.bg then f.health.bg:SetTexture(healthTex) end
if f.power and f.power.bg then f.power.bg:SetTexture(powerTex) end
if SFrames:IsGradientStyle() then
-- Strip backdrops
SFrames:ClearBackdrop(f)
SFrames:ClearBackdrop(f.healthBGFrame)
SFrames:ClearBackdrop(f.powerBGFrame)
-- Health bar full width
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
f.health:SetHeight(18)
end
-- Power bar full width
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -2)
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
end
-- Apply gradient overlays
SFrames:ApplyGradientStyle(f.health)
SFrames:ApplyGradientStyle(f.power)
-- Flush BG frames
if f.healthBGFrame then
f.healthBGFrame:ClearAllPoints()
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
end
if f.powerBGFrame then
f.powerBGFrame:ClearAllPoints()
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
end
-- Hide bar backgrounds (transparent)
if f.healthBGFrame then f.healthBGFrame:Hide() end
if f.powerBGFrame then f.powerBGFrame:Hide() end
if f.health and f.health.bg then f.health.bg:Hide() end
if f.power and f.power.bg then f.power.bg:Hide() end
else
-- Classic style: restore backdrops
SFrames:CreateUnitBackdrop(f)
if f.health and f.health.bg then f.health.bg:Show() end
if f.power and f.power.bg then f.power.bg:Show() end
if f.healthBGFrame then
SFrames:CreateUnitBackdrop(f.healthBGFrame)
f.healthBGFrame:Show()
f.healthBGFrame:ClearAllPoints()
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
end
if f.powerBGFrame then
SFrames:CreateUnitBackdrop(f.powerBGFrame)
f.powerBGFrame:Show()
f.powerBGFrame:ClearAllPoints()
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
end
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
f.health:SetHeight(18)
end
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
end
SFrames:RemoveGradientStyle(f.health)
SFrames:RemoveGradientStyle(f.power)
end
end
function SFrames.Pet:UpdateAll()
if UnitExists("pet") then
if SFramesDB and SFramesDB.showPetFrame == false then
@@ -437,6 +537,9 @@ function SFrames.Pet:UpdateAll()
local r, g, b = 0.33, 0.59, 0.33
self.frame.health:SetStatusBarColor(r, g, b)
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
else
self.frame:Hide()
if self.foodPanel then self.foodPanel:Hide() end
@@ -465,12 +568,6 @@ function SFrames.Pet:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
local hp = UnitHealth("pet") or 0
local maxHp = UnitHealthMax("pet") or 0
@@ -485,7 +582,7 @@ function SFrames.Pet:UpdateHealPrediction()
end
if maxHp <= 0 or UnitIsDeadOrGhost("pet") then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -497,7 +594,7 @@ function SFrames.Pet:UpdateHealPrediction()
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -505,13 +602,13 @@ function SFrames.Pet:UpdateHealPrediction()
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = self.frame.health:GetWidth()
if barWidth <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -521,7 +618,7 @@ function SFrames.Pet:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -587,6 +684,9 @@ function SFrames.Pet:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end
function SFrames.Pet:UpdatePower()
@@ -594,6 +694,7 @@ function SFrames.Pet:UpdatePower()
local maxPower = UnitManaMax("pet")
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "pet")
end
function SFrames.Pet:UpdateHappiness()

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,76 @@ local UNIT_PADDING = 2
local RAID_UNIT_LOOKUP = {}
for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end
-- Pre-allocated tables reused every UpdateAuras call to avoid per-call garbage
local _foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
local _debuffColor = { r = 0, g = 0, b = 0 }
-- Module-level helper: match aura name against a list (no closure allocation)
local function MatchesList(auraName, list)
for _, name in ipairs(list) do
if string.find(auraName, name) then
return true
end
end
return false
end
-- Module-level helper: get buff name via SuperWoW aura ID or tooltip scan
local function RaidGetBuffName(unit, index)
if SFrames.superwow_active and SpellInfo then
local texture, auraID = UnitBuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture
end
end
end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitBuff(unit, index)
local buffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
return buffName, UnitBuff(unit, index)
end
local function RaidGetDebuffName(unit, index)
if SFrames.superwow_active and SpellInfo then
local texture, count, dtype, auraID = UnitDebuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture, count, dtype
end
end
end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitDebuff(unit, index)
local debuffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
local texture, count, dtype = UnitDebuff(unit, index)
return debuffName, texture, count, dtype
end
local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit)
end
local function Clamp(value, minValue, maxValue)
if value < minValue then
return minValue
end
if value > maxValue then
return maxValue
end
return value
end
local function SetTextureIfPresent(region, texturePath)
if region and region.SetTexture and texturePath then
region:SetTexture(texturePath)
end
end
function SFrames.Raid:GetMetrics()
local db = SFramesDB or {}
@@ -25,29 +91,66 @@ function SFrames.Raid:GetMetrics()
local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8)
healthHeight = math.max(10, math.min(height - 6, healthHeight))
local powerHeight = height - healthHeight - 3
local powerHeight = tonumber(db.raidPowerHeight) or (height - healthHeight - 3)
powerHeight = math.max(0, math.min(height - 3, powerHeight))
if not db.raidShowPower then
powerHeight = 0
healthHeight = height - 2
end
local gradientStyle = SFrames:IsGradientStyle()
local availablePowerWidth = width - 2
if availablePowerWidth < 20 then
availablePowerWidth = 20
end
local rawPowerWidth = tonumber(db.raidPowerWidth)
local legacyFullWidth = tonumber(db.raidFrameWidth) or width
local defaultPowerWidth = gradientStyle and width or availablePowerWidth
local maxPowerWidth = gradientStyle and width or availablePowerWidth
local powerWidth
if gradientStyle then
-- 渐变风格:能量条始终与血条等宽(全宽)
powerWidth = width
elseif not rawPowerWidth
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
powerWidth = defaultPowerWidth
else
powerWidth = rawPowerWidth
end
powerWidth = Clamp(math.floor(powerWidth + 0.5), 20, maxPowerWidth)
local powerOffsetX = Clamp(math.floor((tonumber(db.raidPowerOffsetX) or 0) + 0.5), -120, 120)
local powerOffsetY = Clamp(math.floor((tonumber(db.raidPowerOffsetY) or 0) + 0.5), -80, 80)
local hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING
local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING
local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING
local nameFont = tonumber(db.raidNameFontSize) or 10
local valueFont = tonumber(db.raidValueFontSize) or 9
local healthFont = tonumber(db.raidHealthFontSize) or valueFont
local powerFont = tonumber(db.raidPowerFontSize) or valueFont
return {
width = width,
height = height,
healthHeight = healthHeight,
powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.raidPowerOnTop == true,
horizontalGap = hgap,
verticalGap = vgap,
groupGap = groupGap,
nameFont = nameFont,
valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("raidHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("raidPowerTexture", "barTexture"),
showPower = db.raidShowPower ~= false,
}
end
@@ -87,8 +190,8 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
frame.power:Show()
if frame.powerBGFrame then frame.powerBGFrame:Show() end
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
else
frame.power:Hide()
@@ -96,14 +199,80 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
end
end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
SFrames:ApplyStatusBarTexture(frame.health, "raidHealthTexture", "barTexture")
SFrames:ApplyStatusBarTexture(frame.power, "raidPowerTexture", "barTexture")
if frame.health and frame.power then
local healthLevel = frame:GetFrameLevel() + 2
local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
frame.health:SetFrameLevel(healthLevel)
frame.power:SetFrameLevel(powerLevel)
end
SFrames:ApplyConfiguredUnitBackdrop(frame, "raid")
if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "raid") end
if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "raid") end
SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
-- Gradient style preset
if SFrames:IsGradientStyle() then
-- Strip backdrops
SFrames:ClearBackdrop(frame)
SFrames:ClearBackdrop(frame.healthBGFrame)
SFrames:ClearBackdrop(frame.powerBGFrame)
-- Health bar full width
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
frame.health:SetHeight(metrics.healthHeight)
end
-- Power bar full width
if frame.power and metrics.showPower then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
end
-- Apply gradient overlays
SFrames:ApplyGradientStyle(frame.health)
SFrames:ApplyGradientStyle(frame.power)
-- Flush BG frames
if frame.healthBGFrame then
frame.healthBGFrame:ClearAllPoints()
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
end
if frame.powerBGFrame then
frame.powerBGFrame:ClearAllPoints()
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
end
-- Hide bar backgrounds (transparent)
if frame.healthBGFrame then frame.healthBGFrame:Hide() end
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
if frame.health and frame.health.bg then frame.health.bg:Hide() end
if frame.power and frame.power.bg then frame.power.bg:Hide() end
else
SFrames:RemoveGradientStyle(frame.health)
SFrames:RemoveGradientStyle(frame.power)
-- Restore bar backgrounds
if frame.healthBGFrame then frame.healthBGFrame:Show() end
if frame.powerBGFrame then frame.powerBGFrame:Show() end
if frame.health and frame.health.bg then frame.health.bg:Show() end
if frame.power and frame.power.bg then frame.power.bg:Show() end
end
if frame.nameText then
frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "raidNameFontKey", "fontKey")
end
if frame.healthText then
frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "raidHealthFontKey", "fontKey")
end
if frame.powerText then
SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "raidPowerFontKey", "fontKey")
end
end
@@ -724,6 +893,9 @@ function SFrames.Raid:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1)
end
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.health)
end
self:UpdateHealth(unit)
self:UpdatePower(unit)
@@ -794,11 +966,10 @@ function SFrames.Raid:UpdateHealth(unit)
txt = percent .. "%"
elseif db.raidHealthFormat == "deficit" then
if maxHp - hp > 0 then
txt = "-" .. (maxHp - hp)
txt = "-" .. SFrames:FormatCompactNumber(maxHp - hp)
end
else
txt = (math.floor(hp/100)/10).."k" -- default compact e.g. 4.5k
if hp < 1000 then txt = tostring(hp) end
txt = SFrames:FormatCompactNumber(hp)
end
f.healthText:SetText(txt)
@@ -826,14 +997,8 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther
local predOver = f.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists(unit) or not UnitIsConnected(unit) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -855,7 +1020,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end
if maxHp <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -872,7 +1037,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -880,13 +1045,13 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = f:GetWidth() - 2
if barWidth <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -896,7 +1061,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local availableWidth = barWidth - currentPosition
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -972,6 +1137,10 @@ function SFrames.Raid:UpdatePower(unit)
local pType = UnitPowerType(unit)
local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0]
f.power:SetStatusBarColor(color.r, color.g, color.b)
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.power)
end
SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
break
end
end
@@ -1072,7 +1241,11 @@ function SFrames.Raid:UpdateAuras(unit)
local f = frameData.frame
local buffsNeeded = self:GetClassBuffs()
local foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
-- Reuse pre-allocated table
_foundIndicators[1] = false
_foundIndicators[2] = false
_foundIndicators[3] = false
_foundIndicators[4] = false
-- Hide all first
for i = 1, 4 do
@@ -1085,70 +1258,22 @@ function SFrames.Raid:UpdateAuras(unit)
return
end
local function MatchesList(auraName, list)
for _, name in ipairs(list) do
if string.find(auraName, name) then
return true
end
end
return false
end
-- Helper: get buff name via SuperWoW aura ID (fast) or tooltip scan (fallback)
local hasSuperWoW = SFrames.superwow_active and SpellInfo
local function GetBuffName(unit, index)
if hasSuperWoW then
local texture, auraID = UnitBuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture
end
end
end
-- Fallback: tooltip scan
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitBuff(unit, index)
local buffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
return buffName, UnitBuff(unit, index)
end
local function GetDebuffName(unit, index)
if hasSuperWoW then
local texture, count, dtype, auraID = UnitDebuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture, count, dtype
end
end
end
-- Fallback: tooltip scan
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitDebuff(unit, index)
local debuffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
local texture, count, dtype = UnitDebuff(unit, index)
return debuffName, texture, count, dtype
end
-- Check Buffs
-- Check Buffs (using module-level helpers, no closures)
for i = 1, 32 do
local texture, applications = UnitBuff(unit, i)
if not texture then break end
local buffName = GetBuffName(unit, i)
local buffName = RaidGetBuffName(unit, i)
if buffName then
for pos, listData in pairs(buffsNeeded) do
if pos <= 4 and not listData.isDebuff and not foundIndicators[pos] then
if pos <= 4 and not listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(buffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = false
f.indicators[pos]:Show()
foundIndicators[pos] = true
_foundIndicators[pos] = true
end
end
end
@@ -1156,7 +1281,10 @@ function SFrames.Raid:UpdateAuras(unit)
end
local hasDebuff = false
local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
-- Reuse pre-allocated table
_debuffColor.r = _A.slotBg[1]
_debuffColor.g = _A.slotBg[2]
_debuffColor.b = _A.slotBg[3]
-- Check Debuffs
for i = 1, 16 do
@@ -1165,24 +1293,24 @@ function SFrames.Raid:UpdateAuras(unit)
if dispelType then
hasDebuff = true
if dispelType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
elseif dispelType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
elseif dispelType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
elseif dispelType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
if dispelType == "Magic" then _debuffColor.r = 0.2; _debuffColor.g = 0.6; _debuffColor.b = 1
elseif dispelType == "Curse" then _debuffColor.r = 0.6; _debuffColor.g = 0; _debuffColor.b = 1
elseif dispelType == "Disease" then _debuffColor.r = 0.6; _debuffColor.g = 0.4; _debuffColor.b = 0
elseif dispelType == "Poison" then _debuffColor.r = 0; _debuffColor.g = 0.6; _debuffColor.b = 0
end
end
local debuffName = GetDebuffName(unit, i)
local debuffName = RaidGetDebuffName(unit, i)
if debuffName then
for pos, listData in pairs(buffsNeeded) do
if pos <= 4 and listData.isDebuff and not foundIndicators[pos] then
if pos <= 4 and listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(debuffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = true
f.indicators[pos]:Show()
foundIndicators[pos] = true
_foundIndicators[pos] = true
end
end
end
@@ -1190,9 +1318,8 @@ function SFrames.Raid:UpdateAuras(unit)
end
if hasDebuff then
f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
f.health.bg:SetVertexColor(_debuffColor.r, _debuffColor.g, _debuffColor.b, 1)
else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end
end

View File

@@ -182,6 +182,17 @@ local function Clamp(value, minValue, maxValue)
return value
end
local function SetTextureIfPresent(region, texturePath)
if region and region.SetTexture and texturePath then
region:SetTexture(texturePath)
end
end
local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey)
if not fs then return end
SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey)
end
local DIST_BASE_WIDTH = 80
local DIST_BASE_HEIGHT = 24
local DIST_BASE_FONTSIZE = 14
@@ -232,6 +243,33 @@ function SFrames.Target:GetConfig()
local powerHeight = tonumber(db.targetPowerHeight) or 9
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40)
local showPortrait = db.targetShowPortrait ~= false
local gradientStyle = SFrames:IsGradientStyle()
local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2
if classicDefaultPowerWidth < 60 then
classicDefaultPowerWidth = 60
end
local rawPowerWidth = tonumber(db.targetPowerWidth)
local legacyFullWidth = tonumber(db.targetFrameWidth) or width
local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth
local maxPowerWidth = gradientStyle and width or (width - 2)
local powerWidth
if gradientStyle then
-- 渐变风格:能量条始终与血条等宽(全宽)
powerWidth = width
elseif not rawPowerWidth
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then
powerWidth = defaultPowerWidth
else
powerWidth = rawPowerWidth
end
powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth)
local powerOffsetX = Clamp(math.floor((tonumber(db.targetPowerOffsetX) or 0) + 0.5), -120, 120)
local powerOffsetY = Clamp(math.floor((tonumber(db.targetPowerOffsetY) or 0) + 0.5), -80, 80)
local height = healthHeight + powerHeight + 4
height = Clamp(height, 30, 140)
@@ -241,6 +279,12 @@ function SFrames.Target:GetConfig()
local valueFont = tonumber(db.targetValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
local healthFont = tonumber(db.targetHealthFontSize) or valueFont
healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
local powerFont = tonumber(db.targetPowerFontSize) or valueFont
powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
local frameScale = tonumber(db.targetFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8)
@@ -250,8 +294,16 @@ function SFrames.Target:GetConfig()
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.targetPowerOnTop == true,
nameFont = nameFont,
valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("targetHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("targetPowerTexture", "barTexture"),
scale = frameScale,
}
end
@@ -334,8 +386,8 @@ function SFrames.Target:ApplyConfig()
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY)
f.power:SetWidth(cfg.powerWidth)
f.power:SetHeight(cfg.powerHeight)
end
@@ -345,19 +397,71 @@ function SFrames.Target:ApplyConfig()
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
SFrames:ApplyStatusBarTexture(f.health, "targetHealthTexture", "barTexture")
SFrames:ApplyStatusBarTexture(f.power, "targetPowerTexture", "barTexture")
SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture)
SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture)
SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture)
SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture)
SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture)
if f.nameText then
f.nameText:SetFont(fontPath, cfg.nameFont, outline)
end
if f.healthText then
f.healthText:SetFont(fontPath, cfg.valueFont, outline)
end
if f.powerText then
f.powerText:SetFont(fontPath, cfg.valueFont, outline)
-- Gradient style preset
if SFrames:IsGradientStyle() then
-- Hide portrait & its backdrop
if f.portrait then f.portrait:Hide() end
if f.portraitBG then f.portraitBG:Hide() end
-- Strip backdrops
SFrames:ClearBackdrop(f)
SFrames:ClearBackdrop(f.healthBGFrame)
SFrames:ClearBackdrop(f.powerBGFrame)
-- Health bar full width
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
f.health:SetHeight(cfg.healthHeight)
end
-- Power bar full width, below health
if f.power then
f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -2 + cfg.powerOffsetY)
f.power:SetWidth(cfg.powerWidth)
f.power:SetHeight(cfg.powerHeight)
end
-- Apply gradient overlays
SFrames:ApplyGradientStyle(f.health)
SFrames:ApplyGradientStyle(f.power)
-- Flush BG frames (no border padding)
if f.healthBGFrame then
f.healthBGFrame:ClearAllPoints()
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
end
if f.powerBGFrame then
f.powerBGFrame:ClearAllPoints()
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
end
-- Hide bar backgrounds (transparent)
if f.healthBGFrame then f.healthBGFrame:Hide() end
if f.powerBGFrame then f.powerBGFrame:Hide() end
if f.health and f.health.bg then f.health.bg:Hide() end
if f.power and f.power.bg then f.power.bg:Hide() end
else
-- Classic style: remove gradient overlays if they exist
SFrames:RemoveGradientStyle(f.health)
SFrames:RemoveGradientStyle(f.power)
-- Restore bar backgrounds
if f.healthBGFrame then f.healthBGFrame:Show() end
if f.powerBGFrame then f.powerBGFrame:Show() end
if f.health and f.health.bg then f.health.bg:Show() end
if f.power and f.power.bg then f.power.bg:Show() end
end
ApplyFontIfPresent(f.nameText, cfg.nameFont, "targetNameFontKey")
ApplyFontIfPresent(f.healthText, cfg.healthFont, "targetHealthFontKey")
ApplyFontIfPresent(f.powerText, cfg.powerFont, "targetPowerFontKey")
if f.castbar then
f.castbar:ClearAllPoints()
if showPortrait then
@@ -374,6 +478,24 @@ function SFrames.Target:ApplyConfig()
self:ApplyDistanceScale(dScale)
end
if f.distText then
local dfs = (db.targetDistanceFontSize and tonumber(db.targetDistanceFontSize)) or 10
dfs = Clamp(dfs, 8, 24)
SFrames:ApplyFontString(f.distText, dfs, "targetDistanceFontKey", "fontKey")
end
if f.health and f.power then
local healthLevel = f:GetFrameLevel() + 2
local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
f.health:SetFrameLevel(healthLevel)
f.power:SetFrameLevel(powerLevel)
end
SFrames:ApplyConfiguredUnitBackdrop(f, "target")
if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "target") end
if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "target") end
if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "target", true) end
if UnitExists("target") then
self:UpdateAll()
end
@@ -386,8 +508,6 @@ function SFrames.Target:ApplyDistanceScale(scale)
f:SetWidth(DIST_BASE_WIDTH * scale)
f:SetHeight(DIST_BASE_HEIGHT * scale)
if f.text then
local fontPath = SFrames:GetFont()
local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)
local fontSize
if customSize and customSize >= 8 and customSize <= 24 then
@@ -395,7 +515,7 @@ function SFrames.Target:ApplyDistanceScale(scale)
else
fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
end
f.text:SetFont(fontPath, fontSize, outline)
SFrames:ApplyFontString(f.text, fontSize, "targetDistanceFontKey", "fontKey")
end
end
@@ -437,54 +557,46 @@ function SFrames.Target:InitializeDistanceFrame()
f.text:SetShadowColor(0, 0, 0, 1)
f.text:SetShadowOffset(1, -1)
-- Behind indicator text (shown next to distance)
f.behindText = SFrames:CreateFontString(f, fontSize, "LEFT")
f.behindText:SetPoint("LEFT", f.text, "RIGHT", 4, 0)
f.behindText:SetShadowColor(0, 0, 0, 1)
f.behindText:SetShadowOffset(1, -1)
f.behindText:Hide()
SFrames.Target.distanceFrame = f
f:Hide()
f.timer = 0
f:SetScript("OnUpdate", function()
if SFramesDB and SFramesDB.targetDistanceEnabled == false then
if this:IsShown() then this:Hide() end
local ticker = CreateFrame("Frame", nil, UIParent)
ticker:SetWidth(1)
ticker:SetHeight(1)
ticker.timer = 0
ticker:Show()
ticker:SetScript("OnUpdate", function()
local distFrame = SFrames.Target.distanceFrame
if not distFrame then return end
local disabled = SFramesDB and SFramesDB.targetDistanceEnabled == false
local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
local tgtFrame = SFrames.Target and SFrames.Target.frame
local embeddedText = tgtFrame and tgtFrame.distText
if disabled then
if distFrame:IsShown() then distFrame:Hide() end
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
return
end
if not UnitExists("target") then
if this:IsShown() then this:Hide() end
if this.behindText then this.behindText:Hide() end
if distFrame:IsShown() then distFrame:Hide() end
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
return
end
this.timer = this.timer + (arg1 or 0)
if this.timer >= 0.4 then
this.timer = 0
local dist = SFrames.Target:GetDistance("target")
this.text:SetText(dist or "---")
if not this:IsShown() then this:Show() end
local distStr = dist or "---"
-- Behind indicator
if this.behindText then
local showBehind = not SFramesDB or SFramesDB.Tweaks == nil
or SFramesDB.Tweaks.behindIndicator ~= false
if showBehind and IsUnitXPAvailable() then
local ok, isBehind = pcall(UnitXP, "behind", "player", "target")
if ok and isBehind then
this.behindText:SetText("背后")
this.behindText:SetTextColor(0.2, 1.0, 0.3)
this.behindText:Show()
elseif ok then
this.behindText:SetText("正面")
this.behindText:SetTextColor(1.0, 0.35, 0.3)
this.behindText:Show()
else
this.behindText:Hide()
end
else
this.behindText:Hide()
end
if onFrame and embeddedText then
embeddedText:SetText(distStr)
if not embeddedText:IsShown() then embeddedText:Show() end
if distFrame:IsShown() then distFrame:Hide() end
else
distFrame.text:SetText(distStr)
if not distFrame:IsShown() then distFrame:Show() end
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
end
end
end)
@@ -515,15 +627,23 @@ function SFrames.Target:Initialize()
local f = CreateFrame("Button", "SFramesTargetFrame", UIParent)
f:SetWidth(SFrames.Config.width)
f:SetHeight(SFrames.Config.height)
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
local pos = SFramesDB.Positions["TargetFrame"]
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
else
f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) -- Mirrored from player
end
local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1
f:SetScale(frameScale)
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
local pos = SFramesDB.Positions["TargetFrame"]
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
f:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
else
f:SetPoint("CENTER", UIParent, "CENTER", 200, -100)
end
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
@@ -533,25 +653,21 @@ function SFrames.Target:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then
xOfs = (xOfs or 0) * fSc
yOfs = (yOfs or 0) * fSc
end
SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] OnClick fired: " .. tostring(arg1) .. "|r")
if arg1 == "LeftButton" then
-- Shift+左键 = 设为焦点
if IsShiftKeyDown() then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Shift+LeftButton -> SetFocus|r")
if SFrames.Focus and SFrames.Focus.SetFromTarget then
local ok, err = pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
if ok then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Focus set OK|r")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Focus error: " .. tostring(err) .. "|r")
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] SFrames.Focus missing!|r")
pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
end
return
end
@@ -562,31 +678,25 @@ function SFrames.Target:Initialize()
SpellTargetUnit(this.unit)
end
elseif arg1 == "RightButton" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] RightButton hit|r")
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
return
end
if not UnitExists("target") then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] No target, abort|r")
return
end
if not SFrames.Target.dropDown then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Creating dropdown...|r")
local ok1, err1 = pcall(function()
SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate")
end)
if not ok1 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] CreateFrame failed: " .. tostring(err1) .. "|r")
return
end
SFrames.Target.dropDown.displayMode = "MENU"
SFrames.Target.dropDown.initialize = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] initialize() called|r")
local dd = SFrames.Target.dropDown
local name = dd.targetName
if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] initialize: no targetName|r")
return
end
@@ -678,16 +788,9 @@ function SFrames.Target:Initialize()
-- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位)
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Dropdown created OK|r")
end
SFrames.Target.dropDown.targetName = UnitName("target")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Calling ToggleDropDownMenu...|r")
local ok3, err3 = pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
if not ok3 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] ToggleDropDownMenu failed: " .. tostring(err3) .. "|r")
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] ToggleDropDownMenu OK|r")
end
pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
end
end)
f:SetScript("OnReceiveDrag", function()
@@ -799,6 +902,19 @@ function SFrames.Target:Initialize()
f.comboText:SetTextColor(1, 0.8, 0)
f.comboText:SetText("")
-- Embedded distance text (high-level overlay so it's never covered)
local distOverlay = CreateFrame("Frame", nil, f)
distOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 20)
distOverlay:SetAllPoints(f.health)
local distFS = (SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)) or 10
f.distText = SFrames:CreateFontString(distOverlay, distFS, "CENTER")
f.distText:SetPoint("CENTER", f.health, "TOP", 0, 0)
f.distText:SetTextColor(1, 0.82, 0.25)
f.distText:SetShadowColor(0, 0, 0, 1)
f.distText:SetShadowOffset(1, -1)
f.distText:SetText("")
f.distText:Hide()
-- Raid Target Icon (top center of health bar, half outside frame)
local raidIconSize = 22
local raidIconOvr = CreateFrame("Frame", nil, f)
@@ -864,7 +980,8 @@ function SFrames.Target:Initialize()
-- Register movers
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("TargetFrame", f, "目标",
"CENTER", "UIParent", "CENTER", 200, -100)
"CENTER", "UIParent", "CENTER", 200, -100,
nil, { alwaysShowInLayout = true })
if SFrames.Target.distanceFrame then
SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离",
"CENTER", "UIParent", "CENTER", 0, 100)
@@ -1047,18 +1164,24 @@ function SFrames.Target:OnTargetChanged()
if UnitExists("target") then
self.frame:Show()
self:UpdateAll()
local enabled = not (SFramesDB and SFramesDB.targetDistanceEnabled == false)
local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
if SFrames.Target.distanceFrame then
local dist = self:GetDistance("target")
SFrames.Target.distanceFrame.text:SetText(dist or "---")
if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then
SFrames.Target.distanceFrame:Show()
else
if onFrame and self.frame.distText then
self.frame.distText:SetText(dist or "---")
if enabled then self.frame.distText:Show() else self.frame.distText:Hide() end
SFrames.Target.distanceFrame:Hide()
else
SFrames.Target.distanceFrame.text:SetText(dist or "---")
if enabled then SFrames.Target.distanceFrame:Show() else SFrames.Target.distanceFrame:Hide() end
if self.frame.distText then self.frame.distText:Hide() end
end
end
else
self.frame:Hide()
if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end
if self.frame and self.frame.distText then self.frame.distText:Hide() end
end
end
@@ -1153,6 +1276,8 @@ function SFrames.Target:UpdateAll()
end
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
-- Gradient style always uses class colors
if SFrames:IsGradientStyle() then useClassColor = true end
if UnitIsPlayer("target") and useClassColor then
local _, class = UnitClass("target")
@@ -1184,6 +1309,10 @@ function SFrames.Target:UpdateAll()
self.frame.nameText:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b)
end
-- Re-apply gradient after color change
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
end
function SFrames.Target:UpdateHealth()
@@ -1208,9 +1337,9 @@ function SFrames.Target:UpdateHealth()
end
if displayMax > 0 then
self.frame.healthText:SetText(displayHp .. " / " .. displayMax)
self.frame.healthText:SetText(SFrames:FormatCompactPair(displayHp, displayMax))
else
self.frame.healthText:SetText(displayHp)
self.frame.healthText:SetText(SFrames:FormatCompactNumber(displayHp))
end
self:UpdateHealPrediction()
@@ -1222,14 +1351,8 @@ function SFrames.Target:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists("target") then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1251,7 +1374,7 @@ function SFrames.Target:UpdateHealPrediction()
end
if maxHp <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1268,7 +1391,7 @@ function SFrames.Target:UpdateHealPrediction()
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1276,14 +1399,14 @@ function SFrames.Target:UpdateHealPrediction()
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false
local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2)
if barWidth <= 0 then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1293,7 +1416,7 @@ function SFrames.Target:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1359,6 +1482,9 @@ function SFrames.Target:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end
function SFrames.Target:UpdatePower()
@@ -1367,10 +1493,11 @@ function SFrames.Target:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
if maxPower > 0 then
self.frame.powerText:SetText(power .. " / " .. maxPower)
self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
self.frame.powerText:SetText("")
end
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "target")
end
function SFrames.Target:UpdateComboPoints()
@@ -1501,23 +1628,15 @@ end
function SFrames.Target:TickAuras()
if not UnitExists("target") then return end
local timeNow = GetTime()
local tracker = SFrames.AuraTracker
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.FindEffectData
local targetName, targetLevel, targetGUID
if hasNP then
targetName = UnitName("target")
targetLevel = UnitLevel("target") or 0
targetGUID = UnitGUID and UnitGUID("target")
end
-- Buffs
for i = 1, 32 do
local b = self.frame.buffs[i]
if b:IsShown() and b.expirationTime then
local timeLeft = b.expirationTime - timeNow
if timeLeft > 0 and timeLeft < 3600 then
if b:IsShown() then
local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "buff", i)
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text)
@@ -1531,30 +1650,11 @@ function SFrames.Target:TickAuras()
end
end
-- Debuffs: re-query SpellDB for live-accurate timers
-- Debuffs
for i = 1, 32 do
local b = self.frame.debuffs[i]
if b:IsShown() then
local timeLeft = nil
if hasNP and b.effectName then
local data = targetGUID and NanamiPlates_SpellDB:FindEffectData(targetGUID, targetLevel, b.effectName)
if not data and targetName then
data = NanamiPlates_SpellDB:FindEffectData(targetName, targetLevel, b.effectName)
end
if data and data.start and data.duration then
local remaining = data.duration + data.start - timeNow
if remaining > 0 then
timeLeft = remaining
b.expirationTime = timeNow + remaining
end
end
end
if not timeLeft and b.expirationTime then
timeLeft = b.expirationTime - timeNow
end
local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "debuff", i)
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
@@ -1573,6 +1673,11 @@ end
function SFrames.Target:UpdateAuras()
if not UnitExists("target") then return end
local tracker = SFrames.AuraTracker
if tracker and tracker.HandleAuraSnapshot then
tracker:HandleAuraSnapshot("target")
end
local hasSuperWoW = SFrames.superwow_active and SpellInfo
local numBuffs = 0
-- Buffs
@@ -1584,12 +1689,9 @@ function SFrames.Target:UpdateAuras()
b.icon:SetTexture(texture)
-- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swAuraID or nil
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetUnitBuff("target", i)
local timeLeft = SFrames:GetAuraTimeLeft("target", i, true)
SFrames.Tooltip:Hide()
local state = tracker and tracker:GetAuraState("target", "buff", i)
local timeLeft = state and tracker:GetAuraTimeLeft("target", "buff", i)
if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft
b.cdText:SetText(SFrames:FormatTime(timeLeft))
@@ -1621,7 +1723,6 @@ function SFrames.Target:UpdateAuras()
end
-- Debuffs
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
for i = 1, 32 do
@@ -1633,39 +1734,12 @@ function SFrames.Target:UpdateAuras()
-- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swDebuffAuraID or nil
local timeLeft = 0
local effectName = nil
if hasNP then
local effect, rank, _, stacks, dtype, duration, npTimeLeft, isOwn = NanamiPlates_SpellDB:UnitDebuff("target", i)
effectName = effect
if npTimeLeft and npTimeLeft > 0 then
timeLeft = npTimeLeft
elseif effect and effect ~= "" and duration and duration > 0
and NanamiPlates_Auras and NanamiPlates_Auras.timers then
local unitKey = (UnitGUID and UnitGUID("target")) or UnitName("target") or ""
local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect]
if not cached and UnitName("target") then
cached = NanamiPlates_Auras.timers[UnitName("target") .. "_" .. effect]
end
if cached and cached.startTime and cached.duration then
local remaining = cached.duration - (GetTime() - cached.startTime)
if remaining > 0 then timeLeft = remaining end
end
end
end
if timeLeft <= 0 then
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetUnitDebuff("target", i)
timeLeft = SFrames:GetAuraTimeLeft("target", i, false)
SFrames.Tooltip:Hide()
end
local state = tracker and tracker:GetAuraState("target", "debuff", i)
local timeLeft = state and tracker:GetAuraTimeLeft("target", "debuff", i)
if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft
b.effectName = effectName
b.effectName = state and state.name or nil
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text)

View File

@@ -1,12 +1,36 @@
SFrames.ToT = {}
local _A = SFrames.ActiveTheme
function SFrames.ToT:ApplyConfig()
local f = self.frame
if not f then return end
SFrames:ApplyStatusBarTexture(f.health, "totHealthTexture", "barTexture")
local tex = SFrames:ResolveBarTexture("totHealthTexture", "barTexture")
if f.health and f.health.bg then f.health.bg:SetTexture(tex) end
if SFrames:IsGradientStyle() then
SFrames:ApplyGradientStyle(f.health)
if f.hbg then f.hbg:Hide() end
if f.health and f.health.bg then f.health.bg:Hide() end
else
SFrames:RemoveGradientStyle(f.health)
if f.hbg then f.hbg:Show() end
if f.health and f.health.bg then f.health.bg:Show() end
end
end
function SFrames.ToT:Initialize()
local f = CreateFrame("Button", "SFramesToTFrame", UIParent)
f:SetWidth(120)
f:SetHeight(25)
f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ToTFrame"] then
local pos = SFramesDB.Positions["ToTFrame"]
f:SetPoint(pos.point or "BOTTOMLEFT", UIParent, pos.relativePoint or "BOTTOMLEFT",
pos.xOfs or 0, pos.yOfs or 0)
else
f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
end
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
@@ -24,6 +48,7 @@ function SFrames.ToT:Initialize()
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
f.hbg = hbg
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
@@ -35,7 +60,15 @@ function SFrames.ToT:Initialize()
self.frame = f
f:Hide()
self:ApplyConfig()
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("ToTFrame", f, "目标的目标",
"BOTTOMLEFT", "SFramesTargetFrame", "BOTTOMRIGHT", 5, 0,
nil, { alwaysShowInLayout = true })
end
-- Update loop since targettarget changes don't fire precise events in Vanilla
self.updater = CreateFrame("Frame")
self.updater.timer = 0

View File

@@ -52,6 +52,26 @@ local function HookScript(frame, script, fn)
end)
end
local function SafeSetMapToCurrentZone()
if not SetMapToCurrentZone then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
end
return pcall(SetMapToCurrentZone)
end
local function SafeSetMapZoom(continent, zone)
if not SetMapZoom then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
end
return pcall(SetMapZoom, continent, zone)
end
--------------------------------------------------------------------------------
-- 1. Hide Blizzard Decorations
-- Called at init AND on every WorldMapFrame:OnShow to counter Blizzard resets
@@ -951,12 +971,18 @@ local function CreateWaypointPin()
pinLabel:SetText("")
local btnW, btnH = 52, 18
local PIN_BTN_BACKDROP = {
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
}
pinShareBtn = CreateFrame("Button", nil, pinFrame)
pinShareBtn:SetWidth(btnW)
pinShareBtn:SetHeight(btnH)
pinShareBtn:SetPoint("TOPLEFT", pinFrame, "BOTTOMLEFT", -10, -8)
pinShareBtn:SetBackdrop(PANEL_BACKDROP)
pinShareBtn:SetPoint("TOP", pinLabel, "BOTTOM", -(btnW / 2 + 2), -4)
pinShareBtn:SetBackdrop(PIN_BTN_BACKDROP)
pinShareBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
pinShareBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local shareFS = pinShareBtn:CreateFontString(nil, "OVERLAY")
@@ -966,9 +992,11 @@ local function CreateWaypointPin()
shareFS:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
pinShareBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
pinShareBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
pinShareBtn:SetScript("OnClick", function()
WM:ShareWaypoint()
@@ -978,7 +1006,7 @@ local function CreateWaypointPin()
pinClearBtn:SetWidth(btnW)
pinClearBtn:SetHeight(btnH)
pinClearBtn:SetPoint("LEFT", pinShareBtn, "RIGHT", 4, 0)
pinClearBtn:SetBackdrop(PANEL_BACKDROP)
pinClearBtn:SetBackdrop(PIN_BTN_BACKDROP)
pinClearBtn:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
pinClearBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local clearFS = pinClearBtn:CreateFontString(nil, "OVERLAY")
@@ -988,9 +1016,11 @@ local function CreateWaypointPin()
clearFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
pinClearBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
pinClearBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
pinClearBtn:SetScript("OnClick", function()
WM:ClearWaypoint()
@@ -1099,7 +1129,7 @@ function WM:HandleWaypointLink(data)
if waited >= 0.05 then
timer:SetScript("OnUpdate", nil)
if SetMapZoom then
SetMapZoom(pending.continent, pending.zone)
SafeSetMapZoom(pending.continent, pending.zone)
end
WM:SetWaypoint(pending.continent, pending.zone, pending.x, pending.y, pending.name)
end
@@ -1138,7 +1168,7 @@ local function DiscoverDmfZoneIndices()
local target = string.lower(loc.zone)
local zones = { GetMapZones(loc.cont) }
for idx = 1, table.getn(zones) do
SetMapZoom(loc.cont, idx)
SafeSetMapZoom(loc.cont, idx)
local mf = GetMapInfo and GetMapInfo() or ""
if mf ~= "" then
if mf == loc.zone then
@@ -1158,11 +1188,11 @@ local function DiscoverDmfZoneIndices()
end
end
if savedZ > 0 then
SetMapZoom(savedC, savedZ)
SafeSetMapZoom(savedC, savedZ)
elseif savedC > 0 then
SetMapZoom(savedC, 0)
SafeSetMapZoom(savedC, 0)
else
if SetMapToCurrentZone then SetMapToCurrentZone() end
SafeSetMapToCurrentZone()
end
end
@@ -1390,7 +1420,7 @@ SlashCmdList["DMFMAP"] = function(msg)
cf:AddMessage("|cffffcc66[DMF Scan] " .. cname .. " (cont=" .. c .. "):|r")
local zones = { GetMapZones(c) }
for idx = 1, table.getn(zones) do
SetMapZoom(c, idx)
SafeSetMapZoom(c, idx)
local mf = GetMapInfo and GetMapInfo() or "(nil)"
local zname = zones[idx] or "?"
if string.find(string.lower(mf), "elwynn") or string.find(string.lower(zname), "elwynn")
@@ -1401,9 +1431,9 @@ SlashCmdList["DMFMAP"] = function(msg)
end
end
end
if savedZ > 0 then SetMapZoom(savedC, savedZ)
elseif savedC > 0 then SetMapZoom(savedC, 0)
else if SetMapToCurrentZone then SetMapToCurrentZone() end end
if savedZ > 0 then SafeSetMapZoom(savedC, savedZ)
elseif savedC > 0 then SafeSetMapZoom(savedC, 0)
else SafeSetMapToCurrentZone() end
return
end
local ai, iw, dl, ds = GetDmfSchedule()
@@ -1931,7 +1961,7 @@ local function UpdateNavMap()
if not N.frame or not N.tiles or not N.frame:IsVisible() then return end
if WorldMapFrame and WorldMapFrame:IsVisible() then return end
if SetMapToCurrentZone then SetMapToCurrentZone() end
SafeSetMapToCurrentZone()
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile ~= "" and mapFile ~= N.curMap then

View File

@@ -0,0 +1,426 @@
from __future__ import annotations
import argparse
import re
import textwrap
import xml.etree.ElementTree as ET
import zipfile
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path, PurePosixPath
MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
DOC_REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
PKG_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
NS = {
"main": MAIN_NS,
}
ROLE_META = {
"坦克 (物理坦克)": {
"key": "tank_physical",
"detail": "坦克 · 物理坦克",
"color": (0.40, 0.70, 1.00),
},
"坦克 (法系坦克)": {
"key": "tank_caster",
"detail": "坦克 · 法系坦克",
"color": (1.00, 0.80, 0.20),
},
"法系输出": {
"key": "caster_dps",
"detail": "输出 · 法系输出",
"color": (0.65, 0.45, 1.00),
},
"物理近战": {
"key": "melee_dps",
"detail": "输出 · 物理近战",
"color": (1.00, 0.55, 0.25),
},
"物理远程": {
"key": "ranged_dps",
"detail": "输出 · 物理远程",
"color": (0.55, 0.88, 0.42),
},
"治疗": {
"key": "healer",
"detail": "治疗",
"color": (0.42, 1.00, 0.72),
},
}
CATEGORY_ORDER = [
"合剂",
"药剂",
"攻强",
"诅咒之地buff",
"赞达拉",
"武器",
"食物",
"",
"药水",
]
# The exported sheet does not include item IDs. We only preserve a very small
# set of IDs that already existed in the addon and can be matched confidently.
ITEM_ID_OVERRIDES = {
"巨人药剂": 9206,
"猫鼬药剂": 13452,
"精炼智慧合剂": 13511,
"夜鳞鱼汤": 13931,
"烤鱿鱼": 13928,
"炎夏火水": 12820,
"自由行动药水": 5634,
}
HEADER_MAP = {
"A": "role",
"B": "category",
"C": "name",
"D": "effect",
"E": "duration",
}
@dataclass
class Row:
role: str
category: str
name: str
effect: str
duration: str
row_number: int
def read_xlsx_rows(path: Path) -> list[Row]:
with zipfile.ZipFile(path) as archive:
shared_strings = load_shared_strings(archive)
workbook = ET.fromstring(archive.read("xl/workbook.xml"))
relationships = ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
rel_map = {
rel.attrib["Id"]: rel.attrib["Target"]
for rel in relationships.findall(f"{{{PKG_REL_NS}}}Relationship")
}
sheet = workbook.find("main:sheets", NS)[0]
target = rel_map[sheet.attrib[f"{{{DOC_REL_NS}}}id"]]
sheet_path = normalize_sheet_path(target)
root = ET.fromstring(archive.read(sheet_path))
sheet_rows = root.find("main:sheetData", NS).findall("main:row", NS)
rows: list[Row] = []
for row in sheet_rows[1:]:
values = {}
for cell in row.findall("main:c", NS):
ref = cell.attrib.get("r", "")
col_match = re.match(r"([A-Z]+)", ref)
if not col_match:
continue
col = col_match.group(1)
values[col] = read_cell_value(cell, shared_strings).strip()
if not any(values.values()):
continue
normalized = {
field: normalize_text(values.get(col, ""))
for col, field in HEADER_MAP.items()
}
rows.append(
Row(
role=normalized["role"],
category=normalized["category"],
name=normalized["name"],
effect=normalized["effect"],
duration=normalized["duration"],
row_number=int(row.attrib.get("r", "0")),
)
)
return rows
def load_shared_strings(archive: zipfile.ZipFile) -> list[str]:
if "xl/sharedStrings.xml" not in archive.namelist():
return []
root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
values: list[str] = []
for string_item in root.findall("main:si", NS):
text_parts = [
text_node.text or ""
for text_node in string_item.iter(f"{{{MAIN_NS}}}t")
]
values.append("".join(text_parts))
return values
def normalize_sheet_path(target: str) -> str:
if target.startswith("/"):
normalized = PurePosixPath("xl") / PurePosixPath(target).relative_to("/")
else:
normalized = PurePosixPath("xl") / PurePosixPath(target)
return str(normalized).replace("xl/xl/", "xl/")
def read_cell_value(cell: ET.Element, shared_strings: list[str]) -> str:
cell_type = cell.attrib.get("t")
value_node = cell.find("main:v", NS)
if cell_type == "s" and value_node is not None:
return shared_strings[int(value_node.text)]
if cell_type == "inlineStr":
inline_node = cell.find("main:is", NS)
if inline_node is None:
return ""
return "".join(
text_node.text or ""
for text_node in inline_node.iter(f"{{{MAIN_NS}}}t")
)
return value_node.text if value_node is not None else ""
def normalize_text(value: str) -> str:
value = (value or "").strip()
value = value.replace("\u3000", " ")
value = re.sub(r"\s+", " ", value)
value = value.replace("", "(").replace("", ")")
return value
def normalize_rows(rows: list[Row]) -> list[Row]:
normalized: list[Row] = []
for row in rows:
role = row.role
category = row.category
name = row.name
effect = normalize_effect(row.effect)
duration = normalize_duration(row.duration)
if not role or not category or not name:
raise ValueError(
f"存在不完整数据行Excel 行号 {row.row_number}: "
f"{role!r}, {category!r}, {name!r}"
)
normalized.append(
Row(
role=role,
category=category,
name=name,
effect=effect,
duration=duration,
row_number=row.row_number,
)
)
return normalized
def normalize_effect(value: str) -> str:
value = normalize_text(value)
replacements = {
"": " / ",
"": " / ",
}
for old, new in replacements.items():
value = value.replace(old, new)
value = value.replace("提升治疗", "提升治疗效果")
return value
def normalize_duration(value: str) -> str:
value = normalize_text(value)
replacements = {
"2小时1": "2小时",
"瞬发 ": "瞬发",
}
return replacements.get(value, value)
def build_groups(rows: list[Row]) -> list[dict]:
buckets: dict[str, list[Row]] = defaultdict(list)
for row in rows:
buckets[row.role].append(row)
role_order = [
role
for role in ROLE_META
if role in buckets
]
unknown_roles = sorted(role for role in buckets if role not in ROLE_META)
role_order.extend(unknown_roles)
category_index = {name: index for index, name in enumerate(CATEGORY_ORDER)}
groups = []
for role in role_order:
meta = ROLE_META.get(role, {})
role_rows = sorted(
buckets[role],
key=lambda item: (
category_index.get(item.category, len(CATEGORY_ORDER)),
item.row_number,
item.name,
),
)
items = []
for row in role_rows:
items.append(
{
"cat": row.category,
"name": row.name,
"effect": row.effect,
"duration": row.duration,
"id": ITEM_ID_OVERRIDES.get(row.name, 0),
}
)
groups.append(
{
"key": meta.get("key", slugify(role)),
"role": role,
"detail": meta.get("detail", role),
"color": meta.get("color", (0.85, 0.75, 0.90)),
"items": items,
}
)
return groups
def slugify(value: str) -> str:
value = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff]+", "_", value)
value = value.strip("_")
return value.lower() or "role"
def render_lua(groups: list[dict], source_path: Path, item_count: int) -> str:
role_order = [group["role"] for group in groups]
category_order = sorted(
{item["cat"] for group in groups for item in group["items"]},
key=lambda name: CATEGORY_ORDER.index(name)
if name in CATEGORY_ORDER
else len(CATEGORY_ORDER),
)
generated_at = datetime.fromtimestamp(source_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
lines = [
"-" * 80,
"-- Nanami-UI: ConsumableDB.lua",
"-- 食物药剂百科数据库(由导出表生成,请优先更新 Excel 后再重新生成)",
f"-- Source: {source_path}",
f"-- Stats : {len(groups)} roles / {len(category_order)} categories / {item_count} entries",
"-" * 80,
"SFrames = SFrames or {}",
"",
"SFrames.ConsumableDB = {",
f' generatedAt = "{generated_at}",',
" summary = {",
f" roleCount = {len(groups)},",
f" categoryCount = {len(category_order)},",
f" itemCount = {item_count},",
" },",
" roleOrder = {",
]
for role in role_order:
lines.append(f' "{lua_escape(role)}",')
lines.extend(
[
" },",
" categoryOrder = {",
]
)
for category in category_order:
lines.append(f' "{lua_escape(category)}",')
lines.extend(
[
" },",
" groups = {",
]
)
for index, group in enumerate(groups, start=1):
color = ", ".join(f"{component:.2f}" for component in group["color"])
lines.extend(
[
"",
f" -- {index}. {group['detail']}",
" {",
f' key = "{lua_escape(group["key"])}",',
f' role = "{lua_escape(group["role"])}",',
f' detail = "{lua_escape(group["detail"])}",',
f" color = {{ {color} }},",
" items = {",
]
)
for item in group["items"]:
lines.append(
' {{ cat="{cat}", name="{name}", effect="{effect}", duration="{duration}", id={item_id} }},'.format(
cat=lua_escape(item["cat"]),
name=lua_escape(item["name"]),
effect=lua_escape(item["effect"]),
duration=lua_escape(item["duration"]),
item_id=item["id"],
)
)
lines.extend(
[
" },",
" },",
]
)
lines.extend(
[
" },",
"}",
"",
]
)
return "\n".join(lines)
def lua_escape(value: str) -> str:
value = value.replace("\\", "\\\\").replace('"', '\\"')
return value
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate ConsumableDB.lua from an exported WoW consumables XLSX."
)
parser.add_argument("xlsx", type=Path, help="Path to the exported XLSX file")
parser.add_argument(
"-o",
"--output",
type=Path,
default=Path("ConsumableDB.lua"),
help="Output Lua file path",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
rows = normalize_rows(read_xlsx_rows(args.xlsx))
groups = build_groups(rows)
lua = render_lua(groups, args.xlsx, len(rows))
args.output.write_text(lua, encoding="utf-8", newline="\n")
print(
textwrap.dedent(
f"""\
Generated {args.output}
source : {args.xlsx}
roles : {len(groups)}
items : {len(rows)}
categories: {len({item['cat'] for group in groups for item in group['items']})}
"""
).strip()
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,126 @@
# MapReveal Standalone
## 目标
`Nanami-UI` 里的地图迷雾揭示功能单独提纯出来,方便其他插件作者复用,而不依赖整套 `SFrames` / `Nanami-UI` 框架。
## 当前实现包含什么
原始实现位于 `MapReveal.lua`,核心能力有 4 部分:
1. 基于 `LibMapOverlayData` / `MapOverlayData` 补全地图 overlay 数据。
2. Hook `WorldMapFrame_Update`,在世界地图刷新后把未探索区域重新画出来,并降低亮度显示。
3. 被动采集当前角色已经探索过的 overlay持久化到 SavedVariables。
4. 主动扫描全部大陆与区域,把当前角色已经探索到的 overlay 批量收集出来。
## 与 Nanami-UI 的耦合点
真正的强耦合不多,主要只有这些:
1. `SFrames:Print`
用于聊天框输出提示。
2. `SFramesDB` / `SFramesGlobalDB`
分别保存角色配置和账号级扫描数据。
3. `SFrames:CallWithPreservedBattlefieldMinimap`
用来保护战场小地图状态,避免 `SetMapZoom` / `SetMapToCurrentZone` 带来副作用。
4. `/nui ...`
命令入口挂在 `Nanami-UI` 的统一 Slash Command 里。
5. `ConfigUI.lua`
只是配置面板入口,不影响核心迷雾逻辑。
## 提纯后的最小依赖
独立插件只需要:
1. `LibMapOverlayData` 或兼容的 overlay 数据表。
2. `WorldMapFrame_Update`
3. `GetMapInfo` / `GetNumMapOverlays` / `GetMapOverlayInfo`
4. `SetMapZoom` / `SetMapToCurrentZone`
5. SavedVariables
也就是说,这个功能本质上可以完全脱离 UI 框架。
## 推荐拆分方式
建议拆成一个独立插件:
1. `Nanami-MapReveal.toc`
2. `Core.lua`
3. `MapReveal.lua`
其中:
1. `Core.lua` 负责初始化、打印、Slash 命令、配置默认值。
2. `MapReveal.lua` 只保留地图迷雾逻辑、扫描逻辑、数据持久化。
3. 独立版会在世界地图右上角放一个 `Reveal` 勾选框,方便用户直接开关迷雾揭示。
## 数据结构建议
角色级配置:
```lua
NanamiMapRevealDB = {
enabled = true,
unexploredAlpha = 0.7,
}
```
账号级扫描数据:
```lua
NanamiMapRevealGlobalDB = {
scanned = {
["ZoneName"] = {
"OVERLAYNAME:width:height:offsetX:offsetY",
},
},
}
```
## Hook 策略
最稳妥的方式仍然是包裹 `WorldMapFrame_Update`
1. 先隐藏现有 `WorldMapOverlay1..N`
2. 调用原始 `WorldMapFrame_Update`
3. 被动收集当前可见的 explored overlays
4. 如果开关开启,再自行补画 unexplored overlays
这样兼容原版地图逻辑,也不需要重做整张世界地图。
## 兼容注意点
1. `NUM_WORLDMAP_OVERLAYS` 不够时要动态扩容。
2. 最后一行/列贴图可能不是 256必须重新计算 `TexCoord`
3. 个别地图 overlay 偏移有 errata需要单独修正。
4. 扫描地图时可能影响当前地图上下文,结束后要恢复原地图层级。
5. 如果服务端或整合包已经补过 `MapOverlayData`,合并时要按 overlay 名去重。
## 已提取的独立插件样板
仓库里已经附带一个可复用版本:
- [standalone/Nanami-MapReveal/Nanami-MapReveal.toc](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/Nanami-MapReveal.toc)
- [standalone/Nanami-MapReveal/Core.lua](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/Core.lua)
- [standalone/Nanami-MapReveal/MapReveal.lua](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/MapReveal.lua)
## 给其他作者的接入建议
如果对方已经有自己的框架:
1. 保留 `MapReveal.lua` 主体逻辑。
2. 把打印函数替换成自己的日志函数。
3. 把 DB 名称换成自己的 SavedVariables。
4. 把 Slash 命令合并进自己的命令系统。
如果对方只想直接用:
1. 复制 `standalone/Nanami-MapReveal` 整个目录。
2. 放进 `Interface/AddOns/`
3. 确保客户端有 `LibMapOverlayData` 或兼容数据源。
## 后续可继续提纯的方向
1.`TurtleWoW_Zones` 再单独拆成数据文件。
2. 增加一个纯 API 层,只暴露 `Toggle` / `Refresh` / `ScanAllMaps` / `ExportScannedData`
3. 为不同端做兼容层,比如 Turtle WoW、Vanilla、1.12 私服整合端分别适配。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 64 KiB