-- Artillery Spotter script - Multiplayer version
-- by Carsten Gurk aka Don Rudi

local version = "MP 1.2.1"

-- User configurable variables

local user_fireDelay = 10				-- time to impcat of the rounds
local user_quantity = 20				-- how many rounds will be fired in a fire for effect task
local user_spread = 50					-- impact radius of the rounds during fire for effect
local user_spottingDistance = 15		-- max allowable distance from player to target to prevent cheating. In kilometers.
local user_restrictByType = ""        	-- Restriction by type ("", "helo", etc.)
local user_restrictByUnitName = "" 		-- Restriction by unit name ("", "spotter", etc.), not case sensitive	
local user_markerPrefix = ""     		-- Prefix for marker text, for instance "#arty"

-- end of user block

-- Script variables

local SINGLE_ROUND = false  			-- pilot called single round on marker (from F10 menu)

local artyCall     = 0 					-- pilot called arty (from F10 menu)
local artyRadius   = user_spread		-- Artillery Radius
local adjustRadius = 20  				-- fire adjustment
local quantity     = 1   				-- Rounds expanded 
local quantity_effect = user_quantity	-- Rounds expanded during fire for effect
local tntEquivalent = 12				-- TNT equivalent for explosion
local fireDelay = user_fireDelay		-- delay til artillery fires in seconds

local firstShotFired = true

local markerSet = false

local pos = { x = 0, y = 0, z = 0 }
local playerPos = { x = 0, y = 0, z = 0 }

local target = {}

local adjustX = 0
local adjustZ = 0

local adjustDistance = 0				-- Adjust fire (from F10 menu)
local adjustDirection = 0				-- Adjust fire (from F10 menu)

local position = ""

local markerText = ""

local artyTasks = {}

local menuItems = false

-- optional arty enabled user flag, for use in triggers, if the player wants to

trigger.action.setUserFlag( "artyEnabled", 1 )


-- select format of target coordinates MGRS or LAT/LONG

local outputFormat = "MGRS"
--local outputFormat = "LL"


-- set values selected by player through F10 menu

local function setValue( _valueType, _value, _initiatorName )

	if _valueType == "arty" then
	
		artyCall = _value
				
	end
	
	if _valueType == "dist" then
	
		adjustDistance = _value
		
		trigger.action.outText("Fire adjusted by "..adjustDistance.." meters", 10)
		
	end
	
	if _valueType == "dir" then
	
		adjustDirection = _value
		
	end

	artyAction( _initiatorName )
	
end


-- Function to add F10 menu items for a specific group and store references

