Advertisement
Guest User

Untitled

a guest
Aug 24th, 2020
239
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.85 KB | None | 0 0
  1. function widget:GetInfo()
  2.    return {
  3.       name         = "ScoutAvoidance",
  4.       desc         = "Make designated scouts stay out of range or sight of enemy units",
  5.       author       = "Berder",
  6.       date         = "2020",
  7.       license      = "GPLv2",
  8.       layer        = 11,
  9.       enabled      = true
  10.    }
  11. end
  12.  
  13. -- Credit to terve886 for his ShieldtargetAI widget
  14. -- and to snoke for AutoLLT, both of which I drew from.
  15.  
  16. local pi = math.pi
  17. local sin = math.sin
  18. local cos = math.cos
  19. local atan = math.atan
  20. local sqrt = math.sqrt
  21. local sqrt2inv = 1 / sqrt(2)
  22. local UPDATE_FRAME=10
  23. local UnitStack = {}
  24. local GetUnitMaxRange = Spring.GetUnitMaxRange
  25. local GetUnitPosition = Spring.GetUnitPosition
  26. local GetMyAllyTeamID = Spring.GetMyAllyTeamID
  27. local GiveOrderToUnit = Spring.GiveOrderToUnit
  28. local GetGroundHeight = Spring.GetGroundHeight
  29. local GetUnitsInSphere = Spring.GetUnitsInSphere
  30. local GetUnitsInCylinder = Spring.GetUnitsInCylinder
  31. local GetUnitAllyTeam = Spring.GetUnitAllyTeam
  32. local GetUnitIsDead = Spring.GetUnitIsDead
  33. local GetMyTeamID = Spring.GetMyTeamID
  34. local GetUnitDefID = Spring.GetUnitDefID
  35. local GetUnitNearestEnemy = Spring.GetUnitNearestEnemy
  36. local GetUnitShieldState = Spring.GetUnitShieldState
  37. local GetTeamUnits = Spring.GetTeamUnits
  38. local GetUnitStates = Spring.GetUnitStates
  39. local GetSelectedUnits = Spring.GetSelectedUnits
  40.  
  41. local WINDOW_COLOR = {1, 1, 1, 0.7}
  42. local WINDOW_FONT_SIZE = 12
  43. local WINDOW_B_SIZE = 12
  44. local WINDOW_LINE_PADDING = 3
  45. local WINDOW_CELL_PADDING = 1
  46.  
  47. local Echo = Spring.Echo
  48. local GetSpecState = Spring.GetSpectatingState
  49. local FULL_CIRCLE_RADIANT = 2 * pi
  50. local CMD_UNIT_SET_TARGET = 34923
  51. local CMD_UNIT_CANCEL_TARGET = 34924
  52. local CMD_STOP = CMD.STOP
  53. local CMD_ATTACK = CMD.ATTACK
  54.  
  55. -- we start moving when within (enemy range + SafetyBuffer) of the enemy
  56. local SafetyBuffer = 200
  57.  
  58. local havens = {}
  59.  
  60. local window
  61.  
  62. local keypressmem = {}
  63.  
  64. local AlwaysShowWindow = true
  65.  
  66. local ATTACK_RANGE = 1
  67. local SIGHT_RANGE = 2
  68. local OFF = 3
  69.  
  70. local function UpdateHavens()
  71.    local team = GetMyAllyTeamID()
  72.    local count = Spring.GetTeamRulesParam(team, "haven_count")
  73.    havens = {}
  74.    if count ~= nil then
  75.       for i = 1, count do
  76.          havens[i] = {x = Spring.GetTeamRulesParam(team, "haven_x" .. i),
  77.                       z = Spring.GetTeamRulesParam(team, "haven_z" .. i)}
  78.       end
  79.    end
  80. end
  81.  
  82.  
  83. local ScoutAvoidanceController = {
  84.    unitID,
  85.    pos,
  86.    searchRange,
  87.    allyTeamID = GetMyAllyTeamID(),
  88.    avoiding = false,
  89.    mode = ATTACK_RANGE,
  90.    avoidingEnemy = nil,
  91.  
  92.    new = function(self, unitID)
  93.       self = deepcopy(self)
  94.       self.unitID = unitID
  95.       self.pos = {GetUnitPosition(self.unitID)}
  96.       self.searchRange = Spring.GetUnitSensorRadius(unitID, "los")
  97.       return self
  98.    end,
  99.  
  100.    unset = function(self)
  101.       -- Echo("ScoutAvoidanceController removed:" .. self.unitID)
  102.       GiveOrderToUnit(self.unitID,CMD_STOP, {}, {""},1)
  103.       return nil
  104.    end,
  105.  
  106.    getRetreatZone = function (self, retreatVector)
  107.       -- returns the closest retreat zone that is behind us relative to the enemy
  108.       -- or nil if no such zone exists
  109.       -- retreatVector is a vector pointing behind us a short distance relative to the enemy
  110.       local bestZoneIx = -1
  111.       local bestZoneDistance = 999999999999
  112.  
  113.       -- rotate 45 degrees (not preserving length)
  114.       local ret1X = retreatVector[1] + retreatVector[3]
  115.       local ret1Z = -retreatVector[1] + retreatVector[3]
  116.  
  117.       -- rotate 45 degrees the other way (not preserving length)
  118.       local ret2X = retreatVector[1] - retreatVector[3]
  119.       local ret2Z = retreatVector[1] + retreatVector[3]
  120.      
  121.       -- basePos = self.pos + retreatVector
  122.       -- (haven - basePos) . retreatVector > 0
  123.       local baseX = self.pos[1] + retreatVector[1]
  124.       local baseZ = self.pos[3] + retreatVector[3]
  125.      
  126.       for i=1, #havens do
  127.          local dotProd1 = (havens[i].x - baseX) * ret1X + (havens[i].z - baseZ) * ret1Z
  128.          local dotProd2 = (havens[i].x - baseX) * ret2X + (havens[i].z - baseZ) * ret2Z
  129.          --            │ R1      
  130.          --            │      E     R2
  131.          --   R5       │        
  132.          --            │   U            
  133.          --            │ϴ1/  
  134.          --            │ /ϴ2    R3
  135.          --            B─────────────────
  136.          --  R6
  137.          --
  138.          --                 R4
  139.  
  140.          -- E = enemy
  141.          -- U = unit
  142.          -- B = base position (straight away from enemy a short distance)
  143.          -- ϴ1 = ϴ2 = 45 degrees
  144.          --
  145.          -- R1, R2, R3 = invalid retreat zones (blocked by enemy)
  146.          -- R4, R5, R6 = valid retreat zones, of which we pick the closest.
  147.          --
  148.          if (dotProd1 > 0 or dotProd2 > 0) then
  149.             local dx = havens[i].x - self.pos[1]
  150.             local dz = havens[i].z - self.pos[3]
  151.             local sqDist = dx * dx + dz * dz
  152.             if (sqDist < bestZoneDistance) then
  153.                bestZoneIx = i
  154.                bestZoneDistance = sqDist
  155.             end
  156.          end
  157.       end
  158.  
  159.       if (bestZoneIx == -1) then
  160.          return nil
  161.       end
  162.  
  163.       return havens[bestZoneIx]
  164.    end,
  165.  
  166.    getEnemyRange = function (self, enemyID)
  167.       local enemyRange = 0
  168.       local DefID = GetUnitDefID(enemyID)
  169.       if not(DefID == nil) then
  170.          if self.mode == ATTACK_RANGE then
  171.             if UnitDefs[DefID].weapons then
  172.                for j=1, #UnitDefs[DefID].weapons do
  173.                   range = WeaponDefs[UnitDefs[DefID].weapons[j].weaponDef].range
  174.                   if (range and range > enemyRange) then
  175.                      enemyRange = range
  176.                   end
  177.                end
  178.             end
  179.          elseif self.mode == SIGHT_RANGE then
  180.             if UnitDefs[DefID].losRadius then
  181.                enemyRange = UnitDefs[DefID].losRadius
  182.             end
  183.          end
  184.       end
  185.      
  186.       if (enemyRange == 0) then
  187.          enemyRange = 500
  188.       end
  189.  
  190.       return enemyRange
  191.    end,
  192.  
  193.    checkOneEnemy = function(self, enemyID)
  194.       local enemyRange = self:getEnemyRange(enemyID)
  195.  
  196.       local enemySeparation = Spring.GetUnitSeparation(self.unitID, enemyID)
  197.  
  198.       if (enemySeparation == nil) then
  199.          enemySeparation = 1000
  200.       end
  201.       if (enemySeparation < 50) then
  202.          enemySeparation = 50 -- don't divide by 0
  203.       end
  204.       if (enemySeparation < enemyRange + SafetyBuffer) then
  205.          local enemyPos = {GetUnitPosition(enemyID)}
  206.          -- walk in the opposite direction from the enemy
  207.          local retreatX = (self.pos[1] - enemyPos[1]) * 100 / enemySeparation
  208.          local retreatY = 0
  209.          local retreatZ = (self.pos[3] - enemyPos[3]) * 100 / enemySeparation
  210.          
  211.          return {x=retreatX, z=retreatZ}
  212.       end
  213.       return nil
  214.    end,
  215.  
  216.    avoidEnemy = function (self)
  217.       local units = GetUnitsInCylinder(self.pos[1], self.pos[3], self.searchRange, Spring.ENEMY_UNITS)
  218.       for i=1, #units do
  219.          if (GetUnitIsDead(units[i]) == false) then
  220.             local result = self:checkOneEnemy(units[i])
  221.             if result ~= nil then
  222.                local retreatZone = self:getRetreatZone({result.x, 0, result.z})
  223.                if (retreatZone ~= nil) then
  224.                   GiveOrderToUnit(self.unitID, CMD.FIGHT, {retreatZone.x, self.pos[2], retreatZone.z}, {})
  225.                else
  226.                   GiveOrderToUnit(self.unitID, CMD.FIGHT, {self.pos[1] + result.x, self.pos[2], self.pos[3] + result.z}, {} )
  227.                end
  228.                self.retreating = true
  229.                self.avoidingEnemy = enemyID
  230.                return true
  231.             end
  232.          end
  233.       end
  234.       if (self.retreating) then
  235.          -- we were retreating, but now no enemy is near, so stop.
  236.          self.retreating = false
  237.          self.avoidingEnemy = nil
  238.          GiveOrderToUnit(self.unitID,CMD_STOP, {}, {""},1)
  239.       end
  240.       return false
  241.    end,
  242.  
  243.    handle=function(self, n)
  244.       local cQueue = Spring.GetCommandQueue(self.unitID, 1)
  245.       -- avoidance is active when idle or when walk-fighting
  246.       if(cQueue == nil or #cQueue == 0 or cQueue[1].id==CMD.FIGHT) then
  247.          self.pos = {GetUnitPosition(self.unitID)}
  248.          -- if the enemy we were avoiding before is still in range,
  249.          -- and we're still on an attack-move command,
  250.          -- don't issue another command and just keep doing what we were doing.
  251.          -- (for efficiency)
  252.          if cQueue ~= nil and #cQueue > 0 and cQueue[1].id==CMD.FIGHT and self.avoidingEnemy ~= nil then
  253.             local result = self:checkOneEnemy(self.avoidingEnemy)
  254.             if result ~= nil then
  255.                return
  256.             else
  257.                self.avoidingEnemy = nil
  258.             end
  259.          end
  260.          self:avoidEnemy()
  261.       else
  262.          self.retreating = false
  263.       end
  264.    end
  265. }
  266.  
  267. function widget:ToggleAvoidance()
  268.    if keypressmem["ToggleAvoidance"] ~= nil then
  269.       return
  270.    end
  271.    keypressmem["ToggleAvoidance"] = true
  272.    local someAttackRange = false
  273.    local someSightRange = false
  274.    
  275.    for k,unitID in pairs(GetSelectedUnits()) do
  276.       if UnitStack[unitID] ~= nil then
  277.          if UnitStack[unitID].mode == ATTACK_RANGE then
  278.             someAttackRange = true
  279.          elseif UnitStack[unitID].mode == SIGHT_RANGE then
  280.             someSightRange = true
  281.          end
  282.       end
  283.    end
  284.    
  285.    local mode = ATTACK_RANGE
  286.    if someAttackRange then
  287.       mode = SIGHT_RANGE
  288.    elseif someSightRange then
  289.       mode = OFF
  290.    end
  291.    
  292.    for k,unitID in pairs(GetSelectedUnits()) do
  293.       if UnitStack[unitID] ~= nil then
  294.          if mode == OFF then
  295.             UnitStack[unitID]=UnitStack[unitID]:unset()
  296.          else
  297.             UnitStack[unitID].mode = mode
  298.          end
  299.       elseif mode ~= OFF then
  300.          UnitStack[unitID] = ScoutAvoidanceController:new(unitID)
  301.          UnitStack[unitID].mode = mode
  302.       end
  303.    end
  304. end
  305.  
  306.  
  307. function distance ( x1, y1, x2, y2 )
  308.    local dx = (x1 - x2)
  309.    local dy = (y1 - y2)
  310.    return sqrt ( dx * dx + dy * dy )
  311. end
  312.  
  313. function widget:UnitFinished(unitID, unitDefID, unitTeam)
  314. end
  315.  
  316. function widget:CommandNotify(cmdID, cmdParams, cmdOptions)
  317. end
  318.  
  319. function widget:UnitDestroyed(unitID)
  320.    if not (UnitStack[unitID]==nil) then
  321.       UnitStack[unitID]=UnitStack[unitID]:unset();
  322.    end
  323. end
  324.  
  325.  
  326. options_path = "Settings/Unit Behaviour/Scout Avoidance"
  327. options_order = {"show_gui", "hotkey_toggle"}
  328. options = {
  329.    show_gui = {
  330.       name = 'Always Show Window',
  331.       type = 'bool',
  332.       value = true,
  333.       noHotkey = true,
  334.       OnChange = function(a)
  335.          AlwaysShowWindow = a.value
  336.       end,
  337.    },
  338.    hotkey_toggle = {
  339.       name = "Toggle Avoidance Hotkey",
  340.       type = "button",
  341.       noHotkey = false,
  342.       action = "toggle_avoidance",
  343.    },
  344. }
  345.  
  346. function widget:initHotkeyActions()
  347.    widgetHandler:AddAction("toggle_avoidance", ToggleAvoidance, nil, 'tp')
  348. end
  349.  
  350. function widget:UiDisplay()
  351.    window:SetVisibility(false)
  352.    local scoutSelected = false
  353.    local someAttackRange = false
  354.    local someSightRange = false
  355.    local someOff = false
  356.    for k,unitID in pairs(GetSelectedUnits()) do
  357.       if UnitStack[unitID] ~= nil then
  358.          scoutSelected = true
  359.          if UnitStack[unitID].mode == ATTACK_RANGE then
  360.             someAttackRange = true
  361.          elseif UnitStack[unitID].mode == SIGHT_RANGE then
  362.             someSightRange = true
  363.          end
  364.       else
  365.          someOff = true
  366.       end
  367.    end
  368.    
  369.    local mode = "OFF"
  370.    if someAttackRange and someSightRange or someOff and someAttackRange or someSightRange and someOff then
  371.       mode = "mixed"
  372.    elseif someAttackRange then
  373.       mode = "avoid attacks"
  374.    elseif someSightRange then
  375.       mode = "avoid sight"
  376.    end
  377.    window.children[3]:SetCaption(mode)
  378.    
  379.    if scoutSelected then
  380.       window:SetVisibility(true)
  381.    end
  382.    if AlwaysShowWindow and #GetSelectedUnits() > 0 then
  383.       window:SetVisibility(true)
  384.    end
  385. end
  386.  
  387.  
  388. function widget:GameFrame(n)
  389.    keypressmem = {}
  390.    widget:UiDisplay()
  391.    if (n%UPDATE_FRAME==0) then
  392.       UpdateHavens()
  393.       for unitID,unit in pairs(UnitStack) do
  394.          unit:handle()
  395.       end
  396.    end
  397. end
  398.  
  399. function widget:initGUI()
  400.    window = WG.Chili.Window:New{
  401.       name = "Scout Avoidance Window",
  402.       x = 0,
  403.       y = 100,
  404.       savespace = false,
  405.       resizable = false,
  406.       draggable = true,
  407.       autosize = false,
  408.       color = WINDOW_COLOR,
  409.       parent = WG.Chili.Screen0,
  410.       mainHeight = 100,
  411.       maxHeight = 100,
  412.       maxWidth = 300,
  413.       minWidth = 300,
  414.       children = {
  415.          WG.Chili.Label:New{
  416.             x = 0,
  417.             right = 50,
  418.             y = 0,
  419.             height = WINDOW_FONT_SIZE,
  420.             align = "center",
  421.             font = {size = 12, outline = true, color = {1, 1, 0, 1}},
  422.             caption = "- Scout Avoidance Controller -",
  423.             fontSize = WINDOW_FONT_SIZE,
  424.          },
  425.          WG.Chili.Label:New{
  426.             x = 0,
  427.             y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
  428.             right = 0,
  429.             height = WINDOW_FONT_SIZE,
  430.             caption = "Avoidance:",
  431.             fontSize = WINDOW_FONT_SIZE,
  432.          },
  433.          WG.Chili.Label:New{
  434.             x = 100,
  435.             y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
  436.             height = WINDOW_FONT_SIZE,
  437.             caption = "OFF",
  438.             fontSize = WINDOW_FONT_SIZE,
  439.          },
  440.          WG.Chili.Button:New{
  441.             x = 200,
  442.             y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
  443.             height = WINDOW_FONT_SIZE,
  444.             minWidth = 50,
  445.             maxWidth = 50,
  446.             name = "AvoidanceButton",
  447.             fontSize = WINDOW_FONT_SIZE,
  448.             caption = "Toggle",
  449.             OnClick  = {
  450.                function()
  451.                   widget:ToggleAvoidance()
  452.                end
  453.             },
  454.          },
  455.       }
  456.    }
  457.    window:SetVisibility(false)
  458. end
  459.    
  460.  
  461. function deepcopy(orig)
  462.    local orig_type = type(orig)
  463.    local copy
  464.    if orig_type == 'table' then
  465.       copy = {}
  466.       for orig_key, orig_value in next, orig, nil do
  467.          copy[deepcopy(orig_key)] = deepcopy(orig_value)
  468.       end
  469.       setmetatable(copy, deepcopy(getmetatable(orig)))
  470.    else
  471.       copy = orig
  472.    end
  473.    return copy
  474. end
  475.  
  476. function dump(o)
  477.    if type(o) == 'table' then
  478.       local s = '{ '
  479.       for k,v in pairs(o) do
  480.          if type(k) ~= 'number' then k = '"'..k..'"' end
  481.          s = s .. '['..k..'] = ' .. dump(v) .. ','
  482.       end
  483.       return s .. '} '
  484.    else
  485.       return tostring(o)
  486.    end
  487. end
  488.  
  489. local function DisableForSpec()
  490.    if GetSpecState() then
  491.       widgetHandler:RemoveWidget()
  492.    end
  493. end
  494.  
  495.  
  496. function widget:Initialize()
  497.    DisableForSpec()
  498.    widget:initGUI()
  499.    widget:initHotkeyActions()
  500. end
  501.  
  502.  
  503. function widget:PlayerChanged (playerID)
  504.    DisableForSpec()
  505. end
  506.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement