DCS-CTLD/CTLD.lua
Ciaran Fisher a004c538d8 FOB Building
Ability to build FOB
IL-76 able to transport vehicles
Vehicle transport config changed to be separate for RED and BLUE
Crate config changed to allow easy customisation of RED and BLUE
2015-05-24 23:39:23 +01:00

3100 lines
89 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 some 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
Version: 1.05 - 24/05/2015 - Event Bug Fix from Latest DCS Patch
]]
ctld = {} -- DONT REMOVE!
-- ************************************************************************
-- ********************* USER CONFIGURATION ******************************
-- ************************************************************************
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.enableCrates = true -- if false, Helis will not be able to spawn or unpack crates so will be normal CTTS
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 = 1000 -- max distance for troops to move from drop point if no enemy is nearby
ctld.numberOfTroops = 10 -- default number of troops to load on a transport heli or C-130
ctld.vehiclesForTransportRED = { "BRDM-2", "BTR_D" } -- vehicles to load onto Il-76
ctld.vehiclesForTransportBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } -- vehicles to load onto c130
ctld.spawnRPGWithCoalition = true --spawns a friendly RPG unit with Coalition forces
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 crates can only be loaded and dropped by large aircraft, like the C-130 and listed in ctld.vehicleTransportEnabled
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
-- ***************** JTAC CONFIGURATION *****************
ctld.JTAC_LIMIT_RED = 5 -- max number of JTAC Crates for the RED Side
ctld.JTAC_LIMIT_BLUE = 5 -- max number of JTAC Crates for the BLUE Side
ctld.JTAC_dropEnabled = true -- allow JTAC Crate spawn from F10 menu
ctld.JTAC_maxDistance = 5000 -- 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_jtacStatusF10 = true -- enables F10 JTAC Status menu
ctld.JTAC_location = true -- shows location of target in JTAC message
ctld.JTAC_lock = "all" -- "vehicle" OR "troop" OR "all" forces JTAC to only lock vehicles or troops or all ground units
-- ***************** Pickup and dropoff 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
ctld.pickupZones = {
{ "pickzone1", "blue" },
{ "pickzone2", "blue" },
{ "pickzone3", "none" },
{ "pickzone4", "none" },
{ "pickzone5", "none" },
{ "pickzone6", "none" },
{ "pickzone7", "none" },
{ "pickzone8", "none" },
{ "pickzone9", "none" },
{ "pickzone10", "none" },
}
ctld.dropOffZones = {
{ "dropzone1", "red" },
{ "dropzone2", "blue" },
{ "dropzone3", "none" },
{ "dropzone4", "none" },
{ "dropzone5", "none" },
{ "dropzone6", "none" },
{ "dropzone7", "none" },
{ "dropzone8", "none" },
{ "dropzone9", "none" },
{ "dropzone10", "none" },
}
-- ******************** 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",
-- *** 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
"C-130",
}
-- ************** 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
["Ground Forces"] = {
--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
-- dont use that option with the HAWK Crates
{ weight = 1400, desc = "HMMWV - TOW", unit = "M1045 HMMWV TOW" , side = 2 },
{ weight = 1200, desc = "HMMWV - MG", unit = "M1043 HMMWV Armament", side = 2 },
{ weight = 1600, desc = "BTR-D", unit = "BTR_D", side = 1 },
{ weight = 1800, desc = "BRDM-2", unit = "BRDM-2", side = 1 },
{ weight = 1100, desc = "HMMWV - JTAC", unit = "Hummer", side = 2, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled
{ weight = 1500, desc = "SKP-11 - JTAC", unit = "SKP-11", side = 1, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled
{ weight = 200, desc = "2B11 Mortar", unit = "2B11 mortar" },
-- { weight = 500, desc = "M-109", unit = "M-109", cratesRequired = 3 },
},
["AA Crates"] = {
{ weight = 210, desc = "MANPAD", unit = "Stinger manpad" },
{ weight = 1000, desc = "HAWK Launcher", unit = "Hawk ln" },
{ weight = 1010, desc = "HAWK Search Radar", unit = "Hawk sr" },
{ weight = 1020, desc = "HAWK Track Radar", unit = "Hawk tr" },
-- { weight = 505, desc = "M6 Linebacker", unit = "M6 Linebacker", cratesRequired = 3 },
},
}
-- if the unit is on this list, it will be made into a JTAC
ctld.jtacUnitTypes = {
"SKP","Hummer"
}
-- ***************************************************************
-- **************** 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
-- "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 trigger "spawn1"
-- and they will search for enemy or move randomly withing 1000m
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 _number < 1 then
_number = 1
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
-- 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
-- ***************************************************************
-- **************** BE CAREFUL BELOW HERE ************************
-- ***************************************************************
---------------- INTERNAL FUNCTIONS ----------------
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)
local _crate = {
["category"] = "Cargo",
["shape_name"] = "ab-212_cargo",
["type"] = "Cargo1",
["unitId"] = _unitId,
["y"] = _point.z ,
["x"] = _point.x ,
["mass"] = _weight,
["name"] = _name,
["canCargo"] = true,
["heading"] = 0,
-- ["displayName"] = "name 2", -- getCargoDisplayName function exists but no way to set the variable
-- ["DisplayName"] = "name 2",
-- ["cargoDisplayName"] = "cargo123",
-- ["CargoDisplayName"] = "cargo123",
}
local _spawnedCrate = coalition.addStaticObject(_country, _crate)
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,
}
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,
}
local _spawnedCrate = coalition.addStaticObject(_country, _crate)
local _id = mist.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)
return _spawnedCrate
end
--function ctld.spawnFARP(_country,_point)
--
-- local _crate = {
-- ["type"] = "FARP",
-- ["unitId"] = _unitId,
-- ["heliport_modulation"] = 0,
-- ["y"] = _point.z+1,
-- ["x"] = _point.x+1,
-- ["name"] = _name,
-- ["category"] = "Heliports",
-- ["canCargo"] = false,
-- ["heliport_frequency"] = 127.5,
-- ["heliport_callsign_id"] = 1,
-- ["heading"] = 3.1415926535898,
--
-- }
--
--
-- local _farpPiece = {
-- ["shape_name"] = "PalatkaB",
-- ["type"] = "FARP Tent",
--
-- ["y"] = _point.z+1.5,
-- ["x"] = _point.x+1.5,
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["category"] = "Fortifications",
-- ["heading"] = 3.1415926535898,
-- }
--
-- coalition.addStaticObject(_country, _farpPiece)
-- local _farpPiece = {
-- ["shape_name"] = "SetkaKP",
-- ["type"] = "FARP Ammo Dump Coating",
--
-- ["y"] = _point.z+2,
-- ["x"] = _point.x+2,
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["category"] = "Fortifications",
-- ["heading"] = 3.1415926535898,
-- }
-- coalition.addStaticObject(_country, _farpPiece)
-- local _farpPiece = {
-- ["shape_name"] = "GSM Rus",
-- ["type"] = "FARP Fuel Depot",
--
-- ["y"] = _point.z+2.5,
-- ["x"] = _point.x+2.5,
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["category"] = "Fortifications",
-- ["heading"] = 3.1415926535898,
-- }
-- coalition.addStaticObject(_country, _farpPiece)
--
--
--
-- local _farpUnits = {
-- {
--
-- ["type"] = "M978 HEMTT Tanker",
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["heading"] = 4.7822021504645,
-- ["playerCanDrive"] = true,
-- ["skill"] = "Average",
-- ["x"] = _point.x,
-- ["y"] = _point.z,
-- },
-- {
--
-- ["type"] = "M 818",
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["heading"] = 4.7822021504645,
-- ["playerCanDrive"] = true,
-- ["skill"] = "Average",
-- ["x"] = _point.x,
-- ["y"] = _point.z,
--
-- },
-- {
--
-- ["type"] = "M-113",
-- ["name"] = "Unit #"..mist.getNextUnitId(),
-- ["unitId"] = mist.getNextUnitId(),
-- ["heading"] = 4.7822021504645,
-- ["playerCanDrive"] = true,
-- ["skill"] = "Average",
-- ["x"] = _point.x,
-- ["y"] = _point.z,
--
-- },
-- }
--
-- mist.dynAdd({units = _farpUnits,country=_country,category=Group.Category.GROUND})
--
--end
function ctld.spawnCrate(_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 _heli:inAir() == 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()
--try to spawn at 12 oclock to us
local _angle = math.atan2(_position.x.z, _position.x.x)
local _xOffset = math.cos(_angle) * 30
local _yOffset = math.sin(_angle) * 30
-- trigger.action.outText("Spawn Crate".._args[1].." ".._args[2],10)
local _heli = ctld.getTransportUnit(_args[1])
local _point = _heli:getPoint()
local _unitId = mist.getNextUnitId()
local _side = _heli:getCoalition()
local _name = string.format("%s #%i", _crateType.desc, _unitId)
local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(),_unitId,{x=_point.x+_xOffset,z=_point.z + _yOffset},_name,_crateType.weight)
if _side == 1 then
-- _spawnedCrate = coalition.addStaticObject(_side, _spawnedCrate)
ctld.spawnedCratesRED[_name] = _crateType
else
-- _spawnedCrate = coalition.addStaticObject(_side, _spawnedCrate)
ctld.spawnedCratesBLUE[_name] = _crateType
end
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
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.deployTroops(_heli,_troops)
local _onboard = ctld.inTransitTroops[_heli:getName()]
-- deloy troops
if _troops then
if _onboard.troops ~= nil and #_onboard.troops.units > 0 then
local _droppedTroops = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.troops, false)
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
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped troops from " .. _heli:getTypeName() .. " into combat", 10)
end
else
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
trigger.action.outTextForCoalition(_heli:getCoalition(),ctld.getPlayerNameOrType(_heli) .. " dropped vehicles from " .. _heli:getTypeName() .. " into combat", 10)
end
end
end
function ctld.generateTroopTypes(_side,_count, _country)
local _troops = {}
for _i = 1,_count do
local _unitType = "Soldier AK"
if _side == 2 then
_unitType = "Soldier M4"
if _i <= 4 and ctld.spawnRPGWithCoalition then
_unitType = "Paratrooper RPG-16"
end
if _i <= 2 then
_unitType = "Soldier M249"
end
else
_unitType = "Infantry AK"
if _i <= 4 then
_unitType = "Paratrooper RPG-16"
end
if _i <= 2 then
_unitType = "Paratrooper AKS-74"
end
end
local _unitId = mist.getNextUnitId()
_troops[_i] = {type = _unitType, unitId = _unitId, name = string.format("Dropped %s #%i",_unitType,_unitId)}
end
local _groupId = mist.getNextGroupId()
local _details = {units = _troops, groupId = _groupId, groupName = string.format("Dropped Group %i",_groupId), side=_side, country=_country}
return _details
end
-- load troops onto vehicle
function ctld.loadTroops(_heli,_troops, _number)
-- 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 _number == nil then
_number = 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(),_number,_heli:getCountry())
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded ".._number.." troops into " .. _heli:getTypeName(), 10)
else
_onboard.vehicles = ctld.generateVehiclesForTransport(_heli:getCoalition(), _heli:getCountry())
local _count = #_list
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded ".._count.." vehicles into " .. _heli:getTypeName(), 10)
end
ctld.inTransitTroops[_heli:getName()] = _onboard
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= mist.getNextUnitId()
_vehicles[_i] = {type = _type, unitId = _unitId, name = string.format("Dropped %s #%i",_type,_unitId)}
end
local _groupId = mist.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 _heli:inAir() == 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 = mist.getNextUnitId()
local _name = string.format("FOB Crate #%i", _unitId)
local _spawnedCrate = ctld.spawnFOBCrateStatic(_heli:getCountry(),mist.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.getFOBCratesAndDistance(_heli)
local _nearestCrate = ctld.getClosestCrate(_heli, _crates)
if _nearestCrate ~= nil and _nearestCrate.dist < 100 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.loadUnloadTroops(_args)
local _heli = ctld.getTransportUnit(_args[1])
local _troops = _args[2]
if _heli == nil then
return
end
local _inZone = ctld.inPickupZone(_heli)
-- first check for extractable troops regardless of if we're in a zone or not
-- if not ctld.troopsOnboard(_heli,_troops) then
--
-- local _extract
--
-- 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
--
-- if _extract ~= nil then
-- -- search for nearest troops to pickup
-- ctld.extractTroops(_heli,_troops)
--
-- return -- stop
-- end
-- end
if _inZone == true and ctld.troopsOnboard(_heli,_troops) then
if _troops then
ctld.displayMessageToGroup(_heli, "Dropped troops back to base", 20)
ctld.inTransitTroops[_heli:getName()].troops = nil
else
ctld.displayMessageToGroup(_heli, "Dropped vehicles back to base", 20)
ctld.inTransitTroops[_heli:getName()].vehicles = nil
end
elseif _inZone == false and ctld.troopsOnboard(_heli,_troops) then
ctld.deployTroops(_heli,_troops)
elseif _inZone == true and not ctld.troopsOnboard(_heli,_troops) then
ctld.loadTroops(_heli,_troops)
else
-- search for nearest troops to pickup
ctld.extractTroops(_heli,_troops)
end
end
function ctld.extractTroops(_heli,_troops)
local _onboard = ctld.inTransitTroops[_heli:getName()]
if _onboard == nil then
_onboard = { troops = nil, vehicles = nil }
end
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
_onboard.troops = _extractTroops.details
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
--remove
_extractTroops.group:destroy()
else
_onboard.troops = nil
ctld.displayMessageToGroup(_heli, "No extractable troops nearby and not in a pickup zone", 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)
--remove
_extractVehicles.group:destroy()
else
_onboard.vehicles = nil
ctld.displayMessageToGroup(_heli, "No extractable vehicles nearby and not in a pickup zone", 20)
end
end
ctld.inTransitTroops[_heli:getName()] = _onboard
end
function ctld.checkTroopStatus(_args)
--list onboard troops, if c130
local _heli = ctld.getTransportUnit(_args[1])
if _heli == nil then
return
end
local _onboard = ctld.inTransitTroops[_heli:getName()]
if _onboard == nil then
if ctld.inTransitFOBCrates[_heli:getName()] == true then
ctld.displayMessageToGroup(_heli, "1 FOB Crate Onboard", 10)
else
ctld.displayMessageToGroup(_heli, "No troops onboard", 10)
end
else
local _troops = _onboard.troops
local _vehicles = _onboard.vehicles
local _txt = ""
if _troops ~= nil and _troops.units ~= nil and #_troops.units > 0 then
_txt = _txt .. " " .. #_troops.units .. " troops onboard\n"
end
if _vehicles ~= nil and _vehicles.units ~= nil and #_vehicles.units > 0 then
_txt = _txt .. " " .. #_vehicles.units .. " vehicles onboard\n"
end
if ctld.inTransitFOBCrates[_heli:getName()] == true then
_txt = _txt .. " 1 FOB Crate oboard\n"
end
if _txt ~= "" then
ctld.displayMessageToGroup(_heli, _txt, 10)
else
if ctld.inTransitFOBCrates[_heli:getName()] == true then
ctld.displayMessageToGroup(_heli, "1 FOB Crate Onboard", 10)
else
ctld.displayMessageToGroup(_heli, "No troops onboard", 10)
end
end
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
end
end
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)
for _, _crate in pairs(_crates) do
if _crate.dist < 1000 then
_message = string.format("%s\n%s crate - kg %i - %i m", _message, _crate.details.desc, _crate.details.weight, _crate.dist)
end
end
local _fobCrates = ctld.getFOBCratesAndDistance(_heli)
local _fobMsg = ""
for _, _fobCrate in pairs(_fobCrates) do
if _fobCrate.dist < 1000 then
_fobMsg = _fobMsg..string.format("FOB Crate - %d m\n", _fobCrate.dist)
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.displayMessageToGroup(_unit, _text, _time)
trigger.action.outTextForGroup(_unit:getGroup():getID(), _text, _time)
end
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 = StaticObject.getByName(_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 (_crate:inAir() == false or (land.getHeight(_crate:getPoint()) < 200 and mist.vec.mag(_crate:getVelocity()) < 1.0 )) then
local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint())
local _crateDetails = { crateUnit = _crate, dist = _dist, details = _details }
table.insert(_crates, _crateDetails)
end
end
return _crates
end
function ctld.getFOBCratesAndDistance(_heli)
local _crates = {}
local _allCrates
if _heli:getCoalition() == 1 then
_allCrates = ctld.droppedFOBCratesRED
else
_allCrates = ctld.droppedFOBCratesBLUE
end
for _crateName, _details in pairs(_allCrates) do
--get crate
local _crate = StaticObject.getByName(_crateName)
if _crate ~= nil and _crate:getLife() > 0 then
local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint())
local _crateDetails = { crateUnit = _crate, dist = _dist, details = {} }
table.insert(_crates, _crateDetails)
end
end
return _crates
end
function ctld.getClosestCrate(_heli, _crates)
local _closetCrate = nil
local _shortestDistance = -1
local _distance = 0
for _, _crate in pairs(_crates) do
_distance = _crate.dist
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
_closetCrate = _crate
end
end
return _closetCrate
end
function ctld.findNearestHawk(_heli)
local _closestHawkGroup = nil
local _shortestDistance = -1
local _distance = 0
for _, _groupName in pairs(ctld.completeHawkSystems) do
local _hawkGroup = Group.getByName(_groupName)
if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() then
local _leader = _hawkGroup:getUnit(1)
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
end
end
end
if _closestHawkGroup ~= nil then
return {group = _closestHawkGroup, dist = _distance}
end
return nil
end
function ctld.unpackCrates(_args)
-- trigger.action.outText("Unpack Crates".._args[1],10)
local _heli = ctld.getTransportUnit(_args[1])
if _heli ~= nil and _heli:inAir() == false then
local _crates = ctld.getCratesAndDistance(_heli)
local _crate = ctld.getClosestCrate(_heli, _crates)
if _crate ~= nil and _crate.dist < 200 then
-- 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
-- is multi crate?
if ctld.isMultiCrate(_crate.details) then
-- multicrate
ctld.unpackMultiCrate(_heli,_crate,_crates)
else
-- single crate
local _cratePoint = _crate.crateUnit:getPoint()
local _crateName = _crate.crateUnit:getName()
-- ctld.spawnCrateStatic( _heli:getCoalition(),mist.getNextUnitId(),{x=100,z=100},_crateName,100)
--remove crate
_crate.crateUnit:destroy()
local _spawnedGroups = ctld.spawnCrateGroup(_heli, { _cratePoint }, { _crate.details.unit })
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_crateName] = nil
else
ctld.spawnedCratesBLUE[_crateName] = nil
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.JTACAutoLase(_spawnedGroups:getName(),_code) --(_jtacGroupName, _laserCode, _smoke, _lock, _colour)
end
end
else
ctld.displayMessageToGroup(_heli, "No friendly crates close enough to unpack", 20)
end
end
end
-- builds a fob!
function ctld.unpackFOBCrates(_args)
local _heli = ctld.getTransportUnit(_args[1])
if _heli ~= nil and _heli:inAir() == false then
local _crates = ctld.getFOBCratesAndDistance(_heli)
local _nearestCrate = ctld.getClosestCrate(_heli, _crates)
if _nearestCrate ~= nil and _nearestCrate.dist < 750 then
if ctld.inLogisticsZone(_heli) == true then
ctld.displayMessageToGroup(_heli, "You can't unpack the FOB here! Take it to where it's needed!", 20)
return
end
-- unpack multi crate
local _nearbyMultiCrates = {}
for _, _nearbyCrate in pairs(_crates) do
if _nearbyCrate.dist < 750 then
table.insert(_nearbyMultiCrates, _nearbyCrate)
if #_nearbyMultiCrates == ctld.cratesRequiredForFOB then
break
end
end
end
--- check crate count
if #_nearbyMultiCrates == ctld.cratesRequiredForFOB then
-- destroy crates
local _points = {}
for _, _crate in pairs(_nearbyMultiCrates) do
if _heli:getCoalition() == 1 then
ctld.droppedFOBCratesRED[_crate.crateUnit:getName()] = nil
else
ctld.droppedFOBCratesRED[_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 = mist.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())
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
-- spawn smoke
trigger.action.smoke(_args[1],trigger.smokeColor.Green)
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",ctld.getPlayerNameOrType(_heli),#_nearbyMultiCrates,ctld.buildTimeFOB)
trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10)
else
local _txt = string.format("Cannot build FOB!\n\nIt requires %d FOB crates and there are %d \n\nOr the crates are not within 750m of each other",ctld.cratesRequiredForFOB,#_nearbyMultiCrates)
ctld.displayMessageToGroup(_heli, _txt, 20)
end
else
ctld.displayMessageToGroup(_heli, "No friendly FOB crates close enough to unpack", 20)
end
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.isMultiCrate(_crateDetails)
if string.match(_crateDetails.desc, "HAWK")
or (_crateDetails.cratesRequired ~= nil and _crateDetails.cratesRequired > 1) then
return true
else
return false
end
end
function ctld.rearmHawk(_heli,_nearestCrate,_nearbyCrates)
-- are we adding to existing hawk system?
if _nearestCrate.details.unit == "Hawk ln" then
-- find nearest COMPLETE hawk system
local _nearestHawk = ctld.findNearestHawk(_heli)
if _nearestHawk ~=nil and _nearestHawk.dist < 300 then
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil
end
local _types = {}
local _points = {}
local _units = _nearestHawk.group:getUnits()
if _units ~= nil and #_units > 0 then
for x = 1, #_units do
if _units[x]:getLife() > 0 then
table.insert(_types,_units[x]:getTypeName())
table.insert(_points,_units[x]:getPoint())
end
end
end
if #_types == 3 and #_points == 3 then
-- rearm hawk
-- destroy old group
ctld.completeHawkSystems[_nearestHawk.group:getName()] = nil
_nearestHawk.group:destroy()
local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types)
ctld.completeHawkSystems[_spawnedGroup:getName()] = _spawnedGroup:getName()
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully rearmed a full HAWK AA System in the field", 10)
return true -- all done so quit
end
end
end
return false
end
function ctld.unpackHawk(_heli,_nearestCrate,_nearbyCrates)
if ctld.rearmHawk(_heli,_nearestCrate,_nearbyCrates) then
-- rearmed hawk
return
end
-- are there all the pieces close enough together
local _hawkParts = { ["Hawk ln"] = false, ["Hawk tr"] = false, ["Hawk sr"] = false }
for _, _nearbyCrate in pairs(_nearbyCrates) do
if _nearbyCrate.dist < 300 then
if _nearbyCrate.details.unit == "Hawk ln" or _nearbyCrate.details.unit == "Hawk sr" or _nearbyCrate.details.unit == "Hawk tr" then
_hawkParts[_nearbyCrate.details.unit] = _nearbyCrate
else
-- not part of hawk
end
end
end
local _count = 0
local _txt = ""
local _posArray = {}
local _typeArray = {}
for _name, _hawkPart in pairs(_hawkParts) do
if _hawkPart == false then
if _name == "Hawk ln" then
_txt = "Missing HAWK Launcher\n"
elseif _name == "Hawk sr" then
_txt = _txt .. "Missing HAWK Search Radar\n"
else
_txt = _txt .. "Missing HAWK Track Radar\n"
end
else
table.insert(_posArray, _hawkPart.crateUnit:getPoint())
table.insert(_typeArray, _name)
end
end
if _txt ~= "" then
ctld.displayMessageToGroup(_heli, "Cannot build Hawk\n" .. _txt .. "\n\nOr the crates are not close enough together", 20)
return
else
-- destroy crates
for _name, _hawkPart in pairs(_hawkParts) do
if _heli:getCoalition() == 1 then
ctld.spawnedCratesRED[_hawkPart.crateUnit:getName()] = nil
else
ctld.spawnedCratesBLUE[_hawkPart.crateUnit:getName()] = nil
end
--destroy
_hawkPart.crateUnit:destroy()
end
-- HAWK READY!
local _spawnedGroup = ctld.spawnCrateGroup(_heli, _posArray, _typeArray)
ctld.completeHawkSystems[_spawnedGroup:getName()] = _spawnedGroup:getName()
trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed a full HAWK AA System to the field", 10)
end
end
function ctld.unpackMultiCrate(_heli,_nearestCrate,_nearbyCrates)
if string.match(_nearestCrate.details.desc, "HAWK") then
ctld.unpackHawk(_heli,_nearestCrate,_nearbyCrates)
return -- stop processing
end
-- 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
_crate.crateUnit:destroy()
end
local _spawnedGroup = ctld.spawnCrateGroup(_heli,{_point} ,{_nearestCrate.details.unit})
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 = mist.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 = mist.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 = mist.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
local _spawnedGroup = coalition.addGroup(_heli:getCountry(), Group.Category.GROUND, _group)
--activate by moving and so we can set ROE and Alarm state
local _dest = _spawnedGroup:getUnit(1):getPoint()
_dest = { x = _dest.x + 5, _y = _dest.y + 5, z = _dest.z + 5 }
ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _dest)
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
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 _enemyPos = ctld.findNearestEnemy(_details.side,_point,_maxSearch)
ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _enemyPos)
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
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)
return { x = _x, z = _z }
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.orderGroupToMoveToPoint(_leader, _destination)
local _group = _leader:getGroup()
local _mission = {
id = 'Mission',
params = {
route = {
points = {
[1] = {
action = 0,
x = _leader:getPoint().x,
y = _leader:getPoint().z,
speed = 0,
ETA = 100,
ETA_locked = false,
name = "Starting point",
task = nil
},
[2] = {
action = 0,
x = _destination.x,
y = _destination.z,
speed = 100,
ETA = 100,
ETA_locked = false,
name = "End Point",
task = nil
},
}
},
}
}
local _controller = _group: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(_mission)
end
-- are we in pickup zone
function ctld.inPickupZone(_heli)
if _heli:inAir() then
return false
end
local _heliPoint = _heli:getPoint()
for _, _zoneDetails in pairs(ctld.pickupZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
if _triggerZone ~= nil then
--get distance to center
local _dist = ctld.getDistance(_heliPoint, _triggerZone.point)
if _dist <= _triggerZone.radius then
return true
end
end
end
-- now check spawned 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
--get distance to center
local _dist = ctld.getDistance(_heliPoint,_fob:getPoint())
if _dist <= 150 then
return true
end
end
end
return false
end
-- are we in a dropoff zone
function ctld.inDropoffZone(_heli)
if _heli:inAir() 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 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 near friendly logistics zone
function ctld.inLogisticsZone(_heli)
if _heli:inAir() 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
function ctld.refreshSmoke()
if ctld.disableAllSmoke == true then
return
end
for _, _zoneDetails in pairs(ctld.pickupZones) do
local _triggerZone = trigger.misc.getZone(_zoneDetails[1])
if _triggerZone ~= nil and _zoneDetails[2] >= 0 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())
local _found = false
for _,_name in ipairs(ctld.vehicleTransportEnabled) do
-- env.info("TYPE: ".._type)
local _nameLower = string.lower(_name)
-- env.info("NAME: ".._nameLower)
if string.match(_type, _nameLower) ~= nil then
-- env.info("MATCH")
_found = true
break
end
end
return _found
end
function ctld.isJTACUnitType(_type)
_type = string.lower(_type)
local _found = false
for _,_name in ipairs(ctld.jtacUnitTypes) do
local _nameLower = string.lower(_name)
if string.match(_type, _nameLower) ~= nil then
_found = true
break
end
end
return _found
end
-- checks the status of all AI troop carriers and auto loads and unloads troops
function ctld.checkAIStatus()
timer.scheduleFunction(ctld.checkAIStatus,nil,timer.getTime()+5)
for _, _unitName in pairs(ctld.transportPilotNames) do
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil and _unit:getPlayerName() == nil then
-- no player name means AI!
if ctld.inPickupZone(_unit) and not ctld.troopsOnboard(_unit,true) then
ctld.loadTroops(_unit,true)
elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit,true) then
ctld.deployTroops(_unit,true)
end
if ctld.unitCanCarryVehicles(_unit) then
if ctld.inPickupZone(_unit) and not ctld.troopsOnboard(_unit,false) then
ctld.loadTroops(_unit,false)
elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit,false) then
ctld.deployTroops(_unit,false)
end
end
end
end
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() + 5)
for _, _unitName in pairs(ctld.transportPilotNames) do
local _unit = ctld.getTransportUnit(_unitName)
if _unit ~= nil then
local _groupId = _unit:getGroup():getID()
if ctld.addedTo[tostring(_groupId)] == nil and _unit:getPlayerName() ~= nil then
missionCommands.addSubMenuForGroup(_groupId, "Troop Transport")
missionCommands.addCommandForGroup(_groupId, "Load / Unload Troops", { "Troop Transport" }, ctld.loadUnloadTroops, { _unitName,true })
if ctld.unitCanCarryVehicles(_unit) then
missionCommands.addCommandForGroup(_groupId, "Load / Unload Vehicles", { "Troop Transport" }, ctld.loadUnloadTroops, { _unitName,false })
if ctld.enabledFOBBuilding then
missionCommands.addCommandForGroup(_groupId, "Load / Unload FOB Crate", { "Troop Transport" }, ctld.loadUnloadFOBCrate, { _unitName,false })
end
end
missionCommands.addCommandForGroup(_groupId, "Check Status", { "Troop Transport" }, ctld.checkTroopStatus, { _unitName })
if ctld.enableCrates then
if ctld.unitCanCarryVehicles(_unit) == false then
-- add menu for spawning crates
for _subMenuName, _crates in pairs(ctld.spawnableCrates) do
missionCommands.addSubMenuForGroup(_groupId, _subMenuName)
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
missionCommands.addCommandForGroup(_groupId, _crate.desc, {_subMenuName }, ctld.spawnCrate, { _unitName,_crate.weight })
end
end
end
end
end
missionCommands.addSubMenuForGroup(_groupId, "CTLD Commands")
missionCommands.addCommandForGroup(_groupId, "List Nearby Crates", { "CTLD Commands" }, ctld.listNearbyCrates, { _unitName })
missionCommands.addCommandForGroup(_groupId, "Unpack Sling Crates", { "CTLD Commands" }, ctld.unpackCrates, { _unitName })
if ctld.enabledFOBBuilding then
missionCommands.addCommandForGroup(_groupId, "Unpack FOB Crates", { "CTLD Commands" }, ctld.unpackFOBCrates, { _unitName })
end
if ctld.enableSmokeDrop then
missionCommands.addCommandForGroup(_groupId, "Drop Red Smoke", { "CTLD Commands" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Red })
missionCommands.addCommandForGroup(_groupId, "Drop Blue Smoke", { "CTLD Commands" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Blue })
-- missionCommands.addCommandForGroup(_groupId, "Drop Orange Smoke", { "Crate Commands" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Orange })
missionCommands.addCommandForGroup(_groupId, "Drop Green Smoke", { "CTLD Commands" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Green })
end
else
if ctld.enableSmokeDrop then
missionCommands.addSubMenuForGroup(_groupId, "Smoke Markers")
missionCommands.addCommandForGroup(_groupId, "Drop Red Smoke", { "Smoke Markers" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Red })
missionCommands.addCommandForGroup(_groupId, "Drop Blue Smoke", { "Smoke Markers" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Blue })
missionCommands.addCommandForGroup(_groupId, "Drop Orange Smoke", { "Smoke Markers"}, ctld.dropSmoke, { _unitName, trigger.smokeColor.Orange })
missionCommands.addCommandForGroup(_groupId, "Drop Green Smoke", { "Smoke Markers" }, ctld.dropSmoke, { _unitName, trigger.smokeColor.Green })
end
end
ctld.addedTo[tostring(_groupId)] = true
end
else
-- env.info(string.format("unit nil %s",_unitName))
end
end
if ctld.JTAC_jtacStatusF10 then
-- get all BLUE players
ctld.addJTACRadioCommand(coalition.side.BLUE)
-- get all RED players
ctld.addJTACRadioCommand(coalition.side.RED)
end
end
function ctld.addJTACRadioCommand(_side)
local _players = coalition.getPlayers(_side)
if _players ~= nil then
for _,_playerUnit in pairs(_players) do
local _groupId = _playerUnit:getGroup():getID()
-- env.info("adding command for "..index)
if ctld.jtacRadioAdded[tostring(_groupId)] == nil then
-- env.info("about command for "..index)
missionCommands.addCommandForGroup(_groupId, "JTAC Status", nil, ctld.getJTACStatus, _playerUnit:getCoalition())
ctld.jtacRadioAdded[tostring(_groupId)] = true
-- env.info("Added command for " .. index)
end
end
end
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.jtacLaserPoints = {}
ctld.jtacIRPoints = {}
ctld.jtacSmokeMarks = {}
ctld.jtacUnits = {} -- list of JTAC units for f10 command
ctld.jtacCurrentTargets = {}
ctld.jtacRadioAdded = {} --keeps track of who's had the radio command added
ctld.jtacGeneratedLaserCodes = {} -- keeps track of generated codes, cycles when they run out
ctld.jtacLaserPointCodes = {}
function ctld.JTACAutoLase(_jtacGroupName, _laserCode, _smoke, _lock, _colour)
if _lock == nil then
_lock = ctld.JTAC_lock
end
ctld.jtacLaserPointCodes[_jtacGroupName] = _laserCode
local _jtacGroup = ctld.getGroup(_jtacGroupName)
local _jtacUnit
if _jtacGroup == nil or #_jtacGroup == 0 then
if ctld.jtacUnits[_jtacGroupName] ~= nil then
ctld.notifyCoalition("JTAC Group " .. _jtacGroupName .. " KIA!", 10,ctld.jtacUnits[_jtacGroupName].side)
end
--remove from list
ctld.jtacUnits[_jtacGroupName] = nil
ctld.cleanupJTAC(_jtacGroupName)
return
else
_jtacUnit = _jtacGroup[1]
--add to list
ctld.jtacUnits[_jtacGroupName] = {name = _jtacUnit:getName(), side = _jtacUnit:getCoalition()}
-- 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)
env.info(_jtacGroupName .. ' Not Active - Waiting 30 seconds')
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour }, timer.getTime() + 30)
return
end
local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName)
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)
if _tempUnit ~= nil and _tempUnit:getLife() > 0 and _tempUnit:isActive() == true then
ctld.notifyCoalition(_jtacGroupName .. " target " .. _tempUnitInfo.unitType .. " lost. Scanning for Targets. ", 10,_jtacUnit:getCoalition())
else
ctld.notifyCoalition(_jtacGroupName .. " target " .. _tempUnitInfo.unitType .. " KIA. Good Job! Scanning for Targets. ", 10,_jtacUnit:getCoalition())
end
--remove from smoke list
ctld.jtacSmokeMarks[_tempUnitInfo.name] = nil
-- remove from target list
ctld.jtacCurrentTargets[_jtacGroupName] = nil
--stop lasing
ctld.cancelLase(_jtacGroupName)
end
if _enemyUnit == nil then
_enemyUnit = ctld.findNearestVisibleEnemy(_jtacUnit, _lock)
if _enemyUnit ~= nil then
-- store current target for easy lookup
ctld.jtacCurrentTargets[_jtacGroupName] = { name = _enemyUnit:getName(), unitType = _enemyUnit:getTypeName(), unitId = _enemyUnit:getID() }
ctld.notifyCoalition(_jtacGroupName .. " lasing new target " .. _enemyUnit:getTypeName() .. '. CODE: ' .. _laserCode ..ctld.getPositionString(_enemyUnit) , 10,_jtacUnit:getCoalition())
-- create smoke
if _smoke == true then
--create first smoke
ctld.createSmokeMarker(_enemyUnit, _colour)
end
end
end
if _enemyUnit ~= nil then
ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode)
-- env.info('Timer timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName())
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour }, timer.getTime() + 1)
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
-- env.info('LASE: No Enemies Nearby')
-- stop lazing the old spot
ctld.cancelLase(_jtacGroupName)
-- env.info('Timer Slow timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName())
timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour }, timer.getTime() + 5)
end
end
-- used by the timer function
function ctld.timerJTACAutoLase(_args)
ctld.JTACAutoLase(_args[1], _args[2], _args[3], _args[4], _args[5])
end
function ctld.cleanupJTAC(_jtacGroupName)
-- clear laser - just in case
ctld.cancelLase(_jtacGroupName)
-- Cleanup
ctld.jtacUnits[_jtacGroupName] = nil
ctld.jtacCurrentTargets[_jtacGroupName] = nil
end
function ctld.notifyCoalition(_message, _displayFor, _side)
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, y = _enemyPoint.y + 2.0, z = _enemyPoint.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
function ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode)
--cancelLase(jtacGroupName)
local _spots = {}
local _enemyVector = _enemyUnit:getPoint()
local _enemyVectorUpdated = { x = _enemyVector.x, y = _enemyVector.y + 2.0, z = _enemyVector.z }
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
-- 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)
local _x = 1
local _i = 1
local _units = nil
local _groupName = nil
local _nearestUnit = nil
local _nearestDistance = ctld.JTAC_maxDistance
local _enemyGroups
if _jtacUnit:getCoalition() == 1 then
_enemyGroups = coalition.getGroups(2, Group.Category.GROUND)
else
_enemyGroups = coalition.getGroups(1, Group.Category.GROUND)
end
local _jtacPoint = _jtacUnit:getPoint()
local _jtacPosition = _jtacUnit:getPosition()
local _tempPoint = nil
local _tempPosition = nil
local _tempDist = nil
-- finish this function
for _i = 1, #_enemyGroups do
if _enemyGroups[_i] ~= nil then
_groupName = _enemyGroups[_i]:getName()
_units = ctld.getGroup(_groupName)
if #_units > 0 then
for _x = 1, #_units do
--check to see if a JTAC has already targeted this unit
local _targeted = ctld.alreadyTarget(_jtacUnit, _units[_x])
local _allowedTarget = true
if _targetType == "vehicle" then
_allowedTarget = ctld.isVehicle(_units[_x])
elseif _targetType == "troop" then
_allowedTarget = ctld.isInfantry(_units[_x])
end
if _units[_x]:isActive() == true and _targeted == false and _allowedTarget == true then
-- calc distance
_tempPoint = _units[_x]:getPoint()
_tempDist = ctld.getDistance(_tempPoint, _jtacPoint)
if _tempDist < ctld.JTAC_maxDistance and _tempDist < _nearestDistance then
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 }
-- calc visible
if land.isVisible(_offsetEnemyPos, _offsetJTACPos) then
_nearestDistance = _tempDist
_nearestUnit = _units[_x]
end
end
end
end
end
end
end
if _nearestUnit == nil then
return nil
end
return _nearestUnit
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 _groupUnits = Group.getByName(groupName)
local _filteredUnits = {} --contains alive units
local _x = 1
if _groupUnits ~= nil then
_groupUnits = _groupUnits:getUnits()
if _groupUnits ~= nil and #_groupUnits > 0 then
for _x = 1, #_groupUnits do
if _groupUnits[_x]:getLife() > 0 and _groupUnits[_x]:isExist() then
table.insert(_filteredUnits, _groupUnits[_x])
end
end
end
end
return _filteredUnits
end
-- gets the JTAC status and displays to coalition units
function ctld.getJTACStatus(_side)
--returns the status of all JTAC units
local _jtacGroupName = nil
local _jtacUnit = nil
local _message = "JTAC STATUS: \n\n"
for _jtacGroupName, _jtacDetails in pairs(ctld.jtacUnits) do
--look up units
_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]
if _laserCode == nil then
_laserCode = "UNKNOWN"
end
if _enemyUnit ~= nil and _enemyUnit:getLife() > 0 and _enemyUnit:isActive() == true then
_message = _message .. "" .. _jtacGroupName .. " targeting " .. _enemyUnit:getTypeName().. " CODE: ".. _laserCode .. ctld.getPositionString(_enemyUnit) .. "\n"
else
_message = _message .. "" .. _jtacGroupName .. " searching for targets" .. ctld.getPositionString(_jtacUnit) .."\n"
end
end
end
if _message == "JTAC STATUS: \n\n" then
_message = "No Active JTACs"
end
ctld.notifyCoalition(_message, 10,_side)
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 = 1111
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
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,false)
local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_unit:getPosition().p)),5)
return " @ " .. _latLngStr .. " - MGRS ".. _mgrsString
end
-- ***************** SETUP SCRIPT ****************
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.droppedFOBCratesRED = {}
ctld.droppedFOBCratesBLUE = {}
ctld.builtFOBS = {} -- stores fully built fobs
ctld.completeHawkSystems = {} -- stores complete spawned groups from multiple crates
--used to lookup what the crate will contain
ctld.crateLookupTable = {}
-- 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]
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
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
-- Scheduled functions (run cyclically)
timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 5)
timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 5)
timer.scheduleFunction(ctld.checkAIStatus,nil,timer.getTime() + 5)
timer.scheduleFunction(ctld.checkTransportStatus,nil,timer.getTime()+5)
--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("CTLD READY")
--DEBUG FUNCTION
-- for key, value in pairs(getmetatable(_spawnedCrate)) do
-- env.info(tostring(key))
-- env.info(tostring(value))
-- end