From 17bfcf837368f60b3afcd633cf4f84953cc58e10 Mon Sep 17 00:00:00 2001 From: svenvandevelde Date: Sun, 6 Mar 2016 08:50:28 +0100 Subject: [PATCH] Rework of SPAWN - Visible Array - Internal table in SPAWN GROUP functions - Route - RouteToZone - CopyRoute SPAWN functions - SpawnFromUnit - SpawnInZone Replaced SpawnFromCarrier overall --- Moose/Base.lua | 4 +- Moose/Cargo.lua | 32 +- Moose/Client.lua | 24 +- Moose/Database.lua | 8 +- Moose/Group.lua | 177 +++++- Moose/Mission.lua | 10 +- Moose/Routines.lua | 52 +- Moose/Spawn.lua | 850 +++++++++++++++++------------ Moose/Stage.lua | 22 +- Moose/Unit.lua | 36 +- Moose/Zone.lua | 12 + Test Missions/MOOSE_Spawn_Test.lua | 84 ++- Test Missions/MOOSE_Spawn_Test.miz | Bin 19439 -> 42180 bytes 13 files changed, 872 insertions(+), 439 deletions(-) diff --git a/Moose/Base.lua b/Moose/Base.lua index dfc1d888a..cbbfda995 100644 --- a/Moose/Base.lua +++ b/Moose/Base.lua @@ -10,8 +10,8 @@ _TraceClass = { --SEAD = true, --DESTROYBASETASK = true, --MOVEMENT = true, - --SPAWN = true, - --GROUP = true, + SPAWN = true, + GROUP = true, --UNIT = true, } diff --git a/Moose/Cargo.lua b/Moose/Cargo.lua index 3ff9617a8..de3eb4eec 100644 --- a/Moose/Cargo.lua +++ b/Moose/Cargo.lua @@ -52,13 +52,13 @@ function CARGO_ZONE:Spawn() self:T( CargoHostSpawn ) if self.CargoHostSpawn then - local CargoHostGroup = Group.getByName( self.CargoHostSpawn:SpawnGroupName() ) + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex():GetDCSGroup() if CargoHostGroup then if not CargoHostGroup:isExist() then - self.CargoHostSpawn:ReSpawn() + self.CargoHostSpawn:ReSpawn(1) end else - self.CargoHostSpawn:ReSpawn() + self.CargoHostSpawn:ReSpawn(1) end end @@ -70,7 +70,7 @@ function CARGO_ZONE:GetHostUnit() if self.CargoHostName then -- A Host has been given, signal the host - local CargoHostGroup = Group.getByName( self.CargoHostSpawn:SpawnGroupName() ) + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex():GetDCSGroup() local CargoHostUnit if CargoHostGroup == nil then CargoHostUnit = StaticObject.getByName( self.CargoHostName ) @@ -253,8 +253,8 @@ end function CARGO_ZONE:GetCargoHostUnit() self:T() - local CargoHostUnit = Group.getByName( self.CargoHostSpawn:SpawnGroupName() ):getUnit(1) - if CargoHostUnit and CargoHostUnit:isExist() then + local CargoHostUnit = self.CargoHostSpawn:GetGroupFromIndex(1):GetUnit(1) + if CargoHostUnit and CargoHostUnit:IsAlive() then return CargoHostUnit end @@ -347,7 +347,7 @@ self:T() local Valid = true self.CargoClient = Client - local ClientUnit = Client:GetClientGroupUnit() + local ClientUnit = Client:GetClientGroupDCSUnit() return Valid end @@ -499,10 +499,10 @@ self:T() if SpawnCargo then if self.CargoZone:GetCargoHostUnit() then --- ReSpawn the Cargo from the CargoHost - self.CargoGroupName = self.CargoSpawn:FromHost( self.CargoZone:GetCargoHostUnit(), 60, 30, self.CargoName, false ).name + self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30 ):GetName() else --- ReSpawn the Cargo in the CargoZone without a host ... - self.CargoGroupName = self.CargoSpawn:InZone( self.CargoZone:GetCargoZoneName(), self.CargoName ).name + self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone ):GetName() end self:StatusNone() end @@ -534,7 +534,7 @@ self:T() local Valid = true - local ClientUnit = Client:GetClientGroupUnit() + local ClientUnit = Client:GetClientGroupDCSUnit() local CarrierPos = ClientUnit:getPoint() local CarrierPosMove = ClientUnit:getPoint() @@ -630,8 +630,10 @@ self:T() self:T( 'self.CargoName = ' .. self.CargoName ) self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - self.CargoSpawn:FromCarrier( Client:GetClientGroupUnit(), TargetZoneName, self.CargoGroupName ) + --self.CargoSpawn:FromCarrier( Client:GetClientGroupDCSUnit(), TargetZoneName, self.CargoGroupName ) + self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), self.CargoGroupName ):RouteToZone( ZONE:New( TargetZoneName ) ) + self:StatusUnLoaded() return self @@ -666,7 +668,7 @@ self:T() if not self.CargoClientInitGroupSpawn then self.CargoClientInitGroupSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ) end - self.CargoClientInitGroupSpawn:Spawn( self.CargoClient:GetClientGroupName() ) + self.CargoClientInitGroupSpawn:ReSpawn( 1 ) end local SpawnCargo = true @@ -705,7 +707,7 @@ self:T() self:T( self.CargoClient.ClientName ) self:T( 'Client Exists.' ) - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupUnit(), Client:ClientPosition(), 150 ) then + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:ClientPosition(), 150 ) then Near = true end end @@ -720,7 +722,7 @@ self:T() local Valid = true - local ClientUnit = Client:GetClientGroupUnit() + local ClientUnit = Client:GetClientGroupDCSUnit() local CarrierPos = ClientUnit:getPoint() local CarrierPosMove = ClientUnit:getPoint() @@ -812,7 +814,7 @@ self:T() local OnBoarded = false if self.CargoClient and self.CargoClient:ClientGroup() then - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupUnit(), self.CargoClient:ClientPosition(), 10 ) then + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:ClientPosition(), 10 ) then -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. self:StatusLoaded( Client ) diff --git a/Moose/Client.lua b/Moose/Client.lua index fc74435b1..3da8dbbc5 100644 --- a/Moose/Client.lua +++ b/Moose/Client.lua @@ -175,6 +175,24 @@ self:T() local ClientGroup = self:ClientGroup() + if ClientGroup then + if ClientGroup:isExist() then + return UNIT:New( ClientGroup:getUnit(1) ) + else + return UNIT:New( self.ClientGroupUnit ) + end + end + + return nil +end + +--- Returns the DCSUnit of the @{CLIENT}. +-- @treturn DCSUnit +function CLIENT:GetClientGroupDCSUnit() +self:T() + + local ClientGroup = self:ClientGroup() + if ClientGroup then if ClientGroup:isExist() then return ClientGroup:getUnits()[1] @@ -189,7 +207,7 @@ end function CLIENT:GetUnit() self:T() - return UNIT:New( self:GetClientGroupUnit() ) + return UNIT:New( self:GetClientGroupDCSUnit() ) end @@ -198,7 +216,7 @@ end function CLIENT:ClientPosition() --self:T() - ClientGroupUnit = self:GetClientGroupUnit() + ClientGroupUnit = self:GetClientGroupDCSUnit() if ClientGroupUnit then if ClientGroupUnit:isExist() then @@ -294,7 +312,7 @@ self:T() end MESSAGE:New( Message, MessageCategory, MessageDuration, MessageId ):ToClient( self ) else - if self:GetClientGroupUnit() and not self:GetClientGroupUnit():inAir() then + if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then if timer.getTime() - self.Messages[MessageId].MessageTime >= self.Messages[MessageId].MessageDuration + 10 then MESSAGE:New( Message, MessageCategory, MessageDuration, MessageId ):ToClient( self ) self.Messages[MessageId].MessageTime = timer.getTime() diff --git a/Moose/Database.lua b/Moose/Database.lua index 9f41192a4..21a6374bc 100644 --- a/Moose/Database.lua +++ b/Moose/Database.lua @@ -5,6 +5,7 @@ Include.File( "Routines" ) Include.File( "Base" ) Include.File( "Menu" ) +Include.File( "Group" ) DATABASE = { ClassName = "DATABASE", @@ -138,6 +139,9 @@ function DATABASE:Spawn( SpawnTemplate ) self:_RegisterGroup( SpawnTemplate ) coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + local SpawnGroup = GROUP:New( Group.getByName( SpawnTemplate.name ) ) + return SpawnGroup end @@ -206,7 +210,7 @@ end --- Track DCSRTE DEAD or CRASH events for the internal scoring. function DATABASE:OnDeadOrCrash( event ) - self:T( { event } ) + --self:T( { event } ) local TargetUnit = nil local TargetGroup = nil @@ -241,7 +245,7 @@ function DATABASE:OnDeadOrCrash( event ) TargetUnitCategory = DATABASECategory[TargetCategory] TargetUnitType = TargetType - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + --self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) end for PlayerName, PlayerData in pairs( self.Players ) do diff --git a/Moose/Group.lua b/Moose/Group.lua index 48971a3ba..49d14ba12 100644 --- a/Moose/Group.lua +++ b/Moose/Group.lua @@ -13,13 +13,14 @@ GROUP = { ClassName="GROUP", } -function GROUP:New( _Group ) +function GROUP:New( DCSGroup ) local self = BASE:Inherit( self, BASE:New() ) - self:T( _Group:getName() ) + self:T( DCSGroup:getName() ) - self._Group = _Group - self.GroupName = _Group:getName() - self.GroupID = _Group:getID() + self.DCSGroup = DCSGroup + self.GroupName = DCSGroup:getName() + self.GroupID = DCSGroup:getID() + self.Controller = DCSGroup:getController() return self end @@ -29,13 +30,29 @@ function GROUP:NewFromName( GroupName ) local self = BASE:Inherit( self, BASE:New() ) self:T( GroupName ) - self._Group = Group.getByName( GroupName ) - self.GroupName = self._Group:getName() - self.GroupID = self._Group:getID() + self.DCSGroup = Group.getByName( GroupName ) + self.GroupName = self.DCSGroup:getName() + self.GroupID = self.DCSGroup:getID() return self end +function GROUP:GetDCSGroup() + self:T( { self.GroupName } ) + return self.DCSGroup +end + +function GROUP:GetDCSUnit( UnitNumber ) + self:T( { self.GroupName, UnitNumber } ) + return self.DCSGroup:getUnit( UnitNumber ) + +end + +function GROUP:Activate() + self:T( { self.GroupName } ) + trigger.action.activateGroup( self:GetDCSGroup() ) + return self:GetDCSGroup() +end function GROUP:GetName() self:T( self.GroupName ) @@ -43,40 +60,59 @@ function GROUP:GetName() return self.GroupName end +function GROUP:GetPoint() + self:T( self.GroupName ) + + local GroupPoint = self:GetUnit(1):GetPoint() + self:T( GroupPoint ) + return GroupPoint +end + function GROUP:Destroy() self:T( self.GroupName ) - for Index, UnitData in pairs( self._Group:getUnits() ) do + for Index, UnitData in pairs( self.DCSGroup:getUnits() ) do self:CreateEventCrash( timer.getTime(), UnitData ) end - self._Group:destroy() + self.DCSGroup:destroy() end + + function GROUP:GetUnit( UnitNumber ) - self:T( self.GroupName ) - return UNIT:New( self._Group:getUnit( UnitNumber ) ) + self:T( { self.GroupName, UnitNumber } ) + return UNIT:New( self.DCSGroup:getUnit( UnitNumber ) ) end function GROUP:IsAir() self:T() - local IsAirResult = self._Group:getCategory() == Group.Category.AIRPLANE or self._Group:getCategory() == Group.Category.HELICOPTER + local IsAirResult = self.DCSGroup:getCategory() == Group.Category.AIRPLANE or self.DCSGroup:getCategory() == Group.Category.HELICOPTER self:T( IsAirResult ) return IsAirResult end +function GROUP:IsAlive() +self:T() + + local IsAliveResult = self.DCSGroup and self.DCSGroup:isExist() + + self:T( IsAliveResult ) + return IsAliveResult +end + function GROUP:AllOnGround() self:T() local AllOnGroundResult = true - for Index, UnitData in pairs( self._Group:getUnits() ) do + for Index, UnitData in pairs( self.DCSGroup:getUnits() ) do if UnitData:inAir() then AllOnGroundResult = false end @@ -92,7 +128,7 @@ self:T() local MaxVelocity = 0 - for Index, UnitData in pairs( self._Group:getUnits() ) do + for Index, UnitData in pairs( self.DCSGroup:getUnits() ) do local Velocity = UnitData:getVelocity() local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) @@ -129,13 +165,13 @@ end function GROUP:Embarking( Point, Duration, EmbarkingGroup ) -trace.f( self.ClassName, { self.GroupName, Point, Duration, EmbarkingGroup._Group } ) +trace.f( self.ClassName, { self.GroupName, Point, Duration, EmbarkingGroup.DCSGroup } ) local Controller = self:_GetController() trace.i( self.ClassName, EmbarkingGroup.GroupID ) - trace.i( self.ClassName, EmbarkingGroup._Group:getID() ) - trace.i( self.ClassName, EmbarkingGroup._Group.id ) + trace.i( self.ClassName, EmbarkingGroup.DCSGroup:getID() ) + trace.i( self.ClassName, EmbarkingGroup.DCSGroup.id ) Controller:pushTask( { id = 'Embarking', params = { x = Point.x, @@ -169,9 +205,112 @@ trace.f( self.ClassName, { self.GroupName, Point, Radius } ) return self end +function GROUP:Route( GoPoints ) +self:T( GoPoints ) + + local Points = routines.utils.deepCopy( GoPoints ) + local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } + + --self.Controller.setTask( self.Controller, MissionTask ) + + routines.scheduleFunction( self.Controller.setTask, { self.Controller, MissionTask}, timer.getTime() + 1 ) + + return self +end + +function GROUP:RouteToZone( Zone, Randomize, Speed, Formation ) + self:T( Zone ) + + local GroupPoint = self:GetPoint() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Cone" + PointFrom.speed = 20 / 1.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomPoint() + else + ZonePoint = Zone:GetPoint() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 1.6 + end + + local Points = { PointFrom, PointTo } + + self:T( Points ) + + self:Route( Points ) + + return self +end + +function GROUP:CopyRoute( Begin, End, Randomize, Radius ) +self:T( { Begin, End } ) + + local Points = {} + + -- Could be a Spawned Group + local GroupName = string.match( self:GetName(), ".*#" ) + if GroupName then + GroupName = GroupName:sub( 1, -2 ) + else + GroupName = self:GetName() + end + + self:T( { GroupName } ) + + local Template = _Database.Groups[GroupName].Template + + if Template then + if not Begin then + Begin = 0 + end + if not End then + End = 0 + end + + for TPointID = Begin + 1, #Template.route.points - End do + if Template.route.points[TPointID] then + Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) + if Randomize then + if not Radius then + Radius = 500 + end + Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) + Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) + end + end + end + return Points + end + + return nil +end + function GROUP:_GetController() - return self._Group:getController() + return self.DCSGroup:getController() end diff --git a/Moose/Mission.lua b/Moose/Mission.lua index 3d699212f..63f5a12b8 100644 --- a/Moose/Mission.lua +++ b/Moose/Mission.lua @@ -174,12 +174,12 @@ function MISSION:ReportToAll() local AlivePlayers = '' for ClientID, Client in pairs( self._Clients ) do if Client:ClientGroup() then - if Client:GetClientGroupUnit() then - if Client:GetClientGroupUnit():getLife() > 0.0 then + if Client:GetClientGroupDCSUnit() then + if Client:GetClientGroupDCSUnit():getLife() > 0.0 then if AlivePlayers == '' then - AlivePlayers = ' Players: ' .. Client:GetClientGroupUnit():getPlayerName() + AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() else - AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupUnit():getPlayerName() + AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() end end end @@ -486,7 +486,7 @@ trace.scheduled("MISSIONSCHEDULER","Scheduler") if Mission.GoalFunction ~= nil then Mission.GoalFunction( Mission, Client ) end - _Database:_AddMissionTaskScore( Client:GetClientGroupUnit(), Mission.Name, 25 ) + _Database:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) -- if not Mission:IsCompleted() then -- end diff --git a/Moose/Routines.lua b/Moose/Routines.lua index bdcc7924f..c591ef9c4 100644 --- a/Moose/Routines.lua +++ b/Moose/Routines.lua @@ -66,41 +66,51 @@ routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all tbl_str[#tbl_str + 1] = '{' for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} if type(ind) == "number" then - tbl_str[#tbl_str + 1] = '[' - tbl_str[#tbl_str + 1] = tostring(ind) - tbl_str[#tbl_str + 1] = ']=' + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' else --must be a string - tbl_str[#tbl_str + 1] = '[' - tbl_str[#tbl_str + 1] = routines.utils.basicSerialize(ind) - tbl_str[#tbl_str + 1] = ']=' + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' end + local val_str = {} if ((type(val) == 'number') or (type(val) == 'boolean')) then - tbl_str[#tbl_str + 1] = tostring(val) - tbl_str[#tbl_str + 1] = ',' - elseif type(val) == 'string' then - tbl_str[#tbl_str + 1] = routines.utils.basicSerialize(val) - tbl_str[#tbl_str + 1] = ',' + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) elseif type(val) == 'nil' then -- won't ever happen, right? - tbl_str[#tbl_str + 1] = 'nil,' + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) elseif type(val) == 'table' then if ind == "__index" then - tbl_str[#tbl_str + 1] = "__index" - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it else - tbl_str[#tbl_str + 1] = _Serialize(val) - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) end elseif type(val) == 'function' then - tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it else - env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) - env.info( debug.traceback() ) +-- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) +-- env.info( debug.traceback() ) end - + end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) diff --git a/Moose/Spawn.lua b/Moose/Spawn.lua index 3885e0553..ab73efea5 100644 --- a/Moose/Spawn.lua +++ b/Moose/Spawn.lua @@ -8,6 +8,7 @@ Include.File( "Routines" ) Include.File( "Base" ) Include.File( "Database" ) Include.File( "Group" ) +Include.File( "Zone" ) SPAWN = { @@ -33,15 +34,28 @@ function SPAWN:New( SpawnPrefix ) local TemplateGroup = Group.getByName( SpawnPrefix ) if TemplateGroup then self.SpawnPrefix = SpawnPrefix + self.SpawnIndex = 0 self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnScheduled = false -- Reflects if the spawning for this SpawnPrefix is going to be scheduled or not. + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnPrefix is going to be scheduled or not. self.SpawnTemplate = self._GetTemplate( self, SpawnPrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! self.SpawnRepeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. self.SpawnMaxGroupsAlive = 0 -- The maximum amount of groups that can be alive of SpawnPrefix at the same time. self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + self.SpawnGroups[1] = {} + self.SpawnGroups[1].Visible = false + self.SpawnGroups[1].Spawned = false + self.SpawnGroups[1].UnControlled = false + self.SpawnGroups[1].Spawned = false + self.SpawnGroups[1].SpawnTime = 0 + + self.SpawnGroups[1].SpawnPrefix = self.SpawnPrefix + self.SpawnGroups[1].SpawnTemplate = self:_Prepare( self.SpawnPrefix, 1 ) else error( "SPAWN:New: There is no group declared in the mission editor with SpawnPrefix = '" .. SpawnPrefix .. "'" ) end @@ -55,6 +69,29 @@ function SPAWN:New( SpawnPrefix ) return self end + +function SPAWN:Limit( SpawnMaxGroupsAlive, SpawnMaxGroups ) +self:T( { SpawnMaxGroupsAlive, SpawnMaxGroups } ) + + self.SpawnMaxGroupsAlive = SpawnMaxGroupsAlive -- The maximum amount of groups that can be alive of SpawnPrefix at the same time. + self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID] = {} + self.SpawnGroups[SpawnGroupID].Visible = false + self.SpawnGroups[SpawnGroupID].Spawned = false + self.SpawnGroups[SpawnGroupID].UnControlled = false + self.SpawnGroups[SpawnGroupID].Spawned = false + self.SpawnGroups[SpawnGroupID].SpawnTime = 0 + + self.SpawnGroups[SpawnGroupID].SpawnPrefix = self.SpawnPrefix + self.SpawnGroups[SpawnGroupID].SpawnTemplate = self:_Prepare( self.SpawnPrefix, SpawnGroupID ) + end + + return self +end + + --- Randomizes a defined route of the Template Group in the ME when the Group is Spawned. This is very useful to define extra variation in the DCS World run-time environment of the behaviour of Groups like Ground Units, Ships, Planes, Helicopters. -- @tparam number SpawnStartPoint is the waypoint where the randomization begins. Note that the StartPoint = 0 equals the point where the Group is Spawned. This parameter is useful to avoid randomization to start from the first waypoint, but a bit further down the route... -- @tparam number SpawnEndPoint is the waypoint where the randomization ends. this parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. @@ -70,91 +107,20 @@ end function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) self:T( { SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) - self.SpawnStartPoint = SpawnStartPoint -- When the spawning occurs, randomize the route points from SpawnStartPoint. - self.SpawnEndPoint = SpawnEndPoint -- When the spawning occurs, randomize the route points till SpawnEndPoint. - self.SpawnRadius = SpawnRadius -- The Radius of randomization of the route points from SpawnStartPoint till SpawnEndPoint. - self.SpawnRandomize = true - - return self -end - ---- SPAWNs a new Group within varying time intervals. This is useful if you want to have continuity within your missions of certain (AI) Groups to be present (alive) within your missions. --- @tparam number SpawnTime is the time interval defined in seconds between each new SPAWN of new Groups. --- @tparam number SpawnTimeVariation is the variation to be applied on the defined time interval between each new SPAWN. The variation is defined as a value between 0 and 1, which expresses the %-tage of variation to be applied as the low and high time interval boundaries. Between these boundaries a new time interval will be applied. See usage. --- @treturn SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new SPAWN of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) - -function SPAWN:Schedule( SpawnTime, SpawnTimeVariation ) -self:T( { SpawnTime, SpawnTimeVariation } ) - - self.SpawnCurrentTimer = 0 -- The internal timer counter to trigger a scheduled spawning of SpawnPrefix. - self.SpawnSetTimer = 0 -- The internal timer value when a scheduled spawning of SpawnPrefix occurs. - self.AliveFactor = 1 -- - self.SpawnLowTimer = 0 - self.SpawnHighTimer = 0 - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - self.SpawnLowTimer = SpawnTime - SpawnTime / 2 * SpawnTimeVariation - self.SpawnHighTimer = SpawnTime + SpawnTime / 2 * SpawnTimeVariation - self:ScheduleStart() - end - - self:T( { self.SpawnLowTimer, self.SpawnHighTimer } ) - - return self -end - ---- Will start the SPAWNing timers. --- This function is called automatically when @{Schedule} is called. -function SPAWN:ScheduleStart() -self:T() - - --local ClientUnit = #AlivePlayerUnits() - - self.AliveFactor = 10 -- ( 10 - ClientUnit ) / 10 - - if self.SpawnScheduled == false then - self.SpawnScheduled = true - self.SpawnInit = true - self.SpawnSetTimer = math.random( self.SpawnLowTimer * self.AliveFactor / 10 , self.SpawnHighTimer * self.AliveFactor / 10 ) + for GroupID = 1, self.SpawnMaxGroups do - self.SpawnFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 1 ) + local SpawnTemplate = self.SpawnGroups[GroupID].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = SpawnStartPoint + 1, ( RouteCount - SpawnEndPoint ) do + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( SpawnRadius * -1, SpawnRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( SpawnRadius * -1, SpawnRadius ) + SpawnTemplate.route.points[t].alt = nil + --SpawnGroup.route.points[t].alt_type = nil + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end end -end - ---- Will stop the scheduled SPAWNing activity. -function SPAWN:ScheduleStop() -self:T() - self.SpawnScheduled = false -end - ---- Limits the Maximum amount of Units to be alive, and the maximum amount of Groups to be SPAWNed within the DCS World run-time environment. --- Note that this method is exceptionally important to balance the amount of Units alive within the DCSRTE and the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. --- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... --- @tparam number SpawnMaxGroupsAlive is the Maximum amount of Units to be alive. When there are more Units alive in the DCSRTE of SpawnPrefix, then no new SPAWN will happen of the Group, until some of these Units will be destroyed. --- @tparam number SpawnMaxGroups is the Maximum amount of Groups that can be SPAWNed from SpawnPrefix. When there are more Groups alive in the DCSRTE of SpawnPrefix, then no more SPAWNs will happen of the Group. This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which expresses no Group count limits. --- @treturn SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups SPAWNed during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) - -function SPAWN:Limit( SpawnMaxGroupsAlive, SpawnMaxGroups ) -self:T( { SpawnMaxGroupsAlive, SpawnMaxGroups } ) - - self.SpawnMaxGroupsAlive = SpawnMaxGroupsAlive -- The maximum amount of groups that can be alive of SpawnPrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - + return self end @@ -181,9 +147,19 @@ self:T( { SpawnPrefix, SpawnPrefixTable } ) self.SpawnPrefixTable = SpawnPrefixTable + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].SpawnPrefix = self.SpawnPrefixTable[ math.random( 1, #SpawnPrefixTable ) ] + self.SpawnGroups[SpawnGroupID].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnGroupID].SpawnPrefix, SpawnGroupID ) + self.SpawnGroups[SpawnGroupID].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) + self.SpawnGroups[SpawnGroupID].SpawnTemplate.x = self.SpawnTemplate.x + self.SpawnGroups[SpawnGroupID].SpawnTemplate.y = self.SpawnTemplate.y + end + return self end + + --- When a Group got SPAWNed, it has a life within the DCSRTE. For planes and helicopters, when these Units go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the DCSRTE. -- This function is used to Re-Spawn automatically (so no extra call is needed anymore) the same Group after it landed. This will enable a SPAWNed group to be Re-SPAWNed after it lands, until it is destroyed... -- Note: When the Group is respawned, it will @{ReSpawn} at the original airbase where it took off. So ensure that the paths for Groups that ReSpawn, always return to the original airbase. @@ -242,171 +218,361 @@ self:T() self.SpawnCleanUpInterval = SpawnCleanUpInterval self.SpawnCleanUpTimeStamps = {} self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, 60 ) + + return self end -function SPAWN:_SpawnCleanUpScheduler() -self:T() - local SpawnGroup = self:GetFirstAliveGroup() + +--- Makes the Groups visible before start (like a batallion). +-- @tparam number SpawnZone A @{ZONE} where the group will be positioned. The X and Y coordinates of the zone define the start position. +-- @tparam number SpawnAngle The angle in degrees how the Groups and each Unit of the Group will be positioned. +-- @tparam number SpawnFormation The formation of the Units within the Group. +-- @tparam number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @tparam number SpawnDeltaX The space between each Group on the X-axis. +-- @tparam number SpawnDeltaY The space between each Group on the Y-axis. +-- @treturn SPAWN +-- @usage +-- -- Define an array of Groups within Zone "Start". +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( ZONE:New( "Start" ), 90, "Diamond", 10, 100, 50 ) + +function SPAWN:SpawnArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) +self:T( { SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - while SpawnGroup do + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false - if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then - if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() - else - if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then - SpawnGroup:Destroy() - end + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 end - else - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnGroupID].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnGroupID].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnGroupID].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX - 10 * ( u - 1 ) + local TranslatedY = SpawnY + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnGroupID].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnGroupID].SpawnTemplate.units[u].y = SpawnRootY + RotatedY end - SpawnGroup = self:GetNextAliveGroup() + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Group = _Database:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) + + SpawnX = SpawnXIndex * SpawnDeltaX + SpawnY = SpawnYIndex * SpawnDeltaY end + return self end + --- Will SPAWN a Group whenever you want to do this. -- Note that the configuration with the above functions will apply when calling this method: Maxima, Randomization of routes, Scheduler, ... -- Uses @{DATABASE} global object defined in MOOSE. -- @treturn SPAWN -function SPAWN:Spawn( SpawnGroupName ) - self:T( { self.SpawnPrefix, SpawnGroupName } ) - local SpawnTemplate = self:_Prepare( SpawnGroupName ) - if self.SpawnRandomize then - SpawnTemplate = self:_RandomizeRoute( SpawnTemplate ) - end - _Database:Spawn( SpawnTemplate ) - if self.SpawnRepeat then - _Database:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - end - return self +function SPAWN:Spawn() + self:T( { self.SpawnPrefix } ) + + return self:SpawnWithIndex( self.SpawnIndex + 1 ) end - ---- Will SPAWN a Group with a specified index number whenever you want to do this. --- Note that the configuration with the above functions will apply when calling this method: Maxima, Randomization of routes, Scheduler, ... --- Uses @{DATABASE} global object defined in MOOSE. --- @treturn SPAWN -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:T( { self.SpawnPrefix, SpawnIndex } ) - local SpawnTemplate = self:_Prepare( self:SpawnGroupName( SpawnIndex ) ) - if self.SpawnRandomize then - SpawnTemplate = self:_RandomizeRoute( SpawnTemplate ) - end - _Database:Spawn( SpawnTemplate ) - if self.SpawnRepeat then - _Database:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - end - return self -end - - --- Will Re-SPAWN a Group based on a given GroupName. The GroupName must be a group that is already alive within the DCSRTE and should have a Group Template defined in the ME (with Late Activation flag on). -- Note that the configuration with the above functions will apply when calling this method: Maxima, Randomization of routes, Scheduler, ... -- @tparam string SpawnGroupName -- @treturn SPAWN -- Uses _Database global object defined in MOOSE. -function SPAWN:ReSpawn( SpawnGroupName ) -self:T( { SpawnGroupName } ) +function SPAWN:ReSpawn( SpawnIndex ) +self:T( { SpawnIndex } ) - local SpawnGroup = Group.getByName( SpawnGroupName ) - if SpawnGroup then - SpawnGroup:destroy() + local DCSGroup = self:GetGroupFromIndex( SpawnIndex ):GetDCSGroup() + if DCSGroup then + DCSGroup:destroy() end - local SpawnTemplate = self:_Prepare( SpawnGroupName ) + if not SpawnIndex then + SpawnIndex = 1 + end + + return self:SpawnWithIndex( SpawnIndex ) +end - -- Give the units of the Group the name following the SPAWN naming convention, so that they don't replace other units within the ME. - local SpawnUnits = table.getn( SpawnTemplate.units ) - for u = 1, SpawnUnits do - SpawnTemplate.units[u].name = string.format( '%s-%02d', SpawnGroupName, u ) - SpawnTemplate.units[u].unitId = nil +--- Will SPAWN a Group with a specified index number whenever you want to do this. +-- Note that the configuration with the above functions will apply when calling this method: Maxima, Randomization of routes, Scheduler, ... +-- Uses @{DATABASE} global object defined in MOOSE. +-- @treturn GROUP The @{GROUP} that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex ) + self:T( { self.SpawnPrefix, SpawnIndex, self.SpawnMaxGroups } ) + + if self.SpawnMaxGroups == 0 or SpawnIndex <= self.SpawnMaxGroups then + + if self.SpawnGroups[SpawnIndex].Visible then + self.SpawnGroups[SpawnIndex].Group:Activate() + else + self.SpawnGroups[SpawnIndex].Group = _Database:Spawn( self.SpawnGroups[SpawnIndex].SpawnTemplate ) + --if self.SpawnRepeat then + -- _Database:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) + --end + end + self.SpawnGroups[SpawnIndex].Spawned = true + self.SpawnIndex = SpawnIndex + else + env.info( "No more Groups to Spawn" ) end - _Database:Spawn( SpawnTemplate ) + + return self.SpawnGroups[SpawnIndex].Group +end + +--- SPAWNs a new Group within varying time intervals. This is useful if you want to have continuity within your missions of certain (AI) Groups to be present (alive) within your missions. +-- @tparam number SpawnTime is the time interval defined in seconds between each new SPAWN of new Groups. +-- @tparam number SpawnTimeVariation is the variation to be applied on the defined time interval between each new SPAWN. The variation is defined as a value between 0 and 1, which expresses the %-tage of variation to be applied as the low and high time interval boundaries. Between these boundaries a new time interval will be applied. See usage. +-- @treturn SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. +-- -- The time variation in this case will be between 450 seconds and 750 seconds. +-- -- This is calculated as follows: +-- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 +-- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 +-- -- Between these two values, a random amount of seconds will be choosen for each new SPAWN of the helicopters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) + +function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) +self:T( { SpawnTime, SpawnTimeVariation } ) + + self.SpawnCurrentTimer = 0 -- The internal timer counter to trigger a scheduled spawning of SpawnPrefix. + self.SpawnSetTimer = 0 -- The internal timer value when a scheduled spawning of SpawnPrefix occurs. + self.AliveFactor = 1 -- + self.SpawnLowTimer = 0 + self.SpawnHighTimer = 0 + + if SpawnTime ~= nil and SpawnTimeVariation ~= nil then + self.SpawnLowTimer = SpawnTime - SpawnTime / 2 * SpawnTimeVariation + self.SpawnHighTimer = SpawnTime + SpawnTime / 2 * SpawnTimeVariation + self:ScheduleStart() + end + + self:T( { self.SpawnLowTimer, self.SpawnHighTimer } ) + return self end +--- Will start the SPAWNing timers. +-- This function is called automatically when @{Schedule} is called. +function SPAWN:ScheduleStart() +self:T() + + --local ClientUnit = #AlivePlayerUnits() + + self.AliveFactor = 10 -- ( 10 - ClientUnit ) / 10 + + if self.SpawnIsScheduled == false then + self.SpawnIsScheduled = true + self.SpawnInit = true + self.SpawnSetTimer = math.random( self.SpawnLowTimer * self.AliveFactor / 10 , self.SpawnHighTimer * self.AliveFactor / 10 ) + + self.SpawnFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 1 ) + end +end + +--- Will stop the scheduled SPAWNing activity. +function SPAWN:ScheduleStop() +self:T() + self.SpawnIsScheduled = false +end + +--- Limits the Maximum amount of Units to be alive, and the maximum amount of Groups to be SPAWNed within the DCS World run-time environment. +-- Note that this method is exceptionally important to balance the amount of Units alive within the DCSRTE and the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. +-- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... +-- @tparam number SpawnMaxGroupsAlive is the Maximum amount of Units to be alive. When there are more Units alive in the DCSRTE of SpawnPrefix, then no new SPAWN will happen of the Group, until some of these Units will be destroyed. +-- @tparam number SpawnMaxGroups is the Maximum amount of Groups that can be SPAWNed from SpawnPrefix. When there are more Groups alive in the DCSRTE of SpawnPrefix, then no more SPAWNs will happen of the Group. This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which expresses no Group count limits. +-- @treturn SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups SPAWNed during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) + + + --- Will SPAWN a Group whenever you want to do this, but for AIR Groups only to be applied, and will SPAWN the Group in Uncontrolled mode... This will be similar to the Uncontrolled flag setting in the ME. -- @treturn SPAWN -function SPAWN:SpawnUncontrolled() +function SPAWN:UnControlled() self:T() self.UnControlled = true - local SpawnCountStart = self.SpawnCount + 1 - for SpawnCount = SpawnCountStart, self.SpawnMaxGroups do - local SpawnTemplate = self:_Prepare( ) - SpawnTemplate.uncontrolled = true - _Database:Spawn( SpawnTemplate ) + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = true end - self.SpawnCount = SpawnCountStart - 1 return self end ---- Will SPAWN a Group from a Carrier. This function is mostly advisable to be used if you want to simulate SPAWNing from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- @tparam Group HostUnit is the AIR unit or GROUND unit dropping or unloading the Spawn group. --- @tparam string TargetZonePrefix is the Prefix of the Zone defined in the ME where the Group should be moving to after drop. --- @tparam string NewGroupName (forgot this). --- @tparam bool LateActivate (optional) does the SPAWNing with Lateactivation on. -function SPAWN:FromHost( HostUnit, OuterRadius, InnerRadius, NewGroupName, LateActivate ) -self:T( { HostUnit, OuterRadius, InnerRadius, NewGroupName, LateActivate } ) +--- Will SPAWN a Group from a Hosting @{UNIT}. This function is mostly advisable to be used if you want to simulate SPAWNing from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning @{GROUP} is reset to the Point of the spawn. +-- You can use the returned @{GROUP} to further define the route to be followed. +-- @tparam UNIT HostUnit is the AIR unit or GROUND unit dropping or unloading the Spawn group. +-- @treturn GROUP +function SPAWN:SpawnFromUnit( HostUnit ) + self:T( { HostUnit, SpawnFormation, self.SpawnIndex } ) - local SpawnTemplate + if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - if HostUnit and HostUnit:isExist() then -- and HostUnit:getUnit(1):inAir() == false then - - SpawnTemplate = self:_Prepare( NewGroupName ) + self.SpawnIndex = self.SpawnIndex + 1 + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - if ( self.SpawnMaxGroups == 0 ) or ( self.SpawnCount <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxGroupsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxGroupsAlive * #self.SpawnTemplate.units ) or self.UnControlled then + if SpawnTemplate then + + if ( self.SpawnMaxGroups == 0 ) or ( self.SpawnCount <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxGroupsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxGroupsAlive * #self.SpawnTemplate.units ) or self.UnControlled then - if LateActivate ~= nil then - if LateActivate == true then - SpawnTemplate.lateActivation = true - SpawnTemplate.visible = true + local UnitPoint = HostUnit:GetPoint() + --for PointID, Point in pairs( SpawnTemplate.route.points ) do + --Point.x = UnitPoint.x + --Point.y = UnitPoint.y + --Point.alt = nil + --Point.alt_type = nil + --end + + SpawnTemplate.route.points = nil + SpawnTemplate.route.points = {} + SpawnTemplate.route.points[1] = {} + SpawnTemplate.route.points[1].x = UnitPoint.x + SpawnTemplate.route.points[1].y = UnitPoint.y + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].x = UnitPoint.x + SpawnTemplate.units[UnitID].y = UnitPoint.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) end + + local SpawnPos = routines.getRandPointInCircle( UnitPoint, 80, 20 ) + local Point = {} + Point.type = "Turning Point" + Point.x = SpawnPos.x + Point.y = SpawnPos.y + Point.action = "Cone" + Point.speed = 5 + + table.insert( SpawnTemplate.route.points, 2, Point ) + + return self:SpawnWithIndex( self.SpawnIndex ) end - - SpawnTemplate = self:_RandomizeRoute( SpawnTemplate ) - - local RouteCount = table.getn( SpawnTemplate.route.points ) - self:T( "RouteCount = " .. RouteCount ) - - local UnitDeployPosition = HostUnit:getPoint() - for PointID, Point in pairs( SpawnTemplate.route.points ) do - Point.x = UnitDeployPosition.x - Point.y = UnitDeployPosition.z - Point.alt = nil - Point.alt_type = nil - end - - for v = 1, table.getn( SpawnTemplate.units ) do - local SpawnPos = routines.getRandPointInCircle( UnitDeployPosition, OuterRadius, InnerRadius ) - SpawnTemplate.units[v].x = SpawnPos.x - SpawnTemplate.units[v].y = SpawnPos.y - self:T( 'SpawnTemplate.units['..v..'].x = ' .. SpawnTemplate.units[v].x .. ', SpawnTemplate.units['..v..'].y = ' .. SpawnTemplate.units[v].y ) - end - - _Database:Spawn( SpawnTemplate ) end end end - return SpawnTemplate + return nil end +--- Will spawn a Group within a given @{ZONE}. +-- @tparam ZONE The @{ZONE} where the Group is to be SPAWNed. +-- @treturn SpawnTemplate +function SPAWN:SpawnInZone( Zone ) + self:T( Zone ) + + if Zone then + + self.SpawnIndex = self.SpawnIndex + 1 + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + if ( self.SpawnMaxGroups == 0 ) or ( self.SpawnCount <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxGroupsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxGroupsAlive * #self.SpawnTemplate.units ) or self.UnControlled then + + local ZonePoint = Zone:GetPoint() + + SpawnTemplate.route.points = nil + SpawnTemplate.route.points = {} + SpawnTemplate.route.points[1] = {} + SpawnTemplate.route.points[1].x = ZonePoint.x + SpawnTemplate.route.points[1].y = ZonePoint.y + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].x = ZonePoint.x + SpawnTemplate.units[UnitID].y = ZonePoint.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + local SpawnPos = Zone:GetRandomPoint() + local Point = {} + Point.type = "Turning Point" + Point.x = SpawnPos.x + Point.y = SpawnPos.y + Point.action = "Cone" + Point.speed = 5 + + table.insert( SpawnTemplate.route.points, 2, Point ) + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + end + end + + return nil +end + + --- Will SPAWN a Group from a Carrier. This function is mostly advisable to be used if you want to simulate SPAWNing from air units, like helicopters, which are dropping infantry into a defined Landing Zone. -- @tparam Group CarrierUnit is the AIR unit or GROUND unit dropping or unloading the Spawn group. -- @tparam string TargetZonePrefix is the Prefix of the Zone defined in the ME where the Group should be moving to after drop. -- @tparam string NewGroupName (forgot this). -- @tparam bool LateActivate (optional) does the SPAWNing with Lateactivation on. function SPAWN:FromCarrier( CarrierUnit, TargetZonePrefix, NewGroupName, LateActivate ) -self:T( { CarrierUnit, TargetZonePrefix, NewGroupName, LateActivate } ) + self:T( { CarrierUnit, TargetZonePrefix, NewGroupName, LateActivate } ) local SpawnTemplate @@ -482,11 +648,54 @@ function SPAWN:SpawnGroupName( SpawnIndex ) end -function SPAWN:GetIndexFromGroup( Group ) - self:T( { self.SpawnPrefix, Group } ) +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:T( { SpawnIndex } ) - local IndexString = string.match( Group:GetName(), "#.*$" ) - local Index = tonumber( IndexString:sub(2) ) + if SpawnIndex then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + local SpawnGroup = self.SpawnGroups[1].Group + return SpawnGroup + end +end + +function SPAWN:GetGroupIndexFromDCSUnit( Unit ) + self:T() + + local IndexString = string.match( Unit:getName(), "#.*-" ):sub( 2, -2 ) + self:T( IndexString ) + + if IndexString then + local Index = tonumber( IndexString ) + self:T( { "Index:", IndexString, Index } ) + return Index + end + + return nil +end + +function SPAWN:GetGroupFromDCSUnit( DCSUnit ) + self:T() + + local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ):sub( 1, -2 ) + self:T( SpawnPrefix ) + + if self.SpawnPrefix == SpawnPrefix then + local SpawnGroupIndex = self:GetGroupIndexFromDCSUnit( DCSUnit ) + local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group + self:T( SpawnGroup ) + return SpawnGroup + end + + return nil +end + +function SPAWN:GetIndexFromGroup( SpawnGroup ) + self:T( { self.SpawnPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) + local Index = tonumber( IndexString ) self:T( IndexString, Index ) return Index @@ -500,83 +709,53 @@ end -function SPAWN:GetFirstAliveGroup() +function SPAWN:GetFirstAliveGroup( SpawnCursor ) self:T() - self.SpawnIndex = 1 for SpawnIndex = 1, self.SpawnCount do - SpawnGroupName = self:SpawnGroupName( SpawnIndex ) - SpawnGroup = Group.getByName( SpawnGroupName ) - if SpawnGroup and SpawnGroup:isExist() then - self.SpawnIndex = SpawnIndex - return GROUP:New( SpawnGroup ) + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor end end - self.SpawnIndex = nil - return nil + return nil, nil end -function SPAWN:GetNextAliveGroup() +function SPAWN:GetNextAliveGroup( SpawnCursor ) self:T() - self.SpawnIndex = self.SpawnIndex + 1 - for SpawnIndex = self.SpawnIndex, self.SpawnCount do - SpawnGroupName = self:SpawnGroupName( SpawnIndex ) - SpawnGroup = Group.getByName( SpawnGroupName ) - if SpawnGroup and SpawnGroup:isExist() then - self.SpawnIndex = SpawnIndex - return GROUP:New( SpawnGroup ) + SpawnCursor = SpawnCursor + 1 + for SpawnIndex = SpawnCursor, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor end end - - self.SpawnIndex = nil - return nil + + return nil, nil end function SPAWN:GetLastAliveGroup() -self:T() + self:T() - local LastGroupName = self:SpawnGroupName( self:GetLastIndex() ) - - return GROUP:New( Group.getByName( LastGroupName ) ) -end - ---- Will SPAWN a Group within a given ZoneName. --- @tparam string ZonePrefix is the name of the zone where the Group is to be SPAWNed. --- @treturn SpawnTemplate -function SPAWN:InZone( ZonePrefix, SpawnGroupName ) -self:T( ZonePrefix ) - - local SpawnTemplate - - if SpawnGroupName then - SpawnTemplate = self:_Prepare( SpawnGroupName ) - else - SpawnTemplate = self:_Prepare() + self.SpawnIndex = self:GetLastIndex() + for SpawnIndex = self.SpawnIndex, 1, -1 do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + self.SpawnIndex = SpawnIndex + return SpawnGroup + end end - local Zone = trigger.misc.getZone( ZonePrefix ) - local ZonePos = {} - ZonePos.x = Zone.point.x + math.random(Zone.radius * -1, Zone.radius) - ZonePos.z = Zone.point.z + math.random(Zone.radius * -1, Zone.radius) - - local RouteCount = table.getn(SpawnTemplate.route.points) - - SpawnTemplate.route.points[1].x = ZonePos.x - SpawnTemplate.route.points[1].y = ZonePos.z - SpawnTemplate.route.points[1].alt = nil - SpawnTemplate.route.points[1].alt_type = nil - - SpawnTemplate.route.points[RouteCount].x = ZonePos.x - SpawnTemplate.route.points[RouteCount].y = ZonePos.z - - _Database:Spawn( SpawnTemplate ) - - return SpawnTemplate + self.SpawnIndex = nil + return nil end + --- Private -- @section @@ -635,76 +814,34 @@ self:T( SpawnPrefix ) return SpawnTemplate end ---- Prepares the new Group Template before Spawning. -function SPAWN:_Prepare( SpawnGroupName ) -self:T() +--- Prepares the new Group Template. +function SPAWN:_Prepare( SpawnPrefix, SpawnIndex ) + self:T() - local SpawnCount - local SpawnUnits - - local SpawnTemplate = routines.utils.deepCopy( self.SpawnTemplate ) - - if self.SpawnRoute ~= nil then - local SpawnRoute = self:_GetTemplate( self.SpawnRoute ).route - SpawnTemplate.route = routines.utils.deepCopy( SpawnRoute ) - end - - -- Increase the spawn counter for the group - if SpawnGroupName then - SpawnTemplate.name = SpawnGroupName - else - self.SpawnCount = self.SpawnCount + 1 - SpawnTemplate.name = self:SpawnGroupName( self.SpawnCount ) - end - + local SpawnTemplate = routines.utils.deepCopy( self:_GetTemplate( SpawnPrefix ) ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) SpawnTemplate.groupId = nil SpawnTemplate.lateActivation = false + if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then SpawnTemplate.visible = false end + if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then SpawnTemplate.uncontrolled = false end - if self.SpawnPrefixTable ~= nil then - local SpawnTemplatePrefix = self.SpawnPrefixTable[ math.random( 1, #self.SpawnPrefixTable ) ] - SpawnTemplateRandom = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCoalitionID = SpawnTemplateRandom.SpawnCoalitionID - SpawnTemplate.SpawnCategoryID = SpawnTemplateRandom.SpawnCategoryID - SpawnTemplate.SpawnCountryID = SpawnTemplateRandom.SpawnCountryID - SpawnTemplate.units = routines.utils.deepCopy( SpawnTemplateRandom.units ) + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x + SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y end - SpawnUnits = table.getn( SpawnTemplate.units ) - for u = 1, SpawnUnits do - SpawnTemplate.units[u].name = string.format( SpawnTemplate.name .. '-%02d', u ) - SpawnTemplate.units[u].unitId = nil - SpawnTemplate.units[u].x = SpawnTemplate.route.points[1].x + math.random( -50, 50 ) - SpawnTemplate.units[u].y = SpawnTemplate.route.points[1].y + math.random( -50, 50 ) - end - - self:T( SpawnTemplate.name ) - return SpawnTemplate -end - ---- Will randomize the route of the Group Template. -function SPAWN:_RandomizeRoute( SpawnTemplate ) -self:T( SpawnTemplate.name ) - - if self.SpawnRandomize then - local RouteCount = table.getn( SpawnTemplate.route.points ) - for t = self.SpawnStartPoint+1, RouteCount - self.SpawnEndPoint do - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRadius * -1, self.SpawnRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRadius * -1, self.SpawnRadius ) - SpawnTemplate.route.points[t].alt = nil - --SpawnGroup.route.points[t].alt_type = nil - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:T( SpawnTemplate.name ) + self:T( { "Template:", SpawnTemplate } ) return SpawnTemplate + end --- Events @@ -713,16 +850,15 @@ end --- Obscolete -- @todo Need to delete this... _Database does this now ... function SPAWN:OnBirth( event ) -self:T( { event } ) if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line if event.initiator and event.initiator:getName() then - self:T( "Birth object : " .. event.initiator:getName() ) local EventPrefix = string.match( event.initiator:getName(), ".*#" ) if EventPrefix == self.SpawnPrefix .. '#' then + self:T( { "Birth event: " .. event.initiator:getName(), event } ) --MessageToAll( "Mission command: unit " .. SpawnPrefix .. " spawned." , 5, EventPrefix .. '/Event') self.AliveUnits = self.AliveUnits + 1 - self:T( self.AliveUnits ) + self:T( "Alive Units: " .. self.AliveUnits ) end end end @@ -732,59 +868,57 @@ end --- Obscolete -- @todo Need to delete this... _Database does this now ... function SPAWN:OnDeadOrCrash( event ) -self:T( { event } ) + if event.initiator and event.initiator:getName() then - self:T( "Dead object : " .. event.initiator:getName() ) local EventPrefix = string.match( event.initiator:getName(), ".*#" ) if EventPrefix == self.SpawnPrefix .. '#' then + self:T( { "Dead event: " .. event.initiator:getName(), event } ) -- local DestroyedUnit = Unit.getByName( EventPrefix ) -- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then --MessageToAll( "Mission command: unit " .. SpawnPrefix .. " crashed." , 5, EventPrefix .. '/Event') self.AliveUnits = self.AliveUnits - 1 - self:T( self.AliveUnits ) + self:T( "Alive Units: " .. self.AliveUnits ) -- end end end end ---- Will detect AIR Units landing... When the event takes place, the SPAWNed Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:OnLand( event ) -self:T( { event } ) - - if event.initiator and event.initiator:getName() then - self:T( "Landed object : " .. event.initiator:getName() ) - local EventPrefix = string.match( event.initiator:getName(), ".*#" ) - if EventPrefix == self.SpawnPrefix .. '#' then - self.Landed = true - self:T( "self.Landed = true" ) - if self.Landed and self.RepeatOnLanding then - local SpawnGroupName = Unit.getGroup(event.initiator):getName() - self:T( "ReSpawn " .. SpawnGroupName ) - self:ReSpawn( SpawnGroupName ) - end - end - end -end - --- Will detect AIR Units taking off... When the event takes place, the SPAWNed Group is registered as airborne... -- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. -- @todo Need to test for AIR Groups only... function SPAWN:OnTakeOff( event ) -self:T( { event } ) if event.initiator and event.initiator:getName() then - self:T( "TakeOff object : " .. event.initiator:getName() ) - local EventPrefix = string.match( event.initiator:getName(), ".*#" ) - if EventPrefix == self.SpawnPrefix .. '#' then + local SpawnGroup = self:GetGroupFromDCSUnit( event.initiator ) + if SpawnGroup then + self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) self:T( "self.Landed = false" ) self.Landed = false end end end +--- Will detect AIR Units landing... When the event takes place, the SPAWNed Group is registered as landed. +-- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. +-- @todo Need to test for AIR Groups only... +function SPAWN:OnLand( event ) + + if event.initiator and event.initiator:getName() then + local SpawnGroup = self:GetGroupFromDCSUnit( event.initiator ) + if SpawnGroup then + self:T( { "Landed event:" .. event.initiator:getName(), event } ) + self.Landed = true + self:T( "self.Landed = true" ) + if self.Landed and self.RepeatOnLanding then + local SpawnGroupIndex = self:GetIndexFromGroup( SpawnGroup ) + self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) + end + end + end +end + --- Will detect AIR Units shutting down their engines ... -- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the SPAWNed Group will Re-SPAWN. -- But only when the Unit was registered to have landed. @@ -792,16 +926,15 @@ end -- @see OnLand -- @todo Need to test for AIR Groups only... function SPAWN:OnEngineShutDown( event ) -self:T( { event } ) if event.initiator and event.initiator:getName() then - self:T( "EngineShutDown object : " .. event.initiator:getName() ) - local EventPrefix = string.match( event.initiator:getName(), ".*#" ) - if EventPrefix == self.SpawnPrefix .. '#' then + local SpawnGroup = self:GetGroupFromDCSUnit( event.initiator ) + if SpawnGroup then + self:T( { "EngineShutDown event: " .. event.initiator:getName(), event } ) if self.Landed and self.RepeatOnEngineShutDown then - local SpawnGroupName = Unit.getGroup(event.initiator):getName() - self:T( "ReSpawn " .. SpawnGroupName ) - self:ReSpawn( SpawnGroupName ) + local SpawnGroupIndex = self:GetIndexFromGroup( SpawnGroup ) + self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) end end end @@ -813,9 +946,9 @@ end --- This function is called automatically by the Spawning scheduler. -- It is the internal worker method SPAWNing new Groups on the defined time intervals. function SPAWN:_Scheduler() -self:T( self.SpawnPrefix ) if self.SpawnInit or self.SpawnCurrentTimer == self.SpawnSetTimer then + self:T( "Spawn Scheduler:" .. self.SpawnPrefix ) -- Validate if there are still groups left in the batch... if ( self.SpawnMaxGroups == 0 ) or ( self.SpawnCount <= self.SpawnMaxGroups ) then if self.AliveUnits < self.SpawnMaxGroupsAlive * #self.SpawnTemplate.units or self.UnControlled then @@ -823,7 +956,7 @@ self:T( self.SpawnPrefix ) self.SpawnInit = false end end - if self.SpawnScheduled == true then + if self.SpawnIsScheduled == true then --local ClientUnit = #AlivePlayerUnits() self.AliveFactor = 1 -- ( 10 - ClientUnit ) / 10 self.SpawnCurrentTimer = 0 @@ -833,3 +966,30 @@ self:T( self.SpawnPrefix ) self.SpawnCurrentTimer = self.SpawnCurrentTimer + 1 end end + +function SPAWN:_SpawnCleanUpScheduler() + self:T( "CleanUp Scheduler:" .. self.SpawnPrefix ) + + local SpawnCursor + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) + + while SpawnGroup do + + if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then + if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() + else + if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then + SpawnGroup:Destroy() + end + end + else + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + end + +end + + diff --git a/Moose/Stage.lua b/Moose/Stage.lua index 5b79bfd8a..e7bab7a29 100644 --- a/Moose/Stage.lua +++ b/Moose/Stage.lua @@ -226,7 +226,7 @@ self:T() local RouteMessage = "Fly to " self:T( Task.LandingZones ) for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do - RouteMessage = RouteMessage .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupUnit():getPoint(), true, true } ) .. ' km. ' + RouteMessage = RouteMessage .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km. ' end Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Co-Pilot: Route", 20 ) @@ -243,7 +243,7 @@ self:T() -- check if the Client is in the landing zone self:T( Task.LandingZones.LandingZoneNames ) - Task.CurrentLandingZoneName = routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.LandingZones.LandingZoneNames ) + Task.CurrentLandingZoneName = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames ) if Task.CurrentLandingZoneName then @@ -331,7 +331,7 @@ end function STAGELANDING:Validate( Mission, Client, Task ) self:T() - Task.CurrentLandingZoneName = routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.LandingZones.LandingZoneNames ) + Task.CurrentLandingZoneName = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames ) if Task.CurrentLandingZoneName then -- Client is in de landing zone. @@ -357,7 +357,7 @@ self:T() return -1 end - if Task.IsLandingRequired and Client:GetClientGroupUnit():inAir() then + if Task.IsLandingRequired and Client:GetClientGroupDCSUnit():inAir() then return 0 end @@ -397,14 +397,14 @@ end function STAGELANDED:Validate( Mission, Client, Task ) self:T() - if not routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.CurrentLandingZoneName ) then + if not routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName ) then self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) Task.Signalled = false Task:RemoveCargoMenus( Client ) return -2 end - if Task.IsLandingRequired and Client:GetClientGroupUnit():inAir() then + if Task.IsLandingRequired and Client:GetClientGroupDCSUnit():inAir() then self:T( "Client went back in the air. Go back to stage Landing." ) Task.Signalled = false return -1 @@ -464,7 +464,7 @@ function STAGEUNLOAD:Validate( Mission, Client, Task ) self:T() env.info( 'STAGEUNLOAD:Validate()' ) - if routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.CurrentLandingZoneName ) then + if routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName ) then else Task.ExecuteStage = _TransportExecuteStage.FAILED Task:RemoveCargoMenus( Client ) @@ -473,7 +473,7 @@ self:T() return 1 end - if not Client:GetClientGroupUnit():inAir() then + if not Client:GetClientGroupDCSUnit():inAir() then else Task.ExecuteStage = _TransportExecuteStage.FAILED Task:RemoveCargoMenus( Client ) @@ -578,7 +578,7 @@ self:T() self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) if not Task.IsSlingLoad then - if not routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.CurrentLandingZoneName ) then + if not routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName ) then Task:RemoveCargoMenus( Client ) Task.ExecuteStage = _TransportExecuteStage.FAILED Task.CargoName = nil @@ -587,7 +587,7 @@ self:T() return -1 end - if not Client:GetClientGroupUnit():inAir() then + if not Client:GetClientGroupDCSUnit():inAir() then else -- The carrier is back in the air, undo the loading process. Task:RemoveCargoMenus( Client ) @@ -673,7 +673,7 @@ self:T() function STAGEARRIVE:Validate( Mission, Client, Task ) self:T() - Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupUnit(), Task.LandingZones ) + Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) if ( Task.CurrentLandingZoneID ) then else return -1 diff --git a/Moose/Unit.lua b/Moose/Unit.lua index eb5eb9713..3e088e59f 100644 --- a/Moose/Unit.lua +++ b/Moose/Unit.lua @@ -12,27 +12,49 @@ UNIT = { ClassName="UNIT", } -function UNIT:New( _Unit ) +function UNIT:New( DCSUnit ) local self = BASE:Inherit( self, BASE:New() ) - self:T( _Unit:getName() ) + self:T( DCSUnit:getName() ) - self._Unit = _Unit - self.UnitName = _Unit:getName() - self.UnitID = _Unit:getID() + self.DCSUnit = DCSUnit + self.UnitName = DCSUnit:getName() + self.UnitID = DCSUnit:getID() return self end +function UNIT:IsAlive() + self:T( self.UnitName ) + + return ( self.DCSUnit and self.DCSUnit:isExist() ) +end + + function UNIT:GetCallSign() self:T( self.UnitName ) - return self._Unit:getCallsign() + return self.DCSUnit:getCallsign() end + +function UNIT:GetPoint() + self:T( self.UnitName ) + + local UnitPos = self.DCSUnit:getPosition().p + + local UnitPoint = {} + UnitPoint.x = UnitPos.x + UnitPoint.y = UnitPos.z + + self:T( UnitPoint ) + return UnitPoint +end + + function UNIT:GetPositionVec3() self:T( self.UnitName ) - local UnitPos = self._Unit:getPosition().p + local UnitPos = self.DCSUnit:getPosition().p self:T( UnitPos ) return UnitPos diff --git a/Moose/Zone.lua b/Moose/Zone.lua index a4f5fe113..32b1ac19a 100644 --- a/Moose/Zone.lua +++ b/Moose/Zone.lua @@ -30,6 +30,17 @@ trace.f( self.ClassName, ZoneName ) return self end +function ZONE:GetPoint() + self:T( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + local Point = { x = Zone.point.x, y = Zone.point.z } + + self:T( { Zone, Point } ) + + return Point +end + function ZONE:GetRandomPoint() trace.f( self.ClassName, self.ZoneName ) @@ -55,3 +66,4 @@ trace.f( self.ClassName, self.ZoneName ) return Zone.radius end + diff --git a/Test Missions/MOOSE_Spawn_Test.lua b/Test Missions/MOOSE_Spawn_Test.lua index e0ef01e5b..6d0d47275 100644 --- a/Test Missions/MOOSE_Spawn_Test.lua +++ b/Test Missions/MOOSE_Spawn_Test.lua @@ -1,15 +1,81 @@ Include.File( "Spawn" ) -SpawnTest = SPAWN:New( 'TEST' ):Schedule( 1, 1, 15, 0.4 ):Repeat() -SpawnTestPlane = SPAWN:New( 'TESTPLANE' ):Schedule( 1, 1, 15, 0.4 ):RepeatOnLanding() +-- Tests Batumi +--------------- +Spawn_Plane_Scheduled = SPAWN:New( "Spawn Plane Scheduled" ):SpawnScheduled( 30, 0.4 ) +Spawn_Helicopter_Scheduledd = SPAWN:New( "Spawn Helicopter Scheduled" ):SpawnScheduled( 30, 1 ) +Spawn_Ship_Scheduled = SPAWN:New( "Spawn Ship Scheduled" ):SpawnScheduled( 30, 0.5 ) +Spawn_Vehicle_Scheduled = SPAWN:New( "Spawn Vehicle Scheduled" ):SpawnScheduled( 30, 0.5 ) -SpawnTestShipPlane = SPAWN:New( 'SHIPPLANE' ):Schedule( 1, 1, 15, 0.4 ):RepeatOnLanding() - -SpawnTestShipHeli = SPAWN:New( 'SHIPHELI' ):Schedule( 1, 1, 15, 0.4 ):RepeatOnLanding() - -SpawnCH53E = SPAWN:New( 'VEHICLE' ) +-- Tests Tbilisi +---------------- +Spawn_Plane_Limited_Repeat = SPAWN:New( "Spawn Plane Limited Repeat" ):Limit( 1, 1 ):Repeat():Spawn() +Spawn_Plane_Limited_RepeatOnLanding = SPAWN:New( "Spawn Plane Limited RepeatOnLanding" ):Limit( 1, 1 ):Repeat():Spawn() +Spawn_Plane_Limited_RepeatOnEngineShutDown = SPAWN:New( "Spawn Plane Limited RepeatOnEngineShutDown" ):Limit( 1, 1 ):Repeat():Spawn() +Spawn_Helicopter_Limited_Repeat = SPAWN:New( "Spawn Helicopter Limited Repeat" ):Limit( 1, 1 ):Repeat():Spawn() +Spawn_Helicopter_Limited_RepeatOnLanding = SPAWN:New( "Spawn Helicopter Limited RepeatOnLanding" ):Limit( 1, 1 ):Repeat():Spawn() +Spawn_Helicopter_Limited_RepeatOnEngineShutDown = SPAWN:New( "Spawn Helicopter Limited RepeatOnEngineShutDown" ):Limit( 1, 1 ):Repeat():Spawn() -SpawnTestHelicopterCleanUp = SPAWN:New( "TEST_HELI_CLEANUP" ):Limit( 3, 100 ):Schedule( 10, 0 ):RandomizeRoute( 1, 1, 1000 ):CleanUp( 180 ) -SpawnTestVehiclesCleanUp = SPAWN:New( "TEST_AAA_CLEANUP" ):Limit( 3, 100 ):Schedule( 10, 0 ):RandomizeRoute( 1, 1, 1000 ) \ No newline at end of file +-- Tests Soganlug +Spawn_Plane_Limited_Scheduled = SPAWN:New( "Spawn Plane Limited Scheduled" ):Limit( 2, 10 ):SpawnScheduled( 30, 0 ) +Spawn_Helicopter_Limited_Scheduled = SPAWN:New( "Spawn Helicopter Limited Scheduled" ):Limit( 2, 10 ):SpawnScheduled( 30, 0 ) +Spawn_Ground_Limited_Scheduled = SPAWN:New( "Spawn Vehicle Limited Scheduled" ):Limit( 1, 20 ):SpawnScheduled( 90, 0 ) + +-- Tests Sukhumi +Spawn_Plane_Limited_Scheduled_RandomizeRoute = SPAWN:New( "Spawn Plane Limited Scheduled RandomizeRoute" ):Limit( 2, 10 ):RandomizeRoute( 1, 1, 4000 ):SpawnScheduled( 30, 0 ) +Spawn_Helicopter_Limited_Scheduled_RandomizeRoute = SPAWN:New( "Spawn Helicopter Limited Scheduled RandomizeRoute" ):Limit( 2, 10 ):RandomizeRoute( 1, 1, 4000 ):SpawnScheduled( 30, 0 ) +Spawn_Vehicle_Limited_Scheduled_RandomizeRoute = SPAWN:New( "Spawn Vehicle Limited Scheduled RandomizeRoute" ):Limit( 10, 10 ):RandomizeRoute( 1, 1, 1000 ):SpawnScheduled( 1, 0 ) + + +-- Tests Kutaisi +---------------- +-- Spawn Helicopters and Ground attack. +-- Observe when helicopters land but are not dead and are out of the danger zone, that they get removed after a while (+/- 180 seconds) and ReSpawn. +Spawn_Helicopter_Scheduled_CleanUp = SPAWN:New( "Spawn Helicopter Scheduled CleanUp" ):Limit( 3, 100 ):RandomizeRoute( 1, 1, 1000 ):CleanUp( 180 ):SpawnScheduled( 10, 0 ) +Spawn_Vehicle_Scheduled_CleanUp = SPAWN:New( "Spawn Vehicle Scheduled CleanUp" ):Limit( 3, 100 ):RandomizeRoute( 1, 1, 1000 ):SpawnScheduled( 10, 0 ) + +-- Test Matrix Visible Setup + +SpawnTestVisible = SPAWN:New( "Spawn Vehicle Visible Scheduled" ):Limit( 200, 200 ):SpawnArray( 59, 20, 20, 10 ):SpawnScheduled( 10, 0.2 ) + +Spawn_Templates_1 = { "Spawn Vehicle Visible Template A", + "Spawn Vehicle Visible Template B", + "Spawn Vehicle Visible Template C", + "Spawn Vehicle Visible Template D", + "Spawn Vehicle Visible Template E", + "Spawn Vehicle Visible Template F", + "Spawn Vehicle Visible Template G", + "Spawn Vehicle Visible Template H", + "Spawn Vehicle Visible Template I", + "Spawn Vehicle Visible Template J" + } + +Spawn_Vehicle_Visible_RandomizeTemplate_Scheduled_1 = SPAWN:New( "Spawn Vehicle Visible RandomizeTemplate Scheduled" ) + :Limit( 40, 40 ) + :RandomizeTemplate( Spawn_Templates_1 ) + :SpawnArray( 49, 20, 8, 8 ) + :SpawnScheduled( 10, 0.2 ) + + + +-- Tests Maykop +Spawn_Infantry = SPAWN:New( "Spawn Infantry" ) + :Limit( 10, 10 ) + + +Spawn_Vehicle_Host = SPAWN:New( "Spawn Vehicle Host" ) + :Limit( 10, 10 ) + :SpawnArray( 0, 5, 8, 8 ) + :SpawnScheduled( 10, 0.2 ) + + +Spawn_Vehicle_SpawnToZone = SPAWN:New( "Spawn Vehicle SpawnToZone" ) + :Limit( 10, 10 ) + +Spawn_Helicopter_SpawnToZone = SPAWN:New( "Spawn Helicopter SpawnToZone" ) + :Limit( 10, 10 ) + :SpawnScheduled( 60, 0.2 ) + + diff --git a/Test Missions/MOOSE_Spawn_Test.miz b/Test Missions/MOOSE_Spawn_Test.miz index fc2b4022b002b9ec02fe7ccf0c4fd54e38d6ae18..3a4f8dc81f87320794e58f674fe2f95b0727d1ae 100644 GIT binary patch delta 33265 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 I|L5xe03qAY4FCWD delta 10350 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 HfA0PV+e5L8