--- **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 -- -- ![Process](..\Presentations\TACTICS\image.png) -- -- 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. -- -- ![Process](..\Presentations\TACTICS\Tactics_Example_01.png) -- -- -- # 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