#****************************************************************************
#**
#**  File     :  /lua/AIBehaviors.lua
#**  Author(s): Robert Oates, Gautam Vasudevan, ...?
#**
#**  Summary  : 
#**
#**  Copyright © 2005 Gas Powered Games, Inc.  All rights reserved.
#****************************************************************************
#########################################
# Platoon Lua Module                    #
#########################################
local AIUtils = import('/lua/ai/aiutilities.lua')
local Utilities = import('/lua/utilities.lua')
local AIBuildStructures = import('/lua/ai/aibuildstructures.lua')
local UnitUpgradeTemplates = import('/lua/upgradetemplates.lua').UnitUpgradeTemplates
local StructureUpgradeTemplates = import('/lua/upgradetemplates.lua').StructureUpgradeTemplates
local ScenarioFramework = import('/lua/ScenarioFramework.lua')
local AIAttackUtils = import('/lua/ai/aiattackutilities.lua')
local TriggerFile = import('/lua/scenariotriggers.lua')


# ===== CDR ADD BEHAVIORS ===== #

function CDRRunAway( aiBrain, cdr )
    if cdr:GetHealthPercent() < .7 then
        AIUtils.AIFindDefensiveArea( aiBrain, cdr, categories.DEFENSE * categories.ANTIAIR, 10000 )
        #LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR AI ACTIVATE - CDR RUNNING AWAY' )
        local cdrPos = cdr:GetPosition()
        local nmeAir = aiBrain:GetUnitsAroundPoint( categories.AIR, cdrPos, 25, 'Enemy' )
        local nmeLand = aiBrain:GetUnitsAroundPoint( categories.LAND, cdrPos, 25, 'Enemy' )
        local nmeHardcore = aiBrain:GetUnitsAroundPoint( categories.EXPERIMENTAL, cdrPos, 25, 'Enemy' )
        if nmeAir > 3 or nmeLand > 3 or nmeHardcore > 0 then
            if cdr:IsUnitState('Building') then
                cdr.UnitBeingBuiltBehavior = cdr:GetUnitBeingBuilt()
            end
            local canTeleport = cdr:HasEnhancement( 'Teleporter' )
            CDRRevertPriorityChange( aiBrain, cdr )
            local category
            if nmeAir > 3 then
                category = categories.DEFENSE * categories.ANTIAIR
            else
                category = categories.DEFENSE * categories.DIRECTFIRE
            end
            local runSpot, prevSpot
            local plat = aiBrain:MakePlatoon( '', '' )
            aiBrain:AssignUnitsToPlatoon( plat, {cdr}, 'support', 'None' )
            repeat
                #LOG("AI DEBUG: " .. aiBrain.Nickname .. " running")
                if canTeleport then
                    runSpot = AIUtils.AIFindDefensiveArea( aiBrain, cdr, category, 10000 )
                else
                    runSpot = AIUtils.AIFindDefensiveArea( aiBrain, cdr, category, 50 )
                end
                if not prevSpot or runSpot[1] ~= prevSpot[1] or runSpot[3] ~= prevSpot[3] then                
                    plat:Stop()
                    if VDist2( cdrPos[1], cdrPos[3], runSpot[1], runSpot[3] ) >= 10 then
                        if canTeleport then
                            IssueTeleport( {cdr}, runSpot )
                        else
                            cmd = plat:MoveToLocation( runSpot, false )
                        end
                    end
                end
                WaitSeconds(3)
                if not cdr:IsDead() then
                    cdrPos = cdr:GetPosition()
                    nmeAir = aiBrain:GetUnitsAroundPoint( categories.AIR, cdrPos, 25, 'Enemy' )
                    nmeLand = aiBrain:GetUnitsAroundPoint( categories.LAND, cdrPos, 25, 'Enemy' )
                    nmeHardcore = aiBrain:GetUnitsAroundPoint( categories.EXPERIMENTAL, cdrPos, 25, 'Enemy' )
                end
            until cdr:IsDead() or ( nmeAir < 2 and nmeLand < 2 and nmeHardcore == 0 ) or cdr:GetHealthPercent() > .4
            IssueClearCommands( {cdr} )
        end
    end 
end


