Dcs lua tool (#66)
* custom drawings and animations for video * wip * Work in progress refactor for toolkit * wip * Refactored everything for Lua Compile Tool * Trying out the github action * updated the github action ref * initial working version --------- Co-authored-by: ex61wi <tim.rorije@ing.com> Co-authored-by: dutchie031 <dutchie031>
This commit is contained in:
committed by
GitHub
co-authored by
ex61wi
dutchie031 <dutchie031>
parent
c027921527
commit
128aed1d2b
@@ -0,0 +1,295 @@
|
||||
local Events = require("classes.spearhead_events")
|
||||
local Util = require("classes.util.Util")
|
||||
local Logger = require("classes.util.Logger")
|
||||
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
|
||||
|
||||
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
|
||||
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
|
||||
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
|
||||
|
||||
local StagesByName = {}
|
||||
|
||||
---@type table<string, Array<Stage>>
|
||||
local StagesByIndex = {}
|
||||
|
||||
---@type table<string, Array<Stage>>
|
||||
local SideStageByIndex = {}
|
||||
|
||||
---@type table<string, Array<WaitingStage>>
|
||||
local WaitingStagesByIndex = {}
|
||||
|
||||
local currentStage = -99
|
||||
|
||||
---@class GlobalStageManager : StageCompleteListener
|
||||
---@field private database Database
|
||||
---@field private logger Logger
|
||||
---@field private stageConfig StageConfig
|
||||
local GlobalStageManager = {}
|
||||
GlobalStageManager.__index = GlobalStageManager
|
||||
|
||||
GlobalStageManager.getCurrentStage = function() return currentStage end
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param stageConfig StageConfig
|
||||
---@param logLevel LogLevel
|
||||
---@param spawnManager SpawnManager
|
||||
---@return nil
|
||||
function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnManager)
|
||||
local logger = Logger.new("StageManager", logLevel)
|
||||
logger:info("Using Stage Log Level: " .. logLevel)
|
||||
local self = setmetatable({}, GlobalStageManager)
|
||||
self.database = database
|
||||
self.stageConfig = stageConfig
|
||||
|
||||
self.logger = logger
|
||||
if stageConfig.isAutoStages ~= true then
|
||||
logger:warn("Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
|
||||
end
|
||||
|
||||
---@type OnStageChangedListener
|
||||
local OnStageNumberChangedListener = {
|
||||
OnStageNumberChanged = function (self, number)
|
||||
currentStage = number
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
Events.AddStageNumberChangedListener(OnStageNumberChangedListener)
|
||||
|
||||
for _, stageName in pairs(database:getStagezoneNames()) do
|
||||
logger:debug("Found stage zone with name: " .. stageName)
|
||||
|
||||
if Util.startswith(stageName, "missionstage", true) then
|
||||
local valid = true
|
||||
local split = Util.split_string(stageName, "_")
|
||||
if Util.tableLength(split) < 2 then
|
||||
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
|
||||
valid = false
|
||||
end
|
||||
|
||||
if Util.tableLength(split) < 3 then
|
||||
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a stage name")
|
||||
end
|
||||
|
||||
local orderNumber = nil
|
||||
local isSideStage = false
|
||||
if valid == true then
|
||||
local orderNumberString = string.lower(split[2])
|
||||
if Util.startswith(orderNumberString, "x") == true then
|
||||
isSideStage = true
|
||||
|
||||
orderNumberString = string.gsub(orderNumberString, "x", "")
|
||||
orderNumber = tonumber(orderNumberString)
|
||||
else
|
||||
orderNumber = tonumber(split[2])
|
||||
end
|
||||
|
||||
if orderNumber == nil then
|
||||
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number : " .. split[2])
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
|
||||
local stageDisplayName = split[3]
|
||||
local stagelogger = Logger.new(stageName, logLevel)
|
||||
if valid == true and orderNumber then
|
||||
|
||||
---@type StageInitData
|
||||
local initData = {
|
||||
stageDisplayName = stageDisplayName,
|
||||
stageNumber = orderNumber,
|
||||
stageZoneName = stageName,
|
||||
}
|
||||
|
||||
if isSideStage == true then
|
||||
local stage = ExtraStage.New(database, stageConfig, stagelogger, initData, spawnManager)
|
||||
stage:AddStageCompleteListener(self)
|
||||
|
||||
if SideStageByIndex[tostring(orderNumber)] == nil then SideStageByIndex[tostring(orderNumber)] = {} end
|
||||
table.insert(SideStageByIndex[tostring(orderNumber)], stage)
|
||||
else
|
||||
local stage = PrimaryStage.New(database, stageConfig, stagelogger, initData, spawnManager)
|
||||
stage:AddStageCompleteListener(self)
|
||||
|
||||
if StagesByIndex[tostring(orderNumber)] == nil then StagesByIndex[tostring(orderNumber)] = {} end
|
||||
table.insert(StagesByIndex[tostring(orderNumber)], stage)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Util.startswith(stageName, "waitingstage", true) then
|
||||
local valid = true
|
||||
|
||||
local split = Util.split_string(stageName, "_")
|
||||
|
||||
if Util.tableLength(split) < 3 then
|
||||
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
|
||||
valid = false
|
||||
end
|
||||
|
||||
if valid == true then
|
||||
local stageIndexString = split[2]
|
||||
local stageIndex = tonumber(stageIndexString)
|
||||
|
||||
if not stageIndex then
|
||||
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number")
|
||||
valid = false
|
||||
end
|
||||
|
||||
local waitingSecondsString = split[3]
|
||||
local waitingSeconds = tonumber(waitingSecondsString)
|
||||
if not waitingSeconds then
|
||||
MissionEditorWarnings.Add("Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter")
|
||||
valid = false
|
||||
end
|
||||
|
||||
if valid == true then
|
||||
local stagelogger = Logger.new(stageName, logLevel)
|
||||
|
||||
---@type WaitingStageInitData
|
||||
local initData = {
|
||||
stageDisplayName = "Waiting Stage " .. stageIndex,
|
||||
stageNumber = stageIndex or -99,
|
||||
stageZoneName = stageName,
|
||||
waitingSeconds = waitingSeconds --[[@as integer]]
|
||||
}
|
||||
local waitingStage = WaitingStage.New(database, stageConfig, stagelogger, initData, spawnManager)
|
||||
|
||||
if WaitingStagesByIndex[tostring(stageIndex)] == nil then
|
||||
WaitingStagesByIndex[tostring(stageIndex)] = {}
|
||||
end
|
||||
table.insert(WaitingStagesByIndex[tostring(stageIndex)], waitingStage)
|
||||
|
||||
waitingStage:AddStageCompleteListener(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function GlobalStageManager:OnStageComplete(stage)
|
||||
self.logger:debug("Receiving stage complete event from: " .. stage.zoneName)
|
||||
|
||||
local anyIncomplete = false
|
||||
self.logger:debug("Checking stages for index: " .. tostring(currentStage))
|
||||
for index, stage in pairs(StagesByIndex[tostring(currentStage)]) do
|
||||
if stage:IsComplete() == false then
|
||||
anyIncomplete = true
|
||||
self.logger:debug("Need to wait for Stage " .. stage.zoneName .. " to be completed")
|
||||
else
|
||||
self.logger:debug("Stage verified to be completed: " .. stage.zoneName)
|
||||
end
|
||||
end
|
||||
|
||||
if anyIncomplete == false and self.stageConfig.isAutoStages == true then
|
||||
|
||||
-- CHECK WAITING STAGES
|
||||
local nextStage = currentStage + 1
|
||||
|
||||
if WaitingStagesByIndex[tostring(nextStage)] then
|
||||
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
|
||||
if waitingStage:IsActive() == false then
|
||||
waitingStage:ActivateStage()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local anyWaiting = false
|
||||
if WaitingStagesByIndex[tostring(nextStage)] then
|
||||
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
|
||||
if waitingStage:IsComplete() == false then
|
||||
anyWaiting = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if anyWaiting == false then
|
||||
local newStageNumber = currentStage + 1
|
||||
self:UpdateDrawings(newStageNumber)
|
||||
self.logger:debug("Setting next stage to: " .. tostring(newStageNumber))
|
||||
Events.PublishStageNumberChanged(newStageNumber)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function GlobalStageManager:UpdateDrawings(stageNumber)
|
||||
local drawings = self.database:getCustomDrawings()
|
||||
for _, drawing in pairs(drawings) do
|
||||
local startStage, stopStage = drawing:GetStartAndStop()
|
||||
if stageNumber >= startStage and stageNumber < stopStage then
|
||||
drawing:Draw()
|
||||
else
|
||||
drawing:Remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
GlobalStageManager.printFullOverview = function ()
|
||||
|
||||
local logger = Logger.new("StageOverview", "INFO")
|
||||
logger:info("Stage overview:")
|
||||
|
||||
local max = 0
|
||||
local lines = {}
|
||||
for stageIndex, stages in pairs(StagesByIndex) do
|
||||
|
||||
local totalStrike = 0
|
||||
local totalbai = 0
|
||||
local totaldead = 0
|
||||
local totalMissions = 0
|
||||
local totalCas = 0
|
||||
|
||||
for _, stage in pairs(stages) do
|
||||
|
||||
local strike, dead, bai, cas = stage:GetStageStats()
|
||||
|
||||
totalStrike = totalStrike + strike
|
||||
totalbai = totalbai + bai
|
||||
totaldead = totaldead + dead
|
||||
totalCas = totalCas + cas
|
||||
totalMissions = totalMissions + strike + dead + bai + cas
|
||||
end
|
||||
|
||||
local index = tonumber(stageIndex)
|
||||
if index then
|
||||
if index > max then
|
||||
max = index
|
||||
end
|
||||
lines[index] ="Stage# " .. tostring(stageIndex).. " | " .. totalStrike .. " strikes | " .. totaldead .. " dead | " .. totalbai .. " BAI | " .. totalCas .. " CAS | Total:" .. totalMissions
|
||||
else
|
||||
logger:warn("Stage index is not a number: " .. stageIndex)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, max do
|
||||
if lines[i] then
|
||||
logger:info(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param stageNumber number
|
||||
---@return boolean | nil
|
||||
GlobalStageManager.isStageComplete = function (stageNumber)
|
||||
|
||||
local stageIndex = tostring(stageNumber)
|
||||
|
||||
if StagesByIndex[stageIndex] == nil then return nil end
|
||||
|
||||
for _, stage in ipairs(StagesByIndex[stageIndex]) do
|
||||
if stage:IsComplete() == false then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return GlobalStageManager
|
||||
@@ -0,0 +1,190 @@
|
||||
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
|
||||
---@class SpearheadGroup : OnUnitLostListener
|
||||
---@field private _groupName string
|
||||
---@field private _isStatic boolean
|
||||
---@field private _isSpawned boolean
|
||||
---@field private _spawnManager SpawnManager
|
||||
---@field private _isPersistent boolean
|
||||
local SpearheadGroup = {}
|
||||
SpearheadGroup.__index = SpearheadGroup
|
||||
|
||||
---comment
|
||||
---@param groupName string
|
||||
---@param spawnManager SpawnManager
|
||||
---@param isPersistent boolean?
|
||||
---@return SpearheadGroup
|
||||
function SpearheadGroup.New(groupName, spawnManager, isPersistent)
|
||||
local self = setmetatable({}, SpearheadGroup)
|
||||
|
||||
if isPersistent == nil then isPersistent = false end
|
||||
|
||||
self._spawnManager = spawnManager
|
||||
self._isStatic = spawnManager:IsGroupStatic(groupName) == true
|
||||
self._groupName = groupName
|
||||
self._isSpawned = false
|
||||
self._isPersistent = isPersistent
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
function SpearheadGroup:GetName()
|
||||
return self._groupName
|
||||
end
|
||||
|
||||
function SpearheadGroup:IsSpawned()
|
||||
return self._isSpawned
|
||||
end
|
||||
|
||||
function SpearheadGroup:SpawnCorpsesOnly()
|
||||
|
||||
if self._isSpawned == true then return end
|
||||
|
||||
self._spawnManager:SpawnCorpsesOnly(self._groupName)
|
||||
self._isSpawned = true
|
||||
|
||||
end
|
||||
|
||||
---@param lateStart boolean?
|
||||
function SpearheadGroup:Spawn(lateStart)
|
||||
|
||||
if self._isSpawned == true then return end
|
||||
|
||||
---@type SpawnOverrides
|
||||
local overrides = {
|
||||
uncontrolled = lateStart,
|
||||
}
|
||||
|
||||
local spawnedObject, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
|
||||
self._isStatic = isStatic
|
||||
self._isSpawned = true
|
||||
end
|
||||
|
||||
function SpearheadGroup:Destroy()
|
||||
self._isSpawned = false
|
||||
self._spawnManager:DestroyGroup(self._groupName)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function SpearheadGroup:IsStatic()
|
||||
return self._isStatic
|
||||
end
|
||||
|
||||
|
||||
---@return integer
|
||||
function SpearheadGroup:GetCoalition()
|
||||
if self._isStatic == true then
|
||||
local object = StaticObject.getByName(self._groupName)
|
||||
if object == nil then
|
||||
return 0
|
||||
end
|
||||
return object:getCoalition()
|
||||
else
|
||||
local group = Group.getByName(self._groupName)
|
||||
if group == nil then
|
||||
return 0
|
||||
end
|
||||
return group:getCoalition()
|
||||
end
|
||||
end
|
||||
|
||||
---comment
|
||||
---@return table result list of objects
|
||||
function SpearheadGroup:GetObjects()
|
||||
|
||||
local result = {}
|
||||
if self._isStatic == true then
|
||||
local staticObject = StaticObject.getByName(self._groupName)
|
||||
if staticObject then
|
||||
table.insert(result, staticObject)
|
||||
end
|
||||
else
|
||||
local group = Group.getByName(self._groupName)
|
||||
if not group then return {} end
|
||||
for _, unit in pairs(group:getUnits()) do
|
||||
table.insert(result, unit)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---comment
|
||||
---@return Array<Unit> result list of objects
|
||||
function SpearheadGroup:GetAsUnits()
|
||||
|
||||
if self._isStatic == true then
|
||||
return {}
|
||||
end
|
||||
|
||||
local result = {}
|
||||
local group = Group.getByName(self._groupName)
|
||||
if not group then return {} end
|
||||
for _, unit in pairs(group:getUnits()) do
|
||||
table.insert(result, unit)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---@return Array<Vec3>
|
||||
function SpearheadGroup:GetAllUnitPositions()
|
||||
|
||||
local result = {}
|
||||
if self._isStatic == true then
|
||||
local staticObject = StaticObject.getByName(self._groupName)
|
||||
if staticObject then
|
||||
table.insert(result, staticObject:getPoint())
|
||||
end
|
||||
else
|
||||
local group = Group.getByName(self._groupName)
|
||||
if not group then return {} end
|
||||
for _, unit in pairs(group:getUnits()) do
|
||||
table.insert(result, unit:getPoint())
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function SpearheadGroup:SetInvisible()
|
||||
|
||||
if self._isStatic == true then
|
||||
local country = DcsUtil.GetNeutralCountry()
|
||||
|
||||
---@type SpawnOverrides
|
||||
local overrides = {
|
||||
countryID = country
|
||||
}
|
||||
|
||||
self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
|
||||
else
|
||||
local group = Group.getByName(self._groupName)
|
||||
if group then
|
||||
local setInvisible = {
|
||||
id = 'SetInvisible',
|
||||
params = {
|
||||
value = true
|
||||
}
|
||||
}
|
||||
group:getController():setCommand(setInvisible)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function SpearheadGroup:SetVisible()
|
||||
|
||||
local group = Group.getByName(self._groupName)
|
||||
if group then
|
||||
local setInvisible = {
|
||||
id = 'SetInvisible',
|
||||
params = {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
group:getController():setCommand(setInvisible)
|
||||
end
|
||||
end
|
||||
|
||||
return SpearheadGroup
|
||||
@@ -0,0 +1,76 @@
|
||||
local Persistence = require("classes.persistence.Persistence")
|
||||
|
||||
---@class SpearheadSceneryObject
|
||||
---@field private persistentName string The persistent name of the scenery object
|
||||
---@field private objectID number The ID of the scenery object
|
||||
---@field private internalObj table
|
||||
---@field private isDead boolean Indicates if the scenery object is dead
|
||||
local SpearheadSceneryObject = {}
|
||||
SpearheadSceneryObject.__index = SpearheadSceneryObject
|
||||
|
||||
---comment
|
||||
---@param objectID number
|
||||
---@return SpearheadSceneryObject?
|
||||
function SpearheadSceneryObject.New(objectID)
|
||||
|
||||
local self = setmetatable({}, SpearheadSceneryObject)
|
||||
|
||||
if objectID == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
self.persistentName = "SpearheadSceneryObject_" .. objectID
|
||||
self.objectID = objectID
|
||||
self.isDead = false
|
||||
self.internalObj = {
|
||||
["id_"] = objectID
|
||||
}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function SpearheadSceneryObject:IsAlive()
|
||||
|
||||
if Object.isExist(self.internalObj) == false then
|
||||
self:MarkDead()
|
||||
return false
|
||||
end
|
||||
|
||||
if SceneryObject.getLife(self.internalObj) <= 0.10 then
|
||||
self:MarkDead()
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@private
|
||||
function SpearheadSceneryObject:MarkDead()
|
||||
if self.isDead == true then return end
|
||||
self.isDead = true
|
||||
Persistence.UnitKilled(self.persistentName, self:GetPoint(), 0, "Scenery")
|
||||
end
|
||||
|
||||
function SpearheadSceneryObject:UpdateStatePersistently()
|
||||
if self.isDead == true then
|
||||
return
|
||||
end
|
||||
|
||||
local state = Persistence.UnitState(self.persistentName)
|
||||
if state and state.isDead == true then
|
||||
trigger.action.explosion(self:GetPoint(), 1000)
|
||||
self.isDead = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function SpearheadSceneryObject:GetPersistentName()
|
||||
return self.persistentName
|
||||
end
|
||||
|
||||
function SpearheadSceneryObject:GetPoint()
|
||||
return Object.getPoint(self.internalObj)
|
||||
end
|
||||
|
||||
return SpearheadSceneryObject
|
||||
@@ -0,0 +1,155 @@
|
||||
local BuildableZone = require("classes.stageClasses.SpecialZones.abstract.BuildableZone")
|
||||
local SpearheadGroup = require("classes.stageClasses.Groups.SpearheadGroup")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
|
||||
---@class BlueSam : BuildableZone
|
||||
---@field Activate fun(self: BlueSam)
|
||||
---@field private _database Database
|
||||
---@field private _logger Logger
|
||||
---@field private _zoneName string
|
||||
---@field private _blueGroups Array<SpearheadGroup>
|
||||
---@field private _cleanupUnits table<string, boolean>
|
||||
---@field private _buildableCrateKilos number?
|
||||
---@field private _receivedKilos number?
|
||||
---@field private _unitsPerCrate number?
|
||||
---@field private _buildableMission BuildableMission?
|
||||
local BlueSam = {}
|
||||
BlueSam.__index = BlueSam
|
||||
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@param zoneName string
|
||||
---@param spawnManager SpawnManager
|
||||
---@return BlueSam?
|
||||
function BlueSam.New(database, logger, zoneName, spawnManager)
|
||||
|
||||
setmetatable(BlueSam, BuildableZone)
|
||||
local self = setmetatable({}, BlueSam)
|
||||
|
||||
self._database = database
|
||||
self._logger = logger
|
||||
self._zoneName = zoneName
|
||||
|
||||
self._blueGroups = {}
|
||||
self._cleanupUnits = {}
|
||||
|
||||
local blueSamData = database:getBlueSamDataForZone(zoneName)
|
||||
|
||||
if blueSamData == nil then
|
||||
logger:error("Blue SAM data not found for zone: " .. zoneName)
|
||||
return nil
|
||||
end
|
||||
|
||||
self._buildableCrateKilos = blueSamData.buildingKilos
|
||||
self._receivedKilos = 0
|
||||
|
||||
---@type table<string, Vec3>
|
||||
local blueUnitsPos = {}
|
||||
|
||||
---@type table<string, Vec3>
|
||||
local redUnitsPos = {}
|
||||
|
||||
|
||||
local buildable = false
|
||||
if self._buildableCrateKilos and self._buildableCrateKilos > 0 then
|
||||
buildable = true
|
||||
end
|
||||
|
||||
for _, groupName in pairs(blueSamData.groups) do
|
||||
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
if spearheadGroup then
|
||||
|
||||
if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then
|
||||
table.insert(self._blueGroups, spearheadGroup)
|
||||
end
|
||||
|
||||
for _, unit in pairs(spearheadGroup:GetObjects()) do
|
||||
if spearheadGroup:GetCoalition() == 1 then
|
||||
table.insert(blueUnitsPos, unit:getPoint())
|
||||
elseif spearheadGroup:GetCoalition() == 2 then
|
||||
table.insert(redUnitsPos, unit:getPoint())
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
spearheadGroup:Destroy()
|
||||
end
|
||||
|
||||
--Cleanup units
|
||||
local cleanup_distance = 5
|
||||
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
|
||||
for redUnitName, redUnitPos in pairs(redUnitsPos) do
|
||||
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
|
||||
if distance <= cleanup_distance then
|
||||
self._cleanupUnits[redUnitName] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local zone = DcsUtil.getZoneByName(zoneName)
|
||||
if zone then
|
||||
BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return SpearheadTriggerZone?
|
||||
function BlueSam:GetNoLandingZone()
|
||||
|
||||
---@type Array<Vec2>
|
||||
local points = {}
|
||||
|
||||
for _, group in pairs(self._blueGroups) do
|
||||
for _, unitPos in pairs(group:GetAllUnitPositions()) do
|
||||
table.insert(points, { x = unitPos.x, y = unitPos.z })
|
||||
end
|
||||
end
|
||||
|
||||
local vecs = Util.getConvexHull(points)
|
||||
|
||||
local zone = DcsUtil.getZoneByName(self._zoneName)
|
||||
if zone == nil then
|
||||
self._logger:error("Zone not found: " .. self._zoneName)
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type SpearheadTriggerZone
|
||||
local spearheadZone = {
|
||||
name = self._zoneName .. "_noland",
|
||||
location = zone.location,
|
||||
verts = vecs,
|
||||
radius = 0,
|
||||
zone_type = "Polygon"
|
||||
}
|
||||
|
||||
return spearheadZone
|
||||
end
|
||||
|
||||
function BlueSam:Activate()
|
||||
|
||||
if self._buildableMission == nil then
|
||||
self:SpawnGroups()
|
||||
else
|
||||
self:StartBuildable()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function BlueSam:SpawnGroups()
|
||||
for unitName, needsCleanup in pairs(self._cleanupUnits) do
|
||||
DcsUtil.DestroyUnit(unitName)
|
||||
end
|
||||
|
||||
for _, group in pairs(self._blueGroups) do
|
||||
group:Spawn()
|
||||
end
|
||||
end
|
||||
|
||||
function BlueSam:OnBuildingComplete()
|
||||
self:SpawnGroups()
|
||||
end
|
||||
|
||||
return BlueSam
|
||||
@@ -0,0 +1,139 @@
|
||||
local BuildableZone = require("classes.stageClasses.SpecialZones.abstract.BuildableZone")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SupplyHub = require("classes.stageClasses.SpecialZones.SupplyHub")
|
||||
local SpearheadGroup = require("classes.stageClasses.Groups.SpearheadGroup")
|
||||
|
||||
---@class FarpZone: BuildableZone
|
||||
---@field private _startingFarp boolean
|
||||
---@field private _groups Array<SpearheadGroup>
|
||||
---@field private _padNames Array<string>
|
||||
---@field private _database Database
|
||||
---@field private _logger Logger
|
||||
---@field private _zoneName string
|
||||
---@field private _supplyHubs Array<SupplyHub>
|
||||
local FarpZone = {}
|
||||
FarpZone.__index = FarpZone
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@param zoneName string
|
||||
---@param spawnManager SpawnManager
|
||||
---@return FarpZone
|
||||
function FarpZone.New(database, logger, zoneName, spawnManager)
|
||||
setmetatable(FarpZone, BuildableZone)
|
||||
local self = setmetatable({}, FarpZone)
|
||||
|
||||
self._database = database
|
||||
self._logger = logger
|
||||
self._zoneName = zoneName
|
||||
|
||||
local split = Util.split_string(zoneName, "_")
|
||||
if string.lower(split[2]) == "a" then
|
||||
self._startingFarp = true
|
||||
else
|
||||
self._startingFarp = false
|
||||
end
|
||||
|
||||
logger:debug("FARP zone name: " .. zoneName .. " startingFarp" .. tostring(self._startingFarp))
|
||||
|
||||
local farpData = database:getFarpDataForZone(zoneName)
|
||||
self._groups = {}
|
||||
self._padNames = {}
|
||||
self._supplyHubs = {}
|
||||
|
||||
|
||||
if farpData then
|
||||
self._padNames = farpData.padNames
|
||||
|
||||
for _, supplyHubName in pairs(farpData.supplyHubNames) do
|
||||
local supplyHub = SupplyHub.new(database, logger, supplyHubName)
|
||||
if supplyHub then
|
||||
table.insert(self._supplyHubs, supplyHub)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
for _, groupName in pairs(farpData.groups) do
|
||||
local group = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
table.insert(self._groups, group)
|
||||
group:Destroy()
|
||||
end
|
||||
|
||||
local zone = DcsUtil.getZoneByName(zoneName)
|
||||
if zone then
|
||||
self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos")
|
||||
BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database)
|
||||
end
|
||||
end
|
||||
self:Deactivate()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
---@return boolean
|
||||
function FarpZone:IsStartingFarp()
|
||||
return self._startingFarp
|
||||
end
|
||||
|
||||
function FarpZone:Activate()
|
||||
self._logger:info("Activating FARP zone: " .. self._zoneName)
|
||||
|
||||
if self._buildableMission == nil then
|
||||
self:BuildUp()
|
||||
self:SetPadsBlue()
|
||||
self:ActivateSupplyHubs()
|
||||
else
|
||||
self:StartBuildable()
|
||||
end
|
||||
end
|
||||
|
||||
function FarpZone:Deactivate()
|
||||
self:NeutralisePads()
|
||||
end
|
||||
|
||||
function FarpZone:OnBuildingComplete()
|
||||
self:BuildUp()
|
||||
self:SetPadsBlue()
|
||||
self:ActivateSupplyHubs()
|
||||
end
|
||||
|
||||
---@private
|
||||
function FarpZone:BuildUp()
|
||||
for _, group in pairs(self._groups) do
|
||||
group:Spawn()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function FarpZone:ActivateSupplyHubs()
|
||||
for _, supplyHub in pairs(self._supplyHubs) do
|
||||
supplyHub:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function FarpZone:NeutralisePads()
|
||||
for _, name in pairs(self._padNames) do
|
||||
local base = Airbase.getByName(name)
|
||||
if base then
|
||||
base:autoCapture(false) -- Disable auto capture
|
||||
base:setCoalition(1) -- 1 = Red (Can't neutralise)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function FarpZone:SetPadsBlue()
|
||||
for _, name in pairs(self._padNames) do
|
||||
local base = Airbase.getByName(name)
|
||||
if base then
|
||||
base:autoCapture(false) -- Disable auto capture
|
||||
base:setCoalition(2) -- 2 = Blue
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return FarpZone
|
||||
@@ -0,0 +1,196 @@
|
||||
local BuildableZone = require("classes.stageClasses.SpecialZones.abstract.BuildableZone")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SpearheadGroup = require("classes.stageClasses.Groups.SpearheadGroup")
|
||||
local SupplyHub = require("classes.stageClasses.SpecialZones.SupplyHub")
|
||||
local Util = require("classes.util.Util")
|
||||
|
||||
---@class StageBase : BuildableZone
|
||||
---@field private _database Database
|
||||
---@field private _logger Logger
|
||||
---@field private _red_groups Array<SpearheadGroup>
|
||||
---@field private _blue_groups Array<SpearheadGroup>
|
||||
---@field private _cleanup_units table<string, boolean>
|
||||
---@field private _airbase Airbase?
|
||||
---@field private _initialSide number?
|
||||
---@field private _supplyHubs Array<SupplyHub>
|
||||
---@field private _groupsPerKilo number
|
||||
---@field private _requiredBuildingKilos number
|
||||
---@field private _receivedBuildingKilos number
|
||||
---@field private _buildableMission BuildableMission?
|
||||
local StageBase = {}
|
||||
StageBase.__index = StageBase
|
||||
|
||||
---comment
|
||||
---@param databaseManager Database
|
||||
---@param logger table
|
||||
---@param airbaseName string
|
||||
---@param spawnManager SpawnManager
|
||||
---@return StageBase?
|
||||
function StageBase.New(databaseManager, logger, airbaseName, spawnManager)
|
||||
setmetatable(StageBase, BuildableZone)
|
||||
local self = setmetatable({}, StageBase)
|
||||
|
||||
self._database = databaseManager
|
||||
self._logger = logger
|
||||
|
||||
self._red_groups = {}
|
||||
self._blue_groups = {}
|
||||
self._cleanup_units = {}
|
||||
self._supplyHubs = {}
|
||||
|
||||
self._airbase = Airbase.getByName(airbaseName)
|
||||
self._initialSide = DcsUtil.getStartingCoalition(self._airbase)
|
||||
|
||||
do --init
|
||||
local airbaseData = databaseManager:getAirbaseDataForZone(airbaseName)
|
||||
if airbaseData == nil then
|
||||
logger:error("Airbase data not found for airbase: " .. airbaseName)
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type table<string, Vec3>
|
||||
local redUnitsPos = {}
|
||||
|
||||
---@type table<string, Vec3>
|
||||
local blueUnitsPos = {}
|
||||
|
||||
for _, groupName in pairs(airbaseData.RedGroups) do
|
||||
local shGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
table.insert(self._red_groups, shGroup)
|
||||
|
||||
for _, unit in pairs(shGroup:GetObjects()) do
|
||||
redUnitsPos[unit:getName()] = unit:getPoint()
|
||||
end
|
||||
|
||||
shGroup:Destroy()
|
||||
end
|
||||
|
||||
for _, groupName in pairs(airbaseData.BlueGroups) do
|
||||
local shGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
table.insert(self._blue_groups, shGroup)
|
||||
|
||||
for _, unit in pairs(shGroup:GetObjects()) do
|
||||
blueUnitsPos[unit:getName()] = unit:getPoint()
|
||||
end
|
||||
|
||||
shGroup:Destroy()
|
||||
end
|
||||
|
||||
for _, supplyHubName in pairs(airbaseData.supplyHubNames) do
|
||||
local supplyHub = SupplyHub.new(databaseManager, logger,
|
||||
supplyHubName)
|
||||
if supplyHub then
|
||||
table.insert(self._supplyHubs, supplyHub)
|
||||
end
|
||||
end
|
||||
|
||||
do -- check cleanup requirements
|
||||
-- Checks is any of the units are withing range (5m) of another unit.
|
||||
-- If so, make sure to add them to the cleanup list.
|
||||
|
||||
local cleanup_distance = 5
|
||||
|
||||
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
|
||||
for redUnitName, redUnitPos in pairs(redUnitsPos) do
|
||||
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
|
||||
if distance <= cleanup_distance then
|
||||
self._cleanup_units[redUnitName] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local zone = DcsUtil.getAirbaseZoneByName(airbaseName)
|
||||
if zone then
|
||||
BuildableZone.New(self, zone, airbaseData.buildingKilos or 0, "AIRBASE_CRATE", self._blue_groups, logger, databaseManager)
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@private
|
||||
function StageBase:SpawnRedUnits()
|
||||
---comment
|
||||
---@param groups Array<SpearheadGroup>
|
||||
local spawnAsync = function(groups)
|
||||
for _, group in pairs(groups) do
|
||||
group:Spawn()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
timer.scheduleFunction(spawnAsync, self._red_groups, timer.getTime() + 3)
|
||||
end
|
||||
|
||||
---@private
|
||||
function StageBase:CleanRedUnits()
|
||||
for _, value in pairs(self._red_groups) do
|
||||
value:SpawnCorpsesOnly()
|
||||
end
|
||||
|
||||
for unitName, shouldClean in pairs(self._cleanup_units) do
|
||||
if shouldClean == true then
|
||||
DcsUtil.DestroyUnit(unitName)
|
||||
DcsUtil.CleanCorpse(unitName)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function StageBase:SpawnBlueUnits()
|
||||
---comment
|
||||
---@param groups Array<SpearheadGroup>
|
||||
local spawnAsync = function(groups)
|
||||
for _, group in pairs(groups) do
|
||||
group:Spawn()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
timer.scheduleFunction(spawnAsync, self._blue_groups, timer.getTime() + 3)
|
||||
end
|
||||
|
||||
function StageBase:ActivateRedStage()
|
||||
self._logger:debug("Activate red stage for airbase: " .. self._airbase:getName())
|
||||
if self._airbase and (self._initialSide == 2 or self._initialSide == 1) then
|
||||
self._airbase:setCoalition(coalition.side.RED)
|
||||
self._airbase:autoCapture(false)
|
||||
end
|
||||
self:SpawnRedUnits()
|
||||
end
|
||||
|
||||
function StageBase:ActivateBlueStage()
|
||||
self._logger:debug("Activate blue stage for airbase: " .. self._airbase:getName())
|
||||
|
||||
self:CleanRedUnits()
|
||||
|
||||
if self._buildableMission then
|
||||
self:StartBuildable()
|
||||
else
|
||||
self:FinaliseBlueStage()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function StageBase:FinaliseBlueStage()
|
||||
if self._initialSide == 2 and self._airbase then
|
||||
self._airbase:setCoalition(coalition.side.BLUE)
|
||||
self._airbase:autoCapture(false)
|
||||
end
|
||||
|
||||
self:SpawnBlueUnits()
|
||||
|
||||
for _, hub in pairs(self._supplyHubs) do
|
||||
hub:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function StageBase:OnBuildingComplete()
|
||||
self:FinaliseBlueStage()
|
||||
end
|
||||
|
||||
return StageBase
|
||||
@@ -0,0 +1,88 @@
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
|
||||
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
|
||||
|
||||
---@class SupplyHub
|
||||
---@field private _database Database
|
||||
---@field private _logger Logger
|
||||
---@field private _zoneName string
|
||||
---@field private _zone SpearheadTriggerZone?
|
||||
---@field private _supplyUnitsTracker SupplyUnitsTracker
|
||||
---@field private _isCommmandAdded table<string, boolean>
|
||||
---@field private _missionCommandsHelper MissionCommandsHelper
|
||||
---@field private _inZone table<string, boolean>
|
||||
---@field private _drawID number
|
||||
---@field private _cargoInUnits table<table<string, number>>
|
||||
---@field private _activeAtStart boolean
|
||||
---@field private _active boolean
|
||||
local SupplyHub = {}
|
||||
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@param zoneName string
|
||||
---@return SupplyHub?
|
||||
function SupplyHub.new(database, logger, zoneName)
|
||||
|
||||
SupplyHub.__index = SupplyHub
|
||||
local self = setmetatable({}, SupplyHub)
|
||||
|
||||
self._database = database
|
||||
self._logger = logger
|
||||
self._zoneName = zoneName
|
||||
|
||||
local split = Util.split_string(zoneName, "_")
|
||||
if string.lower(split[2]) == "a" then
|
||||
self._activeAtStart = true
|
||||
else
|
||||
self._activeAtStart = false
|
||||
end
|
||||
|
||||
self._zone = DcsUtil.getZoneByName(zoneName)
|
||||
|
||||
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel)
|
||||
self._inZone = {}
|
||||
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
|
||||
|
||||
self._logger:debug("Creating Supply Hub zone: " .. self._zoneName)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function SupplyHub:IsActiveFromStart()
|
||||
return self._activeAtStart
|
||||
end
|
||||
|
||||
function SupplyHub:GetZoneName()
|
||||
return self._zoneName
|
||||
end
|
||||
|
||||
---@return SpearheadTriggerZone?
|
||||
function SupplyHub:GetZone()
|
||||
return self._zone
|
||||
end
|
||||
|
||||
function SupplyHub:Activate()
|
||||
if self._active == true then
|
||||
return
|
||||
end
|
||||
|
||||
self._active = true
|
||||
|
||||
self._logger:debug("Activating Supply Hub zone: " .. self._zoneName)
|
||||
|
||||
local zone = DcsUtil.getZoneByName(self._zoneName)
|
||||
if zone and self._drawID == nil then
|
||||
---@type DrawColor
|
||||
local fillColor = { r=0, g=1, b=0, a=0.2 }
|
||||
---@type DrawColor
|
||||
local lineColor = { r=0, g=1, b=0, a=1}
|
||||
local lineStyle = 1
|
||||
self._drawID = DcsUtil.DrawZone(zone, lineColor, fillColor, lineStyle)
|
||||
end
|
||||
|
||||
self._supplyUnitsTracker:RegisterHub(self)
|
||||
|
||||
end
|
||||
|
||||
return SupplyHub
|
||||
@@ -0,0 +1,203 @@
|
||||
local Util = require("classes.util.Util")
|
||||
local Persistence = require("classes.persistence.Persistence")
|
||||
local BuildableMission = require("classes.stageClasses.missions.BuildableMission")
|
||||
|
||||
---@class BuildableZone : OnCrateDroppedListener
|
||||
---@field protected _targetZone SpearheadTriggerZone
|
||||
---@field protected _requiredKilos number
|
||||
---@field protected _buildableMission BuildableMission?
|
||||
---@field protected _buildableGroups Array<SpearheadGroup>
|
||||
---@field private _groupsPerKilo number
|
||||
---@field private _receivedBuildingKilos number
|
||||
---@field private _buildableLogger Logger
|
||||
---@field protected OnBuildingComplete fun(self:BuildableZone)
|
||||
local BuildableZone = {}
|
||||
BuildableZone.__index = BuildableZone
|
||||
|
||||
---@param targetZone SpearheadTriggerZone
|
||||
---@param kilosRequired number
|
||||
---@param buildableGroups Array<SpearheadGroup>
|
||||
---@param database Database
|
||||
---@param crateType SupplyType
|
||||
---@param logger Logger
|
||||
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database)
|
||||
self._targetZone = targetZone
|
||||
self._requiredKilos = kilosRequired or 0
|
||||
self._buildableGroups = buildableGroups or {}
|
||||
self._buildableLogger = logger
|
||||
local totalGroups = Util.tableLength(self._buildableGroups)
|
||||
self._groupsPerKilo = totalGroups / self._requiredKilos
|
||||
|
||||
self._receivedBuildingKilos = 0
|
||||
local persistedKilos = Persistence.GetZoneDeliveredKilos(targetZone.name)
|
||||
if persistedKilos and persistedKilos > 0 then
|
||||
self._buildableLogger:debug("Zone " .. targetZone.name .. " already has " .. persistedKilos .. " kilos delivered")
|
||||
self._receivedBuildingKilos = persistedKilos
|
||||
|
||||
---@param params UnpackCrateParam
|
||||
---@param time number
|
||||
local startUnpackingCrate = function(params, time)
|
||||
local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2)
|
||||
local alreadySpawned = params.unpackedItems / params.groupsPerKilo
|
||||
local diff = unpacked - alreadySpawned
|
||||
|
||||
local amount = math.floor(diff * params.groupsPerKilo)
|
||||
local spawned = params.self:SpawnAmount(amount)
|
||||
|
||||
params.unpackedItems = params.unpackedItems + amount
|
||||
params.unpackedKilos = unpacked
|
||||
if params.unpackedKilos >= params.kilos or spawned == false then
|
||||
return
|
||||
end
|
||||
|
||||
return time + 0.5
|
||||
end
|
||||
|
||||
---@type UnpackCrateParam
|
||||
local params = {
|
||||
self = self,
|
||||
groupsPerKilo = self._groupsPerKilo,
|
||||
unpackedItems = 0,
|
||||
kilosPerSecond = persistedKilos/30,
|
||||
unpackedKilos = 0,
|
||||
kilos = persistedKilos
|
||||
}
|
||||
|
||||
timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 5)
|
||||
kilosRequired = kilosRequired - persistedKilos
|
||||
end
|
||||
|
||||
local noLandingZone = self:GetNoLandingZone()
|
||||
if kilosRequired and kilosRequired > 0 then
|
||||
self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType)
|
||||
self._buildableMission:AddOnCrateDroppedOfListener(self)
|
||||
else
|
||||
self._buildableMission = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
if self._buildableMission == nil then
|
||||
self._buildableLogger:debug("No buildable mission for zone: " .. targetZone.name)
|
||||
end
|
||||
end
|
||||
|
||||
---@protected
|
||||
function BuildableZone:StartBuildable()
|
||||
self._buildableMission:SpawnActive()
|
||||
end
|
||||
|
||||
---@protected
|
||||
function BuildableZone:OnBuildingComplete() end
|
||||
|
||||
---@class UnpackCrateParam
|
||||
---@field self BuildableZone
|
||||
---@field kilos number
|
||||
---@field groupsPerKilo number
|
||||
---@field kilosPerSecond number
|
||||
---@field unpackedItems number
|
||||
---@field unpackedKilos number
|
||||
|
||||
---@param mission BuildableMission?
|
||||
---@param kilos number
|
||||
function BuildableZone:OnCrateDroppedOff(mission, kilos)
|
||||
self._buildableLogger:debug("Crate dropped off in zone: " .. self._targetZone.name)
|
||||
|
||||
local timeToUnpack = (kilos / 500) * 15
|
||||
|
||||
---@type UnpackCrateParam
|
||||
local params = {
|
||||
self = self,
|
||||
groupsPerKilo = self._groupsPerKilo,
|
||||
unpackedItems = 0,
|
||||
kilosPerSecond = kilos/timeToUnpack,
|
||||
unpackedKilos = 0,
|
||||
kilos = kilos
|
||||
}
|
||||
|
||||
---@param params UnpackCrateParam
|
||||
---@param time number
|
||||
local startUnpackingCrate = function(params, time)
|
||||
local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2)
|
||||
local alreadySpawned = params.unpackedItems / params.groupsPerKilo
|
||||
local diff = unpacked - alreadySpawned
|
||||
|
||||
local amount = math.floor(diff * params.groupsPerKilo)
|
||||
local spawned = params.self:SpawnAmount(amount)
|
||||
|
||||
params.unpackedItems = params.unpackedItems + amount
|
||||
params.unpackedKilos = unpacked
|
||||
if params.unpackedKilos >= params.kilos or spawned == false then
|
||||
params.self:FinaliseCrate(params.kilos)
|
||||
return
|
||||
end
|
||||
|
||||
return time + 2
|
||||
end
|
||||
|
||||
timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2)
|
||||
end
|
||||
|
||||
---@param kilos number
|
||||
function BuildableZone:FinaliseCrate(kilos)
|
||||
self._receivedBuildingKilos = self._receivedBuildingKilos + kilos
|
||||
Persistence.SetZoneDeliveredKilos(self._targetZone.name, self._receivedBuildingKilos)
|
||||
if self._receivedBuildingKilos >= self._requiredKilos then
|
||||
self:OnBuildingComplete()
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return SpearheadTriggerZone?
|
||||
function BuildableZone:GetNoLandingZone()
|
||||
|
||||
---@type Array<Vec2>
|
||||
local points = {}
|
||||
|
||||
for _, group in pairs(self._buildableGroups) do
|
||||
for _, unitPos in pairs(group:GetAllUnitPositions()) do
|
||||
table.insert(points, { x = unitPos.x, y = unitPos.z })
|
||||
end
|
||||
end
|
||||
|
||||
local vecs = Util.getConvexHull(points)
|
||||
|
||||
---@type SpearheadTriggerZone
|
||||
local spearheadZone = {
|
||||
name = self._targetZone.name .. "_noland",
|
||||
location = self._targetZone.location,
|
||||
verts = vecs,
|
||||
radius = 0,
|
||||
zone_type = "Polygon"
|
||||
}
|
||||
|
||||
return spearheadZone
|
||||
end
|
||||
|
||||
|
||||
---comment
|
||||
---@param amount number
|
||||
---@return boolean
|
||||
function BuildableZone:SpawnAmount(amount)
|
||||
local function spawnOne()
|
||||
for _, group in pairs(self._buildableGroups) do
|
||||
if group:IsSpawned() == false then
|
||||
group:Spawn()
|
||||
return true
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, amount do
|
||||
local spawned = spawnOne()
|
||||
if spawned ~= true then
|
||||
self._buildableLogger:debug("No more groups to spawn in zone: " .. self._targetZone.name)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return BuildableZone
|
||||
@@ -0,0 +1,595 @@
|
||||
local SpearheadGroup = require("classes.stageClasses.Groups.SpearheadGroup")
|
||||
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local FarpZone = require("classes.stageClasses.SpecialZones.FarpZone")
|
||||
local SupplyHub = require("classes.stageClasses.SpecialZones.SupplyHub")
|
||||
local Util = require("classes.util.Util")
|
||||
local ZoneMission = require("classes.stageClasses.missions.ZoneMission")
|
||||
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
|
||||
local Persistence = require("classes.persistence.Persistence")
|
||||
local StageBase = require("classes.stageClasses.SpecialZones.StageBase")
|
||||
local BlueSam = require("classes.stageClasses.SpecialZones.BlueSam")
|
||||
local Events = require("classes.spearhead_events")
|
||||
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
|
||||
|
||||
---@alias StageColor
|
||||
---| "RED"
|
||||
---| "BLUE"
|
||||
---| "GRAY"
|
||||
|
||||
--- @class StageData
|
||||
--- @field stageBriefing string?
|
||||
--- @field missionsByCode table<string, Mission>
|
||||
--- @field missionsByName table<string, Mission>
|
||||
--- @field missions Array<ZoneMission>
|
||||
--- @field sams Array<ZoneMission>
|
||||
--- @field blueSams Array<BlueSam>
|
||||
--- @field airbases Array<StageBase>
|
||||
--- @field miscGroups Array<SpearheadGroup>
|
||||
--- @field maxMissions integer
|
||||
--- @field farps Array<FarpZone>
|
||||
--- @field supplyHubs Array<SupplyHub>
|
||||
|
||||
--- @class StageInitData
|
||||
--- @field stageZoneName string
|
||||
--- @field stageNumber integer
|
||||
--- @field stageDisplayName string
|
||||
|
||||
|
||||
--- @class StageCompleteListener
|
||||
--- @field OnStageComplete fun(self:StageCompleteListener, stage:Stage)
|
||||
|
||||
--- @class Stage : MissionCompleteListener, OnStageChangedListener
|
||||
--- @field zoneName string
|
||||
--- @field stageName string?
|
||||
--- @field stageNumber number
|
||||
--- @field protected _missionCommandsHelper MissionCommandsHelper
|
||||
--- @field protected _isActive boolean
|
||||
--- @field protected _isComplete boolean
|
||||
--- @field protected _missionPriority MissionPriority
|
||||
--- @field protected _database Database
|
||||
--- @field protected _db StageData
|
||||
--- @field protected _logger Logger
|
||||
--- @field protected _preActivated boolean
|
||||
--- @field protected _activeStage integer
|
||||
--- @field protected _stageConfig StageConfig
|
||||
--- @field protected _stageDrawingId integer
|
||||
--- @field protected _spawnedGroups Array<string>
|
||||
--- @field protected _stageCompleteListeners Array<StageCompleteListener>
|
||||
--- @field protected CheckContinuousAsync fun(self:Stage, time:number) : number?
|
||||
--- @field protected OnPostStageComplete fun(self:Stage)?
|
||||
--- @field protected OnPostBlueActivated fun(self:Stage)?
|
||||
local Stage = {}
|
||||
|
||||
Stage.__index = Stage
|
||||
|
||||
|
||||
Stage.StageColors = {
|
||||
INVISIBLE = { r=0, g=0, b=0, a=0 },
|
||||
RED_ACTIVE = { r=1, g=0, b=0, a=0.15 },
|
||||
RED_PREACTIVE = { r=1, g=0, b=0, a=0.10},
|
||||
BLUE = { r=0, g=0, b=1, a=0.10},
|
||||
GRAY = { r=80/255, g=80/255, b=80/255, a=0.10 }
|
||||
}
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param stageConfig StageConfig
|
||||
---@param logger Logger
|
||||
---@param initData StageInitData
|
||||
---@param missionPriority MissionPriority
|
||||
---@param spawnManager SpawnManager
|
||||
---@return Stage
|
||||
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager)
|
||||
|
||||
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
|
||||
|
||||
self.zoneName = initData.stageZoneName
|
||||
self.stageNumber = initData.stageNumber
|
||||
self._isActive = false
|
||||
self._isComplete = false
|
||||
self.stageName = initData.stageDisplayName
|
||||
|
||||
self.OnPostStageComplete = nil
|
||||
self.OnPostBlueActivated = nil
|
||||
|
||||
self._database = database
|
||||
self._logger = logger
|
||||
self._db = {
|
||||
stageBriefing = nil,
|
||||
missionsByCode = {},
|
||||
missions = {},
|
||||
sams ={},
|
||||
blueSams = {},
|
||||
airbases ={},
|
||||
miscGroups = {},
|
||||
maxMissions = stageConfig.maxMissionsPerStage,
|
||||
farps = {},
|
||||
missionsByName = {},
|
||||
supplyHubs = {}
|
||||
}
|
||||
|
||||
self._activeStage = -99
|
||||
self._preActivated = false
|
||||
self._stageConfig = stageConfig or {}
|
||||
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
|
||||
|
||||
local zone = DcsUtil.getZoneByName(self.zoneName)
|
||||
if zone then
|
||||
self._stageDrawingId = DcsUtil.DrawZone(zone, Stage.StageColors.INVISIBLE, Stage.StageColors.INVISIBLE, 4)
|
||||
end
|
||||
|
||||
self._spawnedGroups = {}
|
||||
self._missionPriority = missionPriority
|
||||
self._stageCompleteListeners = {}
|
||||
|
||||
local farpNames = database:getFarpNamesInStage(self.zoneName)
|
||||
for _, farpName in pairs(farpNames) do
|
||||
local farp = FarpZone.New(database, logger, farpName, spawnManager)
|
||||
table.insert(self._db.farps, farp)
|
||||
end
|
||||
|
||||
local supplyHubNames = database:getSupplyHubsInStage(self.zoneName)
|
||||
for _, supplyHubName in pairs(supplyHubNames) do
|
||||
local supplyHub = SupplyHub.new(database, logger, supplyHubName)
|
||||
table.insert(self._db.supplyHubs, supplyHub)
|
||||
end
|
||||
|
||||
self._db.stageBriefing = database:getStageBriefingForStage(self.zoneName)
|
||||
self._logger:info("Initiating new Stage with name: " .. self.zoneName)
|
||||
|
||||
---comment
|
||||
---@param self Stage
|
||||
---@param time number?
|
||||
self.CheckContinuousAsync = function (self, time)
|
||||
|
||||
self:CheckAndUpdateSelf()
|
||||
if self:IsComplete() == true then
|
||||
self:NotifyComplete()
|
||||
return nil
|
||||
end
|
||||
|
||||
return time + 20
|
||||
end
|
||||
|
||||
|
||||
do -- load tables
|
||||
local missionZones = database:getMissionsForStage(self.zoneName)
|
||||
self._logger:debug("Found " .. Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName)
|
||||
for _, missionZone in pairs(missionZones) do
|
||||
|
||||
local mission = ZoneMission.new(missionZone, self._missionPriority, database, logger, self, spawnManager)
|
||||
if mission then
|
||||
self._db.missionsByCode[mission.code] = mission
|
||||
|
||||
if mission.name and self._db.missionsByName[mission.name] == nil then
|
||||
self._db.missionsByName[mission.name] = mission
|
||||
else
|
||||
MissionEditorWarnings.Add("DUPLICATE MISSION NAME ALERT: " .. mission.name .. " in zone: " .. self.zoneName)
|
||||
end
|
||||
|
||||
if mission.missionType == "SAM" then
|
||||
table.insert(self._db.sams, mission)
|
||||
else
|
||||
table.insert(self._db.missions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local randomMissionNames = database:getRandomMissionsForStage(self.zoneName)
|
||||
|
||||
---@type table<string, Array<Mission>>
|
||||
local randomMissionByName = {}
|
||||
for _, missionZoneName in pairs(randomMissionNames) do
|
||||
local mission = ZoneMission.new(missionZoneName, self._missionPriority, database, logger, self, spawnManager)
|
||||
if mission then
|
||||
if randomMissionByName[mission.name] == nil then
|
||||
randomMissionByName[mission.name] = {}
|
||||
end
|
||||
table.insert(randomMissionByName[mission.name], mission)
|
||||
end
|
||||
end
|
||||
|
||||
for missionName, missions in pairs(randomMissionByName) do
|
||||
|
||||
local missionZonePicked = Persistence.GetPickedRandomMission(missionName)
|
||||
if missionZonePicked == nil then
|
||||
local mission = Util.randomFromList(missions) --[[@as Mission]]
|
||||
if mission then
|
||||
Persistence.RegisterPickedRandomMission(mission.name, mission.zoneName)
|
||||
|
||||
self._db.missionsByCode[mission.code] = mission
|
||||
|
||||
if mission.name and self._db.missionsByName[mission.name] == nil then
|
||||
self._db.missionsByName[mission.name] = mission
|
||||
else
|
||||
MissionEditorWarnings.Add("DUPLICATE MISSION NAME ALERT: " .. mission.name .. " in zone: " .. self.zoneName)
|
||||
end
|
||||
|
||||
if mission.missionType == "SAM" then
|
||||
table.insert(self._db.sams, mission)
|
||||
else
|
||||
table.insert(self._db.missions, mission)
|
||||
end
|
||||
end
|
||||
else
|
||||
self._logger:info("Using persisted random mission with name: " .. missionName .. " and zone: " .. missionZonePicked)
|
||||
for _, mission in pairs(missions) do
|
||||
if string.lower(mission.zoneName) == string.lower(missionZonePicked) then
|
||||
self._db.missionsByCode[mission.code] = mission
|
||||
if mission.missionType == "SAM" then
|
||||
table.insert(self._db.sams, mission)
|
||||
else
|
||||
table.insert(self._db.missions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, mission in pairs(self._db.missionsByCode) do
|
||||
mission:AddMissionCompleteListener(self)
|
||||
end
|
||||
|
||||
local airbaseNames = database:getAirbaseNamesInStage(self.zoneName)
|
||||
if airbaseNames ~= nil and type(airbaseNames) == "table" then
|
||||
for _, airbaseName in pairs(airbaseNames) do
|
||||
local airbase = StageBase.New(database, logger, airbaseName, spawnManager)
|
||||
table.insert(self._db.airbases, airbase)
|
||||
end
|
||||
end
|
||||
|
||||
for _, samZoneName in pairs(database:getBlueSamsInStage(self.zoneName)) do
|
||||
local blueSam = BlueSam.New(database, logger, samZoneName, spawnManager)
|
||||
table.insert(self._db.blueSams, blueSam)
|
||||
end
|
||||
|
||||
local miscGroups = database:getMiscGroupsAtStage(self.zoneName)
|
||||
for _, groupName in pairs(miscGroups) do
|
||||
local miscGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
|
||||
table.insert(self._db.miscGroups, miscGroup)
|
||||
miscGroup:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
Events.AddStageNumberChangedListener(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Stage:IsComplete()
|
||||
if self._isComplete == true then return true end
|
||||
|
||||
for i, mission in pairs(self._db.sams) do
|
||||
local state = mission:getState()
|
||||
if state == "ACTIVE" or state == "NEW" or state =="WAITING" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for i, mission in pairs(self._db.missions) do
|
||||
local state = mission:getState()
|
||||
if state == "ACTIVE" or state == "NEW" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
self._isComplete = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Stage:IsActive()
|
||||
return self._isActive == true
|
||||
end
|
||||
|
||||
---comment
|
||||
function Stage:CheckAndUpdateSelf()
|
||||
self._logger:debug("Checking on Stage: " .. self.zoneName)
|
||||
|
||||
local dbTables = self:GetStageTables()
|
||||
|
||||
---@return Array<Mission>
|
||||
local getAvailableMissions = function ()
|
||||
---@type Array<Mission>
|
||||
local availableMissions = {}
|
||||
for _, mission in pairs(dbTables.missionsByCode) do
|
||||
if mission:getState() == "NEW" then
|
||||
table.insert(availableMissions, mission)
|
||||
end
|
||||
end
|
||||
return availableMissions
|
||||
end
|
||||
|
||||
---@return number
|
||||
local getActiveMissionsCount = function ()
|
||||
local result = 0
|
||||
for _, mission in pairs(dbTables.missionsByCode) do
|
||||
if mission:getState() == "ACTIVE" then
|
||||
result = result + 1
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local max = dbTables.maxMissions
|
||||
local availableMissionsCount = Util.tableLength(getAvailableMissions())
|
||||
local activeCount = getActiveMissionsCount()
|
||||
if activeCount < max and availableMissionsCount > 0 then
|
||||
for i = activeCount+1, max do
|
||||
if availableMissionsCount == 0 then
|
||||
i = max+1 --exits this loop
|
||||
else
|
||||
local mission = Util.randomFromList(getAvailableMissions()) --[[@as Mission]]
|
||||
if mission then
|
||||
mission:SpawnActive()
|
||||
activeCount = activeCount + 1;
|
||||
else
|
||||
return
|
||||
end
|
||||
availableMissionsCount = availableMissionsCount - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param missionName string
|
||||
---@return boolean
|
||||
function Stage:IsMissionComplete(missionName)
|
||||
|
||||
local mission = self._db.missionsByName[missionName]
|
||||
if not mission then return true end
|
||||
|
||||
return mission:getState() == "COMPLETED"
|
||||
end
|
||||
|
||||
---private use only
|
||||
function Stage:NotifyComplete()
|
||||
|
||||
self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown"))
|
||||
|
||||
for _, listener in pairs(self._stageCompleteListeners) do
|
||||
pcall(function()
|
||||
listener:OnStageComplete(self)
|
||||
end)
|
||||
end
|
||||
|
||||
if self.OnPostStageComplete then
|
||||
timer.scheduleFunction(self.OnPostStageComplete, self, timer.getTime() + 3)
|
||||
end
|
||||
end
|
||||
|
||||
---@param listener StageCompleteListener
|
||||
function Stage:AddStageCompleteListener(listener)
|
||||
table.insert(self._stageCompleteListeners, listener)
|
||||
end
|
||||
|
||||
---Activates all SAMS, Airbase units etc all at once.
|
||||
---@param draw boolean
|
||||
function Stage:PreActivate(draw)
|
||||
if self._preActivated == false then
|
||||
self._preActivated = true
|
||||
for key, mission in pairs(self._db.sams) do
|
||||
if mission then
|
||||
mission:SpawnInactive()
|
||||
end
|
||||
end
|
||||
|
||||
for _, airbase in pairs(self._db.airbases) do
|
||||
airbase:ActivateRedStage()
|
||||
end
|
||||
end
|
||||
|
||||
if draw == true then
|
||||
self:MarkStage(Stage.StageColors.RED_PREACTIVE)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---@param stageColor DrawColor
|
||||
function Stage:MarkStage(stageColor)
|
||||
local lineColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
|
||||
local fillColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
|
||||
|
||||
if stageColor.a > 0 then
|
||||
lineColor.a = 1
|
||||
end
|
||||
|
||||
if stageColor == Stage.StageColors.RED_PREACTIVE then
|
||||
lineColor.a = 0
|
||||
end
|
||||
|
||||
if self._stageDrawingId and self._stageConfig.isDrawStagesEnabled == true then
|
||||
DcsUtil.SetLineColor(self._stageDrawingId, lineColor)
|
||||
DcsUtil.SetFillColor(self._stageDrawingId, fillColor)
|
||||
end
|
||||
end
|
||||
|
||||
function Stage:ActivateStage()
|
||||
self._isActive = true;
|
||||
|
||||
pcall(function()
|
||||
self:MarkStage(Stage.StageColors.RED_ACTIVE)
|
||||
end)
|
||||
|
||||
self:PreActivate(false)
|
||||
|
||||
self._logger:debug("Activating Misc groups for zone. Count: " .. Util.tableLength(self._db.miscGroups))
|
||||
for _, miscGroup in pairs(self._db.miscGroups) do
|
||||
miscGroup:Spawn()
|
||||
end
|
||||
|
||||
for _, mission in pairs(self._db.missions) do
|
||||
if mission.missionType == "DEAD" then
|
||||
mission:SpawnActive()
|
||||
end
|
||||
end
|
||||
|
||||
for _, farp in pairs(self._db.farps) do
|
||||
if farp:IsStartingFarp() == true then
|
||||
farp:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
for _, supplyHub in pairs(self._db.supplyHubs) do
|
||||
if supplyHub:IsActiveFromStart() == true then
|
||||
supplyHub:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3)
|
||||
end
|
||||
|
||||
---Private usage only
|
||||
---@return StageData
|
||||
function Stage:GetStageTables()
|
||||
return self._db
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param self Stage
|
||||
---@param number integer
|
||||
function Stage:OnStageNumberChanged(number)
|
||||
|
||||
if self._activeStage == number then --only activate once for a stage
|
||||
return
|
||||
end
|
||||
|
||||
local previousActive = self._activeStage
|
||||
self._activeStage = number
|
||||
|
||||
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
|
||||
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
|
||||
self:PreActivate(true)
|
||||
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
|
||||
self:PreActivate(false)
|
||||
end
|
||||
|
||||
if number == self.stageNumber then
|
||||
self:ActivateStage()
|
||||
|
||||
if self._db and self._db.stageBriefing then
|
||||
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
|
||||
end
|
||||
end
|
||||
|
||||
if previousActive <= self.stageNumber then
|
||||
if number > self.stageNumber then
|
||||
self:ActivateBlueStage()
|
||||
end
|
||||
end
|
||||
|
||||
if number > self.stageNumber then
|
||||
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
|
||||
end
|
||||
end
|
||||
|
||||
function Stage:GetBriefing()
|
||||
return "Briefing For "
|
||||
end
|
||||
|
||||
---@param self Stage
|
||||
---@param mission Mission
|
||||
Stage.OnMissionComplete = function(self, mission)
|
||||
self:CheckAndUpdateSelf()
|
||||
end
|
||||
|
||||
|
||||
---private use only
|
||||
function Stage:ActivateBlueGroups()
|
||||
|
||||
for _, blueSam in pairs(self._db.blueSams) do
|
||||
blueSam:Activate()
|
||||
end
|
||||
|
||||
for _, airbase in pairs(self._db.airbases) do
|
||||
airbase:ActivateBlueStage()
|
||||
end
|
||||
|
||||
if self.OnPostBlueActivated then
|
||||
pcall(function()
|
||||
self:OnPostBlueActivated()
|
||||
end)
|
||||
end
|
||||
|
||||
for _, farp in pairs(self._db.farps) do
|
||||
if farp:IsStartingFarp() == true then
|
||||
farp:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
for _, supplyHub in pairs(self._db.supplyHubs) do
|
||||
supplyHub:Activate()
|
||||
end
|
||||
end
|
||||
|
||||
---@return number strike
|
||||
---@return number dead
|
||||
---@return number bai
|
||||
---@return number cas
|
||||
function Stage:GetStageStats()
|
||||
|
||||
local strike = 0
|
||||
local dead = 0
|
||||
local bai = 0
|
||||
local cas = 0
|
||||
|
||||
for _, mission in pairs(self._db.missions) do
|
||||
if mission.missionType == "STRIKE" then
|
||||
strike = strike + 1
|
||||
elseif mission.missionType == "DEAD" or mission.missionType == "SAM" then
|
||||
dead = dead + 1
|
||||
elseif mission.missionType == "BAI" then
|
||||
bai = bai + 1
|
||||
elseif mission.missionType == "CAS" then
|
||||
cas = cas + 1
|
||||
end
|
||||
end
|
||||
|
||||
for _, mission in pairs(self._db.sams) do
|
||||
dead = dead + 1
|
||||
end
|
||||
|
||||
return strike, dead, bai, cas
|
||||
|
||||
end
|
||||
|
||||
function Stage:ActivateBlueStage()
|
||||
|
||||
self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue")
|
||||
|
||||
for _, mission in pairs(self._db.missions) do
|
||||
mission:SpawnPersistedState()
|
||||
end
|
||||
|
||||
for _, mission in pairs(self._db.sams) do
|
||||
mission:SpawnPersistedState()
|
||||
end
|
||||
|
||||
for _, miscGroup in pairs(self._db.miscGroups) do
|
||||
miscGroup:Spawn()
|
||||
end
|
||||
|
||||
---@param self Stage
|
||||
local ActivateBlueAsync = function(self)
|
||||
pcall(function()
|
||||
self:MarkStage(Stage.StageColors.BLUE)
|
||||
end)
|
||||
|
||||
self:ActivateBlueGroups()
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
|
||||
end
|
||||
|
||||
return Stage
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
|
||||
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
|
||||
|
||||
---@class ExtraStage : Stage
|
||||
local ExtraStage = {}
|
||||
ExtraStage.__index = ExtraStage
|
||||
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param stageConfig StageConfig
|
||||
---@param logger any
|
||||
---@param initData StageInitData
|
||||
---@param spawnManager SpawnManager
|
||||
---@return ExtraStage
|
||||
function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
|
||||
|
||||
setmetatable(ExtraStage, Stage)
|
||||
|
||||
local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
|
||||
self:superNew(database, stageConfig, logger, initData, "secondary", spawnManager)
|
||||
|
||||
self.OnPostBlueActivated = function (selfStage)
|
||||
|
||||
selfStage:MarkStage(Stage.StageColors.GRAY)
|
||||
end
|
||||
|
||||
self.OnPostStageComplete = function (selfStage)
|
||||
selfStage:ActivateBlueStage()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param self Stage
|
||||
---@param number integer
|
||||
function ExtraStage:OnStageNumberChanged(number)
|
||||
|
||||
if self._activeStage == number then --only activate once for a stage
|
||||
return
|
||||
end
|
||||
|
||||
local previousActive = self._activeStage
|
||||
self._activeStage = number
|
||||
|
||||
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
|
||||
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
|
||||
self:PreActivate(true)
|
||||
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
|
||||
self:PreActivate(false)
|
||||
end
|
||||
|
||||
if number == self.stageNumber then
|
||||
self:ActivateStage()
|
||||
end
|
||||
|
||||
if self._isComplete == true then
|
||||
self:ActivateBlueStage()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
return ExtraStage
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
|
||||
|
||||
---@class PrimaryStage : Stage
|
||||
local PrimaryStage = {}
|
||||
|
||||
PrimaryStage.__index = PrimaryStage
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param stageConfig StageConfig
|
||||
---@param logger any
|
||||
---@param initData StageInitData
|
||||
---@param spawnManager SpawnManager
|
||||
---@return PrimaryStage
|
||||
function PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
|
||||
|
||||
setmetatable(PrimaryStage, Stage)
|
||||
|
||||
local self = setmetatable({}, { __index = PrimaryStage }) --[[@as PrimaryStage]]
|
||||
self:superNew(database, stageConfig, logger, initData, "primary", spawnManager)
|
||||
return self
|
||||
|
||||
end
|
||||
|
||||
return PrimaryStage
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
|
||||
|
||||
---@class WaitingStage : Stage
|
||||
---@field private _waitTimeSeconds integer
|
||||
---@field private _startTime number
|
||||
local WaitingStage = {}
|
||||
|
||||
WaitingStage.__index = WaitingStage
|
||||
|
||||
---@class WaitingStageInitData : StageInitData
|
||||
---@field waitingSeconds integer
|
||||
local WaitingStageInitData = {}
|
||||
|
||||
---comment
|
||||
---@param database Database
|
||||
---@param stageConfig StageConfig
|
||||
---@param logger any
|
||||
---@param initData WaitingStageInitData
|
||||
---@param spawnManager SpawnManager
|
||||
---@return WaitingStage
|
||||
function WaitingStage.New(database, stageConfig, logger, initData, spawnManager)
|
||||
setmetatable(WaitingStage, Stage)
|
||||
|
||||
local self = setmetatable({}, { __index = WaitingStage }) --[[@as WaitingStage]]
|
||||
self:superNew(database, stageConfig, logger, initData, "none", spawnManager)
|
||||
|
||||
self._waitTimeSeconds = 5
|
||||
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
|
||||
self._startTime = nil
|
||||
|
||||
self.CheckContinuousAsync = function (selfA, time)
|
||||
|
||||
if selfA:IsComplete() == true then
|
||||
selfA:NotifyComplete()
|
||||
return nil
|
||||
end
|
||||
|
||||
return time + 2
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
function WaitingStage:ActivateStage()
|
||||
|
||||
self._logger:info("Starting Waiting Stage '" .. self.zoneName .. "' which will complete in about " .. self._waitTimeSeconds .. " seconds")
|
||||
|
||||
self._isActive = true
|
||||
self._startTime = timer.getTime()
|
||||
timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds)
|
||||
end
|
||||
|
||||
function WaitingStage:IsComplete()
|
||||
if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function WaitingStage:OnStageNumberChanged()
|
||||
self._logger:debug("Waiting Stage OnStageNumberChanged override")
|
||||
end
|
||||
|
||||
function WaitingStage:MarkStage(stageColor)
|
||||
self._logger:debug("Waiting Stage MarkStage override")
|
||||
end
|
||||
|
||||
function WaitingStage:GetExpectedTime()
|
||||
return self._startTime + self._waitTimeSeconds
|
||||
end
|
||||
|
||||
return WaitingStage
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
|
||||
local Util = require("classes.util.Util")
|
||||
|
||||
---@class CustomDrawing
|
||||
---@field private _id integer?
|
||||
---@field private _drawingObject DrawingObject
|
||||
---@field private _startingStage number
|
||||
---@field private _removeAtStage number
|
||||
local CustomDrawing = {}
|
||||
CustomDrawing.__index = CustomDrawing
|
||||
|
||||
|
||||
---@param drawingObject DrawingObject
|
||||
---@param id integer?
|
||||
---@return CustomDrawing
|
||||
function CustomDrawing.New(drawingObject, id)
|
||||
local self = setmetatable({}, CustomDrawing)
|
||||
self._drawingObject = drawingObject
|
||||
self._id = id
|
||||
|
||||
local name = drawingObject.name
|
||||
local split = Util.split_string(name or "", "_")
|
||||
local secondPart = split[2] or "1"
|
||||
local splitPart = Util.split_string(secondPart, ":")
|
||||
self._startingStage = tonumber(splitPart[1]) or 1
|
||||
self._removeAtStage = tonumber(splitPart[2]) or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
---@return number start
|
||||
---@return number stop
|
||||
function CustomDrawing:GetStartAndStop()
|
||||
return self._startingStage, self._removeAtStage
|
||||
end
|
||||
|
||||
function CustomDrawing:Draw()
|
||||
self._id = DrawingHelper.Draw(self._drawingObject)
|
||||
end
|
||||
|
||||
function CustomDrawing:Remove()
|
||||
if self._id ~= nil then
|
||||
DrawingHelper.Remove(self._id)
|
||||
self._id = nil
|
||||
end
|
||||
end
|
||||
|
||||
return CustomDrawing
|
||||
@@ -0,0 +1,223 @@
|
||||
|
||||
|
||||
---@class DrawingHelper
|
||||
local DrawingHelper = {}
|
||||
DrawingHelper.__index = DrawingHelper
|
||||
|
||||
local customDrawingIdIncrementer = 4210
|
||||
|
||||
---@param object DrawingObject
|
||||
---@return integer? id
|
||||
function DrawingHelper.Draw(object)
|
||||
if object == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local id = DrawingHelper.GetAndAddId()
|
||||
if(object.primitiveType == "Polygon") then
|
||||
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id)
|
||||
elseif(object.primitiveType == "Line") then
|
||||
DrawingHelper.DrawLine(object--[[@as Line]], id)
|
||||
elseif(object.primitiveType == "TextBox") then
|
||||
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id)
|
||||
end
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
function DrawingHelper.GetAndAddId()
|
||||
customDrawingIdIncrementer = customDrawingIdIncrementer + 1
|
||||
return customDrawingIdIncrementer
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param shapeID ShapeId
|
||||
---@param drawID integer
|
||||
---@param points Array<Vec3>
|
||||
---@param fillColor table
|
||||
---@param lineColor table
|
||||
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle)
|
||||
|
||||
local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. ","
|
||||
for _, point in ipairs(points) do
|
||||
functionString = functionString .. " { x=" .. point.x .. ", y=0,z=" .. point.z .. "},"
|
||||
end
|
||||
functionString = functionString ..
|
||||
"{ " .. lineColor[1] .. "," .. lineColor[2] .. "," .. lineColor[3] .. "," .. lineColor[4] .. "}, " ..
|
||||
"{ " .. fillColor[1] .. "," .. fillColor[2] .. "," .. fillColor[3] .. "," .. fillColor[4] .. "}, " ..
|
||||
lineStyle .. ")"
|
||||
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local f, err = loadstring(functionString)
|
||||
if f then
|
||||
f()
|
||||
else
|
||||
env.error("Something failed when drawing complex drawing" .. err)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param object Polygon
|
||||
---@param id integer
|
||||
function DrawingHelper.DrawPolygon(object, id)
|
||||
if object == nil then
|
||||
return
|
||||
end
|
||||
|
||||
---@param circle Circle
|
||||
local function DrawCircle(circle)
|
||||
local vec3 = { x = circle.mapX, y = 0, z = circle.mapY }
|
||||
local fillColor = DrawingHelper.ColorToColorTable(circle.fillColorString)
|
||||
local colorString = DrawingHelper.ColorToColorTable(circle.colorString)
|
||||
local style = DrawingHelper.ToLineStyleInteger(circle.style)
|
||||
trigger.action.circleToAll(-1, id, vec3, circle.radius, colorString, fillColor, style, true)
|
||||
end
|
||||
|
||||
---@param oval Oval
|
||||
local function DrawOval(oval)
|
||||
---@type Array<Vec3>
|
||||
local points = {}
|
||||
local pointsNo = 30
|
||||
local angleStep = (2 * math.pi) / points
|
||||
|
||||
local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString)
|
||||
local color = DrawingHelper.ColorToColorTable(oval.colorString)
|
||||
local lineStyle = DrawingHelper.ToLineStyleInteger(oval.style)
|
||||
|
||||
for i = 1, pointsNo do
|
||||
local angle = i * angleStep
|
||||
local x = oval.mapX + (oval.r1 * math.cos(angle))
|
||||
local y = oval.mapY + (oval.r2 * math.sin(angle))
|
||||
table.insert(points, { x = x, y = 0, z = y } )
|
||||
end
|
||||
MarkupToAll(7, id, points, fillColor, color, lineStyle)
|
||||
end
|
||||
|
||||
---@param free Free
|
||||
local function DrawFree(free)
|
||||
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
|
||||
local color = DrawingHelper.ColorToColorTable(free.colorString)
|
||||
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
|
||||
|
||||
local points = {}
|
||||
for _, point in ipairs(free.points) do
|
||||
table.insert(points, { x = point.x, y = 0, z = point.y } )
|
||||
end
|
||||
MarkupToAll(7, id, points, fillColor, color, lineStyle)
|
||||
end
|
||||
|
||||
---@param rect Rect
|
||||
local function DrawRect(rect)
|
||||
local fillColor = DrawingHelper.ColorToColorTable(rect.fillColorString)
|
||||
local color = DrawingHelper.ColorToColorTable(rect.colorString)
|
||||
local lineStyle = DrawingHelper.ToLineStyleInteger(rect.style)
|
||||
|
||||
local pointA = { x = rect.mapX, y = 0, z = rect.mapY }
|
||||
local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height }
|
||||
trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true)
|
||||
end
|
||||
|
||||
---@param arrow Arrow
|
||||
local function DrawArrow(arrow)
|
||||
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
|
||||
local color = DrawingHelper.ColorToColorTable(arrow.colorString)
|
||||
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
|
||||
|
||||
local startPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
|
||||
local rad = math.rad(arrow.angle or 0)
|
||||
local length = arrow.length or 100
|
||||
local endPoint = { x = arrow.mapX + length * math.cos(rad), y = 0, z = arrow.mapY + length * math.sin(rad) }
|
||||
trigger.action.arrowToAll(-1, id, startPoint, endPoint, color, fillColor, lineStyle, true)
|
||||
end
|
||||
|
||||
if object.polygonMode == "circle" then
|
||||
DrawCircle(object--[[@as Circle]])
|
||||
elseif object.polygonMode == "oval" then
|
||||
DrawOval(object--[[@as Oval]])
|
||||
elseif object.polygonMode == "free" then
|
||||
DrawFree(object--[[@as Free]])
|
||||
elseif object.polygonMode == "rect" then
|
||||
DrawRect(object--[[@as Rect]])
|
||||
elseif object.polygonMode == "arrow" then
|
||||
DrawArrow(object--[[@as Arrow]])
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param object Line
|
||||
---@param id integer
|
||||
function DrawingHelper.DrawLine(object, id)
|
||||
|
||||
---@type Array<Vec3>
|
||||
local points = {}
|
||||
|
||||
for _, point in ipairs(object.points) do
|
||||
table.insert(points, { x = point.x, y = 0, z = point.y } )
|
||||
end
|
||||
|
||||
local color = DrawingHelper.ColorToColorTable(object.colorString)
|
||||
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
|
||||
MarkupToAll(1, id, points, color, color, lineStyle)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param object TextBox
|
||||
---@param id integer
|
||||
function DrawingHelper.DrawTextBox(object, id)
|
||||
trigger.action.textToAll(-1, id, { x= object.mapX, y = 0, z = object.mapY },
|
||||
DrawingHelper.ColorToColorTable(object.colorString),
|
||||
DrawingHelper.ColorToColorTable(object.fillColorString),
|
||||
object.fontSize or 12,
|
||||
true,
|
||||
object.text or "")
|
||||
end
|
||||
|
||||
function DrawingHelper.Remove(id)
|
||||
trigger.action.removeMark(id)
|
||||
end
|
||||
|
||||
|
||||
|
||||
---@param hexStr string
|
||||
---@return table
|
||||
function DrawingHelper.ColorToColorTable(hexStr)
|
||||
hexStr = hexStr:gsub("0x", "")
|
||||
local a = tonumber(hexStr:sub(1, 2), 16) / 255
|
||||
local r = tonumber(hexStr:sub(3, 4), 16) / 255
|
||||
local g = tonumber(hexStr:sub(5, 6), 16) / 255
|
||||
local b = tonumber(hexStr:sub(7, 8), 16) / 255
|
||||
|
||||
return { r, g , b , a }
|
||||
end
|
||||
|
||||
---@param lineStyle string
|
||||
function DrawingHelper.ToLineStyleInteger(lineStyle)
|
||||
lineStyle = lineStyle:lower()
|
||||
if lineStyle == "no line" then
|
||||
return 0
|
||||
elseif lineStyle == "solid" then
|
||||
return 1
|
||||
elseif lineStyle == "dashed" then
|
||||
return 2
|
||||
elseif lineStyle == "dotted" then
|
||||
return 3
|
||||
elseif lineStyle == "dot dash" then
|
||||
return 4
|
||||
elseif lineStyle == "long dash" then
|
||||
return 5
|
||||
elseif lineStyle == "two dash" then
|
||||
return 6
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
---@class ARGB
|
||||
---@field public a number
|
||||
---@field public r number
|
||||
---@field public g number
|
||||
---@field public b number
|
||||
|
||||
|
||||
return DrawingHelper
|
||||
@@ -0,0 +1,286 @@
|
||||
local Logger = require("classes.util.Logger")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
|
||||
---@class BattleManager
|
||||
---@field private _name string
|
||||
---@field private _logger Logger
|
||||
---@field private _redGroups Array<SpearheadGroup>
|
||||
---@field private _blueGroups Array<SpearheadGroup>
|
||||
---@field private _redShootAtPoints Array<Vec2>
|
||||
---@field private _blueShootAtPoints Array<Vec2>
|
||||
---@field private _isActive boolean
|
||||
local BattleManager = {}
|
||||
BattleManager.__index = BattleManager
|
||||
|
||||
local debugDrawing = false
|
||||
|
||||
---@param redGroups Array<SpearheadGroup>
|
||||
---@param blueGroups Array<SpearheadGroup>
|
||||
---@param name string
|
||||
---@param logLevel LogLevel
|
||||
---@return BattleManager
|
||||
function BattleManager.New(redGroups, blueGroups, name, logLevel)
|
||||
local self = setmetatable({}, BattleManager)
|
||||
|
||||
self._isActive = false
|
||||
self._name = name
|
||||
self._logger = Logger.new("BattleManager_" .. name, logLevel)
|
||||
|
||||
self._redGroups = redGroups
|
||||
self._blueGroups = blueGroups
|
||||
|
||||
self._logger:debug("BattleManager created with name: " .. self._name
|
||||
.. ", red groups: " .. #self._redGroups
|
||||
.. ", blue groups: " .. #self._blueGroups)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@param self BattleManager
|
||||
---@param time number
|
||||
local function CheckTask(self, time)
|
||||
local interval = self:Update()
|
||||
if not interval then return end
|
||||
return time + interval
|
||||
end
|
||||
|
||||
function BattleManager:Start()
|
||||
self._logger:info("BattleManager started: " .. self._name)
|
||||
self._isActive = true
|
||||
self:SetAllInvisible()
|
||||
|
||||
timer.scheduleFunction(CheckTask, self, timer.getTime() + 5)
|
||||
end
|
||||
|
||||
function BattleManager:Stop()
|
||||
self._isActive = false
|
||||
self:SetAllVisible()
|
||||
end
|
||||
|
||||
---@private
|
||||
function BattleManager:SetAllInvisible()
|
||||
for _, group in pairs(self._redGroups) do
|
||||
group:SetInvisible()
|
||||
end
|
||||
|
||||
for _, group in pairs(self._blueGroups) do
|
||||
group:SetInvisible()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@private
|
||||
function BattleManager:SetAllVisible()
|
||||
for _, group in pairs(self._redGroups) do
|
||||
group:SetVisible()
|
||||
end
|
||||
|
||||
for _, group in pairs(self._blueGroups) do
|
||||
group:SetVisible()
|
||||
end
|
||||
end
|
||||
|
||||
---comment
|
||||
---@return number?
|
||||
function BattleManager:Update()
|
||||
if self._isActive == false then
|
||||
return nil
|
||||
end
|
||||
|
||||
self._logger:debug("BattleManager Update called for " .. self._name)
|
||||
|
||||
local shootChance = 1 -- Adjust this value to control the shooting probability (0.0 to 1.0)
|
||||
|
||||
self:LetUnitsShoot(self._redGroups, self._blueGroups)
|
||||
self:LetUnitsShoot(self._blueGroups, self._redGroups)
|
||||
|
||||
return math.random(4, 10) -- Return a random interval between 5 and 10 seconds for the next update
|
||||
end
|
||||
|
||||
|
||||
---@private
|
||||
---@param groups Array<SpearheadGroup>
|
||||
---@param targetGroups Array<SpearheadGroup>
|
||||
function BattleManager:LetUnitsShoot(groups, targetGroups)
|
||||
|
||||
local shootChance = math.random(3, 7) / 10
|
||||
|
||||
local targetHulls = self:ToShootingHulls(targetGroups)
|
||||
|
||||
for _, group in pairs(groups) do
|
||||
|
||||
local units = group:GetAsUnits()
|
||||
|
||||
for _, unit in pairs(units) do
|
||||
|
||||
if self:IsUnitApplicable(unit) == true then
|
||||
|
||||
if unit:hasAttribute("Infantry") == true then
|
||||
shootChance = 0.8
|
||||
end
|
||||
|
||||
if math.random() <= shootChance then
|
||||
local unitPos = unit:getPoint()
|
||||
local point = self:GetRandomPoint({x = unitPos.x, y = unitPos.z }, targetHulls)
|
||||
if point then
|
||||
|
||||
local ammo, qty = self:getBestAmmo(unit)
|
||||
local shootTask = {
|
||||
id = "FireAtPoint",
|
||||
params = {
|
||||
point = point,
|
||||
radius = 1,
|
||||
expendQty = qty,
|
||||
weaponType = ammo,
|
||||
expendQtyEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
if debugDrawing == true then
|
||||
self:DrawDebugLine(point, unit)
|
||||
end
|
||||
|
||||
local controller = unit:getController()
|
||||
if controller then
|
||||
controller:setTask(shootTask)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param unit Unit
|
||||
---@return number
|
||||
---@return number
|
||||
function BattleManager:getBestAmmo(unit)
|
||||
|
||||
local ammo = unit:getAmmo()
|
||||
|
||||
if not ammo then return 3221225470, 1 end -- Default ammo if no ammo is found
|
||||
|
||||
local shells = {}
|
||||
|
||||
for _, entry in pairs(ammo) do
|
||||
if entry.count and entry.count > 0 then
|
||||
if entry.desc.category == Weapon.Category.SHELL then
|
||||
table.insert(shells, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local entry = Util.randomFromList(shells)
|
||||
if entry and entry.desc and entry.desc.warhead then
|
||||
local caliber = entry.desc.warhead.caliber
|
||||
if caliber > 50 then
|
||||
return 258503344128, 1
|
||||
else
|
||||
return 258503344129, 25
|
||||
end
|
||||
end
|
||||
return 3221225470, 1
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unit Unit
|
||||
---@return boolean
|
||||
function BattleManager:IsUnitApplicable(unit)
|
||||
if not unit or not unit:isExist() then
|
||||
return false
|
||||
end
|
||||
|
||||
if
|
||||
unit:hasAttribute("AAA") == true
|
||||
or unit:hasAttribute("Air Defence") == true
|
||||
or unit:hasAttribute("Mobile AAA") == true
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param groups Array<SpearheadGroup>
|
||||
---@return Array<Array<Vec2>>
|
||||
function BattleManager:ToShootingHulls(groups)
|
||||
local result = {}
|
||||
|
||||
local points = {}
|
||||
for _, group in pairs(groups) do
|
||||
for _, unit in pairs(group:GetObjects()) do
|
||||
local pos = unit:getPoint()
|
||||
table.insert(points, {x = pos.x, y = pos.z})
|
||||
end
|
||||
end
|
||||
|
||||
local hulls = Util.getSeparatedConvexHulls(points, 50)
|
||||
local enlargedHulls = {}
|
||||
for _, hull in pairs(hulls) do
|
||||
local enlarged = Util.enlargeConvexHull(hull, 25)
|
||||
if enlarged then
|
||||
table.insert(enlargedHulls, enlarged)
|
||||
end
|
||||
end
|
||||
|
||||
for _, hull in pairs(enlargedHulls) do
|
||||
if #hull > 2 then
|
||||
table.insert(result, hull)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param origin Vec2
|
||||
---@param groupHulls Array<Array<Vec2>>
|
||||
---@return Vec2?
|
||||
function BattleManager:GetRandomPoint(origin, groupHulls)
|
||||
|
||||
local hull = Util.randomFromList(groupHulls) --[[@as Array<Vec2>]]
|
||||
if not hull then return nil end
|
||||
local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin)
|
||||
|
||||
if debugDrawing == true then
|
||||
self:DrawDebugZone({ hull })
|
||||
end
|
||||
|
||||
return Util.randomFromList(shootPoints) --[[@as Vec2]]
|
||||
end
|
||||
|
||||
do --DEBUG
|
||||
|
||||
---@param unit Unit
|
||||
---@param target Vec2
|
||||
function BattleManager:DrawDebugLine(target, unit)
|
||||
local color = {r = 1, g = 0, b = 0, a = 1}
|
||||
if unit:getCoalition() == 2 then
|
||||
color = {r = 0, g = 0, b = 1, a = 1}
|
||||
end
|
||||
|
||||
DcsUtil.DrawLine(unit:getPoint(), {x = target.x, y = 0, z = target.y}, color, 1)
|
||||
end
|
||||
|
||||
---@param hulls Array<Array<Vec2>>
|
||||
function BattleManager:DrawDebugZone(hulls)
|
||||
for _, drawHull in pairs(hulls) do
|
||||
|
||||
---@type SpearheadTriggerZone
|
||||
local zone = {
|
||||
name = "temp",
|
||||
zone_type = "Polygon",
|
||||
radius = 0,
|
||||
verts = drawHull,
|
||||
location = { x=drawHull[1].x, y=drawHull[1].y },
|
||||
}
|
||||
|
||||
DcsUtil.DrawZone(zone, {r =0, g=0, b =1, a = 0.5} ,{r =0, g= 0, b =1, a = 0}, 1)
|
||||
end
|
||||
end
|
||||
end --DEBUG
|
||||
|
||||
return BattleManager
|
||||
@@ -0,0 +1,20 @@
|
||||
---@class MaxLoadConfig
|
||||
---@field maxInternalLoad number
|
||||
|
||||
---@type table<string, MaxLoadConfig>
|
||||
local MaxLoadConfig = {
|
||||
["Mi-8MT"] = {
|
||||
maxInternalLoad = 4000,
|
||||
},
|
||||
["CH-47Fbl1"] = {
|
||||
maxInternalLoad = 10000
|
||||
},
|
||||
["Mi-24P"] = {
|
||||
maxInternalLoad = 2000
|
||||
},
|
||||
["UH-1H"] = {
|
||||
maxInternalLoad = 2000
|
||||
}
|
||||
}
|
||||
|
||||
return MaxLoadConfig
|
||||
@@ -0,0 +1,574 @@
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local Logger = require("classes.util.Logger")
|
||||
local SpearheadEvents = require("classes.spearhead_events")
|
||||
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
|
||||
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
|
||||
|
||||
|
||||
---@class MissionCommandsHelper
|
||||
---@field missionsByCode table<string, Mission> @table of missions by their code
|
||||
---@field enabledByCode table<string, boolean> @table of enabled missions by their code
|
||||
---@field updateNeeded boolean @flag to indicate if an update is needed
|
||||
---@field lastUpdate number @last update time
|
||||
---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
|
||||
---@field pinnedByGroup table<string, Mission> @table of pinned missions by group ID
|
||||
---@field private _stageBriefings table<string, string> @table of stage briefings by stage name
|
||||
---@field private _supplyHubGroups table<string, boolean> @table of supply hub groups by their ID
|
||||
---@field private _logger Logger @logger instance for logging
|
||||
---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
|
||||
local MissionCommandsHelper = {}
|
||||
MissionCommandsHelper.__index = MissionCommandsHelper
|
||||
|
||||
---@param list Array<Mission>
|
||||
---@param groupPos Vec2
|
||||
local function sortMissions(list, groupPos)
|
||||
table.sort(list, function(a, b)
|
||||
local distA = Util.VectorDistance2d(groupPos, a.location or {x=0, y=0})
|
||||
local distB = Util.VectorDistance2d(groupPos, b.location or {x=0, y=0})
|
||||
return distA < distB;
|
||||
end)
|
||||
end
|
||||
|
||||
local id = 0
|
||||
|
||||
local instance = nil
|
||||
|
||||
---@return MissionCommandsHelper
|
||||
---@param logLevel string @log level for the logger
|
||||
function MissionCommandsHelper.getOrCreate(logLevel)
|
||||
if instance == nil then
|
||||
instance = setmetatable({}, MissionCommandsHelper)
|
||||
|
||||
instance._logger = Logger.new("MissionCommandsHelper", logLevel)
|
||||
|
||||
instance._logger:info("Creating MissionCommandsHelper instance")
|
||||
|
||||
instance.missionsByCode = {}
|
||||
instance.enabledByCode = {}
|
||||
instance.updateNeeded = false
|
||||
instance.pinnedByGroup = {}
|
||||
instance.lastUpdate = 0
|
||||
instance._supplyHubGroups = {}
|
||||
instance._stageBriefings = {}
|
||||
|
||||
instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logLevel)
|
||||
|
||||
---comment
|
||||
---@param selfA MissionCommandsHelper
|
||||
---@param time number
|
||||
---@return number
|
||||
instance.updateContinuous = function(selfA, time)
|
||||
if selfA.updateNeeded == false then
|
||||
return time + 10
|
||||
end
|
||||
|
||||
for _, unit in pairs(DcsUtil.getAllPlayerUnits()) do
|
||||
if unit and unit:isExist() then
|
||||
local group = unit:getGroup()
|
||||
if group then
|
||||
selfA:updateCommandsForGroup(group:getID())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
selfA.lastUpdate = timer.getTime()
|
||||
selfA.updateNeeded = false
|
||||
return time + 10
|
||||
end
|
||||
|
||||
timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5)
|
||||
SpearheadEvents.AddOnPlayerEnterUnitListener(instance)
|
||||
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
function MissionCommandsHelper:AddStageBriefing(stageName, briefing)
|
||||
self._stageBriefings[stageName] = briefing
|
||||
end
|
||||
|
||||
function MissionCommandsHelper:RemoveStageBriefing(stageName)
|
||||
self._stageBriefings[stageName] = nil
|
||||
end
|
||||
|
||||
---@param mission Mission
|
||||
function MissionCommandsHelper:AddMissionToCommands(mission)
|
||||
self._logger:debug("Adding mission to commands: [" .. mission.code .. "]" .. mission.name)
|
||||
self.missionsByCode[tostring(mission.code)] = mission
|
||||
self.enabledByCode[tostring(mission.code)] = true
|
||||
self.updateNeeded = true
|
||||
end
|
||||
|
||||
---Removes a mission from the F10 commands menu
|
||||
---@param mission Mission
|
||||
function MissionCommandsHelper:RemoveMissionToCommands(mission)
|
||||
self.enabledByCode[tostring(mission.code)] = false
|
||||
self.updateNeeded = true
|
||||
end
|
||||
|
||||
---@param groupID number
|
||||
function MissionCommandsHelper:MarkUnitInSupplyHub(groupID)
|
||||
self._logger:debug("Marking unit in supply hub: " .. tostring(groupID))
|
||||
local updateNeeded = false
|
||||
if self._supplyHubGroups[tostring(groupID)] ~= true then
|
||||
updateNeeded = true
|
||||
end
|
||||
|
||||
self._supplyHubGroups[tostring(groupID)] = true
|
||||
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
|
||||
end
|
||||
|
||||
|
||||
---@param groupID number
|
||||
function MissionCommandsHelper:MarkUnitOutsideSupplyHub(groupID)
|
||||
self._logger:debug("Marking unit outide supply hub: " .. tostring(groupID))
|
||||
local updateNeeded = false
|
||||
if self._supplyHubGroups[tostring(groupID)] == true then
|
||||
updateNeeded = true
|
||||
end
|
||||
|
||||
self._supplyHubGroups[tostring(groupID)] = false
|
||||
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
|
||||
end
|
||||
|
||||
|
||||
|
||||
---@param unit Unit
|
||||
function MissionCommandsHelper:OnPlayerEntersUnit(unit)
|
||||
if unit then
|
||||
local group = unit:getGroup()
|
||||
if group then self:updateCommandsForGroup(group:getID()) end
|
||||
end
|
||||
end
|
||||
|
||||
---@class MissionBriefingRequestedArgs
|
||||
---@field mission Mission @the mission object
|
||||
---@field groupId integer @the group ID of the player requesting the briefing
|
||||
|
||||
---comment
|
||||
---@param args MissionBriefingRequestedArgs
|
||||
local missionBriefingRequested = function(args)
|
||||
---@type Mission
|
||||
local mission = args.mission
|
||||
local groupID = args.groupId
|
||||
|
||||
mission:ShowBriefing(groupID)
|
||||
end
|
||||
|
||||
---@class PinMissionCommandArgs
|
||||
---@field self MissionCommandsHelper @the MissionCommandsHelper instance
|
||||
---@field groupId integer @the group ID of the player requesting the briefing
|
||||
---@field mission Mission @the mission object
|
||||
|
||||
---@param args PinMissionCommandArgs
|
||||
local pinMissionCommand = function(args)
|
||||
---@type MissionCommandsHelper
|
||||
local self = args.self
|
||||
local groupID = args.groupId
|
||||
local mission = args.mission
|
||||
|
||||
if mission then
|
||||
self:PinMission(mission, groupID)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function MissionCommandsHelper:AddOverviewCommand(groupID)
|
||||
|
||||
local MissionsOverviewToGroup = function (id)
|
||||
|
||||
local text = "Missions Overview\n\n"
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(id)
|
||||
---@type Vec2
|
||||
local groupPos = { x=0, y=0 }
|
||||
if group then
|
||||
local pos = group:getUnit(1):getPosition().p
|
||||
groupPos = { x= pos.x, y=pos.z }
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param mission Mission
|
||||
---@return string
|
||||
local function formatLine(mission)
|
||||
|
||||
local distanceText = "?"
|
||||
if group then
|
||||
local lead = group:getUnit(1)
|
||||
if lead and lead:isExist() == true then
|
||||
local pos = lead:getPoint()
|
||||
local Vec2Pos = { x= pos.x, y=pos.z }
|
||||
local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
|
||||
distanceText = string.format("~%d", math.floor(distance))
|
||||
end
|
||||
end
|
||||
|
||||
return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name, mission:PercentageComplete(), distanceText)
|
||||
end
|
||||
|
||||
for _, briefing in pairs(self._stageBriefings) do
|
||||
text = text .. briefing .. "\n\n"
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
---Primary missions
|
||||
text = text .. "Primary Missions\n"
|
||||
|
||||
---@type Array<Mission>
|
||||
local primaryMissions = {}
|
||||
for code, enabled in pairs(self.enabledByCode) do
|
||||
if enabled == true then
|
||||
local mission = self.missionsByCode[code]
|
||||
if mission and mission:getState() == "ACTIVE" and mission.priority == "primary" then
|
||||
table.insert(primaryMissions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sortMissions(primaryMissions, groupPos)
|
||||
|
||||
for _, mission in pairs(primaryMissions) do
|
||||
text = text .. formatLine(mission)
|
||||
end
|
||||
|
||||
---Secondary missions
|
||||
text = text .. "\nSecondary Missions\n"
|
||||
|
||||
---@type Array<Mission>
|
||||
local secondaryMissions = {}
|
||||
for code, enabled in pairs(self.enabledByCode) do
|
||||
|
||||
if enabled == true then
|
||||
local mission = self.missionsByCode[code]
|
||||
if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then
|
||||
table.insert(secondaryMissions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sortMissions(secondaryMissions, groupPos)
|
||||
for _, mission in pairs(secondaryMissions) do
|
||||
text = text .. formatLine(mission)
|
||||
end
|
||||
|
||||
|
||||
trigger.action.outTextForGroup(id, text, 20, true)
|
||||
end
|
||||
|
||||
missionCommands.removeItemForGroup(groupID, { "Overview" } )
|
||||
missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionsOverviewToGroup, groupID)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param groupID number
|
||||
function MissionCommandsHelper:AddPinnedMission(groupID)
|
||||
|
||||
local pinndedMission = self.pinnedByGroup[tostring(groupID)]
|
||||
missionCommands.removeItemForGroup(groupID, { "Pinned Mission" })
|
||||
|
||||
if pinndedMission and self.enabledByCode[tostring(pinndedMission.code)] == true then
|
||||
missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested, { groupId = groupID, mission = pinndedMission })
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---@param groupID number
|
||||
function MissionCommandsHelper:updateCommandsForGroup(groupID)
|
||||
|
||||
self._logger:debug("Updating commands for group: " .. tostring(groupID))
|
||||
|
||||
self:AddPinnedMission(groupID)
|
||||
self:AddOverviewCommand(groupID)
|
||||
|
||||
self:ResetFolders(groupID)
|
||||
|
||||
self:AddAllMissionCommandsToGroup(groupID)
|
||||
|
||||
self:AddSupplyHubCommandsIfApplicable(groupID)
|
||||
self:AddCargoCommands(groupID)
|
||||
|
||||
---@param id number
|
||||
local clearView = function(id)
|
||||
trigger.action.outTextForGroup(id, "clearing...", 1, true)
|
||||
end
|
||||
|
||||
missionCommands.removeItemForGroup(groupID, { "Clear View" } )
|
||||
missionCommands.addCommandForGroup(groupID, "Clear View", nil, clearView, groupID)
|
||||
|
||||
missionCommands.removeItemForGroup(groupID, { "Refresh Missions" } )
|
||||
missionCommands.addCommandForGroup(groupID, "Refresh Missions", nil, function(refresh_mission_id)
|
||||
self._logger:debug("Manual refresh of missions for group: " .. tostring(refresh_mission_id))
|
||||
self:updateCommandsForGroup(refresh_mission_id)
|
||||
end, groupID)
|
||||
|
||||
end
|
||||
|
||||
local folderNames = {
|
||||
primary = "Primary Missions",
|
||||
secondary = "Secondary Missions",
|
||||
supplyHub = "Supply Hub",
|
||||
cargo = "Cargo"
|
||||
}
|
||||
|
||||
|
||||
---@param mission Mission
|
||||
---@param groupID integer
|
||||
function MissionCommandsHelper:PinMission(mission, groupID)
|
||||
self._logger:debug("Pinning mission: [" .. mission.code .. "]" .. mission.name)
|
||||
self.pinnedByGroup[tostring(groupID)] = mission
|
||||
trigger.action.outTextForGroup(groupID, "Pinned mission: [" .. mission.code .. "]" .. mission.name, 3, true)
|
||||
|
||||
self:updateCommandsForGroup(groupID)
|
||||
mission:ShowBriefing(groupID)
|
||||
end
|
||||
|
||||
function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
|
||||
|
||||
local perFolder = 9
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
---@type Vec2
|
||||
local groupPos = { x=0, y=0 }
|
||||
if group then
|
||||
local pos = group:getUnit(1):getPosition().p
|
||||
groupPos = { x= pos.x, y=pos.z }
|
||||
end
|
||||
|
||||
do --- primary missions
|
||||
local count = 0
|
||||
local path = { [1] = folderNames.primary }
|
||||
|
||||
---@type Array<Mission>
|
||||
local primaryMissions = {}
|
||||
|
||||
for code, enabled in pairs(self.enabledByCode) do
|
||||
if enabled == true then
|
||||
local mission = self.missionsByCode[code]
|
||||
if mission and mission.priority == "primary" then
|
||||
table.insert(primaryMissions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sortMissions(primaryMissions, groupPos)
|
||||
for _, mission in pairs(primaryMissions) do
|
||||
count = count + 1
|
||||
if count <= perFolder then
|
||||
local copied = Util.deepCopyTable(path)
|
||||
self:addMissionCommands(groupID, copied, mission)
|
||||
else
|
||||
local name = "Next Menu ..."
|
||||
missionCommands.addSubMenuForGroup(groupID, name, path)
|
||||
path[#path+1] = name
|
||||
count = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
do --- secondary missions
|
||||
local count = 0
|
||||
local path = { [1] = folderNames.secondary }
|
||||
|
||||
local secondaryMissions = {}
|
||||
for code, enabled in pairs(self.enabledByCode) do
|
||||
if enabled == true then
|
||||
local mission = self.missionsByCode[code]
|
||||
if mission and mission.priority == "secondary" then
|
||||
table.insert(secondaryMissions, mission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sortMissions(secondaryMissions, groupPos)
|
||||
for _, mission in pairs(secondaryMissions) do
|
||||
count = count + 1
|
||||
if count <= perFolder then
|
||||
local copied = Util.deepCopyTable(path)
|
||||
self:addMissionCommands(groupID, copied, mission)
|
||||
else
|
||||
local name = "Next Menu ..."
|
||||
missionCommands.addSubMenuForGroup(groupID, name, path)
|
||||
path[#path+1] = name
|
||||
count = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---comment
|
||||
---@private
|
||||
---@param groupId integer
|
||||
---@param path Array<string>
|
||||
---@param mission Mission
|
||||
function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
|
||||
|
||||
if path then
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupId)
|
||||
local distance = "[?]"
|
||||
if group then
|
||||
local lead = group:getUnit(1)
|
||||
if lead and lead:isExist() == true then
|
||||
local pos = lead:getPoint()
|
||||
local Vec2Pos = { x= pos.x, y=pos.z }
|
||||
local dist = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
|
||||
distance = "[" .. string.format("~%dnM", math.floor(dist)) .. "]"
|
||||
end
|
||||
end
|
||||
|
||||
local missionFolderName = "[" .. mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )"
|
||||
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
|
||||
table.insert(path, missionFolderName)
|
||||
|
||||
---@type MissionBriefingRequestedArgs
|
||||
local missionBriefingRequestedArgs = { groupId = groupId, mission = mission }
|
||||
missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,missionBriefingRequestedArgs)
|
||||
|
||||
---@type PinMissionCommandArgs
|
||||
local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission }
|
||||
missionCommands.addCommandForGroup(groupId, "Pin", path, pinMissionCommand, pinMissionCommandArgs)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param groupID integer
|
||||
function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
|
||||
|
||||
if self._supplyHubGroups[tostring(groupID)] ~= true then return end
|
||||
|
||||
self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID))
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
if group == nil then return end
|
||||
|
||||
local unit = group:getUnit(1)
|
||||
if unit == nil then return end
|
||||
|
||||
---@class LoadCargoCommandParams
|
||||
---@field unitID number
|
||||
---@field groupID number
|
||||
---@field crateType CrateType
|
||||
---@field supplyUnitsTracker SupplyUnitsTracker
|
||||
---@field commandHelper MissionCommandsHelper
|
||||
|
||||
---comment
|
||||
---@param params LoadCargoCommandParams
|
||||
local loadCargoCommand = function(params)
|
||||
local crateType = params.crateType
|
||||
local supplyUnitsTracker = params.supplyUnitsTracker
|
||||
if supplyUnitsTracker then
|
||||
supplyUnitsTracker:UnitRequestCrateLoading(params.groupID, crateType, params.commandHelper)
|
||||
end
|
||||
end
|
||||
|
||||
local path = { [1] = folderNames.supplyHub }
|
||||
|
||||
---@type LoadCargoCommandParams
|
||||
local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000)
|
||||
|
||||
---@type LoadCargoCommandParams
|
||||
local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
|
||||
|
||||
---@type LoadCargoCommandParams
|
||||
local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
|
||||
|
||||
---@type LoadCargoCommandParams
|
||||
local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
|
||||
|
||||
---@type LoadCargoCommandParams
|
||||
local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, airbaseParms2000)
|
||||
end
|
||||
|
||||
function MissionCommandsHelper:AddCargoCommands(groupID)
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
if group == nil then return end
|
||||
|
||||
local unit = group:getUnit(1)
|
||||
if unit == nil then return end
|
||||
|
||||
---@class UnloadCargoCommandParams
|
||||
---@field unitID number
|
||||
---@field crateType CrateType
|
||||
---@field supplyUnitsTracker SupplyUnitsTracker
|
||||
---@field commandHelper MissionCommandsHelper
|
||||
|
||||
---comment
|
||||
---@param params UnloadCargoCommandParams
|
||||
local unloadCargoCommand = function(params)
|
||||
local unitID = params.unitID
|
||||
local crateType = params.crateType
|
||||
local supplyUnitsTracker = params.supplyUnitsTracker
|
||||
params.supplyUnitsTracker:UnloadRequested(unitID, crateType, params.commandHelper)
|
||||
end
|
||||
|
||||
local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID())
|
||||
if cargo then
|
||||
for cargoType, amount in pairs(cargo) do
|
||||
local cargoConfig = SupplyConfigHelper.getSupplyConfig(cargoType)
|
||||
if cargoConfig then
|
||||
for i = 1, amount do
|
||||
local path = { [1] = folderNames.cargo }
|
||||
---@type UnloadCargoCommandParams
|
||||
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
|
||||
missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
---@private
|
||||
---@param groupId integer
|
||||
function MissionCommandsHelper:addMissionFolders(groupId)
|
||||
|
||||
missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
|
||||
missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
|
||||
|
||||
if self._supplyHubGroups[tostring(groupId)] == true then
|
||||
self._logger:debug("Adding supply hub commands folder for group: " .. tostring(groupId))
|
||||
missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub)
|
||||
end
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupId)
|
||||
if group == nil then return end
|
||||
|
||||
local unit = group:getUnit(1)
|
||||
if unit == nil then return end
|
||||
|
||||
local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID())
|
||||
if cargo ~= nil then
|
||||
missionCommands.addSubMenuForGroup(groupId, folderNames.cargo)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param groupId integer
|
||||
function MissionCommandsHelper:removeMissionFolders(groupId)
|
||||
missionCommands.removeItemForGroup(groupId, { folderNames.primary })
|
||||
missionCommands.removeItemForGroup(groupId, { folderNames.secondary })
|
||||
missionCommands.removeItemForGroup(groupId, { folderNames.supplyHub })
|
||||
missionCommands.removeItemForGroup(groupId, { folderNames.cargo })
|
||||
end
|
||||
|
||||
---@private
|
||||
function MissionCommandsHelper:ResetFolders(groupID)
|
||||
-- Cleanup mission folder
|
||||
self:removeMissionFolders(groupID)
|
||||
|
||||
-- Add mission folders
|
||||
self:addMissionFolders(groupID)
|
||||
end
|
||||
|
||||
return MissionCommandsHelper
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
local Util = require("classes.util.Util")
|
||||
|
||||
---@alias SupplyType
|
||||
---| "FARP_CRATE"
|
||||
---| "SAM_CRATE"
|
||||
---| "AIRBASE_CRATE"
|
||||
|
||||
---@alias CrateType
|
||||
---| "FARP_CRATE_500"
|
||||
---| "FARP_CRATE_1000"
|
||||
---| "FARP_CRATE_2000"
|
||||
---| "SAM_CRATE_500"
|
||||
---| "SAM_CRATE_1000"
|
||||
---| "SAM_CRATE_2000"
|
||||
---| "AIRBASE_CRATE_2000"
|
||||
|
||||
---@class SupplyConfig
|
||||
---@field type SupplyType
|
||||
---@field weight number
|
||||
---@field staticType string
|
||||
---@field displayName string
|
||||
|
||||
---@type table<CrateType, SupplyConfig>
|
||||
local SupplyConfig = {
|
||||
["FARP_CRATE_500"] = {
|
||||
type = "FARP_CRATE",
|
||||
weight = 500,
|
||||
displayName = "FARP Crate (500)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["FARP_CRATE_1000"] = {
|
||||
type = "FARP_CRATE",
|
||||
weight = 1000,
|
||||
displayName = "FARP Crate (1000)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["FARP_CRATE_2000"] = {
|
||||
type = "FARP_CRATE",
|
||||
weight = 2000,
|
||||
displayName = "FARP Crate (2000)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["SAM_CRATE_500"] = {
|
||||
type = "SAM_CRATE",
|
||||
weight = 1000,
|
||||
displayName = "SAM Crate (500)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["SAM_CRATE_1000"] = {
|
||||
type = "SAM_CRATE",
|
||||
weight = 1000,
|
||||
displayName = "SAM Crate (1000)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["SAM_CRATE_2000"] = {
|
||||
type = "SAM_CRATE",
|
||||
weight = 2000,
|
||||
displayName = "SAM Crate (2000)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
["AIRBASE_CRATE_2000"] = {
|
||||
type = "AIRBASE_CRATE",
|
||||
weight = 2000,
|
||||
displayName = "Airbase Crate (2000)",
|
||||
staticType = "container_cargo",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
---@class SupplyConfigHelper
|
||||
local SupplyConfigHelper = {}
|
||||
|
||||
---comment
|
||||
---@param name string
|
||||
---@return SupplyConfig?
|
||||
function SupplyConfigHelper.fromObjectName(name)
|
||||
for configName, config in pairs(SupplyConfig) do
|
||||
if Util.startswith(name, configName, true) == true then
|
||||
return config
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param type CrateType
|
||||
function SupplyConfigHelper.getSupplyConfig(type)
|
||||
return SupplyConfig[type]
|
||||
end
|
||||
|
||||
|
||||
return SupplyConfigHelper
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
local Logger = require("classes.util.Logger")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SpearheadEvents = require("classes.spearhead_events")
|
||||
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
|
||||
local MaxLoadConfig = require("classes.stageClasses.helpers.MaxLoadConfig")
|
||||
|
||||
---@class SupplyUnitEventListener
|
||||
---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil
|
||||
---@field enteredSupplyHub fun(self:SupplyUnitEventListener, unit:Unit, hub:SupplyHub) | nil
|
||||
---@field exitedSupplyHub fun(self:SupplyUnitEventListener, unit:Unit, hub:SupplyHub) | nil
|
||||
|
||||
---@class SupplyUnitsTracker
|
||||
---@field private _supplyUnitsByName table<string, Unit>
|
||||
---@field private _cargoInUnits table<string, table<CrateType, number>>
|
||||
---@field private _logger Logger
|
||||
---@field private _unitPositions table<string, Vec3>
|
||||
---@field private _unitInSupplyHub table<string, boolean>
|
||||
---@field private _droppedCrates table<string, StaticObject>
|
||||
---@field private _registeredHubs table<SupplyHub, boolean>
|
||||
---@field private _supplyUnitEventsListeners Array<SupplyUnitEventListener>
|
||||
local SupplyUnitsTracker = {}
|
||||
SupplyUnitsTracker.__index = SupplyUnitsTracker
|
||||
|
||||
-- A single class tracking all units is more than enough.
|
||||
local singleton = nil
|
||||
|
||||
---comment
|
||||
---@param logLevel LogLevel
|
||||
---@return SupplyUnitsTracker
|
||||
function SupplyUnitsTracker.getOrCreate(logLevel)
|
||||
|
||||
if singleton == nil then
|
||||
singleton = setmetatable({}, SupplyUnitsTracker)
|
||||
singleton._logger = Logger.new("SupplyUnitsTracker", logLevel)
|
||||
singleton._unitPositions = {}
|
||||
singleton._cargoInUnits = {}
|
||||
singleton._supplyUnitsByName = {}
|
||||
singleton._droppedCrates = {}
|
||||
singleton._registeredHubs = {}
|
||||
singleton._supplyUnitEventsListeners = {}
|
||||
singleton._unitInSupplyHub = {}
|
||||
|
||||
SpearheadEvents.AddOnPlayerEnterUnitListener(singleton)
|
||||
|
||||
---@param selfA SupplyUnitsTracker
|
||||
local function updateTask(selfA, time)
|
||||
|
||||
selfA:Update()
|
||||
return time + 15
|
||||
end
|
||||
|
||||
timer.scheduleFunction(updateTask, singleton, timer.getTime() + 15)
|
||||
|
||||
---comment
|
||||
---@param selfA SupplyUnitsTracker
|
||||
---@param time number
|
||||
---@return number
|
||||
local function checkUnitsInZone(selfA, time)
|
||||
pcall(function()
|
||||
selfA:CheckUnitsInZones()
|
||||
end)
|
||||
return time + 5
|
||||
end
|
||||
|
||||
timer.scheduleFunction(checkUnitsInZone, singleton, timer.getTime() + 5)
|
||||
|
||||
end
|
||||
|
||||
return singleton
|
||||
|
||||
end
|
||||
|
||||
---@param unit Unit
|
||||
function SupplyUnitsTracker:OnPlayerEntersUnit(unit)
|
||||
if unit == nil then return end
|
||||
|
||||
if self:IsSupplyUnit(unit) == true then
|
||||
self._supplyUnitsByName[unit:getName()] = unit
|
||||
self._cargoInUnits[tostring(unit:getID())] = nil
|
||||
self._unitInSupplyHub[tostring(unit:getID())] = false
|
||||
self._unitPositions[tostring(unit:getID())] = unit:getPoint()
|
||||
end
|
||||
|
||||
for _, listener in pairs(self._supplyUnitEventsListeners) do
|
||||
pcall(function()
|
||||
if listener.supplyUnitSpawned then
|
||||
listener:supplyUnitSpawned(unit)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
---@param listener SupplyUnitEventListener
|
||||
function SupplyUnitsTracker:AddOnSupplyUnitSpawnedListener(listener)
|
||||
if listener == nil then return end
|
||||
|
||||
if self._supplyUnitEventsListeners == nil then
|
||||
self._supplyUnitEventsListeners = {}
|
||||
end
|
||||
|
||||
table.insert(self._supplyUnitEventsListeners, listener)
|
||||
end
|
||||
|
||||
function SupplyUnitsTracker:Update()
|
||||
local players = DcsUtil.getAllPlayerUnits()
|
||||
for _, player in pairs(players) do
|
||||
if player ~= nil and player:isExist() and self:IsSupplyUnit(player) == true then
|
||||
self._supplyUnitsByName[player:getName()] = player
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unit Unit
|
||||
function SupplyUnitsTracker:IsSupplyUnit(unit)
|
||||
if unit == nil then return false end
|
||||
|
||||
if unit:hasAttribute("Transport helicopters") then
|
||||
return true
|
||||
end
|
||||
|
||||
if unit:hasAttribute("Helicopters") and unit:hasAttribute("Transports") then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param unitID number
|
||||
---@param crateType CrateType
|
||||
function SupplyUnitsTracker:AddCargoToUnit(unitID, crateType)
|
||||
|
||||
if unitID == nil or crateType == nil then return end
|
||||
|
||||
local unit = DcsUtil.GetPlayerUnitByID(unitID)
|
||||
if unit == nil then return end
|
||||
|
||||
if self._cargoInUnits[unitID] == nil then
|
||||
self._cargoInUnits[unitID] = {}
|
||||
end
|
||||
|
||||
if self._cargoInUnits[unitID][crateType] == nil then
|
||||
self._cargoInUnits[unitID][crateType] = 0
|
||||
end
|
||||
|
||||
self._cargoInUnits[unitID][crateType] = self._cargoInUnits[unitID][crateType] + 1
|
||||
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unitID number
|
||||
---@param crateType CrateType
|
||||
function SupplyUnitsTracker:RemoveCargoFromUnit(unitID, crateType)
|
||||
|
||||
if unitID == nil or crateType == nil then return end
|
||||
|
||||
local unitIDStr = tostring(unitID)
|
||||
if self._cargoInUnits[unitIDStr] == nil then return end
|
||||
|
||||
if self._cargoInUnits[unitIDStr][crateType] == nil then return end
|
||||
|
||||
self._cargoInUnits[unitIDStr][crateType] = self._cargoInUnits[unitIDStr][crateType] - 1
|
||||
|
||||
local hasCargo = false
|
||||
for type, count in pairs(self._cargoInUnits[unitIDStr]) do
|
||||
if count > 0 then
|
||||
hasCargo = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if hasCargo == false then
|
||||
self._cargoInUnits[unitIDStr] = nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unit Unit
|
||||
function SupplyUnitsTracker:UpdateWeightForUnit(unit)
|
||||
|
||||
local weight = 0
|
||||
if self._cargoInUnits[tostring(unit:getID())] then
|
||||
for crateType, count in pairs(self._cargoInUnits[tostring(unit:getID())]) do
|
||||
local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
if crateConfig and count then
|
||||
weight = weight + (crateConfig.weight * count)
|
||||
end
|
||||
end
|
||||
end
|
||||
trigger.action.setUnitInternalCargo(unit:getName(), weight)
|
||||
end
|
||||
|
||||
|
||||
function SupplyUnitsTracker:CheckUnitsInZones()
|
||||
|
||||
for name, unit in pairs(self._supplyUnitsByName) do
|
||||
if unit ~= nil and unit:isExist() == true then
|
||||
self._logger:debug("Checking unit: " .. unit:getName())
|
||||
local pos = unit:getPoint()
|
||||
|
||||
local group = unit:getGroup()
|
||||
|
||||
for hub, enabled in pairs(self._registeredHubs) do
|
||||
if enabled == true then
|
||||
local zone = hub:GetZone()
|
||||
if zone ~= nil then
|
||||
if Util.is3dPointInZone(pos, zone) then
|
||||
if self._unitInSupplyHub[tostring(unit:getID())] ~= true then
|
||||
self._unitInSupplyHub[tostring(unit:getID())] = true
|
||||
|
||||
for _, listener in pairs(self._supplyUnitEventsListeners) do
|
||||
pcall(function()
|
||||
if listener.enteredSupplyHub then
|
||||
listener:enteredSupplyHub(unit, hub)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
if self._unitInSupplyHub[tostring(unit:getID())] == true then
|
||||
self._unitInSupplyHub[tostring(unit:getID())] = false
|
||||
for _, listener in pairs(self._supplyUnitEventsListeners) do
|
||||
pcall(function()
|
||||
if listener.exitedSupplyHub then
|
||||
listener:exitedSupplyHub(unit, hub)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self._unitPositions[unit:getID()] = pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SupplyUnitsTracker:RegisterHub(hub)
|
||||
if hub == nil then return end
|
||||
|
||||
if self._registeredHubs[hub] == nil then
|
||||
self._registeredHubs[hub] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@param unitID number
|
||||
---@return table<CrateType, number>?
|
||||
function SupplyUnitsTracker:GetCargoInUnit(unitID)
|
||||
if unitID == nil then return end
|
||||
|
||||
local unitIDStr = tostring(unitID)
|
||||
if self._cargoInUnits[unitIDStr] == nil then return end
|
||||
|
||||
return self._cargoInUnits[unitIDStr]
|
||||
end
|
||||
|
||||
---@return table<string, Unit>
|
||||
function SupplyUnitsTracker:GetUnits()
|
||||
return self._supplyUnitsByName
|
||||
end
|
||||
|
||||
local cargoCount = 0
|
||||
|
||||
---comment
|
||||
---@param unitID number
|
||||
---@param crateType CrateType
|
||||
---@param missionCommandsHelper MissionCommandsHelper
|
||||
function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHelper)
|
||||
|
||||
self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
|
||||
|
||||
local unit = DcsUtil.GetPlayerUnitByID(unitID)
|
||||
if unit == nil or unit:isExist() == false then return end
|
||||
local group = unit:getGroup()
|
||||
if group == nil then
|
||||
return
|
||||
end
|
||||
|
||||
self:RemoveCargoFromUnit(unitID, crateType)
|
||||
self:UpdateWeightForUnit(unit)
|
||||
|
||||
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
|
||||
if cargoConfig == nil then
|
||||
self._logger:error("Invalid crate type: " .. crateType)
|
||||
return
|
||||
end
|
||||
|
||||
local cargoPos = self:GetCargoPlacePosition(unit)
|
||||
|
||||
cargoCount = cargoCount + 1
|
||||
local cargoSpawnObject = {
|
||||
name = crateType .. "_" .. cargoCount,
|
||||
type = cargoConfig.staticType,
|
||||
x = cargoPos.x,
|
||||
y = cargoPos.z,
|
||||
}
|
||||
|
||||
local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject)
|
||||
self._droppedCrates[cargoSpawnObject.name] = spawned
|
||||
missionCommandsHelper:updateCommandsForGroup(group:getID())
|
||||
end
|
||||
|
||||
---@return table<string,StaticObject>
|
||||
function SupplyUnitsTracker:GetCargoCratesDropped()
|
||||
return self._droppedCrates
|
||||
end
|
||||
|
||||
---Loads a crate directly into the unit
|
||||
---@param groupID number
|
||||
---@param crateType CrateType
|
||||
---@param missionCommandsHelper MissionCommandsHelper
|
||||
function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionCommandsHelper)
|
||||
|
||||
self._logger:debug("UnitRequestCrateLoading called with groupID: " .. groupID .. " and crateType: " .. crateType)
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
if group ~= nil then
|
||||
|
||||
local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
if crateConfig == nil then
|
||||
self._logger:error("Invalid crate type: " .. crateType)
|
||||
return
|
||||
end
|
||||
|
||||
local unit = group:getUnit(1)
|
||||
|
||||
if unit == nil then return end
|
||||
if unit:isExist() == false then return end
|
||||
|
||||
|
||||
if unit:inAir() == true then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Land first before crates can be loaded", 10)
|
||||
return
|
||||
end
|
||||
|
||||
trigger.action.outTextForUnit(unit:getID(), "Loading crate of type " .. crateType, 13)
|
||||
|
||||
---@class LoadCargoParams
|
||||
---@field self SupplyUnitsTracker
|
||||
---@field unit Unit
|
||||
---@field groupID number
|
||||
---@field crateType CrateType
|
||||
---@field commandHelper MissionCommandsHelper
|
||||
|
||||
---@param params LoadCargoParams
|
||||
local LoadCrateTask = function(params)
|
||||
|
||||
local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType, params.commandHelper)
|
||||
if loaded ~= false then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Loaded crate :" .. params.crateType, 10)
|
||||
end
|
||||
end
|
||||
|
||||
---@type LoadCargoParams
|
||||
local params = {
|
||||
self = self,
|
||||
unit = unit,
|
||||
crateType = crateType,
|
||||
groupID = groupID,
|
||||
commandHelper = missionCommandsHelper
|
||||
}
|
||||
|
||||
timer.scheduleFunction(LoadCrateTask, params, timer.getTime() + 15)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param unit Unit
|
||||
---@param crateType CrateType
|
||||
---@param commandHelper MissionCommandsHelper
|
||||
---@return boolean
|
||||
function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
|
||||
|
||||
local crateConfigA = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
if crateConfigA == nil then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Invalid crate type: " .. crateType, 5)
|
||||
return false
|
||||
end
|
||||
|
||||
local currentWeight = 0
|
||||
for _, cargo in pairs(self._cargoInUnits) do
|
||||
if cargo[crateType] ~= nil then
|
||||
currentWeight = currentWeight + (cargo[crateType] * crateConfigA.weight)
|
||||
end
|
||||
end
|
||||
|
||||
local unitConfig = MaxLoadConfig[unit:getTypeName()]
|
||||
if unitConfig == nil then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5)
|
||||
self._logger:error("Invalid unit type: " .. unit:getTypeName())
|
||||
return false
|
||||
end
|
||||
local maxWeight = unitConfig.maxInternalLoad
|
||||
|
||||
if currentWeight + crateConfigA.weight > maxWeight then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Failed to load crate due to it overloading your max weight of: " .. maxWeight .. "kg", 5)
|
||||
return false
|
||||
end
|
||||
|
||||
self:AddCargoToUnit(unit:getID(), crateType)
|
||||
self:UpdateWeightForUnit(unit)
|
||||
|
||||
local group = unit:getGroup()
|
||||
if group == nil then return false end
|
||||
local groupID = group:getID()
|
||||
commandHelper:updateCommandsForGroup(groupID)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Spawns a crate for sling loading
|
||||
---@param groupID number
|
||||
---@param crateType CrateType
|
||||
function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
if group == nil then
|
||||
|
||||
local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
if crateConfig == nil then
|
||||
self._logger:error("Invalid crate type: " .. crateType)
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@private
|
||||
---@param unit Unit
|
||||
---@return Vec3
|
||||
function SupplyUnitsTracker:GetCargoPlacePosition(unit)
|
||||
|
||||
local pos = unit:getPosition()
|
||||
local preferredPos = {
|
||||
x = pos.p.x - 10 * pos.x.x,
|
||||
y = pos.p.y - 10 * pos.x.y,
|
||||
z = pos.p.z - 10 * pos.x.z
|
||||
}
|
||||
|
||||
return preferredPos
|
||||
|
||||
|
||||
-- local volume = {
|
||||
-- id = world.VolumeType.SPHERE,
|
||||
-- params = {
|
||||
-- point = preferredPos,
|
||||
-- radius = 10
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- local occupiedPosX = {}
|
||||
-- local occupiedPosZ = {}
|
||||
|
||||
-- ---@param foundItem Object
|
||||
-- local found = function(foundItem, val)
|
||||
|
||||
-- local foundPos = foundItem:getPoint()
|
||||
|
||||
-- local z = math.floor(foundPos.z)
|
||||
-- for i = z - 3 , z + 3 do
|
||||
-- occupiedPosZ[i] = true
|
||||
-- end
|
||||
|
||||
-- local x = math.floor(foundPos.x)
|
||||
-- for i = x - 3 , x + 3 do
|
||||
-- occupiedPosX[i] = true
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- world.searchObjects(volume.id, volume.params, found)
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
return SupplyUnitsTracker
|
||||
@@ -0,0 +1,275 @@
|
||||
local Mission = require("classes.stageClasses.missions.baseMissions.Mission")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
|
||||
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
|
||||
local GlobalConfig = require("classes.configuration.GlobalConfig")
|
||||
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
|
||||
|
||||
---@class BuildableMission : Mission, SupplyUnitEventListener
|
||||
---@field private _requiredKilos number
|
||||
---@field private _droppedKilos number
|
||||
---@field private _crateType SupplyType
|
||||
---@field private _targetZone SpearheadTriggerZone
|
||||
---@field private _database Database
|
||||
---@field private _onCrateDroppedOfListeners Array<OnCrateDroppedListener>
|
||||
---@field private _markIDsPerGroup table<string, number>
|
||||
---@field private _supplyUnitsTracker SupplyUnitsTracker
|
||||
---@field private _noLandingZone SpearheadTriggerZone?
|
||||
---@field private _dropOffZone SpearheadTriggerZone?
|
||||
---@field private _noLandingZoneId number
|
||||
---@field private _dropOffZoneId number
|
||||
local BuildableMission = {}
|
||||
BuildableMission.__index = BuildableMission
|
||||
|
||||
---@class OnCrateDroppedListener
|
||||
---@field OnCrateDroppedOff fun(self:OnCrateDroppedListener, mission:BuildableMission, kilos:number)
|
||||
|
||||
---@param database Database
|
||||
---@param targetZone SpearheadTriggerZone
|
||||
---@param requiredKilos number
|
||||
---@param requiredCrateType SupplyType
|
||||
---@param noLandingZone SpearheadTriggerZone?
|
||||
---@param logger Logger
|
||||
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType)
|
||||
|
||||
setmetatable(BuildableMission, Mission)
|
||||
|
||||
local self = setmetatable({}, { __index = BuildableMission })
|
||||
|
||||
self._targetZone = targetZone
|
||||
self._database = database
|
||||
self._requiredKilos = requiredKilos
|
||||
self._droppedKilos = 0
|
||||
|
||||
self._noLandingZone = noLandingZone
|
||||
|
||||
if noLandingZone then
|
||||
|
||||
local verts = noLandingZone.verts
|
||||
local enlarged = Util.enlargeConvexHull(verts, 300)
|
||||
|
||||
---@type SpearheadTriggerZone
|
||||
local dropOfZone = {
|
||||
name = targetZone.name .. "_dropZone",
|
||||
zone_type = "Polygon",
|
||||
radius = 0,
|
||||
verts = enlarged,
|
||||
location = noLandingZone.location,
|
||||
}
|
||||
|
||||
self._dropOffZone = dropOfZone
|
||||
end
|
||||
|
||||
self.code = tostring(database:GetNewMissionCode())
|
||||
self.name = "Resupply"
|
||||
|
||||
local type = "site"
|
||||
if requiredCrateType == "SAM_CRATE" then
|
||||
type = "SAM site"
|
||||
elseif requiredCrateType == "FARP_CRATE" then
|
||||
type = "FARP"
|
||||
end
|
||||
|
||||
self.zoneName = targetZone.name .. "_supply"
|
||||
self._logger = logger
|
||||
self._onCrateDroppedOfListeners = {}
|
||||
self._completeListeners = {}
|
||||
self._markIDsPerGroup = {}
|
||||
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel)
|
||||
self._state = "NEW"
|
||||
|
||||
|
||||
self.location = targetZone.location
|
||||
|
||||
self.missionType = "LOGISTICS"
|
||||
self.missionTypeDisplay = "LOGISTICS"
|
||||
|
||||
self.priority = "secondary"
|
||||
|
||||
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(self._logger.LogLevel)
|
||||
|
||||
self._crateType = requiredCrateType
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@param listener OnCrateDroppedListener
|
||||
function BuildableMission:AddOnCrateDroppedOfListener(listener)
|
||||
table.insert(self._onCrateDroppedOfListeners, listener)
|
||||
end
|
||||
|
||||
function BuildableMission:ShowBriefing(groupID)
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
|
||||
if group == nil then return end
|
||||
|
||||
local unitType = DcsUtil.getUnitTypeFromGroup(group)
|
||||
local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType)
|
||||
|
||||
local siteType = "FARP"
|
||||
if self._crateType == "SAM_CRATE" then
|
||||
siteType = "SAM site"
|
||||
elseif self._crateType == "AIRBASE_CRATE" then
|
||||
siteType = "airbase"
|
||||
end
|
||||
|
||||
local briefing = "Mission [" .. self.code .. "] " .. self.name ..
|
||||
"\n \n" ..
|
||||
"We've dispatched forward units to find a proper spot for a new " .. siteType .. "." ..
|
||||
"\nYou will need to drop off supplies so they can start building." ..
|
||||
"\nThe coords are: " .. coords ..
|
||||
"\n\n" ..
|
||||
"\nKilos still required: " .. self._requiredKilos - self._droppedKilos ..
|
||||
"\n\n" ..
|
||||
"NOTE: Do not land in the orange construction zone!"
|
||||
|
||||
trigger.action.outTextForGroup(groupID, briefing, GlobalConfig:getBriefingTime())
|
||||
end
|
||||
|
||||
function BuildableMission:MarkMissionAreaToGroup(groupID)
|
||||
|
||||
if self._markIDsPerGroup[groupID] then
|
||||
DcsUtil.RemoveMark(self._markIDsPerGroup[groupID])
|
||||
end
|
||||
|
||||
local text = "[" .. self.code .. "] " .. self.name .. " | " .. self._crateType
|
||||
local location = { x= self.location.x, y=land.getHeight(self.location), z=self.location.y }
|
||||
local markID = DcsUtil.AddMarkToGroup(groupID, text, location)
|
||||
|
||||
self._markIDsPerGroup[groupID] = markID
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param crate SupplyConfig
|
||||
function BuildableMission:NotifyCrateDroppedOf(crate)
|
||||
for _, listener in ipairs(self._onCrateDroppedOfListeners) do
|
||||
if listener.OnCrateDroppedOff then
|
||||
listener:OnCrateDroppedOff(self, crate.weight)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BuildableMission:SpawnActive()
|
||||
|
||||
if self._state ~= "NEW" then
|
||||
self._logger:debug("Mission already spawned: " .. self.code)
|
||||
return
|
||||
end
|
||||
|
||||
self._logger:debug("Spawning buildable mission: " .. self.code)
|
||||
|
||||
if self._noLandingZone == nil then
|
||||
self._logger:error("No nolanding zone found for mission: " .. self.code)
|
||||
return
|
||||
end
|
||||
|
||||
---@type DrawColor
|
||||
local lineColor = { r=230/255, g=93/255, b=49/255, a=1}
|
||||
---@type DrawColor
|
||||
local fillColor = { r=230/255, g=93/255, b=49/255, a=0.2}
|
||||
self._noLandingZoneId = DcsUtil.DrawZone(self._noLandingZone, lineColor, fillColor, 6)
|
||||
|
||||
if self._dropOffZone == nil then
|
||||
self._logger:error("No drop off zone found for mission: " .. self.code)
|
||||
return
|
||||
end
|
||||
|
||||
local lineColor2 = { r=0, g=0, b=1, a=1}
|
||||
local fillColor2 = { r=0, g=0, b=1, a=0}
|
||||
self._dropOffZoneId = DcsUtil.DrawZone(self._dropOffZone, lineColor2, fillColor2, 6)
|
||||
|
||||
---@param selfA BuildableMission
|
||||
---@param time number
|
||||
local checkForCrateTasks = function (selfA, time)
|
||||
selfA:CheckCratesInZone()
|
||||
|
||||
if selfA:getState() == "COMPLETED" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return time + 10
|
||||
end
|
||||
|
||||
timer.scheduleFunction(checkForCrateTasks, self, timer.getTime() + 10)
|
||||
|
||||
self:SpawnForwardUnits()
|
||||
self._state = "ACTIVE"
|
||||
|
||||
self._missionCommandsHelper:AddMissionToCommands(self)
|
||||
self._supplyUnitsTracker:AddOnSupplyUnitSpawnedListener(self)
|
||||
|
||||
local units = self._supplyUnitsTracker:GetUnits()
|
||||
if units then
|
||||
for _, unit in pairs(units) do
|
||||
if unit and unit:isExist() then
|
||||
local group = unit:getGroup()
|
||||
if group then
|
||||
self:MarkMissionAreaToGroup(group:getID())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BuildableMission:SpawnForwardUnits()
|
||||
|
||||
end
|
||||
|
||||
---@param unit Unit
|
||||
function BuildableMission:SupplyUnitSpawned(unit)
|
||||
|
||||
if self._state ~= "ACTIVE" then return end
|
||||
|
||||
local group = unit:getGroup()
|
||||
if group == nil then return end
|
||||
|
||||
self:MarkMissionAreaToGroup(unit:getGroup():getID())
|
||||
end
|
||||
|
||||
|
||||
function BuildableMission:CheckCratesInZone()
|
||||
|
||||
---@type Array<Object>
|
||||
local foundCrates = {}
|
||||
|
||||
local crates = self._supplyUnitsTracker:GetCargoCratesDropped()
|
||||
for _, staticObject in pairs(crates) do
|
||||
if staticObject and staticObject:isExist() and Util.startswith(staticObject:getName(), self._crateType, true) then
|
||||
local pos = staticObject:getPoint()
|
||||
|
||||
if Util.is3dPointInZone(pos, self._dropOffZone) then
|
||||
table.insert(foundCrates, staticObject)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, foundCrate in pairs(foundCrates) do
|
||||
local crateConfig = SupplyConfigHelper.fromObjectName(foundCrate:getName())
|
||||
if crateConfig then
|
||||
self._droppedKilos = self._droppedKilos + crateConfig.weight
|
||||
foundCrate:destroy()
|
||||
self:NotifyCrateDroppedOf(crateConfig)
|
||||
end
|
||||
end
|
||||
|
||||
if self._droppedKilos >= self._requiredKilos then
|
||||
DcsUtil.RemoveMark(self._noLandingZoneId)
|
||||
DcsUtil.RemoveMark(self._dropOffZoneId)
|
||||
self:NotifyMissionComplete()
|
||||
self._state = "COMPLETED"
|
||||
end
|
||||
|
||||
if self._state == "COMPLETED" then
|
||||
for groupID, markID in pairs(self._markIDsPerGroup) do
|
||||
if markID then
|
||||
DcsUtil.RemoveMark(markID)
|
||||
self._markIDsPerGroup[groupID] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
return BuildableMission
|
||||
@@ -0,0 +1,543 @@
|
||||
local Mission = require("classes.stageClasses.missions.baseMissions.Mission")
|
||||
local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
|
||||
---@class RunwayStrikeMission : Mission
|
||||
---@field runwayBombingTracker RunwayBombingTracker
|
||||
---@field private _runway Runway
|
||||
---@field private _runwayZone SpearheadTriggerZone
|
||||
---@field private _airportName string
|
||||
---@field private _runwaySections Array<RunwaySection>
|
||||
---@field private _minKilosForDamage number
|
||||
---@field private _repairInProgress boolean
|
||||
local RunwayStrikeMission = {}
|
||||
|
||||
---@class RunwaySection
|
||||
---@field center Vec2
|
||||
---@field corners Array<Vec2>
|
||||
---@field kilosHit number
|
||||
---@field repairGroups Array<string>
|
||||
---@field drawID number?
|
||||
|
||||
|
||||
---@param runway Runway
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@param runwayBombingTracker RunwayBombingTracker
|
||||
---@return RunwayStrikeMission?
|
||||
function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBombingTracker)
|
||||
|
||||
RunwayStrikeMission.__index = RunwayStrikeMission
|
||||
setmetatable(RunwayStrikeMission, Mission)
|
||||
local self = setmetatable({}, RunwayStrikeMission)
|
||||
|
||||
self._airportName = airbaseName
|
||||
local missionBriefing = "Bomb runway " .. runway.Name .. " at " .. airbaseName .. "to delay the CAP effort"
|
||||
|
||||
local success, error = Mission.newSuper(self, "noZone", runway.Name, "OCA", missionBriefing, "secondary", database, logger)
|
||||
|
||||
self.runwayBombingTracker = runwayBombingTracker
|
||||
self._runway = runway
|
||||
self._runwayZone = self:RunwayToSpearheadZone(runway)
|
||||
self._repairInProgress = false
|
||||
self._minKilosForDamage = 100
|
||||
|
||||
local sections = self:ToSections(runway, 5)
|
||||
--[[
|
||||
+-----------+-----------+-----------+-----------+-----------+
|
||||
| Section 1 | Section 2 | Section 3 | Section 4 | Section 5 |
|
||||
+-----------+-----------+-----------+-----------+-----------+
|
||||
]]
|
||||
|
||||
self._runwaySections = {
|
||||
sections[2],
|
||||
sections[3],
|
||||
sections[4],
|
||||
} --only take 2, 3 and 4 cause those are the most imporant parts of the runway
|
||||
|
||||
if not success then
|
||||
logger:error("Failed to create RunwayBombingMission " .. runway.Name .. " => " .. error)
|
||||
return nil
|
||||
end
|
||||
|
||||
self.location = { x= runway.position.x, y = runway.position.z }
|
||||
|
||||
runwayBombingTracker:RegisterRunway(runway, self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---activates the mission
|
||||
function RunwayStrikeMission:SpawnActive()
|
||||
self._missionCommandsHelper:AddMissionToCommands(self)
|
||||
self:Draw()
|
||||
self:UpdateState()
|
||||
end
|
||||
|
||||
---@return SpearheadTriggerZone
|
||||
function RunwayStrikeMission:GetRunwayZone()
|
||||
return self._runwayZone
|
||||
end
|
||||
|
||||
function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
|
||||
|
||||
self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name)
|
||||
for _, section in pairs(self._runwaySections) do
|
||||
|
||||
local zone = self:SectionToSpearheadZone(section)
|
||||
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
|
||||
if section.kilosHit == nil then
|
||||
section.kilosHit = 0
|
||||
end
|
||||
|
||||
section.kilosHit = section.kilosHit + explosiveMass
|
||||
end
|
||||
end
|
||||
|
||||
---@param selfA RunwayStrikeMission
|
||||
local updateState = function(selfA, time)
|
||||
selfA:UpdateState()
|
||||
end
|
||||
|
||||
timer.scheduleFunction(updateState, self, timer.getTime() + 5)
|
||||
|
||||
end
|
||||
|
||||
function RunwayStrikeMission:UpdateState()
|
||||
|
||||
self._logger:debug("Updating state of runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
|
||||
|
||||
self:Draw()
|
||||
|
||||
for _, section in pairs(self._runwaySections) do
|
||||
if section.kilosHit > self._minKilosForDamage then
|
||||
self:StartRepair()
|
||||
self._missionCommandsHelper:RemoveMissionToCommands(self)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type DrawColor
|
||||
local healthyAreaColor = { r=0, g=1, b=0, a=0.5 }
|
||||
---@type DrawColor
|
||||
local healthyAreaLineColor = { r=0, g=1, b=0, a=1 }
|
||||
---@type DrawColor
|
||||
local damagedAreaColor ={ r=1, g=165/255, b=0, a=0.5 }
|
||||
---@type DrawColor
|
||||
local damagedAreaLineColor = { r=1, g=165/255, b=0, a=1 }
|
||||
---@type DrawColor
|
||||
local destroyedAreaColor = { r=1, g=0, b=0, a=0.5 }
|
||||
---@type DrawColor
|
||||
local destroyedAreaLineColor = { r=1, g=0, b=0, a=1 }
|
||||
|
||||
---@private
|
||||
function RunwayStrikeMission:Draw()
|
||||
|
||||
---comment
|
||||
---@param runwaySection RunwaySection
|
||||
local function drawSection(runwaySection)
|
||||
local lineColor = healthyAreaLineColor
|
||||
local fillColor = healthyAreaColor
|
||||
|
||||
local orangeDamage = self._minKilosForDamage - 100
|
||||
if orangeDamage < 0 then orangeDamage = self._minKilosForDamage * 0.8 end
|
||||
|
||||
if runwaySection.kilosHit > self._minKilosForDamage then
|
||||
lineColor = destroyedAreaLineColor
|
||||
fillColor = destroyedAreaColor
|
||||
elseif runwaySection.kilosHit > orangeDamage then
|
||||
lineColor = damagedAreaLineColor
|
||||
fillColor = damagedAreaColor
|
||||
end
|
||||
|
||||
if runwaySection.drawID == nil then
|
||||
local zone = self:SectionToSpearheadZone(runwaySection)
|
||||
|
||||
local color = { r=0, g=1, b=0, a=0.5 }
|
||||
|
||||
runwaySection.drawID = DcsUtil.DrawZone(zone, color, color, 5)
|
||||
else
|
||||
DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
|
||||
DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
for _, section in pairs(self._runwaySections) do
|
||||
drawSection(section)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param section RunwaySection
|
||||
---@return SpearheadTriggerZone
|
||||
function RunwayStrikeMission:SectionToSpearheadZone(section)
|
||||
return {
|
||||
location = { x = self._runway.position.x, y = self._runway.position.z },
|
||||
radius = self._runway.width,
|
||||
name = self._runway.Name,
|
||||
verts = section.corners,
|
||||
zone_type = "Polygon",
|
||||
}
|
||||
end
|
||||
|
||||
---@
|
||||
|
||||
function RunwayStrikeMission:StartRepair()
|
||||
if self._repairInProgress == true then return end
|
||||
|
||||
self._repairInProgress = true
|
||||
self._logger:debug("Starting repair of runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
|
||||
---comment
|
||||
---@param selfA any
|
||||
---@return unknown
|
||||
local repairTask = function (selfA, time)
|
||||
local interval = selfA:DoRepairCycle()
|
||||
if interval == nil then return nil end
|
||||
return time + interval
|
||||
end
|
||||
|
||||
timer.scheduleFunction(repairTask, self, timer.getTime() + 5)
|
||||
end
|
||||
|
||||
function RunwayStrikeMission:DoRepairCycle()
|
||||
|
||||
local interval = 5
|
||||
local repairPerSecond = 5
|
||||
|
||||
self._logger:debug("Repair cycle for" .. self._airportName .. ":" .. self._runway.Name)
|
||||
|
||||
local isHealed = true
|
||||
for _, section in pairs(self._runwaySections) do
|
||||
section.kilosHit = section.kilosHit - interval * repairPerSecond
|
||||
|
||||
if section.kilosHit > self._minKilosForDamage * 0.1 then
|
||||
self:AddOrUpdateRepairStatics(section)
|
||||
isHealed = false
|
||||
else
|
||||
self:RemoveRepairStatics(section)
|
||||
end
|
||||
end
|
||||
|
||||
self:Draw()
|
||||
|
||||
if isHealed == true then
|
||||
self._repairInProgress = false
|
||||
self:FullRepairRunway()
|
||||
self._logger:debug("Repair complete for runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
|
||||
return nil
|
||||
end
|
||||
|
||||
return interval
|
||||
|
||||
end
|
||||
|
||||
---@class StaticSpawn
|
||||
---@field category string
|
||||
---@field type string
|
||||
---@field y number
|
||||
---@field x number
|
||||
---@field heading number
|
||||
|
||||
|
||||
local counter = 1
|
||||
|
||||
---@type Array<Array<StaticSpawn>>
|
||||
local repairStaticConfigs = {
|
||||
[1] = {
|
||||
[1] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "ZIL-135",
|
||||
["y"] = -4,
|
||||
["x"] = 10,
|
||||
["heading"] = 6.2133721370998,
|
||||
},
|
||||
[2] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "Tigr_233036",
|
||||
["y"] = 0,
|
||||
["x"] = 10,
|
||||
["heading"] = 6.2133721370998,
|
||||
},
|
||||
[3] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "Infantry AK ver3",
|
||||
["y"] = 10,
|
||||
["x"] = 7,
|
||||
["heading"] = 4.4331363000656,
|
||||
},
|
||||
[4] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "Infantry AK ver3",
|
||||
["y"] = 12,
|
||||
["x"] = 1,
|
||||
["heading"] = 4.4331363000656,
|
||||
},
|
||||
[5] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "Infantry AK ver3",
|
||||
["y"] = -10,
|
||||
["x"] = -2,
|
||||
["heading"] = 4.1538836197465,
|
||||
},
|
||||
[6] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "CV_59_Large_Forklift",
|
||||
["y"] = -11,
|
||||
["x"] = 0,
|
||||
["heading"] = 1.535889741755,
|
||||
},
|
||||
[7] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "ZiL-131 APA-80",
|
||||
["y"] = 11,
|
||||
["x"] = 5,
|
||||
["heading"] = 0.62831853071796,
|
||||
},
|
||||
[8] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "CV_59_NS60",
|
||||
["y"] = -4,
|
||||
["x"] = -15,
|
||||
["heading"] = 0.62831853071796,
|
||||
}
|
||||
},
|
||||
[2] = {
|
||||
[1] = {
|
||||
["category"] = "Air Defence",
|
||||
["type"] = "generator_5i57",
|
||||
["y"] = 2.7889551542015,
|
||||
["x"] = -4.7698247930386,
|
||||
["heading"] = 4.0666171571468,
|
||||
},
|
||||
[2] = {
|
||||
["category"] = "Infantry",
|
||||
["type"] = "Infantry AK ver3",
|
||||
["y"] = 3.639392285097,
|
||||
["x"] = 1.4300755972836,
|
||||
["heading"] = 5.0440015382636,
|
||||
},
|
||||
[3] = {
|
||||
["category"] = "Infantry",
|
||||
["type"] = "Infantry AK ver3",
|
||||
["y"] = 3.5139072826724,
|
||||
["x"] = -0.32671443666074,
|
||||
["heading"] = 6.16101225954,
|
||||
},
|
||||
[4] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "ATMZ-5",
|
||||
["y"] = -6.9556010010953,
|
||||
["x"] = 3.007681753102,
|
||||
["heading"] = 4.0666171571468,
|
||||
},
|
||||
[5] = {
|
||||
["category"] = "Unarmed",
|
||||
["type"] = "GAZ-66",
|
||||
["y"] = 5.9304017026008,
|
||||
["x"] = 0.80323129595153,
|
||||
["heading"] = 6.2308254296198,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---@private
|
||||
---@param section RunwaySection
|
||||
function RunwayStrikeMission:AddOrUpdateRepairStatics(section)
|
||||
if Util.tableLength(section.repairGroups) > 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local location = { --Can randomise a little later
|
||||
x = section.center.x,
|
||||
y = section.center.y,
|
||||
}
|
||||
|
||||
local repairGroup = Util.randomFromList(repairStaticConfigs)
|
||||
for _, repairStatic in pairs(repairGroup) do
|
||||
|
||||
repairStatic.x = location.x + repairStatic.x
|
||||
repairStatic.y = location.y + repairStatic.y
|
||||
|
||||
repairStatic.hidden = true
|
||||
repairStatic.name = "runway_repairunit_" .. counter
|
||||
counter = counter + 1
|
||||
coalition.addStaticObject(country.id.RUSSIA, repairStatic)
|
||||
table.insert(section.repairGroups, repairStatic.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@private
|
||||
---@param section RunwaySection
|
||||
function RunwayStrikeMission:RemoveRepairStatics(section)
|
||||
if Util.tableLength(section.repairGroups) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for _, repairGroup in pairs(section.repairGroups) do
|
||||
local static = StaticObject.getByName(repairGroup)
|
||||
if static then
|
||||
static:destroy()
|
||||
end
|
||||
end
|
||||
|
||||
section.repairGroups = {}
|
||||
end
|
||||
|
||||
---@private
|
||||
function RunwayStrikeMission:FullRepairRunway()
|
||||
|
||||
for _, section in pairs(self._runwaySections) do
|
||||
section.kilosHit = 0
|
||||
self:RemoveRepairStatics(section)
|
||||
end
|
||||
|
||||
local minX = self._runwayZone.verts[1].x
|
||||
local minY = self._runwayZone.verts[1].y
|
||||
local maxX = self._runwayZone.verts[1].x
|
||||
local maxY = self._runwayZone.verts[1].y
|
||||
|
||||
for _, vert in pairs(self._runwayZone.verts) do
|
||||
if vert.x < minX then
|
||||
minX = vert.x
|
||||
end
|
||||
if vert.x > maxX then
|
||||
maxX = vert.x
|
||||
end
|
||||
if vert.y < minY then
|
||||
minY = vert.y
|
||||
end
|
||||
if vert.y > maxY then
|
||||
maxY = vert.y
|
||||
end
|
||||
end
|
||||
|
||||
---@type Box
|
||||
local box = {
|
||||
id = world.VolumeType.BOX,
|
||||
params = {
|
||||
min = { x = minX, z = minY, y = 0 },
|
||||
max = { x = maxX, z = maxY, y = 10000 },
|
||||
}
|
||||
}
|
||||
world.removeJunk(box)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param runway Runway
|
||||
---@return Array<RunwaySection>
|
||||
function RunwayStrikeMission:ToSections(runway, numSections)
|
||||
|
||||
local sections = {}
|
||||
local center = runway.position
|
||||
local heading = runway.course
|
||||
local width = runway.width
|
||||
local length = runway.length / numSections
|
||||
|
||||
if heading < 0 then
|
||||
heading = math.abs(heading)
|
||||
else
|
||||
heading = 0 - heading
|
||||
end
|
||||
|
||||
local cosH = math.cos(heading)
|
||||
local sinH = math.sin(heading)
|
||||
|
||||
for i = 0, numSections - 1 do
|
||||
local sectionCenterOffset = (i - (numSections / 2) + 0.5) * length
|
||||
|
||||
---@type Vec2
|
||||
local sectionCenter = {
|
||||
x = center.x + sectionCenterOffset * cosH,
|
||||
y = center.z + sectionCenterOffset * sinH
|
||||
}
|
||||
|
||||
local halfWidth = width / 2
|
||||
local halfLength = length / 2
|
||||
|
||||
---@type Array<Vec2>
|
||||
local corners = {
|
||||
{
|
||||
x = sectionCenter.x + (-halfLength * cosH - halfWidth * sinH),
|
||||
y = sectionCenter.y + (-halfLength * sinH + halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = sectionCenter.x + (-halfLength * cosH + halfWidth * sinH),
|
||||
y = sectionCenter.y + (-halfLength * sinH - halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = sectionCenter.x + (halfLength * cosH + halfWidth * sinH),
|
||||
y = sectionCenter.y + (halfLength * sinH - halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = sectionCenter.x + (halfLength * cosH - halfWidth * sinH),
|
||||
y = sectionCenter.y + (halfLength * sinH + halfWidth * cosH)
|
||||
}
|
||||
}
|
||||
|
||||
---@type RunwaySection
|
||||
local section = {
|
||||
center = sectionCenter,
|
||||
corners = corners,
|
||||
kilosHit = 0,
|
||||
repairGroups = {},
|
||||
}
|
||||
|
||||
table.insert(sections, section)
|
||||
end
|
||||
|
||||
return sections
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param runway Runway
|
||||
---@return SpearheadTriggerZone
|
||||
function RunwayStrikeMission:RunwayToSpearheadZone(runway)
|
||||
|
||||
-- Calculate the 4 corner points of the runway based on heading, height, and width
|
||||
local radHeading = runway.course
|
||||
|
||||
if radHeading < 0 then
|
||||
radHeading = math.abs(radHeading)
|
||||
else
|
||||
radHeading = 0 - radHeading
|
||||
end
|
||||
|
||||
local cosH = math.cos(radHeading)
|
||||
local sinH = math.sin(radHeading)
|
||||
|
||||
local halfWidth = runway.width / 2
|
||||
local halfHeight = runway.length / 2
|
||||
|
||||
---@type Array<Vec2>
|
||||
local corners = {
|
||||
{
|
||||
x = runway.position.x + (-halfHeight * cosH - halfWidth * sinH),
|
||||
y = runway.position.z + (-halfHeight * sinH + halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = runway.position.x + (-halfHeight * cosH + halfWidth * sinH),
|
||||
y = runway.position.z + (-halfHeight * sinH - halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = runway.position.x + (halfHeight * cosH + halfWidth * sinH),
|
||||
y = runway.position.z + (halfHeight * sinH - halfWidth * cosH)
|
||||
},
|
||||
{
|
||||
x = runway.position.x + (halfHeight * cosH - halfWidth * sinH),
|
||||
y = runway.position.z + (halfHeight * sinH + halfWidth * cosH)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
location = { x = runway.position.x, y = runway.position.z },
|
||||
radius = runway.width,
|
||||
name = runway.Name,
|
||||
verts = corners,
|
||||
zone_type = "Polygon",
|
||||
}
|
||||
end
|
||||
|
||||
return RunwayStrikeMission
|
||||
@@ -0,0 +1,525 @@
|
||||
local Util = require("classes.util.Util")
|
||||
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
|
||||
local Mission = require("classes.stageClasses.missions.baseMissions.Mission")
|
||||
local SpearheadGroup = require("classes.stageClasses.Groups.SpearheadGroup")
|
||||
local Events = require("classes.spearhead_events")
|
||||
local BattleManager = require("classes.stageClasses.helpers.BattleManager")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
|
||||
--- ZoneMission is missions that are defined by zones in the ME
|
||||
---@class ZoneMission : Mission, OnUnitLostListener
|
||||
---@field private _state MissionState
|
||||
---@field private _missionGroups MissionGroups
|
||||
---@field private _dependencies table<string, boolean>
|
||||
---@field private _completeAtIndex number
|
||||
---@field private _parentStage Stage
|
||||
---@field private _battleManager? BattleManager
|
||||
---@field private _lastContactMarkerID number;
|
||||
local ZoneMission = {}
|
||||
|
||||
--- @class MissionGroups
|
||||
--- @field hasTargets boolean
|
||||
--- @field redGroups Array<SpearheadGroup>
|
||||
--- @field blueGroups Array<SpearheadGroup>
|
||||
--- @field unitsAlive table<string, table<string, boolean>>
|
||||
--- @field targetsAlive table<string, table<string, boolean>>
|
||||
--- @field sceneryTargets Array<SpearheadSceneryObject>
|
||||
--- @field groupNamesPerunit table<string,string>
|
||||
|
||||
---@class ParsedMissionName
|
||||
---@field missionName string
|
||||
---@field type MissionType
|
||||
|
||||
---comment
|
||||
---@param input string
|
||||
---@return ParsedMissionName?
|
||||
local function ParseZoneName(input)
|
||||
local split_name = Util.split_string(input, "_")
|
||||
local split_length = Util.tableLength(split_name)
|
||||
if Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
|
||||
MissionEditorWarnings.Add("Random Mission with zonename " .. input .. " not in right format")
|
||||
return nil
|
||||
elseif split_length < 3 then
|
||||
MissionEditorWarnings.Add("Mission with zonename" .. input .. " not in right format")
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type MissionType
|
||||
local parsedType = "nil"
|
||||
|
||||
local inputType = string.lower(split_name[2])
|
||||
if inputType == "dead" then parsedType = "DEAD" end
|
||||
if inputType == "strike" then parsedType = "STRIKE" end
|
||||
if inputType == "cas" then parsedType = "CAS" end
|
||||
if inputType == "bai" then parsedType = "BAI" end
|
||||
if inputType == "sam" then parsedType = "SAM" end
|
||||
|
||||
if parsedType == "nil" then
|
||||
MissionEditorWarnings.Add("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
|
||||
|
||||
MINIMAL_UNITS_ALIVE_RATIO = 0.21
|
||||
|
||||
---comment
|
||||
---@param zoneName string
|
||||
---@param priority MissionPriority
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@param parentStage Stage
|
||||
---@param spawnManager SpawnManager
|
||||
---@return ZoneMission?
|
||||
function ZoneMission.new(zoneName, priority, database, logger, parentStage, spawnManager)
|
||||
ZoneMission.__index = ZoneMission
|
||||
setmetatable(ZoneMission, Mission)
|
||||
|
||||
local self = setmetatable({}, ZoneMission)
|
||||
|
||||
local parsed = ParseZoneName(zoneName)
|
||||
if not parsed then
|
||||
logger:error("Failed to create ZoneMission " .. zoneName .. " => invalid name")
|
||||
return nil
|
||||
end
|
||||
|
||||
local missionData = database:getMissionDataForZone(zoneName)
|
||||
if not missionData then return end
|
||||
|
||||
local missionBriefing = missionData.description or "No briefing available"
|
||||
|
||||
local success, error = Mission.newSuper(self, zoneName, parsed.missionName, parsed.type, missionBriefing, priority,
|
||||
database, logger)
|
||||
if not success then
|
||||
logger:error("Failed to create ZoneMission " .. zoneName .. " => " .. error)
|
||||
return nil
|
||||
end
|
||||
|
||||
--- parent new done
|
||||
|
||||
if self.missionType == "SAM" then
|
||||
self.missionTypeDisplay = "DEAD"
|
||||
end
|
||||
|
||||
self._missionGroups = {
|
||||
redGroups = {},
|
||||
blueGroups = {},
|
||||
unitsAlive = {},
|
||||
targetsAlive = {},
|
||||
hasTargets = false,
|
||||
groupNamesPerunit = {},
|
||||
sceneryTargets = {}
|
||||
}
|
||||
|
||||
self._parentStage = parentStage
|
||||
self._dependencies = {}
|
||||
|
||||
if missionData.dependsOn then
|
||||
for _, dependency in pairs(missionData.dependsOn) do
|
||||
self._dependencies[dependency] = false
|
||||
end
|
||||
end
|
||||
|
||||
if missionData.completeAt == nil and (self.missionType == "BAI" or self.missionType == "CAS") then
|
||||
self._completeAtIndex = 0.8
|
||||
elseif missionData.completeAt == nil then
|
||||
self._completeAtIndex = 1
|
||||
else
|
||||
self._completeAtIndex = missionData.completeAt
|
||||
end
|
||||
|
||||
self._missionGroups.sceneryTargets = missionData.SceneryTargets or {}
|
||||
if Util.tableLength(self._missionGroups.sceneryTargets) > 0 then
|
||||
self._missionGroups.hasTargets = true
|
||||
end
|
||||
|
||||
for _, groupName in pairs(missionData.BlueGroups) do
|
||||
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
if spearheadGroup then
|
||||
table.insert(self._missionGroups.blueGroups, spearheadGroup)
|
||||
end
|
||||
spearheadGroup:Destroy()
|
||||
end
|
||||
|
||||
for _, groupName in pairs(missionData.RedGroups) do
|
||||
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
|
||||
table.insert(self._missionGroups.redGroups, spearheadGroup)
|
||||
|
||||
local isGroupTarget = Util.startswith(string.lower(groupName), "tgt_")
|
||||
for _, unit in pairs(spearheadGroup:GetObjects()) do
|
||||
local unitName = unit:getName()
|
||||
local isUnitTarget = Util.startswith(string.lower(unitName), "tgt_")
|
||||
|
||||
if self._missionGroups.unitsAlive[groupName] == nil then
|
||||
self._missionGroups.unitsAlive[groupName] = {}
|
||||
end
|
||||
|
||||
self._missionGroups.unitsAlive[groupName][unitName] = true
|
||||
self._missionGroups.groupNamesPerunit[unitName] = groupName
|
||||
|
||||
if isGroupTarget == true or isUnitTarget == true then
|
||||
self._missionGroups.hasTargets = true
|
||||
|
||||
if self._missionGroups.targetsAlive[groupName] == nil then
|
||||
self._missionGroups.targetsAlive[groupName] = {}
|
||||
end
|
||||
|
||||
self._missionGroups.targetsAlive[groupName][unitName] = true
|
||||
end
|
||||
|
||||
Events.addOnUnitLostEventListener(unitName, self)
|
||||
end
|
||||
|
||||
spearheadGroup:Destroy()
|
||||
end
|
||||
|
||||
if self.missionType == "CAS" then
|
||||
self._battleManager = BattleManager.New(self._missionGroups.redGroups, self._missionGroups.blueGroups, self.zoneName, self._logger.LogLevel)
|
||||
|
||||
end
|
||||
|
||||
self._logger:debug("Mission " .. self.name .. " group count: " .. Util.tableLength(missionData.RedGroups))
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@private
|
||||
function ZoneMission:StartCheckingDependencies()
|
||||
self._state = "WAITING"
|
||||
|
||||
---comment
|
||||
---@param mission ZoneMission
|
||||
---@param time any
|
||||
---@return unknown
|
||||
local function CheckDependencies(mission, time)
|
||||
if mission:AllDependenciesMet() == true then
|
||||
mission:SpawnActive()
|
||||
return nil
|
||||
end
|
||||
|
||||
return time + 15
|
||||
end
|
||||
|
||||
timer.scheduleFunction(CheckDependencies, self, timer.getTime() + 15)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function ZoneMission:AllDependenciesMet()
|
||||
local allDependenciesMet = true
|
||||
for missionName, value in pairs(self._dependencies) do
|
||||
if self._parentStage:IsMissionComplete(missionName) == false then
|
||||
allDependenciesMet = false
|
||||
self._dependencies[missionName] = false
|
||||
else
|
||||
self._dependencies[missionName] = true
|
||||
end
|
||||
end
|
||||
|
||||
if allDependenciesMet == true then
|
||||
self._logger:info("All dependencies met for " .. self.name)
|
||||
end
|
||||
|
||||
return allDependenciesMet
|
||||
end
|
||||
|
||||
---@internal
|
||||
---@param checkHealth boolean
|
||||
---@param messageIfDone boolean
|
||||
function ZoneMission:UpdateState(checkHealth, messageIfDone)
|
||||
if checkHealth == nil then checkHealth = false end
|
||||
if messageIfDone == false then messageIfDone = true end
|
||||
|
||||
|
||||
if checkHealth == true then
|
||||
local function unitAliveState(unitName)
|
||||
local staticObject = StaticObject.getByName(unitName)
|
||||
if staticObject then
|
||||
if staticObject:isExist() == true then
|
||||
local life0 = staticObject:getDesc().life
|
||||
if staticObject:getLife() / life0 < 0.3 then
|
||||
self._logger:debug("exploding unit")
|
||||
trigger.action.explosion(staticObject:getPoint(), 100)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
local unit = Unit.getByName(unitName)
|
||||
|
||||
if unit and unit:isExist() then
|
||||
if unit:getLife() / unit:getLife0() < 0.2 then
|
||||
self._logger:debug("exploding unit")
|
||||
trigger.action.explosion(unit:getPoint(), 100)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self._missionGroups.hasTargets == true then
|
||||
for groupName, unitNameDict in pairs(self._missionGroups.targetsAlive) do
|
||||
for unitName, isAlive in pairs(unitNameDict) do
|
||||
if isAlive == true then
|
||||
self._missionGroups.targetsAlive[groupName][unitName] = unitAliveState(unitName)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for groupName, unitNameDict in pairs(self._missionGroups.unitsAlive) do
|
||||
for unitName, isAlive in pairs(unitNameDict) do
|
||||
if isAlive == true then
|
||||
self._missionGroups.unitsAlive[groupName][unitName] = unitAliveState(unitName)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self._missionGroups.hasTargets == true then
|
||||
local total = 0
|
||||
local alive = 0
|
||||
|
||||
for _, units in pairs(self._missionGroups.targetsAlive) do
|
||||
for _, isAlive in pairs(units) do
|
||||
total = total + 1
|
||||
if isAlive == true then
|
||||
alive = alive + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
|
||||
total = total + 1
|
||||
if sceneryObject:IsAlive() == true then
|
||||
alive = alive + 1
|
||||
end
|
||||
end
|
||||
|
||||
local deadRatio = (total - alive) / total
|
||||
if deadRatio >= self._completeAtIndex then
|
||||
self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex)
|
||||
self._state = "COMPLETED"
|
||||
end
|
||||
else
|
||||
local total = 0
|
||||
local alive = 0
|
||||
|
||||
for _, units in pairs(self._missionGroups.unitsAlive) do
|
||||
for _, isAlive in pairs(units) do
|
||||
total = total + 1
|
||||
if isAlive == true then
|
||||
alive = alive + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local deadRatio = (total - alive) / total
|
||||
if deadRatio >= self._completeAtIndex then
|
||||
self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex)
|
||||
|
||||
self._state = "COMPLETED"
|
||||
end
|
||||
end
|
||||
|
||||
if self._state == "COMPLETED" and self._lastContactMarkerID then
|
||||
DcsUtil.RemoveMark(self._lastContactMarkerID)
|
||||
end
|
||||
|
||||
if self._state == "COMPLETED" and self._battleManager then
|
||||
self._battleManager:Stop()
|
||||
end
|
||||
end
|
||||
|
||||
function ZoneMission:SpawnPersistedState()
|
||||
for _, group in pairs(self._missionGroups.redGroups) do
|
||||
group:Spawn()
|
||||
end
|
||||
|
||||
for _, object in pairs(self._missionGroups.sceneryTargets) do
|
||||
object:UpdateStatePersistently()
|
||||
end
|
||||
end
|
||||
|
||||
---spawns the mission, but doesn't add
|
||||
function ZoneMission:SpawnInactive()
|
||||
self._logger:info("PreActivating " .. self.name)
|
||||
|
||||
for _, group in pairs(self._missionGroups.redGroups) do
|
||||
group:Spawn()
|
||||
end
|
||||
end
|
||||
|
||||
function ZoneMission:SpawnActive()
|
||||
if self:AllDependenciesMet() == false then
|
||||
self:SpawnInactive()
|
||||
self:StartCheckingDependencies()
|
||||
return
|
||||
end
|
||||
|
||||
self._logger:info("Activating " .. self.name)
|
||||
|
||||
if self._state == "COMPLETED" or self._state == "ACTIVE" then
|
||||
self._logger:debug("Mission already completed, not spawning")
|
||||
return
|
||||
end
|
||||
|
||||
self._state = "ACTIVE"
|
||||
for _, group in pairs(self._missionGroups.redGroups) do
|
||||
group:Spawn()
|
||||
end
|
||||
|
||||
for _, group in pairs(self._missionGroups.blueGroups) do
|
||||
group:Spawn()
|
||||
end
|
||||
|
||||
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
|
||||
sceneryObject:UpdateStatePersistently()
|
||||
end
|
||||
|
||||
if self._battleManager then
|
||||
self._battleManager:Start()
|
||||
end
|
||||
|
||||
self._missionCommandsHelper:AddMissionToCommands(self)
|
||||
|
||||
self:StartCheckingContinuous()
|
||||
end
|
||||
|
||||
---@private
|
||||
function ZoneMission:StartCheckingContinuous()
|
||||
---comment
|
||||
---@param mission Mission
|
||||
---@param time any
|
||||
---@return unknown
|
||||
local Check = function(mission, time)
|
||||
mission:UpdateState(true, true)
|
||||
|
||||
if mission:getState() == "COMPLETED" then
|
||||
mission:NotifyMissionComplete()
|
||||
return nil
|
||||
end
|
||||
return time + 30
|
||||
end
|
||||
timer.scheduleFunction(Check, self, timer.getTime() + 30)
|
||||
end
|
||||
|
||||
---@return number
|
||||
function ZoneMission:PercentageComplete()
|
||||
if self._missionGroups.hasTargets == true then
|
||||
local dead = 0
|
||||
local total = 0
|
||||
if self._missionGroups.targetsAlive then
|
||||
for _, group in pairs(self._missionGroups.targetsAlive) do
|
||||
for _, isAlive in pairs(group) do
|
||||
total = total + 1
|
||||
if isAlive == false then
|
||||
dead = dead + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
|
||||
total = total + 1
|
||||
if sceneryObject:IsAlive() == false then
|
||||
dead = dead + 1
|
||||
end
|
||||
end
|
||||
|
||||
if total > 0 then
|
||||
return math.floor((dead / total) * 100)
|
||||
end
|
||||
else
|
||||
local dead = 0
|
||||
local total = 0
|
||||
if self._missionGroups.unitsAlive then
|
||||
for _, group in pairs(self._missionGroups.unitsAlive) do
|
||||
for _, isAlive in pairs(group) do
|
||||
total = total + 1
|
||||
if isAlive == false then
|
||||
dead = dead + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total > 0 then
|
||||
return math.floor((dead / total) * 100)
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
---@protected
|
||||
function ZoneMission:ToStateString()
|
||||
return "Units Destroyed: " .. self:PercentageComplete() .. "%"
|
||||
end
|
||||
|
||||
function ZoneMission:OnUnitLost(object)
|
||||
--[[
|
||||
OnUnit lost event
|
||||
]] --
|
||||
self._logger:debug("Getting on unit lost event")
|
||||
|
||||
if SpearheadConfig and SpearheadConfig.StageConfig and SpearheadConfig.StageConfig.markLastContact == true then
|
||||
self:MarkLastContact(object)
|
||||
end
|
||||
|
||||
local category = Object.getCategory(object)
|
||||
if category == Object.Category.UNIT then
|
||||
local unitName = object:getName()
|
||||
self._logger:debug("UnitName:" .. unitName)
|
||||
|
||||
local groupName = self._missionGroups.groupNamesPerunit[unitName]
|
||||
self._missionGroups.unitsAlive[groupName][unitName] = false
|
||||
|
||||
if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then
|
||||
self._missionGroups.targetsAlive[groupName][unitName] = false
|
||||
end
|
||||
elseif category == Object.Category.STATIC then
|
||||
local name = object:getName()
|
||||
self._missionGroups.unitsAlive[name][name] = false
|
||||
|
||||
self._logger:debug("Name " .. name)
|
||||
|
||||
if self._missionGroups.targetsAlive[name] and self._missionGroups.targetsAlive[name][name] then
|
||||
self._missionGroups.targetsAlive[name][name] = false
|
||||
end
|
||||
end
|
||||
self:UpdateState(false, true)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unit Object
|
||||
function ZoneMission:MarkLastContact(unit)
|
||||
|
||||
if not unit then
|
||||
self._logger:error("MarkLastContact called with nil unit")
|
||||
return
|
||||
end
|
||||
|
||||
local point = unit:getPoint()
|
||||
if not point then
|
||||
self._logger:error("MarkLastContact called with unit without point")
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if self._lastContactMarkerID then
|
||||
DcsUtil.RemoveMark(self._lastContactMarkerID)
|
||||
end
|
||||
|
||||
self._lastContactMarkerID = DcsUtil.AddMarkToAll("Last Contact: " .. self.name .. " [" .. self.code .. "]", point)
|
||||
end
|
||||
|
||||
return ZoneMission
|
||||
@@ -0,0 +1,166 @@
|
||||
|
||||
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local Util = require("classes.util.Util")
|
||||
local GlobalConfig = require("classes.configuration.GlobalConfig")
|
||||
|
||||
---@class Mission
|
||||
---@field name string
|
||||
---@field zoneName string
|
||||
---@field missionType MissionType
|
||||
---@field missionTypeDisplay string
|
||||
---@field priority MissionPriority
|
||||
---@field location Vec2?
|
||||
---@field code string
|
||||
---@field protected _state MissionState
|
||||
---@field protected _missionBriefing string
|
||||
---@field getState fun(self: Mission): MissionState @Get the mission state
|
||||
---@field protected _logger Logger
|
||||
---@field protected _database Database
|
||||
---@field protected _missionCommandsHelper MissionCommandsHelper
|
||||
---@field protected _completeListeners Array<MissionCompleteListener>
|
||||
local Mission = {}
|
||||
Mission.__index = Mission
|
||||
|
||||
--- @class MissionCompleteListener
|
||||
--- @field OnMissionComplete fun(self: any, mission:Mission)
|
||||
|
||||
---@protected
|
||||
---@param self Mission
|
||||
---@param zoneName string
|
||||
---@param missionName string
|
||||
---@param missionType MissionType
|
||||
---@param missionBriefing string
|
||||
---@param priority MissionPriority
|
||||
---@param database Database
|
||||
---@param logger Logger
|
||||
---@return boolean, string
|
||||
function Mission.newSuper(self, zoneName, missionName, missionType, missionBriefing, priority, database, logger)
|
||||
|
||||
self.zoneName = zoneName
|
||||
self.name = missionName
|
||||
self.missionType = missionType
|
||||
self.priority = priority
|
||||
self._state = "NEW"
|
||||
self._logger = logger
|
||||
self._database = database
|
||||
self._missionBriefing = missionBriefing
|
||||
self.code = tostring(database:GetNewMissionCode())
|
||||
|
||||
self._completeListeners = {}
|
||||
|
||||
self.location = database:GetLocationForMissionZone(zoneName)
|
||||
self.missionTypeDisplay = self.missionType
|
||||
|
||||
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
|
||||
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
---@return MissionState
|
||||
function Mission:getState()
|
||||
return self._state
|
||||
end
|
||||
|
||||
--region PUBLIC
|
||||
|
||||
function Mission:SpawnPersistedState() end
|
||||
function Mission:SpawnActive() end
|
||||
|
||||
---comment
|
||||
---@param checkHealth boolean
|
||||
---@param messageIfDone boolean
|
||||
function Mission:UpdateState(checkHealth, messageIfDone) end
|
||||
function Mission:StartCheckingContinuous() end
|
||||
|
||||
function Mission:PercentageComplete()
|
||||
return 0
|
||||
end
|
||||
---comment
|
||||
---@param groupId number
|
||||
function Mission:ShowBriefing(groupId)
|
||||
|
||||
local group = DcsUtil.GetPlayerGroupByGroupID(groupId)
|
||||
if group == nil then return end
|
||||
|
||||
local unitType = DcsUtil.getUnitTypeFromGroup(group)
|
||||
local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType)
|
||||
self._logger:debug("Coords converted: " .. coords)
|
||||
|
||||
local stateString = self:ToStateString()
|
||||
if self._missionBriefing == nil or self._missionBriefing == "" then self._missionBriefing = "No briefing available" end
|
||||
|
||||
local briefing = self._missionBriefing
|
||||
|
||||
briefing = Util.replaceString(briefing, "{{coords}}", coords)
|
||||
briefing = Util.replaceString(briefing, "{{ coords }}", coords)
|
||||
|
||||
local text = "Mission [" ..
|
||||
self.code .. "] " .. self.name .. "\n \n" .. briefing .. " \n \n" .. stateString
|
||||
trigger.action.outTextForGroup(groupId, text, GlobalConfig:getBriefingTime());
|
||||
end
|
||||
|
||||
|
||||
---@param listener MissionCompleteListener Object that implements "OnMissionComplete(self, mission)"
|
||||
function Mission:AddMissionCompleteListener(listener)
|
||||
if type(listener) ~= "table" then
|
||||
return
|
||||
end
|
||||
table.insert(self._completeListeners, listener)
|
||||
end
|
||||
|
||||
function Mission:NotifyMissionComplete()
|
||||
self._missionCommandsHelper:RemoveMissionToCommands(self)
|
||||
self._logger:info("Mission Completed: " .. self.zoneName)
|
||||
trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed succesfully", 20)
|
||||
|
||||
for _, listener in pairs(self._completeListeners) do
|
||||
pcall(function()
|
||||
listener:OnMissionComplete(self)
|
||||
end)
|
||||
end
|
||||
|
||||
local succ, err = pcall(function()
|
||||
SpearheadAPI.Internal.notifyMissionComplete(self.zoneName)
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
function Mission:MarkMissionAreaToGroup(groupId) end
|
||||
|
||||
---endregion
|
||||
|
||||
--region PROTECTED
|
||||
|
||||
---@protected
|
||||
function Mission:ToStateString() return "status: in progress" end
|
||||
|
||||
|
||||
--endregion
|
||||
|
||||
do --aliases
|
||||
|
||||
--- @alias MissionPriority
|
||||
--- | "none"
|
||||
--- | "primary"
|
||||
--- | "secondary"
|
||||
|
||||
--- @alias MissionType
|
||||
--- | "nil"
|
||||
--- | "STRIKE"
|
||||
--- | "CAS"
|
||||
--- | "BAI"
|
||||
--- | "DEAD"
|
||||
--- | "SAM"
|
||||
--- | "OCA"
|
||||
--- | "LOGISTICS"
|
||||
|
||||
--- @alias MissionState
|
||||
--- | "NEW"
|
||||
--- | "WAITING"
|
||||
--- | "ACTIVE"
|
||||
--- | "COMPLETED"
|
||||
|
||||
end
|
||||
|
||||
return Mission
|
||||
Reference in New Issue
Block a user