聊天重做前缓存
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find . -name \"*.lua\" -exec grep -l \"database\\\\|DATABASE\\\\|Encyclopedia\\\\|encyclop\" {} \\\\;)",
|
||||
"Bash(ls -lah *.lua)"
|
||||
]
|
||||
}
|
||||
}
|
||||
866
ActionBars.lua
866
ActionBars.lua
File diff suppressed because it is too large
Load Diff
594
AuraTracker.lua
Normal file
594
AuraTracker.lua
Normal 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
|
||||
145
Bindings.xml
145
Bindings.xml
@@ -4,4 +4,149 @@
|
||||
SFrames.WorldMap:ToggleNav()
|
||||
end
|
||||
</Binding>
|
||||
|
||||
<Binding name="NANAMI_EXTRABAR1" header="NANAMI_EXTRABAR" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(1) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR2" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(2) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR3" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(3) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR4" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(4) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR5" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(5) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR6" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(6) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR7" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(7) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR8" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(8) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR9" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(9) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR10" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(10) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR11" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(11) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR12" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(12) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR13" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(13) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR14" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(14) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR15" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(15) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR16" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(16) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR17" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(17) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR18" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(18) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR19" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(19) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR20" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(20) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR21" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(21) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR22" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(22) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR23" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(23) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR24" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(24) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR25" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(25) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR26" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(26) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR27" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(27) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR28" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(28) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR29" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(29) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR30" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(30) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR31" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(31) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR32" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(32) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR33" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(33) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR34" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(34) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR35" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(35) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR36" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(36) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR37" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(37) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR38" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(38) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR39" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(39) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR40" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(40) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR41" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(41) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR42" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(42) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR43" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(43) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR44" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(44) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR45" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(45) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR46" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(46) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR47" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(47) end
|
||||
</Binding>
|
||||
<Binding name="NANAMI_EXTRABAR48" runOnUp="false">
|
||||
if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(48) end
|
||||
</Binding>
|
||||
</Bindings>
|
||||
|
||||
@@ -338,6 +338,58 @@ local BASE_SPELL_CRIT = {
|
||||
DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0,
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Temporary weapon enchant crit detection (sharpening stones / scopes)
|
||||
-- Scans tooltip green text for crit keywords, returns crit% bonus (e.g. 2)
|
||||
--------------------------------------------------------------------------------
|
||||
local _tempEnchTip
|
||||
local function GetTempEnchantCrit(slotId)
|
||||
if not GetWeaponEnchantInfo then return 0 end
|
||||
-- slotId 16=MainHand, 17=OffHand, 18=Ranged
|
||||
local hasMain, _, _, hasOff
|
||||
hasMain, _, _, hasOff = GetWeaponEnchantInfo()
|
||||
if slotId == 16 and not hasMain then return 0 end
|
||||
if slotId == 17 and not hasOff then return 0 end
|
||||
if slotId == 18 and not hasMain and not hasOff then
|
||||
-- ranged slot: some servers report via hasMain for ranged-only classes
|
||||
-- try scanning anyway if there's a ranged weapon equipped
|
||||
if not GetInventoryItemLink("player", 18) then return 0 end
|
||||
end
|
||||
|
||||
if not _tempEnchTip then
|
||||
_tempEnchTip = CreateFrame("GameTooltip", "SFramesCPTempEnchTip", UIParent, "GameTooltipTemplate")
|
||||
_tempEnchTip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -300, -300)
|
||||
end
|
||||
local tip = _tempEnchTip
|
||||
tip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
tip:ClearLines()
|
||||
tip:SetInventoryItem("player", slotId)
|
||||
local n = tip:NumLines()
|
||||
if not n or n < 2 then return 0 end
|
||||
|
||||
for i = 2, n do
|
||||
local obj = _G["SFramesCPTempEnchTipTextLeft" .. i]
|
||||
if obj then
|
||||
local txt = obj:GetText()
|
||||
if txt and txt ~= "" then
|
||||
local r, g, b = obj:GetTextColor()
|
||||
-- green text = enchant/buff line
|
||||
if g > 0.8 and r < 0.5 and b < 0.5 then
|
||||
-- Match patterns like: "+2% 致命一击" / "+2% Critical" / "致命一击几率提高2%"
|
||||
local _, _, pct = string.find(txt, "(%d+)%%%s*致命")
|
||||
if not pct then _, _, pct = string.find(txt, "致命.-(%d+)%%") end
|
||||
if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*[Cc]rit") end
|
||||
if not pct then _, _, pct = string.find(txt, "[Cc]rit.-(%d+)%%") end
|
||||
if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*暴击") end
|
||||
if not pct then _, _, pct = string.find(txt, "暴击.-(%d+)%%") end
|
||||
if pct then return tonumber(pct) or 0 end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function CalcMeleeCrit()
|
||||
local _, class = UnitClass("player")
|
||||
class = class or ""
|
||||
@@ -523,8 +575,9 @@ local function FullMeleeCrit()
|
||||
end
|
||||
local gearCrit = GetGearBonus("CRIT")
|
||||
local talentCrit = GetTalentBonus("meleeCrit")
|
||||
return baseCrit + agiCrit + gearCrit + talentCrit,
|
||||
baseCrit, agiCrit, gearCrit, talentCrit
|
||||
local tempCrit = GetTempEnchantCrit(16)
|
||||
return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
|
||||
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
|
||||
end
|
||||
local function FullRangedCrit()
|
||||
local _, class = UnitClass("player")
|
||||
@@ -537,8 +590,9 @@ local function FullRangedCrit()
|
||||
end
|
||||
local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
|
||||
local talentCrit = GetTalentBonus("rangedCrit")
|
||||
return baseCrit + agiCrit + gearCrit + talentCrit,
|
||||
baseCrit, agiCrit, gearCrit, talentCrit
|
||||
local tempCrit = GetTempEnchantCrit(18)
|
||||
return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
|
||||
baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
|
||||
end
|
||||
local function FullSpellCrit()
|
||||
local _, class = UnitClass("player")
|
||||
@@ -659,6 +713,7 @@ CS.FullSpellHit = FullSpellHit
|
||||
CS.GetTalentDetailsFor = GetTalentDetailsFor
|
||||
CS.GetGearBonus = GetGearBonus
|
||||
CS.GetItemBonusLib = GetItemBonusLib
|
||||
CS.GetTempEnchantCrit = GetTempEnchantCrit
|
||||
CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT
|
||||
|
||||
SFrames.CharacterPanel.CS = CS
|
||||
@@ -2009,7 +2064,7 @@ function CP:BuildEquipmentPage()
|
||||
local crit = CS.SafeGetMeleeCrit()
|
||||
CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5)
|
||||
else
|
||||
local total, base, agiC, gearC, talC = CS.FullMeleeCrit()
|
||||
local total, base, agiC, gearC, talC, tempC = CS.FullMeleeCrit()
|
||||
CS.TipLine("来源分项:", 0.5,0.8,1)
|
||||
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
|
||||
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
|
||||
@@ -2021,6 +2076,9 @@ function CP:BuildEquipmentPage()
|
||||
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
|
||||
end
|
||||
end
|
||||
if tempC and tempC > 0 then
|
||||
CS.TipKV(" 临时附魔(磨刀石):", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
|
||||
end
|
||||
GameTooltip:AddLine(" ")
|
||||
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
|
||||
CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3)
|
||||
@@ -2135,7 +2193,7 @@ function CP:BuildEquipmentPage()
|
||||
if fromAPI then
|
||||
CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5)
|
||||
else
|
||||
local total, base, agiC, gearC, talC = CS.FullRangedCrit()
|
||||
local total, base, agiC, gearC, talC, tempC = CS.FullRangedCrit()
|
||||
CS.TipLine("来源分项:", 0.5,0.8,1)
|
||||
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
|
||||
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
|
||||
@@ -2147,6 +2205,9 @@ function CP:BuildEquipmentPage()
|
||||
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
|
||||
end
|
||||
end
|
||||
if tempC and tempC > 0 then
|
||||
CS.TipKV(" 临时附魔(瞄准镜):", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
|
||||
end
|
||||
GameTooltip:AddLine(" ")
|
||||
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
|
||||
CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3)
|
||||
|
||||
605
Chat.lua
605
Chat.lua
@@ -18,6 +18,7 @@ local DEFAULTS = {
|
||||
topPadding = 30,
|
||||
bottomPadding = 8,
|
||||
bgAlpha = 0.45,
|
||||
hoverTransparent = true,
|
||||
activeTab = 1,
|
||||
editBoxPosition = "bottom",
|
||||
editBoxX = 0,
|
||||
@@ -739,27 +740,87 @@ local function GetTranslateFilterKeyForEvent(event)
|
||||
return TRANSLATE_EVENT_FILTERS[event]
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- HC 公会成员缓存:用于"仅通报工会成员"过滤
|
||||
-- ============================================================
|
||||
local HCGuildMemberCache = {}
|
||||
|
||||
local function RefreshHCGuildCache()
|
||||
HCGuildMemberCache = {}
|
||||
if not (IsInGuild and IsInGuild()) then return end
|
||||
if not GetNumGuildMembers then return end
|
||||
local total = GetNumGuildMembers()
|
||||
for i = 1, total do
|
||||
local name = GetGuildRosterInfo(i)
|
||||
if type(name) == "string" and name ~= "" then
|
||||
HCGuildMemberCache[name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function IsHCGuildMember(name)
|
||||
return type(name) == "string" and HCGuildMemberCache[name] == true
|
||||
end
|
||||
|
||||
-- 从 HC 系统消息中提取主角色名(死亡/升级均支持)
|
||||
local function ParseHCCharacterName(text)
|
||||
if type(text) ~= "string" then return nil end
|
||||
-- 中文死亡格式: "硬核角色 NAME(等级 N)"
|
||||
local _, _, n1 = string.find(text, "硬核角色%s+(.-)(等级")
|
||||
if n1 and n1 ~= "" then return n1 end
|
||||
-- 中文升级格式: "NAME 在硬核模式中已达到"
|
||||
local _, _, n2 = string.find(text, "^(.-)%s+在硬核模式")
|
||||
if n2 and n2 ~= "" then return n2 end
|
||||
-- 英文死亡格式: "Hardcore character NAME (Level N)"
|
||||
local _, _, n3 = string.find(text, "[Hh]ardcore character%s+(.-)%s+%(Level")
|
||||
if n3 and n3 ~= "" then return n3 end
|
||||
-- 英文升级格式: "NAME has reached level"
|
||||
local _, _, n4 = string.find(text, "^(.-)%s+has reached level")
|
||||
if n4 and n4 ~= "" then return n4 end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 检测HC等级里程碑消息(如"达到20级");返回等级数字,否则返回nil
|
||||
local function ParseHardcoreLevelMessage(text)
|
||||
if type(text) ~= "string" or text == "" then return nil end
|
||||
local lower = string.lower(text)
|
||||
-- 英文: "has reached level X" / "reached level X"
|
||||
if string.find(lower, "reached level") then
|
||||
local _, _, lvl = string.find(lower, "reached level%s+(%d+)")
|
||||
return tonumber(lvl) or 1
|
||||
end
|
||||
-- 中文: "达到 X 级" / "已达到X级"
|
||||
if string.find(text, "达到") then
|
||||
local _, _, lvl = string.find(text, "达到%s*(%d+)%s*级")
|
||||
if lvl then return tonumber(lvl) end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 检测HC死亡通报消息;排除等级里程碑;返回等级数字,否则返回nil
|
||||
local function ParseHardcoreDeathMessage(text)
|
||||
if type(text) ~= "string" or text == "" then return nil end
|
||||
if not string.find(text, "硬核") and not string.find(text, "死亡") then
|
||||
-- 先排除等级里程碑消息,避免误判(里程碑消息也含"死亡"字样)
|
||||
if ParseHardcoreLevelMessage(text) then return nil end
|
||||
-- 初步过滤:必须含有死亡/击杀相关关键词
|
||||
local lower = string.lower(text)
|
||||
if not string.find(lower, "hc news") and not string.find(lower, "has fallen")
|
||||
and not string.find(lower, "died") and not string.find(lower, "slain") then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local hasHC = string.find(text, "硬核") or string.find(lower, "hardcore") or string.find(lower, "hc")
|
||||
local hasDead = string.find(text, "死亡") or string.find(text, "击杀")
|
||||
or string.find(lower, "has fallen") or string.find(lower, "slain")
|
||||
or string.find(lower, "died") or string.find(lower, "hc news")
|
||||
if not hasDead then return nil end
|
||||
local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
||||
clean = string.gsub(clean, "|r", "")
|
||||
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
|
||||
-- 英文格式: Level: 17 / Level 17
|
||||
local _, _, lvlStr = string.find(clean, "Level%s*:%s*(%d+)")
|
||||
if lvlStr then return tonumber(lvlStr) end
|
||||
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
|
||||
local _, _, lvlStr2 = string.find(clean, "Level%s+(%d+)")
|
||||
if lvlStr2 then return tonumber(lvlStr2) end
|
||||
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
|
||||
-- 中文格式: 等级 1 / 等级1 / (等级 1)
|
||||
local _, _, lvlStr3 = string.find(clean, "等级%s*(%d+)")
|
||||
if lvlStr3 then return tonumber(lvlStr3) end
|
||||
local lower = string.lower(clean)
|
||||
if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then
|
||||
return 1
|
||||
end
|
||||
-- 兜底:有死亡关键词即返回1
|
||||
if hasDead then return 1 end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -774,6 +835,68 @@ local function CleanTextForTranslation(text)
|
||||
return clean
|
||||
end
|
||||
|
||||
-- HC 死亡消息的句式精确解析:提取怪物名和地点名,翻译后原位替换,玩家名不动
|
||||
-- 中文格式: "...硬核角色 [玩家](等级 N)被 [怪物](等级 N)击杀。这发生在 [地点]。..."
|
||||
-- 英文格式: "...character [PLAYER] (Level N) has been slain by [MONSTER] (Level N)...in [ZONE]."
|
||||
local function TranslateHCDeathParts(text, onDone)
|
||||
local api = _G.STranslateAPI
|
||||
local canTranslate = api and api.IsReady and api.IsReady() and api.ForceToChinese
|
||||
|
||||
-- 提取怪物名(中文句式)
|
||||
local _, _, monster = string.find(text, ")被%s*(.-)(等级")
|
||||
-- 提取地点名(中文句式)
|
||||
local _, _, zone = string.find(text, "这发生在%s*(.-)。")
|
||||
|
||||
-- 英文句式兜底
|
||||
if not monster then
|
||||
local _, _, m = string.find(text, "slain by%s+(.-)%s+%(Level")
|
||||
if m then monster = m end
|
||||
end
|
||||
if not zone then
|
||||
local _, _, z = string.find(text, "This happened in%s+(.-)[%.%!]")
|
||||
if z then zone = z end
|
||||
end
|
||||
|
||||
-- 收集需要翻译的词(含英文字母才有翻译意义)
|
||||
local targets = {}
|
||||
if monster and string.find(monster, "[A-Za-z]") then
|
||||
table.insert(targets, monster)
|
||||
end
|
||||
if zone and string.find(zone, "[A-Za-z]") and zone ~= monster then
|
||||
table.insert(targets, zone)
|
||||
end
|
||||
|
||||
if not canTranslate or table.getn(targets) == 0 then
|
||||
onDone(text)
|
||||
return
|
||||
end
|
||||
|
||||
-- 并行翻译,全部完成后替换回原文
|
||||
local out = text
|
||||
local total = table.getn(targets)
|
||||
local doneCount = 0
|
||||
for i = 1, total do
|
||||
local orig = targets[i]
|
||||
api.ForceToChinese(orig, function(translated, err, meta)
|
||||
if translated and translated ~= "" and translated ~= orig then
|
||||
local esc = string.gsub(orig, "([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1")
|
||||
local safeRepl = string.gsub(translated, "%%", "%%%%")
|
||||
out = string.gsub(out, esc, safeRepl)
|
||||
end
|
||||
doneCount = doneCount + 1
|
||||
if doneCount == total then
|
||||
onDone(out)
|
||||
end
|
||||
end, "Nanami-UI-HC")
|
||||
end
|
||||
end
|
||||
|
||||
-- 等级里程碑消息:玩家名无需翻译(是玩家自选名),整条消息已由服务器本地化,直接转发
|
||||
local function TranslateHCLevelParts(text, onDone)
|
||||
-- 仅当存在非玩家名的英文才翻译;里程碑消息唯一英文就是玩家名,故直接原文转发
|
||||
onDone(text)
|
||||
end
|
||||
|
||||
local function ForceHide(object)
|
||||
if not object then return end
|
||||
object:Hide()
|
||||
@@ -789,6 +912,66 @@ local function ForceInvisible(object)
|
||||
if object.EnableMouse then object:EnableMouse(false) end
|
||||
end
|
||||
|
||||
local function StripChatFrameArtwork(chatFrame)
|
||||
if not chatFrame then return end
|
||||
|
||||
local frameID = chatFrame.GetID and chatFrame:GetID()
|
||||
if frameID then
|
||||
if SetChatWindowColor then
|
||||
pcall(function() SetChatWindowColor(frameID, 0, 0, 0) end)
|
||||
end
|
||||
if SetChatWindowAlpha then
|
||||
pcall(function() SetChatWindowAlpha(frameID, 0) end)
|
||||
end
|
||||
end
|
||||
|
||||
if FCF_SetWindowAlpha then
|
||||
pcall(function() FCF_SetWindowAlpha(chatFrame, 0) end)
|
||||
end
|
||||
|
||||
if chatFrame.SetBackdropColor then
|
||||
chatFrame:SetBackdropColor(0, 0, 0, 0)
|
||||
end
|
||||
if chatFrame.SetBackdropBorderColor then
|
||||
chatFrame:SetBackdropBorderColor(0, 0, 0, 0)
|
||||
end
|
||||
if chatFrame.SetBackdrop then
|
||||
pcall(function() chatFrame:SetBackdrop(nil) end)
|
||||
end
|
||||
|
||||
local frameName = chatFrame.GetName and chatFrame:GetName()
|
||||
if type(frameName) == "string" and frameName ~= "" then
|
||||
local legacyTextures = {
|
||||
frameName .. "Background",
|
||||
frameName .. "BackgroundLeft",
|
||||
frameName .. "BackgroundMiddle",
|
||||
frameName .. "BackgroundRight",
|
||||
frameName .. "BottomButton",
|
||||
frameName .. "ButtonFrame",
|
||||
frameName .. "ButtonFrameBackground",
|
||||
}
|
||||
for _, texName in ipairs(legacyTextures) do
|
||||
local tex = _G[texName]
|
||||
if tex then
|
||||
if tex.SetTexture then tex:SetTexture(nil) end
|
||||
if tex.SetVertexColor then tex:SetVertexColor(0, 0, 0, 0) end
|
||||
if tex.SetAlpha then tex:SetAlpha(0) end
|
||||
tex:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local regions = { chatFrame:GetRegions() }
|
||||
for _, region in ipairs(regions) do
|
||||
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
|
||||
if region.SetTexture then region:SetTexture(nil) end
|
||||
if region.SetVertexColor then region:SetVertexColor(0, 0, 0, 0) end
|
||||
if region.SetAlpha then region:SetAlpha(0) end
|
||||
region:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateFont(parent, size, justify)
|
||||
if SFrames and SFrames.CreateFontString then
|
||||
return SFrames:CreateFontString(parent, size, justify)
|
||||
@@ -1547,6 +1730,9 @@ local function EnsureDB()
|
||||
if type(db.editBoxY) ~= "number" then db.editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY end
|
||||
if db.translateEnabled == nil then db.translateEnabled = true end
|
||||
if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = true end
|
||||
if db.hcDeathToGuild == nil then db.hcDeathToGuild = true end
|
||||
if db.hcLevelToGuild == nil then db.hcLevelToGuild = true end
|
||||
if db.hcGuildMemberOnly == nil then db.hcGuildMemberOnly = false end
|
||||
if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
|
||||
if db.layoutVersion < 2 then
|
||||
db.topPadding = DEFAULTS.topPadding
|
||||
@@ -1895,6 +2081,7 @@ function SFrames.Chat:GetConfig()
|
||||
topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
|
||||
bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
|
||||
bgAlpha = Clamp(db.bgAlpha, 0, 1),
|
||||
hoverTransparent = (db.hoverTransparent ~= false),
|
||||
editBoxPosition = editBoxPosition,
|
||||
editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
|
||||
editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
|
||||
@@ -3321,6 +3508,19 @@ function SFrames.Chat:RefreshConfigFrame()
|
||||
end
|
||||
end
|
||||
|
||||
if self.cfgMonitorSection then
|
||||
self.cfgMonitorSection:SetAlpha(1)
|
||||
end
|
||||
if self.cfgMonitorCb then
|
||||
self.cfgMonitorCb:Enable()
|
||||
end
|
||||
if self.cfgMonitorDesc then
|
||||
self.cfgMonitorDesc:SetTextColor(0.7, 0.7, 0.74)
|
||||
end
|
||||
if self.cfgMonitorReloadHint then
|
||||
self.cfgMonitorReloadHint:SetTextColor(0.9, 0.75, 0.5)
|
||||
end
|
||||
|
||||
if self.configControls then
|
||||
for i = 1, table.getn(self.configControls) do
|
||||
local ctrl = self.configControls[i]
|
||||
@@ -3485,12 +3685,12 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50)
|
||||
transDesc:SetWidth(520)
|
||||
transDesc:SetJustifyH("LEFT")
|
||||
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。")
|
||||
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。聊天监控可独立启用。")
|
||||
transDesc:SetTextColor(0.7, 0.7, 0.74)
|
||||
|
||||
local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath)
|
||||
|
||||
AddControl(CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
|
||||
local monitorCb = CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
|
||||
function() return EnsureDB().chatMonitorEnabled ~= false end,
|
||||
function(checked)
|
||||
EnsureDB().chatMonitorEnabled = (checked == true)
|
||||
@@ -3498,15 +3698,19 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
function()
|
||||
SFrames.Chat:RefreshConfigFrame()
|
||||
end
|
||||
))
|
||||
)
|
||||
AddControl(monitorCb)
|
||||
self.cfgMonitorCb = monitorCb
|
||||
self.cfgMonitorSection = monitorSection
|
||||
|
||||
local monDesc = monitorSection:CreateFontString(nil, "OVERLAY")
|
||||
monDesc:SetFont(fontPath, 10, "OUTLINE")
|
||||
monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50)
|
||||
monDesc:SetWidth(520)
|
||||
monDesc:SetJustifyH("LEFT")
|
||||
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用)。")
|
||||
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、职业染色等功能。\n可独立于 AI 翻译开关使用。关闭后消息将原样通过,[+] 复制等功能不可用。")
|
||||
monDesc:SetTextColor(0.7, 0.7, 0.74)
|
||||
self.cfgMonitorDesc = monDesc
|
||||
|
||||
local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY")
|
||||
reloadHint:SetFont(fontPath, 10, "OUTLINE")
|
||||
@@ -3515,11 +3719,12 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
reloadHint:SetJustifyH("LEFT")
|
||||
reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。")
|
||||
reloadHint:SetTextColor(0.9, 0.75, 0.5)
|
||||
self.cfgMonitorReloadHint = reloadHint
|
||||
end
|
||||
|
||||
local windowPage = CreatePage("window")
|
||||
do
|
||||
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
|
||||
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 304, fontPath)
|
||||
AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
|
||||
function() return EnsureDB().width end,
|
||||
function(v) EnsureDB().width = v end,
|
||||
@@ -3580,13 +3785,18 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
|
||||
function() SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
AddControl(CreateCfgCheck(appearance, "悬停显示背景", 16, -248,
|
||||
function() return EnsureDB().hoverTransparent ~= false end,
|
||||
function(checked) EnsureDB().hoverTransparent = (checked == true) end,
|
||||
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
|
||||
self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY")
|
||||
self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
|
||||
self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
|
||||
self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8)
|
||||
|
||||
local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath)
|
||||
local inputSection = CreateCfgSection(windowPage, "输入框", 0, -320, 584, 114, fontPath)
|
||||
self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY")
|
||||
self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
|
||||
self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
|
||||
@@ -3618,7 +3828,7 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。")
|
||||
inputTip:SetTextColor(0.74, 0.74, 0.8)
|
||||
|
||||
local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath)
|
||||
local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -428, 584, 96, fontPath)
|
||||
CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
|
||||
SFrames.Chat:ResetPosition()
|
||||
SFrames.Chat:RefreshConfigFrame()
|
||||
@@ -4050,7 +4260,7 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
|
||||
local hcPage = CreatePage("hc")
|
||||
do
|
||||
local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath)
|
||||
local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 382, fontPath)
|
||||
|
||||
local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
|
||||
hcStatusText:SetFont(fontPath, 10, "OUTLINE")
|
||||
@@ -4095,7 +4305,7 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
|
||||
deathTip:SetWidth(540)
|
||||
deathTip:SetJustifyH("LEFT")
|
||||
deathTip:SetText("关闭那些“某某在XX级死亡”的系统提示。")
|
||||
deathTip:SetText("关闭那些[某某在XX级死亡]的系统提示。")
|
||||
deathTip:SetTextColor(0.8, 0.7, 0.7)
|
||||
|
||||
AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1,
|
||||
@@ -4104,6 +4314,48 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
|
||||
function() SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
|
||||
AddControl(CreateCfgCheck(hcControls, "翻译死亡通报并转发到公会频道", 16, -138,
|
||||
function() return EnsureDB().hcDeathToGuild ~= false end,
|
||||
function(checked) EnsureDB().hcDeathToGuild = (checked == true) end,
|
||||
function() SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
|
||||
local guildTip = hcControls:CreateFontString(nil, "OVERLAY")
|
||||
guildTip:SetFont(fontPath, 10, "OUTLINE")
|
||||
guildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -164)
|
||||
guildTip:SetWidth(540)
|
||||
guildTip:SetJustifyH("LEFT")
|
||||
guildTip:SetText("开启 AI 翻译时,将死亡黄字系统消息翻译后自动发送到公会频道。需 AI 翻译已启用。")
|
||||
guildTip:SetTextColor(0.8, 0.7, 0.7)
|
||||
|
||||
AddControl(CreateCfgCheck(hcControls, "翻译等级里程碑并转发到公会频道", 16, -204,
|
||||
function() return EnsureDB().hcLevelToGuild ~= false end,
|
||||
function(checked) EnsureDB().hcLevelToGuild = (checked == true) end,
|
||||
function() SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
|
||||
local levelGuildTip = hcControls:CreateFontString(nil, "OVERLAY")
|
||||
levelGuildTip:SetFont(fontPath, 10, "OUTLINE")
|
||||
levelGuildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -228)
|
||||
levelGuildTip:SetWidth(540)
|
||||
levelGuildTip:SetJustifyH("LEFT")
|
||||
levelGuildTip:SetText("开启 AI 翻译时,将[达到X级]里程碑系统消息翻译后转发到公会频道。与死亡通报独立控制。")
|
||||
levelGuildTip:SetTextColor(0.8, 0.7, 0.7)
|
||||
|
||||
AddControl(CreateCfgCheck(hcControls, "仅通报工会成员的死亡/升级消息", 16, -270,
|
||||
function() return EnsureDB().hcGuildMemberOnly == true end,
|
||||
function(checked) EnsureDB().hcGuildMemberOnly = (checked == true) end,
|
||||
function() SFrames.Chat:RefreshConfigFrame() end
|
||||
))
|
||||
|
||||
local guildMemberTip = hcControls:CreateFontString(nil, "OVERLAY")
|
||||
guildMemberTip:SetFont(fontPath, 10, "OUTLINE")
|
||||
guildMemberTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -296)
|
||||
guildMemberTip:SetWidth(540)
|
||||
guildMemberTip:SetJustifyH("LEFT")
|
||||
guildMemberTip:SetText("勾选后,仅当死亡/升级角色为本工会成员时才转发到公会频道。默认关闭(通报所有人)。")
|
||||
guildMemberTip:SetTextColor(0.8, 0.7, 0.7)
|
||||
end
|
||||
|
||||
local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function()
|
||||
@@ -4399,6 +4651,7 @@ function SFrames.Chat:CreateContainer()
|
||||
})
|
||||
chatShadow:SetBackdropColor(0, 0, 0, 0.55)
|
||||
chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
|
||||
f.chatShadow = chatShadow
|
||||
|
||||
local topGlow = f:CreateTexture(nil, "BACKGROUND")
|
||||
topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||||
@@ -4674,6 +4927,9 @@ function SFrames.Chat:CreateContainer()
|
||||
scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
|
||||
scrollTrack:SetWidth(4)
|
||||
scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9)
|
||||
f.scrollUpBtn = scrollUpBtn
|
||||
f.scrollDownBtn = scrollDownBtn
|
||||
f.scrollTrack = scrollTrack
|
||||
|
||||
local resize = CreateFrame("Button", nil, f)
|
||||
resize:SetWidth(16)
|
||||
@@ -4709,6 +4965,67 @@ function SFrames.Chat:CreateContainer()
|
||||
f.resizeHandle = resize
|
||||
self.frame = f
|
||||
|
||||
-- ── Hover-transparent: fade background/chrome when mouse is not over chat ──
|
||||
f.sfHoverAlpha = 0
|
||||
f.sfHoverTarget = 0
|
||||
local FADE_SPEED = 4.0 -- alpha per second
|
||||
-- Apply initial transparent state immediately after first config apply
|
||||
f.sfHoverInitPending = true
|
||||
|
||||
local function IsMouseOverChat()
|
||||
if MouseIsOver(f) then return true end
|
||||
if SFrames.Chat.editBackdrop and SFrames.Chat.editBackdrop:IsShown() and MouseIsOver(SFrames.Chat.editBackdrop) then return true end
|
||||
if SFrames.Chat.configFrame and SFrames.Chat.configFrame:IsShown() then return true end
|
||||
local editBox = ChatFrameEditBox or ChatFrame1EditBox
|
||||
if editBox and editBox:IsShown() then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
local hoverFrame = CreateFrame("Frame", nil, f)
|
||||
hoverFrame:SetScript("OnUpdate", function()
|
||||
if not (SFrames and SFrames.Chat and SFrames.Chat.frame) then return end
|
||||
local cfg = SFrames.Chat:GetConfig()
|
||||
-- On first tick, snap to correct state (no animation)
|
||||
if f.sfHoverInitPending then
|
||||
f.sfHoverInitPending = nil
|
||||
if cfg.hoverTransparent then
|
||||
local over = IsMouseOverChat() and 1 or 0
|
||||
f.sfHoverAlpha = over
|
||||
f.sfHoverTarget = over
|
||||
SFrames.Chat:ApplyHoverAlpha(over)
|
||||
else
|
||||
f.sfHoverAlpha = 1
|
||||
f.sfHoverTarget = 1
|
||||
end
|
||||
return
|
||||
end
|
||||
if not cfg.hoverTransparent then
|
||||
if f.sfHoverAlpha ~= 1 then
|
||||
f.sfHoverAlpha = 1
|
||||
SFrames.Chat:ApplyHoverAlpha(1)
|
||||
end
|
||||
return
|
||||
end
|
||||
local target = IsMouseOverChat() and 1 or 0
|
||||
f.sfHoverTarget = target
|
||||
local cur = f.sfHoverAlpha or 1
|
||||
if math.abs(cur - target) < 0.01 then
|
||||
if cur ~= target then
|
||||
f.sfHoverAlpha = target
|
||||
SFrames.Chat:ApplyHoverAlpha(target)
|
||||
end
|
||||
return
|
||||
end
|
||||
local dt = arg1 or 0.016
|
||||
if target > cur then
|
||||
cur = math.min(cur + FADE_SPEED * dt, target)
|
||||
else
|
||||
cur = math.max(cur - FADE_SPEED * dt, target)
|
||||
end
|
||||
f.sfHoverAlpha = cur
|
||||
SFrames.Chat:ApplyHoverAlpha(cur)
|
||||
end)
|
||||
|
||||
if not self.hiddenConfigButton then
|
||||
local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
|
||||
hiddenConfigButton:SetWidth(74)
|
||||
@@ -4749,9 +5066,45 @@ function SFrames.Chat:CreateContainer()
|
||||
f:SetWidth(Clamp(db.width, 320, 900))
|
||||
f:SetHeight(Clamp(db.height, 120, 460))
|
||||
|
||||
-- Background alpha: always show at configured bgAlpha
|
||||
-- Background alpha: respect hoverTransparent on init
|
||||
local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
|
||||
if db.hoverTransparent ~= false then
|
||||
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0)
|
||||
f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0)
|
||||
if f.chatShadow then
|
||||
f.chatShadow:SetBackdropColor(0, 0, 0, 0)
|
||||
f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0)
|
||||
end
|
||||
if f.innerShade then
|
||||
f.innerShade:SetVertexColor(0, 0, 0, 0)
|
||||
f.innerShade:Hide()
|
||||
end
|
||||
if f.watermark then
|
||||
f.watermark:SetVertexColor(1, 0.78, 0.9, 0)
|
||||
f.watermark:Hide()
|
||||
end
|
||||
if f.title and f.title.SetAlpha then f.title:SetAlpha(0)
|
||||
elseif f.title and f.title.SetTextColor then f.title:SetTextColor(1, 0.82, 0.93, 0) end
|
||||
if f.title then f.title:Hide() end
|
||||
if f.titleBtn then
|
||||
f.titleBtn:EnableMouse(false)
|
||||
f.titleBtn:Hide()
|
||||
end
|
||||
if f.leftCat then
|
||||
f.leftCat:SetVertexColor(1, 0.82, 0.9, 0)
|
||||
f.leftCat:Hide()
|
||||
end
|
||||
if f.tabBar then f.tabBar:SetAlpha(0) f.tabBar:Hide() end
|
||||
if f.configButton then f.configButton:SetAlpha(0) f.configButton:EnableMouse(false) f.configButton:Hide() end
|
||||
if f.whisperButton then f.whisperButton:SetAlpha(0) f.whisperButton:EnableMouse(false) f.whisperButton:Hide() end
|
||||
if f.scrollUpBtn then f.scrollUpBtn:SetAlpha(0) f.scrollUpBtn:EnableMouse(false) f.scrollUpBtn:Hide() end
|
||||
if f.scrollDownBtn then f.scrollDownBtn:SetAlpha(0) f.scrollDownBtn:EnableMouse(false) f.scrollDownBtn:Hide() end
|
||||
if f.scrollTrack then f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0) f.scrollTrack:Hide() end
|
||||
if f.resizeHandle then f.resizeHandle:SetAlpha(0) f.resizeHandle:EnableMouse(false) f.resizeHandle:Hide() end
|
||||
if f.hint then f.hint:Hide() end
|
||||
else
|
||||
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Chat:HideDefaultChrome()
|
||||
@@ -5225,8 +5578,15 @@ function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
|
||||
end
|
||||
if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
|
||||
if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
|
||||
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end
|
||||
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end
|
||||
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(0, 0) end
|
||||
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0) end
|
||||
StripChatFrameArtwork(chatFrame)
|
||||
if not chatFrame.sfArtworkHooked and chatFrame.HookScript then
|
||||
chatFrame.sfArtworkHooked = true
|
||||
chatFrame:HookScript("OnShow", function()
|
||||
StripChatFrameArtwork(chatFrame)
|
||||
end)
|
||||
end
|
||||
|
||||
self:EnforceChatWindowLock(chatFrame)
|
||||
if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
|
||||
@@ -6324,6 +6684,122 @@ function SFrames.Chat:StyleEditBox()
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply hover-transparent alpha to background, shadow, and chrome elements.
|
||||
-- alpha=0 means fully transparent (mouse away), alpha=1 means fully visible (mouse over).
|
||||
function SFrames.Chat:ApplyHoverAlpha(alpha)
|
||||
if not self.frame then return end
|
||||
local f = self.frame
|
||||
local cfg = self:GetConfig()
|
||||
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
|
||||
local chromeVisible = alpha > 0.01
|
||||
|
||||
-- Main backdrop
|
||||
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * alpha)
|
||||
local showBorder = (cfg.showBorder ~= false)
|
||||
local borderR, borderG, borderB = self:GetBorderColorRGB()
|
||||
if showBorder then
|
||||
f:SetBackdropBorderColor(borderR, borderG, borderB, 0.95 * alpha)
|
||||
else
|
||||
f:SetBackdropBorderColor(borderR, borderG, borderB, 0)
|
||||
end
|
||||
|
||||
-- Shadow
|
||||
if f.chatShadow then
|
||||
f.chatShadow:SetBackdropColor(0, 0, 0, 0.55 * alpha)
|
||||
f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4 * alpha)
|
||||
end
|
||||
|
||||
-- Inner shade
|
||||
if f.innerShade then
|
||||
f.innerShade:SetVertexColor(0, 0, 0, 0.2 * alpha)
|
||||
if chromeVisible then f.innerShade:Show() else f.innerShade:Hide() end
|
||||
end
|
||||
|
||||
-- Watermark
|
||||
if f.watermark then
|
||||
f.watermark:SetVertexColor(1, 0.78, 0.9, 0.08 * alpha)
|
||||
if chromeVisible then f.watermark:Show() else f.watermark:Hide() end
|
||||
end
|
||||
|
||||
-- Title, tab bar, config button, whisper button, scroll buttons, resize handle
|
||||
local chromeAlpha = alpha
|
||||
if f.title and f.title.SetAlpha then
|
||||
f.title:SetAlpha(chromeAlpha)
|
||||
if chromeVisible then f.title:Show() else f.title:Hide() end
|
||||
elseif f.title and f.title.SetTextColor then
|
||||
f.title:SetTextColor(1, 0.82, 0.93, chromeAlpha)
|
||||
if chromeVisible then f.title:Show() else f.title:Hide() end
|
||||
end
|
||||
if f.titleBtn then
|
||||
f.titleBtn:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.titleBtn:Show() else f.titleBtn:Hide() end
|
||||
end
|
||||
if f.leftCat then
|
||||
f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8 * chromeAlpha)
|
||||
if chromeVisible then f.leftCat:Show() else f.leftCat:Hide() end
|
||||
end
|
||||
if f.tabBar then
|
||||
f.tabBar:SetAlpha(chromeAlpha)
|
||||
if chromeVisible then f.tabBar:Show() else f.tabBar:Hide() end
|
||||
end
|
||||
if f.configButton then
|
||||
f.configButton:SetAlpha(chromeAlpha)
|
||||
f.configButton:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.configButton:Show() else f.configButton:Hide() end
|
||||
end
|
||||
if f.whisperButton then
|
||||
f.whisperButton:SetAlpha(chromeAlpha)
|
||||
f.whisperButton:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.whisperButton:Show() else f.whisperButton:Hide() end
|
||||
end
|
||||
if f.scrollUpBtn then
|
||||
f.scrollUpBtn:SetAlpha(chromeAlpha)
|
||||
f.scrollUpBtn:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.scrollUpBtn:Show() else f.scrollUpBtn:Hide() end
|
||||
end
|
||||
if f.scrollDownBtn then
|
||||
f.scrollDownBtn:SetAlpha(chromeAlpha)
|
||||
f.scrollDownBtn:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.scrollDownBtn:Show() else f.scrollDownBtn:Hide() end
|
||||
end
|
||||
if f.scrollTrack then
|
||||
f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9 * chromeAlpha)
|
||||
if chromeVisible then f.scrollTrack:Show() else f.scrollTrack:Hide() end
|
||||
end
|
||||
if f.resizeHandle then
|
||||
f.resizeHandle:SetAlpha(chromeAlpha)
|
||||
f.resizeHandle:EnableMouse(chromeVisible)
|
||||
if chromeVisible then f.resizeHandle:Show() else f.resizeHandle:Hide() end
|
||||
end
|
||||
if f.hint then
|
||||
if chromeVisible then f.hint:Show() else f.hint:Hide() end
|
||||
end
|
||||
|
||||
-- Edit backdrop (only backdrop colors, not child alpha — preserve editbox text visibility)
|
||||
if self.editBackdrop then
|
||||
local editBox = ChatFrameEditBox or ChatFrame1EditBox
|
||||
local editBackdropVisible = chromeVisible and editBox and editBox:IsShown()
|
||||
if self.editBackdrop.SetBackdropColor then
|
||||
self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96 * chromeAlpha)
|
||||
end
|
||||
if self.editBackdrop.SetBackdropBorderColor then
|
||||
self.editBackdrop:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98 * chromeAlpha)
|
||||
end
|
||||
if self.editBackdrop.catIcon then
|
||||
self.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9 * chromeAlpha)
|
||||
end
|
||||
if self.editBackdrop.topLine then
|
||||
self.editBackdrop.topLine:SetVertexColor(1, 0.76, 0.9, 0.85 * chromeAlpha)
|
||||
end
|
||||
self.editBackdrop:EnableMouse(editBackdropVisible)
|
||||
if editBackdropVisible then
|
||||
self.editBackdrop:Show()
|
||||
else
|
||||
self.editBackdrop:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Chat:ApplyFrameBorderStyle()
|
||||
if not self.frame then return end
|
||||
|
||||
@@ -6413,7 +6889,12 @@ function SFrames.Chat:ApplyConfig()
|
||||
end
|
||||
|
||||
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
|
||||
local hAlpha = (self.frame.sfHoverAlpha ~= nil) and self.frame.sfHoverAlpha or 1
|
||||
if cfg.hoverTransparent then
|
||||
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * hAlpha)
|
||||
else
|
||||
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
|
||||
end
|
||||
|
||||
self:AttachChatFrame()
|
||||
self:HideDefaultChrome()
|
||||
@@ -6427,6 +6908,11 @@ function SFrames.Chat:ApplyConfig()
|
||||
self:StartStabilizer()
|
||||
self:SetUnlocked(SFrames and SFrames.isUnlocked)
|
||||
self:RefreshConfigFrame()
|
||||
|
||||
-- Re-apply hover alpha so ApplyConfig doesn't leave chrome visible
|
||||
if cfg.hoverTransparent and self.frame.sfHoverAlpha ~= nil then
|
||||
self:ApplyHoverAlpha(self.frame.sfHoverAlpha)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Chat:Initialize()
|
||||
@@ -6524,6 +7010,61 @@ function SFrames.Chat:Initialize()
|
||||
end)
|
||||
end
|
||||
|
||||
-- HC死亡系统消息:AI开启时翻译并转发到公会
|
||||
if not SFrames.Chat._hcDeathGuildHooked then
|
||||
SFrames.Chat._hcDeathGuildHooked = true
|
||||
local hcDeathEvFrame = CreateFrame("Frame", "SFramesChatHCDeathGuildEvents", UIParent)
|
||||
hcDeathEvFrame:RegisterEvent("CHAT_MSG_SYSTEM")
|
||||
hcDeathEvFrame:SetScript("OnEvent", function()
|
||||
if not (SFrames and SFrames.Chat) then return end
|
||||
local db = EnsureDB()
|
||||
-- AI翻译必须开启
|
||||
if db.translateEnabled == false then return end
|
||||
-- 必须在公会中才能发送
|
||||
if not (IsInGuild and IsInGuild()) then return end
|
||||
local messageText = arg1
|
||||
if type(messageText) ~= "string" or messageText == "" then return end
|
||||
|
||||
local isDeath = ParseHardcoreDeathMessage(messageText) ~= nil
|
||||
local isLevel = (not isDeath) and (ParseHardcoreLevelMessage(messageText) ~= nil)
|
||||
if not isDeath and not isLevel then return end
|
||||
|
||||
-- 仅通报工会成员:提取角色名并检查缓存
|
||||
if db.hcGuildMemberOnly then
|
||||
local charName = ParseHCCharacterName(messageText)
|
||||
if not charName or not IsHCGuildMember(charName) then return end
|
||||
end
|
||||
|
||||
-- 死亡通报:检查 hcDeathToGuild 及等级过滤
|
||||
if isDeath then
|
||||
if db.hcDeathToGuild == false then return end
|
||||
if db.hcDeathDisable then return end
|
||||
local deathLvl = ParseHardcoreDeathMessage(messageText)
|
||||
if db.hcDeathLevelMin and deathLvl and deathLvl > 0 and deathLvl < db.hcDeathLevelMin then return end
|
||||
end
|
||||
|
||||
-- 等级里程碑:检查 hcLevelToGuild
|
||||
if isLevel then
|
||||
if db.hcLevelToGuild == false then return end
|
||||
end
|
||||
|
||||
local cleanText = CleanTextForTranslation(messageText)
|
||||
if cleanText == "" then return end
|
||||
local function doSend(finalText)
|
||||
pcall(function() SendChatMessage("[HC] " .. finalText, "GUILD") end)
|
||||
end
|
||||
if isDeath then
|
||||
TranslateHCDeathParts(cleanText, doSend)
|
||||
else
|
||||
TranslateHCLevelParts(cleanText, doSend)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function()
|
||||
RefreshHCGuildCache()
|
||||
end)
|
||||
|
||||
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
|
||||
if SFrames and SFrames.Chat then
|
||||
SFrames.Chat:ApplyConfig()
|
||||
@@ -6532,7 +7073,7 @@ function SFrames.Chat:Initialize()
|
||||
end
|
||||
LoadPersistentClassCache()
|
||||
if IsInGuild and IsInGuild() and GuildRoster then
|
||||
GuildRoster()
|
||||
GuildRoster() -- 触发 GUILD_ROSTER_UPDATE → RefreshHCGuildCache
|
||||
end
|
||||
SFrames:RefreshClassColorCache()
|
||||
|
||||
@@ -6714,6 +7255,17 @@ function SFrames.Chat:Initialize()
|
||||
|
||||
do
|
||||
local db = EnsureDB()
|
||||
|
||||
-- HC系统消息(死亡/里程碑黄字)特殊处理:
|
||||
-- AI翻译未开启时直接透传,不加 [+] 注释;
|
||||
-- AI开启时正常记录,翻译转发由独立的 CHAT_MSG_SYSTEM 事件处理器完成。
|
||||
if ParseHardcoreDeathMessage(text) or ParseHardcoreLevelMessage(text) then
|
||||
if db.translateEnabled == false then
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local chanName = GetChannelNameFromChatLine(text)
|
||||
|
||||
if chanName and IsIgnoredChannelByDefault(chanName) then
|
||||
@@ -7137,4 +7689,3 @@ end
|
||||
end
|
||||
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
|
||||
SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor)
|
||||
|
||||
|
||||
@@ -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] = "史诗坐骑任务:召唤战驹",
|
||||
},
|
||||
}
|
||||
1499
ConfigUI.lua
1499
ConfigUI.lua
File diff suppressed because it is too large
Load Diff
198
ConsumableDB.lua
Normal file
198
ConsumableDB.lua
Normal 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
778
ConsumableUI.lua
Normal 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
164
Core.lua
@@ -32,6 +32,40 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- 保护 ComboFrame,防止 fadeInfo 为 nil 导致的报错
|
||||
-- (ComboFrame.lua:46: attempt to index local 'fadeInfo' (a nil value))
|
||||
if ComboFrame then
|
||||
if not ComboFrame.fadeInfo then
|
||||
ComboFrame.fadeInfo = {}
|
||||
end
|
||||
local origComboScript = ComboFrame.GetScript and ComboFrame:GetScript("OnUpdate")
|
||||
if origComboScript then
|
||||
ComboFrame:SetScript("OnUpdate", function(elapsed)
|
||||
if ComboFrame.fadeInfo then
|
||||
origComboScript(elapsed)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
if ComboFrame_Update then
|
||||
local _orig_ComboUpdate = ComboFrame_Update
|
||||
ComboFrame_Update = function()
|
||||
if ComboFrame and not ComboFrame.fadeInfo then
|
||||
ComboFrame.fadeInfo = {}
|
||||
end
|
||||
return _orig_ComboUpdate()
|
||||
end
|
||||
end
|
||||
if ComboFrame_OnUpdate then
|
||||
local _orig_ComboOnUpdate = ComboFrame_OnUpdate
|
||||
ComboFrame_OnUpdate = function(elapsed)
|
||||
if ComboFrame and not ComboFrame.fadeInfo then
|
||||
ComboFrame.fadeInfo = {}
|
||||
end
|
||||
return _orig_ComboOnUpdate(elapsed)
|
||||
end
|
||||
end
|
||||
|
||||
local origOnUpdate = UIParent and UIParent.GetScript and UIParent:GetScript("OnUpdate")
|
||||
if origOnUpdate then
|
||||
UIParent:SetScript("OnUpdate", function()
|
||||
@@ -43,6 +77,11 @@ end
|
||||
BINDING_HEADER_NANAMI_UI = "Nanami-UI"
|
||||
BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图"
|
||||
|
||||
BINDING_HEADER_NANAMI_EXTRABAR = "Nanami-UI 额外动作条"
|
||||
for _i = 1, 48 do
|
||||
_G["BINDING_NAME_NANAMI_EXTRABAR" .. _i] = "额外动作条 按钮" .. _i
|
||||
end
|
||||
|
||||
SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
|
||||
SFrames.events = {}
|
||||
|
||||
@@ -140,6 +179,52 @@ function SFrames:Print(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg))
|
||||
end
|
||||
|
||||
local function IsBattlefieldMinimapVisible()
|
||||
if BattlefieldMinimap and BattlefieldMinimap.IsVisible and BattlefieldMinimap:IsVisible() then
|
||||
return true
|
||||
end
|
||||
if BattlefieldMinimapFrame and BattlefieldMinimapFrame ~= BattlefieldMinimap
|
||||
and BattlefieldMinimapFrame.IsVisible and BattlefieldMinimapFrame:IsVisible() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function SFrames:CaptureBattlefieldMinimapState()
|
||||
return IsBattlefieldMinimapVisible()
|
||||
end
|
||||
|
||||
function SFrames:RestoreBattlefieldMinimapState(wasVisible)
|
||||
if wasVisible then
|
||||
return
|
||||
end
|
||||
|
||||
local frames = { BattlefieldMinimap, BattlefieldMinimapFrame }
|
||||
local hidden = {}
|
||||
for i = 1, table.getn(frames) do
|
||||
local frame = frames[i]
|
||||
if frame and not hidden[frame] and frame.Hide and frame.IsVisible and frame:IsVisible() then
|
||||
hidden[frame] = true
|
||||
pcall(frame.Hide, frame)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames:CallWithPreservedBattlefieldMinimap(func, a1, a2, a3, a4, a5, a6, a7, a8)
|
||||
if type(func) ~= "function" then
|
||||
return
|
||||
end
|
||||
|
||||
local state = self:CaptureBattlefieldMinimapState()
|
||||
local results = { pcall(func, a1, a2, a3, a4, a5, a6, a7, a8) }
|
||||
self:RestoreBattlefieldMinimapState(state)
|
||||
|
||||
if not results[1] then
|
||||
return nil, results[2]
|
||||
end
|
||||
return unpack(results, 2, table.getn(results))
|
||||
end
|
||||
|
||||
-- Addon Loaded Initializer
|
||||
SFrames:RegisterEvent("PLAYER_LOGIN", function()
|
||||
SFrames:Initialize()
|
||||
@@ -299,6 +384,10 @@ function SFrames:DoFullInitialize()
|
||||
SFrames.Tooltip:SetAlpha(0)
|
||||
SFrames.Tooltip:Hide()
|
||||
|
||||
if SFrames.AuraTracker and SFrames.AuraTracker.Initialize then
|
||||
SFrames.AuraTracker:Initialize()
|
||||
end
|
||||
|
||||
-- Phase 1: Critical modules (unit frames, action bars) — must load immediately
|
||||
if SFramesDB.enableUnitFrames ~= false then
|
||||
if SFramesDB.enablePlayerFrame ~= false then
|
||||
@@ -319,6 +408,10 @@ function SFrames:DoFullInitialize()
|
||||
SFrames.ActionBars:Initialize()
|
||||
end
|
||||
|
||||
if SFrames.ExtraBar and SFrames.ExtraBar.Initialize then
|
||||
SFrames.ExtraBar:Initialize()
|
||||
end
|
||||
|
||||
self:InitSlashCommands()
|
||||
|
||||
-- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike
|
||||
@@ -375,6 +468,13 @@ function SFrames:GetAuraTimeLeft(unit, index, isBuff)
|
||||
end
|
||||
end
|
||||
|
||||
if SFrames.AuraTracker and SFrames.AuraTracker.GetAuraTimeLeft then
|
||||
local trackerTime = SFrames.AuraTracker:GetAuraTimeLeft(unit, isBuff and "buff" or "debuff", index)
|
||||
if trackerTime and trackerTime > 0 then
|
||||
return trackerTime
|
||||
end
|
||||
end
|
||||
|
||||
-- Nanami-Plates SpellDB: combat log + spell DB tracking (most accurate for debuffs)
|
||||
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
|
||||
local effect, rank, tex, stacks, dtype, duration, timeleft, isOwn = NanamiPlates_SpellDB:UnitDebuff(unit, index)
|
||||
@@ -420,6 +520,64 @@ function SFrames:FormatTime(seconds)
|
||||
end
|
||||
end
|
||||
|
||||
local POWER_RAINBOW_TEX = "Interface\\AddOns\\Nanami-UI\\img\\progress"
|
||||
|
||||
function SFrames:UpdateRainbowBar(bar, power, maxPower, unit)
|
||||
-- 彩虹条仅适用于法力(powerType 0),怒气/能量等跳过
|
||||
if unit and UnitPowerType(unit) ~= 0 then
|
||||
if bar._rainbowActive then
|
||||
if bar.rainbowTex then bar.rainbowTex:Hide() end
|
||||
bar._rainbowActive = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
if not (SFramesDB and SFramesDB.powerRainbow) then
|
||||
if bar._rainbowActive then
|
||||
if bar.rainbowTex then bar.rainbowTex:Hide() end
|
||||
bar._rainbowActive = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
if not bar.rainbowTex then
|
||||
bar.rainbowTex = bar:CreateTexture(nil, "OVERLAY")
|
||||
bar.rainbowTex:SetTexture(POWER_RAINBOW_TEX)
|
||||
bar.rainbowTex:Hide()
|
||||
end
|
||||
if maxPower and maxPower > 0 then
|
||||
local pct = power / maxPower
|
||||
if pct >= 1.0 then
|
||||
-- 满条:直接铺满,不依赖 GetWidth()(两锚点定尺寸的框体 GetWidth 可能返回 0)
|
||||
bar.rainbowTex:ClearAllPoints()
|
||||
bar.rainbowTex:SetAllPoints(bar)
|
||||
bar.rainbowTex:SetTexCoord(0, 1, 0, 1)
|
||||
bar.rainbowTex:Show()
|
||||
bar._rainbowActive = true
|
||||
else
|
||||
local barW = bar:GetWidth()
|
||||
-- 双锚点定尺寸的框体(如宠物能量条)GetWidth() 可能返回 0
|
||||
-- 回退到 GetRight()-GetLeft() 获取实际渲染宽度
|
||||
if not barW or barW <= 0 then
|
||||
local left = bar:GetLeft()
|
||||
local right = bar:GetRight()
|
||||
if left and right then
|
||||
barW = right - left
|
||||
end
|
||||
end
|
||||
if barW and barW > 0 then
|
||||
bar.rainbowTex:ClearAllPoints()
|
||||
bar.rainbowTex:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
|
||||
bar.rainbowTex:SetPoint("BOTTOMRIGHT", bar, "BOTTOMLEFT", barW * pct, 0)
|
||||
bar.rainbowTex:SetTexCoord(0, pct, 0, 1)
|
||||
bar.rainbowTex:Show()
|
||||
bar._rainbowActive = true
|
||||
end
|
||||
end
|
||||
else
|
||||
bar.rainbowTex:Hide()
|
||||
bar._rainbowActive = nil
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames:InitSlashCommands()
|
||||
DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.")
|
||||
SLASH_SFRAMES1 = "/nanami"
|
||||
@@ -808,12 +966,16 @@ function SFrames:HideBlizzardFrames()
|
||||
end
|
||||
if ComboFrame then
|
||||
ComboFrame:UnregisterAllEvents()
|
||||
ComboFrame:SetScript("OnUpdate", nil)
|
||||
ComboFrame:Hide()
|
||||
ComboFrame.Show = function() end
|
||||
ComboFrame.fadeInfo = ComboFrame.fadeInfo or {}
|
||||
ComboFrame.fadeInfo = {}
|
||||
if ComboFrame_Update then
|
||||
ComboFrame_Update = function() end
|
||||
end
|
||||
if ComboFrame_OnUpdate then
|
||||
ComboFrame_OnUpdate = function() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
792
ExtraBar.lua
Normal file
792
ExtraBar.lua
Normal 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
|
||||
139
Factory.lua
139
Factory.lua
@@ -1,19 +1,48 @@
|
||||
-- Helper function to generate ElvUI-style backdrop and shadow border
|
||||
function SFrames:CreateBackdrop(frame)
|
||||
function SFrames:ApplyBackdropStyle(frame, opts)
|
||||
opts = opts or {}
|
||||
local radius = tonumber(opts.cornerRadius) or 0
|
||||
local showBorder = opts.showBorder ~= false
|
||||
local useRounded = radius and radius > 0
|
||||
|
||||
if useRounded then
|
||||
local edgeSize = math.max(8, math.min(18, math.floor(radius + 0.5)))
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = showBorder and "Interface\\Tooltips\\UI-Tooltip-Border" or nil,
|
||||
tile = true, tileSize = 16, edgeSize = edgeSize,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
||||
})
|
||||
else
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = showBorder and "Interface\\Buttons\\WHITE8X8" or nil,
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 }
|
||||
})
|
||||
end
|
||||
|
||||
local A = SFrames.ActiveTheme
|
||||
local bgAlpha = opts.bgAlpha
|
||||
if A and A.panelBg then
|
||||
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
|
||||
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:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
|
||||
frame:SetBackdropBorderColor(0, 0, 0, 1)
|
||||
frame:SetBackdropBorderColor(0, 0, 0, 0)
|
||||
end
|
||||
else
|
||||
frame:SetBackdropColor(0.1, 0.1, 0.1, bgAlpha or 0.9)
|
||||
if showBorder then
|
||||
frame:SetBackdropBorderColor(0, 0, 0, 1)
|
||||
else
|
||||
frame:SetBackdropBorderColor(0, 0, 0, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames:CreateBackdrop(frame)
|
||||
self:ApplyBackdropStyle(frame)
|
||||
end
|
||||
|
||||
function SFrames:CreateRoundBackdrop(frame)
|
||||
@@ -38,6 +67,78 @@ function SFrames:CreateUnitBackdrop(frame)
|
||||
frame:SetBackdropBorderColor(0, 0, 0, 1)
|
||||
end
|
||||
|
||||
function SFrames:ApplyConfiguredUnitBackdrop(frame, prefix, isPortrait)
|
||||
if not frame then return end
|
||||
local db = SFramesDB or {}
|
||||
local bgAlphaKey = prefix .. (isPortrait and "PortraitBgAlpha" or "BgAlpha")
|
||||
self:ApplyBackdropStyle(frame, {
|
||||
showBorder = false,
|
||||
cornerRadius = 0,
|
||||
bgAlpha = tonumber(db[bgAlphaKey]) or (isPortrait and tonumber(db[prefix .. "BgAlpha"]) or nil),
|
||||
})
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Frame Style Preset helpers
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function SFrames:GetFrameStylePreset()
|
||||
return (SFramesDB and SFramesDB.frameStylePreset) or "classic"
|
||||
end
|
||||
|
||||
function SFrames:IsGradientStyle()
|
||||
return self:GetFrameStylePreset() == "gradient"
|
||||
end
|
||||
|
||||
-- Apply a gradient darkening overlay on a StatusBar.
|
||||
-- Uses a separate Texture on OVERLAY layer with purple-black vertex color
|
||||
-- and alpha gradient from 0 (left) to ~0.6 (right).
|
||||
-- This approach does NOT touch the StatusBar texture itself,
|
||||
-- so it survives SetStatusBarColor calls.
|
||||
function SFrames:ApplyGradientStyle(bar)
|
||||
if not bar then return end
|
||||
if not bar._gradOverlay then
|
||||
local ov = bar:CreateTexture(nil, "OVERLAY")
|
||||
ov:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||||
bar._gradOverlay = ov
|
||||
end
|
||||
local ov = bar._gradOverlay
|
||||
ov:ClearAllPoints()
|
||||
ov:SetAllPoints(bar:GetStatusBarTexture())
|
||||
-- Dark purple-ish tint color
|
||||
ov:SetVertexColor(0.04, 0.0, 0.08, 1)
|
||||
-- Alpha gradient: left fully transparent → right 65% opaque
|
||||
if ov.SetGradientAlpha then
|
||||
ov:SetGradientAlpha("HORIZONTAL",
|
||||
1, 1, 1, 0,
|
||||
1, 1, 1, 0.65)
|
||||
end
|
||||
ov:Show()
|
||||
end
|
||||
|
||||
function SFrames:RemoveGradientStyle(bar)
|
||||
if not bar then return end
|
||||
if bar._gradOverlay then
|
||||
bar._gradOverlay:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- No-op wrappers kept for compatibility with unit files
|
||||
function SFrames:ApplyBarGradient(bar)
|
||||
-- Gradient is now persistent overlay; no per-color-update needed
|
||||
end
|
||||
function SFrames:ClearBarGradient(bar)
|
||||
self:RemoveGradientStyle(bar)
|
||||
end
|
||||
|
||||
-- Strip backdrop from a frame (used in gradient style)
|
||||
function SFrames:ClearBackdrop(frame)
|
||||
if not frame then return end
|
||||
if frame.SetBackdrop then
|
||||
frame:SetBackdrop(nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- Generator for StatusBars
|
||||
function SFrames:CreateStatusBar(parent, name)
|
||||
local bar = CreateFrame("StatusBar", name, parent)
|
||||
@@ -50,6 +151,11 @@ function SFrames:CreateStatusBar(parent, name)
|
||||
return bar
|
||||
end
|
||||
|
||||
function SFrames:ApplyStatusBarTexture(bar, settingKey, fallbackKey)
|
||||
if not bar or not bar.SetStatusBarTexture then return end
|
||||
bar:SetStatusBarTexture(self:ResolveBarTexture(settingKey, fallbackKey))
|
||||
end
|
||||
|
||||
-- Generator for FontStrings
|
||||
function SFrames:CreateFontString(parent, size, justifyH)
|
||||
local fs = parent:CreateFontString(nil, "OVERLAY")
|
||||
@@ -59,6 +165,29 @@ function SFrames:CreateFontString(parent, size, justifyH)
|
||||
return fs
|
||||
end
|
||||
|
||||
function SFrames:ApplyFontString(fs, size, fontSettingKey, fallbackFontKey, outlineSettingKey, fallbackOutlineKey)
|
||||
if not fs or not fs.SetFont then return end
|
||||
local fontPath = self:ResolveFont(fontSettingKey, fallbackFontKey)
|
||||
local outline = self:ResolveFontOutline(outlineSettingKey, fallbackOutlineKey)
|
||||
fs:SetFont(fontPath, size or 12, outline)
|
||||
end
|
||||
|
||||
function SFrames:FormatCompactNumber(value)
|
||||
local num = tonumber(value) or 0
|
||||
local sign = num < 0 and "-" or ""
|
||||
num = math.abs(num)
|
||||
if num >= 1000000 then
|
||||
return string.format("%s%.1fM", sign, num / 1000000)
|
||||
elseif num >= 10000 then
|
||||
return string.format("%s%.1fK", sign, num / 1000)
|
||||
end
|
||||
return sign .. tostring(math.floor(num + 0.5))
|
||||
end
|
||||
|
||||
function SFrames:FormatCompactPair(currentValue, maxValue)
|
||||
return self:FormatCompactNumber(currentValue) .. " / " .. self:FormatCompactNumber(maxValue)
|
||||
end
|
||||
|
||||
-- Generator for 3D Portraits
|
||||
function SFrames:CreatePortrait(parent, name)
|
||||
local portrait = CreateFrame("PlayerModel", name, parent)
|
||||
|
||||
226
Focus.lua
226
Focus.lua
@@ -327,17 +327,23 @@ function SFrames.Focus:CreateFocusFrame()
|
||||
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["FocusFrame"] then
|
||||
local pos = SFramesDB.Positions["FocusFrame"]
|
||||
-- Validate: if GetTop would be near 0 or negative, position is bad
|
||||
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0)
|
||||
-- After setting, check if visible on screen
|
||||
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
|
||||
(pos.xOfs or 250) / fScale, (pos.yOfs or 0) / fScale)
|
||||
else
|
||||
f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
|
||||
pos.xOfs or 250, pos.yOfs or 0)
|
||||
end
|
||||
local top = f:GetTop()
|
||||
local left = f:GetLeft()
|
||||
if not top or not left or top < 50 or left < 0 then
|
||||
-- Bad position, reset
|
||||
f:ClearAllPoints()
|
||||
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
|
||||
SFramesDB.Positions["FocusFrame"] = nil
|
||||
end
|
||||
elseif SFramesTargetFrame then
|
||||
f:SetPoint("TOPLEFT", SFramesTargetFrame, "BOTTOMLEFT", 0, -75)
|
||||
else
|
||||
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
|
||||
end
|
||||
@@ -352,6 +358,11 @@ function SFrames.Focus:CreateFocusFrame()
|
||||
if not SFramesDB then SFramesDB = {} end
|
||||
if not SFramesDB.Positions then SFramesDB.Positions = {} end
|
||||
local point, _, relativePoint, xOfs, yOfs = this:GetPoint()
|
||||
local fScale = this:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
xOfs = (xOfs or 0) * fScale
|
||||
yOfs = (yOfs or 0) * fScale
|
||||
end
|
||||
SFramesDB.Positions["FocusFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
|
||||
end)
|
||||
|
||||
@@ -546,37 +557,27 @@ function SFrames.Focus:CreateFocusFrame()
|
||||
|
||||
SFrames:CreateUnitBackdrop(f)
|
||||
|
||||
-- Portrait (right side) — EnableMouse(false) so clicks pass through to main Button
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
f.portrait = CreateFrame("PlayerModel", nil, f)
|
||||
-- Portrait placeholder (hidden, focus frame does not use 3D portraits)
|
||||
f.portrait = CreateFrame("Frame", nil, f)
|
||||
f.portrait:SetWidth(pWidth)
|
||||
f.portrait:SetHeight(totalH - 2)
|
||||
f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0)
|
||||
f.portrait:EnableMouse(false)
|
||||
f.portrait:Hide()
|
||||
|
||||
local pbg = CreateFrame("Frame", nil, f)
|
||||
pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0)
|
||||
pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
|
||||
pbg:SetFrameLevel(f:GetFrameLevel())
|
||||
SFrames:CreateUnitBackdrop(pbg)
|
||||
f.portraitBG = pbg
|
||||
pbg:EnableMouse(false)
|
||||
|
||||
if not showPortrait then
|
||||
f.portrait:Hide()
|
||||
pbg:Hide()
|
||||
end
|
||||
|
||||
-- Health bar
|
||||
-- Health bar (full width, no portrait)
|
||||
f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth")
|
||||
f.health:EnableMouse(false)
|
||||
if showPortrait then
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
||||
f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
|
||||
else
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
|
||||
end
|
||||
f.health:SetHeight(hHeight)
|
||||
|
||||
local hbg = CreateFrame("Frame", nil, f)
|
||||
@@ -596,11 +597,7 @@ function SFrames.Focus:CreateFocusFrame()
|
||||
f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower")
|
||||
f.power:EnableMouse(false)
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
|
||||
if showPortrait then
|
||||
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
|
||||
else
|
||||
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
|
||||
end
|
||||
|
||||
local powerbg = CreateFrame("Frame", nil, f)
|
||||
powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
|
||||
@@ -615,9 +612,9 @@ function SFrames.Focus:CreateFocusFrame()
|
||||
f.power.bg:SetTexture(SFrames:GetTexture())
|
||||
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
|
||||
|
||||
-- Class icon
|
||||
-- Class icon (anchored to frame top-right corner)
|
||||
f.classIcon = SFrames:CreateClassIcon(f, 14)
|
||||
f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)
|
||||
f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", 0, 0)
|
||||
f.classIcon.overlay:EnableMouse(false)
|
||||
|
||||
-- Texts
|
||||
@@ -747,12 +744,7 @@ function SFrames.Focus:CreateCastbar()
|
||||
local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar")
|
||||
cb:SetHeight(cbH)
|
||||
cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6)
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
if showPortrait then
|
||||
cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(cbH + 6), 6)
|
||||
else
|
||||
cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
|
||||
end
|
||||
|
||||
local cbbg = CreateFrame("Frame", nil, self.frame)
|
||||
cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
|
||||
@@ -816,7 +808,6 @@ function SFrames.Focus:UpdateAll()
|
||||
self.frame.power:SetMinMaxValues(0, 1)
|
||||
self.frame.power:SetValue(0)
|
||||
self.frame.powerText:SetText("")
|
||||
if self.frame.portrait then self.frame.portrait:Hide() end
|
||||
if self.frame.classIcon then self.frame.classIcon:Hide(); if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end
|
||||
self.frame.raidIcon:Hide()
|
||||
self:HideAuras()
|
||||
@@ -830,19 +821,6 @@ function SFrames.Focus:UpdateAll()
|
||||
self:UpdateRaidIcon()
|
||||
self:UpdateAuras()
|
||||
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
if showPortrait and self.frame.portrait then
|
||||
-- Only reset portrait model on first load / focus change (not every update)
|
||||
if not self.frame._lastPortraitUID or self.frame._lastPortraitUID ~= uid then
|
||||
self.frame.portrait:SetUnit(uid)
|
||||
self.frame.portrait:SetCamera(0)
|
||||
self.frame.portrait:Hide()
|
||||
self.frame.portrait:Show()
|
||||
self.frame.portrait:SetPosition(-1.0, 0, 0)
|
||||
self.frame._lastPortraitUID = uid
|
||||
end
|
||||
end
|
||||
|
||||
local name = UnitName(uid) or ""
|
||||
local level = UnitLevel(uid)
|
||||
local levelText = level
|
||||
@@ -888,6 +866,7 @@ function SFrames.Focus:UpdateAll()
|
||||
|
||||
-- Color by class or reaction
|
||||
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
|
||||
if SFrames:IsGradientStyle() then useClassColor = true end
|
||||
if UnitIsPlayer(uid) and useClassColor then
|
||||
local _, class = UnitClass(uid)
|
||||
if class and SFrames.Config.colors.class[class] then
|
||||
@@ -911,6 +890,9 @@ function SFrames.Focus:UpdateAll()
|
||||
self.frame.nameText:SetText(formattedLevel .. name)
|
||||
self.frame.nameText:SetTextColor(r, g, b)
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.health)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Focus:HideAuras()
|
||||
@@ -933,7 +915,7 @@ function SFrames.Focus:UpdateHealth()
|
||||
self.frame.health:SetValue(hp)
|
||||
if maxHp > 0 then
|
||||
local pct = math.floor(hp / maxHp * 100)
|
||||
self.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
|
||||
self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
|
||||
else
|
||||
self.frame.healthText:SetText("")
|
||||
end
|
||||
@@ -950,6 +932,9 @@ function SFrames.Focus:UpdatePowerType()
|
||||
else
|
||||
self.frame.power:SetStatusBarColor(0, 0, 1)
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.power)
|
||||
end
|
||||
end
|
||||
|
||||
-- STUB: UpdatePower
|
||||
@@ -961,10 +946,11 @@ function SFrames.Focus:UpdatePower()
|
||||
self.frame.power:SetMinMaxValues(0, maxPower)
|
||||
self.frame.power:SetValue(power)
|
||||
if maxPower > 0 then
|
||||
self.frame.powerText:SetText(power .. " / " .. maxPower)
|
||||
self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
|
||||
else
|
||||
self.frame.powerText:SetText("")
|
||||
end
|
||||
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, uid)
|
||||
end
|
||||
|
||||
-- STUB: UpdateRaidIcon
|
||||
@@ -1045,8 +1031,8 @@ function SFrames.Focus:CastbarOnUpdate()
|
||||
|
||||
-- 1) Native UnitCastingInfo / UnitChannelInfo (if available)
|
||||
if uid then
|
||||
local _UCI = UnitCastingInfo or (CastingInfo and function(u) return CastingInfo(u) end)
|
||||
local _UCH = UnitChannelInfo or (ChannelInfo and function(u) return ChannelInfo(u) end)
|
||||
local _UCI = UnitCastingInfo or CastingInfo
|
||||
local _UCH = UnitChannelInfo or ChannelInfo
|
||||
if _UCI then
|
||||
local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid)
|
||||
if ok and cSpell and cStart then
|
||||
@@ -1177,7 +1163,6 @@ function SFrames.Focus:OnFocusChanged()
|
||||
local name = self:GetFocusName()
|
||||
if name then
|
||||
self.frame:Show()
|
||||
self.frame._lastPortraitUID = nil -- Force portrait refresh on focus change
|
||||
self:UpdateAll()
|
||||
else
|
||||
self.frame:Hide()
|
||||
@@ -1206,9 +1191,28 @@ function SFrames.Focus:ApplySettings()
|
||||
local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9
|
||||
local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11
|
||||
local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
local showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false)
|
||||
local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == false)
|
||||
local powerOnTop = SFramesDB and SFramesDB.focusPowerOnTop == true
|
||||
local gradientStyle = SFrames:IsGradientStyle()
|
||||
local defaultPowerWidth = width - 2
|
||||
if defaultPowerWidth < 60 then
|
||||
defaultPowerWidth = 60
|
||||
end
|
||||
local rawPowerWidth = tonumber(SFramesDB and SFramesDB.focusPowerWidth)
|
||||
local legacyFullWidth = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or width
|
||||
local maxPowerWidth = gradientStyle and width or (width - 2)
|
||||
local powerWidth
|
||||
if gradientStyle then
|
||||
powerWidth = width
|
||||
elseif not rawPowerWidth or math.abs(rawPowerWidth - legacyFullWidth) < 0.5 then
|
||||
powerWidth = defaultPowerWidth
|
||||
else
|
||||
powerWidth = rawPowerWidth
|
||||
end
|
||||
powerWidth = math.floor(powerWidth + 0.5)
|
||||
if powerWidth < 60 then powerWidth = 60 end
|
||||
if powerWidth > maxPowerWidth then powerWidth = maxPowerWidth end
|
||||
|
||||
-- Main frame size & scale
|
||||
f:SetWidth(width)
|
||||
@@ -1222,40 +1226,75 @@ function SFrames.Focus:ApplySettings()
|
||||
f:SetBackdropColor(r, g, b, bgAlpha)
|
||||
end
|
||||
|
||||
-- Portrait
|
||||
if f.portrait then
|
||||
f.portrait:SetWidth(pWidth)
|
||||
f.portrait:SetHeight(totalH - 2)
|
||||
if showPortrait then
|
||||
f.portrait:Show()
|
||||
if f.portraitBG then f.portraitBG:Show() end
|
||||
else
|
||||
f.portrait:Hide()
|
||||
-- Portrait always hidden (focus frame uses class icon only)
|
||||
if f.portrait then f.portrait:Hide() end
|
||||
if f.portraitBG then f.portraitBG:Hide() end
|
||||
end
|
||||
end
|
||||
|
||||
-- Health bar anchors
|
||||
-- Health bar anchors (always full width, no portrait)
|
||||
if f.health then
|
||||
f.health:ClearAllPoints()
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
||||
if showPortrait then
|
||||
f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
|
||||
else
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
|
||||
end
|
||||
f.health:SetHeight(hHeight)
|
||||
end
|
||||
|
||||
-- Power bar anchors
|
||||
if f.power then
|
||||
f.power:ClearAllPoints()
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
|
||||
if showPortrait then
|
||||
f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
|
||||
else
|
||||
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -1 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
|
||||
f.power:SetWidth(powerWidth)
|
||||
f.power:SetHeight(pHeight)
|
||||
end
|
||||
|
||||
if f.health and f.power then
|
||||
local healthLevel = f:GetFrameLevel() + 2
|
||||
local powerLevel = powerOnTop and (healthLevel + 1) or (healthLevel - 1)
|
||||
f.health:SetFrameLevel(healthLevel)
|
||||
f.power:SetFrameLevel(powerLevel)
|
||||
end
|
||||
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ClearBackdrop(f)
|
||||
SFrames:ClearBackdrop(f.healthBGFrame)
|
||||
SFrames:ClearBackdrop(f.powerBGFrame)
|
||||
if f.health then
|
||||
f.health:ClearAllPoints()
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
|
||||
f.health:SetHeight(hHeight)
|
||||
end
|
||||
if f.power then
|
||||
f.power:ClearAllPoints()
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -2 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
|
||||
f.power:SetWidth(powerWidth)
|
||||
f.power:SetHeight(pHeight)
|
||||
end
|
||||
SFrames:ApplyGradientStyle(f.health)
|
||||
SFrames:ApplyGradientStyle(f.power)
|
||||
if f.healthBGFrame then
|
||||
f.healthBGFrame:ClearAllPoints()
|
||||
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
|
||||
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
|
||||
f.healthBGFrame:Hide()
|
||||
end
|
||||
if f.powerBGFrame then
|
||||
f.powerBGFrame:ClearAllPoints()
|
||||
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
|
||||
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
|
||||
f.powerBGFrame:Hide()
|
||||
end
|
||||
if f.health and f.health.bg then f.health.bg:Hide() end
|
||||
if f.power and f.power.bg then f.power.bg:Hide() end
|
||||
else
|
||||
SFrames:ApplyConfiguredUnitBackdrop(f, "focus")
|
||||
if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "focus") end
|
||||
if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "focus") end
|
||||
SFrames:RemoveGradientStyle(f.health)
|
||||
SFrames:RemoveGradientStyle(f.power)
|
||||
if f.healthBGFrame then f.healthBGFrame:Show() end
|
||||
if f.powerBGFrame then f.powerBGFrame:Show() end
|
||||
if f.health and f.health.bg then f.health.bg:Show() end
|
||||
if f.power and f.power.bg then f.power.bg:Show() end
|
||||
end
|
||||
|
||||
-- Castbar anchors
|
||||
@@ -1264,11 +1303,7 @@ function SFrames.Focus:ApplySettings()
|
||||
local cbH = 12
|
||||
f.castbar:SetHeight(cbH)
|
||||
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
|
||||
if showPortrait then
|
||||
f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(cbH + 6), 6)
|
||||
else
|
||||
f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
|
||||
end
|
||||
if not showCastBar then
|
||||
f.castbar:Hide()
|
||||
if f.castbar.cbbg then f.castbar.cbbg:Hide() end
|
||||
@@ -1298,8 +1333,6 @@ function SFrames.Focus:ApplySettings()
|
||||
self:UpdateAuras()
|
||||
end
|
||||
|
||||
-- Force portrait refresh
|
||||
f._lastPortraitUID = nil
|
||||
if self:GetFocusName() then
|
||||
self:UpdateAll()
|
||||
end
|
||||
@@ -1336,7 +1369,6 @@ function SFrames.Focus:Initialize()
|
||||
ef:RegisterEvent("UNIT_MAXRAGE")
|
||||
ef:RegisterEvent("UNIT_DISPLAYPOWER")
|
||||
ef:RegisterEvent("UNIT_AURA")
|
||||
ef:RegisterEvent("UNIT_PORTRAIT_UPDATE")
|
||||
ef:RegisterEvent("UNIT_TARGET")
|
||||
ef:RegisterEvent("RAID_TARGET_UPDATE")
|
||||
-- Combat log events for castbar detection (non-SuperWoW fallback)
|
||||
@@ -1378,7 +1410,6 @@ function SFrames.Focus:Initialize()
|
||||
if event == "PLAYER_TARGET_CHANGED" then
|
||||
local focusName = focusSelf:GetFocusName()
|
||||
if focusName and UnitExists("target") and UnitName("target") == focusName then
|
||||
focusSelf.frame._lastPortraitUID = nil
|
||||
focusSelf:UpdateAll()
|
||||
-- Try to grab GUID while we have target
|
||||
if UnitGUID then
|
||||
@@ -1512,7 +1543,7 @@ function SFrames.Focus:Initialize()
|
||||
focusSelf.frame.health:SetValue(hp)
|
||||
if maxHp > 0 then
|
||||
local pct = math.floor(hp / maxHp * 100)
|
||||
focusSelf.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
|
||||
focusSelf.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
|
||||
else
|
||||
focusSelf.frame.healthText:SetText("")
|
||||
end
|
||||
@@ -1526,7 +1557,7 @@ function SFrames.Focus:Initialize()
|
||||
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
|
||||
focusSelf.frame.power:SetValue(power)
|
||||
if maxPower > 0 then
|
||||
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
|
||||
focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
|
||||
else
|
||||
focusSelf.frame.powerText:SetText("")
|
||||
end
|
||||
@@ -1545,22 +1576,13 @@ function SFrames.Focus:Initialize()
|
||||
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
|
||||
focusSelf.frame.power:SetValue(power)
|
||||
if maxPower > 0 then
|
||||
focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
|
||||
focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
|
||||
else
|
||||
focusSelf.frame.powerText:SetText("")
|
||||
end
|
||||
end
|
||||
elseif event == "UNIT_AURA" then
|
||||
focusSelf:UpdateAuras()
|
||||
elseif event == "UNIT_PORTRAIT_UPDATE" then
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
if showPortrait and focusSelf.frame.portrait and evtUID then
|
||||
focusSelf.frame._lastPortraitUID = nil
|
||||
focusSelf.frame.portrait:SetUnit(evtUID)
|
||||
focusSelf.frame.portrait:SetCamera(0)
|
||||
focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
|
||||
focusSelf.frame._lastPortraitUID = evtUID
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1585,8 +1607,6 @@ function SFrames.Focus:Initialize()
|
||||
focusSelf.frame:Show()
|
||||
end
|
||||
|
||||
-- Re-scan for a valid unitID every poll cycle
|
||||
-- This catches cases where focus becomes target/party/raid dynamically
|
||||
local uid = focusSelf:GetUnitID()
|
||||
if uid then
|
||||
focusSelf:UpdateHealth()
|
||||
@@ -1594,31 +1614,19 @@ function SFrames.Focus:Initialize()
|
||||
focusSelf:UpdatePower()
|
||||
focusSelf:UpdateAuras()
|
||||
focusSelf:UpdateRaidIcon()
|
||||
|
||||
-- Only refresh portrait when unitID changes (prevents 3D model flicker)
|
||||
local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
|
||||
if showPortrait and focusSelf.frame.portrait then
|
||||
if uid ~= ef.lastUID then
|
||||
focusSelf.frame.portrait:SetUnit(uid)
|
||||
focusSelf.frame.portrait:SetCamera(0)
|
||||
focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
|
||||
focusSelf.frame.portrait:Show()
|
||||
ef.lastUID = uid
|
||||
end
|
||||
end
|
||||
else
|
||||
ef.lastUID = nil
|
||||
end
|
||||
end)
|
||||
|
||||
-- Register mover
|
||||
-- Register mover (Y aligned with pet frame, X aligned with target frame)
|
||||
if SFrames.Movers and SFrames.Movers.RegisterMover then
|
||||
if SFramesTargetFrame then
|
||||
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
|
||||
"TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -10)
|
||||
"TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -75,
|
||||
nil, { alwaysShowInLayout = true })
|
||||
else
|
||||
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
|
||||
"LEFT", "UIParent", "LEFT", 250, 0)
|
||||
"LEFT", "UIParent", "LEFT", 250, 0,
|
||||
nil, { alwaysShowInLayout = true })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1364,20 +1364,6 @@ function GS:HookTooltips()
|
||||
end
|
||||
end
|
||||
|
||||
local origRef = SetItemRef
|
||||
if origRef then
|
||||
SetItemRef = function(link, text, button)
|
||||
origRef(link, text, button)
|
||||
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
|
||||
pcall(function()
|
||||
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
|
||||
if itemStr then
|
||||
ItemRefTooltip._gsScoreAdded = nil
|
||||
GS:AddScoreToTooltip(ItemRefTooltip, itemStr)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -496,20 +496,31 @@ ShowLootPage = function()
|
||||
row:Show()
|
||||
end
|
||||
|
||||
-- Let the ORIGINAL Blizzard LootFrame_Update run so that native
|
||||
-- LootButton1-4 get their IDs, slot data, and OnClick set up
|
||||
-- through the trusted native code path (required for LootSlot).
|
||||
-- Set up the native LootFrame so it stays alive (required by the
|
||||
-- engine) but completely invisible. We do NOT call origLootFrameUpdate
|
||||
-- because it uses a different items-per-page (3 when paginated vs our 4)
|
||||
-- which mis-calculates slot indices.
|
||||
if LootFrame then
|
||||
LootFrame.numLootItems = numItems
|
||||
LootFrame.page = page
|
||||
if not LootFrame:IsShown() then LootFrame:Show() end
|
||||
end
|
||||
if origLootFrameUpdate then origLootFrameUpdate() end
|
||||
|
||||
-- Now reposition the native buttons on top of our visual rows
|
||||
-- Directly configure native LootButtons with the correct slot for
|
||||
-- each visual row. SetSlot is the C++ binding that the native
|
||||
-- LootButton_OnClick / LootFrameItem_OnClick reads via this.slot.
|
||||
for btnIdx = 1, ITEMS_PER_PAGE do
|
||||
local nb = _G["LootButton" .. btnIdx]
|
||||
local row = lootRows[btnIdx]
|
||||
if nb and row and row:IsShown() and row._qualColor then
|
||||
local realSlot = row.slotIndex
|
||||
|
||||
-- SetSlot is the native C++ method; .slot is the Lua mirror.
|
||||
if nb.SetSlot then nb:SetSlot(realSlot) end
|
||||
nb.slot = realSlot
|
||||
local _, _, _, rq = GetLootSlotInfo(realSlot)
|
||||
nb.quality = rq or 0
|
||||
|
||||
nb:ClearAllPoints()
|
||||
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||
@@ -521,10 +532,10 @@ ShowLootPage = function()
|
||||
|
||||
nb._nanamiRow = row
|
||||
nb:SetScript("OnEnter", function()
|
||||
local slot = this:GetID()
|
||||
if slot then
|
||||
local s = this.slot
|
||||
if s then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetLootItem(slot)
|
||||
GameTooltip:SetLootItem(s)
|
||||
if CursorUpdate then CursorUpdate() end
|
||||
end
|
||||
local r2 = this._nanamiRow
|
||||
@@ -724,14 +735,15 @@ local function GetAlertFrame()
|
||||
return CreateAlertFrame()
|
||||
end
|
||||
|
||||
-- Layout: newest item at bottom (index 1 = oldest = top, last = newest = bottom slot 0)
|
||||
local function LayoutAlerts()
|
||||
CreateAlertAnchor()
|
||||
for i = 1, table.getn(activeAlerts) do
|
||||
local n = table.getn(activeAlerts)
|
||||
for i = 1, n do
|
||||
local af = activeAlerts[i]
|
||||
if af._fadeState ~= "fading" then
|
||||
-- oldest at top, newest at bottom: slot = (n - i)
|
||||
af:ClearAllPoints()
|
||||
af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (i - 1) * (ALERT_HEIGHT + ALERT_GAP))
|
||||
end
|
||||
af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (n - i) * (ALERT_HEIGHT + ALERT_GAP))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -750,36 +762,15 @@ local function RemoveAlert(frame)
|
||||
LayoutAlerts()
|
||||
end
|
||||
|
||||
local function StartAlertFade(frame, delay)
|
||||
frame._fadeState = "waiting"
|
||||
frame._fadeElapsed = 0
|
||||
frame._fadeDelay = delay
|
||||
-- Each alert has its own independent timer; when it expires, just disappear (no float animation)
|
||||
local function StartAlertTimer(frame, delay)
|
||||
frame._timerElapsed = 0
|
||||
frame._timerDelay = delay
|
||||
|
||||
frame:SetScript("OnUpdate", function()
|
||||
this._fadeElapsed = (this._fadeElapsed or 0) + arg1
|
||||
|
||||
if this._fadeState == "waiting" then
|
||||
if this._fadeElapsed >= this._fadeDelay then
|
||||
this._fadeState = "fading"
|
||||
this._fadeElapsed = 0
|
||||
this._baseY = 0
|
||||
for idx = 1, table.getn(activeAlerts) do
|
||||
if activeAlerts[idx] == this then
|
||||
this._baseY = (idx - 1) * (ALERT_HEIGHT + ALERT_GAP)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif this._fadeState == "fading" then
|
||||
local p = this._fadeElapsed / ALERT_FADE_DUR
|
||||
if p >= 1 then
|
||||
this._timerElapsed = (this._timerElapsed or 0) + arg1
|
||||
if this._timerElapsed >= this._timerDelay 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
|
||||
@@ -794,14 +785,15 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
|
||||
local db = GetDB()
|
||||
if not db.alertEnable then return end
|
||||
|
||||
-- Stack same item: update count and reset timer
|
||||
for i = 1, table.getn(activeAlerts) do
|
||||
local af = activeAlerts[i]
|
||||
if af._itemName == name and af._fadeState == "waiting" then
|
||||
if af._itemName == name then
|
||||
af._quantity = (af._quantity or 1) + (quantity or 1)
|
||||
if af._quantity > 1 then
|
||||
af.countFS:SetText("x" .. af._quantity)
|
||||
end
|
||||
af._fadeElapsed = 0
|
||||
af._timerElapsed = 0
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -815,7 +807,6 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
|
||||
f._quantity = quantity or 1
|
||||
f._link = link
|
||||
|
||||
-- Set icon texture
|
||||
local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
f.icon:SetTexture(iconTex)
|
||||
|
||||
@@ -841,12 +832,12 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
|
||||
f:SetAlpha(1)
|
||||
f:Show()
|
||||
|
||||
-- New item appended to end of list = bottom position
|
||||
table.insert(activeAlerts, f)
|
||||
LayoutAlerts()
|
||||
|
||||
local hold = db.alertFadeDelay or ALERT_HOLD
|
||||
local stagger = table.getn(activeAlerts) * ALERT_STAGGER
|
||||
StartAlertFade(f, hold + stagger)
|
||||
StartAlertTimer(f, hold)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1000,8 +991,9 @@ function LD:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
-- After the native LootFrame_Update runs (called by the engine or
|
||||
-- by us), reposition native buttons onto our visual rows.
|
||||
-- Replace LootFrame_Update: run the original for engine compatibility,
|
||||
-- then re-apply the correct slot on each native button based on our
|
||||
-- visual rows (which use ITEMS_PER_PAGE=4, not the native 3-when-paged).
|
||||
LootFrame_Update = function()
|
||||
if origLootFrameUpdate then origLootFrameUpdate() end
|
||||
if not (lootFrame and lootFrame:IsShown()) then return end
|
||||
@@ -1009,6 +1001,11 @@ function LD:Initialize()
|
||||
local nb = _G["LootButton" .. i]
|
||||
local row = lootRows[i]
|
||||
if nb and row and row:IsShown() and row._qualColor then
|
||||
local realSlot = row.slotIndex
|
||||
if realSlot then
|
||||
if nb.SetSlot then nb:SetSlot(realSlot) end
|
||||
nb.slot = realSlot
|
||||
end
|
||||
nb:ClearAllPoints()
|
||||
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||
|
||||
436
Mail.lua
436
Mail.lua
@@ -45,6 +45,7 @@ local S = {
|
||||
inboxRows = {},
|
||||
currentTab = 1,
|
||||
inboxPage = 1,
|
||||
bagPage = 1,
|
||||
inboxChecked = {},
|
||||
collectQueue = {},
|
||||
collectTimer = nil,
|
||||
@@ -53,11 +54,23 @@ local S = {
|
||||
isSending = false,
|
||||
collectElapsed = 0,
|
||||
multiSend = nil, -- active multi-send state table
|
||||
codMode = false, -- send panel: 付款取信 mode toggle
|
||||
}
|
||||
|
||||
local L = {
|
||||
W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28,
|
||||
BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12,
|
||||
BAG_SLOT = 38, BAG_GAP = 3, BAG_PER_ROW = 8, BAG_ROWS = 8,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"] = {
|
||||
text = "确认支付 %s 取回此物品?",
|
||||
button1 = "确认",
|
||||
button2 = "取消",
|
||||
OnAccept = function() end,
|
||||
timeout = 0,
|
||||
whileDead = true,
|
||||
hideOnEscape = true,
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -476,18 +489,27 @@ local function UpdateInbox()
|
||||
if money and money > 0 then
|
||||
row.moneyFrame:SetMoney(money)
|
||||
elseif CODAmount and CODAmount > 0 then
|
||||
row.codFS:SetText("COD:"); row.codFS:Show()
|
||||
row.codFS:SetText("付款:"); row.codFS:Show()
|
||||
row.moneyFrame:SetMoney(CODAmount)
|
||||
else
|
||||
row.moneyFrame:SetMoney(0)
|
||||
end
|
||||
row.expiryFS:SetText(FormatExpiry(daysLeft))
|
||||
local canTake = (hasItem or (money and money > 0)) and (not CODAmount or CODAmount == 0)
|
||||
local canTake = hasItem or (money and money > 0)
|
||||
row.takeBtn:SetDisabled(not canTake)
|
||||
row.takeBtn:SetScript("OnClick", function()
|
||||
if row.mailIndex then
|
||||
if hasItem then
|
||||
if CODAmount and CODAmount > 0 then
|
||||
local codStr = FormatMoneyString(CODAmount)
|
||||
local idx = row.mailIndex
|
||||
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
|
||||
TakeInboxItem(idx)
|
||||
end
|
||||
StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
|
||||
else
|
||||
TakeInboxItem(row.mailIndex)
|
||||
end
|
||||
elseif money and money > 0 then
|
||||
local idx = row.mailIndex
|
||||
TakeInboxMoney(idx)
|
||||
@@ -543,7 +565,7 @@ end
|
||||
local function StopCollecting()
|
||||
S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil
|
||||
if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end
|
||||
UpdateInbox()
|
||||
if S.currentTab == 3 then ML:UpdateMailBag() else UpdateInbox() end
|
||||
end
|
||||
|
||||
local function ProcessCollectQueue()
|
||||
@@ -683,6 +705,8 @@ end
|
||||
local function ResetSendForm()
|
||||
if not S.frame then return end
|
||||
ClearSendItems()
|
||||
S.codMode = false
|
||||
if S.frame.UpdateMoneyToggle then S.frame.UpdateMoneyToggle() end
|
||||
if S.frame.toEditBox then S.frame.toEditBox:SetText("") end
|
||||
if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end
|
||||
if S.frame.bodyEditBox then S.frame.bodyEditBox:SetText("") end
|
||||
@@ -733,8 +757,15 @@ local function DoMultiSend(recipient, subject, body, money)
|
||||
local items = {}
|
||||
for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end
|
||||
|
||||
-- No attachments: plain text / money mail
|
||||
-- No attachments: plain text / money mail (付款取信 requires attachments)
|
||||
if table.getn(items) == 0 then
|
||||
if S.codMode and money and money > 0 then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 付款取信模式需要添加附件物品")
|
||||
if S.frame and S.frame.sendBtn then
|
||||
S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false)
|
||||
end
|
||||
return
|
||||
end
|
||||
if money and money > 0 then SetSendMailMoney(money) end
|
||||
SendMail(recipient, subject, body or "")
|
||||
return
|
||||
@@ -746,6 +777,7 @@ local function DoMultiSend(recipient, subject, body, money)
|
||||
subject = subject or "",
|
||||
body = body or "",
|
||||
money = money,
|
||||
codMode = S.codMode,
|
||||
total = table.getn(items),
|
||||
sentCount = 0,
|
||||
phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ...
|
||||
@@ -803,10 +835,14 @@ local function DoMultiSend(recipient, subject, body, money)
|
||||
return
|
||||
end
|
||||
|
||||
-- Money only on first mail
|
||||
if ms.sentCount == 1 and ms.money and ms.money > 0 then
|
||||
-- Money or 付款取信
|
||||
if ms.money and ms.money > 0 then
|
||||
if ms.codMode then
|
||||
SetSendMailCOD(ms.money)
|
||||
elseif ms.sentCount == 1 then
|
||||
SetSendMailMoney(ms.money)
|
||||
end
|
||||
end
|
||||
|
||||
-- Send this single-attachment mail
|
||||
ms.sendOk = false
|
||||
@@ -903,13 +939,18 @@ local function BuildMainFrame()
|
||||
sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER)
|
||||
sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
|
||||
|
||||
local tabInbox = CreateTabBtn(f, "收件箱", 70)
|
||||
local tabInbox = CreateTabBtn(f, "收件箱", 62)
|
||||
tabInbox:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6))
|
||||
tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end)
|
||||
f.tabInbox = tabInbox
|
||||
|
||||
local tabSend = CreateTabBtn(f, "发送", 70)
|
||||
tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
|
||||
local tabBag = CreateTabBtn(f, "邮包", 50)
|
||||
tabBag:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
|
||||
tabBag:SetScript("OnClick", function() S.currentTab = 3; ML:ShowMailBagPanel() end)
|
||||
f.tabBag = tabBag
|
||||
|
||||
local tabSend = CreateTabBtn(f, "发送", 62)
|
||||
tabSend:SetPoint("LEFT", tabBag, "RIGHT", 4, 0)
|
||||
tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end)
|
||||
f.tabSend = tabSend
|
||||
|
||||
@@ -1128,17 +1169,26 @@ function ML:ShowMailDetail(mailIndex)
|
||||
dp.detailMoney:SetMoney(money); dp.detailMoney:Show()
|
||||
end
|
||||
if CODAmount and CODAmount > 0 then
|
||||
dp.detailCodLabel:SetText("COD:"); dp.detailCodLabel:Show()
|
||||
dp.detailCodLabel:SetText("付款取信:"); dp.detailCodLabel:Show()
|
||||
dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show()
|
||||
end
|
||||
|
||||
-- Take items button
|
||||
local canTakeItem = hasItem and (not CODAmount or CODAmount == 0)
|
||||
local canTakeItem = hasItem
|
||||
dp.takeItemBtn:SetDisabled(not canTakeItem)
|
||||
dp.takeItemBtn:SetScript("OnClick", function()
|
||||
if S.detailMailIndex then
|
||||
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)
|
||||
|
||||
-- Take money button
|
||||
@@ -1205,6 +1255,294 @@ function ML:HideMailDetail()
|
||||
UpdateInbox()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- BUILD: Mail Bag panel (grid view of all inbox items)
|
||||
--------------------------------------------------------------------------------
|
||||
local function BuildMailBagPanel()
|
||||
local f = S.frame
|
||||
local panelTop = L.HEADER + 6 + L.TAB_H + 4
|
||||
local bp = CreateFrame("Frame", nil, f)
|
||||
bp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop)
|
||||
bp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
|
||||
bp:Hide()
|
||||
f.bagPanel = bp
|
||||
|
||||
local font = GetFont()
|
||||
local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
|
||||
|
||||
local infoFS = bp:CreateFontString(nil, "OVERLAY")
|
||||
infoFS:SetFont(font, 10, "OUTLINE")
|
||||
infoFS:SetPoint("TOPLEFT", bp, "TOPLEFT", L.PAD, -2)
|
||||
infoFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
|
||||
f.bagInfoFS = infoFS
|
||||
|
||||
local codLegend = bp:CreateFontString(nil, "OVERLAY")
|
||||
codLegend:SetFont(font, 9, "OUTLINE")
|
||||
codLegend:SetPoint("TOPRIGHT", bp, "TOPRIGHT", -L.PAD, -2)
|
||||
codLegend:SetText("|cFFFF5555■|r 付款取信")
|
||||
codLegend:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
|
||||
|
||||
f.bagSlots = {}
|
||||
local gridTop = 16
|
||||
for i = 1, slotsPerPage do
|
||||
local row = math.floor((i - 1) / L.BAG_PER_ROW)
|
||||
local col = math.mod((i - 1), L.BAG_PER_ROW)
|
||||
local sf = CreateFrame("Button", "SFramesMailBagSlot" .. i, bp)
|
||||
sf:SetWidth(L.BAG_SLOT); sf:SetHeight(L.BAG_SLOT)
|
||||
sf:SetPoint("TOPLEFT", bp, "TOPLEFT",
|
||||
L.PAD + col * (L.BAG_SLOT + L.BAG_GAP),
|
||||
-(gridTop + row * (L.BAG_SLOT + L.BAG_GAP)))
|
||||
sf:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 12,
|
||||
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
||||
})
|
||||
sf:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
|
||||
sf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
|
||||
local ico = sf:CreateTexture(nil, "ARTWORK")
|
||||
ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||
ico:SetPoint("TOPLEFT", 3, -3); ico:SetPoint("BOTTOMRIGHT", -3, 3)
|
||||
ico:Hide()
|
||||
sf.icon = ico
|
||||
|
||||
local cnt = sf:CreateFontString(nil, "OVERLAY")
|
||||
cnt:SetFont(font, 11, "OUTLINE")
|
||||
cnt:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -2, 2)
|
||||
cnt:SetJustifyH("RIGHT")
|
||||
sf.countFS = cnt
|
||||
|
||||
local moneyFS = sf:CreateFontString(nil, "OVERLAY")
|
||||
moneyFS:SetFont(font, 8, "OUTLINE")
|
||||
moneyFS:SetPoint("BOTTOM", sf, "BOTTOM", 0, 2)
|
||||
moneyFS:SetWidth(L.BAG_SLOT)
|
||||
moneyFS:SetJustifyH("CENTER")
|
||||
moneyFS:Hide()
|
||||
sf.moneyFS = moneyFS
|
||||
|
||||
local codFS = sf:CreateFontString(nil, "OVERLAY")
|
||||
codFS:SetFont(font, 7, "OUTLINE")
|
||||
codFS:SetPoint("TOP", sf, "TOP", 0, -1)
|
||||
codFS:SetText("|cFFFF3333付款|r")
|
||||
codFS:Hide()
|
||||
sf.codFS = codFS
|
||||
|
||||
sf.mailData = nil
|
||||
sf:RegisterForClicks("LeftButtonUp")
|
||||
sf:SetScript("OnClick", function()
|
||||
local data = this.mailData
|
||||
if not data then return end
|
||||
if data.codAmount and data.codAmount > 0 then
|
||||
local codStr = FormatMoneyString(data.codAmount)
|
||||
local idx = data.mailIndex
|
||||
StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
|
||||
TakeInboxItem(idx)
|
||||
end
|
||||
StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
|
||||
elseif data.hasItem then
|
||||
TakeInboxItem(data.mailIndex)
|
||||
elseif data.money and data.money > 0 then
|
||||
local idx = data.mailIndex
|
||||
TakeInboxMoney(idx)
|
||||
if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end
|
||||
S.deleteElapsed = 0
|
||||
S.deleteTimer:SetScript("OnUpdate", function()
|
||||
S.deleteElapsed = S.deleteElapsed + arg1
|
||||
if S.deleteElapsed >= 0.5 then
|
||||
this:SetScript("OnUpdate", nil)
|
||||
if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
sf:SetScript("OnEnter", function()
|
||||
this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
|
||||
local data = this.mailData
|
||||
if not data then return end
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
if data.hasItem then
|
||||
pcall(GameTooltip.SetInboxItem, GameTooltip, data.mailIndex)
|
||||
else
|
||||
GameTooltip:AddLine("金币邮件", 1, 0.84, 0)
|
||||
end
|
||||
GameTooltip:AddLine(" ")
|
||||
if data.sender then
|
||||
GameTooltip:AddLine("发件人: " .. data.sender, 0.7, 0.7, 0.7)
|
||||
end
|
||||
if data.subject and data.subject ~= "" then
|
||||
GameTooltip:AddLine(data.subject, 0.5, 0.5, 0.5)
|
||||
end
|
||||
if data.codAmount and data.codAmount > 0 then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("付款取信: " .. FormatMoneyString(data.codAmount), 1, 0.3, 0.3)
|
||||
GameTooltip:AddLine("|cFFFFCC00点击支付并取回|r")
|
||||
elseif data.money and data.money > 0 and not data.hasItem then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("金额: " .. FormatMoneyString(data.money), 1, 0.84, 0)
|
||||
GameTooltip:AddLine("|cFFFFCC00点击收取金币|r")
|
||||
elseif data.hasItem then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("|cFFFFCC00点击收取物品|r")
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
sf:SetScript("OnLeave", function()
|
||||
local data = this.mailData
|
||||
if data and data.codAmount and data.codAmount > 0 then
|
||||
this:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
|
||||
else
|
||||
this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
end
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
f.bagSlots[i] = sf
|
||||
end
|
||||
|
||||
local bsep = bp:CreateTexture(nil, "ARTWORK")
|
||||
bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1)
|
||||
bsep:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", 6, L.BOTTOM)
|
||||
bsep:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -6, L.BOTTOM)
|
||||
bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
|
||||
|
||||
local prev = CreateActionBtn(bp, "<", 28)
|
||||
prev:SetHeight(22); prev:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", L.PAD, 12)
|
||||
prev:SetScript("OnClick", function() S.bagPage = S.bagPage - 1; ML:UpdateMailBag() end)
|
||||
f.bagPrevBtn = prev
|
||||
|
||||
local nxt = CreateActionBtn(bp, ">", 28)
|
||||
nxt:SetHeight(22); nxt:SetPoint("LEFT", prev, "RIGHT", 4, 0)
|
||||
nxt:SetScript("OnClick", function() S.bagPage = S.bagPage + 1; ML:UpdateMailBag() end)
|
||||
f.bagNextBtn = nxt
|
||||
|
||||
local pageFS = bp:CreateFontString(nil, "OVERLAY")
|
||||
pageFS:SetFont(font, 10, "OUTLINE")
|
||||
pageFS:SetPoint("LEFT", nxt, "RIGHT", 8, 0)
|
||||
pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
|
||||
f.bagPageFS = pageFS
|
||||
|
||||
local colAll = CreateActionBtn(bp, "全部收取", 80)
|
||||
colAll:SetHeight(24); colAll:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -L.PAD, 10)
|
||||
colAll:SetScript("OnClick", function()
|
||||
if S.isCollecting then StopCollecting() else CollectAll() end
|
||||
end)
|
||||
f.bagCollectAllBtn = colAll
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Mail Bag: Update
|
||||
--------------------------------------------------------------------------------
|
||||
function ML:UpdateMailBag()
|
||||
if not S.frame or not S.frame:IsVisible() or S.currentTab ~= 3 then return end
|
||||
local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
|
||||
local numMails = GetInboxNumItems()
|
||||
|
||||
local entries = {}
|
||||
for mi = 1, numMails do
|
||||
local _, _, sender, subject, money, CODAmount, daysLeft, hasItem = GetInboxHeaderInfo(mi)
|
||||
if hasItem or (money and money > 0) or (CODAmount and CODAmount > 0) then
|
||||
local itemName, itemTex
|
||||
if hasItem then itemName, itemTex = GetInboxItem(mi) end
|
||||
table.insert(entries, {
|
||||
mailIndex = mi,
|
||||
hasItem = hasItem,
|
||||
itemName = itemName,
|
||||
itemTexture = itemTex,
|
||||
money = money or 0,
|
||||
codAmount = CODAmount or 0,
|
||||
sender = sender or "未知",
|
||||
subject = subject or "",
|
||||
daysLeft = daysLeft,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local totalEntries = table.getn(entries)
|
||||
local totalPages = math.max(1, math.ceil(totalEntries / slotsPerPage))
|
||||
if S.bagPage > totalPages then S.bagPage = totalPages end
|
||||
if S.bagPage < 1 then S.bagPage = 1 end
|
||||
|
||||
S.frame.bagInfoFS:SetText(string.format("共 %d 件可收取 (%d 封邮件)", totalEntries, numMails))
|
||||
S.frame.bagPageFS:SetText(string.format("第 %d/%d 页", S.bagPage, totalPages))
|
||||
S.frame.bagPrevBtn:SetDisabled(S.bagPage <= 1)
|
||||
S.frame.bagNextBtn:SetDisabled(S.bagPage >= totalPages)
|
||||
S.frame.bagCollectAllBtn:SetDisabled(numMails == 0 and not S.isCollecting)
|
||||
if S.isCollecting then
|
||||
S.frame.bagCollectAllBtn.label:SetText("收取中...")
|
||||
else
|
||||
S.frame.bagCollectAllBtn.label:SetText("全部收取")
|
||||
end
|
||||
|
||||
for i = 1, slotsPerPage do
|
||||
local slot = S.frame.bagSlots[i]
|
||||
local ei = (S.bagPage - 1) * slotsPerPage + i
|
||||
local entry = entries[ei]
|
||||
if entry then
|
||||
slot.mailData = entry
|
||||
if entry.hasItem and entry.itemTexture then
|
||||
slot.icon:SetTexture(entry.itemTexture)
|
||||
elseif entry.money > 0 then
|
||||
slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
|
||||
else
|
||||
slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01")
|
||||
end
|
||||
slot.icon:Show()
|
||||
slot.countFS:SetText("")
|
||||
slot.moneyFS:Hide()
|
||||
|
||||
if entry.money > 0 and not entry.hasItem then
|
||||
local g = math.floor(entry.money / 10000)
|
||||
if g > 0 then
|
||||
slot.moneyFS:SetText("|cFFFFD700" .. g .. "g|r")
|
||||
else
|
||||
local sv = math.floor(math.mod(entry.money, 10000) / 100)
|
||||
if sv > 0 then
|
||||
slot.moneyFS:SetText("|cFFC7C7CF" .. sv .. "s|r")
|
||||
else
|
||||
local cv = math.mod(entry.money, 100)
|
||||
slot.moneyFS:SetText("|cFFB87333" .. cv .. "c|r")
|
||||
end
|
||||
end
|
||||
slot.moneyFS:Show()
|
||||
end
|
||||
|
||||
if entry.codAmount > 0 then
|
||||
slot.codFS:Show()
|
||||
slot:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
|
||||
else
|
||||
slot.codFS:Hide()
|
||||
slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
end
|
||||
slot:Show()
|
||||
else
|
||||
slot.mailData = nil
|
||||
slot.icon:Hide()
|
||||
slot.countFS:SetText("")
|
||||
slot.moneyFS:Hide()
|
||||
slot.codFS:Hide()
|
||||
slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
slot:Show()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Mail Bag: Panel Switching
|
||||
--------------------------------------------------------------------------------
|
||||
function ML:ShowMailBagPanel()
|
||||
if not S.frame then return end
|
||||
S.frame.tabInbox:SetActive(false)
|
||||
S.frame.tabBag:SetActive(true)
|
||||
S.frame.tabSend:SetActive(false)
|
||||
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
|
||||
S.detailMailIndex = nil
|
||||
S.frame.inboxPanel:Hide()
|
||||
S.frame.sendPanel:Hide()
|
||||
S.frame.bagPanel:Show()
|
||||
ML:UpdateMailBag()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- BUILD: Send panel
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1555,29 +1893,56 @@ local function BuildSendPanel()
|
||||
bsf:SetScrollChild(bodyEB)
|
||||
f.bodyEditBox = bodyEB
|
||||
|
||||
-- Money row
|
||||
local mLabel = sp:CreateFontString(nil, "OVERLAY")
|
||||
mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
|
||||
mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
|
||||
-- Money mode toggle (附加金币 / 付款取信)
|
||||
local mToggle = CreateActionBtn(sp, "附加金币", 72)
|
||||
mToggle:SetHeight(20); mToggle:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
|
||||
f.moneyToggle = mToggle
|
||||
local function UpdateMoneyToggle()
|
||||
if S.codMode then
|
||||
mToggle.label:SetText("|cFFFF5555付款取信|r")
|
||||
mToggle:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
|
||||
else
|
||||
mToggle.label:SetText("附加金币")
|
||||
mToggle:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
|
||||
end
|
||||
end
|
||||
f.UpdateMoneyToggle = UpdateMoneyToggle
|
||||
mToggle:SetScript("OnClick", function()
|
||||
S.codMode = not S.codMode; UpdateMoneyToggle()
|
||||
end)
|
||||
mToggle:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOPRIGHT")
|
||||
if S.codMode then
|
||||
GameTooltip:AddLine("付款取信模式", 1, 0.5, 0.5)
|
||||
GameTooltip:AddLine("收件人需支付指定金额才能取回附件", 0.8, 0.8, 0.8)
|
||||
else
|
||||
GameTooltip:AddLine("附加金币模式", 1, 0.84, 0)
|
||||
GameTooltip:AddLine("随邮件附送金币给收件人", 0.8, 0.8, 0.8)
|
||||
end
|
||||
GameTooltip:AddLine("|cFFFFCC00点击切换模式|r")
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
mToggle:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
UpdateMoneyToggle()
|
||||
|
||||
local gL = sp:CreateFontString(nil, "OVERLAY")
|
||||
gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mLabel, "RIGHT", 6, 0)
|
||||
gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mToggle, "RIGHT", 6, 0)
|
||||
gL:SetText("金"); gL:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3])
|
||||
local gEB = CreateStyledEditBox(sp, 60, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
|
||||
local gEB = CreateStyledEditBox(sp, 50, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
|
||||
|
||||
local sL = sp:CreateFontString(nil, "OVERLAY")
|
||||
sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0)
|
||||
sL:SetText("银"); sL:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3])
|
||||
local sEB = CreateStyledEditBox(sp, 40, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
|
||||
local sEB = CreateStyledEditBox(sp, 36, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
|
||||
|
||||
local cL = sp:CreateFontString(nil, "OVERLAY")
|
||||
cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0)
|
||||
cL:SetText("铜"); cL:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3])
|
||||
local cEB = CreateStyledEditBox(sp, 40, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
|
||||
local cEB = CreateStyledEditBox(sp, 36, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
|
||||
|
||||
-- Attachments
|
||||
local aLabel = sp:CreateFontString(nil, "OVERLAY")
|
||||
aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mLabel, "TOPLEFT", 0, -28)
|
||||
aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mToggle, "TOPLEFT", 0, -28)
|
||||
aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
|
||||
|
||||
local clrBtn = CreateActionBtn(sp, "清空", 50)
|
||||
@@ -1679,7 +2044,7 @@ local function SetupEvents()
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "MAIL_SHOW" then
|
||||
if SFramesDB and SFramesDB.enableMail == false then return end
|
||||
S.currentTab = 1; S.inboxPage = 1; S.inboxChecked = {}
|
||||
S.currentTab = 1; S.inboxPage = 1; S.bagPage = 1; S.inboxChecked = {}
|
||||
CheckInbox(); f:Show(); ML:ShowInboxPanel()
|
||||
elseif event == "MAIL_INBOX_UPDATE" then
|
||||
if f:IsVisible() then
|
||||
@@ -1689,10 +2054,28 @@ local function SetupEvents()
|
||||
else
|
||||
ML:HideMailDetail()
|
||||
end
|
||||
elseif S.currentTab == 3 then
|
||||
ML:UpdateMailBag()
|
||||
else
|
||||
UpdateInbox()
|
||||
end
|
||||
end
|
||||
-- 收件箱清空后同步小地图信件图标状态
|
||||
if MiniMapMailFrame then
|
||||
if HasNewMail and HasNewMail() then
|
||||
MiniMapMailFrame:Show()
|
||||
elseif GetInboxNumItems() == 0 then
|
||||
MiniMapMailFrame:Hide()
|
||||
end
|
||||
end
|
||||
elseif event == "UPDATE_PENDING_MAIL" then
|
||||
if MiniMapMailFrame then
|
||||
if HasNewMail and HasNewMail() then
|
||||
MiniMapMailFrame:Show()
|
||||
else
|
||||
MiniMapMailFrame:Hide()
|
||||
end
|
||||
end
|
||||
elseif event == "MAIL_CLOSED" then
|
||||
if S.multiSend then AbortMultiSend("邮箱已关闭") end
|
||||
f:Hide()
|
||||
@@ -1721,6 +2104,7 @@ local function SetupEvents()
|
||||
f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE")
|
||||
f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS")
|
||||
f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED")
|
||||
f:RegisterEvent("UPDATE_PENDING_MAIL")
|
||||
|
||||
if MailFrame then
|
||||
local origMailOnShow = MailFrame:GetScript("OnShow")
|
||||
@@ -1728,6 +2112,7 @@ local function SetupEvents()
|
||||
if origMailOnShow then origMailOnShow() end
|
||||
this:ClearAllPoints()
|
||||
this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
|
||||
this:SetAlpha(0)
|
||||
this:EnableMouse(false)
|
||||
end)
|
||||
for i = table.getn(UISpecialFrames), 1, -1 do
|
||||
@@ -1772,6 +2157,7 @@ function ML:Initialize()
|
||||
BuildMainFrame()
|
||||
BuildInboxPanel()
|
||||
BuildDetailPanel()
|
||||
BuildMailBagPanel()
|
||||
BuildSendPanel()
|
||||
SetupEvents()
|
||||
end
|
||||
@@ -1781,19 +2167,19 @@ end
|
||||
--------------------------------------------------------------------------------
|
||||
function ML:ShowInboxPanel()
|
||||
if not S.frame then return end
|
||||
S.frame.tabInbox:SetActive(true); S.frame.tabSend:SetActive(false)
|
||||
S.frame.tabInbox:SetActive(true); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(false)
|
||||
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
|
||||
S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide()
|
||||
S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide(); S.frame.bagPanel:Hide()
|
||||
S.detailMailIndex = nil
|
||||
UpdateInbox()
|
||||
end
|
||||
|
||||
function ML:ShowSendPanel()
|
||||
if not S.frame then return end
|
||||
S.frame.tabInbox:SetActive(false); S.frame.tabSend:SetActive(true)
|
||||
S.frame.tabInbox:SetActive(false); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(true)
|
||||
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
|
||||
S.detailMailIndex = nil
|
||||
S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show()
|
||||
S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show(); S.frame.bagPanel:Hide()
|
||||
if S.frame.sendStatus then S.frame.sendStatus:SetText("") end
|
||||
if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end
|
||||
ML:UpdateSendPanel()
|
||||
|
||||
66
MapIcons.lua
66
MapIcons.lua
@@ -127,6 +127,64 @@ local function GetZoneYards()
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local function IsMapStateProtected()
|
||||
if WorldMapFrame and WorldMapFrame:IsVisible() then
|
||||
return true
|
||||
end
|
||||
if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
|
||||
return true
|
||||
end
|
||||
if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function SafeSetMapToCurrentZone()
|
||||
if not SetMapToCurrentZone then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
|
||||
end
|
||||
return pcall(SetMapToCurrentZone)
|
||||
end
|
||||
|
||||
local function SafeSetMapZoom(continent, zone)
|
||||
if not SetMapZoom then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
|
||||
end
|
||||
return pcall(SetMapZoom, continent, zone)
|
||||
end
|
||||
|
||||
local function WithPlayerZoneMap(func)
|
||||
if type(func) ~= "function" then
|
||||
return
|
||||
end
|
||||
if IsMapStateProtected() or not SetMapToCurrentZone then
|
||||
return func()
|
||||
end
|
||||
|
||||
local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
|
||||
local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0
|
||||
|
||||
SafeSetMapToCurrentZone()
|
||||
local results = { func() }
|
||||
|
||||
if SetMapZoom then
|
||||
if savedZ and savedZ > 0 and savedC and savedC > 0 then
|
||||
SafeSetMapZoom(savedC, savedZ)
|
||||
elseif savedC and savedC > 0 then
|
||||
SafeSetMapZoom(savedC, 0)
|
||||
end
|
||||
end
|
||||
|
||||
return unpack(results)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- 1. World Map + Battlefield Minimap: class icon overlays
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -224,14 +282,11 @@ local function UpdateMinimapDots()
|
||||
return
|
||||
end
|
||||
|
||||
if WorldMapFrame and WorldMapFrame:IsVisible() then
|
||||
if IsMapStateProtected() then
|
||||
return
|
||||
end
|
||||
|
||||
if SetMapToCurrentZone then
|
||||
pcall(SetMapToCurrentZone)
|
||||
end
|
||||
|
||||
WithPlayerZoneMap(function()
|
||||
local px, py = GetPlayerMapPosition("player")
|
||||
if not px or not py or (px == 0 and py == 0) then
|
||||
for i = 1, MAX_PARTY do
|
||||
@@ -315,6 +370,7 @@ local function UpdateMinimapDots()
|
||||
if mmDots[i] then mmDots[i]:Hide() end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,26 @@ local function GetOverlayDB()
|
||||
return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
|
||||
end
|
||||
|
||||
local function SafeSetMapToCurrentZone()
|
||||
if not SetMapToCurrentZone then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
|
||||
end
|
||||
return pcall(SetMapToCurrentZone)
|
||||
end
|
||||
|
||||
local function SafeSetMapZoom(continent, zone)
|
||||
if not SetMapZoom then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
|
||||
end
|
||||
return pcall(SetMapZoom, continent, zone)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Persistence: save/load discovered overlay data to SFramesDB
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -428,7 +448,7 @@ local function ProcessScanZone()
|
||||
end
|
||||
|
||||
local entry = scanQueue[scanIndex]
|
||||
SetMapZoom(entry.cont, entry.zone)
|
||||
SafeSetMapZoom(entry.cont, entry.zone)
|
||||
|
||||
local mapFile = GetMapInfo and GetMapInfo() or ""
|
||||
if mapFile == "" then
|
||||
@@ -490,11 +510,11 @@ function MapReveal:FinishScan()
|
||||
end
|
||||
|
||||
if savedMapZ > 0 then
|
||||
SetMapZoom(savedMapC, savedMapZ)
|
||||
SafeSetMapZoom(savedMapC, savedMapZ)
|
||||
elseif savedMapC > 0 then
|
||||
SetMapZoom(savedMapC, 0)
|
||||
SafeSetMapZoom(savedMapC, 0)
|
||||
else
|
||||
if SetMapToCurrentZone then SetMapToCurrentZone() end
|
||||
SafeSetMapToCurrentZone()
|
||||
end
|
||||
|
||||
local cf = DEFAULT_CHAT_FRAME
|
||||
|
||||
55
Media.lua
55
Media.lua
@@ -62,6 +62,27 @@ function SFrames:GetTexture()
|
||||
return self.Media.statusbar
|
||||
end
|
||||
|
||||
function SFrames:ResolveBarTexture(settingKey, fallbackKey)
|
||||
local key
|
||||
if SFramesDB and settingKey then
|
||||
key = SFramesDB[settingKey]
|
||||
end
|
||||
if (not key or key == "") and SFramesDB and fallbackKey then
|
||||
key = SFramesDB[fallbackKey]
|
||||
end
|
||||
if key and key ~= "" then
|
||||
local builtin = self._barTextureLookup[key]
|
||||
if builtin then return builtin end
|
||||
|
||||
local LSM = self:GetSharedMedia()
|
||||
if LSM then
|
||||
local path = LSM:Fetch("statusbar", key, true)
|
||||
if path then return path end
|
||||
end
|
||||
end
|
||||
return self:GetTexture()
|
||||
end
|
||||
|
||||
function SFrames:GetFont()
|
||||
-- 1. Check built-in font key
|
||||
if SFramesDB and SFramesDB.fontKey then
|
||||
@@ -79,6 +100,40 @@ function SFrames:GetFont()
|
||||
return self.Media.font
|
||||
end
|
||||
|
||||
function SFrames:ResolveFont(settingKey, fallbackKey)
|
||||
local key
|
||||
if SFramesDB and settingKey then
|
||||
key = SFramesDB[settingKey]
|
||||
end
|
||||
if (not key or key == "") and SFramesDB and fallbackKey then
|
||||
key = SFramesDB[fallbackKey]
|
||||
end
|
||||
if key and key ~= "" then
|
||||
local builtin = self._fontLookup[key]
|
||||
if builtin then return builtin end
|
||||
local LSM = self:GetSharedMedia()
|
||||
if LSM then
|
||||
local path = LSM:Fetch("font", key, true)
|
||||
if path then return path end
|
||||
end
|
||||
end
|
||||
return self:GetFont()
|
||||
end
|
||||
|
||||
function SFrames:ResolveFontOutline(settingKey, fallbackKey)
|
||||
local outline
|
||||
if SFramesDB and settingKey then
|
||||
outline = SFramesDB[settingKey]
|
||||
end
|
||||
if (not outline or outline == "") and SFramesDB and fallbackKey then
|
||||
outline = SFramesDB[fallbackKey]
|
||||
end
|
||||
if outline and outline ~= "" then
|
||||
return outline
|
||||
end
|
||||
return self.Media.fontOutline or "OUTLINE"
|
||||
end
|
||||
|
||||
function SFrames:GetSharedMediaList(mediaType)
|
||||
local LSM = self:GetSharedMedia()
|
||||
if LSM and LSM.List then return LSM:List(mediaType) end
|
||||
|
||||
62
Minimap.lua
62
Minimap.lua
@@ -93,6 +93,19 @@ local function GetDB()
|
||||
return db
|
||||
end
|
||||
|
||||
local function IsMapStateProtected()
|
||||
if WorldMapFrame and WorldMapFrame:IsVisible() then
|
||||
return true
|
||||
end
|
||||
if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
|
||||
return true
|
||||
end
|
||||
if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function ResolveStyleKey()
|
||||
local key = GetDB().mapStyle or "auto"
|
||||
if key == "auto" then
|
||||
@@ -152,6 +165,34 @@ local function ApplyPosition()
|
||||
end
|
||||
end
|
||||
|
||||
local function HideTooltipIfOwned(frame)
|
||||
if not frame or not GameTooltip or not GameTooltip:IsVisible() then
|
||||
return
|
||||
end
|
||||
if GameTooltip.IsOwned and GameTooltip:IsOwned(frame) then
|
||||
GameTooltip:Hide()
|
||||
return
|
||||
end
|
||||
if GameTooltip.GetOwner and GameTooltip:GetOwner() == frame then
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function ForceHideMinimapFrame(frame)
|
||||
if not frame then return end
|
||||
HideTooltipIfOwned(frame)
|
||||
if frame.EnableMouse then
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
if frame.Hide then
|
||||
frame:Hide()
|
||||
end
|
||||
if frame.SetAlpha then
|
||||
frame:SetAlpha(0)
|
||||
end
|
||||
frame.Show = function() end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hide default Blizzard minimap chrome
|
||||
-- MUST be called AFTER BuildFrame (Minimap is already reparented)
|
||||
@@ -165,15 +206,13 @@ local function HideDefaultElements()
|
||||
MinimapToggleButton,
|
||||
MiniMapWorldMapButton,
|
||||
GameTimeFrame,
|
||||
TimeManagerClockButton,
|
||||
MinimapZoneTextButton,
|
||||
MiniMapTracking,
|
||||
MinimapBackdrop,
|
||||
}
|
||||
for _, f in ipairs(kill) do
|
||||
if f then
|
||||
f:Hide()
|
||||
f.Show = function() end
|
||||
end
|
||||
ForceHideMinimapFrame(f)
|
||||
end
|
||||
|
||||
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
|
||||
@@ -183,10 +222,7 @@ local function HideDefaultElements()
|
||||
}
|
||||
for _, name in ipairs(trackNames) do
|
||||
local f = _G[name]
|
||||
if f and f.Hide then
|
||||
f:Hide()
|
||||
f.Show = function() end
|
||||
end
|
||||
ForceHideMinimapFrame(f)
|
||||
end
|
||||
|
||||
-- Also hide any tracking textures that are children of Minimap
|
||||
@@ -195,8 +231,7 @@ local function HideDefaultElements()
|
||||
for _, child in ipairs(children) do
|
||||
local n = child.GetName and child:GetName()
|
||||
if n and string.find(n, "Track") then
|
||||
child:Hide()
|
||||
child.Show = function() end
|
||||
ForceHideMinimapFrame(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -455,9 +490,16 @@ local function UpdateZoneText()
|
||||
end
|
||||
|
||||
local function SetZoneMap()
|
||||
if IsMapStateProtected() then
|
||||
return
|
||||
end
|
||||
if SetMapToCurrentZone then
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
|
||||
else
|
||||
pcall(SetMapToCurrentZone)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local clockTimer = 0
|
||||
|
||||
@@ -78,6 +78,43 @@ local DEBUFF_TYPE_COLORS = {
|
||||
local DEBUFF_DEFAULT_COLOR = { r = 0.80, g = 0.00, b = 0.00 }
|
||||
local WEAPON_ENCHANT_COLOR = { r = 0.58, g = 0.22, b = 0.82 }
|
||||
|
||||
local function IsTooltipOwnedBy(frame)
|
||||
if not frame or not GameTooltip or not GameTooltip:IsVisible() then
|
||||
return false
|
||||
end
|
||||
if GameTooltip.IsOwned then
|
||||
return GameTooltip:IsOwned(frame)
|
||||
end
|
||||
if GameTooltip.GetOwner then
|
||||
return GameTooltip:GetOwner() == frame
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function HideOwnedTooltip(frame)
|
||||
if IsTooltipOwnedBy(frame) then
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function SetSlotTooltipKey(btn, key)
|
||||
if btn._sfTooltipKey ~= key then
|
||||
HideOwnedTooltip(btn)
|
||||
btn._sfTooltipKey = key
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearSlotState(btn)
|
||||
HideOwnedTooltip(btn)
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = false
|
||||
btn._sfSimLabel = nil
|
||||
btn._sfSimDesc = nil
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
btn._sfTooltipKey = nil
|
||||
end
|
||||
|
||||
local function HideBlizzardBuffs()
|
||||
for i = 0, 23 do
|
||||
local btn = _G["BuffButton" .. i]
|
||||
@@ -170,18 +207,19 @@ local function CreateSlot(parent, namePrefix, index, isBuff)
|
||||
btn:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
btn:SetScript("OnHide", function()
|
||||
HideOwnedTooltip(this)
|
||||
end)
|
||||
btn:SetScript("OnClick", function()
|
||||
if this._sfSimulated then return end
|
||||
if this._isWeaponEnchant then return end
|
||||
if this.isBuff and this.buffIndex and this.buffIndex >= 0 then
|
||||
HideOwnedTooltip(this)
|
||||
CancelPlayerBuff(this.buffIndex)
|
||||
end
|
||||
end)
|
||||
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = false
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
return btn
|
||||
end
|
||||
@@ -339,9 +377,12 @@ function MB:UpdateBuffs()
|
||||
local btn = self.buffSlots[slotIdx]
|
||||
local texture = GetPlayerBuffTexture(buffIndex)
|
||||
if texture then
|
||||
SetSlotTooltipKey(btn, "buff:" .. tostring(buffIndex))
|
||||
btn.icon:SetTexture(texture)
|
||||
btn.buffIndex = buffIndex
|
||||
btn._sfSimulated = false
|
||||
btn._sfSimLabel = nil
|
||||
btn._sfSimDesc = nil
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
|
||||
@@ -360,10 +401,8 @@ function MB:UpdateBuffs()
|
||||
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
|
||||
btn:Show()
|
||||
else
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
btn.buffIndex = -1
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -378,9 +417,12 @@ function MB:UpdateBuffs()
|
||||
local btn = self.buffSlots[slotIdx]
|
||||
local texture = GetInventoryItemTexture("player", 16)
|
||||
if texture then
|
||||
SetSlotTooltipKey(btn, "weapon:16")
|
||||
btn.icon:SetTexture(texture)
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = false
|
||||
btn._sfSimLabel = nil
|
||||
btn._sfSimDesc = nil
|
||||
btn._isWeaponEnchant = true
|
||||
btn._weaponSlotID = 16
|
||||
|
||||
@@ -407,9 +449,12 @@ function MB:UpdateBuffs()
|
||||
local btn = self.buffSlots[slotIdx]
|
||||
local texture = GetInventoryItemTexture("player", 17)
|
||||
if texture then
|
||||
SetSlotTooltipKey(btn, "weapon:17")
|
||||
btn.icon:SetTexture(texture)
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = false
|
||||
btn._sfSimLabel = nil
|
||||
btn._sfSimDesc = nil
|
||||
btn._isWeaponEnchant = true
|
||||
btn._weaponSlotID = 17
|
||||
|
||||
@@ -432,10 +477,8 @@ function MB:UpdateBuffs()
|
||||
|
||||
for j = slotIdx + 1, MAX_BUFFS do
|
||||
local btn = self.buffSlots[j]
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
btn.buffIndex = -1
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
end
|
||||
|
||||
self:UpdateDebuffs()
|
||||
@@ -450,6 +493,7 @@ function MB:UpdateDebuffs()
|
||||
|
||||
if db.showDebuffs == false then
|
||||
for i = 1, MAX_DEBUFFS do
|
||||
ClearSlotState(self.debuffSlots[i])
|
||||
self.debuffSlots[i]:Hide()
|
||||
end
|
||||
if self.debuffContainer then self.debuffContainer:Hide() end
|
||||
@@ -467,9 +511,14 @@ function MB:UpdateDebuffs()
|
||||
local btn = self.debuffSlots[slotIdx]
|
||||
local texture = GetPlayerBuffTexture(buffIndex)
|
||||
if texture then
|
||||
SetSlotTooltipKey(btn, "debuff:" .. tostring(buffIndex))
|
||||
btn.icon:SetTexture(texture)
|
||||
btn.buffIndex = buffIndex
|
||||
btn._sfSimulated = false
|
||||
btn._sfSimLabel = nil
|
||||
btn._sfSimDesc = nil
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
|
||||
local apps = GetPlayerBuffApplications(buffIndex)
|
||||
if apps and apps > 1 then
|
||||
@@ -495,15 +544,15 @@ function MB:UpdateDebuffs()
|
||||
|
||||
btn:Show()
|
||||
else
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
btn.buffIndex = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for j = slotIdx + 1, MAX_DEBUFFS do
|
||||
ClearSlotState(self.debuffSlots[j])
|
||||
self.debuffSlots[j]:Hide()
|
||||
self.debuffSlots[j].buffIndex = -1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -556,6 +605,7 @@ function MB:SimulateBuffs()
|
||||
local btn = self.buffSlots[i]
|
||||
local sim = SIM_BUFFS[i]
|
||||
if sim then
|
||||
SetSlotTooltipKey(btn, "sim-buff:" .. tostring(i))
|
||||
btn.icon:SetTexture(sim.tex)
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = true
|
||||
@@ -578,6 +628,7 @@ function MB:SimulateBuffs()
|
||||
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
|
||||
btn:Show()
|
||||
else
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
end
|
||||
end
|
||||
@@ -586,11 +637,14 @@ function MB:SimulateBuffs()
|
||||
local btn = self.debuffSlots[i]
|
||||
local sim = SIM_DEBUFFS[i]
|
||||
if sim then
|
||||
SetSlotTooltipKey(btn, "sim-debuff:" .. tostring(i))
|
||||
btn.icon:SetTexture(sim.tex)
|
||||
btn.buffIndex = -1
|
||||
btn._sfSimulated = true
|
||||
btn._sfSimLabel = sim.label
|
||||
btn._sfSimDesc = sim.desc
|
||||
btn._isWeaponEnchant = false
|
||||
btn._weaponSlotID = nil
|
||||
|
||||
btn.timer:SetText(sim.time)
|
||||
ApplyTimerColor(btn, sim.time)
|
||||
@@ -601,6 +655,7 @@ function MB:SimulateBuffs()
|
||||
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
|
||||
btn:Show()
|
||||
else
|
||||
ClearSlotState(btn)
|
||||
btn:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
159
Movers.lua
159
Movers.lua
@@ -207,8 +207,12 @@ local function SyncMoverToFrame(name)
|
||||
local w = (frame:GetWidth() or 100) * scale
|
||||
local h = (frame:GetHeight() or 50) * scale
|
||||
|
||||
mover:SetWidth(math.max(w, 72))
|
||||
mover:SetHeight(math.max(h, 40))
|
||||
local minW = 72
|
||||
local minH = 40
|
||||
if h > w * 3 then minW = 40 end
|
||||
if w > h * 3 then minH = 24 end
|
||||
mover:SetWidth(math.max(w, minW))
|
||||
mover:SetHeight(math.max(h, minH))
|
||||
|
||||
local l = frame:GetLeft()
|
||||
local b = frame:GetBottom()
|
||||
@@ -256,17 +260,12 @@ local function SyncFrameToMover(name)
|
||||
local pos = positions[name]
|
||||
if pos then
|
||||
frame:ClearAllPoints()
|
||||
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)
|
||||
|
||||
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
local newL = frame:GetLeft() or 0
|
||||
local newB = frame:GetBottom() or 0
|
||||
local dL = newL * scale - moverL
|
||||
local dB = newB * scale - moverB
|
||||
if math.abs(dL) > 2 or math.abs(dB) > 2 then
|
||||
SFrames:Print(string.format(
|
||||
"|cffff6666[位置偏差]|r |cffaaddff%s|r anchor=%s ofs=(%.1f,%.1f) dL=%.1f dB=%.1f scale=%.2f",
|
||||
entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -720,11 +719,14 @@ local function CreateControlBar()
|
||||
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
|
||||
local titleC = th.title or { 1, 0.88, 1 }
|
||||
|
||||
local ROW_Y_TOP = 12
|
||||
local ROW_Y_BOT = -12
|
||||
|
||||
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
|
||||
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
controlBar:SetFrameLevel(200)
|
||||
controlBar:SetWidth(480)
|
||||
controlBar:SetHeight(44)
|
||||
controlBar:SetHeight(72)
|
||||
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
|
||||
controlBar:SetClampedToScreen(true)
|
||||
controlBar:SetMovable(true)
|
||||
@@ -739,7 +741,7 @@ local function CreateControlBar()
|
||||
|
||||
local title = controlBar:CreateFontString(nil, "OVERLAY")
|
||||
title:SetFont(Font(), 13, "OUTLINE")
|
||||
title:SetPoint("LEFT", controlBar, "LEFT", 14, 0)
|
||||
title:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_TOP)
|
||||
title:SetText("Nanami 布局")
|
||||
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
|
||||
|
||||
@@ -747,17 +749,19 @@ local function CreateControlBar()
|
||||
sep:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||||
sep:SetWidth(1)
|
||||
sep:SetHeight(24)
|
||||
sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0)
|
||||
sep:SetPoint("LEFT", controlBar, "LEFT", 118, ROW_Y_TOP)
|
||||
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
|
||||
|
||||
local bx = 128
|
||||
|
||||
-- Snap toggle
|
||||
-- Snap toggle (row 1)
|
||||
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
|
||||
local cfg = GetLayoutCfg()
|
||||
cfg.snapEnabled = not cfg.snapEnabled
|
||||
if controlBar._updateSnap then controlBar._updateSnap() end
|
||||
end)
|
||||
snapBtn:ClearAllPoints()
|
||||
snapBtn:SetPoint("LEFT", controlBar, "LEFT", bx, ROW_Y_TOP)
|
||||
controlBar.snapBtn = snapBtn
|
||||
|
||||
local function UpdateSnapBtnText()
|
||||
@@ -773,7 +777,7 @@ local function CreateControlBar()
|
||||
end
|
||||
controlBar._updateSnap = UpdateSnapBtnText
|
||||
|
||||
-- Grid toggle
|
||||
-- Grid toggle (row 1)
|
||||
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
|
||||
local cfg = GetLayoutCfg()
|
||||
cfg.showGrid = not cfg.showGrid
|
||||
@@ -782,6 +786,8 @@ local function CreateControlBar()
|
||||
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
|
||||
end
|
||||
end)
|
||||
gridBtn:ClearAllPoints()
|
||||
gridBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 84, ROW_Y_TOP)
|
||||
controlBar.gridBtn = gridBtn
|
||||
|
||||
local function UpdateGridBtnText()
|
||||
@@ -797,18 +803,20 @@ local function CreateControlBar()
|
||||
end
|
||||
controlBar._updateGrid = UpdateGridBtnText
|
||||
|
||||
-- Reset all
|
||||
-- Reset all (row 1)
|
||||
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
|
||||
M:ResetAllMovers()
|
||||
end)
|
||||
resetBtn:ClearAllPoints()
|
||||
resetBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 176, ROW_Y_TOP)
|
||||
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
|
||||
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
|
||||
|
||||
-- Close
|
||||
-- Close (row 1)
|
||||
local closeBtn = CreateFrame("Button", nil, controlBar)
|
||||
closeBtn:SetWidth(60)
|
||||
closeBtn:SetHeight(26)
|
||||
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
|
||||
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, ROW_Y_TOP)
|
||||
closeBtn:SetBackdrop(ROUND_BACKDROP)
|
||||
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
|
||||
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
|
||||
@@ -837,6 +845,64 @@ local function CreateControlBar()
|
||||
end)
|
||||
controlBar.closeBtn = closeBtn
|
||||
|
||||
-- Row separator
|
||||
local rowSep = controlBar:CreateTexture(nil, "ARTWORK")
|
||||
rowSep:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||||
rowSep:SetHeight(1)
|
||||
rowSep:SetPoint("LEFT", controlBar, "LEFT", 10, 0)
|
||||
rowSep:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
|
||||
rowSep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.35)
|
||||
|
||||
-- Row 2: Preset buttons
|
||||
local presetLabel = controlBar:CreateFontString(nil, "OVERLAY")
|
||||
presetLabel:SetFont(Font(), 10, "OUTLINE")
|
||||
presetLabel:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_BOT)
|
||||
presetLabel:SetText("预设:")
|
||||
presetLabel:SetTextColor(0.7, 0.68, 0.78, 1)
|
||||
|
||||
controlBar._presetBtns = {}
|
||||
local AB = SFrames.ActionBars
|
||||
local presets = AB and AB.PRESETS or {}
|
||||
local px = 56
|
||||
for idx = 1, 3 do
|
||||
local p = presets[idx]
|
||||
local pName = p and p.name or ("方案" .. idx)
|
||||
local pDesc = p and p.desc or ""
|
||||
local pId = idx
|
||||
local pbtn = MakeControlButton(controlBar, pName, 80, px + (idx - 1) * 88, function()
|
||||
if AB and AB.ApplyPreset then
|
||||
AB:ApplyPreset(pId)
|
||||
end
|
||||
end)
|
||||
pbtn:ClearAllPoints()
|
||||
pbtn:SetPoint("LEFT", controlBar, "LEFT", px + (idx - 1) * 88, ROW_Y_BOT)
|
||||
pbtn._text:SetFont(Font(), 9, "OUTLINE")
|
||||
pbtn._presetId = pId
|
||||
|
||||
pbtn:SetScript("OnEnter", function()
|
||||
local a2 = T().accent or { 1, 0.5, 0.8 }
|
||||
this:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.95)
|
||||
this._text:SetTextColor(1, 1, 1, 1)
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
|
||||
GameTooltip:AddLine(pName, 1, 0.85, 0.55)
|
||||
GameTooltip:AddLine(pDesc, 0.75, 0.75, 0.85, true)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
pbtn:SetScript("OnLeave", function()
|
||||
local th2 = T()
|
||||
local bd2 = th2.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
|
||||
local tc2 = th2.buttonText or { 0.85, 0.82, 0.92 }
|
||||
this:SetBackdropColor(th2.buttonBg and th2.buttonBg[1] or 0.16,
|
||||
th2.buttonBg and th2.buttonBg[2] or 0.12,
|
||||
th2.buttonBg and th2.buttonBg[3] or 0.22, 0.94)
|
||||
this:SetBackdropBorderColor(bd2[1], bd2[2], bd2[3], bd2[4] or 0.90)
|
||||
this._text:SetTextColor(tc2[1], tc2[2], tc2[3], 1)
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
controlBar._presetBtns[idx] = pbtn
|
||||
end
|
||||
|
||||
controlBar:Hide()
|
||||
return controlBar
|
||||
end
|
||||
@@ -844,7 +910,7 @@ end
|
||||
--------------------------------------------------------------------------------
|
||||
-- Register mover
|
||||
--------------------------------------------------------------------------------
|
||||
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
|
||||
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved, opts)
|
||||
if not name or not frame then return end
|
||||
|
||||
registry[name] = {
|
||||
@@ -856,6 +922,7 @@ function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, de
|
||||
defaultX = defaultX or 0,
|
||||
defaultY = defaultY or 0,
|
||||
onMoved = onMoved,
|
||||
alwaysShowInLayout = opts and opts.alwaysShowInLayout or false,
|
||||
}
|
||||
|
||||
CreateMoverFrame(name, registry[name])
|
||||
@@ -890,10 +957,19 @@ function M:EnterLayoutMode()
|
||||
UIParent:GetWidth(), UIParent:GetHeight(),
|
||||
UIParent:GetRight() or 0, UIParent:GetTop() or 0))
|
||||
|
||||
for name, _ in pairs(registry) do
|
||||
for name, entry in pairs(registry) do
|
||||
local frame = entry and entry.frame
|
||||
local shouldShow = entry.alwaysShowInLayout
|
||||
or (frame and frame.IsShown and frame:IsShown())
|
||||
SyncMoverToFrame(name)
|
||||
local mover = moverFrames[name]
|
||||
if mover then mover:Show() end
|
||||
if mover then
|
||||
if shouldShow then
|
||||
mover:Show()
|
||||
else
|
||||
mover:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
|
||||
@@ -924,6 +1000,22 @@ function M:IsLayoutMode()
|
||||
return isLayoutMode
|
||||
end
|
||||
|
||||
function M:SetMoverAlwaysShow(name, alwaysShow)
|
||||
local entry = registry[name]
|
||||
if entry then
|
||||
entry.alwaysShowInLayout = alwaysShow
|
||||
end
|
||||
local mover = moverFrames[name]
|
||||
if mover and isLayoutMode then
|
||||
if alwaysShow then
|
||||
SyncMoverToFrame(name)
|
||||
mover:Show()
|
||||
else
|
||||
mover:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Reset movers
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -979,13 +1071,30 @@ function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoin
|
||||
local pos = positions[name]
|
||||
if pos and pos.point and pos.relativePoint then
|
||||
frame:ClearAllPoints()
|
||||
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
frame:SetPoint(pos.point, UIParent, pos.relativePoint,
|
||||
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
|
||||
else
|
||||
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||||
end
|
||||
return true
|
||||
else
|
||||
frame:ClearAllPoints()
|
||||
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
|
||||
if relFrame == UIParent then
|
||||
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
|
||||
(defaultX or 0) / fScale, (defaultY or 0) / fScale)
|
||||
else
|
||||
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
|
||||
defaultX or 0, defaultY or 0)
|
||||
end
|
||||
else
|
||||
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
|
||||
defaultX or 0, defaultY or 0)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -996,3 +1105,7 @@ end
|
||||
function M:GetRegistry()
|
||||
return registry
|
||||
end
|
||||
|
||||
function M:SyncMoverToFrame(name)
|
||||
SyncMoverToFrame(name)
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
Bindings.xml
|
||||
Core.lua
|
||||
Config.lua
|
||||
AuraTracker.lua
|
||||
Movers.lua
|
||||
Media.lua
|
||||
IconMap.lua
|
||||
@@ -27,7 +28,6 @@ MapIcons.lua
|
||||
Tweaks.lua
|
||||
MinimapBuffs.lua
|
||||
Focus.lua
|
||||
ClassSkillData.lua
|
||||
Units\Player.lua
|
||||
Units\Pet.lua
|
||||
Units\Target.lua
|
||||
@@ -39,6 +39,7 @@ GearScore.lua
|
||||
Tooltip.lua
|
||||
Units\Raid.lua
|
||||
ActionBars.lua
|
||||
ExtraBar.lua
|
||||
KeyBindManager.lua
|
||||
|
||||
Bags\Offline.lua
|
||||
@@ -57,6 +58,8 @@ QuestLogSkin.lua
|
||||
TrainerUI.lua
|
||||
TradeSkillDB.lua
|
||||
BeastTrainingUI.lua
|
||||
ConsumableDB.lua
|
||||
ConsumableUI.lua
|
||||
TradeSkillUI.lua
|
||||
CharacterPanel.lua
|
||||
StatSummary.lua
|
||||
|
||||
@@ -444,6 +444,10 @@ local function ApplyChoices()
|
||||
SFramesDB.enableChat = c.enableChat
|
||||
if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end
|
||||
SFramesDB.Chat.translateEnabled = c.translateEnabled
|
||||
-- 翻译关闭时,聊天监控也自动关闭
|
||||
if c.translateEnabled == false then
|
||||
SFramesDB.Chat.chatMonitorEnabled = false
|
||||
end
|
||||
SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable
|
||||
|
||||
SFramesDB.enableUnitFrames = c.enableUnitFrames
|
||||
@@ -1316,9 +1320,14 @@ function SW:DoSkip()
|
||||
self:Hide()
|
||||
return
|
||||
end
|
||||
-- First-run: apply defaults
|
||||
-- First-run: apply defaults then initialize
|
||||
choices = GetDefaultChoices()
|
||||
local ok, err = pcall(ApplyChoices)
|
||||
if not ok then
|
||||
if not SFramesDB then SFramesDB = {} end
|
||||
SFramesDB.setupComplete = true
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Wizard skip apply error: "..tostring(err).."|r")
|
||||
end
|
||||
self:Hide()
|
||||
if completeCb then completeCb() end
|
||||
end
|
||||
|
||||
72
Tooltip.lua
72
Tooltip.lua
@@ -115,6 +115,23 @@ local function TT_DifficultyColor(unitLevel)
|
||||
end
|
||||
end
|
||||
|
||||
local function TT_GetClassificationText(unit)
|
||||
if not UnitExists(unit) then return nil end
|
||||
|
||||
local classif = UnitClassification(unit)
|
||||
if classif == "rareelite" then
|
||||
return "|cffc57cff[稀有 精英]|r"
|
||||
elseif classif == "rare" then
|
||||
return "|cffc57cff[稀有]|r"
|
||||
elseif classif == "elite" then
|
||||
return "|cffffa500[精英]|r"
|
||||
elseif classif == "worldboss" then
|
||||
return "|cffff4040[首领]|r"
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Initialize
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -680,6 +697,29 @@ function SFrames.FloatingTooltip:FormatLines(tooltip)
|
||||
GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b)
|
||||
end
|
||||
end
|
||||
|
||||
local classificationText = TT_GetClassificationText(unit)
|
||||
if classificationText then
|
||||
local numLines = tooltip:NumLines()
|
||||
local appended = false
|
||||
for i = 2, numLines do
|
||||
local left = getglobal("GameTooltipTextLeft" .. i)
|
||||
if left then
|
||||
local txt = left:GetText()
|
||||
if txt and (string.find(txt, "^Level ") or string.find(txt, "^等级 ")) then
|
||||
if not string.find(txt, "%[") then
|
||||
left:SetText(txt .. " " .. classificationText)
|
||||
end
|
||||
appended = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not appended then
|
||||
tooltip:AddLine(classificationText)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
@@ -1268,27 +1308,27 @@ function IC:HookTooltips()
|
||||
---------------------------------------------------------------------------
|
||||
-- SetItemRef (chat item links)
|
||||
---------------------------------------------------------------------------
|
||||
local orig_SetItemRef = SetItemRef
|
||||
if orig_SetItemRef then
|
||||
SetItemRef = function(link, text, button)
|
||||
orig_SetItemRef(link, text, button)
|
||||
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
|
||||
if ItemRefTooltip and ItemRefTooltip.SetHyperlink then
|
||||
local orig_ItemRef_SetHyperlink = ItemRefTooltip.SetHyperlink
|
||||
ItemRefTooltip.SetHyperlink = function(self, link)
|
||||
self._nanamiSellPriceAdded = nil
|
||||
self._gsScoreAdded = nil
|
||||
|
||||
local r1, r2, r3, r4 = orig_ItemRef_SetHyperlink(self, link)
|
||||
|
||||
if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then
|
||||
return r1, r2, r3, r4
|
||||
end
|
||||
|
||||
pcall(function()
|
||||
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
|
||||
if itemStr then
|
||||
ItemRefTooltip._nanamiSellPriceAdded = nil
|
||||
local itemId = IC_GetItemIdFromLink(itemStr)
|
||||
local price = IC_QueryAndLearnPrice(itemStr)
|
||||
if price and price > 0 and not ItemRefTooltip.hasMoney then
|
||||
SetTooltipMoney(ItemRefTooltip, price)
|
||||
ItemRefTooltip:Show()
|
||||
end
|
||||
if itemId then
|
||||
ItemRefTooltip:AddLine("物品ID: " .. itemId, 0.55, 0.55, 0.70)
|
||||
ItemRefTooltip:Show()
|
||||
end
|
||||
local moneyAlreadyShown = self.hasMoney
|
||||
IC_EnhanceTooltip(self, itemStr, nil, moneyAlreadyShown)
|
||||
end
|
||||
end)
|
||||
|
||||
return r1, r2, r3, r4
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -962,12 +962,7 @@ function TSUI.CreateReagentSlot(parent, i)
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
local ok
|
||||
if S.currentMode == "craft" then
|
||||
local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex)
|
||||
if link then
|
||||
ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
|
||||
else
|
||||
ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex)
|
||||
end
|
||||
else
|
||||
ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex)
|
||||
end
|
||||
@@ -1521,6 +1516,7 @@ end
|
||||
function TSUI.UpdateProfTabs()
|
||||
TSUI.ScanProfessions()
|
||||
local currentSkillName = API.GetSkillLineName()
|
||||
local numVisible = 0
|
||||
for i = 1, 10 do
|
||||
local tab = S.profTabs[i]
|
||||
if not tab then break end
|
||||
@@ -1544,10 +1540,16 @@ function TSUI.UpdateProfTabs()
|
||||
tab.glow:Hide(); tab.checked:Hide()
|
||||
end
|
||||
tab:Show()
|
||||
numVisible = numVisible + 1
|
||||
else
|
||||
tab.profName = nil; tab.active = false; tab:Hide()
|
||||
end
|
||||
end
|
||||
if S.encBtn and S.MainFrame then
|
||||
S.encBtn:ClearAllPoints()
|
||||
S.encBtn:SetPoint("TOPLEFT", S.MainFrame, "TOPRIGHT", 2,
|
||||
-(6 + numVisible * (42 + 4) + 10))
|
||||
end
|
||||
end
|
||||
|
||||
function TSUI.IsTabSwitching()
|
||||
@@ -1932,6 +1934,24 @@ function TSUI:Initialize()
|
||||
end
|
||||
end)
|
||||
dIF:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
dIF:SetScript("OnMouseUp", function()
|
||||
if IsShiftKeyDown() and S.selectedIndex then
|
||||
local link = API.GetItemLink(S.selectedIndex)
|
||||
if link then
|
||||
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(link)
|
||||
else
|
||||
if ChatFrame_OpenChat then
|
||||
ChatFrame_OpenChat(link)
|
||||
elseif ChatFrameEditBox then
|
||||
ChatFrameEditBox:Show()
|
||||
ChatFrameEditBox:SetText(link)
|
||||
ChatFrameEditBox:SetFocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local dName = det:CreateFontString(nil, "OVERLAY")
|
||||
dName:SetFont(font, 13, "OUTLINE")
|
||||
@@ -2128,6 +2148,52 @@ function TSUI:Initialize()
|
||||
end)
|
||||
TSUI.CreateProfTabs(MF)
|
||||
|
||||
-- ── 食物药剂百科 按钮(职业图标条底部)───────────────────────────────
|
||||
do
|
||||
local TAB_SZ, TAB_GAP, TAB_TOP = 42, 4, 6
|
||||
local encBtn = CreateFrame("Button", nil, MF)
|
||||
encBtn:SetWidth(TAB_SZ); encBtn:SetHeight(TAB_SZ)
|
||||
encBtn:SetPoint("TOPLEFT", MF, "TOPRIGHT", 2, -(TAB_TOP + 10))
|
||||
encBtn:SetFrameStrata("HIGH")
|
||||
encBtn:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 14,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||||
})
|
||||
encBtn:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
|
||||
encBtn:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
|
||||
local ico = encBtn:CreateTexture(nil, "ARTWORK")
|
||||
ico:SetTexture("Interface\\Icons\\INV_Potion_97")
|
||||
ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||
ico:SetPoint("TOPLEFT", encBtn, "TOPLEFT", 4, -4)
|
||||
ico:SetPoint("BOTTOMRIGHT", encBtn, "BOTTOMRIGHT", -4, 4)
|
||||
|
||||
local hl = encBtn:CreateTexture(nil, "HIGHLIGHT")
|
||||
hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
|
||||
hl:SetBlendMode("ADD"); hl:SetAlpha(0.3); hl:SetAllPoints(ico)
|
||||
|
||||
encBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1)
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText("食物药剂百科", 1, 0.82, 0.60)
|
||||
GameTooltip:AddLine("查看各职业消耗品推荐列表", 0.8, 0.8, 0.8)
|
||||
GameTooltip:AddLine("Shift+点击物品可插入聊天", 0.6, 0.6, 0.65)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
encBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
encBtn:SetScript("OnClick", function()
|
||||
if SFrames.ConsumableUI then SFrames.ConsumableUI:Toggle() end
|
||||
end)
|
||||
encBtn:Show()
|
||||
S.encBtn = encBtn
|
||||
end
|
||||
-- ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
MF:Hide()
|
||||
tinsert(UISpecialFrames, "SFramesTradeSkillFrame")
|
||||
end
|
||||
|
||||
150
TrainerUI.lua
150
TrainerUI.lua
@@ -61,10 +61,11 @@ local currentFilter = "all"
|
||||
local displayList = {}
|
||||
local rowButtons = {}
|
||||
local collapsedCats = {}
|
||||
local isTradeskillTrainerCached = false -- Cache to avoid repeated API calls
|
||||
local isTradeskillTrainerCached = false
|
||||
local function HideBlizzardTrainer()
|
||||
if not ClassTrainerFrame then return end
|
||||
ClassTrainerFrame:SetScript("OnHide", function() end)
|
||||
ClassTrainerFrame:UnregisterAllEvents()
|
||||
if ClassTrainerFrame:IsVisible() then
|
||||
if HideUIPanel then
|
||||
pcall(HideUIPanel, ClassTrainerFrame)
|
||||
@@ -161,123 +162,20 @@ end
|
||||
local function GetVerifiedCategory(index)
|
||||
local name, _, category = GetTrainerServiceInfo(index)
|
||||
if not name then return nil end
|
||||
|
||||
-- "used" is always reliable - player already knows this skill
|
||||
if category == "used" then
|
||||
return "used"
|
||||
if category == "available" or category == "unavailable" or category == "used" then
|
||||
return category
|
||||
end
|
||||
|
||||
-- "unavailable" from API should be trusted - it considers:
|
||||
-- - Level requirements
|
||||
-- - Skill rank prerequisites (e.g., need Fireball Rank 1 before Rank 2)
|
||||
-- - Profession skill requirements
|
||||
if category == "unavailable" then
|
||||
return "unavailable"
|
||||
end
|
||||
|
||||
-- For "available", do extra verification only for tradeskill trainers
|
||||
-- Class trainers' "available" is already accurate
|
||||
if category == "available" then
|
||||
-- Additional check for tradeskill trainers (use cached value)
|
||||
if isTradeskillTrainerCached then
|
||||
local playerLevel = UnitLevel("player") or 1
|
||||
|
||||
-- Check level requirement
|
||||
if GetTrainerServiceLevelReq then
|
||||
local ok, levelReq = pcall(GetTrainerServiceLevelReq, index)
|
||||
if ok and levelReq and levelReq > 0 and playerLevel < levelReq then
|
||||
return "unavailable"
|
||||
end
|
||||
end
|
||||
|
||||
-- Check skill requirement
|
||||
if GetTrainerServiceSkillReq then
|
||||
local ok, skillName, skillRank, hasReq = pcall(GetTrainerServiceSkillReq, index)
|
||||
if ok and skillName and skillName ~= "" and not hasReq then
|
||||
return "unavailable"
|
||||
end
|
||||
end
|
||||
end
|
||||
return "available"
|
||||
end
|
||||
|
||||
-- Fallback: unknown category, treat as unavailable
|
||||
return category or "unavailable"
|
||||
end
|
||||
|
||||
local scanTip = nil
|
||||
|
||||
local function GetServiceTooltipInfo(index)
|
||||
if not scanTip then
|
||||
scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
|
||||
end
|
||||
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
scanTip:ClearLines()
|
||||
local ok = pcall(scanTip.SetTrainerService, scanTip, index)
|
||||
if not ok then return "", "" end
|
||||
|
||||
local infoLines = {}
|
||||
local descLines = {}
|
||||
local numLines = scanTip:NumLines()
|
||||
local foundDesc = false
|
||||
|
||||
for i = 2, numLines do
|
||||
local leftFS = _G["SFramesTrainerScanTipTextLeft" .. i]
|
||||
local rightFS = _G["SFramesTrainerScanTipTextRight" .. i]
|
||||
local leftText = leftFS and leftFS:GetText() or ""
|
||||
local rightText = rightFS and rightFS:GetText() or ""
|
||||
|
||||
if leftText == "" and rightText == "" then
|
||||
if not foundDesc and table.getn(infoLines) > 0 then
|
||||
foundDesc = true
|
||||
end
|
||||
else
|
||||
local line
|
||||
if rightText ~= "" and leftText ~= "" then
|
||||
line = leftText .. " " .. rightText
|
||||
elseif leftText ~= "" then
|
||||
line = leftText
|
||||
else
|
||||
line = rightText
|
||||
end
|
||||
|
||||
local isYellow = leftFS and leftFS.GetTextColor and true
|
||||
local r, g, b
|
||||
if leftFS and leftFS.GetTextColor then
|
||||
r, g, b = leftFS:GetTextColor()
|
||||
end
|
||||
local isWhiteOrYellow = r and (r > 0.9 and g > 0.75)
|
||||
|
||||
if not foundDesc and rightText ~= "" then
|
||||
table.insert(infoLines, line)
|
||||
elseif not foundDesc and not isWhiteOrYellow and string.len(leftText) < 30 then
|
||||
table.insert(infoLines, line)
|
||||
else
|
||||
foundDesc = true
|
||||
table.insert(descLines, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
scanTip:Hide()
|
||||
return table.concat(infoLines, "\n"), table.concat(descLines, "\n")
|
||||
return "", ""
|
||||
end
|
||||
|
||||
local function GetServiceQuality(index)
|
||||
if not scanTip then
|
||||
scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
|
||||
end
|
||||
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
scanTip:ClearLines()
|
||||
local ok = pcall(scanTip.SetTrainerService, scanTip, index)
|
||||
if not ok then scanTip:Hide() return nil end
|
||||
local firstLine = _G["SFramesTrainerScanTipTextLeft1"]
|
||||
if not firstLine or not firstLine.GetTextColor then
|
||||
scanTip:Hide()
|
||||
return nil
|
||||
end
|
||||
local r, g, b = firstLine:GetTextColor()
|
||||
scanTip:Hide()
|
||||
return ColorToQuality(r, g, b)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -392,10 +290,7 @@ end
|
||||
local qualityCache = {}
|
||||
|
||||
local function GetCachedServiceQuality(index)
|
||||
if qualityCache[index] ~= nil then return qualityCache[index] end
|
||||
local q = GetServiceQuality(index)
|
||||
qualityCache[index] = q or false
|
||||
return q
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -604,22 +499,8 @@ local function CreateListRow(parent, idx)
|
||||
self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
|
||||
end
|
||||
|
||||
-- Skip quality scan for tradeskill trainers (performance optimization)
|
||||
if not isTradeskillTrainerCached then
|
||||
local quality = GetCachedServiceQuality(svc.index)
|
||||
local qc = QUALITY_COLORS[quality]
|
||||
if qc and quality and quality >= 2 then
|
||||
self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3])
|
||||
self.qualGlow:Show()
|
||||
self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
|
||||
else
|
||||
self.qualGlow:Hide()
|
||||
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
end
|
||||
else
|
||||
self.qualGlow:Hide()
|
||||
self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
end
|
||||
|
||||
local ok, cost = pcall(GetTrainerServiceCost, svc.index)
|
||||
if ok and cost and cost > 0 then
|
||||
@@ -838,13 +719,7 @@ local function UpdateDetail()
|
||||
detail.icon:SetTexture(iconTex)
|
||||
detail.iconFrame:Show()
|
||||
|
||||
local quality = GetServiceQuality(selectedIndex)
|
||||
local qc = QUALITY_COLORS[quality]
|
||||
if qc and quality and quality >= 2 then
|
||||
detail.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
|
||||
else
|
||||
detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
|
||||
end
|
||||
|
||||
detail.nameFS:SetText(name or "")
|
||||
|
||||
@@ -873,13 +748,10 @@ local function UpdateDetail()
|
||||
end
|
||||
detail.reqFS:SetText(table.concat(reqParts, " "))
|
||||
|
||||
local spellInfo, descText = GetServiceTooltipInfo(selectedIndex)
|
||||
detail.infoFS:SetText(spellInfo)
|
||||
detail.descFS:SetText(descText)
|
||||
detail.descDivider:Show()
|
||||
|
||||
local textH = detail.descFS:GetHeight() or 40
|
||||
detail.descScroll:GetScrollChild():SetHeight(math.max(1, textH))
|
||||
detail.infoFS:SetText("")
|
||||
detail.descFS:SetText("")
|
||||
detail.descDivider:Hide()
|
||||
detail.descScroll:GetScrollChild():SetHeight(1)
|
||||
detail.descScroll:SetVerticalScroll(0)
|
||||
|
||||
local canTrain = (category == "available") and cost and (GetMoney() >= cost)
|
||||
@@ -1342,6 +1214,7 @@ function TUI:Initialize()
|
||||
local function CleanupBlizzardTrainer()
|
||||
if not ClassTrainerFrame then return end
|
||||
ClassTrainerFrame:SetScript("OnHide", function() end)
|
||||
ClassTrainerFrame:UnregisterAllEvents()
|
||||
if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end
|
||||
if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end
|
||||
ClassTrainerFrame:SetAlpha(0)
|
||||
@@ -1361,6 +1234,7 @@ function TUI:Initialize()
|
||||
if event == "TRAINER_SHOW" then
|
||||
if ClassTrainerFrame then
|
||||
ClassTrainerFrame:SetScript("OnHide", function() end)
|
||||
ClassTrainerFrame:UnregisterAllEvents()
|
||||
ClassTrainerFrame:SetAlpha(0)
|
||||
ClassTrainerFrame:EnableMouse(false)
|
||||
end
|
||||
|
||||
149
Tweaks.lua
149
Tweaks.lua
@@ -1148,11 +1148,9 @@ end
|
||||
-- unit under the mouse cursor without changing current target.
|
||||
--
|
||||
-- Strategy:
|
||||
-- UseAction hook: temporarily TargetUnit(moUnit) before the real UseAction,
|
||||
-- then restore previous target afterwards. This preserves the hardware
|
||||
-- event callstack so the client doesn't reject the action.
|
||||
-- CastSpellByName hook (SuperWoW): pass moUnit as 2nd arg directly.
|
||||
-- CastSpellByName hook (no SuperWoW): same target-swap trick.
|
||||
-- Temporarily try the mouseover unit first while preserving the original
|
||||
-- target. If the spell cannot resolve on mouseover, stop the pending target
|
||||
-- mode, restore the original target, and retry there.
|
||||
--------------------------------------------------------------------------------
|
||||
local mouseoverCastEnabled = false
|
||||
local origUseAction = nil
|
||||
@@ -1176,6 +1174,70 @@ local function GetMouseoverUnit()
|
||||
return nil
|
||||
end
|
||||
|
||||
local function CaptureTargetState()
|
||||
local hadTarget = UnitExists("target")
|
||||
return {
|
||||
hadTarget = hadTarget,
|
||||
name = hadTarget and UnitName("target") or nil,
|
||||
}
|
||||
end
|
||||
|
||||
local function RestoreTargetState(state)
|
||||
if not state then return end
|
||||
if state.hadTarget and state.name then
|
||||
TargetLastTarget()
|
||||
if not UnitExists("target") or UnitName("target") ~= state.name then
|
||||
TargetByName(state.name, true)
|
||||
end
|
||||
else
|
||||
ClearTarget()
|
||||
end
|
||||
end
|
||||
|
||||
local function ResolvePendingSpellTarget(unit)
|
||||
if not (SpellIsTargeting and SpellIsTargeting()) then
|
||||
return true
|
||||
end
|
||||
|
||||
if unit then
|
||||
if SpellCanTargetUnit then
|
||||
if SpellCanTargetUnit(unit) then
|
||||
SpellTargetUnit(unit)
|
||||
end
|
||||
else
|
||||
SpellTargetUnit(unit)
|
||||
end
|
||||
end
|
||||
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function TryActionOnUnit(unit, action, cursor, onSelf)
|
||||
if not unit then return false end
|
||||
|
||||
if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
|
||||
TargetUnit(unit)
|
||||
end
|
||||
|
||||
origUseAction(action, cursor, onSelf)
|
||||
return ResolvePendingSpellTarget(unit)
|
||||
end
|
||||
|
||||
local function TryCastSpellOnUnit(unit, spell)
|
||||
if not unit then return false end
|
||||
|
||||
if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
|
||||
TargetUnit(unit)
|
||||
end
|
||||
|
||||
origCastSpellByName(spell)
|
||||
return ResolvePendingSpellTarget(unit)
|
||||
end
|
||||
|
||||
local function MouseoverUseAction(action, cursor, onSelf)
|
||||
-- Don't interfere: picking up action, or re-entrant call
|
||||
if cursor == 1 or inMouseoverAction then
|
||||
@@ -1187,42 +1249,24 @@ local function MouseoverUseAction(action, cursor, onSelf)
|
||||
return origUseAction(action, cursor, onSelf)
|
||||
end
|
||||
|
||||
-- Skip if mouseover IS current target (no swap needed)
|
||||
if UnitIsUnit and UnitExists("target") and UnitIsUnit(moUnit, "target") then
|
||||
return origUseAction(action, cursor, onSelf)
|
||||
end
|
||||
local prevTarget = CaptureTargetState()
|
||||
|
||||
-- Remember current target state
|
||||
local hadTarget = UnitExists("target")
|
||||
local prevTargetName = hadTarget and UnitName("target") or nil
|
||||
|
||||
-- Temporarily target the mouseover unit
|
||||
inMouseoverAction = true
|
||||
TargetUnit(moUnit)
|
||||
local castOnMouseover = TryActionOnUnit(moUnit, action, cursor, onSelf)
|
||||
|
||||
-- Execute the real UseAction on the now-targeted mouseover unit
|
||||
origUseAction(action, cursor, onSelf)
|
||||
|
||||
-- Handle ground-targeted spells (Blizzard, Flamestrike, etc.)
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellTargetUnit(moUnit)
|
||||
end
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellStopTargeting()
|
||||
end
|
||||
|
||||
-- Restore previous target
|
||||
if hadTarget and prevTargetName then
|
||||
-- Target back the previous unit
|
||||
TargetLastTarget()
|
||||
-- Verify restoration worked
|
||||
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
|
||||
-- TargetLastTarget failed, try by name
|
||||
TargetByName(prevTargetName, true)
|
||||
RestoreTargetState(prevTarget)
|
||||
|
||||
if not castOnMouseover and prevTarget.hadTarget then
|
||||
origUseAction(action, cursor, onSelf)
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
if not ResolvePendingSpellTarget("target") then
|
||||
SpellStopTargeting()
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Had no target before, clear
|
||||
ClearTarget()
|
||||
end
|
||||
|
||||
inMouseoverAction = false
|
||||
@@ -1244,43 +1288,26 @@ local function MouseoverCastSpellByName(spell, arg2)
|
||||
return origCastSpellByName(spell)
|
||||
end
|
||||
|
||||
-- SuperWoW: direct unit parameter, no target swap needed
|
||||
if SUPERWOW_VERSION then
|
||||
origCastSpellByName(spell, moUnit)
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellTargetUnit(moUnit)
|
||||
end
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
local prevTarget = CaptureTargetState()
|
||||
|
||||
inMouseoverAction = true
|
||||
local castOnMouseover = TryCastSpellOnUnit(moUnit, spell)
|
||||
|
||||
if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellStopTargeting()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- No SuperWoW: target-swap
|
||||
local hadTarget = UnitExists("target")
|
||||
local prevTargetName = hadTarget and UnitName("target") or nil
|
||||
|
||||
if not (hadTarget and UnitIsUnit and UnitIsUnit(moUnit, "target")) then
|
||||
TargetUnit(moUnit)
|
||||
end
|
||||
RestoreTargetState(prevTarget)
|
||||
|
||||
if not castOnMouseover and prevTarget.hadTarget then
|
||||
origCastSpellByName(spell)
|
||||
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellTargetUnit("target")
|
||||
end
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
if not ResolvePendingSpellTarget("target") then
|
||||
SpellStopTargeting()
|
||||
end
|
||||
|
||||
if hadTarget and prevTargetName then
|
||||
TargetLastTarget()
|
||||
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
|
||||
TargetByName(prevTargetName, true)
|
||||
end
|
||||
elseif not hadTarget then
|
||||
ClearTarget()
|
||||
end
|
||||
inMouseoverAction = false
|
||||
end
|
||||
|
||||
local function InitMouseoverCast()
|
||||
|
||||
301
Units/Party.lua
301
Units/Party.lua
@@ -9,6 +9,9 @@ local PARTY_HORIZONTAL_GAP = 8
|
||||
local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
|
||||
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true }
|
||||
|
||||
-- Pre-allocated table reused every UpdateAuras call
|
||||
local _partyDebuffColor = { r = 0, g = 0, b = 0 }
|
||||
|
||||
local function GetIncomingHeals(unit)
|
||||
return SFrames:GetIncomingHeals(unit)
|
||||
end
|
||||
@@ -36,6 +39,12 @@ local function Clamp(value, minValue, maxValue)
|
||||
return value
|
||||
end
|
||||
|
||||
local function SetTextureIfPresent(region, texturePath)
|
||||
if region and region.SetTexture and texturePath then
|
||||
region:SetTexture(texturePath)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Party:GetMetrics()
|
||||
local db = SFramesDB or {}
|
||||
|
||||
@@ -57,6 +66,34 @@ function SFrames.Party:GetMetrics()
|
||||
local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
|
||||
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6)
|
||||
|
||||
local gradientStyle = SFrames:IsGradientStyle()
|
||||
local availablePowerWidth = width - portraitWidth - 5
|
||||
if availablePowerWidth < 40 then
|
||||
availablePowerWidth = 40
|
||||
end
|
||||
|
||||
local rawPowerWidth = tonumber(db.partyPowerWidth)
|
||||
local legacyFullWidth = tonumber(db.partyFrameWidth) or width
|
||||
local legacyPowerWidth = width - portraitWidth - 3
|
||||
local defaultPowerWidth = gradientStyle and width or availablePowerWidth
|
||||
local maxPowerWidth = gradientStyle and width or availablePowerWidth
|
||||
local powerWidth
|
||||
if gradientStyle then
|
||||
-- 渐变风格:能量条始终与血条等宽(全宽)
|
||||
powerWidth = width
|
||||
elseif not rawPowerWidth
|
||||
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
|
||||
or math.abs(rawPowerWidth - legacyPowerWidth) < 0.5
|
||||
or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
|
||||
powerWidth = defaultPowerWidth
|
||||
else
|
||||
powerWidth = rawPowerWidth
|
||||
end
|
||||
powerWidth = Clamp(math.floor(powerWidth + 0.5), 40, maxPowerWidth)
|
||||
|
||||
local powerOffsetX = Clamp(math.floor((tonumber(db.partyPowerOffsetX) or 0) + 0.5), -120, 120)
|
||||
local powerOffsetY = Clamp(math.floor((tonumber(db.partyPowerOffsetY) or 0) + 0.5), -80, 80)
|
||||
|
||||
if healthHeight + powerHeight + 3 > height then
|
||||
powerHeight = height - healthHeight - 3
|
||||
if powerHeight < 6 then
|
||||
@@ -81,16 +118,30 @@ function SFrames.Party:GetMetrics()
|
||||
local valueFont = tonumber(db.partyValueFontSize) or 10
|
||||
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
|
||||
|
||||
local healthFont = tonumber(db.partyHealthFontSize) or valueFont
|
||||
healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
|
||||
|
||||
local powerFont = tonumber(db.partyPowerFontSize) or valueFont
|
||||
powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
|
||||
|
||||
return {
|
||||
width = width,
|
||||
height = height,
|
||||
portraitWidth = portraitWidth,
|
||||
healthHeight = healthHeight,
|
||||
powerHeight = powerHeight,
|
||||
powerWidth = powerWidth,
|
||||
powerOffsetX = powerOffsetX,
|
||||
powerOffsetY = powerOffsetY,
|
||||
powerOnTop = db.partyPowerOnTop == true,
|
||||
horizontalGap = hgap,
|
||||
verticalGap = vgap,
|
||||
nameFont = nameFont,
|
||||
valueFont = valueFont,
|
||||
healthFont = healthFont,
|
||||
powerFont = powerFont,
|
||||
healthTexture = SFrames:ResolveBarTexture("partyHealthTexture", "barTexture"),
|
||||
powerTexture = SFrames:ResolveBarTexture("partyPowerTexture", "barTexture"),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -142,8 +193,8 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
|
||||
|
||||
if frame.power then
|
||||
frame.power:ClearAllPoints()
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
|
||||
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
|
||||
frame.power:SetWidth(metrics.powerWidth)
|
||||
frame.power:SetHeight(metrics.powerHeight)
|
||||
end
|
||||
|
||||
@@ -153,14 +204,114 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
|
||||
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
|
||||
local fontPath = SFrames:GetFont()
|
||||
SFrames:ApplyStatusBarTexture(frame.health, "partyHealthTexture", "barTexture")
|
||||
SFrames:ApplyStatusBarTexture(frame.power, "partyPowerTexture", "barTexture")
|
||||
if frame.health and frame.power then
|
||||
local healthLevel = frame:GetFrameLevel() + 2
|
||||
local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
|
||||
frame.health:SetFrameLevel(healthLevel)
|
||||
frame.power:SetFrameLevel(powerLevel)
|
||||
end
|
||||
SFrames:ApplyConfiguredUnitBackdrop(frame, "party")
|
||||
if frame.pbg then SFrames:ApplyConfiguredUnitBackdrop(frame.pbg, "party", true) end
|
||||
if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "party") end
|
||||
if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "party") end
|
||||
SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
|
||||
|
||||
-- Gradient style preset
|
||||
if SFrames:IsGradientStyle() then
|
||||
-- Hide portrait & its backdrop
|
||||
if frame.portrait then frame.portrait:Hide() end
|
||||
if frame.pbg then SFrames:ClearBackdrop(frame.pbg); frame.pbg:Hide() end
|
||||
-- Strip backdrops
|
||||
SFrames:ClearBackdrop(frame)
|
||||
SFrames:ClearBackdrop(frame.healthBGFrame)
|
||||
SFrames:ClearBackdrop(frame.powerBGFrame)
|
||||
-- Health bar full width
|
||||
if frame.health then
|
||||
frame.health:ClearAllPoints()
|
||||
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
||||
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
|
||||
frame.health:SetHeight(metrics.healthHeight)
|
||||
end
|
||||
-- Power bar full width
|
||||
if frame.power then
|
||||
frame.power:ClearAllPoints()
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -2 + metrics.powerOffsetY)
|
||||
frame.power:SetWidth(metrics.powerWidth)
|
||||
frame.power:SetHeight(metrics.powerHeight)
|
||||
end
|
||||
-- Apply gradient overlays
|
||||
SFrames:ApplyGradientStyle(frame.health)
|
||||
SFrames:ApplyGradientStyle(frame.power)
|
||||
-- Flush BG frames
|
||||
if frame.healthBGFrame then
|
||||
frame.healthBGFrame:ClearAllPoints()
|
||||
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
|
||||
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
if frame.powerBGFrame then
|
||||
frame.powerBGFrame:ClearAllPoints()
|
||||
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
|
||||
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
-- Hide bar backgrounds (transparent)
|
||||
if frame.healthBGFrame then frame.healthBGFrame:Hide() end
|
||||
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
|
||||
if frame.health and frame.health.bg then frame.health.bg:Hide() end
|
||||
if frame.power and frame.power.bg then frame.power.bg:Hide() end
|
||||
else
|
||||
SFrames:RemoveGradientStyle(frame.health)
|
||||
SFrames:RemoveGradientStyle(frame.power)
|
||||
-- Restore bar backgrounds
|
||||
if frame.healthBGFrame then frame.healthBGFrame:Show() end
|
||||
if frame.powerBGFrame then frame.powerBGFrame:Show() end
|
||||
if frame.health and frame.health.bg then frame.health.bg:Show() end
|
||||
if frame.power and frame.power.bg then frame.power.bg:Show() end
|
||||
|
||||
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
|
||||
if use3D then
|
||||
if frame.portrait then frame.portrait:Show() end
|
||||
if frame.pbg then frame.pbg:Show() end
|
||||
else
|
||||
-- Hide portrait area and extend health/power bars to full width
|
||||
if frame.portrait then frame.portrait:Hide() end
|
||||
if frame.pbg then frame.pbg:Hide() end
|
||||
local fullWidth = metrics.width - 2
|
||||
if frame.health then
|
||||
frame.health:ClearAllPoints()
|
||||
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
|
||||
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
|
||||
frame.health:SetHeight(metrics.healthHeight)
|
||||
end
|
||||
if frame.healthBGFrame then
|
||||
frame.healthBGFrame:ClearAllPoints()
|
||||
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1)
|
||||
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
if frame.power then
|
||||
frame.power:SetWidth(fullWidth)
|
||||
end
|
||||
if frame.powerBGFrame then
|
||||
frame.powerBGFrame:ClearAllPoints()
|
||||
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1)
|
||||
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if frame.nameText then
|
||||
frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
|
||||
SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "partyNameFontKey", "fontKey")
|
||||
end
|
||||
if frame.healthText then
|
||||
frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
|
||||
SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "partyHealthFontKey", "fontKey")
|
||||
end
|
||||
if frame.powerText then
|
||||
SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "partyPowerFontKey", "fontKey")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -250,12 +401,13 @@ function SFrames.Party:ApplyLayout()
|
||||
end
|
||||
end
|
||||
|
||||
local auraRowHeight = 24 -- 20px icon + 2px gap above + 2px padding
|
||||
if mode == "horizontal" then
|
||||
self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
|
||||
self.parent:SetHeight(metrics.height)
|
||||
self.parent:SetHeight(metrics.height + auraRowHeight)
|
||||
else
|
||||
self.parent:SetWidth(metrics.width)
|
||||
self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3))
|
||||
self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3) + auraRowHeight)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -433,10 +585,15 @@ function SFrames.Party:Initialize()
|
||||
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
|
||||
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
|
||||
|
||||
f.powerText = SFrames:CreateFontString(f.power, 9, "RIGHT")
|
||||
f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0)
|
||||
|
||||
f.nameText:SetShadowColor(0, 0, 0, 1)
|
||||
f.nameText:SetShadowOffset(1, -1)
|
||||
f.healthText:SetShadowColor(0, 0, 0, 1)
|
||||
f.healthText:SetShadowOffset(1, -1)
|
||||
f.powerText:SetShadowColor(0, 0, 0, 1)
|
||||
f.powerText:SetShadowOffset(1, -1)
|
||||
|
||||
-- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
|
||||
local roleOvr = CreateFrame("Frame", nil, f)
|
||||
@@ -621,6 +778,9 @@ end
|
||||
|
||||
function SFrames.Party:CreateAuras(index)
|
||||
local f = self.frames[index].frame
|
||||
-- Use self.parent (plain Frame) as parent for aura buttons so they are
|
||||
-- never clipped by the party Button frame's boundaries.
|
||||
local auraParent = self.parent
|
||||
f.buffs = {}
|
||||
f.debuffs = {}
|
||||
local size = 20
|
||||
@@ -628,9 +788,10 @@ function SFrames.Party:CreateAuras(index)
|
||||
|
||||
-- Party Buffs
|
||||
for i = 1, 4 do
|
||||
local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f)
|
||||
local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, auraParent)
|
||||
b:SetWidth(size)
|
||||
b:SetHeight(size)
|
||||
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
|
||||
SFrames:CreateUnitBackdrop(b)
|
||||
|
||||
b.icon = b:CreateTexture(nil, "ARTWORK")
|
||||
@@ -649,7 +810,7 @@ function SFrames.Party:CreateAuras(index)
|
||||
end)
|
||||
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
-- Anchored BELOW the frame on the left side
|
||||
-- Anchored BELOW the party frame on the left side
|
||||
if i == 1 then
|
||||
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
|
||||
else
|
||||
@@ -662,9 +823,10 @@ function SFrames.Party:CreateAuras(index)
|
||||
|
||||
-- Debuffs (Starting right after Buffs to remain linear)
|
||||
for i = 1, 4 do
|
||||
local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f)
|
||||
local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, auraParent)
|
||||
b:SetWidth(size)
|
||||
b:SetHeight(size)
|
||||
b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
|
||||
SFrames:CreateUnitBackdrop(b)
|
||||
|
||||
b.icon = b:CreateTexture(nil, "ARTWORK")
|
||||
@@ -726,6 +888,7 @@ end
|
||||
|
||||
|
||||
function SFrames.Party:TickAuras(unit)
|
||||
if self.testing then return end
|
||||
local data = self:GetFrameByUnit(unit)
|
||||
if not data then return end
|
||||
local f = data.frame
|
||||
@@ -772,7 +935,10 @@ function SFrames.Party:UpdateAll()
|
||||
if inRaid and raidFramesEnabled then
|
||||
for i = 1, 4 do
|
||||
if self.frames[i] and self.frames[i].frame then
|
||||
self.frames[i].frame:Hide()
|
||||
local f = self.frames[i].frame
|
||||
f:Hide()
|
||||
if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
|
||||
if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
|
||||
end
|
||||
end
|
||||
if self._globalUpdateFrame then
|
||||
@@ -793,6 +959,8 @@ function SFrames.Party:UpdateAll()
|
||||
hasVisible = true
|
||||
else
|
||||
f:Hide()
|
||||
if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
|
||||
if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
|
||||
end
|
||||
end
|
||||
if self._globalUpdateFrame then
|
||||
@@ -812,10 +980,15 @@ function SFrames.Party:UpdateFrame(unit)
|
||||
if not data then return end
|
||||
local f = data.frame
|
||||
|
||||
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
|
||||
if use3D then
|
||||
f.portrait:SetUnit(unit)
|
||||
f.portrait:SetCamera(0)
|
||||
f.portrait:Hide()
|
||||
f.portrait:Show()
|
||||
else
|
||||
f.portrait:Hide()
|
||||
end
|
||||
|
||||
local name = UnitName(unit) or ""
|
||||
local level = UnitLevel(unit)
|
||||
@@ -841,6 +1014,10 @@ function SFrames.Party:UpdateFrame(unit)
|
||||
f.nameText:SetTextColor(1, 1, 1)
|
||||
end
|
||||
end
|
||||
-- Re-apply gradient after color change
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(f.health)
|
||||
end
|
||||
|
||||
-- Update Leader/Master Looter
|
||||
if GetPartyLeaderIndex() == data.index then
|
||||
@@ -870,6 +1047,8 @@ function SFrames.Party:UpdatePortrait(unit)
|
||||
local data = self:GetFrameByUnit(unit)
|
||||
if not data then return end
|
||||
local f = data.frame
|
||||
local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
|
||||
if not use3D then return end
|
||||
f.portrait:SetUnit(unit)
|
||||
f.portrait:SetCamera(0)
|
||||
f.portrait:Hide()
|
||||
@@ -917,14 +1096,8 @@ function SFrames.Party:UpdateHealPrediction(unit)
|
||||
local predOther = f.health.healPredOther
|
||||
local predOver = f.health.healPredOver
|
||||
|
||||
local function HidePredictions()
|
||||
predMine:Hide()
|
||||
predOther:Hide()
|
||||
predOver:Hide()
|
||||
end
|
||||
|
||||
if not UnitExists(unit) or not UnitIsConnected(unit) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -946,7 +1119,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
|
||||
end
|
||||
|
||||
if maxHp <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -963,7 +1136,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
|
||||
end
|
||||
local missing = maxHp - hp
|
||||
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -971,13 +1144,19 @@ function SFrames.Party:UpdateHealPrediction(unit)
|
||||
local remaining = missing - mineShown
|
||||
local otherShown = math.min(math.max(0, othersIncoming), remaining)
|
||||
if mineIncoming <= 0 and othersIncoming <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
|
||||
local use3DForPred = not (SFramesDB and SFramesDB.partyPortrait3D == false)
|
||||
local barWidth
|
||||
if use3DForPred then
|
||||
barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
|
||||
else
|
||||
barWidth = f:GetWidth() - 2
|
||||
end
|
||||
if barWidth <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1054,6 +1233,9 @@ function SFrames.Party:UpdatePowerType(unit)
|
||||
else
|
||||
f.power:SetStatusBarColor(0, 0, 1)
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(f.power)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Party:UpdatePower(unit)
|
||||
@@ -1064,6 +1246,7 @@ function SFrames.Party:UpdatePower(unit)
|
||||
if not UnitIsConnected(unit) then
|
||||
f.power:SetMinMaxValues(0, 100)
|
||||
f.power:SetValue(0)
|
||||
if f.powerText then f.powerText:SetText("") end
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1071,6 +1254,14 @@ function SFrames.Party:UpdatePower(unit)
|
||||
local maxPower = UnitManaMax(unit)
|
||||
f.power:SetMinMaxValues(0, maxPower)
|
||||
f.power:SetValue(power)
|
||||
if f.powerText then
|
||||
if maxPower and maxPower > 0 then
|
||||
f.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
|
||||
else
|
||||
f.powerText:SetText("")
|
||||
end
|
||||
end
|
||||
SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
|
||||
end
|
||||
|
||||
function SFrames.Party:UpdateRaidIcons()
|
||||
@@ -1106,6 +1297,7 @@ function SFrames.Party:UpdateRaidIcon(unit)
|
||||
end
|
||||
|
||||
function SFrames.Party:UpdateAuras(unit)
|
||||
if self.testing then return end
|
||||
local data = self:GetFrameByUnit(unit)
|
||||
if not data then return end
|
||||
local f = data.frame
|
||||
@@ -1114,7 +1306,9 @@ function SFrames.Party:UpdateAuras(unit)
|
||||
local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
|
||||
|
||||
local hasDebuff = false
|
||||
local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
|
||||
_partyDebuffColor.r = _A.slotBg[1]
|
||||
_partyDebuffColor.g = _A.slotBg[2]
|
||||
_partyDebuffColor.b = _A.slotBg[3]
|
||||
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
|
||||
@@ -1128,10 +1322,10 @@ function SFrames.Party:UpdateAuras(unit)
|
||||
if texture then
|
||||
if debuffType then
|
||||
hasDebuff = true
|
||||
if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
|
||||
elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
|
||||
elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
|
||||
elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
|
||||
if debuffType == "Magic" then _partyDebuffColor.r = 0.2; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 1
|
||||
elseif debuffType == "Curse" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0; _partyDebuffColor.b = 1
|
||||
elseif debuffType == "Disease" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0.4; _partyDebuffColor.b = 0
|
||||
elseif debuffType == "Poison" then _partyDebuffColor.r = 0; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 0
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1174,7 +1368,7 @@ function SFrames.Party:UpdateAuras(unit)
|
||||
end
|
||||
|
||||
if hasDebuff then
|
||||
f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
|
||||
f.health.bg:SetVertexColor(_partyDebuffColor.r, _partyDebuffColor.g, _partyDebuffColor.b, 1)
|
||||
else
|
||||
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
|
||||
end
|
||||
@@ -1254,9 +1448,37 @@ function SFrames.Party:TestMode()
|
||||
f.masterIcon:Show()
|
||||
end
|
||||
|
||||
-- Show one dummy debuff to test positioning
|
||||
f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain")
|
||||
f.debuffs[1]:Show()
|
||||
-- Show test buffs (all 4)
|
||||
local testBuffIcons = {
|
||||
"Interface\\Icons\\Spell_Holy_PowerWordFortitude",
|
||||
"Interface\\Icons\\Spell_Holy_Renew",
|
||||
"Interface\\Icons\\Spell_Holy_GreaterHeal",
|
||||
"Interface\\Icons\\Spell_Nature_Abolishmagic",
|
||||
}
|
||||
for j = 1, 4 do
|
||||
local fakeTime = math.random(30, 300)
|
||||
f.buffs[j].icon:SetTexture(testBuffIcons[j])
|
||||
f.buffs[j].expirationTime = GetTime() + fakeTime
|
||||
f.buffs[j].cdText:SetText(SFrames:FormatTime(fakeTime))
|
||||
f.buffs[j]:Show()
|
||||
end
|
||||
|
||||
-- Show test debuffs (all 4, different types for color test)
|
||||
local testDebuffIcons = {
|
||||
"Interface\\Icons\\Spell_Shadow_ShadowWordPain", -- Magic (blue)
|
||||
"Interface\\Icons\\Spell_Shadow_Curse", -- Curse (purple)
|
||||
"Interface\\Icons\\Ability_Rogue_FeignDeath", -- Disease (brown)
|
||||
"Interface\\Icons\\Ability_Poisoning", -- Poison (green)
|
||||
}
|
||||
for j = 1, 4 do
|
||||
local debuffTime = math.random(5, 25)
|
||||
f.debuffs[j].icon:SetTexture(testDebuffIcons[j])
|
||||
f.debuffs[j].expirationTime = GetTime() + debuffTime
|
||||
f.debuffs[j].cdText:SetText(SFrames:FormatTime(debuffTime))
|
||||
f.debuffs[j]:Show()
|
||||
end
|
||||
-- Magic debuff background color
|
||||
f.health.bg:SetVertexColor(0.2, 0.6, 1, 1)
|
||||
|
||||
-- Test pet
|
||||
if f.petFrame then
|
||||
@@ -1268,9 +1490,18 @@ function SFrames.Party:TestMode()
|
||||
end
|
||||
end
|
||||
else
|
||||
self:UpdateAll()
|
||||
for i = 1, 4 do
|
||||
self.frames[i].frame.debuffs[1]:Hide()
|
||||
local f = self.frames[i].frame
|
||||
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
|
||||
for j = 1, 4 do
|
||||
f.buffs[j].expirationTime = nil
|
||||
f.buffs[j].cdText:SetText("")
|
||||
f.buffs[j]:Hide()
|
||||
f.debuffs[j].expirationTime = nil
|
||||
f.debuffs[j].cdText:SetText("")
|
||||
f.debuffs[j]:Hide()
|
||||
end
|
||||
end
|
||||
self:UpdateAll()
|
||||
end
|
||||
end
|
||||
|
||||
139
Units/Pet.lua
139
Units/Pet.lua
@@ -226,16 +226,22 @@ function SFrames.Pet:Initialize()
|
||||
f:SetWidth(150)
|
||||
f:SetHeight(30)
|
||||
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
|
||||
local pos = SFramesDB.Positions["PetFrame"]
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
|
||||
else
|
||||
f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 10, -55)
|
||||
end
|
||||
|
||||
local frameScale = (SFramesDB and type(SFramesDB.petFrameScale) == "number") and SFramesDB.petFrameScale or 1
|
||||
f:SetScale(Clamp(frameScale, 0.7, 1.8))
|
||||
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
|
||||
local pos = SFramesDB.Positions["PetFrame"]
|
||||
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint,
|
||||
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
|
||||
else
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||||
end
|
||||
else
|
||||
f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 0, -75)
|
||||
end
|
||||
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:RegisterForDrag("LeftButton")
|
||||
@@ -245,6 +251,11 @@ function SFrames.Pet:Initialize()
|
||||
if not SFramesDB then SFramesDB = {} end
|
||||
if not SFramesDB.Positions then SFramesDB.Positions = {} end
|
||||
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
|
||||
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
xOfs = (xOfs or 0) * fScale
|
||||
yOfs = (yOfs or 0) * fScale
|
||||
end
|
||||
SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
|
||||
end)
|
||||
|
||||
@@ -294,6 +305,8 @@ function SFrames.Pet:Initialize()
|
||||
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
|
||||
SFrames:CreateUnitBackdrop(hbg)
|
||||
|
||||
f.healthBGFrame = hbg
|
||||
|
||||
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
|
||||
f.health.bg:SetAllPoints()
|
||||
f.health.bg:SetTexture(SFrames:GetTexture())
|
||||
@@ -328,6 +341,7 @@ function SFrames.Pet:Initialize()
|
||||
pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
|
||||
pbg:SetFrameLevel(f:GetFrameLevel() - 1)
|
||||
SFrames:CreateUnitBackdrop(pbg)
|
||||
f.powerBGFrame = pbg
|
||||
|
||||
f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
|
||||
f.power.bg:SetAllPoints()
|
||||
@@ -394,11 +408,13 @@ function SFrames.Pet:Initialize()
|
||||
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
|
||||
|
||||
self:InitFoodFeature()
|
||||
self:ApplyConfig()
|
||||
self:UpdateAll()
|
||||
|
||||
if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
|
||||
SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物",
|
||||
"TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 10, -55)
|
||||
"TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 0, -75,
|
||||
nil, { alwaysShowInLayout = true })
|
||||
end
|
||||
|
||||
if StaticPopup_Show then
|
||||
@@ -413,6 +429,90 @@ function SFrames.Pet:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Pet:ApplyConfig()
|
||||
if not self.frame then return end
|
||||
local f = self.frame
|
||||
|
||||
-- Apply bar textures
|
||||
SFrames:ApplyStatusBarTexture(f.health, "petHealthTexture", "barTexture")
|
||||
SFrames:ApplyStatusBarTexture(f.power, "petPowerTexture", "barTexture")
|
||||
local healthTex = SFrames:ResolveBarTexture("petHealthTexture", "barTexture")
|
||||
local powerTex = SFrames:ResolveBarTexture("petPowerTexture", "barTexture")
|
||||
if f.health and f.health.bg then f.health.bg:SetTexture(healthTex) end
|
||||
if f.power and f.power.bg then f.power.bg:SetTexture(powerTex) end
|
||||
|
||||
if SFrames:IsGradientStyle() then
|
||||
-- Strip backdrops
|
||||
SFrames:ClearBackdrop(f)
|
||||
SFrames:ClearBackdrop(f.healthBGFrame)
|
||||
SFrames:ClearBackdrop(f.powerBGFrame)
|
||||
-- Health bar full width
|
||||
if f.health then
|
||||
f.health:ClearAllPoints()
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
|
||||
f.health:SetHeight(18)
|
||||
end
|
||||
-- Power bar full width
|
||||
if f.power then
|
||||
f.power:ClearAllPoints()
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -2)
|
||||
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
-- Apply gradient overlays
|
||||
SFrames:ApplyGradientStyle(f.health)
|
||||
SFrames:ApplyGradientStyle(f.power)
|
||||
-- Flush BG frames
|
||||
if f.healthBGFrame then
|
||||
f.healthBGFrame:ClearAllPoints()
|
||||
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
|
||||
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
if f.powerBGFrame then
|
||||
f.powerBGFrame:ClearAllPoints()
|
||||
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
|
||||
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
-- Hide bar backgrounds (transparent)
|
||||
if f.healthBGFrame then f.healthBGFrame:Hide() end
|
||||
if f.powerBGFrame then f.powerBGFrame:Hide() end
|
||||
if f.health and f.health.bg then f.health.bg:Hide() end
|
||||
if f.power and f.power.bg then f.power.bg:Hide() end
|
||||
else
|
||||
-- Classic style: restore backdrops
|
||||
SFrames:CreateUnitBackdrop(f)
|
||||
if f.health and f.health.bg then f.health.bg:Show() end
|
||||
if f.power and f.power.bg then f.power.bg:Show() end
|
||||
if f.healthBGFrame then
|
||||
SFrames:CreateUnitBackdrop(f.healthBGFrame)
|
||||
f.healthBGFrame:Show()
|
||||
f.healthBGFrame:ClearAllPoints()
|
||||
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
|
||||
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
if f.powerBGFrame then
|
||||
SFrames:CreateUnitBackdrop(f.powerBGFrame)
|
||||
f.powerBGFrame:Show()
|
||||
f.powerBGFrame:ClearAllPoints()
|
||||
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
|
||||
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
if f.health then
|
||||
f.health:ClearAllPoints()
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
|
||||
f.health:SetHeight(18)
|
||||
end
|
||||
if f.power then
|
||||
f.power:ClearAllPoints()
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
|
||||
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
|
||||
end
|
||||
SFrames:RemoveGradientStyle(f.health)
|
||||
SFrames:RemoveGradientStyle(f.power)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Pet:UpdateAll()
|
||||
if UnitExists("pet") then
|
||||
if SFramesDB and SFramesDB.showPetFrame == false then
|
||||
@@ -437,6 +537,9 @@ function SFrames.Pet:UpdateAll()
|
||||
|
||||
local r, g, b = 0.33, 0.59, 0.33
|
||||
self.frame.health:SetStatusBarColor(r, g, b)
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.health)
|
||||
end
|
||||
else
|
||||
self.frame:Hide()
|
||||
if self.foodPanel then self.foodPanel:Hide() end
|
||||
@@ -465,12 +568,6 @@ function SFrames.Pet:UpdateHealPrediction()
|
||||
local predOther = self.frame.health.healPredOther
|
||||
local predOver = self.frame.health.healPredOver
|
||||
|
||||
local function HidePredictions()
|
||||
predMine:Hide()
|
||||
predOther:Hide()
|
||||
predOver:Hide()
|
||||
end
|
||||
|
||||
local hp = UnitHealth("pet") or 0
|
||||
local maxHp = UnitHealthMax("pet") or 0
|
||||
|
||||
@@ -485,7 +582,7 @@ function SFrames.Pet:UpdateHealPrediction()
|
||||
end
|
||||
|
||||
if maxHp <= 0 or UnitIsDeadOrGhost("pet") then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -497,7 +594,7 @@ function SFrames.Pet:UpdateHealPrediction()
|
||||
|
||||
local missing = maxHp - hp
|
||||
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -505,13 +602,13 @@ function SFrames.Pet:UpdateHealPrediction()
|
||||
local remaining = missing - mineShown
|
||||
local otherShown = math.min(math.max(0, othersIncoming), remaining)
|
||||
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local barWidth = self.frame.health:GetWidth()
|
||||
if barWidth <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -521,7 +618,7 @@ function SFrames.Pet:UpdateHealPrediction()
|
||||
|
||||
local availableWidth = barWidth - currentWidth
|
||||
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -587,6 +684,9 @@ function SFrames.Pet:UpdatePowerType()
|
||||
else
|
||||
self.frame.power:SetStatusBarColor(0, 0, 1)
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.power)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Pet:UpdatePower()
|
||||
@@ -594,6 +694,7 @@ function SFrames.Pet:UpdatePower()
|
||||
local maxPower = UnitManaMax("pet")
|
||||
self.frame.power:SetMinMaxValues(0, maxPower)
|
||||
self.frame.power:SetValue(power)
|
||||
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "pet")
|
||||
end
|
||||
|
||||
function SFrames.Pet:UpdateHappiness()
|
||||
|
||||
866
Units/Player.lua
866
Units/Player.lua
File diff suppressed because it is too large
Load Diff
297
Units/Raid.lua
297
Units/Raid.lua
@@ -9,10 +9,76 @@ local UNIT_PADDING = 2
|
||||
local RAID_UNIT_LOOKUP = {}
|
||||
for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end
|
||||
|
||||
-- Pre-allocated tables reused every UpdateAuras call to avoid per-call garbage
|
||||
local _foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
|
||||
local _debuffColor = { r = 0, g = 0, b = 0 }
|
||||
|
||||
-- Module-level helper: match aura name against a list (no closure allocation)
|
||||
local function MatchesList(auraName, list)
|
||||
for _, name in ipairs(list) do
|
||||
if string.find(auraName, name) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Module-level helper: get buff name via SuperWoW aura ID or tooltip scan
|
||||
local function RaidGetBuffName(unit, index)
|
||||
if SFrames.superwow_active and SpellInfo then
|
||||
local texture, auraID = UnitBuff(unit, index)
|
||||
if auraID and SpellInfo then
|
||||
local spellName = SpellInfo(auraID)
|
||||
if spellName and spellName ~= "" then
|
||||
return spellName, texture
|
||||
end
|
||||
end
|
||||
end
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:SetUnitBuff(unit, index)
|
||||
local buffName = SFramesScanTooltipTextLeft1:GetText()
|
||||
SFrames.Tooltip:Hide()
|
||||
return buffName, UnitBuff(unit, index)
|
||||
end
|
||||
|
||||
local function RaidGetDebuffName(unit, index)
|
||||
if SFrames.superwow_active and SpellInfo then
|
||||
local texture, count, dtype, auraID = UnitDebuff(unit, index)
|
||||
if auraID and SpellInfo then
|
||||
local spellName = SpellInfo(auraID)
|
||||
if spellName and spellName ~= "" then
|
||||
return spellName, texture, count, dtype
|
||||
end
|
||||
end
|
||||
end
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:SetUnitDebuff(unit, index)
|
||||
local debuffName = SFramesScanTooltipTextLeft1:GetText()
|
||||
SFrames.Tooltip:Hide()
|
||||
local texture, count, dtype = UnitDebuff(unit, index)
|
||||
return debuffName, texture, count, dtype
|
||||
end
|
||||
|
||||
local function GetIncomingHeals(unit)
|
||||
return SFrames:GetIncomingHeals(unit)
|
||||
end
|
||||
|
||||
local function Clamp(value, minValue, maxValue)
|
||||
if value < minValue then
|
||||
return minValue
|
||||
end
|
||||
if value > maxValue then
|
||||
return maxValue
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function SetTextureIfPresent(region, texturePath)
|
||||
if region and region.SetTexture and texturePath then
|
||||
region:SetTexture(texturePath)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Raid:GetMetrics()
|
||||
local db = SFramesDB or {}
|
||||
|
||||
@@ -25,29 +91,66 @@ function SFrames.Raid:GetMetrics()
|
||||
local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8)
|
||||
healthHeight = math.max(10, math.min(height - 6, healthHeight))
|
||||
|
||||
local powerHeight = height - healthHeight - 3
|
||||
local powerHeight = tonumber(db.raidPowerHeight) or (height - healthHeight - 3)
|
||||
powerHeight = math.max(0, math.min(height - 3, powerHeight))
|
||||
if not db.raidShowPower then
|
||||
powerHeight = 0
|
||||
healthHeight = height - 2
|
||||
end
|
||||
|
||||
local gradientStyle = SFrames:IsGradientStyle()
|
||||
local availablePowerWidth = width - 2
|
||||
if availablePowerWidth < 20 then
|
||||
availablePowerWidth = 20
|
||||
end
|
||||
|
||||
local rawPowerWidth = tonumber(db.raidPowerWidth)
|
||||
local legacyFullWidth = tonumber(db.raidFrameWidth) or width
|
||||
local defaultPowerWidth = gradientStyle and width or availablePowerWidth
|
||||
local maxPowerWidth = gradientStyle and width or availablePowerWidth
|
||||
local powerWidth
|
||||
if gradientStyle then
|
||||
-- 渐变风格:能量条始终与血条等宽(全宽)
|
||||
powerWidth = width
|
||||
elseif not rawPowerWidth
|
||||
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
|
||||
or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
|
||||
powerWidth = defaultPowerWidth
|
||||
else
|
||||
powerWidth = rawPowerWidth
|
||||
end
|
||||
powerWidth = Clamp(math.floor(powerWidth + 0.5), 20, maxPowerWidth)
|
||||
|
||||
local powerOffsetX = Clamp(math.floor((tonumber(db.raidPowerOffsetX) or 0) + 0.5), -120, 120)
|
||||
local powerOffsetY = Clamp(math.floor((tonumber(db.raidPowerOffsetY) or 0) + 0.5), -80, 80)
|
||||
|
||||
local hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING
|
||||
local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING
|
||||
local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING
|
||||
|
||||
local nameFont = tonumber(db.raidNameFontSize) or 10
|
||||
local valueFont = tonumber(db.raidValueFontSize) or 9
|
||||
local healthFont = tonumber(db.raidHealthFontSize) or valueFont
|
||||
local powerFont = tonumber(db.raidPowerFontSize) or valueFont
|
||||
|
||||
return {
|
||||
width = width,
|
||||
height = height,
|
||||
healthHeight = healthHeight,
|
||||
powerHeight = powerHeight,
|
||||
powerWidth = powerWidth,
|
||||
powerOffsetX = powerOffsetX,
|
||||
powerOffsetY = powerOffsetY,
|
||||
powerOnTop = db.raidPowerOnTop == true,
|
||||
horizontalGap = hgap,
|
||||
verticalGap = vgap,
|
||||
groupGap = groupGap,
|
||||
nameFont = nameFont,
|
||||
valueFont = valueFont,
|
||||
healthFont = healthFont,
|
||||
powerFont = powerFont,
|
||||
healthTexture = SFrames:ResolveBarTexture("raidHealthTexture", "barTexture"),
|
||||
powerTexture = SFrames:ResolveBarTexture("raidPowerTexture", "barTexture"),
|
||||
showPower = db.raidShowPower ~= false,
|
||||
}
|
||||
end
|
||||
@@ -87,8 +190,8 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
|
||||
frame.power:Show()
|
||||
if frame.powerBGFrame then frame.powerBGFrame:Show() end
|
||||
frame.power:ClearAllPoints()
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
|
||||
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
|
||||
frame.power:SetWidth(metrics.powerWidth)
|
||||
frame.power:SetHeight(metrics.powerHeight)
|
||||
else
|
||||
frame.power:Hide()
|
||||
@@ -96,14 +199,80 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
|
||||
end
|
||||
end
|
||||
|
||||
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
|
||||
local fontPath = SFrames:GetFont()
|
||||
SFrames:ApplyStatusBarTexture(frame.health, "raidHealthTexture", "barTexture")
|
||||
SFrames:ApplyStatusBarTexture(frame.power, "raidPowerTexture", "barTexture")
|
||||
if frame.health and frame.power then
|
||||
local healthLevel = frame:GetFrameLevel() + 2
|
||||
local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
|
||||
frame.health:SetFrameLevel(healthLevel)
|
||||
frame.power:SetFrameLevel(powerLevel)
|
||||
end
|
||||
SFrames:ApplyConfiguredUnitBackdrop(frame, "raid")
|
||||
if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "raid") end
|
||||
if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "raid") end
|
||||
SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
|
||||
SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
|
||||
|
||||
-- Gradient style preset
|
||||
if SFrames:IsGradientStyle() then
|
||||
-- Strip backdrops
|
||||
SFrames:ClearBackdrop(frame)
|
||||
SFrames:ClearBackdrop(frame.healthBGFrame)
|
||||
SFrames:ClearBackdrop(frame.powerBGFrame)
|
||||
-- Health bar full width
|
||||
if frame.health then
|
||||
frame.health:ClearAllPoints()
|
||||
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
||||
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
|
||||
frame.health:SetHeight(metrics.healthHeight)
|
||||
end
|
||||
-- Power bar full width
|
||||
if frame.power and metrics.showPower then
|
||||
frame.power:ClearAllPoints()
|
||||
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
|
||||
frame.power:SetWidth(metrics.powerWidth)
|
||||
frame.power:SetHeight(metrics.powerHeight)
|
||||
end
|
||||
-- Apply gradient overlays
|
||||
SFrames:ApplyGradientStyle(frame.health)
|
||||
SFrames:ApplyGradientStyle(frame.power)
|
||||
-- Flush BG frames
|
||||
if frame.healthBGFrame then
|
||||
frame.healthBGFrame:ClearAllPoints()
|
||||
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
|
||||
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
if frame.powerBGFrame then
|
||||
frame.powerBGFrame:ClearAllPoints()
|
||||
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
|
||||
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
-- Hide bar backgrounds (transparent)
|
||||
if frame.healthBGFrame then frame.healthBGFrame:Hide() end
|
||||
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
|
||||
if frame.health and frame.health.bg then frame.health.bg:Hide() end
|
||||
if frame.power and frame.power.bg then frame.power.bg:Hide() end
|
||||
else
|
||||
SFrames:RemoveGradientStyle(frame.health)
|
||||
SFrames:RemoveGradientStyle(frame.power)
|
||||
-- Restore bar backgrounds
|
||||
if frame.healthBGFrame then frame.healthBGFrame:Show() end
|
||||
if frame.powerBGFrame then frame.powerBGFrame:Show() end
|
||||
if frame.health and frame.health.bg then frame.health.bg:Show() end
|
||||
if frame.power and frame.power.bg then frame.power.bg:Show() end
|
||||
end
|
||||
|
||||
if frame.nameText then
|
||||
frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
|
||||
SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "raidNameFontKey", "fontKey")
|
||||
end
|
||||
if frame.healthText then
|
||||
frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
|
||||
SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "raidHealthFontKey", "fontKey")
|
||||
end
|
||||
if frame.powerText then
|
||||
SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "raidPowerFontKey", "fontKey")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -724,6 +893,9 @@ function SFrames.Raid:UpdateFrame(unit)
|
||||
f.nameText:SetTextColor(1, 1, 1)
|
||||
end
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(f.health)
|
||||
end
|
||||
|
||||
self:UpdateHealth(unit)
|
||||
self:UpdatePower(unit)
|
||||
@@ -794,11 +966,10 @@ function SFrames.Raid:UpdateHealth(unit)
|
||||
txt = percent .. "%"
|
||||
elseif db.raidHealthFormat == "deficit" then
|
||||
if maxHp - hp > 0 then
|
||||
txt = "-" .. (maxHp - hp)
|
||||
txt = "-" .. SFrames:FormatCompactNumber(maxHp - hp)
|
||||
end
|
||||
else
|
||||
txt = (math.floor(hp/100)/10).."k" -- default compact e.g. 4.5k
|
||||
if hp < 1000 then txt = tostring(hp) end
|
||||
txt = SFrames:FormatCompactNumber(hp)
|
||||
end
|
||||
|
||||
f.healthText:SetText(txt)
|
||||
@@ -826,14 +997,8 @@ function SFrames.Raid:UpdateHealPrediction(unit)
|
||||
local predOther = f.health.healPredOther
|
||||
local predOver = f.health.healPredOver
|
||||
|
||||
local function HidePredictions()
|
||||
predMine:Hide()
|
||||
predOther:Hide()
|
||||
predOver:Hide()
|
||||
end
|
||||
|
||||
if not UnitExists(unit) or not UnitIsConnected(unit) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -855,7 +1020,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
|
||||
end
|
||||
|
||||
if maxHp <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -872,7 +1037,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
|
||||
end
|
||||
local missing = maxHp - hp
|
||||
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -880,13 +1045,13 @@ function SFrames.Raid:UpdateHealPrediction(unit)
|
||||
local remaining = missing - mineShown
|
||||
local otherShown = math.min(math.max(0, othersIncoming), remaining)
|
||||
if mineIncoming <= 0 and othersIncoming <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local barWidth = f:GetWidth() - 2
|
||||
if barWidth <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -896,7 +1061,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
|
||||
|
||||
local availableWidth = barWidth - currentPosition
|
||||
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -972,6 +1137,10 @@ function SFrames.Raid:UpdatePower(unit)
|
||||
local pType = UnitPowerType(unit)
|
||||
local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0]
|
||||
f.power:SetStatusBarColor(color.r, color.g, color.b)
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(f.power)
|
||||
end
|
||||
SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -1072,7 +1241,11 @@ function SFrames.Raid:UpdateAuras(unit)
|
||||
local f = frameData.frame
|
||||
local buffsNeeded = self:GetClassBuffs()
|
||||
|
||||
local foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
|
||||
-- Reuse pre-allocated table
|
||||
_foundIndicators[1] = false
|
||||
_foundIndicators[2] = false
|
||||
_foundIndicators[3] = false
|
||||
_foundIndicators[4] = false
|
||||
|
||||
-- Hide all first
|
||||
for i = 1, 4 do
|
||||
@@ -1085,70 +1258,22 @@ function SFrames.Raid:UpdateAuras(unit)
|
||||
return
|
||||
end
|
||||
|
||||
local function MatchesList(auraName, list)
|
||||
for _, name in ipairs(list) do
|
||||
if string.find(auraName, name) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Helper: get buff name via SuperWoW aura ID (fast) or tooltip scan (fallback)
|
||||
local hasSuperWoW = SFrames.superwow_active and SpellInfo
|
||||
local function GetBuffName(unit, index)
|
||||
if hasSuperWoW then
|
||||
local texture, auraID = UnitBuff(unit, index)
|
||||
if auraID and SpellInfo then
|
||||
local spellName = SpellInfo(auraID)
|
||||
if spellName and spellName ~= "" then
|
||||
return spellName, texture
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Fallback: tooltip scan
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:SetUnitBuff(unit, index)
|
||||
local buffName = SFramesScanTooltipTextLeft1:GetText()
|
||||
SFrames.Tooltip:Hide()
|
||||
return buffName, UnitBuff(unit, index)
|
||||
end
|
||||
|
||||
local function GetDebuffName(unit, index)
|
||||
if hasSuperWoW then
|
||||
local texture, count, dtype, auraID = UnitDebuff(unit, index)
|
||||
if auraID and SpellInfo then
|
||||
local spellName = SpellInfo(auraID)
|
||||
if spellName and spellName ~= "" then
|
||||
return spellName, texture, count, dtype
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Fallback: tooltip scan
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:SetUnitDebuff(unit, index)
|
||||
local debuffName = SFramesScanTooltipTextLeft1:GetText()
|
||||
SFrames.Tooltip:Hide()
|
||||
local texture, count, dtype = UnitDebuff(unit, index)
|
||||
return debuffName, texture, count, dtype
|
||||
end
|
||||
|
||||
-- Check Buffs
|
||||
-- Check Buffs (using module-level helpers, no closures)
|
||||
for i = 1, 32 do
|
||||
local texture, applications = UnitBuff(unit, i)
|
||||
if not texture then break end
|
||||
|
||||
local buffName = GetBuffName(unit, i)
|
||||
local buffName = RaidGetBuffName(unit, i)
|
||||
|
||||
if buffName then
|
||||
for pos, listData in pairs(buffsNeeded) do
|
||||
if pos <= 4 and not listData.isDebuff and not foundIndicators[pos] then
|
||||
if pos <= 4 and not listData.isDebuff and not _foundIndicators[pos] then
|
||||
if MatchesList(buffName, listData) then
|
||||
f.indicators[pos].icon:SetTexture(texture)
|
||||
f.indicators[pos].index = i
|
||||
f.indicators[pos].isDebuff = false
|
||||
f.indicators[pos]:Show()
|
||||
foundIndicators[pos] = true
|
||||
_foundIndicators[pos] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1156,7 +1281,10 @@ function SFrames.Raid:UpdateAuras(unit)
|
||||
end
|
||||
|
||||
local hasDebuff = false
|
||||
local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
|
||||
-- Reuse pre-allocated table
|
||||
_debuffColor.r = _A.slotBg[1]
|
||||
_debuffColor.g = _A.slotBg[2]
|
||||
_debuffColor.b = _A.slotBg[3]
|
||||
|
||||
-- Check Debuffs
|
||||
for i = 1, 16 do
|
||||
@@ -1165,24 +1293,24 @@ function SFrames.Raid:UpdateAuras(unit)
|
||||
|
||||
if dispelType then
|
||||
hasDebuff = true
|
||||
if dispelType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
|
||||
elseif dispelType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
|
||||
elseif dispelType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
|
||||
elseif dispelType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
|
||||
if dispelType == "Magic" then _debuffColor.r = 0.2; _debuffColor.g = 0.6; _debuffColor.b = 1
|
||||
elseif dispelType == "Curse" then _debuffColor.r = 0.6; _debuffColor.g = 0; _debuffColor.b = 1
|
||||
elseif dispelType == "Disease" then _debuffColor.r = 0.6; _debuffColor.g = 0.4; _debuffColor.b = 0
|
||||
elseif dispelType == "Poison" then _debuffColor.r = 0; _debuffColor.g = 0.6; _debuffColor.b = 0
|
||||
end
|
||||
end
|
||||
|
||||
local debuffName = GetDebuffName(unit, i)
|
||||
local debuffName = RaidGetDebuffName(unit, i)
|
||||
|
||||
if debuffName then
|
||||
for pos, listData in pairs(buffsNeeded) do
|
||||
if pos <= 4 and listData.isDebuff and not foundIndicators[pos] then
|
||||
if pos <= 4 and listData.isDebuff and not _foundIndicators[pos] then
|
||||
if MatchesList(debuffName, listData) then
|
||||
f.indicators[pos].icon:SetTexture(texture)
|
||||
f.indicators[pos].index = i
|
||||
f.indicators[pos].isDebuff = true
|
||||
f.indicators[pos]:Show()
|
||||
foundIndicators[pos] = true
|
||||
_foundIndicators[pos] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1190,9 +1318,8 @@ function SFrames.Raid:UpdateAuras(unit)
|
||||
end
|
||||
|
||||
if hasDebuff then
|
||||
f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
|
||||
f.health.bg:SetVertexColor(_debuffColor.r, _debuffColor.g, _debuffColor.b, 1)
|
||||
else
|
||||
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
406
Units/Target.lua
406
Units/Target.lua
@@ -182,6 +182,17 @@ local function Clamp(value, minValue, maxValue)
|
||||
return value
|
||||
end
|
||||
|
||||
local function SetTextureIfPresent(region, texturePath)
|
||||
if region and region.SetTexture and texturePath then
|
||||
region:SetTexture(texturePath)
|
||||
end
|
||||
end
|
||||
|
||||
local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey)
|
||||
if not fs then return end
|
||||
SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey)
|
||||
end
|
||||
|
||||
local DIST_BASE_WIDTH = 80
|
||||
local DIST_BASE_HEIGHT = 24
|
||||
local DIST_BASE_FONTSIZE = 14
|
||||
@@ -232,6 +243,33 @@ function SFrames.Target:GetConfig()
|
||||
local powerHeight = tonumber(db.targetPowerHeight) or 9
|
||||
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40)
|
||||
|
||||
local showPortrait = db.targetShowPortrait ~= false
|
||||
local gradientStyle = SFrames:IsGradientStyle()
|
||||
local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2
|
||||
if classicDefaultPowerWidth < 60 then
|
||||
classicDefaultPowerWidth = 60
|
||||
end
|
||||
|
||||
local rawPowerWidth = tonumber(db.targetPowerWidth)
|
||||
local legacyFullWidth = tonumber(db.targetFrameWidth) or width
|
||||
local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth
|
||||
local maxPowerWidth = gradientStyle and width or (width - 2)
|
||||
local powerWidth
|
||||
if gradientStyle then
|
||||
-- 渐变风格:能量条始终与血条等宽(全宽)
|
||||
powerWidth = width
|
||||
elseif not rawPowerWidth
|
||||
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
|
||||
or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then
|
||||
powerWidth = defaultPowerWidth
|
||||
else
|
||||
powerWidth = rawPowerWidth
|
||||
end
|
||||
powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth)
|
||||
|
||||
local powerOffsetX = Clamp(math.floor((tonumber(db.targetPowerOffsetX) or 0) + 0.5), -120, 120)
|
||||
local powerOffsetY = Clamp(math.floor((tonumber(db.targetPowerOffsetY) or 0) + 0.5), -80, 80)
|
||||
|
||||
local height = healthHeight + powerHeight + 4
|
||||
height = Clamp(height, 30, 140)
|
||||
|
||||
@@ -241,6 +279,12 @@ function SFrames.Target:GetConfig()
|
||||
local valueFont = tonumber(db.targetValueFontSize) or 10
|
||||
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
|
||||
|
||||
local healthFont = tonumber(db.targetHealthFontSize) or valueFont
|
||||
healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
|
||||
|
||||
local powerFont = tonumber(db.targetPowerFontSize) or valueFont
|
||||
powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
|
||||
|
||||
local frameScale = tonumber(db.targetFrameScale) or 1
|
||||
frameScale = Clamp(frameScale, 0.7, 1.8)
|
||||
|
||||
@@ -250,8 +294,16 @@ function SFrames.Target:GetConfig()
|
||||
portraitWidth = portraitWidth,
|
||||
healthHeight = healthHeight,
|
||||
powerHeight = powerHeight,
|
||||
powerWidth = powerWidth,
|
||||
powerOffsetX = powerOffsetX,
|
||||
powerOffsetY = powerOffsetY,
|
||||
powerOnTop = db.targetPowerOnTop == true,
|
||||
nameFont = nameFont,
|
||||
valueFont = valueFont,
|
||||
healthFont = healthFont,
|
||||
powerFont = powerFont,
|
||||
healthTexture = SFrames:ResolveBarTexture("targetHealthTexture", "barTexture"),
|
||||
powerTexture = SFrames:ResolveBarTexture("targetPowerTexture", "barTexture"),
|
||||
scale = frameScale,
|
||||
}
|
||||
end
|
||||
@@ -334,8 +386,8 @@ function SFrames.Target:ApplyConfig()
|
||||
|
||||
if f.power then
|
||||
f.power:ClearAllPoints()
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
|
||||
f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
|
||||
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY)
|
||||
f.power:SetWidth(cfg.powerWidth)
|
||||
f.power:SetHeight(cfg.powerHeight)
|
||||
end
|
||||
|
||||
@@ -345,18 +397,70 @@ function SFrames.Target:ApplyConfig()
|
||||
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
|
||||
local fontPath = SFrames:GetFont()
|
||||
SFrames:ApplyStatusBarTexture(f.health, "targetHealthTexture", "barTexture")
|
||||
SFrames:ApplyStatusBarTexture(f.power, "targetPowerTexture", "barTexture")
|
||||
SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture)
|
||||
SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture)
|
||||
SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture)
|
||||
SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture)
|
||||
SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture)
|
||||
|
||||
if f.nameText then
|
||||
f.nameText:SetFont(fontPath, cfg.nameFont, outline)
|
||||
-- Gradient style preset
|
||||
if SFrames:IsGradientStyle() then
|
||||
-- Hide portrait & its backdrop
|
||||
if f.portrait then f.portrait:Hide() end
|
||||
if f.portraitBG then f.portraitBG:Hide() end
|
||||
-- Strip backdrops
|
||||
SFrames:ClearBackdrop(f)
|
||||
SFrames:ClearBackdrop(f.healthBGFrame)
|
||||
SFrames:ClearBackdrop(f.powerBGFrame)
|
||||
-- Health bar full width
|
||||
if f.health then
|
||||
f.health:ClearAllPoints()
|
||||
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
|
||||
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
|
||||
f.health:SetHeight(cfg.healthHeight)
|
||||
end
|
||||
if f.healthText then
|
||||
f.healthText:SetFont(fontPath, cfg.valueFont, outline)
|
||||
-- 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
|
||||
if f.powerText then
|
||||
f.powerText:SetFont(fontPath, cfg.valueFont, outline)
|
||||
-- Apply gradient overlays
|
||||
SFrames:ApplyGradientStyle(f.health)
|
||||
SFrames:ApplyGradientStyle(f.power)
|
||||
-- Flush BG frames (no border padding)
|
||||
if f.healthBGFrame then
|
||||
f.healthBGFrame:ClearAllPoints()
|
||||
f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
|
||||
f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
if f.powerBGFrame then
|
||||
f.powerBGFrame:ClearAllPoints()
|
||||
f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
|
||||
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
|
||||
end
|
||||
-- Hide bar backgrounds (transparent)
|
||||
if f.healthBGFrame then f.healthBGFrame:Hide() end
|
||||
if f.powerBGFrame then f.powerBGFrame:Hide() end
|
||||
if f.health and f.health.bg then f.health.bg:Hide() end
|
||||
if f.power and f.power.bg then f.power.bg:Hide() end
|
||||
else
|
||||
-- Classic style: remove gradient overlays if they exist
|
||||
SFrames:RemoveGradientStyle(f.health)
|
||||
SFrames:RemoveGradientStyle(f.power)
|
||||
-- Restore bar backgrounds
|
||||
if f.healthBGFrame then f.healthBGFrame:Show() end
|
||||
if f.powerBGFrame then f.powerBGFrame:Show() end
|
||||
if f.health and f.health.bg then f.health.bg:Show() end
|
||||
if f.power and f.power.bg then f.power.bg:Show() end
|
||||
end
|
||||
|
||||
ApplyFontIfPresent(f.nameText, cfg.nameFont, "targetNameFontKey")
|
||||
ApplyFontIfPresent(f.healthText, cfg.healthFont, "targetHealthFontKey")
|
||||
ApplyFontIfPresent(f.powerText, cfg.powerFont, "targetPowerFontKey")
|
||||
|
||||
if f.castbar then
|
||||
f.castbar:ClearAllPoints()
|
||||
@@ -374,6 +478,24 @@ function SFrames.Target:ApplyConfig()
|
||||
self:ApplyDistanceScale(dScale)
|
||||
end
|
||||
|
||||
if f.distText then
|
||||
local dfs = (db.targetDistanceFontSize and tonumber(db.targetDistanceFontSize)) or 10
|
||||
dfs = Clamp(dfs, 8, 24)
|
||||
SFrames:ApplyFontString(f.distText, dfs, "targetDistanceFontKey", "fontKey")
|
||||
end
|
||||
|
||||
if f.health and f.power then
|
||||
local healthLevel = f:GetFrameLevel() + 2
|
||||
local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
|
||||
f.health:SetFrameLevel(healthLevel)
|
||||
f.power:SetFrameLevel(powerLevel)
|
||||
end
|
||||
|
||||
SFrames:ApplyConfiguredUnitBackdrop(f, "target")
|
||||
if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "target") end
|
||||
if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "target") end
|
||||
if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "target", true) end
|
||||
|
||||
if UnitExists("target") then
|
||||
self:UpdateAll()
|
||||
end
|
||||
@@ -386,8 +508,6 @@ function SFrames.Target:ApplyDistanceScale(scale)
|
||||
f:SetWidth(DIST_BASE_WIDTH * scale)
|
||||
f:SetHeight(DIST_BASE_HEIGHT * scale)
|
||||
if f.text then
|
||||
local fontPath = SFrames:GetFont()
|
||||
local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
|
||||
local customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)
|
||||
local fontSize
|
||||
if customSize and customSize >= 8 and customSize <= 24 then
|
||||
@@ -395,7 +515,7 @@ function SFrames.Target:ApplyDistanceScale(scale)
|
||||
else
|
||||
fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
|
||||
end
|
||||
f.text:SetFont(fontPath, fontSize, outline)
|
||||
SFrames:ApplyFontString(f.text, fontSize, "targetDistanceFontKey", "fontKey")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -437,54 +557,46 @@ function SFrames.Target:InitializeDistanceFrame()
|
||||
f.text:SetShadowColor(0, 0, 0, 1)
|
||||
f.text:SetShadowOffset(1, -1)
|
||||
|
||||
-- Behind indicator text (shown next to distance)
|
||||
f.behindText = SFrames:CreateFontString(f, fontSize, "LEFT")
|
||||
f.behindText:SetPoint("LEFT", f.text, "RIGHT", 4, 0)
|
||||
f.behindText:SetShadowColor(0, 0, 0, 1)
|
||||
f.behindText:SetShadowOffset(1, -1)
|
||||
f.behindText:Hide()
|
||||
|
||||
SFrames.Target.distanceFrame = f
|
||||
f:Hide()
|
||||
|
||||
f.timer = 0
|
||||
f:SetScript("OnUpdate", function()
|
||||
if SFramesDB and SFramesDB.targetDistanceEnabled == false then
|
||||
if this:IsShown() then this:Hide() end
|
||||
local ticker = CreateFrame("Frame", nil, UIParent)
|
||||
ticker:SetWidth(1)
|
||||
ticker:SetHeight(1)
|
||||
ticker.timer = 0
|
||||
ticker:Show()
|
||||
ticker:SetScript("OnUpdate", function()
|
||||
local distFrame = SFrames.Target.distanceFrame
|
||||
if not distFrame then return end
|
||||
local disabled = SFramesDB and SFramesDB.targetDistanceEnabled == false
|
||||
local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
|
||||
local tgtFrame = SFrames.Target and SFrames.Target.frame
|
||||
local embeddedText = tgtFrame and tgtFrame.distText
|
||||
|
||||
if disabled then
|
||||
if distFrame:IsShown() then distFrame:Hide() end
|
||||
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
|
||||
return
|
||||
end
|
||||
if not UnitExists("target") then
|
||||
if this:IsShown() then this:Hide() end
|
||||
if this.behindText then this.behindText:Hide() end
|
||||
if distFrame:IsShown() then distFrame:Hide() end
|
||||
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
|
||||
return
|
||||
end
|
||||
this.timer = this.timer + (arg1 or 0)
|
||||
if this.timer >= 0.4 then
|
||||
this.timer = 0
|
||||
local dist = SFrames.Target:GetDistance("target")
|
||||
this.text:SetText(dist or "---")
|
||||
if not this:IsShown() then this:Show() end
|
||||
local distStr = dist or "---"
|
||||
|
||||
-- Behind indicator
|
||||
if this.behindText then
|
||||
local showBehind = not SFramesDB or SFramesDB.Tweaks == nil
|
||||
or SFramesDB.Tweaks.behindIndicator ~= false
|
||||
if showBehind and IsUnitXPAvailable() then
|
||||
local ok, isBehind = pcall(UnitXP, "behind", "player", "target")
|
||||
if ok and isBehind then
|
||||
this.behindText:SetText("背后")
|
||||
this.behindText:SetTextColor(0.2, 1.0, 0.3)
|
||||
this.behindText:Show()
|
||||
elseif ok then
|
||||
this.behindText:SetText("正面")
|
||||
this.behindText:SetTextColor(1.0, 0.35, 0.3)
|
||||
this.behindText:Show()
|
||||
if onFrame and embeddedText then
|
||||
embeddedText:SetText(distStr)
|
||||
if not embeddedText:IsShown() then embeddedText:Show() end
|
||||
if distFrame:IsShown() then distFrame:Hide() end
|
||||
else
|
||||
this.behindText:Hide()
|
||||
end
|
||||
else
|
||||
this.behindText:Hide()
|
||||
end
|
||||
distFrame.text:SetText(distStr)
|
||||
if not distFrame:IsShown() then distFrame:Show() end
|
||||
if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -515,15 +627,23 @@ function SFrames.Target:Initialize()
|
||||
local f = CreateFrame("Button", "SFramesTargetFrame", UIParent)
|
||||
f:SetWidth(SFrames.Config.width)
|
||||
f:SetHeight(SFrames.Config.height)
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
|
||||
local pos = SFramesDB.Positions["TargetFrame"]
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
|
||||
else
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) -- Mirrored from player
|
||||
end
|
||||
|
||||
local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1
|
||||
f:SetScale(frameScale)
|
||||
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
|
||||
local pos = SFramesDB.Positions["TargetFrame"]
|
||||
local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint,
|
||||
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
|
||||
else
|
||||
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||||
end
|
||||
else
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 200, -100)
|
||||
end
|
||||
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:RegisterForDrag("LeftButton")
|
||||
@@ -533,25 +653,21 @@ function SFrames.Target:Initialize()
|
||||
if not SFramesDB then SFramesDB = {} end
|
||||
if not SFramesDB.Positions then SFramesDB.Positions = {} end
|
||||
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
|
||||
local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||||
if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then
|
||||
xOfs = (xOfs or 0) * fSc
|
||||
yOfs = (yOfs or 0) * fSc
|
||||
end
|
||||
SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
|
||||
end)
|
||||
|
||||
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
f:SetScript("OnClick", function()
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] OnClick fired: " .. tostring(arg1) .. "|r")
|
||||
if arg1 == "LeftButton" then
|
||||
-- Shift+左键 = 设为焦点
|
||||
if IsShiftKeyDown() then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Shift+LeftButton -> SetFocus|r")
|
||||
if SFrames.Focus and SFrames.Focus.SetFromTarget then
|
||||
local ok, err = pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
|
||||
if ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Focus set OK|r")
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Focus error: " .. tostring(err) .. "|r")
|
||||
end
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] SFrames.Focus missing!|r")
|
||||
pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -562,31 +678,25 @@ function SFrames.Target:Initialize()
|
||||
SpellTargetUnit(this.unit)
|
||||
end
|
||||
elseif arg1 == "RightButton" then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] RightButton hit|r")
|
||||
if SpellIsTargeting and SpellIsTargeting() then
|
||||
SpellStopTargeting()
|
||||
return
|
||||
end
|
||||
if not UnitExists("target") then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] No target, abort|r")
|
||||
return
|
||||
end
|
||||
if not SFrames.Target.dropDown then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Creating dropdown...|r")
|
||||
local ok1, err1 = pcall(function()
|
||||
SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate")
|
||||
end)
|
||||
if not ok1 then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] CreateFrame failed: " .. tostring(err1) .. "|r")
|
||||
return
|
||||
end
|
||||
SFrames.Target.dropDown.displayMode = "MENU"
|
||||
SFrames.Target.dropDown.initialize = function()
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] initialize() called|r")
|
||||
local dd = SFrames.Target.dropDown
|
||||
local name = dd.targetName
|
||||
if not name then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] initialize: no targetName|r")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -678,16 +788,9 @@ function SFrames.Target:Initialize()
|
||||
|
||||
-- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位)
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Dropdown created OK|r")
|
||||
end
|
||||
SFrames.Target.dropDown.targetName = UnitName("target")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Calling ToggleDropDownMenu...|r")
|
||||
local ok3, err3 = pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
|
||||
if not ok3 then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] ToggleDropDownMenu failed: " .. tostring(err3) .. "|r")
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] ToggleDropDownMenu OK|r")
|
||||
end
|
||||
pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
|
||||
end
|
||||
end)
|
||||
f:SetScript("OnReceiveDrag", function()
|
||||
@@ -799,6 +902,19 @@ function SFrames.Target:Initialize()
|
||||
f.comboText:SetTextColor(1, 0.8, 0)
|
||||
f.comboText:SetText("")
|
||||
|
||||
-- Embedded distance text (high-level overlay so it's never covered)
|
||||
local distOverlay = CreateFrame("Frame", nil, f)
|
||||
distOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 20)
|
||||
distOverlay:SetAllPoints(f.health)
|
||||
local distFS = (SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)) or 10
|
||||
f.distText = SFrames:CreateFontString(distOverlay, distFS, "CENTER")
|
||||
f.distText:SetPoint("CENTER", f.health, "TOP", 0, 0)
|
||||
f.distText:SetTextColor(1, 0.82, 0.25)
|
||||
f.distText:SetShadowColor(0, 0, 0, 1)
|
||||
f.distText:SetShadowOffset(1, -1)
|
||||
f.distText:SetText("")
|
||||
f.distText:Hide()
|
||||
|
||||
-- Raid Target Icon (top center of health bar, half outside frame)
|
||||
local raidIconSize = 22
|
||||
local raidIconOvr = CreateFrame("Frame", nil, f)
|
||||
@@ -864,7 +980,8 @@ function SFrames.Target:Initialize()
|
||||
-- Register movers
|
||||
if SFrames.Movers and SFrames.Movers.RegisterMover then
|
||||
SFrames.Movers:RegisterMover("TargetFrame", f, "目标",
|
||||
"CENTER", "UIParent", "CENTER", 200, -100)
|
||||
"CENTER", "UIParent", "CENTER", 200, -100,
|
||||
nil, { alwaysShowInLayout = true })
|
||||
if SFrames.Target.distanceFrame then
|
||||
SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离",
|
||||
"CENTER", "UIParent", "CENTER", 0, 100)
|
||||
@@ -1047,18 +1164,24 @@ function SFrames.Target:OnTargetChanged()
|
||||
if UnitExists("target") then
|
||||
self.frame:Show()
|
||||
self:UpdateAll()
|
||||
local enabled = not (SFramesDB and SFramesDB.targetDistanceEnabled == false)
|
||||
local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
|
||||
if SFrames.Target.distanceFrame then
|
||||
local dist = self:GetDistance("target")
|
||||
SFrames.Target.distanceFrame.text:SetText(dist or "---")
|
||||
if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then
|
||||
SFrames.Target.distanceFrame:Show()
|
||||
else
|
||||
if onFrame and self.frame.distText then
|
||||
self.frame.distText:SetText(dist or "---")
|
||||
if enabled then self.frame.distText:Show() else self.frame.distText:Hide() end
|
||||
SFrames.Target.distanceFrame:Hide()
|
||||
else
|
||||
SFrames.Target.distanceFrame.text:SetText(dist or "---")
|
||||
if enabled then SFrames.Target.distanceFrame:Show() else SFrames.Target.distanceFrame:Hide() end
|
||||
if self.frame.distText then self.frame.distText:Hide() end
|
||||
end
|
||||
end
|
||||
else
|
||||
self.frame:Hide()
|
||||
if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end
|
||||
if self.frame and self.frame.distText then self.frame.distText:Hide() end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1153,6 +1276,8 @@ function SFrames.Target:UpdateAll()
|
||||
end
|
||||
|
||||
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
|
||||
-- Gradient style always uses class colors
|
||||
if SFrames:IsGradientStyle() then useClassColor = true end
|
||||
|
||||
if UnitIsPlayer("target") and useClassColor then
|
||||
local _, class = UnitClass("target")
|
||||
@@ -1184,6 +1309,10 @@ function SFrames.Target:UpdateAll()
|
||||
self.frame.nameText:SetText(formattedLevel .. name)
|
||||
self.frame.nameText:SetTextColor(r, g, b)
|
||||
end
|
||||
-- Re-apply gradient after color change
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.health)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Target:UpdateHealth()
|
||||
@@ -1208,9 +1337,9 @@ function SFrames.Target:UpdateHealth()
|
||||
end
|
||||
|
||||
if displayMax > 0 then
|
||||
self.frame.healthText:SetText(displayHp .. " / " .. displayMax)
|
||||
self.frame.healthText:SetText(SFrames:FormatCompactPair(displayHp, displayMax))
|
||||
else
|
||||
self.frame.healthText:SetText(displayHp)
|
||||
self.frame.healthText:SetText(SFrames:FormatCompactNumber(displayHp))
|
||||
end
|
||||
|
||||
self:UpdateHealPrediction()
|
||||
@@ -1222,14 +1351,8 @@ function SFrames.Target:UpdateHealPrediction()
|
||||
local predOther = self.frame.health.healPredOther
|
||||
local predOver = self.frame.health.healPredOver
|
||||
|
||||
local function HidePredictions()
|
||||
predMine:Hide()
|
||||
predOther:Hide()
|
||||
predOver:Hide()
|
||||
end
|
||||
|
||||
if not UnitExists("target") then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1251,7 +1374,7 @@ function SFrames.Target:UpdateHealPrediction()
|
||||
end
|
||||
|
||||
if maxHp <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1268,7 +1391,7 @@ function SFrames.Target:UpdateHealPrediction()
|
||||
end
|
||||
local missing = maxHp - hp
|
||||
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1276,14 +1399,14 @@ function SFrames.Target:UpdateHealPrediction()
|
||||
local remaining = missing - mineShown
|
||||
local otherShown = math.min(math.max(0, othersIncoming), remaining)
|
||||
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false
|
||||
local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2)
|
||||
if barWidth <= 0 then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1293,7 +1416,7 @@ function SFrames.Target:UpdateHealPrediction()
|
||||
|
||||
local availableWidth = barWidth - currentWidth
|
||||
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
|
||||
HidePredictions()
|
||||
predMine:Hide(); predOther:Hide(); predOver:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1359,6 +1482,9 @@ function SFrames.Target:UpdatePowerType()
|
||||
else
|
||||
self.frame.power:SetStatusBarColor(0, 0, 1)
|
||||
end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyBarGradient(self.frame.power)
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Target:UpdatePower()
|
||||
@@ -1367,10 +1493,11 @@ function SFrames.Target:UpdatePower()
|
||||
self.frame.power:SetMinMaxValues(0, maxPower)
|
||||
self.frame.power:SetValue(power)
|
||||
if maxPower > 0 then
|
||||
self.frame.powerText:SetText(power .. " / " .. maxPower)
|
||||
self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
|
||||
else
|
||||
self.frame.powerText:SetText("")
|
||||
end
|
||||
SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "target")
|
||||
end
|
||||
|
||||
function SFrames.Target:UpdateComboPoints()
|
||||
@@ -1501,23 +1628,15 @@ end
|
||||
function SFrames.Target:TickAuras()
|
||||
if not UnitExists("target") then return end
|
||||
|
||||
local timeNow = GetTime()
|
||||
local tracker = SFrames.AuraTracker
|
||||
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
|
||||
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.FindEffectData
|
||||
|
||||
local targetName, targetLevel, targetGUID
|
||||
if hasNP then
|
||||
targetName = UnitName("target")
|
||||
targetLevel = UnitLevel("target") or 0
|
||||
targetGUID = UnitGUID and UnitGUID("target")
|
||||
end
|
||||
|
||||
-- Buffs
|
||||
for i = 1, 32 do
|
||||
local b = self.frame.buffs[i]
|
||||
if b:IsShown() and b.expirationTime then
|
||||
local timeLeft = b.expirationTime - timeNow
|
||||
if timeLeft > 0 and timeLeft < 3600 then
|
||||
if b:IsShown() then
|
||||
local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "buff", i)
|
||||
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
b.cdText:SetText(text)
|
||||
@@ -1531,30 +1650,11 @@ function SFrames.Target:TickAuras()
|
||||
end
|
||||
end
|
||||
|
||||
-- Debuffs: re-query SpellDB for live-accurate timers
|
||||
-- Debuffs
|
||||
for i = 1, 32 do
|
||||
local b = self.frame.debuffs[i]
|
||||
if b:IsShown() then
|
||||
local timeLeft = nil
|
||||
|
||||
if hasNP and b.effectName then
|
||||
local data = targetGUID and NanamiPlates_SpellDB:FindEffectData(targetGUID, targetLevel, b.effectName)
|
||||
if not data and targetName then
|
||||
data = NanamiPlates_SpellDB:FindEffectData(targetName, targetLevel, b.effectName)
|
||||
end
|
||||
if data and data.start and data.duration then
|
||||
local remaining = data.duration + data.start - timeNow
|
||||
if remaining > 0 then
|
||||
timeLeft = remaining
|
||||
b.expirationTime = timeNow + remaining
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not timeLeft and b.expirationTime then
|
||||
timeLeft = b.expirationTime - timeNow
|
||||
end
|
||||
|
||||
local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "debuff", i)
|
||||
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
@@ -1573,6 +1673,11 @@ end
|
||||
function SFrames.Target:UpdateAuras()
|
||||
if not UnitExists("target") then return end
|
||||
|
||||
local tracker = SFrames.AuraTracker
|
||||
if tracker and tracker.HandleAuraSnapshot then
|
||||
tracker:HandleAuraSnapshot("target")
|
||||
end
|
||||
|
||||
local hasSuperWoW = SFrames.superwow_active and SpellInfo
|
||||
local numBuffs = 0
|
||||
-- Buffs
|
||||
@@ -1585,11 +1690,8 @@ function SFrames.Target:UpdateAuras()
|
||||
-- Store aura ID when SuperWoW is available
|
||||
b.auraID = hasSuperWoW and swAuraID or nil
|
||||
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:ClearLines()
|
||||
SFrames.Tooltip:SetUnitBuff("target", i)
|
||||
local timeLeft = SFrames:GetAuraTimeLeft("target", i, true)
|
||||
SFrames.Tooltip:Hide()
|
||||
local state = tracker and tracker:GetAuraState("target", "buff", i)
|
||||
local timeLeft = state and tracker:GetAuraTimeLeft("target", "buff", i)
|
||||
if timeLeft and timeLeft > 0 then
|
||||
b.expirationTime = GetTime() + timeLeft
|
||||
b.cdText:SetText(SFrames:FormatTime(timeLeft))
|
||||
@@ -1621,7 +1723,6 @@ function SFrames.Target:UpdateAuras()
|
||||
end
|
||||
|
||||
-- Debuffs
|
||||
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
|
||||
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
|
||||
|
||||
for i = 1, 32 do
|
||||
@@ -1633,39 +1734,12 @@ function SFrames.Target:UpdateAuras()
|
||||
-- Store aura ID when SuperWoW is available
|
||||
b.auraID = hasSuperWoW and swDebuffAuraID or nil
|
||||
|
||||
local timeLeft = 0
|
||||
local effectName = nil
|
||||
|
||||
if hasNP then
|
||||
local effect, rank, _, stacks, dtype, duration, npTimeLeft, isOwn = NanamiPlates_SpellDB:UnitDebuff("target", i)
|
||||
effectName = effect
|
||||
if npTimeLeft and npTimeLeft > 0 then
|
||||
timeLeft = npTimeLeft
|
||||
elseif effect and effect ~= "" and duration and duration > 0
|
||||
and NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
||||
local unitKey = (UnitGUID and UnitGUID("target")) or UnitName("target") or ""
|
||||
local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect]
|
||||
if not cached and UnitName("target") then
|
||||
cached = NanamiPlates_Auras.timers[UnitName("target") .. "_" .. effect]
|
||||
end
|
||||
if cached and cached.startTime and cached.duration then
|
||||
local remaining = cached.duration - (GetTime() - cached.startTime)
|
||||
if remaining > 0 then timeLeft = remaining end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if timeLeft <= 0 then
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:ClearLines()
|
||||
SFrames.Tooltip:SetUnitDebuff("target", i)
|
||||
timeLeft = SFrames:GetAuraTimeLeft("target", i, false)
|
||||
SFrames.Tooltip:Hide()
|
||||
end
|
||||
local state = tracker and tracker:GetAuraState("target", "debuff", i)
|
||||
local timeLeft = state and tracker:GetAuraTimeLeft("target", "debuff", i)
|
||||
|
||||
if timeLeft and timeLeft > 0 then
|
||||
b.expirationTime = GetTime() + timeLeft
|
||||
b.effectName = effectName
|
||||
b.effectName = state and state.name or nil
|
||||
if npFormat then
|
||||
local text, r, g, bc, a = npFormat(timeLeft)
|
||||
b.cdText:SetText(text)
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
SFrames.ToT = {}
|
||||
local _A = SFrames.ActiveTheme
|
||||
|
||||
function SFrames.ToT:ApplyConfig()
|
||||
local f = self.frame
|
||||
if not f then return end
|
||||
SFrames:ApplyStatusBarTexture(f.health, "totHealthTexture", "barTexture")
|
||||
local tex = SFrames:ResolveBarTexture("totHealthTexture", "barTexture")
|
||||
if f.health and f.health.bg then f.health.bg:SetTexture(tex) end
|
||||
if SFrames:IsGradientStyle() then
|
||||
SFrames:ApplyGradientStyle(f.health)
|
||||
if f.hbg then f.hbg:Hide() end
|
||||
if f.health and f.health.bg then f.health.bg:Hide() end
|
||||
else
|
||||
SFrames:RemoveGradientStyle(f.health)
|
||||
if f.hbg then f.hbg:Show() end
|
||||
if f.health and f.health.bg then f.health.bg:Show() end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.ToT:Initialize()
|
||||
local f = CreateFrame("Button", "SFramesToTFrame", UIParent)
|
||||
f:SetWidth(120)
|
||||
f:SetHeight(25)
|
||||
|
||||
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ToTFrame"] then
|
||||
local pos = SFramesDB.Positions["ToTFrame"]
|
||||
f:SetPoint(pos.point or "BOTTOMLEFT", UIParent, pos.relativePoint or "BOTTOMLEFT",
|
||||
pos.xOfs or 0, pos.yOfs or 0)
|
||||
else
|
||||
f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
|
||||
end
|
||||
|
||||
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
f:SetScript("OnClick", function()
|
||||
@@ -24,6 +48,7 @@ function SFrames.ToT:Initialize()
|
||||
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
|
||||
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
|
||||
SFrames:CreateUnitBackdrop(hbg)
|
||||
f.hbg = hbg
|
||||
|
||||
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
|
||||
f.health.bg:SetAllPoints()
|
||||
@@ -36,6 +61,14 @@ function SFrames.ToT:Initialize()
|
||||
self.frame = f
|
||||
f:Hide()
|
||||
|
||||
self:ApplyConfig()
|
||||
|
||||
if SFrames.Movers and SFrames.Movers.RegisterMover then
|
||||
SFrames.Movers:RegisterMover("ToTFrame", f, "目标的目标",
|
||||
"BOTTOMLEFT", "SFramesTargetFrame", "BOTTOMRIGHT", 5, 0,
|
||||
nil, { alwaysShowInLayout = true })
|
||||
end
|
||||
|
||||
-- Update loop since targettarget changes don't fire precise events in Vanilla
|
||||
self.updater = CreateFrame("Frame")
|
||||
self.updater.timer = 0
|
||||
|
||||
56
WorldMap.lua
56
WorldMap.lua
@@ -52,6 +52,26 @@ local function HookScript(frame, script, fn)
|
||||
end)
|
||||
end
|
||||
|
||||
local function SafeSetMapToCurrentZone()
|
||||
if not SetMapToCurrentZone then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
|
||||
end
|
||||
return pcall(SetMapToCurrentZone)
|
||||
end
|
||||
|
||||
local function SafeSetMapZoom(continent, zone)
|
||||
if not SetMapZoom then
|
||||
return
|
||||
end
|
||||
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
|
||||
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
|
||||
end
|
||||
return pcall(SetMapZoom, continent, zone)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- 1. Hide Blizzard Decorations
|
||||
-- Called at init AND on every WorldMapFrame:OnShow to counter Blizzard resets
|
||||
@@ -951,12 +971,18 @@ local function CreateWaypointPin()
|
||||
pinLabel:SetText("")
|
||||
|
||||
local btnW, btnH = 52, 18
|
||||
local PIN_BTN_BACKDROP = {
|
||||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeSize = 1,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||||
}
|
||||
|
||||
pinShareBtn = CreateFrame("Button", nil, pinFrame)
|
||||
pinShareBtn:SetWidth(btnW)
|
||||
pinShareBtn:SetHeight(btnH)
|
||||
pinShareBtn:SetPoint("TOPLEFT", pinFrame, "BOTTOMLEFT", -10, -8)
|
||||
pinShareBtn:SetBackdrop(PANEL_BACKDROP)
|
||||
pinShareBtn:SetPoint("TOP", pinLabel, "BOTTOM", -(btnW / 2 + 2), -4)
|
||||
pinShareBtn:SetBackdrop(PIN_BTN_BACKDROP)
|
||||
pinShareBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
|
||||
pinShareBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
|
||||
local shareFS = pinShareBtn:CreateFontString(nil, "OVERLAY")
|
||||
@@ -966,9 +992,11 @@ local function CreateWaypointPin()
|
||||
shareFS:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
|
||||
pinShareBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
|
||||
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
|
||||
end)
|
||||
pinShareBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
|
||||
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
|
||||
end)
|
||||
pinShareBtn:SetScript("OnClick", function()
|
||||
WM:ShareWaypoint()
|
||||
@@ -978,7 +1006,7 @@ local function CreateWaypointPin()
|
||||
pinClearBtn:SetWidth(btnW)
|
||||
pinClearBtn:SetHeight(btnH)
|
||||
pinClearBtn:SetPoint("LEFT", pinShareBtn, "RIGHT", 4, 0)
|
||||
pinClearBtn:SetBackdrop(PANEL_BACKDROP)
|
||||
pinClearBtn:SetBackdrop(PIN_BTN_BACKDROP)
|
||||
pinClearBtn:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
|
||||
pinClearBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
|
||||
local clearFS = pinClearBtn:CreateFontString(nil, "OVERLAY")
|
||||
@@ -988,9 +1016,11 @@ local function CreateWaypointPin()
|
||||
clearFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
|
||||
pinClearBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
|
||||
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
|
||||
end)
|
||||
pinClearBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
|
||||
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
|
||||
end)
|
||||
pinClearBtn:SetScript("OnClick", function()
|
||||
WM:ClearWaypoint()
|
||||
@@ -1099,7 +1129,7 @@ function WM:HandleWaypointLink(data)
|
||||
if waited >= 0.05 then
|
||||
timer:SetScript("OnUpdate", nil)
|
||||
if SetMapZoom then
|
||||
SetMapZoom(pending.continent, pending.zone)
|
||||
SafeSetMapZoom(pending.continent, pending.zone)
|
||||
end
|
||||
WM:SetWaypoint(pending.continent, pending.zone, pending.x, pending.y, pending.name)
|
||||
end
|
||||
@@ -1138,7 +1168,7 @@ local function DiscoverDmfZoneIndices()
|
||||
local target = string.lower(loc.zone)
|
||||
local zones = { GetMapZones(loc.cont) }
|
||||
for idx = 1, table.getn(zones) do
|
||||
SetMapZoom(loc.cont, idx)
|
||||
SafeSetMapZoom(loc.cont, idx)
|
||||
local mf = GetMapInfo and GetMapInfo() or ""
|
||||
if mf ~= "" then
|
||||
if mf == loc.zone then
|
||||
@@ -1158,11 +1188,11 @@ local function DiscoverDmfZoneIndices()
|
||||
end
|
||||
end
|
||||
if savedZ > 0 then
|
||||
SetMapZoom(savedC, savedZ)
|
||||
SafeSetMapZoom(savedC, savedZ)
|
||||
elseif savedC > 0 then
|
||||
SetMapZoom(savedC, 0)
|
||||
SafeSetMapZoom(savedC, 0)
|
||||
else
|
||||
if SetMapToCurrentZone then SetMapToCurrentZone() end
|
||||
SafeSetMapToCurrentZone()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1390,7 +1420,7 @@ SlashCmdList["DMFMAP"] = function(msg)
|
||||
cf:AddMessage("|cffffcc66[DMF Scan] " .. cname .. " (cont=" .. c .. "):|r")
|
||||
local zones = { GetMapZones(c) }
|
||||
for idx = 1, table.getn(zones) do
|
||||
SetMapZoom(c, idx)
|
||||
SafeSetMapZoom(c, idx)
|
||||
local mf = GetMapInfo and GetMapInfo() or "(nil)"
|
||||
local zname = zones[idx] or "?"
|
||||
if string.find(string.lower(mf), "elwynn") or string.find(string.lower(zname), "elwynn")
|
||||
@@ -1401,9 +1431,9 @@ SlashCmdList["DMFMAP"] = function(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
if savedZ > 0 then SetMapZoom(savedC, savedZ)
|
||||
elseif savedC > 0 then SetMapZoom(savedC, 0)
|
||||
else if SetMapToCurrentZone then SetMapToCurrentZone() end end
|
||||
if savedZ > 0 then SafeSetMapZoom(savedC, savedZ)
|
||||
elseif savedC > 0 then SafeSetMapZoom(savedC, 0)
|
||||
else SafeSetMapToCurrentZone() end
|
||||
return
|
||||
end
|
||||
local ai, iw, dl, ds = GetDmfSchedule()
|
||||
@@ -1931,7 +1961,7 @@ local function UpdateNavMap()
|
||||
if not N.frame or not N.tiles or not N.frame:IsVisible() then return end
|
||||
if WorldMapFrame and WorldMapFrame:IsVisible() then return end
|
||||
|
||||
if SetMapToCurrentZone then SetMapToCurrentZone() end
|
||||
SafeSetMapToCurrentZone()
|
||||
|
||||
local mapFile = GetMapInfo and GetMapInfo() or ""
|
||||
if mapFile ~= "" and mapFile ~= N.curMap then
|
||||
|
||||
426
agent-tools/generate_consumable_db.py
Normal file
426
agent-tools/generate_consumable_db.py
Normal 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()
|
||||
126
docs/MapReveal-Standalone.md
Normal file
126
docs/MapReveal-Standalone.md
Normal 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 私服整合端分别适配。
|
||||
BIN
img/progress.tga
BIN
img/progress.tga
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 64 KiB |
Reference in New Issue
Block a user