function CDROverCharge( aiBrain, cdr )
    local weapBPs = cdr:GetBlueprint().Weapon
    local weapon

    for k,v in weapBPs do
        if v.Label == 'OverCharge' then
            weapon = v
            break
        end
    end
    local distressRange = 30
    local beingBuilt = false
    cdr.UnitBeingBuiltBehavior = false
    local cdrPos = cdr.CDRHome
    local numUnits = aiBrain:GetNumUnitsAroundPoint( categories.LAND - categories.SCOUT, cdrPos, ( weapon.MaxRadius ), 'Enemy' )
    local distressLoc = aiBrain:BaseMonitorDistressLocation(cdrPos)
    local overCharging = false
    local maxRadius = weapon.MaxRadius + 10
    
    if Utilities.XZDistanceTwoVectors(cdrPos, cdr:GetPosition()) > maxRadius then
        return
    end
    
    if numUnits > 0 or ( not cdr.DistressCall and distressLoc and Utilities.XZDistanceTwoVectors( distressLoc, cdrPos ) < distressRange ) then
        CDRRevertPriorityChange( aiBrain, cdr )
        if cdr:GetUnitBeingBuilt() then
            #LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR was building something')
            cdr.UnitBeingBuiltBehavior = cdr:GetUnitBeingBuilt()
        end
        local plat = aiBrain:MakePlatoon( '', '' )
        aiBrain:AssignUnitsToPlatoon( plat, {cdr}, 'support', 'None' )
        plat:Stop()
        local priList = { categories.EXPERIMENTAL, categories.TECH3 * categories.INDIRECTFIRE,
            categories.TECH3 * categories.MOBILE, categories.TECH2 * categories.INDIRECTFIRE, categories.MOBILE * categories.TECH2,
            categories.TECH1 * categories.INDIRECTFIRE, categories.TECH1 * categories.MOBILE, categories.ALLUNITS }
        #LOG('*AI DEBUG: ARMY ' , repr(aiBrain:GetArmyIndex()),  ': CDR AI ACTIVATE - Commander go fight stuff! -- ' .. numUnits)
        local target
        local continueFighting = true
        local counter = 0
        
        local cdrThreat = cdr:GetBlueprint().Defense.SurfaceThreatLevel or 75
        local enemyThreat
        repeat
            #LOG("AI DEBUG: " .. aiBrain.Nickname .. " overcharging")
            overCharging = false
            if counter >= 5 or not target or target:IsDead() or Utilities.XZDistanceTwoVectors(cdrPos, target:GetPosition()) > maxRadius then
                counter = 0
                for k,v in priList do
                    target = plat:FindClosestUnit( 'Support', 'Enemy', true, v )
                    if target and Utilities.XZDistanceTwoVectors(cdrPos, target:GetPosition()) < maxRadius then
                        local cdrLayer = cdr:GetCurrentLayer()
                        local targetLayer = target:GetCurrentLayer()
                        if not (cdrLayer == 'Land' and (targetLayer == 'Air' or targetLayer == 'Sub' or targetLayer == 'Seabed')) and
                           not (cdrLayer == 'Seabed' and (targetLayer == 'Air' or targetLayer == 'Water')) then
                            break
                        end
                    end
                    target = false
                end
                if target then
                    local targetPos = target:GetPosition()
                    enemyThreat = aiBrain:GetThreatAtPosition( targetPos, 1, true, 'AntiSurface')
                    if enemyThreat >= cdrThreat / 2 then
                        return
                    end
                    if aiBrain:GetEconomyStored('ENERGY') >= weapon.EnergyRequired and target and not target:IsDead() then
                        overCharging = true
                        IssueClearCommands({cdr})
                        IssueOverCharge( {cdr}, target )
                    end
                elseif distressLoc then
                    enemyThreat = aiBrain:GetThreatAtPosition( distressLoc, 1, true, 'AntiSurface')
                    if enemyThreat >= cdrThreat / 2 then
                        return
                    end                
                    if distressLoc and ( Utilities.XZDistanceTwoVectors( distressLoc, cdrPos ) < distressRange ) then
                        IssueClearCommands( {cdr} )
                        IssueMove( {cdr}, distressLoc )
                        IssueMove( {cdr}, cdr.CDRHome )
                    end
                end
            end
            if overCharging then
                while target and not target:IsDead() and not cdr:IsDead() and counter <= 5 do
                    WaitSeconds(0.5)
                    counter = counter + 0.5
                end
            else
                WaitSeconds(5)
                counter = counter + 5
            end
            distressLoc = aiBrain:BaseMonitorDistressLocation(cdrPos)
            if cdr:IsDead() then
                return
            end
            if ( aiBrain:GetNumUnitsAroundPoint( categories.LAND - categories.SCOUT, cdrPos, maxRadius, 'Enemy' ) <= 3 )
                and ( not distressLoc or ( Utilities.XZDistanceTwoVectors( distressLoc, cdrPos ) < distressRange )
                and ( Utilities.XZDistanceTwoVectors(cdrPos, cdr:GetPosition()) < maxRadius ) ) then
                continueFighting = false
            end           
        until not continueFighting or not aiBrain:PlatoonExists(plat)
        #LOG("AI DEBUG: " .. aiBrain.Nickname .. " done overcharging")
        IssueClearCommands( {cdr} )
    end
end


function CDRRevertPriorityChange( aiBrain, cdr )
    if cdr.PreviousPriority and cdr.Platoon and aiBrain:PlatoonExists(cdr.Platoon) then
        #LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': Reverting priority of builder')
        aiBrain:PBMSetPriority( cdr.Platoon, cdr.PreviousPriority )
    end
end

function CDRReturnHome(aiBrain, cdr)
    local loc, rad
    loc = cdr.CDRHome
    rad = 75
    # this is a reference... so it will autoupdate
    local cdrPos = cdr:GetPosition()
    local distSqAway = 1600
    if not cdr:IsDead() and VDist2Sq(cdrPos[1], cdrPos[3], loc[1], loc[3]) > distSqAway then
        local plat = aiBrain:MakePlatoon( '', '' )
        aiBrain:AssignUnitsToPlatoon( plat, {cdr}, 'support', 'None' )
        repeat
            #LOG("AI DEBUG: " .. aiBrain.Nickname .. " going home")
            CDRRevertPriorityChange( aiBrain, cdr )
            if not aiBrain:PlatoonExists(plat) then 
                return 
            end
            IssueStop( {cdr} )
            IssueMove( {cdr}, loc )
            cdr.GoingHome = true
            WaitSeconds(7)
       until cdr:IsDead() or VDist2Sq(cdrPos[1], cdrPos[3], loc[1], loc[3]) <= distSqAway
       cdr.GoingHome = false
       IssueClearCommands( {cdr} )
       #LOG("AI DEBUG: " .. aiBrain.Nickname .. " done going home")
    end

end



function SetCDRHome(cdr, plat)
    cdr.CDRHome = table.copy(cdr:GetPosition())
end


function CommanderBehavior(platoon)
    for k,v in platoon:GetPlatoonUnits() do
        if not v:IsDead() and not v.CommanderThread then
            v.CommanderThread = v:ForkThread( CommanderThread, platoon )
        end
    end
end


function CommanderThread(cdr, platoon)
    local aiBrain = cdr:GetAIBrain()
    
    SetCDRHome(cdr, platoon)
    
    aiBrain:BuildScoutLocations()
    
    while not cdr:IsDead() do

        WaitTicks(2)
        # Overcharge
        if not cdr:IsDead() then CDROverCharge( aiBrain, cdr ) end
        WaitTicks(1)
        # Run away (not really useful right now, without teleport ability kicking in... might as well just go home)
        #if not cdr:IsDead() then CDRRunAway( aiBrain, cdr ) end
        #WaitTicks(1)
        # Go back to base
        if not cdr:IsDead() then CDRReturnHome( aiBrain, cdr ) end
        WaitTicks(1)
        
        #call platoon resume building deal...
        if not cdr:IsDead() and cdr:IsIdleState() then
            if not cdr.EngineerBuildQueue or table.getn(cdr.EngineerBuildQueue) == 0 then
                local pool = aiBrain:GetPlatoonUniquelyNamed('ArmyPool')
                aiBrain:AssignUnitsToPlatoon( pool, {cdr}, 'Unassigned', 'None' )
            elseif cdr.EngineerBuildQueue and table.getn(cdr.EngineerBuildQueue) != 0 then
                if not cdr.NotBuildingThread then
                    cdr.NotBuildingThread = cdr:ForkThread(platoon.WatchForNotBuilding)
                end             
            end
        end        
    end
