Add files via upload

This commit is contained in:
Relationship
2026-07-12 14:57:27 +01:00
committed by GitHub
parent d0329d309e
commit 28ef2ed7c9
7 changed files with 621 additions and 105 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
## Interface: 11200 ## Interface: 11200
## Title: RelationshipsThreatPlates - OctoWoW ## Title: RelationshipsThreatPlates - OctoWoW
## Notes: OctoWoW-compatible RelationshipsThreatPlates ## Notes: OctoWoW-compatible RelationshipsThreatPlates
## Author: Packaged By Relationship ## Author: Relationship
## Version: 1.3 ## Version: 1.4
## SavedVariables: RelationshipsThreatPlatesDB ## SavedVariables: RelationshipsThreatPlatesDB
## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates ## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates
## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates ## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates
+14 -1
View File
@@ -12,7 +12,7 @@ addon.MAJOR = 1
addon.MINOR = 1 addon.MINOR = 1
-- Version string -- Version string
TP_VERSION = "2.5.0" TP_VERSION = "2.6.1"
TP_ADDON_NAME = "RelationshipsThreatPlates" TP_ADDON_NAME = "RelationshipsThreatPlates"
-- ================================================================== -- ==================================================================
@@ -40,6 +40,19 @@ local DEFAULT_CONFIG = {
glowEnabled = false, glowEnabled = false,
glowSize = 3, glowSize = 3,
glowAsShadow = true, glowAsShadow = true,
-- New in 2.6
combatOnly = false,
hideBelowPct = 0,
warningSound = false,
warningSoundName = "RaidWarning",
warningThreshold = 80,
flashOnAggro = false,
flashSpeed = 4,
textFormat = "percent", -- percent | tank | status | combo
colorPreset = "kui", -- kui | cool | mono | warband
lockGUI = false,
guiPos = { guiPos = {
point = "CENTER", point = "CENTER",
relPoint = "CENTER", relPoint = "CENTER",
+126 -1
View File
@@ -35,7 +35,7 @@ local C = {
-- Layout -- Layout
local GUI_WIDTH = 320 local GUI_WIDTH = 320
local GUI_HEIGHT = 480 local GUI_HEIGHT = 520
local HEADER_H = 30 local HEADER_H = 30
local MARGIN = 14 local MARGIN = 14
local FONT_NORMAL = "Fonts\\FRIZQT__.TTF" local FONT_NORMAL = "Fonts\\FRIZQT__.TTF"
@@ -428,6 +428,80 @@ function addon.GUI:BuildSettings(parent)
widgets[configKey] = slider widgets[configKey] = slider
end end
-- ==============================================================
-- Helper: Dropdown (uses Blizzard's UIDropDownMenuTemplate)
-- ==============================================================
local function Dropdown(labelText, configKey, options)
local label = parent:CreateFontString(nil, "OVERLAY")
label:SetFont(FONT_NORMAL, FONT_SIZE)
label:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
label:SetTextColor(unpack(C.TEXT))
label:SetText(labelText)
y = y + 16
local dd = CreateFrame("Frame", "RelationshipsThreatPlatesDD_" .. configKey, parent, "UIDropDownMenuTemplate")
dd:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN - 14, -y)
UIDropDownMenu_SetWidth(160, dd)
local function GetLabelFor(val)
for i = 1, table.getn(options) do
if options[i].value == val then return options[i].label end
end
return tostring(val)
end
UIDropDownMenu_Initialize(dd, function()
for i = 1, table.getn(options) do
local opt = options[i]
local info = {}
info.text = opt.label
info.value = opt.value
info.func = function()
RelationshipsThreatPlatesDB[configKey] = this.value
UIDropDownMenu_SetSelectedValue(dd, this.value)
UIDropDownMenu_SetText(GetLabelFor(this.value), dd)
ApplySettingChange(configKey)
end
info.checked = (RelationshipsThreatPlatesDB[configKey] == opt.value)
UIDropDownMenu_AddButton(info)
end
end)
UIDropDownMenu_SetSelectedValue(dd, RelationshipsThreatPlatesDB[configKey])
UIDropDownMenu_SetText(GetLabelFor(RelationshipsThreatPlatesDB[configKey]), dd)
y = y + 34
widgets[configKey] = dd
end
-- ==============================================================
-- Helper: small action button
-- ==============================================================
local function ActionButton(labelText, onClick)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(W - MARGIN * 2)
btn:SetHeight(20)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
SafeSetBackdrop(btn, {
bgFile = C.SOLID,
edgeFile = C.SOLID,
tile = false, tileSize = 0,
edgeSize = 1,
})
btn:SetBackdropColor(unpack(C.BUTTON_BG))
btn:SetBackdropBorderColor(unpack(C.BORDER))
local t = btn:CreateFontString(nil, "OVERLAY")
t:SetFont(FONT_NORMAL, FONT_SIZE, "OUTLINE")
t:SetPoint("CENTER", btn, "CENTER", 0, 0)
t:SetTextColor(unpack(C.BUTTON_TEXT))
t:SetText(labelText)
btn:SetFontString(t)
btn:SetScript("OnEnter", function() this:SetBackdropColor(unpack(C.BUTTON_HOVER)) end)
btn:SetScript("OnLeave", function() this:SetBackdropColor(unpack(C.BUTTON_BG)) end)
btn:SetScript("OnClick", onClick)
y = y + 24
return btn
end
-- ============================================================== -- ==============================================================
-- GENERAL SECTION -- GENERAL SECTION
-- ============================================================== -- ==============================================================
@@ -469,6 +543,57 @@ function addon.GUI:BuildSettings(parent)
CheckBox("Show on Neutral", "showOnNeutral", "Show threat bars on neutral units") CheckBox("Show on Neutral", "showOnNeutral", "Show threat bars on neutral units")
CheckBox("Show on Friendly", "showOnFriendly", "Show threat bars on friendly units") CheckBox("Show on Friendly", "showOnFriendly", "Show threat bars on friendly units")
-- ==============================================================
-- BEHAVIOR SECTION (2.6)
-- ==============================================================
SectionLabel("Behavior")
CheckBox("Combat Only", "combatOnly", "Only show bars while you are in combat")
Slider("Hide Below %", "hideBelowPct", 0, 90, 5, "%")
Dropdown("Text Format", "textFormat", {
{ label = "Percent (85%)", value = "percent" },
{ label = "Tank Lead (+123)", value = "tank" },
{ label = "Status (SAFE/PULL!)", value = "status" },
{ label = "Combo (85% +123)", value = "combo" },
})
Dropdown("Colour Preset", "colorPreset", {
{ label = "Kui (green -> red)", value = "kui" },
{ label = "Cool (blue -> red)", value = "cool" },
{ label = "Mono (grey -> red)", value = "mono" },
{ label = "Warband (cyan -> red)", value = "warband" },
})
-- ==============================================================
-- WARNINGS SECTION (2.6)
-- ==============================================================
SectionLabel("Warnings")
CheckBox("Flash Bar on Aggro", "flashOnAggro", "Pulse the bar when you have 100% aggro")
Slider("Flash Speed", "flashSpeed", 1, 10, 1, "")
CheckBox("Play Warning Sound", "warningSound", "Sound when threat crosses the threshold")
Slider("Warning Threshold", "warningThreshold", 40, 100, 5, "%")
Dropdown("Warning Sound", "warningSoundName", {
{ label = "Raid Warning", value = "RaidWarning" },
{ label = "Tell Message", value = "TellMessage" },
{ label = "Murloc Aggro", value = "MurlocAggroLocal" },
{ label = "Auction Open", value = "AuctionWindowOpen"},
{ label = "Level Up", value = "LEVELUP" },
})
ActionButton("Test Warning Sound", function()
local snd = RelationshipsThreatPlatesDB.warningSoundName or "RaidWarning"
pcall(PlaySound, snd)
end)
-- ==============================================================
-- WINDOW SECTION (2.6)
-- ==============================================================
SectionLabel("Config Window")
ActionButton("Reset Config Window Position", function()
RelationshipsThreatPlatesDB.guiPos = { point = "CENTER", relPoint = "CENTER", x = 0, y = 0 }
if RelationshipsThreatPlatesGUI then
RelationshipsThreatPlatesGUI:ClearAllPoints()
RelationshipsThreatPlatesGUI:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end
end)
-- ============================================================== -- ==============================================================
-- Reset button at bottom -- Reset button at bottom
-- ============================================================== -- ==============================================================
+45 -11
View File
@@ -69,6 +69,37 @@ function addon.Media.Brighten(factor, r, g, b, a)
a a
end end
-- ==================================================================
-- Colour presets for the threat gradient (2.6)
-- Each preset defines 4 stops: low (0%), medium (50%), high (80%), aggro (100%)
-- ==================================================================
addon.Media.COLOR_PRESETS = {
kui = {
low = { 0.33, 0.80, 0.33 },
medium = { 1.00, 0.85, 0.00 },
high = { 1.00, 0.50, 0.00 },
aggro = { 1.00, 0.10, 0.10 },
},
cool = {
low = { 0.20, 0.55, 1.00 },
medium = { 0.55, 0.35, 1.00 },
high = { 1.00, 0.35, 0.55 },
aggro = { 1.00, 0.10, 0.10 },
},
mono = {
low = { 0.55, 0.55, 0.55 },
medium = { 0.80, 0.80, 0.80 },
high = { 1.00, 1.00, 1.00 },
aggro = { 1.00, 0.20, 0.20 },
},
warband = {
low = { 0.20, 0.90, 0.90 },
medium = { 0.60, 0.90, 0.30 },
high = { 1.00, 0.80, 0.20 },
aggro = { 1.00, 0.10, 0.10 },
},
}
-- ================================================================== -- ==================================================================
-- Helper: get smooth interpolated threat colour -- Helper: get smooth interpolated threat colour
-- ================================================================== -- ==================================================================
@@ -76,25 +107,28 @@ function addon.Media.GetThreatColour(pct)
pct = tonumber(pct) or 0 pct = tonumber(pct) or 0
pct = math.min(100, math.max(0, pct)) pct = math.min(100, math.max(0, pct))
local c = addon.Media.COLOURS local presetName = (RelationshipsThreatPlatesDB and RelationshipsThreatPlatesDB.colorPreset) or "kui"
local p = addon.Media.COLOR_PRESETS[presetName] or addon.Media.COLOR_PRESETS.kui
local lo, md, hi, ag = p.low, p.medium, p.high, p.aggro
local r, g, b local r, g, b
if pct < 50 then if pct < 50 then
local t = pct / 50 local t = pct / 50
r = c.THREAT_LOW.r + (c.THREAT_MEDIUM.r - c.THREAT_LOW.r) * t r = lo[1] + (md[1] - lo[1]) * t
g = c.THREAT_LOW.g + (c.THREAT_MEDIUM.g - c.THREAT_LOW.g) * t g = lo[2] + (md[2] - lo[2]) * t
b = c.THREAT_LOW.b + (c.THREAT_MEDIUM.b - c.THREAT_LOW.b) * t b = lo[3] + (md[3] - lo[3]) * t
elseif pct < 80 then elseif pct < 80 then
local t = (pct - 50) / 30 local t = (pct - 50) / 30
r = c.THREAT_MEDIUM.r + (c.THREAT_HIGH.r - c.THREAT_MEDIUM.r) * t r = md[1] + (hi[1] - md[1]) * t
g = c.THREAT_MEDIUM.g + (c.THREAT_HIGH.g - c.THREAT_MEDIUM.g) * t g = md[2] + (hi[2] - md[2]) * t
b = c.THREAT_MEDIUM.b + (c.THREAT_HIGH.b - c.THREAT_MEDIUM.b) * t b = md[3] + (hi[3] - md[3]) * t
else else
local t = (pct - 80) / 20 local t = (pct - 80) / 20
t = math.min(1, math.max(0, t)) if t > 1 then t = 1 end
r = c.THREAT_HIGH.r + (c.THREAT_AGGRO.r - c.THREAT_HIGH.r) * t if t < 0 then t = 0 end
g = c.THREAT_HIGH.g + (c.THREAT_AGGRO.g - c.THREAT_HIGH.g) * t r = hi[1] + (ag[1] - hi[1]) * t
b = c.THREAT_HIGH.b + (c.THREAT_AGGRO.b - c.THREAT_HIGH.b) * t g = hi[2] + (ag[2] - hi[2]) * t
b = hi[3] + (ag[3] - hi[3]) * t
end end
return r, g, b return r, g, b
+240 -82
View File
@@ -890,8 +890,31 @@ function addon.Nameplates:ApplyThreatToPlate(data, pct, situation)
-- Now we only check if there's actual threat data to show. The combat -- Now we only check if there's actual threat data to show. The combat
-- verification is done earlier in the pipeline (IsPlateInCombat, -- verification is done earlier in the pipeline (IsPlateInCombat,
-- GetThreatByName, etc.). -- GetThreatByName, etc.).
-- Combat-only display (2.6)
if RelationshipsThreatPlatesDB.combatOnly then
local inCombat = false
if UnitAffectingCombat then
local ok, r = pcall(UnitAffectingCombat, "player")
if ok then inCombat = r end
end
if not inCombat then
data.container:Hide()
if addon.Nameplates.SetFlashing then addon.Nameplates:SetFlashing(data, false) end
return
end
end
-- Hide below threshold (2.6)
local minShow = RelationshipsThreatPlatesDB.hideBelowPct or 0
if minShow > 0 and (pct or 0) < minShow then
data.container:Hide()
if addon.Nameplates.SetFlashing then addon.Nameplates:SetFlashing(data, false) end
return
end
if pct == 0 then if pct == 0 then
data.container:Hide() data.container:Hide()
if addon.Nameplates.SetFlashing then addon.Nameplates:SetFlashing(data, false) end
return return
end end
@@ -961,24 +984,30 @@ function addon.Nameplates:ApplyThreatToPlate(data, pct, situation)
-- Update text — uses rawPct so values above 100% are shown as-is. -- Update text — uses rawPct so values above 100% are shown as-is.
if data.text then if data.text then
local textStr local fmt = RelationshipsThreatPlatesDB.textFormat or "percent"
if RelationshipsThreatPlatesDB.tankMode and data.plateName then local pctStr = math.floor(rawPct + 0.5) .. "%"
-- Tank mode: show raw threat lead over the 2nd-highest player local tankStr = nil
-- (positive = you have that much lead as top-threat holder; if data.plateName and (RelationshipsThreatPlatesDB.tankMode or fmt == "tank" or fmt == "combo") then
-- negative = you're that much behind the current top). tankStr = addon.Threat:GetTankDisplayForMob(data.plateName)
textStr = addon.Threat:GetTankDisplayForMob(data.plateName)
if not textStr or textStr == "--" then
-- Fall back to % when no absolute values are available.
textStr = math.floor(rawPct + 0.5) .. "%"
end end
local textStr
if RelationshipsThreatPlatesDB.tankMode and tankStr and tankStr ~= "--" then
textStr = tankStr
elseif fmt == "tank" and tankStr and tankStr ~= "--" then
textStr = tankStr
elseif fmt == "status" then
if rawPct >= 100 then textStr = "PULL!"
elseif rawPct >= 80 then textStr = "WARN"
elseif rawPct >= 50 then textStr = "HIGH"
else textStr = "SAFE" end
elseif fmt == "combo" and tankStr and tankStr ~= "--" then
textStr = pctStr .. " " .. tankStr
else else
textStr = math.floor(rawPct + 0.5) .. "%" textStr = pctStr
end end
data.text:SetText(textStr) data.text:SetText(textStr)
if rawPct >= 100 then if rawPct >= 100 then
-- Aggro / over-aggro: bright red text so the top holder
-- stands out even when the bar itself is pegged at full.
data.text:SetTextColor(1, 0.2, 0.2, 1) data.text:SetTextColor(1, 0.2, 0.2, 1)
elseif rawPct >= 60 then elseif rawPct >= 60 then
data.text:SetTextColor(1, 0.9, 0.8, 1) data.text:SetTextColor(1, 0.9, 0.8, 1)
@@ -987,6 +1016,24 @@ function addon.Nameplates:ApplyThreatToPlate(data, pct, situation)
end end
end end
-- Warning sound (edge-triggered, 2.6)
do
local db = RelationshipsThreatPlatesDB
local prevWarn = data.lastWarnPct or 0
local threshold = db.warningThreshold or 80
if db.warningSound and threshold > 0
and rawPct >= threshold and prevWarn < threshold then
local snd = db.warningSoundName or "RaidWarning"
pcall(PlaySound, snd)
end
data.lastWarnPct = rawPct
-- Flash on aggro (2.6)
if addon.Nameplates.SetFlashing then
addon.Nameplates:SetFlashing(data, (db.flashOnAggro and rawPct >= 100) and true or false)
end
end
-- Update threat glow (drive by clamped barPct so it caps at max intensity) -- Update threat glow (drive by clamped barPct so it caps at max intensity)
if data.threatGlow then if data.threatGlow then
if barPct >= 80 then if barPct >= 80 then
@@ -1058,7 +1105,7 @@ end
-- --
-- Returns: unit token string, or nil -- Returns: unit token string, or nil
-- ================================================================== -- ==================================================================
function addon.Nameplates:FindCombatUnitForPlate(data) function addon.Nameplates:FindCombatUnitForPlate(data, excludeUnits)
if not data or not data.plateName then return nil end if not data or not data.plateName then return nil end
if strlen(data.plateName) == 0 then return nil end if strlen(data.plateName) == 0 then return nil end
@@ -1076,25 +1123,67 @@ function addon.Nameplates:FindCombatUnitForPlate(data)
-- Get plate health percentage for matching -- Get plate health percentage for matching
local plateHealthPct = GetPlateHealthPercent(data) local plateHealthPct = GetPlateHealthPercent(data)
-- Same-name mob safety: if THIS plate's health looks idle
-- (near-full and not dropping), refuse to bind any combat
-- unit token. Binding a nearby attacker's unit token to an
-- idle same-name plate is the root cause of the "idle mob
-- shows attacker's threat bar" bug.
local plateLooksIdle = false
if plateHealthPct and plateHealthPct >= 99.5 then
if not data.previousHealth or data.previousHealth >= 99.5 then
plateLooksIdle = true
end
end
if plateLooksIdle then
return nil
end
-- Score each candidate: we want a unit that is: -- Score each candidate: we want a unit that is:
-- 1. In combat (mandatory — idle mobs must never match) -- 1. In combat (mandatory — idle mobs must never match)
-- 2. Health % close to the plate's health bar -- 2. Health % close to the plate's health bar
-- 3. Hostile and attackable -- 3. Hostile and attackable
-- 4. Not already assigned to a different plate this cycle
local bestUnit = nil local bestUnit = nil
local bestScore = -999999 local bestScore = -999999
for i = 1, table.getn(candidates) do for i = 1, table.getn(candidates) do
local c = candidates[i] local c = candidates[i]
-- MANDATORY: Unit must be in combat -- Skip candidates already claimed by another plate this cycle
if c.inCombat then local excluded = false
local score = 0 if excludeUnits then
for j = 1, table.getn(excludeUnits) do
if IsSameUnitToken(excludeUnits[j], c.unit) then
excluded = true
break
end
end
end
-- Health % matching (most important discriminator) -- MANDATORY: Unit must be in combat AND not already claimed.
if c.inCombat and not excluded then
local score = 0
local healthMatched = false
-- Health % matching (most important discriminator).
-- Require a reasonably close match — if the plate is at
-- 40% and the only combat candidate is at 100%, it is
-- almost certainly a DIFFERENT mob with the same name
-- and must NOT be bound to this plate.
if plateHealthPct and c.healthPct then if plateHealthPct and c.healthPct then
local diff = math.abs(plateHealthPct - c.healthPct) local diff = math.abs(plateHealthPct - c.healthPct)
-- Close health match = strong signal this is the right mob if diff <= 8 then
healthMatched = true
score = score + math.max(0, 500 - (diff * 15)) score = score + math.max(0, 500 - (diff * 15))
else
-- Too far apart — skip this candidate entirely.
score = -999999
end
else
-- No health info to compare. Fall back to previous
-- behaviour so freshly-shown plates can still bind
-- when their health bar hasn't been read yet.
healthMatched = true
end end
-- Hostile/attackable bonus -- Hostile/attackable bonus
@@ -1102,7 +1191,7 @@ function addon.Nameplates:FindCombatUnitForPlate(data)
score = score + 50 score = score + 50
end end
if score > bestScore then if score > bestScore and healthMatched then
bestScore = score bestScore = score
bestUnit = c.unit bestUnit = c.unit
end end
@@ -1141,6 +1230,23 @@ end
-- - Mobs attacking party members show threat even when player -- - Mobs attacking party members show threat even when player
-- isn't in combat -- isn't in combat
-- ================================================================== -- ==================================================================
-- ==================================================================
-- Fallback threat values when we know a mob is in combat but have
-- no ThreatEngine percentage. Distinguishes "mob is attacking the
-- player" (full aggro on us) from "mob is attacking a party member"
-- (we're not the aggro target, show a low visible bar).
-- ==================================================================
local function GetFallbackThreatForMob(name)
if name
and addon.ThreatEngine
and addon.ThreatEngine.IsMobAttackingPlayer
and addon.ThreatEngine:IsMobAttackingPlayer(name) then
return 100, 3
end
return 20, 0
end
function addon.Nameplates:UpdateAllPlates() function addon.Nameplates:UpdateAllPlates()
if not RelationshipsThreatPlatesDB or not RelationshipsThreatPlatesDB.enabled then if not RelationshipsThreatPlatesDB or not RelationshipsThreatPlatesDB.enabled then
for nameplate, data in pairs(self.plates) do for nameplate, data in pairs(self.plates) do
@@ -1183,6 +1289,16 @@ function addon.Nameplates:UpdateAllPlates()
end end
-- Step 4: Update threat display for each visible plate. -- Step 4: Update threat display for each visible plate.
-- Track combat unit tokens assigned this cycle so a single
-- combat mob can never be bound to multiple same-name plates
-- (which was causing idle same-name plates to display the
-- attacker's threat bar).
local assignedUnits = {}
for nameplate, data in pairs(self.plates) do
if data.unit then
assignedUnits[table.getn(assignedUnits) + 1] = data.unit
end
end
for nameplate, data in pairs(self.plates) do for nameplate, data in pairs(self.plates) do
if nameplate:IsVisible() then if nameplate:IsVisible() then
local isUniqueName = (platesPerName[data.plateName] or 1) <= 1 local isUniqueName = (platesPerName[data.plateName] or 1) <= 1
@@ -1191,6 +1307,21 @@ function addon.Nameplates:UpdateAllPlates()
local plateInCombat = IsPlateInCombat(data) local plateInCombat = IsPlateInCombat(data)
local hasThreatData = data.plateName and addon.ThreatEngine:HasDataForTarget(data.plateName) local hasThreatData = data.plateName and addon.ThreatEngine:HasDataForTarget(data.plateName)
-- v2.6.2: A mob is also "in combat with us" if the incoming
-- combat-log parser has seen it hit or miss the player /
-- a party member within the last few seconds. In Vanilla
-- 1.12 non-targeted mobs have no unit token, so this is
-- the ONLY reliable signal for "this specific mob is
-- attacking the group" when its health bar hasn't
-- dropped yet. We only apply this signal to unique-name
-- plates to avoid lighting up every same-name plate when
-- only one of them is actually attacking.
local mobAttackingGroup =
data.plateName
and addon.ThreatEngine.IsMobAttackingGroup
and addon.ThreatEngine:IsMobAttackingGroup(data.plateName)
or false
if data.unit and UnitExists(data.unit) then if data.unit and UnitExists(data.unit) then
-- We have a resolved unit token. Verify it's in combat. -- We have a resolved unit token. Verify it's in combat.
local ok_combat, mobInCombat = pcall(UnitAffectingCombat, data.unit) local ok_combat, mobInCombat = pcall(UnitAffectingCombat, data.unit)
@@ -1203,91 +1334,67 @@ function addon.Nameplates:UpdateAllPlates()
self:ApplyThreatToPlate(data, pct, situation or 0) self:ApplyThreatToPlate(data, pct, situation or 0)
end end
elseif isUniqueName and data.plateName then elseif isUniqueName and data.plateName then
-- UNIQUE-NAME MOB with no unit token: -- UNIQUE-NAME MOB with no unit token.
-- This is the common case for non-targeted attacking mobs. --
-- Since only ONE mob on screen has this name, ThreatEngine -- CAUTION: "unique" here only means one plate of this
-- data is unambiguous. Use health-based combat detection. -- name is currently VISIBLE. The camera may have rotated
-- away from a same-name attacker who is still active.
-- Because ThreatEngine data (and IsMobAttackingGroup)
-- are keyed by NAME, trusting them for an idle-looking
-- plate would light up the wrong mob when its twin
-- attacker is off-screen. So we require one of:
-- (a) confirmed health drop on THIS plate, OR
-- (b) a resolved combat unit token whose health
-- matches THIS plate.
-- A name-only signal (mobAttackingGroup / ThreatEngine
-- data) is never enough on its own.
if plateInCombat then if plateInCombat then
-- Mob is in combat (health dropping or below 100%). -- This specific plate's health is dropping — it's
-- Record when we first detected combat for grace period. -- in combat. Safe to trust name-keyed threat data.
if not data.combatDetectedAt then if not data.combatDetectedAt then
data.combatDetectedAt = now data.combatDetectedAt = now
end end
-- Try ThreatEngine data first
local pct, sit = addon.Threat:GetThreatByName(data.plateName) local pct, sit = addon.Threat:GetThreatByName(data.plateName)
if pct > 0 then if pct > 0 then
self:ApplyThreatToPlate(data, pct, sit or 0) self:ApplyThreatToPlate(data, pct, sit or 0)
else else
-- No ThreatEngine data but mob IS in combat.
-- This happens for body pulls, or when only
-- a party member has hit the mob.
--
-- If a group member is in combat, show a default
-- threat bar. The mob is fighting someone.
if groupInCombat then if groupInCombat then
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading do local _fp, _fs = GetFallbackThreatForMob(data.plateName); self:ApplyThreatToPlate(data, _fp, _fs) end
else else
data.container:Hide() data.container:Hide()
end end
end end
elseif hasThreatData and groupInCombat then elseif groupInCombat and (mobAttackingGroup or hasThreatData) then
-- Plate looks idle (full health) but ThreatEngine has -- Name-only signal says a mob of this name is
-- data and group is in combat. This can happen briefly -- attacking the group, but THIS plate shows no
-- after a mob is first hit (health bar hasn't updated) -- health drop. Verify by matching a real combat
-- or for a mob being attacked by someone else. -- unit token to this plate (health % match). If
-- Give a grace period. -- none matches, this plate is an idle bystander
-- (e.g. the real attacker rotated off-screen).
local unit = self:FindCombatUnitForPlate(data, assignedUnits)
if unit then
data.unit = unit
data.guid = addon:GetUnitID(unit)
assignedUnits[table.getn(assignedUnits) + 1] = unit
if not data.combatDetectedAt then if not data.combatDetectedAt then
data.combatDetectedAt = now data.combatDetectedAt = now
end end
if now - data.combatDetectedAt < 0.5 then local pct, situation = addon.Threat:GetThreatPercent(unit)
local pct, sit = addon.Threat:GetThreatByName(data.plateName)
if pct > 0 then if pct > 0 then
self:ApplyThreatToPlate(data, pct, sit or 0) self:ApplyThreatToPlate(data, pct, situation or 0)
else else
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading do local _fp, _fs = GetFallbackThreatForMob(data.plateName); self:ApplyThreatToPlate(data, _fp, _fs) end
end end
else else
-- Grace period expired and health still at 100%. -- Can't verify this plate is the attacker.
-- This is likely an idle mob. Hide bar. -- Do NOT show a bar based on name alone.
data.combatDetectedAt = nil
data.container:Hide()
end
elseif groupInCombat then
-- No health decrease, no ThreatEngine data, but group
-- is in combat. Check if this mob is attacking a group
-- member by looking for a unit token via party targets.
--
-- This catches the case of a body-pulled mob (at 100%
-- health, no damage dealt to it yet, but actively
-- attacking a group member).
local mobAttackingGroup = false
local foundUnit = self:FindUnitByName(data.plateName)
if foundUnit then
local ok_mt, mt = pcall(UnitName, foundUnit .. "target")
if ok_mt and mt then
if mt == UnitName("player") or addon.Threat:IsPartyMember(mt) then
mobAttackingGroup = true
end
end
end
if mobAttackingGroup then
-- Mob is confirmed attacking a group member!
-- Show a high-threat bar.
if not data.combatDetectedAt then
data.combatDetectedAt = now
end
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading
else
-- Can't confirm mob is attacking anyone.
-- It might be idle. Don't show bar.
data.combatDetectedAt = nil data.combatDetectedAt = nil
data.container:Hide() data.container:Hide()
end end
else else
-- No combat detected, no threat data, group not in combat. -- No combat detected, no threat data, or group not in combat.
data.combatDetectedAt = nil data.combatDetectedAt = nil
data.container:Hide() data.container:Hide()
end end
@@ -1303,10 +1410,11 @@ function addon.Nameplates:UpdateAllPlates()
end end
-- Try to find a combat-verified unit with matching health. -- Try to find a combat-verified unit with matching health.
local unit = self:FindCombatUnitForPlate(data) local unit = self:FindCombatUnitForPlate(data, assignedUnits)
if unit then if unit then
data.unit = unit data.unit = unit
data.guid = addon:GetUnitID(unit) data.guid = addon:GetUnitID(unit)
assignedUnits[table.getn(assignedUnits) + 1] = unit
local pct, situation = addon.Threat:GetThreatPercent(unit) local pct, situation = addon.Threat:GetThreatPercent(unit)
self:ApplyThreatToPlate(data, pct, situation or 0) self:ApplyThreatToPlate(data, pct, situation or 0)
elseif hasThreatData then elseif hasThreatData then
@@ -1321,7 +1429,7 @@ function addon.Nameplates:UpdateAllPlates()
-- In combat but no threat data — mob is being -- In combat but no threat data — mob is being
-- attacked by someone we can't track. -- attacked by someone we can't track.
if groupInCombat then if groupInCombat then
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading do local _fp, _fs = GetFallbackThreatForMob(data.plateName); self:ApplyThreatToPlate(data, _fp, _fs) end
else else
data.container:Hide() data.container:Hide()
end end
@@ -1330,7 +1438,7 @@ function addon.Nameplates:UpdateAllPlates()
-- In combat (health dropping) but no ThreatEngine data. -- In combat (health dropping) but no ThreatEngine data.
-- Show default high-threat bar. -- Show default high-threat bar.
if groupInCombat then if groupInCombat then
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading do local _fp, _fs = GetFallbackThreatForMob(data.plateName); self:ApplyThreatToPlate(data, _fp, _fs) end
else else
data.container:Hide() data.container:Hide()
end end
@@ -1340,11 +1448,12 @@ function addon.Nameplates:UpdateAllPlates()
-- But check if a unit token reveals this specific mob -- But check if a unit token reveals this specific mob
-- is attacking a group member (body pull scenario). -- is attacking a group member (body pull scenario).
if groupInCombat then if groupInCombat then
local unit = self:FindCombatUnitForPlate(data) local unit = self:FindCombatUnitForPlate(data, assignedUnits)
if unit then if unit then
-- Found a combat-verified unit for this plate. -- Found a combat-verified unit for this plate.
data.unit = unit data.unit = unit
data.guid = addon:GetUnitID(unit) data.guid = addon:GetUnitID(unit)
assignedUnits[table.getn(assignedUnits) + 1] = unit
local pct, situation = addon.Threat:GetThreatPercent(unit) local pct, situation = addon.Threat:GetThreatPercent(unit)
if pct > 0 then if pct > 0 then
if not data.combatDetectedAt then if not data.combatDetectedAt then
@@ -1413,3 +1522,52 @@ function addon.Nameplates:RecreateAll()
knownPlates = {} knownPlates = {}
self:ScanForNameplates() self:ScanForNameplates()
end end
-- ==================================================================
-- Flash driver (2.6) — pulses bar alpha on aggro plates
-- ==================================================================
addon.Nameplates.flashSet = addon.Nameplates.flashSet or {}
if not addon.Nameplates.flashFrame then
local ff = CreateFrame("Frame")
addon.Nameplates.flashFrame = ff
ff:SetScript("OnUpdate", function()
local db = RelationshipsThreatPlatesDB
if not db then return end
local baseA = db.barAlpha or 1
if not db.flashOnAggro then
-- Restore any lingering flashers to base alpha
for d in pairs(addon.Nameplates.flashSet) do
if d and d.bar then d.bar:SetAlpha(baseA) end
addon.Nameplates.flashSet[d] = nil
end
return
end
local speed = db.flashSpeed or 4
local t = GetTime() * speed
-- fabs(sin) style pulse in [0.35, 1.0]
local s = math.sin(t)
if s < 0 then s = -s end
local a = 0.35 + 0.65 * s
for d in pairs(addon.Nameplates.flashSet) do
if d and d.bar and d.container and d.container:IsShown() then
d.bar:SetAlpha(a * baseA)
end
end
end)
end
function addon.Nameplates:SetFlashing(data, on)
if not data then return end
if on then
addon.Nameplates.flashSet[data] = true
else
if addon.Nameplates.flashSet[data] then
addon.Nameplates.flashSet[data] = nil
if data.bar then
data.bar:SetAlpha(RelationshipsThreatPlatesDB and RelationshipsThreatPlatesDB.barAlpha or 1)
end
end
end
end
+9 -4
View File
@@ -439,14 +439,18 @@ function addon.Threat:GetThreatPercent(unit)
if playerPct > 0 then if playerPct > 0 then
return playerPct, playerSit return playerPct, playerSit
end end
return 0, 0 -- Player has no threat yet, but a party member is in
-- the aggro seat. Show a low visible bar so the plate
-- is still displayed (bar hides completely at pct=0).
return 8, 0
else else
-- Mob targets an outsider, or we can't see its target. -- Mob targets an outsider, or we can't see its target.
-- Show player's own threat only. -- Show player's own threat only.
if playerPct > 0 then if playerPct > 0 then
return playerPct, playerSit return playerPct, playerSit
end end
return 0, 0 -- Fall through: let ThreatEngine / heuristic decide
-- below instead of hiding the bar outright.
end end
end end
@@ -576,12 +580,13 @@ function addon.Threat:GetThreatPercent(unit)
-- has aggro, NOT the player. Showing 100% here would -- has aggro, NOT the player. Showing 100% here would
-- incorrectly light every mob in party combat up as full -- incorrectly light every mob in party combat up as full
-- aggro on the player. Show a low estimate instead — the -- aggro on the player. Show a low estimate instead — the
-- player is not in the aggro seat. -- player is not in the aggro seat, but the bar must still
-- be visible so users know the mob is engaged.
if UnitIsUnit("target", unit) then if UnitIsUnit("target", unit) then
threatPct = 20 threatPct = 20
situation = 0 situation = 0
else else
threatPct = 0 threatPct = 8
situation = 0 situation = 0
end end
else else
+181
View File
@@ -94,6 +94,25 @@ local playerInCombat = false
-- members when the player is NOT in combat themselves. -- members when the player is NOT in combat themselves.
local groupInCombat = false local groupInCombat = false
-- ============================================================
-- Multi-mob incoming-attack tracking (v2.6.2)
--
-- Vanilla WoW 1.12 exposes NO unit token for a mob that isn't
-- currently targeted / mouseovered / party-targeted. That meant
-- earlier versions only lit the threat bar on the ONE enemy the
-- player had targeted. To show bars for EVERY enemy in combat with
-- the group, we parse the incoming-combat chat events (mob->player
-- and mob->party) and remember which mob names have hit or missed
-- a group member recently. UpdateAllPlates then treats any plate
-- whose mob name is in this table as "in combat" and displays a
-- threat bar even when its health bar hasn't dropped yet and no
-- unit token is available.
-- ============================================================
local mobsAttackingGroup = {}
local mobsAttackingPlayer = {}
local MOB_ATTACK_TTL = 6.0 -- seconds a mob stays flagged as attacking
local INCOMING_PARSERS = {}
-- ============================================================ -- ============================================================
-- Safe API helpers -- Safe API helpers
-- ============================================================ -- ============================================================
@@ -414,6 +433,67 @@ local function BuildAllParsers()
" party damage, " .. table.getn(PARTY_HEAL_PARSERS) .. " party damage, " .. table.getn(PARTY_HEAL_PARSERS) ..
" party heal, " .. table.getn(PARTY_PERIODIC_PARSERS) .. " party heal, " .. table.getn(PARTY_PERIODIC_PARSERS) ..
" party periodic parsers built") " party periodic parsers built")
-- ========================================
-- INCOMING (mob -> player/party) parsers (v2.6.2)
-- Used to detect that a specific mob NAME is actively engaged
-- with the group even though we have no unit token for it.
-- We only care about the first capture (the mob / source name).
-- Any OTHERSELF or OTHEROTHER GlobalString whose first %s is the
-- attacker qualifies. Some of these constants may not exist on
-- every Turtle/OctoWoW build; missing ones are silently skipped.
-- ========================================
local incomingSources = {
-- Melee mob -> player
"COMBATHITOTHERSELF", "COMBATHITCRITOTHERSELF",
"COMBATHITSCHOOLOTHERSELF", "COMBATHITCRITSCHOOLOTHERSELF",
"MISSEDOTHERSELF",
"VSDODGEOTHERSELF", "VSPARRYOTHERSELF", "VSBLOCKOTHERSELF",
"VSABSORBOTHERSELF", "VSIMMUNEOTHERSELF", "VSRESISTOTHERSELF",
"VSDEFLECTOTHERSELF", "VSEVADEOTHERSELF",
-- Spell mob -> player
"SPELLLOGOTHERSELF", "SPELLLOGSCHOOLOTHERSELF",
"SPELLLOGCRITOTHERSELF", "SPELLLOGCRITSCHOOLOTHERSELF",
"SPELLMISSOTHERSELF", "SPELLDODGEDOTHERSELF",
"SPELLPARRIEDOTHERSELF", "SPELLBLOCKEDOTHERSELF",
"SPELLRESISTOTHERSELF", "SPELLIMMUNEOTHERSELF",
"SPELLEVADEDOTHERSELF", "SPELLDEFLECTEDOTHERSELF",
"SPELLREFLECTOTHERSELF",
-- Periodic (mob DoT ticking on player)
"PERIODICAURADAMAGEOTHERSELF",
-- Melee mob -> other (party member)
"COMBATHITOTHEROTHER", "COMBATHITCRITOTHEROTHER",
"COMBATHITSCHOOLOTHEROTHER", "COMBATHITCRITSCHOOLOTHEROTHER",
"MISSEDOTHEROTHER",
"VSDODGEOTHEROTHER", "VSPARRYOTHEROTHER", "VSBLOCKOTHEROTHER",
"VSABSORBOTHEROTHER", "VSIMMUNEOTHEROTHER", "VSRESISTOTHEROTHER",
"VSDEFLECTOTHEROTHER", "VSEVADEOTHEROTHER",
-- Spell mob -> other (party member)
"SPELLLOGOTHEROTHER", "SPELLLOGSCHOOLOTHEROTHER",
"SPELLLOGCRITOTHEROTHER", "SPELLLOGCRITSCHOOLOTHEROTHER",
"SPELLMISSOTHEROTHER", "SPELLDODGEDOTHEROTHER",
"SPELLPARRIEDOTHEROTHER", "SPELLBLOCKEDOTHEROTHER",
"SPELLRESISTOTHEROTHER", "SPELLIMMUNEOTHEROTHER",
-- Periodic (mob DoT ticking on party member)
"PERIODICAURADAMAGEOTHEROTHER",
}
for i = 1, table.getn(incomingSources) do
local gname = incomingSources[i]
local fs = getglobal(gname)
if fs then
local r = FormatToRegex(fs)
if r then
local toPlayer = false
if string.find(gname, "OTHERSELF") or string.find(gname, "TOSELF") then
toPlayer = true
end
INCOMING_PARSERS[table.getn(INCOMING_PARSERS) + 1] =
{ regex = r, globalVar = gname, toPlayer = toPlayer }
end
end
end
addon:ui_print("Threat engine: " .. table.getn(INCOMING_PARSERS) ..
" incoming (mob->group) parsers built")
end end
-- ============================================================ -- ============================================================
@@ -1137,6 +1217,79 @@ end
-- Event handler for combat log events -- Event handler for combat log events
-- ============================================================ -- ============================================================
-- Mark a mob name as actively attacking someone in the group.
-- Filters out anything that is obviously a player (self or a cached
-- party/raid member class). Unknown names are assumed to be mobs.
local function MarkMobAttackingGroup(name)
if not name or name == "" then return end
if playerName and name == playerName then return end
if partyClassCache and partyClassCache[name] then return end
mobsAttackingGroup[name] = GetTime()
end
-- Same as MarkMobAttackingGroup, but flags the mob as specifically
-- attacking the player. Used to distinguish "attacking me" from
-- "attacking a party member" in the nameplate fallback branches.
local function MarkMobAttackingPlayer(name)
if not name or name == "" then return end
if playerName and name == playerName then return end
if partyClassCache and partyClassCache[name] then return end
mobsAttackingPlayer[name] = GetTime()
end
-- Parse an incoming combat-log message and, if it matches any known
-- mob->group pattern, flag the first capture (the source mob name).
local function ParseIncomingMessage(msg)
if not msg or not INCOMING_PARSERS then return end
for i = 1, table.getn(INCOMING_PARSERS) do
local p = INCOMING_PARSERS[i]
local _, _, c1 = string.find(msg, p.regex)
if c1 then
MarkMobAttackingGroup(c1)
if p.toPlayer then
MarkMobAttackingPlayer(c1)
end
return
end
end
end
-- Public API: is this mob name currently flagged as attacking the group?
function addon.ThreatEngine:IsMobAttackingGroup(mobName)
if not mobName then return false end
local t = mobsAttackingGroup[mobName]
if not t then return false end
if GetTime() - t > MOB_ATTACK_TTL then
mobsAttackingGroup[mobName] = nil
return false
end
return true
end
-- Public API: is this mob name currently flagged as specifically
-- attacking the player (not just any group member)?
function addon.ThreatEngine:IsMobAttackingPlayer(mobName)
if not mobName then return false end
local t = mobsAttackingPlayer[mobName]
if not t then return false end
if GetTime() - t > MOB_ATTACK_TTL then
mobsAttackingPlayer[mobName] = nil
return false
end
return true
end
-- Public API: return the raw table (name -> lastSeen). Caller must not
-- mutate it. Used mainly by debug commands.
function addon.ThreatEngine:GetMobsAttackingGroup()
return mobsAttackingGroup
end
-- Called by the event frame for any CREATURE_VS_* combat log event.
function addon.ThreatEngine:OnIncomingCombat(ev, msg)
ParseIncomingMessage(msg)
end
function addon.ThreatEngine:OnCombatEvent(ev, msg) function addon.ThreatEngine:OnCombatEvent(ev, msg)
if not msg then return end if not msg then return end
if not playerName then return end if not playerName then return end
@@ -1545,6 +1698,8 @@ end
function addon.ThreatEngine:ClearAll() function addon.ThreatEngine:ClearAll()
threatData = {} threatData = {}
mobsAttackingGroup = {}
mobsAttackingPlayer = {}
end end
-- ============================================================ -- ============================================================
@@ -1809,6 +1964,20 @@ function addon.ThreatEngine:Initialize()
combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE") combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE") combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE")
-- Incoming combat: mob attacking player / party. Used by v2.6.2
-- multi-mob threat display to flag mobs that are engaged with
-- the group but for which we have no unit token.
combatFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS")
combatFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES")
combatFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS")
combatFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_SELF_MISSES")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_PARTY_MISSES")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_VS_SELF_DAMAGE")
combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_VS_PARTY_DAMAGE")
combatFrame:SetScript("OnEvent", function() combatFrame:SetScript("OnEvent", function()
-- Vanilla 1.12: event name is in global 'event', args in global arg1..arg9 -- Vanilla 1.12: event name is in global 'event', args in global arg1..arg9
-- IMPORTANT: we must NOT shadow 'event' with a local or parameter -- IMPORTANT: we must NOT shadow 'event' with a local or parameter
@@ -1838,6 +2007,18 @@ function addon.ThreatEngine:Initialize()
elseif event == "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS" elseif event == "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS"
or event == "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS" then or event == "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS" then
addon.ThreatEngine:OnPartyPeriodicEvent(event, arg1) addon.ThreatEngine:OnPartyPeriodicEvent(event, arg1)
-- Incoming combat: mob attacking player / party (v2.6.2)
elseif event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS"
or event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES"
or event == "CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS"
or event == "CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES"
or event == "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"
or event == "CHAT_MSG_SPELL_CREATURE_VS_SELF_MISSES"
or event == "CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE"
or event == "CHAT_MSG_SPELL_CREATURE_VS_PARTY_MISSES"
or event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_VS_SELF_DAMAGE"
or event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_VS_PARTY_DAMAGE" then
addon.ThreatEngine:OnIncomingCombat(event, arg1)
end end
-- Handle non-combat events -- Handle non-combat events