--[[
# 3. Missions
A mission is a completable objective with a state and a continuous check to see if itself is completed.
The delay between checks is quite big, but it also is checked on unit deaths and other events.
### Placement
The placement of MISSION trigger zones can be anywhere.
The order of unit detection is `CAP` > `MISSION` > `AIRBASE` > `STAGE`
This means that if a unit has a name that starts with `"CAP_"` it will not be included in a mission.
But all other units in a `MISSION` trigger zone will be managed as part of that mission.
Units inside a `MISSION` do not have to stay within the triggerzone.
They just need to be inside the zone at the start of the mission.
You can for example let a `BAI` mission drive back and forth between airbases. The `MISSIONZONE` only needs to be around the units that are part of the objective, not the waypoints.
### Naming
`MISSION__`
`RANDOMMISSION___` (Read RANDOMISATION below)
With:
`name` = A name that is easy to remember and type. Like a codename. Exmaples: BYRON, PLUKE, etc.
`type` = any of the below described types. Special types are marked with an *
TIP: You can click on the type to get more details
SAM*
SAM Sites are managed a little different. SAM Sites can be used to guide players and to protect airfields.
In the future when deepstrike missions might come into scope these SAM sites will also be more important.
SAM sites will be activated when a zone is "Pre-Active".
A stage is "Pre-Active" when there is a CAP base active, or there is other things to do that would need the SAM site to be live (OCA, DEEPSTRIKE, EXTRACTION \<= all feature development)
If you want a SAM site to become active ONLY when the stage is fully active, then `DEAD` is the type for you!
Completion logic
TODO: documentation: Completion logic
DEAD
DEAD missions will be spawned on activation of the stage.
ALL DEAD missions will be activated right at the start of a stage.
This might be against the "randomisation" feel, but it is to make sure mission don't get activated randomly and players get ambushed by a random spawn.
Completion logic
TODO: documentation: Completion logic
BAI
STRIKE
STRIKE missions will be activated randomly until all of them are completed.
A strike mission can be placed anywhere, even on airbases
Completion logic
TODO: documentation: Completion logic
### Randomisation
You can randomise missions.
Spearhead will pick up all mission zones that start with `"RANDOMMISSION_"`
Then it will combine each `RANDOMMISSION` in a zone with the same `` and pick a random one.
It will always pick 1 and only 1.
This means you have some options for randomisation.
For example, if you have 1 missionzone with name `RANDOMMISSION_SAM_PLUKE_1` that is filled with an SA-2 and another zone with `RANDOMMISSION_SAM_PLUKE_2` filled with an SA-3 then some runs of the mission it will spawn an SA-3 and sometimes spawn an SA-3. (works for any mission type)
What you can also do is add empty `RANDOMMISSION_` zones next to the filled `RANDOMMISSION_` zone.
For example. You have a `RANDOMMISSION_DEAD_BYRON_1` filled with an SA-19 driving around and 2 more `RANDOMMISSION_DEAD_BYRON_<2 & 3>` zones then it will have a 33% chance of being spawned.
If a zone is empty it will not be briefed, activated or count towards completion of the `STAGE`
]]
--- A mission Object.
local Mission = {}
do -- INIT Mission Class
local MINIMAL_UNITS_ALIVE_RATIO = 0.20
local Defaults = {}
Defaults.MainMenu = "Missions"
Defaults.SelectMenuSubMenus = { Defaults.MainMenu, "Select Mission" }
Defaults.ShowMissionSubs = { Defaults.MainMenu }
local PlayersInMission = {}
local MissionType = {
UNKNOWN = 0,
STRIKE = 1,
BAI = 2,
DEAD = 3,
SAM = 4,
}
do --INIT MISSION TYPE FUNCTIONS
---Parse string to mission type
---@param input string
MissionType.Parse = function(input)
if input == nil then
return Mission.MissionType.UNKNOWN
end
input = string.lower(input)
if input == "dead" then return MissionType.DEAD end
if input == "strike" then return MissionType.STRIKE end
if input == "bai" then return MissionType.BAI end
if input == "sam" then return MissionType.SAM end
return Mission.MissionType.UNKNOWN
end
---comment
---@param input number missionType
---@return string text
MissionType.toString = function(input)
if input == MissionType.DEAD then return "DEAD" end
if input == MissionType.STRIKE then return "STRIKE" end
if input == MissionType.BAI then return "BAI" end
if input == MissionType.SAM then return "SAM" end
return "?"
end
end
Mission.MissionType = MissionType
Mission.MissionState = {
NEW = 0,
ACTIVE = 1,
COMPLETED = 2,
}
---comment
---@param missionZoneName string missionZoneName
---@param database table db dependency injection
---@return table?
function Mission:new(missionZoneName, database, logger)
local o = {}
setmetatable(o, { __index = self })
local function ParseGroupName(input)
local split_name = Spearhead.Util.split_string(input, "_")
local split_length = Spearhead.Util.tableLength(split_name)
if Spearhead.Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
Spearhead.AddMissionEditorWarning("Random Mission with zonename " .. input .. " not in right format")
return nil
elseif split_length < 3 then
Spearhead.AddMissionEditorWarning("Mission with zonename" .. input .. " not in right format")
return nil
end
local type = split_name[2]
local parsedType = Mission.MissionType.Parse(type)
if parsedType == nil then
Spearhead.AddMissionEditorWarning("Mission with zonename '" .. input .. "' has an unsupported type '" .. (type or "nil" ))
return nil
end
local name = split_name[3]
return {
missionName = name,
type = parsedType
}
end
local parsed = ParseGroupName(missionZoneName)
if parsed == nil then return nil end
o.missionZoneName = missionZoneName
o.database = database
o.groupNames = database:getGroupsForMissionZone(missionZoneName)
o.name = parsed.missionName
o.missionType = parsed.type
o.startingGroups = Spearhead.Util.tableLength(o.groupNames)
o.missionState = Mission.MissionState.NEW
o.missionbriefing = database:GetDescriptionForMission(missionZoneName)
o.startingUnits = 0
o.logger = logger
o.code = database:GetNewMissionCode()
o.groupUnitAliveDict = {}
o.targetAliveStates = {}
o.hasSpecificTargets = false
local CheckStateAsync = function (self, time)
self:CheckAndUpdateSelf()
return nil
end
o.OnUnitLost = function(self, object)
--[[
OnUnit lost event
]]--
local category = object:getCategory()
if category == Object.Category.UNIT then
local unitName = object:getName()
local groupName = object:getGroup():getName()
self.groupUnitAliveDict[groupName][unitName] = false
if self.targetAliveStates[groupName][unitName] then
self.targetAliveStates[groupName][unitName] = false
end
elseif category == Object.Category.STATIC then
local name = object:getName()
self.groupUnitAliveDict[name][name] = false
if self.targetAliveStates[name][name] then
self.targetAliveStates[name][name] = false
end
end
CheckStateAsync(false)
end
o.MissionCompleteListeners = {}
---comment
---@param self table
---@param listener table Object that implements "OnMissionComplete(self, mission)"
o.AddMissionCompleteListener = function(self, listener)
if type(listener) ~= "table" then
return
end
table.insert(self.MissionCompleteListeners, listener)
end
local TriggerMissionComplete = function(self)
for _, callable in pairs(self.MissionCompleteListeners) do
local succ, err = pcall( function()
callable:OnMissionComplete(self)
end)
if err then
self.logger:warn("Error in misstion complete listener:" .. err)
end
end
end
local StartCheckingAndUpdateSelfContinuous = function (self)
local CheckAndUpdate = function(self, time)
self:CheckAndUpdateSelf(true)
if self.missionState == Mission.MissionState.COMPLETED or self.missionState == Mission.MissionState.NEW then
return nil
else
return time + 60
end
end
timer.scheduleFunction(CheckAndUpdate, self, timer.getTime() + 300)
end
local CleanupDelayedAsync = function (self, time)
self:Cleanup()
return nil
end
---comment
---@param self table
---@param checkUnitHealth boolean?
o.CheckAndUpdateSelf = function(self, checkUnitHealth)
if not checkUnitHealth then checkUnitHealth = false end
if self.missionState == Mission.MissionState.COMPLETED then
return
end
--[[
TODO: Check own state based on mission type
]]--
local specificTargetsAlive = false
if self.hasSpecificTargets == true then
for groupName, unitNameDict in pairs(self.targetAliveStates) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
specificTargetsAlive = true
end
end
end
else
local function CountAliveGroups()
local aliveGroups = 0
for _, group in pairs(self.groupUnitAliveDict) do
local groupTotal = 0
local groupDeath = 0
for _, isAlive in pairs(group) do
if isAlive ~= true then
groupDeath = groupDeath + 1
end
groupTotal = groupTotal + 1
end
local aliveRatio = (groupTotal - groupDeath) / groupTotal
if aliveRatio >= MINIMAL_UNITS_ALIVE_RATIO then
aliveGroups = 1
end
end
end
if self.missionType == Mission.MissionType.STRIKE then --strike targets should normally have TGT targets
if CountAliveGroups() == 0 then
self.missionState = Mission.MissionState.COMPLETED
end
elseif self.missionType == Mission.MissionType.BAI then
if CountAliveGroups() == 0 then
self.missionState = Mission.MissionState.COMPLETED
end
end
--[[
TODO: Other checks for mission complete
]]
end
if self.missionState == Mission.MissionState.COMPLETED then
TriggerMissionComplete(self)
--Schedule cleanup after 5 minutes of mission complete
timer.scheduleFunction(CleanupDelayedAsync, self, timer.getTime() + 300)
end
end
---Activates groups for this mission
---@param self table
o.Activate = function(self)
if self.missionState == Mission.MissionState.ACTIVE then
return
end
self.missionState = Mission.MissionState.ACTIVE
do --spawn groups
for key, groupname in pairs(self.groupNames) do
Spearhead.DcsUtil.SpawnGroupTemplate(groupname)
end
end
StartCheckingAndUpdateSelfContinuous(self)
end
o.ShowBriefing = function(self, unitId)
local text = "Mission #" .. self.code .. "\n" .. self.missionbriefing .. " \n \nState TODO"
trigger.action.outTextForUnit(unitId, text, 30);
end
o.Cleanup = function(self)
for key, groupName in pairs(self.groupNames) do
Spearhead.DcsUtil.DestroyGroup(groupName)
end
end
local Init = function(self)
for key, group_name in pairs(self.groupNames) do
self.groupUnitAliveDict[group_name] = {}
self.targetAliveStates[group_name] = {}
if Spearhead.DcsUtil.IsGroupStatic(group_name) then
self.startingUnits = self.startingUnits + 1
Spearhead.Events.addOnUnitLostEventListener(group_name, self)
self.groupUnitAliveDict[group_name][group_name] = true
if Spearhead.Util.startswith(group_name, "TGT_") == true then
self.targetAliveStates[group_name][group_name] = true
end
else
local group = Group.getByName(group_name)
local isGroupTarget = Spearhead.Util.startswith(group_name, "TGT_")
self.startingUnits = self.startingUnits + group:getInitialSize()
for _, unit in pairs(group:getUnits()) do
local unitName = unit:getName()
Spearhead.Events.addOnUnitLostEventListener(unitName, self)
self.groupUnitAliveDict[group_name][unitName] = true
if isGroupTarget == true or Spearhead.Util.startswith(unitName, "TGT_") == true then
self.targetAliveStates[group_name][unitName] = true
end
end
end
end
if Spearhead.Util.tableLength(self.targetAliveStates) > 0 then
self.hasSpecificTargets = true
end
end
Init(o)
return o;
end
end
Spearhead.internal.Mission = Mission