end


# ==== Generic Unit Behaviors === #
function BuildOnceAI(platoon)
    platoon:BuildOnceAI()
end

function AirUnitRefit(self)
    for k,v in self:GetPlatoonUnits() do
        if not v:IsDead() and not v.RefitThread then
            v.RefitThreat = v:ForkThread( AirUnitRefitThread, self:GetPlan(), self.PlatoonData )
        end
    end
end

function AirUnitRefitThread(unit, plan, data)
    unit.PlanName = plan
    if data then
        unit.PlatoonData = data
    end
    local aiBrain = unit:GetAIBrain()
    while not unit:IsDead() do
        local fuel = unit:GetFuelRatio()
        local health = unit:GetHealthPercent()
        if not unit.Loading and ( fuel < .2 or health < .4 ) then
            # Find air stage
            if aiBrain:GetCurrentUnits( categories.AIRSTAGINGPLATFORM ) > 0 then
                local unitPos = unit:GetPosition()
                local plats = AIUtils.GetOwnUnitsAroundPoint( aiBrain, categories.AIRSTAGINGPLATFORM, unitPos, 400 )
                if table.getn( plats ) > 0 then
                    local closest, distance
                    for k,v in plats do
                        if not v:IsDead() then
                            local roomAvailable = false
                            if EntityCategoryContains( categories.CARRIER, v ) then
                                #roomAvailable = v:TransportHasAvailableStorage( unit )
                            else
                                roomAvailable = v:TransportHasSpaceFor( unit )
                            end
                            if roomAvailable then
                                local platPos = v:GetPosition()
                                local tempDist = VDist2( unitPos[1], unitPos[3], platPos[1], platPos[3] )
                                if ( not closest or tempDist < distance ) then
                                    closest = v
                                    distance = tempDist
                                end
                            end
                        end
                    end
                    if closest then
                        local plat = aiBrain:MakePlatoon( '', '' )
                        aiBrain:AssignUnitsToPlatoon( plat, {unit}, 'Attack', 'None' )
                        IssueStop( {unit} )
                        IssueClearCommands( {unit} )
                        IssueTransportLoad( {unit}, closest )
                        if EntityCategoryContains( categories.AIRSTAGINGPLATFORM, closest ) and not closest.AirStaging then
                            closest.AirStaging = closest:ForkThread( AirStagingThread )
                            closest.Refueling = {}
                        elseif EntityCategoryContains( categories.CARRIER, closest) and not closest.CarrierStaging then
                            closest.CarrierStaging = closest:ForkThread( CarrierStagingThread )
                            closest.Refueling = {}
                        end
                        table.insert( closest.Refueling, unit )
                        unit.Loading = true
                    end
                end
            end
        end
        WaitSeconds(1)
    end
end

function AirStagingThread(unit)
    local aiBrain = unit:GetAIBrain()
    while not unit:IsDead() do
        local ready = true
        local numUnits = 0
        for k,v in unit.Refueling do
            if not v:IsDead() and ( v:GetFuelRatio() < .9 or v:GetHealthPercent() < .9 ) then
                ready = false
            elseif not v:IsDead() then
                numUnits = numUnits + 1
            end
        end
        if ready and numUnits > 0 then
            #LOG('*AI DEBUG: Release the doves')
            local pos = unit:GetPosition()
            IssueClearCommands( {unit} )
            IssueTransportUnload( {unit}, { pos[1] + 5, pos[2], pos[3] + 5 } )
            WaitSeconds(2)
            for k,v in unit.Refueling do
                if not v:IsDead() then
                    v.Loading = false
                    local plat
                    if not v.PlanName then
                        plat = aiBrain:MakePlatoon( '', 'HuntAI' )
                    else
                        plat = aiBrain:MakePlatoon( '', v.PlanName )
                    end
                    if v.PlatoonData then
                        plat.PlatoonData = {}
                        plat.PlatoonData = v.PlatoonData
                    end
                    aiBrain:AssignUnitsToPlatoon( plat, {v}, 'Attack', 'GrowthFormation' )
                end
            end
        end
        WaitSeconds( 10 )
    end
end

function AirLandToggle(platoon)
    for k,v in platoon:GetPlatoonUnits() do
        if not v:IsDead() and not v.AirLandToggleThread then
            v.AirLandToggleThread = v:ForkThread( AirLandToggleThread )
        end
    end
end

function AirLandToggleThread(unit)
    local aiBrain = unit:GetAIBrain()
    local bp = unit:GetBlueprint()
    local weapons = bp.Weapon
    local antiAirRange
    local landRange
    local toggleWeapons = {}
    local unitCat = ParseEntityCategory( unit:GetUnitId() )
    for k,v in weapons do
        if v.ToggleWeapon then
            local weaponType = 'Land'
            for n,wType in v.FireTargetLayerCapsTable do
                if string.find( wType, 'Air' ) then
                    weaponType = 'Air'
                    break
                end
            end
            if weaponType == 'Land' then
                landRange = v.MaxRadius
            else
                antiAirRange = v.MaxRadius
            end
        end
    end
    if not landRange or not antiAirRange then
        return
    end
    while not unit:IsDead() and unit:IsUnitState('Busy') do
        WaitSeconds(2)
    end
    while not unit:IsDead() do
        local position = unit:GetPosition()
        local numAir = aiBrain:GetNumUnitsAroundPoint( ( categories.MOBILE * categories.AIR ) - unitCat , position, antiAirRange, 'Enemy' )
        local numGround = aiBrain:GetNumUnitsAroundPoint( ( categories.LAND + categories.NAVAL + categories.STRUCTURE ) - unitCat, position, landRange, 'Enemy' )
        local frndAir = aiBrain:GetNumUnitsAroundPoint( ( categories.MOBILE * categories.AIR ) - unitCat, position, antiAirRange, 'Ally' )
        local frndGround = aiBrain:GetNumUnitsAroundPoint( ( categories.LAND + categories.NAVAL + categories.STRUCTURE ) - unitCat, position, landRange, 'Ally' )
        if numAir > 5 and frndAir < 3 then
            unit:SetScriptBit('RULEUTC_WeaponToggle', false)
        elseif numGround > ( numAir * 1.5 ) then
            unit:SetScriptBit('RULEUTC_WeaponToggle', true)
        elseif frndAir > frndGround then
            unit:SetScriptBit('RULEUTC_WeaponToggle', true)
        else
            unit:SetScriptBit('RULEUTC_WeaponToggle', false)
        end
        WaitSeconds(10)
    end