local function addMenuItems(groupId, initiatorName)

	menuItems = true

    local artyTask = artyTasks[initiatorName]
	
    artyTask.ArtyMenu = missionCommands.addSubMenuForGroup(groupId, 'Artillery Commands')
    artyTask.AdjustDistance = missionCommands.addSubMenuForGroup(groupId, 'Adjust distance', artyTask.ArtyMenu)
    artyTask.AdjustDirection = missionCommands.addSubMenuForGroup(groupId, 'Adjust direction', artyTask.ArtyMenu)

    artyTask.commands = {}
	artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'request single round', artyTask.ArtyMenu, function() setValue("arty", 1, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'request fire for effect', artyTask.ArtyMenu, function() setValue("arty", 2, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 20m', artyTask.AdjustDistance, function() setValue("dist", 20, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 50m', artyTask.AdjustDistance, function() setValue("dist", 50, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 100m', artyTask.AdjustDistance, function() setValue("dist", 100, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 200m', artyTask.AdjustDistance, function() setValue("dist", 200, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 500m', artyTask.AdjustDistance, function() setValue("dist", 500, initiatorName) end)
    
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North', artyTask.AdjustDirection, function() setValue("dir", 360, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North-East', artyTask.AdjustDirection, function() setValue("dir", 45, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire East', artyTask.AdjustDirection, function() setValue("dir", 90, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South-East', artyTask.AdjustDirection, function() setValue("dir", 135, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South', artyTask.AdjustDirection, function() setValue("dir", 180, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South-West', artyTask.AdjustDirection, function() setValue("dir", 225, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire West', artyTask.AdjustDirection, function() setValue("dir", 270, initiatorName) end)
    artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North-West', artyTask.AdjustDirection, function() setValue("dir", 315, initiatorName) end)
end

-- Function to remove F10 menu items for a specific group

local function removeMenuItems(initiatorName)

    local artyTask = artyTasks[initiatorName]
	
    if artyTask then
	
        for _, command in ipairs(artyTask.commands) do
            missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, command)
        end
        missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.AdjustDistance)
        missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.AdjustDirection)
        missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.ArtyMenu)
    end
	
	menuItems = false
	
end

-- Calculate distance

local function getDist(_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

-- Shelling Zone

local function shellZone ( _initiatorName )

	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty Task Created - fire incoming "..quantity.." rounds", 10)
	
	if artyCall == 1 then
		artyRadius = 5
	else
		artyRadius = 50
	end
	
	local _shellPos = artyTasks[_initiatorName].pos
	
	if firstShotFired == true then
		_shellPos.x = _shellPos.x + adjustX
		_shellPos.y = _shellPos.y
		_shellPos.z = _shellPos.z + adjustZ
	end 
	
	for i = 1, quantity do
	
		-- Create a random offset within the given radius
		
		local randomX = math.random(-artyRadius, artyRadius)
		local randomZ = math.random(-artyRadius, artyRadius)
		
		local strikePos = {
		  x = _shellPos.x + randomX,
		  y = _shellPos.y,
		  z = _shellPos.z + randomZ
		}
		
		-- Delay the shelling by 1 second for each shell
		
		timer.scheduleFunction(function()
		  trigger.action.explosion(strikePos, tntEquivalent)  -- Create an explosion at the target position with a predefined power
		end, {}, timer.getTime() + i)
	end	
	
	--[[
	if firstShotFired == false then 
		addMenuItems ()
		firstShotFired = true
	end
	]]--
	
end

-- MGRS conversion to LL to x,z

local function convertMGRStoPos ( _mrgs )

	local lat, lon = coord.MGRStoLL( _mgrs )
    local markerPos = coord.LLtoLO( lat, lon, 0 )
	return markerPos

end

-- x,z coordinates conversion to LAT/LONG and MGRS

local function convertPos2Coord ( _pos, _reply )

	local lat, lon, alt = coord.LOtoLL (_pos)
	local lat_degrees = math.floor (lat)
	local lat_minutes = (60 * (lat - lat_degrees))
	local lat_seconds = math.floor(60 * (lat_minutes - math.floor(lat_minutes))) 
	lat_minutes = math.floor(lat_minutes)

	local lon_degrees = math.floor (lon)
	local lon_minutes = (60 * (lon - lon_degrees))
	local lon_seconds = math.floor (60 * (lon_minutes - math.floor(lon_minutes)))
	lon_minutes = math.floor(lon_minutes)
	  
	local coordStringLL = "N" .. lat_degrees .. " " .. lat_minutes .. " " ..lat_seconds.. " E".. lon_degrees .. " " .. lon_minutes .. " ".. lon_seconds
	  
	local targetMGRS = coord.LLtoMGRS(lat, lon)
	targetMGRS.Easting = math.floor (( targetMGRS.Easting /10 ) + 0.5 )
	targetMGRS.Northing = math.floor (( targetMGRS.Northing / 10 ) + 0.5 )
	--local coordStringMGRS = targetMGRS.UTMZone.." "..targetMGRS.MGRSDigraph.." "..string.sub(targetMGRS.Easting, 1, -2).." "..string.sub(targetMGRS.Northing, 1, -2)
	local coordStringMGRS = targetMGRS.UTMZone.." "..targetMGRS.MGRSDigraph.." "..targetMGRS.Easting.." "..targetMGRS.Northing
	  
	if outputFormat == "MGRS" then
		coordString = coordStringMGRS
	else
		coordString = coordStringLL
	end
	  
	-- return either formated string or MGRS coordinate  
	
	if _reply == "string" then
		return coordString
	elseif _reply == "pos" then
		return targetMGRS
	end
end  

-- Who is the player

-- Function to determine which unit is controlled by the player
--[[
local function getPlayerControlledUnit()
	
  local playerUnit = nil
  
  -- Iterate through all coalitions and their respective player units
  
  for coalitionID = 1, 2 do  -- 1 = Red, 2 = Blue
    local playerUnits = coalition.getPlayers(coalitionID)
    for _, unit in ipairs(playerUnits) do
      if unit and unit:getPlayerName() then
        playerUnit = unit
        break
      end
    end
    if playerUnit then
      break
    end
  end
  
  return playerUnit
end
]]--

-- Check if user has created F10 map marker

artyAction = function ( _initiatorName )
	
  -- Check Call for arty - 1 = single round, 2 = fire for effect
  
  if artyCall == 1 or artyCall == 2 then
      
    if MARKER_FOUND == true and artyTasks[_initiatorName] then
	
       -- check if target is within 15km from player
	   
	   --trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty action marker found.", 10)
	   
		--local _player = _initiator
		local _playerPos = artyTasks[_initiatorName].playerPos
		local _targetPos = artyTasks[_initiatorName].pos
		
		local _dist = math.floor( getDist ( _targetPos, _playerPos ) / 10 ) / 100
		 
	    if  trigger.misc.getUserFlag( "artyEnabled" ) == 1 and _dist <= user_spottingDistance then
	   
		   position = convertPos2Coord ( _targetPos, "string" )
			
			if artyCall == 1 then
				trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty single round requested on "..position, 10)
				quantity = 1
				
			elseif artyCall == 2 then
				trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty fire for effect requested on "..position, 10)
				quantity = quantity_effect
			end
			
			timer.scheduleFunction(shellZone, _initiatorName, timer.getTime() + fireDelay)
			trigger.action.setUserFlag( "artyFired", 1 )
		
		else	
			trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Artillery not available", 10)
		end
		
	else
		trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty Requested Without Marker", 10)
    end
  
     artyCall = 0
	 
  end
  
  -- Check Call for arty direction correction
  
  if adjustDirection == 360 then
     
	adjustX = adjustDistance
	adjustZ = 0
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the North", 10)
	
    adjustDirection = 0
  end
  
  if adjustDirection == 45 then
     
	adjustX = adjustDistance
	adjustZ = adjustDistance
	artyRadius = 5
				
    adjustDirection = 0
  end
  
  if adjustDirection == 90 then
     
	adjustX = 0
	adjustZ = adjustDistance
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the East", 10)
	      
    adjustDirection = 0
  end
  
  if adjustDirection == 135 then
     
	adjustX = -adjustDistance
	adjustZ = adjustDistance
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South-East", 10)
	       
    adjustDirection = 0
  end
  
  if adjustDirection == 180 then

	adjustX = -adjustDistance
	adjustZ = 0
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South", 10)
	         
    adjustDirection = 0
  end
  
  if adjustDirection == 225 then
     
	adjustX = -adjustDistance
	adjustZ = -adjustDistance
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South-West", 10)
	        
    adjustDirection = 0
  end
  
  if adjustDirection == 270 then
     
	adjustX = 0
	adjustZ = -adjustDistance
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted meters to the West", 10)
	          
    adjustDirection = 0
  end
  
  if adjustDirection == 315 then
     
	adjustX = adjustDistance
	adjustZ = -adjustDistance
	artyRadius = 5
				
	trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted meters to the North-West", 10)
	          
    adjustDirection = 0
  end

end

-- Main

trigger.action.outText("Arty spotter script "..version.." loaded", 10)


-- Map Marker Text - read and process

-- Function to remove spaces from a string

local function removeSpaces( _text )

	_text = _text:gsub( " ", "" )
	_text = _text:gsub( "-", "" )
	return _text
	
end

-- Function to validate the structure of the MGRS coordinate

local function checkValidMGRS( _mgrs, len)

	if len == 13 then
  
    -- Pattern: 2 digits 1 letter UTM Zone, 2 letters MGRS Digraph, 4 digits Easting, 4 digits Northing
    return _mgrs:match("^%d%d%u%u%u%d%d%d%d%d%d%d%d$")
	
  elseif len == 10 then
  
    -- Pattern: 2 letters MGRS Digraph, 4 digits Easting, 4 digits Northing
    return _mgrs:match("^%u%u%d%d%d%d%d%d%d%d$")
	
  elseif len == 8 then
  
    -- Pattern: 4 digits Easting, 4 digits Northing
    return _mgrs:match("^%d%d%d%d%d%d%d%d$")
	
  else
    return false
  end
end

-- Function to validate and complete MGRS coordinates

local function processMGRS( _text, _playerPos, initiatorName )
  local _cleanedText = string.upper( removeSpaces( _text ) )
  local len = #_cleanedText
  
  local _isValidMGRS = checkValidMGRS( _cleanedText, len)
  
  if _isValidMGRS then
	
	trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Processing MGRS: " .. _cleanedText, 10)
	
	if len == 13 then
	
		-- Complete MGRS coordinate
		return _cleanedText
		
	elseif len == 10 then
	  
		-- Add UTM Zone based on player position
		local _utmZone = coord.LLtoMGRS(_playerPos.Lat, _playerPos.Lon).UTMZone
		return _utmZone .. _cleanedText
		
	elseif len == 8 then
	  
		-- Add UTM Zone and MGRS Digraph based on player position
		local _mgrs = coord.LLtoMGRS( _playerPos.Lat, _playerPos.Lon )
		return _mgrs.UTMZone .. _mgrs.MGRSDigraph .. _cleanedText
		
	else
  
		-- Invalid MGRS coordinate
		return nil
	end
	
  else
  
	trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Invalid text input: " .. _cleanedText, 10)
	return nil
  
  end
	
  
end

-- Function to convert a valid MGRS to vec3
local function MGRStoVec3( _mgrs )

  local lat, lon = coord.MGRStoLL( _mgrs )
  local vec3 = coord.LLtoLO( lat, lon, 0 )
  return vec3
end

-- Function to check if the initiator is valid based on restrictions

local function isValidInitiator(initiator)
    if not initiator then return false end
    
    -- Check type restriction
	
    if user_restrictByType == "helo" then
	
        if not initiator:getDesc().category == Unit.Category.Helicopter then
            return false
        end
    end

    -- Check name restriction
	
    if user_restrictByUnitName ~= "" then
	
        local name = initiator:getName():lower()
		
        if not name:find(user_restrictByUnitName:lower()) then
            return false
        end
    end

    return true
end

-- Function to check if the marker text has the required prefix and remove it

local function checkAndRemovePrefix(text)

    if user_markerPrefix ~= "" and text:sub(1, #user_markerPrefix) == user_markerPrefix then
	
        return true, text:sub(#user_markerPrefix + 1)
    
	elseif user_markerPrefix == "" then
	
		return true, text
		
	else
	
		return false, text
		
	end
	
end


-- Event handler for map marker creation
local function onPlayerAddMarker(event)

	
	if event.id == world.event.S_EVENT_MARK_ADDED and user_markerPrefix == "" then
	
		 if isValidInitiator(event.initiator) then
		 
			--local hasPrefix, cleanedText = checkAndRemovePrefix(event.text)
			hasPrefix = true
		 
            if hasPrefix then
		
				MARKER_FOUND = true
				pos = event.pos
				
				adjustX = 0
				adjustZ = 0
				adjustDistance = 0
				
				if event.initiator then
				
					local initiatorName = event.initiator:getName()
					
					local playerUnit = event.initiator
					local playerPos = playerUnit:getPoint()
					
					-- Store position
										
					if not artyTasks[initiatorName] then
						artyTasks[initiatorName] = {}
					end
					
					trigger.action.outTextForUnit( event.initiator:getID(), "Marker added", 5)
					
					artyTasks[initiatorName].playerPos = playerPos
					artyTasks[initiatorName].pos = pos
					artyTasks[initiatorName].unitID = event.initiator:getID()
				
					-- Add menu items for the initiator's group
					local groupId = event.initiator:getGroup():getID()
					artyTasks[initiatorName].groupID = groupId
				
					if menuItems == false then 
						addMenuItems(groupId, initiatorName)
					end
		
				end
				
			end
			
		else
            --trigger.action.outText("You do not have permission to add a marker.", 5)
        end
		    
	elseif event.id == world.event.S_EVENT_MARK_CHANGE then
	 
        if isValidInitiator(event.initiator) then
		
			local hasPrefix, cleanedText = checkAndRemovePrefix(event.text)
			
            if hasPrefix then
			
				MARKER_FOUND = true
		
				local markText = cleanedText
				
				trigger.action.outText("Text: "..markText, 10)
				
				if markText and event.initiator then
				
					local initiatorName = event.initiator:getName()
					local playerUnit = event.initiator
					
					if not artyTasks[initiatorName] then
						artyTasks[initiatorName] = {}
					end
					
					artyTasks[initiatorName].initiator = event.initiator
					artyTasks[initiatorName].unitID = event.initiator:getID()
					
					trigger.action.outTextForUnit( event.initiator:getID(), "Marker changed", 5)
					
					if playerUnit then
					
						local playerPos = playerUnit:getPoint()
						local lat, lon = coord.LOtoLL(playerPos)
						local playerPosition = { Lat = lat, Lon = lon }
						local validMGRS = processMGRS(markText, playerPosition, initiatorName)

						if validMGRS then
						
							trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Valid MGRS: " .. validMGRS, 10)
							
							local tmpMGRS = {
								UTMZone = string.sub(validMGRS, 1, 3),
								MGRSDigraph = string.sub(validMGRS, 4, 5),
								Easting = tonumber(string.sub(validMGRS, 6, 9)) * 10,
								Northing = tonumber(string.sub(validMGRS, 10, 13)) * 10
							}
							
							local targetPoint = MGRStoVec3(tmpMGRS)
							targetPoint.y = land.getHeight({ x = targetPoint.x, y = targetPoint.z })

							
							artyTasks[initiatorName].pos = targetPoint
							artyTasks[initiatorName].playerPos = playerPos
							
														
						else
							trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Invalid MGRS coordinate entered.", 10)
						end
						
						local groupId = event.initiator:getGroup():getID()
						artyTasks[initiatorName].groupID = groupId
						
						if menuItems == false then 
							addMenuItems(groupId, initiatorName)
						end
					end
				end
			end
		else
            --trigger.action.outText("You do not have permission to change this marker.", 5)
		end
		
    elseif event.id == world.event.S_EVENT_MARK_REMOVED then
	
        trigger.action.outText("Marker removed", 5)
		
        if event.initiator then
		
            local initiatorName = event.initiator:getName()
			
			if artyTasks[initiatorName] then
			
				removeMenuItems(initiatorName)
                artyTasks[initiatorName] = nil
				
            end
			
        end
	end
end

-- Register the event handler
local eventHandler = { f = onPlayerAddMarker }
function eventHandler:onEvent(e)
  self.f(e)
end
world.addEventHandler(eventHandler)




