Merge branch 'new-wingman-system'

This commit is contained in:
Ambroise Garel 2025-07-23 22:21:10 +02:00
commit 850a4c7c50
8 changed files with 218 additions and 3 deletions

View File

@ -198,6 +198,28 @@ do
return callsignTable
end
function DCSEx.unitCallsignMaker.getNextGroupCallSign(callsign)
if not callsign then return nil end
local callsignName = callsign:sub(1, #callsign - 2):lower()
local callsignGroupNumber = tonumber(callsign:sub(#callsign - 2, #callsign - 1))
local callsignUnitNumber = tonumber(callsign:sub(#callsign - 1, #callsign))
for csType,_ in pairs(CALLSIGNS) do
for csNameIndex,_ in pairs(CALLSIGNS[csType]) do
if CALLSIGNS[csType][csNameIndex]:lower() == callsignName then
return {
[1] = csNameIndex,
[2] = callsignGroupNumber,
[3] = callsignUnitNumber,
["name"] = CALLSIGNS[csType][csNameIndex]..tostring(callsignGroupNumber),
}
end
end
end
return nil
end
do
local missionGroups = DCSEx.envMission.getGroups()
for _,g in ipairs(missionGroups) do

View File

@ -101,6 +101,26 @@ do
return groupTable
end
local function setAircraftTaskFollow(groupTable, followedGroupID)
groupTable.task = "Escort"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "Follow",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = {
groupId = followedGroupID,
pos = { x = -100, y = 0, z = -100 },
lastWptIndexFlag = false,
lastWptIndex = -1
},
})
return groupTable
end
local function setAircraftTaskOrbit(groupTable, options)
-- TODO: oval orbit
table.insert(groupTable.route.points[#groupTable.route.points].task.params.tasks,
@ -341,7 +361,11 @@ do
if isAirUnit then
if options.taskAwacs then setAircraftTaskAwacs(groupTable) end
if options.taskCAP then setAircraftTaskCAP(groupTable) end
setAircraftTaskOrbit(groupTable, options)
if options.taskFollow then
setAircraftTaskFollow(groupTable, options.taskFollow)
else
setAircraftTaskOrbit(groupTable, options)
end
groupCallsign = DCSEx.unitCallsignMaker.getCallsign(unitTypes[1])
groupTable.name = groupCallsign.name
end

View File

@ -70,6 +70,21 @@ Library.radioMessages = {
pilotWarningMANPADS = { "MANPADS launch! Flare, flare, flare!", "Flight, MANPADS in the air. Dump flares, now!", "Coming from the ground, MANPADS hot!", "Go defensive, MANPADS off your nose! Flare, flare!", "MANPADS just came up from the deck, break hard and pop everything!" },
pilotWarningSAMLaunch = { "Spike! SAM just launched, break!", "SAM up! Defensive now!", "Launch! SAM, coming fast, pump chaff, go cold!", "SAM in the air, break hard!", "SAM fired, visual smoke! Extend, extend!" },
pilotWingmanOrbit = {
"Wilco, holding here.",
"Copy, orbiting now.",
"Roger, in the hold.",
"Affirm, setting up the orbit.",
"Orbiting at your pos."
},
pilotWingmanRejoin = {
"Off the perch, rejoining your side.",
"Tally visual, coming to you.",
"Clear, rejoining to route.",
"Pushing up to formation.",
"Visual, sliding back into position."
},
atcSafeLanding = { "Be advised: $1 is wheels down at $2 and clear of runway.", "All aircraft, $1 has landed at $2 and vacated active. Runway is open for next inbound.", "Traffic, $1 is on deck at $2 and heading to parking. Runway clear.", "All flights, $1 just rolled out at $2 and cleared the active.", "Heads up, $1 landed at $2 and moving to the ramp. Runway available for next approach." },
atcSafeLandingPlayer = { "$1, wheels on deck, welcome back. You may taxi to the parking area.", "$1, good copy on landing. Exit when able, proceed to the parking area.", "$1, touchdown confirmed. Continue to parking.", "$1, welcome home. Clear of runway and taxi to parking area.", "$1, nice landing. Taxi to parking when ready." },
@ -186,6 +201,21 @@ Library.radioMessages = {
"Command, pass coordinates for objective $1.",
"Command, confirm grid on objective $1."
},
playerFlightOrbit = {
"Flight, orbit my position.",
"Flight, set up an orbit on me.",
"Flight, hold on me.",
"Flight, anchor on my current pos.",
"Flight, orbit overhead"
},
playerFlightRejoin =
{
"Flight, rejoin my side",
"Flight, push it up, rejoin formation.",
"Flight, come back to route.",
"Flight, tighten it up.",
"Flight, rejoin tactical."
},
playerJTACSmoke = {
"$1, request smoke on objective $2, over.",
"$1, mark objective $2 with smoke, how copy?",

View File

@ -107,11 +107,12 @@ do
function eventHandler:onEvent(event)
if not event then return end -- No event
TUM.ambientRadio.onEvent(event) -- Must be first so other (more important) radio messages with interrupt the "ambient" ones
TUM.ambientRadio.onEvent(event) -- Must be first so other (more important) radio messages will interrupt the "ambient" ones
TUM.ambientWorld.onEvent(event)
TUM.objectives.onEvent(event)
TUM.playerScore.onEvent(event)
TUM.mission.onEvent(event)
TUM.supportWingmen.onEvent(event)
end
function TUM.onEvent(event)

View File

@ -55,6 +55,13 @@ do
TUM.playerScore.awardCompletedObjective()
end
local function doSimulatePlayerTakeOff()
local playerUnit = coalition.getPlayers(TUM.settings.getPlayerCoalition())[1]
local takeOffEvent = { id = world.event.S_EVENT_TAKEOFF, initiator = playerUnit }
TUM.onEvent(takeOffEvent)
end
local function doSimulatePlayerLanding()
local playerUnit = coalition.getPlayers(TUM.settings.getPlayerCoalition())[1]
@ -72,6 +79,7 @@ do
missionCommands.addCommand("Detonate \"boom\" map markers", rootMenu, doMarkersBoom, nil)
missionCommands.addCommand("Detonate aircraft near \"airboom\" map markers", rootMenu, doMarkersAirBoom, nil)
missionCommands.addCommand("Award 100 points and 1 objective", rootMenu, doAwardPointsAndObjectives, nil)
missionCommands.addCommand("Simulate player takeoff", rootMenu, doSimulatePlayerTakeOff, nil)
missionCommands.addCommand("Simulate player landing", rootMenu, doSimulatePlayerLanding, nil)
missionCommands.addCommand("Reset player stats", rootMenu, TUM.playerCareer.reset, nil)
end

View File

@ -29,6 +29,7 @@ do
local function closeMission(removeAllUnits)
if removeAllUnits then
TUM.supportWingmen.removeAll()
TUM.airForce.removeAll()
TUM.ambientWorld.removeAll()
TUM.enemyAirDefense.removeAll()
@ -72,7 +73,6 @@ do
end
TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already
TUM.enemyAirDefense.create() -- Must be called once objectives have been created
TUM.airForce.create() -- Must be called once objectives have been created
TUM.missionMenu.create() -- Must be called once objectives have been created

View File

@ -46,6 +46,7 @@ do
end
end
TUM.supportWingmen.createMenu()
TUM.supportAWACS.createMenu()
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then -- If not multiplayer, add "show mission score" command

View File

@ -0,0 +1,129 @@
-- ====================================================================================
-- TUM.SUPPORTWINGMEN - HANDLES THE PLAYER'S WINGMEN
-- ====================================================================================
-- ====================================================================================
TUM.supportWingmen = {}
do
TUM.supportWingmen.orderID = {
ORBIT = 1,
REJOIN = 2,
}
local wingmenGroupID = nil
local function doWingmenOrder(orderID)
local player = world:getPlayer()
if not player then return end
if orderID == TUM.supportWingmen.orderID.ORBIT then
TUM.radio.playForAll("playerFlightOrbit", nil, player:getCallsign(), false)
elseif orderID == TUM.supportWingmen.orderID.REJOIN then
TUM.radio.playForAll("playerFlightRejoin", nil, player:getCallsign(), false)
end
if not wingmenGroupID then return end
local wingmenGroup = DCSEx.world.getGroupByID(wingmenGroupID)
if not wingmenGroup then return end
if #wingmenGroup:getUnits() == 0 then return end
local wingmenCtrl = wingmenGroup:getController()
if not wingmenCtrl then return end
local wingmanCallsign = wingmenGroup:getUnit(1):getCallsign()
local taskTable = nil
if orderID == TUM.supportWingmen.orderID.ORBIT then
taskTable = {
id = "Orbit",
params = {
pattern = "Circle",
point = DCSEx.math.vec3ToVec2(player:getPoint()),
altitude = player:getPoint().y
}
}
TUM.radio.playForAll("pilotWingmanOrbit", nil, wingmanCallsign, true)
elseif orderID == TUM.supportWingmen.orderID.REJOIN then
taskTable = {
id = "Follow",
params = {
groupId = DCSEx.dcs.getObjectIDAsNumber(world:getPlayer():getGroup()),
pos = { x = -100, y = 0, z = -100 },
lastWptIndexFlag = false,
lastWptIndex = -1
}
}
TUM.radio.playForAll("pilotWingmanRejoin", nil, wingmanCallsign, true)
end
if not taskTable then return end
wingmenCtrl:setTask(taskTable)
end
local function createWingmen()
TUM.supportWingmen.removeAll() -- Destroy all pre-existing wingmen
TUM.log("Creating wingmen...")
local player = world:getPlayer()
if not player then return end
local playerTypeName = player:getTypeName()
local groupInfo = DCSEx.unitGroupMaker.create(
TUM.settings.getPlayerCoalition(),
Group.Category.AIRPLANE, -- TODO: or helicopter!
DCSEx.math.randomPointInCircle(DCSEx.math.vec3ToVec2(player:getPoint()), 500, 250),
{ playerTypeName, playerTypeName },
{
callsign = DCSEx.unitCallsignMaker.getNextGroupCallSign(player:getCallsign()),
silenced = true,
taskFollow = DCSEx.dcs.getObjectIDAsNumber(player:getGroup()),
unlimitedFuel = true
}
)
if not groupInfo then
TUM.log("Failed to spawn AI wingmen", TUM.logLevel.WARNING)
return
end
TUM.log("Spawned AI wingmen")
wingmenGroupID = groupInfo.groupID
end
function TUM.supportWingmen.removeAll()
if wingmenGroupID then TUM.log("Removing all wingmen...") end
DCSEx.world.destroyGroupByID(wingmenGroupID)
wingmenGroupID = nil
end
function TUM.supportWingmen.createMenu()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No wingmen in multiplayer
local rootPath = missionCommands.addSubMenu("Flight")
missionCommands.addCommand("Orbit", rootPath, doWingmenOrder, TUM.supportWingmen.orderID.ORBIT)
missionCommands.addCommand("Rejoin", rootPath, doWingmenOrder, TUM.supportWingmen.orderID.REJOIN)
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.supportWingmen.onEvent(event)
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No wingmen in multiplayer
if TUM.mission.getStatus() == TUM.mission.status.NONE then return end
if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if not event.initiator:getPlayerName() then return end
if event.id == world.event.S_EVENT_TAKEOFF then -- Create wingmen on takeoff
createWingmen()
elseif event.id == world.event.S_EVENT_LAND then
TUM.supportWingmen.removeAll() -- Remove wingmen on landing
end
end
end