end


#-----------------------------------------------------
#   Table: SurfacePriorities AKA "Your stuff just got wrecked" priority list.
#   Description:
#       Provides a list of target priorities an experimental should use when
#       wrecking stuff or deciding what stuff should be wrecked next.
#-----------------------------------------------------
local SurfacePriorities = { 
    'COMMAND',
    'EXPERIMENTAL ENERGYPRODUCTION STRUCTURE',
    'TECH3 ENERGYPRODUCTION STRUCTURE',
    'TECH2 ENERGYPRODUCTION STRUCTURE',
    'TECH3 MASSEXTRACTION STRUCTURE',
    'TECH3 INTELLIGENCE STRUCTURE',
    'TECH2 INTELLIGENCE STRUCTURE',
    'TECH1 INTELLIGENCE STRUCTURE',
    'TECH3 SHIELD STRUCTURE',
    'TECH2 SHIELD STRUCTURE',
    'TECH2 MASSEXTRACTION STRUCTURE',
    'TECH3 FACTORY LAND STRUCTURE',
    'TECH3 FACTORY AIR STRUCTURE',
    'TECH3 FACTORY NAVAL STRUCTURE',
    'TECH2 FACTORY LAND STRUCTURE',
    'TECH2 FACTORY AIR STRUCTURE',
    'TECH2 FACTORY NAVAL STRUCTURE',
    'TECH1 FACTORY LAND STRUCTURE',
    'TECH1 FACTORY AIR STRUCTURE',
    'TECH1 FACTORY NAVAL STRUCTURE',
    'TECH1 MASSEXTRACTION STRUCTURE',
    'TECH3 STRUCTURE',
    'TECH2 STRUCTURE',
    'TECH1 STRUCTURE',
    'TECH3 MOBILE LAND',
    'TECH2 MOBILE LAND',
    'TECH1 MOBILE LAND',
    'EXPERIMENTAL LAND',
}

#-----------------------------------------------------
#   Function: CommanderOverrideCheck
#   Args:
#       platoon - the single-experimental platoon to run the behavior on
#   Description:
#       Checks if an enemy commander is within range of the unit's main weapon but not currently targetted. 
#       If true, forces weapons to reacquire targets 
#   Returns:  
#       the commander that was found, else nil
#-----------------------------------------------------
CommanderOverrideCheck = function(self)
    local aiBrain = self:GetBrain()
    local experimental = self:GetPlatoonUnits()[1]
    
    local mainWeapon = experimental:GetWeapon(1)
    local weaponRange = mainWeapon:GetBlueprint().MaxRadius
   
    local commanders = aiBrain:GetUnitsAroundPoint(categories.COMMAND, self:GetPlatoonPosition(), weaponRange, 'Enemy')
    
    if table.getn(commanders) == 0 or commanders[1]:IsDead() then
        return false
    end

    local currentTarget = mainWeapon:GetCurrentTarget()
    
    if commanders[1] ~= currentTarget then
        #Commander in range who isn't our current target. Force weapons to reacquire targets so they'll grab him.
        for i=1, experimental:GetWeaponCount() do
            experimental:GetWeapon(i):ResetTarget()
        end
    end
    
    #return the commander so an attack order can be issued or something
    return commanders[1]
end

#-----------------------------------------------------
#   Function: GetExperimentalUnit
#   Args:
#       platoon - the platoon
#   Description:
#       Finds the experiemental unit in the platoon (assumes platoons are only experimentals)
#   Returns:  
#       experimental or nil 
#-----------------------------------------------------
GetExperimentalUnit = function(platoon)
    local unit = nil
    for k,v in platoon:GetPlatoonUnits() do
        if not v:IsDead() then
            unit = v
            break
        end
    end
    return unit
end

#-----------------------------------------------------
#   Function: AssignExperimentalPriorities
#   Args:
#       platoon - the single-experimental platoon to run the behavior on
#   Description:
#       Sets the experimental's land weapon target priorities to the SurfacePriorities table.
#   Returns:  
#       nil 
#-----------------------------------------------------
AssignExperimentalPriorities = function(platoon)
	local experimental = GetExperimentalUnit(platoon)
	if experimental then
    	experimental:SetLandTargetPriorities( SurfacePriorities )
	end
end

#-----------------------------------------------------
#   Function: WreckBase
#   Args:
#       platoon - the single-experimental platoon to run the behavior on
#       scoutLocation - the base to wreck
#   Description:
#       Finds a unit in the base we're currently wrecking.
#   Returns:  
#       Unit to wreck, base. Else nil.
#-----------------------------------------------------
WreckBase = function(self, base)   
    for _, priority in SurfacePriorities do
        local numUnitsAtBase = 0
        local notDeadUnit = false
        local unitsAtBase = self:GetBrain():GetUnitsAroundPoint(ParseEntityCategory(priority), base.Position, 100, 'Enemy')
        
        for _,unit in unitsAtBase do
            if not unit:IsDead() then
                notDeadUnit = unit
                numUnitsAtBase = numUnitsAtBase + 1
            end
        end        
        
        if numUnitsAtBase > 0 then
            return notDeadUnit, base
        end
    end
end

