聊天重做前缓存

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() SFrames.WorldMap:ToggleNav()
end end
</Binding> </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> </Bindings>

View File

@@ -338,6 +338,58 @@ local BASE_SPELL_CRIT = {
DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0, 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 function CalcMeleeCrit()
local _, class = UnitClass("player") local _, class = UnitClass("player")
class = class or "" class = class or ""
@@ -523,8 +575,9 @@ local function FullMeleeCrit()
end end
local gearCrit = GetGearBonus("CRIT") local gearCrit = GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("meleeCrit") local talentCrit = GetTalentBonus("meleeCrit")
return baseCrit + agiCrit + gearCrit + talentCrit, local tempCrit = GetTempEnchantCrit(16)
baseCrit, agiCrit, gearCrit, talentCrit return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end end
local function FullRangedCrit() local function FullRangedCrit()
local _, class = UnitClass("player") local _, class = UnitClass("player")
@@ -537,8 +590,9 @@ local function FullRangedCrit()
end end
local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT") local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("rangedCrit") local talentCrit = GetTalentBonus("rangedCrit")
return baseCrit + agiCrit + gearCrit + talentCrit, local tempCrit = GetTempEnchantCrit(18)
baseCrit, agiCrit, gearCrit, talentCrit return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end end
local function FullSpellCrit() local function FullSpellCrit()
local _, class = UnitClass("player") local _, class = UnitClass("player")
@@ -659,6 +713,7 @@ CS.FullSpellHit = FullSpellHit
CS.GetTalentDetailsFor = GetTalentDetailsFor CS.GetTalentDetailsFor = GetTalentDetailsFor
CS.GetGearBonus = GetGearBonus CS.GetGearBonus = GetGearBonus
CS.GetItemBonusLib = GetItemBonusLib CS.GetItemBonusLib = GetItemBonusLib
CS.GetTempEnchantCrit = GetTempEnchantCrit
CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT
SFrames.CharacterPanel.CS = CS SFrames.CharacterPanel.CS = CS
@@ -2009,7 +2064,7 @@ function CP:BuildEquipmentPage()
local crit = CS.SafeGetMeleeCrit() local crit = CS.SafeGetMeleeCrit()
CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5) CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5)
else 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) CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) 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) d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end end
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(" ") GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) 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) CS.TipLine("Buff 暴击未计入)", 0.8,0.5,0.3)
@@ -2135,7 +2193,7 @@ function CP:BuildEquipmentPage()
if fromAPI then if fromAPI then
CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5) CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5)
else 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) CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) 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) d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end end
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(" ") GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) 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) CS.TipLine("Buff 暴击未计入)", 0.8,0.5,0.3)

611
Chat.lua
View File

