RotorOps/scripts/CTLD.lua
2025-02-01 12:59:00 -08:00

6950 lines
246 KiB
Lua

--[[
Combat Troop and Logistics Drop
Allows Huey, Mi-8 and C130 to transport troops internally and Helicopters to transport Logistic / Vehicle units to the field via sling-loads
without requiring external mods.
Supports all of the original CTTS functionality such as AI auto troop load and unload as well as group spawning and preloading of troops into units.
Supports deployment of Auto Lasing JTAC to the field
See https://github.com/ciribob/DCS-CTLD for a user manual and the latest version
Contributors:
- Steggles - https://github.com/Bob7heBuilder
- mvee - https://github.com/mvee
- jmontleon - https://github.com/jmontleon
- emilianomolina - https://github.com/emilianomolina
- davidp57 - https://github.com/veaf
- Queton1-1 - https://github.com/Queton1-1
- Proxy404 - https://github.com/Proxy404
- GRIMM - https://github.com/spencershepard
]]
ctld = {} -- DONT REMOVE!
--- Identifier. All output in DCS.log will start with this.
ctld.Id = "CTLD - "
--- Version.
ctld.Version = "2025-02-01-GRIMM"
--https://github.com/spencershepard/DCS-CTLD/tree/rotorops
-- To add debugging messages to dcs.log, change the following log levels to `true`; `Debug` is less detailed than `Trace`
ctld.Debug = false
ctld.Trace = false
ctld.alreadyInitialized = false -- if true, ctld.initialize() will not run
-- ************************************************************************
-- ********************* USER CONFIGURATION ******************************
-- ************************************************************************
ctld.staticBugWorkaround = false -- DCS had a bug where destroying statics would cause a crash. If this happens again, set this to TRUE
ctld.disableAllSmoke = false -- if true, all smoke is diabled at pickup and drop off zones regardless of settings below. Leave false to respect settings below
ctld.hoverPickup = true -- if set to false you can load crates with the F10 menu instead of hovering... Only if not using real crates!
ctld.loadCrateFromMenu = false -- if set to true, you can load crates with the F10 menu OR hovering, in case of using choppers and planes for example.
ctld.enableCrates = true -- if false, Helis will not be able to spawn or unpack crates so will be normal CTTS
ctld.slingLoad = false -- if false, crates can be used WITHOUT slingloading, by hovering above the crate, simulating slingloading but not the weight...
-- There are some bug with Sling-loading that can cause crashes, if these occur set slingLoad to false
-- to use the other method.
-- Set staticBugFix to FALSE if use set ctld.slingLoad to TRUE
ctld.enableSmokeDrop = true -- if false, helis and c-130 will not be able to drop smoke
ctld.maxExtractDistance = 125 -- max distance from vehicle to troops to allow a group extraction
ctld.maximumDistanceLogistic = 200 -- max distance from vehicle to logistics to allow a loading or spawning operation
ctld.maximumSearchDistance = 4000 -- max distance for troops to search for enemy
ctld.maximumMoveDistance = 2000 -- max distance for troops to move from drop point if no enemy is nearby
ctld.minimumDeployDistance = 1000 -- minimum distance from a friendly pickup zone where you can deploy a crate
ctld.numberOfTroops = 10 -- default number of troops to load on a transport heli or C-130
-- also works as maximum size of group that'll fit into a helicopter unless overridden
ctld.enableFastRopeInsertion = true -- allows you to drop troops by fast rope
ctld.fastRopeMaximumHeight = 18.28 -- in meters which is 60 ft max fast rope (not rappell) safe height
ctld.vehiclesForTransportRED = { "BRDM-2", "BTR_D" } -- vehicles to load onto Il-76 - Alternatives {"Strela-1 9P31","BMP-1"}
ctld.vehiclesForTransportBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } -- vehicles to load onto c130 - Alternatives {"M1128 Stryker MGS","M1097 Avenger"}
ctld.vehiclesWeight = {
["BRDM-2"] = 7000,
["BTR_D"] = 8000,
["M1045 HMMWV TOW"] = 3220,
["M1043 HMMWV Armament"] = 2500
}
ctld.aaLaunchers = 3 -- controls how many launchers to add to the kub/buk when its spawned.
ctld.hawkLaunchers = 8 -- controls how many launchers to add to the hawk when its spawned.
ctld.spawnRPGWithCoalition = true --spawns a friendly RPG unit with Coalition forces
ctld.spawnStinger = false -- spawns a stinger / igla soldier with a group of 6 or more soldiers!
ctld.enabledFOBBuilding = true -- if true, you can load a crate INTO a C-130 than when unpacked creates a Forward Operating Base (FOB) which is a new place to spawn (crates) and carry crates from
-- In future i'd like it to be a FARP but so far that seems impossible...
-- You can also enable troop Pickup at FOBS
ctld.cratesRequiredForFOB = 3 -- The amount of crates required to build a FOB. Once built, helis can spawn crates at this outpost to be carried and deployed in another area.
-- The large crates can only be loaded and dropped by large aircraft, like the C-130 and listed in ctld.vehicleTransportEnabled
-- Small FOB crates can be moved by helicopter. The FOB will require ctld.cratesRequiredForFOB larges crates and small crates are 1/3 of a large fob crate
-- To build the FOB entirely out of small crates you will need ctld.cratesRequiredForFOB * 3
ctld.troopPickupAtFOB = true -- if true, troops can also be picked up at a created FOB
ctld.buildTimeFOB = 120 --time in seconds for the FOB to be built
ctld.crateWaitTime = 40 -- time in seconds to wait before you can spawn another crate
ctld.forceCrateToBeMoved = true -- a crate must be picked up at least once and moved before it can be unpacked. Helps to reduce crate spam
ctld.radioSound = "beacon.ogg" -- the name of the sound file to use for the FOB radio beacons. If this isnt added to the mission BEACONS WONT WORK!
ctld.radioSoundFC3 = "beaconsilent.ogg" -- name of the second silent radio file, used so FC3 aircraft dont hear ALL the beacon noises... :)
ctld.deployedBeaconBattery = 30 -- the battery on deployed beacons will last for this number minutes before needing to be re-deployed
ctld.enabledRadioBeaconDrop = true -- if its set to false then beacons cannot be dropped by units
ctld.allowRandomAiTeamPickups = false -- Allows the AI to randomize the loading of infantry teams (specified below) at pickup zones
-- Simulated Sling load configuration
ctld.minimumHoverHeight = 7.5 -- Lowest allowable height for crate hover
ctld.maximumHoverHeight = 12.0 -- Highest allowable height for crate hover
ctld.maxDistanceFromCrate = 5.5 -- Maximum distance from from crate for hover
ctld.hoverTime = 10 -- Time to hold hover above a crate for loading in seconds
-- end of Simulated Sling load configuration
-- AA SYSTEM CONFIG --
-- Sets a limit on the number of active AA systems that can be built for RED.
-- A system is counted as Active if its fully functional and has all parts
-- If a system is partially destroyed, it no longer counts towards the total
-- When this limit is hit, a player will still be able to get crates for an AA system, just unable
-- to unpack them
ctld.AASystemLimitRED = 20 -- Red side limit
ctld.AASystemLimitBLUE = 20 -- Blue side limit
--END AA SYSTEM CONFIG --
-- ***************** JTAC CONFIGURATION *****************
ctld.JTAC_LIMIT_RED = 10 -- max number of JTAC Crates for the RED Side
ctld.JTAC_LIMIT_BLUE = 10 -- max number of JTAC Crates for the BLUE Side
ctld.JTAC_dropEnabled = true -- allow JTAC Crate spawn from F10 menu
ctld.JTAC_maxDistance = 10000 -- How far a JTAC can "see" in meters (with Line of Sight)
ctld.JTAC_smokeOn_RED = true -- enables marking of target with smoke for RED forces
ctld.JTAC_smokeOn_BLUE = true -- enables marking of target with smoke for BLUE forces
ctld.JTAC_smokeColour_RED = 4 -- RED side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4
ctld.JTAC_smokeColour_BLUE = 1 -- BLUE side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4
ctld.JTAC_smokeOffset_x = 0.0 -- distance in the X direction from target to smoke (meters)
ctld.JTAC_smokeOffset_y = 2.0 -- distance in the Y direction from target to smoke (meters)
ctld.JTAC_smokeOffset_z = 0.0 -- distance in the z direction from target to smoke (meters)
ctld.JTAC_jtacStatusF10 = true -- enables F10 JTAC Status menu
ctld.JTAC_location = true -- shows location of target in JTAC message
ctld.location_DMS = false -- shows coordinates as Degrees Minutes Seconds instead of Degrees Decimal minutes
ctld.JTAC_lock = "all" -- "vehicle" OR "troop" OR "all" forces JTAC to only lock vehicles or troops or all ground units
ctld.JTAC_allowStandbyMode = true -- if true, allow players to toggle lasing on/off
ctld.JTAC_laseSpotCorrections = true -- if true, each JTAC will have a special option (toggle on/off) available in it's menu to attempt to lead the target, taking into account current wind conditions and the speed of the target (particularily useful against moving heavy armor)
ctld.JTAC_allowSmokeRequest = true -- if true, allow players to request a smoke on target (temporary)
ctld.JTAC_allow9Line = true -- if true, allow players to ask for a 9Line (individual) for a specific JTAC's target
-- ***************** Pickup, dropoff and waypoint zones *****************
-- Available colors (anything else like "none" disables smoke): "green", "red", "white", "orange", "blue", "none",
-- Use any of the predefined names or set your own ones
-- You can add number as a third option to limit the number of soldier or vehicle groups that can be loaded from a zone.
-- Dropping back a group at a limited zone will add one more to the limit
-- If a zone isn't ACTIVE then you can't pickup from that zone until the zone is activated by ctld.activatePickupZone
-- using the Mission editor
-- You can pickup from a SHIP by adding the SHIP UNIT NAME instead of a zone name
-- Side - Controls which side can load/unload troops at the zone
-- Flag Number - Optional last field. If set the current number of groups remaining can be obtained from the flag value
--pickupZones = { "Zone name or Ship Unit Name", "smoke color", "limit (-1 unlimited)", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", flag number (optional) }
ctld.pickupZones = {
{ "pickzone1", "blue", -1, "yes", 0 },
{ "pickzone2", "red", -1, "yes", 0 },
{ "pickzone3", "none", -1, "yes", 0 },
{ "pickzone4", "none", -1, "yes", 0 },
{ "pickzone5", "none", -1, "yes", 0 },
{ "pickzone6", "none", -1, "yes", 0 },
{ "pickzone7", "none", -1, "yes", 0 },
{ "pickzone8", "none", -1, "yes", 0 },
{ "pickzone9", "none", 5, "yes", 1 }, -- limits pickup zone 9 to 5 groups of soldiers or vehicles, only red can pick up
{ "pickzone10", "none", 10, "yes", 2 }, -- limits pickup zone 10 to 10 groups of soldiers or vehicles, only blue can pick up
{ "pickzone11", "blue", 20, "no", 2 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive!
{ "pickzone12", "red", 20, "no", 1 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive!
{ "pickzone13", "none", -1, "yes", 0 },
{ "pickzone14", "none", -1, "yes", 0 },
{ "pickzone15", "none", -1, "yes", 0 },
{ "pickzone16", "none", -1, "yes", 0 },
{ "pickzone17", "none", -1, "yes", 0 },
{ "pickzone18", "none", -1, "yes", 0 },
{ "pickzone19", "none", 5, "yes", 0 },
{ "pickzone20", "none", 10, "yes", 0, 1000 }, -- optional extra flag number to store the current number of groups available in
{ "USA Carrier", "blue", 10, "yes", 0, 1001 }, -- instead of a Zone Name you can also use the UNIT NAME of a ship
}
-- dropOffZones = {"name","smoke colour",0,side 1 = Red or 2 = Blue or 0 = Both sides}
ctld.dropOffZones = {
{ "dropzone1", "green", 2 },
{ "dropzone2", "blue", 2 },
{ "dropzone3", "orange", 2 },
{ "dropzone4", "none", 2 },
{ "dropzone5", "none", 1 },
{ "dropzone6", "none", 1 },
{ "dropzone7", "none", 1 },
{ "dropzone8", "none", 1 },
{ "dropzone9", "none", 1 },
{ "dropzone10", "none", 1 },
}
--wpZones = { "Zone name", "smoke color", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", }
ctld.wpZones = {
{ "wpzone1", "green","yes", 2 },
{ "wpzone2", "blue","yes", 2 },
{ "wpzone3", "orange","yes", 2 },
{ "wpzone4", "none","yes", 2 },
{ "wpzone5", "none","yes", 2 },
{ "wpzone6", "none","yes", 1 },
{ "wpzone7", "none","yes", 1 },
{ "wpzone8", "none","yes", 1 },
{ "wpzone9", "none","yes", 1 },
{ "wpzone10", "none","no", 0 }, -- Both sides as its set to 0
}
-- ******************** Transports names **********************
-- Use any of the predefined names or set your own ones
ctld.transportPilotNames = {
"helicargo1",
"helicargo2",
"helicargo3",
"helicargo4",
"helicargo5",
"helicargo6",
"helicargo7",
"helicargo8",
"helicargo9",
"helicargo10",
"helicargo11",
"helicargo12",
"helicargo13",
"helicargo14",
"helicargo15",
"helicargo16",
"helicargo17",
"helicargo18",
"helicargo19",
"helicargo20",
"helicargo21",
"helicargo22",
"helicargo23",
"helicargo24",
"helicargo25",
"MEDEVAC #1",
"MEDEVAC #2",
"MEDEVAC #3",
"MEDEVAC #4",
"MEDEVAC #5",
"MEDEVAC #6",
"MEDEVAC #7",
"MEDEVAC #8",
"MEDEVAC #9",
"MEDEVAC #10",
"MEDEVAC #11",
"MEDEVAC #12",
"MEDEVAC #13",
"MEDEVAC #14",
"MEDEVAC #15",
"MEDEVAC #16",
"MEDEVAC RED #1",
"MEDEVAC RED #2",
"MEDEVAC RED #3",
"MEDEVAC RED #4",
"MEDEVAC RED #5",
"MEDEVAC RED #6",
"MEDEVAC RED #7",
"MEDEVAC RED #8",
"MEDEVAC RED #9",
"MEDEVAC RED #10",
"MEDEVAC RED #11",
"MEDEVAC RED #12",
"MEDEVAC RED #13",
"MEDEVAC RED #14",
"MEDEVAC RED #15",
"MEDEVAC RED #16",
"MEDEVAC RED #17",
"MEDEVAC RED #18",
"MEDEVAC RED #19",
"MEDEVAC RED #20",
"MEDEVAC RED #21",
"MEDEVAC BLUE #1",
"MEDEVAC BLUE #2",
"MEDEVAC BLUE #3",
"MEDEVAC BLUE #4",
"MEDEVAC BLUE #5",
"MEDEVAC BLUE #6",
"MEDEVAC BLUE #7",
"MEDEVAC BLUE #8",
"MEDEVAC BLUE #9",
"MEDEVAC BLUE #10",
"MEDEVAC BLUE #11",
"MEDEVAC BLUE #12",
"MEDEVAC BLUE #13",
"MEDEVAC BLUE #14",
"MEDEVAC BLUE #15",
"MEDEVAC BLUE #16",
"MEDEVAC BLUE #17",
"MEDEVAC BLUE #18",
"MEDEVAC BLUE #19",
"MEDEVAC BLUE #20",
"MEDEVAC BLUE #21",
-- *** AI transports names (different names only to ease identification in mission) ***
-- Use any of the predefined names or set your own ones
"transport1",
"transport2",
"transport3",
"transport4",
"transport5",
"transport6",
"transport7",
"transport8",
"transport9",
"transport10",
"transport11",
"transport12",
"transport13",
"transport14",
"transport15",
"transport16",
"transport17",
"transport18",
"transport19",
"transport20",
"transport21",
"transport22",
"transport23",
"transport24",
"transport25",
}
-- *************** Optional Extractable GROUPS *****************
-- Use any of the predefined names or set your own ones
ctld.extractableGroups = {
"extract1",
"extract2",
"extract3",
"extract4",
"extract5",
"extract6",
"extract7",
"extract8",
"extract9",
"extract10",
"extract11",
"extract12",
"extract13",
"extract14",
"extract15",
"extract16",
"extract17",
"extract18",
"extract19",
"extract20",
"extract21",
"extract22",
"extract23",
"extract24",
"extract25",
}
-- ************** Logistics UNITS FOR CRATE SPAWNING ******************
-- Use any of the predefined names or set your own ones
-- When a logistic unit is destroyed, you will no longer be able to spawn crates
ctld.logisticUnits = {
"logistic1",
"logistic2",
"logistic3",
"logistic4",
"logistic5",
"logistic6",
"logistic7",
"logistic8",
"logistic9",
"logistic10",
}
-- ************** UNITS ABLE TO TRANSPORT VEHICLES ******************
-- Add the model name of the unit that you want to be able to transport and deploy vehicles
-- units db has all the names or you can extract a mission.miz file by making it a zip and looking
-- in the contained mission file
ctld.vehicleTransportEnabled = {
"76MD", -- the il-76 mod doesnt use a normal - sign so il-76md wont match... !!!! GRR
"Hercules",
}
-- ************** Units able to use DCS dynamic cargo system ******************
-- DCS (version) added the ability to load and unload cargo from aircraft.
-- Units listed here will spawn a cargo static that can be loaded with the standard DCS cargo system
-- We will also use this to make modifications to the menu and other checks and messages
ctld.dynamicCargoUnits = {
"CH-47Fbl1",
}
-- ************** Maximum Units SETUP for UNITS ******************
-- Put the name of the Unit you want to limit group sizes too
-- i.e
-- ["UH-1H"] = 10,
--
-- Will limit UH1 to only transport groups with a size 10 or less
-- Make sure the unit name is exactly right or it wont work
ctld.unitLoadLimits = {
-- Remove the -- below to turn on options
-- ["SA342Mistral"] = 4,
-- ["SA342L"] = 4,
-- ["SA342M"] = 4,
}
-- ************** Allowable actions for UNIT TYPES ******************
-- Put the name of the Unit you want to limit actions for
-- NOTE - the unit must've been listed in the transportPilotNames list above
-- This can be used in conjunction with the options above for group sizes
-- By default you can load both crates and troops unless overriden below
-- i.e
-- ["UH-1H"] = {crates=true, troops=false},
--
-- Will limit UH1 to only transport CRATES but NOT TROOPS
--
-- ["SA342Mistral"] = {crates=fales, troops=true},
-- Will allow Mistral Gazelle to only transport crates, not troops
ctld.unitActions = {
-- Remove the -- below to turn on options
-- ["SA342Mistral"] = {crates=true, troops=true},
-- ["SA342L"] = {crates=false, troops=true},
-- ["SA342M"] = {crates=false, troops=true},
}
-- ************** WEIGHT CALCULATIONS FOR INFANTRY GROUPS ******************
-- Infantry groups weight is calculated based on the soldiers' roles, and the weight of their kit
-- Every soldier weights between 90% and 120% of ctld.SOLDIER_WEIGHT, and they all carry a backpack and their helmet (ctld.KIT_WEIGHT)
-- Standard grunts have a rifle and ammo (ctld.RIFLE_WEIGHT)
-- AA soldiers have a MANPAD tube (ctld.MANPAD_WEIGHT)
-- Anti-tank soldiers have a RPG and a rocket (ctld.RPG_WEIGHT)
-- Machine gunners have the squad MG and 200 bullets (ctld.MG_WEIGHT)
-- JTAC have the laser sight, radio and binoculars (ctld.JTAC_WEIGHT)
-- Mortar servants carry their tube and a few rounds (ctld.MORTAR_WEIGHT)
ctld.SOLDIER_WEIGHT = 80 -- kg, will be randomized between 90% and 120%
ctld.KIT_WEIGHT = 20 -- kg
ctld.RIFLE_WEIGHT = 5 -- kg
ctld.MANPAD_WEIGHT = 18 -- kg
ctld.RPG_WEIGHT = 7.6 -- kg
ctld.MG_WEIGHT = 10 -- kg
ctld.MORTAR_WEIGHT = 26 -- kg
ctld.JTAC_WEIGHT = 15 -- kg
-- ************** INFANTRY GROUPS FOR PICKUP ******************
-- Unit Types
-- inf is normal infantry
-- mg is M249
-- at is RPG-16
-- aa is Stinger or Igla
-- mortar is a 2B11 mortar unit
-- jtac is a JTAC soldier, which will use JTACAutoLase
-- You must add a name to the group for it to work
-- You can also add an optional coalition side to limit the group to one side
-- for the side - 2 is BLUE and 1 is RED
ctld.loadableGroups = {
{name = "Standard Group", inf = 6, mg = 2, at = 2 }, -- will make a loadable group with 6 infantry, 2 MGs and 2 anti-tank for both coalitions
{name = "Anti Air", inf = 2, aa = 3 },
{name = "Anti Tank", inf = 2, at = 6 },
{name = "Mortar Squad", mortar = 6 },
{name = "JTAC Group", inf = 4, jtac = 1 }, -- will make a loadable group with 4 infantry and a JTAC soldier for both coalitions
{name = "Single JTAC", jtac = 1 }, -- will make a loadable group witha single JTAC soldier for both coalitions
-- {name = "Mortar Squad Red", inf = 2, mortar = 5, side =1 }, --would make a group loadable by RED only
}
-- ************** SPAWNABLE CRATES ******************
-- Weights must be unique as we use the weight to change the cargo to the correct unit
-- when we unpack
--
ctld.spawnableCrates = {
-- name of the sub menu on F10 for spawning crates
["Combat Vehicles"] = {
--crates you can spawn
-- weight in KG
-- Desc is the description on the F10 MENU
-- unit is the model name of the unit to spawn
-- cratesRequired - if set requires that many crates of the same type within 100m of each other in order build the unit
-- side is optional but 2 is BLUE and 1 is RED
-- Some descriptions are filtered to determine if JTAC or not!
--- BLUE
{ weight = 1000, desc = "Humvee - MG", unit = "M1043 HMMWV Armament", side = 2 }, --careful with the names as the script matches the desc to JTAC types
{ weight = 1001, desc = "Humvee - TOW", unit = "M1045 HMMWV TOW", side = 2, cratesRequired = 2 },
{ weight = 1002, desc = "Light Tank - MRAP", unit="MaxxPro_MRAP", side = 2, cratesRequired = 2 },
{ weight = 1003, desc = "Med Tank - LAV-25", unit="LAV-25", side = 2, cratesRequired = 3 },
{ weight = 1004, desc = "Heavy Tank - Abrams", unit="M1A2C_SEP_V3", side = 2, cratesRequired = 4 },
--- RED
{ weight = 1005, desc = "BTR-D", unit = "BTR_D", side = 1 },
{ weight = 1006, desc = "BRDM-2", unit = "BRDM-2", side = 1 },
-- need more redfor!
},
["Support"] = {
--- BLUE
{ weight = 1007, desc = "Hummer - JTAC", unit = "Hummer", side = 2, cratesRequired = 2 }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled
{ weight = 1008, desc = "M-818 Ammo Truck", unit = "M 818", side = 2, cratesRequired = 2 },
--- RED
{ weight = 1009, desc = "SKP-11 - JTAC", unit = "SKP-11", side = 1 }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled
{ weight = 1010, desc = "Ural-375 Ammo Truck", unit = "Ural-375", side = 1, cratesRequired = 2 },
--- Both
{ weight = 1011, desc = "EWR Radar", unit="FPS-117", cratesRequired = 3 },
{ weight = 1012, desc = "FOB Crate - Small", unit = "FOB-SMALL" }, -- Builds a FOB! - requires 3 * ctld.cratesRequiredForFOB
},
["Artillery"] = {
--- BLUE
{ weight = 1013, desc = "MLRS", unit = "MLRS", side=2, cratesRequired = 3 },
{ weight = 1014, desc = "SpGH DANA", unit = "SpGH_Dana", side=2, cratesRequired = 3 },
{ weight = 1015, desc = "T155 Firtina", unit = "T155_Firtina", side=2, cratesRequired = 3 },
{ weight = 1016, desc = "Howitzer", unit = "M-109", side=2, cratesRequired = 3 },
--- RED
{ weight = 1017, desc = "SPH 2S19 Msta", unit = "SAU Msta", side = 1, cratesRequired = 3 },
},
["SAM short range"] = {
--- BLUE
{ weight = 1018, desc = "M1097 Avenger", unit = "M1097 Avenger", side = 2, cratesRequired = 3 },
--- RED
{ weight = 1019, desc = "Strela-1 9P31", unit = "Strela-1 9P31", side = 1, cratesRequired = 3 },
},
["SAM mid range"] = {
--- BLUE
-- HAWK System
{ weight = 1020, desc = "HAWK Launcher", unit = "Hawk ln", side = 2},
{ weight = 1021, desc = "HAWK Search Radar", unit = "Hawk sr", side = 2 },
{ weight = 1022, desc = "HAWK Track Radar", unit = "Hawk tr", side = 2 },
{ weight = 1023, desc = "HAWK PCP", unit = "Hawk pcp" , side = 2 },
{ weight = 1024, desc = "HAWK CWAR", unit = "Hawk cwar" , side = 2 },
{ weight = 1025, desc = "HAWK Repair", unit = "HAWK Repair" , side = 2 },
-- End of HAWK
--- RED
-- KUB SYSTEM
{ weight = 1026, desc = "KUB Launcher", unit = "Kub 2P25 ln", side = 1},
{ weight = 1027, desc = "KUB Radar", unit = "Kub 1S91 str", side = 1 },
{ weight = 1028, desc = "KUB Repair", unit = "KUB Repair", side = 1},
-- End of KUB
},
["SAM long range"] = {
--- BLUE
-- Patriot System
{ weight = 1029, desc = "Patriot Launcher", unit = "Patriot ln", side = 2 },
{ weight = 1030, desc = "Patriot Radar", unit = "Patriot str" , side = 2 },
{ weight = 1031, desc = "Patriot ECS", unit = "Patriot ECS", side = 2 },
{ weight = 1032, desc = "Patriot AMG (optional)", unit = "Patriot AMG" , side = 2 },
{ weight = 1033, desc = "Patriot Repair", unit = "Patriot Repair" , side = 2 },
-- End of Patriot
},
}
ctld.spawnableCratesModels = {
["load"] = {
["category"] = "Fortifications",
["shape_name"] = "GeneratorF",
["type"] = "GeneratorF",
["canCargo"] = false,
},
["sling"] = {
["category"] = "Cargos",
["shape_name"] = "bw_container_cargo",
["type"] = "container_cargo",
["canCargo"] = true
},
["dynamic"] = {
["category"] = "Cargos",
["shape_name"] = "m117_cargo",
["type"] = "m117_cargo",
["canCargo"] = true
}
}
--[[ Placeholder for different type of cargo containers. Let's say pipes and trunks, fuel for FOB building
["shape_name"] = "ab-212_cargo",
["type"] = "uh1h_cargo" --new type for the container previously used
["shape_name"] = "ammo_box_cargo",
["type"] = "ammo_cargo",
["shape_name"] = "barrels_cargo",
["type"] = "barrels_cargo",
["shape_name"] = "bw_container_cargo",
["type"] = "container_cargo",
["shape_name"] = "f_bar_cargo",
["type"] = "f_bar_cargo",
["shape_name"] = "fueltank_cargo",
["type"] = "fueltank_cargo",
["shape_name"] = "iso_container_cargo",
["type"] = "iso_container",
["shape_name"] = "iso_container_small_cargo",
["type"] = "iso_container_small",
["shape_name"] = "oiltank_cargo",
["type"] = "oiltank_cargo",
["shape_name"] = "pipes_big_cargo",
["type"] = "pipes_big_cargo",
["shape_name"] = "pipes_small_cargo",
["type"] = "pipes_small_cargo",
["shape_name"] = "tetrapod_cargo",
["type"] = "tetrapod_cargo",
["shape_name"] = "trunks_long_cargo",
["type"] = "trunks_long_cargo",
["shape_name"] = "trunks_small_cargo",
["type"] = "trunks_small_cargo",
]]--
-- if the unit is on this list, it will be made into a JTAC when deployed
ctld.jtacUnitTypes = {
"SKP", "Hummer" -- there are some wierd encoding issues so if you write SKP-11 it wont match as the - sign is encoded differently...
}
-- ***************************************************************
-- **************** Mission Editor Functions *********************
-- ***************************************************************
-----------------------------------------------------------------
-- Spawn group at a trigger and set them as extractable. Usage:
-- ctld.spawnGroupAtTrigger("groupside", number, "triggerName", radius)
-- Variables:
-- "groupSide" = "red" for Russia "blue" for USA
-- _number = number of groups to spawn OR Group description
-- "triggerName" = trigger name in mission editor between commas
-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy)
--
-- Example: ctld.spawnGroupAtTrigger("red", 2, "spawn1", 1000)
--
-- This example will spawn 2 groups of russians at the specified point
-- and they will search for enemy or move randomly withing 1000m
-- OR
--
-- ctld.spawnGroupAtTrigger("blue", {mg=1,at=2,aa=3,inf=4,mortar=5},"spawn2", 2000)
-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars
--
function ctld.spawnGroupAtTrigger(_groupSide, _number, _triggerName, _searchRadius)
local _spawnTrigger = trigger.misc.getZone(_triggerName) -- trigger to use as reference position
if _spawnTrigger == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find trigger called " .. _triggerName, 10)
return
end
local _country
if _groupSide == "red" then
_groupSide = 1
_country = 0
else
_groupSide = 2
_country = 2
end
if _searchRadius < 0 then
_searchRadius = 0
end
local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country)
local _droppedTroops = ctld.spawnDroppedGroup(_pos3, _groupDetails, false, _searchRadius);
if _groupSide == 1 then
table.insert(ctld.droppedTroopsRED, _droppedTroops:getName())
else
table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName())
end
end
-----------------------------------------------------------------
-- Spawn group at a Vec3 Point and set them as extractable. Usage:
-- ctld.spawnGroupAtPoint("groupside", number,Vec3 Point, radius)
-- Variables:
-- "groupSide" = "red" for Russia "blue" for USA
-- _number = number of groups to spawn OR Group Description
-- Vec3 Point = A vec3 point like {x=1,y=2,z=3}. Can be obtained from a unit like so: Unit.getName("Unit1"):getPoint()
-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy)
--
-- Example: ctld.spawnGroupAtPoint("red", 2, {x=1,y=2,z=3}, 1000)
--
-- This example will spawn 2 groups of russians at the specified point
-- and they will search for enemy or move randomly withing 1000m
-- OR
--
-- ctld.spawnGroupAtPoint("blue", {mg=1,at=2,aa=3,inf=4,mortar=5}, {x=1,y=2,z=3}, 2000)
-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars
function ctld.spawnGroupAtPoint(_groupSide, _number, _point, _searchRadius)
local _country
if _groupSide == "red" then
_groupSide = 1
_country = 0
else
_groupSide = 2
_country = 2
end
if _searchRadius < 0 then
_searchRadius = 0
end
local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country)
local _droppedTroops = ctld.spawnDroppedGroup(_point, _groupDetails, false, _searchRadius);
if _groupSide == 1 then
table.insert(ctld.droppedTroopsRED, _droppedTroops:getName())
else
table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName())
end
end
-- Preloads a transport with troops or vehicles
-- replaces any troops currently on board
function ctld.preLoadTransport(_unitName, _number, _troops)
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil then
-- will replace any units currently on board
-- if not ctld.troopsOnboard(_unit,_troops) then
ctld.loadTroops(_unit, _troops, _number)
-- end
end
end
-- Continuously counts the number of crates in a zone and sets the value of the passed in flag
-- to the count amount
-- This means you can trigger actions based on the count and also trigger messages before the count is reached
-- Just pass in the zone name and flag number like so as a single (NOT Continuous) Trigger
-- This will now work for Mission Editor and Spawned Crates
-- e.g. ctld.cratesInZone("DropZone1", 5)
function ctld.cratesInZone(_zone, _flagNumber)
local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _zonePos = mist.utils.zoneToVec3(_zone)
--ignore side, if crate has been used its discounted from the count
local _crateTables = { ctld.spawnedCratesRED, ctld.spawnedCratesBLUE, ctld.missionEditorCargoCrates }
local _crateCount = 0
for _, _crates in pairs(_crateTables) do
for _crateName, _dontUse in pairs(_crates) do
--get crate
local _crate = ctld.getCrateObject(_crateName)
--in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude
if _crate ~= nil and _crate:getLife() > 0
and (ctld.inAir(_crate) == false) then
local _dist = ctld.getDistance(_crate:getPoint(), _zonePos)
if _dist <= _triggerZone.radius then
_crateCount = _crateCount + 1
end
end
end
end
--set flag stuff
trigger.action.setUserFlag(_flagNumber, _crateCount)
-- env.info("FLAG ".._flagNumber.." crates ".._crateCount)
--retrigger in 5 seconds
timer.scheduleFunction(function(_args)
ctld.cratesInZone(_args[1], _args[2])
end, { _zone, _flagNumber }, timer.getTime() + 5)
end
-- Creates an extraction zone
-- any Soldiers (not vehicles) dropped at this zone by a helicopter will disappear
-- and be added to a running total of soldiers for a set flag number
-- The idea is you can then drop say 20 troops in a zone and trigger an action using the mission editor triggers
-- and the flag value
--
-- The ctld.createExtractZone function needs to be called once in a trigger action do script.
-- if you dont want smoke, pass -1 to the function.
--Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4, NO SMOKE = -1
--
-- e.g. ctld.createExtractZone("extractzone1", 2, -1) will create an extraction zone at trigger zone "extractzone1", store the number of troops dropped at
-- the zone in flag 2 and not have smoke
--
--
--
function ctld.createExtractZone(_zone, _flagNumber, _smoke)
local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
trigger.action.setUserFlag(_flagNumber, 0) --start at 0
local _details = { point = _pos3, name = _zone, smoke = _smoke, flag = _flagNumber, radius = _triggerZone.radius}
ctld.extractZones[_zone.."-".._flagNumber] = _details
if _smoke ~= nil and _smoke > -1 then
local _smokeFunction
_smokeFunction = function(_args)
local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber]
-- check zone is still active
if _extractDetails == nil then
-- stop refreshing smoke, zone is done
return
end
trigger.action.smoke(_args.point, _args.smoke)
--refresh in 5 minutes
timer.scheduleFunction(_smokeFunction, _args, timer.getTime() + 300)
end
--run local function
_smokeFunction(_details)
end
end
-- Removes an extraction zone
--
-- The smoke will take up to 5 minutes to disappear depending on the last time the smoke was activated
--
-- The ctld.removeExtractZone function needs to be called once in a trigger action do script.
--
-- e.g. ctld.removeExtractZone("extractzone1", 2) will remove an extraction zone at trigger zone "extractzone1"
-- that was setup with flag 2
--
--
--
function ctld.removeExtractZone(_zone,_flagNumber)
local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber]
if _extractDetails ~= nil then
--remove zone
ctld.extractZones[_zone.."-".._flagNumber] = nil
end
end
-- CONTINUOUS TRIGGER FUNCTION
-- This function will count the current number of extractable RED and BLUE
-- GROUPS in a zone and store the values in two flags
-- A group is only counted as being in a zone when the leader of that group
-- is in the zone
-- Use: ctld.countDroppedGroupsInZone("Zone Name", flagBlue, flagRed)
function ctld.countDroppedGroupsInZone(_zone, _blueFlag, _redFlag)
local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _zonePos = mist.utils.zoneToVec3(_zone)
local _redCount = 0;
local _blueCount = 0;
local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE}
for _, _extractGroups in pairs(_allGroups) do
for _,_groupName in pairs(_extractGroups) do
local _groupUnits = ctld.getGroup(_groupName)
if #_groupUnits > 0 then
local _zonePos = mist.utils.zoneToVec3(_zone)
local _dist = ctld.getDistance(_groupUnits[1]:getPoint(), _zonePos)
if _dist <= _triggerZone.radius then
if (_groupUnits[1]:getCoalition() == 1) then
_redCount = _redCount + 1;
else
_blueCount = _blueCount + 1;
end
end
end
end
end
--set flag stuff
trigger.action.setUserFlag(_blueFlag, _blueCount)
trigger.action.setUserFlag(_redFlag, _redCount)
-- env.info("Groups in zone ".._blueCount.." ".._redCount)
end
-- CONTINUOUS TRIGGER FUNCTION
-- This function will count the current number of extractable RED and BLUE
-- UNITS in a zone and store the values in two flags
-- Use: ctld.countDroppedUnitsInZone("Zone Name", flagBlue, flagRed)
function ctld.countDroppedUnitsInZone(_zone, _blueFlag, _redFlag)
local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _zonePos = mist.utils.zoneToVec3(_zone)
local _redCount = 0;
local _blueCount = 0;
local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE}
for _, _extractGroups in pairs(_allGroups) do
for _,_groupName in pairs(_extractGroups) do
local _groupUnits = ctld.getGroup(_groupName)
if #_groupUnits > 0 then
local _zonePos = mist.utils.zoneToVec3(_zone)
for _,_unit in pairs(_groupUnits) do
local _dist = ctld.getDistance(_unit:getPoint(), _zonePos)
if _dist <= _triggerZone.radius then
if (_unit:getCoalition() == 1) then
_redCount = _redCount + 1;
else
_blueCount = _blueCount + 1;
end
end
end
end
end
end
--set flag stuff
trigger.action.setUserFlag(_blueFlag, _blueCount)
trigger.action.setUserFlag(_redFlag, _redCount)
-- env.info("Units in zone ".._blueCount.." ".._redCount)
end
-- Creates a radio beacon on a random UHF - VHF and HF/FM frequency for homing
-- This WILL NOT WORK if you dont add beacon.ogg and beaconsilent.ogg to the mission!!!
-- e.g. ctld.createRadioBeaconAtZone("beaconZone","red", 1440,"Waypoint 1") will create a beacon at trigger zone "beaconZone" for the Red side
-- that will last 1440 minutes (24 hours ) and named "Waypoint 1" in the list of radio beacons
--
-- e.g. ctld.createRadioBeaconAtZone("beaconZoneBlue","blue", 20) will create a beacon at trigger zone "beaconZoneBlue" for the Blue side
-- that will last 20 minutes
function ctld.createRadioBeaconAtZone(_zone, _coalition, _batteryLife, _name)
local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _zonePos = mist.utils.zoneToVec3(_zone)
ctld.beaconCount = ctld.beaconCount + 1
if _name == nil or _name == "" then
_name = "Beacon #" .. ctld.beaconCount
end
if _coalition == "red" then
ctld.createRadioBeacon(_zonePos, 1, 0, _name, _batteryLife) --1440
else
ctld.createRadioBeacon(_zonePos, 2, 2, _name, _batteryLife) --1440
end
end
-- Activates a pickup zone
-- Activates a pickup zone when called from a trigger
-- EG: ctld.activatePickupZone("pickzone3")
-- This is enable pickzone3 to be used as a pickup zone for the team set
function ctld.activatePickupZone(_zoneName)
local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position
if _triggerZone == nil then
local _ship = ctld.getTransportUnit(_triggerZone)
if _ship then
local _point = _ship:getPoint()
_triggerZone = {}
_triggerZone.point = _point
end
end
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone or ship called " .. _zoneName, 10)
end
for _, _zoneDetails in pairs(ctld.pickupZones) do
if _zoneName == _zoneDetails[1] then
--smoke could get messy if designer keeps calling this on an active zone, check its not active first
if _zoneDetails[4] == 1 then
-- they might have a continuous trigger so i've hidden the warning
--trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10)
return
end
_zoneDetails[4] = 1 --activate zone
if ctld.disableAllSmoke == true then --smoke disabled
return
end
if _zoneDetails[2] >= 0 then
-- Trigger smoke marker
-- This will cause an overlapping smoke marker on next refreshsmoke call
-- but will only happen once
local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
trigger.action.smoke(_pos3, _zoneDetails[2])
end
end
end
end
-- Deactivates a pickup zone
-- Deactivates a pickup zone when called from a trigger
-- EG: ctld.deactivatePickupZone("pickzone3")
-- This is disables pickzone3 and can no longer be used to as a pickup zone
-- These functions can be called by triggers, like if a set of buildings is used, you can trigger the zone to be 'not operational'
-- once they are destroyed
function ctld.deactivatePickupZone(_zoneName)
local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position
if _triggerZone == nil then
local _ship = ctld.getTransportUnit(_triggerZone)
if _ship then
local _point = _ship:getPoint()
_triggerZone = {}
_triggerZone.point = _point
end
end
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10)
return
end
for _, _zoneDetails in pairs(ctld.pickupZones) do
if _zoneName == _zoneDetails[1] then
-- i'd just ignore it if its already been deactivated
-- if _zoneDetails[4] == 0 then --this really needed??
-- trigger.action.outText("CTLD.lua ERROR: Pickup Zone already deactiveated: " .. _zoneName, 10)
-- return
-- end
_zoneDetails[4] = 0 --deactivate zone
end
end
end
-- Change the remaining groups currently available for pickup at a zone
-- e.g. ctld.changeRemainingGroupsForPickupZone("pickup1", 5) -- adds 5 groups
-- ctld.changeRemainingGroupsForPickupZone("pickup1", -3) -- remove 3 groups
function ctld.changeRemainingGroupsForPickupZone(_zoneName, _amount)
local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position
if _triggerZone == nil then
local _ship = ctld.getTransportUnit(_triggerZone)
if _ship then
local _point = _ship:getPoint()
_triggerZone = {}
_triggerZone.point = _point
end
end
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ctld.changeRemainingGroupsForPickupZone ERROR: Cant find zone called " .. _zoneName, 10)
return
end
for _, _zoneDetails in pairs(ctld.pickupZones) do
if _zoneName == _zoneDetails[1] then
ctld.updateZoneCounter(_zoneName, _amount)
end
end
end
-- Activates a Waypoint zone
-- Activates a Waypoint zone when called from a trigger
-- EG: ctld.activateWaypointZone("pickzone3")
-- This means that troops dropped within the radius of the zone will head to the center
-- of the zone instead of searching for troops
function ctld.activateWaypointZone(_zoneName)
local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10)
return
end
for _, _zoneDetails in pairs(ctld.wpZones) do
if _zoneName == _zoneDetails[1] then
--smoke could get messy if designer keeps calling this on an active zone, check its not active first
if _zoneDetails[3] == 1 then
-- they might have a continuous trigger so i've hidden the warning
--trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10)
return
end
_zoneDetails[3] = 1 --activate zone
if ctld.disableAllSmoke == true then --smoke disabled
return
end
if _zoneDetails[2] >= 0 then
-- Trigger smoke marker
-- This will cause an overlapping smoke marker on next refreshsmoke call
-- but will only happen once
local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
trigger.action.smoke(_pos3, _zoneDetails[2])
end
end
end
end
-- Deactivates a Waypoint zone
-- Deactivates a Waypoint zone when called from a trigger
-- EG: ctld.deactivateWaypointZone("wpzone3")
-- This disables wpzone3 so that troops dropped in this zone will search for troops as normal
-- These functions can be called by triggers
function ctld.deactivateWaypointZone(_zoneName)
local _triggerZone = trigger.misc.getZone(_zoneName)
if _triggerZone == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10)
return
end
for _, _zoneDetails in pairs(ctld.pickupZones) do
if _zoneName == _zoneDetails[1] then
_zoneDetails[3] = 0 --deactivate zone
end
end
end
-- Continuous Trigger Function
-- Causes an AI unit with the specified name to unload troops / vehicles when
-- an enemy is detected within a specified distance
-- The enemy must have Line or Sight to the unit to be detected
function ctld.unloadInProximityToEnemy(_unitName,_distance)
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil and _unit:getPlayerName() == nil then
-- no player name means AI!
-- the findNearest visible enemy you'd want to modify as it'll find enemies quite far away
-- limited by ctld.JTAC_maxDistance
local _nearestEnemy = ctld.findNearestVisibleEnemy(_unit,"all",_distance)
if _nearestEnemy ~= nil then
if ctld.troopsOnboard(_unit, true) then
ctld.deployTroops(_unit, true)
return true
end
if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then
ctld.deployTroops(_unit, false)
return true
end
end
end
return false
end
-- Unit will unload any units onboard if the unit is on the ground
-- when this function is called
function ctld.unloadTransport(_unitName)
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil then
if ctld.troopsOnboard(_unit, true) then
ctld.unloadTroops({_unitName,true})
end
if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then
ctld.unloadTroops({_unitName,false})
end
end
end
-- Loads Troops and Vehicles from a zone or picks up nearby troops or vehicles
function ctld.loadTransport(_unitName)
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil then
ctld.loadTroopsFromZone({ _unitName, true,"",true })
if ctld.unitCanCarryVehicles(_unit) then
ctld.loadTroopsFromZone({ _unitName, false,"",true })
end
end
end
-- adds a callback that will be called for many actions ingame
function ctld.addCallback(_callback)
table.insert(ctld.callbacks,_callback)
end
-- Spawns a sling loadable crate at a Trigger Zone
--
-- Weights can be found in the ctld.spawnableCrates list
-- e.g. ctld.spawnCrateAtZone("red", 500,"triggerzone1") -- spawn a humvee at triggerzone 1 for red side
-- e.g. ctld.spawnCrateAtZone("blue", 505,"triggerzone1") -- spawn a tow humvee at triggerzone1 for blue side
--
function ctld.spawnCrateAtZone(_side, _weight,_zone)
local _spawnTrigger = trigger.misc.getZone(_zone) -- trigger to use as reference position
if _spawnTrigger == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10)
return
end
local _crateType = ctld.crateLookupTable[tostring(_weight)]
if _crateType == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10)
return
end
local _country
if _side == "red" then
_side = 1
_country = 0
else
_side = 2
_country = 2
end
local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z }
local _alt = land.getHeight(_pos2)
local _point = { x = _pos2.x, y = _alt, z = _pos2.y }
local _unitId = ctld.getNextUnitId()
local _name = string.format("%s #%i", _crateType.desc, _unitId)
local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side)
end
-- Spawns a sling loadable crate at a Point
--
-- Weights can be found in the ctld.spawnableCrates list
-- Points can be made by hand or obtained from a Unit position by Unit.getByName("PilotName"):getPoint()
-- e.g. ctld.spawnCrateAtZone("red", 500,{x=1,y=2,z=3}) -- spawn a humvee at triggerzone 1 for red side at a specified point
-- e.g. ctld.spawnCrateAtZone("blue", 505,{x=1,y=2,z=3}) -- spawn a tow humvee at triggerzone1 for blue side at a specified point
--
--
function ctld.spawnCrateAtPoint(_side, _weight,_point)
local _crateType = ctld.crateLookupTable[tostring(_weight)]
if _crateType == nil then
trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10)
return
end
local _country
if _side == "red" then
_side = 1
_country = 0
else
_side = 2
_country = 2
end
local _unitId = ctld.getNextUnitId()
local _name = string.format("%s #%i", _crateType.desc, _unitId)
local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side)
end
-- ***************************************************************
-- **************** BE CAREFUL BELOW HERE ************************
-- ***************************************************************
--- Tells CTLD What multipart AA Systems there are and what parts they need
-- A New system added here also needs the launcher added
ctld.AASystemTemplate = {
{
name = "HAWK AA System",
count = 4,
parts = {
{name = "Hawk ln", desc = "HAWK Launcher", launcher = true},
{name = "Hawk tr", desc = "HAWK Track Radar"},
{name = "Hawk sr", desc = "HAWK Search Radar"},
{name = "Hawk pcp", desc = "HAWK PCP"},
{name = "Hawk cwar", desc = "HAWK CWAR"},
},
repair = "HAWK Repair",
},
{
name = "Patriot AA System",
count = 4,
parts = {
{name = "Patriot ln", desc = "Patriot Launcher", launcher = true},
{name = "Patriot ECS", desc = "Patriot Control Unit"},
{name = "Patriot str", desc = "Patriot Search and Track Radar"},
},
repair = "Patriot Repair",
},
{
name = "BUK AA System",
count = 3,
parts = {
{name = "SA-11 Buk LN 9A310M1", desc = "BUK Launcher" , launcher = true},
{name = "SA-11 Buk CC 9S470M1", desc = "BUK CC Radar"},
{name = "SA-11 Buk SR 9S18M1", desc = "BUK Search Radar"},
},
repair = "BUK Repair",
},
{
name = "KUB AA System",
count = 2,
parts = {
{name = "Kub 2P25 ln", desc = "KUB Launcher", launcher = true},
{name = "Kub 1S91 str", desc = "KUB Radar"},
},
repair = "KUB Repair",
},
}
ctld.crateWait = {}
ctld.crateMove = {}
---------------- INTERNAL FUNCTIONS ----------------
---
---
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utility methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--- print an object for a debugging log
function ctld.p(o, level)
local MAX_LEVEL = 20
if level == nil then level = 0 end
if level > MAX_LEVEL then
ctld.logError("max depth reached in ctld.p : "..tostring(MAX_LEVEL))
return ""
end
local text = ""
if (type(o) == "table") then
text = "\n"
for key,value in pairs(o) do
for i=0, level do
text = text .. " "
end
text = text .. ".".. key.."="..ctld.p(value, level+1) .. "\n"
end
elseif (type(o) == "function") then
text = "[function]"
elseif (type(o) == "boolean") then
if o == true then
text = "[true]"
else
text = "[false]"
end
else
if o == nil then
text = "[nil]"
else
text = tostring(o)
end
end
return text
end
function ctld.logError(message)
env.info(" E - " .. ctld.Id .. message)
end
function ctld.logInfo(message)
env.info(" I - " .. ctld.Id .. message)
end
function ctld.logDebug(message)
if message and ctld.Debug then
env.info(" D - " .. ctld.Id .. message)
end
end
function ctld.logTrace(message)
if message and ctld.Trace then
env.info(" T - " .. ctld.Id .. message)
end
end
ctld.nextUnitId = 1;
ctld.getNextUnitId = function()
ctld.nextUnitId = ctld.nextUnitId + 1
return ctld.nextUnitId
end
ctld.nextGroupId = 1;
ctld.getNextGroupId = function()
ctld.nextGroupId = ctld.nextGroupId + 1
return ctld.nextGroupId
end
function ctld.getTransportUnit(_unitName)
if _unitName == nil then
return nil
end
local _heli = Unit.getByName(_unitName)
if _heli ~= nil and _heli:isActive() and _heli:getLife() > 0 then
return _heli
end
return nil
end
function ctld.spawnCrateStatic(_country, _unitId, _point, _name, _weight, _side, _model_type)
local _crate
local _spawnedCrate
if ctld.staticBugWorkaround and ctld.slingLoad == false then
local _groupId = ctld.getNextGroupId()
local _groupName = "Crate Group #".._groupId
local _group = {
["visible"] = false,
-- ["groupId"] = _groupId,
["hidden"] = false,
["units"] = {},
-- ["y"] = _positions[1].z,
-- ["x"] = _positions[1].x,
["name"] = _groupName,
["task"] = {},
}
_group.units[1] = ctld.createUnit(_point.x , _point.z , 0, {type="UAZ-469",name=_name,unitId=_unitId})
--switch to MIST
_group.category = Group.Category.GROUND;
_group.country = _country;
local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name)
-- Turn off AI
trigger.action.setGroupAIOff(_spawnedGroup)
_spawnedCrate = Unit.getByName(_name)
else
if _model_type ~= nil then
_crate = mist.utils.deepCopy(ctld.spawnableCratesModels[_model_type])
elseif ctld.slingLoad then
_crate = mist.utils.deepCopy(ctld.spawnableCratesModels["sling"])
else
_crate = mist.utils.deepCopy(ctld.spawnableCratesModels["load"])
end
_crate["y"] = _point.z
_crate["x"] = _point.x
_crate["mass"] = _weight
_crate["name"] = _name
_crate["heading"] = 0
_crate["country"] = _country
mist.dynAddStatic(_crate)
_spawnedCrate = StaticObject.getByName(_crate["name"])
end
local _crateType = ctld.crateLookupTable[tostring(_weight)]
if _side == 1 then
ctld.spawnedCratesRED[_name] =_crateType
else
ctld.spawnedCratesBLUE[_name] = _crateType
end
return _spawnedCrate
end
function ctld.spawnFOBCrateStatic(_country, _unitId, _point, _name)
local _crate = {
["category"] = "Fortifications",
["shape_name"] = "konteiner_red1",
["type"] = "Container red 1",
-- ["unitId"] = _unitId,
["y"] = _point.z,
["x"] = _point.x,
["name"] = _name,
["canCargo"] = false,
["heading"] = 0,
}
_crate["country"] = _country
mist.dynAddStatic(_crate)
local _spawnedCrate = StaticObject.getByName(_crate["name"])
--local _spawnedCrate = coalition.addStaticObject(_country, _crate)
return _spawnedCrate
end
function ctld.spawnFOB(_country, _unitId, _point, _name)
local _crate = {
["category"] = "Fortifications",
["type"] = "outpost",
-- ["unitId"] = _unitId,
["y"] = _point.z,
["x"] = _point.x,
["name"] = _name,
["canCargo"] = false,
["heading"] = 0,
}
_crate["country"] = _country
mist.dynAddStatic(_crate)
local _spawnedCrate = StaticObject.getByName(_crate["name"])
--local _spawnedCrate = coalition.addStaticObject(_country, _crate)
local _id = ctld.getNextUnitId()
local _tower = {
["type"] = "house2arm",
-- ["unitId"] = _id,
["rate"] = 100,
["y"] = _point.z + -36.57142857,
["x"] = _point.x + 14.85714286,
["name"] = "FOB Watchtower #" .. _id,
["category"] = "Fortifications",
["canCargo"] = false,
["heading"] = 0,
}
--coalition.addStaticObject(_country, _tower)
_tower["country"] = _country
mist.dynAddStatic(_tower)
return _spawnedCrate
end
function ctld.spawnCrate(_arguments)
local _status, _err = pcall(function(_args)
-- use the cargo weight to guess the type of unit as no way to add description :(
local _crateType = ctld.crateLookupTable[tostring(_args[2])]
local _heli = ctld.getTransportUnit(_args[1])
if _crateType ~= nil and _heli ~= nil and ctld.inAir(_heli) == false then
if ctld.inLogisticsZone(_heli) == false then
ctld.displayMessageToGroup(_heli, "You are not close enough to friendly logistics to get a crate!", 10)
return
end
if ctld.isJTACUnitType(_crateType.unit) then
local _limitHit = false
if _heli:getCoalition() == 1 then
if ctld.JTAC_LIMIT_RED == 0 then
_limitHit = true
else
ctld.JTAC_LIMIT_RED = ctld.JTAC_LIMIT_RED - 1
end
else
if ctld.JTAC_LIMIT_BLUE == 0 then
_limitHit = true
else
ctld.JTAC_LIMIT_BLUE = ctld.JTAC_LIMIT_BLUE - 1
end
end
if _limitHit then
ctld.displayMessageToGroup(_heli, "No more JTAC Crates Left!", 10)
return
end
end
local _position = _heli:getPosition()
-- check crate spam
if _heli:getPlayerName() ~= nil and ctld.crateWait[_heli:getPlayerName()] and ctld.crateWait[_heli:getPlayerName()] > timer.getTime() then
ctld.displayMessageToGroup(_heli,"Sorry you must wait "..(ctld.crateWait[_heli:getPlayerName()] - timer.getTime()).. " seconds before you can get another crate", 20)
return
end
if _heli:getPlayerName() ~= nil then
ctld.crateWait[_heli:getPlayerName()] = timer.getTime() + ctld.crateWaitTime
end
-- trigger.action.outText("Spawn Crate".._args[1].." ".._args[2],10)
local _heli = ctld.getTransportUnit(_args[1])
local _point = ctld.getPointAt12Oclock(_heli, 30)
local _unitId = ctld.getNextUnitId()
local _side = _heli:getCoalition()
local _name = string.format("%s #%i", _crateType.desc, _unitId)
local _model_type = nil
if ctld.unitDynamicCargoCapable(_heli) then
_model_type = "dynamic"
end
local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _crateType.weight, _side, _model_type)
-- add to move table
ctld.crateMove[_name] = _name
ctld.displayMessageToGroup(_heli, string.format("A %s crate weighing %s kg has been brought out and is at your 12 o'clock ", _crateType.desc, _crateType.weight), 20)
else
env.info("Couldn't find crate item to spawn")
end
end, _arguments)
if (not _status) then
env.error(string.format("CTLD ERROR: %s", _err))
end
end
function ctld.getPointAt12Oclock(_unit, _offset)
local _position = _unit:getPosition()
local _angle = math.atan2(_position.x.z, _position.x.x)
local _xOffset = math.cos(_angle) * _offset
local _yOffset = math.sin(_angle) * _offset
local _point = _unit:getPoint()
return { x = _point.x + _xOffset, z = _point.z + _yOffset, y = _point.y }
end
function ctld.troopsOnboard(_heli, _troops)
if ctld.inTransitTroops[_heli:getName()] ~= nil then
local _onboard = ctld.inTransitTroops[_heli:getName()]
if _troops then
if _onboard.troops ~= nil and _onboard.troops.units ~= nil and #_onboard.troops.units > 0 then
return true
else
return false
end
else
if _onboard.vehicles ~= nil and _onboard.vehicles.units ~= nil and #_onboard.vehicles.units > 0 then
return true
else
return false
end
end
else
return false
end
end
-- if its dropped by AI then there is no player name so return the type of unit
function ctld.getPlayerNameOrType(_heli)
if _heli:getPlayerName() == nil then
return _heli:getTypeName()
else
return _heli:getPlayerName()
end
end
function ctld.inExtractZone(_heli)
local _heliPoint = _heli:getPoint()
for _, _zoneDetails in pairs(ctld.extractZones) do
--get distance to center
local _dist = ctld.getDistance(_heliPoint, _zoneDetails.point)
if _dist <= _zoneDetails.radius then
return _zoneDetails
end
end
return false
end
-- safe to fast rope if speed is less than 0.5 Meters per second
function ctld.safeToFastRope(_heli)
if ctld.enableFastRopeInsertion == false then
return false
end
--landed or speed is less than 8 km/h and height is less than fast rope height
if (ctld.inAir(_heli) == false or (ctld.heightDiff(_heli) <= ctld.fastRopeMaximumHeight + 3.0 and mist.vec.mag(_heli:getVelocity()) < 2.2)) then
return true
end
end
function ctld.metersToFeet(_meters)
local _feet = _meters * 3.2808399
return mist.utils.round(_feet)
end
function ctld.inAir(_heli)
if _heli:inAir() == false then
return false
end
-- less than 5 cm/s a second so landed
-- BUT AI can hold a perfect hover so ignore AI
if mist.vec.mag(_heli:getVelocity()) < 0.05 and _heli:getPlayerName() ~= nil then
return false
end
return true
end
function ctld.deployTroops(_heli, _troops)
local _onboard = ctld.inTransitTroops[_heli:getName()]
-- deloy troops
if _troops then
if _onboard.troops ~= nil and #_onboard.troops.units > 0 then
if ctld.inAir(_heli) == false or ctld.safeToFastRope(_heli) then
-- check we're not in extract zone
local _extractZone = ctld.inExtractZone(_heli)
if _extractZone == false then
local _droppedTroops = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.troops, false)
if _onboard.troops.jtac or _droppedTroops:getName():lower():find("jtac") then
local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1)
table.insert(ctld.jtacGeneratedLaserCodes, _code)
ctld.JTACStart(_droppedTroops:getName(), _code)
end
if _heli:getCoalition() == 1 then
table.insert(ctld.droppedTroopsRED, _droppedTroops:getName())
else
table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName())
end
ctld.inTransitTroops[_heli:getName()].troops = nil
ctld.adaptWeightToCargo(_heli:getName())
if ctld.inAir(_heli) then
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " fast-ropped troops from " .. _heli:getTypeName() .. " into combat", 10)
else
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped troops from " .. _heli:getTypeName() .. " into combat", 10)
end
ctld.processCallback({unit = _heli, unloaded = _droppedTroops, action = "dropped_troops"})
else
--extract zone!
local _droppedCount = trigger.misc.getUserFlag(_extractZone.flag)
_droppedCount = (#_onboard.troops.units) + _droppedCount
trigger.action.setUserFlag(_extractZone.flag, _droppedCount)
ctld.inTransitTroops[_heli:getName()].troops = nil
ctld.adaptWeightToCargo(_heli:getName())
if ctld.inAir(_heli) then
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops fast-ropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10)
else
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops dropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10)
end
end
else
ctld.displayMessageToGroup(_heli, "Too high or too fast to drop troops into combat! Hover below " .. ctld.metersToFeet(ctld.fastRopeMaximumHeight) .. " feet or land.", 10)
end
end
else
if ctld.inAir(_heli) == false then
if _onboard.vehicles ~= nil and #_onboard.vehicles.units > 0 then
local _droppedVehicles = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.vehicles, true)
if _heli:getCoalition() == 1 then
table.insert(ctld.droppedVehiclesRED, _droppedVehicles:getName())
else
table.insert(ctld.droppedVehiclesBLUE, _droppedVehicles:getName())
end
ctld.inTransitTroops[_heli:getName()].vehicles = nil
ctld.adaptWeightToCargo(_heli:getName())
ctld.processCallback({unit = _heli, unloaded = _droppedVehicles, action = "dropped_vehicles"})
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped vehicles from " .. _heli:getTypeName() .. " into combat", 10)
end
end
end
end
function ctld.insertIntoTroopsArray(_troopType,_count,_troopArray,_troopName)
for _i = 1, _count do
local _unitId = ctld.getNextUnitId()
table.insert(_troopArray, { type = _troopType, unitId = _unitId, name = string.format("Dropped %s #%i", _troopName or _troopType, _unitId) })
end
return _troopArray
end
function ctld.generateTroopTypes(_side, _countOrTemplate, _country)
local _troops = {}
local _weight = 0
local _hasJTAC = false
local function getSoldiersWeight(count, additionalWeight)
local _weight = 0
for i = 1, count do
local _soldierWeight = math.random(90, 120) * ctld.SOLDIER_WEIGHT / 100
_weight = _weight + _soldierWeight + ctld.KIT_WEIGHT + additionalWeight
end
return _weight
end
if type(_countOrTemplate) == "table" then
if _countOrTemplate.aa then
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier stinger",_countOrTemplate.aa,_troops)
else
_troops = ctld.insertIntoTroopsArray("SA-18 Igla manpad",_countOrTemplate.aa,_troops)
end
_weight = _weight + getSoldiersWeight(_countOrTemplate.aa, ctld.MANPAD_WEIGHT)
end
if _countOrTemplate.inf then
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.inf,_troops)
else
_troops = ctld.insertIntoTroopsArray("Infantry AK",_countOrTemplate.inf,_troops)
end
_weight = _weight + getSoldiersWeight(_countOrTemplate.inf, ctld.RIFLE_WEIGHT)
end
if _countOrTemplate.mg then
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier M249",_countOrTemplate.mg,_troops)
else
_troops = ctld.insertIntoTroopsArray("Paratrooper AKS-74",_countOrTemplate.mg,_troops)
end
_weight = _weight + getSoldiersWeight(_countOrTemplate.mg, ctld.MG_WEIGHT)
end
if _countOrTemplate.at then
_troops = ctld.insertIntoTroopsArray("Paratrooper RPG-16",_countOrTemplate.at,_troops)
_weight = _weight + getSoldiersWeight(_countOrTemplate.at, ctld.RPG_WEIGHT)
end
if _countOrTemplate.mortar then
_troops = ctld.insertIntoTroopsArray("2B11 mortar",_countOrTemplate.mortar,_troops)
_weight = _weight + getSoldiersWeight(_countOrTemplate.mortar, ctld.MORTAR_WEIGHT)
end
if _countOrTemplate.jtac then
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.jtac,_troops, "JTAC")
else
_troops = ctld.insertIntoTroopsArray("Infantry AK",_countOrTemplate.jtac,_troops, "JTAC")
end
_hasJTAC = true
_weight = _weight + getSoldiersWeight(_countOrTemplate.jtac, ctld.JTAC_WEIGHT + ctld.RIFLE_WEIGHT)
end
else
for _i = 1, _countOrTemplate do
local _unitType = "Infantry AK"
if _side == 2 then
if _i <=2 then
_unitType = "Soldier M249"
_weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT)
elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then
_unitType = "Paratrooper RPG-16"
_weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT)
elseif ctld.spawnStinger and _i > 4 and _i <= 5 then
_unitType = "Soldier stinger"
_weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT)
else
_unitType = "Soldier M4 GRG"
_weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT)
end
else
if _i <=2 then
_unitType = "Paratrooper AKS-74"
_weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT)
elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then
_unitType = "Paratrooper RPG-16"
_weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT)
elseif ctld.spawnStinger and _i > 4 and _i <= 5 then
_unitType = "SA-18 Igla manpad"
_weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT)
else
_unitType = "Infantry AK"
_weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT)
end
end
local _unitId = ctld.getNextUnitId()
_troops[_i] = { type = _unitType, unitId = _unitId, name = string.format("Dropped %s #%i", _unitType, _unitId) }
end
end
local _groupId = ctld.getNextGroupId()
local _groupName = "Dropped Group"
if _hasJTAC then
_groupName = "Dropped JTAC Group"
end
local _details = { units = _troops, groupId = _groupId, groupName = string.format("%s %i", _groupName, _groupId), side = _side, country = _country, weight = _weight, jtac = _hasJTAC }
return _details
end
--Special F10 function for players for troops
function ctld.unloadExtractTroops(_args)
local _heli = ctld.getTransportUnit(_args[1])
if _heli == nil then
return false
end
local _extract = nil
if not ctld.inAir(_heli) then
if _heli:getCoalition() == 1 then
_extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED)
else
_extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE)
end
end
if _extract ~= nil and not ctld.troopsOnboard(_heli, true) then
-- search for nearest troops to pickup
return ctld.extractTroops({_heli:getName(), true})
else
return ctld.unloadTroops({_heli:getName(),true,true})
end
end
-- load troops onto vehicle
function ctld.loadTroops(_heli, _troops, _numberOrTemplate)
-- load troops + vehicles if c130 or herc
-- "M1045 HMMWV TOW"
-- "M1043 HMMWV Armament"
local _onboard = ctld.inTransitTroops[_heli:getName()]
--number doesnt apply to vehicles
if _numberOrTemplate == nil or (type(_numberOrTemplate) ~= "table" and type(_numberOrTemplate) ~= "number") then
_numberOrTemplate = ctld.numberOfTroops
end
if _onboard == nil then
_onboard = { troops = {}, vehicles = {} }
end
local _list
if _heli:getCoalition() == 1 then
_list = ctld.vehiclesForTransportRED
else
_list = ctld.vehiclesForTransportBLUE
end
if _troops then
_onboard.troops = ctld.generateTroopTypes(_heli:getCoalition(), _numberOrTemplate, _heli:getCountry())
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded troops into " .. _heli:getTypeName(), 10)
ctld.processCallback({unit = _heli, onboard = _onboard.troops, action = "load_troops"})
else
_onboard.vehicles = ctld.generateVehiclesForTransport(_heli:getCoalition(), _heli:getCountry())
local _count = #_list
ctld.processCallback({unit = _heli, onboard = _onboard.vehicles, action = "load_vehicles"})
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded " .. _count .. " vehicles into " .. _heli:getTypeName(), 10)
end
ctld.inTransitTroops[_heli:getName()] = _onboard
ctld.adaptWeightToCargo(_heli:getName())
end
function ctld.generateVehiclesForTransport(_side, _country)
local _vehicles = {}
local _list
if _side == 1 then
_list = ctld.vehiclesForTransportRED
else
_list = ctld.vehiclesForTransportBLUE
end
for _i, _type in ipairs(_list) do
local _unitId = ctld.getNextUnitId()
local _weight = ctld.vehiclesWeight[_type] or 2500
_vehicles[_i] = { type = _type, unitId = _unitId, name = string.format("Dropped %s #%i", _type, _unitId), weight = _weight }
end
local _groupId = ctld.getNextGroupId()
local _details = { units = _vehicles, groupId = _groupId, groupName = string.format("Dropped Group %i", _groupId), side = _side, country = _country }
return _details
end
function ctld.loadUnloadFOBCrate(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _troops = _args[2]
if _heli == nil then
return
end
if ctld.inAir(_heli) == true then
return
end
local _side = _heli:getCoalition()
local _inZone = ctld.inLogisticsZone(_heli)
local _crateOnboard = ctld.inTransitFOBCrates[_heli:getName()] ~= nil
if _inZone == false and _crateOnboard == true then
ctld.inTransitFOBCrates[_heli:getName()] = nil
local _position = _heli:getPosition()
--try to spawn at 6 oclock to us
local _angle = math.atan2(_position.x.z, _position.x.x)
local _xOffset = math.cos(_angle) * -60
local _yOffset = math.sin(_angle) * -60
local _point = _heli:getPoint()
local _side = _heli:getCoalition()
local _unitId = ctld.getNextUnitId()
local _name = string.format("FOB Crate #%i", _unitId)
local _spawnedCrate = ctld.spawnFOBCrateStatic(_heli:getCountry(), ctld.getNextUnitId(), { x = _point.x + _xOffset, z = _point.z + _yOffset }, _name)
if _side == 1 then
ctld.droppedFOBCratesRED[_name] = _name
else
ctld.droppedFOBCratesBLUE[_name] = _name
end
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " delivered a FOB Crate", 10)
ctld.displayMessageToGroup(_heli, "Delivered FOB Crate 60m at 6'oclock to you", 10)
elseif _inZone == true and _crateOnboard == true then
ctld.displayMessageToGroup(_heli, "FOB Crate dropped back to base", 10)
ctld.inTransitFOBCrates[_heli:getName()] = nil
elseif _inZone == true and _crateOnboard == false then
ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10)
ctld.inTransitFOBCrates[_heli:getName()] = true
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10)
else
-- nearest Crate
local _crates = ctld.getCratesAndDistance(_heli)
local _nearestCrate = ctld.getClosestCrate(_heli, _crates, "FOB")
if _nearestCrate ~= nil and _nearestCrate.dist < 150 then
ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10)
ctld.inTransitFOBCrates[_heli:getName()] = true
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10)
if _side == 1 then
ctld.droppedFOBCratesRED[_nearestCrate.crateUnit:getName()] = nil
else
ctld.droppedFOBCratesBLUE[_nearestCrate.crateUnit:getName()] = nil
end
--remove
_nearestCrate.crateUnit:destroy()
else
ctld.displayMessageToGroup(_heli, "There are no friendly logistic units nearby to load a FOB crate from!", 10)
end
end
end
function ctld.loadTroopsFromZone(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _troops = _args[2]
local _groupTemplate = _args[3] or ""
local _allowExtract = _args[4]
if _heli == nil then
return false
end
local _zone = ctld.inPickupZone(_heli)
if ctld.troopsOnboard(_heli, _troops) then
if _troops then
ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10)
else
ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10)
end
return false
end
local _extract
if _allowExtract then
-- first check for extractable troops regardless of if we're in a zone or not
if _troops then
if _heli:getCoalition() == 1 then
_extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED)
else
_extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE)
end
else
if _heli:getCoalition() == 1 then
_extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED)
else
_extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE)
end
end
end
if _extract ~= nil then
-- search for nearest troops to pickup
return ctld.extractTroops({_heli:getName(), _troops})
elseif _zone.inZone == true then
if _zone.limit - 1 >= 0 then
-- decrease zone counter by 1
ctld.updateZoneCounter(_zone.index, -1)
ctld.loadTroops(_heli, _troops,_groupTemplate)
return true
else
ctld.displayMessageToGroup(_heli, "This area has no more reinforcements available!", 20)
return false
end
else
if _allowExtract then
ctld.displayMessageToGroup(_heli, "You are not in a pickup zone and no one is nearby to extract", 10)
else
ctld.displayMessageToGroup(_heli, "You are not in a pickup zone", 10)
end
return false
end
end
function ctld.unloadTroops(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _troops = _args[2]
if _heli == nil then
return false
end
local _zone = ctld.inPickupZone(_heli)
if not ctld.troopsOnboard(_heli, _troops) then
ctld.displayMessageToGroup(_heli, "No one to unload", 10)
return false
else
-- troops must be onboard to get here
if _zone.inZone == true then
if _troops then
ctld.displayMessageToGroup(_heli, "Dropped troops back to base", 20)
ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].troops, action = "unload_troops_zone"})
ctld.inTransitTroops[_heli:getName()].troops = nil
else
ctld.displayMessageToGroup(_heli, "Dropped vehicles back to base", 20)
ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].vehicles, action = "unload_vehicles_zone"})
ctld.inTransitTroops[_heli:getName()].vehicles = nil
end
ctld.adaptWeightToCargo(_heli:getName())
-- increase zone counter by 1
ctld.updateZoneCounter(_zone.index, 1)
return true
elseif ctld.troopsOnboard(_heli, _troops) then
return ctld.deployTroops(_heli, _troops)
end
end
end
function ctld.extractTroops(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _troops = _args[2]
if _heli == nil then
return false
end
if ctld.inAir(_heli) then
return false
end
if ctld.troopsOnboard(_heli, _troops) then
if _troops then
ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10)
else
ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10)
end
return false
end
local _onboard = ctld.inTransitTroops[_heli:getName()]
if _onboard == nil then
_onboard = { troops = nil, vehicles = nil }
end
local _extracted = false
if _troops then
local _extractTroops
if _heli:getCoalition() == 1 then
_extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED)
else
_extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE)
end
if _extractTroops ~= nil then
local _limit = ctld.getTransportLimit(_heli:getTypeName())
local _size = #_extractTroops.group:getUnits()
if _limit < #_extractTroops.group:getUnits() then
ctld.displayMessageToGroup(_heli, "Sorry - The group of ".._size.." is too large to fit. \n\nLimit is ".._limit.." for ".._heli:getTypeName(), 20)
return
end
_onboard.troops = _extractTroops.details
_onboard.troops.weight = #_extractTroops.group:getUnits() * 130 -- default to 130kg per soldier
if _extractTroops.group:getName():lower():find("jtac") then
_onboard.troops.jtac = true
end
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted troops in " .. _heli:getTypeName() .. " from combat", 10)
if _heli:getCoalition() == 1 then
ctld.droppedTroopsRED[_extractTroops.group:getName()] = nil
else
ctld.droppedTroopsBLUE[_extractTroops.group:getName()] = nil
end
ctld.processCallback({unit = _heli, extracted = _extractTroops, action = "extract_troops"})
--remove
_extractTroops.group:destroy()
_extracted = true
else
_onboard.troops = nil
ctld.displayMessageToGroup(_heli, "No extractable troops nearby!", 20)
end
else
local _extractVehicles
if _heli:getCoalition() == 1 then
_extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED)
else
_extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE)
end
if _extractVehicles ~= nil then
_onboard.vehicles = _extractVehicles.details
if _heli:getCoalition() == 1 then
ctld.droppedVehiclesRED[_extractVehicles.group:getName()] = nil
else
ctld.droppedVehiclesBLUE[_extractVehicles.group:getName()] = nil
end
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted vehicles in " .. _heli:getTypeName() .. " from combat", 10)
ctld.processCallback({unit = _heli, extracted = _extractVehicles, action = "extract_vehicles"})
--remove
_extractVehicles.group:destroy()
_extracted = true
else
_onboard.vehicles = nil
ctld.displayMessageToGroup(_heli, "No extractable vehicles nearby!", 20)
end
end
ctld.inTransitTroops[_heli:getName()] = _onboard
ctld.adaptWeightToCargo(_heli:getName())
return _extracted
end
function ctld.checkTroopStatus(_args)
local _unitName = _args[1]
--list onboard troops, if c130
local _heli = ctld.getTransportUnit(_unitName)
if _heli == nil then
return
end
local _, _message = ctld.getWeightOfCargo(_unitName)
if _message and _message ~= "" then
ctld.displayMessageToGroup(_heli, _message, 10)
end
end
-- Removes troops from transport when it dies
function ctld.checkTransportStatus()
timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 3)
for _, _name in ipairs(ctld.transportPilotNames) do
local _transUnit = ctld.getTransportUnit(_name)
if _transUnit == nil then
--env.info("CTLD Transport Unit Dead event")
ctld.inTransitTroops[_name] = nil
ctld.inTransitFOBCrates[_name] = nil
ctld.inTransitSlingLoadCrates[_name] = nil
end
end
end
function ctld.adaptWeightToCargo(unitName)
local _weight = ctld.getWeightOfCargo(unitName)
trigger.action.setUnitInternalCargo(unitName, _weight)
end
function ctld.getWeightOfCargo(unitName)
local FOB_CRATE_WEIGHT = 800
local _weight = 0
local _description = ""
-- add troops weight
if ctld.inTransitTroops[unitName] then
local _inTransit = ctld.inTransitTroops[unitName]
if _inTransit then
local _troops = _inTransit.troops
if _troops and _troops.units then
_description = _description .. string.format("%s troops onboard (%s kg)\n", #_troops.units, _troops.weight)
_weight = _weight + _troops.weight
end
local _vehicles = _inTransit.vehicles
if _vehicles and _vehicles.units then
for _, _unit in pairs(_vehicles.units) do
_weight = _weight + _unit.weight
end
_description = _description .. string.format("%s vehicles onboard (%s kg)\n", #_vehicles.units, _weight)
end
end
end
-- add FOB crates weight
if ctld.inTransitFOBCrates[unitName] then
_weight = _weight + FOB_CRATE_WEIGHT
_description = _description .. string.format("1 FOB Crate oboard (%s kg)\n", FOB_CRATE_WEIGHT)
end
-- add simulated slingload crates weight
local _crate = ctld.inTransitSlingLoadCrates[unitName]
if _crate then
if _crate.simulatedSlingload then
_weight = _weight + _crate.weight
_description = _description .. string.format("1 %s crate onboard (%s kg)\n", _crate.desc, _crate.weight)
end
end
if _description ~= "" then
_description = _description .. string.format("Total weight of cargo : %s kg\n", _weight)
else
_description = "No cargo."
end
return _weight, _description
end
function ctld.checkHoverStatus()
timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1.0)
local _status, _result = pcall(function()
for _, _name in ipairs(ctld.transportPilotNames) do
local _reset = true
local _transUnit = ctld.getTransportUnit(_name)
--only check transports that are hovering and not planes
if _transUnit ~= nil and ctld.inTransitSlingLoadCrates[_name] == nil and ctld.inAir(_transUnit) and ctld.unitCanCarryVehicles(_transUnit) == false and not ctld.unitDynamicCargoCapable(_transUnit) then
local _crates = ctld.getCratesAndDistance(_transUnit)
for _, _crate in pairs(_crates) do
if _crate.dist < ctld.maxDistanceFromCrate and _crate.details.unit ~= "FOB" then
--check height!
local _height = _transUnit:getPoint().y - _crate.crateUnit:getPoint().y
--env.info("HEIGHT " .. _name .. " " .. _height .. " " .. _transUnit:getPoint().y .. " " .. _crate.crateUnit:getPoint().y)
-- ctld.heightDiff(_transUnit)
--env.info("HEIGHT ABOVE GROUD ".._name.." ".._height.." ".._transUnit:getPoint().y.." ".._crate.crateUnit:getPoint().y)
if _height > ctld.minimumHoverHeight and _height <= ctld.maximumHoverHeight then
local _time = ctld.hoverStatus[_transUnit:getName()]
if _time == nil then
ctld.hoverStatus[_transUnit:getName()] = ctld.hoverTime
_time = ctld.hoverTime
else
_time = ctld.hoverStatus[_transUnit:getName()] - 1
ctld.hoverStatus[_transUnit:getName()] = _time
end
if _time > 0 then
ctld.displayMessageToGroup(_transUnit, "Hovering above " .. _crate.details.desc .. " crate. \n\nHold hover for " .. _time .. " seconds! \n\nIf the countdown stops you're too far away!", 10,true)
else
ctld.hoverStatus[_transUnit:getName()] = nil
ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true)
--crates been moved once!
ctld.crateMove[_crate.crateUnit:getName()] = nil
if _transUnit:getCoalition() == 1 then
ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil
end
_crate.crateUnit:destroy()
local _copiedCrate = mist.utils.deepCopy(_crate.details)
_copiedCrate.simulatedSlingload = true
ctld.inTransitSlingLoadCrates[_name] = _copiedCrate
ctld.adaptWeightToCargo(_name)
end
_reset = false
break
elseif _height <= ctld.minimumHoverHeight then
ctld.displayMessageToGroup(_transUnit, "Too low to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5,true)
break
else
ctld.displayMessageToGroup(_transUnit, "Too high to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5, true)
break
end
end
end
end
if _reset then
ctld.hoverStatus[_name] = nil
end
end
end)
if (not _status) then
env.error(string.format("CTLD ERROR: %s", _result))
end
end
function ctld.loadNearbyCrate(_name)
local _transUnit = ctld.getTransportUnit(_name)
if _transUnit ~= nil then
if ctld.inAir(_transUnit) then
ctld.displayMessageToGroup(_transUnit, "You must land before you can load a crate!", 10,true)
return
end
if ctld.inTransitSlingLoadCrates[_name] == nil then
local _crates = ctld.getCratesAndDistance(_transUnit)
for _, _crate in pairs(_crates) do
if _crate.dist < 50.0 then
ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true)
if _transUnit:getCoalition() == 1 then
ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil
end
ctld.crateMove[_crate.crateUnit:getName()] = nil
_crate.crateUnit:destroy()
local _copiedCrate = mist.utils.deepCopy(_crate.details)
_copiedCrate.simulatedSlingload = true
ctld.inTransitSlingLoadCrates[_name] = _copiedCrate
ctld.adaptWeightToCargo(_name)
return
end
end
ctld.displayMessageToGroup(_transUnit, "No Crates within 50m to load!", 10,true)
else
-- crate onboard
ctld.displayMessageToGroup(_transUnit, "You already have a "..ctld.inTransitSlingLoadCrates[_name].desc.." crate onboard!", 10,true)
end
end
end
--check each minute if the beacons' batteries have failed, and stop them accordingly
--there's no more need to actually refresh the beacons, since we set "loop" to true.
function ctld.refreshRadioBeacons()
timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 60)
for _index, _beaconDetails in ipairs(ctld.deployedRadioBeacons) do
--trigger.action.outTextForCoalition(_beaconDetails.coalition,_beaconDetails.text,10)
if ctld.updateRadioBeacon(_beaconDetails) == false then
--search used frequencies + remove, add back to unused
for _i, _freq in ipairs(ctld.usedUHFFrequencies) do
if _freq == _beaconDetails.uhf then
table.insert(ctld.freeUHFFrequencies, _freq)
table.remove(ctld.usedUHFFrequencies, _i)
end
end
for _i, _freq in ipairs(ctld.usedVHFFrequencies) do
if _freq == _beaconDetails.vhf then
table.insert(ctld.freeVHFFrequencies, _freq)
table.remove(ctld.usedVHFFrequencies, _i)
end
end
for _i, _freq in ipairs(ctld.usedFMFrequencies) do
if _freq == _beaconDetails.fm then
table.insert(ctld.freeFMFrequencies, _freq)
table.remove(ctld.usedFMFrequencies, _i)
end
end
--clean up beacon table
table.remove(ctld.deployedRadioBeacons, _index)
end
end
end
function ctld.getClockDirection(_heli, _crate)
-- Source: Helicopter Script - Thanks!
local _position = _crate:getPosition().p -- get position of crate
local _playerPosition = _heli:getPosition().p -- get position of helicopter
local _relativePosition = mist.vec.sub(_position, _playerPosition)
local _playerHeading = mist.getHeading(_heli) -- the rest of the code determines the 'o'clock' bearing of the missile relative to the helicopter
local _headingVector = { x = math.cos(_playerHeading), y = 0, z = math.sin(_playerHeading) }
local _headingVectorPerpendicular = { x = math.cos(_playerHeading + math.pi / 2), y = 0, z = math.sin(_playerHeading + math.pi / 2) }
local _forwardDistance = mist.vec.dp(_relativePosition, _headingVector)
local _rightDistance = mist.vec.dp(_relativePosition, _headingVectorPerpendicular)
local _angle = math.atan2(_rightDistance, _forwardDistance) * 180 / math.pi
if _angle < 0 then
_angle = 360 + _angle
end
_angle = math.floor(_angle * 12 / 360 + 0.5)
if _angle == 0 then
_angle = 12
end
return _angle
end
function ctld.getCompassBearing(_ref, _unitPos)
_ref = mist.utils.makeVec3(_ref, 0) -- turn it into Vec3 if it is not already.
_unitPos = mist.utils.makeVec3(_unitPos, 0) -- turn it into Vec3 if it is not already.
local _vec = { x = _unitPos.x - _ref.x, y = _unitPos.y - _ref.y, z = _unitPos.z - _ref.z }
local _dir = mist.utils.getDir(_vec, _ref)
local _bearing = mist.utils.round(mist.utils.toDegree(_dir), 0)
return _bearing
end
function ctld.listNearbyCrates(_args)
local _message = ""
local _heli = ctld.getTransportUnit(_args[1])
if _heli == nil then
return -- no heli!
end
local _crates = ctld.getCratesAndDistance(_heli)
--sort
local _sort = function( a,b ) return a.dist < b.dist end
table.sort(_crates,_sort)
for _, _crate in pairs(_crates) do
if _crate.dist < 1000 and _crate.details.unit ~= "FOB" then
_message = string.format("%s\n%s crate - kg %i - %i m - %d o'clock", _message, _crate.details.desc, _crate.details.weight, _crate.dist, ctld.getClockDirection(_heli, _crate.crateUnit))
end
end
local _fobMsg = ""
for _, _fobCrate in pairs(_crates) do
if _fobCrate.dist < 1000 and _fobCrate.details.unit == "FOB" then
_fobMsg = _fobMsg .. string.format("FOB Crate - %d m - %d o'clock\n", _fobCrate.dist, ctld.getClockDirection(_heli, _fobCrate.crateUnit))
end
end
if _message ~= "" or _fobMsg ~= "" then
local _txt = ""
if _message ~= "" then
_txt = "Nearby Crates:\n" .. _message
end
if _fobMsg ~= "" then
if _message ~= "" then
_txt = _txt .. "\n\n"
end
_txt = _txt .. "Nearby FOB Crates (Not Slingloadable):\n" .. _fobMsg
end
ctld.displayMessageToGroup(_heli, _txt, 20)
else
--no crates nearby
local _txt = "No Nearby Crates"
ctld.displayMessageToGroup(_heli, _txt, 20)
end
end
function ctld.listFOBS(_args)
local _msg = "FOB Positions:"
local _heli = ctld.getTransportUnit(_args[1])
if _heli == nil then
return -- no heli!
end
-- get fob positions
local _fobs = ctld.getSpawnedFobs(_heli)
-- now check spawned fobs
for _, _fob in ipairs(_fobs) do
_msg = string.format("%s\nFOB @ %s", _msg, ctld.getFOBPositionString(_fob))
end
if _msg == "FOB Positions:" then
ctld.displayMessageToGroup(_heli, "Sorry, there are no active FOBs!", 20)
else
ctld.displayMessageToGroup(_heli, _msg, 20)
end
end
function ctld.getFOBPositionString(_fob)
local _lat, _lon = coord.LOtoLL(_fob:getPosition().p)
local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS)
-- local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_fob:getPosition().p)), 5)
local _message = _latLngStr
local _beaconInfo = ctld.fobBeacons[_fob:getName()]
if _beaconInfo ~= nil then
_message = string.format("%s - %.2f KHz ", _message, _beaconInfo.vhf / 1000)
_message = string.format("%s - %.2f MHz ", _message, _beaconInfo.uhf / 1000000)
_message = string.format("%s - %.2f MHz ", _message, _beaconInfo.fm / 1000000)
end
return _message
end
function ctld.displayMessageToGroup(_unit, _text, _time,_clear)
local _groupId = ctld.getGroupId(_unit)
if _groupId then
if _clear == true then
trigger.action.outTextForGroup(_groupId, _text, _time,_clear)
else
trigger.action.outTextForGroup(_groupId, _text, _time)
end
end
end
function ctld.heightDiff(_unit)
local _point = _unit:getPoint()
-- env.info("heightunit " .. _point.y)
--env.info("heightland " .. land.getHeight({ x = _point.x, y = _point.z }))
return _point.y - land.getHeight({ x = _point.x, y = _point.z })
end
--includes fob crates!
function ctld.getCratesAndDistance(_heli)
local _crates = {}
local _allCrates
if _heli:getCoalition() == 1 then
_allCrates = ctld.spawnedCratesRED
else
_allCrates = ctld.spawnedCratesBLUE
end
for _crateName, _details in pairs(_allCrates) do
--get crate
local _crate = ctld.getCrateObject(_crateName)
--in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude
if _crate ~= nil and _crate:getLife() > 0
and (ctld.inAir(_crate) == false) then
local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint())
local _crateDetails = { crateUnit = _crate, dist = _dist, details = _details }
table.insert(_crates, _crateDetails)
end
end
local _fobCrates
if _heli:getCoalition() == 1 then
_fobCrates = ctld.droppedFOBCratesRED
else
_fobCrates = ctld.droppedFOBCratesBLUE
end
for _crateName, _details in pairs(_fobCrates) do
--get crate
local _crate = ctld.getCrateObject(_crateName)
if _crate ~= nil and _crate:getLife() > 0 then
local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint())
local _crateDetails = { crateUnit = _crate, dist = _dist, details = { unit = "FOB" }, }
table.insert(_crates, _crateDetails)
end
end
return _crates
end
function ctld.getClosestCrate(_heli, _crates, _type)
local _closetCrate = nil
local _shortestDistance = -1
local _distance = 0
local _minimumDistance = 5 -- prevents dynamic cargo crates from unpacking while in cargo hold
for _, _crate in pairs(_crates) do
if (_crate.details.unit == _type or _type == nil) then
_distance = _crate.dist
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) and _distance > _minimumDistance then
_shortestDistance = _distance
_closetCrate = _crate
end
end
end
return _closetCrate
end
function ctld.findNearestAASystem(_heli,_aaSystem)
local _closestHawkGroup = nil
local _shortestDistance = -1
local _distance = 0
for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do
local _hawkGroup = Group.getByName(_groupName)
-- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails))
if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() and _hawkDetails[1].system.name == _aaSystem.name then
local _units = _hawkGroup:getUnits()
for _, _leader in pairs(_units) do
if _leader ~= nil and _leader:getLife() > 0 then
_distance = ctld.getDistance(_leader:getPoint(), _heli:getPoint())
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
_closestHawkGroup = _hawkGroup
end
break
end
end
end
end
if _closestHawkGroup ~= nil then
return { group = _closestHawkGroup, dist = _shortestDistance }
end
return nil
end
function ctld.getCrateObject(_name)
local _crate
if ctld.staticBugWorkaround then
_crate = Unit.getByName(_name)
else
_crate = StaticObject.getByName(_name)
end
return _crate
end
function ctld.unpackCrates(_arguments)
local _status, _err = pcall(function(_args)
-- trigger.action.outText("Unpack Crates".._args[1],10)
local _heli = ctld.getTransportUnit(_args[1])
if _heli ~= nil and ctld.inAir(_heli) == false then
local _crates = ctld.getCratesAndDistance(_heli)
local _crate = ctld.getClosestCrate(_heli, _crates)
if ctld.inLogisticsZone(_heli) == true or ctld.farEnoughFromLogisticZone(_heli) == false then
ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20)
return
end
if _crate ~= nil and _crate.dist < 750
and (_crate.details.unit == "FOB" or _crate.details.unit == "FOB-SMALL") then
ctld.unpackFOBCrates(_crates, _heli)
return
elseif _crate ~= nil and _crate.dist < 200 then
if ctld.forceCrateToBeMoved and ctld.crateMove[_crate.crateUnit:getName()] and not ctld.unitDynamicCargoCapable(_heli) then
ctld.displayMessageToGroup(_heli,"Sorry you must move this crate before you unpack it!", 20)
return
end
local _aaTemplate = ctld.getAATemplate(_crate.details.unit)
if _aaTemplate then
if _crate.details.unit == _aaTemplate.repair then
ctld.repairAASystem(_heli, _crate,_aaTemplate)
else
ctld.unpackAASystem(_heli, _crate, _crates,_aaTemplate)
end
return -- stop processing
-- is multi crate?
elseif _crate.details.cratesRequired ~= nil and _crate.details.cratesRequired > 1 then
-- multicrate
ctld.unpackMultiCrate(_heli, _crate, _crates)
return
else
-- single crate
local _cratePoint = _crate.crateUnit:getPoint()
local _crateName = _crate.crateUnit:getName()
-- ctld.spawnCrateStatic( _heli:getCoalition(),ctld.getNextUnitId(),{x=100,z=100},_crateName,100)
--remove crate
-- if ctld.slingLoad == false then
_crate.crateUnit:destroy()
-- end
local _spawnedGroups = ctld.spawnCrateGroup(_heli, { _cratePoint }, { _crate.details.unit })
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_crateName] = nil
else
ctld.spawnedCratesBLUE[_crateName] = nil
end
ctld.processCallback({unit = _heli, crate = _crate , spawnedGroup = _spawnedGroups, action = "unpack"})
if _crate.details.unit == "1L13 EWR" then
ctld.addEWRTask(_spawnedGroups)
-- env.info("Added EWR")
end
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed " .. _crate.details.desc .. " to the field", 10)
if ctld.isJTACUnitType(_crate.details.unit) and ctld.JTAC_dropEnabled then
local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1)
--put to the end
table.insert(ctld.jtacGeneratedLaserCodes, _code)
ctld.JTACStart(_spawnedGroups:getName(), _code) --(_jtacGroupName, _laserCode, _smoke, _lock, _colour)
end
end
else
ctld.displayMessageToGroup(_heli, "No friendly crates close enough to unpack, or crate too close to aircraft.", 20)
end
end
end, _arguments)
if (not _status) then
env.error(string.format("CTLD ERROR: %s", _err))
end
end
-- builds a fob!
function ctld.unpackFOBCrates(_crates, _heli)
if ctld.inLogisticsZone(_heli) == true then
ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20)
return
end
-- unpack multi crate
local _nearbyMultiCrates = {}
local _bigFobCrates = 0
local _smallFobCrates = 0
local _totalCrates = 0
for _, _nearbyCrate in pairs(_crates) do
if _nearbyCrate.dist < 750 then
if _nearbyCrate.details.unit == "FOB" then
_bigFobCrates = _bigFobCrates + 1
table.insert(_nearbyMultiCrates, _nearbyCrate)
elseif _nearbyCrate.details.unit == "FOB-SMALL" then
_smallFobCrates = _smallFobCrates + 1
table.insert(_nearbyMultiCrates, _nearbyCrate)
end
--catch divide by 0
if _smallFobCrates > 0 then
_totalCrates = _bigFobCrates + (_smallFobCrates/3.0)
else
_totalCrates = _bigFobCrates
end
if _totalCrates >= ctld.cratesRequiredForFOB then
break
end
end
end
--- check crate count
if _totalCrates >= ctld.cratesRequiredForFOB then
-- destroy crates
local _points = {}
for _, _crate in pairs(_nearbyMultiCrates) do
if _heli:getCoalition() == 1 then
ctld.droppedFOBCratesRED[_crate.crateUnit:getName()] = nil
ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil
else
ctld.droppedFOBCratesBLUE[_crate.crateUnit:getName()] = nil
ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil
end
table.insert(_points, _crate.crateUnit:getPoint())
--destroy
_crate.crateUnit:destroy()
end
local _centroid = ctld.getCentroid(_points)
timer.scheduleFunction(function(_args)
local _unitId = ctld.getNextUnitId()
local _name = "Deployed FOB #" .. _unitId
local _fob = ctld.spawnFOB(_args[2], _unitId, _args[1], _name)
--make it able to deploy crates
table.insert(ctld.logisticUnits, _fob:getName())
ctld.beaconCount = ctld.beaconCount + 1
local _radioBeaconName = "FOB Beacon #" .. ctld.beaconCount
local _radioBeaconDetails = ctld.createRadioBeacon(_args[1], _args[3], _args[2], _radioBeaconName, nil, true)
ctld.fobBeacons[_name] = { vhf = _radioBeaconDetails.vhf, uhf = _radioBeaconDetails.uhf, fm = _radioBeaconDetails.fm }
if ctld.troopPickupAtFOB == true then
table.insert(ctld.builtFOBS, _fob:getName())
trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates and Troops can now be picked up.", 10)
else
trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates can now be picked up.", 10)
end
end, { _centroid, _heli:getCountry(), _heli:getCoalition() }, timer.getTime() + ctld.buildTimeFOB)
local _txt = string.format("%s started building FOB using %d FOB crates, it will be finished in %d seconds.\nPosition marked with smoke.", ctld.getPlayerNameOrType(_heli), _totalCrates, ctld.buildTimeFOB)
ctld.processCallback({unit = _heli, position = _centroid, action = "fob"})
trigger.action.smoke(_centroid, trigger.smokeColor.Green)
trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10)
else
local _txt = string.format("Cannot build FOB!\n\nIt requires %d Large FOB crates ( 3 small FOB crates equal 1 large FOB Crate) and there are the equivalent of %d large FOB crates nearby\n\nOr the crates are not within 750m of each other", ctld.cratesRequiredForFOB, _totalCrates)
ctld.displayMessageToGroup(_heli, _txt, 20)
end
end
--unloads the sling crate when the helicopter is on the ground or between 4.5 - 10 meters
function ctld.dropSlingCrate(_args)
local _heli = ctld.getTransportUnit(_args[1])
if _heli == nil then
return -- no heli!
end
local _currentCrate = ctld.inTransitSlingLoadCrates[_heli:getName()]
if _currentCrate == nil then
if ctld.hoverPickup then
ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate, hover for "..ctld.hoverTime.." seconds above the crate", 10)
else
ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate - land and use F10 Crate Commands to load one.", 10)
end
else
local _heli = ctld.getTransportUnit(_args[1])
local _point = _heli:getPoint()
local _unitId = ctld.getNextUnitId()
local _side = _heli:getCoalition()
local _name = string.format("%s #%i", _currentCrate.desc, _unitId)
local _heightDiff = ctld.heightDiff(_heli)
if ctld.inAir(_heli) == false or _heightDiff <= 7.5 then
ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely unhooked and is at your 12 o'clock", 10)
_point = ctld.getPointAt12Oclock(_heli, 30)
-- elseif _heightDiff > 40.0 then
-- ctld.inTransitSlingLoadCrates[_heli:getName()] = nil
-- ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10)
-- return
elseif _heightDiff > 7.5 and _heightDiff <= 40.0 then
ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely dropped below you", 10)
else -- _heightDiff > 40.0
ctld.inTransitSlingLoadCrates[_heli:getName()] = nil
ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10)
return
end
--remove crate from cargo
ctld.inTransitSlingLoadCrates[_heli:getName()] = nil
ctld.adaptWeightToCargo(_heli:getName())
local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _currentCrate.weight,_side)
end
end
--spawns a radio beacon made up of two units,
-- one for VHF and one for UHF
-- The units are set to to NOT engage
function ctld.createRadioBeacon(_point, _coalition, _country, _name, _batteryTime, _isFOB)
local _freq = ctld.generateADFFrequencies()
--create timeout
local _battery
if _batteryTime == nil then
_battery = timer.getTime() + (ctld.deployedBeaconBattery * 60)
else
_battery = timer.getTime() + (_batteryTime * 60)
end
local _lat, _lon = coord.LOtoLL(_point)
local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS)
--local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_point)), 5)
local _freqsText = _name
if _isFOB then
-- _message = "FOB " .. _message
_battery = -1 --never run out of power!
end
_freqsText = _freqsText .. " - " .. _latLngStr
_freqsText = string.format("%.2f kHz - %.2f / %.2f MHz", _freq.vhf / 1000, _freq.uhf / 1000000, _freq.fm / 1000000)
local _uhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, _name, _freqsText)
local _vhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, _name, _freqsText)
local _fmGroup = ctld.spawnRadioBeaconUnit(_point, _country, _name, _freqsText)
local _beaconDetails = {
vhf = _freq.vhf,
vhfGroup = _vhfGroup:getName(),
uhf = _freq.uhf,
uhfGroup = _uhfGroup:getName(),
fm = _freq.fm,
fmGroup = _fmGroup:getName(),
text = _freqsText,
battery = _battery,
coalition = _coalition,
}
ctld.updateRadioBeacon(_beaconDetails)
table.insert(ctld.deployedRadioBeacons, _beaconDetails)
return _beaconDetails
end
function ctld.generateADFFrequencies()
if #ctld.freeUHFFrequencies <= 3 then
ctld.freeUHFFrequencies = ctld.usedUHFFrequencies
ctld.usedUHFFrequencies = {}
end
--remove frequency at RANDOM
local _uhf = table.remove(ctld.freeUHFFrequencies, math.random(#ctld.freeUHFFrequencies))
table.insert(ctld.usedUHFFrequencies, _uhf)
if #ctld.freeVHFFrequencies <= 3 then
ctld.freeVHFFrequencies = ctld.usedVHFFrequencies
ctld.usedVHFFrequencies = {}
end
local _vhf = table.remove(ctld.freeVHFFrequencies, math.random(#ctld.freeVHFFrequencies))
table.insert(ctld.usedVHFFrequencies, _vhf)
if #ctld.freeFMFrequencies <= 3 then
ctld.freeFMFrequencies = ctld.usedFMFrequencies
ctld.usedFMFrequencies = {}
end
local _fm = table.remove(ctld.freeFMFrequencies, math.random(#ctld.freeFMFrequencies))
table.insert(ctld.usedFMFrequencies, _fm)
return { uhf = _uhf, vhf = _vhf, fm = _fm }
--- return {uhf=_uhf,vhf=_vhf}
end
function ctld.spawnRadioBeaconUnit(_point, _country, _name, _freqsText)
local _groupId = ctld.getNextGroupId()
local _unitId = ctld.getNextUnitId()
local _radioGroup = {
["visible"] = false,
-- ["groupId"] = _groupId,
["hidden"] = false,
["units"] = {
[1] = {
["y"] = _point.z,
["type"] = "TACAN_beacon",
["name"] = "Unit #" .. _unitId .. " - " .. _name .. " [" .. _freqsText .. "]",
-- ["unitId"] = _unitId,
["heading"] = 0,
["playerCanDrive"] = true,
["skill"] = "Excellent",
["x"] = _point.x,
}
},
-- ["y"] = _positions[1].z,
-- ["x"] = _positions[1].x,
["name"] = "Group #" .. _groupId .. " - " .. _name,
["task"] = {},
--added two fields below for MIST
["category"] = Group.Category.GROUND,
["country"] = _country
}
-- return coalition.addGroup(_country, Group.Category.GROUND, _radioGroup)
return Group.getByName(mist.dynAdd(_radioGroup).name)
end
function ctld.updateRadioBeacon(_beaconDetails)
local _vhfGroup = Group.getByName(_beaconDetails.vhfGroup)
local _uhfGroup = Group.getByName(_beaconDetails.uhfGroup)
local _fmGroup = Group.getByName(_beaconDetails.fmGroup)
local _radioLoop = {}
if _vhfGroup ~= nil and _vhfGroup:getUnits() ~= nil and #_vhfGroup:getUnits() == 1 then
table.insert(_radioLoop, { group = _vhfGroup, freq = _beaconDetails.vhf, silent = false, mode = 0 })
end
if _uhfGroup ~= nil and _uhfGroup:getUnits() ~= nil and #_uhfGroup:getUnits() == 1 then
table.insert(_radioLoop, { group = _uhfGroup, freq = _beaconDetails.uhf, silent = true, mode = 0 })
end
if _fmGroup ~= nil and _fmGroup:getUnits() ~= nil and #_fmGroup:getUnits() == 1 then
table.insert(_radioLoop, { group = _fmGroup, freq = _beaconDetails.fm, silent = false, mode = 1 })
end
local _batLife = _beaconDetails.battery - timer.getTime()
if (_batLife <= 0 and _beaconDetails.battery ~= -1) or #_radioLoop ~= 3 then
-- ran out of batteries
if _vhfGroup ~= nil then
trigger.action.stopRadioTransmission(_vhfGroup:getName())
_vhfGroup:destroy()
end
if _uhfGroup ~= nil then
trigger.action.stopRadioTransmission(_uhfGroup:getName())
_uhfGroup:destroy()
end
if _fmGroup ~= nil then
trigger.action.stopRadioTransmission(_fmGroup:getName())
_fmGroup:destroy()
end
return false
end
--fobs have unlimited battery life
-- if _battery ~= -1 then
-- _text = _text.." "..mist.utils.round(_batLife).." seconds of battery"
-- end
for _, _radio in pairs(_radioLoop) do
local _groupController = _radio.group:getController()
local _sound = ctld.radioSound
if _radio.silent then
_sound = ctld.radioSoundFC3
end
_sound = "l10n/DEFAULT/".._sound
_groupController:setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD)
-- stop the transmission at each call to the ctld.updateRadioBeacon method (default each minute)
trigger.action.stopRadioTransmission(_radio.group:getName())
-- restart it as the battery is still up
-- the transmission is set to loop and has the name of the transmitting DCS group (that includes the type - i.e. FM, UHF, VHF)
trigger.action.radioTransmission(_sound, _radio.group:getUnit(1):getPoint(), _radio.mode, true, _radio.freq, 1000, _radio.group:getName())
end
return true
end
function ctld.listRadioBeacons(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _message = ""
if _heli ~= nil then
for _x, _details in pairs(ctld.deployedRadioBeacons) do
if _details.coalition == _heli:getCoalition() then
_message = _message .. _details.text .. "\n"
end
end
if _message ~= "" then
ctld.displayMessageToGroup(_heli, "Radio Beacons:\n" .. _message, 20)
else
ctld.displayMessageToGroup(_heli, "No Active Radio Beacons", 20)
end
end
end
function ctld.dropRadioBeacon(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _message = ""
if _heli ~= nil and ctld.inAir(_heli) == false then
--deploy 50 m infront
--try to spawn at 12 oclock to us
local _point = ctld.getPointAt12Oclock(_heli, 50)
ctld.beaconCount = ctld.beaconCount + 1
local _name = "Beacon #" .. ctld.beaconCount
local _radioBeaconDetails = ctld.createRadioBeacon(_point, _heli:getCoalition(), _heli:getCountry(), _name, nil, false)
-- mark with flare?
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " deployed a Radio Beacon.\n\n" .. _radioBeaconDetails.text, 20)
else
ctld.displayMessageToGroup(_heli, "You need to land before you can deploy a Radio Beacon!", 20)
end
end
--remove closet radio beacon
function ctld.removeRadioBeacon(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _message = ""
if _heli ~= nil and ctld.inAir(_heli) == false then
-- mark with flare?
local _closetBeacon = nil
local _shortestDistance = -1
local _distance = 0
for _x, _details in pairs(ctld.deployedRadioBeacons) do
if _details.coalition == _heli:getCoalition() then
local _group = Group.getByName(_details.vhfGroup)
if _group ~= nil and #_group:getUnits() == 1 then
_distance = ctld.getDistance(_heli:getPoint(), _group:getUnit(1):getPoint())
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
_closetBeacon = _details
end
end
end
end
if _closetBeacon ~= nil and _shortestDistance then
local _vhfGroup = Group.getByName(_closetBeacon.vhfGroup)
local _uhfGroup = Group.getByName(_closetBeacon.uhfGroup)
local _fmGroup = Group.getByName(_closetBeacon.fmGroup)
if _vhfGroup ~= nil then
trigger.action.stopRadioTransmission(_vhfGroup:getName())
_vhfGroup:destroy()
end
if _uhfGroup ~= nil then
trigger.action.stopRadioTransmission(_uhfGroup:getName())
_uhfGroup:destroy()
end
if _fmGroup ~= nil then
trigger.action.stopRadioTransmission(_fmGroup:getName())
_fmGroup:destroy()
end
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " removed a Radio Beacon.\n\n" .. _closetBeacon.text, 20)
else
ctld.displayMessageToGroup(_heli, "No Radio Beacons within 500m.", 20)
end
else
ctld.displayMessageToGroup(_heli, "You need to land before remove a Radio Beacon", 20)
end
end
-- gets the center of a bunch of points!
-- return proper DCS point with height
function ctld.getCentroid(_points)
local _tx, _ty = 0, 0
for _index, _point in ipairs(_points) do
_tx = _tx + _point.x
_ty = _ty + _point.z
end
local _npoints = #_points
local _point = { x = _tx / _npoints, z = _ty / _npoints }
_point.y = land.getHeight({ _point.x, _point.z })
return _point
end
function ctld.getAATemplate(_unitName)
for _,_system in pairs(ctld.AASystemTemplate) do
if _system.repair == _unitName then
return _system
end
for _,_part in pairs(_system.parts) do
if _unitName == _part.name then
return _system
end
end
end
return nil
end
function ctld.getLauncherUnitFromAATemplate(_aaTemplate)
for _,_part in pairs(_aaTemplate.parts) do
if _part.launcher then
return _part.name
end
end
return nil
end
function ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates, _aaSystemTemplate)
-- are we adding to existing aa system?
-- check to see if the crate is a launcher
if ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate) == _nearestCrate.details.unit then
-- find nearest COMPLETE AA system
local _nearestSystem = ctld.findNearestAASystem(_heli, _aaSystemTemplate)
if _nearestSystem ~= nil and _nearestSystem.dist < 300 then
local _uniqueTypes = {} -- stores each unique part of system
local _types = {}
local _points = {}
local _units = _nearestSystem.group:getUnits()
if _units ~= nil and #_units > 0 then
for x = 1, #_units do
if _units[x]:getLife() > 0 then
--this allows us to count each type once
_uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName()
table.insert(_points, _units[x]:getPoint())
table.insert(_types, _units[x]:getTypeName())
end
end
end
-- do we have the correct number of unique pieces and do we have enough points for all the pieces
if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then
-- rearm aa system
-- destroy old group
ctld.completeAASystems[_nearestSystem.group:getName()] = nil
_nearestSystem.group:destroy()
local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types)
ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup, _aaSystemTemplate)
ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "rearm"})
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully rearmed a full ".._aaSystemTemplate.name.." in the field", 10)
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil
end
-- remove crate
-- if ctld.slingLoad == false then
_nearestCrate.crateUnit:destroy()
-- end
return true -- all done so quit
end
end
end
return false
end
function ctld.getAASystemDetails(_hawkGroup,_aaSystemTemplate)
local _units = _hawkGroup:getUnits()
local _hawkDetails = {}
for _, _unit in pairs(_units) do
table.insert(_hawkDetails, { point = _unit:getPoint(), unit = _unit:getTypeName(), name = _unit:getName(), system =_aaSystemTemplate})
end
return _hawkDetails
end
function ctld.countTableEntries(_table)
if _table == nil then
return 0
end
local _count = 0
for _key, _value in pairs(_table) do
_count = _count + 1
end
return _count
end
function ctld.unpackAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate)
if ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate) then
-- rearmed hawk
return
end
-- are there all the pieces close enough together
local _systemParts = {}
--initialise list of parts
for _,_part in pairs(_aaSystemTemplate.parts) do
_systemParts[_part.name] = {name = _part.name,desc = _part.desc,found = false}
end
-- find all nearest crates and add them to the list if they're part of the AA System
for _, _nearbyCrate in pairs(_nearbyCrates) do
if _nearbyCrate.dist < 500 then
if _systemParts[_nearbyCrate.details.unit] ~= nil and _systemParts[_nearbyCrate.details.unit].found == false then
local _foundPart = _systemParts[_nearbyCrate.details.unit]
_foundPart.found = true
_foundPart.crate = _nearbyCrate
_systemParts[_nearbyCrate.details.unit] = _foundPart
end
end
end
local _count = 0
local _txt = ""
local _posArray = {}
local _typeArray = {}
for _name, _systemPart in pairs(_systemParts) do
if _systemPart.found == false then
_txt = _txt.."Missing ".._systemPart.desc.."\n"
else
local _launcherPart = ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate)
--handle multiple launchers from one crate
if (_name == "Hawk ln" and ctld.hawkLaunchers > 1)
or (_launcherPart == _name and ctld.aaLaunchers > 1) then
--add multiple launcher
local _launchers = ctld.aaLaunchers
if _name == "Hawk ln" then
_launchers = ctld.hawkLaunchers
end
for _i = 1, _launchers do
-- spawn in a circle around the crate
local _angle = math.pi * 2 * (_i - 1) / _launchers
local _xOffset = math.cos(_angle) * 12
local _yOffset = math.sin(_angle) * 12
local _point = _systemPart.crate.crateUnit:getPoint()
_point = { x = _point.x + _xOffset, y = _point.y, z = _point.z + _yOffset }
table.insert(_posArray, _point)
table.insert(_typeArray, _name)
end
else
table.insert(_posArray, _systemPart.crate.crateUnit:getPoint())
table.insert(_typeArray, _name)
end
end
end
local _activeLaunchers = ctld.countCompleteAASystems(_heli)
local _allowed = ctld.getAllowedAASystems(_heli)
env.info("Active: ".._activeLaunchers.." Allowed: ".._allowed)
if _activeLaunchers + 1 > _allowed then
trigger.action.outTextForCoalition(_heli:getCoalition(), "Out of parts for AA Systems. Current limit is ".._allowed.." \n", 10)
return
end
if _txt ~= "" then
ctld.displayMessageToGroup(_heli, "Cannot build ".._aaSystemTemplate.name.."\n" .. _txt .. "\n\nOr the crates are not close enough together", 20)
return
else
-- destroy crates
for _name, _systemPart in pairs(_systemParts) do
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_systemPart.crate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_systemPart.crate.crateUnit:getName()] = nil
end
--destroy
-- if ctld.slingLoad == false then
_systemPart.crate.crateUnit:destroy()
--end
end
-- HAWK / BUK READY!
local _spawnedGroup = ctld.spawnCrateGroup(_heli, _posArray, _typeArray)
ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystemTemplate)
ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"})
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed a full ".._aaSystemTemplate.name.." to the field. \n\nAA Active System limit is: ".._allowed.."\nActive: "..(_activeLaunchers+1), 10)
end
end
--count the number of captured cities, sets the amount of allowed AA Systems
function ctld.getAllowedAASystems(_heli)
if _heli:getCoalition() == 1 then
return ctld.AASystemLimitBLUE
else
return ctld.AASystemLimitRED
end
end
function ctld.countCompleteAASystems(_heli)
local _count = 0
for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do
local _hawkGroup = Group.getByName(_groupName)
-- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails))
if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() then
local _units = _hawkGroup:getUnits()
if _units ~=nil and #_units > 0 then
--get the system template
local _aaSystemTemplate = _hawkDetails[1].system
local _uniqueTypes = {} -- stores each unique part of system
local _types = {}
local _points = {}
if _units ~= nil and #_units > 0 then
for x = 1, #_units do
if _units[x]:getLife() > 0 then
--this allows us to count each type once
_uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName()
table.insert(_points, _units[x]:getPoint())
table.insert(_types, _units[x]:getTypeName())
end
end
end
-- do we have the correct number of unique pieces and do we have enough points for all the pieces
if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then
_count = _count +1
end
end
end
end
return _count
end
function ctld.repairAASystem(_heli, _nearestCrate,_aaSystem)
-- find nearest COMPLETE AA system
local _nearestHawk = ctld.findNearestAASystem(_heli,_aaSystem)
if _nearestHawk ~= nil and _nearestHawk.dist < 300 then
local _oldHawk = ctld.completeAASystems[_nearestHawk.group:getName()]
--spawn new one
local _types = {}
local _points = {}
for _, _part in pairs(_oldHawk) do
table.insert(_points, _part.point)
table.insert(_types, _part.unit)
end
--remove old system
ctld.completeAASystems[_nearestHawk.group:getName()] = nil
_nearestHawk.group:destroy()
local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types)
ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystem)
ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "repair"})
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully repaired a full ".._aaSystem.name.." in the field", 10)
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil
end
-- remove crate
-- if ctld.slingLoad == false then
_nearestCrate.crateUnit:destroy()
-- end
else
ctld.displayMessageToGroup(_heli, "Cannot repair ".._aaSystem.name..". No damaged ".._aaSystem.name.." within 300m", 10)
end
end
function ctld.unpackMultiCrate(_heli, _nearestCrate, _nearbyCrates)
-- unpack multi crate
local _nearbyMultiCrates = {}
for _, _nearbyCrate in pairs(_nearbyCrates) do
if _nearbyCrate.dist < 300 then
if _nearbyCrate.details.unit == _nearestCrate.details.unit then
table.insert(_nearbyMultiCrates, _nearbyCrate)
if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then
break
end
end
end
end
--- check crate count
if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then
local _point = _nearestCrate.crateUnit:getPoint()
-- destroy crates
for _, _crate in pairs(_nearbyMultiCrates) do
if _point == nil then
_point = _crate.crateUnit:getPoint()
end
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil
end
--destroy
-- if ctld.slingLoad == false then
_crate.crateUnit:destroy()
-- end
end
local _spawnedGroup = ctld.spawnCrateGroup(_heli, { _point }, { _nearestCrate.details.unit })
ctld.setGrpROE(_spawnedGroup)
ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"})
local _txt = string.format("%s successfully deployed %s to the field using %d crates", ctld.getPlayerNameOrType(_heli), _nearestCrate.details.desc, #_nearbyMultiCrates)
trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10)
else
local _txt = string.format("Cannot build %s!\n\nIt requires %d crates and there are %d \n\nOr the crates are not within 300m of each other", _nearestCrate.details.desc, _nearestCrate.details.cratesRequired, #_nearbyMultiCrates)
ctld.displayMessageToGroup(_heli, _txt, 20)
end
end
function ctld.spawnCrateGroup(_heli, _positions, _types)
local _id = ctld.getNextGroupId()
local _groupName = _types[1] .. " #" .. _id
local _side = _heli:getCoalition()
local _group = {
["visible"] = false,
-- ["groupId"] = _id,
["hidden"] = false,
["units"] = {},
-- ["y"] = _positions[1].z,
-- ["x"] = _positions[1].x,
["name"] = _groupName,
["task"] = {},
}
if #_positions == 1 then
local _unitId = ctld.getNextUnitId()
local _details = { type = _types[1], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[1], _unitId) }
_group.units[1] = ctld.createUnit(_positions[1].x + 5, _positions[1].z + 5, 120, _details)
else
for _i, _pos in ipairs(_positions) do
local _unitId = ctld.getNextUnitId()
local _details = { type = _types[_i], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[_i], _unitId) }
_group.units[_i] = ctld.createUnit(_pos.x + 5, _pos.z + 5, 120, _details)
end
end
--mist function
_group.category = Group.Category.GROUND
_group.country = _heli:getCountry()
local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name)
return _spawnedGroup
end
-- spawn normal group
function ctld.spawnDroppedGroup(_point, _details, _spawnBehind, _maxSearch)
local _groupName = _details.groupName
local _group = {
["visible"] = false,
-- ["groupId"] = _details.groupId,
["hidden"] = false,
["units"] = {},
-- ["y"] = _positions[1].z,
-- ["x"] = _positions[1].x,
["name"] = _groupName,
["task"] = {},
}
if _spawnBehind == false then
-- spawn in circle around heli
local _pos = _point
for _i, _detail in ipairs(_details.units) do
local _angle = math.pi * 2 * (_i - 1) / #_details.units
local _xOffset = math.cos(_angle) * 30
local _yOffset = math.sin(_angle) * 30
_group.units[_i] = ctld.createUnit(_pos.x + _xOffset, _pos.z + _yOffset, _angle, _detail)
end
else
local _pos = _point
--try to spawn at 6 oclock to us
local _angle = math.atan2(_pos.z, _pos.x)
local _xOffset = math.cos(_angle) * -30
local _yOffset = math.sin(_angle) * -30
for _i, _detail in ipairs(_details.units) do
_group.units[_i] = ctld.createUnit(_pos.x + (_xOffset + 10 * _i), _pos.z + (_yOffset + 10 * _i), _angle, _detail)
end
end
--switch to MIST
_group.category = Group.Category.GROUND;
_group.country = _details.country;
local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name)
--local _spawnedGroup = coalition.addGroup(_details.country, Group.Category.GROUND, _group)
-- find nearest enemy and head there
if _maxSearch == nil then
_maxSearch = ctld.maximumSearchDistance
end
local _wpZone = ctld.inWaypointZone(_point,_spawnedGroup:getCoalition())
if _wpZone.inZone then
ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _wpZone.point)
env.info("Heading to waypoint - In Zone ".._wpZone.name)
else
local _enemyPos = ctld.findNearestEnemy(_details.side, _point, _maxSearch)
ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _enemyPos)
end
return _spawnedGroup
end
function ctld.findNearestEnemy(_side, _point, _searchDistance)
local _closestEnemy = nil
local _groups
local _closestEnemyDist = _searchDistance
local _heliPoint = _point
if _side == 2 then
_groups = coalition.getGroups(1, Group.Category.GROUND)
else
_groups = coalition.getGroups(2, Group.Category.GROUND)
end
for _, _group in pairs(_groups) do
if _group ~= nil then
local _units = _group:getUnits()
if _units ~= nil and #_units > 0 then
local _leader = nil
-- find alive leader
for x = 1, #_units do
if _units[x]:getLife() > 0 then
_leader = _units[x]
break
end
end
if _leader ~= nil then
local _leaderPos = _leader:getPoint()
local _dist = ctld.getDistance(_heliPoint, _leaderPos)
if _dist < _closestEnemyDist then
_closestEnemyDist = _dist
_closestEnemy = _leaderPos
end
end
end
end
end
-- no enemy - move to random point
if _closestEnemy ~= nil then
-- env.info("found enemy")
return _closestEnemy
else
local _x = _heliPoint.x + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance)
local _z = _heliPoint.z + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance)
local _y = _heliPoint.y + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance)
return { x = _x, z = _z,y=_y }
end
end
function ctld.findNearestGroup(_heli, _groups)
local _closestGroupDetails = {}
local _closestGroup = nil
local _closestGroupDist = ctld.maxExtractDistance
local _heliPoint = _heli:getPoint()
for _, _groupName in pairs(_groups) do
local _group = Group.getByName(_groupName)
if _group ~= nil then
local _units = _group:getUnits()
if _units ~= nil and #_units > 0 then
local _leader = nil
local _groupDetails = { groupId = _group:getID(), groupName = _group:getName(), side = _group:getCoalition(), units = {} }
-- find alive leader
for x = 1, #_units do
if _units[x]:getLife() > 0 then
if _leader == nil then
_leader = _units[x]
-- set country based on leader
_groupDetails.country = _leader:getCountry()
end
local _unitDetails = { type = _units[x]:getTypeName(), unitId = _units[x]:getID(), name = _units[x]:getName() }
table.insert(_groupDetails.units, _unitDetails)
end
end
if _leader ~= nil then
local _leaderPos = _leader:getPoint()
local _dist = ctld.getDistance(_heliPoint, _leaderPos)
if _dist < _closestGroupDist then
_closestGroupDist = _dist
_closestGroupDetails = _groupDetails
_closestGroup = _group
end
end
end
end
end
if _closestGroup ~= nil then
return { group = _closestGroup, details = _closestGroupDetails }
else
return nil
end
end
function ctld.createUnit(_x, _y, _angle, _details)
local _newUnit = {
["y"] = _y,
["type"] = _details.type,
["name"] = _details.name,
-- ["unitId"] = _details.unitId,
["heading"] = _angle,
["playerCanDrive"] = true,
["skill"] = "Excellent",
["x"] = _x,
}
return _newUnit
end
function ctld.addEWRTask(_group)
-- delayed 2 second to work around bug
timer.scheduleFunction(function(_ewrGroup)
local _grp = ctld.getAliveGroup(_ewrGroup)
if _grp ~= nil then
local _controller = _grp:getController();
local _EWR = {
id = 'EWR',
auto = true,
params = {
}
}
_controller:setTask(_EWR)
end
end
, _group:getName(), timer.getTime() + 2)
end
function ctld.orderGroupToMoveToPoint(_leader, _destination)
local _group = _leader:getGroup()
local _path = {}
table.insert(_path, mist.ground.buildWP(_leader:getPoint(), 'Off Road', 50))
table.insert(_path, mist.ground.buildWP(_destination, 'Off Road', 50))
local _mission = {
id = 'Mission',
params = {
route = {
points =_path
},
},
}
-- delayed 2 second to work around bug
timer.scheduleFunction(function(_arg)
local _grp = ctld.getAliveGroup(_arg[1])
if _grp ~= nil then
local _controller = _grp:getController();
Controller.setOption(_controller, AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO)
Controller.setOption(_controller, AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE)
_controller:setTask(_arg[2])
end
end
, {_group:getName(), _mission}, timer.getTime() + 2)
end
-- are we in pickup zone
function ctld.inPickupZone(_heli)
if ctld.inAir(_heli) then
return { inZone = false, limit = -1, index = -1 }
end
local _heliPoint = _heli:getPoint()
for _i, _zoneDetails in pairs(ctld.pickupZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
if _triggerZone == nil then
local _ship = ctld.getTransportUnit(_zoneDetails[1])
if _ship then
local _point = _ship:getPoint()
_triggerZone = {}
_triggerZone.point = _point
_triggerZone.radius = 200 -- should be big enough for ship
end
end
if _triggerZone ~= nil then
--get distance to center
local _dist = ctld.getDistance(_heliPoint, _triggerZone.point)
if _dist <= _triggerZone.radius then
local _heliCoalition = _heli:getCoalition()
if _zoneDetails[4] == 1 and (_zoneDetails[5] == _heliCoalition or _zoneDetails[5] == 0) then
return { inZone = true, limit = _zoneDetails[3], index = _i }
end
end
end
end
local _fobs = ctld.getSpawnedFobs(_heli)
-- now check spawned fobs
for _, _fob in ipairs(_fobs) do
--get distance to center
local _dist = ctld.getDistance(_heliPoint, _fob:getPoint())
if _dist <= 150 then
return { inZone = true, limit = 10000, index = -1 };
end
end
return { inZone = false, limit = -1, index = -1 };
end
function ctld.getSpawnedFobs(_heli)
local _fobs = {}
for _, _fobName in ipairs(ctld.builtFOBS) do
local _fob = StaticObject.getByName(_fobName)
if _fob ~= nil and _fob:isExist() and _fob:getCoalition() == _heli:getCoalition() and _fob:getLife() > 0 then
table.insert(_fobs, _fob)
end
end
return _fobs
end
-- are we in a dropoff zone
function ctld.inDropoffZone(_heli)
if ctld.inAir(_heli) then
return false
end
local _heliPoint = _heli:getPoint()
for _, _zoneDetails in pairs(ctld.dropOffZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
if _triggerZone ~= nil and (_zoneDetails[3] == _heli:getCoalition() or _zoneDetails[3]== 0) then
--get distance to center
local _dist = ctld.getDistance(_heliPoint, _triggerZone.point)
if _dist <= _triggerZone.radius then
return true
end
end
end
return false
end
-- are we in a waypoint zone
function ctld.inWaypointZone(_point,_coalition)
for _, _zoneDetails in pairs(ctld.wpZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
--right coalition and active?
if _triggerZone ~= nil and (_zoneDetails[4] == _coalition or _zoneDetails[4]== 0) and _zoneDetails[3] == 1 then
--get distance to center
local _dist = ctld.getDistance(_point, _triggerZone.point)
if _dist <= _triggerZone.radius then
return {inZone = true, point = _triggerZone.point, name = _zoneDetails[1]}
end
end
end
return {inZone = false}
end
-- are we near friendly logistics zone
function ctld.inLogisticsZone(_heli)
if ctld.inAir(_heli) then
return false
end
local _heliPoint = _heli:getPoint()
for _, _name in pairs(ctld.logisticUnits) do
local _logistic = StaticObject.getByName(_name)
if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then
--get distance
local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint())
if _dist <= ctld.maximumDistanceLogistic then
return true
end
end
end
return false
end
-- are far enough from a friendly logistics zone
function ctld.farEnoughFromLogisticZone(_heli)
if ctld.inAir(_heli) then
return false
end
local _heliPoint = _heli:getPoint()
local _farEnough = true
for _, _name in pairs(ctld.logisticUnits) do
local _logistic = StaticObject.getByName(_name)
if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then
--get distance
local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint())
-- env.info("DIST ".._dist)
if _dist <= ctld.minimumDeployDistance then
-- env.info("TOO CLOSE ".._dist)
_farEnough = false
end
end
end
return _farEnough
end
function ctld.refreshSmoke()
if ctld.disableAllSmoke == true then
return
end
for _, _zoneGroup in pairs({ ctld.pickupZones, ctld.dropOffZones }) do
for _, _zoneDetails in pairs(_zoneGroup) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
if _triggerZone == nil then
local _ship = ctld.getTransportUnit(_triggerZone)
if _ship then
local _point = _ship:getPoint()
_triggerZone = {}
_triggerZone.point = _point
end
end
--only trigger if smoke is on AND zone is active
if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[4] == 1 then
-- Trigger smoke markers
local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
trigger.action.smoke(_pos3, _zoneDetails[2])
end
end
end
--waypoint zones
for _, _zoneDetails in pairs(ctld.wpZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
--only trigger if smoke is on AND zone is active
if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[3] == 1 then
-- Trigger smoke markers
local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y }
trigger.action.smoke(_pos3, _zoneDetails[2])
end
end
--refresh in 5 minutes
timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 300)
end
function ctld.dropSmoke(_args)
local _heli = ctld.getTransportUnit(_args[1])
if _heli ~= nil then
local _colour = ""
if _args[2] == trigger.smokeColor.Red then
_colour = "RED"
elseif _args[2] == trigger.smokeColor.Blue then
_colour = "BLUE"
elseif _args[2] == trigger.smokeColor.Green then
_colour = "GREEN"
elseif _args[2] == trigger.smokeColor.Orange then
_colour = "ORANGE"
end
local _point = _heli:getPoint()
local _pos2 = { x = _point.x, y = _point.z }
local _alt = land.getHeight(_pos2)
local _pos3 = { x = _point.x, y = _alt, z = _point.z }
trigger.action.smoke(_pos3, _args[2])
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped " .. _colour .. " smoke ", 10)
end
end
function ctld.unitCanCarryVehicles(_unit)
local _type = string.lower(_unit:getTypeName())
for _, _name in ipairs(ctld.vehicleTransportEnabled) do
local _nameLower = string.lower(_name)
if string.find(_type, _nameLower, 1, true) then
return true
end
end
return false
end
function ctld.unitDynamicCargoCapable(_unit)
local _type = string.lower(_unit:getTypeName())
for _, _name in ipairs(ctld.dynamicCargoUnits) do
local _nameLower = string.lower(_name)
if string.find(_type, _nameLower, 1, true) then --string.match does not work with patterns containing '-' as it is a magic character
return true
end
end
return false
end
function ctld.isJTACUnitType(_type)
_type = string.lower(_type)
for _, _name in ipairs(ctld.jtacUnitTypes) do
local _nameLower = string.lower(_name)
if string.match(_type, _nameLower) then
return true
end
end
return false
end
function ctld.updateZoneCounter(_index, _diff)
if ctld.pickupZones[_index] ~= nil then
ctld.pickupZones[_index][3] = ctld.pickupZones[_index][3] + _diff
if ctld.pickupZones[_index][3] < 0 then
ctld.pickupZones[_index][3] = 0
end
if ctld.pickupZones[_index][6] ~= nil then
trigger.action.setUserFlag(ctld.pickupZones[_index][6], ctld.pickupZones[_index][3])
end
-- env.info(ctld.pickupZones[_index][1].." = " ..ctld.pickupZones[_index][3])
end
end
function ctld.processCallback(_callbackArgs)
for _, _callback in pairs(ctld.callbacks) do
local _status, _result = pcall(function()
_callback(_callbackArgs)
end)
if (not _status) then
env.error(string.format("CTLD Callback Error: %s", _result))
end
end
end
-- checks the status of all AI troop carriers and auto loads and unloads troops
-- as long as the troops are on the ground
function ctld.checkAIStatus()
timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 2)
for _, _unitName in pairs(ctld.transportPilotNames) do
local status, error = pcall(function()
local _unit = ctld.getTransportUnit(_unitName)
-- no player name means AI!
if _unit ~= nil and _unit:getPlayerName() == nil then
local _zone = ctld.inPickupZone(_unit)
-- env.error("Checking.. ".._unit:getName())
if _zone.inZone == true and not ctld.troopsOnboard(_unit, true) then
-- env.error("in zone, loading.. ".._unit:getName())
if ctld.allowRandomAiTeamPickups == true then
-- Random troop pickup implementation
local _team = nil
if _unit:getCoalition() == 1 then
_team = math.floor((math.random(#ctld.redTeams * 100) / 100) + 1)
ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.redTeams[_team]],true })
else
_team = math.floor((math.random(#ctld.blueTeams * 100) / 100) + 1)
ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.blueTeams[_team]],true })
end
else
ctld.loadTroopsFromZone({ _unitName, true,"",true })
end
elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, true) then
-- env.error("in dropoff zone, unloading.. ".._unit:getName())
ctld.unloadTroops( { _unitName, true })
end
if ctld.unitCanCarryVehicles(_unit) then
if _zone.inZone == true and not ctld.troopsOnboard(_unit, false) then
ctld.loadTroopsFromZone({ _unitName, false,"",true })
elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, false) then
ctld.unloadTroops( { _unitName, false })
end
end
end
end)
if (not status) then
env.error(string.format("Error with ai status: %s", error), false)
end
end
end
function ctld.getTransportLimit(_unitType)
if ctld.unitLoadLimits[_unitType] then
return ctld.unitLoadLimits[_unitType]
end
return ctld.numberOfTroops
end
function ctld.getUnitActions(_unitType)
if ctld.unitActions[_unitType] then
return ctld.unitActions[_unitType]
end
return {crates=true,troops=true}
end
-- Adds menuitem to all heli units that are active
function ctld.addF10MenuOptions()
-- Loop through all Heli units
timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 10)
for _, _unitName in pairs(ctld.transportPilotNames) do
local status, error = pcall(function()
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil then
local _groupId = ctld.getGroupId(_unit)
if _groupId then
if ctld.addedTo[tostring(_groupId)] == nil then
local _rootPath = missionCommands.addSubMenuForGroup(_groupId, "CTLD")
local _unitActions = ctld.getUnitActions(_unit:getTypeName())
missionCommands.addCommandForGroup(_groupId, "Check Cargo", _rootPath, ctld.checkTroopStatus, { _unitName })
if _unitActions.troops then
local _troopCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Troop Transport", _rootPath)
missionCommands.addCommandForGroup(_groupId, "Unload / Extract Troops", _troopCommandsPath, ctld.unloadExtractTroops, { _unitName })
-- local _loadPath = missionCommands.addSubMenuForGroup(_groupId, "Load From Zone", _troopCommandsPath)
local _transportLimit = ctld.getTransportLimit(_unit:getTypeName())
for _,_loadGroup in pairs(ctld.loadableGroups) do
if not _loadGroup.side or _loadGroup.side == _unit:getCoalition() then
-- check size & unit
if _transportLimit >= _loadGroup.total then
missionCommands.addCommandForGroup(_groupId, "Load ".._loadGroup.name, _troopCommandsPath, ctld.loadTroopsFromZone, { _unitName, true,_loadGroup,false })
end
end
end
if ctld.unitCanCarryVehicles(_unit) then
local _vehicleCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Vehicle / FOB Transport", _rootPath)
missionCommands.addCommandForGroup(_groupId, "Unload Vehicles", _vehicleCommandsPath, ctld.unloadTroops, { _unitName, false })
missionCommands.addCommandForGroup(_groupId, "Load / Extract Vehicles", _vehicleCommandsPath, ctld.loadTroopsFromZone, { _unitName, false,"",true })
if ctld.enabledFOBBuilding and ctld.staticBugWorkaround == false then
missionCommands.addCommandForGroup(_groupId, "Load / Unload FOB Crate", _vehicleCommandsPath, ctld.loadUnloadFOBCrate, { _unitName, false })
end
missionCommands.addCommandForGroup(_groupId, "Check Cargo", _vehicleCommandsPath, ctld.checkTroopStatus, { _unitName })
end
end
if ctld.enableCrates and _unitActions.crates then
if ctld.unitCanCarryVehicles(_unit) == false then
-- local _cratePath = missionCommands.addSubMenuForGroup(_groupId, "Spawn Crate", _rootPath)
-- add menu for spawning crates
for _subMenuName, _crates in pairs(ctld.spawnableCrates) do
local _cratePath = missionCommands.addSubMenuForGroup(_groupId, _subMenuName, _rootPath)
for _, _crate in pairs(_crates) do
if ctld.isJTACUnitType(_crate.unit) == false
or (ctld.isJTACUnitType(_crate.unit) == true and ctld.JTAC_dropEnabled) then
if _crate.side == nil or (_crate.side == _unit:getCoalition()) then
local _crateRadioMsg = _crate.desc
--add in the number of crates required to build something
if _crate.cratesRequired ~= nil and _crate.cratesRequired > 1 then
_crateRadioMsg = _crateRadioMsg.." (".._crate.cratesRequired..")"
end
missionCommands.addCommandForGroup(_groupId,_crateRadioMsg, _cratePath, ctld.spawnCrate, { _unitName, _crate.weight })
end
end
end
end
end
end
if (ctld.enabledFOBBuilding or ctld.enableCrates) and _unitActions.crates then
local _crateCommands = missionCommands.addSubMenuForGroup(_groupId, "CTLD Commands", _rootPath)
if ctld.hoverPickup == false or ctld.loadCrateFromMenu == true then
if ctld.slingLoad == false then
missionCommands.addCommandForGroup(_groupId, "Load Nearby Crate", _crateCommands, ctld.loadNearbyCrate, _unitName )
end
end
missionCommands.addCommandForGroup(_groupId, "Unpack Any Crate", _crateCommands, ctld.unpackCrates, { _unitName })
if ctld.slingLoad == false then
missionCommands.addCommandForGroup(_groupId, "Drop Crate", _crateCommands, ctld.dropSlingCrate, { _unitName })
end
missionCommands.addCommandForGroup(_groupId, "List Nearby Crates", _crateCommands, ctld.listNearbyCrates, { _unitName })
if ctld.enabledFOBBuilding then
missionCommands.addCommandForGroup(_groupId, "List FOBs", _crateCommands, ctld.listFOBS, { _unitName })
end
end
if ctld.enableSmokeDrop then
local _smokeMenu = missionCommands.addSubMenuForGroup(_groupId, "Smoke Markers", _rootPath)
missionCommands.addCommandForGroup(_groupId, "Drop Red Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Red })
missionCommands.addCommandForGroup(_groupId, "Drop Blue Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Blue })
missionCommands.addCommandForGroup(_groupId, "Drop Orange Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Orange })
missionCommands.addCommandForGroup(_groupId, "Drop Green Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Green })
end
if ctld.enabledRadioBeaconDrop then
local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath)
missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName })
missionCommands.addCommandForGroup(_groupId, "Drop Beacon", _radioCommands, ctld.dropRadioBeacon, { _unitName })
missionCommands.addCommandForGroup(_groupId, "Remove Closet Beacon", _radioCommands, ctld.removeRadioBeacon, { _unitName })
elseif ctld.deployedRadioBeacons ~= {} then
local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath)
missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName })
end
ctld.addedTo[tostring(_groupId)] = true
end
end
else
-- env.info(string.format("unit nil %s",_unitName))
end
end)
if (not status) then
env.error(string.format("Error adding f10 to transport: %s", error), false)
end
end
local status, error = pcall(function()
-- now do any player controlled aircraft that ARENT transport units
if ctld.enabledRadioBeaconDrop then
-- get all BLUE players
ctld.addRadioListCommand(2)
-- get all RED players
ctld.addRadioListCommand(1)
end
if ctld.JTAC_jtacStatusF10 then
-- get all BLUE players
ctld.addJTACRadioCommand(2)
-- get all RED players
ctld.addJTACRadioCommand(1)
end
end)
if (not status) then
env.error(string.format("Error adding f10 to other players: %s", error), false)
end
end
--add to all players that arent transport
function ctld.addRadioListCommand(_side)
local _players = coalition.getPlayers(_side)
if _players ~= nil then
for _, _playerUnit in pairs(_players) do
local _groupId = ctld.getGroupId(_playerUnit)
if _groupId then
if ctld.addedTo[tostring(_groupId)] == nil then
missionCommands.addCommandForGroup(_groupId, "List Radio Beacons", nil, ctld.listRadioBeacons, { _playerUnit:getName() })
ctld.addedTo[tostring(_groupId)] = true
end
end
end
end
end
function ctld.addJTACRadioCommand(_side)
local _players = coalition.getPlayers(_side)
if _players ~= nil then
for _, _playerUnit in pairs(_players) do
local _groupId = ctld.getGroupId(_playerUnit)
if _groupId then
local newGroup = false
if ctld.jtacRadioAdded[tostring(_groupId)] == nil then
newGroup = true
local JTACpath = missionCommands.addSubMenuForGroup(_groupId, ctld.jtacMenuName)
missionCommands.addCommandForGroup(_groupId, "JTAC Status", JTACpath, ctld.getJTACStatus, { _playerUnit:getName() })
ctld.jtacRadioAdded[tostring(_groupId)] = true
end
--fetch the time to check for a regular refresh
local time = timer.getTime()
--depending on the delay, this part of the radio menu will be refreshed less often or as often as the static JTAC status command, this is for better reliability for the user when navigating through the menus. New groups will get the lists regardless and if a new JTAC is added all lists will be refreshed regardless of the delay.
if ctld.jtacLastRadioRefresh + ctld.jtacRadioRefreshDelay <= time or ctld.refreshJTACmenu[_side] or newGroup then
ctld.jtacLastRadioRefresh = time
--build the path to the CTLD JTAC menu
local jtacCurrentPagePath = {[1]=ctld.jtacMenuName}
--build the path for the NextPage submenu on the first page of the CTLD JTAC menu
local NextPageText = "Next Page"
local MainNextPagePath = {[1]=ctld.jtacMenuName, [2]=NextPageText}
--remove it along with everything that's in it
missionCommands.removeItemForGroup(_groupId, MainNextPagePath)
--counter to know when to add the next page submenu to fit all of the JTAC group submenus
local jtacCounter = 0
for _jtacGroupName,jtacUnit in pairs(ctld.jtacUnits) do
ctld.logTrace(string.format("JTAC - MENU - [%s] - processing menu", ctld.p(_jtacGroupName)))
--if the JTAC is on the same team as the group being considered
local jtacCoalition = ctld.jtacUnits[_jtacGroupName].side
if jtacCoalition and jtacCoalition == _side then
--only bother removing the submenus on the first page of the CTLD JTAC menu as the other pages were deleted entirely above
if ctld.jtacGroupSubMenuPath[_jtacGroupName] and #ctld.jtacGroupSubMenuPath[_jtacGroupName]==2 then
missionCommands.removeItemForGroup(_groupId, ctld.jtacGroupSubMenuPath[_jtacGroupName])
end
ctld.logTrace(string.format("JTAC - MENU - [%s] - jtacTargetsList = %s", ctld.p(_jtacGroupName), ctld.p(ctld.jtacTargetsList[_jtacGroupName])))
ctld.logTrace(string.format("JTAC - MENU - [%s] - jtacCurrentTargets = %s", ctld.p(_jtacGroupName), ctld.p(ctld.jtacCurrentTargets[_jtacGroupName])))
local jtacActionMenu = false
for _,_specialOptionTable in pairs(ctld.jtacSpecialOptions) do
if _specialOptionTable.globalToggle then
jtacActionMenu = true
break
end
end
--if JTAC has at least one other target in sight or (if special options are available (NOTE : accessed through the JTAC's own menu also) and the JTAC has at least one target)
if (ctld.jtacTargetsList[_jtacGroupName] and #ctld.jtacTargetsList[_jtacGroupName] >= 1) or (ctld.jtacCurrentTargets[_jtacGroupName] and jtacActionMenu) then
local jtacGroupSubMenuName = string.format(_jtacGroupName .. " Selection")
jtacCounter = jtacCounter + 1
--F2 through F10 makes 9 entries possible per page, with one being the NextMenu submenu. F1 is taken by JTAC status entry.
if jtacCounter % 9 == 0 then
--recover the path to the current page with space available for JTAC group submenus
jtacCurrentPagePath = missionCommands.addSubMenuForGroup(_groupId, NextPageText, jtacCurrentPagePath)
end
--add the JTAC group submenu to the current page
ctld.jtacGroupSubMenuPath[_jtacGroupName] = missionCommands.addSubMenuForGroup(_groupId, jtacGroupSubMenuName, jtacCurrentPagePath)
ctld.logTrace(string.format("JTAC - MENU - [%s] - jtacGroupSubMenuPath = %s", ctld.p(_jtacGroupName), ctld.p(ctld.jtacGroupSubMenuPath[_jtacGroupName])))
--make a copy of the JTAC group submenu's path to insert the target's list on as many pages as required. The JTAC's group submenu path only leads to the first page
local jtacTargetPagePath = mist.utils.deepCopy(ctld.jtacGroupSubMenuPath[_jtacGroupName])
--counter to know when to add the next page submenu to fit all of the targets in the JTAC's group submenu. SMay not actually start at 0 due to static items being present on the first page
local itemCounter = 0
local jtacSpecialOptPagePath = nil
if jtacActionMenu then
--special options
local SpecialOptionsCounter = 0
for _,_specialOption in pairs(ctld.jtacSpecialOptions) do
if _specialOption.globalToggle then
if not jtacSpecialOptPagePath then
itemCounter = itemCounter + 1 --one item is added to the first JTAC target page
jtacSpecialOptPagePath = missionCommands.addSubMenuForGroup(_groupId, "Actions", jtacTargetPagePath)
end
SpecialOptionsCounter = SpecialOptionsCounter+1
if SpecialOptionsCounter%10 == 0 then
jtacSpecialOptPagePath = missionCommands.addSubMenuForGroup(_groupId, NextPageText, jtacSpecialOptPagePath)
SpecialOptionsCounter = SpecialOptionsCounter+1 --Added Next Page item
end
if _specialOption.jtacs then
if _specialOption.jtacs[_jtacGroupName] then
missionCommands.addCommandForGroup(_groupId, "DISABLE " .. _specialOption.message, jtacSpecialOptPagePath, _specialOption.setter, {jtacGroupName = _jtacGroupName, value = false})
else
missionCommands.addCommandForGroup(_groupId, "ENABLE " .. _specialOption.message, jtacSpecialOptPagePath, _specialOption.setter, {jtacGroupName = _jtacGroupName, value = true})
end
else
missionCommands.addCommandForGroup(_groupId, "REQUEST " .. _specialOption.message, jtacSpecialOptPagePath, _specialOption.setter, {jtacGroupName = _jtacGroupName, value = false}) --value is not used here
end
end
end
end
if #ctld.jtacTargetsList[_jtacGroupName] >= 1 then
ctld.logTrace(string.format("JTAC - MENU - [%s] - adding targets menu", ctld.p(_jtacGroupName)))
--add a reset targeting option to revert to automatic JTAC unit targeting
missionCommands.addCommandForGroup(_groupId, "Reset TGT Selection", jtacTargetPagePath, ctld.setJTACTarget, {jtacGroupName = _jtacGroupName, targetName = nil})
itemCounter = itemCounter + 1 --one item is added to the first JTAC target page
--indicator table to know which unitType was already added to the radio submenu
local typeNameList = {}
for _,target in pairs(ctld.jtacTargetsList[_jtacGroupName]) do
local targetName = target.unit:getName()
--check if the jtac has a current target before filtering it out if possible
if (ctld.jtacCurrentTargets[_jtacGroupName] and targetName ~= ctld.jtacCurrentTargets[_jtacGroupName].name) then
local targetType_name = target.unit:getTypeName()
if targetType_name then
if typeNameList[targetType_name] then
typeNameList[targetType_name].amount = typeNameList[targetType_name].amount + 1
else
typeNameList[targetType_name] = {}
typeNameList[targetType_name].targetName = targetName --store the first targetName
typeNameList[targetType_name].amount = 1
end
end
end
end
for typeName,info in pairs(typeNameList) do
local amount = info.amount
local targetName = info.targetName
itemCounter = itemCounter + 1
--F1 through F10 makes 10 entries possible per page, with one being the NextMenu submenu.
if itemCounter%10 == 0 then
jtacTargetPagePath = missionCommands.addSubMenuForGroup(_groupId, NextPageText, jtacTargetPagePath)
itemCounter = itemCounter + 1 --added the next page item
end
missionCommands.addCommandForGroup(_groupId, string.format(typeName .. "(" .. amount .. ")"), jtacTargetPagePath, ctld.setJTACTarget, {jtacGroupName = _jtacGroupName, targetName = targetName})
end
end
end
end
end
end
end
end
if ctld.refreshJTACmenu[_side] then
ctld.refreshJTACmenu[_side] = false
end
end
end
function ctld.getGroupId(_unit)
local _unitDB = mist.DBs.unitsById[tonumber(_unit:getID())]
if _unitDB ~= nil and _unitDB.groupId then
return _unitDB.groupId
end
return nil
end
--get distance in meters assuming a Flat world
function ctld.getDistance(_point1, _point2)
local xUnit = _point1.x
local yUnit = _point1.z
local xZone = _point2.x
local yZone = _point2.z
local xDiff = xUnit - xZone
local yDiff = yUnit - yZone
return math.sqrt(xDiff * xDiff + yDiff * yDiff)
end
------------ JTAC -----------
ctld.jtacMenuName = "JTAC" --name of the CTLD JTAC radio menu
ctld.jtacLaserPoints = {}
ctld.jtacIRPoints = {}
ctld.jtacSmokeMarks = {}
ctld.jtacUnits = {} -- list of JTAC units for f10 command
ctld.jtacStop = {} -- jtacs to tell to stop lasing
ctld.jtacCurrentTargets = {}
ctld.jtacTargetsList = {} --current available targets to each JTAC for lasing (targets from other JTACs are filtered out). Contains DCS unit objects with their methods and the distance to the JTAC {unit, dist}
ctld.jtacSelectedTarget = {} --currently user selected target if it contains a unit's name, otherwise contains 1 or nil (if not initialized)
ctld.jtacSpecialOptions = { --list which contains the status of special options for each jtac, ordered for them to show up in the correct order in the corresponding radio menu
standbyMode = { --#1
globalToggle = ctld.JTAC_allowStandbyMode;
message = "Standby Mode";
setter = nil; --ctld.setStdbMode, will be set after declaration of said function
jtacs = {
--enable flag for each JTAC
};
}; --disable designation by the JTAC
smokeMarker = { --#4
globalToggle = ctld.JTAC_allowSmokeRequest;
message = "Smoke on TGT";
setter = nil; --ctld.setSmokeOnTarget
}; --smoke marker on target
laseSpotCorrections = { --#2
globalToggle = ctld.JTAC_laseSpotCorrections;
message = "Speed Corrections";
setter = nil; --ctld.setLaseCompensation
jtacs = {
--enable flag for each JTAC
};
}; --target speed and wind compensation for laser spot
_9Line = { --#3
globalToggle = ctld.JTAC_allow9Line;
message = "9 Line";
setter = nil; --ctld.setJTAC9Line
}; --9Line message for JTAC
}
ctld.jtacRadioAdded = {} --keeps track of who's had the radio command added
ctld.jtacGroupSubMenuPath = {} --keeps track of which submenu contains each JTAC's target selection menu
ctld.jtacRadioRefreshDelay = 120 --determines how often in seconds the dynamic parts of the jtac radio menu (target lists) will be refreshed
ctld.jtacLastRadioRefresh = 0 -- time at which the target lists were refreshed for everyone at least
ctld.refreshJTACmenu = {} --indicator to know when a new JTAC is added to a coalition in order to rebuild the corresponding target lists
ctld.jtacGeneratedLaserCodes = {} -- keeps track of generated codes, cycles when they run out
ctld.jtacLaserPointCodes = {}
ctld.jtacRadioData = {}
--[[
Called when a new JTAC is spawned, it will wait one second for DCS to have time to fill the group with units, and then call ctld.JTACAutoLase.
The goal here is to correct a bug: when a group is respawned (i.e. when any group with the name of a previously existing group is spawned),
DCS spawns a group which exists (Group.getByName gets a valid table, and group:isExist returns true), but has no units (i.e. group:getUnits returns an empty table).
This causes JTACAutoLase to call cleanupJTAC because it does not find the JTAC unit, and the JTAC to be put out of the JTACAutoLase loop, and never processed again.
By waiting a bit, the group gets populated before JTACAutoLase is called, hence avoiding a trip to cleanupJTAC.
]]
function ctld.JTACStart(_jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio)
mist.scheduleFunction(ctld.JTACAutoLase, {_jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio}, timer.getTime()+1)
end
function ctld.JTACAutoLase(_jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio)
ctld.logDebug(string.format("ctld.JTACAutoLase(_jtacGroupName=%s, _laserCode=%s", ctld.p(_jtacGroupName), ctld.p(_laserCode)))
local _radio = _radio
if not _radio then
_radio = {}
if _laserCode then
local _laserCode = tonumber(_laserCode)
if _laserCode and _laserCode >= 1111 and _laserCode <= 1688 then
local _laserB = math.floor((_laserCode - 1000)/100)
local _laserCD = _laserCode - 1000 - _laserB*100
local _frequency = tostring(30+_laserB+_laserCD*0.05)
ctld.logTrace(string.format("_laserB=%s", ctld.p(_laserB)))
ctld.logTrace(string.format("_laserCD=%s", ctld.p(_laserCD)))
ctld.logTrace(string.format("_frequency=%s", ctld.p(_frequency)))
_radio.freq = _frequency
_radio.mod = "fm"
end
end
end
if _radio and not _radio.name then
_radio.name = _jtacGroupName
end
if ctld.jtacStop[_jtacGroupName] == true then
ctld.jtacStop[_jtacGroupName] = nil -- allow it to be started again
ctld.cleanupJTAC(_jtacGroupName)
return
end
if _lock == nil then
_lock = ctld.JTAC_lock
end
ctld.jtacLaserPointCodes[_jtacGroupName] = _laserCode
ctld.jtacRadioData[_jtacGroupName] = _radio
local _jtacGroup = ctld.getGroup(_jtacGroupName)
local _jtacUnit
if _jtacGroup == nil or #_jtacGroup == 0 then
--check not in a heli
if ctld.inTransitTroops then
for _, _onboard in pairs(ctld.inTransitTroops) do
if _onboard ~= nil then
if _onboard.troops ~= nil and _onboard.troops.groupName ~= nil and _onboard.troops.groupName == _jtacGroupName then
--jtac soldier being transported by heli
ctld.cleanupJTAC(_jtacGroupName)
ctld.logTrace(string.format("JTAC - LASE - [%s] - in transport, waiting - scheduling JTACAutoLase in %ss at %s", ctld.p(_jtacGroupName), ctld.p(10), ctld.p(timer.getTime() + 10)))
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10)
return
end
if _onboard.vehicles ~= nil and _onboard.vehicles.groupName ~= nil and _onboard.vehicles.groupName == _jtacGroupName then
--jtac vehicle being transported by heli
ctld.cleanupJTAC(_jtacGroupName)
ctld.logTrace(string.format("JTAC - LASE - [%s] - in transport, waiting - scheduling JTACAutoLase in %ss at %s", ctld.p(_jtacGroupName), ctld.p(10), ctld.p(timer.getTime() + 10)))
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10)
return
end
end
end
end
if ctld.jtacUnits[_jtacGroupName] ~= nil then
ctld.notifyCoalition("JTAC Group " .. _jtacGroupName .. " KIA!", 10, ctld.jtacUnits[_jtacGroupName].side, _radio)
end
--remove from list
ctld.cleanupJTAC(_jtacGroupName)
return
else
_jtacUnit = _jtacGroup[1]
local _jtacCoalition = _jtacUnit:getCoalition()
--add to list
ctld.jtacUnits[_jtacGroupName] = { name = _jtacUnit:getName(), side = _jtacCoalition, radio = _radio }
--Targets list, special options and Selected target initialization
if not ctld.jtacTargetsList[_jtacGroupName] then
--Target list
ctld.jtacTargetsList[_jtacGroupName] = {}
if _jtacCoalition then ctld.refreshJTACmenu[_jtacCoalition] = true end
--Special Options
for _,_specialOption in pairs(ctld.jtacSpecialOptions) do
if _specialOption.jtacs then
_specialOption.jtacs[_jtacGroupName] = false
end
end
end
if not ctld.jtacSelectedTarget[_jtacGroupName] then
ctld.jtacSelectedTarget[_jtacGroupName] = 1
end
-- work out smoke colour
if _colour == nil then
if _jtacUnit:getCoalition() == 1 then
_colour = ctld.JTAC_smokeColour_RED
else
_colour = ctld.JTAC_smokeColour_BLUE
end
end
if _smoke == nil then
if _jtacUnit:getCoalition() == 1 then
_smoke = ctld.JTAC_smokeOn_RED
else
_smoke = ctld.JTAC_smokeOn_BLUE
end
end
end
-- search for current unit
if _jtacUnit:isActive() == false then
ctld.cleanupJTAC(_jtacGroupName)
ctld.logTrace(string.format("JTAC - LASE - [%s] - not active, scheduling JTACAutoLase in 30s at %s", ctld.p(_jtacGroupName), ctld.p(timer.getTime() + 30)))
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 30)
return
end
local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName)
--update targets list and store the next potential target if the selected one was lost
local _defaultEnemyUnit = ctld.findNearestVisibleEnemy(_jtacUnit, _lock)
-- if the JTAC sees a unit and a target was selected by users but is not the current unit, check if the selected target is in the targets list, if it is, then it's been reacquired
if _enemyUnit and ctld.jtacSelectedTarget[_jtacGroupName] ~= 1 and ctld.jtacSelectedTarget[_jtacGroupName] ~= _enemyUnit:getName() then
for _,target in pairs(ctld.jtacTargetsList[_jtacGroupName]) do
if target then
local targetUnit = target.unit
local targetName = targetUnit:getName()
if ctld.jtacSelectedTarget[_jtacGroupName] == targetName then
ctld.jtacCurrentTargets[_jtacGroupName] = { name = targetName, unitType = targetUnit:getTypeName(), unitId = targetUnit:getID() }
_enemyUnit = targetUnit
local message = _jtacGroupName .. ", selected target reacquired, " .. _enemyUnit:getTypeName()
local fullMessage = message .. '. CODE: ' .. _laserCode .. ". POSITION: " .. ctld.getPositionString(_enemyUnit)
ctld.notifyCoalition(fullMessage, 10, _jtacUnit:getCoalition(), _radio, message)
end
end
end
end
local targetDestroyed = false
local targetLost = false
local wasSelected = false
if _enemyUnit == nil and ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then
local _tempUnitInfo = ctld.jtacCurrentTargets[_jtacGroupName]
-- env.info("TEMP UNIT INFO: " .. tempUnitInfo.name .. " " .. tempUnitInfo.unitType)
local _tempUnit = Unit.getByName(_tempUnitInfo.name)
wasSelected = (ctld.jtacCurrentTargets[_jtacGroupName].name == ctld.jtacSelectedTarget[_jtacGroupName])
if _tempUnit ~= nil and _tempUnit:getLife() > 0 and _tempUnit:isActive() == true then
targetLost = true
else
targetDestroyed = true
ctld.jtacSelectedTarget[_jtacGroupName] = 1
end
--remove from smoke list
ctld.jtacSmokeMarks[_tempUnitInfo.name] = nil
-- JTAC Unit: resume his route ------------
trigger.action.groupContinueMoving(Group.getByName(_jtacGroupName))
-- remove from target list
ctld.jtacCurrentTargets[_jtacGroupName] = nil
--stop lasing
ctld.cancelLase(_jtacGroupName)
end
if _enemyUnit == nil then
if _defaultEnemyUnit ~= nil then
-- store current target for easy lookup
ctld.jtacCurrentTargets[_jtacGroupName] = { name = _defaultEnemyUnit:getName(), unitType = _defaultEnemyUnit:getTypeName(), unitId = _defaultEnemyUnit:getID() }
--add check for lasing or not
local action = "new target, "
if ctld.jtacSpecialOptions.standbyMode.jtacs[_jtacGroupName] then
action = "standing by on " .. action
else
action = "lasing " .. action
end
if wasSelected and targetLost then
action = ", temporarily " .. action
else
action = ", " .. action
end
if targetLost then
action = "target lost" .. action
elseif targetDestroyed then
action = "target destroyed" .. action
end
if wasSelected then
action = ", selected " .. action
elseif targetLost or targetDestroyed then
action = ", " .. action
end
wasSelected = false
targetDestroyed = false
targetLost = false
local message = _jtacGroupName .. action .. _defaultEnemyUnit:getTypeName()
local fullMessage = message .. '. CODE: ' .. _laserCode .. ". POSITION: " .. ctld.getPositionString(_defaultEnemyUnit)
ctld.notifyCoalition(fullMessage, 10, _jtacUnit:getCoalition(), _radio, message)
-- JTAC Unit stop his route -----------------
trigger.action.groupStopMoving(Group.getByName(_jtacGroupName)) -- stop JTAC
-- create smoke
if _smoke == true then
--create first smoke
ctld.createSmokeMarker(_defaultEnemyUnit, _colour)
end
end
end
if _enemyUnit ~= nil and not ctld.jtacSpecialOptions.standbyMode.jtacs[_jtacGroupName] then
local refreshDelay = 15 --delay in between JTACAutoLase scheduled calls when a target is tracked
local targetSpeedVec = _enemyUnit:getVelocity()
local targetSpeed = math.sqrt(targetSpeedVec.x^2+targetSpeedVec.y^2+targetSpeedVec.z^2)
local maxUpdateDist = 5 --maximum distance the unit will be allowed to travel before the lase spot is updated again
ctld.logTrace(string.format("targetSpeed=%s", ctld.p(targetSpeed)))
ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode)
--if the target is going sufficiently fast for it to wander off futher than the maxUpdateDist, schedule laseUnit calls to update the lase spot only (we consider that the unit lives and drives on between JTACAutoLase calls)
if targetSpeed >= maxUpdateDist/refreshDelay then
local updateTimeStep = maxUpdateDist/targetSpeed --calculate the time step so that the target is never more than maxUpdateDist from it's last lased position
ctld.logTrace(string.format("JTAC - LASE - [%s] - target is moving at %s m/s, schedulting lasing steps every %ss", ctld.p(_jtacGroupName), ctld.p(targetSpeed), ctld.p(updateTimeStep)))
local i = 1
while i*updateTimeStep <= refreshDelay - updateTimeStep do --while the scheduled time for the laseUnit call isn't greater than the time between two JTACAutoLase() calls minus one time step (because at the next time step JTACAutoLase() should have been called and this in term also calls laseUnit())
timer.scheduleFunction(ctld.timerLaseUnit,{_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode}, timer.getTime()+i*updateTimeStep)
i = i + 1
end
ctld.logTrace(string.format("JTAC - LASE - [%s] - scheduled %s moving target lasing steps", ctld.p(_jtacGroupName), ctld.p(i)))
end
ctld.logTrace(string.format("JTAC - LASE - [%s] - scheduling JTACAutoLase in %ss at %s", ctld.p(_jtacGroupName), ctld.p(refreshDelay), ctld.p(timer.getTime() + refreshDelay)))
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + refreshDelay)
if _smoke == true then
local _nextSmokeTime = ctld.jtacSmokeMarks[_enemyUnit:getName()]
--recreate smoke marker after 5 mins
if _nextSmokeTime ~= nil and _nextSmokeTime < timer.getTime() then
ctld.createSmokeMarker(_enemyUnit, _colour)
end
end
else
ctld.logDebug(string.format("JTAC - MODE - [%s] - No Enemies Nearby / Standby mode", ctld.p(_jtacGroupName)))
-- stop lazing the old spot
ctld.logDebug(string.format("JTAC - LASE - [%s] - canceling lasing of the old spot", ctld.p(_jtacGroupName)))
ctld.cancelLase(_jtacGroupName)
ctld.logTrace(string.format("JTAC - LASE - [%s] - scheduling JTACAutoLase in %ss at %s", ctld.p(_jtacGroupName), ctld.p(5), ctld.p(timer.getTime() + 5)))
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 5)
end
local action = ", "
if wasSelected then
action = action .. "selected "
end
if targetLost then
ctld.notifyCoalition(_jtacGroupName .. action .. "target lost.", 10, _jtacUnit:getCoalition(), _radio)
elseif targetDestroyed then
ctld.notifyCoalition(_jtacGroupName .. action .. "target destroyed.", 10, _jtacUnit:getCoalition(), _radio)
end
end
function ctld.JTACAutoLaseStop(_jtacGroupName)
ctld.jtacStop[_jtacGroupName] = true
end
-- used by the timer function
function ctld.timerJTACAutoLase(_args)
ctld.JTACAutoLase(_args[1], _args[2], _args[3], _args[4], _args[5], _args[6])
end
function ctld.cleanupJTAC(_jtacGroupName)
-- clear laser - just in case
ctld.cancelLase(_jtacGroupName)
-- Cleanup
ctld.jtacCurrentTargets[_jtacGroupName] = nil
ctld.jtacTargetsList[_jtacGroupName] = nil
ctld.jtacSelectedTarget[_jtacGroupName] = nil
for _,_specialOption in pairs(ctld.jtacSpecialOptions) do --delete jtac specific settings for all special options
if _specialOption.jtacs then
_specialOption.jtacs[_jtacGroupName] = nil
end
end
ctld.jtacRadioData[_jtacGroupName] = nil
--remove the JTAC's group submenu and all of the target pages it potentially contained if the JTAC has or had a menu
if ctld.jtacUnits[_jtacGroupName] and ctld.jtacUnits[_jtacGroupName].side and ctld.jtacGroupSubMenuPath[_jtacGroupName] then
local _players = coalition.getPlayers(ctld.jtacUnits[_jtacGroupName].side)
if _players ~= nil then
for _, _playerUnit in pairs(_players) do
local _groupId = ctld.getGroupId(_playerUnit)
if _groupId then
missionCommands.removeItemForGroup(_groupId, ctld.jtacGroupSubMenuPath[_jtacGroupName])
end
end
end
end
ctld.jtacUnits[_jtacGroupName] = nil
ctld.jtacGroupSubMenuPath[_jtacGroupName] = nil
end
--- send a message to the coalition
--- if _radio is set, the message will be read out loud via SRS
function ctld.notifyCoalition(_message, _displayFor, _side, _radio, _shortMessage)
local _shortMessage = _shortMessage
if _shortMessage == nil then
_shortMessage = _message
end
if STTS and STTS.TextToSpeech and _radio and _radio.freq then
local _freq = _radio.freq
local _modulation = _radio.mod or "FM"
local _volume = _radio.volume or "1.0"
local _name = _radio.name or "JTAC"
local _gender = _radio.gender or "male"
local _culture = _radio.culture or "en-US"
local _voice = _radio.voice
local _googleTTS = _radio.googleTTS or false
STTS.TextToSpeech(_shortMessage, _freq, _modulation, _volume, _name, _side, nil, 1, _gender, _culture, _voice, _googleTTS)
end
trigger.action.outTextForCoalition(_side, _message, _displayFor)
trigger.action.outSoundForCoalition(_side, "radiobeep.ogg")
end
function ctld.createSmokeMarker(_enemyUnit, _colour)
--recreate in 5 mins
ctld.jtacSmokeMarks[_enemyUnit:getName()] = timer.getTime() + 300.0
-- move smoke 2 meters above target for ease
local _enemyPoint = _enemyUnit:getPoint()
trigger.action.smoke({ x = _enemyPoint.x + ctld.JTAC_smokeOffset_x, y = _enemyPoint.y + ctld.JTAC_smokeOffset_y, z = _enemyPoint.z + ctld.JTAC_smokeOffset_z }, _colour)
end
function ctld.cancelLase(_jtacGroupName)
--local index = "JTAC_"..jtacUnit:getID()
local _tempLase = ctld.jtacLaserPoints[_jtacGroupName]
if _tempLase ~= nil then
Spot.destroy(_tempLase)
ctld.jtacLaserPoints[_jtacGroupName] = nil
-- env.info('Destroy laze '..index)
_tempLase = nil
end
local _tempIR = ctld.jtacIRPoints[_jtacGroupName]
if _tempIR ~= nil then
Spot.destroy(_tempIR)
ctld.jtacIRPoints[_jtacGroupName] = nil
-- env.info('Destroy laze '..index)
_tempIR = nil
end
end
-- used by the timer function
function ctld.timerLaseUnit(_args)
ctld.laseUnit(_args[1], _args[2], _args[3], _args[4])
end
function ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode)
--cancelLase(jtacGroupName)
ctld.logTrace("ctld.laseUnit()")
local _spots = {}
if _enemyUnit:isExist() then
local _enemyVector = _enemyUnit:getPoint()
local _enemyVectorUpdated = { x = _enemyVector.x, y = _enemyVector.y + 2.0, z = _enemyVector.z }
if ctld.jtacSpecialOptions.laseSpotCorrections.jtacs[_jtacGroupName] then
local _enemySpeedVector = _enemyUnit:getVelocity()
ctld.logTrace(string.format("_enemySpeedVector=%s", ctld.p(_enemySpeedVector)))
local _WindSpeedVector = atmosphere.getWind(_enemyVectorUpdated)
ctld.logTrace(string.format("_WindSpeedVector=%s", ctld.p(_WindSpeedVector)))
--if target speed is greater than 0, calculated using absolute value norm
if math.abs(_enemySpeedVector.x) + math.abs(_enemySpeedVector.y) + math.abs(_enemySpeedVector.z) > 0 then
local CorrectionFactor = 1 --correction factor in seconds applied to the target speed components to determine the lasing spot for a direct hit on a moving vehicle
--correct in the direction of the movement
_enemyVectorUpdated.x = _enemyVectorUpdated.x + _enemySpeedVector.x * CorrectionFactor
_enemyVectorUpdated.y = _enemyVectorUpdated.y + _enemySpeedVector.y * CorrectionFactor
_enemyVectorUpdated.z = _enemyVectorUpdated.z + _enemySpeedVector.z * CorrectionFactor
end
--if wind speed is greater than 0, calculated using absolute value norm
if math.abs(_WindSpeedVector.x) + math.abs(_WindSpeedVector.y) + math.abs(_WindSpeedVector.z) > 0 then
local CorrectionFactor = 1.05 --correction factor in seconds applied to the wind speed components to determine the lasing spot for a direct hit in adverse conditions
--correct to the opposite of the wind direction
_enemyVectorUpdated.x = _enemyVectorUpdated.x - _WindSpeedVector.x * CorrectionFactor
_enemyVectorUpdated.y = _enemyVectorUpdated.y - _WindSpeedVector.y * CorrectionFactor --not sure about correcting altitude but that component is always 0 in testing
_enemyVectorUpdated.z = _enemyVectorUpdated.z - _WindSpeedVector.z * CorrectionFactor
end
--combination of both should result in near perfect accuracy if the bomb doesn't stall itself following fast vehicles or correcting for heavy winds, correction factors can be adjusted but should work up to 40kn of wind for vehicles moving at 90kph (beware to drop the bomb in a way to not stall it, facing which ever is larger, target speed or wind)
end
local _oldLase = ctld.jtacLaserPoints[_jtacGroupName]
local _oldIR = ctld.jtacIRPoints[_jtacGroupName]
if _oldLase == nil or _oldIR == nil then
-- create lase
local _status, _result = pcall(function()
_spots['irPoint'] = Spot.createInfraRed(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated)
_spots['laserPoint'] = Spot.createLaser(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated, _laserCode)
return _spots
end)
if not _status then
env.error('ERROR: ' .. _result, false)
else
if _result.irPoint then
-- env.info(jtacUnit:getName() .. ' placed IR Pointer on '..enemyUnit:getName())
ctld.jtacIRPoints[_jtacGroupName] = _result.irPoint --store so we can remove after
end
if _result.laserPoint then
-- env.info(jtacUnit:getName() .. ' is Lasing '..enemyUnit:getName()..'. CODE:'..laserCode)
ctld.jtacLaserPoints[_jtacGroupName] = _result.laserPoint
end
end
else
-- update lase
if _oldLase ~= nil then
_oldLase:setPoint(_enemyVectorUpdated)
end
if _oldIR ~= nil then
_oldIR:setPoint(_enemyVectorUpdated)
end
end
end
end
-- get currently selected unit and check they're still in range
function ctld.getCurrentUnit(_jtacUnit, _jtacGroupName)
local _unit = nil
if ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then
_unit = Unit.getByName(ctld.jtacCurrentTargets[_jtacGroupName].name)
end
local _tempPoint = nil
local _tempDist = nil
local _tempPosition = nil
local _jtacPosition = _jtacUnit:getPosition()
local _jtacPoint = _jtacUnit:getPoint()
if _unit ~= nil and _unit:getLife() > 0 and _unit:isActive() == true then
-- calc distance
_tempPoint = _unit:getPoint()
-- tempPosition = unit:getPosition()
_tempDist = ctld.getDistance(_unit:getPoint(), _jtacUnit:getPoint())
if _tempDist < ctld.JTAC_maxDistance then
-- calc visible
-- check slightly above the target as rounding errors can cause issues, plus the unit has some height anyways
local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z }
local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z }
if land.isVisible(_offsetEnemyPos, _offsetJTACPos) then
return _unit
end
end
end
return nil
end
-- Find nearest enemy to JTAC that isn't blocked by terrain
function ctld.findNearestVisibleEnemy(_jtacUnit, _targetType,_distance)
--local startTime = os.clock()
local _maxDistance = _distance or ctld.JTAC_maxDistance
local _nearestDistance = _maxDistance
local _jtacGroupName = _jtacUnit:getGroup():getName()
local _jtacPoint = _jtacUnit:getPoint()
local _coa = _jtacUnit:getCoalition()
local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z }
local _volume = {
id = world.VolumeType.SPHERE,
params = {
point = _offsetJTACPos,
radius = _maxDistance
}
}
local _unitList = {}
local _search = function(_unit, _coa)
pcall(function()
if _unit ~= nil
and _unit:getLife() > 0
and _unit:isActive()
and _unit:getCoalition() ~= _coa
and not _unit:inAir()
and not ctld.alreadyTarget(_jtacUnit,_unit) then
local _tempPoint = _unit:getPoint()
local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z }
if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then
local _dist = ctld.getDistance(_offsetJTACPos, _offsetEnemyPos)
if _dist < _maxDistance then
table.insert(_unitList,{unit=_unit, dist=_dist})
end
end
end
end)
return true
end
world.searchObjects(Object.Category.UNIT, _volume, _search, _coa)
--log.info(string.format("JTAC Search elapsed time: %.4f\n", os.clock() - startTime))
-- generate list order by distance & visible
-- first check
-- hpriority
-- priority
-- vehicle
-- unit
ctld.jtacTargetsList[_jtacGroupName] = _unitList
--from the units in range, build the targets list, unsorted as to keep consistency between radio menu refreshes
local _sort = function( a,b ) return a.dist < b.dist end
table.sort(_unitList,_sort)
-- sort list
-- check for hpriority
for _, _enemyUnit in ipairs(_unitList) do
local _enemyName = _enemyUnit.unit:getName()
if string.match(_enemyName, "hpriority") then
return _enemyUnit.unit
end
end
for _, _enemyUnit in ipairs(_unitList) do
local _enemyName = _enemyUnit.unit:getName()
if string.match(_enemyName, "priority") then
return _enemyUnit.unit
end
end
local result = nil
for _, _enemyUnit in ipairs(_unitList) do
local _enemyName = _enemyUnit.unit:getName()
--log.info(string.format("CTLD - checking _enemyName=%s", _enemyName))
-- check for air defenses
--log.info(string.format("CTLD - _enemyUnit.unit:getDesc()[attributes]=%s", ctld.p(_enemyUnit.unit:getDesc()["attributes"])))
local airdefense = (_enemyUnit.unit:getDesc()["attributes"]["Air Defence"] ~= nil)
--log.info(string.format("CTLD - airdefense=%s", tostring(airdefense)))
if (_targetType == "vehicle" and ctld.isVehicle(_enemyUnit.unit)) or _targetType == "all" then
if airdefense then
return _enemyUnit.unit
else
result = _enemyUnit.unit
end
elseif (_targetType == "troop" and ctld.isInfantry(_enemyUnit.unit)) or _targetType == "all" then
if airdefense then
return _enemyUnit.unit
else
result = _enemyUnit.unit
end
end
end
return result
end
function ctld.listNearbyEnemies(_jtacUnit)
local _maxDistance = ctld.JTAC_maxDistance
local _jtacPoint = _jtacUnit:getPoint()
local _coa = _jtacUnit:getCoalition()
local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z }
local _volume = {
id = world.VolumeType.SPHERE,
params = {
point = _offsetJTACPos,
radius = _maxDistance
}
}
local _enemies = nil
local _search = function(_unit, _coa)
pcall(function()
if _unit ~= nil
and _unit:getLife() > 0
and _unit:isActive()
and _unit:getCoalition() ~= _coa
and not _unit:inAir() then
local _tempPoint = _unit:getPoint()
local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z }
if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then
if not _enemies then
_enemies = {}
end
_enemies[_unit:getTypeName()] = _unit:getTypeName()
end
end
end)
return true
end
world.searchObjects(Object.Category.UNIT, _volume, _search, _coa)
return _enemies
end
-- tests whether the unit is targeted by another JTAC
function ctld.alreadyTarget(_jtacUnit, _enemyUnit)
for _, _jtacTarget in pairs(ctld.jtacCurrentTargets) do
if _jtacTarget.unitId == _enemyUnit:getID() then
-- env.info("ALREADY TARGET")
return true
end
end
return false
end
-- Returns only alive units from group but the group / unit may not be active
function ctld.getGroup(groupName)
local _group = Group.getByName(groupName)
local _filteredUnits = {} --contains alive units
local _x = 1
if _group ~= nil then
ctld.logTrace(string.format("ctld.getGroup - %s - group ~= nil", ctld.p(groupName)))
if _group:isExist() then
ctld.logTrace(string.format("ctld.getGroup - %s - group:isExist()", ctld.p(groupName)))
local _groupUnits = _group:getUnits()
if _groupUnits ~= nil and #_groupUnits > 0 then
ctld.logTrace(string.format("ctld.getGroup - %s - group has %s units", ctld.p(groupName), ctld.p(#_groupUnits)))
for _x = 1, #_groupUnits do
if _groupUnits[_x]:getLife() > 0 then -- removed and _groupUnits[_x]:isExist() as isExist doesnt work on single units!
table.insert(_filteredUnits, _groupUnits[_x])
else
ctld.logTrace(string.format("ctld.getGroup - %s - dead unit %s", ctld.p(groupName), ctld.p(_groupUnits[_x]:getName())))
end
end
end
end
end
return _filteredUnits
end
function ctld.getAliveGroup(_groupName)
local _group = Group.getByName(_groupName)
if _group and _group:isExist() == true and #_group:getUnits() > 0 then
return _group
end
return nil
end
-- gets the JTAC status and displays to coalition units
function ctld.getJTACStatus(_args)
--returns the status of all JTAC units unless the status of a single JTAC is asked for (by inserting it's groupName in _args[2])
local _playerUnit = ctld.getTransportUnit(_args[1])
local _singleJtacGroupName = _args[2]
if _playerUnit == nil and _singleJtacGroupName == nil then
return
end
local _side = nil
if _playerUnit == nil then
_side = ctld.jtacUnits[_singleJtacGroupName].side
else
_side = _playerUnit:getCoalition()
end
local _jtacGroupName = nil
local _jtacUnit = nil
local _message = "JTAC STATUS: \n\n"
for _jtacGroupName, _jtacDetails in pairs(ctld.jtacUnits) do
--look up units
if _singleJtacGroupName == nil or (_singleJtacGroupName and _singleJtacGroupName == _jtacGroupName) then --if the status of a single JTAC or if the status of a single JTAC was asked and this is the correct JTAC we're going over in the loop
_jtacUnit = Unit.getByName(_jtacDetails.name)
if _jtacUnit ~= nil and _jtacUnit:getLife() > 0 and _jtacUnit:isActive() == true and _jtacUnit:getCoalition() == _side then
local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName)
local _laserCode = ctld.jtacLaserPointCodes[_jtacGroupName]
local _start = "->" .. _jtacGroupName
if (_jtacDetails.radio) then
_start = _start .. ", available on ".._jtacDetails.radio.freq.." ".._jtacDetails.radio.mod ..","
end
if _laserCode == nil then
_laserCode = "UNKNOWN"
end
if _enemyUnit ~= nil and _enemyUnit:getLife() > 0 and _enemyUnit:isActive() == true then
local action = " targeting "
if ctld.jtacSelectedTarget[_jtacGroupName] == _enemyUnit:getName() then
action = " targeting selected unit "
else
if ctld.jtacSelectedTarget[_jtacGroupName] ~= 1 then
action = " attempting to find selected unit, temporarily targeting "
end
end
if ctld.jtacSpecialOptions.standbyMode.jtacs[_jtacGroupName] then
action = action .. "(Laser OFF) "
end
_message = _message .. "" .. _start .. action .. _enemyUnit:getTypeName() .. " CODE: " .. _laserCode .. ctld.getPositionString(_enemyUnit) .. "\n"
local _list = ctld.listNearbyEnemies(_jtacUnit)
if _list then
_message = _message.."Visual On: "
for _,_type in pairs(_list) do
_message = _message.._type..", "
end
_message = _message.."\n"
end
else
_message = _message .. "" .. _start .. " searching for targets" .. ctld.getPositionString(_jtacUnit) .. "\n"
end
end
end
end
if _message == "JTAC STATUS: \n\n" then
_message = "No Active JTACs"
end
ctld.notifyCoalition(_message, 10, _side)
end
function ctld.setJTACTarget(_args)
if _args then
local _jtacGroupName = _args.jtacGroupName
local targetName = _args.targetName
if _jtacGroupName and targetName and ctld.jtacSelectedTarget[_jtacGroupName] and ctld.jtacTargetsList[_jtacGroupName] then
--look for the unit's (target) name in the Targets List, create the required data structure for jtacCurrentTargets and then assign it to the JTAC called _jtacGroupName
for _, target in pairs(ctld.jtacTargetsList[_jtacGroupName]) do
if target then
local ListedTargetUnit = target.unit
local ListedTargetName = ListedTargetUnit:getName()
if ListedTargetName == targetName then
ctld.jtacSelectedTarget[_jtacGroupName] = targetName
ctld.jtacCurrentTargets[_jtacGroupName] = { name = targetName, unitType = ListedTargetUnit:getTypeName(), unitId = ListedTargetUnit:getID() }
local message = _jtacGroupName .. ", targeting selected unit, " .. ListedTargetUnit:getTypeName()
local fullMessage = message .. '. CODE: ' .. ctld.jtacLaserPointCodes[_jtacGroupName] .. ". POSITION: " .. ctld.getPositionString(ListedTargetUnit)
ctld.notifyCoalition(fullMessage, 10, ctld.jtacUnits[_jtacGroupName].side, ctld.jtacRadioData[_jtacGroupName], message)
end
end
end
elseif not targetName and ctld.jtacSelectedTarget[_jtacGroupName] ~= 1 then
ctld.jtacSelectedTarget[_jtacGroupName] = 1
ctld.jtacCurrentTargets[_jtacGroupName] = nil
local message = _jtacGroupName .. ", target selection reset."
ctld.notifyCoalition(message, 10, ctld.jtacUnits[_jtacGroupName].side, ctld.jtacRadioData[_jtacGroupName])
if ctld.jtacSpecialOptions.laseSpotCorrections.jtacs[_jtacGroupName] then
ctld.setLaseCompensation({jtacGroupName = _jtacGroupName, value = false}) --disable laser spot corrections
end
if ctld.jtacSpecialOptions.standbyMode.jtacs[_jtacGroupName] then
ctld.setStdbMode({jtacGroupName = _jtacGroupName, value = false}) --make the JTAC exit standby mode after either target selection or targeting selection reset
end
end
ctld.refreshJTACmenu[ctld.jtacUnits[_jtacGroupName].side] = true
end
end
--special option setters (make sure to affect the function pointer to the corresponding .setter in the special options table after declaration of said function)
function ctld.setSpecialOptionArgsCheck(_args)
if _args then
local _jtacGroupName = _args.jtacGroupName
local _value = _args.value --expected boolean
local _notOutput = _args.noOutput --expected boolean
if _jtacGroupName then
return {jtacGroupName = _jtacGroupName, value = _value, noOutput = _notOutput}
end
end
return nil
end
function ctld.setStdbMode(_args)
local parsedArgs = ctld.setSpecialOptionArgsCheck(_args)
if parsedArgs then
local _jtacGroupName = parsedArgs.jtacGroupName
local _value = parsedArgs.value
local _noOutput = parsedArgs.noOutput
local message_end = " enabled"
if _value then
message_end = " disabled"
end
if not _noOutput then
ctld.notifyCoalition(_jtacGroupName .. ", Laser and Smokes" .. message_end, 10, ctld.jtacUnits[_jtacGroupName].side, ctld.jtacRadioData[_jtacGroupName])
end
ctld.jtacSpecialOptions.standbyMode.jtacs[_jtacGroupName] = _value
ctld.refreshJTACmenu[ctld.jtacUnits[_jtacGroupName].side] = true
end
end
ctld.jtacSpecialOptions.standbyMode.setter = ctld.setStdbMode
function ctld.setLaseCompensation(_args)
local parsedArgs = ctld.setSpecialOptionArgsCheck(_args)
if parsedArgs then
local _jtacGroupName = parsedArgs.jtacGroupName
local _value = parsedArgs.value
local _noOutput = parsedArgs.noOutput
local message_end = " disabled."
if _value then
message_end = " enabled."
end
if not _noOutput then
ctld.notifyCoalition(_jtacGroupName .. ", Wind and Target speed Laser Spot compensations" .. message_end, 10, ctld.jtacUnits[_jtacGroupName].side, ctld.jtacRadioData[_jtacGroupName])
end
ctld.jtacSpecialOptions.laseSpotCorrections.jtacs[_jtacGroupName] = _value
ctld.refreshJTACmenu[ctld.jtacUnits[_jtacGroupName].side] = true
end
end
ctld.jtacSpecialOptions.laseSpotCorrections.setter = ctld.setLaseCompensation
function ctld.setSmokeOnTarget(_args)
local parsedArgs = ctld.setSpecialOptionArgsCheck(_args)
if parsedArgs then
local _jtacGroupName = parsedArgs.jtacGroupName
local _noOutput = parsedArgs.noOutput
local _enemyUnit = Unit.getByName(ctld.jtacCurrentTargets[_jtacGroupName].name)
if _enemyUnit then
if not _noOutput then
ctld.notifyCoalition(_jtacGroupName .. ", WHITE Smoke deployed near TGT", 10, ctld.jtacUnits[_jtacGroupName].side, ctld.jtacRadioData[_jtacGroupName])
end
local _enemyPoint = _enemyUnit:getPoint()
local randomCircleDiam = 30;
trigger.action.smoke({ x = _enemyPoint.x + math.random(randomCircleDiam,-randomCircleDiam), y = _enemyPoint.y + 2.0, z = _enemyPoint.z + math.random(randomCircleDiam,-randomCircleDiam)}, 2)
end
end
end
ctld.jtacSpecialOptions.smokeMarker.setter = ctld.setSmokeOnTarget
function ctld.setJTAC9Line(_args)
local parsedArgs = ctld.setSpecialOptionArgsCheck(_args)
if parsedArgs then
local _jtacGroupName = parsedArgs.jtacGroupName
ctld.getJTACStatus({nil, _jtacGroupName})
end
end
ctld.jtacSpecialOptions._9Line.setter = ctld.setJTAC9Line
function ctld.setGrpROE(_grp, _ROE)
if _ROE == nil then
_ROE = AI.Option.Ground.val.ROE.OPEN_FIRE
end
_grp = ctld.getAliveGroup(_grp)
if _grp ~= nil then
local _controller = _grp:getController();
Controller.setOption(_controller, AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO)
Controller.setOption(_controller, AI.Option.Ground.id.ROE, _ROE)
_controller:setTask(_grp)
end
end
function ctld.isInfantry(_unit)
local _typeName = _unit:getTypeName()
--type coerce tostring
_typeName = string.lower(_typeName .. "")
local _soldierType = { "infantry", "paratrooper", "stinger", "manpad", "mortar" }
for _key, _value in pairs(_soldierType) do
if string.match(_typeName, _value) then
return true
end
end
return false
end
-- assume anything that isnt soldier is vehicle
function ctld.isVehicle(_unit)
if ctld.isInfantry(_unit) then
return false
end
return true
end
-- The entered value can range from 1111 - 1788,
-- -- but the first digit of the series must be a 1 or 2
-- -- and the last three digits must be between 1 and 8.
-- The range used to be bugged so its not 1 - 8 but 0 - 7.
-- function below will use the range 1-7 just incase
function ctld.generateLaserCode()
ctld.jtacGeneratedLaserCodes = {}
-- generate list of laser codes
local _code = 1511
local _count = 1
while _code < 1777 and _count < 30 do
while true do
_code = _code + 1
if not ctld.containsDigit(_code, 8)
and not ctld.containsDigit(_code, 9)
and not ctld.containsDigit(_code, 0) then
table.insert(ctld.jtacGeneratedLaserCodes, _code)
--env.info(_code.." Code")
break
end
end
_count = _count + 1
end
end
function ctld.containsDigit(_number, _numberToFind)
local _thisNumber = _number
local _thisDigit = 0
while _thisNumber ~= 0 do
_thisDigit = _thisNumber % 10
_thisNumber = math.floor(_thisNumber / 10)
if _thisDigit == _numberToFind then
return true
end
end
return false
end
-- 200 - 400 in 10KHz
-- 400 - 850 in 10 KHz
-- 850 - 1250 in 50 KHz
function ctld.generateVHFrequencies()
--ignore list
--list of all frequencies in KHZ that could conflict with
-- 191 - 1290 KHz, beacon range
local _skipFrequencies = {
745, --Astrahan
381,
384,
300.50,
312.5,
1175,
342,
735,
300.50,
353.00,
440,
795,
525,
520,
690,
625,
291.5,
300.50,
435,
309.50,
920,
1065,
274,
312.50,
580,
602,
297.50,
750,
485,
950,
214,
1025, 730, 995, 455, 307, 670, 329, 395, 770,
380, 705, 300.5, 507, 740, 1030, 515,
330, 309.5,
348, 462, 905, 352, 1210, 942, 435,
324,
320, 420, 311, 389, 396, 862, 680, 297.5,
920, 662,
866, 907, 309.5, 822, 515, 470, 342, 1182, 309.5, 720, 528,
337, 312.5, 830, 740, 309.5, 641, 312, 722, 682, 1050,
1116, 935, 1000, 430, 577,
326 -- Nevada
}
ctld.freeVHFFrequencies = {}
local _start = 200000
-- first range
while _start < 400000 do
-- skip existing NDB frequencies
local _found = false
for _, value in pairs(_skipFrequencies) do
if value * 1000 == _start then
_found = true
break
end
end
if _found == false then
table.insert(ctld.freeVHFFrequencies, _start)
end
_start = _start + 10000
end
_start = 400000
-- second range
while _start < 850000 do
-- skip existing NDB frequencies
local _found = false
for _, value in pairs(_skipFrequencies) do
if value * 1000 == _start then
_found = true
break
end
end
if _found == false then
table.insert(ctld.freeVHFFrequencies, _start)
end
_start = _start + 10000
end
_start = 850000
-- third range
while _start <= 1250000 do
-- skip existing NDB frequencies
local _found = false
for _, value in pairs(_skipFrequencies) do
if value * 1000 == _start then
_found = true
break
end
end
if _found == false then
table.insert(ctld.freeVHFFrequencies, _start)
end
_start = _start + 50000
end
end
-- 220 - 399 MHZ, increments of 0.5MHZ
function ctld.generateUHFrequencies()
ctld.freeUHFFrequencies = {}
local _start = 220000000
while _start < 399000000 do
table.insert(ctld.freeUHFFrequencies, _start)
_start = _start + 500000
end
end
-- 220 - 399 MHZ, increments of 0.5MHZ
-- -- first digit 3-7MHz
-- -- second digit 0-5KHz
-- -- third digit 0-9
-- -- fourth digit 0 or 5
-- -- times by 10000
--
function ctld.generateFMFrequencies()
ctld.freeFMFrequencies = {}
local _start = 220000000
while _start < 399000000 do
_start = _start + 500000
end
for _first = 3, 7 do
for _second = 0, 5 do
for _third = 0, 9 do
local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit
table.insert(ctld.freeFMFrequencies, _frequency)
end
end
end
end
function ctld.getPositionString(_unit)
if ctld.JTAC_location == false then
return ""
end
local _lat, _lon = coord.LOtoLL(_unit:getPosition().p)
local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS)
local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_unit:getPosition().p)), 5)
return " @ " .. _latLngStr .. " - MGRS " .. _mgrsString
end
-- ***************** SETUP SCRIPT ****************
function ctld.initialize(force)
ctld.logInfo(string.format("Initializing version %s", ctld.Version))
ctld.logTrace(string.format("ctld.alreadyInitialized=%s", ctld.p(ctld.alreadyInitialized)))
ctld.logTrace(string.format("force=%s", ctld.p(force)))
if ctld.alreadyInitialized and not force then
ctld.logInfo(string.format("Bypassing initialization because ctld.alreadyInitialized = true"))
return
end
assert(mist ~= nil, "\n\n** HEY MISSION-DESIGNER! **\n\nMiST has not been loaded!\n\nMake sure MiST 3.6 or higher is running\n*before* running this script!\n")
ctld.addedTo = {}
ctld.spawnedCratesRED = {} -- use to store crates that have been spawned
ctld.spawnedCratesBLUE = {} -- use to store crates that have been spawned
ctld.droppedTroopsRED = {} -- stores dropped troop groups
ctld.droppedTroopsBLUE = {} -- stores dropped troop groups
ctld.droppedVehiclesRED = {} -- stores vehicle groups for c-130 / hercules
ctld.droppedVehiclesBLUE = {} -- stores vehicle groups for c-130 / hercules
ctld.inTransitTroops = {}
ctld.inTransitFOBCrates = {}
ctld.inTransitSlingLoadCrates = {} -- stores crates that are being transported by helicopters for alternative to real slingload
ctld.droppedFOBCratesRED = {}
ctld.droppedFOBCratesBLUE = {}
ctld.builtFOBS = {} -- stores fully built fobs
ctld.completeAASystems = {} -- stores complete spawned groups from multiple crates
ctld.fobBeacons = {} -- stores FOB radio beacon details, refreshed every 60 seconds
ctld.deployedRadioBeacons = {} -- stores details of deployed radio beacons
ctld.beaconCount = 1
ctld.usedUHFFrequencies = {}
ctld.usedVHFFrequencies = {}
ctld.usedFMFrequencies = {}
ctld.freeUHFFrequencies = {}
ctld.freeVHFFrequencies = {}
ctld.freeFMFrequencies = {}
--used to lookup what the crate will contain
ctld.crateLookupTable = {}
ctld.extractZones = {} -- stored extract zones
ctld.missionEditorCargoCrates = {} --crates added by mission editor for triggering cratesinzone
ctld.hoverStatus = {} -- tracks status of a helis hover above a crate
ctld.callbacks = {} -- function callback
-- Remove intransit troops when heli / cargo plane dies
--ctld.eventHandler = {}
--function ctld.eventHandler:onEvent(_event)
--
-- if _event == nil or _event.initiator == nil then
-- env.info("CTLD null event")
-- elseif _event.id == 9 then
-- -- Pilot dead
-- ctld.inTransitTroops[_event.initiator:getName()] = nil
--
-- elseif world.event.S_EVENT_EJECTION == _event.id or _event.id == 8 then
-- -- env.info("Event unit - Pilot Ejected or Unit Dead")
-- ctld.inTransitTroops[_event.initiator:getName()] = nil
--
-- -- env.info(_event.initiator:getName())
-- end
--
--end
-- create crate lookup table
for _subMenuName, _crates in pairs(ctld.spawnableCrates) do
for _, _crate in pairs(_crates) do
-- convert number to string otherwise we'll have a pointless giant
-- table. String means 'hashmap' so it will only contain the right number of elements
ctld.crateLookupTable[tostring(_crate.weight)] = _crate
end
end
--sort out pickup zones
for _, _zone in pairs(ctld.pickupZones) do
local _zoneName = _zone[1]
local _zoneColor = _zone[2]
local _zoneActive = _zone[4]
if _zoneColor == "green" then
_zone[2] = trigger.smokeColor.Green
elseif _zoneColor == "red" then
_zone[2] = trigger.smokeColor.Red
elseif _zoneColor == "white" then
_zone[2] = trigger.smokeColor.White
elseif _zoneColor == "orange" then
_zone[2] = trigger.smokeColor.Orange
elseif _zoneColor == "blue" then
_zone[2] = trigger.smokeColor.Blue
else
_zone[2] = -1 -- no smoke colour
end
-- add in counter for troops or units
if _zone[3] == -1 then
_zone[3] = 10000;
end
-- change active to 1 / 0
if _zoneActive == "yes" then
_zone[4] = 1
else
_zone[4] = 0
end
end
--sort out dropoff zones
for _, _zone in pairs(ctld.dropOffZones) do
local _zoneColor = _zone[2]
if _zoneColor == "green" then
_zone[2] = trigger.smokeColor.Green
elseif _zoneColor == "red" then
_zone[2] = trigger.smokeColor.Red
elseif _zoneColor == "white" then
_zone[2] = trigger.smokeColor.White
elseif _zoneColor == "orange" then
_zone[2] = trigger.smokeColor.Orange
elseif _zoneColor == "blue" then
_zone[2] = trigger.smokeColor.Blue
else
_zone[2] = -1 -- no smoke colour
end
--mark as active for refresh smoke logic to work
_zone[4] = 1
end
--sort out waypoint zones
for _, _zone in pairs(ctld.wpZones) do
local _zoneColor = _zone[2]
if _zoneColor == "green" then
_zone[2] = trigger.smokeColor.Green
elseif _zoneColor == "red" then
_zone[2] = trigger.smokeColor.Red
elseif _zoneColor == "white" then
_zone[2] = trigger.smokeColor.White
elseif _zoneColor == "orange" then
_zone[2] = trigger.smokeColor.Orange
elseif _zoneColor == "blue" then
_zone[2] = trigger.smokeColor.Blue
else
_zone[2] = -1 -- no smoke colour
end
--mark as active for refresh smoke logic to work
-- change active to 1 / 0
if _zone[3] == "yes" then
_zone[3] = 1
else
_zone[3] = 0
end
end
-- Sort out extractable groups
for _, _groupName in pairs(ctld.extractableGroups) do
local _group = Group.getByName(_groupName)
if _group ~= nil then
if _group:getCoalition() == 1 then
table.insert(ctld.droppedTroopsRED, _group:getName())
else
table.insert(ctld.droppedTroopsBLUE, _group:getName())
end
end
end
-- Seperate troop teams into red and blue for random AI pickups
if ctld.allowRandomAiTeamPickups == true then
ctld.redTeams = {}
ctld.blueTeams = {}
for _,_loadGroup in pairs(ctld.loadableGroups) do
if not _loadGroup.side then
table.insert(ctld.redTeams, _)
table.insert(ctld.blueTeams, _)
elseif _loadGroup.side == 1 then
table.insert(ctld.redTeams, _)
elseif _loadGroup.side == 2 then
table.insert(ctld.blueTeams, _)
end
end
end
-- add total count
for _,_loadGroup in pairs(ctld.loadableGroups) do
_loadGroup.total = 0
if _loadGroup.aa then
_loadGroup.total = _loadGroup.aa + _loadGroup.total
end
if _loadGroup.inf then
_loadGroup.total = _loadGroup.inf + _loadGroup.total
end
if _loadGroup.mg then
_loadGroup.total = _loadGroup.mg + _loadGroup.total
end
if _loadGroup.at then
_loadGroup.total = _loadGroup.at + _loadGroup.total
end
if _loadGroup.mortar then
_loadGroup.total = _loadGroup.mortar + _loadGroup.total
end
end
-- Scheduled functions (run cyclically) -- but hold execution for a second so we can override parts
timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 1)
timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 5)
timer.scheduleFunction(function()
timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 5)
timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 5)
timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 5)
if ctld.enableCrates == true and ctld.slingLoad == false and ctld.hoverPickup == true then
timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1)
end
end,nil, timer.getTime()+1 )
--event handler for deaths
--world.addEventHandler(ctld.eventHandler)
--env.info("CTLD event handler added")
env.info("Generating Laser Codes")
ctld.generateLaserCode()
env.info("Generated Laser Codes")
env.info("Generating UHF Frequencies")
ctld.generateUHFrequencies()
env.info("Generated UHF Frequencies")
env.info("Generating VHF Frequencies")
ctld.generateVHFrequencies()
env.info("Generated VHF Frequencies")
env.info("Generating FM Frequencies")
ctld.generateFMFrequencies()
env.info("Generated FM Frequencies")
-- Search for crates
-- Crates are NOT returned by coalition.getStaticObjects() for some reason
-- Search for crates in the mission editor instead
env.info("Searching for Crates")
for _coalitionName, _coalitionData in pairs(env.mission.coalition) do
if (_coalitionName == 'red' or _coalitionName == 'blue')
and type(_coalitionData) == 'table' then
if _coalitionData.country then --there is a country table
for _, _countryData in pairs(_coalitionData.country) do
if type(_countryData) == 'table' then
for _objectTypeName, _objectTypeData in pairs(_countryData) do
if _objectTypeName == "static" then
if ((type(_objectTypeData) == 'table')
and _objectTypeData.group
and (type(_objectTypeData.group) == 'table')
and (#_objectTypeData.group > 0)) then
for _groupId, _group in pairs(_objectTypeData.group) do
if _group and _group.units and type(_group.units) == 'table' then
for _unitNum, _unit in pairs(_group.units) do
if _unit.canCargo == true then
local _cargoName = env.getValueDictByKey(_unit.name)
ctld.missionEditorCargoCrates[_cargoName] = _cargoName
env.info("Crate Found: " .. _unit.name.." - Unit: ".._cargoName)
end
end
end
end
end
end
end
end
end
end
end
end
env.info("END search for crates")
-- don't initialize more than once
ctld.alreadyInitialized = true
env.info("CTLD READY")
end
-- initialize the random number generator to make it almost random
math.random(); math.random(); math.random()
--- Enable/Disable error boxes displayed on screen.
env.setErrorMessageBoxEnabled(false)
-- initialize CTLD in 2 seconds, so other scripts have a chance to modify the configuration before initialization
ctld.logInfo(string.format("Loading version %s in 2 seconds", ctld.Version))
timer.scheduleFunction(ctld.initialize, nil, timer.getTime() + 2)
--DEBUG FUNCTION
-- for key, value in pairs(getmetatable(_spawnedCrate)) do
-- env.info(tostring(key))
-- env.info(tostring(value))
-- end