#-----------------------------------------------------
#   Function: FindExperimentalTarget
#   Args:
#       platoon - the single-experimental platoon to run the behavior on
#   Description:
#       Goes through the SurfacePriorities table looking for the enemy base (high priority scouting location. See ScoutingAI in platoon.lua) 
#       with the most number of the highest priority targets.
#   Returns:  
#       target unit, target base, else nil
#-----------------------------------------------------
FindExperimentalTarget = function(self)
    local aiBrain = self:GetBrain()
    local enemyBases = aiBrain.InterestList.HighPriority
   
    if not aiBrain.InterestList or not aiBrain.InterestList.HighPriority then
        #No target. :(
        return
    end
   
    #For each priority in SurfacePriorities list, check against each enemy base we're aware of (through scouting/intel),
    #The base with the most number of the highest-priority targets gets selected. If there's a tie, pick closer. 
    for _, priority in SurfacePriorities do
        local bestBase = false
        local mostUnits = 0
        local bestUnit = false
        
        for _, base in enemyBases do
            local unitsAtBase = aiBrain:GetUnitsAroundPoint(ParseEntityCategory(priority), base.Position, 100, 'Enemy')
            local numUnitsAtBase = 0
            local notDeadUnit = false
            
            for _,unit in unitsAtBase do
                if not unit:IsDead() then
                    notDeadUnit = unit
                    numUnitsAtBase = numUnitsAtBase + 1
                end
            end
            
            if numUnitsAtBase > 0 then
                if numUnitsAtBase > mostUnits then
                    bestBase = base
                    mostUnits = numUnitsAtBase
                    bestUnit = notDeadUnit
                elseif numUnitsAtBase == mostUnits then
                    local myPos = self:GetPlatoonPosition()
                    local dist1 = VDist2(myPos[1], myPos[3], base.Position[1], base.Position[3])
                    local dist2 = VDist2(myPos[1], myPos[3], bestBase.Position[1], bestBase.Position[3])
                    
                    if dist1 < dist2 then
                        bestBase = base
                        bestUnit = notDeadUnit
                    end
                end
            end
        end
        
        if bestBase and bestUnit then
            return bestUnit, bestBase
        end
    end
end

# ==== Indivitual Unit Behaviors ==== #

#-----------------------------------------------------
#   Function: BehemothBehavior
#   Args:
#       self - the single-experimental platoon to run the behavior on
#   Description:
#       Generic experimental AI. Find a base with good stuff to destroy, and go attack it. 
#       If an enemy commander comes within range of the main weapon, attack it.
#   Returns:  
#       nil (function loops until experimental dies)
#-----------------------------------------------------
function BehemothBehavior(self)   
    AssignExperimentalPriorities(self)
    
    local experimental = GetExperimentalUnit(self)
    local targetUnit = false
    local lastBase = false
    local airUnit = EntityCategoryContains(categories.AIR, experimental)
    
    #Find target loop
    while experimental and not experimental:IsDead() do
        if lastBase then
            targetUnit, lastBase = WreckBase(self, lastBase)
        end
        if not lastBase then
            targetUnit, lastBase = FindExperimentalTarget(self)
        end
        
        if targetUnit then
            IssueClearCommands({experimental})
            IssueAttack({experimental}, targetUnit)
        end
        
        #Walk to and kill target loop
        while not experimental:IsDead() and not experimental:IsIdleState() do
            local nearCommander = CommanderOverrideCheck(self)
            
            if nearCommander and nearCommander ~= targetUnit then
                IssueClearCommands({experimental})
                IssueAttack({experimental}, nearCommander)
                targetUnit = nearCommander
            end
            
            #Check if we or the target are under a shield
            local closestBlockingShield = false
            if not airUnit then
                closestBlockingShield = GetClosestShieldProtectingTarget(experimental, experimental) 
            end
            closestBlockingShield = closestBlockingShield or GetClosestShieldProtectingTarget(experimental, targetUnit)
            
            #Kill shields loop
            while closestBlockingShield do
                IssueClearCommands({experimental})
                IssueAttack({experimental}, closestBlockingShield)
                
                #Wait for shield to die loop
                while not closestBlockingShield:IsDead() and not experimental:IsDead() do
                    WaitSeconds(1)
                end             

                closestBlockingShield = false
                if not airUnit then
                    closestBlockingShield = GetClosestShieldProtectingTarget(experimental, experimental) 
                end
                closestBlockingShield = closestBlockingShield or GetClosestShieldProtectingTarget(experimental, targetUnit)
                
                WaitTicks(1)
            end
            
            WaitSeconds(1)
        end
    
        WaitSeconds(1)
    end
end

#-----------------------------------------------------
#   Function: GetShieldRadiusAboveGroundSquared
#   Args:
#       unit - the shield to check the radius of
#   Description:
#       Since a shield can be vertically offset, its blueprint radius is not truly indicative of its
#       protective coverage at ground level. This function gets the square of the actual protective radius of the shield
#   Returns:  
#       The square of the shield's radius at the surface.
#-----------------------------------------------------
function GetShieldRadiusAboveGroundSquared(shield)
    local BP = shield:GetBlueprint().Defense.Shield
    local width = BP.ShieldSize
    local height = BP.ShieldVerticalOffset
    
    return width*width - height*height
end

#-----------------------------------------------------
#   Function: GetClosestShieldProtectingTarget
#   Args:
#       unit - the attacking unit
#       unit - the unit being attacked
#   Description:
#       Gets the closest shield protecting the target unit
#   Returns:  
#       The shield, else false
#-----------------------------------------------------
function GetClosestShieldProtectingTarget(attackingUnit, targetUnit)
    local aiBrain = attackingUnit:GetAIBrain()
    local tPos = targetUnit:GetPosition()
    local aPos = attackingUnit:GetPosition()
    
    local blockingList = {}
    
    #If targetUnit is within the radius of any shields, the shields need to be destroyed.
    local shields = aiBrain:GetUnitsAroundPoint( categories.SHIELD * categories.STRUCTURE, targetUnit:GetPosition(), 50, 'Enemy' )
    for _,shield in shields do
        if not shield:IsDead() then
            local shieldPos = shield:GetPosition()
            local shieldSizeSq = GetShieldRadiusAboveGroundSquared(shield)
            
            if VDist2Sq(tPos[1], tPos[3], shieldPos[1], shieldPos[3]) < shieldSizeSq then
                table.insert(blockingList, shield)
            end
        end
    end

    #return the closest blocking shield
    local closest = false
    local closestDistSq = 999999
    for _,shield in blockingList do
        local shieldPos = shield:GetPosition()
        local distSq = VDist2Sq(aPos[1], aPos[3], shieldPos[1], shieldPos[3])
        
        if distSq < closestDistSq then
            closest = shield
            closestDistSq = distSq
        end
    end
    
    return closest
end