@@ -18,6 +18,7 @@ local DEFAULTS = {
topPadding = 30, topPadding = 30,
bottomPadding = 8, bottomPadding = 8,
bgAlpha = 0.45, bgAlpha = 0.45,
hoverTransparent = true,
activeTab = 1, activeTab = 1,
editBoxPosition = "bottom", editBoxPosition = "bottom",
editBoxX = 0, editBoxX = 0,
@@ -739,27 +740,87 @@ local function GetTranslateFilterKeyForEvent(event)
return TRANSLATE_EVENT_FILTERS[event] return TRANSLATE_EVENT_FILTERS[event]
end end
local function ParseHardcoreDeathMessage(text) -- ============================================================
if type(text) ~= "string" or text == "" then return nil end -- HC 公会成员缓存:用于"仅通报工会成员"过滤
if not string.find(text, "硬核") and not string.find(text, "死亡") then -- ============================================================
local lower = string.lower(text) local HCGuildMemberCache = {}
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 local function RefreshHCGuildCache()
return nil 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 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", "") local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "") 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 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 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 if lvlStr3 then return tonumber(lvlStr3) end
local lower = string.lower(clean) -- 兜底有死亡关键词即返回1
if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then if hasDead then return 1 end
return 1
end
return nil return nil
end end
@@ -774,6 +835,68 @@ local function CleanTextForTranslation(text)
return clean return clean
end 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) local function ForceHide(object)
if not object then return end if not object then return end
object:Hide() object:Hide()
@@ -789,6 +912,66 @@ local function ForceInvisible(object)
if object.EnableMouse then object:EnableMouse(false) end if object.EnableMouse then object:EnableMouse(false) end
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) local function CreateFont(parent, size, justify)
if SFrames and SFrames.CreateFontString then if SFrames and SFrames.CreateFontString then
return SFrames:CreateFontString(parent, size, justify) 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 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.translateEnabled == nil then db.translateEnabled = true end
if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = 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 type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
if db.layoutVersion < 2 then if db.layoutVersion < 2 then
db.topPadding = DEFAULTS.topPadding db.topPadding = DEFAULTS.topPadding
@@ -1895,6 +2081,7 @@ function SFrames.Chat:GetConfig()
topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5), topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5), bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
bgAlpha = Clamp(db.bgAlpha, 0, 1), bgAlpha = Clamp(db.bgAlpha, 0, 1),
hoverTransparent = (db.hoverTransparent ~= false),
editBoxPosition = editBoxPosition, editBoxPosition = editBoxPosition,
editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX, editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY, editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
@@ -3321,6 +3508,19 @@ function SFrames.Chat:RefreshConfigFrame()
end end
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 if self.configControls then
for i = 1, table.getn(self.configControls) do for i = 1, table.getn(self.configControls) do
local ctrl = self.configControls[i] local ctrl = self.configControls[i]
@@ -3485,12 +3685,12 @@ function SFrames.Chat:EnsureConfigFrame()
transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50) transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50)
transDesc:SetWidth(520) transDesc:SetWidth(520)
transDesc:SetJustifyH("LEFT") transDesc:SetJustifyH("LEFT")
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。") transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。聊天监控可独立启用。")
transDesc:SetTextColor(0.7, 0.7, 0.74) transDesc:SetTextColor(0.7, 0.7, 0.74)
local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath) 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() return EnsureDB().chatMonitorEnabled ~= false end,
function(checked) function(checked)
EnsureDB().chatMonitorEnabled = (checked == true) EnsureDB().chatMonitorEnabled = (checked == true)
@@ -3498,15 +3698,19 @@ function SFrames.Chat:EnsureConfigFrame()
function() function()
SFrames.Chat:RefreshConfigFrame() SFrames.Chat:RefreshConfigFrame()
end end
)) )
AddControl(monitorCb)
self.cfgMonitorCb = monitorCb
self.cfgMonitorSection = monitorSection
local monDesc = monitorSection:CreateFontString(nil, "OVERLAY") local monDesc = monitorSection:CreateFontString(nil, "OVERLAY")
monDesc:SetFont(fontPath, 10, "OUTLINE") monDesc:SetFont(fontPath, 10, "OUTLINE")
monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50) monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50)
monDesc:SetWidth(520) monDesc:SetWidth(520)
monDesc:SetJustifyH("LEFT") monDesc:SetJustifyH("LEFT")
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用") monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、职业染色等功能。\n可独立于 AI 翻译开关使用。关闭后消息将原样通过,[+] 复制等功能不可用。")
monDesc:SetTextColor(0.7, 0.7, 0.74) monDesc:SetTextColor(0.7, 0.7, 0.74)
self.cfgMonitorDesc = monDesc
local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY") local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY")
reloadHint:SetFont(fontPath, 10, "OUTLINE") reloadHint:SetFont(fontPath, 10, "OUTLINE")
@@ -3515,11 +3719,12 @@ function SFrames.Chat:EnsureConfigFrame()
reloadHint:SetJustifyH("LEFT") reloadHint:SetJustifyH("LEFT")
reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。") reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。")
reloadHint:SetTextColor(0.9, 0.75, 0.5) reloadHint:SetTextColor(0.9, 0.75, 0.5)
self.cfgMonitorReloadHint = reloadHint
end end
local windowPage = CreatePage("window") local windowPage = CreatePage("window")
do 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, AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
function() return EnsureDB().width end, function() return EnsureDB().width end,
function(v) EnsureDB().width = v end, function(v) EnsureDB().width = v end,
@@ -3580,13 +3785,18 @@ function SFrames.Chat:EnsureConfigFrame()
function(checked) EnsureDB().showPlayerLevel = (checked == true) end, function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() 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 = appearance:CreateFontString(nil, "OVERLAY")
self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE") self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10) self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8) 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 = inputSection:CreateFontString(nil, "OVERLAY")
self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE") self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30) self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
@@ -3618,7 +3828,7 @@ function SFrames.Chat:EnsureConfigFrame()
inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。") inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。")
inputTip:SetTextColor(0.74, 0.74, 0.8) 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() CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
SFrames.Chat:ResetPosition() SFrames.Chat:ResetPosition()
SFrames.Chat:RefreshConfigFrame() SFrames.Chat:RefreshConfigFrame()
@@ -4050,7 +4260,7 @@ function SFrames.Chat:EnsureConfigFrame()
local hcPage = CreatePage("hc") local hcPage = CreatePage("hc")
do 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") local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
hcStatusText:SetFont(fontPath, 10, "OUTLINE") hcStatusText:SetFont(fontPath, 10, "OUTLINE")
@@ -4095,7 +4305,7 @@ function SFrames.Chat:EnsureConfigFrame()
deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112) deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
deathTip:SetWidth(540) deathTip:SetWidth(540)
deathTip:SetJustifyH("LEFT") deathTip:SetJustifyH("LEFT")
deathTip:SetText("关闭那些某某在XX级死亡的系统提示。") deathTip:SetText("关闭那些[某某在XX级死亡]的系统提示。")
deathTip:SetTextColor(0.8, 0.7, 0.7) deathTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1, 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(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
function() SFrames.Chat:RefreshConfigFrame() 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 end
local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function() 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:SetBackdropColor(0, 0, 0, 0.55)
chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4) chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
f.chatShadow = chatShadow
local topGlow = f:CreateTexture(nil, "BACKGROUND") local topGlow = f:CreateTexture(nil, "BACKGROUND")
topGlow:SetTexture("Interface\\Buttons\\WHITE8X8") topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
@@ -4674,6 +4927,9 @@ function SFrames.Chat:CreateContainer()
scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2) scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
scrollTrack:SetWidth(4) scrollTrack:SetWidth(4)
scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9) 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) local resize = CreateFrame("Button", nil, f)
resize:SetWidth(16) resize:SetWidth(16)
@@ -4709,6 +4965,67 @@ function SFrames.Chat:CreateContainer()
f.resizeHandle = resize f.resizeHandle = resize
self.frame = f 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 if not self.hiddenConfigButton then
local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate") local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
hiddenConfigButton:SetWidth(74) hiddenConfigButton:SetWidth(74)
@@ -4749,9 +5066,45 @@ function SFrames.Chat:CreateContainer()
f:SetWidth(Clamp(db.width, 320, 900)) f:SetWidth(Clamp(db.width, 320, 900))
f:SetHeight(Clamp(db.height, 120, 460)) 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) 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 end
function SFrames.Chat:HideDefaultChrome() function SFrames.Chat:HideDefaultChrome()
@@ -5225,8 +5578,15 @@ function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
end end
if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(0, 0) end
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) 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) self:EnforceChatWindowLock(chatFrame)
if not chatFrame.sfDragLockHooked and chatFrame.HookScript then if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
@@ -6324,6 +6684,122 @@ function SFrames.Chat:StyleEditBox()
end end
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() function SFrames.Chat:ApplyFrameBorderStyle()
if not self.frame then return end if not self.frame then return end
@@ -6413,7 +6889,12 @@ function SFrames.Chat:ApplyConfig()
end end
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1) 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:AttachChatFrame()
self:HideDefaultChrome() self:HideDefaultChrome()
@@ -6427,6 +6908,11 @@ function SFrames.Chat:ApplyConfig()
self:StartStabilizer() self:StartStabilizer()
self:SetUnlocked(SFrames and SFrames.isUnlocked) self:SetUnlocked(SFrames and SFrames.isUnlocked)
self:RefreshConfigFrame() 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 end
function SFrames.Chat:Initialize() function SFrames.Chat:Initialize()
@@ -6524,6 +7010,61 @@ function SFrames.Chat:Initialize()
end) end)
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() SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
if SFrames and SFrames.Chat then if SFrames and SFrames.Chat then
SFrames.Chat:ApplyConfig() SFrames.Chat:ApplyConfig()
@@ -6532,7 +7073,7 @@ function SFrames.Chat:Initialize()
end end
LoadPersistentClassCache() LoadPersistentClassCache()
if IsInGuild and IsInGuild() and GuildRoster then if IsInGuild and IsInGuild() and GuildRoster then
GuildRoster() GuildRoster() -- 触发 GUILD_ROSTER_UPDATE → RefreshHCGuildCache
end end
SFrames:RefreshClassColorCache() SFrames:RefreshClassColorCache()
@@ -6714,6 +7255,17 @@ function SFrames.Chat:Initialize()
do do
local db = EnsureDB() 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) local chanName = GetChannelNameFromChatLine(text)
if chanName and IsIgnoredChannelByDefault(chanName) then if chanName and IsIgnoredChannelByDefault(chanName) then
@@ -7137,4 +7689,3 @@ end
end end
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor) SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", 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
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") local origOnUpdate = UIParent and UIParent.GetScript and UIParent:GetScript("OnUpdate")
if origOnUpdate then if origOnUpdate then
UIParent:SetScript("OnUpdate", function() UIParent:SetScript("OnUpdate", function()
@@ -43,6 +77,11 @@ end
BINDING_HEADER_NANAMI_UI = "Nanami-UI" BINDING_HEADER_NANAMI_UI = "Nanami-UI"
BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图" 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.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
SFrames.events = {} SFrames.events = {}
@@ -140,6 +179,52 @@ function SFrames:Print(msg)
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg)) DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg))
end 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 -- Addon Loaded Initializer
SFrames:RegisterEvent("PLAYER_LOGIN", function() SFrames:RegisterEvent("PLAYER_LOGIN", function()
SFrames:Initialize() SFrames:Initialize()
@@ -299,6 +384,10 @@ function SFrames:DoFullInitialize()
SFrames.Tooltip:SetAlpha(0) SFrames.Tooltip:SetAlpha(0)
SFrames.Tooltip:Hide() 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 -- Phase 1: Critical modules (unit frames, action bars) — must load immediately
if SFramesDB.enableUnitFrames ~= false then if SFramesDB.enableUnitFrames ~= false then
if SFramesDB.enablePlayerFrame ~= false then if SFramesDB.enablePlayerFrame ~= false then
@@ -319,6 +408,10 @@ function SFrames:DoFullInitialize()
SFrames.ActionBars:Initialize() SFrames.ActionBars:Initialize()
end end
if SFrames.ExtraBar and SFrames.ExtraBar.Initialize then
SFrames.ExtraBar:Initialize()
end
self:InitSlashCommands() self:InitSlashCommands()
-- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike -- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike
@@ -375,6 +468,13 @@ function SFrames:GetAuraTimeLeft(unit, index, isBuff)
end end
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) -- Nanami-Plates SpellDB: combat log + spell DB tracking (most accurate for debuffs)
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then 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) local effect, rank, tex, stacks, dtype, duration, timeleft, isOwn = NanamiPlates_SpellDB:UnitDebuff(unit, index)
@@ -420,6 +520,64 @@ function SFrames:FormatTime(seconds)
end end
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() function SFrames:InitSlashCommands()
DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.") DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.")
SLASH_SFRAMES1 = "/nanami" SLASH_SFRAMES1 = "/nanami"
@@ -808,12 +966,16 @@ function SFrames:HideBlizzardFrames()
end end
if ComboFrame then if ComboFrame then
ComboFrame:UnregisterAllEvents() ComboFrame:UnregisterAllEvents()
ComboFrame:SetScript("OnUpdate", nil)
ComboFrame:Hide() ComboFrame:Hide()
ComboFrame.Show = function() end ComboFrame.Show = function() end
ComboFrame.fadeInfo = ComboFrame.fadeInfo or {} ComboFrame.fadeInfo = {}
if ComboFrame_Update then if ComboFrame_Update then
ComboFrame_Update = function() end ComboFrame_Update = function() end
end end
if ComboFrame_OnUpdate then
ComboFrame_OnUpdate = function() end
end
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 -- Helper function to generate ElvUI-style backdrop and shadow border
function SFrames:CreateBackdrop(frame) function SFrames:ApplyBackdropStyle(frame, opts)
frame:SetBackdrop({ opts = opts or {}
bgFile = "Interface\\Buttons\\WHITE8X8", local radius = tonumber(opts.cornerRadius) or 0
edgeFile = "Interface\\Buttons\\WHITE8X8", local showBorder = opts.showBorder ~= false
tile = false, tileSize = 0, edgeSize = 1, local useRounded = radius and radius > 0
insets = { left = 1, right = 1, top = 1, bottom = 1 }
}) if useRounded then
local A = SFrames.ActiveTheme local edgeSize = math.max(8, math.min(18, math.floor(radius + 0.5)))
if A and A.panelBg then frame:SetBackdrop({
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9) bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1) 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 else
frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9) frame:SetBackdrop({
frame:SetBackdropBorderColor(0, 0, 0, 1) 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 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 end
function SFrames:CreateRoundBackdrop(frame) function SFrames:CreateRoundBackdrop(frame)
@@ -38,6 +67,78 @@ function SFrames:CreateUnitBackdrop(frame)
frame:SetBackdropBorderColor(0, 0, 0, 1) frame:SetBackdropBorderColor(0, 0, 0, 1)
end 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 -- Generator for StatusBars
function SFrames:CreateStatusBar(parent, name) function SFrames:CreateStatusBar(parent, name)
local bar = CreateFrame("StatusBar", name, parent) local bar = CreateFrame("StatusBar", name, parent)
@@ -50,6 +151,11 @@ function SFrames:CreateStatusBar(parent, name)
return bar return bar
end 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 -- Generator for FontStrings
function SFrames:CreateFontString(parent, size, justifyH) function SFrames:CreateFontString(parent, size, justifyH)
local fs = parent:CreateFontString(nil, "OVERLAY") local fs = parent:CreateFontString(nil, "OVERLAY")
@@ -59,6 +165,29 @@ function SFrames:CreateFontString(parent, size, justifyH)
return fs return fs
end 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 -- Generator for 3D Portraits
function SFrames:CreatePortrait(parent, name) function SFrames:CreatePortrait(parent, name)
local portrait = CreateFrame("PlayerModel", name, parent) 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 if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["FocusFrame"] then
local pos = SFramesDB.Positions["FocusFrame"] local pos = SFramesDB.Positions["FocusFrame"]
-- Validate: if GetTop would be near 0 or negative, position is bad local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0) if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
-- After setting, check if visible on screen 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 top = f:GetTop()
local left = f:GetLeft() local left = f:GetLeft()
if not top or not left or top < 50 or left < 0 then if not top or not left or top < 50 or left < 0 then
-- Bad position, reset
f:ClearAllPoints() f:ClearAllPoints()
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0) f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
SFramesDB.Positions["FocusFrame"] = nil SFramesDB.Positions["FocusFrame"] = nil
end end
elseif SFramesTargetFrame then
f:SetPoint("TOPLEFT", SFramesTargetFrame, "BOTTOMLEFT", 0, -75)
else else
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0) f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
end end
@@ -352,6 +358,11 @@ function SFrames.Focus:CreateFocusFrame()
if not SFramesDB then SFramesDB = {} end if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, _, relativePoint, xOfs, yOfs = this:GetPoint() 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 } SFramesDB.Positions["FocusFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end) end)
@@ -546,37 +557,27 @@ function SFrames.Focus:CreateFocusFrame()
SFrames:CreateUnitBackdrop(f) SFrames:CreateUnitBackdrop(f)
-- Portrait (right side) — EnableMouse(false) so clicks pass through to main Button -- Portrait placeholder (hidden, focus frame does not use 3D portraits)
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) f.portrait = CreateFrame("Frame", nil, f)
f.portrait = CreateFrame("PlayerModel", nil, f)
f.portrait:SetWidth(pWidth) f.portrait:SetWidth(pWidth)
f.portrait:SetHeight(totalH - 2) f.portrait:SetHeight(totalH - 2)
f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0) f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0)
f.portrait:EnableMouse(false) f.portrait:EnableMouse(false)
f.portrait:Hide()
local pbg = CreateFrame("Frame", nil, f) local pbg = CreateFrame("Frame", nil, f)
pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0)
pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
pbg:SetFrameLevel(f:GetFrameLevel()) pbg:SetFrameLevel(f:GetFrameLevel())
SFrames:CreateUnitBackdrop(pbg)
f.portraitBG = pbg f.portraitBG = pbg
pbg:EnableMouse(false) pbg:EnableMouse(false)
pbg:Hide()
if not showPortrait then -- Health bar (full width, no portrait)
f.portrait:Hide()
pbg:Hide()
end
-- Health bar
f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth") f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth")
f.health:EnableMouse(false) f.health:EnableMouse(false)
if showPortrait then f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
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:SetHeight(hHeight) f.health:SetHeight(hHeight)
local hbg = CreateFrame("Frame", nil, f) local hbg = CreateFrame("Frame", nil, f)
@@ -596,11 +597,7 @@ function SFrames.Focus:CreateFocusFrame()
f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower") f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower")
f.power:EnableMouse(false) f.power:EnableMouse(false)
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
if showPortrait then f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
else
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
end
local powerbg = CreateFrame("Frame", nil, f) local powerbg = CreateFrame("Frame", nil, f)
powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) 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:SetTexture(SFrames:GetTexture())
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) 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 = 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) f.classIcon.overlay:EnableMouse(false)
-- Texts -- Texts
@@ -747,12 +744,7 @@ function SFrames.Focus:CreateCastbar()
local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar") local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar")
cb:SetHeight(cbH) cb:SetHeight(cbH)
cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6) cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6)
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
if showPortrait then
cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(cbH + 6), 6)
else
cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
end
local cbbg = CreateFrame("Frame", nil, self.frame) local cbbg = CreateFrame("Frame", nil, self.frame)
cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
@@ -816,7 +808,6 @@ function SFrames.Focus:UpdateAll()
self.frame.power:SetMinMaxValues(0, 1) self.frame.power:SetMinMaxValues(0, 1)
self.frame.power:SetValue(0) self.frame.power:SetValue(0)
self.frame.powerText:SetText("") 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 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.frame.raidIcon:Hide()
self:HideAuras() self:HideAuras()
@@ -830,19 +821,6 @@ function SFrames.Focus:UpdateAll()
self:UpdateRaidIcon() self:UpdateRaidIcon()
self:UpdateAuras() 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 name = UnitName(uid) or ""
local level = UnitLevel(uid) local level = UnitLevel(uid)
local levelText = level local levelText = level
@@ -888,6 +866,7 @@ function SFrames.Focus:UpdateAll()
-- Color by class or reaction -- Color by class or reaction
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
if SFrames:IsGradientStyle() then useClassColor = true end
if UnitIsPlayer(uid) and useClassColor then if UnitIsPlayer(uid) and useClassColor then
local _, class = UnitClass(uid) local _, class = UnitClass(uid)
if class and SFrames.Config.colors.class[class] then 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:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b) self.frame.nameText:SetTextColor(r, g, b)
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
end end
function SFrames.Focus:HideAuras() function SFrames.Focus:HideAuras()
@@ -933,7 +915,7 @@ function SFrames.Focus:UpdateHealth()
self.frame.health:SetValue(hp) self.frame.health:SetValue(hp)
if maxHp > 0 then if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100) local pct = math.floor(hp / maxHp * 100)
self.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)") self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else else
self.frame.healthText:SetText("") self.frame.healthText:SetText("")
end end
@@ -950,6 +932,9 @@ function SFrames.Focus:UpdatePowerType()
else else
self.frame.power:SetStatusBarColor(0, 0, 1) self.frame.power:SetStatusBarColor(0, 0, 1)
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end end
-- STUB: UpdatePower -- STUB: UpdatePower
@@ -961,10 +946,11 @@ function SFrames.Focus:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower) self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power) self.frame.power:SetValue(power)
if maxPower > 0 then if maxPower > 0 then
self.frame.powerText:SetText(power .. " / " .. maxPower) self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else else
self.frame.powerText:SetText("") self.frame.powerText:SetText("")
end end
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, uid)
end end
-- STUB: UpdateRaidIcon -- STUB: UpdateRaidIcon
@@ -1045,8 +1031,8 @@ function SFrames.Focus:CastbarOnUpdate()
-- 1) Native UnitCastingInfo / UnitChannelInfo (if available) -- 1) Native UnitCastingInfo / UnitChannelInfo (if available)
if uid then if uid then
local _UCI = UnitCastingInfo or (CastingInfo and function(u) return CastingInfo(u) end) local _UCI = UnitCastingInfo or CastingInfo
local _UCH = UnitChannelInfo or (ChannelInfo and function(u) return ChannelInfo(u) end) local _UCH = UnitChannelInfo or ChannelInfo
if _UCI then if _UCI then
local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid) local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid)
if ok and cSpell and cStart then if ok and cSpell and cStart then
@@ -1177,7 +1163,6 @@ function SFrames.Focus:OnFocusChanged()
local name = self:GetFocusName() local name = self:GetFocusName()
if name then if name then
self.frame:Show() self.frame:Show()
self.frame._lastPortraitUID = nil -- Force portrait refresh on focus change
self:UpdateAll() self:UpdateAll()
else else
self.frame:Hide() self.frame:Hide()
@@ -1206,9 +1191,28 @@ function SFrames.Focus:ApplySettings()
local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9 local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9
local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11 local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11
local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10 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 showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false)
local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == 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 -- Main frame size & scale
f:SetWidth(width) f:SetWidth(width)
@@ -1222,40 +1226,75 @@ function SFrames.Focus:ApplySettings()
f:SetBackdropColor(r, g, b, bgAlpha) f:SetBackdropColor(r, g, b, bgAlpha)
end end
-- Portrait -- Portrait always hidden (focus frame uses class icon only)
if f.portrait then if f.portrait then f.portrait:Hide() end
f.portrait:SetWidth(pWidth) if f.portraitBG then f.portraitBG:Hide() end
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
-- Health bar anchors -- Health bar anchors (always full width, no portrait)
if f.health then if f.health then
f.health:ClearAllPoints() f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
if showPortrait then f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
else
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
end
f.health:SetHeight(hHeight) f.health:SetHeight(hHeight)
end end
-- Power bar anchors -- Power bar anchors
if f.power then if f.power then
f.power:ClearAllPoints() f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -1 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
if showPortrait then f.power:SetWidth(powerWidth)
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0) f.power:SetHeight(pHeight)
else end
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
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 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 end
-- Castbar anchors -- Castbar anchors
@@ -1264,11 +1303,7 @@ function SFrames.Focus:ApplySettings()
local cbH = 12 local cbH = 12
f.castbar:SetHeight(cbH) f.castbar:SetHeight(cbH)
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6) f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
if showPortrait then f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(cbH + 6), 6)
else
f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
end
if not showCastBar then if not showCastBar then
f.castbar:Hide() f.castbar:Hide()
if f.castbar.cbbg then f.castbar.cbbg:Hide() end if f.castbar.cbbg then f.castbar.cbbg:Hide() end
@@ -1298,8 +1333,6 @@ function SFrames.Focus:ApplySettings()
self:UpdateAuras() self:UpdateAuras()
end end
-- Force portrait refresh
f._lastPortraitUID = nil
if self:GetFocusName() then if self:GetFocusName() then
self:UpdateAll() self:UpdateAll()
end end
@@ -1336,7 +1369,6 @@ function SFrames.Focus:Initialize()
ef:RegisterEvent("UNIT_MAXRAGE") ef:RegisterEvent("UNIT_MAXRAGE")
ef:RegisterEvent("UNIT_DISPLAYPOWER") ef:RegisterEvent("UNIT_DISPLAYPOWER")
ef:RegisterEvent("UNIT_AURA") ef:RegisterEvent("UNIT_AURA")
ef:RegisterEvent("UNIT_PORTRAIT_UPDATE")
ef:RegisterEvent("UNIT_TARGET") ef:RegisterEvent("UNIT_TARGET")
ef:RegisterEvent("RAID_TARGET_UPDATE") ef:RegisterEvent("RAID_TARGET_UPDATE")
-- Combat log events for castbar detection (non-SuperWoW fallback) -- Combat log events for castbar detection (non-SuperWoW fallback)
@@ -1378,7 +1410,6 @@ function SFrames.Focus:Initialize()
if event == "PLAYER_TARGET_CHANGED" then if event == "PLAYER_TARGET_CHANGED" then
local focusName = focusSelf:GetFocusName() local focusName = focusSelf:GetFocusName()
if focusName and UnitExists("target") and UnitName("target") == focusName then if focusName and UnitExists("target") and UnitName("target") == focusName then
focusSelf.frame._lastPortraitUID = nil
focusSelf:UpdateAll() focusSelf:UpdateAll()
-- Try to grab GUID while we have target -- Try to grab GUID while we have target
if UnitGUID then if UnitGUID then
@@ -1512,7 +1543,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.health:SetValue(hp) focusSelf.frame.health:SetValue(hp)
if maxHp > 0 then if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100) local pct = math.floor(hp / maxHp * 100)
focusSelf.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)") focusSelf.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else else
focusSelf.frame.healthText:SetText("") focusSelf.frame.healthText:SetText("")
end end
@@ -1526,7 +1557,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower) focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power) focusSelf.frame.power:SetValue(power)
if maxPower > 0 then if maxPower > 0 then
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower) focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else else
focusSelf.frame.powerText:SetText("") focusSelf.frame.powerText:SetText("")
end end
@@ -1545,22 +1576,13 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower) focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power) focusSelf.frame.power:SetValue(power)
if maxPower > 0 then if maxPower > 0 then
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower) focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else else
focusSelf.frame.powerText:SetText("") focusSelf.frame.powerText:SetText("")
end end
end end
elseif event == "UNIT_AURA" then elseif event == "UNIT_AURA" then
focusSelf:UpdateAuras() 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 end
end) end)
@@ -1585,8 +1607,6 @@ function SFrames.Focus:Initialize()
focusSelf.frame:Show() focusSelf.frame:Show()
end end
-- Re-scan for a valid unitID every poll cycle
-- This catches cases where focus becomes target/party/raid dynamically
local uid = focusSelf:GetUnitID() local uid = focusSelf:GetUnitID()
if uid then if uid then
focusSelf:UpdateHealth() focusSelf:UpdateHealth()
@@ -1594,31 +1614,19 @@ function SFrames.Focus:Initialize()
focusSelf:UpdatePower() focusSelf:UpdatePower()
focusSelf:UpdateAuras() focusSelf:UpdateAuras()
focusSelf:UpdateRaidIcon() 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
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 SFrames.Movers and SFrames.Movers.RegisterMover then
if SFramesTargetFrame then if SFramesTargetFrame then
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
"TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -10) "TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -75,
nil, { alwaysShowInLayout = true })
else else
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
"LEFT", "UIParent", "LEFT", 250, 0) "LEFT", "UIParent", "LEFT", 250, 0,
nil, { alwaysShowInLayout = true })
end end
end end

