Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- function widget:GetInfo()
- return {
- name = "ScoutAvoidance",
- desc = "Make designated scouts stay out of range or sight of enemy units",
- author = "Berder",
- date = "2020",
- license = "GPLv2",
- layer = 11,
- enabled = true
- }
- end
- -- Credit to terve886 for his ShieldtargetAI widget
- -- and to snoke for AutoLLT, both of which I drew from.
- local pi = math.pi
- local sin = math.sin
- local cos = math.cos
- local atan = math.atan
- local sqrt = math.sqrt
- local sqrt2inv = 1 / sqrt(2)
- local UPDATE_FRAME=10
- local UnitStack = {}
- local GetUnitMaxRange = Spring.GetUnitMaxRange
- local GetUnitPosition = Spring.GetUnitPosition
- local GetMyAllyTeamID = Spring.GetMyAllyTeamID
- local GiveOrderToUnit = Spring.GiveOrderToUnit
- local GetGroundHeight = Spring.GetGroundHeight
- local GetUnitsInSphere = Spring.GetUnitsInSphere
- local GetUnitsInCylinder = Spring.GetUnitsInCylinder
- local GetUnitAllyTeam = Spring.GetUnitAllyTeam
- local GetUnitIsDead = Spring.GetUnitIsDead
- local GetMyTeamID = Spring.GetMyTeamID
- local GetUnitDefID = Spring.GetUnitDefID
- local GetUnitNearestEnemy = Spring.GetUnitNearestEnemy
- local GetUnitShieldState = Spring.GetUnitShieldState
- local GetTeamUnits = Spring.GetTeamUnits
- local GetUnitStates = Spring.GetUnitStates
- local GetSelectedUnits = Spring.GetSelectedUnits
- local WINDOW_COLOR = {1, 1, 1, 0.7}
- local WINDOW_FONT_SIZE = 12
- local WINDOW_B_SIZE = 12
- local WINDOW_LINE_PADDING = 3
- local WINDOW_CELL_PADDING = 1
- local Echo = Spring.Echo
- local GetSpecState = Spring.GetSpectatingState
- local FULL_CIRCLE_RADIANT = 2 * pi
- local CMD_UNIT_SET_TARGET = 34923
- local CMD_UNIT_CANCEL_TARGET = 34924
- local CMD_STOP = CMD.STOP
- local CMD_ATTACK = CMD.ATTACK
- -- we start moving when within (enemy range + SafetyBuffer) of the enemy
- local SafetyBuffer = 200
- local havens = {}
- local window
- local keypressmem = {}
- local AlwaysShowWindow = true
- local ATTACK_RANGE = 1
- local SIGHT_RANGE = 2
- local OFF = 3
- local function UpdateHavens()
- local team = GetMyAllyTeamID()
- local count = Spring.GetTeamRulesParam(team, "haven_count")
- havens = {}
- if count ~= nil then
- for i = 1, count do
- havens[i] = {x = Spring.GetTeamRulesParam(team, "haven_x" .. i),
- z = Spring.GetTeamRulesParam(team, "haven_z" .. i)}
- end
- end
- end
- local ScoutAvoidanceController = {
- unitID,
- pos,
- searchRange,
- allyTeamID = GetMyAllyTeamID(),
- avoiding = false,
- mode = ATTACK_RANGE,
- avoidingEnemy = nil,
- new = function(self, unitID)
- self = deepcopy(self)
- self.unitID = unitID
- self.pos = {GetUnitPosition(self.unitID)}
- self.searchRange = Spring.GetUnitSensorRadius(unitID, "los")
- return self
- end,
- unset = function(self)
- -- Echo("ScoutAvoidanceController removed:" .. self.unitID)
- GiveOrderToUnit(self.unitID,CMD_STOP, {}, {""},1)
- return nil
- end,
- getRetreatZone = function (self, retreatVector)
- -- returns the closest retreat zone that is behind us relative to the enemy
- -- or nil if no such zone exists
- -- retreatVector is a vector pointing behind us a short distance relative to the enemy
- local bestZoneIx = -1
- local bestZoneDistance = 999999999999
- -- rotate 45 degrees (not preserving length)
- local ret1X = retreatVector[1] + retreatVector[3]
- local ret1Z = -retreatVector[1] + retreatVector[3]
- -- rotate 45 degrees the other way (not preserving length)
- local ret2X = retreatVector[1] - retreatVector[3]
- local ret2Z = retreatVector[1] + retreatVector[3]
- -- basePos = self.pos + retreatVector
- -- (haven - basePos) . retreatVector > 0
- local baseX = self.pos[1] + retreatVector[1]
- local baseZ = self.pos[3] + retreatVector[3]
- for i=1, #havens do
- local dotProd1 = (havens[i].x - baseX) * ret1X + (havens[i].z - baseZ) * ret1Z
- local dotProd2 = (havens[i].x - baseX) * ret2X + (havens[i].z - baseZ) * ret2Z
- -- │ R1
- -- │ E R2
- -- R5 │
- -- │ U
- -- │ϴ1/
- -- │ /ϴ2 R3
- -- B─────────────────
- -- R6
- --
- -- R4
- -- E = enemy
- -- U = unit
- -- B = base position (straight away from enemy a short distance)
- -- ϴ1 = ϴ2 = 45 degrees
- --
- -- R1, R2, R3 = invalid retreat zones (blocked by enemy)
- -- R4, R5, R6 = valid retreat zones, of which we pick the closest.
- --
- if (dotProd1 > 0 or dotProd2 > 0) then
- local dx = havens[i].x - self.pos[1]
- local dz = havens[i].z - self.pos[3]
- local sqDist = dx * dx + dz * dz
- if (sqDist < bestZoneDistance) then
- bestZoneIx = i
- bestZoneDistance = sqDist
- end
- end
- end
- if (bestZoneIx == -1) then
- return nil
- end
- return havens[bestZoneIx]
- end,
- getEnemyRange = function (self, enemyID)
- local enemyRange = 0
- local DefID = GetUnitDefID(enemyID)
- if not(DefID == nil) then
- if self.mode == ATTACK_RANGE then
- if UnitDefs[DefID].weapons then
- for j=1, #UnitDefs[DefID].weapons do
- range = WeaponDefs[UnitDefs[DefID].weapons[j].weaponDef].range
- if (range and range > enemyRange) then
- enemyRange = range
- end
- end
- end
- elseif self.mode == SIGHT_RANGE then
- if UnitDefs[DefID].losRadius then
- enemyRange = UnitDefs[DefID].losRadius
- end
- end
- end
- if (enemyRange == 0) then
- enemyRange = 500
- end
- return enemyRange
- end,
- checkOneEnemy = function(self, enemyID)
- local enemyRange = self:getEnemyRange(enemyID)
- local enemySeparation = Spring.GetUnitSeparation(self.unitID, enemyID)
- if (enemySeparation == nil) then
- enemySeparation = 1000
- end
- if (enemySeparation < 50) then
- enemySeparation = 50 -- don't divide by 0
- end
- if (enemySeparation < enemyRange + SafetyBuffer) then
- local enemyPos = {GetUnitPosition(enemyID)}
- -- walk in the opposite direction from the enemy
- local retreatX = (self.pos[1] - enemyPos[1]) * 100 / enemySeparation
- local retreatY = 0
- local retreatZ = (self.pos[3] - enemyPos[3]) * 100 / enemySeparation
- return {x=retreatX, z=retreatZ}
- end
- return nil
- end,
- avoidEnemy = function (self)
- local units = GetUnitsInCylinder(self.pos[1], self.pos[3], self.searchRange, Spring.ENEMY_UNITS)
- for i=1, #units do
- if (GetUnitIsDead(units[i]) == false) then
- local result = self:checkOneEnemy(units[i])
- if result ~= nil then
- local retreatZone = self:getRetreatZone({result.x, 0, result.z})
- if (retreatZone ~= nil) then
- GiveOrderToUnit(self.unitID, CMD.FIGHT, {retreatZone.x, self.pos[2], retreatZone.z}, {})
- else
- GiveOrderToUnit(self.unitID, CMD.FIGHT, {self.pos[1] + result.x, self.pos[2], self.pos[3] + result.z}, {} )
- end
- self.retreating = true
- self.avoidingEnemy = enemyID
- return true
- end
- end
- end
- if (self.retreating) then
- -- we were retreating, but now no enemy is near, so stop.
- self.retreating = false
- self.avoidingEnemy = nil
- GiveOrderToUnit(self.unitID,CMD_STOP, {}, {""},1)
- end
- return false
- end,
- handle=function(self, n)
- local cQueue = Spring.GetCommandQueue(self.unitID, 1)
- -- avoidance is active when idle or when walk-fighting
- if(cQueue == nil or #cQueue == 0 or cQueue[1].id==CMD.FIGHT) then
- self.pos = {GetUnitPosition(self.unitID)}
- -- if the enemy we were avoiding before is still in range,
- -- and we're still on an attack-move command,
- -- don't issue another command and just keep doing what we were doing.
- -- (for efficiency)
- if cQueue ~= nil and #cQueue > 0 and cQueue[1].id==CMD.FIGHT and self.avoidingEnemy ~= nil then
- local result = self:checkOneEnemy(self.avoidingEnemy)
- if result ~= nil then
- return
- else
- self.avoidingEnemy = nil
- end
- end
- self:avoidEnemy()
- else
- self.retreating = false
- end
- end
- }
- function widget:ToggleAvoidance()
- if keypressmem["ToggleAvoidance"] ~= nil then
- return
- end
- keypressmem["ToggleAvoidance"] = true
- local someAttackRange = false
- local someSightRange = false
- for k,unitID in pairs(GetSelectedUnits()) do
- if UnitStack[unitID] ~= nil then
- if UnitStack[unitID].mode == ATTACK_RANGE then
- someAttackRange = true
- elseif UnitStack[unitID].mode == SIGHT_RANGE then
- someSightRange = true
- end
- end
- end
- local mode = ATTACK_RANGE
- if someAttackRange then
- mode = SIGHT_RANGE
- elseif someSightRange then
- mode = OFF
- end
- for k,unitID in pairs(GetSelectedUnits()) do
- if UnitStack[unitID] ~= nil then
- if mode == OFF then
- UnitStack[unitID]=UnitStack[unitID]:unset()
- else
- UnitStack[unitID].mode = mode
- end
- elseif mode ~= OFF then
- UnitStack[unitID] = ScoutAvoidanceController:new(unitID)
- UnitStack[unitID].mode = mode
- end
- end
- end
- function distance ( x1, y1, x2, y2 )
- local dx = (x1 - x2)
- local dy = (y1 - y2)
- return sqrt ( dx * dx + dy * dy )
- end
- function widget:UnitFinished(unitID, unitDefID, unitTeam)
- end
- function widget:CommandNotify(cmdID, cmdParams, cmdOptions)
- end
- function widget:UnitDestroyed(unitID)
- if not (UnitStack[unitID]==nil) then
- UnitStack[unitID]=UnitStack[unitID]:unset();
- end
- end
- options_path = "Settings/Unit Behaviour/Scout Avoidance"
- options_order = {"show_gui", "hotkey_toggle"}
- options = {
- show_gui = {
- name = 'Always Show Window',
- type = 'bool',
- value = true,
- noHotkey = true,
- OnChange = function(a)
- AlwaysShowWindow = a.value
- end,
- },
- hotkey_toggle = {
- name = "Toggle Avoidance Hotkey",
- type = "button",
- noHotkey = false,
- action = "toggle_avoidance",
- },
- }
- function widget:initHotkeyActions()
- widgetHandler:AddAction("toggle_avoidance", ToggleAvoidance, nil, 'tp')
- end
- function widget:UiDisplay()
- window:SetVisibility(false)
- local scoutSelected = false
- local someAttackRange = false
- local someSightRange = false
- local someOff = false
- for k,unitID in pairs(GetSelectedUnits()) do
- if UnitStack[unitID] ~= nil then
- scoutSelected = true
- if UnitStack[unitID].mode == ATTACK_RANGE then
- someAttackRange = true
- elseif UnitStack[unitID].mode == SIGHT_RANGE then
- someSightRange = true
- end
- else
- someOff = true
- end
- end
- local mode = "OFF"
- if someAttackRange and someSightRange or someOff and someAttackRange or someSightRange and someOff then
- mode = "mixed"
- elseif someAttackRange then
- mode = "avoid attacks"
- elseif someSightRange then
- mode = "avoid sight"
- end
- window.children[3]:SetCaption(mode)
- if scoutSelected then
- window:SetVisibility(true)
- end
- if AlwaysShowWindow and #GetSelectedUnits() > 0 then
- window:SetVisibility(true)
- end
- end
- function widget:GameFrame(n)
- keypressmem = {}
- widget:UiDisplay()
- if (n%UPDATE_FRAME==0) then
- UpdateHavens()
- for unitID,unit in pairs(UnitStack) do
- unit:handle()
- end
- end
- end
- function widget:initGUI()
- window = WG.Chili.Window:New{
- name = "Scout Avoidance Window",
- x = 0,
- y = 100,
- savespace = false,
- resizable = false,
- draggable = true,
- autosize = false,
- color = WINDOW_COLOR,
- parent = WG.Chili.Screen0,
- mainHeight = 100,
- maxHeight = 100,
- maxWidth = 300,
- minWidth = 300,
- children = {
- WG.Chili.Label:New{
- x = 0,
- right = 50,
- y = 0,
- height = WINDOW_FONT_SIZE,
- align = "center",
- font = {size = 12, outline = true, color = {1, 1, 0, 1}},
- caption = "- Scout Avoidance Controller -",
- fontSize = WINDOW_FONT_SIZE,
- },
- WG.Chili.Label:New{
- x = 0,
- y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
- right = 0,
- height = WINDOW_FONT_SIZE,
- caption = "Avoidance:",
- fontSize = WINDOW_FONT_SIZE,
- },
- WG.Chili.Label:New{
- x = 100,
- y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
- height = WINDOW_FONT_SIZE,
- caption = "OFF",
- fontSize = WINDOW_FONT_SIZE,
- },
- WG.Chili.Button:New{
- x = 200,
- y = (WINDOW_FONT_SIZE + WINDOW_LINE_PADDING) * 2,
- height = WINDOW_FONT_SIZE,
- minWidth = 50,
- maxWidth = 50,
- name = "AvoidanceButton",
- fontSize = WINDOW_FONT_SIZE,
- caption = "Toggle",
- OnClick = {
- function()
- widget:ToggleAvoidance()
- end
- },
- },
- }
- }
- window:SetVisibility(false)
- end
- function deepcopy(orig)
- local orig_type = type(orig)
- local copy
- if orig_type == 'table' then
- copy = {}
- for orig_key, orig_value in next, orig, nil do
- copy[deepcopy(orig_key)] = deepcopy(orig_value)
- end
- setmetatable(copy, deepcopy(getmetatable(orig)))
- else
- copy = orig
- end
- return copy
- end
- function dump(o)
- if type(o) == 'table' then
- local s = '{ '
- for k,v in pairs(o) do
- if type(k) ~= 'number' then k = '"'..k..'"' end
- s = s .. '['..k..'] = ' .. dump(v) .. ','
- end
- return s .. '} '
- else
- return tostring(o)
- end
- end
- local function DisableForSpec()
- if GetSpecState() then
- widgetHandler:RemoveWidget()
- end
- end
- function widget:Initialize()
- DisableForSpec()
- widget:initGUI()
- widget:initHotkeyActions()
- end
- function widget:PlayerChanged (playerID)
- DisableForSpec()
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement