Buildable (#36)

- Buildable missions now working. 
- FARPS and BLUESAM zones are now buildable. 
- Secondary Logistics missions are created as buildables become available. 
- Logistic missions can be "marked" with a MAP marker. 
- Supplies can only be picked up in a SUPPLYHUB zone. 
- SUPPLYHUB zones are active once their parents are active. A supplyhub parent can be oneof: FARP, AIRBASE, STAGE. 
- Required supplies for a zone can be configured in kilos. 
- Crates can be picked up with 1000 or 2000 kilo per crate. 
- Max load for a helicopter is set based on public data. 
- TO COME: Sling load options
This commit is contained in:
2025-05-18 20:08:26 +02:00
committed by GitHub
parent 5b6ea49217
commit 3e65df9d20
21 changed files with 1708 additions and 181 deletions
+7 -4
View File
@@ -26,8 +26,11 @@
---@field y number directed to the east ---@field y number directed to the east
---@field z number directed up (away from ground) ---@field z number directed up (away from ground)
---@class Position : Vec3 ---@class Position
---@field p Vec3 Point on map ---@field p Vec3 Point on map
---@field x Vec3 Direction vector
---@field y Vec3 Direction vector
---@field z Vec3 Direction vector
---@class Array<T>: { [number]: T } ---@class Array<T>: { [number]: T }
@@ -216,14 +219,14 @@ do -- world
---@field min Vec3 ---@field min Vec3
---@field max Vec3 ---@field max Vec3
---@class Sphere ---@class Sphere : Volume
---@field params SphereParams ---@field params SphereParams
---@class SphereParams ---@class SphereParams
---@field point Vec3 ---@field point Vec3
---@field radius number ---@field radius number
---@class VolumePyramid ---@class VolumePyramid : Volume
---@field params VolumePyramidParams ---@field params VolumePyramidParams
---@class VolumePyramidParams ---@class VolumePyramidParams
@@ -348,7 +351,7 @@ do -- trigger
---@class MarkCommands ---@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 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 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 removeMark fun(id: number) Removes a mark panel from the f10 map
---@field markupToAll fun(shapeID: ShapeId, coalition: DrawCoalition, id: number, ... : any) Complex parameters. <br/> See: https://wiki.hoggitworld.com/view/DCS_func_markupToAll ---@field markupToAll fun(shapeID: ShapeId, coalition: DrawCoalition, id: number, ... : any) Complex parameters. <br/> 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. ---@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.
+40
View File
@@ -78,6 +78,8 @@
</ul> </ul>
</li> </li>
<li><a href="#randomization" class="side-nav-h2">Randomization</a></li> <li><a href="#randomization" class="side-nav-h2">Randomization</a></li>
<li><a href="#in-stage-dependencies" class="side-nav-h2">In stage dependencies</a></li>
<li><a href="#buildable" class="side-nav-h2">Buildables</a></li>
<li><a href="#runway-bombing" class="side-nav-h2">Runway Bombing</a></li> <li><a href="#runway-bombing" class="side-nav-h2">Runway Bombing</a></li>
</ul> </ul>
</div> </div>
@@ -165,6 +167,22 @@
</p> </p>
<h3 id="supply-hub">Supply Hub</h3>
<p>
<strong>Format:</strong> <span class="inline-lua"><span class="lua-variable">SUPPLYHUB_[Name]</span></span> <br />
<strong>Format:</strong> <span class="inline-lua"><span class="lua-variable">SUPPLYHUB_A_[Name]</span></span> <br />
<strong>Example:</strong> <span class="inline-lua"><span class="lua-variable">SUPPLYHUB_HOUSTON</span></span> <br/> <br/>
Supply hubs are used as zones to pick up logistics for logistic missions. <br/>
They are activated when their parent is activated. <br/>
Activated depends on what it's parent is. <br/>
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. <br/>
If you place the Supply Hub trigger zone inside of a FARP zone the supply hub will activate when the FARP zone is activated. <br/>
If the FARP needs to be built first, then that means the supply hub also is activated later. <br/>
NOTE: The supply hub does not spawn anything, but merely give options to pick up logistics.
</p>
<h2 id="mission-types">Mission Types</h2> <h2 id="mission-types">Mission Types</h2>
<p> <p>
Each mission type has specific logic and completion criteria. Below are the supported types: Each mission type has specific logic and completion criteria. Below are the supported 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. <br/> The mission will be "pre-activated" (spawned) but won't have a f10 mission menu until the mission(s) it depends on is completed. <br/>
</p> </p>
<h2 id="buildable">Buildables</h2>
<p>
Current buildables: <br/>
<span class="inline-lua"><span class="lua-variable">FARP_</span></span> <br/>
<span class="inline-lua"><span class="lua-variable">BLUESAM_</span></span> <br/>
<br/>
Buildables are zones or objects that first require logistics. <br/>
Eg. A forward SAM site or FARP requires logistic crates before it can be built. <br/>
To make an item buildable add a text box inside of the trigger zone and name it: <span class="inline-lua"><span class="lua-variable">buildable_[freeform]</span></span> <br/>
Then in the text box type amount of kilo's you want to be transfered before the logistisc mission is complete. <br/>
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. <br/>
Right now a crate takes 15 seconds to unpack per 500kg. Meaning 2 crates of 1000kg will be faster than 1 crate of 2000kg. <br/>
All helicopters have a max limit set based on quick google searches and cannot be changed but the ME at this point: <br/>
UH-1H : 2000kg <br/>
Mi-8 : 4000kg <br/>
CH-47 : 10000kg <br/>
MI-24 : 2000kg <br/>
</p>
<h2>Runway Bombing</h2> <h2>Runway Bombing</h2>
<p> <p>
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. <br/> 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. <br/>
Binary file not shown.
+5 -1
View File
@@ -32,7 +32,11 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
CapBase.__index = CapBase CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as 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.database = database
self.runwayBombingTracker = runwayBombingTracker self.runwayBombingTracker = runwayBombingTracker
self.runwayStrikeMissions = {} self.runwayStrikeMissions = {}
-5
View File
@@ -12,10 +12,5 @@ do -- mission aliases
---@class Array<T>: { [integer]: T } ---@class Array<T>: { [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 end
+2 -2
View File
@@ -8,7 +8,7 @@ do
---@class DeathState ---@class DeathState
---@field isDead boolean ---@field isDead boolean
---@field pos Position ---@field pos Vec3
---@field heading number ---@field heading number
---@field type string ---@field type string
---@field country_id integer ---@field country_id integer
@@ -204,7 +204,7 @@ do
---Pass the unit to be saved as "dead" ---Pass the unit to be saved as "dead"
---@param name string ---@param name string
---@param position Position { x, y ,z } ---@param position Vec3 { x, y ,z }
---@param heading number ---@param heading number
---@param type string ---@param type string
---@param country_id number ---@param country_id number
+50
View File
@@ -206,6 +206,22 @@ do -- INIT UTIL
return false return false
end 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 ---comment
---@param points Array<Vec2> points ---@param points Array<Vec2> points
---@return Array<Vec2> hullPoints ---@return Array<Vec2> hullPoints
@@ -974,6 +990,27 @@ do -- INIT DCS_UTIL
return drawID return drawID
end 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 ---comment
---@param drawID number ---@param drawID number
---@param lineColor DrawColor ---@param lineColor DrawColor
@@ -1092,6 +1129,19 @@ do -- INIT DCS_UTIL
end end
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(); DCS_UTIL.__INIT();
end end
Spearhead.DcsUtil = DCS_UTIL Spearhead.DcsUtil = DCS_UTIL
+183 -68
View File
@@ -11,6 +11,7 @@
---@field CapDataPerStageNumber table<integer, CapData> table<StageNumber, table> ---@field CapDataPerStageNumber table<integer, CapData> table<StageNumber, table>
---@field CarrierRouteZones Array<string> All Carrier routes zones ---@field CarrierRouteZones Array<string> All Carrier routes zones
---@field BlueSams Array<string> All blue sam zones ---@field BlueSams Array<string> All blue sam zones
---@field SupplyHubZones Array<string> All supply hub zones
---@field MissionAnnotations table<string, MissionAnnotations> table<ZoneName, MissionAnnotations> ---@field MissionAnnotations table<string, MissionAnnotations> table<ZoneName, MissionAnnotations>
---@field AirbaseDataPerAirfield table<string, AirbaseData> ---@field AirbaseDataPerAirfield table<string, AirbaseData>
---@field BlueSamDataPerZone table<string, BlueSamData> ---@field BlueSamDataPerZone table<string, BlueSamData>
@@ -39,15 +40,19 @@
---@field RandomMissionZones Array<string> ---@field RandomMissionZones Array<string>
---@field StageIndex string ---@field StageIndex string
---@field BlueSamZones Array<string> ---@field BlueSamZones Array<string>
---@field SupplyHubZones Array<string>
---@field SupplyHubZonesInFarp table<string, string>
---@field MiscGroups Array<string> ---@field MiscGroups Array<string>
---@class AirbaseData ---@class AirbaseData
---@field CapGroups Array<string> ---@field CapGroups Array<string>
---@field RedGroups Array<string> ---@field RedGroups Array<string>
---@field BlueGroups Array<string> ---@field BlueGroups Array<string>
---@field supplyHubNames Array<string>
---@class BlueSamData ---@class BlueSamData
---@field Groups Array<string> ---@field groups Array<string>
---@field buildingKilos number?
---@class MissionZoneData ---@class MissionZoneData
---@field Groups Array<string> ---@field Groups Array<string>
@@ -58,6 +63,8 @@
---@class FarpZoneData ---@class FarpZoneData
---@field groups Array<string> ---@field groups Array<string>
---@field padNames Array<string> ---@field padNames Array<string>
---@field buildingKilos number?
---@field supplyHubNames Array<string>
---@class Database ---@class Database
---@field private _tables DatabaseTables ---@field private _tables DatabaseTables
@@ -87,7 +94,8 @@ function Database.New(Logger)
MissionZoneData = {}, MissionZoneData = {},
FarpZoneData = {}, FarpZoneData = {},
missionCodes = {}, missionCodes = {},
MissionAnnotations = {} MissionAnnotations = {},
SupplyHubZones = {}
} }
Database.__index = Database Database.__index = Database
@@ -126,7 +134,9 @@ function Database.New(Logger)
FarpZones = {}, FarpZones = {},
MissionZones = {}, MissionZones = {},
RandomMissionZones = {}, RandomMissionZones = {},
MiscGroups = {} MiscGroups = {},
SupplyHubZones = {},
SupplyHubZonesInFarp = {}
} }
self._tables.StageZones[zone_name] = stageData self._tables.StageZones[zone_name] = stageData
end end
@@ -161,25 +171,37 @@ function Database.New(Logger)
if string.lower(split_string[1]) == "bluesam" then if string.lower(split_string[1]) == "bluesam" then
table.insert(self._tables.BlueSams, zone_name) table.insert(self._tables.BlueSams, zone_name)
end end
if string.lower(split_string[1]) == "supplyhub" then
table.insert(self._tables.SupplyHubZones, zone_name)
end
end end
end end
self._logger:debug("initiated zone tables, continuing with descriptions") self._logger:debug("initiated zone tables, continuing with descriptions")
do --load markers do --load markers
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do 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) local annotationData = self:getMissionMetaDataForDrawLayer(layer_object)
if annotationData then if annotationData then
local number = tonumber(layer_object.text) local number = tonumber(layer_object.text)
if number and number > 1 then if number and number > 1 then
number = number / 100 number = number / 100
end end
annotationData.completeAt = number annotationData.completeAt = number
end end
@@ -204,6 +226,12 @@ function Database.New(Logger)
end end
end end
---@type table<string, boolean>
local availableSupplyHubs = {}
for _, supplyHubZoneName in pairs(self._tables.SupplyHubZones) do
availableSupplyHubs[supplyHubZoneName] = true
end
for _, stageZoneName in pairs(self._tables.StageZoneNames) do for _, stageZoneName in pairs(self._tables.StageZoneNames) do
local stageData = self._tables.StageZones[stageZoneName] local stageData = self._tables.StageZones[stageZoneName]
if stageData then if stageData then
@@ -214,6 +242,63 @@ function Database.New(Logger)
end end
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 -- fill missions
for key, missionZone in pairs(self._tables.MissionZones) do for key, missionZone in pairs(self._tables.MissionZones) do
if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
@@ -227,24 +312,6 @@ function Database.New(Logger)
table.insert(stageData.RandomMissionZones, missionZone) table.insert(stageData.RandomMissionZones, missionZone)
end end
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
end end
@@ -268,7 +335,8 @@ function Database.New(Logger)
if self._tables.FarpZoneData[farpZoneName] == nil then if self._tables.FarpZoneData[farpZoneName] == nil then
self._tables.FarpZoneData[farpZoneName] = { self._tables.FarpZoneData[farpZoneName] = {
groups = {}, groups = {},
padNames = {} padNames = {},
supplyHubNames = {}
} }
end end
local position = airbase:getPoint() local position = airbase:getPoint()
@@ -288,7 +356,8 @@ function Database.New(Logger)
baseData = { baseData = {
CapGroups = {}, CapGroups = {},
RedGroups = {}, RedGroups = {},
BlueGroups = {} BlueGroups = {},
supplyHubNames = {}
} }
self._tables.AirbaseDataPerAirfield[baseName] = baseData self._tables.AirbaseDataPerAirfield[baseName] = baseData
end end
@@ -417,13 +486,65 @@ function Database:initAvailableUnits()
end end
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 ---@private
---@param layer_object table ---@param layer_object table
---@return MissionAnnotations? ---@return MissionAnnotations?
function Database:getMissionMetaDataForDrawLayer(layer_object) function Database:getMissionMetaDataForDrawLayer(layer_object)
for _, zonename in pairs(self._tables.MissionZones) do for _, zonename in pairs(self._tables.MissionZones) do
if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
if self._tables.MissionAnnotations[zonename] == nil then if self._tables.MissionAnnotations[zonename] == nil then
self._tables.MissionAnnotations[zonename] = { self._tables.MissionAnnotations[zonename] = {
description = nil, description = nil,
@@ -437,7 +558,6 @@ function Database:getMissionMetaDataForDrawLayer(layer_object)
for _, zonename in pairs(self._tables.RandomMissionZones) do for _, zonename in pairs(self._tables.RandomMissionZones) do
if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then
if self._tables.MissionAnnotations[zonename] == nil then if self._tables.MissionAnnotations[zonename] == nil then
self._tables.MissionAnnotations[zonename] = { self._tables.MissionAnnotations[zonename] = {
description = nil, description = nil,
@@ -487,13 +607,11 @@ end
function Database:loadBlueSamUnits() function Database:loadBlueSamUnits()
local all_groups = Spearhead.DcsUtil.getAllGroupNames() local all_groups = Spearhead.DcsUtil.getAllGroupNames()
for _, blueSamZone in pairs(self._tables.BlueSams) do for _, blueSamZone in pairs(self._tables.BlueSams) do
self._tables.BlueSamDataPerZone[blueSamZone] = { local samData = self:getOrCreateBlueSamDataForZone(blueSamZone)
Groups = {}
}
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, blueSamZone) local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, blueSamZone)
for _, groupName in pairs(groups) do for _, groupName in pairs(groups) do
is_group_taken[groupName] = true is_group_taken[groupName] = true
table.insert(self._tables.BlueSamDataPerZone[blueSamZone].Groups, groupName) table.insert(samData.groups, groupName)
end end
end end
end end
@@ -533,17 +651,12 @@ end
function Database:loadFarpGroups() function Database:loadFarpGroups()
local all_groups = getAvailableGroups() local all_groups = getAvailableGroups()
for _, farpZone in pairs(self._tables.AllFarpZones) do for _, farpZone in pairs(self._tables.AllFarpZones) do
if self._tables.FarpZoneData[farpZone] == nil then local farpzoneData = self:getOrCreateFarpDataForZone(farpZone)
self._tables.FarpZoneData[farpZone] = {
groups = {},
padNames = {}
}
end
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, farpZone) local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, farpZone)
for _, groupName in pairs(groups) do for _, groupName in pairs(groups) do
is_group_taken[groupName] = true is_group_taken[groupName] = true
table.insert(self._tables.FarpZoneData[farpZone].groups, groupName) table.insert(farpzoneData.groups, groupName)
end end
end end
end end
@@ -628,7 +741,6 @@ function Database:loadMiscGroupsInStages()
end end
end end
---@return string? ---@return string?
function Database:getMissionBriefingForMissionZone(missionZoneName) function Database:getMissionBriefingForMissionZone(missionZoneName)
if not self._tables.MissionAnnotations[missionZoneName] then if not self._tables.MissionAnnotations[missionZoneName] then
@@ -638,7 +750,6 @@ function Database:getMissionBriefingForMissionZone(missionZoneName)
return self._tables.MissionAnnotations[missionZoneName].description return self._tables.MissionAnnotations[missionZoneName].description
end end
---@param missionZoneName string ---@param missionZoneName string
---@return Array<string> ---@return Array<string>
function Database:getMissionDependencies(missionZoneName) function Database:getMissionDependencies(missionZoneName)
@@ -757,7 +868,6 @@ function Database:getGroupsForMissionZone(missionZoneName)
return missionZoneData.Groups return missionZoneData.Groups
end end
---@param stageName string ---@param stageName string
---@return table result airbase Names ---@return table result airbase Names
function Database:getAirbaseNamesInStage(stageName) function Database:getAirbaseNamesInStage(stageName)
@@ -780,14 +890,15 @@ function Database:getFarpDataForZone(farpZoneName)
return farpData return farpData
end end
---@param airbaseName string ---@return AirbaseData?
---@return Array<string> ---@param baseName string
function Database:getCapGroupsAtAirbase(airbaseName) function Database:getAirbaseDataForZone(baseName)
local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName] local baseData = self._tables.AirbaseDataPerAirfield[baseName]
if not airbaseData then return {} end if not baseData then return nil end
return airbaseData.CapGroups return baseData
end end
---@param stageName string ---@param stageName string
---@return Array<string> ---@return Array<string>
function Database:getBlueSamsInStage(stageName) function Database:getBlueSamsInStage(stageName)
@@ -796,29 +907,33 @@ function Database:getBlueSamsInStage(stageName)
return stageData.BlueSamZones return stageData.BlueSamZones
end end
---@param stageName string
---@return Array<string>
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 ---@param samZone string
---@return Array<string> ---@return BlueSamData?
function Database:getBlueSamGroupsInZone(samZone) function Database:getBlueSamDataForZone(samZone)
local blueSamData = self._tables.BlueSamDataPerZone[samZone] return self._tables.BlueSamDataPerZone[samZone]
if not blueSamData then return {} end
return blueSamData.Groups
end end
---@param airbaseName string
---@return Array<string>
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<string>
function Database:getBlueGroupsAtAirbase(airbaseName)
local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName]
if not airbaseData then return {} end
return airbaseData.BlueGroups
end
function Database:getMiscGroupsAtStage(stageName) function Database:getMiscGroupsAtStage(stageName)
local stageZone = self._tables.StageZones[stageName] local stageZone = self._tables.StageZones[stageName]
@@ -21,6 +21,10 @@ function SpearheadGroup.New(groupName)
return self return self
end end
function SpearheadGroup:IsSpawned()
return self._isSpawned
end
function SpearheadGroup:SpawnCorpsesOnly() function SpearheadGroup:SpawnCorpsesOnly()
if self._isSpawned == true then return end if self._isSpawned == true then return end
@@ -146,6 +150,26 @@ function SpearheadGroup:GetUnits()
return result return result
end 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
if not Spearhead.classes then Spearhead.classes = {} end if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Groups then Spearhead.classes.stageClasses.Groups = {} end if not Spearhead.classes.stageClasses.Groups then Spearhead.classes.stageClasses.Groups = {} end
+93 -9
View File
@@ -1,13 +1,21 @@
---@class BlueSam ---@class BlueSam : OnCrateDroppedListener, MissionCompleteListener
---@field Activate fun(self: BlueSam) ---@field Activate fun(self: BlueSam)
---@field private _database Database ---@field private _database Database
---@field private _logger Logger ---@field private _logger Logger
---@field private _zoneName string ---@field private _zoneName string
---@field private _blueGroups Array<SpearheadGroup> ---@field private _blueGroups Array<SpearheadGroup>
---@field private _cleanupUnits table<string, boolean> ---@field private _cleanupUnits table<string, boolean>
---@field private _buildableCrateKilos number?
---@field private _receivedKilos number?
---@field private _unitsPerCrate number?
---@field private _buildableMission BuildableMission?
local BlueSam = {} local BlueSam = {}
---@param database Database
---@param logger Logger
---@param zoneName string
---@return BlueSam?
function BlueSam.New(database, logger, zoneName) function BlueSam.New(database, logger, zoneName)
BlueSam.__index = BlueSam BlueSam.__index = BlueSam
local self = setmetatable({}, BlueSam) local self = setmetatable({}, BlueSam)
@@ -19,8 +27,15 @@ function BlueSam.New(database, logger, zoneName)
self._blueGroups = {} self._blueGroups = {}
self._cleanupUnits = {} self._cleanupUnits = {}
do local blueSamData = database:getBlueSamDataForZone(zoneName)
local groups = database:getBlueSamGroupsInZone(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> ---@type table<string, Vec3>
local blueUnitsPos = {} local blueUnitsPos = {}
@@ -28,7 +43,8 @@ function BlueSam.New(database, logger, zoneName)
---@type table<string, Vec3> ---@type table<string, Vec3>
local redUnitsPos = {} local redUnitsPos = {}
for _, groupName in pairs(groups) do
for _, groupName in pairs(blueSamData.groups) do
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName) local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
if SpearheadGroup then if SpearheadGroup then
@@ -49,10 +65,7 @@ function BlueSam.New(database, logger, zoneName)
SpearheadGroup:Destroy() SpearheadGroup:Destroy()
end end
do -- check cleanup requirements --Cleanup units
-- 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 local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
@@ -62,13 +75,66 @@ function BlueSam.New(database, logger, zoneName)
end end
end 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 end
end
return self return self
end 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 = 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() 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 for unitName, needsCleanup in pairs(self._cleanupUnits) do
if needsCleanup == true then if needsCleanup == true then
Spearhead.DcsUtil.DestroyUnit(unitName) Spearhead.DcsUtil.DestroyUnit(unitName)
@@ -86,6 +152,24 @@ function BlueSam:Activate()
end 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 == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+174 -3
View File
@@ -1,12 +1,17 @@
---@class FarpZone ---@class FarpZone : OnCrateDroppedListener, MissionCompleteListener
---@field private _startingFarp boolean ---@field private _startingFarp boolean
---@field private _groups Array<SpearheadGroup> ---@field private _groups Array<SpearheadGroup>
---@field private _padNames Array<string> ---@field private _padNames Array<string>
---@field private _database Database ---@field private _database Database
---@field private _logger Logger ---@field private _logger Logger
---@field private _zoneName string ---@field private _zoneName string
---@field private _requiredBuildingKilos number
---@field private _receivedBuildingKilos number
---@field private _groupsPerKilo number
---@field private _buildableMission BuildableMission?
---@field private _supplyHubs Array<SupplyHub>
local FarpZone = {} local FarpZone = {}
FarpZone.__index = FarpZone FarpZone.__index = FarpZone
@@ -36,22 +41,50 @@ function FarpZone.New(database, logger, zoneName)
local farpData = database:getFarpDataForZone(zoneName) local farpData = database:getFarpDataForZone(zoneName)
self._groups = {} self._groups = {}
self._padNames = {} self._padNames = {}
self._supplyHubs = {}
if farpData then if farpData then
self._padNames = farpData.padNames 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 for _, groupName in pairs(farpData.groups) do
local group = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName) local group = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._groups, group) table.insert(self._groups, group)
group:Destroy() 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 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() self:Deactivate()
return self return self
end end
---@return boolean ---@return boolean
function FarpZone:IsStartingFarp() function FarpZone:IsStartingFarp()
return self._startingFarp return self._startingFarp
@@ -59,14 +92,112 @@ end
function FarpZone:Activate() function FarpZone:Activate()
self._logger:info("Activating FARP zone: " .. self._zoneName) self._logger:info("Activating FARP zone: " .. self._zoneName)
if self._buildableMission == nil then
self:BuildUp() self:BuildUp()
self:SetPadsBlue() self:SetPadsBlue()
self:ActivateSupplyHubs()
else
self._buildableMission:SpawnActive()
end
end end
function FarpZone:Deactivate() function FarpZone:Deactivate()
self:NeutralisePads() 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 end
@@ -76,6 +207,13 @@ function FarpZone:BuildUp()
end end
end end
function FarpZone:ActivateSupplyHubs()
for _, supplyHub in pairs(self._supplyHubs) do
supplyHub:Activate()
end
end
---@private ---@private
function FarpZone:NeutralisePads() function FarpZone:NeutralisePads()
for _, name in pairs(self._padNames) do for _, name in pairs(self._padNames) do
@@ -99,6 +237,39 @@ function FarpZone:SetPadsBlue()
end end
---@private
---@return SpearheadTriggerZone?
function FarpZone:GetNoLandingZone()
---@type Array<Vec2>
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 == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
@@ -8,13 +8,14 @@
---@field private _cleanup_units table<string, boolean> ---@field private _cleanup_units table<string, boolean>
---@field private _airbase Airbase? ---@field private _airbase Airbase?
---@field private _initialSide number? ---@field private _initialSide number?
---@field private _supplyHubs Array<SupplyHub>
local StageBase = {} local StageBase = {}
---comment ---comment
---@param databaseManager Database ---@param databaseManager Database
---@param logger table ---@param logger table
---@param airbaseName string ---@param airbaseName string
---@return StageBase ---@return StageBase?
function StageBase.New(databaseManager, logger, airbaseName) function StageBase.New(databaseManager, logger, airbaseName)
StageBase.__index = StageBase StageBase.__index = StageBase
@@ -26,22 +27,26 @@ function StageBase.New(databaseManager, logger, airbaseName)
self._red_groups = {} self._red_groups = {}
self._blue_groups = {} self._blue_groups = {}
self._cleanup_units = {} self._cleanup_units = {}
self._supplyHubs = {}
self._airbase = Airbase.getByName(airbaseName) self._airbase = Airbase.getByName(airbaseName)
self._initialSide = Spearhead.DcsUtil.getStartingCoalition(self._airbase) self._initialSide = Spearhead.DcsUtil.getStartingCoalition(self._airbase)
do --init 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> ---@type table<string, Vec3>
local redUnitsPos = {} local redUnitsPos = {}
---@type table<string, Vec3> ---@type table<string, Vec3>
local blueUnitsPos = {} local blueUnitsPos = {}
do -- fill tables for _, groupName in pairs(airbaseData.RedGroups) do
local redGroups = databaseManager:getRedGroupsAtAirbase(airbaseName)
if redGroups then
for _, groupName in pairs(redGroups) do
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName) local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._red_groups, shGroup) table.insert(self._red_groups, shGroup)
@@ -51,11 +56,8 @@ function StageBase.New(databaseManager, logger, airbaseName)
shGroup:Destroy() shGroup:Destroy()
end end
end
local blueGroups = databaseManager:getBlueGroupsAtAirbase(airbaseName) for _, groupName in pairs(airbaseData.BlueGroups) do
if blueGroups then
for _, groupName in pairs(blueGroups) do
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName) local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._blue_groups, shGroup) table.insert(self._blue_groups, shGroup)
@@ -65,6 +67,11 @@ function StageBase.New(databaseManager, logger, airbaseName)
shGroup:Destroy() shGroup:Destroy()
end 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
end end
@@ -152,6 +159,10 @@ function StageBase:ActivateBlueStage()
self:CleanRedUnits() self:CleanRedUnits()
self:SpawnBlueUnits() self:SpawnBlueUnits()
for _, hub in pairs(self._supplyHubs) do
hub:Activate()
end
end end
@@ -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<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 = 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
@@ -14,6 +14,7 @@
--- @field miscGroups Array<SpearheadGroup> --- @field miscGroups Array<SpearheadGroup>
--- @field maxMissions integer --- @field maxMissions integer
--- @field farps Array<FarpZone> --- @field farps Array<FarpZone>
--- @field supplyHubs Array<SupplyHub>
--- @class StageInitData --- @class StageInitData
--- @field stageZoneName string --- @field stageZoneName string
@@ -90,7 +91,9 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
maxMissions = stageConfig.maxMissionsPerStage, maxMissions = stageConfig.maxMissionsPerStage,
farps = {}, farps = {},
missionsByName = {}, missionsByName = {},
supplyHubs = {}
} }
self._activeStage = -99 self._activeStage = -99
self._preActivated = false self._preActivated = false
self._stageConfig = stageConfig or {} self._stageConfig = stageConfig or {}
@@ -110,6 +113,11 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
table.insert(self._db.farps, farp) table.insert(self._db.farps, farp)
end 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) self._logger:info("Initiating new Stage with name: " .. self.zoneName)
@@ -404,6 +412,12 @@ function Stage:ActivateStage()
end end
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) timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3)
end end
@@ -476,6 +490,10 @@ function Stage:ActivateBlueGroups()
farp:Activate() farp:Activate()
end end
end end
for _, supplyHub in pairs(self._db.supplyHubs) do
supplyHub:Activate()
end
end end
---@return number strike ---@return number strike
@@ -5,10 +5,13 @@
---@field lastUpdate number @last update time ---@field lastUpdate number @last update time
---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously ---@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 pinnedByGroup table<string, Mission> @table of pinned missions by group ID
---@field private _supplyHubGroups table<string, boolean> @table of supply hub groups by their ID
---@field private _logger Logger @logger instance for logging ---@field private _logger Logger @logger instance for logging
---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
local MissionCommandsHelper = {} local MissionCommandsHelper = {}
MissionCommandsHelper.__index = MissionCommandsHelper MissionCommandsHelper.__index = MissionCommandsHelper
local id = 0
local instance = nil local instance = nil
@@ -16,21 +19,25 @@ local instance = nil
---@param logLevel string @log level for the logger ---@param logLevel string @log level for the logger
function MissionCommandsHelper.getOrCreate(logLevel) function MissionCommandsHelper.getOrCreate(logLevel)
if instance == nil then 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 = {} instance._logger:info("Creating MissionCommandsHelper instance")
self.enabledByCode = {}
self.updateNeeded = false instance.missionsByCode = {}
self.pinnedByGroup = {} instance.enabledByCode = {}
self.lastUpdate = 0 instance.updateNeeded = false
instance.pinnedByGroup = {}
instance.lastUpdate = 0
instance._supplyHubGroups = {}
instance._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logLevel)
---comment ---comment
---@param selfA MissionCommandsHelper ---@param selfA MissionCommandsHelper
---@param time number ---@param time number
---@return number ---@return number
self.updateContinuous = function(selfA, time) instance.updateContinuous = function(selfA, time)
if selfA.updateNeeded == false then if selfA.updateNeeded == false then
return time + 10 return time + 10
end end
@@ -49,14 +56,63 @@ function MissionCommandsHelper.getOrCreate(logLevel)
return time + 10 return time + 10
end end
timer.scheduleFunction(self.updateContinuous, self, timer.getTime() + 5) timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5)
Spearhead.Events.AddOnPlayerEnterUnitListener(self) Spearhead.Events.AddOnPlayerEnterUnitListener(instance)
instance = self
end end
return instance return instance
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
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 ---@class MissionBriefingRequestedArgs
---@field mission Mission @the mission object ---@field mission Mission @the mission object
@@ -72,6 +128,22 @@ local missionBriefingRequested = function(args)
mission:ShowBriefing(groupID) mission:ShowBriefing(groupID)
end 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 ---@class PinMissionCommandArgs
---@field self MissionCommandsHelper @the MissionCommandsHelper instance ---@field self MissionCommandsHelper @the MissionCommandsHelper instance
@@ -114,7 +186,7 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
end end
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 end
---Primary missions ---Primary missions
@@ -161,11 +233,11 @@ function MissionCommandsHelper:AddPinnedMission(groupID)
end end
---comment
---@param groupID number ---@param groupID number
function MissionCommandsHelper:updateCommandsForGroup(groupID) function MissionCommandsHelper:updateCommandsForGroup(groupID)
self._logger:debug("Updating commands for group: " .. tostring(groupID))
self:AddPinnedMission(groupID) self:AddPinnedMission(groupID)
self:AddOverviewCommand(groupID) self:AddOverviewCommand(groupID)
@@ -180,6 +252,9 @@ function MissionCommandsHelper:updateCommandsForGroup(groupID)
end end
end end
self:AddSupplyHubCommandsIfApplicable(groupID)
self:AddCargoCommands(groupID)
---@param id number ---@param id number
local clearView = function(id) local clearView = function(id)
trigger.action.outTextForGroup(id, "clearing...", 1, true) trigger.action.outTextForGroup(id, "clearing...", 1, true)
@@ -192,7 +267,9 @@ end
local folderNames = { local folderNames = {
primary = "Primary Missions", 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 local missionFolderName = "[" .. mission.code .. "]" .. mission.name
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path) missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName) 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
end end
---@param mission Mission ---@private
function MissionCommandsHelper:AddMissionToCommands(mission) ---@param groupID integer
self._logger:debug("Adding mission to commands: [" .. mission.code .. "]" .. mission.name) function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
self.missionsByCode[tostring(mission.code)] = mission
self.enabledByCode[tostring(mission.code)] = true if self._supplyHubGroups[tostring(groupID)] ~= true then return end
self.updateNeeded = true
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 end
---Removes a mission from the F10 commands menu local path = { [1] = folderNames.supplyHub }
---@param mission Mission
function MissionCommandsHelper:RemoveMissionToCommands(mission)
self.enabledByCode[tostring(mission.code)] = false ---@type LoadCargoCommandParams
self.updateNeeded = true 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 end
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 ---@private
---@param groupId integer ---@param groupId integer
function MissionCommandsHelper:addMissionFolders(groupId) function MissionCommandsHelper:addMissionFolders(groupId)
missionCommands.addSubMenuForGroup(groupId, folderNames.primary) missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
missionCommands.addSubMenuForGroup(groupId, folderNames.secondary) 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 end
---@private ---@private
@@ -253,6 +434,8 @@ end
function MissionCommandsHelper:removeMissionFolders(groupId) function MissionCommandsHelper:removeMissionFolders(groupId)
missionCommands.removeItemForGroup(groupId, { folderNames.primary }) missionCommands.removeItemForGroup(groupId, { folderNames.primary })
missionCommands.removeItemForGroup(groupId, { folderNames.secondary }) missionCommands.removeItemForGroup(groupId, { folderNames.secondary })
missionCommands.removeItemForGroup(groupId, { folderNames.supplyHub })
missionCommands.removeItemForGroup(groupId, { folderNames.cargo })
end end
---@private ---@private
@@ -264,15 +447,6 @@ function MissionCommandsHelper:ResetFolders(groupID)
self:addMissionFolders(groupID) self:addMissionFolders(groupID)
end 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 == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
@@ -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<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",
},
}
---@class MaxLoadConfig
---@field maxInternalLoad number
---@type table<string, MaxLoadConfig>
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
@@ -0,0 +1,407 @@
---@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 _commandsHelper MissionCommandsHelper
---@field private _droppedCrates table<string, StaticObject>
---@field private _registeredHubs table<SupplyHub, boolean>
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<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
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<string,StaticObject>
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
@@ -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<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 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<Object>
local foundCrates = {}
local crates = self._supplyUnitsTracker:GetCargoCratesDropped()
for _, staticObject in pairs(crates) do
if staticObject and staticObject:isExist() and Spearhead.Util.startswith(staticObject:getName(), self._crateType, true) then
local pos = staticObject:getPoint()
if Spearhead.Util.is3dPointInZone(pos, self._dropOffZone) then
table.insert(foundCrates, staticObject)
end
end
end
for _, foundCrate in pairs(foundCrates) do
local crateConfig = Spearhead.classes.stageClasses.helpers.supplies.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
Spearhead.DcsUtil.RemoveMark(self._noLandingZoneId)
Spearhead.DcsUtil.RemoveMark(self._dropOffZoneId)
self:NotifyMissionComplete()
self._state = "COMPLETED"
end
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.missions then Spearhead.classes.stageClasses.missions = {} end
Spearhead.classes.stageClasses.missions.BuildableMission = BuildableMission
@@ -1,5 +1,6 @@
---@class Mission ---@class Mission
---@field name string ---@field name string
---@field zoneName string ---@field zoneName string
@@ -120,6 +121,8 @@ function Mission:NotifyMissionComplete()
end end
function Mission:MarkMissionAreaToGroup(groupId) end
---endregion ---endregion
--region PROTECTED --region PROTECTED
@@ -155,6 +158,7 @@ do --aliases
--- | "DEAD" --- | "DEAD"
--- | "SAM" --- | "SAM"
--- | "OCA" --- | "OCA"
--- | "LOGISTICS"
--- @alias MissionState --- @alias MissionState
--- | "NEW" --- | "NEW"
+4
View File
@@ -23,6 +23,7 @@ assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))() assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))() assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))() assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\BuildableMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))() assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
@@ -34,10 +35,13 @@ assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))() assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))() assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyUnitsTracker.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.lua"))()
assert(loadfile(classPath .. "capClasses\\CapGroup.lua"))() assert(loadfile(classPath .. "capClasses\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))() assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()