View File

@@ -1364,20 +1364,6 @@ function GS:HookTooltips()
end end
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 end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

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

442
Mail.lua
View File

@@ -45,6 +45,7 @@ local S = {
inboxRows = {}, inboxRows = {},
currentTab = 1, currentTab = 1,
inboxPage = 1, inboxPage = 1,
bagPage = 1,
inboxChecked = {}, inboxChecked = {},
collectQueue = {}, collectQueue = {},
collectTimer = nil, collectTimer = nil,
@@ -53,11 +54,23 @@ local S = {
isSending = false, isSending = false,
collectElapsed = 0, collectElapsed = 0,
multiSend = nil, -- active multi-send state table multiSend = nil, -- active multi-send state table
codMode = false, -- send panel: 付款取信 mode toggle
} }
local L = { local L = {
W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28, W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28,
BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12, 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 if money and money > 0 then
row.moneyFrame:SetMoney(money) row.moneyFrame:SetMoney(money)
elseif CODAmount and CODAmount > 0 then elseif CODAmount and CODAmount > 0 then
row.codFS:SetText("COD:"); row.codFS:Show() row.codFS:SetText("付款:"); row.codFS:Show()
row.moneyFrame:SetMoney(CODAmount) row.moneyFrame:SetMoney(CODAmount)
else else
row.moneyFrame:SetMoney(0) row.moneyFrame:SetMoney(0)
end end
row.expiryFS:SetText(FormatExpiry(daysLeft)) 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:SetDisabled(not canTake)
row.takeBtn:SetScript("OnClick", function() row.takeBtn:SetScript("OnClick", function()
if row.mailIndex then if row.mailIndex then
if hasItem 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 elseif money and money > 0 then
local idx = row.mailIndex local idx = row.mailIndex
TakeInboxMoney(idx) TakeInboxMoney(idx)
@@ -543,7 +565,7 @@ end
local function StopCollecting() local function StopCollecting()
S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil
if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end
UpdateInbox() if S.currentTab == 3 then ML:UpdateMailBag() else UpdateInbox() end
end end
local function ProcessCollectQueue() local function ProcessCollectQueue()
@@ -683,6 +705,8 @@ end
local function ResetSendForm() local function ResetSendForm()
if not S.frame then return end if not S.frame then return end
ClearSendItems() 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.toEditBox then S.frame.toEditBox:SetText("") end
if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end
if S.frame.bodyEditBox then S.frame.bodyEditBox: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 = {} local items = {}
for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end 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 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 if money and money > 0 then SetSendMailMoney(money) end
SendMail(recipient, subject, body or "") SendMail(recipient, subject, body or "")
return return
@@ -746,6 +777,7 @@ local function DoMultiSend(recipient, subject, body, money)
subject = subject or "", subject = subject or "",
body = body or "", body = body or "",
money = money, money = money,
codMode = S.codMode,
total = table.getn(items), total = table.getn(items),
sentCount = 0, sentCount = 0,
phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ... phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ...
@@ -803,9 +835,13 @@ local function DoMultiSend(recipient, subject, body, money)
return return
end end
-- Money only on first mail -- Money or 付款取信
if ms.sentCount == 1 and ms.money and ms.money > 0 then if ms.money and ms.money > 0 then
SetSendMailMoney(ms.money) if ms.codMode then
SetSendMailCOD(ms.money)
elseif ms.sentCount == 1 then
SetSendMailMoney(ms.money)
end
end end
-- Send this single-attachment mail -- Send this single-attachment mail
@@ -903,13 +939,18 @@ local function BuildMainFrame()
sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER) sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER)
sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) 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:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6))
tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end) tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end)
f.tabInbox = tabInbox f.tabInbox = tabInbox
local tabSend = CreateTabBtn(f, "发送", 70) local tabBag = CreateTabBtn(f, "邮包", 50)
tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0) 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) tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end)
f.tabSend = tabSend f.tabSend = tabSend
@@ -1128,16 +1169,25 @@ function ML:ShowMailDetail(mailIndex)
dp.detailMoney:SetMoney(money); dp.detailMoney:Show() dp.detailMoney:SetMoney(money); dp.detailMoney:Show()
end end
if CODAmount and CODAmount > 0 then 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() dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show()
end end
-- Take items button -- Take items button
local canTakeItem = hasItem and (not CODAmount or CODAmount == 0) local canTakeItem = hasItem
dp.takeItemBtn:SetDisabled(not canTakeItem) dp.takeItemBtn:SetDisabled(not canTakeItem)
dp.takeItemBtn:SetScript("OnClick", function() dp.takeItemBtn:SetScript("OnClick", function()
if S.detailMailIndex then 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
end) end)
@@ -1205,6 +1255,294 @@ function ML:HideMailDetail()
UpdateInbox() UpdateInbox()
end 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 -- BUILD: Send panel
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -1555,29 +1893,56 @@ local function BuildSendPanel()
bsf:SetScrollChild(bodyEB) bsf:SetScrollChild(bodyEB)
f.bodyEditBox = bodyEB f.bodyEditBox = bodyEB
-- Money row -- Money mode toggle (附加金币 / 付款取信)
local mLabel = sp:CreateFontString(nil, "OVERLAY") local mToggle = CreateActionBtn(sp, "附加金币", 72)
mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10) mToggle:SetHeight(20); mToggle:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) 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") 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]) 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") local sL = sp:CreateFontString(nil, "OVERLAY")
sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0) 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]) 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") local cL = sp:CreateFontString(nil, "OVERLAY")
cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0) 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]) 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 -- Attachments
local aLabel = sp:CreateFontString(nil, "OVERLAY") 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]) aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local clrBtn = CreateActionBtn(sp, "清空", 50) local clrBtn = CreateActionBtn(sp, "清空", 50)
@@ -1679,7 +2044,7 @@ local function SetupEvents()
f:SetScript("OnEvent", function() f:SetScript("OnEvent", function()
if event == "MAIL_SHOW" then if event == "MAIL_SHOW" then
if SFramesDB and SFramesDB.enableMail == false then return end 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() CheckInbox(); f:Show(); ML:ShowInboxPanel()
elseif event == "MAIL_INBOX_UPDATE" then elseif event == "MAIL_INBOX_UPDATE" then
if f:IsVisible() then if f:IsVisible() then
@@ -1689,10 +2054,28 @@ local function SetupEvents()
else else
ML:HideMailDetail() ML:HideMailDetail()
end end
elseif S.currentTab == 3 then
ML:UpdateMailBag()
else else
UpdateInbox() UpdateInbox()
end end
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 elseif event == "MAIL_CLOSED" then
if S.multiSend then AbortMultiSend("邮箱已关闭") end if S.multiSend then AbortMultiSend("邮箱已关闭") end
f:Hide() f:Hide()
@@ -1721,6 +2104,7 @@ local function SetupEvents()
f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE") f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE")
f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS") f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS")
f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED") f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED")
f:RegisterEvent("UPDATE_PENDING_MAIL")
if MailFrame then if MailFrame then
local origMailOnShow = MailFrame:GetScript("OnShow") local origMailOnShow = MailFrame:GetScript("OnShow")
@@ -1728,6 +2112,7 @@ local function SetupEvents()
if origMailOnShow then origMailOnShow() end if origMailOnShow then origMailOnShow() end
this:ClearAllPoints() this:ClearAllPoints()
this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
this:SetAlpha(0)
this:EnableMouse(false) this:EnableMouse(false)
end) end)
for i = table.getn(UISpecialFrames), 1, -1 do for i = table.getn(UISpecialFrames), 1, -1 do
@@ -1772,6 +2157,7 @@ function ML:Initialize()
BuildMainFrame() BuildMainFrame()
BuildInboxPanel() BuildInboxPanel()
BuildDetailPanel() BuildDetailPanel()
BuildMailBagPanel()
BuildSendPanel() BuildSendPanel()
SetupEvents() SetupEvents()
end end
@@ -1781,19 +2167,19 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
function ML:ShowInboxPanel() function ML:ShowInboxPanel()
if not S.frame then return end 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 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 S.detailMailIndex = nil
UpdateInbox() UpdateInbox()
end end
function ML:ShowSendPanel() function ML:ShowSendPanel()
if not S.frame then return end 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 if S.frame.detailPanel then S.frame.detailPanel:Hide() end
S.detailMailIndex = nil 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.frame.sendStatus then S.frame.sendStatus:SetText("") end
if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end
ML:UpdateSendPanel() ML:UpdateSendPanel()

View File

@@ -127,6 +127,64 @@ local function GetZoneYards()
return nil, nil return nil, nil
end 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 -- 1. World Map + Battlefield Minimap: class icon overlays
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -224,97 +282,95 @@ local function UpdateMinimapDots()
return return
end end
if WorldMapFrame and WorldMapFrame:IsVisible() then if IsMapStateProtected() then
return return
end end
if SetMapToCurrentZone then WithPlayerZoneMap(function()
pcall(SetMapToCurrentZone) local px, py = GetPlayerMapPosition("player")
end 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 for i = 1, MAX_PARTY do
if mmDots[i] then mmDots[i]:Hide() end local unit = "party" .. i
end if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then
return local mx, my = GetPlayerMapPosition(unit)
end 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 doRotate then
if not zw or not zh or zw == 0 or zh == 0 then local s = math.sin(facing)
for i = 1, MAX_PARTY do local c = math.cos(facing)
if mmDots[i] then mmDots[i]:Hide() end dx, dy = dx * c + dy * s, -dx * s + dy * c
end
return
end
local now = GetTime()
if now - indoorCheckTime > 3 then
indoorCheckTime = now
cachedIndoor = DetectIndoor()
end
local zoom = Minimap:GetZoom()
local mmYards = MM_ZOOM[cachedIndoor] and MM_ZOOM[cachedIndoor][zoom]
or MM_ZOOM[1][zoom] or 466.67
local mmHalfYards = mmYards / 2
local mmHalfPx = Minimap:GetWidth() / 2
local facing = 0
local doRotate = false
local okCvar, rotateVal = pcall(GetCVar, "rotateMinimap")
if okCvar and rotateVal == "1" and GetPlayerFacing then
local ok2, f = pcall(GetPlayerFacing)
if ok2 and f then
facing = f
doRotate = true
end
end
for i = 1, MAX_PARTY do
local unit = "party" .. i
if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then
local mx, my = GetPlayerMapPosition(unit)
if mx and my and (mx ~= 0 or my ~= 0) then
local dx = (mx - px) * zw
local dy = (py - my) * zh
if doRotate then
local s = math.sin(facing)
local c = math.cos(facing)
dx, dy = dx * c + dy * s, -dx * s + dy * c
end
local dist = math.sqrt(dx * dx + dy * dy)
if dist < mmHalfYards * 0.92 then
local scale = mmHalfPx / mmHalfYards
if not mmDots[i] then
mmDots[i] = CreateMinimapDot(i)
end end
local dot = mmDots[i]
local _, class = UnitClass(unit) local dist = math.sqrt(dx * dx + dy * dy)
local cc = class and CLASS_COLORS and CLASS_COLORS[class] if dist < mmHalfYards * 0.92 then
if cc then local scale = mmHalfPx / mmHalfYards
dot.icon:SetVertexColor(cc.r, cc.g, cc.b, 1)
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 else
dot.icon:SetVertexColor(1, 0.82, 0, 1) if mmDots[i] then mmDots[i]:Hide() end
end end
dot:ClearAllPoints()
dot:SetPoint("CENTER", Minimap, "CENTER", dx * scale, dy * scale)
dot:Show()
else else
if mmDots[i] then mmDots[i]:Hide() end if mmDots[i] then mmDots[i]:Hide() end
end end
else else
if mmDots[i] then mmDots[i]:Hide() end if mmDots[i] then mmDots[i]:Hide() end
end end
else
if mmDots[i] then mmDots[i]:Hide() end
end end
end end)
end end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

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

