-- AnySCT plates.lua -- nameplate lookup and screen placement maths. -- -- There is no world-to-screen API in the 1.12 client, so the only way to float text over a mob is -- to ride its nameplate. This file finds the plate frame of a unit, and decides WHERE a line goes: -- mode 1 plate found, centre on screen -> spawn at the plate -- mode 2 plate found, centre off screen / in the edge margin -> clamp to the screen -- mode 3 no usable plate (or placement says so) -> fixed anchor -- mode 0 no plate and the user chose to hide -- -- Plate detection (verified for TurtleWoW): child of WorldFrame, object type "Button", first region -- a Texture "Interface\\Tooltips\\Nameplate-Border". Region order: border, glow, name, level, -- levelicon, raidicon; children: healthbar, castbar. The GUID comes from plate:GetName(1) under -- SuperWoW (>= 1.0) / nampower (>= 2.28); it is accepted only if it looks like "0x...". AnySCT_Plates = {} local P = AnySCT_Plates P.plates = {} -- every plate frame seen so far P.nPlates = 0 P.byGuid = {} -- normalised guid -> visible plate (rebuilt each scan) P.guidCapable = false -- true once any visible plate returned a GUID P.lastCount = 0 P.lastScan = -1 P.gen = 0 -- bumped by every scan; lines re-check their plate only when it changes P.SCAN_INTERVAL = 0.2 local function now() if GetTime then return GetTime() end return 0 end -- Forget every cached plate (tests; also usable if the plate frames are ever rebuilt). function P.Reset() P.plates = {} P.nPlates = 0 P.byGuid = {} P.guidCapable = false P.lastCount = 0 P.lastScan = -1 P.gen = 0 end -- ---- pure clamp maths -------------------------------------------------------------------------- -- Clamp a point into [margin, size - margin] on both axes. Third result: true if it moved. -- If the screen is smaller than twice the margin the axis collapses to its centre. local function clampAxis(v, size, margin) local lo, hi = margin, size - margin if hi < lo then return size / 2 end if v < lo then return lo end if v > hi then return hi end return v end function AnySCT_ClampToScreen(x, y, width, height, margin) margin = margin or 0 local cx, cy = clampAxis(x, width, margin), clampAxis(y, height, margin) return cx, cy, (cx ~= x or cy ~= y) end -- ---- detection ---------------------------------------------------------------------------------- function P.IsPlate(f) if not f or not f.GetObjectType or f:GetObjectType() ~= "Button" then return false end local r = f:GetRegions() if not r or not r.GetObjectType or r:GetObjectType() ~= "Texture" then return false end return r:GetTexture() == "Interface\\Tooltips\\Nameplate-Border" end -- The GUID string of a plate, or nil. function P.PlateGuid(plate) local ok, g = pcall(plate.GetName, plate, 1) if ok and type(g) == "string" and string.find(g, "^0x") then return g end return nil end local function addPlate(f) P.nPlates = P.nPlates + 1 P.plates[P.nPlates] = f -- cache what the name fallback needs (regions: border, glow, name, ...) local r1, r2, r3 = f:GetRegions() f.asctNameRegion = r3 f.asctBar = f:GetChildren() end -- Scan for new plates (only when WorldFrame's child count changed) and rebuild the GUID map. -- Throttled to SCAN_INTERVAL unless force. function P.Scan(force) local t = now() if not force and t - P.lastScan < P.SCAN_INTERVAL then return end P.lastScan = t local n = WorldFrame:GetNumChildren() if n ~= P.lastCount then local kids = { WorldFrame:GetChildren() } for i = P.lastCount + 1, n do local f = kids[i] if f and P.IsPlate(f) then addPlate(f) end end P.lastCount = n end local map = P.byGuid for k in pairs(map) do map[k] = nil end for i = 1, P.nPlates do local pl = P.plates[i] if pl:IsVisible() then local g = P.PlateGuid(pl) if g then P.guidCapable = true map[AnySCT_Core.NormGuid(g)] = pl end end end P.gen = P.gen + 1 end -- Health fraction of a plate's bar, or nil. local function barFraction(plate) local bar = plate.asctBar if not bar or not bar.GetValue then return nil end local lo, hi = bar:GetMinMaxValues() if not hi or hi <= lo then return nil end return (bar:GetValue() - lo) / (hi - lo) end -- Debug dump of a failed lookup: victim GUID, normalised form, scan-map size and up to 10 visible -- plates with name and GUID. Only called with /asct debug on. local function dumpMiss(guid, name) local C = AnySCT_Core local ng = guid and C.NormGuid(guid) or "-" local n = 0 for _ in pairs(P.byGuid) do n = n + 1 end C.Dbg("plate miss: guid=" .. tostring(guid) .. " norm=" .. tostring(ng) .. " name=" .. tostring(name) .. " mapped=" .. n .. " plates=" .. P.nPlates) local shown = 0 for i = 1, P.nPlates do local pl = P.plates[i] if shown < 10 and pl:IsVisible() then shown = shown + 1 local g = P.PlateGuid(pl) local nm = pl.asctNameRegion and pl.asctNameRegion.GetText and pl.asctNameRegion:GetText() or "?" C.Dbg(" plate '" .. tostring(nm) .. "' guid=" .. tostring(g) .. " norm=" .. tostring(g and C.NormGuid(g))) end end end -- Find the plate of a unit. guid: SuperWoW GUID string or nil; name: unit name; unit: token for the -- health comparison of the name fallback. Returns a visible plate or nil. -- Chain, never stopping early: scan map by GUID -> one forced rescan -> UnitNameplate(guid) -> -- plate name (event name, else UnitName(guid)) -> nil. function P.Find(guid, name, unit) P.Scan(false) if guid then local ng = AnySCT_Core.NormGuid(guid) local pl = P.byGuid[ng] if not pl and now() - P.lastScan > 0.05 then P.Scan(true) pl = P.byGuid[ng] end if pl and pl:IsVisible() then return pl end if UnitNameplate then local ok, f = pcall(UnitNameplate, guid) if ok and type(f) == "table" and f.IsVisible and f:IsVisible() then return f end end if not name and UnitName then local ok, n = pcall(UnitName, guid) if ok and type(n) == "string" then name = n end end end if not name then if AnySCT_Core.debug then dumpMiss(guid, name) end return nil end local want if unit and UnitHealth and UnitHealthMax then local mx = UnitHealthMax(unit) if mx and mx > 0 then want = UnitHealth(unit) / mx end end local best = P.FindByName(name, want) if not best and now() - P.lastScan > 0.05 then P.Scan(true) -- the plate may be new since the last scan: one forced rescan on a miss best = P.FindByName(name, want) end if not best and AnySCT_Core.debug then dumpMiss(guid, name) end return best end -- Visible plate whose name text is `name`; with several, the one whose health is closest to `want`. function P.FindByName(name, want) local best, bestDiff for i = 1, P.nPlates do local pl = P.plates[i] if pl:IsVisible() and pl.asctNameRegion and pl.asctNameRegion.GetText and pl.asctNameRegion:GetText() == name then local diff = 0 if want then local fr = barFraction(pl) if fr then diff = want - fr; if diff < 0 then diff = -diff end end end if not best or diff < bestDiff then best, bestDiff = pl, diff end end end return best end -- ---- placement ----------------------------------------------------------------------------------- -- Spawn point of a plate in UIParent coordinates (horizontal centre, TOP edge, like the Blizzard -- pet text) plus the screen size, or nil when the plate has no position. function P.PlatePosition(plate) if not plate then return nil end local cx, cy = plate:GetCenter() if not cx then return nil end local top = plate.GetTop and plate:GetTop() or nil if top then cy = top end local ps = plate:GetEffectiveScale() local us = UIParent:GetEffectiveScale() if ps and us and us > 0 then cx = cx * ps / us cy = cy * ps / us end return cx, cy, UIParent:GetWidth(), UIParent:GetHeight() end -- Decide the display mode. opt needs: placement ("clamp" | "anchorOff" | "anchor"), noPlate -- ("anchor" | "hide"), edgeMargin. Returns mode, x, y, screenW, screenH (x, y only for mode 1/2). function P.Select(plate, opt) if opt.placement == "anchor" then return 3 end local cx, cy, w, h = P.PlatePosition(plate) if not cx then if opt.noPlate == "hide" then return 0 end return 3 end local m = opt.edgeMargin or 0 if cx >= m and cx <= w - m and cy >= m and cy <= h - m then return 1, cx, cy, w, h end if opt.placement == "anchorOff" then return 3 end return 2, cx, cy, w, h end AnySCT_SelectMode = P.Select -- Does the plate still show the mob? Uses the last scan only (no per-frame GetName). function P.PlateMatches(plate, guidNorm) if not plate:IsVisible() then return false end if guidNorm and P.guidCapable then local mapped = P.byGuid[guidNorm] if mapped then return mapped == plate end -- the mob's GUID is not exposed (plate was found by name): keep following unless the plate -- now shows a different, GUID-exposing mob for k, pl in pairs(P.byGuid) do if pl == plate then return false end end end return true end -- Per-frame follow of a line (fields: plate, guidNorm, gen, valid, lastX, lastY, lastW, lastH). -- The identity check runs only when a new scan generation exists; between scans a frame costs a -- visibility test and a position read, no allocation. function P.FollowLine(s) if s.gen ~= P.gen then s.gen = P.gen s.valid = P.PlateMatches(s.plate, s.guidNorm) end if s.valid and s.plate:IsVisible() then local px, py, w, h = P.PlatePosition(s.plate) if px then s.lastX, s.lastY, s.lastW, s.lastH = px, py, w, h end end end