#-----------------------------------------------------
#   Function: FatBoyBehavior
#   Args:
#       self - the single-experimental platoon to run the behavior on
#   Description:
#       Find a base to attack. Sit outside of the base in weapon range and build units.
#   Returns:  
#       nil (function loops until experimental dies)
#-----------------------------------------------------
function FatBoyBehavior(self)   
    local aiBrain = self:GetBrain()
    AssignExperimentalPriorities(self)
    
    local experimental = GetExperimentalUnit(self)
    local targetUnit = false
    local lastBase = false
    local airUnit = EntityCategoryContains(categories.AIR, experimental)
    
    local mainWeapon = experimental:GetWeapon(1)
    local weaponRange = mainWeapon:GetBlueprint().MaxRadius
    
    experimental.Platoons = experimental.Platoons or {}
    
    #Find target loop
    while experimental and not experimental:IsDead() do
        targetUnit, lastBase = FindExperimentalTarget(self)
        
        if targetUnit then
            IssueClearCommands({experimental})
            IssueAttack({experimental}, targetUnit)       
        
            local pos = experimental:GetPosition()
            
            #Wait to get in range
            while VDist2(pos[1], pos[3], lastBase.Position[1], lastBase.Position[3]) > weaponRange + 50
            and not experimental:IsDead() and not experimental:IsIdleState() do
                WaitSeconds(5)
            end
            
            IssueClearCommands({experimental})
            
            #Send our homies to wreck this base
            local goodList = {}
            for _, platoon in experimental.Platoons do
                local platoonUnits = false
                
                if aiBrain:PlatoonExists(platoon) then
                    platoonUnits = platoon:GetPlatoonUnits()
                end
                
                if platoonUnits and table.getn(platoonUnits) > 0 then
                    table.insert(goodList, platoon)
                end
            end
            
            experimental.Platoons = goodList
            
            for _, platoon in goodList do
                platoon:ForkAIThread(FatboyChildBehavior, experimental, lastBase)
            end
            
            #Setup shop outside this guy's base
            while not experimental:IsDead() and WreckBase(self, lastBase) do
                #Build stuff if we haven't hit the unit cap.
                FatBoyBuildCheck(self)
                
                #Once we have 20 units, form them into a platoon and send them to attack the base we're attacking!
                if experimental.NewPlatoon and table.getn(experimental.NewPlatoon:GetPlatoonUnits()) >= 20 then
                    experimental.NewPlatoon:ForkAIThread(FatboyChildBehavior, experimental, lastBase)
                    
                    table.insert(experimental.Platoons, experimental.NewPlatoon)
                    experimental.NewPlatoon = nil
                end
                            
                WaitSeconds(1)
            end
        end
    
        WaitSeconds(1)
    end
end

#-----------------------------------------------------
#   Function: FatBoyBuildCheck
#   Args:
#       self - single-fatboy platoon to build a unit with
#   Description:
#       Builds a random T3 land unit
#   Returns:  
#       nil
#-----------------------------------------------------
function FatBoyBuildCheck(self)
    local aiBrain = self:GetBrain()
    local experimental = GetExperimentalUnit(self)
    
    #Randomly build T3 MMLs, siege bots, and percivals.
    
    local buildUnits = {'uel0303', 'xel0305', 'xel0306',}
    local unitToBuild = buildUnits[Random(1,table.getn(buildUnits))]
        
    aiBrain:BuildUnit( experimental, unitToBuild, 1 )
    
    WaitTicks(1)
    
    local unitBeingBuilt = false
    repeat 
        unitBeingBuilt = unitBeingBuilt or experimental:GetUnitBeingBuilt()
        WaitSeconds(1)
    until experimental:IsDead() or unitBeingBuilt or aiBrain:GetArmyStat("UnitCap_MaxCap", 0.0).Value - aiBrain:GetArmyStat("UnitCap_Current", 0.0).Value < 10
    
    repeat
        WaitSeconds(3)
    until experimental:IsDead() or experimental:IsIdleState() or aiBrain:GetArmyStat("UnitCap_MaxCap", 0.0).Value - aiBrain:GetArmyStat("UnitCap_Current", 0.0).Value < 10

    if not experimental.NewPlatoon or not aiBrain:PlatoonExists(experimental.NewPlatoon) then
        experimental.NewPlatoon = aiBrain:MakePlatoon( '', '' )
    end
    
    if unitBeingBuilt and not unitBeingBuilt:IsDead() then
        aiBrain:AssignUnitsToPlatoon( experimental.NewPlatoon, {unitBeingBuilt}, 'Attack', 'NoFormation' )
        IssueClearCommands({unitBeingBuilt})
        IssueGuard({unitBeingBuilt}, experimental)
    end
end

#-----------------------------------------------------
#   Function: FatboyChildBehavior
#   Args:
#       self - the platoon of fatboy children to run the behavior on
#       parent - the parent fatboy that the child platoon belongs to
#       base - the base to be attacked
#   Description:
#       AI for fatboy child platoons. Wrecks the base that the fatboy has selected.
#       Once the base is wrecked, the units will return to guard the fatboy until a new
#       target base is reached, at which point they will attack it.
#   Returns:  
#       nil
#-----------------------------------------------------
function FatboyChildBehavior(self, parent, base)   
    local aiBrain = self:GetBrain()
    local targetUnit = false
     
    #Find target loop
    while aiBrain:PlatoonExists(self) and table.getn(self:GetPlatoonUnits()) > 0 do

        targetUnit, base = WreckBase(self, base)
        
        local units = self:GetPlatoonUnits()

        if not base then
            #Wrecked base. Kill AI thread
            IssueClearCommands(units)
            IssueGuard(units, parent)
            return
        end
        
        if targetUnit then
            IssueClearCommands(units)
            IssueAggressiveMove(units, targetUnit)
        end
        
        #Walk to and kill target loop
        while aiBrain:PlatoonExists(self) and table.getn(self:GetPlatoonUnits()) > 0 and not targetUnit:IsDead() do           
            WaitSeconds(3)
        end
    
        WaitSeconds(1)
    end
end

