AnySCT 0.2.0
This commit is contained in:
+267
@@ -0,0 +1,267 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user