mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-08-15 10:47:21 +00:00
3072 lines
128 KiB
Lua
3072 lines
128 KiB
Lua
--- **Functional** - Improve the autonomous behaviour of ground AI.
|
|
--
|
|
-- ===
|
|
--
|
|
-- ## Features:
|
|
--
|
|
-- * Mechanized Infantry Tactics
|
|
-- * Move and attack
|
|
-- * etc
|
|
--
|
|
-- ===
|
|
--
|
|
-- ## Missions:
|
|
--
|
|
-- ## [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS)
|
|
--
|
|
-- ===
|
|
--
|
|
-- Short description
|
|
--
|
|
-- ====
|
|
--
|
|
-- # YouTube Channel
|
|
--
|
|
-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
|
|
--
|
|
-- ===
|
|
--
|
|
-- ### Author: **Statua**
|
|
--
|
|
-- ### Contributions: FlightControl
|
|
--
|
|
-- ===
|
|
--
|
|
-- @module Functional.Tactics
|
|
-- @image Tactics.JPG
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- TACTICS class
|
|
-- @type TACTICS
|
|
-- @field #string ClassName Name of the class.
|
|
-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players.
|
|
-- @field #string lid String for DCS log file.
|
|
-- @extends Core.Fsm#FSM
|
|
--
|
|
|
|
--- Improve the autonomous behaviour of ground AI.
|
|
--
|
|
-- ## Title
|
|
--
|
|
-- Body
|
|
--
|
|
-- ### Subtitle
|
|
--
|
|
-- Sub body
|
|
--
|
|
-- 
|
|
--
|
|
-- Talking about this function/method @{#TACTICS.OnAfterTroopsDropped}()
|
|
--
|
|
-- # Examples
|
|
--
|
|
-- ## Mechanized Infantry
|
|
-- This example shows how to set up a basic mechanized infantry group which will respond to enemy contact by dispersing, deploying troops, and using the troops to fight back.
|
|
--
|
|
-- 
|
|
--
|
|
--
|
|
-- # Customization and Fine Tuning
|
|
-- The following user functions can be used to change the default values
|
|
--
|
|
-- * @{#TACTICS.DefaultTroopAttackDist}() can be used to set the default maximum distance troops will move to attack a target after disembarking
|
|
--
|
|
--
|
|
-- @field #TACTICS
|
|
TACTICS = {}
|
|
TACTICS_UTILS = {}
|
|
TACTICS.ClassName = "TACTICS"
|
|
TACTICS.Debug = false
|
|
TACTICS.lid = nil
|
|
TACTICS_UTILS.GroupsRed = {}
|
|
TACTICS_UTILS.GroupsBlue = {}
|
|
TACTICS_UTILS.SetGroups = nil
|
|
TACTICS_UTILS.SetActive = false
|
|
|
|
--- TACTICS version.
|
|
-- @field #number version
|
|
TACTICS.version="0.1.0"
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--TODO list
|
|
--Check timer cutoffs in deadResponse
|
|
--Check nil for dead groups in all timers
|
|
--Check stuck in movement loop timers
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--[[Event Callbacks]]
|
|
--UnitLost(EventData)
|
|
--GroupDead(EventData)
|
|
|
|
--StartTravel(StartPoint, EndPoint)
|
|
--Redirect(CurrentPoint, EndPoint)
|
|
--EndTravel(ActualPoint, Endpoint)
|
|
|
|
--StartEngaging(WasHit, UnitName, WeaponName, EventData)
|
|
--EngageToIdle
|
|
--EngageToTravel
|
|
--EngageToRearming
|
|
--EngageToWinchester
|
|
|
|
--IsWinchester
|
|
--StartRearming
|
|
--RearmedToIdle
|
|
--RearmedToTravel
|
|
|
|
--StartAttacking
|
|
--AttackToIdle
|
|
--AttackToTravel
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Creates a new object to handle Tactics for a given Wrapper.Group#GROUP object.
|
|
-- @param #TACTICS self
|
|
-- @param Wrapper.Group#GROUP group The GROUP object for which tactics should be applied.
|
|
-- @return #TACTICS self
|
|
function TACTICS:New(group)
|
|
|
|
----------[INITIALIZATION]----------
|
|
TACTICS_UTILS.SetGroups = SET_GROUP:new()
|
|
--Start Set
|
|
if not TACTICS_UTILS.SetActive then
|
|
TACTICS_UTILS.SetActive = true
|
|
TACTICS_UTILS.SetGroups:FilterStart()
|
|
end
|
|
|
|
--Inherit from MOOSE
|
|
local self=BASE:Inherit(self, FSM:New()) -- #TACTICS
|
|
|
|
|
|
--Validate Wrapper.GROUP
|
|
if group:GetCoordinate() ~=nil then
|
|
self.lid=string.format("TACTICS %s | ", tostring(group:GetName()))
|
|
self:T("TACTICS version "..TACTICS.version..": Starting tactics handler for "..group:GetName())
|
|
else
|
|
self:E("TACTICS: Requested group does not exist! (Has to be a MOOSE group that is also alive)")
|
|
return nil
|
|
end
|
|
|
|
|
|
--Warn on non-ground group
|
|
if group:IsGround() == false then
|
|
self:E("TACTICS: WARNING! "..group:GetName().." is not a ground group. Tactics are designed for ground units only and may result in errors or strange behaviour with this group.")
|
|
end
|
|
|
|
|
|
--Establish FSM Capabilities
|
|
self:SetStartState( "Stopped" )
|
|
self:AddTransition( "Stopped", "Start", "Idle" )
|
|
self:AddTransition( "*", "Stop", "Stopped" )
|
|
self:AddTransition( "*", "GroupDead", "Dead" )
|
|
self:AddTransition( "*", "UnitLost", "*" )
|
|
self:AddTransition( "*", "Winchester", "*" )
|
|
self:AddTransition( "*", "FullyRearmed", "*" )
|
|
self:AddTransition( "*", "TargetSpotted", "*" )
|
|
self:AddTransition( "*", "AttackManeuver", "*" )
|
|
self:AddTransition( "*", "TroopsDropped", "*" )
|
|
self:AddTransition( "*", "TroopsReturned", "*" )
|
|
self:AddTransition( "*", "TroopsExtracted", "*" )
|
|
self:AddTransition ("*", "SupportRequested", "*")
|
|
self:AddTransition ("*", "Supporting", "*")
|
|
self:AddTransition ("*", "Abandoned", "*")
|
|
self:AddTransition ("*", "CrewRecovered", "*")
|
|
|
|
|
|
|
|
-- (Travel)
|
|
self:AddTransition( "Idle", "StartTravel", "Travelling" )
|
|
self:AddTransition( "Travelling", "Redirect", "Travelling" )
|
|
self:AddTransition( "Travelling", "EndTravel", "Idle" )
|
|
|
|
-- (Engage)
|
|
self:AddTransition( {"Idle", "Travelling","Attacking","Rearming"}, "StartEngaging", "Engaging" )
|
|
self:AddTransition( "Engaging", "EngageToIdle", "Idle" )
|
|
self:AddTransition( "Engaging", "EngageToTravel", "Travelling" )
|
|
|
|
-- (Rearming)
|
|
self:AddTransition( "*", "RequestRearming", "Rearming" )
|
|
self:AddTransition( "Rearming", "RearmedToIdle", "Idle" )
|
|
self:AddTransition( "Rearming", "RearmedToTravel", "Travelling" )
|
|
|
|
-- (Attacking)
|
|
self:AddTransition( {"Idle","Travelling"}, "StartAttack", "Attacking" )
|
|
self:AddTransition( "Attacking", "AttackToIdle", "Idle" )
|
|
self:AddTransition( "Attacking", "AttackToTravel", "Travelling" )
|
|
self:AddTransition( {"Idle","Travelling"}, "StartHold", "Holding" )
|
|
self:AddTransition( "Holding", "HoldToIdle", "Idle" )
|
|
self:AddTransition( "Holding", "HoldToTravel", "Travelling" )
|
|
self:AddTransition( {"Idle","Travelling"}, "StartAvoid", "Avoiding" )
|
|
self:AddTransition( "Avoiding", "AvoidToIdle", "Idle" )
|
|
self:AddTransition( "Avoiding", "AvoidToTravel", "Travelling" )
|
|
|
|
-- (Retreating)
|
|
self:AddTransition( "*", "StartRetreat", "Retreating" )
|
|
self:AddTransition( "Retreating", "EndRetreat", "Idle" )
|
|
|
|
|
|
--Initialize Group Data
|
|
self.TickRate = 1
|
|
self.Group = group --The Wrapper.Group#GROUP object of the group.
|
|
self.Groupname = group:GetName() --The DCS GroupName of the Wrapper.Group#GROUP object
|
|
self.groupCoalition = self.Group:GetCoalition()
|
|
self.enemyCoalition = coalition.side.RED
|
|
self.groupCoalitionString = "blue"
|
|
self.enemyCoalitionString = "red"
|
|
if self.groupCoalition == coalition.side.RED then
|
|
self.enemyCoalition = coalition.side.BLUE
|
|
self.groupCoalitionString = "red"
|
|
self.enemyCoalitionString = "blue"
|
|
end
|
|
self.MessageOutput = false
|
|
self.MessageDuration = 10
|
|
self.MessageSound = "squelch2.ogg"
|
|
self.MessageCallsign = "ANONYMOUS"
|
|
self.Sleeping = false
|
|
|
|
|
|
--(Group Units)
|
|
self.UnitTable = self.Group:GetUnits() --A table containing all of the Wrapper.Unit#UNIT objects in the group with nested data.
|
|
for i = 1, #self.UnitTable do
|
|
self.UnitTable[i].UnitName = self.UnitTable[i]:GetName() --Name of the Wrapper.Unit#UNIT object
|
|
self.UnitTable[i].TroopTransport = false --Is the unit a troop transport?
|
|
self.UnitTable[i].TroopsBoarded = false --Are any troops on board?
|
|
self.UnitTable[i].TroopGroup = nil --The Wrapper.Group#GROUP object of deployed troops when dropped
|
|
self.UnitTable[i].TroopsAlive = nil --Number of troops alive from the original template
|
|
end
|
|
|
|
--(Ammunition/Rearming)
|
|
self.groupAmmo = {}
|
|
self.LowAmmoPercent = 0.25
|
|
self.FullAmmoPercent = 0.95
|
|
self.IsWinchester = false
|
|
self.IsRearming = false
|
|
self.RearmTemplate = nil
|
|
self.RearmSpawnCoord = nil
|
|
self.AllowRearming = false
|
|
self.DespawnRearmingGroup = false
|
|
self.AllowRearmRespawn = true
|
|
self.RearmGroup = nil
|
|
self.RearmGroupBase = nil
|
|
self.RearmGroupRTB = true
|
|
self.RearmGroupUseRoads = true
|
|
self.RearmGroupSpeed = 60
|
|
self.RearmGroupFormation = "Cone"
|
|
self.RearmDrawing = false
|
|
self.DrawDataR = {}
|
|
|
|
--(Troop Transport)
|
|
self.TroopTransport = false --Set to true to enable troop transport. Additional options required
|
|
self.TransportUnitPrefix = nil --Provide a #string to identify specific units that will carry troops. Leave as nil to enable troop transport for all units of the group
|
|
self.TroopTemplate = nil --A late activated Wrapper.Group#GROUP object to use as the template for troop deployment
|
|
self.TroopAttackDist = 1000 --How far troops deployed will move to attack the closest enemy (also affects when an attacking group stops to deploy troops)
|
|
self.TroopDismountDistMin = 100 -- If engaging beyond of TroopAttackDist, how far will troops move away from the carriers when disembarking (min random value)
|
|
self.TroopDismountDistMax = 300 -- If engaging beyond of TroopAttackDist, how far will troops move away from the carriers when disembarking (max random value)
|
|
self.TroopFormation = "Diamond" -- Formation the troops will move in when attacking
|
|
self.TroopMoveSpeed = 24
|
|
self.TroopsAttacking = false
|
|
self.DeployedTroops = {}
|
|
self.AttackingTroops = {}
|
|
self.ExtractTimeLimit = 300
|
|
|
|
--(Travel)
|
|
self.Destination = nil --The last provided destination in the TravelTo call.
|
|
self.DestinationRate = 30 --How often in seconds to check up on the traveling group and see if its at its destination or is stuck
|
|
self.DestinationRadius = 100 --How close the group needs to be to its destination to consider it as arrived
|
|
self.FixStuck = true --Allow the group to make short movements in different directions if they've stopped before reaching their destination without switching states
|
|
self.UnstuckDist = 100 --How far to place a waypoint for the group to try and get it unstuck
|
|
self.MaxUnstuckAttempts = 10 --How many attempts to make to get unstuck before it stops trying. Returns state to Idle if DestroyStuck is false
|
|
self.DestroyStuck = false --Set to true to remove the group from the game if MaxUnstuckAttempts has been reached.
|
|
self.StuckHeadingIncrements = 60
|
|
self.InclineLimit = 35
|
|
|
|
self.travelToStored = nil
|
|
self.stuckPosit = nil
|
|
self.lastStuckHeading = nil
|
|
self.countStuck = 0
|
|
|
|
--(Detection)
|
|
self.UseDetection = true
|
|
self.DetectionRate = 30
|
|
self.TacticalROE = "Attack" --Options = "Attack", "Avoid", "Hold", "Ignore"
|
|
self.UseLOS = true
|
|
self.UseDetectionChance = true
|
|
self.MaxDetection = 6000
|
|
self.FullDetection = 500
|
|
self.FilterDetectionZones = nil
|
|
self.FilterEnemyType = nil --Options = "Helicopter", "Airplane", "Ground Unit", "Ship", "Train"
|
|
self.ManageAlarmState = true
|
|
self.DefaultAlarmState = "Auto"
|
|
self.ClosestEnemy = nil
|
|
self.DrawEnemySpots = true
|
|
self.EnemySpotStale = 120
|
|
self.markSpot = nil
|
|
self.markSpotTimer = nil
|
|
self.zoneDetection = ZONE_RADIUS:New("DetZone_"..self.Groupname, self.Group:GetVec2(), self.MaxDetection, true)
|
|
|
|
--(Attack)
|
|
self.CombatDistance = 2000
|
|
self.AttackFarUsesRoads = false
|
|
self.AttackPositionDist = 200
|
|
self.attackLastCoord = nil
|
|
self.AttackSpeed = 20
|
|
self.AttackSpeedFar = 60
|
|
self.AttackFormationFar = "Diamond"
|
|
self.AttackFormation = "Rank"
|
|
self.AttackTimeout = 300
|
|
self.LastTargetThreshold = 25
|
|
|
|
self.HoldingTime = 120
|
|
self.HoldDistance = 4000
|
|
|
|
self.AvoidDistance = 2000
|
|
self.AvoidAzimuthMax = 30
|
|
self.AvoidUseRoads = false
|
|
self.AvoidSpeed = 60
|
|
self.AvoidFormation = 'Cone'
|
|
self.avoidDest = nil
|
|
self.AvoidRate = 30
|
|
|
|
--(Engagement)
|
|
self.ReactToHits = true
|
|
self.ReactAfterShooting = true
|
|
self.EvadeDistance = 25
|
|
self.EngageCooldown = 120
|
|
self.DisperseOnShoot = true
|
|
self.DisperseOnHit = true
|
|
self.DrawDataE = {}
|
|
self.EngageDrawing = false
|
|
self.EngageDrawingFresh = 120
|
|
self.EngageDrawingStale = 300
|
|
|
|
--(Retreating)
|
|
self.RetreatAfterLosses = nil
|
|
self.RetreatZone = nil
|
|
self.RetreatSpeed = 60
|
|
self.RetreatOnRoads = true
|
|
self.RetreatFormation = "Diamond"
|
|
self.DespawnAfterRetreat = false
|
|
self.retreatDestination = nil
|
|
|
|
--(SUPPORT REQUEST)
|
|
self.AllowSupportRequests = true
|
|
self.SupportRadius = 4000
|
|
self.SupportGroupLimit = 3
|
|
self.RespondToSupport = true
|
|
self.SupportLevel = 3 --1 = On Spotted, 2 = On Engage, 3 = On Hit, 4 = On Unit Loss
|
|
self.supportTable = {}
|
|
self.SupportCooldown = 300
|
|
|
|
--(ABANDON VEHICLE)
|
|
self.AbandonEnabled = false
|
|
self.CrewTemplate = nil
|
|
self.AbandonHealth = 0.5
|
|
self.AbandonDistance = 1000
|
|
self.AllowSelfRecover = true
|
|
self.RecoveryVehicle = nil
|
|
self.RecoverySpawnZone = nil
|
|
self.RecoveryUseRoads = true
|
|
self.RecoverySpeed = 60
|
|
self.RecoveryFormation = "Diamond"
|
|
self.SmokeAbandoned = false
|
|
self.StaticSmokeTimeout = 300
|
|
|
|
--Event Handling
|
|
self.Group:HandleEvent(EVENTS.Hit)
|
|
self.Group:HandleEvent(EVENTS.ShootingStart)
|
|
self.Group:HandleEvent(EVENTS.Shot)
|
|
self.Group:HandleEvent(EVENTS.Dead)
|
|
|
|
local function handleContact(type, EventData)
|
|
if not self.Sleeping then
|
|
if not self:Is("Retreating") then
|
|
if type == "Hit" then
|
|
if self.ReactToHits then
|
|
if not self:Is("Engaging") then self:_InitEngagement(true,EventData) end
|
|
self.TimeEngageActive = true
|
|
self.TimeEngageStamp = timer.getAbsTime()
|
|
if self.SupportLevel <= 3 and not self.TimeSupportActive and self.AllowSupportRequests then
|
|
self:RequestSupport(EventData.IniUnit)
|
|
end
|
|
end
|
|
end
|
|
if type == "Shooting" then
|
|
if self.ReactAfterShooting then
|
|
if not self:Is("Engaging") then self:_InitEngagement(false,EventData) end
|
|
self.TimeEngageActive = true
|
|
self.TimeEngageStamp = timer.getAbsTime()
|
|
if self.SupportLevel <= 2 and not self.TimeSupportActive and self.AllowSupportRequests then
|
|
self:RequestSupport(EventData.TgtUnit)
|
|
end
|
|
end
|
|
end
|
|
if type == "Shot" then
|
|
if self.ReactAfterShooting then
|
|
if not self:Is("Engaging") then self:_InitEngagement(false,EventData) end
|
|
self.TimeEngageActive = true
|
|
self.TimeEngageStamp = timer.getAbsTime()
|
|
if self.SupportLevel <= 2 and not self.TimeSupportActive and self.AllowSupportRequests then
|
|
self:RequestSupport(EventData.TgtUnit)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if type == "Dead" then
|
|
self:_DeadResponse(EventData)
|
|
end
|
|
end
|
|
|
|
function self.Group:OnEventHit(EventData)
|
|
handleContact("Hit", EventData)
|
|
end
|
|
|
|
function self.Group:OnEventShot(EventData)
|
|
handleContact("Shot", EventData)
|
|
end
|
|
|
|
function self.Group:OnEventShootingStart(EventData)
|
|
handleContact("Shooting", EventData)
|
|
end
|
|
|
|
function self.Group:OnEventDead(EventData)
|
|
handleContact("Dead", EventData)
|
|
end
|
|
|
|
|
|
|
|
--Build group ammunition table if able
|
|
for i = 1, #self.UnitTable do
|
|
local tableAmmo = self.UnitTable[i]:GetAmmo()
|
|
if tableAmmo then
|
|
for a = 1, #tableAmmo do
|
|
self:T( self.UnitTable[i].UnitName .. " has " .. tableAmmo[a]["count"] .. " " .. tableAmmo[a]["desc"]["typeName"])
|
|
end
|
|
table.insert(self.groupAmmo, tableAmmo)
|
|
else
|
|
self:T( self.UnitTable[i].UnitName .. " does not use ammunition.")
|
|
table.insert(self.groupAmmo, nil)
|
|
end
|
|
end
|
|
|
|
--Set Group Initialization
|
|
-- TIMER:New(function()
|
|
-- self.setEnemy = SET_UNIT:New():FilterCoalitions(self.enemyCoalitionString):FilterActive()
|
|
-- if self.FilterEnemyType then self.setEnemy:FilterCategories(self.FilterEnemyType) end
|
|
-- if self.FilterDetectionZones then self.setEnemy:FilterZones(self.FilterDetectionZones) end
|
|
-- self.setEnemy:FilterStart()
|
|
-- end):Start(1)
|
|
|
|
|
|
|
|
|
|
----------[TIMER BASED FUNCTIONS]----------
|
|
|
|
|
|
--Master Time Tracking
|
|
local initialTime = timer.getAbsTime()
|
|
self.TimeTravelActive = false
|
|
self.TimeTravelStamp = initialTime
|
|
self.TimeDetectionActive = false
|
|
self.TimeDetectionStamp = initialTime
|
|
self.TimeRearmActive = false
|
|
self.TimeRearmStamp = initialTime
|
|
self.TimeRetreatActive = false
|
|
self.TimeRetreatStamp = initialTime
|
|
self.TimeAvoidActive = false
|
|
self.TimeAvoidStamp = initialTime
|
|
|
|
self.TimeEngageActive = false
|
|
self.TimeEngageStamp = initialTime
|
|
self.TimeHoldActive = false
|
|
self.TimeHoldStamp = initialTime
|
|
self.TimeAttackActive = false
|
|
self.TimeAttackStamp = initialTime
|
|
self.TimeSupportActive = false
|
|
self.TimeSupportStamp = initialTime
|
|
|
|
|
|
--Master Timer
|
|
self.MasterTime = TIMER:New(function()
|
|
local timeNow = timer.getAbsTime()
|
|
|
|
----{LOOPS}----
|
|
--Travel Tracker
|
|
if self.TimeTravelActive then
|
|
if timeNow >= self.TimeTravelStamp + self.DestinationRate then
|
|
self.TimeTravelStamp = timer.getAbsTime()
|
|
self:_TravelCheck()
|
|
end
|
|
end
|
|
|
|
--Detection
|
|
if self.TimeDetectionActive then
|
|
if timeNow >= self.TimeDetectionStamp + self.DetectionRate then
|
|
self.TimeDetectionStamp = timer.getAbsTime()
|
|
self:_DetectionCycle()
|
|
end
|
|
end
|
|
|
|
--Avoid
|
|
if self.TimeAvoidActive then
|
|
if timeNow >= self.TimeAvoidStamp + self.AvoidRate then
|
|
self.TimeAvoidStamp = timer.getAbsTime()
|
|
self:_AvoidTimer()
|
|
end
|
|
end
|
|
|
|
--Rearmed
|
|
if self.TimeRearmActive then
|
|
if timeNow >= self.TimeRearmStamp + 60 then
|
|
self.TimeRearmStamp = timer.getAbsTime()
|
|
self:_RearmCheckTimer()
|
|
end
|
|
end
|
|
|
|
--Retreat
|
|
if self.TimeRetreatActive then
|
|
if timeNow >= self.TimeRetreatStamp + 60 then
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
self:_RetreatTimer()
|
|
end
|
|
end
|
|
|
|
|
|
----{COOLDOWNS}----
|
|
--Engagement
|
|
if self.TimeEngageActive then
|
|
if timeNow >= self.TimeEngageStamp + self.EngageCooldown then
|
|
self.TimeEngageActive = false
|
|
self:_EngageCooldown()
|
|
end
|
|
end
|
|
|
|
--Hold
|
|
if self.TimeHoldActive then
|
|
if timeNow >= self.TimeHoldStamp + self.HoldingTime then
|
|
self.TimeHoldActive = false
|
|
self:_HoldTimer()
|
|
end
|
|
end
|
|
|
|
--Attack
|
|
if self.TimeAttackActive then
|
|
if timeNow >= self.TimeAttackStamp + self.AttackTimeout then
|
|
self.TimeAttackActive = false
|
|
self:_AttackTimer()
|
|
end
|
|
end
|
|
|
|
--Support
|
|
if self.TimeSupportActive then
|
|
if timeNow >= self.TimeSupportStamp + self.SupportCooldown then
|
|
self.TimeSupportActive = false
|
|
end
|
|
end
|
|
end)
|
|
|
|
|
|
----------[USER FUNCTIONS]----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
--Constructor related stuff
|
|
if self.groupCoalition == coalition.side.RED then
|
|
table.insert(TACTICS_UTILS.GroupsRed, self)
|
|
else
|
|
table.insert(TACTICS_UTILS.GroupsBlue, self)
|
|
end
|
|
|
|
self:__Start(1)
|
|
return self
|
|
end
|
|
|
|
|
|
|
|
|
|
--------[INTERNAL FSM CALLBACKS]---------
|
|
|
|
--- [Internal] FSM Function onafterStart
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStart(From,Event,To)
|
|
self:T({From, Event, To})
|
|
self.MasterTime:Start(self.TickRate,self.TickRate)
|
|
if self.UseDetection == true then self.TimeDetectionActive = true end
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is now active and available for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterStop
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStop(From,Event,To)
|
|
self:T({From, Event, To})
|
|
self.MasterTime:Stop()
|
|
return self
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterStartTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStartTravel(From,Event,To,CoordGroup,CoordDest)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is initiating travel to our assigned destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterRedirect
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterRedirect(From,Event,To,CoordGroup,CoordDest)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." received. Redirecting to the new destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterEndTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterEndTravel(From,Event,To,GroupCoord,DestCoord)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." has arrived at our destination. Ready for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterStartAvoid
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStartAvoid(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is avoiding the target.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterAvoidToTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterAvoidToTravel(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer avoiding the target. Resuming travel to destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterAvoidToIdle
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterAvoidToIdle(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer avoiding the target. Ready for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterEngageToTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterEngageToTravel(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer engaged with the enemy. Resuming travel to destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterEngageToIdle
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterEngageToIdle(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer engaged with the enemy. Ready for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterHoldToTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterHoldToTravel(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer holding from the enemy contact. Resuming travel to destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterHoldToIdle
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterHoldToIdle(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer holding from the enemy contact. Ready for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterStartAttack
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStartAttack(From,Event,To,_enemy,distance,hdgEnemy)
|
|
if self.MessageOutput then
|
|
local tgtType = "enemy target"
|
|
if _enemy:GetClassName() and not _enemy:GetClassName() == "COORDINATE" then tgtType = _enemy:GetTypeName() end
|
|
MESSAGE:New(self.MessageCallsign.." is moving to attack a "..tostring(tgtType).." at "..math.floor(hdgEnemy).." for "..math.floor(distance).."m.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterAttackToTravel
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterAttackToTravel(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer attacking the enemy. Resuming travel to destination.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterAttackToIdle
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterAttackToIdle(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer attacking the enemy. Ready for tasking.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterStartEngaging
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterStartEngaging(From,Event,To,WasHit,UnitName,WeaponName,EventData)
|
|
if self.MessageOutput then
|
|
if WasHit then
|
|
if EventData.IniTypeName then
|
|
MESSAGE:New(self.MessageCallsign.." is engaged with and receiving fire from a "..tostring(EventData.IniTypeName).."!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
else
|
|
MESSAGE:New(self.MessageCallsign.." is engaged with and receiving fire from the enemy!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
end
|
|
else
|
|
if EventData.TgtTypeName then
|
|
MESSAGE:New(self.MessageCallsign.." is engaged with and attacking a "..tostring(EventData.TgtTypeName).."!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
else
|
|
MESSAGE:New(self.MessageCallsign.." is engaged with and attacking the enemy!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterRetreat
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterRetreat(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." has taken significant losses and is retreating!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterEndRetreat
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterEndRetreat(From,Event,To,CoordGroup,CoordRetreat)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is no longer retreating.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterGroupDead
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterGroupDead(From,Event,To,EventData)
|
|
if self.MessageOutput and EventData then
|
|
MESSAGE:New(self.MessageCallsign.." is KIA!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterWinchester
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterWinchester(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is winchester!",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterRequestRearming
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterRequestRearming(From,Event,To,GroupRearm)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." requesting rearming.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterFullyRearmed
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterFullyRearmed(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." is fully rearmed.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterTroopsDropped
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterTroopsDropped(From,Event,To,TargetCoord,IsAttacking)
|
|
if self.MessageOutput then
|
|
local attackMessage = " to attack a target"
|
|
if not IsAttacking then attackMessage = "" end
|
|
MESSAGE:New(self.MessageCallsign.."'s infantry are now disembarked"..attackMessage..".",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterTroopsReturned
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterTroopsReturned(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." has embarked all of our troops.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterTroopsExtracted
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterTroopsExtracted(From,Event,To)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." has extracted troops.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterSupportRequested
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterSupportRequested(From,Event,To,CoordTarget,UnitTarget)
|
|
if self.MessageOutput then
|
|
local addMessage = ""
|
|
if UnitTarget then addMessage = " Our target is a "..tostring(UnitTarget:GetTypeName()).."." end
|
|
MESSAGE:New(self.MessageCallsign.." requesting support!"..addMessage,self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterSupporting
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterSupporting(From,Event,To,Group,CoordTarget,Callsign)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." will provide support for "..tostring(Callsign)..".",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterAbandoned
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterAbandoned(From,Event,To,UnitCoord,UnitName,UnitType)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." has abandoned a "..UnitType.." due to excessive damage.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
--- [Internal] FSM Function onafterCrewRecovered
|
|
-- @param #TACTICS self
|
|
-- @param #string From
|
|
-- @param #string Event
|
|
-- @param #string To
|
|
-- @return #TACTICS self
|
|
function TACTICS:onafterCrewRecovered(From,Event,To,RecoveryUnit,RecoveryName)
|
|
if self.MessageOutput then
|
|
MESSAGE:New(self.MessageCallsign.." the abandoning crew has been recovered by another vehicle.",self.MessageDuration):ToCoalition(self.groupCoalition)
|
|
USERSOUND:New(self.MessageSound):ToCoalition(self.groupCoalition)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
--------[TIMER METHODS]---------
|
|
|
|
---LOOP: Travel Tracker
|
|
--- [Internal] Travek Chek
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_TravelCheck()
|
|
if self:Is("Travelling") then
|
|
self:T2("TACTICS: "..self.Groupname.." travel check timer.")
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if not coordGroup then coordGroup = self.Group:GetCoordinate() end
|
|
if not coordGroup then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
elseif self.Destination then
|
|
local distGroup = coordGroup:Get3DDistance(self.Destination)
|
|
local speedGroup = self.Group:GetVelocityKMH()
|
|
|
|
--Arrived at destination
|
|
if distGroup <= self.DestinationRadius then
|
|
self.Group:RouteStop()
|
|
self:T("TACTICS: "..self.Groupname.." arrived at their destination.")
|
|
self.TimeTravelActive = false
|
|
self.travelToStored = nil
|
|
self:EndTravel(self.Group:GetAverageCoordinate(), self.Destination)
|
|
self.Destination = nil
|
|
elseif speedGroup < 1 and self.FixStuck or self.countStuck > 0 then -- Attempt to get unstuck if it's stopped before arriving at the destination.
|
|
self.countStuck = self.countStuck + 1
|
|
self:T("TACTICS: "..self.Groupname.." is stopped when it should be moving. Check #"..self.countStuck)
|
|
if self.countStuck == 1 then -- Initial unstuck check
|
|
self.stuckPosit = self.Group:GetAverageCoordinate()
|
|
elseif self.countStuck > 1 and self.countStuck < self.MaxUnstuckAttempts then
|
|
local distStuck = coordGroup:Get3DDistance(self.stuckPosit)
|
|
if distStuck <= self.UnstuckDist/4 then -- Attempt to get unstuck
|
|
if self.lastStuckHeading == nil then
|
|
self.lastStuckHeading = (self.stuckPosit:HeadingTo(self.Destination) + self.StuckHeadingIncrements) % 360
|
|
else
|
|
self.lastStuckHeading = (self.lastStuckHeading + self.StuckHeadingIncrements) % 360
|
|
end
|
|
local unstuckDest = coordGroup:Translate(self.UnstuckDist,self.lastStuckHeading,false,false)
|
|
self.Group:RouteGroundTo(unstuckDest, 10, 'Cone', 1)
|
|
self:T("TACTICS: "..self.Groupname.." is attempting to get unstuck by driving "..self.UnstuckDist .."m toward heading "..self.lastStuckHeading)
|
|
else -- Group has moved enough to resume the route
|
|
self:T("TACTICS: "..self.Groupname.." has gotten unstuck. Resuming on the path...")
|
|
self.countStuck = 0
|
|
self.stuckPosit = nil
|
|
self.TimeTravelActive = false
|
|
self:_InitiateTravel()
|
|
end
|
|
elseif self.countStuck >= self.MaxUnstuckAttempts then -- Max stuck attempts exceeded
|
|
if self.DestroyStuck then --EITHER Destroy the group
|
|
self:E("TACTICS: "..self.Groupname.." has been stuck for too long and was removed from the game...")
|
|
self.Group:Destroy()
|
|
self:_DeadResponse(false)
|
|
else --OR Return the group to Idle
|
|
self:E("TACTICS: "..self.Groupname.." has been stuck for too long and was returned to the idle state...")
|
|
self.Group:RouteStop()
|
|
self.TimeTravelActive = false
|
|
self.travelToStored = nil
|
|
self:EndTravel(self.Group:GetAverageCoordinate(), self.Destination)
|
|
self.Destination = nil
|
|
end
|
|
end
|
|
else
|
|
self.countStuck = 0
|
|
self.stuckPosit = nil
|
|
end
|
|
else
|
|
self:E("TACTCS ERROR: "..self.Groupname.." attempted to compare distance to travel destination but 'self.Destination' is a nil value!")
|
|
end
|
|
else
|
|
self.TimeTravelActive = false
|
|
self.stuckPosit = nil
|
|
self.lastStuckHeading = nil
|
|
self.countStuck = 0
|
|
end
|
|
return self
|
|
end
|
|
|
|
|
|
--- [Internal] Detection Cycle
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_DetectionCycle()
|
|
if not self:Is("Engaging") then
|
|
self:T2("TACTICS: ---- Detection Cycle ---- - [GRP: "..self.Groupname.."]")
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if not coordGroup then coordGroup = self.Group:GetCoordinate() end
|
|
if not coordGroup then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
end
|
|
if coordGroup then
|
|
--Try to detect an enemy.
|
|
local distClosestEnemy = 9999999
|
|
local newSpot = false
|
|
local hdgClosestEnemy = 0
|
|
self.zoneDetection:SetVec2(coordGroup:GetVec2())
|
|
|
|
TACTICS_UTILS.SetGroups:ForEachGroupAnyInZone(self.zoneDetection, function(_setgroup)
|
|
local coordEnemy = _setgroup:GetCoordinate()
|
|
local validUnit = true
|
|
if not coordEnemy then
|
|
validUnit = false
|
|
elseif _setgroup:GetCoalition() == self.groupCoalition then
|
|
validUnit = false
|
|
elseif self.FilterEnemyType and not _setgroup:GetCategoryName() == self.FilterEnemyType then
|
|
validUnit = false
|
|
elseif self.FilterDetectionZones and type(self.FilterDetectionZones) == "table" then
|
|
local inZone = false
|
|
for i = 1, #self.FilterDetectionZones do
|
|
if _setgroup:IsAnyInZone(self.FilterDetectionZones[i]) then
|
|
inZone = true
|
|
break
|
|
end
|
|
end
|
|
validUnit = inZone
|
|
end
|
|
|
|
if validUnit then
|
|
local tableEnemyUnits = _setgroup:GetUnits()
|
|
for i = 1,#tableEnemyUnits do
|
|
local _enemy = tableEnemyUnits[i]
|
|
if _enemy:IsInZone(self.zoneDetection) then
|
|
local speedEnemy = _enemy:GetVelocityKMH()
|
|
local distEnemy = coordGroup:Get3DDistance(coordEnemy)
|
|
self:T2("TACTICS: Checking "..tostring(_enemy:GetName()).." at a range of "..tostring(distEnemy).."m / "..tostring(self.MaxDetection).."m")
|
|
if distEnemy < self.MaxDetection then
|
|
local checkLOS = true
|
|
if self.UseLOS then checkLOS = coordGroup:IsLOS(coordEnemy) end
|
|
self:T2("TACTICS: "..tostring(_enemy:GetName()).." is LOS = "..tostring(checkLOS))
|
|
if distEnemy < self.FullDetection and distEnemy < distClosestEnemy then
|
|
self:T2("TACTICS: "..tostring(_enemy:GetName()).." is the closest target and is within Full Detection range ("..tostring(self.FullDetection).."m)")
|
|
distClosestEnemy = distEnemy
|
|
hdgClosestEnemy = coordGroup:HeadingTo(coordEnemy)
|
|
self.ClosestEnemy = _enemy
|
|
newSpot = true
|
|
elseif checkLOS then
|
|
local distVarChance = 2
|
|
if self.UseDetectionChance then distVarChance = (self.MaxDetection - distEnemy)/self.MaxDetection end
|
|
local checkChance = math.random(1,100)
|
|
if self.FilterEnemyType == "ground" then
|
|
if speedEnemy > 5 and speedEnemy < 60 then checkChance = checkChance / 2 end
|
|
end
|
|
if self.FilterEnemyType == "helicopter" and self.UseDetectionChance or self.FilterEnemyType == "airplane" and self.UseDetectionChance then
|
|
local altVarChance = 0.05
|
|
local altHelo = _enemy:GetAltitude()
|
|
local altGroup = _grp:GetAltitude()
|
|
local altDiff = altHelo - altGroup
|
|
local degHelo = 0
|
|
if altDiff > 0 then
|
|
local angHelo = math.asin(altDiff/distEnemy)
|
|
degHelo = math.deg(angHelo)
|
|
if degHelo > 15 then altVarChance = 1.0 elseif degHelo > 5 then altVarChance = (degHelo/15) end
|
|
end
|
|
distVarChance = distVarChance + altVarChance
|
|
end
|
|
|
|
self:T2("TACTICS: Detection roll: Is "..tostring(checkChance).." <= "..tostring((distVarChance/2)*100).."?")
|
|
if checkChance <= (distVarChance/2)*100 and distEnemy < distClosestEnemy then
|
|
self:T2("TACTICS: "..tostring(_enemy:GetName()).." was detected by "..self.Groupname.." and is the closest target")
|
|
distClosestEnemy = distEnemy
|
|
hdgClosestEnemy = coordGroup:HeadingTo(coordEnemy)
|
|
self.ClosestEnemy = _enemy
|
|
newSpot = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
if newSpot then
|
|
self:_SpotTarget(self.ClosestEnemy,distClosestEnemy)
|
|
self:TargetSpotted(self.ClosestEnemy,distClosestEnemy,hdgClosestEnemy)
|
|
if self.DrawEnemySpots then
|
|
self:_ContactMark(self.ClosestEnemy:GetCoordinate(),2)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] Avoid Timer Function
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_AvoidTimer()
|
|
self:T("TACTICS: >> AVOID TIMER << - [GRP: "..self.Groupname.."]")
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
else
|
|
if self:Is("Engaging") then
|
|
self.TimeAvoidActive = false
|
|
else
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if not coordGroup then coordGroup = self.Group:GetCoordinate() end
|
|
local distGroup = coordGroup:Get3DDistance(self.avoidDest)
|
|
if distGroup <= self.DestinationRadius then
|
|
self:I("TACTICS: "..self.Groupname.." is no longer avoiding the threat.")
|
|
if self.ManageAlarmState then
|
|
if self.DefaultAlarmState == "Auto" then
|
|
self.Group:OptionAlarmStateAuto()
|
|
elseif self.DefaultAlarmState == "Red" then
|
|
self.Group:OptionAlarmStateRed()
|
|
elseif self.DefaultAlarmState == "Green" then
|
|
self.Group:OptionAlarmStateGreen()
|
|
end
|
|
end
|
|
if self.travelToStored then
|
|
self:AvoidToTravel()
|
|
self:_InitiateTravel()
|
|
else
|
|
self.Group:RouteStop()
|
|
self:AvoidToIdle()
|
|
end
|
|
self.TimeAvoidActive = false
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] Rearm check timer
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_RearmCheckTimer()
|
|
self:T("TACTICS: >> REARM CHECK TIMER << - [GRP: "..self.Groupname.."]")
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
else
|
|
--Finish up if full
|
|
local isWinchester, isFull = self:CheckAmmo()
|
|
if isFull or self:Is("Retreating") then
|
|
if not self:Is("Retreating") then
|
|
self:I("TACTICS: "..self.Groupname.." is fully rearmed.")
|
|
else
|
|
self:I("TACTICS: "..self.Groupname.." interrupted rearming to retreat.")
|
|
end
|
|
self.TimeRearmActive = false
|
|
self.IsWinchester = false
|
|
self.IsRearming = false
|
|
self:FullyRearmed()
|
|
self:_RTBRearmGroup()
|
|
if self.DrawDataR.DrawText then
|
|
trigger.action.removeMark(self.DrawDataR.DrawText)
|
|
self.DrawDataR.DrawText = nil
|
|
end
|
|
if self.DrawDataR.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataR.DrawPoly)
|
|
self.DrawDataR.DrawPoly = nil
|
|
end
|
|
|
|
--Return to activity
|
|
if self.Destination and not self:Is("Retreating") then
|
|
self:RearmedToTravel()
|
|
self:_InitiateTravel()
|
|
elseif not self:Is("Retreating") then
|
|
self:RearmedToIdle()
|
|
end
|
|
end
|
|
|
|
--Dispatch another group if lost
|
|
if not self:Is("Retreating") then
|
|
if not self.RearmGroup and not self.RearmGroupBase then
|
|
self:T("TACTICS: "..self.Groupname.." does not have a rearm group or base. ")
|
|
elseif not self.RearmGroup:GetCoordinate() and self.AllowRearmRespawn then
|
|
self:_CallForRearm(true)
|
|
elseif not self.RearmGroup:GetCoordinate() and not self.AllowRearmRespawn then
|
|
self:I("TACTICS: "..self.Groupname.." can no longer be rearmed.")
|
|
self.TimeRearmActive = false
|
|
self.IsWinchester = false
|
|
self.IsRearming = false
|
|
self.AllowRearming = false
|
|
|
|
--Return to activity
|
|
if self.Destination then
|
|
self:RearmedToTravel()
|
|
self:_InitiateTravel()
|
|
else
|
|
self:RearmedToIdle()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] Retreat check timer
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_RetreatTimer()
|
|
self:T("TACTICS: >> RETREAT CHECK TIMER << - [GRP: "..self.Groupname.."]")
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
elseif self:Is("Retreating") then
|
|
local unitsRemain, selfRecover = self:CheckUnitLife()
|
|
if unitsRemain > 0 then
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if not coordGroup then coordGroup = self.Group:GetCoordinate() end
|
|
local distGroup = coordGroup:Get3DDistance(self.retreatDestination)
|
|
local speedGroup = self.Group:GetVelocityKMH()
|
|
|
|
--Arrived at destination
|
|
if distGroup <= self.DestinationRadius then
|
|
self.Group:RouteStop()
|
|
self:T("TACTICS: "..self.Groupname.." arrived at their destination.")
|
|
self.RetreatTimer:Stop()
|
|
self.travelToStored = nil
|
|
self:EndRetreat(self.Group:GetAverageCoordinate(), self.retreatDestination)
|
|
self.retreatDestination = nil
|
|
if self.DespawnAfterRetreat then
|
|
self.Group:Destroy()
|
|
self:_DeadResponse(false)
|
|
else
|
|
if self.UseDetection == true then
|
|
self.TimeDetectionActive = true
|
|
self.TimeDetectionStamp = timer.getAbsTime()
|
|
end
|
|
end
|
|
elseif speedGroup < 1 and self.FixStuck or self.countStuck > 0 then -- Attempt to get unstuck if it's stopped before arriving at the destination.
|
|
self.countStuck = self.countStuck + 1
|
|
self:T("TACTICS: "..self.Groupname.." is stopped when it should be moving. Check #"..self.countStuck)
|
|
if self.countStuck == 1 then -- Initial unstuck check
|
|
self.stuckPosit = self.Group:GetAverageCoordinate()
|
|
elseif self.countStuck > 1 and self.countStuck < self.MaxUnstuckAttempts then
|
|
local distStuck = coordGroup:Get3DDistance(self.stuckPosit)
|
|
if distStuck <= self.UnstuckDist/4 then -- Attempt to get unstuck
|
|
if self.lastStuckHeading == nil then
|
|
self.lastStuckHeading = (self.stuckPosit:HeadingTo(self.retreatDestination) + self.StuckHeadingIncrements) % 360
|
|
else
|
|
self.lastStuckHeading = (self.lastStuckHeading + self.StuckHeadingIncrements) % 360
|
|
end
|
|
local unstuckDest = coordGroup:Translate(self.UnstuckDist,self.lastStuckHeading,false,false)
|
|
self.Group:RouteGroundTo(unstuckDest, 10, 'Cone', 1)
|
|
self:T("TACTICS: "..self.Groupname.." is attempting to get unstuck by driving "..self.UnstuckDist .."m toward heading "..self.lastStuckHeading)
|
|
else -- Group has moved enough to resume the route
|
|
self:T("TACTICS: "..self.Groupname.." has gotten unstuck. Resuming on the path...")
|
|
self.countStuck = 0
|
|
self.stuckPosit = nil
|
|
if not self.TroopsAttacking then
|
|
if self.RetreatOnRoads then
|
|
self.Group:RouteGroundOnRoad(self.retreatDestination, self.RetreatSpeed, 1, self.RetreatFormation)
|
|
else
|
|
self.Group:RouteGroundTo(self.retreatDestination, self.a, self.RetreatFormation, 1)
|
|
end
|
|
end
|
|
end
|
|
elseif self.countStuck >= self.MaxUnstuckAttempts then -- Max stuck attempts exceeded
|
|
if self.DestroyStuck then --EITHER Destroy the group
|
|
self:E("TACTICS: "..self.Groupname.." has been stuck for too long and was removed from the game...")
|
|
self:EndRetreat(self.Group:GetAverageCoordinate(), self.retreatDestination)
|
|
self.Group:Destroy()
|
|
self:_DeadResponse(false)
|
|
else --OR Return the group to Idle
|
|
self:E("TACTICS: "..self.Groupname.." has been stuck for too long and was returned to the idle state...")
|
|
self.Group:RouteStop()
|
|
self.RetreatTimer:Stop()
|
|
self.travelToStored = nil
|
|
self:EndRetreat(self.Group:GetAverageCoordinate(), self.retreatDestination)
|
|
self.retreatDestination = nil
|
|
if self.DespawnAfterRetreat then
|
|
self.Group:Destroy()
|
|
self:_DeadResponse(false)
|
|
else
|
|
if self.UseDetection == true then
|
|
self.TimeDetectionActive = true
|
|
self.TimeDetectionStamp = timer.getAbsTime()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
self.countStuck = 0
|
|
self.stuckPosit = nil
|
|
end
|
|
end
|
|
else
|
|
self.TimeTravelActive = false
|
|
self.stuckPosit = nil
|
|
self.lastStuckHeading = nil
|
|
self.countStuck = 0
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] Cooldown engagement
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_EngageCooldown()
|
|
self:T("TACTICS: >> ENGAGE COOLDOWN TIMER << - [GRP: "..self.Groupname.."]")
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
else
|
|
local unitsRemain, selfRecover = self:CheckUnitLife()
|
|
if unitsRemain > 0 then
|
|
local isWinchester, isFull = self:CheckAmmo()
|
|
if isWinchester and self.AllowRearming then
|
|
self:_CallForRearm()
|
|
if self.TroopsAttacking then self:_ReturnTroops() end
|
|
elseif not selfRecover then
|
|
if self.TroopsAttacking then
|
|
self:_ReturnTroops()
|
|
elseif self.Destination then
|
|
self:EngageToTravel()
|
|
self:_InitiateTravel()
|
|
else
|
|
self:EngageToIdle()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if self.DrawDataE.DrawText then
|
|
trigger.action.removeMark(self.DrawDataE.DrawText)
|
|
self.DrawDataE.DrawText = nil
|
|
end
|
|
if self.DrawDataE.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataE.DrawPoly)
|
|
self.DrawDataE.DrawPoly = nil
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Hold timer
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_HoldTimer()
|
|
self:T("TACTICS: >> HOLD POSITION TIMER << - [GRP: "..self.Groupname.."]")
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
else
|
|
if not self:Is("Engaging") then
|
|
self:I("TACTICS: "..self.Groupname.." is no longer holding after spotting a threat.")
|
|
if self.ManageAlarmState then
|
|
if self.DefaultAlarmState == "Auto" then
|
|
self.Group:OptionAlarmStateAuto()
|
|
elseif self.DefaultAlarmState == "Red" then
|
|
self.Group:OptionAlarmStateRed()
|
|
elseif self.DefaultAlarmState == "Green" then
|
|
self.Group:OptionAlarmStateGreen()
|
|
end
|
|
end
|
|
if self.TroopsAttacking then
|
|
self:_ReturnTroops()
|
|
elseif self.Destination then
|
|
self:HoldToTravel()
|
|
self:_InitiateTravel()
|
|
else
|
|
self:HoldToIdle()
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Attacktimer
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_AttackTimer()
|
|
self:T("TACTICS: >> ATTACK TIMER << - [GRP: "..self.Groupname.."]")
|
|
self.attackLastCoord = nil
|
|
if not self.Group:GetCoordinate() then
|
|
if not self:Is("Dead") then
|
|
self:_DeadResponse()
|
|
end
|
|
else
|
|
if self:Is("Attacking") then
|
|
self:I("TACTICS: "..self.Groupname.." does not see their target anymore and will stop attacking.")
|
|
if self:Is("Attacking") then
|
|
if self.TroopsAttacking then
|
|
self:_ReturnTroops()
|
|
elseif self.Destination then
|
|
self:AttackToTravel()
|
|
self:_InitiateTravel()
|
|
else
|
|
self.Group:RouteStop()
|
|
self:AttackToIdle()
|
|
end
|
|
end
|
|
if self.ManageAlarmState then
|
|
if self.DefaultAlarmState == "Auto" then
|
|
self.Group:OptionAlarmStateAuto()
|
|
elseif self.DefaultAlarmState == "Red" then
|
|
self.Group:OptionAlarmStateRed()
|
|
elseif self.DefaultAlarmState == "Green" then
|
|
self.Group:OptionAlarmStateGreen()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal EXTERNAL TIMER: Despawn Rearm Group
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_RearmingGroupDespawn()
|
|
self.RearmingGroupDespawnTimer = TIMER:New(function()
|
|
self:T({"TACTICS: >> REARM GROUP DESPAWN TIMER <<",GroupName = self.Groupname})
|
|
if self.RearmGroup and self.RearmGroup:GetCoordinate() then
|
|
self:I("TACTICS: checking if "..self.RearmGroup:GetName().." can despawn.")
|
|
local speedGroup = self.RearmGroup:GetVelocityKMH()
|
|
if speedGroup < 1 then
|
|
self:I("TACTICS: removing "..self.RearmGroup:GetName().." from the simulation")
|
|
self.RearmGroup:Destroy()
|
|
self.RearmingGroupDespawnTimer:Stop()
|
|
self.RearmingGroupDespawnTimer = nil
|
|
end
|
|
else
|
|
self.RearmingGroupDespawnTimer:Stop()
|
|
self.RearmingGroupDespawnTimer = nil
|
|
end
|
|
end)
|
|
self.RearmingGroupDespawnTimer:Start(60,60)
|
|
return self
|
|
end
|
|
|
|
|
|
|
|
|
|
--------[INTERNAL METHODS]---------
|
|
|
|
--- [Internal Initiate Travel
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_InitiateTravel()
|
|
if self.Destination then
|
|
self:T({"TACTICS TRAVEL INITIATED:", Groupname = self.Groupname, UseRoads = self.travelToStored.UseRoads, ToCoordinate = self.travelToStored.ToCoordinate, Speed = self.travelToStored.Speed, Formation = self.travelToStored.Formation, DelaySeconds = self.travelToStored.DelaySeconds, WaypointFunction = self.travelToStored.WaypointFunction, WaypointFunctionArguments = self.travelToStored.WaypointFunctionArguments, CurrentState = self:GetState()})
|
|
if self.travelToStored.UseRoads then
|
|
self.Group:RouteGroundOnRoad(self.travelToStored.ToCoordinate, self.travelToStored.Speed, self.travelToStored.DelaySeconds, self.travelToStored.Formation, self.travelToStored.WaypointFunction, self.travelToStored.WaypointFunctionArguments)
|
|
else
|
|
self.Group:RouteGroundTo(self.travelToStored.ToCoordinate, self.travelToStored.Speed, self.travelToStored.Formation, self.travelToStored.DelaySeconds, self.travelToStored.WaypointFunction, self.travelToStored.WaypointFunctionArguments)
|
|
end
|
|
if self:Is("Idle") then
|
|
self:StartTravel(self.Group:GetAverageCoordinate(), self.Destination)
|
|
elseif self:Is("Travelling") then
|
|
self:Redirect(self.Group:GetAverageCoordinate(), self.Destination)
|
|
end
|
|
self.TimeTravelActive = true
|
|
self.TimeTravelStamp = timer.getAbsTime()
|
|
else
|
|
self:E("TACTICS ERROR: "..self.Groupname.." attempted to travel somewhere but doesn't have a destination!")
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Initiate Engagements
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_InitEngagement(WasHit,EventData)
|
|
self:T({"TACTICS: "..self.Groupname.." started engaging something.",WasHit,EventData})
|
|
local UnitName = EventData.IniDCSUnitName
|
|
if not WasHit then UnitName = EventData.TgtDCSUnitName end
|
|
local WeaponName = EventData.WeaponName
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if not coordGroup then coordGroup = self.Group:GetCoordinate() end
|
|
if coordGroup then
|
|
local unitEnemy = UNIT:FindByName(UnitName)
|
|
local coordEnemy = nil
|
|
if unitEnemy then coordEnemy = unitEnemy:GetCoordinate() end
|
|
local targetCoord = coordEnemy
|
|
if not targetCoord then targetCoord = coordGroup end
|
|
|
|
--Deploy troops if able
|
|
if not self.TroopsAttacking and self.TroopTransport then
|
|
self:DropTroops(targetCoord,true)
|
|
end
|
|
|
|
--Disperse if able
|
|
if self.DisperseOnShoot and not WasHit or self.DisperseOnHit and WasHit then
|
|
self:T("TACTICS: "..self.Groupname.." will attempt to disperse.")
|
|
local headingGroup = self.Group:GetHeading()
|
|
if headingGroup then
|
|
local roadside = math.random(2)
|
|
if roadside == 1 then headingGroup = headingGroup + 45 else headingGroup = headingGroup - 45 end
|
|
if headingGroup < 1 then headingGroup = headingGroup + 360 elseif headingGroup > 359 then headingGroup = headingGroup - 360 end
|
|
local coordEvade = coordGroup:Translate(headingGroup, self.EvadeDistance, false, false)
|
|
if not coordEvade:IsInFlatArea(25, self.InclineLimit) or coordEvade:IsSurfaceTypeWater() or coordEvade:IsSurfaceTypeShallowWater() then
|
|
if roadside == 2 then headingGroup = headingGroup + 45 else headingGroup = headingGroup - 45 end
|
|
if headingGroup < 1 then headingGroup = headingGroup + 360 elseif headingGroup > 359 then headingGroup = headingGroup - 360 end
|
|
coordEvade:Translate(headingGroup, self.EvadeDistance, false, true)
|
|
end
|
|
if coordEvade:IsInFlatArea(25, self.InclineLimit) and not coordEvade:IsSurfaceTypeWater() and not coordEvade:IsSurfaceTypeShallowWater() then
|
|
self:T({"TACTICS: "..self.Groupname.." is dispersing.",headingGroup,self.EvadeDistance})
|
|
self.Group:RouteGroundTo(coordEvade, 20, 'Cone', 1)
|
|
TIMER:New(function()
|
|
if not self:Is("Retreating") then self.Group:RouteStop() end
|
|
end):Start(15)
|
|
else
|
|
self:T("TACTICS: "..self.Groupname.." could not disperse due to unsuitable terrain.")
|
|
self.Group:RouteStop()
|
|
end
|
|
end
|
|
elseif self.Is("Traveling") then
|
|
self.Group:RouteStop()
|
|
end
|
|
|
|
--Interrupt rearming if required
|
|
if self.IsWinchester then
|
|
self.IsWinchester = false
|
|
if self.IsRearming then
|
|
self.IsRearming = false
|
|
self.TimeRearmActive = false
|
|
if self.RearmGroup and self.RearmGroup:GetCoordinate() then
|
|
self:_RTBRearmGroup()
|
|
end
|
|
if self.DrawDataR.DrawText then
|
|
trigger.action.removeMark(self.DrawDataR.DrawText)
|
|
self.DrawDataR.DrawText = nil
|
|
end
|
|
if self.DrawDataR.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataR.DrawPoly)
|
|
self.DrawDataR.DrawPoly = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
--Draw on map if able
|
|
if self.EngageDrawing then
|
|
if self.DrawDataE.DrawText then
|
|
trigger.action.removeMark(self.DrawDataE.DrawText)
|
|
end
|
|
if self.DrawDataE.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataE.DrawPoly)
|
|
end
|
|
|
|
self.DrawDataE.DrawText = coordGroup:TextToAll(self.DrawDataE.Text,self.DrawDataE.Coalition,self.DrawDataE.Color1,self.DrawDataE.Alpha1,self.DrawDataE.Color2,self.DrawDataE.Alpha2,self.DrawDataE.Font,false)
|
|
self.DrawDataE.DrawPoly = coordGroup:CircleToAll(self.DrawDataE.Radius,self.DrawDataE.Coalition,self.DrawDataE.Color1,self.DrawDataE.Alpha1,self.DrawDataE.Color2,self.DrawDataE.Alpha2/2,self.DrawDataE.Linetype,false)
|
|
|
|
if self.DrawDataE.MarkEnemy then
|
|
local tempType = 0
|
|
if not coordEnemy then tempType = 1 end
|
|
self:_ContactMark(targetCoord,tempType)
|
|
end
|
|
end
|
|
|
|
self:StartEngaging(WasHit, UnitName, WeaponName, EventData)
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Dead unit response
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_DeadResponse(EventData)
|
|
if not self:Is("Dead") then
|
|
if EventData then
|
|
self:UnitLost(EventData)
|
|
self:T("TACTICS: "..self.Groupname.." lost a unit.")
|
|
if self.SupportLevel <= 4 and not self.TimeSupportActive and self.Group:GetCoordinate() and self.AllowSupportRequests then
|
|
self:RequestSupport()
|
|
end
|
|
end
|
|
if self.Group:GetUnits() == nil or #self.Group:GetUnits() <= 0 then
|
|
self:I("TACTICS: "..self.Groupname.." has died or was removed from the sim.")
|
|
if self.MasterTime and self.MasterTime:IsRunning() then self.MasterTime:Stop() end
|
|
|
|
--Garbage Day
|
|
self.MasterTime = nil
|
|
self.UnitTable = nil
|
|
-- self.setEnemy:FilterStop()
|
|
-- self.setEnemy = nil
|
|
|
|
if self.DrawDataR.DrawText then
|
|
trigger.action.removeMark(self.DrawDataR.DrawText)
|
|
self.DrawDataR.DrawText = nil
|
|
end
|
|
if self.DrawDataR.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataR.DrawPoly)
|
|
self.DrawDataR.DrawPoly = nil
|
|
end
|
|
if self.DrawDataE.DrawText then
|
|
trigger.action.removeMark(self.DrawDataE.DrawText)
|
|
self.DrawDataE.DrawText = nil
|
|
end
|
|
if self.DrawDataE.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataE.DrawPoly)
|
|
self.DrawDataE.DrawPoly = nil
|
|
end
|
|
|
|
self:GroupDead(EventData)
|
|
elseif self.RetreatAfterLosses and #self.Group:GetUnits() <= (#self.UnitTable * self.RetreatAfterLosses) and not self:Is("Retreating") then
|
|
self:Retreat()
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Ammo check
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:CheckAmmo()
|
|
self:T("TACTICS: Ammo check called for "..self.Groupname)
|
|
local checkUnits = self.Group:GetUnits()
|
|
local ammoLow = false
|
|
local ammoFull = true
|
|
for u = 1, #checkUnits do
|
|
for z = 1, #self.UnitTable do
|
|
if checkUnits[u]:GetName() == self.UnitTable[z].UnitName then
|
|
self:T("TACTICS: Checking "..self.UnitTable[z].UnitName)
|
|
local currentAmmo = checkUnits[u]:GetAmmo()
|
|
if self.groupAmmo[z] and #self.groupAmmo[z] > 0 then
|
|
for i = 1, #self.groupAmmo[z] do
|
|
local ammoCount = 0
|
|
if currentAmmo and #currentAmmo > 0 then
|
|
for v = 1, #currentAmmo do
|
|
if currentAmmo[v]["desc"]["typeName"] == self.groupAmmo[z][i]["desc"]["typeName"] then
|
|
ammoCount = currentAmmo[v]["count"]
|
|
self:T( "TACTICS: "..self.groupAmmo[z][i]["desc"]["typeName"].." = "..currentAmmo[v]["count"].."/"..self.groupAmmo[z][i]["count"])
|
|
end
|
|
end
|
|
else
|
|
ammoLow = true
|
|
end
|
|
if ammoCount < (self.groupAmmo[z][i]["count"] * self.LowAmmoPercent) then
|
|
ammoLow = true
|
|
end
|
|
if ammoCount < (self.groupAmmo[z][i]["count"] * self.FullAmmoPercent) or ammoLow then
|
|
ammoFull = false
|
|
end
|
|
end
|
|
else
|
|
self:T(self.UnitTable[z].UnitName.." does not use ammo.")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return ammoLow, ammoFull
|
|
end
|
|
|
|
--- [Internal Call for rearm
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_CallForRearm(respawnCall)
|
|
if respawnCall then
|
|
self:I("TACTICS: "..self.Groupname.." requesting respawn of rearm group.")
|
|
else
|
|
self:I("TACTICS: "..self.Groupname.." is winchester.")
|
|
end
|
|
|
|
self.IsWinchester = true
|
|
|
|
--Manage rearming if able
|
|
if self.AllowRearming and not self:Is("Retreating") then
|
|
if self.RearmingGroupDespawnTimer and self.RearmingGroupDespawnTimer:IsRunning() then
|
|
self.RearmingGroupDespawnTimer:Stop()
|
|
self.RearmingGroupDespawnTimer = nil
|
|
end
|
|
--Dispatch ammo truck if available
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
if self.RearmGroup and self.RearmGroup:GetCoordinate() then
|
|
self:I("TACTICS: Dispatching "..self.RearmGroup:GetName().." to rearm "..self.Groupname)
|
|
if self.RearmGroupUseRoads then
|
|
self.RearmGroup:RouteGroundOnRoad(coordGroup, self.RearmGroupSpeed, 1, self.RearmGroupFormation)
|
|
else
|
|
self.RearmGroup:RouteGroundTo(coordGroup, self.RearmGroupSpeed, self.RearmGroupFormation, 1)
|
|
end
|
|
elseif self.RearmTemplate and self.RearmSpawnCoord then
|
|
--Spawn new if able and no truck is available
|
|
local spawnIndex = math.random(9999)
|
|
local rearmSpawn = SPAWN:NewWithAlias(self.RearmTemplate, self.RearmTemplate..spawnIndex)
|
|
rearmSpawn:OnSpawnGroup(function(_grp)
|
|
self.RearmGroup = _grp
|
|
if self.RearmGroupUseRoads then
|
|
_grp:RouteGroundOnRoad(coordGroup, self.RearmGroupSpeed, 1, self.RearmGroupFormation)
|
|
else
|
|
_grp:RouteGroundTo(coordGroup, self.RearmGroupSpeed, self.RearmGroupFormation, 1)
|
|
end
|
|
self:I("TACTICS: Spawned and dispatched ".._grp:GetName().." to rearm "..self.Groupname)
|
|
end)
|
|
rearmSpawn:SpawnFromCoordinate(self.RearmSpawnCoord)
|
|
else
|
|
self:I("TACTICS: "..self.Groupname.." does not have a rearming group assigned to it. They can mark but will remain stationary until rearmed.")
|
|
end
|
|
|
|
--Draw on map if able
|
|
if self.RearmDrawing then
|
|
if self.DrawDataR.DrawText then
|
|
trigger.action.removeMark(self.DrawDataR.DrawText)
|
|
end
|
|
if self.DrawDataR.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataR.DrawPoly)
|
|
end
|
|
|
|
self.DrawDataR.DrawText = coordGroup:TextToAll(self.DrawDataR.Text,self.DrawDataR.Coalition,self.DrawDataR.Color1,self.DrawDataR.Alpha1,self.DrawDataR.Color2,self.DrawDataR.Alpha2,self.DrawDataR.Font,false)
|
|
self.DrawDataR.DrawPoly = coordGroup:CircleToAll(self.DrawDataR.Radius,self.DrawDataR.Coalition,self.DrawDataR.Color1,self.DrawDataR.Alpha1,self.DrawDataR.Color2,self.DrawDataR.Alpha2/2,self.DrawDataR.Linetype,false)
|
|
end
|
|
|
|
--Group units up if spread out
|
|
local unitList = self.Group:GetUnits()
|
|
for i = 1,#unitList do
|
|
local coordUnit = unitList[i]:GetCoordinate()
|
|
if coordUnit:Get3DDistance(coordGroup) >= 300 then
|
|
self.Group:RouteGroundTo(coordGroup)
|
|
break
|
|
end
|
|
end
|
|
|
|
self.IsRearming = true
|
|
self:RequestRearming(self.RearmGroup)
|
|
self.TimeRearmActive = true
|
|
self.TimeRearmStamp = timer.getAbsTime()
|
|
end
|
|
self:Winchester()
|
|
return self
|
|
end
|
|
|
|
--- [Internal RTB Rearm Group
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_RTBRearmGroup()
|
|
if self.RearmGroup and self.RearmGroup:GetCoordinate() and self.RearmGroupBase and self.RearmGroupRTB then
|
|
self:I("TACTICS: Sending "..self.RearmGroup:GetName().." back to base.")
|
|
if self.RearmGroupUseRoads then
|
|
self.RearmGroup:RouteGroundOnRoad(self.RearmGroupBase, self.RearmGroupSpeed, 1, self.RearmGroupFormation)
|
|
else
|
|
self.RearmGroup:RouteGroundTo(self.RearmGroupBase, self.RearmGroupSpeed, self.RearmGroupFormation, 1)
|
|
end
|
|
if self.DespawnRearmingGroup and not self.RearmingGroupDespawnTimer then
|
|
self:_RearmingGroupDespawn()
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal] Spot target
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_SpotTarget(_enemy,distance)
|
|
self:T("TACTICS: "..self.Groupname.." detected "..tostring(_enemy:GetName()).." at a range of "..distance.."m. Current Tactical ROE: "..self.TacticalROE)
|
|
if self.ManageAlarmState then self.Group:OptionAlarmStateRed() end
|
|
--Attack
|
|
if self.TacticalROE == "Attack" then
|
|
self:AttackTarget(_enemy,distance)
|
|
|
|
--Hold
|
|
elseif self.TacticalROE == "Hold" then
|
|
if distance <= self.HoldDistance then
|
|
if self:Is("Travelling") or self:Is("Idle") then
|
|
self.Group:RouteStop()
|
|
self:StartHold()
|
|
end
|
|
--Deploy troops if able
|
|
if not self.TroopsAttacking and self.TroopTransport and _enemy:GetCoordinate() then
|
|
self:DropTroops(_enemy:GetCoordinate(),true)
|
|
end
|
|
self.TimeHoldActive = true
|
|
self.TimeHoldStamp = timer.getAbsTime()
|
|
end
|
|
|
|
--Avoid
|
|
elseif self.TacticalROE == "Avoid" then
|
|
if distance <= self.AvoidDistance then
|
|
self:AvoidTarget(_enemy,distance)
|
|
end
|
|
|
|
--Ignore
|
|
else
|
|
if self.ManageAlarmState then
|
|
TIMER:New(function()
|
|
if self.DefaultAlarmState == "Auto" then
|
|
self.Group:OptionAlarmStateAuto()
|
|
elseif self.DefaultAlarmState == "Red" then
|
|
self.Group:OptionAlarmStateRed()
|
|
elseif self.DefaultAlarmState == "Green" then
|
|
self.Group:OptionAlarmStateGreen()
|
|
end
|
|
end):Start(self.DetectionRate-1)
|
|
end
|
|
end
|
|
|
|
if self.SupportLevel <= 1 and not self.TimeSupportActive and self.AllowSupportRequests then
|
|
self:RequestSupport(_enemy:GetCoordinate())
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Attack a target
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:AttackTarget(_enemy,distance)
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
local coordEnemy = nil
|
|
local hdgEnemy = 0
|
|
local className = _enemy:GetClassName()
|
|
if className == "COORDINATE" then
|
|
self:T("TACTICS: "..self.Groupname.." is attacking a provided coordinate "..distance.."m away.")
|
|
coordEnemy = _enemy
|
|
elseif not className then
|
|
self:E("TACTICS ERROR: The provided enemy is not a valid Core.COORDINATE or Wrapper.POSITIONABLE object.")
|
|
else
|
|
self:T("TACTICS: "..self.Groupname.." is attacking "..tostring(_enemy:GetName()).." "..distance.."m away.")
|
|
coordEnemy = _enemy:GetCoordinate()
|
|
end
|
|
|
|
if coordEnemy and coordGroup then
|
|
hdgEnemy = coordGroup:HeadingTo(coordEnemy)
|
|
local coordTargetDest = nil
|
|
local skipNewDest = false
|
|
if self.attackLastCoord and self.attackLastCoord:Get3DDistance(coordEnemy) <= self.LastTargetThreshold then
|
|
skipNewDest = true
|
|
local tempDist = self.attackLastCoord:Get3DDistance(coordEnemy)
|
|
self:T("TACTICS: "..self.Groupname.."'s target is only "..tempDist.."m away from where it was last. Threshold set to "..self.LastTargetThreshold)
|
|
end
|
|
|
|
--Get attack move-to position
|
|
if distance > 25 then
|
|
local hdgTgt = coordGroup:HeadingTo(coordEnemy)
|
|
coordTargetDest = coordEnemy:Translate(-self.AttackPositionDist,hdgTgt,false,false)
|
|
if distance < self.AttackPositionDist+20 then
|
|
coordTargetDest = coordEnemy:Translate(-(distance/2),hdgTgt,false,false)
|
|
skipNewDest = false
|
|
end
|
|
local booIsFlat = false
|
|
local checkFlat = 0
|
|
if not skipNewDest then
|
|
while not booIsFlat and checkFlat < 10 do
|
|
checkFlat = checkFlat + 1
|
|
if coordTargetDest:IsInFlatArea(25, self.InclineLimit) and not coordTargetDest:IsSurfaceTypeWater() and not coordTargetDest:IsSurfaceTypeShallowWater() then
|
|
if distance < self.CombatDistance then
|
|
self.Group:RouteGroundTo(coordTargetDest, self.AttackSpeed, self.AttackFormation, 1)
|
|
else
|
|
if self.AttackFarUsesRoads then self.Group:RouteGroundOnRoad(coordTargetDest, self.AttackSpeedFar, 1, self.AttackFormationFar)
|
|
else self.Group:RouteGroundTo(coordTargetDest, self.AttackSpeedFar, self.AttackFormationFar, 1) end
|
|
end
|
|
booIsFlat = true
|
|
checkFlat = 10
|
|
else
|
|
coordTargetDest:Translate(25,(hdgTgt+90) % 360 ,false,true)
|
|
end
|
|
end
|
|
if not booIsFlat then
|
|
coordTargetDest = coordEnemy:Translate(-self.AttackPositionDist,hdgTgt,false,false)
|
|
if distance < self.AttackPositionDist+20 then coordTargetDest = coordEnemy:Translate(-(distance/2),hdgTgt,false,false) end
|
|
if distance < self.CombatDistance then
|
|
self.Group:RouteGroundTo(coordTargetDest, self.AttackSpeed, self.AttackFormation, 1)
|
|
else
|
|
if self.AttackFarUsesRoads then self.Group:RouteGroundOnRoad(coordTargetDest, self.AttackSpeedFar, 1, self.AttackFormationFar)
|
|
else self.Group:RouteGroundTo(coordTargetDest, self.AttackSpeedFar, self.AttackFormationFar, 1) end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
--Move both groups toward a convergence point
|
|
if className == "GROUP" or className == "UNIT" then
|
|
self:T("TACTICS: "..self.Groupname.." and "..tostring(_enemy:GetName()).." are too close without engaging (>25m). Attempting to move both groups.")
|
|
local coordTargetDest = coordEnemy:Translate(25,90,false,false)
|
|
if not coordTargetDest:IsInFlatArea(25, self.InclineLimit) or coordTargetDest:IsSurfaceTypeWater() or coordTargetDest:IsSurfaceTypeShallowWater() then
|
|
coordTargetDest = coordEnemy:Translate(25,-90 ,false,false)
|
|
end
|
|
if not coordTargetDest:IsInFlatArea(25, self.InclineLimit) or coordTargetDest:IsSurfaceTypeWater() or coordTargetDest:IsSurfaceTypeShallowWater() then
|
|
coordTargetDest = coordEnemy:Translate(25,0 ,false,false)
|
|
end
|
|
if not coordTargetDest:IsInFlatArea(25, self.InclineLimit) or coordTargetDest:IsSurfaceTypeWater() or coordTargetDest:IsSurfaceTypeShallowWater() then
|
|
coordTargetDest = coordEnemy:Translate(25,180 ,false,false)
|
|
end
|
|
self.Group:RouteGroundTo(coordTargetDest, self.AttackSpeed, self.AttackFormation, 1)
|
|
_enemy:RouteGroundTo(coordTargetDest, self.AttackSpeed, self.AttackFormation, 1)
|
|
end
|
|
end
|
|
|
|
--Deploy Troops If Able
|
|
if not self.TroopsAttacking and self.TroopTransport and distance <= self.TroopAttackDist then
|
|
self:T("TACTICS: "..self.Groupname.." is deploying troops to attack "..tostring(_enemy:GetName()))
|
|
self.Group:RouteStop()
|
|
self:DropTroops(coordEnemy,true)
|
|
TIMER:New(function()
|
|
self.Group:RouteResume()
|
|
end):Start(5)
|
|
end
|
|
|
|
self.attackLastCoord = coordEnemy
|
|
self.TimeAttackActive = true
|
|
self.TimeAttackStamp = timer.getAbsTime()
|
|
self:AttackManeuver(_enemy,distance,hdgEnemy)
|
|
if not self:Is("Attacking") then self:StartAttack(_enemy,distance,hdgEnemy) end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Avoid target
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:AvoidTarget(_enemy,distance)
|
|
local coordGroup = self.Group:GetAverageCoordinate()
|
|
local coordEnemy = nil
|
|
local className = _enemy:GetClassName()
|
|
if className == "COORDINATE" then
|
|
self:T("TACTICS: "..self.Groupname.." is avoiding a provided coordinate "..distance.."m away.")
|
|
coordEnemy = _enemy
|
|
elseif not className then
|
|
self:E("TACTICS ERROR: The provided enemy is not a valid Core.COORDINATE or Wrapper.POSITIONABLE object.")
|
|
else
|
|
self:T("TACTICS: "..self.Groupname.." is avoiding "..tostring(_enemy:GetName()).." "..distance.."m away.")
|
|
coordEnemy = _enemy:GetCoordinate()
|
|
end
|
|
local isAvoiding = false
|
|
|
|
if coordGroup and coordEnemy then
|
|
self:T("TACTICS: "..self.Groupname.." will try to avoid "..tostring(_enemy:GetName()))
|
|
for i = 1,100 do
|
|
local hdgAvoid = coordEnemy:HeadingTo(coordGroup)
|
|
if self.AvoidAzimuthMax then
|
|
hdgAvoid = ((hdgAvoid - self.AvoidAzimuthMax) + (math.random(self.AvoidAzimuthMax*2))) % 360
|
|
if hdgAvoid < 1 then hdgAvoid = hdgAvoid + 360 end
|
|
end
|
|
local avoidPoint = coordGroup:Translate(self.AvoidDistance, hdgAvoid, false, false)
|
|
if avoidPoint:IsInFlatArea(25, self.InclineLimit) and not avoidPoint:IsSurfaceTypeWater() and not avoidPoint:IsSurfaceTypeShallowWater() then
|
|
isAvoiding = true
|
|
if self.AvoidUseRoads then
|
|
self.Group:RouteGroundOnRoad(avoidPoint, self.AvoidSpeed, 1, self.AvoidFormation)
|
|
self.avoidDest = avoidPoint
|
|
self.TimeAvoidActive = true
|
|
self.TimeAvoidStamp = timer.getAbsTime()
|
|
else
|
|
self.Group:RouteGroundTo(avoidPoint, self.AvoidSpeed, self.AvoidFormation, 1)
|
|
self.avoidDest = avoidPoint
|
|
self.TimeAvoidActive = true
|
|
self.TimeAvoidStamp = timer.getAbsTime()
|
|
end
|
|
if self:Is("Travelling") or self:Is("Idle") then self:StartAvoid() end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not isAvoiding then
|
|
self:E("TACTICS ERROR: "..self.Groupname.." tried to avoid ".._enemy:GetName().." but could not find a destination with suitable terrain after 100 attempts.")
|
|
end
|
|
end
|
|
|
|
--- [Internal Drop troops
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:DropTroops(_targetCoord,_attacking,_limit)
|
|
self:T("TACTICS: "..self.Groupname.." called dropTroops. Attacking = "..tostring(_attacking))
|
|
local coordGroup = self.Group:GetCoordinate()
|
|
local hdgGroup = self.Group:GetHeading()
|
|
|
|
--Check if enemy is close enough to attack
|
|
local distTarget = _targetCoord:Get3DDistance(coordGroup)
|
|
local hdgTarget = nil
|
|
local coordAttack = nil
|
|
|
|
if distTarget <= self.TroopAttackDist and _attacking then
|
|
self:T("TACTICS: "..self.Groupname.." dropped troops are in range and will attack the target.")
|
|
hdgTarget = coordGroup:HeadingTo(_targetCoord)
|
|
coordAttack = coordGroup:Translate(distTarget-20,hdgTarget,false,false)
|
|
elseif _attacking then
|
|
self:T("TACTICS: "..self.Groupname.." dropped troops are too far from the target and will disperse instead.")
|
|
else
|
|
coordAttack = _targetCoord
|
|
end
|
|
|
|
--Get spawn parameters
|
|
local intSpawnIndex = math.random(1,9999)
|
|
local nameDroppedTroops = self.TroopTemplate.."_"..self.Groupname..intSpawnIndex
|
|
local troopsOffset = math.random(self.TroopDismountDistMin,self.TroopDismountDistMax)
|
|
local directionX = math.random(1,100)
|
|
if directionX > 50 then
|
|
troopsOffset = -troopsOffset
|
|
end
|
|
|
|
--Set up troop spawn
|
|
local spawnDropTroops = SPAWN:NewWithAlias(self.TroopTemplate, nameDroppedTroops):InitGroupHeading(hdgGroup)
|
|
spawnDropTroops:OnSpawnGroup(function(_troops)
|
|
local coordMoveTo = nil
|
|
if coordAttack then coordMoveTo = coordAttack:GetCoordinate() end
|
|
if coordMoveTo and _attacking then
|
|
local headingFlank = 0
|
|
local checkFlank = math.random(0,2)
|
|
if checkFlank == 0 then headingFlank = hdgTarget-90 elseif checkFlank == 2 then headingFlank = hdgTarget+90 end
|
|
if headingFlank < 0 then headingFlank = headingFlank+360 elseif headingFlank > 360 then headingFlank = headingFlank-360 end
|
|
if checkFlank ~= 1 then
|
|
coordMoveTo:Translate(100,headingFlank,false,true)
|
|
coordMoveTo:Translate(40,hdgTarget,false,true)
|
|
end
|
|
elseif _attacking then
|
|
coordMoveTo = _troops:GetUnit(1):GetOffsetCoordinate(0,0,troopsOffset)
|
|
elseif not _attacking then
|
|
coordMoveTo = _targetCoord
|
|
end
|
|
_troops:RouteGroundTo(coordMoveTo,self.TroopMoveSpeed,self.TroopFormation)
|
|
if _attacking then
|
|
table.insert(self.AttackingTroops,_troops)
|
|
self.TroopsAttacking = true
|
|
else
|
|
table.insert(self.DeployedTroops,_troops)
|
|
end
|
|
self:T("TACTICS: "..self.Groupname.." deployed ".._troops:GetName())
|
|
end)
|
|
|
|
--Timer to drop troops
|
|
local timerDropTroops = TIMER:New(function()
|
|
coordGroup = self.Group:GetCoordinate()
|
|
local unitTable = self.Group:GetUnits()
|
|
local dropAmount = #unitTable
|
|
if _limit then dropAmount = _limit end
|
|
for i = 1, dropAmount do
|
|
local coordDrop = unitTable[i]:GetOffsetCoordinate(-10,0,0)
|
|
spawnDropTroops:SpawnFromCoordinate(coordDrop)
|
|
end
|
|
self:TroopsDropped(_targetCoord,_attacking)
|
|
end)
|
|
timerDropTroops:Start(5)
|
|
return self
|
|
end
|
|
|
|
--- [Internal Return troops
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_ReturnTroops()
|
|
self:I("TACTICS: "..self.Groupname.." attempting to recover deployed troops.")
|
|
self.Group:RouteStop()
|
|
|
|
local homeCoord = self.Group:GetAverageCoordinate()
|
|
if not homeCoord then homeCoord = self.Group:GetCoordinate() end
|
|
local countTroops = 0
|
|
|
|
--Function to see if all troops have boarded
|
|
local function checkAllReturned()
|
|
if not self:Is("Dead") then
|
|
self:T("TACTICS: "..self.Groupname.."'s troops remaining to be extracted = "..tostring(countTroops))
|
|
if countTroops == 0 then
|
|
self.AttackingTroops = {}
|
|
self.TroopsAttacking = false
|
|
self:I("TACTICS: "..self.Groupname.." picked up all deployed troops and will resume tasking.")
|
|
local isWinchester, isFull = self:CheckAmmo()
|
|
if isWinchester and self.AllowRearming and not self.IsRearming then
|
|
self:_CallForRearm()
|
|
end
|
|
if self:Is("Retreating") then
|
|
if self.RetreatOnRoads then
|
|
self.Group:RouteGroundOnRoad(self.retreatDestination, self.RetreatSpeed, 1, self.RetreatFormation)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
else
|
|
self.Group:RouteGroundTo(self.retreatDestination, self.a, self.RetreatFormation, 1)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
end
|
|
elseif not self.IsRearming then
|
|
if self.Destination then
|
|
if self:Is("Engaging") then self:EngageToTravel()
|
|
elseif self:Is("Attacking") then self:AttackToTravel()
|
|
elseif self:Is("Holding") then self:HoldToTravel()
|
|
elseif self:Is("Idle") then self:StartTravel() end
|
|
self:_InitiateTravel()
|
|
else
|
|
if self:Is("Engaging") then self:EngageToIdle()
|
|
elseif self:Is("Holding") then self:HoldToIdle()
|
|
elseif self:Is("Attacking") then self:AttackToIdle()end
|
|
end
|
|
end
|
|
self:TroopsReturned()
|
|
end
|
|
end
|
|
end
|
|
|
|
--Send troops back to group position
|
|
if #self.AttackingTroops == 0 then
|
|
self:I("TACTICS: All of "..self.Groupname.."'s troops are KIA!")
|
|
checkAllReturned()
|
|
else
|
|
for i = #self.AttackingTroops,1,-1 do
|
|
local troopGroup = self.AttackingTroops[i]
|
|
if troopGroup:GetCoordinate() then
|
|
self:T("TACTICS: "..self.Groupname.." is recovering "..troopGroup:GetName())
|
|
local despawn = {}
|
|
troopGroup:RouteGroundTo(homeCoord,self.TroopMoveSpeed,self.TroopFormation)
|
|
|
|
--Check to despawn troops
|
|
local timerLimit = math.ceil(self.ExtractTimeLimit/20)
|
|
despawn.Timer = TIMER:New(function()
|
|
timerLimit = timerLimit - 1
|
|
self:T("TACTICS: "..troopGroup:GetName().." recovery limit check = "..timerLimit.."/"..math.ceil(self.ExtractTimeLimit/20))
|
|
local checkCoord = troopGroup:GetCoordinate()
|
|
local checkHome = self.Group:GetCoordinate()
|
|
if checkCoord and checkHome then
|
|
local distHome = checkCoord:Get3DDistance(homeCoord)
|
|
if distHome <= 25 then
|
|
despawn.Timer:Stop()
|
|
troopGroup:Destroy()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
else
|
|
despawn.Timer:Stop()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
if despawn.Timer:IsRunning() and timerLimit <= 0 then
|
|
despawn.Timer:Stop()
|
|
troopGroup:Destroy()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
end)
|
|
despawn.Timer:Start(20,20)
|
|
else
|
|
table.remove(self.AttackingTroops,i)
|
|
end
|
|
countTroops = #self.AttackingTroops
|
|
end
|
|
end
|
|
|
|
--Backup in the event of a failure
|
|
TIMER:New(function()
|
|
if countTroops > 0 then
|
|
for i = 1, #self.AttackingTroops do
|
|
self.AttackingTroops[i]:Destroy()
|
|
end
|
|
countTroops = 0
|
|
checkAllReturned()
|
|
end
|
|
end):Start(self.ExtractTimeLimit+5)
|
|
return self
|
|
end
|
|
|
|
--- [Internal Exctract Troops
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:ExtractTroops()
|
|
if not self:Is("Retreating") then
|
|
self:I("TACTICS: "..self.Groupname.." attempting to extract troops from the field.")
|
|
self.Group:RouteStop()
|
|
|
|
local homeCoord = self.Group:GetAverageCoordinate()
|
|
if not homeCoord then homeCoord = self.Group:GetCoordinate() end
|
|
local countTroops = 0
|
|
|
|
--Function to see if all troops have boarded
|
|
local function checkAllReturned()
|
|
self:T("TACTICS: "..self.Groupname.."'s troops remaining to be extracted = "..tostring(countTroops))
|
|
if countTroops == 0 then
|
|
self.DeployedTroops = {}
|
|
self:I("TACTICS: "..self.Groupname.." picked up all deployed troops and will resume tasking.")
|
|
if self:Is("Retreating") then
|
|
if self.RetreatOnRoads then
|
|
self.Group:RouteGroundOnRoad(self.retreatDestination, self.RetreatSpeed, 1, self.RetreatFormation)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
else
|
|
self.Group:RouteGroundTo(self.retreatDestination, self.a, self.RetreatFormation, 1)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
end
|
|
elseif not self.IsRearming then
|
|
if self.Destination then
|
|
if self:Is("Engaging") then self:EngageToTravel()
|
|
elseif self:Is("Attacking") then self:AttackToTravel()
|
|
elseif self:Is("Idle") then self:StartTravel() end
|
|
self:_InitiateTravel()
|
|
else
|
|
if self:Is("Engaging") then self:EngageToIdle()
|
|
elseif self:Is("Attacking") then self:AttackToIdle()end
|
|
end
|
|
end
|
|
self:TroopsExtracted()
|
|
end
|
|
end
|
|
|
|
--Send troops back to group position
|
|
if #self.DeployedTroops == 0 then
|
|
self:I("TACTICS: All of "..self.Groupname.."'s deployed troops are KIA!")
|
|
checkAllReturned()
|
|
else
|
|
for i = #self.DeployedTroops,1,-1 do
|
|
local troopGroup = self.DeployedTroops[i]
|
|
if troopGroup:GetCoordinate() then
|
|
local despawn = {}
|
|
troopGroup:RouteGroundTo(homeCoord,self.TroopMoveSpeed,self.TroopFormation)
|
|
|
|
--Check to despawn troops
|
|
local timerLimit = math.ceil(self.ExtractTimeLimit/20)
|
|
despawn.Timer = TIMER:New(function()
|
|
timerLimit = timerLimit - 1
|
|
local checkCoord = troopGroup:GetCoordinate()
|
|
local checkHome = self.Group:GetCoordinate()
|
|
if checkCoord and checkHome then
|
|
local distHome = checkCoord:Get3DDistance(homeCoord)
|
|
if distHome <= 25 then
|
|
despawn.Timer:Stop()
|
|
troopGroup:Destroy()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
else
|
|
despawn.Timer:Stop()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
if despawn.Timer:IsRunning() and timerLimit <= 0 then
|
|
despawn.Timer:Stop()
|
|
troopGroup:Destroy()
|
|
countTroops = countTroops - 1
|
|
checkAllReturned()
|
|
end
|
|
end)
|
|
despawn.Timer:Start(20,20)
|
|
else
|
|
table.remove(self.DeployedTroops,i)
|
|
end
|
|
countTroops = #self.DeployedTroops
|
|
end
|
|
end
|
|
|
|
--Backup in the event of a failure
|
|
TIMER:New(function()
|
|
if countTroops > 0 then
|
|
for i = 1, #self.DeployedTroops do
|
|
self.DeployedTroops[i]:Destroy()
|
|
end
|
|
countTroops = 0
|
|
checkAllReturned()
|
|
end
|
|
end):Start(self.ExtractTimeLimit+5)
|
|
return self
|
|
else
|
|
self:E("TACTICS ERROR: "..self.Groupname.." cannot extract troops while retreating.")
|
|
end
|
|
end
|
|
|
|
--- [Internal Contact marks
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:_ContactMark(_coord,_type)
|
|
|
|
--Get coordinates for the correct shape
|
|
local coordQuad1 = _coord:Translate(200,359,false,false)
|
|
local coordQuad2 = _coord:Translate(200,1,false,false)
|
|
local coordQuad3 = _coord:Translate(200,120,false,false)
|
|
local coordQuad4 = _coord:Translate(200,240,false,false)
|
|
if _type == 1 then
|
|
coordQuad1 = _coord:Translate(200,179,false,false)
|
|
coordQuad2 = _coord:Translate(200,181,false,false)
|
|
coordQuad3 = _coord:Translate(200,300,false,false)
|
|
coordQuad4 = _coord:Translate(200,60,false,false)
|
|
end
|
|
if _type == 2 then
|
|
coordQuad1 = _coord:Translate(200,0,false,false)
|
|
coordQuad2 = _coord:Translate(200,90,false,false)
|
|
coordQuad3 = _coord:Translate(200,180,false,false)
|
|
coordQuad4 = _coord:Translate(200,270,false,false)
|
|
end
|
|
|
|
--Get coalition params
|
|
local coalitionNum = 2
|
|
local coalitionColorA = {1,0,0}
|
|
local coalitionColorB = {0.5,0,0}
|
|
if self.groupCoalition == coalition.side.RED then
|
|
coalitionColorA = {0,0,1}
|
|
coalitionColorB = {0,0,0.5}
|
|
coalitionNum = 1
|
|
end
|
|
|
|
--Get timeout values
|
|
local timeoutFresh = self.EngageDrawingFresh
|
|
local timeoutStale = self.EngageDrawingStale
|
|
if _type == 2 then
|
|
timeoutFresh = self.DetectionRate-1
|
|
timeoutStale = self.EnemySpotStale
|
|
end
|
|
|
|
--Draw marker
|
|
if self.markSpot then trigger.action.removeMark(self.markSpot) end
|
|
self.markSpot = coordQuad1:QuadToAll(coordQuad2, coordQuad3, coordQuad4, coalitionNum,coalitionColorA,1,coalitionColorA,0.15,1,true)
|
|
if self.markSpotTimer and self.markSpotTimer:IsRunning() then
|
|
self.markSpotTimer:Stop()
|
|
self.markSpotTimer = nil
|
|
end
|
|
self.markSpotTimer = TIMER:New(function()
|
|
trigger.action.removeMark(self.markSpot)
|
|
self.markSpot = coordQuad1:QuadToAll(coordQuad2, coordQuad3, coordQuad4, coalitionNum,coalitionColorB,1,coalitionColorB,0.15,3,true)
|
|
self.markSpotTimer = nil
|
|
self.markSpotTimer = TIMER:New(function()
|
|
trigger.action.removeMark(self.markSpot)
|
|
self.markSpot = nil
|
|
self.markSpotTimer = nil
|
|
end):Start(timeoutStale)
|
|
end):Start(timeoutFresh)
|
|
return self
|
|
end
|
|
|
|
--- [Internal Request Support
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:RequestSupport(target)
|
|
local targetName = "nil target"
|
|
local coordGroup = self.Group:GetCoordinate()
|
|
local coordTarget = coordGroup
|
|
local unitTarget = nil
|
|
if target and target:GetClassName() == "COORDINATE" then
|
|
coordTarget = target
|
|
elseif target and target:GetClassName() == "UNIT" then
|
|
coordTarget = target:GetCoordinate()
|
|
unitTarget = target
|
|
targetName = target:GetName()
|
|
end
|
|
self:T({"TACTICS: "..self.Groupname.." requested support.","Target = "..tostring(targetName)})
|
|
if coordGroup then
|
|
self:T2("TACTICS: "..self.Groupname.." will search for nearby friendlies.")
|
|
local countGroups = 0
|
|
local friendlyTable = TACTICS_UTILS.GroupsRed
|
|
if self.groupCoalition == coalition.side.BLUE then friendlyTable = TACTICS_UTILS.GroupsBlue end
|
|
for i = 1,#friendlyTable do
|
|
self:T2("TACTICS: Group table index "..i.." for coalition "..tostring(self.groupCoalition))
|
|
local coordFriendly = friendlyTable[i].Group:GetCoordinate()
|
|
if coordFriendly then
|
|
self:T({"TACTICS: "..self.Groupname.." is checking if "..friendlyTable[i].Groupname.." can provide support.",RespondToSupport = friendlyTable[i].RespondToSupport, State = friendlyTable[i]:GetState()})
|
|
if friendlyTable[i].Groupname ~= self.Groupname and friendlyTable[i].RespondToSupport then
|
|
if friendlyTable[i]:Is("Idle") or friendlyTable[i]:Is("Travelling") then
|
|
local distFriendly = coordFriendly:Get3DDistance(coordGroup)
|
|
self:T("TACTICS: "..friendlyTable[i].Groupname.." is "..distFriendly.."m / "..self.SupportRadius.."m from "..self.Groupname)
|
|
if distFriendly <= self.SupportRadius then
|
|
local distTarget = coordFriendly:Get3DDistance(coordTarget)
|
|
friendlyTable[i]:AttackTarget(coordTarget,distTarget)
|
|
table.insert(self.supportTable,friendlyTable[i])
|
|
countGroups = countGroups + 1
|
|
self:I("TACTICS: "..friendlyTable[i].Groupname.." is moving to support "..self.Groupname)
|
|
friendlyTable[i]:Supporting(self.Group,coordTarget,self.MessageCallsign)
|
|
if self.SupportGroupLimit and countGroups >= self.SupportGroupLimit then
|
|
self:I("TACTICS: "..self.Groupname.." reached the support group limit: "..self.SupportGroupLimit)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self.TimeSupportActive = true
|
|
self.TimeSupportStamp = timer.getAbsTime()
|
|
self:SupportRequested(coordTarget,unitTarget)
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [Internal Check Unit Health
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:CheckUnitLife()
|
|
local unitsRemain = 0
|
|
local willRecover = false
|
|
local unitList = self.Group:GetUnits()
|
|
if unitList and #unitList > 0 then
|
|
self:T({"TACTICS: Checking health of all units in "..self.Groupname})
|
|
for i = 1,#unitList do
|
|
unitsRemain = unitsRemain + 1
|
|
local unitLife = unitList[i]:GetLifeRelative()
|
|
self:T({"TACTICS: "..unitList[i]:GetName(),"RelativeLife = "..unitLife.."/"..self.AbandonHealth,GroupUnitCount = unitsRemain})
|
|
if unitLife < self.AbandonHealth then
|
|
unitsRemain = unitsRemain - 1
|
|
if self.AllowSelfRecover then willRecover = true end
|
|
self:T({"TACTICS: "..unitList[i]:GetName().." is below the life threshold. Crew will abandon vehicle.",GroupUnitCount = unitsRemain,SelfRecover = willRecover})
|
|
if self:Is("Retreating") then
|
|
self.Group:RouteStop()
|
|
TIMER:New(function()
|
|
self:AbandonVehicle(unitList[i])
|
|
end):Start(5)
|
|
else
|
|
self:AbandonVehicle(unitList[i])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return unitsRemain,willRecover
|
|
end
|
|
|
|
--- [Internal Abandon vehicle
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:AbandonVehicle(Unit)
|
|
local coordUnit = Unit:GetCoordinate()
|
|
local headingUnit = Unit:GetHeading()
|
|
local healthUnit = Unit:GetLife()
|
|
local nameUnit = Unit:GetName()
|
|
local countryUnit = Unit:GetCountry()
|
|
local typeUnit = Unit:GetTypeName()
|
|
local crewPosition = coordUnit:Translate(5,(headingUnit + 90)%360,false,false)
|
|
Unit:Destroy()
|
|
self:T({"TACTICS: "..nameUnit.." is being abandoned."})
|
|
self:_DeadResponse(false)
|
|
|
|
--Crew spawning logic
|
|
local spawnCrew = SPAWN:NewWithAlias(self.CrewTemplate,"Crew_"..nameUnit)
|
|
spawnCrew:OnSpawnGroup(function(_grp)
|
|
self:T({"TACTICS: ".._grp:GetName().." spawned. Checking next step.", CanRecover = self.AllowSelfRecover})
|
|
local canRecover = false
|
|
local crewName = _grp:GetName()
|
|
|
|
--Check if group can self recover
|
|
if self.Group:GetCoordinate() and self.AllowSelfRecover then
|
|
local groupUnits = self.Group:GetUnits()
|
|
local closestDist = 99999999
|
|
local closestCoord = nil
|
|
local closestUnit = nil
|
|
local closestUnitName = "invalid"
|
|
for i = 1,#groupUnits do
|
|
local coordNearby = groupUnits[i]:GetCoordinate()
|
|
local distNearby = coordNearby:Get3DDistance(crewPosition)
|
|
if distNearby < closestDist then
|
|
closestDist = distNearby
|
|
closestCoord = coordNearby
|
|
closestUnit = groupUnits[i]
|
|
canRecover = true
|
|
end
|
|
end
|
|
if canRecover then
|
|
self:T({"TACTICS: "..crewName.." will move to "..closestUnitName.." for recovery."})
|
|
_grp:RouteGroundTo(closestCoord,20,"Diamond",1)
|
|
local tbl = {}
|
|
tbl.recoverTime = TIMER:New(function()
|
|
local coordGrp = _grp:GetCoordinate()
|
|
local coordRecoverUnit = closestUnit:GetCoordinate()
|
|
if coordGrp and coordRecoverUnit then
|
|
local distGrp = coordGrp:Get3DDistance(closestCoord)
|
|
self:T2({"TACTICS: "..crewName.." is "..distGrp.."/25m from "..closestUnitName})
|
|
if distGrp <= 25 then
|
|
self:T({"TACTICS: "..crewName.." is within 25m of "..closestUnitName.." and will board the vehicle.",Distance = distGrp})
|
|
_grp:Destroy()
|
|
self:CrewRecovered(closestUnit,nameUnit)
|
|
tbl.recoverTime:Stop()
|
|
if self.TroopsAttacking then
|
|
self:_ReturnTroops()
|
|
elseif self.Destination and not self:Is("Retreating") then
|
|
self:EngageToTravel()
|
|
self:_InitiateTravel()
|
|
elseif self:Is("Retreating") then
|
|
self.Group:RouteResume()
|
|
else
|
|
self:EngageToIdle()
|
|
end
|
|
end
|
|
else
|
|
self:T({"TACTICS: "..crewName.." was destroyed."})
|
|
tbl.recoverTime:Stop()
|
|
if self.TroopsAttacking then
|
|
self:_ReturnTroops()
|
|
elseif self.Destination and not self:Is("Retreating") then
|
|
self:EngageToTravel()
|
|
self:_InitiateTravel()
|
|
elseif self:Is("Retreating") then
|
|
self.Group:RouteResume()
|
|
else
|
|
self:EngageToIdle()
|
|
end
|
|
end
|
|
end)
|
|
tbl.recoverTime:Start(20,20)
|
|
end
|
|
return self
|
|
end
|
|
|
|
--Run away and dispatch recovery vehicle if avail
|
|
if not canRecover then
|
|
local hdgEscape = math.random(90,270)
|
|
if self:Is("Retreating") then hdgEscape = math.random(270,450) end
|
|
hdgEscape = (hdgEscape + headingUnit)%360
|
|
self:T({"TACTICS: "..crewName.." will run "..self.AbandonDistance.."m away at a heading of "..hdgEscape})
|
|
local coordEscape = coordUnit:Translate(hdgEscape,self.AbandonDistance)
|
|
_grp:RouteGroundTo(coordEscape,20,"Diamond",1)
|
|
if self.RecoveryVehicle and self.RecoverySpawnZone then
|
|
self:T({"TACTICS: "..crewName.." will request a recovery vehicle pickup from their destination."})
|
|
local spawnRecovery = SPAWN:NewWithAlias(self.RecoveryVehicle,"Recovery_"..nameUnit)
|
|
spawnRecovery:OnSpawnGroup(function(_veh)
|
|
local nameRecovery = _veh:GetName()
|
|
local isRTB = false
|
|
if self.RecoveryUseRoads then
|
|
_veh:RouteGroundOnRoad(coordEscape, self.RecoverySpeed, 1, self.RecoveryFormation)
|
|
else
|
|
_veh:RouteGroundTo(coordEscape,self.RecoverySpeed,self.RecoveryFormation,1)
|
|
end
|
|
local tbl = {}
|
|
tbl.recoverTimer = TIMER:New(function()
|
|
local coordVeh = _veh:GetCoordinate()
|
|
local coordGrp = _grp:GetCoordinate()
|
|
if coordVeh then
|
|
if not isRTB then
|
|
if coordGrp then
|
|
local distRecover = coordVeh:Get3DDistance(coordGrp)
|
|
self:T2({"TACTICS: "..nameRecovery.." is "..distRecover.."/25m from "..crewName})
|
|
if distRecover < 25 then
|
|
self:T({"TACTICS: "..crewName.." is within 25m of "..nameRecovery.." and will board the vehicle.",Distance = distRecover})
|
|
_grp:Destroy()
|
|
self:CrewRecovered(_veh,nameUnit)
|
|
isRTB = true
|
|
if self.RecoveryUseRoads then
|
|
_veh:RouteGroundOnRoad(self.RecoverySpawnZone:GetCoordinate(), self.RecoverySpeed, 1, self.RecoveryFormation)
|
|
else
|
|
_veh:RouteGroundTo(self.RecoverySpawnZone:GetCoordinate(),self.RecoverySpeed,self.RecoveryFormation,1)
|
|
end
|
|
end
|
|
else
|
|
self:T({"TACTICS: "..crewName.." was destroyed. "..nameRecovery.." is RTB."})
|
|
isRTB = true
|
|
if self.RecoveryUseRoads then
|
|
_veh:RouteGroundOnRoad(self.RecoverySpawnZone:GetCoordinate(), self.RecoverySpeed, 1, self.RecoveryFormation)
|
|
else
|
|
_veh:RouteGroundTo(self.RecoverySpawnZone:GetCoordinate(),self.RecoverySpeed,self.RecoveryFormation,1)
|
|
end
|
|
end
|
|
else
|
|
if _veh:IsAnyInZone(self.RecoverySpawnZone) then
|
|
self:T({"TACTICS: "..nameRecovery.." returned to base and will despawn."})
|
|
_veh:Destroy()
|
|
tbl.recoverTimer:Stop()
|
|
end
|
|
end
|
|
else
|
|
tbl.recoverTimer:Stop()
|
|
end
|
|
end)
|
|
tbl.recoverTimer:Start(10,10)
|
|
self:T({"TACTICS: "..nameRecovery.." spawned and is moving to recover "..crewName,UseRoads = self.RecoveryUseRoads,Speed = self.RecoverySpeed,Formation = self.RecoveryFormation})
|
|
end)
|
|
spawnRecovery:SpawnInZone(self.RecoverySpawnZone,true)
|
|
end
|
|
end
|
|
end)
|
|
spawnCrew:SpawnFromCoordinate(crewPosition)
|
|
|
|
--New static spawn
|
|
self:T({"TACTICS: Replacing "..nameUnit.." with a static object."})
|
|
local vec2Unit = coordUnit:GetVec2()
|
|
local staticInfo = {
|
|
["heading"] = headingUnit * (math.pi*2) / 360,
|
|
["Country"] = countryUnit,
|
|
["y"] = vec2Unit.y,
|
|
["x"] = vec2Unit.x,
|
|
["name"] = nameUnit.."_Abandoned",
|
|
["category"] = "Ground Identifiable",
|
|
["type"] = typeUnit,
|
|
["dead"] = false,
|
|
--["livery_id"] = SaveStatics[k]["livery_id"]
|
|
}
|
|
coalition.addStaticObject(countryUnit, staticInfo)
|
|
if self.SmokeAbandoned then
|
|
coordUnit:BigSmokeSmall(0.01)
|
|
TIMER:New(function()
|
|
coordUnit:StopBigSmokeAndFire()
|
|
end):Start(self.StaticSmokeTimeout)
|
|
end
|
|
self:Abandoned(coordUnit,nameUnit,typeUnit)
|
|
end
|
|
|
|
|
|
--------[USER METHODS]---------
|
|
|
|
--- [User] Find a group by name
|
|
-- @param #TACTICS self
|
|
-- @param #string Groupname Name to find
|
|
-- @return Wrapper.Group#GROUP Group found
|
|
function TACTICS:FindByName(Groupname)
|
|
for i = 1,#TACTICS_UTILS.GroupsRed do
|
|
if TACTICS_UTILS.GroupsRed[i].Groupname == Groupname then
|
|
return TACTICS_UTILS.GroupsRed[i]
|
|
end
|
|
end
|
|
for i = 1,#TACTICS_UTILS.GroupsBlue do
|
|
if TACTICS_UTILS.GroupsBlue[i].Groupname == Groupname then
|
|
return TACTICS_UTILS.GroupsBlue[i]
|
|
end
|
|
end
|
|
BASE:E("TACTICS ERROR: Could not find a group named "..Groupname.." with Tactics capabilities enabled.")
|
|
return nil
|
|
end
|
|
|
|
--- [User] Get Group Name
|
|
-- @param #TACTICS self
|
|
-- @return #string Name Name of the group
|
|
function TACTICS:GetName()
|
|
return self.Groupname
|
|
end
|
|
|
|
|
|
--- [User] Travel To: Assigns a destination for the group to make its way to
|
|
-- @param #TACTICS self
|
|
-- @param #boolean UseRoads Set to true to use road connections
|
|
-- @param Core.Point#COORDINATE ToCoordinate Coordinate to go to
|
|
-- @param #number Speed Speed to set on the group
|
|
-- @param #string Formation Formation to be used
|
|
-- @param #number DelaySeconds Delay in seconds
|
|
-- @param #function WaypointFunction Function to execute at a waypoint
|
|
-- @param #table WaypointFunctionArguments Arguments to be passed to the prior function
|
|
-- @return #TACTICS self
|
|
function TACTICS:TravelTo(UseRoads, ToCoordinate, Speed, Formation, DelaySeconds, WaypointFunction, WaypointFunctionArguments)
|
|
if not self:Is("Dead") then
|
|
self.Destination = ToCoordinate
|
|
self.travelToStored = {UseRoads = UseRoads, ToCoordinate = ToCoordinate, Speed = Speed, Formation = Formation, DelaySeconds = DelaySeconds, WaypointFunction = WaypointFunction, WaypointFunctionArguments = WaypointFunctionArguments}
|
|
if self:Is("Idle") or self:Is("Travelling") then
|
|
self:_InitiateTravel()
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
|
|
--- [User] Stop Travel
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:StopTravel()
|
|
if not self:Is("Dead") then
|
|
if self:Is("Travelling") then
|
|
self.Group:RouteStop()
|
|
self:T("TACTICS: "..self.Groupname.." was forced to stop travelling.")
|
|
self:EndTravel(self.Group:GetAverageCoordinate(), self.Destination)
|
|
self.TimeTravelActive = false
|
|
self.Destination = nil
|
|
self.travelToStored = nil
|
|
else
|
|
self.Destination = nil
|
|
self.travelToStored = nil
|
|
self:T("TACTICS: Removing travel to from "..self.Groupname.."'s queue.")
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [User] Set tactical ROE
|
|
-- @param #TACTICS self
|
|
-- @param #string rtype Type or ROE, can be "Ignore" or "Attack" or "Hold" or "Avoid"
|
|
-- @return #TACTICS self
|
|
function TACTICS:SetTacticalROE(rtype)
|
|
if rtype == "Ignore" or rtype == "Attack" or rtype == "Hold" or rtype == "Avoid" then
|
|
self.TacticalROE = rtype
|
|
else
|
|
self:E("TACTICS ERROR: "..tostring(rtype).." is not a valid TacticalROE! Please enter one of the following: 'Attack' | 'Hold' | 'Avoid' | 'Ignore'")
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable/Disable Detection
|
|
-- @param #TACTICS self
|
|
-- @param #boolean OnOff
|
|
-- @param Core.Zone#ZONE FilterZone
|
|
-- @param #string FilterType
|
|
-- @return #TACTICS self
|
|
function TACTICS:DetectionEnabled(OnOff,FilterZone,FilterType)
|
|
if OnOff == true then
|
|
self.UseDetection = true
|
|
self.TimeDetectionActive = true
|
|
else
|
|
self.UseDetection = false
|
|
self.TimeDetectionActive = false
|
|
end
|
|
if FilterZone then self.FilterDetectionZones = FilterZone end
|
|
if FilterType then self.FilterEnemyType = FilterType end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable/Disable Rearming
|
|
-- @param #TACTICS self
|
|
-- @param #boolean OnOff
|
|
-- @param #number LowPercent
|
|
-- @param #string FullPercent
|
|
-- @param #boolean RearmGroup
|
|
-- @param Core.Point#COORDINATE BaseCoord
|
|
-- @param #boolean AllowRTB
|
|
-- @param #string SpawnTemplate
|
|
-- @param Core.Point#COORDINATE SpawnCoord
|
|
-- @param #boolean UseRoads
|
|
-- @param #number Speed
|
|
-- @param #string Formation
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableRearming(OnOff,LowPercent,FullPercent,RearmGroup,BaseCoord,AllowRTB,SpawnTemplate,SpawnCoord,UseRoads,Speed,Formation)
|
|
self.AllowRearming = OnOff
|
|
self.LowAmmoPercent = LowPercent or 0.25
|
|
self.FullAmmoPercent = FullPercent or 0.95
|
|
self.RearmGroup = GroupAlive
|
|
self.RearmTemplate = SpawnTemplate
|
|
self.RearmSpawnCoord = SpawnCoord or BaseCoord
|
|
self.DespawnRearmingGroup = false
|
|
self.RearmGroupBase = BaseCoord
|
|
self.RearmGroupRTB = AllowRTB or false
|
|
self.RearmGroupUseRoads = UseRoads or false
|
|
self.RearmGroupSpeed = Speed or 60
|
|
self.RearmGroupFormation = Formation or "Diamond"
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable Troop Transport
|
|
-- @param #TACTICS self
|
|
-- @param #string template
|
|
-- @param #number attackdist
|
|
-- @param #number attackreturn
|
|
-- @param string unitprefix
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableTroopTransport(template,attackdist,attackreturn,unitprefix)
|
|
self.TroopTransport = true
|
|
self.TroopTemplate = template
|
|
if attackdist then self.TroopAttackDist = attackdist end
|
|
if attackreturn then self.TroopReturnTime = attackreturn end
|
|
if unitprefix then self.TransportUnitPrefix = unitprefix end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable drawing rearming requests
|
|
-- @param #TACTICS self
|
|
-- @param #string Text
|
|
-- @param #number FontSize
|
|
-- @param #number Radius
|
|
-- @param #table BorderTextColor
|
|
-- @param #number BorderTextAlpha
|
|
-- @param #table FillColor
|
|
-- @param #number FillAlpha
|
|
-- @param #number LineType
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableRearmDrawings(Text,FontSize,Radius,BorderTextColor,BorderTextAlpha,FillColor,FillAlpha,LineType)
|
|
self.RearmDrawing = true
|
|
self.DrawDataR.DrawText = nil
|
|
self.DrawDataR.DrawPoly = nil
|
|
self.DrawDataR.Text = Text
|
|
self.DrawDataR.Font = FontSize
|
|
self.DrawDataR.Radius = Radius
|
|
self.DrawDataR.Color1 = BorderTextColor
|
|
self.DrawDataR.Color2 = FillColor
|
|
self.DrawDataR.Alpha1 = BorderTextAlpha
|
|
self.DrawDataR.Alpha2 = FillAlpha
|
|
self.DrawDataR.Linetype = LineType
|
|
self.DrawDataR.Coalition = 2
|
|
if self.groupCoalition == coalition.side.RED then self.DrawDataR.Coalition = 1 end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable drawing rgroups that are engaged with enemy forces
|
|
-- @param #TACTICS self
|
|
-- @param #string Text
|
|
-- @param #number FontSize
|
|
-- @param #number Radius
|
|
-- @param #table BorderTextColor
|
|
-- @param #number BorderTextAlpha
|
|
-- @param #table FillColor
|
|
-- @param #number FillAlpha
|
|
-- @param #number LineType
|
|
-- @param #boolean MarkEnemy
|
|
-- @param #number MarkFreshTime
|
|
-- @param #number MarkStaleTime
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableEngageDrawings(Text,FontSize,Radius,BorderTextColor,BorderTextAlpha,FillColor,FillAlpha,LineType,MarkEnemy,MarkFreshTime,MarkStaleTime)
|
|
self.EngageDrawing = true
|
|
self.DrawDataE.DrawText = nil
|
|
self.DrawDataE.DrawPoly = nil
|
|
self.DrawDataE.Text = Text
|
|
self.DrawDataE.Font = FontSize
|
|
self.DrawDataE.Radius = Radius
|
|
self.DrawDataE.Color1 = BorderTextColor
|
|
self.DrawDataE.Color2 = FillColor
|
|
self.DrawDataE.Alpha1 = BorderTextAlpha
|
|
self.DrawDataE.Alpha2 = FillAlpha
|
|
self.DrawDataE.Linetype = LineType
|
|
if MarkEnemy then self.DrawDataE.MarkEnemy = MarkEnemy end
|
|
if MarkFreshTime then self.EngageDrawingFresh = MarkFreshTime end
|
|
if MarkStaleTime then self.EngageDrawingStale = MarkStaleTime end
|
|
self.DrawDataE.Coalition = 2
|
|
if self.groupCoalition == coalition.side.RED then self.DrawDataE.Coalition = 1 end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable drawing rgroups that are engaged with enemy forces
|
|
-- @param #TACTICS self
|
|
-- @param #number StaleTimeout
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableSpotDrawings(StaleTimeout)
|
|
self.DrawEnemySpots = true
|
|
if StaleTimeout then self.EnemySpotStale = StaleTimeout end
|
|
return self
|
|
end
|
|
|
|
--- [User] Retreat to the retreat zone
|
|
-- @param #TACTICS self
|
|
-- @param Core.Point#COORDINATE Destination
|
|
-- @param #boolean Despawn
|
|
-- @param #number Speed
|
|
-- @param #string Formation
|
|
-- @param #boolean UseRoads
|
|
-- @return #TACTICS self
|
|
function TACTICS:Retreat(Destination,Despawn,Speed,Formation,UseRoads)
|
|
if not self:Is("Retreating") then
|
|
if not Destination and self.RetreatZone then
|
|
self:I("RETREAT: "..self.Groupname.." will attempt to retreat to the defined retreat zone.")
|
|
Destination = self.RetreatZone:GetRandomCoordinate()
|
|
if not Speed then Speed = self.RetreatSpeed else self.RetreatSpeed = Speed end
|
|
if not Formation then Formation = self.RetreatFormation else self.RetreatFormation = Formation end
|
|
if not UseRoads then UseRoads = false end
|
|
if not Despawn then Despawn = false end
|
|
self.retreatDestination = Destination
|
|
self.TimeTravelActive = false
|
|
self.TimeEngageActive = false
|
|
self.TimeHoldActive = false
|
|
self.TimeAvoidActive = false
|
|
self.TimeAttackActive = false
|
|
self.TimeDetectionActive = false
|
|
|
|
if self.DrawDataR.DrawText then
|
|
trigger.action.removeMark(self.DrawDataR.DrawText)
|
|
self.DrawDataR.DrawText = nil
|
|
end
|
|
if self.DrawDataR.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataR.DrawPoly)
|
|
self.DrawDataR.DrawPoly = nil
|
|
end
|
|
if self.DrawDataE.DrawText then
|
|
trigger.action.removeMark(self.DrawDataE.DrawText)
|
|
self.DrawDataE.DrawText = nil
|
|
end
|
|
if self.DrawDataE.DrawPoly then
|
|
trigger.action.removeMark(self.DrawDataE.DrawPoly)
|
|
self.DrawDataE.DrawPoly = nil
|
|
end
|
|
|
|
if not self.TroopsAttacking then
|
|
if self.RetreatOnRoads then
|
|
self.Group:RouteGroundOnRoad(self.retreatDestination, self.RetreatSpeed, 1, self.RetreatFormation)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
else
|
|
self.Group:RouteGroundTo(self.retreatDestination, self.a, self.RetreatFormation, 1)
|
|
self.TimeRetreatActive = true
|
|
self.TimeRetreatStamp = timer.getAbsTime()
|
|
end
|
|
else
|
|
self:_ReturnTroops()
|
|
end
|
|
self:StartRetreat(self.retreatDestination)
|
|
else
|
|
self:E("TACICS ERROR: "..self.Groupname.." attempted to retreat but no retreat zone was declared and no destination was provided.")
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable retreating
|
|
-- @param #TACTICS self
|
|
-- @param Core.Zone#ZONE RetreatZone
|
|
-- @param #number LossPercentage
|
|
-- @param #boolean DespawnAfterRetreat
|
|
-- @param #number RetreatSpeed
|
|
-- @param #string RetreatFormation
|
|
-- @param #boolean UseRoads
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableRetreating(RetreatZone,LossPercentage,DespawnAfterRetreat,RetreatSpeed,RetreatFormation,UseRoads)
|
|
if RetreatZone then
|
|
self.RetreatZone = RetreatZone
|
|
else
|
|
self:E("TACTICS WARNING: No retreat zone declared when retreating was enabled for "..self.Groupname..". Retreating can only be conducted if executed manually.")
|
|
end
|
|
if LossPercentage then self.RetreatAfterLosses = LossPercentage end
|
|
if RetreatSpeed then self.RetreatSpeed = RetreatSpeed end
|
|
if RetreatFormation then self.RetreatFormation = RetreatFormation end
|
|
if UseRoads or UseRoads == false then self.RetreatOnRoads = UseRoads end
|
|
if DespawnAfterRetreat or DespawnAfterRetreat == false then self.DespawnAfterRetreat = DespawnAfterRetreat end
|
|
self:T({"TACTCS: Retreating enabled for "..self.Groupname,RetreatZone = RetreatZone,LossPercentage = LossPercentage,RetreatSpeed = RetreatSpeed,RetreatFormation = RetreatFormation,UseRoads = UseRoads,DespawnAfterRetreat = DespawnAfterRetreat})
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable Abandoning
|
|
-- @param #TACTICS self
|
|
-- @param #string CrewTemplate
|
|
-- @param #boolean AllowSelfRecover
|
|
-- @param #string RecoveryVehicle
|
|
-- @param Core.Zone#ZONE RecoverySpawnZone
|
|
-- @param #boolean SmokeAbandoned
|
|
-- @param #number AbandonHealth
|
|
-- @param #number AbandonDistance
|
|
-- @param #boolean RecoveryUseRoads
|
|
-- @param #number RecoverySpeed
|
|
-- @param #string RecoveryFormation
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableAbandon(CrewTemplate,AllowSelfRecover,RecoveryVehicle,RecoverySpawnZone,SmokeAbandoned,AbandonHealth,AbandonDistance,RecoveryUseRoads,RecoverySpeed,RecoveryFormation)
|
|
if CrewTemplate then
|
|
self.AbandonEnabled = true
|
|
self.CrewTemplate = CrewTemplate
|
|
if AllowSelfRecover then self.AllowSelfRecover = AllowSelfRecover end
|
|
if RecoveryVehicle then self.RecoveryVehicle = RecoveryVehicle end
|
|
if RecoverySpawnZone then self.RecoverySpawnZone = RecoverySpawnZone end
|
|
if SmokeAbandoned then self.SmokeAbandoned = true else self.SmokeAbandoned = false end
|
|
if AbandonHealth then self.AbandonHealth = AbandonHealth end
|
|
if AbandonDistance then self.AbandonDistance = AbandonDistance end
|
|
if RecoveryUseRoads then self.RecoveryUseRoads = true else self.RecoveryUseRoads = false end
|
|
if RecoverySpeed then self.RecoverySpeed = RecoverySpeed end
|
|
if RecoveryFormation then self.RecoveryFormation = RecoveryFormation end
|
|
else
|
|
self:E("TACTICS ERROR: A crew template is required for vehicle abandoning!")
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [User] Set Detection Range
|
|
-- @param #TACTICS self
|
|
-- @param #number val
|
|
-- @return #TACTICS self
|
|
function TACTICS:SetDetectionMax(val)
|
|
self.zoneDetection:SetRadius(val)
|
|
self.MaxDetection = val
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable Message Output
|
|
-- @param #TACTICS self
|
|
-- @param #boolean OnOff
|
|
-- @param #string Callsign
|
|
-- @param #string sound
|
|
-- @param #number duration
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableMessageOutput(OnOff,Callsign,Sound,Duration)
|
|
self.MessageOutput = OnOff or true
|
|
if Callsign then self.MessageCallsign = Callsign end
|
|
if Sound then self.MessageSound = Sound end
|
|
if Duration then self.MessageDuration = Duration end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable Support Requests
|
|
-- @param #TACTICS self
|
|
-- @param #boolean OnOff
|
|
-- @param #number Radius
|
|
-- @param #number GroupLimit
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableSupportRequest(OnOff,Radius,GroupLimit)
|
|
self.AllowSupportRequests = OnOff or true
|
|
if Radius then self.SupportRadius = Radius end
|
|
if GroupLimit then self.SupportGroupLimit = GroupLimit end
|
|
return self
|
|
end
|
|
|
|
--- [User] Enable Support Response
|
|
-- @param #TACTICS self
|
|
-- @param #boolean OnOff
|
|
-- @return #TACTICS self
|
|
function TACTICS:EnableSupportRespond(OnOff)
|
|
self.RespondToSupport = OnOff or true
|
|
return self
|
|
end
|
|
|
|
--- [User] Pause timers, halt group, and disable AI
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:Sleep()
|
|
if not self.Sleeping then
|
|
if self.MasterTime then
|
|
self.MasterTime:Stop()
|
|
self.Group:RouteStop()
|
|
self.Group:SetAIOff()
|
|
self.Sleeping = true
|
|
else
|
|
self.E("Failed attempt to sleep "..self.Groupname.." as it is no longer active!")
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- [User] Resume timers after 5 seconds and enable AI immediately
|
|
-- @param #TACTICS self
|
|
-- @return #TACTICS self
|
|
function TACTICS:Wake()
|
|
if self.Sleeping then
|
|
if self.MasterTime then
|
|
self.MasterTime:Start(5,self.TickRate)
|
|
self.Group:SetAIOn()
|
|
self.Sleeping = false
|
|
else
|
|
self.E("Failed attempt to wake "..self.Groupname.." as it is no longer active!")
|
|
end
|
|
end
|
|
return self
|
|
end
|