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
## Title: RelationshipsThreatPlates - OctoWoW
## Notes: OctoWoW-compatible RelationshipsThreatPlates
## Author: Packaged By Relationship
## Version: 1.3
## Author: Relationship
## Version: 1.4
## SavedVariables: RelationshipsThreatPlatesDB
## X-Website: 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
-- Version string
TP_VERSION = "2.5.0"
TP_VERSION = "2.6.1"
TP_ADDON_NAME = "RelationshipsThreatPlates"
-- ==================================================================
@@ -40,6 +40,19 @@ local DEFAULT_CONFIG = {
glowEnabled = false,
glowSize = 3,
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 = {
point = "CENTER",
relPoint = "CENTER",
+126 -1
View File
@@ -35,7 +35,7 @@ local C = {
-- Layout
local GUI_WIDTH = 320
local GUI_HEIGHT = 480
local GUI_HEIGHT = 520
local HEADER_H = 30
local MARGIN = 14
local FONT_NORMAL = "Fonts\\FRIZQT__.TTF"
@@ -428,6 +428,80 @@ function addon.GUI:BuildSettings(parent)
widgets[configKey] = slider
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
-- ==============================================================
@@ -469,6 +543,57 @@ function addon.GUI:BuildSettings(parent)
CheckBox("Show on Neutral", "showOnNeutral", "Show threat bars on neutral 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
-- ==============================================================
+45 -11
View File
@@ -69,6 +69,37 @@ function addon.Media.Brighten(factor, r, g, b, a)
a
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
-- ==================================================================
@@ -76,25 +107,28 @@ function addon.Media.GetThreatColour(pct)
pct = tonumber(pct) or 0
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
if pct < 50 then
local t = pct / 50
r = c.THREAT_LOW.r + (c.THREAT_MEDIUM.r - c.THREAT_LOW.r) * t
g = c.THREAT_LOW.g + (c.THREAT_MEDIUM.g - c.THREAT_LOW.g) * t
b = c.THREAT_LOW.b + (c.THREAT_MEDIUM.b - c.THREAT_LOW.b) * t
r = lo[1] + (md[1] - lo[1]) * t
g = lo[2] + (md[2] - lo[2]) * t
b = lo[3] + (md[3] - lo[3]) * t
elseif pct < 80 then
local t = (pct - 50) / 30
r = c.THREAT_MEDIUM.r + (c.THREAT_HIGH.r - c.THREAT_MEDIUM.r) * t
g = c.THREAT_MEDIUM.g + (c.THREAT_HIGH.g - c.THREAT_MEDIUM.g) * t
b = c.THREAT_MEDIUM.b + (c.THREAT_HIGH.b - c.THREAT_MEDIUM.b) * t
r = md[1] + (hi[1] - md[1]) * t
g = md[2] + (hi[2] - md[2]) * t
b = md[3] + (hi[3] - md[3]) * t
else
local t = (pct - 80) / 20
t = math.min(1, math.max(0, t))
r = c.THREAT_HIGH.r + (c.THREAT_AGGRO.r - c.THREAT_HIGH.r) * t
g = c.THREAT_HIGH.g + (c.THREAT_AGGRO.g - c.THREAT_HIGH.g) * t
b = c.THREAT_HIGH.b + (c.THREAT_AGGRO.b - c.THREAT_HIGH.b) * t
if t > 1 then t = 1 end
if t < 0 then t = 0 end
r = hi[1] + (ag[1] - hi[1]) * t
g = hi[2] + (ag[2] - hi[2]) * t
b = hi[3] + (ag[3] - hi[3]) * t
end
return r, g, b
+244 -86
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
-- verification is done earlier in the pipeline (IsPlateInCombat,
-- 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
data.container:Hide()
if addon.Nameplates.SetFlashing then addon.Nameplates:SetFlashing(data, false) end
return
end
@@ -961,24 +984,30 @@ function addon.Nameplates:ApplyThreatToPlate(data, pct, situation)
-- Update text — uses rawPct so values above 100% are shown as-is.
if data.text then
local fmt = RelationshipsThreatPlatesDB.textFormat or "percent"
local pctStr = math.floor(rawPct + 0.5) .. "%"
local tankStr = nil
if data.plateName and (RelationshipsThreatPlatesDB.tankMode or fmt == "tank" or fmt == "combo") then
tankStr = addon.Threat:GetTankDisplayForMob(data.plateName)
end
local textStr
if RelationshipsThreatPlatesDB.tankMode and data.plateName then
-- Tank mode: show raw threat lead over the 2nd-highest player
-- (positive = you have that much lead as top-threat holder;
-- negative = you're that much behind the current top).
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
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
textStr = math.floor(rawPct + 0.5) .. "%"
textStr = pctStr
end
data.text:SetText(textStr)
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)
elseif rawPct >= 60 then
data.text:SetTextColor(1, 0.9, 0.8, 1)
@@ -987,6 +1016,24 @@ function addon.Nameplates:ApplyThreatToPlate(data, pct, situation)
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)
if data.threatGlow then
if barPct >= 80 then
@@ -1058,7 +1105,7 @@ end
--
-- 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 strlen(data.plateName) == 0 then return nil end
@@ -1076,25 +1123,67 @@ function addon.Nameplates:FindCombatUnitForPlate(data)
-- Get plate health percentage for matching
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:
-- 1. In combat (mandatory — idle mobs must never match)
-- 2. Health % close to the plate's health bar
-- 3. Hostile and attackable
-- 4. Not already assigned to a different plate this cycle
local bestUnit = nil
local bestScore = -999999
for i = 1, table.getn(candidates) do
local c = candidates[i]
-- MANDATORY: Unit must be in combat
if c.inCombat then
local score = 0
-- Skip candidates already claimed by another plate this cycle
local excluded = false
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
local diff = math.abs(plateHealthPct - c.healthPct)
-- Close health match = strong signal this is the right mob
score = score + math.max(0, 500 - (diff * 15))
if diff <= 8 then
healthMatched = true
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
-- Hostile/attackable bonus
@@ -1102,7 +1191,7 @@ function addon.Nameplates:FindCombatUnitForPlate(data)
score = score + 50
end
if score > bestScore then
if score > bestScore and healthMatched then
bestScore = score
bestUnit = c.unit
end
@@ -1141,6 +1230,23 @@ end
-- - Mobs attacking party members show threat even when player
-- 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()
if not RelationshipsThreatPlatesDB or not RelationshipsThreatPlatesDB.enabled then
for nameplate, data in pairs(self.plates) do
@@ -1183,6 +1289,16 @@ function addon.Nameplates:UpdateAllPlates()
end
-- 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
if nameplate:IsVisible() then
local isUniqueName = (platesPerName[data.plateName] or 1) <= 1
@@ -1191,6 +1307,21 @@ function addon.Nameplates:UpdateAllPlates()
local plateInCombat = IsPlateInCombat(data)
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
-- We have a resolved unit token. Verify it's in combat.
local ok_combat, mobInCombat = pcall(UnitAffectingCombat, data.unit)
@@ -1203,91 +1334,67 @@ function addon.Nameplates:UpdateAllPlates()
self:ApplyThreatToPlate(data, pct, situation or 0)
end
elseif isUniqueName and data.plateName then
-- 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
-- data is unambiguous. Use health-based combat detection.
-- UNIQUE-NAME MOB with no unit token.
--
-- CAUTION: "unique" here only means one plate of this
-- 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
-- Mob is in combat (health dropping or below 100%).
-- Record when we first detected combat for grace period.
-- This specific plate's health is dropping — it's
-- in combat. Safe to trust name-keyed threat data.
if not data.combatDetectedAt then
data.combatDetectedAt = now
end
-- Try ThreatEngine data first
local pct, sit = addon.Threat:GetThreatByName(data.plateName)
if pct > 0 then
self:ApplyThreatToPlate(data, pct, sit or 0)
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
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
data.container:Hide()
end
end
elseif hasThreatData and groupInCombat then
-- Plate looks idle (full health) but ThreatEngine has
-- data and group is in combat. This can happen briefly
-- after a mob is first hit (health bar hasn't updated)
-- or for a mob being attacked by someone else.
-- Give a grace period.
if not data.combatDetectedAt then
data.combatDetectedAt = now
end
if now - data.combatDetectedAt < 0.5 then
local pct, sit = addon.Threat:GetThreatByName(data.plateName)
if pct > 0 then
self:ApplyThreatToPlate(data, pct, sit or 0)
else
self:ApplyThreatToPlate(data, 20, 0) -- was 100,3: showing full aggro for any party-combat mob with no threat data was misleading
end
else
-- Grace period expired and health still at 100%.
-- This is likely an idle mob. Hide bar.
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.
elseif groupInCombat and (mobAttackingGroup or hasThreatData) then
-- Name-only signal says a mob of this name is
-- attacking the group, but THIS plate shows no
-- health drop. Verify by matching a real combat
-- unit token to this plate (health % match). If
-- 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
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
local pct, situation = addon.Threat:GetThreatPercent(unit)
if pct > 0 then
self:ApplyThreatToPlate(data, pct, situation or 0)
else
do local _fp, _fs = GetFallbackThreatForMob(data.plateName); self:ApplyThreatToPlate(data, _fp, _fs) end
end
else
-- Can't confirm mob is attacking anyone.
-- It might be idle. Don't show bar.
-- Can't verify this plate is the attacker.
-- Do NOT show a bar based on name alone.
data.combatDetectedAt = nil
data.container:Hide()
end
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.container:Hide()
end
@@ -1303,10 +1410,11 @@ function addon.Nameplates:UpdateAllPlates()
end
-- Try to find a combat-verified unit with matching health.
local unit = self:FindCombatUnitForPlate(data)
local unit = self:FindCombatUnitForPlate(data, assignedUnits)
if unit then
data.unit = unit
data.guid = addon:GetUnitID(unit)
assignedUnits[table.getn(assignedUnits) + 1] = unit
local pct, situation = addon.Threat:GetThreatPercent(unit)
self:ApplyThreatToPlate(data, pct, situation or 0)
elseif hasThreatData then
@@ -1321,7 +1429,7 @@ function addon.Nameplates:UpdateAllPlates()
-- In combat but no threat data — mob is being
-- attacked by someone we can't track.
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
data.container:Hide()
end
@@ -1330,7 +1438,7 @@ function addon.Nameplates:UpdateAllPlates()
-- In combat (health dropping) but no ThreatEngine data.
-- Show default high-threat bar.
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
data.container:Hide()
end
@@ -1340,11 +1448,12 @@ function addon.Nameplates:UpdateAllPlates()
-- But check if a unit token reveals this specific mob
-- is attacking a group member (body pull scenario).
if groupInCombat then
local unit = self:FindCombatUnitForPlate(data)
local unit = self:FindCombatUnitForPlate(data, assignedUnits)
if unit then
-- Found a combat-verified unit for this plate.
data.unit = unit
data.guid = addon:GetUnitID(unit)
assignedUnits[table.getn(assignedUnits) + 1] = unit
local pct, situation = addon.Threat:GetThreatPercent(unit)
if pct > 0 then
if not data.combatDetectedAt then
@@ -1413,3 +1522,52 @@ function addon.Nameplates:RecreateAll()
knownPlates = {}
self:ScanForNameplates()
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
return playerPct, playerSit
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
-- Mob targets an outsider, or we can't see its target.
-- Show player's own threat only.
if playerPct > 0 then
return playerPct, playerSit
end
return 0, 0
-- Fall through: let ThreatEngine / heuristic decide
-- below instead of hiding the bar outright.
end
end
@@ -576,12 +580,13 @@ function addon.Threat:GetThreatPercent(unit)
-- has aggro, NOT the player. Showing 100% here would
-- incorrectly light every mob in party combat up as full
-- 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
threatPct = 20
situation = 0
else
threatPct = 0
threatPct = 8
situation = 0
end
else
+181
View File
@@ -94,6 +94,25 @@ local playerInCombat = false
-- members when the player is NOT in combat themselves.
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
-- ============================================================
@@ -414,6 +433,67 @@ local function BuildAllParsers()
" party damage, " .. table.getn(PARTY_HEAL_PARSERS) ..
" party heal, " .. table.getn(PARTY_PERIODIC_PARSERS) ..
" 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
-- ============================================================
@@ -1137,6 +1217,79 @@ end
-- 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)
if not msg then return end
if not playerName then return end
@@ -1545,6 +1698,8 @@ end
function addon.ThreatEngine:ClearAll()
threatData = {}
mobsAttackingGroup = {}
mobsAttackingPlayer = {}
end
-- ============================================================
@@ -1809,6 +1964,20 @@ function addon.ThreatEngine:Initialize()
combatFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_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()
-- 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
@@ -1838,6 +2007,18 @@ function addon.ThreatEngine:Initialize()
elseif event == "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS"
or event == "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS" then
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
-- Handle non-combat events