Add files via upload
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
local _, playerClass = UnitClass("player")
|
||||
local COMBO_CLASS = (playerClass == "ROGUE" or playerClass == "DRUID")
|
||||
|
||||
local MAX_POINTS = 5
|
||||
local POINT_GAP = 1
|
||||
local POINT_SIZE = 16
|
||||
local CUSTOM_POINT_TEXTURE = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\combo_point_orb.tga"
|
||||
|
||||
local function DarkComboPointBorderActive()
|
||||
-- Keep this independent from both ShaguTweaks and Dark Nameplate Border.
|
||||
-- Players can choose the dark Combo Point rim without changing any other UI.
|
||||
return BNP.IsDarkComboPointBorderEnabled
|
||||
and BNP:IsDarkComboPointBorderEnabled()
|
||||
end
|
||||
|
||||
local function ApplyComboPointTexture(point, force)
|
||||
if not point or not point.texture then return end
|
||||
local dark = DarkComboPointBorderActive() and true or false
|
||||
|
||||
-- Gold and dark are stored side by side in one atlas. Switching texture
|
||||
-- coordinates is reliable on old 1.12 clients and avoids their texture-file
|
||||
-- cache retaining the gold asset when a second filename is selected.
|
||||
if force or point.BNPComboPointTexture ~= CUSTOM_POINT_TEXTURE then
|
||||
point.texture:SetTexture(CUSTOM_POINT_TEXTURE)
|
||||
point.BNPComboPointTexture = CUSTOM_POINT_TEXTURE
|
||||
end
|
||||
|
||||
if force or point.BNPComboPointDarkBorder ~= dark then
|
||||
if dark then
|
||||
point.texture:SetTexCoord(0.5, 1, 0, 1)
|
||||
else
|
||||
point.texture:SetTexCoord(0, 0.5, 0, 1)
|
||||
end
|
||||
if point.texture.SetVertexColor then
|
||||
point.texture:SetVertexColor(1, 1, 1, 1)
|
||||
end
|
||||
point.BNPComboPointDarkBorder = dark
|
||||
end
|
||||
end
|
||||
|
||||
local function ApplyComboPointHolderStyle(holder, force)
|
||||
if not holder or not holder.points then return end
|
||||
local i
|
||||
for i = 1, MAX_POINTS do
|
||||
ApplyComboPointTexture(holder.points[i], force)
|
||||
end
|
||||
end
|
||||
|
||||
-- Combo points can remain on the previous unit after changing target on this
|
||||
-- server. Track the owning GUID ourselves instead of tying the display to the
|
||||
-- currently selected target.
|
||||
local comboOwnerGUID = nil
|
||||
local comboOwnerCount = 0
|
||||
local comboOwnerSynthetic = false
|
||||
|
||||
-- Some 1.12 clients do not immediately expose the first point on a newly
|
||||
-- selected target through GetComboPoints(). Remember the generator CAST, but
|
||||
-- do NOT move ownership from CAST alone: melee generators can miss/dodge/parry.
|
||||
-- Ownership is moved immediately when PLAYER_COMBO_POINTS confirms that a
|
||||
-- combo-point change actually happened. This is event-driven and adds no scan.
|
||||
local pendingGenerator = nil
|
||||
local GENERATOR_CONFIRM_WINDOW = 0.75
|
||||
|
||||
local function Enabled()
|
||||
return COMBO_CLASS and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled()
|
||||
end
|
||||
|
||||
local function GetUnitGUID(unit)
|
||||
local exists, guid = UnitExists(unit)
|
||||
if exists and guid then return guid end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetTargetGUID()
|
||||
return GetUnitGUID("target")
|
||||
end
|
||||
|
||||
local function GetPlayerGUID()
|
||||
return GetUnitGUID("player")
|
||||
end
|
||||
|
||||
local function GetComboCount()
|
||||
if not GetComboPoints then return 0 end
|
||||
local count = tonumber(GetComboPoints()) or 0
|
||||
if count < 0 then count = 0 end
|
||||
if count > MAX_POINTS then count = MAX_POINTS end
|
||||
return count
|
||||
end
|
||||
|
||||
local ROGUE_GENERATOR_IDS = {
|
||||
-- Sinister Strike
|
||||
[1752]=1,[1757]=1,[1758]=1,[1759]=1,[1760]=1,[8621]=1,[11293]=1,[11294]=1,
|
||||
-- Backstab
|
||||
[53]=1,[2589]=1,[2590]=1,[2591]=1,[8721]=1,[11279]=1,[11280]=1,[11281]=1,
|
||||
-- Gouge
|
||||
[1776]=1,[1777]=1,[8629]=1,[11285]=1,[11286]=1,
|
||||
-- Ambush
|
||||
[8676]=1,[8724]=1,[8725]=1,[11267]=1,[11268]=1,[11269]=1,
|
||||
-- Garrote
|
||||
[703]=1,[8631]=1,[8632]=1,[8633]=1,[11289]=1,[11290]=1,
|
||||
-- Cheap Shot / Hemorrhage / Ghostly Strike / Premeditation
|
||||
[1833]=1,[16511]=1,[17347]=1,[17348]=1,[14278]=1,[14183]=2,
|
||||
}
|
||||
|
||||
local DRUID_GENERATOR_IDS = {
|
||||
-- Claw
|
||||
[1082]=1,[3029]=1,[5201]=1,[9849]=1,[9850]=1,
|
||||
-- Rake
|
||||
[1822]=1,[1823]=1,[1824]=1,[9904]=1,
|
||||
-- Shred
|
||||
[5221]=1,[6800]=1,[8992]=1,[9829]=1,[9830]=1,
|
||||
-- Ravage
|
||||
[6785]=1,[6787]=1,[9866]=1,[9867]=1,
|
||||
-- Pounce
|
||||
[9005]=1,[9823]=1,[9827]=1,
|
||||
-- Mangle (Cat), for clients/servers that expose TBC/custom IDs
|
||||
[33876]=1,[33982]=1,[33983]=1,
|
||||
}
|
||||
|
||||
local ROGUE_GENERATOR_NAMES = {
|
||||
["sinister strike"]=1,["finsterer stoß"]=1,
|
||||
["backstab"]=1,["meucheln"]=1,
|
||||
["gouge"]=1,["solarplexus"]=1,
|
||||
["ambush"]=1,["hinterhalt"]=1,
|
||||
["garrote"]=1,["erdrosseln"]=1,
|
||||
["cheap shot"]=1,["fieser trick"]=1,
|
||||
["hemorrhage"]=1,["blutsturz"]=1,
|
||||
["ghostly strike"]=1,["geisterhafter stoß"]=1,
|
||||
["premeditation"]=2,
|
||||
}
|
||||
|
||||
local DRUID_GENERATOR_NAMES = {
|
||||
["claw"]=1,["klaue"]=1,
|
||||
["rake"]=1,["krallenhieb"]=1,
|
||||
["shred"]=1,["schreddern"]=1,
|
||||
["ravage"]=1,["verheeren"]=1,
|
||||
["pounce"]=1,["anspringen"]=1,
|
||||
["mangle"]=1,["mangle (cat)"]=1,["zerfleischen"]=1,["zerfleischen (katze)"]=1,
|
||||
}
|
||||
|
||||
local function ResolveSpellName(spellID)
|
||||
if not spellID or not SpellInfo then return nil end
|
||||
local ok, name = pcall(SpellInfo, spellID)
|
||||
if ok and name then return string.lower(tostring(name)) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetGeneratorGain(spellID)
|
||||
local id = tonumber(spellID)
|
||||
if playerClass == "ROGUE" then
|
||||
if id and ROGUE_GENERATOR_IDS[id] then return ROGUE_GENERATOR_IDS[id] end
|
||||
local name = ResolveSpellName(id)
|
||||
if name then return ROGUE_GENERATOR_NAMES[name] end
|
||||
elseif playerClass == "DRUID" then
|
||||
if id and DRUID_GENERATOR_IDS[id] then return DRUID_GENERATOR_IDS[id] end
|
||||
local name = ResolveSpellName(id)
|
||||
if name then return DRUID_GENERATOR_NAMES[name] end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function BeginPendingGenerator(targetGUID, spellID, gain)
|
||||
if not targetGUID or not gain then return end
|
||||
pendingGenerator = {
|
||||
guid = targetGUID,
|
||||
spellID = tonumber(spellID),
|
||||
gain = tonumber(gain) or 1,
|
||||
started = GetTime and GetTime() or 0,
|
||||
}
|
||||
end
|
||||
|
||||
local function CancelPendingGenerator(targetGUID, spellID)
|
||||
if not pendingGenerator then return end
|
||||
if targetGUID and pendingGenerator.guid ~= targetGUID then return end
|
||||
if spellID and pendingGenerator.spellID and tonumber(spellID) ~= pendingGenerator.spellID then return end
|
||||
pendingGenerator = nil
|
||||
end
|
||||
|
||||
local function ApplyPendingGenerator()
|
||||
if not pendingGenerator then return false end
|
||||
|
||||
local now = GetTime and GetTime() or 0
|
||||
local started = pendingGenerator.started or now
|
||||
|
||||
-- PLAYER_COMBO_POINTS should arrive essentially immediately after a real
|
||||
-- point gain. Ignore stale CAST candidates so a missed attack can never be
|
||||
-- applied later by an unrelated combo-point event.
|
||||
if now - started > GENERATOR_CONFIRM_WINDOW then
|
||||
pendingGenerator = nil
|
||||
return false
|
||||
end
|
||||
|
||||
local guid = pendingGenerator.guid
|
||||
local gain = pendingGenerator.gain or 1
|
||||
local targetGUID = GetTargetGUID()
|
||||
local apiCount = 0
|
||||
|
||||
-- Only trust GetComboPoints for the pending unit while it is the current
|
||||
-- target. If the broken 1.12 client still reports zero, PLAYER_COMBO_POINTS
|
||||
-- itself is the confirmation and we use the inferred generator gain.
|
||||
if targetGUID and targetGUID == guid then
|
||||
apiCount = GetComboCount()
|
||||
end
|
||||
|
||||
if apiCount > 0 then
|
||||
comboOwnerGUID = guid
|
||||
comboOwnerCount = apiCount
|
||||
comboOwnerSynthetic = false
|
||||
else
|
||||
if comboOwnerGUID == guid then
|
||||
comboOwnerCount = math.min(MAX_POINTS, (comboOwnerCount or 0) + gain)
|
||||
else
|
||||
-- First confirmed generator on a different target: old owner's points
|
||||
-- are gone and ownership switches immediately to this GUID.
|
||||
comboOwnerGUID = guid
|
||||
comboOwnerCount = math.min(MAX_POINTS, gain)
|
||||
end
|
||||
comboOwnerSynthetic = true
|
||||
end
|
||||
|
||||
pendingGenerator = nil
|
||||
return true
|
||||
end
|
||||
|
||||
-- Read authoritative API state when available. A zero on some *other* current
|
||||
-- target must never delete the stored points from the previous owner.
|
||||
local function SyncComboOwnerFromAPI(allowZeroOnOwner)
|
||||
local targetGUID = GetTargetGUID()
|
||||
if not targetGUID then return end
|
||||
|
||||
local count = GetComboCount()
|
||||
if count > 0 then
|
||||
comboOwnerGUID = targetGUID
|
||||
comboOwnerCount = count
|
||||
comboOwnerSynthetic = false
|
||||
BNP.spentComboSnapshot = nil
|
||||
pendingGenerator = nil
|
||||
return
|
||||
end
|
||||
|
||||
if allowZeroOnOwner and comboOwnerGUID == targetGUID and not pendingGenerator then
|
||||
if comboOwnerCount > 0 then
|
||||
BNP.spentComboSnapshot = {
|
||||
guid = comboOwnerGUID,
|
||||
count = comboOwnerCount,
|
||||
time = GetTime and GetTime() or 0,
|
||||
}
|
||||
end
|
||||
comboOwnerGUID = nil
|
||||
comboOwnerCount = 0
|
||||
comboOwnerSynthetic = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Finishers need the points that belonged to their exact cast target. Prefer
|
||||
-- our GUID owner because the client may already report zero after spending the
|
||||
-- points; fall back to the live API only for the matching current target.
|
||||
function BNP:GetTrackedComboPointsForGUID(guid)
|
||||
if guid and comboOwnerGUID == guid and comboOwnerCount > 0 then
|
||||
return comboOwnerCount
|
||||
end
|
||||
|
||||
local snapshot = BNP.spentComboSnapshot
|
||||
local now = GetTime and GetTime() or 0
|
||||
if guid and snapshot and snapshot.guid == guid
|
||||
and now - (snapshot.time or 0) <= 1.0
|
||||
and (snapshot.count or 0) > 0 then
|
||||
return snapshot.count
|
||||
end
|
||||
|
||||
local targetGUID = GetTargetGUID()
|
||||
if targetGUID and (not guid or targetGUID == guid) then
|
||||
return GetComboCount()
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
local function GetSourcePoint()
|
||||
if getglobal then return getglobal("TargetFrameComboPoint1") end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetComboYOffset()
|
||||
if BNP.GetComboPointsYOffset then return BNP:GetComboPointsYOffset() end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function AnchorComboHolder(plate, holder)
|
||||
if not plate or not holder or not plate.name then return end
|
||||
holder:ClearAllPoints()
|
||||
holder:SetPoint("BOTTOM", plate.name, "TOP", 0, 2 + GetComboYOffset())
|
||||
end
|
||||
|
||||
local function ApplyTargetFrameScale(holder)
|
||||
if not holder then return end
|
||||
local source = GetSourcePoint()
|
||||
local parent = holder:GetParent()
|
||||
local sourceScale = 1.0
|
||||
local parentScale = 1.0
|
||||
|
||||
if source and source.GetEffectiveScale then
|
||||
sourceScale = tonumber(source:GetEffectiveScale()) or 1.0
|
||||
elseif UIParent and UIParent.GetEffectiveScale then
|
||||
sourceScale = tonumber(UIParent:GetEffectiveScale()) or 1.0
|
||||
elseif UIParent and UIParent.GetScale then
|
||||
sourceScale = tonumber(UIParent:GetScale()) or 1.0
|
||||
end
|
||||
|
||||
if parent and parent.GetEffectiveScale then
|
||||
parentScale = tonumber(parent:GetEffectiveScale()) or 1.0
|
||||
end
|
||||
|
||||
if sourceScale <= 0 then sourceScale = 1.0 end
|
||||
if parentScale <= 0 then parentScale = 1.0 end
|
||||
holder:SetScale(sourceScale / parentScale)
|
||||
end
|
||||
|
||||
local function CreateCustomPoint(parent)
|
||||
local point = CreateFrame("Frame", nil, parent)
|
||||
point:SetWidth(POINT_SIZE)
|
||||
point:SetHeight(POINT_SIZE)
|
||||
|
||||
local tex = point:CreateTexture(nil, "ARTWORK")
|
||||
tex:SetTexture(CUSTOM_POINT_TEXTURE)
|
||||
tex:SetAllPoints(point)
|
||||
tex:SetTexCoord(0, 0.5, 0, 1)
|
||||
tex:SetBlendMode("BLEND")
|
||||
point.texture = tex
|
||||
point.BNPComboPointTexture = CUSTOM_POINT_TEXTURE
|
||||
point.BNPComboPointDarkBorder = false
|
||||
ApplyComboPointTexture(point)
|
||||
|
||||
point:Hide()
|
||||
return point
|
||||
end
|
||||
|
||||
local function EnsurePoints(plate)
|
||||
if plate.BNPComboPoints then
|
||||
AnchorComboHolder(plate, plate.BNPComboPoints)
|
||||
ApplyTargetFrameScale(plate.BNPComboPoints)
|
||||
ApplyComboPointHolderStyle(plate.BNPComboPoints)
|
||||
return plate.BNPComboPoints
|
||||
end
|
||||
|
||||
if not plate or not plate.name then return nil end
|
||||
|
||||
local parent = plate.BNPScaleWrapper or plate
|
||||
local holder = CreateFrame("Frame", nil, parent)
|
||||
holder:SetWidth(MAX_POINTS * POINT_SIZE + (MAX_POINTS - 1) * POINT_GAP)
|
||||
holder:SetHeight(POINT_SIZE)
|
||||
AnchorComboHolder(plate, holder)
|
||||
holder:SetFrameLevel((plate:GetFrameLevel() or 1) + 12)
|
||||
holder.points = {}
|
||||
holder:Hide()
|
||||
|
||||
local i
|
||||
for i = 1, MAX_POINTS do
|
||||
local point = CreateCustomPoint(holder)
|
||||
if i == 1 then
|
||||
point:SetPoint("LEFT", holder, "LEFT", 0, 0)
|
||||
else
|
||||
point:SetPoint("LEFT", holder.points[i - 1], "RIGHT", POINT_GAP, 0)
|
||||
end
|
||||
holder.points[i] = point
|
||||
end
|
||||
|
||||
ApplyComboPointHolderStyle(holder)
|
||||
ApplyTargetFrameScale(holder)
|
||||
plate.BNPComboPoints = holder
|
||||
return holder
|
||||
end
|
||||
|
||||
local function HidePoints(plate)
|
||||
local holder = plate and plate.BNPComboPoints
|
||||
if not holder then return end
|
||||
local i
|
||||
for i = 1, MAX_POINTS do holder.points[i]:Hide() end
|
||||
holder:Hide()
|
||||
end
|
||||
|
||||
local function UpdatePlate(plate)
|
||||
if not plate or not plate:IsShown() then return end
|
||||
|
||||
if not Enabled() then HidePoints(plate) return end
|
||||
|
||||
local plateGUID = plate.GetName and plate:GetName(1) or nil
|
||||
if not plateGUID or not comboOwnerGUID or plateGUID ~= comboOwnerGUID then
|
||||
HidePoints(plate)
|
||||
return
|
||||
end
|
||||
|
||||
if comboOwnerCount <= 0 then
|
||||
HidePoints(plate)
|
||||
return
|
||||
end
|
||||
|
||||
local holder = EnsurePoints(plate)
|
||||
if not holder then return end
|
||||
ApplyTargetFrameScale(holder)
|
||||
|
||||
local i
|
||||
for i = 1, MAX_POINTS do
|
||||
if i <= comboOwnerCount then holder.points[i]:Show() else holder.points[i]:Hide() end
|
||||
end
|
||||
holder:Show()
|
||||
end
|
||||
|
||||
function BNP:RefreshComboPoints()
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate:IsShown() then UpdatePlate(plate) else HidePoints(plate) end
|
||||
end
|
||||
if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end
|
||||
end
|
||||
|
||||
-- Settings can be changed while no unit owns Combo Points. Update every
|
||||
-- already-created holder directly instead of depending on UpdatePlate(), which
|
||||
-- correctly skips plates without an active Combo Point owner.
|
||||
function BNP:RefreshComboPointBorderStyle()
|
||||
local plate
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate and plate.BNPComboPoints then
|
||||
ApplyComboPointHolderStyle(plate.BNPComboPoints, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(BNP.libnameplate.OnInit, function(plate)
|
||||
if COMBO_CLASS then EnsurePoints(plate) HidePoints(plate) end
|
||||
end)
|
||||
|
||||
table.insert(BNP.libnameplate.OnShow, function(plate)
|
||||
if COMBO_CLASS then
|
||||
-- Positive API counts may be adopted when a plate appears, but a zero on a
|
||||
-- newly selected target must not clear the previous owner's points.
|
||||
SyncComboOwnerFromAPI(false)
|
||||
HidePoints(plate)
|
||||
UpdatePlate(plate)
|
||||
end
|
||||
end)
|
||||
|
||||
table.insert(BNP.libnameplate.OnUpdate, function(plate)
|
||||
if COMBO_CLASS then UpdatePlate(plate) end
|
||||
end)
|
||||
|
||||
if COMBO_CLASS then
|
||||
local events = CreateFrame("Frame")
|
||||
events:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
events:RegisterEvent("PLAYER_COMBO_POINTS")
|
||||
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
events:RegisterEvent("UNIT_CASTEVENT")
|
||||
|
||||
events:SetScript("OnEvent", function()
|
||||
if event == "UNIT_CASTEVENT" then
|
||||
local casterGUID = arg1
|
||||
local targetGUID = arg2
|
||||
local eventType = arg3
|
||||
local spellID = arg4
|
||||
local playerGUID = GetPlayerGUID()
|
||||
if not playerGUID or casterGUID ~= playerGUID then return end
|
||||
|
||||
local gain = GetGeneratorGain(spellID)
|
||||
if not gain then return end
|
||||
|
||||
if eventType == "CAST" then
|
||||
BeginPendingGenerator(targetGUID or GetTargetGUID(), spellID, gain)
|
||||
elseif eventType == "FAIL" then
|
||||
CancelPendingGenerator(targetGUID, spellID)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_TARGET_CHANGED" then
|
||||
-- This is intentionally positive-only. Changing target by itself must
|
||||
-- never clear the combo points stored on the previous GUID.
|
||||
SyncComboOwnerFromAPI(false)
|
||||
BNP:RefreshComboPoints()
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_COMBO_POINTS" then
|
||||
-- This is the success confirmation. Switch ownership immediately here,
|
||||
-- never on UNIT_CASTEVENT CAST alone. If the Blizzard frame/API still
|
||||
-- returns zero, the pending generator provides the synthetic first point.
|
||||
if pendingGenerator then
|
||||
if not ApplyPendingGenerator() then
|
||||
SyncComboOwnerFromAPI(true)
|
||||
end
|
||||
else
|
||||
SyncComboOwnerFromAPI(true)
|
||||
end
|
||||
BNP:RefreshComboPoints()
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
pendingGenerator = nil
|
||||
SyncComboOwnerFromAPI(false)
|
||||
BNP:RefreshComboPoints()
|
||||
end
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user