diff --git a/DcsTypes.lua b/DcsTypes.lua
index d09f2b1..1bf8544 100644
--- a/DcsTypes.lua
+++ b/DcsTypes.lua
@@ -25,9 +25,12 @@
---@field x number directed to the north
---@field y number directed to the east
---@field z number directed up (away from ground)
-
----@class Position : Vec3
+
+---@class Position
---@field p Vec3 Point on map
+---@field x Vec3 Direction vector
+---@field y Vec3 Direction vector
+---@field z Vec3 Direction vector
---@class Array: { [number]: T }
@@ -216,14 +219,14 @@ do -- world
---@field min Vec3
---@field max Vec3
- ---@class Sphere
+ ---@class Sphere : Volume
---@field params SphereParams
---@class SphereParams
---@field point Vec3
---@field radius number
- ---@class VolumePyramid
+ ---@class VolumePyramid : Volume
---@field params VolumePyramidParams
---@class VolumePyramidParams
@@ -348,7 +351,7 @@ do -- trigger
---@class MarkCommands
---@field markToAll fun(id: number, text:string, point:Vec3, readOnly: boolean?, message: string?) Adds a mark point to all on the F10 map with attached text.
---@field markToCoalition fun(id: number, text: string, point: Vec3, coalitionID: CoalitionSide, readOnly: boolean?, message: string) Adds a mark point to a coalition on the F10 map with attached text.
- ---@field markToGroup fun(id: number, text: string, point: Vec3, groupID: number, readOnly: boolean?, message: string) Adds a mark point to a group on the F10 map with attached text.
+ ---@field markToGroup fun(id: number, text: string, point: Vec3, groupID: number, readOnly: boolean?, message: string?) Adds a mark point to a group on the F10 map with attached text.
---@field removeMark fun(id: number) Removes a mark panel from the f10 map
---@field markupToAll fun(shapeID: ShapeId, coalition: DrawCoalition, id: number, ... : any) Complex parameters. See: https://wiki.hoggitworld.com/view/DCS_func_markupToAll
---@field lineToAll fun(coalition: DrawCoalition, id: number, startPoint:Vec3, endPoint: Vec3, color:table, lineType: LineType, readonly: boolean?, message: string?) Creates a line on the F10 map from one point to another.
diff --git a/_docs/pages/reference.html b/_docs/pages/reference.html
index 3a2d817..fda5751 100644
--- a/_docs/pages/reference.html
+++ b/_docs/pages/reference.html
@@ -78,6 +78,8 @@
+
+ Supply hubs are used as zones to pick up logistics for logistic missions.
+ They are activated when their parent is activated.
+ Activated depends on what it's parent is.
+ If it's a stage. It can be SUPPLYHUB_A_[name] when you want it to be active when the stage is initiated. (red state) or SUPPLYHUB_B_ it you want it activated when it's BLUE only.
+ If you place the Supply Hub trigger zone inside of a FARP zone the supply hub will activate when the FARP zone is activated.
+ If the FARP needs to be built first, then that means the supply hub also is activated later.
+
+ NOTE: The supply hub does not spawn anything, but merely give options to pick up logistics.
+
Mission Types
@@ -258,6 +276,28 @@
The mission will be "pre-activated" (spawned) but won't have a f10 mission menu until the mission(s) it depends on is completed.
+
Buildables
+
+ Current buildables:
+ FARP_
+ BLUESAM_
+
+
+ Buildables are zones or objects that first require logistics.
+ Eg. A forward SAM site or FARP requires logistic crates before it can be built.
+
+ To make an item buildable add a text box inside of the trigger zone and name it: buildable_[freeform]
+ Then in the text box type amount of kilo's you want to be transfered before the logistisc mission is complete.
+ The base will slowly build up with each crate giving both a nice view of it happening AND it's good for performance as there's not a big addition of objects at once.
+ Right now a crate takes 15 seconds to unpack per 500kg. Meaning 2 crates of 1000kg will be faster than 1 crate of 2000kg.
+
+ All helicopters have a max limit set based on quick google searches and cannot be changed but the ME at this point:
+ UH-1H : 2000kg
+ Mi-8 : 4000kg
+ CH-47 : 10000kg
+ MI-24 : 2000kg
+
+
Runway Bombing
Runway bombing can be a very effective OCA tactic. With Spearhead we've tried adding as much logic and detail to it so it will feel as engaging as possible.
diff --git a/_miz/SPEARHEADDEV.miz b/_miz/SPEARHEADDEV.miz
index 093f1d7..7d45243 100644
Binary files a/_miz/SPEARHEADDEV.miz and b/_miz/SPEARHEADDEV.miz differ
diff --git a/classes/capClasses/CapAirbase.lua b/classes/capClasses/CapAirbase.lua
index 25273fa..29741fb 100644
--- a/classes/capClasses/CapAirbase.lua
+++ b/classes/capClasses/CapAirbase.lua
@@ -32,7 +32,11 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
- self.groupNames = database:getCapGroupsAtAirbase(airbaseName)
+ local baseData = database:getAirbaseDataForZone(airbaseName)
+ local groupNames = {}
+ if baseData then groupNames = baseData.CapGroups or {} end
+
+ self.groupNames = groupNames
self.database = database
self.runwayBombingTracker = runwayBombingTracker
self.runwayStrikeMissions = {}
diff --git a/classes/definitions/aliases.lua b/classes/definitions/aliases.lua
index 5459251..bffd2df 100644
--- a/classes/definitions/aliases.lua
+++ b/classes/definitions/aliases.lua
@@ -12,10 +12,5 @@ do -- mission aliases
---@class Array: { [integer]: T }
- --- @class Position
- --- @field x number x position (Top-Down on Map)
- --- @field y number y altitude
- --- @field z number z position (Left-Right on Map)
-
end
diff --git a/classes/persistence/Persistence.lua b/classes/persistence/Persistence.lua
index 851f2dc..5efba98 100644
--- a/classes/persistence/Persistence.lua
+++ b/classes/persistence/Persistence.lua
@@ -8,7 +8,7 @@ do
---@class DeathState
---@field isDead boolean
- ---@field pos Position
+ ---@field pos Vec3
---@field heading number
---@field type string
---@field country_id integer
@@ -204,7 +204,7 @@ do
---Pass the unit to be saved as "dead"
---@param name string
- ---@param position Position { x, y ,z }
+ ---@param position Vec3 { x, y ,z }
---@param heading number
---@param type string
---@param country_id number
diff --git a/classes/spearhead_base.lua b/classes/spearhead_base.lua
index 7ae0884..76ab771 100644
--- a/classes/spearhead_base.lua
+++ b/classes/spearhead_base.lua
@@ -206,6 +206,22 @@ do -- INIT UTIL
return false
end
+ ---@param point Vec2
+ ---@param zone SpearheadTriggerZone
+ function UTIL.is2dPointInZone(point, zone)
+ if zone.zone_type == "Polygon" and zone.verts then
+ if UTIL.IsPointInPolygon(zone.verts, point.x, point.y) == true then
+ return true
+ end
+ else
+ if (((point.x - zone.location.x) ^ 2 + (point.y - zone.location.y) ^ 2) ^ 0.5 <= zone.radius) then
+ return true
+ end
+ end
+
+ return false
+ end
+
---comment
---@param points Array points
---@return Array hullPoints
@@ -974,6 +990,27 @@ do -- INIT DCS_UTIL
return drawID
end
+
+ local _markID = 200
+
+ ---@param groupID number
+ ---@param text string
+ ---@param location Vec3
+ ---@return number markID
+ function DCS_UTIL.AddMarkToGroup(groupID, text, location)
+
+ _markID = _markID + 1
+ trigger.action.markToGroup(_markID, text, location, groupID, true, nil)
+ return _markID
+ end
+
+ ---@param markId number
+ function DCS_UTIL.RemoveMark(markId)
+ if markId ~= nil then
+ trigger.action.removeMark(markId)
+ end
+ end
+
---comment
---@param drawID number
---@param lineColor DrawColor
@@ -1092,6 +1129,19 @@ do -- INIT DCS_UTIL
end
end
+ ---@param unitID number
+ ---@return Unit?
+ function DCS_UTIL.GetPLayerUnitByID(unitID)
+ for i = 0, 2 do
+ local players = coalition.getPlayers(i)
+ for key, unit in pairs(players) do
+ if unit and unit:getID() == unitID then
+ return unit
+ end
+ end
+ end
+ end
+
DCS_UTIL.__INIT();
end
Spearhead.DcsUtil = DCS_UTIL
diff --git a/classes/spearhead_db.lua b/classes/spearhead_db.lua
index baf5279..10b423f 100644
--- a/classes/spearhead_db.lua
+++ b/classes/spearhead_db.lua
@@ -11,6 +11,7 @@
---@field CapDataPerStageNumber table table
---@field CarrierRouteZones Array All Carrier routes zones
---@field BlueSams Array All blue sam zones
+---@field SupplyHubZones Array All supply hub zones
---@field MissionAnnotations table table
---@field AirbaseDataPerAirfield table
---@field BlueSamDataPerZone table
@@ -39,15 +40,19 @@
---@field RandomMissionZones Array
---@field StageIndex string
---@field BlueSamZones Array
+---@field SupplyHubZones Array
+---@field SupplyHubZonesInFarp table
---@field MiscGroups Array
---@class AirbaseData
---@field CapGroups Array
---@field RedGroups Array
---@field BlueGroups Array
+---@field supplyHubNames Array
---@class BlueSamData
----@field Groups Array
+---@field groups Array
+---@field buildingKilos number?
---@class MissionZoneData
---@field Groups Array
@@ -58,6 +63,8 @@
---@class FarpZoneData
---@field groups Array
---@field padNames Array
+---@field buildingKilos number?
+---@field supplyHubNames Array
---@class Database
---@field private _tables DatabaseTables
@@ -87,7 +94,8 @@ function Database.New(Logger)
MissionZoneData = {},
FarpZoneData = {},
missionCodes = {},
- MissionAnnotations = {}
+ MissionAnnotations = {},
+ SupplyHubZones = {}
}
Database.__index = Database
@@ -126,7 +134,9 @@ function Database.New(Logger)
FarpZones = {},
MissionZones = {},
RandomMissionZones = {},
- MiscGroups = {}
+ MiscGroups = {},
+ SupplyHubZones = {},
+ SupplyHubZonesInFarp = {}
}
self._tables.StageZones[zone_name] = stageData
end
@@ -161,25 +171,37 @@ function Database.New(Logger)
if string.lower(split_string[1]) == "bluesam" then
table.insert(self._tables.BlueSams, zone_name)
end
+
+ if string.lower(split_string[1]) == "supplyhub" then
+ table.insert(self._tables.SupplyHubZones, zone_name)
+ end
end
end
self._logger:debug("initiated zone tables, continuing with descriptions")
do --load markers
-
if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do
+ if Spearhead.Util.startswith(string.lower(layer_object.name), "buildable", true) == true then
+ local blueSamData = self:getBlueSamDataForDrawLayer(layer_object)
+ if blueSamData then
+ local number = tonumber(layer_object.text)
+ blueSamData.buildingKilos = number
+ end
- if Spearhead.Util.startswith(string.lower(layer_object.name), "completeat", true) == true then
-
+ local farpData = self:getFarpDataForDrawLayer(layer_object)
+ if farpData then
+ local number = tonumber(layer_object.text)
+ farpData.buildingKilos = number
+ end
+ elseif Spearhead.Util.startswith(string.lower(layer_object.name), "completeat", true) == true then
local annotationData = self:getMissionMetaDataForDrawLayer(layer_object)
if annotationData then
local number = tonumber(layer_object.text)
if number and number > 1 then
number = number / 100
-
end
annotationData.completeAt = number
end
@@ -204,6 +226,12 @@ function Database.New(Logger)
end
end
+ ---@type table
+ local availableSupplyHubs = {}
+ for _, supplyHubZoneName in pairs(self._tables.SupplyHubZones) do
+ availableSupplyHubs[supplyHubZoneName] = true
+ end
+
for _, stageZoneName in pairs(self._tables.StageZoneNames) do
local stageData = self._tables.StageZones[stageZoneName]
if stageData then
@@ -214,6 +242,63 @@ function Database.New(Logger)
end
end
+ --- fill farp zones
+ for _, farpZoneName in pairs(self._tables.AllFarpZones) do
+ if Spearhead.DcsUtil.isZoneInZone(farpZoneName, stageZoneName) then
+ table.insert(stageData.FarpZones, farpZoneName)
+
+ for hubZoneName, available in pairs(availableSupplyHubs) do
+ if available == true and Spearhead.DcsUtil.isZoneInZone(hubZoneName, farpZoneName) == true then
+ local farpZoneData = self:getOrCreateFarpDataForZone(farpZoneName)
+ if farpZoneData then
+ table.insert(farpZoneData.supplyHubNames, hubZoneName)
+ availableSupplyHubs[hubZoneName] = false
+ end
+ end
+ end
+ end
+ end
+
+ -- fill airbases
+ for _, airbase in pairs(world.getAirbases()) do
+ local point = airbase:getPoint()
+
+ if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, stageZoneName) == true then
+ if airbase:getDesc().category == 0 then
+ table.insert(stageData.AirbaseNames, airbase:getName())
+
+ local airbaseZone = Spearhead.DcsUtil.getAirbaseZoneByName(airbase:getName())
+ for hubZoneName, available in pairs(availableSupplyHubs) do
+ local zone = Spearhead.DcsUtil.getZoneByName(hubZoneName)
+ if zone and airbaseZone then
+ if available == true and Spearhead.Util.is2dPointInZone(zone.location, airbaseZone) == true then
+ local airbaseData = self:getOrCreateAirbaseData(airbase:getName())
+ if airbaseData then
+ table.insert(airbaseData.supplyHubNames, hubZoneName)
+ availableSupplyHubs[hubZoneName] = false
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- fill supply hubs
+ for supplyHubZone, available in pairs(availableSupplyHubs) do
+ if available == true and Spearhead.DcsUtil.isZoneInZone(supplyHubZone, stageZoneName) == true then
+ table.insert(stageData.SupplyHubZones, supplyHubZone)
+ end
+ end
+
+ for _, farpZoneName in pairs(stageData.FarpZones) do
+ for _, supplyHubZone in pairs(self._tables.SupplyHubZones) do
+ if Spearhead.DcsUtil.isZoneInZone(supplyHubZone, farpZoneName) == true then
+ stageData.SupplyHubZonesInFarp[supplyHubZone] = farpZoneName
+ end
+ end
+ end
+
-- fill missions
for key, missionZone in pairs(self._tables.MissionZones) do
if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
@@ -227,24 +312,6 @@ function Database.New(Logger)
table.insert(stageData.RandomMissionZones, missionZone)
end
end
-
- --- fill farp zones
- for _, farpZoneName in pairs(self._tables.AllFarpZones) do
- if Spearhead.DcsUtil.isZoneInZone(farpZoneName, stageZoneName) then
- table.insert(stageData.FarpZones, farpZoneName)
- end
- end
-
- -- fill airbases
- for _, airbase in pairs(world.getAirbases()) do
- local point = airbase:getPoint()
-
- if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, stageZoneName) == true then
- if airbase:getDesc().category == 0 then
- table.insert(stageData.AirbaseNames, airbase:getName())
- end
- end
- end
end
end
@@ -268,7 +335,8 @@ function Database.New(Logger)
if self._tables.FarpZoneData[farpZoneName] == nil then
self._tables.FarpZoneData[farpZoneName] = {
groups = {},
- padNames = {}
+ padNames = {},
+ supplyHubNames = {}
}
end
local position = airbase:getPoint()
@@ -288,7 +356,8 @@ function Database.New(Logger)
baseData = {
CapGroups = {},
RedGroups = {},
- BlueGroups = {}
+ BlueGroups = {},
+ supplyHubNames = {}
}
self._tables.AirbaseDataPerAirfield[baseName] = baseData
end
@@ -417,13 +486,65 @@ function Database:initAvailableUnits()
end
end
+---comment
+---@private
+---@param layer_object table
+---@return BlueSamData?
+function Database:getBlueSamDataForDrawLayer(layer_object)
+ for _, zonename in pairs(self._tables.BlueSams) do
+ if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
+ return self:getOrCreateBlueSamDataForZone(zonename)
+ end
+ end
+ return nil
+end
+
+---@private
+---@param layer_object table
+---@return FarpZoneData?
+function Database:getFarpDataForDrawLayer(layer_object)
+ for _, zonename in pairs(self._tables.AllFarpZones) do
+ if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
+ return self:getOrCreateFarpDataForZone(zonename)
+ end
+ end
+ return nil
+end
+
+---@private
+function Database:getOrCreateBlueSamDataForZone(zoneName)
+ local blueSamData = self._tables.BlueSamDataPerZone[zoneName]
+ if blueSamData == nil then
+ blueSamData = {
+ groups = {},
+ buildingCrates = nil
+ }
+ self._tables.BlueSamDataPerZone[zoneName] = blueSamData
+ end
+ return blueSamData
+end
+
+---@private
+function Database:getOrCreateFarpDataForZone(zoneName)
+ local farpData = self._tables.FarpZoneData[zoneName]
+ if farpData == nil then
+ farpData = {
+ padNames = {},
+ groups = {},
+ buildingCrates = nil,
+ supplyHubNames = {}
+ }
+ self._tables.FarpZoneData[zoneName] = farpData
+ end
+ return farpData
+end
+
---@private
---@param layer_object table
---@return MissionAnnotations?
function Database:getMissionMetaDataForDrawLayer(layer_object)
for _, zonename in pairs(self._tables.MissionZones) do
if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
-
if self._tables.MissionAnnotations[zonename] == nil then
self._tables.MissionAnnotations[zonename] = {
description = nil,
@@ -437,7 +558,6 @@ function Database:getMissionMetaDataForDrawLayer(layer_object)
for _, zonename in pairs(self._tables.RandomMissionZones) do
if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
-
if self._tables.MissionAnnotations[zonename] == nil then
self._tables.MissionAnnotations[zonename] = {
description = nil,
@@ -487,13 +607,11 @@ end
function Database:loadBlueSamUnits()
local all_groups = Spearhead.DcsUtil.getAllGroupNames()
for _, blueSamZone in pairs(self._tables.BlueSams) do
- self._tables.BlueSamDataPerZone[blueSamZone] = {
- Groups = {}
- }
+ local samData = self:getOrCreateBlueSamDataForZone(blueSamZone)
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, blueSamZone)
for _, groupName in pairs(groups) do
is_group_taken[groupName] = true
- table.insert(self._tables.BlueSamDataPerZone[blueSamZone].Groups, groupName)
+ table.insert(samData.groups, groupName)
end
end
end
@@ -533,17 +651,12 @@ end
function Database:loadFarpGroups()
local all_groups = getAvailableGroups()
for _, farpZone in pairs(self._tables.AllFarpZones) do
- if self._tables.FarpZoneData[farpZone] == nil then
- self._tables.FarpZoneData[farpZone] = {
- groups = {},
- padNames = {}
- }
- end
+ local farpzoneData = self:getOrCreateFarpDataForZone(farpZone)
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, farpZone)
for _, groupName in pairs(groups) do
is_group_taken[groupName] = true
- table.insert(self._tables.FarpZoneData[farpZone].groups, groupName)
+ table.insert(farpzoneData.groups, groupName)
end
end
end
@@ -628,7 +741,6 @@ function Database:loadMiscGroupsInStages()
end
end
-
---@return string?
function Database:getMissionBriefingForMissionZone(missionZoneName)
if not self._tables.MissionAnnotations[missionZoneName] then
@@ -638,7 +750,6 @@ function Database:getMissionBriefingForMissionZone(missionZoneName)
return self._tables.MissionAnnotations[missionZoneName].description
end
-
---@param missionZoneName string
---@return Array
function Database:getMissionDependencies(missionZoneName)
@@ -757,7 +868,6 @@ function Database:getGroupsForMissionZone(missionZoneName)
return missionZoneData.Groups
end
-
---@param stageName string
---@return table result airbase Names
function Database:getAirbaseNamesInStage(stageName)
@@ -780,14 +890,15 @@ function Database:getFarpDataForZone(farpZoneName)
return farpData
end
----@param airbaseName string
----@return Array
-function Database:getCapGroupsAtAirbase(airbaseName)
- local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName]
- if not airbaseData then return {} end
- return airbaseData.CapGroups
+---@return AirbaseData?
+---@param baseName string
+function Database:getAirbaseDataForZone(baseName)
+ local baseData = self._tables.AirbaseDataPerAirfield[baseName]
+ if not baseData then return nil end
+ return baseData
end
+
---@param stageName string
---@return Array
function Database:getBlueSamsInStage(stageName)
@@ -796,29 +907,33 @@ function Database:getBlueSamsInStage(stageName)
return stageData.BlueSamZones
end
+---@param stageName string
+---@return Array
+function Database:getSupplyHubsInStage(stageName)
+ local stageData = self._tables.StageZones[stageName]
+ if not stageData then return {} end
+ return stageData.SupplyHubZones
+end
+
+---comment
+---@param stageName any
+---@param supplyZoneName any
+---@return nil
+function Database:getFarpDependencyForSupplyHub(stageName, supplyZoneName)
+ local stageData = self._tables.StageZones[stageName]
+ if not stageData then return nil end
+ return stageData.SupplyHubZonesInFarp[supplyZoneName]
+end
+
---@param samZone string
----@return Array
-function Database:getBlueSamGroupsInZone(samZone)
- local blueSamData = self._tables.BlueSamDataPerZone[samZone]
- if not blueSamData then return {} end
- return blueSamData.Groups
+---@return BlueSamData?
+function Database:getBlueSamDataForZone(samZone)
+ return self._tables.BlueSamDataPerZone[samZone]
end
----@param airbaseName string
----@return Array
-function Database:getRedGroupsAtAirbase(airbaseName)
- local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName]
- if not airbaseData then return {} end
- return airbaseData.RedGroups
-end
----@param airbaseName string
----@return Array
-function Database:getBlueGroupsAtAirbase(airbaseName)
- local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName]
- if not airbaseData then return {} end
- return airbaseData.BlueGroups
-end
+
+
function Database:getMiscGroupsAtStage(stageName)
local stageZone = self._tables.StageZones[stageName]
diff --git a/classes/stageClasses/Groups/SpearheadGroup.lua b/classes/stageClasses/Groups/SpearheadGroup.lua
index ee5defd..a915c74 100644
--- a/classes/stageClasses/Groups/SpearheadGroup.lua
+++ b/classes/stageClasses/Groups/SpearheadGroup.lua
@@ -21,6 +21,10 @@ function SpearheadGroup.New(groupName)
return self
end
+function SpearheadGroup:IsSpawned()
+ return self._isSpawned
+end
+
function SpearheadGroup:SpawnCorpsesOnly()
if self._isSpawned == true then return end
@@ -146,6 +150,26 @@ function SpearheadGroup:GetUnits()
return result
end
+---@return Array
+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
+
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Groups then Spearhead.classes.stageClasses.Groups = {} end
diff --git a/classes/stageClasses/SpecialZones/BlueSam.lua b/classes/stageClasses/SpecialZones/BlueSam.lua
index c3b8ad8..00e9850 100644
--- a/classes/stageClasses/SpecialZones/BlueSam.lua
+++ b/classes/stageClasses/SpecialZones/BlueSam.lua
@@ -1,13 +1,21 @@
----@class BlueSam
+---@class BlueSam : OnCrateDroppedListener, MissionCompleteListener
---@field Activate fun(self: BlueSam)
---@field private _database Database
---@field private _logger Logger
---@field private _zoneName string
---@field private _blueGroups Array
---@field private _cleanupUnits table
+---@field private _buildableCrateKilos number?
+---@field private _receivedKilos number?
+---@field private _unitsPerCrate number?
+---@field private _buildableMission BuildableMission?
local BlueSam = {}
+---@param database Database
+---@param logger Logger
+---@param zoneName string
+---@return BlueSam?
function BlueSam.New(database, logger, zoneName)
BlueSam.__index = BlueSam
local self = setmetatable({}, BlueSam)
@@ -19,56 +27,114 @@ function BlueSam.New(database, logger, zoneName)
self._blueGroups = {}
self._cleanupUnits = {}
- do
- local groups = database:getBlueSamGroupsInZone(zoneName)
+ local blueSamData = database:getBlueSamDataForZone(zoneName)
- ---@type table
- local blueUnitsPos = {}
+ if blueSamData == nil then
+ logger:error("Blue SAM data not found for zone: " .. zoneName)
+ return nil
+ end
- ---@type table
- local redUnitsPos = {}
+ self._buildableCrateKilos = blueSamData.buildingKilos
+ self._receivedKilos = 0
- for _, groupName in pairs(groups) do
- local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
- if SpearheadGroup then
-
- if SpearheadGroup:GetCoalition() == 2 then
- table.insert(self._blueGroups, SpearheadGroup)
- end
+ ---@type table
+ local blueUnitsPos = {}
+ ---@type table
+ local redUnitsPos = {}
- for _, unit in pairs(SpearheadGroup:GetUnits()) do
- if SpearheadGroup:GetCoalition() == 1 then
- table.insert(blueUnitsPos, unit:getPoint())
- elseif SpearheadGroup:GetCoalition() == 2 then
- table.insert(redUnitsPos, unit:getPoint())
- end
- end
-
+
+ for _, groupName in pairs(blueSamData.groups) do
+ local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
+ if SpearheadGroup then
+
+ if SpearheadGroup:GetCoalition() == 2 then
+ table.insert(self._blueGroups, SpearheadGroup)
end
- SpearheadGroup:Destroy()
- 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 = Spearhead.Util.VectorDistance3d(blueUnitPos, redUnitPos)
- if distance <= cleanup_distance then
- self._cleanupUnits[redUnitName] = true
- end
+
+ for _, unit in pairs(SpearheadGroup:GetUnits()) 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 = Spearhead.Util.VectorDistance3d(blueUnitPos, redUnitPos)
+ if distance <= cleanup_distance then
+ self._cleanupUnits[redUnitName] = true
+ end
end
end
+ if self._buildableCrateKilos ~= nil and self._buildableCrateKilos ~= 0 then
+ self._logger:debug("Buildable mission creating for zone: " .. zoneName .. " with crates: " .. self._buildableCrateKilos)
+
+ local noLandingZone = self:GetNoLandingZone()
+
+ self._buildableMission = Spearhead.classes.stageClasses.missions.BuildableMission.new(database, logger, zoneName, noLandingZone, self._buildableCrateKilos, "SAM_CRATE")
+ self._buildableMission:AddOnCrateDroppedOfListener(self)
+ self._buildableMission:AddMissionCompleteListener(self)
+ end
+
+
+
return self
end
+---@private
+---@return SpearheadTriggerZone?
+function BlueSam:GetNoLandingZone()
+
+ ---@type Array
+ 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 = Spearhead.Util.getConvexHull(points)
+
+ local zone = Spearhead.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._buildableMission:SpawnActive()
+ end
+
+end
+
+function BlueSam:SpawnGroups()
for unitName, needsCleanup in pairs(self._cleanupUnits) do
if needsCleanup == true then
Spearhead.DcsUtil.DestroyUnit(unitName)
@@ -86,6 +152,24 @@ function BlueSam:Activate()
end
+---@param buildableMission BuildableMission
+---@param kilos number
+function BlueSam:OnCrateDroppedOff(buildableMission, kilos)
+
+ self._logger:debug("Crate dropped off in zone: " .. self._zoneName)
+ self._receivedKilos = self._receivedKilos + kilos
+
+end
+
+---@param buildableMission Mission
+function BlueSam:OnMissionComplete(buildableMission)
+ self._logger:debug("Buildable mission complete for zone: " .. self._zoneName)
+
+ self:SpawnGroups()
+
+end
+
+
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
diff --git a/classes/stageClasses/SpecialZones/FarpZone.lua b/classes/stageClasses/SpecialZones/FarpZone.lua
index ac6d716..456ac11 100644
--- a/classes/stageClasses/SpecialZones/FarpZone.lua
+++ b/classes/stageClasses/SpecialZones/FarpZone.lua
@@ -1,12 +1,17 @@
----@class FarpZone
+---@class FarpZone : OnCrateDroppedListener, MissionCompleteListener
---@field private _startingFarp boolean
---@field private _groups Array
---@field private _padNames Array
---@field private _database Database
---@field private _logger Logger
---@field private _zoneName string
+---@field private _requiredBuildingKilos number
+---@field private _receivedBuildingKilos number
+---@field private _groupsPerKilo number
+---@field private _buildableMission BuildableMission?
+---@field private _supplyHubs Array
local FarpZone = {}
FarpZone.__index = FarpZone
@@ -36,22 +41,50 @@ function FarpZone.New(database, logger, zoneName)
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 = Spearhead.classes.stageClasses.SpecialZones.SupplyHub.new(database, logger, supplyHubName)
+ if supplyHub then
+ table.insert(self._supplyHubs, supplyHub)
+ end
+ end
for _, groupName in pairs(farpData.groups) do
local group = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._groups, group)
group:Destroy()
end
- end
+ self._requiredBuildingKilos = farpData.buildingKilos
+ self._receivedBuildingKilos = 0
+
+ if self._requiredBuildingKilos ~= nil and self._requiredBuildingKilos > 0 then
+ self._logger:debug("FARP zone " .. zoneName .. " requires " .. self._requiredBuildingKilos .. " crates to be dropped off")
+ local noLandingZone = self:GetNoLandingZone()
+ self._buildableMission = Spearhead.classes.stageClasses.missions.BuildableMission.new(database, logger, zoneName, noLandingZone, self._requiredBuildingKilos, "FARP_CRATE")
+ if self._buildableMission then
+ self._buildableMission:AddOnCrateDroppedOfListener(self)
+ self._buildableMission:AddMissionCompleteListener(self)
+ end
+
+ local totalGroups = Spearhead.Util.tableLength(self._groups)
+ self._groupsPerKilo = totalGroups / self._requiredBuildingKilos
+ end
+ end
+ if self._buildableMission == nil then
+ self._logger:debug("No buildable mission for zone: " .. zoneName)
+ end
self:Deactivate()
return self
end
+
---@return boolean
function FarpZone:IsStartingFarp()
return self._startingFarp
@@ -59,14 +92,112 @@ end
function FarpZone:Activate()
self._logger:info("Activating FARP zone: " .. self._zoneName)
- self:BuildUp()
- self:SetPadsBlue()
+ if self._buildableMission == nil then
+ self:BuildUp()
+ self:SetPadsBlue()
+ self:ActivateSupplyHubs()
+ else
+ self._buildableMission:SpawnActive()
+ end
end
function FarpZone:Deactivate()
-
self:NeutralisePads()
+end
+
+---@class UnpackCrateParam
+---@field self FarpZone
+---@field kilos number
+---@field groupsPerKilo number
+---@field kilosPerSecond number
+---@field unpackedItems number
+---@field unpackedKilos number
+
+---@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
+
+---comment
+---@param amount number
+---@return boolean
+function FarpZone:SpawnAmount(amount)
+
+ local function spawnOne()
+ for _, group in pairs(self._groups) 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._logger:debug("No more groups to spawn in zone: " .. self._zoneName)
+ return false
+ end
+ end
+
+ return true
+end
+
+---@param buildableMission BuildableMission
+function FarpZone:OnCrateDroppedOff(buildableMission, kilos)
+
+ self._logger:debug("Crate dropped off in zone: " .. self._zoneName)
+
+ local timeToUnpack = (kilos / 500) * 15
+
+ ---@type UnpackCrateParam
+ local params = {
+ self = self,
+ groupsPerKilo = self._groupsPerKilo,
+ unpackedItems = 0,
+ kilosPerSecond = kilos/timeToUnpack,
+ unpackedKilos = 0,
+ kilos = kilos
+ }
+
+ timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2)
+
+end
+
+---@param kilos number
+function FarpZone:FinaliseCrate(kilos)
+ self._receivedBuildingKilos = self._receivedBuildingKilos + kilos
+ if self._receivedBuildingKilos >= self._requiredBuildingKilos then
+ self:BuildUp()
+ self:SetPadsBlue()
+ self:ActivateSupplyHubs()
+ end
+end
+
+---@param buildableMission Mission
+function FarpZone:OnMissionComplete(buildableMission)
+ self._logger:debug("Buildable mission complete for zone: " .. self._zoneName)
+
+ -- self:BuildUp()
+ -- self:SetPadsBlue()
+ -- self:ActivateSupplyHubs()
end
@@ -76,6 +207,13 @@ function FarpZone:BuildUp()
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
@@ -99,6 +237,39 @@ function FarpZone:SetPadsBlue()
end
+---@private
+---@return SpearheadTriggerZone?
+function FarpZone:GetNoLandingZone()
+
+ ---@type Array
+ local points = {}
+
+ for _, group in pairs(self._groups) do
+ for _, unitPos in pairs(group:GetAllUnitPositions()) do
+ table.insert(points, { x = unitPos.x, y = unitPos.z })
+ end
+ end
+
+ local vecs = Spearhead.Util.getConvexHull(points)
+
+ local zone = Spearhead.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
+
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
diff --git a/classes/stageClasses/SpecialZones/StageBase.lua b/classes/stageClasses/SpecialZones/StageBase.lua
index cb4e126..4589665 100644
--- a/classes/stageClasses/SpecialZones/StageBase.lua
+++ b/classes/stageClasses/SpecialZones/StageBase.lua
@@ -8,13 +8,14 @@
---@field private _cleanup_units table
---@field private _airbase Airbase?
---@field private _initialSide number?
+---@field private _supplyHubs Array
local StageBase = {}
---comment
---@param databaseManager Database
---@param logger table
---@param airbaseName string
----@return StageBase
+---@return StageBase?
function StageBase.New(databaseManager, logger, airbaseName)
StageBase.__index = StageBase
@@ -26,45 +27,51 @@ function StageBase.New(databaseManager, logger, airbaseName)
self._red_groups = {}
self._blue_groups = {}
self._cleanup_units = {}
+ self._supplyHubs = {}
self._airbase = Airbase.getByName(airbaseName)
self._initialSide = Spearhead.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
local redUnitsPos = {}
---@type table
local blueUnitsPos = {}
- do -- fill tables
- local redGroups = databaseManager:getRedGroupsAtAirbase(airbaseName)
- if redGroups then
- for _, groupName in pairs(redGroups) do
- local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
- table.insert(self._red_groups, shGroup)
+ for _, groupName in pairs(airbaseData.RedGroups) do
+ local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
+ table.insert(self._red_groups, shGroup)
- for _, unit in pairs(shGroup:GetUnits()) do
- redUnitsPos[unit:getName()] = unit:getPoint()
- end
-
- shGroup:Destroy()
- end
+ for _, unit in pairs(shGroup:GetUnits()) do
+ redUnitsPos[unit:getName()] = unit:getPoint()
end
- local blueGroups = databaseManager:getBlueGroupsAtAirbase(airbaseName)
- if blueGroups then
- for _, groupName in pairs(blueGroups) do
- local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
- table.insert(self._blue_groups, shGroup)
+ shGroup:Destroy()
+ end
- for _, unit in pairs(shGroup:GetUnits()) do
- blueUnitsPos[unit:getName()] = unit:getPoint()
- end
+ for _, groupName in pairs(airbaseData.BlueGroups) do
+ local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
+ table.insert(self._blue_groups, shGroup)
- shGroup:Destroy()
+ for _, unit in pairs(shGroup:GetUnits()) do
+ blueUnitsPos[unit:getName()] = unit:getPoint()
end
+
+ shGroup:Destroy()
+ end
+
+ for _, supplyHubName in pairs(airbaseData.supplyHubNames) do
+ local supplyHub = Spearhead.classes.stageClasses.SpecialZones.SupplyHub.new(databaseManager, logger, supplyHubName)
+ if supplyHub then
+ table.insert(self._supplyHubs, supplyHub)
end
end
@@ -151,6 +158,10 @@ function StageBase:ActivateBlueStage()
self:CleanRedUnits()
self:SpawnBlueUnits()
+
+ for _, hub in pairs(self._supplyHubs) do
+ hub:Activate()
+ end
end
diff --git a/classes/stageClasses/SpecialZones/SupplyHub.lua b/classes/stageClasses/SpecialZones/SupplyHub.lua
new file mode 100644
index 0000000..8d578b7
--- /dev/null
+++ b/classes/stageClasses/SpecialZones/SupplyHub.lua
@@ -0,0 +1,88 @@
+---@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
+---@field private _missionCommandsHelper MissionCommandsHelper
+---@field private _inZone table
+---@field private _drawID number
+---@field private _cargoInUnits table
>
+---@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 = Spearhead.Util.split_string(zoneName, "_")
+ if string.lower(split[2]) == "a" then
+ self._activeAtStart = true
+ else
+ self._activeAtStart = false
+ end
+
+ self._zone = Spearhead.DcsUtil.getZoneByName(zoneName)
+
+ self._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logger.LogLevel)
+ self._inZone = {}
+ self._missionCommandsHelper = Spearhead.classes.stageClasses.helpers.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 = Spearhead.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 = Spearhead.DcsUtil.DrawZone(zone, lineColor, fillColor, lineStyle)
+ end
+
+ self._supplyUnitsTracker:RegisterHub(self)
+
+end
+
+
+if not Spearhead then Spearhead = {} end
+if Spearhead.classes == nil then Spearhead.classes = {} end
+if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+if Spearhead.classes.stageClasses.SpecialZones == nil then Spearhead.classes.stageClasses.SpecialZones = {} end
+Spearhead.classes.stageClasses.SpecialZones.SupplyHub = SupplyHub
\ No newline at end of file
diff --git a/classes/stageClasses/Stages/BaseStage/Stage.lua b/classes/stageClasses/Stages/BaseStage/Stage.lua
index 4ac05cb..b9c2303 100644
--- a/classes/stageClasses/Stages/BaseStage/Stage.lua
+++ b/classes/stageClasses/Stages/BaseStage/Stage.lua
@@ -14,6 +14,7 @@
--- @field miscGroups Array
--- @field maxMissions integer
--- @field farps Array
+--- @field supplyHubs Array
--- @class StageInitData
--- @field stageZoneName string
@@ -90,7 +91,9 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
maxMissions = stageConfig.maxMissionsPerStage,
farps = {},
missionsByName = {},
+ supplyHubs = {}
}
+
self._activeStage = -99
self._preActivated = false
self._stageConfig = stageConfig or {}
@@ -110,6 +113,11 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
table.insert(self._db.farps, farp)
end
+ local supplyHubNames = database:getSupplyHubsInStage(self.zoneName)
+ for _, supplyHubName in pairs(supplyHubNames) do
+ local supplyHub = Spearhead.classes.stageClasses.SpecialZones.SupplyHub.new(database, logger, supplyHubName)
+ table.insert(self._db.supplyHubs, supplyHub)
+ end
self._logger:info("Initiating new Stage with name: " .. self.zoneName)
@@ -404,6 +412,12 @@ function Stage:ActivateStage()
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
@@ -476,6 +490,10 @@ function Stage:ActivateBlueGroups()
farp:Activate()
end
end
+
+ for _, supplyHub in pairs(self._db.supplyHubs) do
+ supplyHub:Activate()
+ end
end
---@return number strike
diff --git a/classes/stageClasses/Stages/PrimaryStage.lua b/classes/stageClasses/Stages/PrimaryStage.lua
index c8681e7..c25c82e 100644
--- a/classes/stageClasses/Stages/PrimaryStage.lua
+++ b/classes/stageClasses/Stages/PrimaryStage.lua
@@ -18,7 +18,7 @@ function PrimaryStage.New(database, stageConfig, logger, initData)
local self = setmetatable({}, { __index = PrimaryStage }) --[[@as PrimaryStage]]
self:superNew(database, stageConfig, logger, initData, "primary")
return self
-
+
end
if not Spearhead.classes then Spearhead.classes = {} end
diff --git a/classes/stageClasses/helpers/MissionCommandsHelper.lua b/classes/stageClasses/helpers/MissionCommandsHelper.lua
index dbc1d78..6b03b4b 100644
--- a/classes/stageClasses/helpers/MissionCommandsHelper.lua
+++ b/classes/stageClasses/helpers/MissionCommandsHelper.lua
@@ -5,10 +5,13 @@
---@field lastUpdate number @last update time
---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
---@field pinnedByGroup table @table of pinned missions by group ID
+---@field private _supplyHubGroups table @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
+local id = 0
local instance = nil
@@ -16,21 +19,25 @@ local instance = nil
---@param logLevel string @log level for the logger
function MissionCommandsHelper.getOrCreate(logLevel)
if instance == nil then
- local self = setmetatable({}, MissionCommandsHelper)
+ instance = setmetatable({}, MissionCommandsHelper)
- self._logger = Spearhead.LoggerTemplate.new("MissionCommandsHelper", logLevel)
+ instance._logger = Spearhead.LoggerTemplate.new("MissionCommandsHelper", logLevel)
- self.missionsByCode = {}
- self.enabledByCode = {}
- self.updateNeeded = false
- self.pinnedByGroup = {}
- self.lastUpdate = 0
+ instance._logger:info("Creating MissionCommandsHelper instance")
+
+ instance.missionsByCode = {}
+ instance.enabledByCode = {}
+ instance.updateNeeded = false
+ instance.pinnedByGroup = {}
+ instance.lastUpdate = 0
+ instance._supplyHubGroups = {}
+ instance._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logLevel)
---comment
---@param selfA MissionCommandsHelper
---@param time number
---@return number
- self.updateContinuous = function(selfA, time)
+ instance.updateContinuous = function(selfA, time)
if selfA.updateNeeded == false then
return time + 10
end
@@ -49,14 +56,63 @@ function MissionCommandsHelper.getOrCreate(logLevel)
return time + 10
end
- timer.scheduleFunction(self.updateContinuous, self, timer.getTime() + 5)
- Spearhead.Events.AddOnPlayerEnterUnitListener(self)
- instance = self
+ timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5)
+ Spearhead.Events.AddOnPlayerEnterUnitListener(instance)
+
end
return instance
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
@@ -72,6 +128,22 @@ local missionBriefingRequested = function(args)
mission:ShowBriefing(groupID)
end
+---@class MarkRequestedArgs
+---@field mission Mission @the mission object
+---@field groupId integer @the group ID of the player requesting the briefing
+
+---@param args MarkRequestedArgs
+local markRequested = function(args)
+
+ local mission = args.mission
+ if not mission then return end
+
+ if mission.missionType == "LOGISTICS" then
+ mission:MarkMissionAreaToGroup(args.groupId)
+ end
+end
+
+
---@class PinMissionCommandArgs
---@field self MissionCommandsHelper @the MissionCommandsHelper instance
@@ -114,7 +186,7 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
end
end
- return string.format("[%s] %-15s %-20s %10s nM\n", mission.code, mission.missionTypeDisplay, mission.name, distanceText)
+ return string.format("[%s]\t%s \t%s \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name, distanceText)
end
---Primary missions
@@ -161,11 +233,11 @@ function MissionCommandsHelper:AddPinnedMission(groupID)
end
-
----comment
---@param groupID number
function MissionCommandsHelper:updateCommandsForGroup(groupID)
+ self._logger:debug("Updating commands for group: " .. tostring(groupID))
+
self:AddPinnedMission(groupID)
self:AddOverviewCommand(groupID)
@@ -180,6 +252,9 @@ function MissionCommandsHelper:updateCommandsForGroup(groupID)
end
end
+ self:AddSupplyHubCommandsIfApplicable(groupID)
+ self:AddCargoCommands(groupID)
+
---@param id number
local clearView = function(id)
trigger.action.outTextForGroup(id, "clearing...", 1, true)
@@ -192,7 +267,9 @@ end
local folderNames = {
primary = "Primary Missions",
- secondary = "Secondary Missions"
+ secondary = "Secondary Missions",
+ supplyHub = "Supply Hub",
+ cargo = "Cargo"
}
@@ -221,31 +298,135 @@ function MissionCommandsHelper:addMissionCommands(groupId, mission)
local missionFolderName = "[" .. mission.code .. "]" .. mission.name
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName)
- missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,{ groupId = groupId, mission = mission })
- missionCommands.addCommandForGroup(groupId, "Pin", path, pinMissionCommand, { self = self, groupId = groupId, mission = mission })
+
+ ---@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)
+
+ if mission.missionType == "LOGISTICS" then
+ ---@type MarkRequestedArgs
+ local markRequestArgs = { groupId = groupId, mission = mission }
+ missionCommands.addCommandForGroup(groupId, "Mark", path, markRequested, markRequestArgs)
+ end
+
end
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
+---@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 = Spearhead.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
+
+ ---comment
+ ---@param params LoadCargoCommandParams
+ local loadCargoCommand = function(params)
+ local crateType = params.crateType
+ local supplyUnitsTracker = params.supplyUnitsTracker
+ if supplyUnitsTracker then
+ supplyUnitsTracker:UnitRequestCrateLoading(params.groupID, crateType)
+ end
+ end
+
+ local path = { [1] = folderNames.supplyHub }
+
+
+ ---@type LoadCargoCommandParams
+ local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker }
+ 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 }
+ missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
+
+ ---@type LoadCargoCommandParams
+ local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
+
+ ---@type LoadCargoCommandParams
+ local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
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
+function MissionCommandsHelper:AddCargoCommands(groupID)
+
+ local group = Spearhead.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
+
+ ---comment
+ ---@param params UnloadCargoCommandParams
+ local unloadCargoCommand = function(params)
+ local unitID = params.unitID
+ local crateType = params.crateType
+ params.supplyUnitsTracker:UnloadRequested(unitID, crateType)
+ end
+
+ local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID())
+ if cargo then
+ for cargoType, amount in pairs(cargo) do
+ local cargoConfig = Spearhead.classes.stageClasses.helpers.supplies.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 }
+ 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 = Spearhead.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
@@ -253,6 +434,8 @@ end
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
@@ -264,15 +447,6 @@ function MissionCommandsHelper:ResetFolders(groupID)
self:addMissionFolders(groupID)
end
----comment
----@param unit Unit
-function MissionCommandsHelper:OnPlayerEntersUnit(unit)
- if unit then
- local group = unit:getGroup()
- if group then self:updateCommandsForGroup(group:getID()) end
- end
-end
-
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
diff --git a/classes/stageClasses/helpers/SupplyConfig.lua b/classes/stageClasses/helpers/SupplyConfig.lua
new file mode 100644
index 0000000..97908a1
--- /dev/null
+++ b/classes/stageClasses/helpers/SupplyConfig.lua
@@ -0,0 +1,106 @@
+
+---@alias SupplyType
+---| "FARP_CRATE"
+---| "SAM_CRATE"
+
+---@alias CrateType
+---| "FARP_CRATE_500"
+---| "FARP_CRATE_1000"
+---| "FARP_CRATE_2000"
+---| "SAM_CRATE_500"
+---| "SAM_CRATE_1000"
+---| "SAM_CRATE_2000"
+
+---@class SupplyConfig
+---@field type SupplyType
+---@field weight number
+---@field staticType string
+---@field displayName string
+
+---@type table
+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",
+ },
+}
+
+---@class MaxLoadConfig
+---@field maxInternalLoad number
+
+---@type table
+MaxLoadConfig = {
+ ["Mi-8MT"] = {
+ maxInternalLoad = 4000,
+ },
+ ["CH-47Fbl1"] = {
+ maxInternalLoad = 10000
+ },
+ ["Mi-24P"] = {
+ maxInternalLoad = 2000
+ },
+ ["UH-1H"] = {
+ maxInternalLoad = 2000
+ }
+}
+
+---@class SupplyConfigHelper
+local SupplyConfigHelper = {}
+
+---comment
+---@param name string
+---@return SupplyConfig?
+function SupplyConfigHelper.fromObjectName(name)
+ for configName, config in pairs(SupplyConfig) do
+ if Spearhead.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
+
+if Spearhead == nil then Spearhead = {} end
+if Spearhead.classes == nil then Spearhead.classes = {} end
+if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
+if Spearhead.classes.stageClasses.helpers.supplies == nil then Spearhead.classes.stageClasses.helpers.supplies = {} end
+Spearhead.classes.stageClasses.helpers.supplies.MaxLoadConfig = MaxLoadConfig
+Spearhead.classes.stageClasses.helpers.supplies.SupplyConfigHelper = SupplyConfigHelper
+
diff --git a/classes/stageClasses/helpers/SupplyUnitsTracker.lua b/classes/stageClasses/helpers/SupplyUnitsTracker.lua
new file mode 100644
index 0000000..4374429
--- /dev/null
+++ b/classes/stageClasses/helpers/SupplyUnitsTracker.lua
@@ -0,0 +1,407 @@
+---@class SupplyUnitsTracker
+---@field private _supplyUnitsByName table
+---@field private _cargoInUnits table>
+---@field private _logger Logger
+---@field private _unitPositions table
+---@field private _commandsHelper MissionCommandsHelper
+---@field private _droppedCrates table
+---@field private _registeredHubs table
+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 = Spearhead.LoggerTemplate.new("SupplyUnitsTracker", logLevel)
+ singleton._unitPositions = {}
+ singleton._cargoInUnits = {}
+ singleton._supplyUnitsByName = {}
+ singleton._droppedCrates = {}
+ singleton._registeredHubs = {}
+
+ singleton._commandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(singleton._logger.LogLevel)
+
+ Spearhead.Events.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
+ end
+end
+
+function SupplyUnitsTracker:Update()
+ local players = Spearhead.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
+function SupplyUnitsTracker:IsSupplyUnit(unit)
+ if unit == nil then return false end
+
+ if unit:hasAttribute("Transport helicopters") 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 = Spearhead.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
+
+---@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
+
+
+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 Spearhead.Util.is3dPointInZone(pos, zone) then
+ self._commandsHelper:MarkUnitInSupplyHub(group:getID())
+ else
+ self._commandsHelper:MarkUnitOutsideSupplyHub(group:getID())
+ 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?
+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
+function SupplyUnitsTracker:GetUnits()
+ return self._supplyUnitsByName
+end
+
+local cargoCount = 0
+function SupplyUnitsTracker:UnloadRequested(unitID, crateType)
+
+ self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
+
+ local unit = Spearhead.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)
+
+ local cargoConfig = Spearhead.classes.stageClasses.helpers.supplies.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
+ self._commandsHelper:updateCommandsForGroup(group:getID())
+end
+
+---@return table
+function SupplyUnitsTracker:GetCargoCratesDropped()
+ return self._droppedCrates
+end
+
+---Loads a crate directly into the unit
+---@param groupID number
+---@param crateType CrateType
+function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType)
+
+ self._logger:debug("UnitRequestCrateLoading called with groupID: " .. groupID .. " and crateType: " .. crateType)
+
+ local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ if group ~= nil then
+
+ local crateConfig = Spearhead.classes.stageClasses.helpers.supplies.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
+
+ ---@param params LoadCargoParams
+ local LoadCrateTask = function(params)
+
+ local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType)
+ 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
+ }
+
+ timer.scheduleFunction(LoadCrateTask, params, timer.getTime() + 15)
+
+ end
+end
+
+---comment
+---@param unit Unit
+---@param crateType CrateType
+---@return boolean
+function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType)
+
+ local crateConfigA = Spearhead.classes.stageClasses.helpers.supplies.SupplyConfigHelper.getSupplyConfig(crateType)
+ if crateConfigA == nil then
+ trigger.action.outText("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 = Spearhead.classes.stageClasses.helpers.supplies.MaxLoadConfig[unit:getTypeName()]
+ if unitConfig == nil then
+ trigger.action.outText("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.outText("Failed to load crate due to it overloading your max weight of: " .. maxWeight .. "kg", 5)
+ return false
+ end
+
+ trigger.action.setUnitInternalCargo(unit:getName(), crateConfigA.weight)
+ self:AddCargoToUnit(unit:getID(), crateType)
+
+ local group = unit:getGroup()
+ if group == nil then return false end
+ local groupID = group:getID()
+ self._commandsHelper:updateCommandsForGroup(groupID)
+
+ return true
+end
+
+---Spawns a crate for sling loading
+---@param groupID number
+---@param crateType CrateType
+function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
+
+ local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ if group == nil then
+
+ local crateConfig = Spearhead.classes.stageClasses.helpers.supplies.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 preferedPos = {
+ 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 preferedPos
+
+
+ -- local volume = {
+ -- id = world.VolumeType.SPHERE,
+ -- params = {
+ -- point = preferedPos,
+ -- 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
+
+
+
+if Spearhead == nil then Spearhead = {} end
+if Spearhead.classes == nil then Spearhead.classes = {} end
+if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
+Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker = SupplyUnitsTracker
\ No newline at end of file
diff --git a/classes/stageClasses/missions/BuildableMission.lua b/classes/stageClasses/missions/BuildableMission.lua
new file mode 100644
index 0000000..a5deb86
--- /dev/null
+++ b/classes/stageClasses/missions/BuildableMission.lua
@@ -0,0 +1,229 @@
+
+
+---@class BuildableMission : Mission
+---@field private _requiredKilos number
+---@field private _droppedKilos number
+---@field private _crateType SupplyType
+---@field private _targetZoneName string
+---@field private _database Database
+---@field private _onCrateDroppedOfListeners Array
+---@field private _markIDsPerGroup table
+---@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 targetZoneName string
+---@param requiredKilos number
+---@param requiredCrateType SupplyType
+---@param noLandingZone SpearheadTriggerZone?
+---@param logger Logger
+function BuildableMission.new(database, logger, targetZoneName, noLandingZone, requiredKilos, requiredCrateType)
+
+ local Mission = Spearhead.classes.stageClasses.missions.baseMissions.Mission
+ setmetatable(BuildableMission, Mission)
+
+ local self = setmetatable({}, { __index = BuildableMission })
+
+ self._targetZoneName = targetZoneName
+ self._database = database
+ self._requiredKilos = requiredKilos
+ self._droppedKilos = 0
+
+ self._noLandingZone = noLandingZone
+
+ if noLandingZone then
+
+ local verts = noLandingZone.verts
+ local enlarged = Spearhead.Util.enlargeConvexHull(verts, 300)
+
+ ---@type SpearheadTriggerZone
+ local dropOfZone = {
+ name = targetZoneName .. "_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 = targetZoneName .. "_supply"
+ self._logger = logger
+ self._onCrateDroppedOfListeners = {}
+ self._completeListeners = {}
+ self._markIDsPerGroup = {}
+ self._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logger.LogLevel)
+ self._state = "NEW"
+
+ local spearheadZone = Spearhead.DcsUtil.getZoneByName(targetZoneName)
+ if spearheadZone == nil then
+ logger:error("Zone not found: " .. targetZoneName)
+ return nil
+ end
+
+ self.location = spearheadZone.location
+
+ self.missionType = "LOGISTICS"
+ self.missionTypeDisplay = "LOGISTICS"
+
+ self.priority = "secondary"
+
+ self._missionCommandsHelper = Spearhead.classes.stageClasses.helpers.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 = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ if group == nil then return end
+
+ local unitType = Spearhead.DcsUtil.getUnitTypeFromGroup(group)
+ local coords = Spearhead.DcsUtil.convertVec2ToUnitUsableType(self.location, unitType)
+
+ local siteType = "FARP"
+ if self._crateType == "SAM_CRATE" then
+ siteType = "SAM site"
+ 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, 30)
+end
+
+function BuildableMission:MarkMissionAreaToGroup(groupID)
+
+ local text = "[" .. self.code .. "] " .. self.name
+ local location = { x= self.location.x, y=land.getHeight(self.location), z=self.location.y }
+ local markID = Spearhead.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()
+
+ self._logger:debug("Spawning buildable mission: " .. self.code)
+
+ if self._noLandingZone == nil then
+ self._logger:error("No no landing 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 = Spearhead.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 = Spearhead.DcsUtil.DrawZone(self._dropOffZone, lineColor2, fillColor2, 6)
+
+ ---@param selfA BuildableMission
+ ---@param time number
+ local checkForCrateTasks = function (selfA, time)
+ selfA:CheckCratesInZone()
+ return time + 10
+ end
+
+ timer.scheduleFunction(checkForCrateTasks, self, timer.getTime() + 10)
+
+ self:SpawnForwardUnits()
+ self._state = "ACTIVE"
+
+ self._missionCommandsHelper:AddMissionToCommands(self)
+
+end
+
+function BuildableMission:SpawnForwardUnits()
+
+end
+
+function BuildableMission:CheckCratesInZone()
+
+ ---@type Array