View File

@@ -62,6 +62,27 @@ function SFrames:GetTexture()
return self.Media.statusbar return self.Media.statusbar
end 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() function SFrames:GetFont()
-- 1. Check built-in font key -- 1. Check built-in font key
if SFramesDB and SFramesDB.fontKey then if SFramesDB and SFramesDB.fontKey then
@@ -79,6 +100,40 @@ function SFrames:GetFont()
return self.Media.font return self.Media.font
end 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) function SFrames:GetSharedMediaList(mediaType)
local LSM = self:GetSharedMedia() local LSM = self:GetSharedMedia()
if LSM and LSM.List then return LSM:List(mediaType) end if LSM and LSM.List then return LSM:List(mediaType) end

View File

@@ -93,6 +93,19 @@ local function GetDB()
return db return db
end 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 function ResolveStyleKey()
local key = GetDB().mapStyle or "auto" local key = GetDB().mapStyle or "auto"
if key == "auto" then if key == "auto" then
@@ -152,6 +165,34 @@ local function ApplyPosition()
end end
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 -- Hide default Blizzard minimap chrome
-- MUST be called AFTER BuildFrame (Minimap is already reparented) -- MUST be called AFTER BuildFrame (Minimap is already reparented)
@@ -165,15 +206,13 @@ local function HideDefaultElements()
MinimapToggleButton, MinimapToggleButton,
MiniMapWorldMapButton, MiniMapWorldMapButton,
GameTimeFrame, GameTimeFrame,
TimeManagerClockButton,
MinimapZoneTextButton, MinimapZoneTextButton,
MiniMapTracking, MiniMapTracking,
MinimapBackdrop, MinimapBackdrop,
} }
for _, f in ipairs(kill) do for _, f in ipairs(kill) do
if f then ForceHideMinimapFrame(f)
f:Hide()
f.Show = function() end
end
end end
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.) -- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
@@ -183,10 +222,7 @@ local function HideDefaultElements()
} }
for _, name in ipairs(trackNames) do for _, name in ipairs(trackNames) do
local f = _G[name] local f = _G[name]
if f and f.Hide then ForceHideMinimapFrame(f)
f:Hide()
f.Show = function() end
end
end end
-- Also hide any tracking textures that are children of Minimap -- Also hide any tracking textures that are children of Minimap
@@ -195,8 +231,7 @@ local function HideDefaultElements()
for _, child in ipairs(children) do for _, child in ipairs(children) do
local n = child.GetName and child:GetName() local n = child.GetName and child:GetName()
if n and string.find(n, "Track") then if n and string.find(n, "Track") then
child:Hide() ForceHideMinimapFrame(child)
child.Show = function() end
end end
end end
end end
@@ -455,8 +490,15 @@ local function UpdateZoneText()
end end
local function SetZoneMap() local function SetZoneMap()
if IsMapStateProtected() then
return
end
if SetMapToCurrentZone then if SetMapToCurrentZone then
pcall(SetMapToCurrentZone) if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
else
pcall(SetMapToCurrentZone)
end
end 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 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 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() local function HideBlizzardBuffs()
for i = 0, 23 do for i = 0, 23 do
local btn = _G["BuffButton" .. i] local btn = _G["BuffButton" .. i]
@@ -170,18 +207,19 @@ local function CreateSlot(parent, namePrefix, index, isBuff)
btn:SetScript("OnLeave", function() btn:SetScript("OnLeave", function()
GameTooltip:Hide() GameTooltip:Hide()
end) end)
btn:SetScript("OnHide", function()
HideOwnedTooltip(this)
end)
btn:SetScript("OnClick", function() btn:SetScript("OnClick", function()
if this._sfSimulated then return end if this._sfSimulated then return end
if this._isWeaponEnchant then return end if this._isWeaponEnchant then return end
if this.isBuff and this.buffIndex and this.buffIndex >= 0 then if this.isBuff and this.buffIndex and this.buffIndex >= 0 then
HideOwnedTooltip(this)
CancelPlayerBuff(this.buffIndex) CancelPlayerBuff(this.buffIndex)
end end
end) end)
btn.buffIndex = -1 ClearSlotState(btn)
btn._sfSimulated = false
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn:Hide() btn:Hide()
return btn return btn
end end
@@ -339,9 +377,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx] local btn = self.buffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex) local texture = GetPlayerBuffTexture(buffIndex)
if texture then if texture then
SetSlotTooltipKey(btn, "buff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture) btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex btn.buffIndex = buffIndex
btn._sfSimulated = false btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = false btn._isWeaponEnchant = false
btn._weaponSlotID = nil btn._weaponSlotID = nil
@@ -360,10 +401,8 @@ function MB:UpdateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show() btn:Show()
else else
ClearSlotState(btn)
btn:Hide() btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end end
end end
end end
@@ -378,9 +417,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx] local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 16) local texture = GetInventoryItemTexture("player", 16)
if texture then if texture then
SetSlotTooltipKey(btn, "weapon:16")
btn.icon:SetTexture(texture) btn.icon:SetTexture(texture)
btn.buffIndex = -1 btn.buffIndex = -1
btn._sfSimulated = false btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = true btn._isWeaponEnchant = true
btn._weaponSlotID = 16 btn._weaponSlotID = 16
@@ -407,9 +449,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx] local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 17) local texture = GetInventoryItemTexture("player", 17)
if texture then if texture then
SetSlotTooltipKey(btn, "weapon:17")
btn.icon:SetTexture(texture) btn.icon:SetTexture(texture)
btn.buffIndex = -1 btn.buffIndex = -1
btn._sfSimulated = false btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = true btn._isWeaponEnchant = true
btn._weaponSlotID = 17 btn._weaponSlotID = 17
@@ -432,10 +477,8 @@ function MB:UpdateBuffs()
for j = slotIdx + 1, MAX_BUFFS do for j = slotIdx + 1, MAX_BUFFS do
local btn = self.buffSlots[j] local btn = self.buffSlots[j]
ClearSlotState(btn)
btn:Hide() btn:Hide()
btn.buffIndex = -1
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
end end
self:UpdateDebuffs() self:UpdateDebuffs()
@@ -450,6 +493,7 @@ function MB:UpdateDebuffs()
if db.showDebuffs == false then if db.showDebuffs == false then
for i = 1, MAX_DEBUFFS do for i = 1, MAX_DEBUFFS do
ClearSlotState(self.debuffSlots[i])
self.debuffSlots[i]:Hide() self.debuffSlots[i]:Hide()
end end
if self.debuffContainer then self.debuffContainer:Hide() end if self.debuffContainer then self.debuffContainer:Hide() end
@@ -467,9 +511,14 @@ function MB:UpdateDebuffs()
local btn = self.debuffSlots[slotIdx] local btn = self.debuffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex) local texture = GetPlayerBuffTexture(buffIndex)
if texture then if texture then
SetSlotTooltipKey(btn, "debuff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture) btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex btn.buffIndex = buffIndex
btn._sfSimulated = false btn._sfSimulated = false
btn._sfSimLabel = nil
btn._sfSimDesc = nil
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
local apps = GetPlayerBuffApplications(buffIndex) local apps = GetPlayerBuffApplications(buffIndex)
if apps and apps > 1 then if apps and apps > 1 then
@@ -495,15 +544,15 @@ function MB:UpdateDebuffs()
btn:Show() btn:Show()
else else
ClearSlotState(btn)
btn:Hide() btn:Hide()
btn.buffIndex = -1
end end
end end
end end
for j = slotIdx + 1, MAX_DEBUFFS do for j = slotIdx + 1, MAX_DEBUFFS do
ClearSlotState(self.debuffSlots[j])
self.debuffSlots[j]:Hide() self.debuffSlots[j]:Hide()
self.debuffSlots[j].buffIndex = -1
end end
end end
@@ -556,6 +605,7 @@ function MB:SimulateBuffs()
local btn = self.buffSlots[i] local btn = self.buffSlots[i]
local sim = SIM_BUFFS[i] local sim = SIM_BUFFS[i]
if sim then if sim then
SetSlotTooltipKey(btn, "sim-buff:" .. tostring(i))
btn.icon:SetTexture(sim.tex) btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1 btn.buffIndex = -1
btn._sfSimulated = true btn._sfSimulated = true
@@ -578,6 +628,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show() btn:Show()
else else
ClearSlotState(btn)
btn:Hide() btn:Hide()
end end
end end
@@ -586,11 +637,14 @@ function MB:SimulateBuffs()
local btn = self.debuffSlots[i] local btn = self.debuffSlots[i]
local sim = SIM_DEBUFFS[i] local sim = SIM_DEBUFFS[i]
if sim then if sim then
SetSlotTooltipKey(btn, "sim-debuff:" .. tostring(i))
btn.icon:SetTexture(sim.tex) btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1 btn.buffIndex = -1
btn._sfSimulated = true btn._sfSimulated = true
btn._sfSimLabel = sim.label btn._sfSimLabel = sim.label
btn._sfSimDesc = sim.desc btn._sfSimDesc = sim.desc
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
btn.timer:SetText(sim.time) btn.timer:SetText(sim.time)
ApplyTimerColor(btn, sim.time) ApplyTimerColor(btn, sim.time)
@@ -601,6 +655,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
btn:Show() btn:Show()
else else
ClearSlotState(btn)
btn:Hide() btn:Hide()
end end
end end

View File

@@ -207,8 +207,12 @@ local function SyncMoverToFrame(name)
local w = (frame:GetWidth() or 100) * scale local w = (frame:GetWidth() or 100) * scale
local h = (frame:GetHeight() or 50) * scale local h = (frame:GetHeight() or 50) * scale
mover:SetWidth(math.max(w, 72)) local minW = 72
mover:SetHeight(math.max(h, 40)) 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 l = frame:GetLeft()
local b = frame:GetBottom() local b = frame:GetBottom()
@@ -256,17 +260,12 @@ local function SyncFrameToMover(name)
local pos = positions[name] local pos = positions[name]
if pos then if pos then
frame:ClearAllPoints() 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
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale() frame:SetPoint(pos.point, UIParent, pos.relativePoint,
local newL = frame:GetLeft() or 0 (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
local newB = frame:GetBottom() or 0 else
local dL = newL * scale - moverL frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
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))
end end
end end
@@ -720,11 +719,14 @@ local function CreateControlBar()
local accent = th.accent or { 1, 0.5, 0.8, 0.98 } local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
local titleC = th.title or { 1, 0.88, 1 } 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 = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
controlBar:SetFrameStrata("FULLSCREEN_DIALOG") controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
controlBar:SetFrameLevel(200) controlBar:SetFrameLevel(200)
controlBar:SetWidth(480) controlBar:SetWidth(480)
controlBar:SetHeight(44) controlBar:SetHeight(72)
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8) controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
controlBar:SetClampedToScreen(true) controlBar:SetClampedToScreen(true)
controlBar:SetMovable(true) controlBar:SetMovable(true)
@@ -739,7 +741,7 @@ local function CreateControlBar()
local title = controlBar:CreateFontString(nil, "OVERLAY") local title = controlBar:CreateFontString(nil, "OVERLAY")
title:SetFont(Font(), 13, "OUTLINE") 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:SetText("Nanami 布局")
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1) title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
@@ -747,17 +749,19 @@ local function CreateControlBar()
sep:SetTexture("Interface\\Buttons\\WHITE8x8") sep:SetTexture("Interface\\Buttons\\WHITE8x8")
sep:SetWidth(1) sep:SetWidth(1)
sep:SetHeight(24) 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) sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
local bx = 128 local bx = 128
-- Snap toggle -- Snap toggle (row 1)
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function() local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
local cfg = GetLayoutCfg() local cfg = GetLayoutCfg()
cfg.snapEnabled = not cfg.snapEnabled cfg.snapEnabled = not cfg.snapEnabled
if controlBar._updateSnap then controlBar._updateSnap() end if controlBar._updateSnap then controlBar._updateSnap() end
end) end)
snapBtn:ClearAllPoints()
snapBtn:SetPoint("LEFT", controlBar, "LEFT", bx, ROW_Y_TOP)
controlBar.snapBtn = snapBtn controlBar.snapBtn = snapBtn
local function UpdateSnapBtnText() local function UpdateSnapBtnText()
@@ -773,7 +777,7 @@ local function CreateControlBar()
end end
controlBar._updateSnap = UpdateSnapBtnText controlBar._updateSnap = UpdateSnapBtnText
-- Grid toggle -- Grid toggle (row 1)
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function() local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
local cfg = GetLayoutCfg() local cfg = GetLayoutCfg()
cfg.showGrid = not cfg.showGrid cfg.showGrid = not cfg.showGrid
@@ -782,6 +786,8 @@ local function CreateControlBar()
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
end end
end) end)
gridBtn:ClearAllPoints()
gridBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 84, ROW_Y_TOP)
controlBar.gridBtn = gridBtn controlBar.gridBtn = gridBtn
local function UpdateGridBtnText() local function UpdateGridBtnText()
@@ -797,18 +803,20 @@ local function CreateControlBar()
end end
controlBar._updateGrid = UpdateGridBtnText controlBar._updateGrid = UpdateGridBtnText
-- Reset all -- Reset all (row 1)
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function() local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
M:ResetAllMovers() M:ResetAllMovers()
end) end)
resetBtn:ClearAllPoints()
resetBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 176, ROW_Y_TOP)
local wbGold = th.wbGold or { 1, 0.88, 0.55 } local wbGold = th.wbGold or { 1, 0.88, 0.55 }
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1) resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
-- Close -- Close (row 1)
local closeBtn = CreateFrame("Button", nil, controlBar) local closeBtn = CreateFrame("Button", nil, controlBar)
closeBtn:SetWidth(60) closeBtn:SetWidth(60)
closeBtn:SetHeight(26) closeBtn:SetHeight(26)
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0) closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, ROW_Y_TOP)
closeBtn:SetBackdrop(ROUND_BACKDROP) closeBtn:SetBackdrop(ROUND_BACKDROP)
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95) closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90) closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
@@ -837,6 +845,64 @@ local function CreateControlBar()
end) end)
controlBar.closeBtn = closeBtn 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() controlBar:Hide()
return controlBar return controlBar
end end
@@ -844,7 +910,7 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Register mover -- 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 if not name or not frame then return end
registry[name] = { registry[name] = {
@@ -856,6 +922,7 @@ function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, de
defaultX = defaultX or 0, defaultX = defaultX or 0,
defaultY = defaultY or 0, defaultY = defaultY or 0,
onMoved = onMoved, onMoved = onMoved,
alwaysShowInLayout = opts and opts.alwaysShowInLayout or false,
} }
CreateMoverFrame(name, registry[name]) CreateMoverFrame(name, registry[name])
@@ -890,10 +957,19 @@ function M:EnterLayoutMode()
UIParent:GetWidth(), UIParent:GetHeight(), UIParent:GetWidth(), UIParent:GetHeight(),
UIParent:GetRight() or 0, UIParent:GetTop() or 0)) 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) SyncMoverToFrame(name)
local mover = moverFrames[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 end
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸") SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
@@ -924,6 +1000,22 @@ function M:IsLayoutMode()
return isLayoutMode return isLayoutMode
end 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 -- Reset movers
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -979,13 +1071,30 @@ function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoin
local pos = positions[name] local pos = positions[name]
if pos and pos.point and pos.relativePoint then if pos and pos.point and pos.relativePoint then
frame:ClearAllPoints() 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 return true
else else
frame:ClearAllPoints() frame:ClearAllPoints()
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER", if relFrame == UIParent then
defaultX or 0, defaultY or 0) 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 return false
end end
end end
@@ -996,3 +1105,7 @@ end
function M:GetRegistry() function M:GetRegistry()
return registry return registry
end end
function M:SyncMoverToFrame(name)
SyncMoverToFrame(name)
end

