mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
971 lines
33 KiB
Lua
971 lines
33 KiB
Lua
--[[
|
|
Simple AFAC System v1.0
|
|
========================
|
|
|
|
A lightweight, standalone Forward Air Controller system for DCS World.
|
|
No external dependencies required.
|
|
|
|
Features:
|
|
- Automatic AFAC aircraft detection
|
|
- Target scanning with line-of-sight verification
|
|
- Laser designation with customizable codes
|
|
- Visual marking (smoke/flares)
|
|
- F10 map markers with detailed target information
|
|
- Auto and manual targeting modes
|
|
- Clean F10 menu integration
|
|
|
|
Author: Based on original FAC script, streamlined for simplicity
|
|
]]
|
|
|
|
-- =====================================================
|
|
-- CONFIGURATION
|
|
-- =====================================================
|
|
|
|
AFAC = {}
|
|
|
|
AFAC.Config = {
|
|
-- Detection ranges
|
|
maxRange = 18520, -- Maximum AFAC detection range in meters
|
|
|
|
-- Aircraft types that auto-qualify as AFAC (perfect for dynamic spawning)
|
|
afacAircraft = {
|
|
-- Helicopters
|
|
"SA342L",
|
|
"SA342Mistral",
|
|
"SA342Minigun",
|
|
"UH-60L",
|
|
"UH-1H",
|
|
"OH-58D",
|
|
"AH-64D_BLK_II",
|
|
"Mi-8MT",
|
|
"Mi-24P",
|
|
"Ka-50",
|
|
"Ka-50_3",
|
|
"AH-1W",
|
|
|
|
-- Light Aircraft
|
|
"TF-51D",
|
|
"P-51D-30-NA",
|
|
"Bf-109K-4",
|
|
"FW-190D9",
|
|
"I-16",
|
|
"C-101EB",
|
|
"C-101CC",
|
|
"L-39ZA",
|
|
"L-39C",
|
|
|
|
-- Jets (for high-speed FAC)
|
|
--"A-10C",
|
|
--"A-10C_2",
|
|
--"AV8BNA",
|
|
--"F-16C_50",
|
|
--"FA-18C_hornet",
|
|
--"F-14",
|
|
--"A-4E-C"
|
|
},
|
|
|
|
-- Available laser codes
|
|
laserCodes = {'1688', '1677', '1666', '1113', '1115', '1111'},
|
|
|
|
-- Smoke/flare colors
|
|
smokeColors = {
|
|
GREEN = 0,
|
|
RED = 1,
|
|
WHITE = 2,
|
|
ORANGE = 3,
|
|
BLUE = 4
|
|
},
|
|
|
|
-- Default marker settings
|
|
defaultSmokeColor = {
|
|
[1] = 4, -- RED coalition uses BLUE smoke
|
|
[2] = 3 -- BLUE coalition uses ORANGE smoke
|
|
},
|
|
|
|
-- Marker duration (seconds)
|
|
mapMarkerDuration = 120,
|
|
smokeInterval = 300,
|
|
|
|
-- Target update intervals
|
|
autoUpdateInterval = 1.0,
|
|
manualScanRange = 18520,
|
|
|
|
-- Debug mode
|
|
debug = true
|
|
}
|
|
|
|
-- =====================================================
|
|
-- CORE DATA STRUCTURES
|
|
-- =====================================================
|
|
|
|
AFAC.Data = {
|
|
pilots = {}, -- Active AFAC pilots
|
|
targets = {}, -- Current targets per pilot
|
|
laserPoints = {}, -- Laser designations
|
|
irPoints = {}, -- IR pointers
|
|
smokeMarks = {}, -- Smoke marker tracking
|
|
onStation = {}, -- On-station status
|
|
laserCodes = {}, -- Assigned laser codes per pilot
|
|
markerSettings = {}, -- Per-pilot marker preferences
|
|
menuIds = {}, -- F10 menu tracking
|
|
manualTargets = {}, -- Manual mode target lists
|
|
nextMarkerId = 1000 -- Map marker ID counter
|
|
}
|
|
|
|
-- =====================================================
|
|
-- UTILITY FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Debug logging
|
|
function AFAC.log(message)
|
|
if AFAC.Config.debug then
|
|
env.info("AFAC: " .. tostring(message))
|
|
end
|
|
end
|
|
|
|
-- Check if table contains value
|
|
function AFAC.contains(table, value)
|
|
for i = 1, #table do
|
|
if table[i] == value then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Calculate distance between two points
|
|
function AFAC.getDistance(point1, point2)
|
|
local dx = point1.x - point2.x
|
|
local dz = point1.z - point2.z
|
|
return math.sqrt(dx * dx + dz * dz)
|
|
end
|
|
|
|
-- Get unit heading in radians
|
|
function AFAC.getHeading(unit)
|
|
local unitPos = unit:getPosition()
|
|
if unitPos then
|
|
local heading = math.atan2(unitPos.x.z, unitPos.x.x)
|
|
if heading < 0 then
|
|
heading = heading + 2 * math.pi
|
|
end
|
|
return heading
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- Convert coordinates to Lat/Long string
|
|
function AFAC.coordToString(point)
|
|
local lat, lon = coord.LOtoLL(point)
|
|
local latDeg = math.floor(lat)
|
|
local latMin = (lat - latDeg) * 60
|
|
local lonDeg = math.floor(lon)
|
|
local lonMin = (lon - lonDeg) * 60
|
|
|
|
local latDir = latDeg >= 0 and "N" or "S"
|
|
local lonDir = lonDeg >= 0 and "E" or "W"
|
|
|
|
return string.format("%02d°%06.3f'%s %03d°%06.3f'%s",
|
|
math.abs(latDeg), latMin, latDir,
|
|
math.abs(lonDeg), lonMin, lonDir)
|
|
end
|
|
|
|
-- Get MGRS coordinate string
|
|
function AFAC.getMGRS(point)
|
|
local lat, lon = coord.LOtoLL(point)
|
|
-- Simplified MGRS - in real implementation you'd want full MGRS conversion
|
|
return string.format("MGRS: %02d%s%05d%05d",
|
|
math.floor(lat), "ABC"[math.random(1,3)],
|
|
math.random(10000, 99999), math.random(10000, 99999))
|
|
end
|
|
|
|
-- Get group ID from unit
|
|
function AFAC.getGroupId(unit)
|
|
local group = unit:getGroup()
|
|
if group then
|
|
return group:getID()
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Notify coalition
|
|
function AFAC.notifyCoalition(message, duration, coalition)
|
|
trigger.action.outTextForCoalition(coalition, message, duration or 10)
|
|
end
|
|
|
|
-- =====================================================
|
|
-- AIRCRAFT DETECTION & MANAGEMENT
|
|
-- =====================================================
|
|
|
|
-- Check if unit qualifies as AFAC (by aircraft type only)
|
|
function AFAC.isAFAC(unit)
|
|
if not unit then
|
|
AFAC.log("isAFAC: unit is nil")
|
|
return false
|
|
end
|
|
|
|
local unitType = unit:getTypeName()
|
|
AFAC.log("Checking unit type: " .. tostring(unitType))
|
|
|
|
-- Check aircraft type only - perfect for dynamic spawning
|
|
local result = AFAC.contains(AFAC.Config.afacAircraft, unitType)
|
|
AFAC.log("isAFAC result for " .. unitType .. ": " .. tostring(result))
|
|
|
|
return result
|
|
end
|
|
|
|
-- Add pilot to AFAC system
|
|
function AFAC.addPilot(unit)
|
|
local unitName = unit:getName()
|
|
local groupId = AFAC.getGroupId(unit)
|
|
|
|
if not groupId then return end
|
|
|
|
-- Initialize pilot data
|
|
AFAC.Data.pilots[unitName] = {
|
|
name = unitName,
|
|
unit = unit,
|
|
coalition = unit:getCoalition(),
|
|
groupId = groupId
|
|
}
|
|
|
|
-- Set default laser code
|
|
AFAC.Data.laserCodes[unitName] = AFAC.Config.laserCodes[1]
|
|
|
|
-- Set default marker settings
|
|
AFAC.Data.markerSettings[unitName] = {
|
|
type = "SMOKE",
|
|
color = AFAC.Config.defaultSmokeColor[unit:getCoalition()]
|
|
}
|
|
|
|
-- Set on station
|
|
AFAC.Data.onStation[unitName] = true
|
|
|
|
-- Notify player
|
|
local message = string.format("AFAC System Active\nAircraft: %s\nUse F10 menu to control targeting",
|
|
unit:getTypeName())
|
|
trigger.action.outTextForGroup(groupId, message, 15)
|
|
|
|
-- Start auto-lasing
|
|
AFAC.startAutoLasing(unitName)
|
|
|
|
AFAC.log("Added AFAC pilot: " .. unitName)
|
|
end
|
|
|
|
-- Remove pilot from system
|
|
function AFAC.removePilot(unitName)
|
|
if not AFAC.Data.pilots[unitName] then return end
|
|
|
|
-- Clean up laser points
|
|
AFAC.cancelLasing(unitName)
|
|
|
|
-- Clean up data
|
|
AFAC.Data.pilots[unitName] = nil
|
|
AFAC.Data.targets[unitName] = nil
|
|
AFAC.Data.laserPoints[unitName] = nil
|
|
AFAC.Data.irPoints[unitName] = nil
|
|
AFAC.Data.smokeMarks[unitName] = nil
|
|
AFAC.Data.onStation[unitName] = nil
|
|
AFAC.Data.laserCodes[unitName] = nil
|
|
AFAC.Data.markerSettings[unitName] = nil
|
|
AFAC.Data.manualTargets[unitName] = nil
|
|
|
|
AFAC.log("Removed AFAC pilot: " .. unitName)
|
|
end
|
|
|
|
-- =====================================================
|
|
-- TARGET DETECTION
|
|
-- =====================================================
|
|
|
|
-- Find nearest visible enemy to AFAC unit
|
|
function AFAC.findNearestTarget(afacUnit, maxRange)
|
|
local afacPoint = afacUnit:getPoint()
|
|
local afacCoalition = afacUnit:getCoalition()
|
|
local enemyCoalition = afacCoalition == 1 and 2 or 1
|
|
|
|
local nearestTarget = nil
|
|
local nearestDistance = maxRange or AFAC.Config.maxRange
|
|
|
|
-- Search for enemy units
|
|
local searchVolume = {
|
|
id = world.VolumeType.SPHERE,
|
|
params = {
|
|
point = afacPoint,
|
|
radius = nearestDistance
|
|
}
|
|
}
|
|
|
|
local function checkUnit(foundUnit)
|
|
if foundUnit:getCoalition() ~= enemyCoalition then return end
|
|
if not foundUnit:isActive() then return end
|
|
if foundUnit:inAir() then return end
|
|
if foundUnit:getLife() <= 1 then return end
|
|
|
|
local unitPoint = foundUnit:getPoint()
|
|
local distance = AFAC.getDistance(afacPoint, unitPoint)
|
|
|
|
if distance >= nearestDistance then return end
|
|
|
|
-- Check line of sight
|
|
local offsetAfacPos = {x = afacPoint.x, y = afacPoint.y + 2, z = afacPoint.z}
|
|
local offsetUnitPos = {x = unitPoint.x, y = unitPoint.y + 2, z = unitPoint.z}
|
|
|
|
if not land.isVisible(offsetAfacPos, offsetUnitPos) then return end
|
|
|
|
-- Priority system: SAMs first, then vehicles, then infantry
|
|
local priority = 1
|
|
if foundUnit:hasAttribute("SAM TR") or foundUnit:hasAttribute("IR Guided SAM") then
|
|
priority = 0.1 -- Highest priority
|
|
elseif foundUnit:hasAttribute("Vehicles") then
|
|
priority = 0.5
|
|
end
|
|
|
|
local adjustedDistance = distance * priority
|
|
|
|
if adjustedDistance < nearestDistance then
|
|
nearestDistance = adjustedDistance
|
|
nearestTarget = foundUnit
|
|
end
|
|
end
|
|
|
|
world.searchObjects(Object.Category.UNIT, searchVolume, checkUnit)
|
|
|
|
return nearestTarget
|
|
end
|
|
|
|
-- Scan area for multiple targets (manual mode)
|
|
function AFAC.scanForTargets(afacUnit, maxCount)
|
|
local afacPoint = afacUnit:getPoint()
|
|
local afacCoalition = afacUnit:getCoalition()
|
|
local enemyCoalition = afacCoalition == 1 and 2 or 1
|
|
|
|
local targets = {}
|
|
local samTargets = {}
|
|
|
|
local searchVolume = {
|
|
id = world.VolumeType.SPHERE,
|
|
params = {
|
|
point = afacPoint,
|
|
radius = AFAC.Config.manualScanRange
|
|
}
|
|
}
|
|
|
|
local function checkUnit(foundUnit)
|
|
if foundUnit:getCoalition() ~= enemyCoalition then return end
|
|
if not foundUnit:isActive() then return end
|
|
if foundUnit:inAir() then return end
|
|
if foundUnit:getLife() <= 1 then return end
|
|
|
|
local unitPoint = foundUnit:getPoint()
|
|
|
|
-- Check line of sight
|
|
local offsetAfacPos = {x = afacPoint.x, y = afacPoint.y + 2, z = afacPoint.z}
|
|
local offsetUnitPos = {x = unitPoint.x, y = unitPoint.y + 2, z = unitPoint.z}
|
|
|
|
if not land.isVisible(offsetAfacPos, offsetUnitPos) then return end
|
|
|
|
-- Separate SAMs from other targets
|
|
if foundUnit:hasAttribute("SAM TR") or foundUnit:hasAttribute("IR Guided SAM") then
|
|
table.insert(samTargets, foundUnit)
|
|
else
|
|
table.insert(targets, foundUnit)
|
|
end
|
|
end
|
|
|
|
world.searchObjects(Object.Category.UNIT, searchVolume, checkUnit)
|
|
|
|
-- Priority: SAMs first, then others
|
|
local finalTargets = {}
|
|
for i = 1, math.min(#samTargets, maxCount or 10) do
|
|
table.insert(finalTargets, samTargets[i])
|
|
end
|
|
|
|
local remainingSlots = (maxCount or 10) - #finalTargets
|
|
for i = 1, math.min(#targets, remainingSlots) do
|
|
table.insert(finalTargets, targets[i])
|
|
end
|
|
|
|
return finalTargets
|
|
end
|
|
|
|
-- =====================================================
|
|
-- LASER DESIGNATION SYSTEM
|
|
-- =====================================================
|
|
|
|
-- Start laser designation on target
|
|
function AFAC.startLasing(afacUnit, target, laserCode)
|
|
local unitName = afacUnit:getName()
|
|
|
|
-- Cancel existing lasing
|
|
AFAC.cancelLasing(unitName)
|
|
|
|
local targetPoint = target:getPoint()
|
|
local targetVector = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z}
|
|
|
|
-- Create laser and IR points
|
|
local success, result = pcall(function()
|
|
local laserSpot = Spot.createLaser(afacUnit, {x = 0, y = 2, z = 0}, targetVector, laserCode)
|
|
local irSpot = Spot.createInfraRed(afacUnit, {x = 0, y = 2, z = 0}, targetVector)
|
|
return {laser = laserSpot, ir = irSpot}
|
|
end)
|
|
|
|
if success and result then
|
|
AFAC.Data.laserPoints[unitName] = result.laser
|
|
AFAC.Data.irPoints[unitName] = result.ir
|
|
AFAC.log("Started lasing target for " .. unitName)
|
|
else
|
|
AFAC.log("Failed to create laser designation for " .. unitName)
|
|
end
|
|
end
|
|
|
|
-- Update laser position
|
|
function AFAC.updateLasing(unitName, target)
|
|
local laserSpot = AFAC.Data.laserPoints[unitName]
|
|
local irSpot = AFAC.Data.irPoints[unitName]
|
|
|
|
if not laserSpot or not irSpot then return end
|
|
|
|
local targetPoint = target:getPoint()
|
|
local targetVector = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z}
|
|
|
|
laserSpot:setPoint(targetVector)
|
|
irSpot:setPoint(targetVector)
|
|
end
|
|
|
|
-- Cancel laser designation
|
|
function AFAC.cancelLasing(unitName)
|
|
local laserSpot = AFAC.Data.laserPoints[unitName]
|
|
local irSpot = AFAC.Data.irPoints[unitName]
|
|
|
|
if laserSpot then
|
|
Spot.destroy(laserSpot)
|
|
AFAC.Data.laserPoints[unitName] = nil
|
|
end
|
|
|
|
if irSpot then
|
|
Spot.destroy(irSpot)
|
|
AFAC.Data.irPoints[unitName] = nil
|
|
end
|
|
end
|
|
|
|
-- =====================================================
|
|
-- VISUAL MARKING SYSTEM
|
|
-- =====================================================
|
|
|
|
-- Create smoke or flare marker
|
|
function AFAC.createVisualMarker(target, markerType, color)
|
|
local targetPoint = target:getPoint()
|
|
local markerPoint = {x = targetPoint.x, y = targetPoint.y + 2, z = targetPoint.z}
|
|
|
|
if markerType == "SMOKE" then
|
|
trigger.action.smoke(markerPoint, color)
|
|
else -- FLARE
|
|
trigger.action.signalFlare(markerPoint, color, 0)
|
|
end
|
|
end
|
|
|
|
-- Create F10 map marker with target details
|
|
function AFAC.createMapMarker(target, spotter)
|
|
local targetPoint = target:getPoint()
|
|
local coalition = AFAC.Data.pilots[spotter].coalition
|
|
|
|
-- Get target details
|
|
local velocity = target:getVelocity()
|
|
local speed = 0
|
|
if velocity then
|
|
speed = math.sqrt(velocity.x^2 + velocity.z^2) * 2.237 -- Convert to mph
|
|
end
|
|
|
|
local heading = AFAC.getHeading(target) * 180 / math.pi
|
|
local coords = AFAC.coordToString(targetPoint)
|
|
local mgrs = AFAC.getMGRS(targetPoint)
|
|
local altitude = math.floor(targetPoint.y)
|
|
|
|
local markerText = string.format("%s\n%s\n%s\nAlt: %dm/%dft\nHdg: %03d°\nSpd: %.0f mph\nSpotter: %s",
|
|
target:getTypeName(),
|
|
coords,
|
|
mgrs,
|
|
altitude,
|
|
altitude * 3.28084,
|
|
heading,
|
|
speed,
|
|
spotter
|
|
)
|
|
|
|
local markerId = AFAC.Data.nextMarkerId
|
|
AFAC.Data.nextMarkerId = AFAC.Data.nextMarkerId + 1
|
|
|
|
trigger.action.markToCoalition(markerId, markerText, targetPoint, coalition, false, "AFAC Target")
|
|
|
|
-- Schedule marker removal
|
|
timer.scheduleFunction(
|
|
function(args)
|
|
trigger.action.removeMark(args[1])
|
|
end,
|
|
{markerId},
|
|
timer.getTime() + AFAC.Config.mapMarkerDuration
|
|
)
|
|
|
|
return markerId
|
|
end
|
|
|
|
-- =====================================================
|
|
-- AUTOMATIC LASING SYSTEM
|
|
-- =====================================================
|
|
|
|
-- Start automatic target lasing
|
|
function AFAC.startAutoLasing(unitName)
|
|
timer.scheduleFunction(AFAC.autoLaseUpdate, {unitName}, timer.getTime() + 1)
|
|
end
|
|
|
|
-- Auto-lasing update function
|
|
function AFAC.autoLaseUpdate(args)
|
|
local unitName = args[1]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
-- Check if pilot still exists and is on station
|
|
if not pilot or not AFAC.Data.onStation[unitName] then
|
|
return -- Don't reschedule
|
|
end
|
|
|
|
local afacUnit = pilot.unit
|
|
if not afacUnit or not afacUnit:isActive() or afacUnit:getLife() <= 0 then
|
|
AFAC.removePilot(unitName)
|
|
return
|
|
end
|
|
|
|
local currentTarget = AFAC.Data.targets[unitName]
|
|
local laserCode = AFAC.Data.laserCodes[unitName]
|
|
local markerSettings = AFAC.Data.markerSettings[unitName]
|
|
|
|
-- Check if current target is still valid
|
|
if currentTarget and (not currentTarget:isActive() or currentTarget:getLife() <= 1) then
|
|
-- Target destroyed
|
|
local message = string.format("[%s] Target %s destroyed. Good job! Scanning for new targets.",
|
|
unitName, currentTarget:getTypeName())
|
|
AFAC.notifyCoalition(message, 10, pilot.coalition)
|
|
|
|
AFAC.Data.targets[unitName] = nil
|
|
AFAC.cancelLasing(unitName)
|
|
currentTarget = nil
|
|
end
|
|
|
|
-- Find new target if needed
|
|
if not currentTarget then
|
|
local newTarget = AFAC.findNearestTarget(afacUnit)
|
|
if newTarget then
|
|
AFAC.Data.targets[unitName] = newTarget
|
|
|
|
-- Start lasing
|
|
AFAC.startLasing(afacUnit, newTarget, laserCode)
|
|
|
|
-- Create visual markers
|
|
AFAC.createVisualMarker(newTarget, markerSettings.type, markerSettings.color)
|
|
AFAC.createMapMarker(newTarget, unitName)
|
|
|
|
-- Notify coalition
|
|
local message = string.format("[%s] Lasing new target: %s, CODE: %s",
|
|
unitName, newTarget:getTypeName(), laserCode)
|
|
AFAC.notifyCoalition(message, 10, pilot.coalition)
|
|
|
|
currentTarget = newTarget
|
|
end
|
|
end
|
|
|
|
-- Update laser position if we have a target
|
|
if currentTarget then
|
|
AFAC.updateLasing(unitName, currentTarget)
|
|
|
|
-- Update smoke markers periodically
|
|
local nextSmokeTime = AFAC.Data.smokeMarks[unitName]
|
|
if not nextSmokeTime or nextSmokeTime < timer.getTime() then
|
|
AFAC.createVisualMarker(currentTarget, markerSettings.type, markerSettings.color)
|
|
AFAC.Data.smokeMarks[unitName] = timer.getTime() + AFAC.Config.smokeInterval
|
|
end
|
|
end
|
|
|
|
-- Schedule next update
|
|
timer.scheduleFunction(AFAC.autoLaseUpdate, args, timer.getTime() + AFAC.Config.autoUpdateInterval)
|
|
end
|
|
|
|
-- =====================================================
|
|
-- F10 MENU SYSTEM
|
|
-- =====================================================
|
|
|
|
-- Add F10 menus for AFAC pilot
|
|
function AFAC.addMenus(unitName)
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
if not pilot then return end
|
|
|
|
local groupId = pilot.groupId
|
|
|
|
-- Main AFAC menu
|
|
local mainMenu = missionCommands.addSubMenuForGroup(groupId, "AFAC Control")
|
|
|
|
-- Targeting mode menu
|
|
local targetMenu = missionCommands.addSubMenuForGroup(groupId, "Targeting Mode", mainMenu)
|
|
missionCommands.addCommandForGroup(groupId, "Auto Mode ON", targetMenu, AFAC.setAutoMode, {unitName, true})
|
|
missionCommands.addCommandForGroup(groupId, "Auto Mode OFF", targetMenu, AFAC.setAutoMode, {unitName, false})
|
|
|
|
-- Manual targeting
|
|
local manualMenu = missionCommands.addSubMenuForGroup(groupId, "Manual Targeting", targetMenu)
|
|
missionCommands.addCommandForGroup(groupId, "Scan for Targets", manualMenu, AFAC.manualScan, {unitName})
|
|
|
|
-- Target selection (will be populated after scan)
|
|
local selectMenu = missionCommands.addSubMenuForGroup(groupId, "Select Target", manualMenu)
|
|
for i = 1, 10 do
|
|
missionCommands.addCommandForGroup(groupId, "Target " .. i, selectMenu, AFAC.selectManualTarget, {unitName, i})
|
|
end
|
|
|
|
-- Laser code menu
|
|
local laserMenu = missionCommands.addSubMenuForGroup(groupId, "Laser Codes", mainMenu)
|
|
for _, code in ipairs(AFAC.Config.laserCodes) do
|
|
missionCommands.addCommandForGroup(groupId, "Code: " .. code, laserMenu, AFAC.setLaserCode, {unitName, code})
|
|
end
|
|
|
|
-- Marker settings
|
|
local markerMenu = missionCommands.addSubMenuForGroup(groupId, "Marker Settings", mainMenu)
|
|
|
|
-- Smoke colors
|
|
local smokeMenu = missionCommands.addSubMenuForGroup(groupId, "Smoke Color", markerMenu)
|
|
for colorName, colorValue in pairs(AFAC.Config.smokeColors) do
|
|
missionCommands.addCommandForGroup(groupId, colorName, smokeMenu, AFAC.setMarkerColor, {unitName, "SMOKE", colorValue})
|
|
end
|
|
|
|
-- Flare colors (limited selection)
|
|
local flareMenu = missionCommands.addSubMenuForGroup(groupId, "Flare Color", markerMenu)
|
|
missionCommands.addCommandForGroup(groupId, "GREEN", flareMenu, AFAC.setMarkerColor, {unitName, "FLARE", 0})
|
|
missionCommands.addCommandForGroup(groupId, "WHITE", flareMenu, AFAC.setMarkerColor, {unitName, "FLARE", 2})
|
|
missionCommands.addCommandForGroup(groupId, "ORANGE", flareMenu, AFAC.setMarkerColor, {unitName, "FLARE", 3})
|
|
|
|
-- Status
|
|
missionCommands.addCommandForGroup(groupId, "AFAC Status", mainMenu, AFAC.showStatus, {unitName})
|
|
end
|
|
|
|
-- =====================================================
|
|
-- F10 MENU FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Set auto/manual mode
|
|
function AFAC.setAutoMode(args)
|
|
local unitName = args[1]
|
|
local autoMode = args[2]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
AFAC.Data.onStation[unitName] = autoMode
|
|
|
|
if autoMode then
|
|
trigger.action.outTextForGroup(pilot.groupId, "Auto targeting mode enabled", 10)
|
|
AFAC.startAutoLasing(unitName)
|
|
else
|
|
trigger.action.outTextForGroup(pilot.groupId, "Auto targeting mode disabled", 10)
|
|
AFAC.cancelLasing(unitName)
|
|
AFAC.Data.targets[unitName] = nil
|
|
end
|
|
end
|
|
|
|
-- Manual target scan
|
|
function AFAC.manualScan(args)
|
|
local unitName = args[1]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
local targets = AFAC.scanForTargets(pilot.unit, 10)
|
|
AFAC.Data.manualTargets[unitName] = targets
|
|
|
|
-- Report found targets
|
|
if #targets > 0 then
|
|
local afacPoint = pilot.unit:getPoint()
|
|
trigger.action.outTextForGroup(pilot.groupId, "Targets found:", 5)
|
|
|
|
for i, target in ipairs(targets) do
|
|
local targetPoint = target:getPoint()
|
|
local distance = AFAC.getDistance(afacPoint, targetPoint)
|
|
local bearing = math.atan2(targetPoint.z - afacPoint.z, targetPoint.x - afacPoint.x) * 180 / math.pi
|
|
if bearing < 0 then bearing = bearing + 360 end
|
|
|
|
local message = string.format("Target %d: %s, Bearing %03d°, Range %.1fkm",
|
|
i, target:getTypeName(), bearing, distance / 1000)
|
|
trigger.action.outTextForGroup(pilot.groupId, message, 15)
|
|
|
|
-- Create map marker
|
|
AFAC.createMapMarker(target, unitName)
|
|
end
|
|
else
|
|
trigger.action.outTextForGroup(pilot.groupId, "No targets found in range", 10)
|
|
end
|
|
end
|
|
|
|
-- Select manual target
|
|
function AFAC.selectManualTarget(args)
|
|
local unitName = args[1]
|
|
local targetIndex = args[2]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
local targets = AFAC.Data.manualTargets[unitName]
|
|
if not targets or not targets[targetIndex] then
|
|
trigger.action.outTextForGroup(pilot.groupId, "Invalid target selection", 10)
|
|
return
|
|
end
|
|
|
|
local target = targets[targetIndex]
|
|
if not target:isActive() or target:getLife() <= 1 then
|
|
trigger.action.outTextForGroup(pilot.groupId, "Selected target is no longer active", 10)
|
|
return
|
|
end
|
|
|
|
-- Set as current target
|
|
AFAC.Data.targets[unitName] = target
|
|
|
|
-- Start lasing
|
|
local laserCode = AFAC.Data.laserCodes[unitName]
|
|
AFAC.startLasing(pilot.unit, target, laserCode)
|
|
|
|
-- Create markers
|
|
local markerSettings = AFAC.Data.markerSettings[unitName]
|
|
AFAC.createVisualMarker(target, markerSettings.type, markerSettings.color)
|
|
|
|
-- Notify
|
|
local message = string.format("Designating Target %d: %s, CODE: %s",
|
|
targetIndex, target:getTypeName(), laserCode)
|
|
trigger.action.outTextForGroup(pilot.groupId, message, 10)
|
|
|
|
AFAC.notifyCoalition("[" .. unitName .. "] " .. message, 10, pilot.coalition)
|
|
end
|
|
|
|
-- Set laser code
|
|
function AFAC.setLaserCode(args)
|
|
local unitName = args[1]
|
|
local laserCode = args[2]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
AFAC.Data.laserCodes[unitName] = laserCode
|
|
trigger.action.outTextForGroup(pilot.groupId, "Laser code set to: " .. laserCode, 10)
|
|
|
|
-- Update current lasing if active
|
|
local currentTarget = AFAC.Data.targets[unitName]
|
|
if currentTarget then
|
|
AFAC.startLasing(pilot.unit, currentTarget, laserCode)
|
|
end
|
|
end
|
|
|
|
-- Set marker color/type
|
|
function AFAC.setMarkerColor(args)
|
|
local unitName = args[1]
|
|
local markerType = args[2]
|
|
local color = args[3]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
AFAC.Data.markerSettings[unitName] = {
|
|
type = markerType,
|
|
color = color
|
|
}
|
|
|
|
local colorNames = {"GREEN", "RED", "WHITE", "ORANGE", "BLUE"}
|
|
trigger.action.outTextForGroup(pilot.groupId,
|
|
string.format("Marker set to %s %s", colorNames[color + 1] or "UNKNOWN", markerType), 10)
|
|
end
|
|
|
|
-- Show AFAC status
|
|
function AFAC.showStatus(args)
|
|
local unitName = args[1]
|
|
local pilot = AFAC.Data.pilots[unitName]
|
|
|
|
if not pilot then return end
|
|
|
|
local status = "AFAC STATUS:\n\n"
|
|
|
|
for pilotName, pilotData in pairs(AFAC.Data.pilots) do
|
|
if pilotData.coalition == pilot.coalition thenn then
|
|
local target = AFAC.Data.targets[pilotName]
|
|
local laserCode = AFAC.Data.laserCodes[pilotName]
|
|
local onStation = AFAC.Data.onStation[pilotName]
|
|
|
|
if target and target:isActive() then
|
|
status = status .. string.format("%s: Targeting %s, CODE: %s\n",
|
|
pilotName, target:getTypeName(), laserCode)
|
|
elseif onStation then
|
|
status = status .. string.format("%s: On station, searching for targets\n", pilotName)
|
|
else
|
|
status = status .. string.format("%s: Off station\n", pilotName)
|
|
end
|
|
end
|
|
end
|
|
|
|
trigger.action.outTextForGroup(pilot.groupId, status, 30)
|
|
end
|
|
|
|
-- =====================================================
|
|
-- EVENT HANDLERS
|
|
-- =====================================================
|
|
|
|
-- Event handler for unit spawning and player connections
|
|
AFAC.EventHandler = {}
|
|
|
|
function AFAC.EventHandler:onEvent(event)
|
|
-- Handle new unit spawning
|
|
if event.id == world.event.S_EVENT_BIRTH then
|
|
AFAC.log("S_EVENT_BIRTH triggered")
|
|
local unit = event.initiator
|
|
|
|
if unit and Object.getCategory(unit) == Object.Category.UNIT then
|
|
local objDesc = unit:getDesc()
|
|
AFAC.log("Birth event - Unit category: " .. tostring(objDesc.category))
|
|
if objDesc.category == Unit.Category.AIRPLANE or objDesc.category == Unit.Category.HELICOPTER then
|
|
AFAC.log("Birth event - Aircraft detected: " .. unit:getTypeName())
|
|
if AFAC.isAFAC(unit) then
|
|
AFAC.log("Birth event - AFAC aircraft confirmed, scheduling add")
|
|
-- Delay slightly to ensure unit is fully initialized
|
|
timer.scheduleFunction(
|
|
function(args)
|
|
local u = args[1]
|
|
if u and u:isActive() then
|
|
AFAC.log("Adding pilot from birth event: " .. u:getName())
|
|
AFAC.addPilot(u)
|
|
AFAC.addMenus(u:getName())
|
|
end
|
|
end,
|
|
{unit},
|
|
timer.getTime() + 2
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Debug: Log ALL player enter events to see what's happening
|
|
if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then
|
|
AFAC.log("PLAYER ENTERED UNIT - Type: " .. (event.initiator and event.initiator:getTypeName() or "UNKNOWN"))
|
|
end
|
|
|
|
-- Handle player entering existing unit (like joining a running UH-1H)
|
|
if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then
|
|
AFAC.log("S_EVENT_PLAYER_ENTER_UNIT triggered")
|
|
local unit = event.initiator
|
|
|
|
if unit and Object.getCategory(unit) == Object.Category.UNIT then
|
|
local objDesc = unit:getDesc()
|
|
AFAC.log("Player enter - Unit category: " .. tostring(objDesc.category))
|
|
if objDesc.category == Unit.Category.AIRPLANE or objDesc.category == Unit.Category.HELICOPTER then
|
|
AFAC.log("Player enter - Aircraft detected: " .. unit:getTypeName())
|
|
if AFAC.isAFAC(unit) then
|
|
local unitName = unit:getName()
|
|
AFAC.log("Player enter - AFAC aircraft confirmed: " .. unitName)
|
|
|
|
-- Check if already registered
|
|
if not AFAC.Data.pilots[unitName] then
|
|
AFAC.log("Player enter - Not registered, adding pilot")
|
|
-- Add pilot and menus for existing aircraft
|
|
timer.scheduleFunction(
|
|
function(args)
|
|
local u = args[1]
|
|
if u and u:isActive() then
|
|
AFAC.log("Adding pilot from player enter event: " .. u:getName())
|
|
AFAC.addPilot(u)
|
|
AFAC.addMenus(u:getName())
|
|
end
|
|
end,
|
|
{unit},
|
|
timer.getTime() + 1
|
|
)
|
|
else
|
|
AFAC.log("Player enter - Already registered")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- =====================================================
|
|
-- INITIALIZATION
|
|
-- =====================================================
|
|
|
|
-- Add event handler
|
|
world.addEventHandler(AFAC.EventHandler)
|
|
|
|
-- Continuous check for new AFAC pilots (catches players joining existing aircraft)
|
|
function AFAC.checkForNewPilots()
|
|
for coalitionId = 1, 2 do
|
|
local airGroups = coalition.getGroups(coalitionId, Group.Category.AIRPLANE)
|
|
local heliGroups = coalition.getGroups(coalitionId, Group.Category.HELICOPTER)
|
|
|
|
local allGroups = {}
|
|
for _, group in ipairs(airGroups) do table.insert(allGroups, group) end
|
|
for _, group in ipairs(heliGroups) do table.insert(allGroups, group) end
|
|
|
|
for _, group in ipairs(allGroups) do
|
|
local units = group:getUnits()
|
|
if units then
|
|
for _, unit in ipairs(units) do
|
|
if unit and unit:isActive() and AFAC.isAFAC(unit) then
|
|
local unitName = unit:getName()
|
|
|
|
-- Check if this is a player-controlled AFAC that we haven't registered yet
|
|
if unit:getPlayerName() and not AFAC.Data.pilots[unitName] then
|
|
AFAC.log("Found new player AFAC: " .. unitName .. " (Player: " .. unit:getPlayerName() .. ")")
|
|
AFAC.addPilot(unit)
|
|
AFAC.addMenus(unitName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Reschedule check every 10 seconds
|
|
timer.scheduleFunction(AFAC.checkForNewPilots, nil, timer.getTime() + 10)
|
|
end
|
|
|
|
-- Initialize existing units (for mission restart)
|
|
timer.scheduleFunction(
|
|
function()
|
|
for coalitionId = 1, 2 do
|
|
local airGroups = coalition.getGroups(coalitionId, Group.Category.AIRPLANE)
|
|
local heliGroups = coalition.getGroups(coalitionId, Group.Category.HELICOPTER)
|
|
|
|
local allGroups = {}
|
|
for _, group in ipairs(airGroups) do table.insert(allGroups, group) end
|
|
for _, group in ipairs(heliGroups) do table.insert(allGroups, group) end
|
|
|
|
for _, group in ipairs(allGroups) do
|
|
local units = group:getUnits()
|
|
if units then
|
|
for _, unit in ipairs(units) do
|
|
if unit and unit:isActive() and AFAC.isAFAC(unit) then
|
|
AFAC.addPilot(unit)
|
|
AFAC.addMenus(unit:getName())
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
AFAC.log("AFAC System initialized")
|
|
|
|
-- Start continuous pilot checking
|
|
AFAC.checkForNewPilots()
|
|
end,
|
|
nil,
|
|
timer.getTime() + 5
|
|
)
|
|
|
|
-- Success message
|
|
trigger.action.outText("Simple AFAC System v1.0 loaded successfully!", 10)
|
|
AFAC.log("AFAC Script fully loaded and initialized")
|
|
|
|
-- Test message every 30 seconds to verify script is running
|
|
function AFAC.heartbeat()
|
|
AFAC.log("AFAC System heartbeat - script is running")
|
|
timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30)
|
|
end
|
|
|
|
timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30) |