TempestBehavior = function(self)
    local aiBrain = self:GetBrain()
    local unit
    for k,v in self:GetPlatoonUnits() do
        if not v:IsDead() then
            unit = v
            break
        end
    end
    if not unit then
        return
    end
    if not EntityCategoryContains( categories.uas0401, unit ) then
        return
    end
    local otherPlat
    unit.BuiltUnitCount = 0
    self.Patrolling = false
    while aiBrain:PlatoonExists(self) and not unit:IsDead() do
        local position = unit:GetPosition()
        local numStrucs = aiBrain:GetNumUnitsAroundPoint( categories.STRUCTURE - ( categories.MASSEXTRACTION + categories.WALL ), position, 65, 'Enemy' )
        local numNaval = aiBrain:GetNumUnitsAroundPoint( ( categories.MOBILE * categories.NAVAL ) * ( categories.EXPERIMENTAL + categories.TECH3 + categories.TECH2 ), position, 65, 'Enemy' )
        while unit.BuiltUnitCount < 8 and ( numStrucs > 5 or numNaval > 10 ) do
            self.Patrolling = false
            self.BreakOff = true
            local numAir = aiBrain:GetNumUnitsAroundPoint( ( categories.MOBILE * categories.AIR ) * ( categories.EXPERIMENTAL + categories.TECH3 ), position, 65, 'Enemy' )
            local numDef = aiBrain:GetNumUnitsAroundPoint( categories.STRUCTURE * ( categories.DEFENSE + categories.STRATEGIC ), position, 75, 'Enemy' )
            local unitToBuild = false
            if numDef > numNaval and numDef > numAir then
                if Random(1,2) == 1 then
                    unitToBuild = 'uas0201'
                else
                    unitToBuild = 'uas0201'
                    #unitToBuild = 'ual0309'
                end
                # Build fatboy protectin
            elseif numNaval > numStrucs and numNaval > numAir then
                unitToBuild = 'uas0203'
                # Build Land killin
            elseif numStrucs > numAir and numStrucs > numLand then
                unitToBuild = 'uas0201'
                # Build struc killin
            else
                unitToBuild = 'uas0202'
                # build air killin
            end
            if unitToBuild then
                IssueStop( {unit} )
                IssueClearCommands( {unit} )
                aiBrain:BuildUnit( unit, unitToBuild, 1 )
            end
            local unitBeingBuilt = false
            local building
            repeat
                WaitSeconds(5)
                unitBeingBuilt = unit:GetUnitBeingBuilt()
                building = false
                for k,v in self:GetPlatoonUnits() do
                    if not v:IsDead() and v:IsUnitState('Building') then
                        building = true
                        break
                    end
                end
            until not building
            if unitBeingBuilt and not unitBeingBuilt:IsDead() then
                local testHeading = false
                testHeading = unit:GetHeading()
                unit.BuiltUnitCount = unit.BuiltUnitCount + 1
                ScenarioFramework.CreateUnitDestroyedTrigger( TempestUnitDeath, unitBeingBuilt )
                aiBrain:AssignUnitsToPlatoon( self, {unitBeingBuilt}, 'Attack', 'GrowthFormation' )
                IssueClearCommands( {unitBeingBuilt} )
                unitBeingBuilt:ForkThread( TempestBuiltUnitMoveOut, position, testHeading )
            end
            self.BreakOff = false
            numStrucs = aiBrain:GetNumUnitsAroundPoint( categories.STRUCTURE - ( categories.MASSEXTRACTION + categories.WALL ), position, 65, 'Enemy' )
            numNaval = aiBrain:GetNumUnitsAroundPoint( ( categories.MOBILE * categories.NAVAL ) * ( categories.EXPERIMENTAL + categories.TECH3 + categories.TECH2 ), position, 65, 'Enemy' )
        end
        if aiBrain:PlatoonExists(self) and self:IsOpponentAIRunning() and not self.Patrolling then
            self:Stop()
            self.Patrolling = true
            scoutPath = AIUtils.AIGetSortedNavalLocations(self:GetBrain())
            for k, v in scoutPath do
                self:Patrol(v)
            end
        end
        WaitSeconds( 5 )
    end
end

function TempestUnitDeath(unit)
    if unit.Tempest and not unit.Tempest:IsDead() and unit.Tempest.BuiltUnitCount then
        unit.Tempest.BuildUnitCount = unit.Tempest.BuildUnitCount - 1
    end
end

function TempestBuiltUnitMoveOut(unit, platoon, position, heading)
    if heading >= 270 or heading <= 90 then
        position =  { position[1], position[2], position[3] + 20 }
    else
        position = { position[1], position[2], position[3] - 20 }
    end
    local counter = 0
    repeat
        IssueClearCommands( {unit} )
        IssueMove( {unit}, position )
        WaitSeconds(5)
        if unit:IsDead() then
            return
        end
        counter = counter + 1
    until counter == 4 or platoon.Patrolling
end


#-----------------------------------------------------
#   Function: CzarBehavior
#   Args:
#       self - the single-experimental platoon to run the behavior on
#   Description:
#       Finds a good base to attack and attacks it.  Prefers to find a commander to kill.
#       Is unique in that it will issue a ground attack and then a move to keep the beam
#       on while moving, instead of attacking specific targets
#   Returns:  
#       nil (function loops until experimental dies)
#-----------------------------------------------------
CzarBehavior = function(self)
    local aiBrain = self:GetBrain()
    local experimental = GetExperimentalUnit(self)
    if not experimental then
        return
    end
    if not EntityCategoryContains( categories.uaa0310, experimental ) then
        return
    end
    
    AssignExperimentalPriorities(self)
    
    local targetUnit, targetBase = FindExperimentalTarget(self)
    local oldTargetUnit = nil
    while not experimental:IsDead() do

        if targetUnit and targetUnit != oldTargetUnit then
            IssueClearCommands({experimental})
            WaitTicks(5)
            IssueAttack({experimental}, experimental:GetPosition())
            WaitTicks(5)
            IssueMove({experimental}, targetUnit:GetPosition())
        end
        
        
        local nearCommander = CommanderOverrideCheck(self)
        local oldCommander = nil
        while nearCommander and not experimental:IsDead() and not experimental:IsIdleState() do
            
            if nearCommander and nearCommander != oldCommander and nearCommander != targetUnit then
                IssueClearCommands({experimental})
                WaitTicks(5)
                IssueAttack({experimental}, experimental:GetPosition())
                WaitTicks(5)
                IssueMove({experimental}, nearCommander:GetPosition())
                targetUnit = nearCommander
            end
            
            WaitSeconds(1)
            oldCommander = nearCommander
            nearCommander = CommanderOverrideCheck(self)
        end
    
        WaitSeconds(1)
        oldTargetUnit = targetUnit
        targetUnit, targetBase = FindExperimentalTarget(self)
    end
