diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index b7b3b641a..7b03c5692 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3477,7 +3477,7 @@ do -- AI_A2A_DISPATCHER if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеÑ�а вверх.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колёса вверх.", DefenderGroup ) end --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit @@ -3498,7 +3498,7 @@ do -- AI_A2A_DISPATCHER if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват Ñ�амолетов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехватывая боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) elseif Squadron.Language == "DE" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) end @@ -3520,7 +3520,7 @@ do -- AI_A2A_DISPATCHER if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñ�амолеты в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", задействуя боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) end end self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) @@ -3538,7 +3538,7 @@ do -- AI_A2A_DISPATCHER if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаÑ�Ñ�ÑŒ на базу.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращение на базу.", DefenderGroup ) end end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -3569,7 +3569,7 @@ do -- AI_A2A_DISPATCHER if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñ�амолеты в поÑ�адка на базу.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", посадка на базу.", DefenderGroup ) end if Action and Action == "Destroy" then diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 5bcd94437..43d5eb358 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -47,19 +47,21 @@ function AI_CARGO:New( Carrier, CargoSet ) self:SetStartState( "Unloaded" ) - self:AddTransition( "Unloaded", "Pickup", "*" ) - self:AddTransition( "Loaded", "Deploy", "*" ) + -- Board + self:AddTransition( "Unloaded", "Pickup", "Unloaded" ) + self:AddTransition( "*", "Load", "*" ) + self:AddTransition( "*", "Reload", "*" ) + self:AddTransition( "*", "Board", "*" ) + self:AddTransition( "*", "Loaded", "Loaded" ) + self:AddTransition( "Loaded", "PickedUp", "Loaded" ) - self:AddTransition( "*", "Load", "Boarding" ) - self:AddTransition( "Boarding", "Board", "Boarding" ) - self:AddTransition( "Loaded", "Board", "Loaded" ) - self:AddTransition( "Boarding", "Loaded", "Boarding" ) - self:AddTransition( "Boarding", "PickedUp", "Loaded" ) - - self:AddTransition( "Loaded", "Unload", "Unboarding" ) - self:AddTransition( "Unboarding", "Unboard", "Unboarding" ) - self:AddTransition( "Unboarding", "Unloaded", "Unboarding" ) - self:AddTransition( "Unboarding", "Deployed", "Unloaded" ) + -- Unload + self:AddTransition( "Loaded", "Deploy", "*" ) + self:AddTransition( "*", "Unload", "*" ) + self:AddTransition( "*", "Unboard", "*" ) + self:AddTransition( "*", "Unloaded", "Unloaded" ) + self:AddTransition( "Unloaded", "Deployed", "Unloaded" ) + --- Pickup Handler OnBefore for AI_CARGO -- @function [parent=#AI_CARGO] OnBeforePickup @@ -393,7 +395,7 @@ end function AI_CARGO:onafterBoard( Carrier, From, Event, To, Cargo, CarrierUnit, PickupZone ) self:F( { Carrier, From, Event, To, Cargo, CarrierUnit:GetName() } ) - if Carrier and Carrier:IsAlive() and From == "Boarding" then + if Carrier and Carrier:IsAlive() then self:F({ IsLoaded = Cargo:IsLoaded(), Cargo:GetName(), Carrier:GetName() } ) if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then self:__Board( -10, Cargo, CarrierUnit, PickupZone ) @@ -509,7 +511,7 @@ end function AI_CARGO:onafterUnboard( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } ) - if Carrier and Carrier:IsAlive() and From == "Unboarding" then + if Carrier and Carrier:IsAlive() then if not Cargo:IsUnLoaded() then self:__Unboard( 10, Cargo, CarrierUnit, DeployZone, Defend ) return @@ -580,4 +582,3 @@ function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend end end - diff --git a/Moose Development/Moose/AI/AI_Cargo_APC.lua b/Moose Development/Moose/AI/AI_Cargo_APC.lua index bee08e1b1..6bc78debb 100644 --- a/Moose Development/Moose/AI/AI_Cargo_APC.lua +++ b/Moose Development/Moose/AI/AI_Cargo_APC.lua @@ -98,7 +98,8 @@ function AI_CARGO_APC:New( APC, CargoSet, CombatRadius ) self:AddTransition( "*", "Guard", "Unloaded" ) self:AddTransition( "*", "Home", "*" ) self:AddTransition( "*", "Reload", "Boarding" ) - + self:AddTransition( "*", "Deployed", "*" ) + self:AddTransition( "*", "PickedUp", "*" ) self:AddTransition( "*", "Destroyed", "Destroyed" ) self:SetCombatRadius( CombatRadius ) diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua index 127c67dbb..bba9b7108 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua @@ -174,8 +174,8 @@ function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZone self:SetPickupSpeed( 350, 150 ) self:SetDeploySpeed( 350, 150 ) - self:SetPickupRadius( 0, 0 ) - self:SetDeployRadius( 0, 0 ) + self:SetPickupRadius( 40, 12 ) + self:SetDeployRadius( 40, 12 ) self:SetPickupHeight( 500, 200 ) self:SetDeployHeight( 500, 200 ) @@ -186,6 +186,9 @@ end function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet ) - return AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + local dispatcher = AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + dispatcher:SetLandingSpeedAndHeight(27, 6) + return dispatcher + end diff --git a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua index 3a83a48ae..2c1d5c028 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua @@ -64,20 +64,24 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) self.Zone = ZONE_GROUP:New( Helicopter:GetName(), Helicopter, 300 ) self:SetStartState( "Unloaded" ) + -- Boarding + self:AddTransition( "Unloaded", "Pickup", "Unloaded" ) + self:AddTransition( "*", "Landed", "*" ) + self:AddTransition( "*", "Load", "*" ) + self:AddTransition( "*", "Loaded", "Loaded" ) + self:AddTransition( "Loaded", "PickedUp", "Loaded" ) - self:AddTransition( "Unloaded", "Pickup", "*" ) - self:AddTransition( "Loaded", "Deploy", "*" ) - self:AddTransition( "*", "Loaded", "Loaded" ) - self:AddTransition( "Unboarding", "Pickup", "Unloaded" ) - self:AddTransition( "Unloaded", "Unboard", "Unloaded" ) - self:AddTransition( "Unloaded", "Unloaded", "Unloaded" ) - self:AddTransition( "*", "PickedUp", "*" ) - self:AddTransition( "*", "Landed", "*" ) - self:AddTransition( "*", "Queue", "*" ) - self:AddTransition( "*", "Orbit" , "*" ) + -- Unboarding + self:AddTransition( "Loaded", "Deploy", "*" ) + self:AddTransition( "*", "Queue", "*" ) + self:AddTransition( "*", "Orbit" , "*" ) + self:AddTransition( "*", "Destroyed", "*" ) + self:AddTransition( "*", "Unload", "*" ) + self:AddTransition( "*", "Unloaded", "Unloaded" ) + self:AddTransition( "Unloaded", "Deployed", "Unloaded" ) + + -- RTB self:AddTransition( "*", "Home" , "*" ) - - self:AddTransition( "*", "Destroyed", "Destroyed" ) --- Pickup Handler OnBefore for AI_CARGO_HELICOPTER -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforePickup @@ -207,6 +211,9 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) self:SetCarrier( Helicopter ) + self.landingspeed = 15 -- kph + self.landingheight = 5.5 -- meter + return self end @@ -255,6 +262,25 @@ function AI_CARGO_HELICOPTER:SetCarrier( Helicopter ) return self end +--- Set landingspeed and -height for helicopter landings. Adjust after tracing if your helis get stuck after landing. +-- @param #AI_CARGO_HELICOPTER self +-- @param #number speed Landing speed in kph(!), e.g. 15 +-- @param #number height Landing height in meters(!), e.g. 5.5 +-- @return #AI_CARGO_HELICOPTER self +-- @usage If your choppers get stuck, add tracing to your script to determine if they hit the right parameters like so: +-- +-- BASE:TraceOn() +-- BASE:TraceClass("AI_CARGO_HELICOPTER") +-- +-- Watch the DCS.log for entries stating `Helicopter:, Height = Helicopter:, Velocity = Helicopter:` +-- Adjust if necessary. +function AI_CARGO_HELICOPTER:SetLandingSpeedAndHeight(speed, height) + local _speed = speed or 15 + local _height = height or 5.5 + self.landingheight = _height + self.landingspeed = _speed + return self +end --- @param #AI_CARGO_HELICOPTER self -- @param Wrapper.Group#GROUP Helicopter @@ -271,13 +297,13 @@ function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To ) -- 1 - When the helo lands normally on the ground. -- 2 - when the helo is hit and goes RTB or even when it is destroyed. -- For point 2, this is an issue, the infantry may not unload in this case! - -- So we check if the helo is on the ground, and velocity< 5. + -- So we check if the helo is on the ground, and velocity< 15. -- Only then the infantry can unload (and load too, for consistency)! - self:F( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } ) + self:T( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } ) if self.RoutePickup == true then - if Helicopter:GetHeight( true ) <= 5.5 and Helicopter:GetVelocityKMH() < 15 then + if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then --self:Load( Helicopter:GetPointVec2() ) self:Load( self.PickupZone ) self.RoutePickup = false @@ -285,7 +311,7 @@ function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To ) end if self.RouteDeploy == true then - if Helicopter:GetHeight( true ) <= 5.5 and Helicopter:GetVelocityKMH() < 15 then + if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then self:Unload( self.DeployZone ) self.RouteDeploy = false end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index df836f7dd..618e34cbf 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -300,23 +300,23 @@ do -- Zones for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do local ZoneName = ZoneData.name - + -- Color local color=ZoneData.color or {1, 0, 0, 0.15} - + -- Create new Zone local Zone=nil --Core.Zone#ZONE_BASE - + if ZoneData.type==0 then - + --- -- Circular zone --- - + self:I(string.format("Register ZONE: %s (Circular)", ZoneName)) - + Zone=ZONE:New(ZoneName) - + else --- @@ -324,51 +324,51 @@ do -- Zones --- self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) - + Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies) - + --for i,vec2 in pairs(ZoneData.verticies) do -- local coord=COORDINATE:NewFromVec2(vec2) -- coord:MarkToAll(string.format("%s Point %d", ZoneName, i)) --end - + end - + if Zone then - -- Store color of zone. + -- Store color of zone. Zone.Color=color - + -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName - + -- Add zone. self:AddZone(ZoneName, Zone) - + end - + end -- Polygon zones defined by late activated groups. for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do if ZoneGroupName:match("#ZONE_POLYGON") then - + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) -- Debug output self:I(string.format("Register ZONE: %s (Polygon)", ZoneName)) - + -- Create a new polygon zone. local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup ) - + -- Set color. Zone_Polygon:SetColor({1, 0, 0}, 0.15) - + -- Store name in DB. self.ZONENAMES[ZoneName] = ZoneName - + -- Add zone to DB. self:AddZone( ZoneName, Zone_Polygon ) end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 4245e9591..43eb4af78 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1881,7 +1881,7 @@ do -- COORDINATE --- Big smoke and fire at the coordinate. -- @param #COORDINATE self - -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (0=small smoke and fire, 1=medium smoke and fire, 2=large smoke and fire, 3=huge smoke and fire, 4=small smoke, 5=medium smoke, 6=large smoke, 7=huge smoke). + -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke). -- @param #number density (Optional) Smoke density. Number in [0,...,1]. Default 0.5. function COORDINATE:BigSmokeAndFire( preset, density ) self:F2( { preset=preset, density=density } ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 017df9cb1..dd94ae09d 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -4042,7 +4042,25 @@ do -- SET_CLIENT return self end + + --- Iterate the SET_CLIENT and count alive units. + -- @param #SET_CLIENT self + -- @return #number count + function SET_CLIENT:CountAlive() + local Set = self:GetSet() + + local CountU = 0 + for UnitID, UnitData in pairs(Set) do -- For each GROUP in SET_GROUP + if UnitData and UnitData:IsAlive() then + CountU = CountU + 1 + end + + end + + return CountU + end + --- -- @param #SET_CLIENT self -- @param Wrapper.Client#CLIENT MClient @@ -4790,7 +4808,7 @@ do -- SET_AIRBASE local airbaseName, airbase=self:FindInDatabase(EventData) - if airbase and airbase:IsShip() or airbase:IsHelipad() then + if airbase and (airbase:IsShip() or airbase:IsHelipad()) then self:RemoveAirbasesByName(airbaseName) end diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 95b41655d..5fde1c12c 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -138,24 +138,24 @@ SPAWNSTATIC = { -- @return #SPAWNSTATIC self function SPAWNSTATIC:NewFromStatic(SpawnTemplateName, SpawnCountryID) - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC - local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName) - - if TemplateStatic then - self.SpawnTemplatePrefix = SpawnTemplateName - self.TemplateStaticUnit = UTILS.DeepCopy(TemplateStatic.units[1]) - self.CountryID = SpawnCountryID or CountryID - self.CategoryID = CategoryID - self.CoalitionID = CoalitionID - self.SpawnIndex = 0 - else - error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" ) - end + local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName) + + if TemplateStatic then + self.SpawnTemplatePrefix = SpawnTemplateName + self.TemplateStaticUnit = UTILS.DeepCopy(TemplateStatic.units[1]) + self.CountryID = SpawnCountryID or CountryID + self.CategoryID = CategoryID + self.CoalitionID = CoalitionID + self.SpawnIndex = 0 + else + error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" ) + end self:SetEventPriority( 5 ) - return self + return self end --- Creates the main object to spawn a @{Static} given a template table. @@ -422,7 +422,11 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) end if self.InitCargo~=nil then - Template.isCargo=self.InitCargo + Template.canCargo=self.InitCargo + end + + if self.InitCargoMass~=nil then + Template.mass=self.InitCargoMass end if self.InitLinkUnit then diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 279474280..eec1dace9 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -183,12 +183,12 @@ function ZONE_BASE:IsCoordinateInZone( Coordinate ) return InZone end ---- Returns if a PointVec2 is within the zone. +--- Returns if a PointVec2 is within the zone. (Name is misleading, actually takes a #COORDINATE) -- @param #ZONE_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test. +-- @param Core.Point#COORDINATE PointVec2 The coordinate to test. -- @return #boolean true if the PointVec2 is within the zone. -function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - local InZone = self:IsVec2InZone( PointVec2:GetVec2() ) +function ZONE_BASE:IsPointVec2InZone( Coordinate ) + local InZone = self:IsVec2InZone( Coordinate:GetVec2() ) return InZone end @@ -515,8 +515,8 @@ end -- -- @field #ZONE_RADIUS ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } + ClassName="ZONE_RADIUS", + } --- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. -- @param #ZONE_RADIUS self @@ -527,15 +527,15 @@ ZONE_RADIUS = { function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) -- Inherit ZONE_BASE. - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS + self:F( { ZoneName, Vec2, Radius } ) - self.Radius = Radius - self.Vec2 = Vec2 + self.Radius = Radius + self.Vec2 = Vec2 - --self.Coordinate=COORDINATE:NewFromVec2(Vec2) + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) - return self + return self end --- Update zone from a 2D vector. @@ -763,11 +763,11 @@ end -- @param #ZONE_RADIUS self -- @return DCS#Vec2 The location of the zone. function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) + self:F2( self.ZoneName ) - self:T2( { self.Vec2 } ) + self:T2( { self.Vec2 } ) - return self.Vec2 + return self.Vec2 end --- Sets the @{DCS#Vec2} of the zone. diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 5c5ec9f20..efcdc99a5 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -1707,8 +1707,8 @@ end --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #FOX self -- @param DCS#Weapon weapon The weapon. --- @return #number Notching heading right, i.e. missile heading +90� --- @return #number Notching heading left, i.e. missile heading -90�. +-- @return #number Notching heading right, i.e. missile heading +90°. +-- @return #number Notching heading left, i.e. missile heading -90°. function FOX:_GetNotchingHeadings(weapon) if weapon then diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 6891fd3fd..f9e10f545 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -1,31 +1,31 @@ --- **Functional** -- Modular, Automatic and Network capable Targeting and Interception System for Air Defenses --- +-- -- === --- +-- -- **MANTIS** - Moose derived Modular, Automatic and Network capable Targeting and Interception System -- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy -- Leverage evasiveness from SEAD -- Leverage attack range setup added by DCS in 11/20 --- +-- -- === --- +-- -- ## Missions: -- -- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MTS%20-%20Mantis/MTS-010%20-%20Basic%20Mantis%20Demo) --- +-- -- === --- +-- -- ### Author : **applevangelist ** --- +-- -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- Date: July 2021 ------------------------------------------------------------------------- ---- **MANTIS** class, extends #Core.Base#BASE +--- **MANTIS** class, extends Core.Base#BASE -- @type MANTIS --- @field #string Classname +-- @field #string ClassName -- @field #string name Name of this Mantis -- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites -- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP @@ -59,10 +59,10 @@ -- @extends Core.Base#BASE ---- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frdric Bastiat --- +--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat +-- -- Simple Class for a more intelligent Air Defense System --- +-- -- #MANTIS -- Moose derived Modular, Automatic and Network capable Targeting and Interception System. -- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy. @@ -72,62 +72,62 @@ -- Set up your SAM sites in the mission editor. Name the groups with common prefix like "Red SAM". -- Set up your EWR system in the mission editor. Name the groups with common prefix like "Red EWR". Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. -- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. --- +-- -- # 1. Basic tactical considerations when setting up your SAM sites --- +-- -- ## 1.1 Radar systems and AWACS --- +-- -- Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track. -- **Location** is of highest importantance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system -- doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units --- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. --- +-- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. +-- -- ## 1.2 SAM sites --- --- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-10/11, midrange like SA-6 and short-range like --- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, +-- +-- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-10/11, midrange like SA-6 and short-range like +-- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, -- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage. --- +-- -- ## 1.3 Typical problems --- --- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are --- --- * bad placement of radar units, --- * overestimation how far units can "see" and --- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching to RED, acquiring the target and firing. --- +-- +-- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are +-- +-- * bad placement of radar units, +-- * overestimation how far units can "see" and +-- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching to RED, acquiring the target and firing. +-- -- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the missione editor to understand distances and zones. Take into account that the ranges given by the circles -- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**. --- +-- -- # 2. Start up your MANTIS with a basic setting --- --- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` +-- +-- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` -- `myredmantis:Start()` --- --- [optional] Use --- --- * `MANTIS:SetEWRGrouping(radius)` --- * `MANTIS:SetEWRRange(radius)` --- * `MANTIS:SetSAMRadius(radius)` +-- +-- [optional] Use +-- +-- * `MANTIS:SetEWRGrouping(radius)` +-- * `MANTIS:SetEWRRange(radius)` +-- * `MANTIS:SetSAMRadius(radius)` -- * `MANTIS:SetDetectInterval(interval)` -- * `MANTIS:SetAutoRelocate(hq, ewr)` --- +-- -- before starting #MANTIS to fine-tune your setup. --- +-- -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: --- --- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` +-- +-- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` -- `mybluemantis:Start()` -- -- # 3. Default settings --- +-- -- By default, the following settings are active: -- -- * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM" -- * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit -- * checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` -- * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)` --- * acceptrange = 80000 (meters) - Detection (EWR) will on consider flights inside a 80km radius - `MANTIS:SetEWRRange(radius)` +-- * acceptrange = 80000 (meters) - Detection (EWR) will on consider flights inside a 80km radius - `MANTIS:SetEWRRange(radius)` -- * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)` -- * engagerange = 85 (percent) - SAMs will only fire if flights are inside of a 85% radius of their max firerange - `MANTIS:SetSAMRange(range)` -- * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic)` @@ -135,28 +135,28 @@ -- * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)` -- -- # 4. Advanced Mode --- --- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. --- --- E.g. `mymantis:SetAdvancedMode( true, 90 )` --- --- Use this option if you want to make use of or allow advanced SEAD tactics. --- +-- +-- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. +-- +-- E.g. `mymantis:SetAdvancedMode( true, 90 )` +-- +-- Use this option if you want to make use of or allow advanced SEAD tactics. +-- -- # 5. Integrate SHORAD --- +-- -- You can also choose to integrate Mantis with @{Functional.Shorad#SHORAD} for protection against HARMs and AGMs. When SHORAD detects a missile fired at one of MANTIS' SAM sites, it will activate SHORAD systems in -- the given defense checkradius around that SAM site. Create a SHORAD object first, then integrate with MANTIS like so: --- +-- -- `local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()` -- `myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")` -- `-- now set up MANTIS` -- `mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` -- `mymantis:AddShorad(myshorad,720)` -- `mymantis:Start()` --- +-- -- and (optionally) remove the link later on with --- --- `mymantis:RemoveShorad()` +-- +-- `mymantis:RemoveShorad()` -- -- @field #MANTIS MANTIS = { @@ -197,6 +197,7 @@ MANTIS = { SamStateTracker = {}, DLink = false, DLTimeStamp = 0, + Padding = 10, } --- Advanced state enumerator @@ -221,30 +222,31 @@ do --@param #string coaltion Coalition side of your setup, e.g. "blue", "red" or "neutral" --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) --@param #string awacs Group name of your Awacs (optional) - --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN + --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional) + --@param #number Padding For #SEAD - Extra number of seconds to add to radar switch-back-on time (optional) --@return #MANTIS self --@usage Start up your MANTIS with a basic setting -- - -- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` + -- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` -- `myredmantis:Start()` - -- - -- [optional] Use - -- - -- * `MANTIS:SetEWRGrouping(radius)` - -- * `MANTIS:SetEWRRange(radius)` - -- * `MANTIS:SetSAMRadius(radius)` + -- + -- [optional] Use + -- + -- * `MANTIS:SetEWRGrouping(radius)` + -- * `MANTIS:SetEWRRange(radius)` + -- * `MANTIS:SetSAMRadius(radius)` -- * `MANTIS:SetDetectInterval(interval)` -- * `MANTIS:SetAutoRelocate(hq, ewr)` - -- + -- -- before starting #MANTIS to fine-tune your setup. - -- + -- -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: - -- - -- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` + -- + -- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` -- `mybluemantis:Start()` - -- - function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff) - + -- + function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff, Padding) + -- DONE: Create some user functions for these -- DONE: Make HQ useful -- DONE: Set SAMs to auto if EWR dies @@ -267,7 +269,7 @@ do self.autorelocateunits = { HQ = false, EWR = false} self.advanced = false self.adv_ratio = 100 - self.adv_state = 0 + self.adv_state = 0 self.verbose = false self.Adv_EWR_Group = nil self.AWACS_Prefix = awacs or nil @@ -281,27 +283,28 @@ do self.state2flag = false self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode self.DLink = false - + self.Padding = Padding or 10 + if EmOnOff then if EmOnOff == false then self.UseEmOnOff = false else self.UseEmOnOff = true end - end - + end + if type(awacs) == "string" then self.advAwacs = true else self.advAwacs = false end - + -- Inherit everything from BASE class. local self = BASE:Inherit(self, FSM:New()) -- #MANTIS - + -- Set the string id for output to DCS.log file. self.lid=string.format("MANTIS %s | ", self.name) - + -- Debug trace. if self.debug then BASE:TraceOnOff(true) @@ -309,7 +312,7 @@ do --BASE:TraceClass("SEAD") BASE:TraceLevel(1) end - + if self.dynamic then -- Set SAM SET_GROUP self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() @@ -321,18 +324,18 @@ do -- Set EWR SET_GROUP self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterOnce() end - + -- set up CC if self.HQ_Template_CC then self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) end - + -- @field #string version self.version="0.6.2" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) - + --- FSM Functions --- - + -- Start State. self:SetStartState("Stopped") @@ -346,11 +349,11 @@ do self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. -- @function [parent=#MANTIS] Start -- @param #MANTIS self @@ -376,7 +379,7 @@ do -- @function [parent=#MANTIS] __Status -- @param #MANTIS self -- @param #number delay Delay in seconds. - + --- On After "Relocating" event. HQ and/or EWR moved. -- @function [parent=#MANTIS] OnAfterRelocating -- @param #MANTIS self @@ -384,7 +387,7 @@ do -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self - + --- On After "GreenState" event. A SAM group was switched to GREEN alert. -- @function [parent=#MANTIS] OnAfterGreenState -- @param #MANTIS self @@ -393,7 +396,7 @@ do -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self - + --- On After "RedState" event. A SAM group was switched to RED alert. -- @function [parent=#MANTIS] OnAfterRedState -- @param #MANTIS self @@ -402,7 +405,7 @@ do -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self - + --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. -- @function [parent=#MANTIS] OnAfterAdvStateChange -- @param #MANTIS self @@ -413,7 +416,7 @@ do -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red -- @param #number Interval Calculated detection interval based on state and advanced feature setting -- @return #MANTIS self - + --- On After "ShoradActivated" event. Mantis has activated a SHORAD. -- @function [parent=#MANTIS] OnAfterShoradActivated -- @param #MANTIS self @@ -424,21 +427,21 @@ do -- @param #number Radius Radius around the named group to find SHORAD groups -- @param #number Ontime Seconds the SHORAD will stay active - return self + return self end ----------------------------------------------------------------------- -- MANTIS helper functions ------------------------------------------------------------------------ - +----------------------------------------------------------------------- + --- [Internal] Function to get the self.SAM_Table -- @param #MANTIS self - -- @return #table table + -- @return #table table function MANTIS:_GetSAMTable() self:T(self.lid .. "GetSAMTable") return self.SAM_Table end - + --- [Internal] Function to set the self.SAM_Table -- @param #MANTIS self -- @return #MANTIS self @@ -447,7 +450,7 @@ do self.SAM_Table = table return self end - + --- Function to set the grouping radius of the detection in meters -- @param #MANTIS self -- @param #number radius Radius upon which detected objects will be grouped @@ -467,17 +470,17 @@ do self.acceptrange = radius return self end - + --- Function to set switch-on/off zone for the SAM sites in meters -- @param #MANTIS self - -- @param #number radius Radius of the firing zone + -- @param #number radius Radius of the firing zone function MANTIS:SetSAMRadius(radius) self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 self.checkradius = radius return self end - + --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 -- @param #MANTIS self -- @param #number range Percent of the max fire range @@ -490,7 +493,7 @@ do self.engagerange = range return self end - + --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night -- @param #MANTIS self -- @param #number range Percent of the max fire range @@ -505,7 +508,7 @@ do self.mysead.EngagementRange = range return self end - + --- Function to set switch-on/off the debug state -- @param #MANTIS self -- @param #boolean onoff Set true to switch on @@ -523,7 +526,7 @@ do end return self end - + --- Function to get the HQ object for further use -- @param #MANTIS self -- @return Wrapper.GROUP#GROUP The HQ #GROUP object or *nil* if it doesn't exist @@ -532,10 +535,10 @@ do if self.HQ_CC then return self.HQ_CC else - return nil - end + return nil + end end - + --- Function to set separate AWACS detection instance -- @param #MANTIS self -- @param #string prefix Name of the AWACS group in the mission editor @@ -559,7 +562,7 @@ do self.awacsrange = range return self end - + --- Function to set the HQ object for further use -- @param #MANTIS self -- @param Wrapper.GROUP#GROUP group The #GROUP object to be set as HQ @@ -577,7 +580,7 @@ do end return self end - + --- Function to set the detection interval -- @param #MANTIS self -- @param #number interval The interval in seconds @@ -586,8 +589,8 @@ do local interval = interval or 30 self.detectinterval = interval return self - end - + end + --- Function to set Advanded Mode -- @param #MANTIS self -- @param #boolean onoff If true, will activate Advanced Mode @@ -596,7 +599,7 @@ do -- E.g. `mymantis:SetAdvancedMode(true, 90)` function MANTIS:SetAdvancedMode(onoff, ratio) self:T(self.lid .. "SetAdvancedMode") - --self.T({onoff, ratio}) + --self:T({onoff, ratio}) local onoff = onoff or false local ratio = ratio or 100 if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then @@ -612,7 +615,7 @@ do end return self end - + --- Set using Emissions on/off instead of changing alarm state -- @param #MANTIS self -- @param #boolean switch Decide if we are changing alarm state or Emission state @@ -621,7 +624,7 @@ do self.UseEmOnOff = switch or false return self end - + --- Set using an #INTEL_DLINK object instead of #DETECTION -- @param #MANTIS self -- @param Ops.Intelligence#INTEL_DLINK DLink The data link object to be used. @@ -632,7 +635,7 @@ do self.DLTimeStamp = timer.getAbsTime() return self end - + --- [Internal] Function to check if HQ is alive -- @param #MANTIS self -- @return #boolean True if HQ is alive, else false @@ -647,15 +650,15 @@ do local hqgrp = GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive - --self.T(self.lid.." HQ is alive!") + --self:T(self.lid.." HQ is alive!") return true else - --self.T(self.lid.." HQ is dead!") - return false + --self:T(self.lid.." HQ is dead!") + return false end end end - return self + return self end --- [Internal] Function to check if EWR is (at least partially) alive @@ -664,7 +667,7 @@ do function MANTIS:_CheckEWRState() self:T(self.lid .. "CheckEWRState") local text = self.lid.." Checking EWR State" - --self.T(text) + --self:T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end -- start check @@ -680,14 +683,14 @@ do end end end - --self.T(self.lid..string.format(" No of EWR alive is %d", nalive)) + --self:T(self.lid..string.format(" No of EWR alive is %d", nalive)) if nalive > 0 then return true else return false end end - return self + return self end --- [Internal] Function to determine state of the advanced mode @@ -717,30 +720,30 @@ do local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 if self.debug or self.verbose then local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) - --self.T(text) + --self:T(text) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end end return newinterval, currstate end - + --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS! -- @param #MANTIS self -- @param #boolean hq If true, will relocate HQ object -- @param #boolean ewr If true, will relocate EWR objects function MANTIS:SetAutoRelocate(hq, ewr) self:T(self.lid .. "SetAutoRelocate") - --self.T({hq, ewr}) + --self:T({hq, ewr}) local hqrel = hq or false local ewrel = ewr or false if hqrel or ewrel then self.autorelocate = true self.autorelocateunits = { HQ = hqrel, EWR = ewrel } - --self.T({self.autorelocate, self.autorelocateunits}) + --self:T({self.autorelocate, self.autorelocateunits}) end return self - end - + end + --- [Internal] Function to execute the relocation -- @param #MANTIS self function MANTIS:_RelocateGroups() @@ -753,7 +756,7 @@ do local HQGroup = self.HQ_CC if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists local _hqgrp = self.HQ_CC - --self.T(self.lid.." Relocating HQ") + --self:T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) @@ -766,7 +769,7 @@ do local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP for _,_grp in pairs (EWR_Grps) do if _grp:IsAlive() and _grp:IsGround() then - --self.T(self.lid.." Relocating EWR ".._grp:GetName()) + --self:T(self.lid.." Relocating EWR ".._grp:GetName()) local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end @@ -777,7 +780,7 @@ do end return self end - + --- [Internal] Function to check if any object is in the given SAM zone -- @param #MANTIS self -- @param #table dectset Table of coordinates of detected items @@ -793,12 +796,12 @@ do local coord = _coord -- get current coord to check -- output for cross-check local targetdistance = samcoordinate:DistanceFromPointVec2(coord) - if self.verbose or self.debug then + if self.verbose or self.debug then local dectstring = coord:ToStringLLDMS() local samstring = samcoordinate:ToStringLLDMS() local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - self:I(self.lid..text) + self:I(self.lid..text) end -- end output to cross-check if targetdistance <= radius then @@ -813,20 +816,20 @@ do -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() self:T(self.lid.."Starting Detection") - + -- start detection local groupset = self.EWR_Group local grouping = self.grouping or 5000 local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) MANTISdetection:SetAcceptRange(acceptrange) MANTISdetection:SetRefreshTimeInterval(interval) MANTISdetection:Start() - + function MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false @@ -835,30 +838,30 @@ do local text = "MANTIS: Detection at "..Coordinate:ToStringLLDMS() local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end - end + end return MANTISdetection end - + --- [Internal] Function to start the detection via AWACS if defined as separate -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() self:T(self.lid.."Starting Awacs Detection") - + -- start detection local group = self.AWACS_Prefix local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() local grouping = self.grouping or 5000 --local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) MANTISAwacs:SetAcceptRange(self.awacsrange) --250km MANTISAwacs:SetRefreshTimeInterval(interval) MANTISAwacs:Start() - + function MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false @@ -867,10 +870,10 @@ do local text = "Awacs Detection at "..Coordinate:ToStringLLDMS() local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end - end + end return MANTISAwacs end - + --- [Internal] Function to set the SAM start state -- @param #MANTIS self -- @return #MANTIS self @@ -904,12 +907,12 @@ do end self.SAM_Table = SAM_Tbl -- make SAMs evasive - local mysead = SEAD:New( SEAD_Grps ) + local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD mysead:SetEngagementRange(engagerange) self.mysead = mysead return self end - + --- [Internal] Function to update SAM table and SEAD state -- @param #MANTIS self -- @return #MANTIS self @@ -941,7 +944,7 @@ do end return self end - + --- Function to link up #MANTIS with a #SHORAD installation -- @param #MANTIS self -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object @@ -958,7 +961,7 @@ do end return self end - + --- Function to unlink #MANTIS from a #SHORAD installation -- @param #MANTIS self function MANTIS:RemoveShorad() @@ -966,11 +969,11 @@ do self.ShoradLink = false return self end - + ----------------------------------------------------------------------- -- MANTIS main functions ------------------------------------------------------------------------ - +----------------------------------------------------------------------- + --- [Internal] Check detection function -- @param #MANTIS self -- @param Functional.Detection#DETECTION_AREAS detection Detection object @@ -1021,7 +1024,7 @@ do if self.verbose then self:I(self.lid..text) end end end --end alive - else + else if samgroup:IsAlive() then -- switch off SAM if self.UseEmOnOff then @@ -1032,7 +1035,7 @@ do self:__GreenState(1,samgroup) self.SamStateTracker[name] = "GREEN" end - if self.debug or self.verbose then + if self.debug or self.verbose then local text = string.format("SAM %s switched to alarm state GREEN!", name) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid..text) end @@ -1041,8 +1044,8 @@ do end --end check end --for for loop return self - end - + end + --- [Internal] Relocation relay function -- @param #MANTIS self -- @return #MANTIS self @@ -1051,7 +1054,7 @@ do self:_RelocateGroups() return self end - + --- [Internal] Check advanced state -- @param #MANTIS self -- @return #MANTIS self @@ -1086,7 +1089,7 @@ do end -- end newstate vs oldstate return self end - + --- [Internal] Check DLink state -- @param #MANTIS self -- @return #MANTIS self @@ -1100,7 +1103,7 @@ do self:I(self.lid .. "Intel DLink not running - switching back to single detection!") end end - + --- [Internal] Function to set start state -- @param #MANTIS self -- @param #string From The From State @@ -1117,10 +1120,10 @@ do if self.advAwacs then self.AWACS_Detection = self:StartAwacsDetection() end - self:__Status(-math.random(1,10)) + self:__Status(-math.random(1,10)) return self end - + --- [Internal] Before status function for MANTIS -- @param #MANTIS self -- @param #string From The From State @@ -1138,7 +1141,7 @@ do if self.advAwacs and not self.state2flag then self:_Check(self.AWACS_Detection) end - + -- relocate HQ and EWR if self.autorelocate then local relointerval = self.relointerval @@ -1146,26 +1149,26 @@ do local timepassed = thistime - self.TimeStamp local halfintv = math.floor(timepassed / relointerval) - + --self:T({timepassed=timepassed, halfintv=halfintv}) - + if halfintv >= 1 then self.TimeStamp = timer.getAbsTime() self:_Relocate() self:__Relocating(1) end end - + -- advanced state check if self.advanced then self:_CheckAdvState() end - + -- check DLink state if self.DLink then self:_CheckDLinkState() end - + return self end @@ -1188,7 +1191,7 @@ do self:__Status(interval) return self end - + --- [Internal] Function to stop MANTIS -- @param #MANTIS self -- @param #string From The From State @@ -1197,9 +1200,9 @@ do -- @return #MANTIS self function MANTIS:onafterStop(From, Event, To) self:T({From, Event, To}) - return self + return self end - + --- [Internal] Function triggered by Event Relocating -- @param #MANTIS self -- @param #string From The From State @@ -1208,9 +1211,9 @@ do -- @return #MANTIS self function MANTIS:onafterRelocating(From, Event, To) self:T({From, Event, To}) - return self + return self end - + --- [Internal] Function triggered by Event GreenState -- @param #MANTIS self -- @param #string From The From State @@ -1220,9 +1223,9 @@ do -- @return #MANTIS self function MANTIS:onafterGreenState(From, Event, To, Group) self:T({From, Event, To, Group}) - return self + return self end - + --- [Internal] Function triggered by Event RedState -- @param #MANTIS self -- @param #string From The From State @@ -1232,9 +1235,9 @@ do -- @return #MANTIS self function MANTIS:onafterRedState(From, Event, To, Group) self:T({From, Event, To, Group}) - return self + return self end - + --- [Internal] Function triggered by Event AdvStateChange -- @param #MANTIS self -- @param #string From The From State @@ -1246,9 +1249,9 @@ do -- @return #MANTIS self function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) self:T({From, Event, To, Oldstate, Newstate, Interval}) - return self + return self end - + --- [Internal] Function triggered by Event ShoradActivated -- @param #MANTIS self -- @param #string From The From State @@ -1259,7 +1262,7 @@ do -- @param #number Ontime Seconds the SHORAD will stay active function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) self:T({From, Event, To, Name, Radius, Ontime}) - return self + return self end end ----------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/MissileTrainer.lua b/Moose Development/Moose/Functional/MissileTrainer.lua index eb9c50e86..cc8de571b 100644 --- a/Moose Development/Moose/Functional/MissileTrainer.lua +++ b/Moose Development/Moose/Functional/MissileTrainer.lua @@ -1,28 +1,28 @@ --- **Functional** -- Train missile defence and deflection. --- +-- -- === -- -- ## Features: --- +-- -- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. --- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range � +-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range -- * Provide alerts when a missile would have killed your aircraft. -- * Provide alerts when the missile self destructs. -- * Enable / Disable and Configure the Missile Trainer using the various menu options. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MIT%20-%20Missile%20Trainer) --- +-- -- === --- +-- -- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, -- the class will destroy the missile within a certain range, to avoid damage to your aircraft. --- +-- -- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: --- +-- -- * **Messages**: Menu to configure all messages. -- * **Messages On**: Show all messages. -- * **Messages Off**: Disable all messages. @@ -45,23 +45,23 @@ -- * **Range Off**: Disable range information when a missile is fired to a target. -- * **Bearing On**: Shows bearing information when a missile is fired to a target. -- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. +-- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. -- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. -- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. -- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. -- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- +-- -- === --- +-- -- ### Authors: **FlightControl** --- +-- -- ### Contributions: --- --- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. --- Danny has shared his ideas and together we made a design. +-- +-- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. +-- Danny has shared his ideas and together we made a design. -- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! -- * **132nd Squadron**: Testing and optimizing the logic. --- +-- -- === -- -- @module Functional.MissileTrainer @@ -76,7 +76,7 @@ --- -- -- # Constructor: --- +-- -- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: -- -- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. @@ -84,7 +84,7 @@ -- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. -- -- # Initialization: --- +-- -- A MISSILETRAINER object will behave differently based on the usage of initialization methods: -- -- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. @@ -97,8 +97,8 @@ -- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- @field #MISSILETRAINER +-- +-- @field #MISSILETRAINER MISSILETRAINER = { ClassName = "MISSILETRAINER", TrackingMissiles = {}, @@ -167,7 +167,7 @@ end -- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. -- @param #MISSILETRAINER self -- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. -- @return #MISSILETRAINER function MISSILETRAINER:New( Distance, Briefing ) local self = BASE:Inherit( self, BASE:New() ) @@ -194,8 +194,8 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self:F( "ForEach:" .. Client.UnitName ) -- Client:Alive( self._Alive, self ) -- end --- - self.DBClients:ForEachClient( +-- + self.DBClients:ForEachClient( function( Client ) self:F( "ForEach:" .. Client.UnitName ) Client:Alive( self._Alive, self ) @@ -207,9 +207,9 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self.DB:ForEachClient( -- --- @param Wrapper.Client#CLIENT Client -- function( Client ) --- +-- -- ... actions ... --- +-- -- end -- ) @@ -225,7 +225,7 @@ function MISSILETRAINER:New( Distance, Briefing ) self.DetailsRangeOnOff = true self.DetailsBearingOnOff = true - + self.MenusOnOff = true self.TrackingMissiles = {} @@ -293,7 +293,7 @@ end --- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. -- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. -- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. -- @return #MISSILETRAINER self function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) self:F( TrackingFrequency ) @@ -478,30 +478,30 @@ function MISSILETRAINER:OnEventShot( EVentData ) if TrainerTargetDCSUnit then local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - + self:T(TrainerTargetDCSUnitName ) - + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) if Client then - + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - + local Message = MESSAGE:New( string.format( "%s launched a %s", TrainerSourceUnit:GetTypeName(), TrainerWeaponName ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - + if self.AlertsToAll then Message:ToAll() else Message:ToClient( Client ) end end - + local ClientID = Client:GetID() self:T( ClientID ) local MissileData = {} @@ -579,52 +579,52 @@ function MISSILETRAINER:_TrackMissiles() end -- ALERTS PART - + -- Loop for all Player Clients to check the alerts and deletion of missiles. for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do local Client = ClientData.Client - + if Client and Client:IsAlive() then for MissileDataID, MissileData in pairs( ClientData.MissileData ) do self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then local PositionMissile = TrainerWeapon:getPosition().p local TargetVec3 = Client:GetVec3() - + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + ( PositionMissile.y - TargetVec3.y )^2 + ( PositionMissile.z - TargetVec3.z )^2 ) ^ 0.5 / 1000 - + if Distance <= self.Distance then -- Hit alert TrainerWeapon:destroy() if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - + self:T( "killed" ) - + local Message = MESSAGE:New( string.format( "%s launched by %s killed %s", TrainerWeapon:getTypeName(), TrainerSourceUnit:GetTypeName(), TrainerTargetUnit:GetPlayerName() ), 15, "Hit Alert" ) - + if self.AlertsToAll == true then Message:ToAll() else Message:ToClient( Client ) end - + MissileData = nil table.remove( ClientData.MissileData, MissileDataID ) self:T(ClientData.MissileData) @@ -639,7 +639,7 @@ function MISSILETRAINER:_TrackMissiles() TrainerWeaponTypeName, TrainerSourceUnit:GetTypeName() ), 5, "Tracking" ) - + if self.AlertsToAll == true then Message:ToAll() else @@ -660,41 +660,41 @@ function MISSILETRAINER:_TrackMissiles() if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. -- TRACKING PART - + -- For the current client, the missile range and bearing details are displayed To the Player Client. -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - + -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. + -- Main Player Client loop for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - + local Client = ClientData.Client --self:T2( { Client:GetName() } ) - - + + ClientData.MessageToClient = "" ClientData.MessageToAll = "" - + -- Other Players Client loop for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - + for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do --self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - + if ShowMessages == true then local TrackingTo TrackingTo = string.format( " -> %s", TrainerWeaponTypeName ) - + if ClientDataID == TrackingDataID then if ClientData.MessageToClient == "" then ClientData.MessageToClient = "Missiles to You:\n" @@ -712,7 +712,7 @@ function MISSILETRAINER:_TrackMissiles() end end end - + -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 691f53338..231a964c8 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -1,13 +1,13 @@ --- **Functional** - Create random airtraffic in your missions. --- +-- -- === --- --- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. --- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. +-- +-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. +-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. -- Even the mission designer will not know where aircraft will be spawned and which route they follow. --- +-- -- ## Features: --- +-- -- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. -- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. -- * Specific departure and/or destination airports can be chosen. @@ -19,37 +19,37 @@ -- * Aircraft can report their status during the route. -- * All of the above can be customized by the user if necessary. -- * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported. --- +-- -- The RAT class creates an entry in the F10 radio menu which allows to: --- +-- -- * Create new groups on-the-fly, i.e. at run time within the mission, -- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), -- * Request the status of all RAT aircraft or individual groups, -- * Place markers at waypoints on the F10 map for each group. --- +-- -- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. --- +-- -- More of the documentation include some simple examples can be found further down this page. --- +-- -- === --- +-- -- ## Missions: -- -- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) --- +-- -- === --- +-- -- # YouTube Channel --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) --- +-- -- === -- @module Functional.Rat -- @image RAT.JPG @@ -108,7 +108,7 @@ -- @field #number FLminuser Minimum flight level set by user. -- @field #number FLmaxuser Maximum flight level set by user. -- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. --- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. +-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. -- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way. -- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. -- @field #number ngroups Number of groups to be spawned in total. @@ -143,7 +143,7 @@ -- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. -- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. -- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m. --- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. -- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. -- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m. -- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase. @@ -157,25 +157,25 @@ --- Implements an easy to use way to randomly fill your map with AI aircraft. -- -- ## Airport Selection --- +-- -- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png) --- +-- -- ### Default settings: --- +-- -- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports. -- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft. -- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount. --- +-- -- ### The default behavior can be changed: --- +-- -- * A specific departure and/or destination airport can be chosen. -- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". -- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. --- +-- -- ## Flight Plan --- +-- -- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png) --- +-- -- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach. -- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown. -- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft. @@ -187,56 +187,56 @@ -- * The altitude of theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration. -- * If an aircraft is spawned in air, the procedure omitts taxi and take-off and starts with the climb/cruising part. -- * All values are randomized for each spawned aircraft. --- +-- -- ## Mission Editor Setup --- +-- -- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png) --- +-- -- Basic mission setup is very simple and essentially a three step process: --- +-- -- * Place your aircraft **anywhere** on the map. It really does not matter where you put it. -- * Give the group a good name. In the example above the group is named "RAT_YAK". --- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. --- +-- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. +-- -- Voilà, your already done! --- +-- -- Optionally, you can set a specific livery for the aircraft or give it some weapons. -- However, the aircraft will by default not engage any enemies. Think of them as beeing on a peaceful or ferry mission. --- +-- -- ## Basic Lua Script --- +-- -- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png) --- +-- -- The basic Lua script for one template group consits of two simple lines as shown in the picture above. --- +-- -- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK". -- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object. -- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type. --- +-- -- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport. -- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map. -- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... -- This ensures that you always have a constant number of AI aircraft on your map. --- +-- -- ## Parking Problems --- --- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and +-- +-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and -- airstripes. This can lead to multiple problems in DCS. --- +-- -- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immidiately despawned the moment its wheels touch the runway, i.e. -- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing -- and not when it shuts down its engines. See the @{RAT.RespawnAfterLanding}() function. -- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter. -- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by. --- +-- -- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp. -- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase. --- +-- -- ### Setting the Terminal Type -- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have. -- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*) -- function. The parameter *terminaltype* can be set as follows --- +-- -- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers. -- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map. -- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. @@ -244,96 +244,96 @@ -- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. -- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. --- +-- -- So for example -- c17=RAT:New("C-17") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- c17:Spawn(5) --- +-- -- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed -- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without beeing despawned immidiately. --- +-- -- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is availabe at the -- moment of spawning, the group is automatically spawned in air above the selected airport. --- +-- -- ## Examples --- +-- -- Here are a few examples, how you can modify the default settings of RAT class objects. --- +-- -- ### Specify Departure and Destinations --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png) --- +-- -- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations. --- +-- -- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively. --- +-- -- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports. -- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport. -- * If you fix departure and destination airports, aircraft will only travel from between those airports. --- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. --- +-- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. +-- -- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function. -- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination. -- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ... --- +-- -- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc. -- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly. -- Then the aircraft will fly back and forth between those two airports indefinetly. --- --- +-- +-- -- ### Spawn in Air --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png) --- +-- -- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air". --- +-- -- By default, aircraft are spawned randomly above airports of the map. --- +-- -- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones. -- Aircraft will then be spawned at a random point within the zone or zones. --- +-- -- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers. -- Again, aircraft are spawned at random points within these zones around the airport. --- +-- -- ### Misc Options --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png) --- +-- -- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines. -- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines. -- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are: --- +-- -- * @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off, -- * @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on, -- * @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff. -- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jamms on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected. --- --- +-- +-- -- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example --- +-- -- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport. -- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport. --- +-- -- ![Process](..\Presentations\RAT\RAT_Gaussian.png) --- +-- -- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution -- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away -- one gets from the expectation value. --- +-- -- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}(). -- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}() --- +-- -- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}(). --- +-- -- For example: --- +-- -- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300. -- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination. --- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. +-- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. -- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height. --- --- +-- +-- -- @field #RAT RAT={ ClassName = "RAT", -- Name of class: RAT = Random Air Traffic. @@ -391,7 +391,7 @@ RAT={ homebase=nil, -- Home base for commute. continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. alive=0, -- Number of groups which are alive. - ngroups=nil, -- Number of groups to be spawned in total. + ngroups=nil, -- Number of groups to be spawned in total. f10menu=false, -- Add an F10 menu for RAT. Menu={}, -- F10 menu items for this RAT object. SubMenuName=nil, -- Submenu name for RAT object. @@ -414,11 +414,11 @@ RAT={ uncontrolled=false, -- Spawn uncontrolled aircraft. invisible=false, -- Spawn aircraft as invisible. immortal=false, -- Spawn aircraft as indestructible. - activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). + activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. - activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. onboardnum=nil, -- Tail number. onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. checkonrunway=true, -- Check whether aircraft have been spawned on the runway. @@ -436,7 +436,7 @@ RAT={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Categories of the RAT class. +--- Categories of the RAT class. -- @list cat -- @field #string plane Plane. -- @field #string heli Heli. @@ -452,7 +452,7 @@ RAT.wp={ air=1, runway=2, hot=3, - cold=4, + cold=4, climb=5, cruise=6, descent=7, @@ -597,7 +597,7 @@ RAT.version={ --- Create a new RAT object. -- @param #RAT self -- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. --- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. +-- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. -- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor. -- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". -- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. @@ -615,22 +615,22 @@ function RAT:New(groupname, alias) -- Welcome message. self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) - + -- Set alias. alias=alias or groupname - + -- Alias of groupname. self.alias=alias - + -- Get template group defined in the mission editor. local DCSgroup=Group.getByName(groupname) - + -- Check the group actually exists. if DCSgroup==nil then self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) return nil end - + -- Store template group. self.templategroup=GROUP:FindByName(groupname) @@ -639,13 +639,13 @@ function RAT:New(groupname, alias) -- Set own coalition. self.coalition=DCSgroup:getCoalition() - + -- Initialize aircraft parameters based on ME group template. self:_InitAircraft(DCSgroup) - + -- Get all airports of current map (Caucasus, NTTR, Normandy, ...). self:_GetAirportsOfMap() - + return self end @@ -670,46 +670,46 @@ function RAT:Spawn(naircraft) -- Number of aircraft to spawn. Default is one. self.ngroups=naircraft or 1 - + -- Init RAT ATC if not already done. if self.ATCswitch and not RAT.ATC.init then self:_ATCInit(self.airports_map) end - + -- Create F10 main menu if it does not exists yet. if self.f10menu and not RAT.MenuF10 then RAT.MenuF10 = MENU_MISSION:New("RAT") end - + -- Set the coalition table based on choice of self.coalition and self.friendly. self:_SetCoalitionTable() - + -- Get all airports of this map beloning to friendly coalition(s). self:_GetAirportsOfCoalition() - + -- Set submenuname if it has not been set by user. if not self.SubMenuName then self.SubMenuName=self.alias end - -- Get all departure airports inside a Moose zone. + -- Get all departure airports inside a Moose zone. if self.departure_Azone~=nil then self.departure_ports=self:_GetAirportsInZone(self.departure_Azone) end - - -- Get all destination airports inside a Moose zone. + + -- Get all destination airports inside a Moose zone. if self.destination_Azone~=nil then self.destination_ports=self:_GetAirportsInZone(self.destination_Azone) end - + -- Add all friendly airports to possible departures/destinations if self.addfriendlydepartures then self:_AddFriendlyAirports(self.departure_ports) end if self.addfriendlydestinations then self:_AddFriendlyAirports(self.destination_ports) - end - + end + -- Setting and possibly correction min/max/cruise flight levels. if self.FLcruise==nil then -- Default flight level (ASL). @@ -722,11 +722,11 @@ function RAT:Spawn(naircraft) end end - -- Enable helos to go to destinations 100 meters away. + -- Enable helos to go to destinations 100 meters away. if self.category==RAT.cat.heli then self.mindist=50 - end - + end + -- Run consistency checks. self:_CheckConsistency() @@ -759,7 +759,7 @@ function RAT:Spawn(naircraft) text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) @@ -805,7 +805,7 @@ function RAT:Spawn(naircraft) end text=text..string.format("******************************************************\n") self:T(RAT.id..text) - + -- Create submenus. if self.f10menu then self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10) @@ -814,7 +814,7 @@ function RAT:Spawn(naircraft) MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true) end - + -- Schedule spawning of aircraft. local Tstart=self.spawndelay local dt=self.spawninterval @@ -823,10 +823,10 @@ function RAT:Spawn(naircraft) dt=math.max(dt, 180) end local Tstop=Tstart+dt*(self.ngroups-1) - + -- Status check and report scheduler. SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) - + -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) @@ -841,15 +841,15 @@ function RAT:Spawn(naircraft) if self.ngroups==0 then return nil end - + -- Start scheduled spawning. SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) - + -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) end - + return true end @@ -864,7 +864,7 @@ function RAT:_CheckConsistency() -- User has used SetDeparture() if not self.random_departure then - + -- Count departure airports and zones. for _,name in pairs(self.departure_ports) do if self:_AirportExists(name) then @@ -873,7 +873,7 @@ function RAT:_CheckConsistency() self.Ndeparture_Zones=self.Ndeparture_Zones+1 end end - + -- What can go wrong? -- Only zones but not takeoff air == > Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then @@ -891,7 +891,7 @@ function RAT:_CheckConsistency() -- User has used SetDestination() if not self.random_destination then - + -- Count destination airports and zones. for _,name in pairs(self.destination_ports) do if self:_AirportExists(name) then @@ -899,10 +899,10 @@ function RAT:_CheckConsistency() elseif self:_ZoneExists(name) then self.Ndestination_Zones=self.Ndestination_Zones+1 end - end - + end + -- One zone specified as destination ==> Enable destination zone. - -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. + -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then self.landing=RAT.wp.air self.destinationzone=true @@ -915,19 +915,19 @@ function RAT:_CheckConsistency() self:E(RAT.id.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end - end - + end + -- Destination zone and return zone should not be used together. if self.destinationzone and self.returnzone then self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") self.returnzone=false end - -- If returning to a zone, we set the landing type to "air" if takeoff is in air. + -- If returning to a zone, we set the landing type to "air" if takeoff is in air. -- Because if we start in air we want to end in air. But default landing is ground. if self.returnzone and self.takeoff==RAT.wp.air then self.landing=RAT.wp.air end - + -- Ensure that neither FLmin nor FLmax are above the aircrafts service ceiling. if self.FLminuser then self.FLminuser=math.min(self.FLminuser, self.aircraft.ceiling) @@ -938,17 +938,17 @@ function RAT:_CheckConsistency() if self.FLcruise then self.FLcruise=math.min(self.FLcruise, self.aircraft.ceiling) end - + -- FL min > FL max case ==> spaw values if self.FLminuser and self.FLmaxuser then if self.FLminuser > self.FLmaxuser then local min=self.FLminuser local max=self.FLmaxuser self.FLminuser=max - self.FLmaxuser=min + self.FLmaxuser=min end end - + -- Cruise alt < FL min if self.FLminuser and self.FLcruise FL max if self.FLmaxuser and self.FLcruise>self.FLmaxuser then self.FLcruise=self.FLmaxuser end - + -- Uncontrolled aircraft must start with engines off. if self.uncontrolled then -- SOLVED: Strangly, it does not work with RAT.wp.cold only with RAT.wp.hot! @@ -995,11 +995,11 @@ end --- Set coalition of RAT group. You can make red templates blue and vice versa. -- Note that a country is also set automatically if it has not done before via RAT:SetCountry. --- +-- -- * For blue, the country is set to USA. -- * For red, the country is set to RUSSIA. -- * For neutral, the country is set to SWITZERLAND. --- +-- -- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft. -- You can set the country explicitly via the RAT:SetCountry() function if necessary. -- @param #RAT self @@ -1021,14 +1021,14 @@ function RAT:SetCoalitionAircraft(color) self.coalition=coalition.side.NEUTRAL if not self.country then self.country=country.id.SWITZERLAND - end + end end return self end --- Set country of RAT group. -- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country). --- +-- -- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition. -- @param #RAT self -- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. @@ -1045,7 +1045,7 @@ end -- @param #RAT self -- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX. -- @return #RAT RAT self object. --- +-- -- @usage -- c17=RAT:New("C-17 BIG Plane") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used. @@ -1111,35 +1111,35 @@ function RAT:SetDespawnAirOFF() end --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. --- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. +-- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self -- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". -- @return #RAT RAT self object. -- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. -- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. --- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. +-- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. function RAT:SetTakeoff(type) self:F2(type) - + local _Type if type:lower()=="takeoff-cold" or type:lower()=="cold" then _Type=RAT.wp.cold elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then _Type=RAT.wp.hot - elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then + elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then _Type=RAT.wp.runway elseif type:lower()=="air" then _Type=RAT.wp.air else _Type=RAT.wp.coldorhot end - + self.takeoff=_Type - + return self end ---- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. +--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffCold() @@ -1147,7 +1147,7 @@ function RAT:SetTakeoffCold() return self end ---- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. +--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffHot() @@ -1155,7 +1155,7 @@ function RAT:SetTakeoffHot() return self end ---- Set takeoff type to runway. Aircraft will spawn directly on the runway. +--- Set takeoff type to runway. Aircraft will spawn directly on the runway. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffRunway() @@ -1163,7 +1163,7 @@ function RAT:SetTakeoffRunway() return self end ---- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. +--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffColdOrHot() @@ -1171,7 +1171,7 @@ function RAT:SetTakeoffColdOrHot() return self end ---- Set takeoff type to air. Aircraft will spawn in the air. +--- Set takeoff type to air. Aircraft will spawn in the air. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffAir() @@ -1191,21 +1191,21 @@ function RAT:SetDeparture(departurenames) -- Random departure is deactivated now that user specified departure ports. self.random_departure=false - + -- Convert input to table. local names if type(departurenames)=="table" then - names=departurenames + names=departurenames elseif type(departurenames)=="string" then names={departurenames} else -- error message self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.departure_ports, name) @@ -1215,9 +1215,9 @@ function RAT:SetDeparture(departurenames) else self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) end - + end - + return self end @@ -1231,7 +1231,7 @@ function RAT:SetDestination(destinationnames) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Convert input to table local names if type(destinationnames)=="table" then @@ -1242,10 +1242,10 @@ function RAT:SetDestination(destinationnames) -- Error message. self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.destination_ports, name) @@ -1255,7 +1255,7 @@ function RAT:SetDestination(destinationnames) else self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) end - + end return self @@ -1266,13 +1266,13 @@ end -- @return #RAT RAT self object. function RAT:DestinationZone() self:F2() - + -- Destination is a zone. Needs special care. self.destinationzone=true - + -- Landing type is "air" because we don't actually land at the airport. self.landing=RAT.wp.air - + return self end @@ -1296,10 +1296,10 @@ function RAT:SetDestinationsFromZone(zone) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Set zone. self.destination_Azone=zone - + return self end @@ -1309,13 +1309,13 @@ end -- @return #RAT RAT self object. function RAT:SetDeparturesFromZone(zone) self:F2(zone) - + -- Random departure is deactivated now that user specified departure ports. self.random_departure=false -- Set zone. self.departure_Azone=zone - + return self end @@ -1351,7 +1351,7 @@ function RAT:ExcludedAirports(ports) return self end ---- Set skill of AI aircraft. Default is "High". +--- Set skill of AI aircraft. Default is "High". -- @param #RAT self -- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive. -- @return #RAT RAT self object. @@ -1422,7 +1422,7 @@ function RAT:Commute(starshape) return self end ---- Set the delay before first group is spawned. +--- Set the delay before first group is spawned. -- @param #RAT self -- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds. -- @return #RAT RAT self object. @@ -1537,7 +1537,7 @@ end --- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. -- @param #RAT self -- @param #boolean switch If true, check is performed. If false, this check is omitted. --- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. +-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. -- @return #RAT RAT self object. function RAT:CheckOnRunway(switch, distance) self:F2(switch) @@ -1617,7 +1617,7 @@ function RAT:RadioModulation(modulation) return self end ---- Radio menu On. Default is off. +--- Radio menu On. Default is off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuON() @@ -1626,7 +1626,7 @@ function RAT:RadioMenuON() return self end ---- Radio menu Off. This is the default setting. +--- Radio menu Off. This is the default setting. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuOFF() @@ -1635,7 +1635,7 @@ function RAT:RadioMenuOFF() return self end ---- Aircraft are invisible. +--- Aircraft are invisible. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Invisible() @@ -1644,7 +1644,7 @@ function RAT:Invisible() return self end ---- Turn EPLRS datalink on/off. +--- Turn EPLRS datalink on/off. -- @param #RAT self -- @param #boolean switch If true (or nil), turn EPLRS on. -- @return #RAT RAT self object. @@ -1657,7 +1657,7 @@ function RAT:SetEPLRS(switch) return self end ---- Aircraft are immortal. +--- Aircraft are immortal. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Immortal() @@ -1675,7 +1675,7 @@ function RAT:Uncontrolled() return self end ---- Activate uncontrolled aircraft. +--- Activate uncontrolled aircraft. -- @param #RAT self -- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. -- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. @@ -1690,21 +1690,21 @@ function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand) self.activate_delay=delay or 1 self.activate_delta=delta or 1 self.activate_frand=frand or 0 - + -- Ensure min delay is one second. self.activate_delay=math.max(self.activate_delay,1) - + -- Ensure min delta is one second. self.activate_delta=math.max(self.activate_delta,0) - + -- Ensure frand is in [0,...,1] self.activate_frand=math.max(self.activate_frand,0) self.activate_frand=math.min(self.activate_frand,1) - + return self end ---- Set the time after which inactive groups will be destroyed. +--- Set the time after which inactive groups will be destroyed. -- @param #RAT self -- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds. -- @return #RAT RAT self object. @@ -1759,7 +1759,7 @@ end -- @return #RAT RAT self object. function RAT:SetROE(roe) self:F2(roe) - if roe=="return" then + if roe=="return" then self.roe=RAT.ROE.returnfire elseif roe=="free" then self.roe=RAT.ROE.weaponfree @@ -1811,7 +1811,7 @@ end --- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #boolean switch Enable (true) or disable (false) messages from ATC. --- @return #RAT RAT self object. +-- @return #RAT RAT self object. function RAT:ATC_Messages(switch) self:F2(switch) if switch==nil then @@ -1821,7 +1821,7 @@ function RAT:ATC_Messages(switch) return self end ---- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! +--- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2. -- @return #RAT RAT self object. @@ -1831,7 +1831,7 @@ function RAT:ATC_Clearance(n) return self end ---- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! +--- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec. -- @return #RAT RAT self object. @@ -1987,7 +1987,7 @@ end --- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken. -- @param #RAT self --- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... +-- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... -- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. -- @return #RAT RAT self object. function RAT:SetOnboardNum(tailnumprefix, zero) @@ -2013,7 +2013,7 @@ function RAT:_InitAircraft(DCSgroup) local DCSdesc=DCSunit:getDesc() local DCScategory=DCSgroup:getCategory() local DCStype=DCSunit:getTypeName() - + -- set category if DCScategory==Group.Category.AIRPLANE then self.category=RAT.cat.plane @@ -2023,37 +2023,37 @@ function RAT:_InitAircraft(DCSgroup) self.category="other" self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") end - + -- Get type of aircraft. self.aircraft.type=DCStype - + -- inital fuel in % self.aircraft.fuel=DCSunit:getFuel() -- operational range in NM converted to m self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m - + -- effective range taking fuel into accound and a 5% reserve self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.95 - + -- max airspeed from group self.aircraft.Vmax = DCSdesc.speedMax - + -- max climb speed in m/s self.aircraft.Vymax=DCSdesc.VyMax - + -- service ceiling in meters self.aircraft.ceiling=DCSdesc.Hmax - + -- Store all descriptors. --self.aircraft.descriptors=DCSdesc - + -- aircraft dimensions self.aircraft.length=DCSdesc.box.max.x self.aircraft.height=DCSdesc.box.max.y self.aircraft.width=DCSdesc.box.max.z self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) - + -- info message local text=string.format("\n******************************************************\n") text=text..string.format("Aircraft parameters:\n") @@ -2099,7 +2099,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Set takeoff type. local takeoff=self.takeoff local landing=self.landing - + -- Overrule takeoff/landing by what comes in. if _takeoff then takeoff=_takeoff @@ -2107,27 +2107,27 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if _landing then landing=_landing end - + -- Random choice between cold and hot. if takeoff==RAT.wp.coldorhot then local temp={RAT.wp.cold, RAT.wp.hot} takeoff=temp[math.random(2)] end - + -- Number of respawn attempts after spawning on runway. local nrespawn=0 if _nrespawn then nrespawn=_nrespawn end - + -- Set flight plan. local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Return nil if we could not find a departure destination or waypoints if not (departure and destination and waypoints) then return nil end - + -- Set (another) livery. local livery if _livery then @@ -2141,20 +2141,20 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live else livery=nil end - + -- Modify the spawn template to follow the flight plan. local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) if not successful then return nil end - + -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP - + -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) - + -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then @@ -2163,34 +2163,34 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:_ATCAddFlight(group:GetName(), destination:GetName()) end end - + -- Place markers of waypoints on F10 map. if self.placemarkers then self:_PlaceMarkers(waypoints, self.SpawnIndex) end - + -- Set group to be invisible. if self.invisible then self:_CommandInvisible(group, true) end - + -- Set group to be immortal. if self.immortal then self:_CommandImmortal(group, true) end - + -- Set group to be immortal. if self.eplrs then group:CommandEPLRS(true, 1) - end - + end + -- Set ROE, default is "weapon hold". self:_SetROE(group, self.roe) - + -- Set ROT, default is "no reaction". self:_SetROT(group, self.rot) - -- Init ratcraft array. + -- Init ratcraft array. self.ratcraft[self.SpawnIndex]={} self.ratcraft[self.SpawnIndex]["group"]=group self.ratcraft[self.SpawnIndex]["destination"]=destination @@ -2218,28 +2218,28 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Distance"]=0 - + -- Each aircraft gets its own takeoff type. self.ratcraft[self.SpawnIndex].takeoff=takeoff self.ratcraft[self.SpawnIndex].landing=landing self.ratcraft[self.SpawnIndex].wpholding=WPholding self.ratcraft[self.SpawnIndex].wpfinal=WPfinal - + -- Aircraft is active or spawned in uncontrolled state. self.ratcraft[self.SpawnIndex].active=not self.uncontrolled - + -- Set status to spawned. This will be overwritten in birth event. self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned - + -- Livery self.ratcraft[self.SpawnIndex].livery=livery - + -- If this switch is set to true, the aircraft will be despawned the next time the status function is called. self.ratcraft[self.SpawnIndex].despawnme=false - + -- Number of preformed spawn attempts for this group. self.ratcraft[self.SpawnIndex].nrespawn=nrespawn - + -- Create submenu for this group. if self.f10menu then local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) @@ -2254,13 +2254,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) - MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) + MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) -- F10/RAT//Group X/ MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) end - + return self.SpawnIndex end @@ -2280,10 +2280,10 @@ end -- @param Core.Point#COORDINATE lastpos Last known position of the group. -- @param #number delay Delay before respawn function RAT:_Respawn(index, lastpos, delay) - + -- Get the spawn index from group --local index=self:GetSpawnIndexFromGroup(group) - + -- Get departure and destination from previous journey. local departure=self.ratcraft[index].departure local destination=self.ratcraft[index].destination @@ -2292,7 +2292,7 @@ function RAT:_Respawn(index, lastpos, delay) local livery=self.ratcraft[index].livery local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] --local lastpos=group:GetCoordinate() - + local _departure=nil local _destination=nil local _takeoff=nil @@ -2300,15 +2300,15 @@ function RAT:_Respawn(index, lastpos, delay) local _livery=nil local _lastwp=nil local _lastpos=nil - + if self.continuejourney then - + -- We continue our journey from the old departure airport. _departure=destination:GetName() - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. @@ -2318,15 +2318,15 @@ function RAT:_Respawn(index, lastpos, delay) _lastpos=lastpos end end - + if self.destinationzone then - + -- Case: X --> Zone --> Zone --> Zone _takeoff=RAT.wp.air _landing=RAT.wp.air - + elseif self.returnzone then - + -- Case: X --> Zone --> X, X --> Zone --> X -- We flew to a zone and back. Takeoff type does not change. _takeoff=self.takeoff @@ -2337,22 +2337,22 @@ function RAT:_Respawn(index, lastpos, delay) else _landing=RAT.wp.landing end - + -- Departure stays the same. (The destination is the zone here.) _departure=departure:GetName() - + else - + -- Default case. Takeoff and landing type does not change. _takeoff=self.takeoff _landing=self.landing - + end - + elseif self.commute then - + -- We commute between departure and destination. - + if self.starshape==true then if destination:GetName()==self.homebase then -- We are at our home base ==> destination is again randomly selected. @@ -2368,10 +2368,10 @@ function RAT:_Respawn(index, lastpos, delay) _departure=destination:GetName() _destination=departure:GetName() end - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. @@ -2379,22 +2379,22 @@ function RAT:_Respawn(index, lastpos, delay) -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). if destination:GetCategory()==4 then _lastpos=lastpos - end + end end - + -- Handle takeoff type. if self.destinationzone then -- self.takeoff is either RAT.wp.air or RAT.wp.cold -- self.landing is RAT.wp.Air - + if self.takeoff==RAT.wp.air then - + -- Case: Zone <--> Zone (both have takeoff air) _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) _landing=RAT.wp.air -- = self.landing (because destinationzone) - + else - + -- Case: Airport <--> Zone if takeoff==RAT.wp.air then -- Last takeoff was air so we are at the airport now, takeoff is from ground. @@ -2405,31 +2405,31 @@ function RAT:_Respawn(index, lastpos, delay) _takeoff=RAT.wp.air _landing=RAT.wp.landing end - + end - + elseif self.returnzone then - + -- We flew to a zone and back. No need to swap departure and destination. _departure=departure:GetName() _destination=destination:GetName() - + -- Takeoff and landing should also not change. _takeoff=self.takeoff _landing=self.landing - - end - + + end + end - + -- Take the last waypoint as initial waypoint for next plane. if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then _lastwp=lastwp end - + -- Debug self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) - + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local respawndelay if delay then @@ -2439,7 +2439,7 @@ function RAT:_Respawn(index, lastpos, delay) else respawndelay=3 end - + -- Spawn new group. local arg={} arg.self=self @@ -2452,7 +2452,7 @@ function RAT:_Respawn(index, lastpos, delay) arg.lastpos=_lastpos self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) - + end --- Delayed spawn function called by scheduler. @@ -2476,7 +2476,7 @@ end -- @return #table Table of flight plan waypoints. -- @return #nil If no valid departure or destination airport could be found. function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Max cruise speed. local VxCruiseMax if self.Vcruisemax then @@ -2486,39 +2486,39 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) end - + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - + -- Cruise speed (randomized). Expectation value at midpoint between min and max. local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - + -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(self.aircraft.Vmax*0.60, 140) - + -- Holding speed is 90% of descent speed. local VxHolding = VxDescent*0.9 - + -- Final leg is 90% of holding speed. local VxFinal = VxHolding*0.9 - + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax) - + -- Climb angle in rad. local AlphaClimb=math.asin(VyClimb/VxClimb) - + -- Descent angle in rad. local AlphaDescent=math.rad(self.AlphaDescent) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=self.FLcruise - - - -- DEPARTURE AIRPORT + + + -- DEPARTURE AIRPORT -- Departure airport or zone. local departure=nil if _departure then @@ -2535,15 +2535,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) self:E(RAT.id..text) - end - + end + else departure=self:_PickDeparture(takeoff) if self.commute and self.starshape==true and self.homebase==nil then self.homebase=departure:GetName() end end - + -- Return nil if no departure could be found. if not departure then local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) @@ -2561,11 +2561,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- For an air start, we take a random point within the spawn zone. local vec2=departure:GetRandomVec2() Pdeparture=COORDINATE:NewFromVec2(vec2) - end + end else Pdeparture=departure:GetCoordinate() end - + -- Height ASL of departure point. local H_departure if takeoff==RAT.wp.air then @@ -2576,7 +2576,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Hmin=50 end - -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). + -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). H_departure=self:_Randomize(FLcruise_expect*0.7, 0.3, Pdeparture.y+Hmin, FLcruise_expect) if self.FLminuser then H_departure=math.max(H_departure,self.FLminuser) @@ -2588,18 +2588,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else H_departure=Pdeparture.y end - + -- Adjust min distance between departure and destination for user set min flight level. local mindist=self.mindist if self.FLminuser then - + -- We can conly consider the symmetric case, because no destination selected yet. local hclimb=self.FLminuser-H_departure local hdescent=self.FLminuser-H_departure - + -- Minimum distance for l local Dclimb, Ddescent, Dtot=self:_MinDistance(AlphaClimb, AlphaDescent, hclimb, hdescent) - + if takeoff==RAT.wp.air and landing==RAT.wpair then mindist=0 -- Takeoff and landing are in air. No mindist required. elseif takeoff==RAT.wp.air then @@ -2609,32 +2609,32 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else mindist=Dtot -- Takeoff and landing on ground. Need both space to climb and descent. end - + -- Mindist is at least self.mindist. mindist=math.max(self.mindist, mindist) - + local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m) self:T(RAT.id..text) end - + -- DESTINATION AIRPORT local destination=nil if _destination then - + if self:_AirportExists(_destination) then - + destination=AIRBASE:FindByName(_destination) if landing==RAT.wp.air or self.returnzone then destination=destination:GetZone() end - + elseif self:_ZoneExists(_destination) then destination=ZONE:New(_destination) else local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) self:E(RAT.id.."ERROR: "..text) end - + else -- This handles the case where we have a journey and the first flight is done, i.e. _departure is set. @@ -2644,7 +2644,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.continuejourney and _departure and #self.destination_ports<3 then random=true end - + -- In case of a returnzone the destination (i.e. return point) is always a zone. local mylanding=landing local acrange=self.aircraft.Reff @@ -2652,11 +2652,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) mylanding=RAT.wp.air acrange=self.aircraft.Reff/2 -- Aircraft needs to go to zone and back home. end - + -- Pick a destination airport. destination=self:_PickDestination(departure, Pdeparture, mindist, math.min(acrange, self.maxdist), random, mylanding) end - + -- Return nil if no departure could be found. if not destination then local text=string.format("No valid destination airport could be found for %s!", self.alias) @@ -2664,14 +2664,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self:E(RAT.id.."ERROR: "..text) return nil end - + -- Check that departure and destination are not the same. Should not happen due to mindist. if destination:GetName()==departure:GetName() then local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName()) MESSAGE:New(text, 30):ToAll() self:E(RAT.id.."ERROR: "..text) end - + -- Get a random point inside zone return zone. local Preturn local destination_returnzone @@ -2684,7 +2684,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Set departure to destination. destination=departure end - + -- Get destination coordinate. Either in a zone or exactly at the airport. local Pdestination if landing==RAT.wp.air then @@ -2693,10 +2693,10 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Pdestination=destination:GetCoordinate() end - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + -- DESCENT/HOLDING POINT -- Get a random point between 5 and 10 km away from the destination. local Rhmin=8000 @@ -2706,14 +2706,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax, Rhmin) local Pholding=COORDINATE:NewFromVec2(Vholding) - + -- AGL height of holding point. local H_holding=Pholding.y - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding if self.category==RAT.cat.plane then @@ -2722,38 +2722,38 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) h_holding=150 end h_holding=self:_Randomize(h_holding, 0.2) - + -- This is the actual height ASL of the holding point we want to fly to local Hh_holding=H_holding+h_holding - + -- When we dont land, we set the holding altitude to the departure or cruise alt. -- This is used in the calculations. if landing==RAT.wp.air then Hh_holding=H_departure end - + -- Distance from holding point to final destination. local d_holding=Pholding:Get2DDistance(Pdestination) - + -- GENERAL local heading local d_total if self.returnzone then - + -- Heading from departure to destination in return zone. heading=self:_Course(Pdeparture, Preturn) - + -- Total distance to return zone and back. d_total=Pdeparture:Get2DDistance(Preturn) + Preturn:Get2DDistance(Pholding) - + else -- Heading from departure to holding point of destination. heading=self:_Course(Pdeparture, Pholding) - + -- Total distance between departure and holding point near destination. d_total=Pdeparture:Get2DDistance(Pholding) end - + -- Max height in case of air start, i.e. if we only would descent to holding point for the given distance. if takeoff==RAT.wp.air then local H_departure_max @@ -2764,15 +2764,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end H_departure=math.min(H_departure, H_departure_max) end - + -------------------------------------------- - + -- Height difference between departure and destination. local deltaH=math.abs(H_departure-Hh_holding) - + -- Slope between departure and destination. local phi = math.atan(deltaH/d_total) - + -- Adjusted climb/descent angles. local phi_climb local phi_descent @@ -2791,18 +2791,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else D_total = math.sqrt(deltaH*deltaH+d_total*d_total) end - + -- SSA triangle for sloped case. local gamma=math.rad(180)-phi_climb-phi_descent local a = D_total*math.sin(phi_climb)/math.sin(gamma) local b = D_total*math.sin(phi_descent)/math.sin(gamma) local hphi_max = b*math.sin(phi_climb) local hphi_max2 = a*math.sin(phi_descent) - + -- Height of triangle. local h_max1 = b*math.sin(AlphaClimb) local h_max2 = a*math.sin(AlphaDescent) - + -- Max height relative to departure or destination. local h_max if (H_departure > Hh_holding) then @@ -2810,23 +2810,23 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else h_max=math.max(h_max1, h_max2) end - + -- Max flight level aircraft can reach for given angles and distance. local FLmax = h_max+H_departure - - --CRUISE + + --CRUISE -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. local FLmin=math.max(H_departure, Hh_holding) - + -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m. - if self.category==RAT.cat.heli then + if self.category==RAT.cat.heli then FLmin=math.max(H_departure, H_destination)+50 FLmax=math.max(H_departure, H_destination)+1000 end - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, self.aircraft.ceiling) - + -- Overrule setting if user specified min/max flight level explicitly. if self.FLminuser then FLmin=math.max(self.FLminuser, FLmin) -- Still take care that we dont fly too high. @@ -2834,12 +2834,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.FLmaxuser then FLmax=math.min(self.FLmaxuser, FLmax) -- Still take care that we dont fly too low. end - + -- If the route is very short we set FLmin a bit lower than FLmax. if FLmin>FLmax then FLmin=FLmax end - + -- Expected cruise altitude - peak of gaussian distribution. if FLcruise_expectFLmax then FLcruise_expect=FLmax end - + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) - + -- Overrule setting if user specified a flight level explicitly. if self.FLuser then FLcruise=self.FLuser @@ -2862,12 +2862,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Distances. local d_climb = h_climb/math.tan(AlphaClimb) local d_descent = h_descent/math.tan(AlphaDescent) - local d_cruise = d_total-d_climb-d_descent - + local d_cruise = d_total-d_climb-d_descent + -- debug message local text=string.format("\n******************************************************\n") text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) @@ -2899,7 +2899,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m) text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m) text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m) - text=text..string.format("\nAngles:\n") + text=text..string.format("\nAngles:\n") text=text..string.format("Alpha climb = %6.2f Deg\n", math.deg(AlphaClimb)) text=text..string.format("Alpha descent = %6.2f Deg\n", math.deg(AlphaDescent)) text=text..string.format("Phi (slope) = %6.2f Deg\n", math.deg(phi)) @@ -2909,7 +2909,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max heights and distances if we would travel at FLmax. local h_climb_max = FLmax - H_departure local h_descent_max = FLmax - Hh_holding - local d_climb_max = h_climb_max/math.tan(AlphaClimb) + local d_climb_max = h_climb_max/math.tan(AlphaClimb) local d_descent_max = h_descent_max/math.tan(AlphaDescent) local d_cruise_max = d_total-d_climb_max-d_descent_max text=text..string.format("Heading = %6.1f Deg\n", heading) @@ -2932,7 +2932,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end text=text..string.format("******************************************************\n") self:T2(RAT.id..text) - + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then d_cruise=100 @@ -2943,80 +2943,80 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local c={} local wpholding=nil local wpfinal=nil - + -- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure) self.waypointdescriptions[#wp]="Departure" self.waypointstatus[#wp]=RAT.status.Departure - + -- Climb if takeoff==RAT.wp.air then - + -- Air start. if d_climb < 5000 or d_cruise < 5000 then -- We omit the climb phase completely and add it to the cruise part. d_cruise=d_cruise+d_climb - else + else -- Only one waypoint at the end of climb = begin of cruise. c[#c+1]=c[#c]:Translate(d_climb, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise end - + else - + -- Ground start. c[#c+1]=c[#c]:Translate(d_climb/2, heading) c[#c+1]=c[#c]:Translate(d_climb/2, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb, c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2) self.waypointdescriptions[#wp]="Climb" self.waypointstatus[#wp]=RAT.status.Climb - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise - + end - + -- Cruise - + -- First add the little bit from begin of cruise to the return point. - if self.returnzone then - c[#c+1]=Preturn + if self.returnzone then + c[#c+1]=Preturn wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Return Zone" self.waypointstatus[#wp]=RAT.status.Uturn end - + if landing==RAT.wp.air then - + -- Next waypoint is already the final destination. c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + elseif self.returnzone then - - -- The little bit back to end of cruise. - c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) + + -- The little bit back to end of cruise. + c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + else - + c[#c+1]=c[#c]:Translate(d_cruise, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + end - + -- Descent (only if we acually want to land) if landing==RAT.wp.landing then if self.returnzone then @@ -3031,45 +3031,45 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self.waypointstatus[#wp]=RAT.status.DescentHolding end end - + -- Holding and final destination. if landing==RAT.wp.landing then -- Holding point - c[#c+1]=Pholding + c[#c+1]=Pholding wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) self.waypointdescriptions[#wp]="Holding Point" self.waypointstatus[#wp]=RAT.status.Holding wpholding=#wp -- Final destination. - c[#c+1]=Pdestination + c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + end - + -- Final Waypoint wpfinal=#wp - + -- Fill table with waypoints. local waypoints={} for _,p in ipairs(wp) do table.insert(waypoints, p) end - + -- Some info on the route. self:_Routeinfo(waypoints, "Waypoint info in set_route:") - + -- Return departure, destination and waypoints. if self.returnzone then -- We return the actual zone here because returning the departure leads to problems with commute. - return departure, destination_returnzone, waypoints, wpholding, wpfinal + return departure, destination_returnzone, waypoints, wpholding, wpfinal else return departure, destination, waypoints, wpholding, wpfinal end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3084,41 +3084,41 @@ function RAT:_PickDeparture(takeoff) -- Array of possible departure airports or zones. local departures={} - + if self.random_departure then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do - + local airport=_airport --Wrapper.Airbase#AIRBASE - + local name=airport:GetName() if not self:_Excluded(name) then if takeoff==RAT.wp.air then - + table.insert(departures, airport:GetZone()) -- insert zone object. - + else - + -- Check if airbase has the right terminals. local nspots=1 if self.termtype~=nil then nspots=airport:GetParkingSpotsNumber(self.termtype) end - + if nspots>0 then table.insert(departures, airport) -- insert airport object. end end end - + end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.departure_ports) do - + local dep=nil if self:_AirportExists(name) then if takeoff==RAT.wp.air then @@ -3143,22 +3143,22 @@ function RAT:_PickDeparture(takeoff) else self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) end - + -- Add to departures table. if dep then table.insert(departures, dep) end - - end - + + end + end - + -- Info message. self:T(RAT.id..string.format("Number of possible departures for %s= %d", self.alias, #departures)) - + -- Select departure airport or zone. local departure=departures[math.random(#departures)] - + local text if departure and departure:GetName() then if takeoff==RAT.wp.air then @@ -3172,7 +3172,7 @@ function RAT:_PickDeparture(takeoff) self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end - + return departure end @@ -3193,18 +3193,18 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) -- All possible destinations. local destinations={} - + if random then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do local airport=_airport --Wrapper.Airbase#AIRBASE local name=airport:GetName() if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then - + -- Distance from departure to possible destination local distance=q:Get2DDistance(airport:GetCoordinate()) - + -- Check if distance form departure to destination is within min/max range. if distance>=minrange and distance<=maxrange then if landing==RAT.wp.air then @@ -3222,12 +3222,12 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end end end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.destination_ports) do - + -- Make sure departure and destination are not identical. if name ~= departure:GetName() then @@ -3255,11 +3255,11 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) end - + if dest then -- Distance from departure to possible destination local distance=q:Get2DDistance(dest:GetCoordinate()) - + -- Add as possible destination if zone is within range. if distance>=minrange and distance<=maxrange then table.insert(destinations, dest) @@ -3268,14 +3268,14 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) self:T(RAT.id..text) end end - + end - end + end end - + -- Info message. self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) - + if #destinations > 0 then --- Compare distance of destination airports. -- @param Core.Point#COORDINATE a Coordinate of point a. @@ -3290,15 +3290,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else destinations=nil end - - + + -- Randomly select one possible destination. local destination if destinations and #destinations>0 then - + -- Random selection. destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE - + -- Debug message. local text if landing==RAT.wp.air then @@ -3308,15 +3308,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end self:T(RAT.id..text) --MESSAGE:New(text, 30):ToAllIf(self.Debug) - + else self:E(RAT.id.."ERROR! No destination airport or zone found.") destination=nil end - + -- Return the chosen destination. - return destination - + return destination + end --- Find airports within a zone. @@ -3328,7 +3328,7 @@ function RAT:_GetAirportsInZone(zone) for _,airport in pairs(self.airports) do local name=airport:GetName() local coord=airport:GetCoordinate() - + if zone:IsPointVec3InZone(coord) then table.insert(airports, name) end @@ -3369,9 +3369,9 @@ end -- @param #RAT self function RAT:_GetAirportsOfMap() local _coalition - + for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE - + -- set coalition if i==0 then _coalition=coalition.side.NEUTRAL @@ -3380,33 +3380,33 @@ function RAT:_GetAirportsOfMap() elseif i==2 then _coalition=coalition.side.BLUE end - + -- get airbases of coalition local ab=coalition.getAirbases(i) - + -- loop over airbases and put them in a table for _,airbase in pairs(ab) do - + local _id=airbase:getID() local _p=airbase:getPosition().p local _name=airbase:getName() local _myab=AIRBASE:FindByName(_name) - + if _myab then - + -- Add airport to table. table.insert(self.airports_map, _myab) - + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() self:T(RAT.id..text) - + else - + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) - + end end - + end end @@ -3424,7 +3424,7 @@ function RAT:_GetAirportsOfCoalition() -- Planes cannot land on ships. --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP - + -- Check that airport has the requested terminal types. -- NOT good here because we would also not allow any airport zones! --[[ @@ -3434,14 +3434,14 @@ function RAT:_GetAirportsOfCoalition() end local condition3 = nspots==0 ]] - + if not (condition1 or condition2) then table.insert(self.airports, airport) end end end end - + if #self.airports==0 then local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) MESSAGE:New(text, 10):ToAll() @@ -3460,26 +3460,26 @@ function RAT:Status(message, forID) -- Optional arguments. if message==nil then message=false - end + end if forID==nil then forID=false end - + -- Current time. local Tnow=timer.getTime() - + -- Alive counter. local nalive=0 - + -- Loop over all ratcraft. for spawnindex,ratcraft in ipairs(self.ratcraft) do - + -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then nalive=nalive+1 - + -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) local life=self:_GetLife(group) @@ -3495,7 +3495,7 @@ function RAT:Status(message, forID) local active=ratcraft.active local Nunits=ratcraft.nunits -- group:GetSize() local N0units=group:GetInitialSize() - + -- Monitor time and distance on ground. local Tg=0 local Dg=0 @@ -3512,51 +3512,51 @@ function RAT:Status(message, forID) if ratcraft["Tground"] then -- Aircraft was already on ground. Calculate total time on ground. Tg=Tnow-ratcraft["Tground"] - + -- Distance on ground since last check. Dg=coords:Get2DDistance(ratcraft["Pground"]) - + -- Time interval since last check. dTlast=Tnow-ratcraft["Tlastcheck"] - + -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. if dTlast > self.Tinactive then - + --[[ if Dg<50 and active and status~=RAT.status.EventBirth then stationary=true end ]] - + -- Loop over all units. for _,_unit in pairs(group:GetUnits()) do - + if _unit and _unit:IsAlive() then - + -- Unit name, coord and distance since last check. local unitname=_unit:GetName() local unitcoord=_unit:GetCoordinate() local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) - + -- Debug info self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) - + -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - -- Aircraft which are spawned uncontrolled or starting their engines are not counted. + -- Aircraft which are spawned uncontrolled or starting their engines are not counted. if Ug<50 and active and status~=RAT.status.EventBirth then stationary=true end - + -- Update coords. ratcraft["Uground"][unitname]=unitcoord end end - + -- Set the current time to know when the next check is necessary. ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords + ratcraft["Pground"]=coords end - + else -- First time we see that the aircraft is on ground. Initialize the times and position. ratcraft["Tground"]=Tnow @@ -3569,18 +3569,18 @@ function RAT:Status(message, forID) end end end - + -- Monitor travelled distance since last check. local Pn=coords local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) ratcraft["Pnow"]=Pn - + -- Add up the travelled distance. ratcraft["Distance"]=ratcraft["Distance"]+Dtravel - + -- Distance remaining to destination. local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) - + -- Status report. if (forID and spawnindex==forID) or (not forID) then local text=string.format("ID %i of flight %s", spawnindex, prefix) @@ -3617,44 +3617,44 @@ function RAT:Status(message, forID) MESSAGE:New(text, 20):ToAll() end end - + -- Despawn groups if they are on ground and don't move or are damaged. if not airborne then - + -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) self:T(RAT.id..text) self:_Despawn(group) end - + -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) self:T(RAT.id..text) self:_Despawn(group) end - + end - + -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then - + local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) - + -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then local idx=self:GetSpawnIndexFromGroup(group) - local coord=group:GetCoordinate() + local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) end - + -- Despawn old group. if self.despawnair then self:_Despawn(group, 0) end - + end else @@ -3662,14 +3662,14 @@ function RAT:Status(message, forID) local text=string.format("Group does not exist in loop ratcraft status.") self:T2(RAT.id..text) end - + end - + -- Alive groups. local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) self:T(RAT.id..text) MESSAGE:New(text, 20):ToAllIf(message and not forID) - + end --- Get (relative) life of first unit of a group. @@ -3701,26 +3701,26 @@ function RAT:_SetStatus(group, status) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. self.ratcraft[index].status=status - + -- No status update message for "first waypoint", "holding" local no1 = status==RAT.status.Departure local no2 = status==RAT.status.EventBirthAir local no3 = status==RAT.status.Holding - + local text=string.format("Flight %s: %s.", group:GetName(), status) self:T(RAT.id..text) - + if not (no1 or no2 or no3) then MESSAGE:New(text, 10):ToAllIf(self.reportstatus) end - + end - + end end @@ -3734,16 +3734,16 @@ function RAT:GetStatus(group) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. return self.ratcraft[index].status - + end - + end - + return "nonexistant" end @@ -3758,20 +3758,20 @@ function RAT:_OnBirth(EventData) self:T3(RAT.id.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(RAT.id..text) - + -- Set status. local status="unknown in birth" if SpawnGroup:InAir() then @@ -3782,7 +3782,7 @@ function RAT:_OnBirth(EventData) status=RAT.status.EventBirth end self:_SetStatus(SpawnGroup, status) - + -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) local _departure=self.ratcraft[i].departure:GetName() @@ -3791,10 +3791,10 @@ function RAT:_OnBirth(EventData) local _takeoff=self.ratcraft[i].takeoff local _landing=self.ratcraft[i].landing local _livery=self.ratcraft[i].livery - + -- Some is only useful for an actual airbase (not a zone). local _airbase=AIRBASE:FindByName(_departure) - + -- Check if aircraft group was accidentally spawned on the runway. -- This can happen due to no parking slots available and other DCS bugs. local onrunway=false @@ -3802,12 +3802,12 @@ function RAT:_OnBirth(EventData) -- Check that we did not want to spawn at a runway or in air. if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) - end + end end - + -- Workaround if group was spawned on runway. if onrunway then - + -- Error message. local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) @@ -3815,42 +3815,42 @@ function RAT:_OnBirth(EventData) if self.Debug then SpawnGroup:FlareRed() end - + -- Despawn the group. self:_Despawn(SpawnGroup) - + -- Try to respawn the group if there is at least another airport or random airport selection is used. if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn new state %s.", SpawnGroup:GetName(), currentstate, status) self:T(RAT.id..text) - + -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() @@ -4041,13 +4041,13 @@ function RAT:_OnEngineShutdown(EventData) -- Despawn group. text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." self:T(RAT.id..text) - self:_Despawn(SpawnGroup) + self:_Despawn(SpawnGroup) end end end - + else self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") end @@ -4059,19 +4059,19 @@ end function RAT:_OnHit(EventData) self:F3(EventData) self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) - + local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then -- Debug info. self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) - + local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) end @@ -4084,37 +4084,37 @@ end function RAT:_OnDeadOrCrash(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event DeadOrCrash!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + -- Decrease group alive counter. self.alive=self.alive-1 - + -- Debug info. - local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) self:T(RAT.id..text) - + -- Split crash and dead events. if EventData.id == world.event.S_EVENT_CRASH then - - -- Call crash event. This handles when a group crashed or + + -- Call crash event. This handles when a group crashed or self:_OnCrash(EventData) - + elseif EventData.id == world.event.S_EVENT_DEAD then - + -- Call dead event. self:_OnDead(EventData) - + end end end @@ -4127,26 +4127,26 @@ end function RAT:_OnDead(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event Dead!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - - local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) + + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventDead self:_SetStatus(SpawnGroup, status) - + end end @@ -4163,29 +4163,29 @@ function RAT:_OnCrash(EventData) self:T3(RAT.id.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then - + -- Update number of alive units in the group. local _i=self:GetSpawnIndexFromGroup(SpawnGroup) self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 local _n=self.ratcraft[_i].nunits local _n0=SpawnGroup:GetInitialSize() - - -- Debug info. + + -- Debug info. local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventCrash self:_SetStatus(SpawnGroup, status) - + -- Respawn group if all units are dead. if _n==0 and self.respawn_after_crash and not self.norespawn then local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) @@ -4197,7 +4197,7 @@ function RAT:_OnCrash(EventData) end end - + else if self.Debug then self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") @@ -4213,16 +4213,16 @@ end function RAT:_Despawn(group, delay) if group ~= nil then - + -- Get spawnindex of group. local index=self:GetSpawnIndexFromGroup(group) - + if index ~= nil then - + self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" - - --TODO: Maybe here could be some more arrays deleted? + + --TODO: Maybe here could be some more arrays deleted? --TODO: Somehow this causes issues. --[[ --self.ratcraft[index]["group"]=group @@ -4248,8 +4248,8 @@ function RAT:_Despawn(group, delay) ]] -- Remove ratcraft table entry. --table.remove(self.ratcraft, index) - - + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local despawndelay=0 if delay then @@ -4259,20 +4259,20 @@ function RAT:_Despawn(group, delay) -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. despawndelay=self.respawn_delay end - - -- This will destroy the DCS group and create a single DEAD event. + + -- This will destroy the DCS group and create a single DEAD event. --if despawndelay>0.5 then self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) --else --self:_Destroy(group) - --end + --end -- Remove submenu for this group. if self.f10menu and self.SubMenuName ~= nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end - + end end end @@ -4288,23 +4288,23 @@ function RAT:_Destroy(group) local DCSGroup = group:GetDCSObject() -- DCS#Group if DCSGroup and DCSGroup:isExist() then - + -- Cread one single Dead event and delete units from database. local triggerdead=true for _,DCSUnit in pairs(DCSGroup:getUnits()) do - + -- Dead event. if DCSUnit then if triggerdead then self:_CreateEventDead(timer.getTime(), DCSUnit) triggerdead=false end - + -- Delete from data base. _DATABASE:DeleteUnit(DCSUnit:getName()) end end - + -- Destroy DCS group. DCSGroup:destroy() DCSGroup = nil @@ -4345,10 +4345,10 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport -- Altitude of input parameter or y-component of 3D-coordinate. local _Altitude=Altitude or Coord.y - + -- Land height at given coordinate. local Hland=Coord:GetLandHeight() - + -- convert type and action in DCS format local _Type=nil local _Action=nil @@ -4361,7 +4361,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport _Altitude = 10 _alttype="RADIO" elseif Type==RAT.wp.hot then - -- take-off with engine on + -- take-off with engine on _Type="TakeOffParkingHot" _Action="From Parking Area Hot" _Altitude = 10 @@ -4434,7 +4434,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport end text=text.."******************************************************\n" self:T2(RAT.id..text) - + -- define waypoint local RoutePoint = {} -- coordinates and altitude @@ -4443,7 +4443,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.alt = _Altitude -- altitude type: BARO=ASL or RADIO=AGL RoutePoint.alt_type = _alttype - -- type + -- type RoutePoint.type = _Type RoutePoint.action = _Action -- speed in m/s @@ -4454,7 +4454,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.ETA_locked = false -- waypoint description RoutePoint.name=description - + if (Airport~=nil) and (Type~=RAT.wp.air) then local AirbaseID = Airport:GetID() local AirbaseCategory = Airport:GetAirbaseCategory() @@ -4468,7 +4468,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.airdromeId = AirbaseID else self:T(RAT.id.."Unknown Airport category in _Waypoint()!") - end + end end -- properties RoutePoint.properties = { @@ -4482,16 +4482,16 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport local TaskCombo = {} local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) - + RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} - + TaskCombo[#TaskCombo+1]=TaskWaypoint if Type==RAT.wp.holding then TaskCombo[#TaskCombo+1]=TaskHolding end - + RoutePoint.task.params.tasks = TaskCombo -- Return waypoint. @@ -4533,10 +4533,10 @@ function RAT:_Routeinfo(waypoints, comment) end text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") - + -- Debug info. self:T2(RAT.id..text) - + -- return total route length in meters return total end @@ -4562,7 +4562,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) dx=200 dy=0 end - + local P2={} P2.x=P1.x+dx P2.y=P1.y+dy @@ -4577,12 +4577,12 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) altitude = Altitude } } - + local DCSTask={} DCSTask.id="ControlledTask" DCSTask.params={} DCSTask.params.task=Task - + if self.ATCswitch then -- Set stop condition for holding. Either flag=1 or after max. X min holding. local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) @@ -4591,7 +4591,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) else DCSTask.params.stopCondition={duration=Duration} end - + return DCSTask end @@ -4604,32 +4604,32 @@ function RAT._WaypointFunction(group, rat, wp) -- Current time and Spawnindex. local Tnow=timer.getTime() local sdx=rat:GetSpawnIndexFromGroup(group) - + -- Departure and destination names. local departure=rat.ratcraft[sdx].departure:GetName() local destination=rat.ratcraft[sdx].destination:GetName() local landing=rat.ratcraft[sdx].landing local WPholding=rat.ratcraft[sdx].wpholding local WPfinal=rat.ratcraft[sdx].wpfinal - - + + -- For messages local text - + -- Info on passing waypoint. text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp]) BASE.T(rat, RAT.id..text) - + -- New status. local status=rat.waypointstatus[wp] rat:_SetStatus(group, status) - + if wp==WPholding then - + -- Aircraft arrived at holding point text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - + -- Register aircraft at ATC. if rat.ATCswitch then if rat.f10menu then @@ -4638,12 +4638,12 @@ function RAT._WaypointFunction(group, rat, wp) rat._ATCRegisterFlight(rat, group:GetName(), Tnow) end end - + if wp==WPfinal then text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) BASE.T(rat, RAT.id..text) - + if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) MESSAGE:New(text, 10):ToAllIf(rat.Debug) @@ -4659,14 +4659,14 @@ end -- @param #string FunctionString Name of the function to be called. function RAT:_TaskFunction(FunctionString, ... ) self:F2({FunctionString, arg}) - + local DCSTask local ArgumentKey - + -- Templatename and anticipated name the group will get local templatename=self.templategroup:GetName() local groupname=self:_AnticipatedGroupName() - + local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") " DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " @@ -4679,7 +4679,7 @@ function RAT:_TaskFunction(FunctionString, ... ) else DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - + DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) return DCSTask @@ -4687,7 +4687,7 @@ end --- Anticipated group name from alias and spawn index. -- @param #RAT self --- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. +-- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. -- @return #string Name the group will get after it is spawned. function RAT:_AnticipatedGroupName(index) local index=index or self.SpawnIndex+1 @@ -4700,21 +4700,21 @@ end -- @param #RAT self function RAT:_ActivateUncontrolled() self:F() - - -- Spawn indices of uncontrolled inactive aircraft. + + -- Spawn indices of uncontrolled inactive aircraft. local idx={} local rat={} - + -- Number of active aircraft. local nactive=0 - + -- Loop over RAT groups and count the active ones. for spawnindex,ratcraft in pairs(self.ratcraft) do - + local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) self:T2(RAT.id..text) @@ -4723,22 +4723,22 @@ function RAT:_ActivateUncontrolled() else table.insert(idx, spawnindex) end - + end end - + -- Debug message. local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) self:T(RAT.id..text) - + if #idx>0 and nactive Less effort. @@ -5231,7 +5231,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take -- Terminal type specified explicitly. self:T(RAT.id..string.format("Helo group %s is at %s using terminal type %d.", self.alias, departure:GetName(), termtype)) spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) - nfree=#spots + nfree=#spots end else -- Fixed wing aircraft is spawned. @@ -5248,15 +5248,15 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take nfree=#spots if nfree=1 then - + -- All units get the same spot. DCS takes care of the rest. for i=1,nunits do table.insert(parkingspots, spots[1].Coordinate) @@ -5292,46 +5292,46 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end -- This is actually used... PointVec3=spots[1].Coordinate - + else -- If there is absolutely not spot ==> air start! _notenough=true end - + elseif spawnonairport then - + if nfree>=nunits then - + for i=1,nunits do table.insert(parkingspots, spots[i].Coordinate) table.insert(parkingindex, spots[i].TerminalID) end - + else -- Not enough spots for the whole group ==> air start! - _notenough=true - end + _notenough=true + end end - + -- Not enough spots ==> Prepare airstart. if _notenough then - - if self.respawn_inair and not self.SpawnUnControlled then + + if self.respawn_inair and not self.SpawnUnControlled then self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) - + -- Not enough parking spots at the airport ==> Spawn in air. spawnonground=false spawnonship=false spawnonfarp=false spawnonrunway=false - + -- Set waypoint type/action to turning point. waypoints[1].type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point - + -- Adjust altitude to be 500-1000 m above the airbase. PointVec3.x=PointVec3.x+math.random(-1500,1500) - PointVec3.z=PointVec3.z+math.random(-1500,1500) + PointVec3.z=PointVec3.z+math.random(-1500,1500) if self.category==RAT.cat.heli then PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) else @@ -5343,122 +5343,122 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take return nil end end - + else - + -- Air start requested initially! - + --PointVec3.y is already set from first waypoint here! - + end --- new - + -- Translate the position of the Group Template to the Vec3. for UnitID = 1, nunits do - + -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] - - -- Tranlate position and preserve the relative position/formation of all aircraft. + + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y local TX = PointVec3.x + (SX-BX) local TY = PointVec3.z + (SY-BY) - + if spawnonground then - - -- Sh�ps and FARPS seem to have a build in queue. + + -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway or automatic then self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) -- Spawn on ship. We take only the position of the ship. SpawnTemplate.units[UnitID].x = PointVec3.x --TX SpawnTemplate.units[UnitID].y = PointVec3.z --TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y else self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) - + -- Get coordinates of parking spot. SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z - SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y end - - else + + else self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) - + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. SpawnTemplate.units[UnitID].x = TX SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y end - - -- Place marker at spawn position. + + -- Place marker at spawn position. if self.Debug then local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y) unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID)) end - + -- Parking spot id. UnitTemplate.parking = nil UnitTemplate.parking_id = nil if parkingindex[UnitID] and not automatic then UnitTemplate.parking = parkingindex[UnitID] end - + -- Debug info. self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) - + -- Set initial heading. SpawnTemplate.units[UnitID].heading = heading SpawnTemplate.units[UnitID].psi = -heading - + -- Set livery (will be the same for all units of the group). if livery then SpawnTemplate.units[UnitID].livery_id = livery end - + -- Set type of aircraft. if self.actype then SpawnTemplate.units[UnitID]["type"] = self.actype end - + -- Set AI skill. SpawnTemplate.units[UnitID]["skill"] = self.skill - + -- Onboard number. if self.onboardnum then SpawnTemplate.units[UnitID]["onboard_num"] = string.format("%s%d%02d", self.onboardnum, (self.SpawnIndex-1)%10, (self.onboardnum0-1)+UnitID) end - + -- Modify coaltion and country of template. SpawnTemplate.CoalitionID=self.coalition if self.country then SpawnTemplate.CountryID=self.country end - + end - + -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) for i,wp in ipairs(waypoints) do SpawnTemplate.route.points[i]=wp end - + -- Also modify x,y of the template. Not sure why. SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - + -- Enable/disable radio. Same as checking the COMM box in the ME if self.radio then SpawnTemplate.communication=self.radio end - + -- Set radio frequency and modulation. if self.frequency then SpawnTemplate.frequency=self.frequency @@ -5466,12 +5466,12 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.modulation then SpawnTemplate.modulation=self.modulation end - + -- Debug output. self:T(SpawnTemplate) end end - + return true end @@ -5544,15 +5544,15 @@ function RAT:_ATCStatus() -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.flight) do -- Holding time at destination. local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + if hold >= 0 then - + -- Some string whether the runway is busy or not. local busy="Runway state is unknown" if RAT.ATC.airport[dest].Nonfinal>0 then @@ -5560,29 +5560,29 @@ function RAT:_ATCStatus() else busy="Runway is currently clear" end - + -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.onfinal then - + -- Aircarft is on final approach for landing. local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - + local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.unregistered then - + -- Aircraft has not arrived at holding point. --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) - + else BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end - + end --- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. @@ -5591,17 +5591,17 @@ function RAT:_ATCCheck() -- Init queue of flights at all airports. RAT:_ATCQueue() - + -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.airport) do - + for qID,flight in ipairs(RAT.ATC.airport[name].queue) do - + -- Number of aircraft in queue. local nqueue=#RAT.ATC.airport[name].queue - + -- Conditions to clear an aircraft for landing local landing1 if RAT.ATC.airport[name].Tlastclearance then @@ -5615,31 +5615,31 @@ function RAT:_ATCCheck() if not landing1 and not landing2 then - + -- Update holding time. RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive - + -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + else - + local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) - + end - + end - + end - + -- Update queue of flights at all airports. RAT:_ATCQueue() - + end --- Giving landing clearance for aircraft by setting user flag. @@ -5656,13 +5656,13 @@ function RAT:_ATCClearForLanding(airport, flight) -- Number of planes on final approach. RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 -- Last time an aircraft got landing clearance. - RAT.ATC.airport[airport].Tlastclearance=timer.getTime() + RAT.ATC.airport[airport].Tlastclearance=timer.getTime() -- Current time. RAT.ATC.flight[flight].Tonfinal=timer.getTime() -- Set user flag to 1 ==> stop condition for holding. trigger.action.setUserFlag(flight, 1) local flagvalue=trigger.misc.getUserFlag(flight) - + -- Debug message. local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) @@ -5676,33 +5676,33 @@ end function RAT:_ATCFlightLanded(name) if RAT.ATC.flight[name] then - + -- Destination airport. local dest=RAT.ATC.flight[name].destination - + -- Times for holding and final approach. local Tnow=timer.getTime() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive - + -- Airport is not busy any more. RAT.ATC.airport[dest].busy=false - + -- No aircraft on final any more. RAT.ATC.airport[dest].onfinal[name]=nil - + -- Decrease number of aircraft on final. RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1 - + -- Remove this flight from list of flights. RAT:_ATCDelFlight(RAT.ATC.flight, name) - + -- Increase landing counter to monitor traffic. RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 - + -- Number of planes landing per hour. local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 - + -- Debug info local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) @@ -5713,7 +5713,7 @@ function RAT:_ATCFlightLanded(name) BASE:T(RAT.id..text3) MESSAGE:New(text4, 10):ToAllIf(RAT.ATC.messages) end - + end --- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. @@ -5721,7 +5721,7 @@ end function RAT:_ATCQueue() for airport,_ in pairs(RAT.ATC.airport) do - + -- Local airport queue. local _queue={} @@ -5729,26 +5729,26 @@ function RAT:_ATCQueue() for name,_ in pairs(RAT.ATC.flight) do --fvh local Tnow=timer.getTime() - + -- Update holding time (unless holing is set to onfinal=-100) if RAT.ATC.flight[name].holding>=0 then RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive end local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + -- Flight is holding at this airport. if hold>=0 and airport==dest then _queue[#_queue+1]={name,hold} end end - + -- Sort queue w.r.t holding time in ascending order. local function compare(a,b) return a[2] > b[2] end table.sort(_queue, compare) - + -- Transfer queue to airport queue. RAT.ATC.airport[airport].queue={} for k,v in ipairs(_queue) do @@ -5776,36 +5776,36 @@ end -- @extends Core.Base#BASE ---# RATMANAGER class, extends @{Core.Base#BASE} --- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. +-- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. -- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function. --- +-- -- Instead, these objects can be added to the manager via the @{#RATMANAGER.Add}(ratobject, min) function, where the first parameter "ratobject" is the @{#RAT} object, while the second parameter "min" defines the -- minimum number of RAT aircraft of that object, which are alive at all time. --- +-- -- The @{#RATMANAGER} must be started by the @{#RATMANAGER.Start}(startime) function, where the optional argument "startime" specifies the delay time in seconds after which the manager is started and the spawning beginns. -- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops. -- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero. --- +-- -- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups. --- +-- -- ## Example -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. --- +-- -- local a10c=RAT:New("RAT_A10C", "A-10C managed") -- a10c:SetDeparture({"Batumi"}) --- +-- -- local f15c=RAT:New("RAT_F15C", "F15C managed") -- f15c:SetDeparture({"Sochi-Adler"}) -- f15c:DestinationZone() -- f15c:SetDestination({"Zone C"}) --- +-- -- local av8b=RAT:New("RAT_AV8B", "AV8B managed") -- av8b:SetDeparture({"Zone C"}) -- av8b:SetTakeoff("air") -- av8b:DestinationZone() -- av8b:SetDestination({"Zone A"}) --- +-- -- local manager=RATMANAGER:New(25) -- manager:Add(a10c, 5) -- manager:Add(f15c, 5) @@ -5841,13 +5841,13 @@ function RATMANAGER:New(ntot) -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #RATMANAGER - + -- Total number of RAT groups. self.ntot=ntot or 1 - + -- Debug info self:E(RATMANAGER.id..string.format("Creating manager for %d groups.", ntot)) - + return self end @@ -5862,21 +5862,21 @@ function RATMANAGER:Add(ratobject,min) --Automatic respawning is disabled. ratobject.norespawn=true ratobject.f10menu=false - + -- Increase RAT object counter. self.nrat=self.nrat+1 - + self.rat[self.nrat]=ratobject self.alive[self.nrat]=0 self.name[self.nrat]=ratobject.alias self.min[self.nrat]=min or 1 - + -- Debug info. self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d", self.name[self.nrat],self.min[self.nrat])) - + -- Call spawn to initialize RAT parameters. ratobject:Spawn(0) - + return self end @@ -5900,7 +5900,7 @@ function RATMANAGER:Start(delay) -- Start scheduler. SCHEDULER:New(nil, self._Start, {self}, delay) - + return self end @@ -5918,7 +5918,7 @@ function RATMANAGER:_Start() -- Get randum number of new RAT groups. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn groups. local time=0.0 for i=1,self.nrat do @@ -5927,26 +5927,26 @@ function RATMANAGER:_Start() SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) end end - + -- Start activation scheduler for uncontrolled aircraft. - for i=1,self.nrat do + for i=1,self.nrat do if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then -- Start activating stuff but not before the latest spawn has happend. - local Tactivate=math.max(time+1, self.rat[i].activate_delay) + local Tactivate=math.max(time+1, self.rat[i].activate_delay) SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) end end - + -- Start the manager. But not earlier than the latest spawn has happened! local TstartManager=math.max(time+1, self.Tcheck) - + -- Start manager scheduler. self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER - + -- Info local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) self:E(text) - + return self end @@ -5995,14 +5995,14 @@ function RATMANAGER:_Manage() -- Count total number of groups. local ntot=self:_Count() - + -- Debug info. local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) self:T(RATMANAGER.id..text) - + -- Get number of necessary spawns. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn new groups if necessary. local time=0.0 for i=1,self.nrat do @@ -6019,13 +6019,13 @@ function RATMANAGER:_Count() -- Init total counter. local ntotal=0 - + -- Loop over all RAT objects. for i=1,self.nrat do local n=0 - + local ratobject=self.rat[i] --#RAT - + -- Loop over the RAT groups of this object. for spawnindex,ratcraft in pairs(ratobject.ratcraft) do local group=ratcraft.group --Wrapper.Group#GROUP @@ -6033,18 +6033,18 @@ function RATMANAGER:_Count() n=n+1 end end - + -- Alive groups of this RAT object. self.alive[i]=n - + -- Grand total. ntotal=ntotal+n - + -- Debug output. local text=string.format("Number of alive groups of %s = %d", self.name[i], n) self:T(RATMANAGER.id..text) end - + -- Return grand total. return ntotal end @@ -6056,7 +6056,7 @@ end -- @param #table min Minimum number of groups for each RAT object. -- @param #table alive Number of alive groups of each RAT object. function RATMANAGER:_RollDice(nrat,ntot,min,alive) - + -- Calculate sum. local function sum(A,index) local summe=0 @@ -6064,8 +6064,8 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) summe=summe+A[i] end return summe - end - + end + -- Table of number of groups. local N={} local M={} @@ -6075,57 +6075,57 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) M[#M+1]=math.max(alive[i], min[i]) P[#P+1]=math.max(min[i]-alive[i],0) end - + -- Min/max group arrays. local mini={} local maxi={} - + -- Arrays. local rattab={} for i=1,nrat do table.insert(rattab,i) end local done={} - + -- Number of new groups to be added. local nnew=ntot for i=1,nrat do nnew=nnew-alive[i] end - + for i=1,nrat-1 do - + -- Random entry from . local r=math.random(#rattab) -- Get value local j=rattab[r] - + table.remove(rattab, r) table.insert(done,j) - + -- Sum up the number of already distributed groups. local sN=sum(N, done) - -- Sum up the minimum number of yet to be distributed groups. + -- Sum up the minimum number of yet to be distributed groups. local sP=sum(P, rattab) - + -- Max number that can be distributed for this object. maxi[j]=nnew-sN-sP - + -- Min number that should be distributed for this object mini[j]=P[j] - + -- Random number of new groups for this RAT object. if maxi[j] >= mini[j] then N[j]=math.random(mini[j], maxi[j]) else N[j]=0 end - + -- Debug info self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) - + end - + -- Last RAT object, number of groups is determined from number of already distributed groups and nnew. local j=rattab[1] N[j]=nnew-sum(N, done) @@ -6133,7 +6133,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) maxi[j]=nnew-sum(N, done) table.remove(rattab, 1) table.insert(done,j) - + -- Debug info local text=RATMANAGER.id.."\n" for i=1,nrat do @@ -6141,8 +6141,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) end text=text..string.format("Total # of groups to add = %d", sum(N, done)) self:T(text) - + -- Return number of groups to be spawned. return N end - diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 7ba9dcfc7..9e5db273e 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -32,7 +32,9 @@ -- -- === -- --- ## Missions: Example missions will be added later. +-- ## Missions: +-- +-- * [MAR - On the Range - MOOSE - SC](https://www.digitalcombatsimulator.com/en/files/3317765/) by shagrat -- -- === -- @@ -2558,7 +2560,7 @@ function RANGE:_DisplayBombTargets(_unitname) -- Get elevation local elevation=coord:GetLandHeight() local eltxt=string.format("%d m", elevation) - if _settings:IsImperial() then + if not _settings:IsMetric() then elevation=UTILS.MetersToFeet(elevation) eltxt=string.format("%d ft", elevation) end @@ -2829,6 +2831,7 @@ function RANGE:_CheckInZone(_unitName) local accur=0 if shots>0 then accur=_result.hits/shots*100 + if accur > 100 then accur = 100 end end -- Message text. diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index c99e1f241..550305ddc 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -1,54 +1,56 @@ --- **Functional** -- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- === --- +-- -- ## Features: --- +-- -- * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible. -- * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion) --- +-- -- === --- +-- -- ### Authors: **FlightControl**, **applevangelist** --- --- Last Update: July 2021 --- +-- +-- Last Update: Aug 2021 +-- -- === --- +-- -- @module Functional.Sead -- @image SEAD.JPG ---- @type SEAD +--- +-- @type SEAD -- @extends Core.Base#BASE --- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. --- +-- -- # Constructor: --- +-- -- Use the @{#SEAD.New}() constructor to create a new SEAD object. --- +-- -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) --- +-- -- @field #SEAD SEAD = { - ClassName = "SEAD", + ClassName = "SEAD", TargetSkill = { Average = { Evade = 30, DelayOn = { 40, 60 } } , Good = { Evade = 20, DelayOn = { 30, 50 } } , High = { Evade = 15, DelayOn = { 20, 40 } } , - Excellent = { Evade = 10, DelayOn = { 10, 30 } } - }, + Excellent = { Evade = 10, DelayOn = { 10, 30 } } + }, SEADGroupPrefixes = {}, SuppressedGroups = {}, - EngagementRange = 75 -- default 75% engagement range Feature Request #1355 + EngagementRange = 75, -- default 75% engagement range Feature Request #1355 + Padding = 10, } --- Missile enumerators @@ -59,7 +61,7 @@ SEAD = { ["AGM_122"] = "AGM_122", ["AGM_84"] = "AGM_84", ["AGM_45"] = "AGM_45", - ["ALARN"] = "ALARM", + ["ALARM"] = "ALARM", ["LD-10"] = "LD-10", ["X_58"] = "X_58", ["X_28"] = "X_28", @@ -67,22 +69,40 @@ SEAD = { ["X_31"] = "X_31", ["Kh25"] = "Kh25", } - + + --- Missile enumerators - from DCS ME and Wikipedia + -- @field HarmData + SEAD.HarmData = { + -- km and mach + ["AGM_88"] = { 150, 3}, + ["AGM_45"] = { 12, 2}, + ["AGM_122"] = { 16.5, 2.3}, + ["AGM_84"] = { 280, 0.85}, + ["ALARM"] = { 45, 2}, + ["LD-10"] = { 60, 4}, + ["X_58"] = { 70, 4}, + ["X_28"] = { 80, 2.5}, + ["X_25"] = { 25, 0.76}, + ["X_31"] = {150, 3}, + ["Kh25"] = {25, 0.8}, + } + --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. -- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... -- Chances are big that the missile will miss. -- @param #SEAD self --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken. --- @return SEAD +-- @param #table SEADGroupPrefixes Table of #string entries or single #string, which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken. +-- @param #number Padding (Optional) Extra number of seconds to add to radar switch-back-on time +-- @return #SEAD self -- @usage -- -- CCCP SEAD Defenses -- -- Defends the Russian SA installations from SEAD attacks. -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) +function SEAD:New( SEADGroupPrefixes, Padding ) local self = BASE:Inherit( self, BASE:New() ) self:F( SEADGroupPrefixes ) - + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -90,9 +110,14 @@ function SEAD:New( SEADGroupPrefixes ) else self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes end - + + local padding = Padding or 10 + if padding < 10 then padding = 10 end + self.Padding = padding + self:HandleEvent( EVENTS.Shot, self.HandleEventShot ) - self:I("*** SEAD - Started Version 0.2.8") + + self:I("*** SEAD - Started Version 0.3.1") return self end @@ -102,8 +127,8 @@ end -- @return #SEAD self function SEAD:UpdateSet( SEADGroupPrefixes ) - self:F( SEADGroupPrefixes ) - + self:T( SEADGroupPrefixes ) + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -118,9 +143,9 @@ end --- Sets the engagement range of the SAMs. Defaults to 75% to make it more deadly. Feature Request #1355 -- @param #SEAD self -- @param #number range Set the engagement range in percent, e.g. 50 --- @return self +-- @return #SEAD self function SEAD:SetEngagementRange(range) - self:F( { range } ) + self:T( { range } ) range = range or 75 if range < 0 or range > 100 then range = 75 @@ -130,100 +155,175 @@ function SEAD:SetEngagementRange(range) return self end +--- Set the padding in seconds, which extends the radar off time calculated by SEAD +-- @param #SEAD self +-- @param #number Padding Extra number of seconds to add for the switch-on +-- @return #SEAD self +function SEAD:SetPadding(Padding) + self:T( { Padding } ) + local padding = Padding or 10 + if padding < 10 then padding = 10 end + self.Padding = padding + return self +end + --- Check if a known HARM was fired -- @param #SEAD self -- @param #string WeaponName -- @return #boolean Returns true for a match + -- @return #string name Name of hit in table function SEAD:_CheckHarms(WeaponName) - self:F( { WeaponName } ) + self:T( { WeaponName } ) local hit = false + local name = "" for _,_name in pairs (SEAD.Harms) do - if string.find(WeaponName,_name,1) then hit = true end + if string.find(WeaponName,_name,1) then + hit = true + name = _name + break + end end - return hit + return hit, name + end + + --- (Internal) Return distance in meters between two coordinates or -1 on error. + -- @param #SEAD self + -- @param Core.Point#COORDINATE _point1 Coordinate one + -- @param Core.Point#COORDINATE _point2 Coordinate two + -- @return #number Distance in meters + function SEAD:_GetDistance(_point1, _point2) + self:T("_GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:T({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end end --- Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. -- @see SEAD -- @param #SEAD -- @param Core.Event#EVENTDATA EventData +-- @return #SEAD self function SEAD:HandleEventShot( EventData ) - self:T( { EventData } ) - + self:T( { EventData.id } ) + local SEADPlane = EventData.IniUnit -- Wrapper.Unit#UNIT + local SEADPlanePos = SEADPlane:GetCoordinate() -- Core.Point#COORDINATE local SEADUnit = EventData.IniDCSUnit local SEADUnitName = EventData.IniDCSUnitName local SEADWeapon = EventData.Weapon -- Identify the weapon fired local SEADWeaponName = EventData.WeaponName -- return weapon type self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) - self:T({ SEADWeapon }) - + --self:T({ SEADWeapon }) + if self:_CheckHarms(SEADWeaponName) then + self:T( '*** SEAD - Weapon Match' ) local _targetskill = "Random" - local _targetMimgroupName = "none" - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object + local _targetgroupname = "none" + local _target = EventData.Weapon:getTarget() -- Identify target + local _targetUnit = UNIT:Find(_target) -- Wrapper.Unit#UNIT + local _targetgroup = nil -- Wrapper.Group#GROUP if _targetUnit and _targetUnit:IsAlive() then - local _targetMimgroup = _targetUnit:GetGroup() - local _targetMimgroupName = _targetMimgroup:GetName() -- group name - --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) + _targetgroup = _targetUnit:GetGroup() + _targetgroupname = _targetgroup:GetName() -- group name + local _targetUnitName = _targetUnit:GetName() + _targetUnit:GetSkill() + _targetskill = _targetUnit:GetSkill() end -- see if we are shot at local SEADGroupFound = false for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + self:T( _targetgroupname, SEADGroupPrefix ) + if string.find( _targetgroupname, SEADGroupPrefix ) then SEADGroupFound = true - self:T( '*** SEAD - Group Found' ) + self:T( '*** SEAD - Group Match Found' ) break end - end + end if SEADGroupFound == true then -- yes we are being attacked if _targetskill == "Random" then -- when skill is random, choose a skill local Skills = { "Average", "Good", "High", "Excellent" } _targetskill = Skills[ math.random(1,4) ] end - self:T( _targetskill ) + --self:T( _targetskill ) if self.TargetSkill[_targetskill] then + local _evade = math.random (1,100) -- random number for chance of evading action if (_evade > self.TargetSkill[_targetskill].Evade) then - - self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) - - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - - --tracker ID table to switch groups off and on again - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - - local function SuppressionEnd(id) --switch group back on - local range = self.EngagementRange -- Feature Request #1355 - self:T(string.format("*** SEAD - Engagement Range is %d", range)) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - --id.groupName:enableEmission(true) - id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 - self.SuppressedGroups[id.groupName] = nil --delete group id from table when done + self:T("*** SEAD - Evading") + -- calculate distance of attacker + local _targetpos = _targetgroup:GetCoordinate() + local _distance = self:_GetDistance(SEADPlanePos, _targetpos) + -- weapon speed + local hit, data = self:_CheckHarms(SEADWeaponName) + local wpnspeed = 666 -- ;) + local reach = 10 + if hit then + local wpndata = SEAD.HarmData[data] + reach = wpndata[1] * 1,1 + local mach = wpndata[2] + wpnspeed = math.floor(mach * 340.29) end - -- randomize switch-on time - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - local SuppressionEndTime = timer.getTime() + delay - --create entry - if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet - self.SuppressedGroups[id.groupName] = { - SuppressionEndTime = delay - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - --_targetMimgroup:enableEmission(false) - timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function + -- time to impact + local _tti = math.floor(_distance / wpnspeed) -- estimated impact time + if _distance > 0 then + _distance = math.floor(_distance / 1000) -- km + else + _distance = 0 + end + + self:T( string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec", _targetskill, _distance,reach,_tti )) + + if reach >= _distance then + self:T("*** SEAD - Shot in Reach") + + local function SuppressionStart(args) + self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + grp:OptionAlarmStateGreen() + grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond") + end + + local function SuppressionStop(args) + self:T(string.format("*** SEAD - %s Radar On",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + grp:OptionAlarmStateRed() + grp:OptionEngageRange(self.EngagementRange) + self.SuppressedGroups[args[2]] = false + end + + -- randomize switch-on time + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if delay > _tti then delay = delay / 2 end -- speed up + if _tti > (3*delay) then delay = (_tti / 2) * 0.9 end -- shot from afar + + local SuppressionStartTime = timer.getTime() + delay + local SuppressionEndTime = timer.getTime() + _tti + self.Padding + + if not self.SuppressedGroups[_targetgroupname] then + self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay)) + timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname},SuppressionStartTime) + timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime) + self.SuppressedGroups[_targetgroupname] = true + end + end end end end end + return self end diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index 958877ec8..ce2d93d20 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -113,7 +113,7 @@ do ["AGM_122"] = "AGM_122", ["AGM_84"] = "AGM_84", ["AGM_45"] = "AGM_45", - ["ALARN"] = "ALARM", + ["ALARM"] = "ALARM", ["LD-10"] = "LD-10", ["X_58"] = "X_58", ["X_28"] = "X_28", diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d49d52b6f..5d5fe5161 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -32,6 +32,8 @@ -- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] -- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] -- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] +-- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6)) (LHA-6) [**WIP**] +-- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) [**WIP**] -- -- **Supported Aircraft:** -- @@ -48,8 +50,8 @@ -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- --- The AV-8B Harrier and the USS Tarawa are WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and --- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. +-- The AV-8B Harrier, the USS Tarawa, USS America and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and +-- no other fixed wing aircraft (human or AI controlled) are supposed to land on these ships. Currently only Case I is supported. Case II/III take slightly different steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. -- -- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. Same goes for the A version. @@ -102,12 +104,13 @@ -- ### Wags DCS Hornet Videos: -- -- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) --- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) +-- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) -- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) -- -- ### AV-8B Harrier at USS Tarawa -- -- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) +-- * [Harrier Practice pattern USS America](https://youtu.be/99NigITYmcI) -- -- === -- @@ -295,6 +298,8 @@ -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) -- -- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line. +-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. -- -- -- ## CASE III @@ -919,9 +924,9 @@ -- -- ## Sound Packs -- --- The AIRBOSS currently has two different "sound packs" for both LSO and Marshal radios. These contain voice overs by different actors. +-- The AIRBOSS currently has two different "sound packs" for LSO and three different "sound Packs" for Marshal radios. These contain voice overs by different actors. -- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. --- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). +-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}(), @{#AIRBOSS.SetVoiceOversMarshalByGabriella}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). -- Also combinations can be used, e.g. -- -- airbossStennis:SetVoiceOversLSOByFF() @@ -1256,7 +1261,7 @@ AIRBOSS = { --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier --- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa. +-- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa, USS America and Juan Carlos I. -- @field #string A4EC A-4E Community mod. -- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. -- @field #string F14A F-14A by Heatblur. @@ -1292,6 +1297,8 @@ AIRBOSS.AircraftCarrier={ -- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] -- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] -- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string AMERICA USS America (LHA-6) +-- @field #string JCARLOS Juan Carlos I (L61) -- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) AIRBOSS.CarrierType={ ROOSEVELT="CVN_71", @@ -1301,6 +1308,8 @@ AIRBOSS.CarrierType={ STENNIS="Stennis", VINSON="VINSON", TARAWA="LHA_Tarawa", + AMERICA="USS America LHA-6", + JCARLOS="L61", KUZNETSOV="KUZNECOW", } @@ -1420,8 +1429,8 @@ AIRBOSS.PatternStep={ -- @field #string IM "IM": In the middle. -- @field #string IC "IC": In close. -- @field #string AR "AR": At the ramp. --- @field #string AL "AL": Abeam landing position (Tarawa). --- @field #string LC "LC": Level crossing (Tarawa). +-- @field #string AL "AL": Abeam landing position (V/STOL). +-- @field #string LC "LC": Level crossing (V/STOL). -- @field #string IW "IW": In the wires. AIRBOSS.GroovePos={ X0="X0", @@ -1486,6 +1495,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. -- @field #AIRBOSS.RadioCall EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. -- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT5 "Expect spot 5" call. -- @field #AIRBOSS.RadioCall FAST "You're fast" call. -- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. -- @field #AIRBOSS.RadioCall HIGH "You're high" call. @@ -1970,6 +1980,12 @@ function AIRBOSS:New(carriername, alias) elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Tarawa parameters. self:_InitTarawa() + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + -- Use America parameters. + self:_InitAmerica() + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + -- Use Juan Carlos parameters. + self:_InitJcarlos() elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then -- Kusnetsov parameters - maybe... self:_InitStennis() @@ -2061,7 +2077,7 @@ function AIRBOSS:New(carriername, alias) -- Carrier specific. - if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA then + if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS then -- Flare wires. local w1=stern:Translate(self.carrierparam.wire1, FB) @@ -2834,7 +2850,7 @@ function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, self.lue.LeftMed=LeftMed or -2.0 self.lue.LEFT=LEFT or -3.0 self.lue.Right=Right or 1.0 - self.lue.RightMed=RightMed or 2.0 + self.lue.RightMed=RightMed or 2.0 self.lue.RIGHT=RIGHT or 3.0 return self end @@ -4404,6 +4420,85 @@ function AIRBOSS:_InitTarawa() end +--- Init parameters for LHA-6 America carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitAmerica() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 20 --67 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=257 + self.carrierparam.totwidthport=11 + self.carrierparam.totwidthstarboard=25 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 240 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end + +--- Init parameters for L61 Juan Carlos carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitJcarlos() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 20 --67 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=231 + self.carrierparam.totwidthport=10 + self.carrierparam.totwidthstarboard=22 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 202 + self.carrierparam.rwywidth = 14 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end --- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. @@ -4558,6 +4653,7 @@ function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) self.LSOCall.DEPARTANDREENTER.duration=1.10 self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 self.LSOCall.EXPECTSPOT75.duration=1.85 + self.LSOCall.EXPECTSPOT5.duration=1.3 self.LSOCall.FAST.duration=0.75 self.LSOCall.FOULDECK.duration=0.75 self.LSOCall.HIGH.duration=0.65 @@ -4616,6 +4712,7 @@ function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) self.LSOCall.DEPARTANDREENTER.duration=1.10 self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 self.LSOCall.EXPECTSPOT75.duration=2.00 + self.LSOCall.EXPECTSPOT5.duration=1.3 self.LSOCall.FAST.duration=0.70 self.LSOCall.FOULDECK.duration=0.62 self.LSOCall.HIGH.duration=0.65 @@ -4883,6 +4980,14 @@ function AIRBOSS:_InitVoiceOvers() subtitle="Expect spot 7.5", duration=2.0, subduration=5, + }, + EXPECTSPOT5={ + file="LSO-ExpectSpot5", + suffix="ogg", + loud=false, + subtitle="Expect spot 5", + duration=1.3, + subduration=5, }, STABILIZED={ file="LSO-Stabilized", @@ -5543,14 +5648,14 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.Fast = 8.25 --=17.5/2 aoa.FAST = 8.00 --=16.5/2 elseif harrier then - -- AV-8B Harrier parameters. This might need further tuning. + -- AV-8B Harrier parameters. Tuning done on the Fast AoA to allow for abeam and ninety at Nozzles 60 - 73. aoa.SLOW = 14.0 aoa.Slow = 13.0 aoa.OnSpeedMax = 12.0 aoa.OnSpeed = 11.0 aoa.OnSpeedMin = 10.0 - aoa.Fast = 9.0 - aoa.FAST = 8.0 + aoa.Fast = 8.0 + aoa.FAST = 7.5 end return aoa @@ -5810,7 +5915,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(300) --? elseif harrier then -- 300-325 ft - alt=UTILS.FeetToMeters(300) + alt=UTILS.FeetToMeters(300)-- Need to verify end aoa=aoaac.OnSpeed @@ -6751,8 +6856,8 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Second point 1.5 NM ahead. p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) - -- Tarawa Delta pattern. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa,LHA,LHD Delta patterns. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Pattern is directly overhead the carrier. p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) @@ -8597,7 +8702,7 @@ function AIRBOSS:OnEventLand(EventData) self:T(self.lid..text) -- Check carrier type. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Power "Idle". self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) @@ -8632,7 +8737,7 @@ function AIRBOSS:OnEventLand(EventData) -- AI unit landed -- -------------------- - if self.carriertype~=AIRBOSS.CarrierType.TARAWA then + if self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS then -- Coordinate at landing event local coord=EventData.IniUnit:GetCoordinate() @@ -9539,8 +9644,10 @@ function AIRBOSS:_Bullseye(playerData) -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - -- LSO expect spot 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- LSO expect spot 5 or 7.5 call + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) end @@ -9676,8 +9783,8 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- 1.6 NM from carrier is too far. local limit=UTILS.NMToMeters(-1.6) - -- For the tarawa we give a bit more space. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- For the tarawa, other LHA and LHD we give a bit more space. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then limit=UTILS.NMToMeters(-2.0) end @@ -9722,8 +9829,10 @@ function AIRBOSS:_Abeam(playerData) -- Paddles contact. self:RadioTransmission(self.LSORadio, self.LSOCall.PADDLESCONTACT, nil, nil, nil, true) - -- LSO expect spot 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- LSO expect spot 5 or 7.5 call + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true) + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5, nil, true) end @@ -9760,7 +9869,7 @@ function AIRBOSS:_Ninety(playerData) self:_PlayerHint(playerData) -- Next step: wake. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Harrier has no wake stop. It stays port of the boat. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) else @@ -10113,7 +10222,7 @@ function AIRBOSS:_Groove(playerData) -- Drift on lineup. if rho>=RAR and rho<=RIM then - if gd.LUE>0.22 and lineupError<-0.22 then + if gd.LUE>0.22 and lineupError<-0.22 then env.info" Drift Right across centre ==> DR-" gd.Drift=" DR" self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) @@ -10128,7 +10237,7 @@ function AIRBOSS:_Groove(playerData) elseif gd.LUE<-0.13 and lineupError>0.14 then env.info" Little Drift Left across centre ==> (DL-)" gd.Drift=" (DL)" - self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) end end @@ -10434,7 +10543,7 @@ function AIRBOSS:_GetSternCoord() --local stern=self:GetCoordinate() -- Stern coordinate (sterndist<0). - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Tarawa: Translate 8 meters port. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -10472,7 +10581,7 @@ function AIRBOSS:_GetWire(Lcoord, dc) -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. local d=Ldist-dc - + -- Multiplayer wire correction. if self.mpWireCorrection then d=d-self.mpWireCorrection @@ -11177,7 +11286,7 @@ function AIRBOSS:_GetZoneHolding(case, stack) self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) -- Delta pattern. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) end @@ -11230,7 +11339,7 @@ function AIRBOSS:_GetZoneCommence(case, stack) -- Three position local Three=self:GetCoordinate():Translate(D, hdg+275) - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then local Dx=UTILS.NMToMeters(2.25) @@ -11521,7 +11630,7 @@ function AIRBOSS:_GetAltCarrier(unit) return h end ---- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa we take the abeam landing spot 120 ft abeam the 7.5 position. +--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa and America we take the abeam landing spot 120 ft abeam the 7.5 position, for the Juan Carlos I it is 120 ft and abeam the 5 position. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Optimal landing coordinate. function AIRBOSS:_GetOptLandingCoordinate() @@ -11541,6 +11650,23 @@ function AIRBOSS:_GetOptLandingCoordinate() self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + -- Alitude 120 ft. + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + + -- Landing 100 ft abeam, 120 ft alt. To allow adjustments to match different deck configurations. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + + -- Alitude 120 ft. + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Landing 100 ft abeam, 120 ft alt. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-100, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-100) + -- Alitude 120 ft. self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) @@ -11578,6 +11704,21 @@ function AIRBOSS:_GetLandingSpotCoordinate() -- Primary landing spot 7.5 self.landingspotcoord:Translate(57, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 7.5 a little further forwad on the America + self.landingspotcoord:Translate(59, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 5.0 -- TODO voice for different landing Spots. + self.landingspotcoord:Translate(89, hdg, true, true):SetAltitude(self.carrierparam.deckheight) end @@ -12070,6 +12211,11 @@ end -- * > 24 seconds: No Grade "--" -- -- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- No groove time for Harrier on LHA, LHD set to Tgroove Unicorn as starting point to allow possible _OK_ 5.0. +-- If time in the AV-8B +-- +-- * < 90 seconds: OK V/STOL +-- * > 91 Seconds: SLOW V/STOL (Early hover stop selection) -- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -12088,6 +12234,13 @@ function AIRBOSS:_EvalGrooveTime(playerData) grade="OK Groove" elseif t<=24 then grade="(LIG)" + -- Time in groove for AV-8B + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B. + grade="FAST V/STOL Groove" + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<90 then -- VSTOL Operations with AV-8B. + grade="OK V/STOL Groove" + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=91 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. + grade="SLOW V/STOL Groove" else grade="LIG" end @@ -12097,6 +12250,11 @@ function AIRBOSS:_EvalGrooveTime(playerData) grade="_OK_" end + -- V/STOL Unicorn! + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=65.0 and t<=75.0) then + grade="_OK_ V/STOL" + end + return grade end @@ -12113,7 +12271,7 @@ function AIRBOSS:_LSOgrade(playerData) return select(2, string.gsub(base, pattern, "")) end - -- Analyse flight data and conver to LSO text. + -- Analyse flight data and convert to LSO text. local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) @@ -12122,25 +12280,37 @@ function AIRBOSS:_LSOgrade(playerData) -- Put everything together. local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR - -- Count number of minor, normal and major deviations. + -- Count number of minor, normal and major deviations. TODO - work on Harrier counts due slower approach speed. local N=nXX+nIM+nIC+nAR local nL=count(G, '_')/2 local nS=count(G, '%(') local nN=N-nS-nL - -- Groove time 15-18.99 sec for a unicorn. + -- Groove time 15-18.99 sec for a unicorn. Or 65-70 for V/STOL unicorn. local Tgroove=playerData.Tgroove local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false + local TgrooveVstolUnicorn=Tgroove and (Tgroove>=65.0 and Tgroove<=70.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false local grade local points - if N==0 and TgrooveUnicorn then + if N==0 and (TgrooveUnicorn or TgrooveVstolUnicorn ) then -- No deviations, should be REALLY RARE! grade="_OK_" points=5.0 G="Unicorn" else - if nL>0 then + + -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) + -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation. + if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>2 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- Only average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + elseif nL>0 then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 @@ -12153,7 +12323,8 @@ function AIRBOSS:_LSOgrade(playerData) grade="OK" points=4.0 end - end + +end -- Replace" )"( and "__" G=G:gsub("%)%(", "") @@ -12263,35 +12434,35 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- Aircraft specific AoA values. local acaoa=self:_GetAircraftAoA(playerData) - + --Angled Approach. local P=nil if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then if LUE>self.lue.RIGHT then P=underline("AA") - elseif + elseif LUE>self.lue.RightMed then P="AA " - elseif + elseif LUE>self.lue.Right then P=little("AA") end end - + --Overshoot Start. local O=nil if step==AIRBOSS.PatternStep.GROOVE_XX then - if LUEacaoa.SLOW then @@ -12361,7 +12532,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) if P then G=G..P n=n - end + end -- Speed. if S then G=G..S @@ -12387,7 +12558,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) G=G..O n=n+1 end - + -- Add current step. local step=self:_GS(step) step=step:gsub("XX","X") @@ -12449,7 +12620,7 @@ function AIRBOSS:_GS(step, n) if n==-1 then gp=AIRBOSS.GroovePos.IC elseif n==1 then - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then gp=AIRBOSS.GroovePos.AL else gp=AIRBOSS.GroovePos.IW @@ -14339,17 +14510,17 @@ function AIRBOSS:_IsCarrierAircraft(unit) -- Get aircraft type name local aircrafttype=unit:GetTypeName() - -- Special case for Harrier which can only land on Tarawa. + -- Special case for Harrier which can only land on Tarawa, LHA and LHD. if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then return true else return false end end - -- Also only Harriers can land on the Tarawa. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Also only Harriers can land on the Tarawa, LHA and LHD. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then return false end @@ -17718,8 +17889,8 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) end - -- Tarawa landing spots. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa, LHA and LHD landing spots. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then text=text.."\n* abeam landing stop with RED flares" -- Abeam landing spot zone. local ALSPT=self:_GetZoneAbeamLandingSpot() diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 8b718f7be..6817db0e1 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -1,4 +1,3 @@ - --- **Ops** -- Combat Search and Rescue. -- -- === @@ -23,7 +22,7 @@ -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: July 2021 +-- Date: Sep 2021 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -70,6 +69,7 @@ -- -- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. -- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! +-- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. -- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. -- self.autosmokedistance = 1000 -- distance for autosmoke -- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. @@ -95,6 +95,8 @@ -- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters -- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters -- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- -- (added 0.1.9) +-- self.suppressmessages = false -- switch off all messaging if you want to do your own -- -- ## 2.1 Experimental Features -- @@ -214,26 +216,6 @@ CSAR = { -- @field Wrapper.Group#GROUP group Spawned group object. -- @field #number timestamp Timestamp for approach process -- @field #boolean alive Group is alive or dead/rescued --- ---- Updated and sorted list of known NDB beacons (in kHz!) from the available maps. - ---[[ Moved to Utils --- @field #CSAR.SkipFrequencies -CSAR.SkipFrequencies = { - 214,274,291.5,295,297.5, - 300.5,304,307,309.5,311,312,312.5,316, - 320,324,328,329,330,336,337, - 342,343,348,351,352,353,358, - 363,365,368,372.5,374, - 380,381,384,389,395,396, - 414,420,430,432,435,440,450,455,462,470,485, - 507,515,520,525,528,540,550,560,570,577,580,602,625,641,662,670,680,682,690, - 705,720,722,730,735,740,745,750,770,795, - 822,830,862,866, - 905,907,920,935,942,950,995, - 1000,1025,1030,1050,1065,1116,1175,1182,1210 - } ---]] --- All slot / Limit settings -- @type CSAR.AircraftType @@ -251,7 +233,7 @@ CSAR.AircraftType["Mi-24V"] = 8 --- CSAR class version. -- @field #string version -CSAR.version="0.1.8r3" +CSAR.version="0.1.10r5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -361,12 +343,13 @@ function CSAR:New(Coalition, Template, Alias) self.loadtimemax = 135 -- seconds self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase + self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. self.max_units = 6 --max number of pilots that can be carried self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! self.template = Template or "generic" -- template for downed pilot self.mashprefix = {"MASH"} -- prefixes used to find MASHes - self.mash = SET_GROUP:New():FilterCoalitions(self.coalition):FilterPrefixes(self.mashprefix):FilterOnce() -- currently only GROUP objects, maybe support STATICs also? + self.autosmoke = false -- automatically smoke location when heli is near self.autosmokedistance = 2000 -- distance for autosmoke -- added 0.1.4 @@ -378,6 +361,7 @@ function CSAR:New(Coalition, Template, Alias) self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters self.pilotmustopendoors = false -- switch to true to enable check on open doors + self.suppressmessages = false -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua @@ -642,7 +626,7 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _typeName = _typeName or "Pilot" if not noMessage then - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, 10) + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) end if _freq then @@ -734,7 +718,6 @@ function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessa return self end --- TODO: Split in functions per Event type --- (Internal) Event handler. -- @param #CSAR self function CSAR:_EventHandler(EventData) @@ -806,8 +789,7 @@ function CSAR:_EventHandler(EventData) if self:_DoubleEjection(_unitname) then return end - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!", self.coalition, 10) - --local m = MESSAGE:New("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!",10,"Info"):ToCoalition(self.coalition) + else self:T(self.lid .. " Pilot has not taken off, ignore") end @@ -886,13 +868,15 @@ function CSAR:_EventHandler(EventData) self:T(self.lid .. " Landing Place Nil") return -- error! end + + -- anyone on board? + if self.inTransitGroups[_event.IniUnitName] == nil then + -- ignore + return + end if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_event.IniUnitName) then - self:_DisplayMessageToSAR(_unit, "Open the door to let me out!", self.messageTime, true) - else - self:_RescuePilots(_unit) - end + self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) else self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) end @@ -912,7 +896,6 @@ end function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) self:T(self.lid .. " _InitSARForPilot") local _leader = _downedGroup:GetUnit(1) - --local _groupName = _downedGroup:GetName() local _groupName = _GroupName local _freqk = _freq / 1000 local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) @@ -928,7 +911,7 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) end -- trigger FSM event - self:__PilotDown(2,_downedGroup, _freqk, _leadername, _coordinatesText) + self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText) return self end @@ -1098,7 +1081,6 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam local grouptable = downedgrouptable --#CSAR.DownedPilot self.inTransitGroups[_heliName][_woundedGroupName] = { - -- DONE: Fix with #CSAR.DownedPilot originalUnit = grouptable.originalUnit, woundedGroup = _woundedGroupName, side = self.coalition, @@ -1106,7 +1088,7 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam player = grouptable.player, } - _woundedGroup:Destroy() + _woundedGroup:Destroy(false) self:_RemoveNameFromDownedPilots(_woundedGroupName,true) self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) @@ -1137,39 +1119,8 @@ end -- @return #boolean outcome The outcome. function CSAR:_IsLoadingDoorOpen( unit_name ) self:T(self.lid .. " _IsLoadingDoorOpen") - local ret_val = false - local unit = Unit.getByName(unit_name) - if unit ~= nil then - local type_name = unit:getTypeName() - - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then - self:T(unit_name .. " Cargo doors are open or cargo door not present") - ret_val = true - end - - if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then - self:T(unit_name .. " a side door is open") - ret_val = true - end - - if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then - self:T(unit_name .. " a side door is open ") - ret_val = true - end - - if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then - self:T(unit_name .. " front door(s) are open") - ret_val = true - end - - if ret_val == false then - self:T(unit_name .. " all doors are closed") - end - return ret_val - - end -- nil + return UTILS.IsLoadingDoorOpen(unit_name) - return false end --- (Internal) Function to check if heli is close to group. @@ -1200,14 +1151,12 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG else self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) end - --mark as shown for THIS heli and THIS group self.heliCloseMessage[_lookupKeyHeli] = true end -- have we landed close enough? if not _heliUnit:InAir() then - -- if you land on them, doesnt matter if they were heading to someone else as you\'re closer, you win! :) if self.pilotRuntoExtractPoint == true then if (_distance < self.extractDistance) then local _time = self.landedStatus[_lookupKeyHeli] @@ -1308,7 +1257,8 @@ end -- @param #CSAR self -- @param #string heliname Heli name -- @param #string groupname Group name -function CSAR:_ScheduledSARFlight(heliname,groupname) +-- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP +function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) self:T(self.lid .. " _ScheduledSARFlight") self:T({heliname,groupname}) local _heliUnit = self:_GetSARHeli(heliname) @@ -1331,8 +1281,8 @@ function CSAR:_ScheduledSARFlight(heliname,groupname) return end - if _dist < 200 and _heliUnit:InAir() == false then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(heliname) then + if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then + if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) else self:_RescuePilots(_heliUnit) @@ -1341,7 +1291,7 @@ function CSAR:_ScheduledSARFlight(heliname,groupname) end --queue up - self:__Returning(-5,heliname,_woundedGroupName) + self:__Returning(-5,heliname,_woundedGroupName, isairport) return self end @@ -1357,8 +1307,7 @@ function CSAR:_RescuePilots(_heliUnit) -- Groups already rescued return end - - -- DONE: count saved units? + local PilotsSaved = self:_PilotsOnboard(_heliName) self.inTransitGroups[_heliName] = nil @@ -1397,7 +1346,9 @@ function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) local group = _unit:GetGroup() local _clear = _clear or nil local _time = _time or self.messageTime - local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + if not self.suppressmessages then + local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + end -- integrate SRS if _speak and self.useSRS then local srstext = SOUNDTEXT:New(_text) @@ -1452,7 +1403,6 @@ function CSAR:_DisplayActiveSAR(_unitName) local _groupName = _value.name self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) self:T({Table=_value}) - --local _woundedGroup = GROUP:FindByName(_groupName) local _woundedGroup = _value.group if _woundedGroup and _value.alive then local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) @@ -1495,10 +1445,17 @@ function CSAR:_GetClosestDownedPilot(_heli) local _shortestDistance = -1 local _distance = 0 local _closestGroupInfo = nil - local _heliCoord = _heli:GetCoordinate() + local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate() + + if _heliCoord == nil then + self:E("****Error obtaining coordinate!") + return nil + end local DownedPilotsTable = self.downedPilots - for _, _groupInfo in pairs(DownedPilotsTable) do + + for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do + --for _, _groupInfo in pairs(DownedPilotsTable) do local _woundedName = _groupInfo.name local _tempWounded = _groupInfo.group @@ -1564,12 +1521,11 @@ end -- @param #number _messagetime How long to show. function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) self:T(self.lid .. " _DisplayToAllSAR") + local messagetime = _messagetime or self.messageTime for _, _unitName in pairs(self.csarUnits) do local _unit = self:_GetSARHeli(_unitName) - if _unit then - if not _messagetime then - self:_DisplayMessageToSAR(_unit, _message, _messagetime) - end + if _unit and not self.suppressmessages then + self:_DisplayMessageToSAR(_unit, _message, _messagetime) end end return self @@ -1741,9 +1697,20 @@ end function CSAR:_GetDistance(_point1, _point2) self:T(self.lid .. " _GetDistance") if _point1 and _point2 then - local distance = _point1:DistanceFromPointVec2(_point2) - return distance + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) return -1 end end @@ -1752,7 +1719,6 @@ end -- @param #CSAR self function CSAR:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") - --local _skipFrequencies = self.SkipFrequencies local FreeVHFFrequencies = {} FreeVHFFrequencies = UTILS.GenerateVHFrequencies() @@ -1792,7 +1758,6 @@ function CSAR:_GetClockDirection(_heli, _group) if _heading then local Aspect = Angle - _heading if Aspect == 0 then Aspect = 360 end - --clock = math.floor(Aspect / 30) clock = math.abs(UTILS.Round((Aspect / 30),0)) if clock == 0 then clock = 12 end end @@ -1901,6 +1866,7 @@ function CSAR:onafterStart(From, Event, To) else self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() end + self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? self:__Status(-10) return self end @@ -1909,19 +1875,19 @@ end -- @param #CSAR self function CSAR:_CheckDownedPilotTable() local pilots = self.downedPilots - for _,_entry in pairs (pilots) do - self:T("Checking for " .. _entry.name) - self:T({entry=_entry}) - local group = _entry.group - if not group:IsAlive() then - self:T("Group is dead") - if _entry.alive == true then - self:T("Switching .alive to false") + local npilots = {} + + for _ind,_entry in pairs(pilots) do + local _group = _entry.group + if _group:IsAlive() then + npilots[_ind] = _entry + else + if _entry.alive then self:__KIA(1,_entry.desc) - self:_RemoveNameFromDownedPilots(_entry.name,true) end end end + self.downedPilots = npilots return self end @@ -2041,9 +2007,10 @@ end -- @param #string To To state. -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. -function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname) +-- @param #boolean IsAirport True if heli has landed on an AFB (from event land). +function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort) self:T({From, Event, To, Heliname, Woundedgroupname}) - self:_ScheduledSARFlight(Heliname,Woundedgroupname) + self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) return self end diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 3ab505402..97b33e734 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -18,26 +18,302 @@ -- -- === -- --- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), bbirchnz (additional code!!) -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Date: July 2021 +-- Date: Sep 2021 do ------------------------------------------------------ ---- **CTLD_CARGO** class, extends #Core.Base#BASE +--- **CTLD_ENGINEERING** class, extends Core.Base#BASE +-- @type CTLD_ENGINEERING +-- @field #string ClassName +-- @field #string lid +-- @field #string Name +-- @field Wrapper.Group#GROUP Group +-- @field Wrapper.Unit#UNIT Unit +-- @field Wrapper.Group#GROUP HeliGroup +-- @field Wrapper.Unit#UNIT HeliUnit +-- @field #string State +-- @extends Core.Base#BASE +CTLD_ENGINEERING = { + ClassName = "CTLD_ENGINEERING", + lid = "", + Name = "none", + Group = nil, + Unit = nil, + --C_Ops = nil, + HeliGroup = nil, + HeliUnit = nil, + State = "", + } + + --- CTLD_ENGINEERING class version. + -- @field #string version + CTLD_ENGINEERING.Version = "0.0.3" + + --- Create a new instance. + -- @param #CTLD_ENGINEERING self + -- @param #string Name + -- @param #string GroupName Name of Engineering #GROUP object + -- @param Wrapper.Group#GROUP HeliGroup HeliGroup + -- @param Wrapper.Unit#UNIT HeliUnit HeliUnit + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:New(Name, GroupName, HeliGroup, HeliUnit) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD_ENGINEERING + + --BASE:I({Name, GroupName}) + + self.Name = Name or "Engineer Squad" -- #string + self.Group = GROUP:FindByName(GroupName) -- Wrapper.Group#GROUP + self.Unit = self.Group:GetUnit(1) -- Wrapper.Unit#UNIT + --self.C_Ops = C_Ops -- Ops.CTLD#CTLD + self.HeliGroup = HeliGroup -- Wrapper.Group#GROUP + self.HeliUnit = HeliUnit -- Wrapper.Unit#UNIT + --self.distance = Distance or UTILS.NMToMeters(1) + self.currwpt = nil -- Core.Point#COORDINATE + self.lid = string.format("%s (%s) | ",self.Name, self.Version) + -- Start State. + self.State = "Stopped" + self.marktimer = 300 -- wait this many secs before trying a crate again + + --[[ Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Search", "Searching") + self:AddTransition("*", "Move", "Moving") + self:AddTransition("*", "Arrive", "Arrived") + self:AddTransition("*", "Build", "Building") + self:AddTransition("*", "Done", "Running") + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:__Start(5) + --]] + self:Start() + local parent = self:GetParent(self) + return self + end + + --- (Internal) Set the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:SetStatus(State) + self.State = State + return self + end + + --- (Internal) Get the status + -- @param #CTLD_ENGINEERING self + -- @return #string State + function CTLD_ENGINEERING:GetStatus() + return self.State + end + + --- (Internal) Check the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsStatus(State) + return self.State == State + end + + --- (Internal) Check the negative status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsNotStatus(State) + return self.State ~= State + end + + --- (Internal) Set start status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Start() + self:T(self.lid.."Start") + self:SetStatus("Running") + return self + end + + --- (Internal) Set stop status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Stop() + self:T(self.lid.."Stop") + self:SetStatus("Stopped") + return self + end + + --- (Internal) Set build status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Build() + self:T(self.lid.."Build") + self:SetStatus("Building") + return self + end + + --- (Internal) Set done status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Done() + self:T(self.lid.."Done") + local grp = self.Group -- Wrapper.Group#GROUP + grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond") + self:SetStatus("Running") + return self + end + + --- (Internal) Search for crates in reach. + -- @param #CTLD_ENGINEERING self + -- @param #table crates Table of found crate Ops.CTLD#CTLD_CARGO objects. + -- @param #number number Number of crates found. + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Search(crates,number) + self:T(self.lid.."Search") + self:SetStatus("Searching") + -- find crates close by + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local dist = self.distance -- #number + local group = self.Group -- Wrapper.Group#GROUP + --local crates,number = COps:_FindCratesNearby(group,nil, dist) -- #table + local ctable = {} + local ind = 0 + if number > 0 then + -- get set of dropped only + for _,_cargo in pairs (crates) do + local cgotype = _cargo:GetType() + if _cargo:WasDropped() and cgotype ~= CTLD_CARGO.Enum.STATIC then + local ok = false + local chalk = _cargo:GetMark() + if chalk == nil then + ok = true + else + -- have we tried this cargo recently? + local tag = chalk.tag or "none" + local timestamp = chalk.timestamp or 0 + --self:I({chalk}) + -- enough time gone? + local gone = timer.getAbsTime() - timestamp + --self:I({time=gone}) + if gone >= self.marktimer then + ok = true + _cargo:WipeMark() + end -- end time check + end -- end chalk + if ok then + local chalk = {} + chalk.tag = "Engineers" + chalk.timestamp = timer.getAbsTime() + _cargo:AddMark(chalk) + ind = ind + 1 + table.insert(ctable,ind,_cargo) + end + end -- end dropped + end -- end for + end -- end number + + if ind > 0 then + local crate = ctable[1] -- Ops.CTLD#CTLD_CARGO + local static = crate:GetPositionable() -- Wrapper.Static#STATIC + local crate_pos = static:GetCoordinate() -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,crate_pos) + self:T(string.format("%s Distance to crate: %d", self.lid, distance)) + -- move there + if distance > 30 and distance ~= -1 and self:IsStatus("Searching") then + group:RouteGroundTo(crate_pos,15,"Line abreast",1) + self.currwpt = crate_pos -- Core.Point#COORDINATE + self:Move() + elseif distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + else + self:T(self.lid.."No crates in reach!") + end + return self + end + + --- (Internal) Move towards crates in reach. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Move() + self:T(self.lid.."Move") + self:SetStatus("Moving") + -- check if we arrived on target + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local group = self.Group -- Wrapper.Group#GROUP + local tgtpos = self.currwpt -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,tgtpos) + self:T(string.format("%s Distance remaining: %d", self.lid, distance)) + if distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + return self + end + + --- (Internal) Arrived at crates in reach. Stop group. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Arrive() + self:T(self.lid.."Arrive") + self:SetStatus("Arrived") + self.currwpt = nil + local Grp = self.Group -- Wrapper.Group#GROUP + Grp:RouteStop() + return self + end + + --- (Internal) Return distance in meters between two coordinates. + -- @param #CTLD_ENGINEERING self + -- @param Core.Point#COORDINATE _point1 Coordinate one + -- @param Core.Point#COORDINATE _point2 Coordinate two + -- @return #number Distance in meters or -1 + function CTLD_ENGINEERING:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:I({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + end +------------------------------------------------------ +--- **CTLD_CARGO** class, extends Core.Base#BASE -- @type CTLD_CARGO -- @field #number ID ID of this cargo. -- @field #string Name Name for menu. -- @field #table Templates Table of #POSITIONABLE objects. --- @field #CTLD_CARGO.Enum Type Enumerator of Type. +-- @field #CTLD_CARGO.Enum CargoType Enumerator of Type. -- @field #boolean HasBeenMoved Flag for moving. -- @field #boolean LoadDirectly Flag for direct loading. -- @field #number CratesNeeded Crates needed to build. -- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. -- @field #boolean HasBeenDropped True if dropped from heli. --- @extends Core.Fsm#FSM +-- @field #number PerCrateMass Mass in kg +-- @field #number Stock Number of builds available, -1 for unlimited +-- @extends Core.Base#BASE CTLD_CARGO = { ClassName = "CTLD_CARGO", ID = 0, @@ -49,6 +325,9 @@ CTLD_CARGO = { CratesNeeded = 0, Positionable = nil, HasBeenDropped = false, + PerCrateMass = 0, + Stock = nil, + Mark = nil, } --- Define cargo types. @@ -60,6 +339,8 @@ CTLD_CARGO = { ["FOB"] = "FOB", -- #string FOB ["CRATE"] = "Crate", -- #string crate ["REPAIR"] = "Repair", -- #string repair + ["ENGINEERS"] = "Engineers", -- #string engineers + ["STATIC"] = "Static", -- #string engineers } --- Function to create new CTLD_CARGO object. @@ -73,8 +354,10 @@ CTLD_CARGO = { -- @param #number CratesNeeded Crates needed to build. -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper. + -- @param #number PerCrateMass Mass in kg + -- @param #number Stock Number of builds available, nil for unlimited -- @return #CTLD_CARGO self - function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped) + function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock) -- Inherit everything from BASE class. local self=BASE:Inherit(self, BASE:New()) -- #CTLD self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) @@ -87,6 +370,9 @@ CTLD_CARGO = { self.CratesNeeded = CratesNeeded or 0 -- #number self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE self.HasBeenDropped = Dropped or false --#boolean + self.PerCrateMass = PerCrateMass or 0 -- #number + self.Stock = Stock or nil --#number + self.Mark = nil return self end @@ -97,6 +383,12 @@ CTLD_CARGO = { return self.ID end + --- Query Mass. + -- @param #CTLD_CARGO self + -- @return #number Mass in kg + function CTLD_CARGO:GetMass() + return self.PerCrateMass + end --- Query Name. -- @param #CTLD_CARGO self -- @return #string Name @@ -178,6 +470,42 @@ CTLD_CARGO = { self.HasBeenDropped = dropped or false end + --- Get Stock. + -- @param #CTLD_CARGO self + -- @return #number Stock + function CTLD_CARGO:GetStock() + if self.Stock then + return self.Stock + else + return -1 + end + end + + --- Add Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to add, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:AddStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock + number + end + return self + end + + --- Remove Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to reduce, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:RemoveStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock - number + if self.Stock < 0 then self.Stock = 0 end + end + return self + end + --- Query crate type for REPAIR -- @param #CTLD_CARGO self -- @param #boolean @@ -188,6 +516,31 @@ CTLD_CARGO = { return false end end + + --- Query crate type for STATIC + -- @param #CTLD_CARGO self + -- @param #boolean + function CTLD_CARGO:IsStatic() + if self.CargoType == "Static" then + return true + else + return false + end + end + + function CTLD_CARGO:AddMark(Mark) + self.Mark = Mark + return self + end + + function CTLD_CARGO:GetMark(Mark) + return self.Mark + end + + function CTLD_CARGO:WipeMark() + self.Mark = nil + return self + end end @@ -213,7 +566,8 @@ do -- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. -- * No need for extra MIST loading. -- * Additional events to tailor your mission. --- * ANY late activated group can serve as cargo, either as troops or crates, which have to be build on-location. +-- * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests. +-- * Option to persist (save&load) your dropped troops, crates and vehicles. -- -- ## 0. Prerequisites -- @@ -237,26 +591,43 @@ do -- -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3 -- -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed) -- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3) +-- -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully. +-- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80) -- --- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4 --- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4) +-- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4. No weight. We only have 2 in stock: +-- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2) +-- +-- -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped, +-- -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay +-- -- to build or repair or under fire. +-- my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4) +-- myctld.EngineerSearch = 2000 -- teams will search for crates in this radius. -- -- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build -- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) +-- -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Currently no max weight checked. Fly carefully. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775) +-- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) -- -- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: -- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) -- -- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair -- my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1) +-- my_ctld.repairtime = 300 -- takes 300 seconds to repair something +-- +-- -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, +-- -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg. +-- my_ctld:AddStaticsCargo("Ammunition",500) -- -- ## 1.3 Add logistics zones -- -- Add zones for loading troops and crates and dropping, building crates -- -- -- Add a zone of type LOAD to our setup. Players can load troops and crates. --- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside of the zone. +-- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone. -- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. -- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) -- @@ -271,17 +642,23 @@ do -- -- my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true) -- +-- -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship +-- -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone. +-- -- The ship is 240 meters long and 20 meters wide. +-- -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work. +-- -- When the ship is moving, forcing hoverload might not be a good idea. +-- my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20) -- -- ## 2. Options -- -- The following options are available (with their defaults). Only set the ones you want changed: -- -- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. --- my_ctld.CrateDistance = 30 -- List and Load crates in this radius only. +-- my_ctld.CrateDistance = 35 -- List and Load crates in this radius only. -- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. -- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load. -- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load. --- my_ctld.forcehoverload = true -- Crates (not: troops) can only be loaded while hovering. +-- my_ctld.forcehoverload = true -- Crates (not: troops) can **only** be loaded while hovering. -- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. -- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). -- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... @@ -289,6 +666,9 @@ do -- my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters) -- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. -- my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit. +-- my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups. +-- my_ctld.allowcratepickupagain = true -- allow re-pickup crates that were dropped. +-- my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types -- -- ## 2.1 User functions -- @@ -298,19 +678,20 @@ do -- -- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. -- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type: --- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8) +-- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8, 12) -- --- Default unit type capabilities are: +-- -- Default unit type capabilities are: -- --- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, --- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, --- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, --- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, --- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, --- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, --- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, +-- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, +-- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, +-- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, +-- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, +-- ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, +-- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, +-- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, +-- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, -- -- -- ### 2.1.2 Activate and deactivate zones @@ -325,6 +706,26 @@ do -- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: -- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) -- +-- ## 2.1.3 Limit and manage available resources +-- +-- When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g. +-- +-- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) +-- +-- You can manually add or remove the available stock like so: +-- +-- -- Crates +-- my_ctld:AddStockCrates("Humvee", 2) +-- my_ctld:RemoveStockCrates("Humvee", 2) +-- +-- -- Troops +-- my_ctld:AddStockTroops("Anti-Air", 2) +-- my_ctld:RemoveStockTroops("Anti-Air", 2) +-- +-- Notes: +-- Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost. +-- -- ## 3. Events -- -- The class comes with a number of FSM-based events that missions designers can use to shape their mission. @@ -388,16 +789,20 @@ do -- -- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) -- local points = 10 --- local PlayerName = Unit:GetPlayerName() --- my_scoring:_AddPlayerFromUnit( Unit ) --- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- if Unit then +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- end -- end -- -- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) -- local points = 5 --- local PlayerName = Unit:GetPlayerName() --- my_scoring:_AddPlayerFromUnit( Unit ) --- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- if Unit then + -- local PlayerName = Unit:GetPlayerName() + -- my_scoring:_AddPlayerFromUnit( Unit ) + -- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- end -- end -- -- ## 4. F10 Menu structure @@ -428,7 +833,11 @@ do -- ## 4.6 Show hover parameters -- -- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights. --- +-- +-- ## 4.7 List Inventory +-- +-- Lists invetory of available units to drop or build. +-- -- ## 5. Support for Hercules mod by Anubis -- -- Basic support for the Hercules mod By Anubis has been build into CTLD. Currently this does **not** cover objects and troops which can @@ -441,7 +850,7 @@ do -- my_ctld.enableHercules = true -- my_ctld.HercMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft -- my_ctld.HercMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft --- my_ctld.HercMaxSpeed = 77 -- 77mps or 270 kph or 150 kn +-- my_ctld.HercMaxSpeed = 77 -- 77mps or 270kph or 150kn -- -- Also, the following options need to be set to `true`: -- @@ -451,6 +860,27 @@ do -- -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers -- +-- ## 6. Save and load back units - persistance +-- +-- You can save and later load back units dropped or build to make your mission persistent. +-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts. +-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. +-- +-- Use the following options to manage your saves: +-- +-- my_ctld.enableLoadSave = true -- allow auto-saving and loading of files +-- my_ctld.saveinterval = 600 -- save every 10 minutes +-- my_ctld.filename = "missionsave.csv" -- example filename +-- my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path +-- my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions) +-- +-- Then use an initial load at the beginning of your mission: +-- +-- my_ctld:__Load(10) +-- +-- **Caveat:** +-- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved. +-- -- @field #CTLD CTLD = { ClassName = "CTLD", @@ -464,18 +894,17 @@ CTLD = { FreeUHFFrequencies = {}, -- Table of UHF FreeFMFrequencies = {}, -- Table of FM CargoCounter = 0, - dropOffZones = {}, wpZones = {}, Cargo_Troops = {}, -- generic troops objects Cargo_Crates = {}, -- generic crate objects Loaded_Cargo = {}, -- cargo aboard units Spawned_Crates = {}, -- Holds objects for crates spawned generally Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects - CrateDistance = 30, -- list crates in this radius + CrateDistance = 35, -- list crates in this radius debug = false, wpZones = {}, - pickupZones = {}, dropOffZones = {}, + pickupZones = {}, } ------------------------------ @@ -483,13 +912,18 @@ CTLD = { -- DONE: TEST Hover load and unload -- DONE: Crate unload -- DONE: Hover (auto-)load --- TODO: (More) Housekeeping +-- DONE: (More) Housekeeping -- DONE: Troops running to WP Zone -- DONE: Zone Radio Beacons -- DONE: Stats Running -- DONE: Added support for Hercules -- TODO: Possibly - either/or loading crates and troops --- TODO: Limit of troops, crates buildable? +-- DONE: Make inject respect existing cargo types +-- TODO: Drop beacons or flares/smoke +-- DONE: Add statics as cargo +-- DONE: List cargo in stock +-- DONE: Limit of troops, crates buildable? +-- DONE: Allow saving of Troops & Vehicles ------------------------------ --- Radio Beacons @@ -503,11 +937,13 @@ CTLD = { -- @field #string name Name of Zone. -- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red. -- @field #boolean active Active or not. --- @field #string type Type of zone, i.e. load,drop,move +-- @field #string type Type of zone, i.e. load,drop,move,ship -- @field #boolean hasbeacon Create and run radio beacons if active. -- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon -- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon -- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #number shiplength For ships - length of ship +-- @field #number shipwidth For ships - width of ship --- Zone Type Info. -- @type CTLD.CargoZoneType @@ -515,6 +951,7 @@ CTLD.CargoZoneType = { LOAD = "load", DROP = "drop", MOVE = "move", + SHIP = "ship", } --- Buildable table info. @@ -534,22 +971,23 @@ CTLD.CargoZoneType = { -- @field #number cratelimit Number of crates transportable. -- @field #number trooplimit Number of troop units transportable. CTLD.UnitTypes = { - ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, - ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, - ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, - ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, - ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, - ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, - ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, - ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, - ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, - ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, - ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers + ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, + ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, + ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, + ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, + ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, + ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, + ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, + ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, + ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, + ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, -- 19t cargo, 64 paratroopers. + --Actually it's longer, but the center coord is off-center of the model. } --- CTLD class version. -- @field #string version -CTLD.version="0.1.4r3" +CTLD.version="0.2.2a4" --- Instantiate a new CTLD. -- @param #CTLD self @@ -603,7 +1041,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State + -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- CTLD status update. self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. @@ -613,7 +1051,9 @@ function CTLD:New(Coalition, Prefixes, Alias) self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. - self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "Load", "*") -- CTLD load event. + self:AddTransition("*", "Save", "*") -- CTLD save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables @@ -635,10 +1075,12 @@ function CTLD:New(Coalition, Prefixes, Alias) self.pickupZones = {} self.dropOffZones = {} self.wpZones = {} + self.shipZones = {} -- Cargo self.Cargo_Crates = {} self.Cargo_Troops = {} + self.Cargo_Statics = {} self.Loaded_Cargo = {} self.Spawned_Crates = {} self.Spawned_Cargo = {} @@ -649,8 +1091,14 @@ function CTLD:New(Coalition, Prefixes, Alias) self.CrateCounter = 0 self.TroopCounter = 0 + -- added engineering + self.Engineers = 0 -- #number use as counter + self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects + self.EngineerSearch = 2000 -- #number search distance for crates to build or repair + -- setup - self.CrateDistance = 30 -- list/load crates in this radius + self.CrateDistance = 35 -- list/load crates in this radius + self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor self.prefixes = Prefixes or {"Cargoheli"} --self.I({prefixes = self.prefixes}) self.useprefix = true @@ -677,6 +1125,31 @@ function CTLD:New(Coalition, Prefixes, Alias) -- time to repair a unit/group self.repairtime = 300 + -- place spawned crates in front of aircraft + self.placeCratesAhead = false + + -- country of crates spawned + self.cratecountry = country.id.GERMANY + + if self.coalition == coalition.side.RED then + self.cratecountry = country.id.RUSSIA + end + + -- load and save dropped TROOPS + self.enableLoadSave = false + self.filepath = nil + self.saveinterval = 600 + self.eventoninject = true + + local AliaS = string.gsub(self.alias," ","_") + self.filename = string.format("CTLD_%s_Persist.csv",AliaS) + + -- allow re-pickup crates + self.allowcratepickupagain = true + + -- slingload + self.enableslingload = false + for i=1,100 do math.random() end @@ -685,7 +1158,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self:_GenerateUHFrequencies() self:_GenerateFMFrequencies() - ------------------------ + ------------------------ --- Pseudo Functions --- ------------------------ @@ -715,6 +1188,24 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #CTLD self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Load". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Load" after a delay. + -- @function [parent=#CTLD] __Load + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Save". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Save" after a delay. + -- @function [parent=#CTLD] __Save + -- @param #CTLD self + -- @param #number delay Delay in seconds. + --- FSM Function OnAfterTroopsPickedUp. -- @function [parent=#CTLD] OnAfterTroopsPickedUp -- @param #CTLD self @@ -801,6 +1292,24 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. + --- FSM Function OnAfterLoad. + -- @function [parent=#CTLD] OnAfterLoad + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + + --- FSM Function OnAfterSave. + -- @function [parent=#CTLD] OnAfterSave + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". + return self end @@ -825,6 +1334,7 @@ function CTLD:_GetUnitCapabilities(Unit) capabilities.cratelimit = 0 capabilities.trooplimit = 0 capabilities.type = "generic" + capabilities.length = 20 end return capabilities end @@ -914,11 +1424,23 @@ end -- @param #CTLD_CARGO Cargotype function CTLD:_LoadTroops(Group, Unit, Cargotype) self:T(self.lid .. " _LoadTroops") + -- check if we have stock + local instock = Cargotype:GetStock() + local cgoname = Cargotype:GetName() + local cgotype = Cargotype:GetType() + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + -- nothing left over + self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group) + return self + end -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) -- check if we are in LOAD zone local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end if not inzone then self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) if not self.debug then return self end @@ -955,13 +1477,15 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) return else self.CargoCounter = self.CargoCounter + 1 - local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) loaded.Troopsloaded = loaded.Troopsloaded + troopsize table.insert(loaded.Cargo,loadcargotype) self.Loaded_Cargo[unitname] = loaded self:_SendMessage("Troops boarded!", 10, false, Group) self:__TroopsPickedUp(1,Group, Unit, Cargotype) + self:_UpdateUnitCargoMass(Unit) + Cargotype:RemoveStock() end return self end @@ -973,24 +1497,26 @@ function CTLD:_FindRepairNearby(Group, Unit, Repairtype) -- find nearest group of deployed groups local nearestGroup = nil local nearestGroupIndex = -1 - local nearestDistance = 10000000 + local nearestDistance = 10000 for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) - if distance < nearestDistance and distance ~= -1 then + local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT + local desc = unit:GetDesc() or nil + --self:I({desc = desc.attributes}) + if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then nearestGroup = v nearestGroupIndex = k nearestDistance = distance end end - + -- found one and matching distance? - if nearestGroup == nil or nearestDistance > 1000 then + if nearestGroup == nil or nearestDistance > self.EngineerSearch then self:_SendMessage("No unit close enough to repair!", 10, false, Group) return nil, nil end local groupname = nearestGroup:GetName() - --self:I(string.format("***** Found Group %s",groupname)) -- helper to find matching template local function matchstring(String,Table) @@ -1036,7 +1562,8 @@ end -- @param #table Crates Table of #CTLD_CARGO objects near the unit. -- @param #CTLD.Buildable Build Table build object. -- @param #number Number Number of objects in Crates (found) to limit search. -function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) +-- @param #boolean Engineering If true it is an Engineering repair. +function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering) self:T(self.lid .. " _RepairObjectFromCrates") local build = Build -- -- #CTLD.Buildable --self:I({Build=Build}) @@ -1045,7 +1572,9 @@ function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) --self:I({Repairtype=Repairtype, CargoType=CargoType, NearestGroup=NearestGroup}) if NearestGroup ~= nil then if self.repairtime < 2 then self.repairtime = 30 end -- noob catch - self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + if not Engineering then + self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + end -- now we can build .... --NearestGroup:Destroy(false) local name = CargoType:GetName() @@ -1062,11 +1591,15 @@ function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) self:_CleanUpCrates(Crates,Build,Number) local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self) desttimer:Start(self.repairtime - 1) - local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true) + local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate()) buildtimer:Start(self.repairtime) --self:_BuildObjectFromCrates(Group,Unit,object) else - self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + if not Engineering then + self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + else + self:T("Can't repair this unit with " .. build.Name) + end end return self end @@ -1075,7 +1608,6 @@ end -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit - -- @param #CTLD_CARGO Cargotype function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz! self:T(self.lid .. " _ExtractTroops") -- landed or hovering over load zone? @@ -1100,64 +1632,98 @@ end local nearestGroup = nil local nearestGroupIndex = -1 local nearestDistance = 10000000 + local nearestList = {} + local distancekeys = {} + local extractdistance = self.CrateDistance * self.ExtractFactor for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) - if distance < nearestDistance and distance ~= -1 then + if distance <= extractdistance and distance ~= -1 then nearestGroup = v nearestGroupIndex = k nearestDistance = distance + table.insert(nearestList, math.floor(distance), v) + distancekeys[#distancekeys+1] = math.floor(distance) end end - - if nearestGroup == nil or nearestDistance > self.CrateDistance then + + if nearestGroup == nil or nearestDistance > extractdistance then self:_SendMessage("No units close enough to extract!", 10, false, Group) return self end - -- find matching cargo type - local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") - local Cargotype = nil - for k,v in pairs(self.Cargo_Troops) do - if v.Name == groupType then - Cargotype = v - break + + -- sort reference keys + table.sort(distancekeys) + + local secondarygroups = {} + + for i=1,#distancekeys do + local nearestGroup = nearestList[distancekeys[i]] + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + local comparison = "" + if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end + if comparison == groupType then + Cargotype = v + break + end + end + if Cargotype == nil then + self:_SendMessage("Can't onboard " .. groupType, 10, false, Group) + else + + local troopsize = Cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + --return self + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:_UpdateUnitCargoMass(Unit) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + --table.remove(self.DroppedTroops, nearestGroupIndex) + if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then + --self:I("*****This CargoType has multiple templates: "..Cargotype.Name) + for _,_key in pairs (Cargotype.Templates) do + table.insert(secondarygroups,_key) + end + end + nearestGroup:Destroy(false) + end end end - - if Cargotype == nil then - self:_SendMessage("Can't find a matching cargo type for " .. groupType, 10, false, Group) - return self - end - - local troopsize = Cargotype:GetCratesNeeded() -- #number - -- have we loaded stuff already? - local numberonboard = 0 - local loaded = {} - if self.Loaded_Cargo[unitname] then - loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo - numberonboard = loaded.Troopsloaded or 0 - else - loaded = {} -- #CTLD.LoadedCargo - loaded.Troopsloaded = 0 - loaded.Cratesloaded = 0 - loaded.Cargo = {} - end - if troopsize + numberonboard > trooplimit then - self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) - return - else - self.CargoCounter = self.CargoCounter + 1 - local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) - self:T({cargotype=loadcargotype}) - loaded.Troopsloaded = loaded.Troopsloaded + troopsize - table.insert(loaded.Cargo,loadcargotype) - self.Loaded_Cargo[unitname] = loaded - self:_SendMessage("Troops boarded!", 10, false, Group) - self:__TroopsExtracted(1,Group, Unit, nearestGroup) - - -- clean up: - table.remove(self.DroppedTroops, nearestGroupIndex) - nearestGroup:Destroy() + -- clean up secondary groups + for _,_name in pairs(secondarygroups) do + for _,_group in pairs(nearestList) do + if _group and _group:IsAlive() then + local groupname = string.match(_group:GetName(), "(.+)-(.+)$") + if _name == groupname then + _group:Destroy(false) + end + end + end end + self:CleanDroppedTroops() return self end @@ -1170,12 +1736,26 @@ end -- @param #boolean drop If true we\'re dropping from heli rather than loading. function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) self:T(self.lid .. " _GetCrates") - local cgoname = Cargo:GetName() - -- check if we are in LOAD zone + if not drop then + local cgoname = Cargo:GetName() + -- check if we have stock + local instock = Cargo:GetStock() + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + -- nothing left over + self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group) + return self + end + end + -- check if we are in LOAD zone local inzone = false local drop = drop or false + local ship = nil + local width = 20 if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end else if self.dropcratesanywhere then -- #1570 inzone = true @@ -1192,7 +1772,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) -- avoid crate spam local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities local canloadcratesno = capabilities.cratelimit - local loaddist = self.CrateDistance or 30 + local loaddist = self.CrateDistance or 35 local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) if numbernearby >= canloadcratesno and not drop then self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) @@ -1200,58 +1780,154 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) end -- spawn crates in front of helicopter local IsHerc = self:IsHercules(Unit) -- Herc - local cargotype = Cargo -- #CTLD_CARGO + local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO local number = number or cargotype:GetCratesNeeded() --#number local cratesneeded = cargotype:GetCratesNeeded() --#number local cratename = cargotype:GetName() local cratetemplate = "Container"-- #string + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end -- get position and heading of heli local position = Unit:GetCoordinate() local heading = Unit:GetHeading() + 1 local height = Unit:GetHeight() local droppedcargo = {} + local cratedistance = 0 + local rheading = 0 + local angleOffNose = 0 + local addon = 0 + if IsHerc then + -- spawn behind the Herc + addon = 180 + end -- loop crates needed for i=1,number do local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) - local cratedistance = i*4 + 8 - if IsHerc then - -- wider radius - cratedistance = i*4 + 12 + if not self.placeCratesAhead then + cratedistance = (i-1)*2.5 + capabilities.length + if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end + -- altered heading logic + -- DONE: right standard deviation? + rheading = UTILS.RandomGaussian(0,30,-90,90,100) + rheading = math.fmod((heading + rheading + addon), 360) + else + local initialSpacing = IsHerc and 16 or 12 -- initial spacing of the first crates + local crateSpacing = 4 -- further spacing of remaining crates + local lateralSpacing = 4 -- lateral spacing of crates + local nrSideBySideCrates = 3 -- number of crates that are placed side-by-side + + if cratesneeded == 1 then + -- single crate needed spawns straight ahead + cratedistance = initialSpacing + rheading = heading + else + if (i - 1) % nrSideBySideCrates == 0 then + cratedistance = i == 1 and initialSpacing or cratedistance + crateSpacing + angleOffNose = math.ceil(math.deg(math.atan(lateralSpacing / cratedistance))) + rheading = heading - angleOffNose + else + rheading = rheading + angleOffNose + end + end end - for i=1,50 do - math.random(90,270) - end - local rheading = math.floor(math.random(90,270) * heading + 1 / 360) - if not IsHerc then - rheading = rheading + 180 -- mirror for Helis - end - if rheading > 360 then rheading = rheading - 360 end -- catch > 360 local cratecoord = position:Translate(cratedistance,rheading) local cratevec2 = cratecoord:GetVec2() - self.CrateCounter = self.CrateCounter + 1 - self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType("container_cargo","Cargos",country.id.GERMANY) - :InitCoordinate(cratecoord) + self.CrateCounter = self.CrateCounter + 1 + local basetype = "container_cargo" + if isstatic then + basetype = cratetemplate + end + if type(ship) == "string" then + self:T("Spawning on ship "..ship) + local Ship = UNIT:FindByName(ship) + local shipcoord = Ship:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local dist = shipcoord:Get2DDistance(unitcoord) + dist = dist - (20 + math.random(1,10)) + local width = width / 2 + local Offy = math.random(-width,width) + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + --:InitCoordinate(cratecoord) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :InitLinkToUnit(Ship,dist,Offy,0) :Spawn(270,cratealias) - + else + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCoordinate(cratecoord) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + --:InitLinkToUnit(Unit,OffsetX,OffsetY,OffsetAngle) + :Spawn(270,cratealias) + end local templ = cargotype:GetTemplates() local sorte = cargotype:GetType() - self.CargoCounter = self.CargoCounter +1 + self.CargoCounter = self.CargoCounter + 1 local realcargo = nil if drop then - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) table.insert(droppedcargo,realcargo) else - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter]) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) + Cargo:RemoveStock() end table.insert(self.Spawned_Cargo, realcargo) end - local text = string.format("Crates for %s have been positioned near you!",cratename) - if drop then - text = string.format("Crates for %s have been dropped!",cratename) - self:__CratesDropped(1, Group, Unit, droppedcargo) - end - self:_SendMessage(text, 10, false, Group) + local text = string.format("Crates for %s have been positioned near you!",cratename) + if drop then + text = string.format("Crates for %s have been dropped!",cratename) + self:__CratesDropped(1, Group, Unit, droppedcargo) + end + self:_SendMessage(text, 10, false, Group) + return self +end + +--- Inject crates and static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. +-- @param #CTLD_CARGO Cargo The cargo type to spawn. +-- @return #CTLD self +function CTLD:InjectStatics(Zone, Cargo) + self:T(self.lid .. " InjectStatics") + local cratecoord = Zone:GetCoordinate() + local surface = cratecoord:GetSurfaceType() + if surface == land.SurfaceType.WATER then return self + end + local cargotype = Cargo -- #CTLD_CARGO + --local number = 1 + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratetemplate = "Container"-- #string + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + local cratename = cargotype:GetName() + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end + local basetype = "container_cargo" + if isstatic then + basetype = cratetemplate + end + self.CrateCounter = self.CrateCounter + 1 + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :InitCoordinate(cratecoord) + :Spawn(270,cratealias) + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter + 1 + cargotype.Positionable = self.Spawned_Crates[self.CrateCounter] + table.insert(self.Spawned_Cargo, cargotype) + return self end --- (Internal) Function to find and list nearby crates. @@ -1261,7 +1937,7 @@ end -- @return #CTLD self function CTLD:_ListCratesNearby( _group, _unit) self:T(self.lid .. " _ListCratesNearby") - local finddist = self.CrateDistance or 30 + local finddist = self.CrateDistance or 35 local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table if number > 0 then local text = REPORT:New("Crates Found Nearby:") @@ -1271,9 +1947,9 @@ function CTLD:_ListCratesNearby( _group, _unit) local name = entry:GetName() --#string local dropped = entry:WasDropped() if dropped then - text:Add(string.format("Dropped crate for %s",name)) + text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass)) else - text:Add(string.format("Crate for %s",name)) + text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass)) end end if text:GetCount() == 1 then @@ -1295,9 +1971,21 @@ end function CTLD:_GetDistance(_point1, _point2) self:T(self.lid .. " _GetDistance") if _point1 and _point2 then - local distance = _point1:DistanceFromPointVec2(_point2) - return distance + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:I({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) return -1 end end @@ -1346,7 +2034,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) local unitname = unit:GetName() -- see if this heli can load crates local unittype = unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(Unit) + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number @@ -1367,6 +2055,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) else -- have we loaded stuff already? local numberonboard = 0 + local massonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo @@ -1378,11 +2067,16 @@ function CTLD:_LoadCratesNearby(Group, Unit) loaded.Cargo = {} end -- get nearby crates - local finddist = self.CrateDistance or 30 + local finddist = self.CrateDistance or 35 local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table - if number == 0 or numberonboard == cratelimit then - self:_SendMessage("Sorry no loadable crates nearby or fully loaded!", 10, false, Group) - return -- exit + if number == 0 and self.hoverautoloading then + return self -- exit + elseif number == 0 then + self:_SendMessage("Sorry no loadable crates nearby!", 10, false, Group) + return self -- exit + elseif numberonboard == cratelimit then + self:_SendMessage("Sorry no fully loaded!", 10, false, Group) + return self -- exit else -- go through crates and load local capacity = cratelimit - numberonboard @@ -1393,8 +2087,14 @@ function CTLD:_LoadCratesNearby(Group, Unit) local crateind = 0 -- get crate with largest index for _ind,_crate in pairs (nearcrates) do - if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then - crateind = _crate:GetID() + if self.allowcratepickupagain then + if _crate:GetID() > crateind and _crate.Positionable ~= nil then + crateind = _crate:GetID() + end + else + if not _crate:HasMoved() and _crate:WasDropped() and _crate:GetID() > crateind then + crateind = _crate:GetID() + end end end -- load one if we found one @@ -1402,16 +2102,19 @@ function CTLD:_LoadCratesNearby(Group, Unit) local crate = nearcrates[crateind] -- #CTLD_CARGO loaded.Cratesloaded = loaded.Cratesloaded + 1 crate:SetHasMoved(true) + crate:SetWasDropped(false) table.insert(loaded.Cargo, crate) table.insert(crateidsloaded,crate:GetID()) -- destroy crate - crate:GetPositionable():Destroy() + crate:GetPositionable():Destroy(false) crate.Positionable = nil - self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + table.remove(nearcrates,crate:GetID()) self:__CratesPickedUp(1, Group, Unit, crate) end end self.Loaded_Cargo[unitname] = loaded + self:_UpdateUnitCargoMass(Unit) -- clean up real world crates local existingcrates = self.Spawned_Cargo -- #table local newexcrates = {} @@ -1431,6 +2134,44 @@ function CTLD:_LoadCratesNearby(Group, Unit) return self end +--- (Internal) Function to get current loaded mass +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #number mass in kgs +function CTLD:_GetUnitCargoMass(Unit) + self:T(self.lid .. " _GetUnitCargoMass") + local unitname = Unit:GetName() + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local loadedmass = 0 -- #number + if self.Loaded_Cargo[unitname] then + local cargotable = loadedcargo.Cargo or {} -- #table + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded()) + end + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not cargo:WasDropped() then + loadedmass = loadedmass + cargo.PerCrateMass + end + end + end + return loadedmass +end + +--- (Internal) Function to calculate and set Unit internal cargo mass +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UpdateUnitCargoMass(Unit) + self:T(self.lid .. " _UpdateUnitCargoMass") + local calculatedMass = self:_GetUnitCargoMass(Unit) + Unit:SetUnitInternalCargo(calculatedMass) + --local report = REPORT:New("Loadmaster report") + --report:Add("Carrying " .. calculatedMass .. "Kg") + --self:_SendMessage(report:Text(),10,false,Unit:GetGroup()) + return self +end + --- (Internal) Function to list loaded cargo. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -1444,6 +2185,7 @@ function CTLD:_ListCargo(Group, Unit) local trooplimit = capabilities.trooplimit -- #boolean local cratelimit = capabilities.cratelimit -- #number local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local loadedmass = self:_GetUnitCargoMass(Unit) -- #number if self.Loaded_Cargo[unitname] then local no_troops = loadedcargo.Troopsloaded or 0 local no_crates = loadedcargo.Cratesloaded or 0 @@ -1456,7 +2198,7 @@ function CTLD:_ListCargo(Group, Unit) for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) end end @@ -1469,7 +2211,7 @@ function CTLD:_ListCargo(Group, Unit) for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type ~= CTLD_CARGO.Enum.TROOPS then + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then report:Add(string.format("Crate: %s size 1",cargo:GetName())) cratecount = cratecount + 1 end @@ -1478,6 +2220,7 @@ function CTLD:_ListCargo(Group, Unit) report:Add(" N O N E") end report:Add("------------------------------------------------------------") + report:Add("Total Mass: ".. loadedmass .. " kg") local text = report:Text() self:_SendMessage(text, 30, true, Group) else @@ -1486,6 +2229,100 @@ function CTLD:_ListCargo(Group, Unit) return self end +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListInventory(Group, Unit) + self:T(self.lid .. " _ListInventory") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local cgotypes = self.Cargo_Crates + local trptypes = self.Cargo_Troops + local stctypes = self.Cargo_Statics + + local function countcargo(cgotable) + local counter = 0 + for _,_cgo in pairs(cgotable) do + counter = counter + 1 + end + return counter + end + + local crateno = countcargo(cgotypes) + local troopno = countcargo(trptypes) + local staticno = countcargo(stctypes) + + if (crateno > 0 or troopno > 0 or staticno > 0) then + + local report = REPORT:New("Inventory Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(trptypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cgotypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + cratecount = cratecount + 1 + end + end + -- Statics + for _,_cargo in pairs(stctypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock)) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) + end + return self +end + --- (Internal) Function to check if a unit is a Hercules C-130. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit @@ -1507,6 +2344,9 @@ function CTLD:_UnloadTroops(Group, Unit) -- check if we are in LOAD zone local droppingatbase = false local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end if inzone then droppingatbase = true end @@ -1529,7 +2369,7 @@ function CTLD:_UnloadTroops(Group, Unit) for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then -- unload troops local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} @@ -1549,12 +2389,21 @@ function CTLD:_UnloadTroops(Group, Unit) :InitRandomizeUnits(true,20,2) :InitDelayOff() :SpawnFromVec2(randomcoord) - if self.movetroopstowpzone then + if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) end end -- template loop cargo:SetWasDropped(true) - self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + -- engineering group? + --self:I("Dropped Troop Type: "..type) + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + self:_SendMessage(string.format("Dropped Engineers %s into action!",name), 10, false, Group) + else + self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + end self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter]) end -- if type end end -- cargotable loop @@ -1573,13 +2422,28 @@ function CTLD:_UnloadTroops(Group, Unit) local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local dropped = cargo:WasDropped() - if type ~= CTLD_CARGO.Enum.TROOP and not dropped then + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded = loaded.Cratesloaded + 1 + else + -- add troops back to stock + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then + -- find right generic type + local name = cargo:GetName() + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + local stock = _troop:GetStock() + -- avoid making unlimited stock limited + if stock and tonumber(stock) >= 0 then _troop:AddStock() end + end + end + end end end self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded + self:_UpdateUnitCargoMass(Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) @@ -1593,7 +2457,7 @@ end --- (Internal) Function to unload crates from heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit +-- @param Wrapper.Unit#UNIT Unit function CTLD:_UnloadCrates(Group, Unit) self:T(self.lid .. " _UnloadCrates") @@ -1619,13 +2483,13 @@ function CTLD:_UnloadCrates(Group, Unit) -- Get what we have loaded local unitname = Unit:GetName() if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then - local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo - -- looking for troops + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for crate local cargotable = loadedcargo.Cargo for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and (not cargo:WasDropped() or self.allowcratepickupagain) then -- unload crates self:_GetCrates(Group, Unit, cargo, 1, true) cargo:SetWasDropped(true) @@ -1633,21 +2497,24 @@ function CTLD:_UnloadCrates(Group, Unit) end end -- cleanup load list - local loaded = {} -- #CTLD.LoadedCargo + local loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} + for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local size = cargo:GetCratesNeeded() - if type == CTLD_CARGO.Enum.TROOP then + if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then table.insert(loaded.Cargo,_cargo) - loaded.Cratesloaded = loaded.Troopsloaded + size + loaded.Troopsloaded = loaded.Troopsloaded + size end end self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded + + self:_UpdateUnitCargoMass(Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) @@ -1661,11 +2528,21 @@ end --- (Internal) Function to build nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit -function CTLD:_BuildCrates(Group, Unit) +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true build is by an engineering team. +function CTLD:_BuildCrates(Group, Unit,Engineering) self:T(self.lid .. " _BuildCrates") + -- avoid users trying to build from flying Hercs + local type = Unit:GetTypeName() + if type == "Hercules" and self.enableHercules and not Engineering then + local speed = Unit:GetVelocityKMH() + if speed > 1 then + self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) + return self + end + end -- get nearby crates - local finddist = self.CrateDistance or 30 + local finddist = self.CrateDistance or 35 local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table local buildables = {} local foundbuilds = false @@ -1674,7 +2551,7 @@ function CTLD:_BuildCrates(Group, Unit) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() and not Crate:IsRepair() then + if Crate:WasDropped() and not Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() @@ -1719,7 +2596,11 @@ function CTLD:_BuildCrates(Group, Unit) if not foundbuilds then report:Add(" --- None Found ---") end report:Add("------------------------------------------------------------") local text = report:Text() - self:_SendMessage(text, 30, true, Group) + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end -- let\'s get going if canbuild then -- loop again @@ -1732,7 +2613,7 @@ function CTLD:_BuildCrates(Group, Unit) end end else - self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end @@ -1740,12 +2621,13 @@ end --- (Internal) Function to repair nearby vehicles / FOBs -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit -function CTLD:_RepairCrates(Group, Unit) +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true, this is an engineering role +function CTLD:_RepairCrates(Group, Unit, Engineering) self:T(self.lid .. " _RepairCrates") -- get nearby crates - local finddist = self.CrateDistance or 30 - local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local finddist = self.CrateDistance or 35 + local crates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -1753,7 +2635,7 @@ function CTLD:_RepairCrates(Group, Unit) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() and Crate:IsRepair() then + if Crate:WasDropped() and Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() @@ -1798,19 +2680,23 @@ function CTLD:_RepairCrates(Group, Unit) if not foundbuilds then report:Add(" --- None Found ---") end report:Add("------------------------------------------------------------") local text = report:Text() - self:_SendMessage(text, 30, true, Group) + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end -- let\'s get going if canbuild then -- loop again for _,_build in pairs(buildables) do local build = _build -- #CTLD.Buildable if build.CanBuild then - self:_RepairObjectFromCrates(Group,Unit,crates,build,number) + self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering) end end end else - self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end @@ -1821,41 +2707,54 @@ end -- @param Wrapper.Group#UNIT Unit -- @param #CTLD.Buildable Build -- @param #boolean Repair If true this is a repair and not a new build -function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair) +-- @param Core.Point#COORDINATE Coordinate Location for repair (e.g. where the destroyed unit was) +function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) self:T(self.lid .. " _BuildObjectFromCrates") -- Spawn-a-crate-content - local position = Unit:GetCoordinate() or Group:GetCoordinate() - local unitname = Unit:GetName() or Group:GetName() - local name = Build.Name - local type = Build.Type -- #CTLD_CARGO.Enum - local canmove = false - if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end - local temptable = Build.Template or {} - local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) - local randomcoord = zone:GetRandomCoordinate(35):GetVec2() - for _,_template in pairs(temptable) do - self.TroopCounter = self.TroopCounter + 1 - if canmove then - local alias = string.format("%s-%d", _template, math.random(1,100000)) - self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - :InitRandomizeUnits(true,20,2) - :InitDelayOff() - :SpawnFromVec2(randomcoord) - else -- don't random position of e.g. SAM units build as FOB - self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - --:InitRandomizeUnits(true,20,2) - :InitDelayOff() - :SpawnFromVec2(randomcoord) + if Group and Group:IsAlive() then + local position = Unit:GetCoordinate() or Group:GetCoordinate() + local unitname = Unit:GetName() or Group:GetName() + local name = Build.Name + local ctype = Build.Type -- #CTLD_CARGO.Enum + local canmove = false + if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end + if ctype == CTLD_CARGO.Enum.STATIC then + return self end - if self.movetroopstowpzone and canmove then - self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + local temptable = Build.Template or {} + if type(temptable) == "string" then + temptable = {temptable} end + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) + local randomcoord = zone:GetRandomCoordinate(35):GetVec2() if Repair then - self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) - else - self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + randomcoord = RepairLocation:GetVec2() end - end -- template loop + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + if canmove then + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + if Repair then + self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + else + self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + else + self:T(self.lid.."Group KIA while building!") + end return self end @@ -1909,8 +2808,9 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) if name == nametype then -- matching crate type table.insert(destIDs,thisID) found = found + 1 - nowcrate:GetPositionable():Destroy() + nowcrate:GetPositionable():Destroy(false) nowcrate.Positionable = nil + nowcrate.HasBeenDropped = false end if found == numberdest then break end -- got enough end @@ -1969,27 +2869,14 @@ function CTLD:_RefreshF10Menus() local cancrates = capabilities.crates -- top menu local topmenu = MENU_GROUP:New(_group,"CTLD",nil) - local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) + local invtry = MENU_GROUP_COMMAND:New(_group,"Inventory",topmenu, self._ListInventory, self, _group, _unit) + local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, false) local smokemenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, true):Refresh() -- sub menus - -- sub menu crates management - if cancrates then - local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) - local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) - for _,_entry in pairs(self.Cargo_Crates) do - local entry = _entry -- #CTLD_CARGO - menucount = menucount + 1 - local menutext = string.format("Get crate for %s",entry.Name) - menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) - end - listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) - local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) - local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) - local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() - end -- sub menu troops management if cantroops then local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) @@ -2001,7 +2888,27 @@ function CTLD:_RefreshF10Menus() local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end - local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) + -- sub menu crates management + if cancrates then + local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) + local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end + for _,_entry in pairs(self.Cargo_Statics) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end + listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) + local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) + local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) + local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() + end if unittype == "Hercules" then local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() else @@ -2023,11 +2930,14 @@ function CTLD:_RefreshF10Menus() -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop. -- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops) +-- @param #number PerTroopMass Mass in kg of each soldier +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) self:T(self.lid .. " AddTroopsCargo") + self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) table.insert(self.Cargo_Troops,cargo) return self end @@ -2038,26 +2948,46 @@ end -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. -- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. -- @param #number NoCrates Number of crates needed to build this cargo. -function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates) +-- @param #number PerCrateMass Mass in kg of each crate +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock) self:T(self.lid .. " AddCratesCargo") self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock) table.insert(self.Cargo_Crates,cargo) return self end +--- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo as set in the mission editor (not: UNIT name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddStaticsCargo(Name,Mass,Stock) + self:T(self.lid .. " AddStaticsCargo") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock) + table.insert(self.Cargo_Statics,cargo) + return self +end + --- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". -- @param #string Template Template of VEHICLE or FOB cargo that this can repair. -- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR. -- @param #number NoCrates Number of crates needed to build this cargo. -function CTLD:AddCratesRepair(Name,Template,Type,NoCrates) +-- @param #number PerCrateMass Mass in kg of each crate +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock) self:T(self.lid .. " AddCratesRepair") self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock) table.insert(self.Cargo_Crates,cargo) return self end @@ -2072,6 +3002,8 @@ function CTLD:AddZone(Zone) table.insert(self.pickupZones,zone) elseif zone.type == CTLD.CargoZoneType.DROP then table.insert(self.dropOffZones,zone) + elseif zone.type == CTLD.CargoZoneType.SHIP then + table.insert(self.shipZones,zone) else table.insert(self.wpZones,zone) end @@ -2097,6 +3029,8 @@ function CTLD:ActivateZone(Name,ZoneType,NewState) table = self.pickupZones elseif ZoneType == CTLD.CargoZoneType.DROP then table = self.dropOffZones + elseif ZoneType == CTLD.CargoZoneType.SHIP then + table = self.shipZones else table = self.wpZones end @@ -2194,8 +3128,10 @@ end -- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red -- @param #string Active Is this zone currently active? -- @param #string HasBeacon Does this zone have a beacon if it is active? +-- @param #number Shiplength Length of Ship for shipzones +-- @param #number Shipwidth Width of Ship for shipzones -- @return #CTLD self -function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon) +function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth) self:T(self.lid .. " AddCTLDZone") local ctldzone = {} -- #CTLD.CargoZone @@ -2215,6 +3151,11 @@ function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon) ctldzone.vhfbeacon = nil end + if Type == CTLD.CargoZoneType.SHIP then + ctldzone.shiplength = Shiplength or 100 + ctldzone.shipwidth = Shipwidth or 10 + end + self:AddZone(ctldzone) return self end @@ -2227,8 +3168,8 @@ function CTLD:_ListRadioBeacons(Group, Unit) self:T(self.lid .. " _ListRadioBeacons") local report = REPORT:New("Active Zone Beacons") report:Add("------------------------------------------------------------") - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} - for i=1,3 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone @@ -2258,9 +3199,15 @@ end -- @param #string Sound Name of soundfile. -- @param #number Mhz Frequency in Mhz. -- @param #number Modulation Modulation AM or FM. -function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation) +-- @param #boolean IsShip If true zone is a ship. +function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip) self:T(self.lid .. " _AddRadioBeacon") - local Zone = ZONE:FindByName(Name) + local Zone = nil + if IsShip then + Zone = UNIT:FindByName(Name) + else + Zone = ZONE:FindByName(Name) + end local Sound = Sound or "beacon.ogg" if Zone then local ZoneCoord = Zone:GetCoordinate() @@ -2277,8 +3224,10 @@ end function CTLD:_RefreshRadioBeacons() self:T(self.lid .. " _RefreshRadioBeacons") - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} - for i=1,3 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do + local IsShip = false + if i == 4 then IsShip = true end for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone @@ -2291,9 +3240,9 @@ function CTLD:_RefreshRadioBeacons() local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency -- KHz local UHF = UHFbeacon.frequency -- MHz - self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM) - self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM) - self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM) + self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM, IsShip) + self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM, IsShip) + self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM, IsShip) end end end @@ -2306,10 +3255,12 @@ end -- @param #CTLD.CargoZoneType Zonetype Zonetype -- @return #boolean Outcome Is in zone or not -- @return #string name Closest zone name --- @return #string zone Closest Core.Zone#ZONE object +-- @return Core.Zone#ZONE zone Closest Core.Zone#ZONE object -- @return #number distance Distance to closest zone +-- @return #number width Radius of zone or width of ship function CTLD:IsUnitInZone(Unit,Zonetype) self:T(self.lid .. " IsUnitInZone") + self:T(Zonetype) local unitname = Unit:GetName() local zonetable = {} local outcome = false @@ -2317,6 +3268,8 @@ function CTLD:IsUnitInZone(Unit,Zonetype) zonetable = self.pickupZones -- #table elseif Zonetype == CTLD.CargoZoneType.DROP then zonetable = self.dropOffZones -- #table + elseif Zonetype == CTLD.CargoZoneType.SHIP then + zonetable = self.shipZones -- #table else zonetable = self.wpZones -- #table end @@ -2325,16 +3278,29 @@ function CTLD:IsUnitInZone(Unit,Zonetype) local colorret = nil local maxdist = 1000000 -- 100km local zoneret = nil + local zonewret = nil local zonenameret = nil for _,_cargozone in pairs(zonetable) do local czone = _cargozone -- #CTLD.CargoZone local unitcoord = Unit:GetCoordinate() local zonename = czone.name - local zone = ZONE:FindByName(zonename) - zonecoord = zone:GetCoordinate() local active = czone.active local color = czone.color - local zoneradius = zone:GetRadius() + local zone = nil + local zoneradius = 100 + local zonewidth = 20 + if Zonetype == CTLD.CargoZoneType.SHIP then + self:T("Checking Type Ship: "..zonename) + zone = UNIT:FindByName(zonename) + zonecoord = zone:GetCoordinate() + zoneradius = czone.shiplength + zonewidth = czone.shipwidth + else + zone = ZONE:FindByName(zonename) + zonecoord = zone:GetCoordinate() + zoneradius = zone:GetRadius() + zonewidth = zoneradius + end local distance = self:_GetDistance(zonecoord,unitcoord) if distance <= zoneradius and active then outcome = true @@ -2343,10 +3309,15 @@ function CTLD:IsUnitInZone(Unit,Zonetype) maxdist = distance zoneret = zone zonenameret = zonename + zonewret = zonewidth colorret = color end end - return outcome, zonenameret, zoneret, maxdist + if Zonetype == CTLD.CargoZoneType.SHIP then + return outcome, zonenameret, zoneret, maxdist, zonewret + else + return outcome, zonenameret, zoneret, maxdist + end end --- User function - Start smoke in a zone close to the Unit. @@ -2360,12 +3331,17 @@ function CTLD:SmokeZoneNearBy(Unit, Flare) local Group = Unit:GetGroup() local smokedistance = self.smokedistance local smoked = false - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} - for i=1,3 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do for index,cargozone in pairs(zones[i]) do local CZone = cargozone --#CTLD.CargoZone local zonename = CZone.name - local zone = ZONE:FindByName(zonename) + local zone = nil + if i == 4 then + zone = UNIT:FindByName(zonename) + else + zone = ZONE:FindByName(zonename) + end local zonecoord = zone:GetCoordinate() local active = CZone.active local color = CZone.color @@ -2395,11 +3371,12 @@ end --- User - Function to add/adjust unittype capabilities. -- @param #CTLD self -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. - -- @param #boolean Cancrates Unit can load crates. - -- @param #boolean Cantroops Unit can load troops. - -- @param #number Cratelimit Unit can carry number of crates. - -- @param #number Trooplimit Unit can carry number of troops. - function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit) + -- @param #boolean Cancrates Unit can load crates. Default false. + -- @param #boolean Cantroops Unit can load troops. Default false. + -- @param #number Cratelimit Unit can carry number of crates. Default 0. + -- @param #number Trooplimit Unit can carry number of troops. Default 0. + -- @param #number Length Unit lenght (in mteres) for the load radius. Default 20. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length) self:T(self.lid .. " UnitCapabilities") local unittype = nil local unit = nil @@ -2418,6 +3395,7 @@ end capabilities.troops = Cantroops or false capabilities.cratelimit = Cratelimit or 0 capabilities.trooplimit = Trooplimit or 0 + capabilities.length = Length or 20 self.UnitTypes[unittype] = capabilities return self end @@ -2529,6 +3507,9 @@ end self:T(self.lid .. " CanHoverLoad") if self:IsHercules(Unit) then return false end local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit) + if not outcome then + outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) --and self:IsCorrectHover(Unit) + end return outcome end @@ -2590,7 +3571,7 @@ end if self.hoverautoloading then for _,_pilot in pairs (self.CtldUnits) do local Unit = UNIT:FindByName(_pilot) - self:AutoHoverLoad(Unit) + if self:CanHoverLoad(Unit) then self:AutoHoverLoad(Unit) end end end return self @@ -2600,16 +3581,276 @@ end -- @param #CTLD self -- @return #CTLD self function CTLD:CleanDroppedTroops() + -- Troops local troops = self.DroppedTroops local newtable = {} for _index, _group in pairs (troops) do - if _group and _group:IsAlive() then - newtable[_index] = _group + self:T({_group.ClassName}) + if _group and _group.ClassName == "GROUP" then + if _group:IsAlive() then + newtable[_index] = _group + end end end self.DroppedTroops = newtable + -- Engineers + local engineers = self.EngineersInField + local engtable = {} + for _index, _group in pairs (engineers) do + self:T({_group.ClassName}) + if _group and _group:IsNotStatus("Stopped") then + engtable[_index] = _group + end + end + self.EngineersInField = engtable return self end + + --- User - function to add stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to add stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to remove stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + end + + --- User - function to remove stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + return self + end + + --- (Internal) Check on engineering teams + -- @param #CTLD self + -- @return #CTLD self + function CTLD:_CheckEngineers() + self:T(self.lid.." CheckEngineers") + local engtable = self.EngineersInField + for _ind,_engineers in pairs (engtable) do + local engineers = _engineers -- #CTLD_ENGINEERING + local wrenches = engineers.Group -- Wrapper.Group#GROUP + self:T(_engineers.lid .. _engineers:GetStatus()) + if wrenches and wrenches:IsAlive() then + if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then + local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch) -- #table + engineers:Search(crates,number) + elseif engineers:IsStatus("Moving") then + engineers:Move() + elseif engineers:IsStatus("Arrived") then + engineers:Build() + local unit = wrenches:GetUnit(1) + self:_BuildCrates(wrenches,unit,true) + self:_RepairCrates(wrenches,unit,true) + engineers:Done() + end + else + engineers:Stop() + end + end + return self + end + + --- (User) Pre-populate troops in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectTroops(dropzone,InjectTroopsType) + function CTLD:InjectTroops(Zone,Cargo) + self:T(self.lid.." InjectTroops") + local cargo = Cargo -- #CTLD_CARGO + + local function IsTroopsMatch(cargo) + local match = false + local cgotbl = self.Cargo_Troops + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsTroopsMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Troops,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + -- engineering group? + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + --self:I(string.format("%s Injected Engineers %s into action!",self.lid, name)) + else + --self:I(string.format("%s Injected Troops %s into action!",self.lid, name)) + end + if self.eventoninject then + self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter]) + end + end -- if type end + return self + end + + --- (User) Pre-populate vehicles in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectVehicles(dropzone,InjectVehicleType) + function CTLD:InjectVehicles(Zone,Cargo) + self:T(self.lid.." InjectVehicles") + local cargo = Cargo -- #CTLD_CARGO + + local function IsVehicMatch(cargo) + local match = false + local cgotbl = self.Cargo_Crates + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsVehicMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Crates,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + cargo:SetWasDropped(true) + local canmove = false + if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + if canmove then + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + if self.eventoninject then + self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter]) + end + end -- end loop + end -- if type end + return self + end + ------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------- @@ -2622,7 +3863,7 @@ end -- @return #CTLD self function CTLD:onafterStart(From, Event, To) self:T({From, Event, To}) - self:I(self.lid .. "Started.") + self:I(self.lid .. "Started ("..self.version..")") if self.useprefix or self.enableHercules then local prefix = self.prefixes if self.enableHercules then @@ -2638,6 +3879,14 @@ end self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) self:__Status(-5) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end return self end @@ -2653,6 +3902,7 @@ end self:_RefreshF10Menus() self:_RefreshRadioBeacons() self:CheckAutoHoverload() + self:_CheckEngineers() return self end @@ -2682,7 +3932,25 @@ end if self.debug or self.verbose > 0 then local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) - local m = MESSAGE:New(text,10,"CTLD"):ToAll() + local m = MESSAGE:New(text,10,"CTLD"):ToAll() + if self.verbose > 0 then + self:I(self.lid.."Cargo and Troops in Stock:") + for _,_troop in pairs (self.Cargo_Crates) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Statics) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Troops) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t %d", name, stock)) + end + end end self:__Status(-30) return self @@ -2800,6 +4068,348 @@ end return self end + --- On before "Save" event. Checks if io and lfs are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". + function CTLD:onbeforeSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true + end + + --- On after "Save" event. Player data is saved to file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. + -- @param #string filename (Optional) File name for saving. Default is Default is "CTLD__Persist.csv". + function CTLD:onafterSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + -- Thanks to @FunkyFranky + if not self.enableLoadSave then + return self + end + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=self.filepath or lfs.writedir() + end + + -- Set file name. + filename=filename or self.filename + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + local grouptable = self.DroppedTroops -- #table + local cgovehic = self.Cargo_Crates + local cgotable = self.Cargo_Troops + local stcstable = self.Spawned_Cargo + + local statics = nil + local statics = {} + self:I(self.lid.."Bulding Statics Table for Saving") + for _,_cargo in pairs (stcstable) do + local cargo = _cargo -- #CTLD_CARGO + local object = cargo:GetPositionable() -- Wrapper.Static#STATIC + if object and object:IsAlive() and cargo:WasDropped() then + self:I({_cargo}) + statics[#statics+1] = cargo + end + end + + -- find matching cargo + local function FindCargoType(name,table) + -- name matching a template in the table + local match = false + local cargo = nil + for _ind,_cargo in pairs (table) do + local thiscargo = _cargo -- #CTLD_CARGO + local template = thiscargo:GetTemplates() + if type(template) == "string" then + template = { template } + end + for _,_name in pairs (template) do + --self:I(string.format("*** Saving CTLD: Matching %s with %s",name,_name)) + if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then + match = true + cargo = thiscargo + end + end + if match then break end + end + return match, cargo + end + + + --local data = "LoadedData = {\n" + local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass\n" + local n = 0 + for _,_grp in pairs(grouptable) do + local group = _grp -- Wrapper.Group#GROUP + if group and group:IsAlive() then + -- get template name + local name = group:GetName() + local template = string.gsub(name,"-(.+)$","") + if string.find(template,"#") then + template = string.gsub(name,"#(%d+)$","") + end + + local match, cargo = FindCargoType(template,cgotable) + if not match then + match, cargo = FindCargoType(template,cgovehic) + end + if match then + n = n + 1 + local cargo = cargo -- #CTLD_CARGO + local cgoname = cargo.Name + local cgotemp = cargo.Templates + local cgotype = cargo.CargoType + local cgoneed = cargo.CratesNeeded + local cgomass = cargo.PerCrateMass + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local location = group:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + end + end + + for _,_cgo in pairs(statics) do + local object = _cgo -- #CTLD_CARGO + local cgoname = object.Name + local cgotemp = object.Templates + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local cgotype = object.CargoType + local cgoneed = object.CratesNeeded + local cgomass = object.PerCrateMass + local crateobj = object.Positionable + local location = crateobj:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + + _savefile(filename, data) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self + end + + --- On before "Load" event. Checks if io and lfs and the file are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + function CTLD:onbeforeLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) + return false + --return self + end + + end + + --- On after "Load" event. Loads dropped units from file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + function CTLD:onafterLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that loads data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading CTLD state from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + local file=assert(io.open(filename, "rb")) + + local loadeddata = {} + for line in file:lines() do + --self:I({line=type(line)}) + loadeddata[#loadeddata+1] = line + end + file:close() + + -- remove header + table.remove(loadeddata, 1) + + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass + local groupname = dataset[1] + local vec2 = {} + vec2.x = tonumber(dataset[2]) + vec2.y = tonumber(dataset[4]) + local cargoname = dataset[5] + local cargotype = dataset[7] + if type(groupname) == "string" and groupname ~= "STATIC" then + local cargotemplates = dataset[6] + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + --self:I({groupname,vec3,cargoname,cargotemplates,cargotype,size,mass}) + -- inject at Vec2 + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectVehicles(dropzone,injectvehicle) + elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then + local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectTroops(dropzone,injecttroops) + end + elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then + local cargotemplates = dataset[6] + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + -- STATIC,-84037,154,834021,Humvee,{Humvee;},Vehicle,1,100 + -- STATIC,-84036,154,834018,Ammunition-1,ammo_cargo,Static,1,500 + local injectstatic = nil + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + end + if injectstatic then + self:InjectStatics(dropzone,injectstatic) + end + end + end + + return self + end end -- end do ------------------------------------------------------------------- -- End Ops.CTLD.lua diff --git a/Moose Development/Moose/Sound/RadioSpeech.lua b/Moose Development/Moose/Sound/RadioSpeech.lua index f77c446a2..ae951053f 100644 --- a/Moose Development/Moose/Sound/RadioSpeech.lua +++ b/Moose Development/Moose/Sound/RadioSpeech.lua @@ -1,9 +1,9 @@ --- **Core** - Makes the radio talk. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Send text strings using a vocabulary that is converted in spoken language. -- * Possiblity to implement multiple language. -- @@ -15,10 +15,10 @@ -- @image Core_Radio.JPG --- Makes the radio speak. --- +-- -- # RADIOSPEECH usage --- --- +-- +-- -- @type RADIOSPEECH -- @extends Core.RadioQueue#RADIOQUEUE RADIOSPEECH = { @@ -59,24 +59,24 @@ RADIOSPEECH.Vocabulary.EN = { ["70"] = { "70", 0.48 }, ["80"] = { "80", 0.26 }, ["90"] = { "90", 0.36 }, - ["100"] = { "100", 0.55 }, - ["200"] = { "200", 0.55 }, - ["300"] = { "300", 0.61 }, - ["400"] = { "400", 0.60 }, - ["500"] = { "500", 0.61 }, - ["600"] = { "600", 0.65 }, - ["700"] = { "700", 0.70 }, - ["800"] = { "800", 0.54 }, - ["900"] = { "900", 0.60 }, - ["1000"] = { "1000", 0.60 }, - ["2000"] = { "2000", 0.61 }, - ["3000"] = { "3000", 0.64 }, - ["4000"] = { "4000", 0.62 }, - ["5000"] = { "5000", 0.69 }, - ["6000"] = { "6000", 0.69 }, - ["7000"] = { "7000", 0.75 }, - ["8000"] = { "8000", 0.59 }, - ["9000"] = { "9000", 0.65 }, + ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, ["chevy"] = { "chevy", 0.35 }, ["colt"] = { "colt", 0.35 }, @@ -94,10 +94,10 @@ RADIOSPEECH.Vocabulary.EN = { ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, ["feet"] = { "feet", 0.29 }, - + ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - + ["returning to base"] = { "returning_to_base", 0.85 }, ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, @@ -143,52 +143,52 @@ RADIOSPEECH.Vocabulary.RU = { ["70"] = { "70", 0.68 }, ["80"] = { "80", 0.84 }, ["90"] = { "90", 0.71 }, - ["100"] = { "100", 0.35 }, - ["200"] = { "200", 0.59 }, - ["300"] = { "300", 0.53 }, - ["400"] = { "400", 0.70 }, - ["500"] = { "500", 0.50 }, - ["600"] = { "600", 0.58 }, - ["700"] = { "700", 0.64 }, - ["800"] = { "800", 0.77 }, - ["900"] = { "900", 0.75 }, - ["1000"] = { "1000", 0.87 }, - ["2000"] = { "2000", 0.83 }, - ["3000"] = { "3000", 0.84 }, - ["4000"] = { "4000", 1.00 }, - ["5000"] = { "5000", 0.77 }, - ["6000"] = { "6000", 0.90 }, - ["7000"] = { "7000", 0.77 }, - ["8000"] = { "8000", 0.92 }, - ["9000"] = { "9000", 0.87 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, - ["Ñ�тепени"] = { "degrees", 0.5 }, - ["километров"] = { "kilometers", 0.65 }, + ["градусы"] = { "degrees", 0.5 }, + ["километры"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, - ["миль"] = { "miles", 0.45 }, + ["мили"] = { "miles", 0.45 }, ["mi"] = { "miles", 0.45 }, - ["метры"] = { "meters", 0.41 }, + ["метров"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, - ["ноги"] = { "feet", 0.37 }, - + ["ноги"] = { "feet", 0.37 }, + ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - - ["возвращаÑ�Ñ�ÑŒ на базу"] = { "returning_to_base", 1.40 }, - ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, - ["перехват Ñ�амолетов"] = { "intercepting_bogeys", 1.22 }, - ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, - ["захватывающие Ñ�амолеты"] = { "engaging_bogeys", 1.68 }, - ["колеÑ�а вверх"] = { "wheels_up", 0.92 }, - ["поÑ�адка на базу"] = { "landing at base", 1.04 }, - ["патрулирующий"] = { "patrolling", 0.96 }, - ["за"] = { "for", 0.27 }, - ["и"] = { "and", 0.17 }, - ["в"] = { "at", 0.19 }, - ["dot"] = { "dot", 0.51 }, - ["defender"] = { "defender", 0.45 }, + ["возвращение на базу"] = { "returning_to_base", 1.40 }, + ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, + ["перехват боги"] = { "intercepting_bogeys", 1.22 }, + ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, + ["привлечение болотных птиц"] = { "engaging_bogeys", 1.68 }, + ["колёса вверх..."] = { "wheels_up", 0.92 }, + ["посадка на базу"] = { "landing at base", 1.04 }, + ["патрулирование"] = { "patrolling", 0.96 }, + + ["для"] = { "for", 0.27 }, + ["и"] = { "and", 0.17 }, + ["на сайте"] = { "at", 0.19 }, + ["точка"] = { "dot", 0.51 }, + ["защитник"] = { "defender", 0.45 }, } --- Create a new RADIOSPEECH object for a given radio frequency/modulation. @@ -200,11 +200,11 @@ function RADIOSPEECH:New(frequency, modulation) -- Inherit base local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH - + self.Language = "EN" - + self:BuildTree() - + return self end @@ -262,7 +262,7 @@ end function RADIOSPEECH:BuildTree() self.Speech = {} - + for Language, Sentences in pairs( self.Vocabulary ) do self:I( { Language = Language, Sentences = Sentences }) self.Speech[Language] = {} @@ -271,7 +271,7 @@ function RADIOSPEECH:BuildTree() self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) end end - + self:I( { Speech = self.Speech } ) return self @@ -290,7 +290,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) - + if Word then if Word ~= "" and tonumber(Word) == nil then @@ -302,7 +302,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) if Speech[Word].Next == nil then self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) - else + else if RemainderSentence and RemainderSentence ~= "" then return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) end @@ -310,11 +310,11 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" - end - + end + end --- Speak a sentence. @@ -333,7 +333,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) if Digits then if Digits ~= "" and tonumber( Digits ) ~= nil then - + -- Construct numbers local Number = tonumber( Digits ) local Multiple = nil @@ -357,7 +357,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" end @@ -374,26 +374,26 @@ function RADIOSPEECH:Speak( Sentence, Language ) self:I( { Sentence, Language } ) local Language = Language or "EN" - + self:I( { Language = Language } ) - + -- If there is no node for Speech, then we start at the first nodes of the language. local Speech = self.Speech[Language] - + self:I( { Speech = Speech, Language = Language } ) - + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) - + repeat Sentence = self:SpeakWords( Sentence, Speech, Language ) - + self:I( { Sentence = Sentence } ) Sentence = self:SpeakDigits( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) - + -- Sentence = self:SpeakSymbols( Sentence, Speech ) -- -- self:I( { Sentence = Sentence } ) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 9829b9ac1..0e25e3751 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -507,7 +507,7 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end - -- 024� 23' 12"N or 024� 23' 12.03"N + -- 024° 23' 12"N or 024° 23' 12.03"N return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' ' .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi @@ -767,12 +767,12 @@ end function UTILS.GetCharacters(str) local chars={} - + for i=1,#str do local c=str:sub(i,i) table.insert(chars, c) end - + return chars end @@ -923,7 +923,7 @@ function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) local x1=math.random() local x2=math.random() - -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°). r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 i=i+1 @@ -1441,7 +1441,7 @@ function UTILS.GMTToLocalTimeDifference() elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours elseif theatre==DCSMAP.MarianaIslands then - return 10 -- Guam is UTC+10 hours. + return 10 -- Guam is UTC+10 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 @@ -1615,7 +1615,7 @@ end --@return #table function UTILS.ShuffleTable(t) if t == nil or type(t) ~= "table" then - BASE:I("Error in ShuffleTable: Missing or wrong tyåe of Argument") + BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") return end math.random() @@ -1639,17 +1639,17 @@ function UTILS.IsLoadingDoorOpen( unit_name ) local unit = Unit.getByName(unit_name) if unit ~= nil then local type_name = unit:getTypeName() - - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then BASE:T(unit_name .. " Cargo doors are open or cargo door not present") ret_val = true end - + if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then BASE:T(unit_name .. " a side door is open") ret_val = true end - + if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then BASE:T(unit_name .. " a side door is open ") ret_val = true @@ -1664,9 +1664,9 @@ function UTILS.IsLoadingDoorOpen( unit_name ) BASE:T(unit_name .. " all doors are closed") end return ret_val - + end -- nil - + return nil end @@ -1705,13 +1705,13 @@ function UTILS.GenerateVHFrequencies() 905,907,920,935,942,950,995, 1000,1025,1030,1050,1065,1116,1175,1182,1210 } - + local FreeVHFFrequencies = {} - + -- first range local _start = 200000 while _start < 400000 do - + -- skip existing NDB frequencies# local _found = false for _, value in pairs(_skipFrequencies) do @@ -1725,7 +1725,7 @@ function UTILS.GenerateVHFrequencies() end _start = _start + 10000 end - + -- second range _start = 400000 while _start < 850000 do @@ -1742,7 +1742,7 @@ function UTILS.GenerateVHFrequencies() end _start = _start + 10000 end - + -- third range _start = 850000 while _start <= 999000 do -- adjusted for Gazelle @@ -1782,7 +1782,7 @@ end -- @return #table Laser Codes. function UTILS.GenerateLaserCodes() local jtacGeneratedLaserCodes = {} - + -- helper function local function ContainsDigit(_number, _numberToFind) local _thisNumber = _number @@ -1796,14 +1796,14 @@ function UTILS.GenerateLaserCodes() end return false end - + -- generate list of laser codes local _code = 1111 local _count = 1 while _code < 1777 and _count < 30 do while true do _code = _code + 1 - if not self:_ContainsDigit(_code, 8) + if not ContainsDigit(_code, 8) and not ContainsDigit(_code, 9) and not ContainsDigit(_code, 0) then table.insert(jtacGeneratedLaserCodes, _code) @@ -1813,4 +1813,4 @@ function UTILS.GenerateLaserCodes() _count = _count + 1 end return jtacGeneratedLaserCodes -end \ No newline at end of file +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 8db1eacf0..2b9198330 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -142,7 +142,7 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Pahute_Mesa_Airstrip -- * AIRBASE.Nevada.Tonopah_Airport -- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield --- +-- -- @field Nevada AIRBASE.Nevada = { ["Creech_AFB"] = "Creech AFB", @@ -197,7 +197,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere -- * AIRBASE.Normandy.Ford_AF --- +-- -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -271,7 +271,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sirri_Island -- * AIRBASE.PersianGulf.Tunb_Island_AFB -- * AIRBASE.PersianGulf.Tunb_Kochak --- +-- -- @field PersianGulf AIRBASE.PersianGulf = { ["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl", @@ -554,7 +554,7 @@ function AIRBASE:Register(AirbaseName) -- Get descriptors. self.descriptors=self:GetDesc() - + -- Debug info. --self:I({airbase=AirbaseName, descriptors=self.descriptors}) @@ -1134,7 +1134,7 @@ function AIRBASE:MarkParkingSpots(termtype, mark) -- Get airbase name. local airbasename=self:GetName() - self:E(string.format("Parking spots at %s for termial type %s:", airbasename, tostring(termtype))) + self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype))) for _,_spot in pairs(parkingdata) do @@ -1211,14 +1211,25 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) -- Get the aircraft size, i.e. it's longest side of x,z. - local aircraft=group:GetUnit(1) - local _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + local aircraft = nil + local _aircraftsize, ax,ay,az + if group and group.ClassName == "GROUP" then + aircraft=group:GetUnit(1) + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + else + -- SU27 dimensions + _aircraftsize = 23 + ax = 23 -- length + ay = 7 -- height + az = 17 -- width + end + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() -- Debug info. - self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at termial type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) + self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) -- Table of valid spots. local validspots={} @@ -1341,6 +1352,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Retrun spots we found, even if there were not enough. return validspots + end --- Check black and white lists. @@ -1359,7 +1371,7 @@ function AIRBASE:_CheckParkingLists(TerminalID) end end - + -- Check if a whitelist was defined. if self.parkingWhitelist and #self.parkingWhitelist>0 then for _,terminalID in pairs(self.parkingWhitelist or {}) do @@ -1469,7 +1481,7 @@ function AIRBASE:GetRunwayData(magvar, mark) name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or name==AIRBASE.PersianGulf.Dubai_Intl or name==AIRBASE.PersianGulf.Shiraz_International_Airport or - name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport or name==AIRBASE.MarianaIslands.Andersen_AFB then -- 1-->4, 2-->3, 3-->2, 4-->1 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 397832e85..428b931a2 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1851,27 +1851,27 @@ do -- Patrol methods -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() - + -- test for submarine local depth = 0 local IsSub = false if PatrolGroup:IsShip() then local navalvec3 = FromCoord:GetVec3() - if navalvec3.y < 0 then + if navalvec3.y < 0 then depth = navalvec3.y IsSub = true end end - - + + local Waypoint = Waypoints[1] local Speed = Waypoint.speed or (20 / 3.6) local From = FromCoord:WaypointGround( Speed ) - - if IsSub then + + if IsSub then From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) end - + table.insert( Waypoints, 1, From ) local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) @@ -1916,7 +1916,7 @@ do -- Patrol methods local IsSub = false if PatrolGroup:IsShip() then local navalvec3 = FromCoord:GetVec3() - if navalvec3.y < 0 then + if navalvec3.y < 0 then depth = navalvec3.y IsSub = true end @@ -1982,16 +1982,16 @@ do -- Patrol methods self:F( { PatrolGroup = PatrolGroup:GetName() } ) if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() - + -- test for submarine local depth = 0 local IsSub = false if PatrolGroup:IsShip() then local navalvec3 = FromCoord:GetVec3() - if navalvec3.y < 0 then + if navalvec3.y < 0 then depth = navalvec3.y IsSub = true end @@ -3773,12 +3773,13 @@ end --- (GROUND) Relocate controllable to a random point within a given radius; use e.g.for evasive actions; Note that not all ground controllables can actually drive, also the alarm state of the controllable might stop it from moving. -- @param #CONTROLLABLE self --- @param #number speed Speed of the controllable, default 20 --- @param #number radius Radius of the relocation zone, default 500 --- @param #boolean onroad If true, route on road (less problems with AI way finding), default true --- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false +-- @param #number speed Speed of the controllable, default 20 +-- @param #number radius Radius of the relocation zone, default 500 +-- @param #boolean onroad If true, route on road (less problems with AI way finding), default true +-- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false +-- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road" -- @return #CONTROLLABLE self -function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortcut) +function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortcut, formation) self:F2( { self.ControllableName } ) local _coord = self:GetCoordinate() @@ -3789,14 +3790,14 @@ function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortc local _grptsk = {} local _candoroad = false local _shortcut = shortcut or false + local _formation = formation or "Off Road" -- create a DCS Task an push it on the group - -- TaskGroundOnRoad(ToCoordinate,Speed,OffRoadFormation,Shortcut,FromCoordinate,WaypointFunction,WaypointFunctionArguments) if onroad then - _grptsk, _candoroad = self:TaskGroundOnRoad(_tocoord,_speed,"Off Road",_shortcut) + _grptsk, _candoroad = self:TaskGroundOnRoad(_tocoord,_speed,_formation,_shortcut) self:Route(_grptsk,5) else - self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,"Off Road") + self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,_formation) end return self diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 8eb360206..ea63c7da0 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1132,7 +1132,7 @@ end -- @return #number Number of shells left. -- @return #number Number of rockets left. -- @return #number Number of bombs left. --- @return #number Number of missiles left. +-- @return #number Number of missiles left. function GROUP:GetAmmunition() self:F( self.ControllableName ) @@ -1142,6 +1142,7 @@ function GROUP:GetAmmunition() local Nshells=0 local Nrockets=0 local Nmissiles=0 + local Nbombs=0 if DCSControllable then @@ -1150,18 +1151,19 @@ function GROUP:GetAmmunition() local Unit = UnitData -- Wrapper.Unit#UNIT -- Get ammo of the unit - local ntot, nshells, nrockets, nmissiles = Unit:GetAmmunition() + local ntot, nshells, nrockets, nbombs, nmissiles = Unit:GetAmmunition() Ntot=Ntot+ntot Nshells=Nshells+nshells Nrockets=Nrockets+nrockets - Nmissiles=Nmissiles+nmissiles + Nmissiles=Nmissiles+nmissiles + Nbombs=Nbombs+nbombs end end - return Ntot, Nshells, Nrockets, Nmissiles + return Ntot, Nshells, Nrockets, Nbombs, Nmissiles end @@ -2589,6 +2591,17 @@ function GROUP:SetCommandImmortal(switch) return self end +--- Get skill from Group. Effectively gets the skill from Unit 1 as the group holds no skill value. +-- @param #GROUP self +-- @return #string Skill String of skill name. +function GROUP:GetSkill() + self:F2( self.GroupName ) + local unit = self:GetUnit(1) + local name = unit:GetName() + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end + --do -- Smoke -- ----- Signal a flare at the position of the GROUP. diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 6589193df..bc0a82dd4 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1517,6 +1517,7 @@ do -- Cargo ["Ural-4320 APA-5D"] = 10, ["Ural-4320T"] = 14, ["ZBD04A"] = 7, -- new by kappa + ["VAB_Mephisto"] = 8, -- new by Apple } local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95 diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 7c32bbb51..19664867a 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -637,6 +637,18 @@ function UNIT:GetAmmo() return nil end +--- Sets the Unit's Internal Cargo Mass, in kg +-- @param #UNIT self +-- @param #number mass to set cargo to +-- @return #UNIT self +function UNIT:SetUnitInternalCargo(mass) + local DCSUnit = self:GetDCSObject() + if DCSUnit then + trigger.action.setUnitInternalCargo(DCSUnit:getName(), mass) + end + return self +end + --- Get the number of ammunition and in particular the number of shells, rockets, bombs and missiles a unit currently has. -- @param #UNIT self -- @return #number Total amount of ammo the unit has left. This is the sum of shells, rockets, bombs and missiles. @@ -1471,3 +1483,13 @@ function UNIT:EnableEmission(switch) return self end + +--- Get skill from Unit. +-- @param #UNIT self +-- @return #string Skill String of skill name. +function UNIT:GetSkill() + self:F2( self.UnitName ) + local name = self.UnitName + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end