--[[ JTAC Automatic Targeting and Laser Script Allows a JTAC to mark and hold an IR and Laser point on a target allowing TGP's to lock onto the lase and ease of target location using NV Goggles The JTAC will automatically switch targets when a target is destroyed or goes out of Line of Sight NOTE: LOS doesn't include buildings or tree's... Sorry! The script can also be useful in daylight by enabling the JTAC to mark enemy positions with Smoke. The JTAC will only move the smoke to the target every 5 minutes (to stop a huge trail of smoke markers) unless the target is destroyed, in which case the new target will be marked straight away with smoke. You can also enable an F10 menu option for coalition units allowing the JTAC(s) to report their current status. If a JTAC is down it won't report in. USAGE: Place JTAC units on the map with the mission editor putting each JTAC in it's own group containing only itself and no other units. Name the group something easy to remember e.g. JTAC1 and make sure the JTAC units have a unique name which must not be the same as the group name. The editor should do this for you but be careful if you copy and paste. Load the script at the start of the mission with a Trigger Once or as the init script of the mission Run the code below as a DO SCRIPT at the start of the mission, or after a delay if you prefer JTACAutoLase('JTAC1', 1688) OR JTACAutoLase('JTAC1', 1688, false,"all") - means no smoke marks for this jtac and it will target all ground troops OR JTACAutoLase('JTAC1', 1688, true,"vehicle") - means smoke marks for this jtac and it will target ONLY ground vehicles OR JTACAutoLase('JTAC1', 1688, true,"troop") - means smoke marks for this jtac and it will target ONLY ground troops OR JTACAutoLase('JTAC1', 1688, true,"all", 4) - means BLUE smoke marks for this jtac and it will target all ground troops Where JTAC1 is the Group name of the JTAC Group with one and only one JTAC unit. The script doesn't care if the unit isn't activated when run, as it'll automatically activate when the JTAC is activated in the mission but there can be a delay of up to 30 seconds after activation for the JTAC to start searching for targets. You can also run the code at any time if a JTAC is dynamically added to the map as long as you know the Group name of the JTAC. Last Edit: 21/04/2015 Change log: Added new config of JTAC smoke colour globally or per JTAC Added new config of JTAC targetting to either target only troops or vehicles or all Added new induvidual config of JTAC smoke and location which will override global setting Added Lat / Lon + MGRS to JTAC Target position Changed Lasing method to update laser marker rather than create and destroy - This gives much better tracking behaviour! Fixed JTAC lasing through terrain. Fixed Lase staying on when JTAC Dies Fixed Lase staying on when target dies and there are no other targets Added Radio noise when message is shown Stop multiple JTACS targeting the same target Parts of MIST used. Source is https://github.com/mrSkortch/MissionScriptingTools/blob/master/mist.lua ]] -- CONFIG JTAC_maxDistance = 50000 -- How far a JTAC can "see" in meters (with LOS) JTAC_smokeOn = true -- enables marking of target with smoke, can be overriden by the JTACAutoLase in editor JTAC_smokeColour = 1 -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4 JTAC_jtacStatusF10 = true -- enables F10 JTAC Status menu JTAC_location = true -- shows location in JTAC message, can be overriden by the JTACAutoLase in editor JTAC_lock = "all" -- "vehicle" OR "troop" OR "all" forces JTAC to only lock vehicles or troops or all ground units -- END CONFIG -- BE CAREFUL MODIFYING BELOW HERE GLOBAL_JTAC_LASE = {} GLOBAL_JTAC_IR = {} GLOBAL_JTAC_SMOKE = {} GLOBAL_JTAC_UNITS = {} -- list of JTAC units for f10 command GLOBAL_JTAC_CURRENT_TARGETS = {} GLOBAL_JTAC_RADIO_ADDED = {} --keeps track of who's had the radio command added GLOBAL_JTAC_LASER_CODES = {} -- keeps track of laser codes for jtac function JTACAutoLase(jtacGroupName, laserCode,smoke,lock,colour) if smoke == nil then smoke = JTAC_smokeOn end if lock == nil then lock = JTAC_lock end if colour == nil then colour = JTAC_smokeColour end GLOBAL_JTAC_LASER_CODES[jtacGroupName] = laserCode local jtacGroup = getGroup(jtacGroupName) local jtacUnit if jtacGroup == nil or #jtacGroup == 0 then notify("JTAC Group " .. jtacGroupName .. " KIA!", 10) --remove from list GLOBAL_JTAC_UNITS[jtacGroupName] = nil cleanupJTAC(jtacGroupName) return else jtacUnit = jtacGroup[1] --add to list GLOBAL_JTAC_UNITS[jtacGroupName] = jtacUnit:getName() end -- search for current unit if jtacUnit:isActive() == false then cleanupJTAC(jtacGroupName) env.info(jtacGroupName .. ' Not Active - Waiting 30 seconds') timer.scheduleFunction(timerJTACAutoLase, { jtacGroupName, laserCode,smoke,lock,colour}, timer.getTime() + 30) return end local enemyUnit = getCurrentUnit(jtacUnit, jtacGroupName) if enemyUnit == nil and GLOBAL_JTAC_CURRENT_TARGETS[jtacGroupName] ~= nil then local tempUnitInfo = GLOBAL_JTAC_CURRENT_TARGETS[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 notify(jtacGroupName .. " target " .. tempUnitInfo.unitType .. " lost. Scanning for Targets. ", 10) else notify(jtacGroupName .. " target " .. tempUnitInfo.unitType .. " KIA. Good Job! Scanning for Targets. ", 10) end --remove from smoke list GLOBAL_JTAC_SMOKE[tempUnitInfo.name] = nil -- remove from target list GLOBAL_JTAC_CURRENT_TARGETS[jtacGroupName] = nil --stop lasing cancelLase(jtacGroupName) end if enemyUnit == nil then enemyUnit = findNearestVisibleEnemy(jtacUnit,lock) if enemyUnit ~= nil then -- store current target for easy lookup GLOBAL_JTAC_CURRENT_TARGETS[jtacGroupName] = { name = enemyUnit:getName(), unitType = enemyUnit:getTypeName(), unitId = enemyUnit:getID() } notify(jtacGroupName .. " lasing new target " .. enemyUnit:getTypeName() .. '. CODE: ' .. laserCode ..getPositionString(enemyUnit) , 10) -- create smoke if smoke == true then --create first smoke createSmokeMarker(enemyUnit,colour) end end end if enemyUnit ~= nil then laseUnit(enemyUnit, jtacUnit, jtacGroupName, laserCode) -- env.info('Timer timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) timer.scheduleFunction(timerJTACAutoLase, { jtacGroupName, laserCode, smoke,lock,colour }, timer.getTime() + 1) if smoke == true then local nextSmokeTime = GLOBAL_JTAC_SMOKE[enemyUnit:getName()] --recreate smoke marker after 5 mins if nextSmokeTime ~= nil and nextSmokeTime < timer.getTime() then createSmokeMarker(enemyUnit, colour) end end else -- env.info('LASE: No Enemies Nearby') -- stop lazing the old spot cancelLase(jtacGroupName) -- env.info('Timer Slow timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) timer.scheduleFunction(timerJTACAutoLase, { jtacGroupName, laserCode, smoke,lock,colour }, timer.getTime() + 5) end end -- used by the timer function function timerJTACAutoLase(args) JTACAutoLase(args[1], args[2], args[3],args[4]) end function cleanupJTAC(jtacGroupName) -- clear laser - just in case cancelLase(jtacGroupName) -- Cleanup GLOBAL_JTAC_UNITS[jtacGroupName] = nil GLOBAL_JTAC_CURRENT_TARGETS[jtacGroupName] = nil end function notify(message, displayFor) trigger.action.outTextForCoalition(coalition.side.BLUE, message, displayFor) trigger.action.outSoundForCoalition(coalition.side.BLUE, "radiobeep.ogg") end function createSmokeMarker(enemyUnit,colour) --recreate in 5 mins GLOBAL_JTAC_SMOKE[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 cancelLase(jtacGroupName) --local index = "JTAC_"..jtacUnit:getID() local tempLase = GLOBAL_JTAC_LASE[jtacGroupName] if tempLase ~= nil then Spot.destroy(tempLase) GLOBAL_JTAC_LASE[jtacGroupName] = nil -- env.info('Destroy laze '..index) tempLase = nil end local tempIR = GLOBAL_JTAC_IR[jtacGroupName] if tempIR ~= nil then Spot.destroy(tempIR) GLOBAL_JTAC_IR[jtacGroupName] = nil -- env.info('Destroy laze '..index) tempIR = nil end end function 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 = GLOBAL_JTAC_LASE[jtacGroupName] local oldIR = GLOBAL_JTAC_IR[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: ' .. assert(result), false) else if result.irPoint then -- env.info(jtacUnit:getName() .. ' placed IR Pointer on '..enemyUnit:getName()) GLOBAL_JTAC_IR[jtacGroupName] = result.irPoint --store so we can remove after end if result.laserPoint then -- env.info(jtacUnit:getName() .. ' is Lasing '..enemyUnit:getName()..'. CODE:'..laserCode) GLOBAL_JTAC_LASE[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 getCurrentUnit(jtacUnit, jtacGroupName) local unit = nil if GLOBAL_JTAC_CURRENT_TARGETS[jtacGroupName] ~= nil then unit = Unit.getByName(GLOBAL_JTAC_CURRENT_TARGETS[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 = getDistance(tempPoint.x, tempPoint.z, jtacPoint.x, jtacPoint.z) if tempDist < 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 findNearestVisibleEnemy(jtacUnit, targetType) local x = 1 local i = 1 local units = nil local groupName = nil local nearestUnit = nil local nearestDistance = JTAC_maxDistance local redGroups = coalition.getGroups(1, Group.Category.GROUND) local jtacPoint = jtacUnit:getPoint() local jtacPosition = jtacUnit:getPosition() local tempPoint = nil local tempPosition = nil local tempDist = nil -- finish this function for i = 1, #redGroups do if redGroups[i] ~= nil then groupName = redGroups[i]:getName() units = getGroup(groupName) if #units > 0 then for x = 1, #units do --check to see if a JTAC has already targeted this unit local targeted = alreadyTarget(jtacUnit,units[x]) local allowedTarget = true if targetType == "vehicle" then allowedTarget = isVehicle(units[x]) elseif targetType == "troop" then allowedTarget = isInfantry(units[x]) end if units[x]:isActive() == true and targeted == false and allowedTarget == true then -- calc distance tempPoint = units[x]:getPoint() -- tempPosition = units[x]:getPosition() tempDist = getDistance(tempPoint.x, tempPoint.z, jtacPoint.x, jtacPoint.z) -- env.info("tempDist" ..tempDist) -- env.info("JTAC_maxDistance" ..JTAC_maxDistance) if tempDist < 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 alreadyTarget(jtacUnit, enemyUnit) for y , jtacTarget in pairs(GLOBAL_JTAC_CURRENT_TARGETS) 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 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 then table.insert(filteredUnits, groupUnits[x]) end end end end return filteredUnits end -- Distance measurement between two positions, assume flat world function getDistance(xUnit, yUnit, xZone, yZone) local xDiff = xUnit - xZone local yDiff = yUnit - yZone return math.sqrt(xDiff * xDiff + yDiff * yDiff) end -- gets the JTAC status and displays to coalition units function getJTACStatus() --returns the status of all JTAC units local jtacGroupName = nil local jtacUnit = nil local jtacUnitName = nil local message = "JTAC STATUS: \n\n" for jtacGroupName, jtacUnitName in pairs(GLOBAL_JTAC_UNITS) do --look up units jtacUnit = Unit.getByName(jtacUnitName) if jtacUnit ~= nil and jtacUnit:getLife() > 0 and jtacUnit:isActive() == true then local enemyUnit = getCurrentUnit(jtacUnit, jtacGroupName) local laserCode = GLOBAL_JTAC_LASER_CODES[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 .. getPositionString(enemyUnit) .. "\n" else message = message .. "" .. jtacGroupName .. " searching for targets" .. getPositionString(jtacUnit) .."\n" end end end notify(message, 25) end -- Radio command for players (F10 Menu) function addRadioCommands() --looop over all players and add command -- missionCommands.addCommandForCoalition( coalition.side.BLUE, "JTAC Status" ,nil, getJTACStatus ,nil) timer.scheduleFunction(addRadioCommands, nil, timer.getTime() + 10) local blueGroups = coalition.getGroups(coalition.side.BLUE) local x = 1 if blueGroups ~= nil then for x, tmpGroup in pairs(blueGroups) do local index = "GROUP_" .. Group.getID(tmpGroup) -- env.info("adding command for "..index) if GLOBAL_JTAC_RADIO_ADDED[index] == nil then -- env.info("about command for "..index) missionCommands.addCommandForGroup(Group.getID(tmpGroup), "JTAC Status", nil, getJTACStatus, nil) GLOBAL_JTAC_RADIO_ADDED[index] = true -- env.info("Added command for " .. index) end end end end function isInfantry(unit) local typeName = unit:getTypeName() --type coerce tostring typeName = string.lower(typeName.."") local soldierType = { "infantry","paratrooper","stinger","manpad"} 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 isVehicle(unit) if isInfantry(unit) then return false end return true end function getPositionString(unit) if JTAC_location == false then return "" end local latLngStr = latLngString(unit,3) local mgrsString = MGRSString(coord.LLtoMGRS(coord.LOtoLL(unit:getPosition().p)),5) return " @ " .. latLngStr .. " - MGRS "..mgrsString end -- source of Function MIST - https://github.com/mrSkortch/MissionScriptingTools/blob/master/mist.lua function latLngString(unit, acc) local lat, lon = coord.LOtoLL(unit:getPosition().p) local latHemi, lonHemi if lat > 0 then latHemi = 'N' else latHemi = 'S' end if lon > 0 then lonHemi = 'E' else lonHemi = 'W' end lat = math.abs(lat) lon = math.abs(lon) local latDeg = math.floor(lat) local latMin = (lat - latDeg)*60 local lonDeg = math.floor(lon) local lonMin = (lon - lonDeg)*60 -- degrees, decimal minutes. latMin = roundNumber(latMin, acc) lonMin = roundNumber(lonMin, acc) if latMin == 60 then latMin = 0 latDeg = latDeg + 1 end if lonMin == 60 then lonMin = 0 lonDeg = lonDeg + 1 end local minFrmtStr -- create the formatting string for the minutes place if acc <= 0 then -- no decimal place. minFrmtStr = '%02d' else local width = 3 + acc -- 01.310 - that's a width of 6, for example. minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi end -- source of Function MIST - https://github.com/mrSkortch/MissionScriptingTools/blob/master/mist.lua function MGRSString(MGRS, acc) if acc == 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', roundNumber(MGRS.Easting/(10^(5-acc)), 0)) .. ' ' .. string.format('%0' .. acc .. 'd', roundNumber(MGRS.Northing/(10^(5-acc)), 0)) end end -- From http://lua-users.org/wiki/SimpleRound function roundNumber(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end -- add the radio commands if JTAC_jtacStatusF10 == true then timer.scheduleFunction(addRadioCommands, nil, timer.getTime() + 1) end --DEBUG FUNCTIONS - IGNORE --function print_functions(o) -- -- for key,value in pairs(getmetatable(o)) do -- env.info(tostring(key) .." ".. tostring(value)) -- end --end -- --function dump_table(o) -- if type(o) == 'table' then -- local s = '{ ' -- for k,v in pairs(o) do -- if type(k) ~= 'number' then k = '"'..k..'"' end -- s = s .. '['..k..'] = ' .. dump_table(v) .. ',' -- end -- return s .. '} ' -- else -- return tostring(o) -- end --end