View File

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

View File

@@ -444,6 +444,10 @@ local function ApplyChoices()
SFramesDB.enableChat = c.enableChat SFramesDB.enableChat = c.enableChat
if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end
SFramesDB.Chat.translateEnabled = c.translateEnabled SFramesDB.Chat.translateEnabled = c.translateEnabled
-- 翻译关闭时,聊天监控也自动关闭
if c.translateEnabled == false then
SFramesDB.Chat.chatMonitorEnabled = false
end
SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable
SFramesDB.enableUnitFrames = c.enableUnitFrames SFramesDB.enableUnitFrames = c.enableUnitFrames
@@ -1316,9 +1320,14 @@ function SW:DoSkip()
self:Hide() self:Hide()
return return
end end
-- First-run: apply defaults -- First-run: apply defaults then initialize
if not SFramesDB then SFramesDB = {} end choices = GetDefaultChoices()
SFramesDB.setupComplete = true 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() self:Hide()
if completeCb then completeCb() end if completeCb then completeCb() end
end end

View File

@@ -115,6 +115,23 @@ local function TT_DifficultyColor(unitLevel)
end end
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 -- Initialize
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -680,6 +697,29 @@ function SFrames.FloatingTooltip:FormatLines(tooltip)
GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b) GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b)
end end
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 end
-------------------------------------------------------------------------- --------------------------------------------------------------------------
@@ -1268,27 +1308,27 @@ function IC:HookTooltips()
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- SetItemRef (chat item links) -- SetItemRef (chat item links)
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
local orig_SetItemRef = SetItemRef if ItemRefTooltip and ItemRefTooltip.SetHyperlink then
if orig_SetItemRef then local orig_ItemRef_SetHyperlink = ItemRefTooltip.SetHyperlink
SetItemRef = function(link, text, button) ItemRefTooltip.SetHyperlink = function(self, link)
orig_SetItemRef(link, text, button) self._nanamiSellPriceAdded = nil
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end 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() pcall(function()
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)") local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
if itemStr then if itemStr then
ItemRefTooltip._nanamiSellPriceAdded = nil local moneyAlreadyShown = self.hasMoney
local itemId = IC_GetItemIdFromLink(itemStr) IC_EnhanceTooltip(self, itemStr, nil, moneyAlreadyShown)
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
end end
end) end)
return r1, r2, r3, r4
end end
end end

View File

@@ -962,12 +962,7 @@ function TSUI.CreateReagentSlot(parent, i)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
local ok local ok
if S.currentMode == "craft" then if S.currentMode == "craft" then
local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex) ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
if link then
ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
else
ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex)
end
else else
ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex) ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex)
end end
@@ -1521,6 +1516,7 @@ end
function TSUI.UpdateProfTabs() function TSUI.UpdateProfTabs()
TSUI.ScanProfessions() TSUI.ScanProfessions()
local currentSkillName = API.GetSkillLineName() local currentSkillName = API.GetSkillLineName()
local numVisible = 0
for i = 1, 10 do for i = 1, 10 do
local tab = S.profTabs[i] local tab = S.profTabs[i]
if not tab then break end if not tab then break end
@@ -1544,10 +1540,16 @@ function TSUI.UpdateProfTabs()
tab.glow:Hide(); tab.checked:Hide() tab.glow:Hide(); tab.checked:Hide()
end end
tab:Show() tab:Show()
numVisible = numVisible + 1
else else
tab.profName = nil; tab.active = false; tab:Hide() tab.profName = nil; tab.active = false; tab:Hide()
end end
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 end
function TSUI.IsTabSwitching() function TSUI.IsTabSwitching()
@@ -1932,6 +1934,24 @@ function TSUI:Initialize()
end end
end) end)
dIF:SetScript("OnLeave", function() GameTooltip:Hide() 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") local dName = det:CreateFontString(nil, "OVERLAY")
dName:SetFont(font, 13, "OUTLINE") dName:SetFont(font, 13, "OUTLINE")
@@ -2128,6 +2148,52 @@ function TSUI:Initialize()
end) end)
TSUI.CreateProfTabs(MF) 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() MF:Hide()
tinsert(UISpecialFrames, "SFramesTradeSkillFrame") tinsert(UISpecialFrames, "SFramesTradeSkillFrame")
end end

View File

@@ -61,10 +61,11 @@ local currentFilter = "all"
local displayList = {} local displayList = {}
local rowButtons = {} local rowButtons = {}
local collapsedCats = {} local collapsedCats = {}
local isTradeskillTrainerCached = false -- Cache to avoid repeated API calls local isTradeskillTrainerCached = false
local function HideBlizzardTrainer() local function HideBlizzardTrainer()
if not ClassTrainerFrame then return end if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
if ClassTrainerFrame:IsVisible() then if ClassTrainerFrame:IsVisible() then
if HideUIPanel then if HideUIPanel then
pcall(HideUIPanel, ClassTrainerFrame) pcall(HideUIPanel, ClassTrainerFrame)
@@ -161,123 +162,20 @@ end
local function GetVerifiedCategory(index) local function GetVerifiedCategory(index)
local name, _, category = GetTrainerServiceInfo(index) local name, _, category = GetTrainerServiceInfo(index)
if not name then return nil end if not name then return nil end
if category == "available" or category == "unavailable" or category == "used" then
-- "used" is always reliable - player already knows this skill return category
if category == "used" then
return "used"
end end
return "unavailable"
-- "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"
end end
local scanTip = nil local scanTip = nil
local function GetServiceTooltipInfo(index) local function GetServiceTooltipInfo(index)
if not scanTip then return "", ""
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")
end end
local function GetServiceQuality(index) local function GetServiceQuality(index)
if not scanTip then return nil
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)
end end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -392,10 +290,7 @@ end
local qualityCache = {} local qualityCache = {}
local function GetCachedServiceQuality(index) local function GetCachedServiceQuality(index)
if qualityCache[index] ~= nil then return qualityCache[index] end return nil
local q = GetServiceQuality(index)
qualityCache[index] = q or false
return q
end end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -604,22 +499,8 @@ local function CreateListRow(parent, idx)
self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
end end
-- Skip quality scan for tradeskill trainers (performance optimization) self.qualGlow:Hide()
if not isTradeskillTrainerCached then self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
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
local ok, cost = pcall(GetTrainerServiceCost, svc.index) local ok, cost = pcall(GetTrainerServiceCost, svc.index)
if ok and cost and cost > 0 then if ok and cost and cost > 0 then
@@ -838,13 +719,7 @@ local function UpdateDetail()
detail.icon:SetTexture(iconTex) detail.icon:SetTexture(iconTex)
detail.iconFrame:Show() detail.iconFrame:Show()
local quality = GetServiceQuality(selectedIndex) detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
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.nameFS:SetText(name or "") detail.nameFS:SetText(name or "")
@@ -873,13 +748,10 @@ local function UpdateDetail()
end end
detail.reqFS:SetText(table.concat(reqParts, " ")) detail.reqFS:SetText(table.concat(reqParts, " "))
local spellInfo, descText = GetServiceTooltipInfo(selectedIndex) detail.infoFS:SetText("")
detail.infoFS:SetText(spellInfo) detail.descFS:SetText("")
detail.descFS:SetText(descText) detail.descDivider:Hide()
detail.descDivider:Show() detail.descScroll:GetScrollChild():SetHeight(1)
local textH = detail.descFS:GetHeight() or 40
detail.descScroll:GetScrollChild():SetHeight(math.max(1, textH))
detail.descScroll:SetVerticalScroll(0) detail.descScroll:SetVerticalScroll(0)
local canTrain = (category == "available") and cost and (GetMoney() >= cost) local canTrain = (category == "available") and cost and (GetMoney() >= cost)
@@ -1342,6 +1214,7 @@ function TUI:Initialize()
local function CleanupBlizzardTrainer() local function CleanupBlizzardTrainer()
if not ClassTrainerFrame then return end if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end
if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end
ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:SetAlpha(0)
@@ -1361,6 +1234,7 @@ function TUI:Initialize()
if event == "TRAINER_SHOW" then if event == "TRAINER_SHOW" then
if ClassTrainerFrame then if ClassTrainerFrame then
ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:SetScript("OnHide", function() end)
ClassTrainerFrame:UnregisterAllEvents()
ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:SetAlpha(0)
ClassTrainerFrame:EnableMouse(false) ClassTrainerFrame:EnableMouse(false)
end end

View File

