Snippet #77369

TTL: forever — WordwrapView raw

on 2022/09/17 9:13:14 (UTC) by Anonymous as Lua

  1. function widget:GetInfo()
  2.     return {
  3.         name      = "Antinuke Coverage Remastered",
  4.         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.",
  5.         author    = "rollmops, based on Google Frog's code",
  6.         date      = "Sep 2022",
  7.         license   = "GNU GPL v2 or later",
  8.         layer     = 0,
  9.         enabled   = true
  10.     }
  11. end
  12.  
  13. --------------------------------------------------------------------------------
  14. -- Speedups
  15. --------------------------------------------------------------------------------
  16.  
  17. local spGetUnitDefID            = Spring.GetUnitDefID
  18. local spTraceScreenRay          = Spring.TraceScreenRay
  19. local spGetMouseState           = Spring.GetMouseState
  20. local spGetUnitPosition         = Spring.GetUnitPosition
  21. local spGetFeaturePosition      = Spring.GetFeaturePosition
  22. local spGetFeatureDefID         = Spring.GetFeatureDefID
  23. local spGetUnitAllyTeam         = Spring.GetUnitAllyTeam
  24. local spGetUnitIsStunned        = Spring.GetUnitIsStunned
  25. local spGetUnitHealth           = Spring.GetUnitHealth
  26. local spAreTeamsAllied          = Spring.AreTeamsAllied
  27. local spGetPositionLosState     = Spring.GetPositionLosState
  28. local spGetPressedKeys          = Spring.GetPressedKeys
  29. local spGetKeyCode              = Spring.GetKeyCode
  30. local spGetSpectatingState      = Spring.GetSpectatingState
  31.  
  32. local glColor                   = gl.Color
  33. local glLineWidth               = gl.LineWidth
  34. local glDrawGroundCircle        = gl.DrawGroundCircle
  35. local glVertex                  = gl.Vertex
  36. local glBeginEnd                = gl.BeginEnd
  37. local GL_LINES                  = GL.LINES
  38.  
  39.  
  40. --------------------------------------------------------------------------------
  41. -- Config
  42. --------------------------------------------------------------------------------
  43.  
  44. -- You can change the hotkey (f5) and desired "thick" line width (4)
  45. local keyF5 = spGetKeyCode("f5")
  46. local function SetLineWidth()
  47.     if spGetPressedKeys()[ keyF5 ] then
  48.         glLineWidth(4)
  49.     else
  50.         glLineWidth(1)
  51.     end
  52. end
  53.  
  54. -- You can change the colors here (the four values are RGB + saturation)
  55. local colors = {
  56.     intercepting = {
  57.         finished  = {   1,   0,   0,   1 },   -- red
  58.         advanced  = { 0.9, 0.5,   0,   1 },   -- orange
  59.         initial   = {   1,   1,   0,   1 },   -- yellow
  60.         none      = {   0,   1,   0,   1 },   -- green
  61.     },
  62.     regular = {
  63.         finished  = {    0,   1,   0, 0.5 },  -- faded green
  64.         advanced  = {  0.3, 0.6, 0.6, 0.5 },  -- faded cyan
  65.         initial   = {  0.7, 0.7, 0.7, 0.5 },  -- faded gray
  66.     },
  67. }
  68.  
  69. -- At what value the building progress switches from 'initial' to 'advanced'
  70. local progressThreshold = 0.4     -- 40%
  71.  
  72. --------------------------------------------------------------------------------
  73. -- Constants
  74. --------------------------------------------------------------------------------
  75.  
  76. local antiDefID = UnitDefNames.staticantinuke.id
  77. local antiRange = UnitDefNames.staticantinuke.customParams.nuke_coverage
  78. local nukeDefID = UnitDefNames.staticnuke.id
  79. local myAllyTeamID = Spring.GetMyAllyTeamID()
  80.  
  81. --------------------------------------------------------------------------------
  82. -- Globals
  83. --------------------------------------------------------------------------------
  84.  
  85. local antinukes = {
  86.     enemy = {}, -- each element is an enemy antinuke with its position and stage of building progress
  87.     ally  = {}, -- similar, but for allied antinukes and for all antinukes if spectating
  88. }
  89.  
  90. local spectating       = spGetSpectatingState() -- Boolean
  91. local rangeFudgeMargin = 0                      -- see GetMouseTargetPosition()
  92. local nukeSelected                              -- If a nuke is currently selected, keeps its coordinates
  93.  
  94. --------------------------------------------------------------------------------
  95. -- Antinukes stack management
  96. --------------------------------------------------------------------------------
  97.  
  98. local function AddIfAnti( unitID )      -- if the unit is antinuke, add it to the relevant stack
  99.     if spGetUnitDefID(unitID) == antiDefID then
  100.         local stack = ( spectating or spGetUnitAllyTeam(unitID) == myAllyTeamID ) and antinukes.ally or antinukes.enemy
  101.         if stack[unitID] then
  102.             return
  103.         end
  104.  
  105.         local _, _, _, _, buildProgress = spGetUnitHealth(unitID)
  106.         local x,_,z = spGetUnitPosition(unitID)
  107.  
  108.         if buildProgress and x and z then
  109.             stack[unitID] = {
  110.                 stage = ( buildProgress < progressThreshold  and "initial" ) or ( buildProgress < 1 and "advanced" ) or "finished",
  111.                 x     = x,
  112.                 z     = z,
  113.             }
  114.         end
  115.     end
  116. end
  117.  
  118. function widget:UnitEnteredLos( unitID )
  119.     AddIfAnti( unitID )
  120. end
  121.  
  122. function widget:UnitCreated( unitID )
  123.     AddIfAnti( unitID )
  124. end
  125.  
  126. function widget:UnitDestroyed( unitID )
  127.     antinukes.enemy[unitID] = nil
  128.     antinukes.ally[unitID]  = nil
  129. end
  130.  
  131. local function ReaddUnits()     -- re-initializing antinukes stacks by iterating over all existing units, used in widget:Initialize() or when a player turned to spectator
  132.  
  133.     antinukes.enemy = {}
  134.     antinukes.ally = {}
  135.  
  136.     local units = Spring.GetAllUnits()
  137.  
  138.     for _, unitID in pairs(units) do
  139.         AddIfAnti( unitID )
  140.     end
  141. end
  142.  
  143. function widget:Initialize()
  144.     ReaddUnits()
  145. end
  146.  
  147. -- Game frame is used to check status of antinukes.
  148. -- Also updates spectating state.
  149. function widget:GameFrame(n)
  150.  
  151.     if n%15 ~= 3 then return end
  152.  
  153.     for _, stack in pairs( antinukes ) do
  154.  
  155.         for unitID, def in pairs( stack ) do
  156.  
  157.             local _, _, _, _, buildProgress = spGetUnitHealth(unitID)
  158.             if buildProgress then
  159.                     def.stage = ( buildProgress < progressThreshold  and "initial" ) or ( buildProgress < 1 and "advanced" ) or "finished"
  160.             elseif select(2, spGetPositionLosState(def.x, 0, def.z)) then   -- can't get buildProgress, but the location is in LoS, so it's dead
  161.                 stack[ unitID ] = nil
  162.             end
  163.  
  164.         end
  165.     end
  166.  
  167.     if not spectating and spGetSpectatingState() then
  168.         spectating = true
  169.         ReaddUnits()
  170.     end
  171. end
  172.  
  173.  
  174. --------------------------------------------------------------------------------
  175. -- Aux functions for drawing
  176. --------------------------------------------------------------------------------
  177.  
  178. -- is nuke currently selected?
  179. function widget:SelectionChanged(newSelection)
  180.     nukeSelected = false
  181.     for _, unitID in pairs( newSelection ) do
  182.         local unitDefID = spGetUnitDefID(unitID)
  183.         if unitDefID and unitDefID == nukeDefID then
  184.             local x,y,z = Spring.GetUnitWeaponVectors(unitID, 1)
  185.             nukeSelected = {x,y,z}
  186.             return
  187.         end
  188.     end
  189. end
  190.  
  191. local function GetMouseTargetPosition()
  192.     local mx, my = spGetMouseState()
  193.     if not (mx and my) then return nil end
  194.     local targetType, target = spTraceScreenRay(mx, my, false, true, false, true)
  195.     rangeFudgeMargin = 0
  196.     if targetType == "ground" then
  197.         -- Even though nuke is not water-capable, this traces the ray through water to the sea-floor.
  198.         -- That's what the attack-ground order will do if you click on water, so we have to match it.
  199.         return {target[1], target[2], target[3]}
  200.     elseif targetType == "unit" then
  201.         -- Target coordinate is the center of target unit.
  202.         return {spGetUnitPosition(target)}
  203.     elseif targetType == "feature" then
  204.         -- Target is the exact point where the mouse-ray hits the feature's colvol.
  205.         -- FIXME But TraceScreenRay doesn't tell us that point, so we have to approximate.
  206.         rangeFudgeMargin = FeatureDefs[spGetFeatureDefID(target)].radius
  207.         return {spGetFeaturePosition(target)}
  208.     else
  209.         return nil
  210.     end
  211. end
  212.  
  213. local function VertexList(point)
  214.     for i = 1, #point do
  215.         glVertex(point[i])
  216.     end
  217. end
  218.  
  219. --------------------------------------------------------------------------------
  220. -- Drawing
  221. --------------------------------------------------------------------------------
  222.  
  223. local function DrawEnemyInterceptors( mouse )   -- called by widget:DrawWorldPreUnit();
  224.                                                 -- 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);
  225.                                                 -- returns the highest building progress of all intercepting antinukes.
  226.     local Nx, Nz = nukeSelected[1], nukeSelected[3]
  227.     local Tx, Tz = mouse[1], mouse[3]
  228.  
  229.     if ( not ( Nx and Nz and Tx and Tz) ) then return nil end
  230.  
  231.     glLineWidth(2)
  232.  
  233.     local interceptedBy = { none = "none" }     -- Each value is equal to its key. Used to decide the return value (see comment above).
  234.                                                 -- Each intercepting antinuke will (re-)set the key-value representing its building progress.
  235.                                                 -- After the loop is over, the highest key-value will be chosen to return.
  236.  
  237.     for unitID, def in pairs(antinukes.enemy) do    -- iterate over all enemy antinukes
  238.  
  239.         local Ax, Az = def.x, def.z
  240.  
  241.         if Ax and Az then
  242.  
  243.             -- location points:
  244.             -- (Ax, Az) - enemy Antinuke
  245.             -- (Nx, Nz) - selected Nuke silo
  246.             -- (Tx, Tz) - Target (mouse pointer)
  247.  
  248.             -- prepare some squared distances for the interception test below (using squared distances in inequalities to avoid unnecessary SQRT function)
  249.             local NT_squared = ( Nx - Tx ) ^ 2 + ( Nz - Tz ) ^ 2
  250.             local NA_squared = ( Nx - Ax ) ^ 2 + ( Nz - Az ) ^ 2
  251.             local AT_squared = ( Ax - Tx ) ^ 2 + ( Az - Tz ) ^ 2
  252.  
  253.             local R_squared  = ( antiRange + rangeFudgeMargin ) ^ 2  -- squared antinuke coverage radius (possibly corrected by rangeFudgeMargin)
  254.  
  255.             -- check whether current antinuke intercepts.
  256.             -- it intercepts if one of the two conditions is true:
  257.  
  258.             if  AT_squared <= R_squared     -- 1. target resides inside antinuke circle
  259.  
  260.                 or (                        -- 2. target resides in antinuke "shadow" (behind the circle).
  261.  
  262.                     -- this second condition equal to two statements, both of which have to be true:
  263.  
  264.                     -- 2A. line from N to T crosses antinuke circle (of which A is the center and R is the radius).
  265.                     --     this means: point-line distance from A to line NT should be less than R: Dist( NT, A ) <= R
  266.                     --     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:
  267.                     -- Dist( NT, A ) <= R
  268.                     -- becomes:
  269.                     -- (formula's numerator)^2 <= (formula's denominator^2, that is, remove sqrt that was there) * R^2
  270.  
  271.                         ( ( Tx - Nx ) * ( Nz - Az ) - ( Nx - Ax ) * ( Tz - Nz ) ) ^ 2 <= NT_squared * R_squared
  272.  
  273.                     and
  274.  
  275.                     -- 2B. T is behind the circle (if looking from N).
  276.                     --     Let P be a tangent point (point of intersection of tangent line from N to circle with the circle)
  277.                     --     Then statement 2B means NT >= NP (namely: for N, target is farther than P), or, if squared, NT^2 >= NP^2
  278.                     --     Since tangent line is orthogonal to radius, NP^2 is calculated via Pythagoras theorem in triangle NAP (AP=R)
  279.  
  280.                         NT_squared >= NA_squared - R_squared
  281.                 )
  282.  
  283.             then
  284.                 interceptedBy[ def.stage ] = def.stage              -- see comment in the declaration of this table
  285.                 glColor( unpack( colors.intercepting[ def.stage ])) -- set the "intercepting" circle color
  286.             else
  287.                 glColor( unpack( colors.regular[ def.stage ]))      -- this anti is not intercepting, set the "regular" color
  288.             end
  289.             glDrawGroundCircle(Ax, 0, Az, antiRange, 40 )
  290.         end
  291.     end
  292.     return interceptedBy.finished or interceptedBy.advanced or interceptedBy.initial or interceptedBy.none -- return the highest building progress of all antinukes that intercept given target
  293. end
  294.  
  295. function widget:DrawWorldPreUnit()  -- called each draw frame
  296.  
  297.     local mouse = GetMouseTargetPosition()
  298.  
  299.     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
  300.  
  301.         local interceptedBy = DrawEnemyInterceptors( mouse )
  302.  
  303.         if interceptedBy then
  304.  
  305.             glColor( unpack( colors.intercepting[ interceptedBy ]))     -- set color for 'nuke -> mouse pointer' line
  306.             glLineWidth(3)
  307.             glBeginEnd( GL_LINES, VertexList, {nukeSelected, mouse} )   -- draw the line
  308.  
  309.         end
  310.  
  311.     else                                                -- otherwise, use the "regular" colors for enemy antinukes circles
  312.         for unitID, def in pairs(antinukes.enemy) do
  313.             local ux, uz = def.x, def.z
  314.             if ux and uz then
  315.                 glColor( unpack( colors.regular[ def.stage ]))
  316.                 SetLineWidth()                          -- if a special key is pressed (F5 by default, see "Config"), make circle line thicker
  317.                 glDrawGroundCircle(ux, 0, uz, antiRange, 40 )
  318.             end
  319.         end
  320.     end
  321.  
  322.     for unitID, def in pairs(antinukes.ally) do         -- always draw allied (if spectating, all) antinukes circles in regular colors
  323.         local ux, uz = def.x, def.z
  324.         if ux and uz then
  325.             glColor( unpack( colors.regular[ def.stage ]))
  326.             SetLineWidth()                              -- if a special key is pressed (F5 by default, see "Config"), make circle lines thicker
  327.             glDrawGroundCircle(ux, 0, uz, antiRange, 40 )
  328.         end
  329.     end
  330. end

Recent Snippets