function widget:GetInfo() return { name = "Antinuke Coverage Remastered", desc = "Displays antinuke coverage of enemies and allies. Takes antinuke shadow into account. To avoid interference, uncheck 'Ally/Enemy/Spectator Nuke Defence' in 'settings -> interface -> defense ranges'. Antinukes' building progress divided into three stages: initial ( <40%, regular color - gray, intercepting line/circle - yellow); advanced ( 40%-99%, regular color - cyan, intercepting line/circle - orange); finished ( 100%, regular color - green, intercepting line/circle - red). While spectating, and for allied antinukes, and for enemy antinukes if no nuke is selected, uses regular colors (press F5 to make line thicker). When nuke is selected, shows enemy antinukes circles and line to mouse pointer in intercepting colors (even without Attack command). No mini-map support.", author = "rollmops, based on Google Frog's code", date = "Sep 2022", license = "GNU GPL v2 or later", layer = 0, enabled = true } end -------------------------------------------------------------------------------- -- Speedups -------------------------------------------------------------------------------- local spGetUnitDefID = Spring.GetUnitDefID local spTraceScreenRay = Spring.TraceScreenRay local spGetMouseState = Spring.GetMouseState local spGetUnitPosition = Spring.GetUnitPosition local spGetFeaturePosition = Spring.GetFeaturePosition local spGetFeatureDefID = Spring.GetFeatureDefID local spGetUnitAllyTeam = Spring.GetUnitAllyTeam local spGetUnitIsStunned = Spring.GetUnitIsStunned local spGetUnitHealth = Spring.GetUnitHealth local spAreTeamsAllied = Spring.AreTeamsAllied local spGetPositionLosState = Spring.GetPositionLosState local spGetPressedKeys = Spring.GetPressedKeys local spGetKeyCode = Spring.GetKeyCode local spGetSpectatingState = Spring.GetSpectatingState local glColor = gl.Color local glLineWidth = gl.LineWidth local glDrawGroundCircle = gl.DrawGroundCircle local glVertex = gl.Vertex local glBeginEnd = gl.BeginEnd local GL_LINES = GL.LINES -------------------------------------------------------------------------------- -- Config -------------------------------------------------------------------------------- -- You can change the hotkey (f5) and desired "thick" line width (4) local keyF5 = spGetKeyCode("f5") local function SetLineWidth() if spGetPressedKeys()[ keyF5 ] then glLineWidth(4) else glLineWidth(1) end end -- You can change the colors here (the four values are RGB + saturation) local colors = { intercepting = { finished = { 1, 0, 0, 1 }, -- red advanced = { 0.9, 0.5, 0, 1 }, -- orange initial = { 1, 1, 0, 1 }, -- yellow none = { 0, 1, 0, 1 }, -- green }, regular = { finished = { 0, 1, 0, 0.5 }, -- faded green advanced = { 0.3, 0.6, 0.6, 0.5 }, -- faded cyan initial = { 0.7, 0.7, 0.7, 0.5 }, -- faded gray }, } -- At what value the building progress switches from 'initial' to 'advanced' local progressThreshold = 0.4 -- 40% -------------------------------------------------------------------------------- -- Constants -------------------------------------------------------------------------------- local antiDefID = UnitDefNames.staticantinuke.id local antiRange = UnitDefNames.staticantinuke.customParams.nuke_coverage local nukeDefID = UnitDefNames.staticnuke.id local myAllyTeamID = Spring.GetMyAllyTeamID() -------------------------------------------------------------------------------- -- Globals -------------------------------------------------------------------------------- local antinukes = { enemy = {}, -- each element is an enemy antinuke with its position and stage of building progress ally = {}, -- similar, but for allied antinukes and for all antinukes if spectating } local spectating = spGetSpectatingState() -- Boolean local rangeFudgeMargin = 0 -- see GetMouseTargetPosition() local nukeSelected -- If a nuke is currently selected, keeps its coordinates -------------------------------------------------------------------------------- -- Antinukes stack management -------------------------------------------------------------------------------- local function AddIfAnti( unitID ) -- if the unit is antinuke, add it to the relevant stack if spGetUnitDefID(unitID) == antiDefID then local stack = ( spectating or spGetUnitAllyTeam(unitID) == myAllyTeamID ) and antinukes.ally or antinukes.enemy if stack[unitID] then return end local _, _, _, _, buildProgress = spGetUnitHealth(unitID) local x,_,z = spGetUnitPosition(unitID) if buildProgress and x and z then stack[unitID] = { stage = ( buildProgress < progressThreshold and "initial" ) or ( buildProgress < 1 and "advanced" ) or "finished", x = x, z = z, } end end end function widget:UnitEnteredLos( unitID ) AddIfAnti( unitID ) end function widget:UnitCreated( unitID ) AddIfAnti( unitID ) end function widget:UnitDestroyed( unitID ) antinukes.enemy[unitID] = nil antinukes.ally[unitID] = nil end local function ReaddUnits() -- re-initializing antinukes stacks by iterating over all existing units, used in widget:Initialize() or when a player turned to spectator antinukes.enemy = {} antinukes.ally = {} local units = Spring.GetAllUnits() for _, unitID in pairs(units) do AddIfAnti( unitID ) end end function widget:Initialize() ReaddUnits() end -- Game frame is used to check status of antinukes. -- Also updates spectating state. function widget:GameFrame(n) if n%15 ~= 3 then return end for _, stack in pairs( antinukes ) do for unitID, def in pairs( stack ) do local _, _, _, _, buildProgress = spGetUnitHealth(unitID) if buildProgress then def.stage = ( buildProgress < progressThreshold and "initial" ) or ( buildProgress < 1 and "advanced" ) or "finished" elseif select(2, spGetPositionLosState(def.x, 0, def.z)) then -- can't get buildProgress, but the location is in LoS, so it's dead stack[ unitID ] = nil end end end if not spectating and spGetSpectatingState() then spectating = true ReaddUnits() end end -------------------------------------------------------------------------------- -- Aux functions for drawing -------------------------------------------------------------------------------- -- is nuke currently selected? function widget:SelectionChanged(newSelection) nukeSelected = false for _, unitID in pairs( newSelection ) do local unitDefID = spGetUnitDefID(unitID) if unitDefID and unitDefID == nukeDefID then local x,y,z = Spring.GetUnitWeaponVectors(unitID, 1) nukeSelected = {x,y,z} return end end end local function GetMouseTargetPosition() local mx, my = spGetMouseState() if not (mx and my) then return nil end local targetType, target = spTraceScreenRay(mx, my, false, true, false, true) rangeFudgeMargin = 0 if targetType == "ground" then -- Even though nuke is not water-capable, this traces the ray through water to the sea-floor. -- That's what the attack-ground order will do if you click on water, so we have to match it. return {target[1], target[2], target[3]} elseif targetType == "unit" then -- Target coordinate is the center of target unit. return {spGetUnitPosition(target)} elseif targetType == "feature" then -- Target is the exact point where the mouse-ray hits the feature's colvol. -- FIXME But TraceScreenRay doesn't tell us that point, so we have to approximate. rangeFudgeMargin = FeatureDefs[spGetFeatureDefID(target)].radius return {spGetFeaturePosition(target)} else return nil end end local function VertexList(point) for i = 1, #point do glVertex(point[i]) end end -------------------------------------------------------------------------------- -- Drawing -------------------------------------------------------------------------------- local function DrawEnemyInterceptors( mouse ) -- called by widget:DrawWorldPreUnit(); -- for each enemy antinuke, draws a circle (color depends on the build progress and whether it will intercept if the target is at mouse pointer); -- returns the highest building progress of all intercepting antinukes. local Nx, Nz = nukeSelected[1], nukeSelected[3] local Tx, Tz = mouse[1], mouse[3] if ( not ( Nx and Nz and Tx and Tz) ) then return nil end glLineWidth(2) local interceptedBy = { none = "none" } -- Each value is equal to its key. Used to decide the return value (see comment above). -- Each intercepting antinuke will (re-)set the key-value representing its building progress. -- After the loop is over, the highest key-value will be chosen to return. for unitID, def in pairs(antinukes.enemy) do -- iterate over all enemy antinukes local Ax, Az = def.x, def.z if Ax and Az then -- location points: -- (Ax, Az) - enemy Antinuke -- (Nx, Nz) - selected Nuke silo -- (Tx, Tz) - Target (mouse pointer) -- prepare some squared distances for the interception test below (using squared distances in inequalities to avoid unnecessary SQRT function) local NT_squared = ( Nx - Tx ) ^ 2 + ( Nz - Tz ) ^ 2 local NA_squared = ( Nx - Ax ) ^ 2 + ( Nz - Az ) ^ 2 local AT_squared = ( Ax - Tx ) ^ 2 + ( Az - Tz ) ^ 2 local R_squared = ( antiRange + rangeFudgeMargin ) ^ 2 -- squared antinuke coverage radius (possibly corrected by rangeFudgeMargin) -- check whether current antinuke intercepts. -- it intercepts if one of the two conditions is true: if AT_squared <= R_squared -- 1. target resides inside antinuke circle or ( -- 2. target resides in antinuke "shadow" (behind the circle). -- this second condition equal to two statements, both of which have to be true: -- 2A. line from N to T crosses antinuke circle (of which A is the center and R is the radius). -- this means: point-line distance from A to line NT should be less than R: Dist( NT, A ) <= R -- for Dist( NT, A ) apply 'point-line distance' formula (https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points); then move the formula's denominator to the right side of the inequality (i.e., multiply both sides by formula's denominator) to avoid dividing by zero or by very small values, then square both sides, so the inequality: -- Dist( NT, A ) <= R -- becomes: -- (formula's numerator)^2 <= (formula's denominator^2, that is, remove sqrt that was there) * R^2 ( ( Tx - Nx ) * ( Nz - Az ) - ( Nx - Ax ) * ( Tz - Nz ) ) ^ 2 <= NT_squared * R_squared and -- 2B. T is behind the circle (if looking from N). -- Let P be a tangent point (point of intersection of tangent line from N to circle with the circle) -- Then statement 2B means NT >= NP (namely: for N, target is farther than P), or, if squared, NT^2 >= NP^2 -- Since tangent line is orthogonal to radius, NP^2 is calculated via Pythagoras theorem in triangle NAP (AP=R) NT_squared >= NA_squared - R_squared ) then interceptedBy[ def.stage ] = def.stage -- see comment in the declaration of this table glColor( unpack( colors.intercepting[ def.stage ])) -- set the "intercepting" circle color else glColor( unpack( colors.regular[ def.stage ])) -- this anti is not intercepting, set the "regular" color end glDrawGroundCircle(Ax, 0, Az, antiRange, 40 ) end end return interceptedBy.finished or interceptedBy.advanced or interceptedBy.initial or interceptedBy.none -- return the highest building progress of all antinukes that intercept given target end function widget:DrawWorldPreUnit() -- called each draw frame local mouse = GetMouseTargetPosition() if not spectating and nukeSelected and mouse then -- if playing and a nuke is selected, use DrawEnemyInterceptors() to draw circles and to get the max building progress of all antinukes that could intercept the target at the mouse pointer local interceptedBy = DrawEnemyInterceptors( mouse ) if interceptedBy then glColor( unpack( colors.intercepting[ interceptedBy ])) -- set color for 'nuke -> mouse pointer' line glLineWidth(3) glBeginEnd( GL_LINES, VertexList, {nukeSelected, mouse} ) -- draw the line end else -- otherwise, use the "regular" colors for enemy antinukes circles for unitID, def in pairs(antinukes.enemy) do local ux, uz = def.x, def.z if ux and uz then glColor( unpack( colors.regular[ def.stage ])) SetLineWidth() -- if a special key is pressed (F5 by default, see "Config"), make circle line thicker glDrawGroundCircle(ux, 0, uz, antiRange, 40 ) end end end for unitID, def in pairs(antinukes.ally) do -- always draw allied (if spectating, all) antinukes circles in regular colors local ux, uz = def.x, def.z if ux and uz then glColor( unpack( colors.regular[ def.stage ])) SetLineWidth() -- if a special key is pressed (F5 by default, see "Config"), make circle lines thicker glDrawGroundCircle(ux, 0, uz, antiRange, 40 ) end end end