@@ -1148,11 +1148,9 @@ end
-- unit under the mouse cursor without changing current target. -- unit under the mouse cursor without changing current target.
-- --
-- Strategy: -- Strategy:
-- UseAction hook: temporarily TargetUnit(moUnit) before the real UseAction, -- Temporarily try the mouseover unit first while preserving the original
-- then restore previous target afterwards. This preserves the hardware -- target. If the spell cannot resolve on mouseover, stop the pending target
-- event callstack so the client doesn't reject the action. -- mode, restore the original target, and retry there.
-- CastSpellByName hook (SuperWoW): pass moUnit as 2nd arg directly.
-- CastSpellByName hook (no SuperWoW): same target-swap trick.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
local mouseoverCastEnabled = false local mouseoverCastEnabled = false
local origUseAction = nil local origUseAction = nil
@@ -1176,6 +1174,70 @@ local function GetMouseoverUnit()
return nil return nil
end 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) local function MouseoverUseAction(action, cursor, onSelf)
-- Don't interfere: picking up action, or re-entrant call -- Don't interfere: picking up action, or re-entrant call
if cursor == 1 or inMouseoverAction then if cursor == 1 or inMouseoverAction then
@@ -1187,42 +1249,24 @@ local function MouseoverUseAction(action, cursor, onSelf)
return origUseAction(action, cursor, onSelf) return origUseAction(action, cursor, onSelf)
end end
-- Skip if mouseover IS current target (no swap needed) local prevTarget = CaptureTargetState()
if UnitIsUnit and UnitExists("target") and UnitIsUnit(moUnit, "target") then
return origUseAction(action, cursor, onSelf)
end
-- Remember current target state
local hadTarget = UnitExists("target")
local prevTargetName = hadTarget and UnitName("target") or nil
-- Temporarily target the mouseover unit
inMouseoverAction = true inMouseoverAction = true
TargetUnit(moUnit) local castOnMouseover = TryActionOnUnit(moUnit, action, cursor, onSelf)
-- Execute the real UseAction on the now-targeted mouseover unit if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
origUseAction(action, cursor, onSelf)
-- Handle ground-targeted spells (Blizzard, Flamestrike, etc.)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(moUnit)
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting() SpellStopTargeting()
end end
-- Restore previous target RestoreTargetState(prevTarget)
if hadTarget and prevTargetName then
-- Target back the previous unit if not castOnMouseover and prevTarget.hadTarget then
TargetLastTarget() origUseAction(action, cursor, onSelf)
-- Verify restoration worked if SpellIsTargeting and SpellIsTargeting() then
if not UnitExists("target") or UnitName("target") ~= prevTargetName then if not ResolvePendingSpellTarget("target") then
-- TargetLastTarget failed, try by name SpellStopTargeting()
TargetByName(prevTargetName, true) end
end end
else
-- Had no target before, clear
ClearTarget()
end end
inMouseoverAction = false inMouseoverAction = false
@@ -1244,43 +1288,26 @@ local function MouseoverCastSpellByName(spell, arg2)
return origCastSpellByName(spell) return origCastSpellByName(spell)
end end
-- SuperWoW: direct unit parameter, no target swap needed local prevTarget = CaptureTargetState()
if SUPERWOW_VERSION then
origCastSpellByName(spell, moUnit)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(moUnit)
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
return
end
-- No SuperWoW: target-swap inMouseoverAction = true
local hadTarget = UnitExists("target") local castOnMouseover = TryCastSpellOnUnit(moUnit, spell)
local prevTargetName = hadTarget and UnitName("target") or nil
if not (hadTarget and UnitIsUnit and UnitIsUnit(moUnit, "target")) then if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
TargetUnit(moUnit)
end
origCastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit("target")
end
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting() SpellStopTargeting()
end end
if hadTarget and prevTargetName then RestoreTargetState(prevTarget)
TargetLastTarget()
if not UnitExists("target") or UnitName("target") ~= prevTargetName then if not castOnMouseover and prevTarget.hadTarget then
TargetByName(prevTargetName, true) origCastSpellByName(spell)
if SpellIsTargeting and SpellIsTargeting() then
if not ResolvePendingSpellTarget("target") then
SpellStopTargeting()
end
end end
elseif not hadTarget then
ClearTarget()
end end
inMouseoverAction = false
end end
local function InitMouseoverCast() 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 PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = 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) local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit) return SFrames:GetIncomingHeals(unit)
end end
@@ -36,6 +39,12 @@ local function Clamp(value, minValue, maxValue)
return value return value
end end
local function SetTextureIfPresent(region, texturePath)
if region and region.SetTexture and texturePath then
region:SetTexture(texturePath)
end
end
function SFrames.Party:GetMetrics() function SFrames.Party:GetMetrics()
local db = SFramesDB or {} local db = SFramesDB or {}
@@ -57,6 +66,34 @@ function SFrames.Party:GetMetrics()
local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3) local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6) 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 if healthHeight + powerHeight + 3 > height then
powerHeight = height - healthHeight - 3 powerHeight = height - healthHeight - 3
if powerHeight < 6 then if powerHeight < 6 then
@@ -81,16 +118,30 @@ function SFrames.Party:GetMetrics()
local valueFont = tonumber(db.partyValueFontSize) or 10 local valueFont = tonumber(db.partyValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) 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 { return {
width = width, width = width,
height = height, height = height,
portraitWidth = portraitWidth, portraitWidth = portraitWidth,
healthHeight = healthHeight, healthHeight = healthHeight,
powerHeight = powerHeight, powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.partyPowerOnTop == true,
horizontalGap = hgap, horizontalGap = hgap,
verticalGap = vgap, verticalGap = vgap,
nameFont = nameFont, nameFont = nameFont,
valueFont = valueFont, valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("partyHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("partyPowerTexture", "barTexture"),
} }
end end
@@ -142,8 +193,8 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
if frame.power then if frame.power then
frame.power:ClearAllPoints() frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1) frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0) frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight) frame.power:SetHeight(metrics.powerHeight)
end end
@@ -153,14 +204,114 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1) frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
end end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" SFrames:ApplyStatusBarTexture(frame.health, "partyHealthTexture", "barTexture")
local fontPath = SFrames:GetFont() 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 if frame.nameText then
frame.nameText:SetFont(fontPath, metrics.nameFont, outline) SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "partyNameFontKey", "fontKey")
end end
if frame.healthText then 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
end end
@@ -250,12 +401,13 @@ function SFrames.Party:ApplyLayout()
end end
end end
local auraRowHeight = 24 -- 20px icon + 2px gap above + 2px padding
if mode == "horizontal" then if mode == "horizontal" then
self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3)) self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
self.parent:SetHeight(metrics.height) self.parent:SetHeight(metrics.height + auraRowHeight)
else else
self.parent:SetWidth(metrics.width) 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
end end
@@ -432,11 +584,16 @@ function SFrames.Party:Initialize()
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) 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:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1) f.nameText:SetShadowOffset(1, -1)
f.healthText:SetShadowColor(0, 0, 0, 1) f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -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) -- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
local roleOvr = CreateFrame("Frame", nil, f) local roleOvr = CreateFrame("Frame", nil, f)
@@ -621,74 +778,79 @@ end
function SFrames.Party:CreateAuras(index) function SFrames.Party:CreateAuras(index)
local f = self.frames[index].frame 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.buffs = {}
f.debuffs = {} f.debuffs = {}
local size = 20 local size = 20
local spacing = 2 local spacing = 2
-- Party Buffs -- Party Buffs
for i = 1, 4 do 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:SetWidth(size)
b:SetHeight(size) b:SetHeight(size)
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b) SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK") b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints() b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
b.cdText = SFrames:CreateFontString(b, 9, "CENTER") b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0) b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1) b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1) b.cdText:SetShadowOffset(1, -1)
b:SetScript("OnEnter", function() b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitBuff(f.unit, this:GetID()) GameTooltip:SetUnitBuff(f.unit, this:GetID())
end) end)
b:SetScript("OnLeave", function() GameTooltip:Hide() 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 if i == 1 then
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2) b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
else else
b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0) b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0)
end end
b:Hide() b:Hide()
f.buffs[i] = b f.buffs[i] = b
end end
-- Debuffs (Starting right after Buffs to remain linear) -- Debuffs (Starting right after Buffs to remain linear)
for i = 1, 4 do 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:SetWidth(size)
b:SetHeight(size) b:SetHeight(size)
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b) SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK") b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints() b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
b.cdText = SFrames:CreateFontString(b, 9, "CENTER") b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0) b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1) b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1) b.cdText:SetShadowOffset(1, -1)
b:SetScript("OnEnter", function() b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitDebuff(f.unit, this:GetID()) GameTooltip:SetUnitDebuff(f.unit, this:GetID())
end) end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end) b:SetScript("OnLeave", function() GameTooltip:Hide() end)
if i == 1 then if i == 1 then
b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0) b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0)
else else
b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0) b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0)
end end
b:Hide() b:Hide()
f.debuffs[i] = b f.debuffs[i] = b
end end
@@ -726,6 +888,7 @@ end
function SFrames.Party:TickAuras(unit) function SFrames.Party:TickAuras(unit)
if self.testing then return end
local data = self:GetFrameByUnit(unit) local data = self:GetFrameByUnit(unit)
if not data then return end if not data then return end
local f = data.frame local f = data.frame
@@ -772,7 +935,10 @@ function SFrames.Party:UpdateAll()
if inRaid and raidFramesEnabled then if inRaid and raidFramesEnabled then
for i = 1, 4 do for i = 1, 4 do
if self.frames[i] and self.frames[i].frame then 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
end end
if self._globalUpdateFrame then if self._globalUpdateFrame then
@@ -793,6 +959,8 @@ function SFrames.Party:UpdateAll()
hasVisible = true hasVisible = true
else else
f:Hide() 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
end end
if self._globalUpdateFrame then if self._globalUpdateFrame then
@@ -812,11 +980,16 @@ function SFrames.Party:UpdateFrame(unit)
if not data then return end if not data then return end
local f = data.frame local f = data.frame
f.portrait:SetUnit(unit) local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
f.portrait:SetCamera(0) if use3D then
f.portrait:Hide() f.portrait:SetUnit(unit)
f.portrait:Show() f.portrait:SetCamera(0)
f.portrait:Hide()
f.portrait:Show()
else
f.portrait:Hide()
end
local name = UnitName(unit) or "" local name = UnitName(unit) or ""
local level = UnitLevel(unit) local level = UnitLevel(unit)
if level == -1 then level = "??" end if level == -1 then level = "??" end
@@ -841,6 +1014,10 @@ function SFrames.Party:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1) f.nameText:SetTextColor(1, 1, 1)
end end
end end
-- Re-apply gradient after color change
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.health)
end
-- Update Leader/Master Looter -- Update Leader/Master Looter
if GetPartyLeaderIndex() == data.index then if GetPartyLeaderIndex() == data.index then
@@ -870,6 +1047,8 @@ function SFrames.Party:UpdatePortrait(unit)
local data = self:GetFrameByUnit(unit) local data = self:GetFrameByUnit(unit)
if not data then return end if not data then return end
local f = data.frame local f = data.frame
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
if not use3D then return end
f.portrait:SetUnit(unit) f.portrait:SetUnit(unit)
f.portrait:SetCamera(0) f.portrait:SetCamera(0)
f.portrait:Hide() f.portrait:Hide()
@@ -917,14 +1096,8 @@ function SFrames.Party:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther local predOther = f.health.healPredOther
local predOver = f.health.healPredOver local predOver = f.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists(unit) or not UnitIsConnected(unit) then if not UnitExists(unit) or not UnitIsConnected(unit) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -946,7 +1119,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end end
if maxHp <= 0 then if maxHp <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -963,7 +1136,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end end
local missing = maxHp - hp local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -971,13 +1144,19 @@ function SFrames.Party:UpdateHealPrediction(unit)
local remaining = missing - mineShown local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining) local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end 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 if barWidth <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1054,6 +1233,9 @@ function SFrames.Party:UpdatePowerType(unit)
else else
f.power:SetStatusBarColor(0, 0, 1) f.power:SetStatusBarColor(0, 0, 1)
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.power)
end
end end
function SFrames.Party:UpdatePower(unit) function SFrames.Party:UpdatePower(unit)
@@ -1064,6 +1246,7 @@ function SFrames.Party:UpdatePower(unit)
if not UnitIsConnected(unit) then if not UnitIsConnected(unit) then
f.power:SetMinMaxValues(0, 100) f.power:SetMinMaxValues(0, 100)
f.power:SetValue(0) f.power:SetValue(0)
if f.powerText then f.powerText:SetText("") end
return return
end end
@@ -1071,6 +1254,14 @@ function SFrames.Party:UpdatePower(unit)
local maxPower = UnitManaMax(unit) local maxPower = UnitManaMax(unit)
f.power:SetMinMaxValues(0, maxPower) f.power:SetMinMaxValues(0, maxPower)
f.power:SetValue(power) 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 end
function SFrames.Party:UpdateRaidIcons() function SFrames.Party:UpdateRaidIcons()
@@ -1106,6 +1297,7 @@ function SFrames.Party:UpdateRaidIcon(unit)
end end
function SFrames.Party:UpdateAuras(unit) function SFrames.Party:UpdateAuras(unit)
if self.testing then return end
local data = self:GetFrameByUnit(unit) local data = self:GetFrameByUnit(unit)
if not data then return end if not data then return end
local f = data.frame local f = data.frame
@@ -1114,7 +1306,9 @@ function SFrames.Party:UpdateAuras(unit)
local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false) local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
local hasDebuff = 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") SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
@@ -1128,10 +1322,10 @@ function SFrames.Party:UpdateAuras(unit)
if texture then if texture then
if debuffType then if debuffType then
hasDebuff = true hasDebuff = true
if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1} if debuffType == "Magic" then _partyDebuffColor.r = 0.2; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 1
elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1} elseif debuffType == "Curse" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0; _partyDebuffColor.b = 1
elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0} elseif debuffType == "Disease" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0.4; _partyDebuffColor.b = 0
elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0} elseif debuffType == "Poison" then _partyDebuffColor.r = 0; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 0
end end
end end
@@ -1174,7 +1368,7 @@ function SFrames.Party:UpdateAuras(unit)
end end
if hasDebuff then 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 else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end end
@@ -1254,10 +1448,38 @@ function SFrames.Party:TestMode()
f.masterIcon:Show() f.masterIcon:Show()
end end
-- Show one dummy debuff to test positioning -- Show test buffs (all 4)
f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain") local testBuffIcons = {
f.debuffs[1]:Show() "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 -- Test pet
if f.petFrame then if f.petFrame then
f.petFrame:Show() f.petFrame:Show()
@@ -1268,9 +1490,18 @@ function SFrames.Party:TestMode()
end end
end end
else else
self:UpdateAll()
for i = 1, 4 do 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 end
self:UpdateAll()
end end
end end

View File