end



#-----------------------------------------------------
#   Function: AhwassaBehavior
#   Args:
#       self - the single-experimental platoon to run the behavior on
#   Description:
#       Finds a good base to attack and attacks it.  
#       Is unique in that it will look for a cluster of units to hit with its large AOE bomb.
#   Returns:  
#       nil (function loops until experimental dies)
#-----------------------------------------------------
AhwassaBehavior = function(self)
    local aiBrain = self:GetBrain()
    local experimental = GetExperimentalUnit(self)
    if not experimental then
        return
    end
    if not EntityCategoryContains( categories.xsa0402, experimental ) then
        return
    end
    
    AssignExperimentalPriorities(self)
    local targetLocation = GetHighestThreatClusterLocation(aiBrain, experimental)
    local oldTargetLocation = nil
    while not experimental:IsDead() do

        if targetLocation and targetLocation != oldTargetLocation then
            IssueClearCommands({experimental})
            IssueAttack({experimental}, targetLocation)           
            WaitSeconds(25)
        end
       
        WaitSeconds(1)
        oldTargetLocation = targetLocation
        targetLocation = GetHighestThreatClusterLocation(aiBrain, experimental)
    end
end

#-----------------------------------------------------
#   Function: TickBehavior
#   Args:
#       self - the single-experimental platoon to run the behavior on
#   Description:
#       Finds a good base to attack and attacks it.  
#       Is unique in that it will look for a cluster of units to hit with its gunshipness.
#   Returns:  
#       nil (function loops until experimental dies)
#-----------------------------------------------------
TickBehavior = function(self)
    local aiBrain = self:GetBrain()
    local experimental = GetExperimentalUnit(self)
    if not experimental then
        return
    end
    if not EntityCategoryContains( categories.ura0401, experimental ) then
        return
    end
    
    AssignExperimentalPriorities(self)
    local targetLocation = GetHighestThreatClusterLocation(aiBrain, experimental)
    local oldTargetLocation = nil
    while not experimental:IsDead() do

        if targetLocation and targetLocation != oldTargetLocation then
            IssueClearCommands({experimental})
            IssueAggressiveMove({experimental}, targetLocation)           
            WaitSeconds(25)
        end
       
        WaitSeconds(1)
        oldTargetLocation = targetLocation
        targetLocation = GetHighestThreatClusterLocation(aiBrain, experimental)
    end
end

#-----------------------------------------------------
#   Function: GetHighestThreatClusterLocation
#   Args:
#       aiBrain - aiBrain for experimental
#       experimental - the unit itself
#   Description:
#       Finds the commander first, or a high economic threat that has a lot of units
#       Good for AoE type attacks
#   Returns:  
#       position of best place to attack, nil if nothing found
#-----------------------------------------------------
GetHighestThreatClusterLocation = function(aiBrain, experimental)

    if not aiBrain or not experimental then
        return nil
    end
    
    # look for commander first
    local position = experimental:GetPosition()
    local threatTable = aiBrain:GetThreatsAroundPosition(position, 16, true, 'Commander')
    
    for _,threat in threatTable do
        if threat[3] > 0 then
            local unitsAtLocation = aiBrain:GetUnitsAroundPoint(ParseEntityCategory('COMMAND'), {threat[1],0,threat[2]}, ScenarioInfo.size[1] / 16, 'Enemy')
            local validUnit = false
            for _,unit in unitsAtLocation do
                if not unit:IsDead() then
                    validUnit = unit
                    break
                end
            end
            if validUnit then
                return table.copy(validUnit:GetPosition())
            end         
        end
    end    

    # now look through the bases for the highest economic threat and largest cluster of units
    local enemyBases = aiBrain.InterestList.HighPriority
   
    if not aiBrain.InterestList or not aiBrain.InterestList.HighPriority then
        #No target. :(
        return aiBrain:GetHighestThreatPosition( 0, true, 'Economy' )
    end
    
    local bestBaseThreat = nil
    local maxBaseThreat = 0
    for _,base in enemyBases do
        local threatTable = aiBrain:GetThreatsAroundPosition(base.Position, 1, true, 'Economy')
        
        if table.getn(threatTable) != 0 then
            if threatTable[1][3] > maxBaseThreat then
                maxBaseThreat = threatTable[1][3]
                bestBaseThreat = threatTable
            end
        end
    end
    
    if not bestBaseThreat then
        #no threat
        return
    end
    
    local maxUnits = -1
    local maxThreat = 0
    local bestThreat = 1
    
    # look for a cluster of structures
    for idx, threat in bestBaseThreat do
        if threat[3] > 0 then
            local unitsAtLocation = aiBrain:GetUnitsAroundPoint(ParseEntityCategory('STRUCTURE'), {threat[1],0,threat[2]}, ScenarioInfo.size[1] / 16, 'Enemy')
            local numunits = table.getn(unitsAtLocation)

            if numunits > maxUnits then
                maxUnits = numunits
                bestThreat = idx
            end 
        end
    end
    
    
    if bestBaseThreat[bestThreat] then
        local bestPos = {0,0,0}
        local maxUnits = 0
        local lookAroundTable = {-2,-1,0,1,2}
        local squareRadius = (ScenarioInfo.size[1] / 16) / table.getn(lookAroundTable)
        for ix, offsetX in lookAroundTable do
            for iz, offsetZ in lookAroundTable do
                local unitsAtLocation = aiBrain:GetUnitsAroundPoint(ParseEntityCategory('STRUCTURE'), {bestBaseThreat[bestThreat][1] + offsetX*squareRadius,0,bestBaseThreat[bestThreat][2]+offsetZ*squareRadius}, squareRadius, 'Enemy')
                local numUnits = table.getn(unitsAtLocation)
                if numUnits > maxUnits then
                    maxUnits = numUnits
                    bestPos = table.copy(unitsAtLocation[1]:GetPosition())
                end
            end
        end
        
        if bestPos[1] != 0 and bestPos[3] != 0 then
            return bestPos
        end
    end
   
    
    return nil    

end