Snippet #77369
on 2022/09/17 9:13:14 (UTC) by Anonymous as Lua
-
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
Recent Snippets
- #157071 by Anonymous (493 days ago)
- #156876 by Anonymous (494 days ago)
- #154030 by Anonymous (498 days ago)
- #140524 by Anonymous (517 days ago)
- #137907 by rotense (521 days ago)
- #131892 by Anonymous (528 days ago)