@@ -225,16 +225,22 @@ function SFrames.Pet:Initialize()
local f = CreateFrame("Button", "SFramesPetFrame", UIParent) local f = CreateFrame("Button", "SFramesPetFrame", UIParent)
f:SetWidth(150) f:SetWidth(150)
f:SetHeight(30) 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 local frameScale = (SFramesDB and type(SFramesDB.petFrameScale) == "number") and SFramesDB.petFrameScale or 1
f:SetScale(Clamp(frameScale, 0.7, 1.8)) 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:SetMovable(true)
f:EnableMouse(true) f:EnableMouse(true)
@@ -245,6 +251,11 @@ function SFrames.Pet:Initialize()
if not SFramesDB then SFramesDB = {} end if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() 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 } SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end) end)
@@ -294,6 +305,8 @@ function SFrames.Pet:Initialize()
hbg:SetFrameLevel(f:GetFrameLevel() - 1) hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg) SFrames:CreateUnitBackdrop(hbg)
f.healthBGFrame = hbg
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints() f.health.bg:SetAllPoints()
f.health.bg:SetTexture(SFrames:GetTexture()) f.health.bg:SetTexture(SFrames:GetTexture())
@@ -328,6 +341,7 @@ function SFrames.Pet:Initialize()
pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
pbg:SetFrameLevel(f:GetFrameLevel() - 1) pbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(pbg) SFrames:CreateUnitBackdrop(pbg)
f.powerBGFrame = pbg
f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
f.power.bg:SetAllPoints() f.power.bg:SetAllPoints()
@@ -394,11 +408,13 @@ function SFrames.Pet:Initialize()
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end) SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
self:InitFoodFeature() self:InitFoodFeature()
self:ApplyConfig()
self:UpdateAll() self:UpdateAll()
if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物", SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物",
"TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 10, -55) "TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 0, -75,
nil, { alwaysShowInLayout = true })
end end
if StaticPopup_Show then if StaticPopup_Show then
@@ -413,6 +429,90 @@ function SFrames.Pet:Initialize()
end end
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() function SFrames.Pet:UpdateAll()
if UnitExists("pet") then if UnitExists("pet") then
if SFramesDB and SFramesDB.showPetFrame == false 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 local r, g, b = 0.33, 0.59, 0.33
self.frame.health:SetStatusBarColor(r, g, b) self.frame.health:SetStatusBarColor(r, g, b)
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
else else
self.frame:Hide() self.frame:Hide()
if self.foodPanel then self.foodPanel:Hide() end if self.foodPanel then self.foodPanel:Hide() end
@@ -465,12 +568,6 @@ function SFrames.Pet:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver local predOver = self.frame.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
local hp = UnitHealth("pet") or 0 local hp = UnitHealth("pet") or 0
local maxHp = UnitHealthMax("pet") or 0 local maxHp = UnitHealthMax("pet") or 0
@@ -485,7 +582,7 @@ function SFrames.Pet:UpdateHealPrediction()
end end
if maxHp <= 0 or UnitIsDeadOrGhost("pet") then if maxHp <= 0 or UnitIsDeadOrGhost("pet") then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -497,7 +594,7 @@ function SFrames.Pet:UpdateHealPrediction()
local missing = maxHp - hp local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -505,13 +602,13 @@ function SFrames.Pet:UpdateHealPrediction()
local remaining = missing - mineShown local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining) local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
local barWidth = self.frame.health:GetWidth() local barWidth = self.frame.health:GetWidth()
if barWidth <= 0 then if barWidth <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -521,7 +618,7 @@ function SFrames.Pet:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -587,6 +684,9 @@ function SFrames.Pet:UpdatePowerType()
else else
self.frame.power:SetStatusBarColor(0, 0, 1) self.frame.power:SetStatusBarColor(0, 0, 1)
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end end
function SFrames.Pet:UpdatePower() function SFrames.Pet:UpdatePower()
@@ -594,6 +694,7 @@ function SFrames.Pet:UpdatePower()
local maxPower = UnitManaMax("pet") local maxPower = UnitManaMax("pet")
self.frame.power:SetMinMaxValues(0, maxPower) self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power) self.frame.power:SetValue(power)
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "pet")
end end
function SFrames.Pet:UpdateHappiness() 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 = {} local RAID_UNIT_LOOKUP = {}
for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end 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) local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit) return SFrames:GetIncomingHeals(unit)
end 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() function SFrames.Raid:GetMetrics()
local db = SFramesDB or {} local db = SFramesDB or {}
@@ -25,29 +91,66 @@ function SFrames.Raid:GetMetrics()
local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8) local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8)
healthHeight = math.max(10, math.min(height - 6, healthHeight)) 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 if not db.raidShowPower then
powerHeight = 0 powerHeight = 0
healthHeight = height - 2 healthHeight = height - 2
end 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 hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING
local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING
local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING
local nameFont = tonumber(db.raidNameFontSize) or 10 local nameFont = tonumber(db.raidNameFontSize) or 10
local valueFont = tonumber(db.raidValueFontSize) or 9 local valueFont = tonumber(db.raidValueFontSize) or 9
local healthFont = tonumber(db.raidHealthFontSize) or valueFont
local powerFont = tonumber(db.raidPowerFontSize) or valueFont
return { return {
width = width, width = width,
height = height, height = height,
healthHeight = healthHeight, healthHeight = healthHeight,
powerHeight = powerHeight, powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.raidPowerOnTop == true,
horizontalGap = hgap, horizontalGap = hgap,
verticalGap = vgap, verticalGap = vgap,
groupGap = groupGap, groupGap = groupGap,
nameFont = nameFont, nameFont = nameFont,
valueFont = valueFont, valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("raidHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("raidPowerTexture", "barTexture"),
showPower = db.raidShowPower ~= false, showPower = db.raidShowPower ~= false,
} }
end end
@@ -87,8 +190,8 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
frame.power:Show() frame.power:Show()
if frame.powerBGFrame then frame.powerBGFrame:Show() end if frame.powerBGFrame then frame.powerBGFrame:Show() end
frame.power:ClearAllPoints() frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1) frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0) frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight) frame.power:SetHeight(metrics.powerHeight)
else else
frame.power:Hide() frame.power:Hide()
@@ -96,14 +199,80 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
end end
end end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" SFrames:ApplyStatusBarTexture(frame.health, "raidHealthTexture", "barTexture")
local fontPath = SFrames:GetFont() 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 if frame.nameText then
frame.nameText:SetFont(fontPath, metrics.nameFont, outline) SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "raidNameFontKey", "fontKey")
end end
if frame.healthText then 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
end end
@@ -724,6 +893,9 @@ function SFrames.Raid:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1) f.nameText:SetTextColor(1, 1, 1)
end end
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.health)
end
self:UpdateHealth(unit) self:UpdateHealth(unit)
self:UpdatePower(unit) self:UpdatePower(unit)
@@ -794,11 +966,10 @@ function SFrames.Raid:UpdateHealth(unit)
txt = percent .. "%" txt = percent .. "%"
elseif db.raidHealthFormat == "deficit" then elseif db.raidHealthFormat == "deficit" then
if maxHp - hp > 0 then if maxHp - hp > 0 then
txt = "-" .. (maxHp - hp) txt = "-" .. SFrames:FormatCompactNumber(maxHp - hp)
end end
else else
txt = (math.floor(hp/100)/10).."k" -- default compact e.g. 4.5k txt = SFrames:FormatCompactNumber(hp)
if hp < 1000 then txt = tostring(hp) end
end end
f.healthText:SetText(txt) f.healthText:SetText(txt)
@@ -826,14 +997,8 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther local predOther = f.health.healPredOther
local predOver = f.health.healPredOver local predOver = f.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists(unit) or not UnitIsConnected(unit) then if not UnitExists(unit) or not UnitIsConnected(unit) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -855,7 +1020,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end end
if maxHp <= 0 then if maxHp <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -872,7 +1037,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end end
local missing = maxHp - hp local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -880,13 +1045,13 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local remaining = missing - mineShown local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining) local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
local barWidth = f:GetWidth() - 2 local barWidth = f:GetWidth() - 2
if barWidth <= 0 then if barWidth <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -896,7 +1061,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local availableWidth = barWidth - currentPosition local availableWidth = barWidth - currentPosition
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -972,6 +1137,10 @@ function SFrames.Raid:UpdatePower(unit)
local pType = UnitPowerType(unit) local pType = UnitPowerType(unit)
local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0] local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0]
f.power:SetStatusBarColor(color.r, color.g, color.b) 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 break
end end
end end
@@ -1072,7 +1241,11 @@ function SFrames.Raid:UpdateAuras(unit)
local f = frameData.frame local f = frameData.frame
local buffsNeeded = self:GetClassBuffs() 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 -- Hide all first
for i = 1, 4 do for i = 1, 4 do
@@ -1085,70 +1258,22 @@ function SFrames.Raid:UpdateAuras(unit)
return return
end end
local function MatchesList(auraName, list) -- Check Buffs (using module-level helpers, no closures)
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
for i = 1, 32 do for i = 1, 32 do
local texture, applications = UnitBuff(unit, i) local texture, applications = UnitBuff(unit, i)
if not texture then break end if not texture then break end
local buffName = GetBuffName(unit, i) local buffName = RaidGetBuffName(unit, i)
if buffName then if buffName then
for pos, listData in pairs(buffsNeeded) do 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 if MatchesList(buffName, listData) then
f.indicators[pos].icon:SetTexture(texture) f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i f.indicators[pos].index = i
f.indicators[pos].isDebuff = false f.indicators[pos].isDebuff = false
f.indicators[pos]:Show() f.indicators[pos]:Show()
foundIndicators[pos] = true _foundIndicators[pos] = true
end end
end end
end end
@@ -1156,7 +1281,10 @@ function SFrames.Raid:UpdateAuras(unit)
end end
local hasDebuff = false 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 -- Check Debuffs
for i = 1, 16 do for i = 1, 16 do
@@ -1165,24 +1293,24 @@ function SFrames.Raid:UpdateAuras(unit)
if dispelType then if dispelType then
hasDebuff = true hasDebuff = true
if dispelType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1} if dispelType == "Magic" then _debuffColor.r = 0.2; _debuffColor.g = 0.6; _debuffColor.b = 1
elseif dispelType == "Curse" then debuffColor = {r=0.6, g=0, b=1} elseif dispelType == "Curse" then _debuffColor.r = 0.6; _debuffColor.g = 0; _debuffColor.b = 1
elseif dispelType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0} elseif dispelType == "Disease" then _debuffColor.r = 0.6; _debuffColor.g = 0.4; _debuffColor.b = 0
elseif dispelType == "Poison" then debuffColor = {r=0, g=0.6, b=0} elseif dispelType == "Poison" then _debuffColor.r = 0; _debuffColor.g = 0.6; _debuffColor.b = 0
end end
end end
local debuffName = GetDebuffName(unit, i) local debuffName = RaidGetDebuffName(unit, i)
if debuffName then if debuffName then
for pos, listData in pairs(buffsNeeded) do 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 if MatchesList(debuffName, listData) then
f.indicators[pos].icon:SetTexture(texture) f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i f.indicators[pos].index = i
f.indicators[pos].isDebuff = true f.indicators[pos].isDebuff = true
f.indicators[pos]:Show() f.indicators[pos]:Show()
foundIndicators[pos] = true _foundIndicators[pos] = true
end end
end end
end end
@@ -1190,9 +1318,8 @@ function SFrames.Raid:UpdateAuras(unit)
end end
if hasDebuff then 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 else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end end
end end

View File

@@ -182,6 +182,17 @@ local function Clamp(value, minValue, maxValue)
return value return value
end 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_WIDTH = 80
local DIST_BASE_HEIGHT = 24 local DIST_BASE_HEIGHT = 24
local DIST_BASE_FONTSIZE = 14 local DIST_BASE_FONTSIZE = 14
@@ -232,6 +243,33 @@ function SFrames.Target:GetConfig()
local powerHeight = tonumber(db.targetPowerHeight) or 9 local powerHeight = tonumber(db.targetPowerHeight) or 9
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40) 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 local height = healthHeight + powerHeight + 4
height = Clamp(height, 30, 140) height = Clamp(height, 30, 140)
@@ -241,6 +279,12 @@ function SFrames.Target:GetConfig()
local valueFont = tonumber(db.targetValueFontSize) or 10 local valueFont = tonumber(db.targetValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) 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 local frameScale = tonumber(db.targetFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8) frameScale = Clamp(frameScale, 0.7, 1.8)
@@ -250,8 +294,16 @@ function SFrames.Target:GetConfig()
portraitWidth = portraitWidth, portraitWidth = portraitWidth,
healthHeight = healthHeight, healthHeight = healthHeight,
powerHeight = powerHeight, powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.targetPowerOnTop == true,
nameFont = nameFont, nameFont = nameFont,
valueFont = valueFont, valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("targetHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("targetPowerTexture", "barTexture"),
scale = frameScale, scale = frameScale,
} }
end end
@@ -334,8 +386,8 @@ function SFrames.Target:ApplyConfig()
if f.power then if f.power then
f.power:ClearAllPoints() f.power:ClearAllPoints()
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY)
f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0) f.power:SetWidth(cfg.powerWidth)
f.power:SetHeight(cfg.powerHeight) f.power:SetHeight(cfg.powerHeight)
end end
@@ -345,19 +397,71 @@ function SFrames.Target:ApplyConfig()
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
end end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" SFrames:ApplyStatusBarTexture(f.health, "targetHealthTexture", "barTexture")
local fontPath = SFrames:GetFont() 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 -- Gradient style preset
f.nameText:SetFont(fontPath, cfg.nameFont, outline) if SFrames:IsGradientStyle() then
end -- Hide portrait & its backdrop
if f.healthText then if f.portrait then f.portrait:Hide() end
f.healthText:SetFont(fontPath, cfg.valueFont, outline) if f.portraitBG then f.portraitBG:Hide() end
end -- Strip backdrops
if f.powerText then SFrames:ClearBackdrop(f)
f.powerText:SetFont(fontPath, cfg.valueFont, outline) 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 end
ApplyFontIfPresent(f.nameText, cfg.nameFont, "targetNameFontKey")
ApplyFontIfPresent(f.healthText, cfg.healthFont, "targetHealthFontKey")
ApplyFontIfPresent(f.powerText, cfg.powerFont, "targetPowerFontKey")
if f.castbar then if f.castbar then
f.castbar:ClearAllPoints() f.castbar:ClearAllPoints()
if showPortrait then if showPortrait then
@@ -374,6 +478,24 @@ function SFrames.Target:ApplyConfig()
self:ApplyDistanceScale(dScale) self:ApplyDistanceScale(dScale)
end 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 if UnitExists("target") then
self:UpdateAll() self:UpdateAll()
end end
@@ -386,8 +508,6 @@ function SFrames.Target:ApplyDistanceScale(scale)
f:SetWidth(DIST_BASE_WIDTH * scale) f:SetWidth(DIST_BASE_WIDTH * scale)
f:SetHeight(DIST_BASE_HEIGHT * scale) f:SetHeight(DIST_BASE_HEIGHT * scale)
if f.text then 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 customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)
local fontSize local fontSize
if customSize and customSize >= 8 and customSize <= 24 then if customSize and customSize >= 8 and customSize <= 24 then
@@ -395,7 +515,7 @@ function SFrames.Target:ApplyDistanceScale(scale)
else else
fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5)) fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
end end
f.text:SetFont(fontPath, fontSize, outline) SFrames:ApplyFontString(f.text, fontSize, "targetDistanceFontKey", "fontKey")
end end
end end
@@ -437,54 +557,46 @@ function SFrames.Target:InitializeDistanceFrame()
f.text:SetShadowColor(0, 0, 0, 1) f.text:SetShadowColor(0, 0, 0, 1)
f.text:SetShadowOffset(1, -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 SFrames.Target.distanceFrame = f
f:Hide() f:Hide()
f.timer = 0 local ticker = CreateFrame("Frame", nil, UIParent)
f:SetScript("OnUpdate", function() ticker:SetWidth(1)
if SFramesDB and SFramesDB.targetDistanceEnabled == false then ticker:SetHeight(1)
if this:IsShown() then this:Hide() end 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 return
end end
if not UnitExists("target") then if not UnitExists("target") then
if this:IsShown() then this:Hide() end if distFrame:IsShown() then distFrame:Hide() end
if this.behindText then this.behindText:Hide() end if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
return return
end end
this.timer = this.timer + (arg1 or 0) this.timer = this.timer + (arg1 or 0)
if this.timer >= 0.4 then if this.timer >= 0.4 then
this.timer = 0 this.timer = 0
local dist = SFrames.Target:GetDistance("target") local dist = SFrames.Target:GetDistance("target")
this.text:SetText(dist or "---") local distStr = dist or "---"
if not this:IsShown() then this:Show() end
-- Behind indicator if onFrame and embeddedText then
if this.behindText then embeddedText:SetText(distStr)
local showBehind = not SFramesDB or SFramesDB.Tweaks == nil if not embeddedText:IsShown() then embeddedText:Show() end
or SFramesDB.Tweaks.behindIndicator ~= false if distFrame:IsShown() then distFrame:Hide() end
if showBehind and IsUnitXPAvailable() then else
local ok, isBehind = pcall(UnitXP, "behind", "player", "target") distFrame.text:SetText(distStr)
if ok and isBehind then if not distFrame:IsShown() then distFrame:Show() end
this.behindText:SetText("背后") if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
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
end end
end end
end) end)
@@ -515,15 +627,23 @@ function SFrames.Target:Initialize()
local f = CreateFrame("Button", "SFramesTargetFrame", UIParent) local f = CreateFrame("Button", "SFramesTargetFrame", UIParent)
f:SetWidth(SFrames.Config.width) f:SetWidth(SFrames.Config.width)
f:SetHeight(SFrames.Config.height) 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 local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1
f:SetScale(frameScale) 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:SetMovable(true)
f:EnableMouse(true) f:EnableMouse(true)
f:RegisterForDrag("LeftButton") f:RegisterForDrag("LeftButton")
@@ -533,25 +653,21 @@ function SFrames.Target:Initialize()
if not SFramesDB then SFramesDB = {} end if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() 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 } SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end) end)
f:RegisterForClicks("LeftButtonUp", "RightButtonUp") f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function() f:SetScript("OnClick", function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] OnClick fired: " .. tostring(arg1) .. "|r")
if arg1 == "LeftButton" then if arg1 == "LeftButton" then
-- Shift+左键 = 设为焦点 -- Shift+左键 = 设为焦点
if IsShiftKeyDown() then if IsShiftKeyDown() then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Shift+LeftButton -> SetFocus|r")
if SFrames.Focus and SFrames.Focus.SetFromTarget then if SFrames.Focus and SFrames.Focus.SetFromTarget then
local ok, err = pcall(SFrames.Focus.SetFromTarget, SFrames.Focus) 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")
end end
return return
end end
@@ -562,31 +678,25 @@ function SFrames.Target:Initialize()
SpellTargetUnit(this.unit) SpellTargetUnit(this.unit)
end end
elseif arg1 == "RightButton" then elseif arg1 == "RightButton" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] RightButton hit|r")
if SpellIsTargeting and SpellIsTargeting() then if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting() SpellStopTargeting()
return return
end end
if not UnitExists("target") then if not UnitExists("target") then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] No target, abort|r")
return return
end end
if not SFrames.Target.dropDown then if not SFrames.Target.dropDown then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Creating dropdown...|r")
local ok1, err1 = pcall(function() local ok1, err1 = pcall(function()
SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate") SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate")
end) end)
if not ok1 then if not ok1 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] CreateFrame failed: " .. tostring(err1) .. "|r")
return return
end end
SFrames.Target.dropDown.displayMode = "MENU" SFrames.Target.dropDown.displayMode = "MENU"
SFrames.Target.dropDown.initialize = function() SFrames.Target.dropDown.initialize = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] initialize() called|r")
local dd = SFrames.Target.dropDown local dd = SFrames.Target.dropDown
local name = dd.targetName local name = dd.targetName
if not name then if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] initialize: no targetName|r")
return return
end end
@@ -678,16 +788,9 @@ function SFrames.Target:Initialize()
-- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位) -- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位)
end end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Dropdown created OK|r")
end end
SFrames.Target.dropDown.targetName = UnitName("target") SFrames.Target.dropDown.targetName = UnitName("target")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Calling ToggleDropDownMenu...|r") pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
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
end end
end) end)
f:SetScript("OnReceiveDrag", function() f:SetScript("OnReceiveDrag", function()
@@ -799,6 +902,19 @@ function SFrames.Target:Initialize()
f.comboText:SetTextColor(1, 0.8, 0) f.comboText:SetTextColor(1, 0.8, 0)
f.comboText:SetText("") 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) -- Raid Target Icon (top center of health bar, half outside frame)
local raidIconSize = 22 local raidIconSize = 22
local raidIconOvr = CreateFrame("Frame", nil, f) local raidIconOvr = CreateFrame("Frame", nil, f)
@@ -864,7 +980,8 @@ function SFrames.Target:Initialize()
-- Register movers -- Register movers
if SFrames.Movers and SFrames.Movers.RegisterMover then if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("TargetFrame", f, "目标", SFrames.Movers:RegisterMover("TargetFrame", f, "目标",
"CENTER", "UIParent", "CENTER", 200, -100) "CENTER", "UIParent", "CENTER", 200, -100,
nil, { alwaysShowInLayout = true })
if SFrames.Target.distanceFrame then if SFrames.Target.distanceFrame then
SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离", SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离",
"CENTER", "UIParent", "CENTER", 0, 100) "CENTER", "UIParent", "CENTER", 0, 100)
@@ -1047,18 +1164,24 @@ function SFrames.Target:OnTargetChanged()
if UnitExists("target") then if UnitExists("target") then
self.frame:Show() self.frame:Show()
self:UpdateAll() self:UpdateAll()
local enabled = not (SFramesDB and SFramesDB.targetDistanceEnabled == false)
local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
if SFrames.Target.distanceFrame then if SFrames.Target.distanceFrame then
local dist = self:GetDistance("target") local dist = self:GetDistance("target")
SFrames.Target.distanceFrame.text:SetText(dist or "---") if onFrame and self.frame.distText then
if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then self.frame.distText:SetText(dist or "---")
SFrames.Target.distanceFrame:Show() if enabled then self.frame.distText:Show() else self.frame.distText:Hide() end
else
SFrames.Target.distanceFrame:Hide() 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
end end
else else
self.frame:Hide() self.frame:Hide()
if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end 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
end end
@@ -1153,6 +1276,8 @@ function SFrames.Target:UpdateAll()
end end
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) 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 if UnitIsPlayer("target") and useClassColor then
local _, class = UnitClass("target") local _, class = UnitClass("target")
@@ -1184,6 +1309,10 @@ function SFrames.Target:UpdateAll()
self.frame.nameText:SetText(formattedLevel .. name) self.frame.nameText:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b) self.frame.nameText:SetTextColor(r, g, b)
end end
-- Re-apply gradient after color change
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.health)
end
end end
function SFrames.Target:UpdateHealth() function SFrames.Target:UpdateHealth()
@@ -1208,9 +1337,9 @@ function SFrames.Target:UpdateHealth()
end end
if displayMax > 0 then if displayMax > 0 then
self.frame.healthText:SetText(displayHp .. " / " .. displayMax) self.frame.healthText:SetText(SFrames:FormatCompactPair(displayHp, displayMax))
else else
self.frame.healthText:SetText(displayHp) self.frame.healthText:SetText(SFrames:FormatCompactNumber(displayHp))
end end
self:UpdateHealPrediction() self:UpdateHealPrediction()
@@ -1222,14 +1351,8 @@ function SFrames.Target:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver local predOver = self.frame.health.healPredOver
local function HidePredictions()
predMine:Hide()
predOther:Hide()
predOver:Hide()
end
if not UnitExists("target") then if not UnitExists("target") then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1251,7 +1374,7 @@ function SFrames.Target:UpdateHealPrediction()
end end
if maxHp <= 0 then if maxHp <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1268,7 +1391,7 @@ function SFrames.Target:UpdateHealPrediction()
end end
local missing = maxHp - hp local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1276,14 +1399,14 @@ function SFrames.Target:UpdateHealPrediction()
local remaining = missing - mineShown local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining) local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false
local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2) local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2)
if barWidth <= 0 then if barWidth <= 0 then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1293,7 +1416,7 @@ function SFrames.Target:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions() predMine:Hide(); predOther:Hide(); predOver:Hide()
return return
end end
@@ -1359,6 +1482,9 @@ function SFrames.Target:UpdatePowerType()
else else
self.frame.power:SetStatusBarColor(0, 0, 1) self.frame.power:SetStatusBarColor(0, 0, 1)
end end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(self.frame.power)
end
end end
function SFrames.Target:UpdatePower() function SFrames.Target:UpdatePower()
@@ -1367,10 +1493,11 @@ function SFrames.Target:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower) self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power) self.frame.power:SetValue(power)
if maxPower > 0 then if maxPower > 0 then
self.frame.powerText:SetText(power .. " / " .. maxPower) self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else else
self.frame.powerText:SetText("") self.frame.powerText:SetText("")
end end
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "target")
end end
function SFrames.Target:UpdateComboPoints() function SFrames.Target:UpdateComboPoints()
@@ -1501,23 +1628,15 @@ end
function SFrames.Target:TickAuras() function SFrames.Target:TickAuras()
if not UnitExists("target") then return end if not UnitExists("target") then return end
local timeNow = GetTime() local tracker = SFrames.AuraTracker
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime 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 -- Buffs
for i = 1, 32 do for i = 1, 32 do
local b = self.frame.buffs[i] local b = self.frame.buffs[i]
if b:IsShown() and b.expirationTime then if b:IsShown() then
local timeLeft = b.expirationTime - timeNow local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "buff", i)
if timeLeft > 0 and timeLeft < 3600 then if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then if npFormat then
local text, r, g, bc, a = npFormat(timeLeft) local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text) b.cdText:SetText(text)
@@ -1531,30 +1650,11 @@ function SFrames.Target:TickAuras()
end end
end end
-- Debuffs: re-query SpellDB for live-accurate timers -- Debuffs
for i = 1, 32 do for i = 1, 32 do
local b = self.frame.debuffs[i] local b = self.frame.debuffs[i]
if b:IsShown() then if b:IsShown() then
local timeLeft = nil local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "debuff", i)
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
if timeLeft and timeLeft > 0 and timeLeft < 3600 then if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then if npFormat then
local text, r, g, bc, a = npFormat(timeLeft) local text, r, g, bc, a = npFormat(timeLeft)
@@ -1573,6 +1673,11 @@ end
function SFrames.Target:UpdateAuras() function SFrames.Target:UpdateAuras()
if not UnitExists("target") then return end 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 hasSuperWoW = SFrames.superwow_active and SpellInfo
local numBuffs = 0 local numBuffs = 0
-- Buffs -- Buffs
@@ -1584,12 +1689,9 @@ function SFrames.Target:UpdateAuras()
b.icon:SetTexture(texture) b.icon:SetTexture(texture)
-- Store aura ID when SuperWoW is available -- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swAuraID or nil b.auraID = hasSuperWoW and swAuraID or nil
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") local state = tracker and tracker:GetAuraState("target", "buff", i)
SFrames.Tooltip:ClearLines() local timeLeft = state and tracker:GetAuraTimeLeft("target", "buff", i)
SFrames.Tooltip:SetUnitBuff("target", i)
local timeLeft = SFrames:GetAuraTimeLeft("target", i, true)
SFrames.Tooltip:Hide()
if timeLeft and timeLeft > 0 then if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft b.expirationTime = GetTime() + timeLeft
b.cdText:SetText(SFrames:FormatTime(timeLeft)) b.cdText:SetText(SFrames:FormatTime(timeLeft))
@@ -1621,7 +1723,6 @@ function SFrames.Target:UpdateAuras()
end end
-- Debuffs -- Debuffs
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
for i = 1, 32 do for i = 1, 32 do
@@ -1633,39 +1734,12 @@ function SFrames.Target:UpdateAuras()
-- Store aura ID when SuperWoW is available -- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swDebuffAuraID or nil b.auraID = hasSuperWoW and swDebuffAuraID or nil
local timeLeft = 0 local state = tracker and tracker:GetAuraState("target", "debuff", i)
local effectName = nil local timeLeft = state and tracker:GetAuraTimeLeft("target", "debuff", i)
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
if timeLeft and timeLeft > 0 then if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft b.expirationTime = GetTime() + timeLeft
b.effectName = effectName b.effectName = state and state.name or nil
if npFormat then if npFormat then
local text, r, g, bc, a = npFormat(timeLeft) local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text) b.cdText:SetText(text)

View File

@@ -1,12 +1,36 @@
SFrames.ToT = {} SFrames.ToT = {}
local _A = SFrames.ActiveTheme 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() function SFrames.ToT:Initialize()
local f = CreateFrame("Button", "SFramesToTFrame", UIParent) local f = CreateFrame("Button", "SFramesToTFrame", UIParent)
f:SetWidth(120) f:SetWidth(120)
f:SetHeight(25) 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:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function() f:SetScript("OnClick", function()
if arg1 == "LeftButton" then if arg1 == "LeftButton" then
@@ -24,6 +48,7 @@ function SFrames.ToT:Initialize()
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
hbg:SetFrameLevel(f:GetFrameLevel() - 1) hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg) SFrames:CreateUnitBackdrop(hbg)
f.hbg = hbg
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints() f.health.bg:SetAllPoints()
@@ -35,7 +60,15 @@ function SFrames.ToT:Initialize()
self.frame = f self.frame = f
f:Hide() 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 -- Update loop since targettarget changes don't fire precise events in Vanilla
self.updater = CreateFrame("Frame") self.updater = CreateFrame("Frame")
self.updater.timer = 0 self.updater.timer = 0

View File

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