diff --git a/.DS_Store b/.DS_Store
index e35c557..fb84059 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6a3e68d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+**/.DS_Store
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a7d14c9..75d8e18 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,14 @@
],
"Lua.diagnostics.globals": [
"lfs"
- ]
+ ],
+ "maptz.regionfolder": {
+ "[lua]" : {
+ "foldEnd": "--endregion",
+ "foldEndRegex": "[\\s]*--endregion",
+ "foldStart": "--region [NAME]",
+ "foldStartRegex": "[\\s]*--region[\\s]*(.*)",
+ "disableFolding": false
+ }
+ }
}
\ No newline at end of file
diff --git a/_docs/pages/reference.html b/_docs/pages/reference.html
index abd4fea..0f73df6 100644
--- a/_docs/pages/reference.html
+++ b/_docs/pages/reference.html
@@ -292,10 +292,25 @@
Missions can be randomized using the RANDOMMISSION prefix.
Spearhead will pick one random mission from zones with the same name.
+
+ 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.
+
+ Firstly, the runway is split up in 5 sections and only sections 2, 3 and 4 are then actually tracked.
+
+
+ +-----------+-----------+-----------+-----------+-----------+
+ | Section 1 | Section 2 | Section 3 | Section 4 | Section 5 |
+ +-----------+-----------+-----------+-----------+-----------+
+
+
+
+
+
-
© 2025 Spearhead Project
diff --git a/_miz/SPEARHEADDEV.miz b/_miz/SPEARHEADDEV.miz
index c73a6b9..4084ef3 100644
Binary files a/_miz/SPEARHEADDEV.miz and b/_miz/SPEARHEADDEV.miz differ
diff --git a/classes/_baseClasses/Queue.lua b/classes/_baseClasses/Queue.lua
new file mode 100644
index 0000000..5d50c5c
--- /dev/null
+++ b/classes/_baseClasses/Queue.lua
@@ -0,0 +1,50 @@
+
+---@class Queue
+---@field private _items Array
+---@field private _first number
+---@field private _last number
+local Queue = {}
+Queue.__index = Queue
+
+---@return Queue
+function Queue.new()
+
+ local self = setmetatable({}, Queue)
+ self._items = {}
+ self._first = 1
+ self._last = 0
+ return self
+end
+
+---@return nil
+function Queue:push(item)
+ self._last = self._last + 1
+ self._items[self._last] = item
+end
+
+---@return any?
+function Queue:pop()
+ if self._first > self._last then
+ return nil
+ end
+
+ local item = self._items[self._first]
+ self._items[self._first] = nil
+ self._first = self._first + 1
+ return item
+end
+
+
+---@return Array
+function Queue:toList()
+ local items = {}
+ for i = self._first, self._last do
+ items[#items + 1] = self._items[i]
+ end
+ return items
+end
+
+
+if Spearhead == nil then Spearhead = {} end
+if Spearhead._baseClasses == nil then Spearhead._baseClasses = {} end
+Spearhead._baseClasses.Queue = Queue
\ No newline at end of file
diff --git a/classes/capClasses/CapAirbase.lua b/classes/capClasses/CapAirbase.lua
index 7db9b05..25273fa 100644
--- a/classes/capClasses/CapAirbase.lua
+++ b/classes/capClasses/CapAirbase.lua
@@ -10,6 +10,8 @@
---@field groupsByName table
---@field PrimaryGroups Array
---@field BackupGroups Array
+---@field private runwayBombingTracker RunwayBombingTracker
+---@field private runwayStrikeMissions table
local CapBase = {}
@@ -23,14 +25,17 @@ end
---@param logger table
---@param capConfig table
---@param stageConfig table
+---@param runwayBombingTracker RunwayBombingTracker
---@return CapBase
-function CapBase.new(airbaseName, database, logger, capConfig, stageConfig)
+function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker)
CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
self.groupNames = database:getCapGroupsAtAirbase(airbaseName)
self.database = database
+ self.runwayBombingTracker = runwayBombingTracker
+ self.runwayStrikeMissions = {}
self.airbaseName = airbaseName
self.logger = logger
@@ -44,7 +49,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig)
self.BackupGroups = {}
for key, name in pairs(self.groupNames) do
- local capGroup = Spearhead.internal.CapGroup.new(name, airbaseName, logger, database, capConfig)
+ local capGroup = Spearhead.classes.capClasses.CapGroup.new(name, airbaseName, logger, database, capConfig)
if capGroup then
self.groupsByName[name] = capGroup
@@ -57,21 +62,40 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig)
capGroup:AddOnStateUpdatedListener(self)
end
end
- logger:info("Airbase with name '" .. airbaseName .. "' has a total of " .. Spearhead.Util.tableLength(self.groupsByName) .. "cap flights registered")
+ logger:info("Airbase with name '" .. airbaseName .. "' has a total of " .. Spearhead.Util.tableLength(self.groupsByName) .. " cap flights registered")
+ self:CreateRunwayStrikeMission()
Spearhead.Events.AddStageNumberChangedListener(self)
return self
end
+---@private
+function CapBase:CreateRunwayStrikeMission()
+
+ local airbase = Airbase.getByName(self.airbaseName)
+ if not airbase then
+ self.logger:debug("Could not find a airbase with name to create runway mission" .. self.airbaseName)
+ return
+ end
+
+ for _, runway in pairs(airbase:getRunways()) do
+ if runway then
+ self.logger:debug("Runway " .. runway.Name .. " at airbase " .. self.airbaseName .. " with heading " .. runway.course .. " and length " .. runway.length .. " and width " .. runway.width)
+ local mission = Spearhead.classes.stageClasses.missions.RunwayStrikeMission.new(runway, self.airbaseName, self.database, self.logger, self.runwayBombingTracker)
+ self.runwayStrikeMissions[runway.Name] = mission
+ end
+ end
+end
+
function CapBase:onGroupStateUpdated(capGroup)
--[[
There is no update needed for INTRANSIT, ONSTATION or REARMING as the PREVIOUS state already was checked and nothing changes in the actual overal state.
]]--
- if capGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT
- or capGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION
- or capGroup.state == Spearhead.internal.CapGroup.GroupState.REARMING
+ if capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT
+ or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION
+ or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.REARMING
then
return
end
@@ -84,7 +108,7 @@ function CapBase:SpawnIfApplicable()
local targetStage = capGroup:GetTargetZone(self.activeStage)
- if targetStage ~= nil and capGroup.state == Spearhead.internal.CapGroup.GroupState.UNSPAWNED then
+ if targetStage ~= nil and capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.UNSPAWNED then
capGroup:SpawnOnTheRamp()
end
end
@@ -99,7 +123,7 @@ function CapBase:CheckAndScheduleCAP()
--Count back up groups that are active or reassign to the new zone if that's needed
for _, backupGroup in pairs(self.BackupGroups) do
- if backupGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT or backupGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION then
+ if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT or backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION then
local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
if supposedTargetStage then
if supposedTargetStage ~= backupGroup.assignedStageNumber then
@@ -113,7 +137,7 @@ function CapBase:CheckAndScheduleCAP()
else
backupGroup:SendRTBAndDespawn()
end
- elseif backupGroup.state == Spearhead.internal.CapGroup.GroupState.RTBINTEN and backupGroup:GetTargetZone(self.activeStage) ~= backupGroup.assignedStageNumber then
+ elseif backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and backupGroup:GetTargetZone(self.activeStage) ~= backupGroup.assignedStageNumber then
backupGroup:SendRTB()
end
end
@@ -133,12 +157,12 @@ function CapBase:CheckAndScheduleCAP()
requiredPerStage[supposedTargetStage] = requiredPerStage[supposedTargetStage] + 1
- if primaryGroup.state == Spearhead.internal.CapGroup.GroupState.READYONRAMP then
+ if primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.READYONRAMP then
if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
primaryGroup:SendToStage(supposedTargetStage)
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
end
- elseif primaryGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT or primaryGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION then
+ elseif primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT or primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION then
if supposedTargetStage ~= primaryGroup.assignedStageNumber then
if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
primaryGroup:SendToStage(supposedTargetStage)
@@ -148,7 +172,7 @@ function CapBase:CheckAndScheduleCAP()
end
end
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
- elseif primaryGroup.state == Spearhead.internal.CapGroup.GroupState.RTBINTEN and primaryGroup:GetTargetZone(self.activeStage) ~= primaryGroup.assignedStageNumber then
+ elseif primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and primaryGroup:GetTargetZone(self.activeStage) ~= primaryGroup.assignedStageNumber then
primaryGroup:SendRTB()
end
else
@@ -157,7 +181,7 @@ function CapBase:CheckAndScheduleCAP()
end
for _, backupGroup in pairs(self.BackupGroups) do
- if backupGroup.state == Spearhead.internal.CapGroup.GroupState.READYONRAMP then
+ if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.READYONRAMP then
local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
if supposedTargetStage then
if countPerStage[supposedTargetStage] == nil then
@@ -177,6 +201,13 @@ function CapBase:CheckAndScheduleCAP()
function CapBase:OnStageNumberChanged(number)
self.activeStage = number
+
+ if self:IsBaseActiveWhenStageIsActive(number) == true then
+ for _, mission in pairs(self.runwayStrikeMissions) do
+ mission:SpawnActive()
+ end
+ end
+
self:SpawnIfApplicable()
timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5)
end
@@ -195,5 +226,7 @@ function CapBase:IsBaseActiveWhenStageIsActive(stageNumber)
end
-if not Spearhead.internal then Spearhead.internal = {} end
-Spearhead.internal.CapAirbase = CapBase
\ No newline at end of file
+if not Spearhead then Spearhead = {} end
+if not Spearhead.classes then Spearhead.classes = {} end
+if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
+Spearhead.classes.capClasses.CapAirbase = CapBase
diff --git a/classes/capClasses/CapGroup.lua b/classes/capClasses/CapGroup.lua
index 4bd9112..0039699 100644
--- a/classes/capClasses/CapGroup.lua
+++ b/classes/capClasses/CapGroup.lua
@@ -261,6 +261,11 @@ function CapGroup:SendToStage(stageZoneNumber)
local controller = group:getController()
local capPoints = self.database:getCapRouteInZone(stageZoneNumber, self.airbaseName)
+ if not capPoints then
+ self.logger:error("No CAP route found for group " .. self.groupName .. " in zone " .. stageZoneNumber)
+ return
+ end
+
local altitude = math.random(self.capConfig:getMinAlt(), self.capConfig:getMaxAlt())
local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed())
local attackHelos = false
@@ -418,5 +423,7 @@ function CapGroup:OnUnitLost(initiatorUnit)
end
-if not Spearhead.internal then Spearhead.internal = {} end
-Spearhead.internal.CapGroup = CapGroup
+if not Spearhead then Spearhead = {} end
+if not Spearhead.classes then Spearhead.classes = {} end
+if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
+Spearhead.classes.capClasses.CapGroup = CapGroup
diff --git a/classes/capClasses/GlobalCapManager.lua b/classes/capClasses/GlobalCapManager.lua
index 0b600d8..da61669 100644
--- a/classes/capClasses/GlobalCapManager.lua
+++ b/classes/capClasses/GlobalCapManager.lua
@@ -9,12 +9,15 @@ do
---comment
---@param database Database
- ---@param capConfig any
- ---@param stageConfig any
- function GlobalCapManager.start(database, capConfig, stageConfig)
+ ---@param capConfig table
+ ---@param stageConfig StageConfig
+ ---@param logLevel LogLevel
+ function GlobalCapManager.start(database, capConfig, stageConfig, logLevel)
if initiated == true then return end
- local logger = Spearhead.LoggerTemplate.new("AirbaseManager", capConfig.logLevel)
+ local logger = Spearhead.LoggerTemplate.new("AirbaseManager", logLevel)
+ local bombTrackLogger = Spearhead.LoggerTemplate.new("RunwayBombingTracker", logLevel)
+ local runwayBombingTracker = Spearhead.classes.capClasses.runwayBombing.RunwayBombingTracker.new(bombTrackLogger)
local zones = database:getStagezoneNames()
if zones then
@@ -27,8 +30,8 @@ do
if airbaseNames then
for _, airbaseName in pairs(airbaseNames) do
if airbaseName then
- local airbaseSpecificLogger = Spearhead.LoggerTemplate.new("CAP_" .. airbaseName, capConfig.logLevel)
- local airbase = Spearhead.internal.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig)
+ local airbaseSpecificLogger = Spearhead.LoggerTemplate.new("CAP_" .. airbaseName, logLevel)
+ local airbase = Spearhead.classes.capClasses.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker)
if airbase then
table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase
@@ -61,5 +64,9 @@ do
end
end
-if not Spearhead.internal then Spearhead.internal = {} end
-Spearhead.internal.GlobalCapManager = GlobalCapManager
+
+
+if not Spearhead then Spearhead = {} end
+if not Spearhead.classes then Spearhead.classes = {} end
+if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
+Spearhead.classes.capClasses.GlobalCapManager = GlobalCapManager
diff --git a/classes/capClasses/runwayBombing/RunwayBombingTracker.lua b/classes/capClasses/runwayBombing/RunwayBombingTracker.lua
new file mode 100644
index 0000000..12ca7bf
--- /dev/null
+++ b/classes/capClasses/runwayBombing/RunwayBombingTracker.lua
@@ -0,0 +1,129 @@
+---@class RunwayBombingTracker : OnWeaponFiredListener
+---@field trackedRunways table
+---@field private _logger Logger
+local RunwayBombingTracker = {}
+RunwayBombingTracker.__index = RunwayBombingTracker
+
+--- Constructor
+--- @param logger Logger
+--- @return RunwayBombingTracker
+function RunwayBombingTracker.new(logger)
+ local self = setmetatable({}, RunwayBombingTracker)
+
+ self._logger = logger
+
+ Spearhead.Events.AddWeaponFiredListener(self)
+
+ return self
+end
+
+---comment
+---@param weapon Weapon
+function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
+
+ if weapon == nil then
+ return
+ end
+
+ self._logger:debug("Tracking weapon for runway impact onWeaponFired")
+
+ local desc = weapon:getDesc()
+
+ local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE)
+
+ if isTrackable == true then
+
+ ---@type WeaponTrackingArgs
+ local weaponTrackingArgs = {
+ weapon = weapon,
+ self = self
+ }
+
+ timer.scheduleFunction(RunwayBombingTracker.trackWeaponTask, weaponTrackingArgs, timer.getTime() + 1)
+ end
+end
+
+function RunwayBombingTracker:RegisterRunway(runway, strikeMission)
+ if not self.trackedRunways then
+ self.trackedRunways = {}
+ end
+
+ if not self.trackedRunways[runway] then
+ self.trackedRunways[runway] = strikeMission
+ end
+end
+
+---@class WeaponTrackingArgs
+---@field weapon Weapon
+---@field self RunwayBombingTracker
+
+---@private
+---@param weaponTrackingArgs WeaponTrackingArgs
+function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
+
+ local weapon = weaponTrackingArgs.weapon
+ local self = weaponTrackingArgs.self
+
+ if not weapon then return nil end
+
+ local pos = weapon:getPoint()
+ local velocity = weapon:getVelocity()
+ local ground = land.getHeight({ x = pos.x, y = pos.z })
+ local MpS = velocity.y -- increase the speed to make sure you don't miss it.
+
+ if MpS > 0 then
+ return time + 3
+ end
+
+ local nextInterval = (pos.y - ground) / math.abs(MpS)
+
+ if nextInterval < 1 then
+
+ -- Calculate the impact point of the weapon
+ ---@type Vec2
+ local impactPoint = {
+ x = pos.x + velocity.x * nextInterval,
+ y = pos.z + velocity.z * nextInterval
+ }
+
+
+ self:OnWeaponImpact(weapon:getDesc(), impactPoint)
+
+
+ return nil
+ end
+
+ if nextInterval > 5 then
+ nextInterval = 5
+ end
+
+ return time + (nextInterval /2)
+end
+
+
+---comment
+---@param weaponDesc table
+---@param impactPoint Vec2
+function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint)
+
+ self._logger:debug("RunwayBombingTracker:OnWeaponImpact")
+
+ local warhead = weaponDesc.warhead
+ local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass
+
+ for runway, strikeMission in pairs(self.trackedRunways) do
+
+ local zone= strikeMission:GetRunwayZone()
+
+ if Spearhead.Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
+ strikeMission:RunwayHit(impactPoint, explosiveMass)
+ end
+ end
+end
+
+
+if not Spearhead then Spearhead = {} end
+if not Spearhead.classes then Spearhead.classes = {} end
+if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
+if not Spearhead.classes.capClasses.runwayBombing then Spearhead.classes.capClasses.runwayBombing = {} end
+Spearhead.classes.capClasses.runwayBombing.RunwayBombingTracker = RunwayBombingTracker
\ No newline at end of file
diff --git a/classes/configuration/StageConfig.lua b/classes/configuration/StageConfig.lua
index 2b04679..956f21d 100644
--- a/classes/configuration/StageConfig.lua
+++ b/classes/configuration/StageConfig.lua
@@ -2,10 +2,11 @@
--- @class StageConfig
--- @field isEnabled boolean
--- @field isDrawStagesEnabled boolean
+--- @field isDrawPreActivatedEnabled boolean
--- @field isAutoStages boolean
--- @field startingStage integer
--- @field maxMissionsPerStage integer
---- @field logLevel LogLevel
+--- @field AmountPreactivateStage integer
local StageConfig = {};
@@ -24,13 +25,10 @@ function StageConfig:new()
isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10,
- logLevel = "INFO"
+ isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated or true,
+ AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1,
}
-
- if SpearheadConfig.StageConfig.debugEnabled == true then
- o.logLevel = "DEBUG"
- end
-
+
setmetatable(o, { __index = self })
return o;
diff --git a/classes/definitions/aliases.lua b/classes/definitions/aliases.lua
index 0663066..5459251 100644
--- a/classes/definitions/aliases.lua
+++ b/classes/definitions/aliases.lua
@@ -3,23 +3,6 @@
do -- mission aliases
- --- @alias MissionPriority
- --- | "none"
- --- | "primary"
- --- | "secondary"
-
- --- @alias missionType
- --- | "nil"
- --- | "STRIKE"
- --- | "BAI"
- --- | "DEAD"
- --- | "SAM"
-
- --- @alias MissionState
- --- | "NEW"
- --- | "ACTIVE"
- --- | "COMPLETED"
-
--- @alias LogLevel
--- | "DEBUG"
--- | "INFO"
@@ -27,8 +10,6 @@ do -- mission aliases
--- | "ERROR"
--- | "NONE"
-
-
---@class Array: { [integer]: T }
--- @class Position
diff --git a/classes/fleetClasses/FleetGroup.lua b/classes/fleetClasses/FleetGroup.lua
index b0c8ad0..e792f82 100644
--- a/classes/fleetClasses/FleetGroup.lua
+++ b/classes/fleetClasses/FleetGroup.lua
@@ -1,5 +1,10 @@
local FleetGroup = {}
+---comment
+---@param fleetGroupName string
+---@param database Database
+---@param logger Logger
+---@return nil
function FleetGroup:new(fleetGroupName, database, logger)
local o = {}
@@ -30,10 +35,10 @@ function FleetGroup:new(fleetGroupName, database, logger)
Spearhead.AddMissionEditorWarning(
"CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName)
else
+
+ ---@param zone SpearheadTriggerZone
+ ---@return Vec2, Vec2
local function GetTwoFurthestPoints(zone)
- local function getDist(a, b)
- return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
- end
local biggest = nil
local biggestA = zone.verts[1]
@@ -43,7 +48,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
for ii = i + 1, 4 do
local a = zone.verts[i]
local b = zone.verts[ii]
- local dist = getDist(a, b)
+ local dist = Spearhead.Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then
biggestA = a
@@ -52,7 +57,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
end
end
end
- return { x = biggestA.x, z = biggestA.z }, { x = biggestB.x, z = biggestB.z }
+ return { x = biggestA.x, y = biggestA.y }, { x = biggestB.x, y = biggestB.y }
end
local function getMinMaxStage(namePart)
@@ -90,7 +95,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
for i = first, second do
o.targetZonePerStage[tostring(i)] = zoneName
end
- o.pointsPerZone[zoneName] = { pointA = pointA, pointB = pointB }
+ o.pointsPerZone[zoneName] = { pointA = { x = pointA.x, z = pointA.y, y = 0 }, pointB = { x = pointB.x, z = pointB.y, y = 0} }
else
Spearhead.AddMissionEditorWarning("CARRIERROUTE zone stage numbers not in the format _[-]: " .. zoneName)
end
diff --git a/classes/spearhead_base.lua b/classes/spearhead_base.lua
index 8834c11..1e6c8a2 100644
--- a/classes/spearhead_base.lua
+++ b/classes/spearhead_base.lua
@@ -82,7 +82,6 @@ do -- INIT UTIL
---@param ignoreCase boolean?
---@return boolean
UTIL.startswith = function(str, findable, ignoreCase)
-
if ignoreCase == true then
return string.lower(str):find('^' .. string.lower(findable)) ~= nil
end
@@ -122,8 +121,8 @@ do -- INIT UTIL
end
---comment
- ---@param a Vec2 DCS Point vector {x, z , y}
- ---@param b Vec2 DCS Point vector {x, z , y}
+ ---@param a Vec2
+ ---@param b Vec2
---@return number
function UTIL.VectorDistance2d(a, b)
return math.sqrt((b.x - a.x) ^ 2 + (b.y - a.y) ^ 2)
@@ -144,95 +143,118 @@ do -- INIT UTIL
return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5
end
-
- ---comment
- ---@param polygon table of pairs { x, z }
- ---@param x number X location
- ---@param z number Y location
- ---@return boolean
- function UTIL.IsPointInPolygon(polygon, x, z)
- local function isInComplexPolygon(polygon, x, z)
- local function getEdges(poly)
- local result = {}
- for i = 1, #poly do
- local point1 = poly[i]
- local point2Index = i + 1
- if point2Index > #poly then point2Index = 1 end
- local point2 = poly[point2Index]
- local edge = { x1 = point1.x, z1 = point1.z, x2 = point2.x, z2 = point2.z }
- table.insert(result, edge)
- end
- return result
+ local function isInComplexPolygon(polygon, x, y)
+ local function getEdges(poly)
+ local result = {}
+ for i = 1, #poly do
+ local point1 = poly[i]
+ local point2Index = i + 1
+ if point2Index > #poly then point2Index = 1 end
+ local point2 = poly[point2Index]
+ local edge = { x1 = point1.x, z1 = point1.y, x2 = point2.x, z2 = point2.y }
+ table.insert(result, edge)
end
-
- local edges = getEdges(polygon)
- local count = 0;
- for _, edge in pairs(edges) do
- if (x < edge.x1) ~= (x < edge.x2) and z < edge.z1 + ((x - edge.x1) / (edge.x2 - edge.x1)) * (edge.z2 - edge.z1) then
- count = count + 1
- -- if (yp < y1) != (yp < y2) and xp < x1 + ((yp-y1)/(y2-y1))*(x2-x1) then
- -- count = count + 1
- end
- end
- return count % 2 == 1
+ return result
end
- return isInComplexPolygon(polygon, x, z)
+
+ local edges = getEdges(polygon)
+ local count = 0;
+ for _, edge in pairs(edges) do
+ if (x < edge.x1) ~= (x < edge.x2) and y < edge.z1 + ((x - edge.x1) / (edge.x2 - edge.x1)) * (edge.z2 - edge.z1) then
+ count = count + 1
+ -- if (yp < y1) != (yp < y2) and xp < x1 + ((yp-y1)/(y2-y1))*(x2-x1) then
+ -- count = count + 1
+ end
+ end
+ return count % 2 == 1
end
---comment
- ---@param points table points { x, z }
- ---@return table hullPoints { x, z }
+ ---@param polygon Array of pairs { x, y }
+ ---@param x number X location
+ ---@param y number Y location
+ ---@return boolean
+ function UTIL.IsPointInPolygon(polygon, x, y)
+ return isInComplexPolygon(polygon, x, y)
+ end
+
+ ---@param point Vec3
+ ---@param zone SpearheadTriggerZone
+ function UTIL.is3dPointInZone(point, zone)
+ if zone.zone_type == "Polygon" and zone.verts then
+ if UTIL.IsPointInPolygon(zone.verts, point.x, point.z) == true then
+ return true
+ end
+ else
+ if (((point.x - zone.location.x) ^ 2 + (point.z - zone.location.y) ^ 2) ^ 0.5 <= zone.radius) then
+ return true
+ end
+ end
+
+ return false
+ end
+
+ ---comment
+ ---@param points Array points
+ ---@return Array hullPoints
function UTIL.getConvexHull(points)
if #points == 0 then
return {}
end
- local function ccw(a,b,c)
- return (b.z - a.z) * (c.x - a.x) > (b.x - a.x) * (c.z - a.z)
+ ---comment
+ ---@param a Vec2
+ ---@param b Vec2
+ ---@param c Vec2
+ ---@return boolean
+ local function ccw(a, b, c)
+ return (b.y - a.y) * (c.x - a.x) > (b.x - a.x) * (c.y - a.y)
end
- table.sort(points, function(left,right)
- return left.z < right.z
+ table.sort(points, function(left, right)
+ return left.y < right.y
end)
local hull = {}
-- lower hull
- for _,point in pairs(points) do
- while #hull >= 2 and not ccw(hull[#hull-1], hull[#hull], point) do
- table.remove(hull,#hull)
+ for _, point in pairs(points) do
+ while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do
+ table.remove(hull, #hull)
end
- table.insert(hull,point)
+ table.insert(hull, point)
end
-- upper hull
local t = #hull + 1
- for i=#points, 1, -1 do
+ for i = #points, 1, -1 do
local point = points[i]
- while #hull >= t and not ccw(hull[#hull-1], hull[#hull], point) do
- table.remove(hull,#hull)
+ while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do
+ table.remove(hull, #hull)
end
- table.insert(hull,point)
+ table.insert(hull, point)
end
- table.remove(hull,#hull)
+ table.remove(hull, #hull)
return hull
end
+ ---@param points Array
function UTIL.enlargeConvexHull(points, meters)
+ ---@type Array
+ local allpoints = {}
- local allpoints = {}
-
for _, point in pairs(points) do
table.insert(allpoints, point)
- table.insert(allpoints, { x = point.x + meters, z = point.z, y= 0 })
- table.insert(allpoints, { x = point.x - meters, z = point.z, y= 0 })
- table.insert(allpoints, { x = point.x, z = point.z + meters, y= 0 })
- table.insert(allpoints, { x = point.x, z = point.z - meters, y= 0 })
- table.insert(allpoints, { x = point.x + math.cos(math.rad(45)) * meters, z = point.z + math.sin(math.rad(45)) * meters, y= 0 })
- table.insert(allpoints, { x = point.x - math.cos(math.rad(45)) * meters, z = point.z - math.sin(math.rad(45)) * meters, y= 0 })
- table.insert(allpoints, { x = point.x - math.cos(math.rad(45)) * meters, z = point.z + math.sin(math.rad(45)) * meters, y= 0 })
- table.insert(allpoints, { x = point.x + math.cos(math.rad(45)) * meters, z = point.z - math.sin(math.rad(45)) * meters, y= 0 })
+ allpoints[#allpoints + 1] = point
+ allpoints[#allpoints+1] = { x = point.x + meters, y = point.y, }
+ allpoints[#allpoints+1] = { x = point.x - meters, y = point.y, }
+ allpoints[#allpoints+1] = { x = point.x, y = point.y + meters, }
+ allpoints[#allpoints+1] = { x = point.x, y = point.y - meters, }
+ allpoints[#allpoints+1] = { x = point.x + math.cos(math.rad(45)) * meters, y = point.y + math.sin(math.rad(45)) * meters, }
+ allpoints[#allpoints+1] = { x = point.x - math.cos(math.rad(45)) * meters, y = point.y - math.sin(math.rad(45)) * meters, }
+ allpoints[#allpoints+1] = { x = point.x - math.cos(math.rad(45)) * meters, y = point.y + math.sin(math.rad(45)) * meters, }
+ allpoints[#allpoints+1] = { x = point.x + math.cos(math.rad(45)) * meters, y = point.y - math.sin(math.rad(45)) * meters, }
end
return UTIL.getConvexHull(allpoints)
@@ -262,12 +284,25 @@ do -- INIT DCS_UTIL
zone_type,
x,
- z,
+ y,
radius
verts,
}
]] --
+
+ ---@alias SpearheadTriggerZoneType
+ ---| "Cilinder"
+ ---| "Polygon"
+
+ ---@class SpearheadTriggerZone
+ ---@field name string
+ ---@field location Vec2
+ ---@field radius number
+ ---@field verts Array
+ ---@field zone_type SpearheadTriggerZoneType
+
+ ---@type Array
DCS_UTIL.__trigger_zones = {}
end
@@ -293,18 +328,9 @@ do -- INIT DCS_UTIL
}
DCS_UTIL.__airbaseNamesById = {}
- --[[
- zone = {
- name,
- zone_type,
- x,
- z,
- radius
- verts,
- }
- ]] --
- DCS_UTIL.__airbaseZonesById = {}
+ ---@type table
+ DCS_UTIL.__airbaseZonesByName = {}
DCS_UTIL.__airportsStartingCoalition = {}
DCS_UTIL.__warehouseStartingCoalition = {}
@@ -362,21 +388,25 @@ do -- INIT DCS_UTIL
do --init trigger zones
for i, trigger_zone in pairs(env.mission.triggers.zones) do
-
-- reorder verts as they are not ordered correctly in the ME
- verts = {}
- if Spearhead.Util.tableLength(trigger_zone.verticies) >=4 then
- table.insert(verts, { x = trigger_zone.verticies[4].x , z = trigger_zone.verticies[4].y })
- table.insert(verts, { x = trigger_zone.verticies[3].x , z = trigger_zone.verticies[3].y })
- table.insert(verts, { x = trigger_zone.verticies[2].x , z = trigger_zone.verticies[2].y })
- table.insert(verts, { x = trigger_zone.verticies[1].x , z = trigger_zone.verticies[1].y })
+ local verts = {}
+ if Spearhead.Util.tableLength(trigger_zone.verticies) >= 4 then
+ table.insert(verts, { x = trigger_zone.verticies[4].x, y = trigger_zone.verticies[4].y })
+ table.insert(verts, { x = trigger_zone.verticies[3].x, y = trigger_zone.verticies[3].y })
+ table.insert(verts, { x = trigger_zone.verticies[2].x, y = trigger_zone.verticies[2].y })
+ table.insert(verts, { x = trigger_zone.verticies[1].x, y = trigger_zone.verticies[1].y })
end
-
+
+ local zoneType = "Cilinder"
+ if trigger_zone.type == DCS_UTIL.ZoneType.Polygon then
+ zoneType = "Polygon"
+ end
+
+ ---@type SpearheadTriggerZone
local zone = {
name = trigger_zone.name,
- zone_type = trigger_zone.type,
- x = trigger_zone.x,
- z = trigger_zone.y,
+ zone_type = zoneType,
+ location = { x = trigger_zone.x, y = trigger_zone.y },
radius = trigger_zone.radius,
verts = verts
}
@@ -408,35 +438,36 @@ do -- INIT DCS_UTIL
end
end
- do -- fill airbaseNames and zones
+ do -- fill airbaseNames and zones
local airbases = world.getAirbases()
if airbases then
for _, airbase in pairs(airbases) do
local name = airbase:getName()
- local id = tostring(airbase:getID())
- if name and id then
- DCS_UTIL.__airbaseNamesById[id] = name
+ DCS_UTIL.__airbaseNamesById[tostring(airbase:getID())] = name
+ if name then
local relevantPoints = {}
for _, x in pairs(airbase:getRunways()) do
if x.position and x.position.x and x.position.z then
- table.insert(relevantPoints, { x = x.position.x, z = x.position.z, y=0})
+ table.insert(relevantPoints, { x = x.position.x, z = x.position.z, y = 0 })
end
end
for _, x in pairs(airbase:getParking()) do
if x.vTerminalPos and x.vTerminalPos.x and x.vTerminalPos.z then
- table.insert(relevantPoints, { x = x.vTerminalPos.x, z = x.vTerminalPos.z, y=0})
+ table.insert(relevantPoints, { x = x.vTerminalPos.x, z = x.vTerminalPos.z, y = 0 })
end
end
-
+
local points = UTIL.getConvexHull(relevantPoints)
local enlargedPoints = UTIL.enlargeConvexHull(points, 750)
- DCS_UTIL.__airbaseZonesById[id] = {
+ DCS_UTIL.__airbaseZonesByName[name] = {
name = name,
- zone_type = DCS_UTIL.ZoneType.Polygon,
+ location = { x = airbase:getPoint().x, y = airbase:getPoint().z },
+ zone_type = "Polygon",
+ radius = 0,
verts = enlargedPoints
}
end
@@ -484,7 +515,7 @@ do -- INIT DCS_UTIL
end
---destroy the given group
- ---@param groupName string
+ ---@param groupName string
function DCS_UTIL.DestroyGroup(groupName)
if DCS_UTIL.IsGroupStatic(groupName) then
local object = StaticObject.getByName(groupName)
@@ -514,13 +545,14 @@ do -- INIT DCS_UTIL
end
end
-
--- takes a list of units and returns all the units that are in any of the zones
---@param unit_names table unit names
---@param zone_names table zone names
---@return table unit list of objects { unit = UNIT, zone_name = zoneName}
function DCS_UTIL.getUnitsInZones(unit_names, zone_names)
local units = {}
+
+ ---@type Array
local zones = {}
for k = 1, #unit_names do
@@ -544,14 +576,9 @@ do -- INIT DCS_UTIL
local lCat = Object.getCategory(lUnit)
for zone_name, zone in pairs(zones) do
if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit
- if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
- if UTIL.IsPointInPolygon(zone.verts, unit_pos.x, unit_pos.z) == true then
- in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
- end
- else
- if (((unit_pos.x - zone.x) ^ 2 + (unit_pos.z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
- in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
- end
+ local isInZone = UTIL.is3dPointInZone(unit_pos, zone)
+ if isInZone == true then
+ in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
end
end
end
@@ -569,22 +596,13 @@ do -- INIT DCS_UTIL
return {}
end
- -- MAP Just for mapping sake
- local custom_zone = {
- x = zone.x,
- z = zone.z,
- zone_type = zone.zone_type,
- radius = zone.radius,
- verts = zone.verts
- }
-
- return DCS_UTIL.areGroupsInCustomZone(group_names, custom_zone)
+ return DCS_UTIL.areGroupsInCustomZone(group_names, zone)
end
--- takes a x, y poistion and checks if it is inside any of the zones
- ---@param group_names table North South position
- ---@param zone table { x, z, zonetype, radius, verts }
- ---@return table groupnames list of groups that are in the zone
+ ---@param group_names Array North South position
+ ---@param zone SpearheadTriggerZone
+ ---@return Array groupnames list of groups that are in the zone
function DCS_UTIL.areGroupsInCustomZone(group_names, zone)
local units = {}
if Spearhead.Util.tableLength(group_names) < 1 then return {} end
@@ -606,14 +624,9 @@ do -- INIT DCS_UTIL
local result_groups = {}
for _, entry in pairs(units) do
local pos = entry.unit:getPoint()
- if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
- if UTIL.IsPointInPolygon(zone.verts, pos.x, pos.z) == true then
- table.insert(result_groups, entry.groupname)
- end
- else
- if (((pos.x - zone.x) ^ 2 + (pos.z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
- table.insert(result_groups, entry.groupname)
- end
+ local isInZone = UTIL.is3dPointInZone(pos, zone)
+ if isInZone == true then
+ table.insert(result_groups, entry.groupname)
end
end
return result_groups
@@ -625,6 +638,7 @@ do -- INIT DCS_UTIL
---@param zone_names table zone names
---@return table zones list of objects { zone_name = zoneName}
function DCS_UTIL.isPositionInZones(x, z, zone_names)
+ ---@type Array
local zones = {}
for index, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name]
@@ -635,14 +649,8 @@ do -- INIT DCS_UTIL
local result_zones = {}
for zone_name, zone in pairs(zones) do
- if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
- if UTIL.IsPointInPolygon(zone.verts, x, z) == true then
- result_zones[#result_zones + 1] = zone.name
- end
- else
- if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
- result_zones[#result_zones + 1] = zone.name
- end
+ if UTIL.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then
+ result_zones[#result_zones + 1] = zone.name
end
end
return result_zones
@@ -655,14 +663,8 @@ do -- INIT DCS_UTIL
---@return boolean result
function DCS_UTIL.isPositionInZone(x, z, zone_name)
local zone = DCS_UTIL.__trigger_zones[zone_name]
- if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
- if UTIL.IsPointInPolygon(zone.verts, x, z) == true then
- return true
- end
- else
- if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
- return true
- end
+ if UTIL.is3dPointInZone({ x = x, y = 0, z = z }, zone) then
+ return true
end
return false
end
@@ -672,58 +674,29 @@ do -- INIT DCS_UTIL
---@param parent_zone_name string
---@return boolean result
function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name)
- local zoneA = DCS_UTIL.__trigger_zones[zone_name]
- local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name]
-
- if zoneB.zone_type == DCS_UTIL.ZoneType.Polygon and zoneB.verts then
- if UTIL.IsPointInPolygon(zoneB.verts, zoneA.x, zoneA.z) == true then
- return true
- end
- else
- if (((zoneA.x - zoneB.x) ^ 2 + (zoneA.z - zoneB.z) ^ 2) ^ 0.5 <= zoneB.radius) then
- return true
- end
- end
- return false
- end
-
- --- takes a x, y poistion and checks if it is inside any of the zones
- ---@param x number North South position
- ---@param z number West East position
- ---@param zone table { x, z, zonetype, radius }
- ---@return boolean result
- function DCS_UTIL.isPositionInCustomZone(x, z, zone)
- if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
- if UTIL.IsPointInPolygon(zone.verts, x, z) == true then
- return true
- end
- else
- if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
- return true
- end
- end
- return false
+ local zoneA = DCS_UTIL.__trigger_zones[zone_name] --[[@as SpearheadTriggerZone]]
+ if zoneA == nil then return false end
+ local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name] --[[@as SpearheadTriggerZone]]
+ if zoneB == nil then return false end
+ return UTIL.is3dPointInZone({ x = zoneA.location.x, y = 0, z = zoneA.location.y }, zoneB)
end
---comment
---@param zone_name any
- ---@return table? zone { name,b zone_type, x, z, radius, verts }
+ ---@return SpearheadTriggerZone? zone { name,b zone_type, x, z, radius, verts }
function DCS_UTIL.getZoneByName(zone_name)
if zone_name == nil then return nil end
return DCS_UTIL.__trigger_zones[zone_name]
end
---comment
- ---@param airbaseId any
- ---@return table? zone { name,b zone_type, x, z, radius, verts }
- function DCS_UTIL.getAirbaseZoneById(airbaseId)
- local string = tostring(airbaseId)
+ ---@param airbaseName string
+ ---@return SpearheadTriggerZone? zone { name,b zone_type, x, z, radius, verts }
+ function DCS_UTIL.getAirbaseZoneByName(airbaseName)
if string == nil then return nil end
- return DCS_UTIL.__airbaseZonesById[string]
+ return DCS_UTIL.__airbaseZonesByName[string]
end
-
-
---maps the category name to the DCS group category
---@param input string the name
---@return integer?
@@ -790,7 +763,7 @@ do -- INIT DCS_UTIL
---@return Array units
function DCS_UTIL.getAllPlayerUnits()
local units = {}
- for i = 0,2 do
+ for i = 0, 2 do
local players = coalition.getPlayers(i)
for key, unit in pairs(players) do
units[#units + 1] = unit
@@ -809,7 +782,7 @@ do -- INIT DCS_UTIL
---get base from id
---@param baseId number
- ---@return table? table
+ ---@return Airbase? table
function DCS_UTIL.getAirbaseById(baseId)
local name = DCS_UTIL.getAirbaseName(baseId)
if name == nil then return nil end
@@ -817,7 +790,7 @@ do -- INIT DCS_UTIL
end
---Get the starting coalition of a farp or airbase
- ---@param airbase Airbase
+ ---@param airbase Airbase
---@return number? coalition
function DCS_UTIL.getStartingCoalition(airbase)
if airbase == nil then
@@ -865,6 +838,92 @@ do -- INIT DCS_UTIL
end
end
+ ---@class DrawColor
+ ---@field r number
+ ---@field g number
+ ---@field b number
+ ---@field a number
+
+ local drawID = 400
+ ---@param zone SpearheadTriggerZone
+ ---@param lineColor DrawColor
+ ---@param fillColor DrawColor
+ ---@param lineStyle LineType
+ ---@return number drawID
+ function DCS_UTIL.DrawZone(zone, lineColor, fillColor, lineStyle)
+ if lineStyle == nil then lineStyle = 4 end
+ drawID = drawID + 1
+ if zone.zone_type == "Cilinder" then
+ trigger.action.circleToAll(-1, drawID, { x = zone.location.x, y = 0, z = zone.location.y }, zone.radius,
+ { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, lineStyle, true)
+ else
+ local functionString = "trigger.action.markupToAll(7, -1, " .. drawID .. ","
+ for _, vecpoint in pairs(zone.verts) do
+ functionString = functionString .. " { x=" .. vecpoint.x .. ", y=0,z=" .. vecpoint.y .. "},"
+ end
+ functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
+
+ env.info(functionString)
+ ---@diagnostic disable-next-line: deprecated
+ local f, err = loadstring(functionString)
+ if f then
+ f()
+ else
+ env.error("Something failed when drawing complex drawing" .. err)
+ end
+ end
+ local fillColorMapped = {
+ fillColor.r or 0,
+ fillColor.g or 0,
+ fillColor.b or 0,
+ fillColor.a or 0.5
+ }
+
+ local lineColorMapped = {
+ lineColor.r or 0,
+ lineColor.g or 0,
+ lineColor.b or 0,
+ lineColor.a or 1
+ }
+
+ trigger.action.setMarkupColorFill(drawID, fillColorMapped)
+ trigger.action.setMarkupColor(drawID, lineColorMapped)
+
+ return drawID
+ end
+
+ ---comment
+ ---@param drawID number
+ ---@param lineColor DrawColor
+ function DCS_UTIL.SetLineColor(drawID, lineColor)
+ local lineColorMapped = {
+ lineColor.r or 0,
+ lineColor.g or 0,
+ lineColor.b or 0,
+ lineColor.a or 1
+ }
+ trigger.action.setMarkupColor(drawID, lineColorMapped)
+ end
+
+ ---comment
+ ---@param drawID number
+ ---@param fillColor DrawColor
+ function DCS_UTIL.SetFillColor(drawID, fillColor)
+ local lineColorMapped = {
+ fillColor.r or 0,
+ fillColor.g or 0,
+ fillColor.b or 0,
+ fillColor.a or 1
+ }
+ trigger.action.setMarkupColorFill(drawID, lineColorMapped)
+ end
+
+ function DCS_UTIL.RemoveZoneDraw(drawID)
+ if drawID ~= nil then
+ trigger.action.removeMark(drawID)
+ end
+ end
+
--- spawns the units as specified in the mission file itself
--- location and route can be nil and will then use default route
---@param groupName string
@@ -930,15 +989,15 @@ do -- INIT DCS_UTIL
end
end
end
-
+
return false
end
---comment
---@param groupId number
- ---@return Group?
+ ---@return Group?
function DCS_UTIL.GetPlayerGroupByGroupID(groupId)
- for i = 0,2 do
+ for i = 0, 2 do
local players = coalition.getPlayers(i)
for key, unit in pairs(players) do
if unit and unit:isExist() == true then
diff --git a/classes/spearhead_db.lua b/classes/spearhead_db.lua
index 5b12e3d..3d1fdc0 100644
--- a/classes/spearhead_db.lua
+++ b/classes/spearhead_db.lua
@@ -101,7 +101,7 @@ function Database.New(Logger)
local zone_name = zone_data.name
---@type Vec2
- local zoneLocation = { x = zone_data.x, y = zone_data.z }
+ local zoneLocation = { x = zone_data.location.x, y = zone_data.location.y }
local split_string = Spearhead.Util.split_string(zone_name, "_")
table.insert(self._tables.AllZoneNames, zone_name)
@@ -311,12 +311,9 @@ function Database.New(Logger)
if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
- table.insert(capData.routes, { point1 = { x = zone.x, z = zone.z }, point2 = nil })
+ table.insert(capData.routes, { point1 = { x = zone.location.x, z = zone.location.y, y = 0 }, point2 = nil })
else
- local function getDist(a, b)
- return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
- end
-
+
local biggest = nil
local biggestA = nil
local biggestB = nil
@@ -325,7 +322,7 @@ function Database.New(Logger)
for ii = i + 1, 4 do
local a = zone.verts[i]
local b = zone.verts[ii]
- local dist = getDist(a, b)
+ local dist = Spearhead.Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then
biggestA = a
@@ -338,8 +335,8 @@ function Database.New(Logger)
if biggestA and biggestB then
table.insert(capData.routes,
{
- point1 = { x = biggestA.x, z = biggestA.z },
- point2 = { x = biggestB.x, z = biggestB.z }
+ point1 = { x = biggestA.x, z = biggestA.y },
+ point2 = { x = biggestB.x, z = biggestB.y }
})
end
end
@@ -348,9 +345,24 @@ function Database.New(Logger)
end
end
+ local totalUnits = 0
+ local missions = 0
+ for _, data in pairs(self._tables.MissionZoneData) do
+ missions = missions + 1
+ for _, groupName in pairs(data.Groups) do
+ local group = Group.getByName(groupName)
+ if group then
+ totalUnits = totalUnits + group:getInitialSize()
+ end
+ end
+ end
+
+ if missions == 0 then missions = 1 end
+
self._logger:info("initiated the database with amount of zones: ")
self._logger:info("Stages: " .. Spearhead.Util.tableLength(self._tables.StageZones))
self._logger:info("Total Missions: " .. Spearhead.Util.tableLength(self._tables.MissionZoneData))
+ self._logger:info("Average units per mission: " .. totalUnits / missions)
self._logger:info("Random Missions: " .. Spearhead.Util.tableLength(self._tables.RandomMissionZones))
self._logger:info("Farps: " .. Spearhead.Util.tableLength(self._tables.AllFarpZones))
self._logger:info("Airbases: " .. Spearhead.Util.tableLength(self._tables.AirbaseDataPerAirfield))
@@ -393,9 +405,21 @@ function Database:loadCapUnits()
local all_groups = getAvailableCAPGroups()
local airbases = world.getAirbases()
for _, airbase in pairs(airbases) do
- local baseId = airbase:getID()
local point = airbase:getPoint()
- local zone = Spearhead.DcsUtil.getAirbaseZoneById(baseId) or { x = point.x, z = point.z, radius = 4000 }
+
+ ---@type SpearheadTriggerZone?
+ local zone = Spearhead.DcsUtil.getAirbaseZoneByName(airbase:getName())
+
+ if zone == nil then
+ zone = {
+ location = {x = point.x, y = point.z} ,
+ radius = 4000,
+ name = "temp_zone",
+ verts = {},
+ zone_type = "Cilinder"
+ }
+ end
+
local baseData = self:getOrCreateAirbaseData(airbase:getName())
local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, zone)
@@ -480,9 +504,19 @@ function Database:loadAirbaseGroups()
if base then
local basedata = self:getOrCreateAirbaseData(baseName)
- local baseId = base:getID()
local point = base:getPoint()
- local airbaseZone = Spearhead.DcsUtil.getAirbaseZoneById(baseId) or { x = point.x, z = point.z, radius = 4000 }
+ local airbaseZone = Spearhead.DcsUtil.getAirbaseZoneByName(baseName)
+
+ if airbaseZone == nil then
+ airbaseZone = {
+ location = {x = point.x, y = point.z} ,
+ radius = 4000,
+ name = "temp_zone",
+ verts = {},
+ zone_type = "Cilinder"
+ }
+ end
+
if airbaseZone and base:getDesc().category == Airbase.Category.AIRDROME then
local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, airbaseZone)
@@ -554,7 +588,11 @@ function Database:GetLocationForMissionZone(missionZoneName)
return self._tables.MissionZonesLocations[missionZoneName]
end
-function Database:getCapRouteInZone(stageNumber, baseId)
+---comment
+---@param stageNumber string
+---@param baseName string
+---@return table?
+function Database:getCapRouteInZone(stageNumber, baseName)
local stageNumber = tostring(stageNumber) or "nothing"
local routeData = self._tables.CapDataPerStageNumber[stageNumber]
if routeData then
@@ -576,24 +614,24 @@ function Database:getCapRouteInZone(stageNumber, baseId)
local aY = pC.z + vY / magV * radius;
return { x = aX, z = aY }
end
- local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or
- "none"
+ local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or "none"
local stagezone = Spearhead.DcsUtil.getZoneByName(stageZoneName)
if stagezone then
- local base = Spearhead.DcsUtil.getAirbaseById(baseId)
+
+ ---@type Airbase?
+ local base = Airbase.getByName(baseName)
if base then
local closest = nil
if stagezone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
- closest = GetClosestPointOnCircle({ x = stagezone.x, z = stagezone.z }, stagezone.radius,
+ closest = GetClosestPointOnCircle({ x = stagezone.location.x, z = stagezone.location.y }, stagezone.radius,
base:getPoint())
else
- local function getDist(a, b)
- return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
- end
local closestDistance = -1
for _, vert in pairs(stagezone.verts) do
- local distance = getDist(vert, base:getPoint())
+ local pos = base:getPoint()
+ local vec2 = { x = pos.x, y = pos.z }
+ local distance = Spearhead.Util.VectorDistance2d(vert, vec2)
if closestDistance == -1 or distance < closestDistance then
closestDistance = distance
closest = vert
@@ -602,17 +640,19 @@ function Database:getCapRouteInZone(stageNumber, baseId)
end
if math.random(1, 2) % 2 == 0 then
- return { point1 = closest, point2 = { x = stagezone.x, z = stagezone.z } }
+ return { point1 = closest, point2 = { x = stagezone.location.x, z = stagezone.location.y } }
else
- return { point1 = { x = stagezone.x, z = stagezone.z }, point2 = closest }
+ return { point1 = { x = stagezone.location.x, z = stagezone.location.y }, point2 = closest }
end
end
end
+
+ return nil
end
end
----@return table result a list of stage zone names
+---@return Array result a list of stage zone names
function Database:getStagezoneNames()
return self._tables.StageZoneNames
end
diff --git a/classes/spearhead_events.lua b/classes/spearhead_events.lua
index 1928d5d..762c9e3 100644
--- a/classes/spearhead_events.lua
+++ b/classes/spearhead_events.lua
@@ -73,7 +73,35 @@ do
end
end
-
+ ---@class OnWeaponFiredListener
+ ---@field OnWeaponFired fun(self:OnWeaponFiredListener, unit:Unit?, weapon:Weapon?, target:Object?)
+
+ ---@type Array
+ local onWeaponFiredListeners = {}
+
+ ---comment
+ ---@param weaponFiredListener OnWeaponFiredListener
+ SpearheadEvents.AddWeaponFiredListener = function(weaponFiredListener)
+ if type(weaponFiredListener) ~= "table" then
+ warn("Event handler not of type table/object")
+ return
+ end
+
+ table.insert(onWeaponFiredListeners, weaponFiredListener)
+ end
+
+ local triggerWeaponFired = function(unit, weapon, target)
+ for _, callable in pairs(onWeaponFiredListeners) do
+ local succ, err = pcall(function()
+ callable:OnWeaponFired(unit, weapon, target)
+ end)
+
+ if err then
+ logError(err)
+ end
+ end
+ end
+
local onLandEventListeners = {}
---Add an event listener to a specific unit
@@ -311,6 +339,15 @@ do
end
+ if event.id == world.event.S_EVENT_SHOT then
+
+ local shooter = event.initiator
+ local weapon = event.weapon
+ local target = event.target
+ triggerWeaponFired(shooter, weapon, target)
+
+ end
+
if event.id == world.event.S_EVENT_MISSION_END then
Spearhead.classes.persistence.Persistence.UpdateNow()
end
diff --git a/classes/spearhead_routeutil.lua b/classes/spearhead_routeutil.lua
index 590994e..4d27da9 100644
--- a/classes/spearhead_routeutil.lua
+++ b/classes/spearhead_routeutil.lua
@@ -212,12 +212,11 @@ do --setup route util
---@param durationOnStation number
---@param attackHelos boolean
---@param deviationDistance number
- ---@return table route
- ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed,
- durationOnStation, attackHelos, deviationDistance)
+ ---@return table? route
+ ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance)
local baseName = Spearhead.DcsUtil.getAirbaseName(airdromeId)
if baseName == nil then
- return {}
+ return nil
end
durationOnStation = durationOnStation or 1800
@@ -228,7 +227,7 @@ do --setup route util
local base = Airbase.getByName(baseName)
if base == nil then
- return {}
+ return nil
end
local additionalFlyOverTasks = {
diff --git a/classes/stageClasses/GlobalStageManager.lua b/classes/stageClasses/GlobalStageManager.lua
index 6ac1d5c..9bc51e1 100644
--- a/classes/stageClasses/GlobalStageManager.lua
+++ b/classes/stageClasses/GlobalStageManager.lua
@@ -19,10 +19,11 @@ GlobalStageManager = {}
---comment
---@param database Database
---@param stageConfig StageConfig
+---@param logLevel LogLevel
---@return nil
-function GlobalStageManager:NewAndStart(database, stageConfig)
- local logger = Spearhead.LoggerTemplate.new("StageManager", stageConfig.logLevel)
- logger:info("Using Stage Log Level: " .. stageConfig.logLevel)
+function GlobalStageManager:NewAndStart(database, stageConfig, logLevel)
+ local logger = Spearhead.LoggerTemplate.new("StageManager", logLevel)
+ logger:info("Using Stage Log Level: " .. logLevel)
local o = {}
setmetatable(o, { __index = self })
@@ -122,7 +123,7 @@ function GlobalStageManager:NewAndStart(database, stageConfig)
end
local stageDisplayName = split[3]
- local stagelogger = Spearhead.LoggerTemplate.new(stageName, stageConfig.logLevel)
+ local stagelogger = Spearhead.LoggerTemplate.new(stageName, logLevel)
if valid == true and orderNumber then
---@type StageInitData
@@ -175,7 +176,7 @@ function GlobalStageManager:NewAndStart(database, stageConfig)
end
if valid == true then
- local stagelogger = Spearhead.LoggerTemplate.new(stageName, stageConfig.logLevel)
+ local stagelogger = Spearhead.LoggerTemplate.new(stageName, logLevel)
---@type WaitingStageInitData
local initData = {
diff --git a/classes/stageClasses/SpecialZones/BlueSam.lua b/classes/stageClasses/SpecialZones/BlueSam.lua
index f79b98e..6777315 100644
--- a/classes/stageClasses/SpecialZones/BlueSam.lua
+++ b/classes/stageClasses/SpecialZones/BlueSam.lua
@@ -57,7 +57,6 @@ function BlueSam.New(database, logger, zoneName)
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Spearhead.Util.VectorDistance3d(blueUnitPos, redUnitPos)
- env.info("distance: " .. tostring(distance))
if distance <= cleanup_distance then
self._cleanupUnits[redUnitName] = true
end
@@ -91,4 +90,4 @@ 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.SpecialZones == nil then Spearhead.classes.stageClasses.SpecialZones = {} end
-Spearhead.classes.stageClasses.Missions.SpecialZones = BlueSam
+Spearhead.classes.stageClasses.missions.SpecialZones = BlueSam
diff --git a/classes/stageClasses/Stages/BaseStage/Stage.lua b/classes/stageClasses/Stages/BaseStage/Stage.lua
index 9333c77..52553f0 100644
--- a/classes/stageClasses/Stages/BaseStage/Stage.lua
+++ b/classes/stageClasses/Stages/BaseStage/Stage.lua
@@ -6,8 +6,8 @@
--- @class StageData
--- @field missionsByCode table
---- @field missions Array
---- @field sams Array
+--- @field missions Array
+--- @field sams Array
--- @field blueSams Array
--- @field airbases Array
--- @field miscGroups Array
@@ -24,7 +24,7 @@
--- @class Stage : MissionCompleteListener, OnStageChangedListener
--- @field zoneName string
---- @field stageName string
+--- @field stageName string?
--- @field stageNumber number
--- @field protected _isActive boolean
--- @field protected _isComplete boolean
@@ -47,6 +47,14 @@ Stage.__index = Stage
local stageDrawingId = 100
+Stage.StageColors = {
+ INVISIBLE = { r=0, g=0, b=0, a=0 },
+ RED_ACTIVE = { r=1, g=0, b=0, a=0.15 },
+ RED_PREACTIVE = { r=230/255, g=153/255, b=0, a=0.3},
+ BLUE = { r=0, g=0, b=1, a=0.15},
+ GRAY = { r=80/255, g=80/255, b=80/255, a=0.15 }
+}
+
---comment
---@param database Database
---@param stageConfig StageConfig
@@ -83,7 +91,12 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
self._activeStage = -99
self._preActivated = false
self._stageConfig = stageConfig or {}
- self._stageDrawingId = stageDrawingId + 1
+
+ local zone = Spearhead.DcsUtil.getZoneByName(self.zoneName)
+ if zone then
+ self._stageDrawingId = Spearhead.DcsUtil.DrawZone(zone, Stage.StageColors.INVISIBLE, Stage.StageColors.INVISIBLE, 4)
+ end
+
self._spawnedGroups = {}
self._missionPriority = missionPriority
self._stageCompleteListeners = {}
@@ -108,8 +121,9 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
do -- load tables
local missionZones = database:getMissionsForStage(self.zoneName)
+ self._logger:debug("Found " .. Spearhead.Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName)
for _, missionZone in pairs(missionZones) do
- local mission = Spearhead.classes.stageClasses.Missions.Mission.New(missionZone, self._missionPriority, database, logger)
+ local mission = Spearhead.classes.stageClasses.missions.ZoneMission.new(missionZone, self._missionPriority, database, logger)
if mission then
self._db.missionsByCode[mission.code] = mission
if mission.missionType == "SAM" then
@@ -125,7 +139,7 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
---@type table>
local randomMissionByName = {}
for _, missionZoneName in pairs(randomMissionNames) do
- local mission = Spearhead.classes.stageClasses.Missions.Mission.New(missionZoneName, self._missionPriority, database, logger)
+ local mission = Spearhead.classes.stageClasses.missions.ZoneMission.new(missionZoneName, self._missionPriority, database, logger)
if mission then
if randomMissionByName[mission.name] == nil then
randomMissionByName[mission.name] = {}
@@ -200,14 +214,14 @@ function Stage:IsComplete()
if self._isComplete == true then return true end
for i, mission in pairs(self._db.sams) do
- local state = mission:GetState()
+ local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then
return false
end
end
for i, mission in pairs(self._db.missions) do
- local state = mission:GetState()
+ local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then
return false
end
@@ -231,7 +245,7 @@ function Stage:CheckAndUpdateSelf()
local availableMissions = {}
for _, mission in pairs(dbTables.missionsByCode) do
- local state = mission:GetState()
+ local state = mission:getState()
if state == "ACTIVE" then
activeCount = activeCount + 1
@@ -266,7 +280,7 @@ end
---private use only
function Stage:NotifyComplete()
- self._logger:info("Stage complete: " .. self.stageName)
+ self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown"))
for _, listener in pairs(self._stageCompleteListeners) do
pcall(function()
@@ -285,12 +299,13 @@ function Stage:AddStageCompleteListener(listener)
end
---Activates all SAMS, Airbase units etc all at once.
-function Stage:PreActivate()
+---@param draw boolean
+function Stage:PreActivate(draw)
if self._preActivated == false then
self._preActivated = true
for key, mission in pairs(self._db.sams) do
if mission then
- mission:SpawnActive()
+ mission:SpawnInactive()
end
end
@@ -298,36 +313,25 @@ function Stage:PreActivate()
airbase:ActivateRedStage()
end
end
-end
----@param stageColor StageColor
-function Stage:MarkStage(stageColor)
- local fillColor = {1, 0, 0, 0.1}
- local line ={ 1, 0,0, 1 }
-
- if stageColor == "RED" then
- fillColor = {1, 0, 0, 0.1}
- line ={ 1, 0,0, 1 }
- elseif stageColor =="BLUE" then
- fillColor = {0, 0, 1, 0.1}
- line ={ 0, 0,1, 1 }
- elseif stageColor == "GRAY" then
- fillColor = {80/255, 80/255, 80/255, 0.15}
- line ={ 80/255, 80/255,80/255, 1 }
+ if draw == true then
+ self:MarkStage(Stage.StageColors.RED_PREACTIVE)
end
- local zone = Spearhead.DcsUtil.getZoneByName(self.zoneName)
- if zone and self._stageConfig.isDrawStagesEnabled == true then
- self._logger:debug("drawing stage: " .. self.zoneName)
- if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
- trigger.action.circleToAll(-1, self._stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, {0,0,0,0}, {0,0,0,0},4, true)
- else
- --trigger.action.circleToAll(-1, self.stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, { 1, 0,0, 1 }, {1,0,0,1},4, true)
- trigger.action.quadToAll( -1, self._stageDrawingId, zone.verts[1], zone.verts[2], zone.verts[3], zone.verts[4], {0,0,0,0}, {0,0,0,0}, 4, true)
- end
+end
- trigger.action.setMarkupColorFill(self._stageDrawingId, fillColor)
- trigger.action.setMarkupColor(self._stageDrawingId, line)
+---@param stageColor DrawColor
+function Stage:MarkStage(stageColor)
+ local lineColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
+ local fillColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
+
+ if stageColor.a > 0 then
+ lineColor.a = 1
+ end
+
+ if self._stageDrawingId and self._stageConfig.isDrawStagesEnabled == true then
+ Spearhead.DcsUtil.SetLineColor(self._stageDrawingId, lineColor)
+ Spearhead.DcsUtil.SetFillColor(self._stageDrawingId, fillColor)
end
end
@@ -335,10 +339,10 @@ function Stage:ActivateStage()
self._isActive = true;
pcall(function()
- self:MarkStage("RED")
+ self:MarkStage(Stage.StageColors.RED_ACTIVE)
end)
- self:PreActivate()
+ self:PreActivate(false)
self._logger:debug("Activating Misc groups for zone. Count: " .. Spearhead.Util.tableLength(self._db.miscGroups))
for _, miscGroup in pairs(self._db.miscGroups) do
@@ -371,8 +375,12 @@ function Stage:OnStageNumberChanged(number)
local previousActive = self._activeStage
self._activeStage = number
- if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
- self:PreActivate()
+
+ if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
+ self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
+ self:PreActivate(true)
+ elseif Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
+ self:PreActivate(false)
end
if number == self.stageNumber then
@@ -434,7 +442,7 @@ function Stage:ActivateBlueStage()
---@param self Stage
local ActivateBlueAsync = function(self)
pcall(function()
- self:MarkStage("BLUE")
+ self:MarkStage(Stage.StageColors.BLUE)
end)
self:ActivateBlueGroups()
diff --git a/classes/stageClasses/Stages/ExtraStage.lua b/classes/stageClasses/Stages/ExtraStage.lua
index 9cf1db3..e1269a6 100644
--- a/classes/stageClasses/Stages/ExtraStage.lua
+++ b/classes/stageClasses/Stages/ExtraStage.lua
@@ -1,10 +1,8 @@
---@class ExtraStage : Stage
local ExtraStage = {}
-
ExtraStage.__index = ExtraStage
-local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
-setmetatable(ExtraStage, Stage)
+
---comment
---@param database Database
@@ -14,11 +12,15 @@ setmetatable(ExtraStage, Stage)
---@return ExtraStage
function ExtraStage.New(database, stageConfig, logger, initData)
+ local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
+ setmetatable(ExtraStage, Stage)
+
local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
self:superNew(database, stageConfig, logger, initData, "secondary")
self.OnPostBlueActivated = function (selfStage)
- selfStage:MarkStage("GRAY")
+
+ selfStage:MarkStage(Stage.StageColors.GRAY)
end
self.OnPostStageComplete = function (selfStage)
@@ -35,7 +37,7 @@ function ExtraStage:OnStageNumberChanged(number)
self._activeStage = number
if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
- self:PreActivate()
+ self:PreActivate(false)
end
if number == self.stageNumber then
diff --git a/classes/stageClasses/Stages/WaitingStage.lua b/classes/stageClasses/Stages/WaitingStage.lua
index 5ade89a..163f773 100644
--- a/classes/stageClasses/Stages/WaitingStage.lua
+++ b/classes/stageClasses/Stages/WaitingStage.lua
@@ -27,9 +27,10 @@ function WaitingStage.New(database, stageConfig, logger, initData)
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
self._startTime = nil
- self.CheckContinuousAsync = function (self, time)
- if self:IsComplete() then
- self:NotifyComplete()
+ self.CheckContinuousAsync = function (selfA, time)
+
+ if selfA:IsComplete() == true then
+ selfA:NotifyComplete()
return nil
end
diff --git a/classes/stageClasses/helpers/MissionCommandsHelper.lua b/classes/stageClasses/helpers/MissionCommandsHelper.lua
index 241ace9..dbc1d78 100644
--- a/classes/stageClasses/helpers/MissionCommandsHelper.lua
+++ b/classes/stageClasses/helpers/MissionCommandsHelper.lua
@@ -114,7 +114,7 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
end
end
- return string.format("[%s] %-15s %-20s %10s nM\n", mission.code, mission.displayMissionType, mission.name, distanceText)
+ return string.format("[%s] %-15s %-20s %10s nM\n", mission.code, mission.missionTypeDisplay, mission.name, distanceText)
end
---Primary missions
@@ -218,7 +218,6 @@ function MissionCommandsHelper:addMissionCommands(groupId, mission)
end
if path then
- self._logger:debug("Registering command: [" .. mission.code .. "]" .. mission.name)
local missionFolderName = "[" .. mission.code .. "]" .. mission.name
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName)
diff --git a/classes/stageClasses/missions/RunwayStrikeMission.lua b/classes/stageClasses/missions/RunwayStrikeMission.lua
new file mode 100644
index 0000000..6016671
--- /dev/null
+++ b/classes/stageClasses/missions/RunwayStrikeMission.lua
@@ -0,0 +1,543 @@
+---@class RunwayStrikeMission : Mission
+---@field runwayBombingTracker RunwayBombingTracker
+---@field private _runway Runway
+---@field private _runwayZone SpearheadTriggerZone
+---@field private _airportName string
+---@field private _runwaySections Array
+---@field private _minKilosForDamage number
+---@field private _repairInProgress boolean
+local RunwayStrikeMission = {}
+
+---@class RunwaySection
+---@field center Vec2
+---@field corners Array
+---@field kilosHit number
+---@field repairGroups Array
+---@field drawID number?
+
+
+---@param runway Runway
+---@param database Database
+---@param logger Logger
+---@param runwayBombingTracker RunwayBombingTracker
+---@return RunwayStrikeMission?
+function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBombingTracker)
+
+ local Mission = Spearhead.classes.stageClasses.missions.baseMissions.Mission
+ RunwayStrikeMission.__index = RunwayStrikeMission
+ setmetatable(RunwayStrikeMission, Mission)
+ local self = setmetatable({}, RunwayStrikeMission)
+
+ self._airportName = airbaseName
+ local missionBriefing = "Bomb runway " .. runway.Name .. " at " .. airbaseName .. "to delay the CAP effort"
+
+ local success, error = Mission.newSuper(self, "noZone", runway.Name, "OCA", missionBriefing, "secondary", database, logger)
+
+ self.runwayBombingTracker = runwayBombingTracker
+ self._runway = runway
+ self._runwayZone = self:RunwayToSpearheadZone(runway)
+ self._repairInProgress = false
+ self._minKilosForDamage = 100
+
+ local sections = self:ToSections(runway, 5)
+ --[[
+ +-----------+-----------+-----------+-----------+-----------+
+ | Section 1 | Section 2 | Section 3 | Section 4 | Section 5 |
+ +-----------+-----------+-----------+-----------+-----------+
+ ]]
+
+ self._runwaySections = {
+ sections[2],
+ sections[3],
+ sections[4],
+ } --only take 2, 3 and 4 cause those are the most imporant parts of the runway
+
+ if not success then
+ logger:error("Failed to create RunwayBombingMission " .. runway.Name .. " => " .. error)
+ return nil
+ end
+
+ runwayBombingTracker:RegisterRunway(runway, self)
+
+ return self
+end
+
+---activates the mission
+function RunwayStrikeMission:SpawnActive()
+ self._missionCommandsHelper:AddMissionToCommands(self)
+ self:Draw()
+ self:UpdateState()
+end
+
+---@return SpearheadTriggerZone
+function RunwayStrikeMission:GetRunwayZone()
+ return self._runwayZone
+end
+
+function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
+
+ self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name)
+ for _, section in pairs(self._runwaySections) do
+
+ local zone = self:SectionToSpearheadZone(section)
+ if Spearhead.Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
+ if section.kilosHit == nil then
+ section.kilosHit = 0
+ end
+
+ section.kilosHit = section.kilosHit + explosiveMass
+ end
+ end
+
+ ---@param selfA RunwayStrikeMission
+ local updateState = function(selfA, time)
+ selfA:UpdateState()
+ end
+
+ timer.scheduleFunction(updateState, self, timer.getTime() + 5)
+
+end
+
+function RunwayStrikeMission:UpdateState()
+
+ self._logger:debug("Updating state of runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
+
+ self:Draw()
+
+ for _, section in pairs(self._runwaySections) do
+ if section.kilosHit > self._minKilosForDamage then
+ self:StartRepair()
+ self._missionCommandsHelper:RemoveMissionToCommands(self)
+ break
+ end
+ end
+end
+
+---@type DrawColor
+local healthyAreaColor = { r=0, g=1, b=0, a=0.5 }
+---@type DrawColor
+local healthyAreaLineColor = { r=0, g=1, b=0, a=1 }
+---@type DrawColor
+local damagedAreaColor ={ r=1, g=165/255, b=0, a=0.5 }
+---@type DrawColor
+local damagedAreaLineColor = { r=1, g=165/255, b=0, a=1 }
+---@type DrawColor
+local destroyedAreaColor = { r=1, g=0, b=0, a=0.5 }
+---@type DrawColor
+local destroyedAreaLineColor = { r=1, g=0, b=0, a=1 }
+
+---@private
+function RunwayStrikeMission:Draw()
+
+ ---comment
+ ---@param runwaySection RunwaySection
+ local function drawSection(runwaySection)
+ local lineColor = healthyAreaLineColor
+ local fillColor = healthyAreaColor
+
+ local orangeDamage = self._minKilosForDamage - 100
+ if orangeDamage < 0 then orangeDamage = self._minKilosForDamage * 0.8 end
+
+ if runwaySection.kilosHit > self._minKilosForDamage then
+ lineColor = destroyedAreaLineColor
+ fillColor = destroyedAreaColor
+ elseif runwaySection.kilosHit > orangeDamage then
+ lineColor = damagedAreaLineColor
+ fillColor = damagedAreaColor
+ end
+
+ if runwaySection.drawID == nil then
+ local zone = self:SectionToSpearheadZone(runwaySection)
+
+ local color = { r=0, g=1, b=0, a=0.5 }
+
+ runwaySection.drawID = Spearhead.DcsUtil.DrawZone(zone, color, color, 5)
+ else
+ Spearhead.DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
+ Spearhead.DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
+ end
+
+ end
+
+ for _, section in pairs(self._runwaySections) do
+ drawSection(section)
+ end
+end
+
+---@private
+---@param section RunwaySection
+---@return SpearheadTriggerZone
+function RunwayStrikeMission:SectionToSpearheadZone(section)
+ return {
+ location = { x = self._runway.position.x, y = self._runway.position.z },
+ radius = self._runway.width,
+ name = self._runway.Name,
+ verts = section.corners,
+ zone_type = "Polygon",
+ }
+end
+
+---@
+
+function RunwayStrikeMission:StartRepair()
+ if self._repairInProgress == true then return end
+
+ self._repairInProgress = true
+ self._logger:debug("Starting repair of runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
+ ---comment
+ ---@param selfA any
+ ---@return unknown
+ local repairTask = function (selfA, time)
+ local interval = selfA:DoRepairCycle()
+ if interval == nil then return nil end
+ return time + interval
+ end
+
+ timer.scheduleFunction(repairTask, self, timer.getTime() + 5)
+end
+
+function RunwayStrikeMission:DoRepairCycle()
+
+ local interval = 5
+ local repairPerSecond = 5
+
+ self._logger:debug("Repair cycle for" .. self._airportName .. ":" .. self._runway.Name)
+
+ local isHealed = true
+ for _, section in pairs(self._runwaySections) do
+ section.kilosHit = section.kilosHit - interval * repairPerSecond
+
+ if section.kilosHit > self._minKilosForDamage * 0.1 then
+ self:AddOrUpdateRepairStatics(section)
+ isHealed = false
+ else
+ self:RemoveRepairStatics(section)
+ end
+ end
+
+ self:Draw()
+
+ if isHealed == true then
+ self._repairInProgress = false
+ self:FullRepairRunway()
+ self._logger:debug("Repair complete for runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
+ return nil
+ end
+
+ return interval
+
+end
+
+---@class StaticSpawn
+---@field category string
+---@field type string
+---@field y number
+---@field x number
+---@field heading number
+
+
+local counter = 1
+
+---@type Array>
+local repairStaticConfigs = {
+ [1] = {
+ [1] = {
+ ["category"] = "Unarmed",
+ ["type"] = "ZIL-135",
+ ["y"] = -4,
+ ["x"] = 10,
+ ["heading"] = 6.2133721370998,
+ },
+ [2] = {
+ ["category"] = "Unarmed",
+ ["type"] = "Tigr_233036",
+ ["y"] = 0,
+ ["x"] = 10,
+ ["heading"] = 6.2133721370998,
+ },
+ [3] = {
+ ["category"] = "Unarmed",
+ ["type"] = "Infantry AK ver3",
+ ["y"] = 10,
+ ["x"] = 7,
+ ["heading"] = 4.4331363000656,
+ },
+ [4] = {
+ ["category"] = "Unarmed",
+ ["type"] = "Infantry AK ver3",
+ ["y"] = 12,
+ ["x"] = 1,
+ ["heading"] = 4.4331363000656,
+ },
+ [5] = {
+ ["category"] = "Unarmed",
+ ["type"] = "Infantry AK ver3",
+ ["y"] = -10,
+ ["x"] = -2,
+ ["heading"] = 4.1538836197465,
+ },
+ [6] = {
+ ["category"] = "Unarmed",
+ ["type"] = "CV_59_Large_Forklift",
+ ["y"] = -11,
+ ["x"] = 0,
+ ["heading"] = 1.535889741755,
+ },
+ [7] = {
+ ["category"] = "Unarmed",
+ ["type"] = "ZiL-131 APA-80",
+ ["y"] = 11,
+ ["x"] = 5,
+ ["heading"] = 0.62831853071796,
+ },
+ [8] = {
+ ["category"] = "Unarmed",
+ ["type"] = "CV_59_NS60",
+ ["y"] = -4,
+ ["x"] = -15,
+ ["heading"] = 0.62831853071796,
+ }
+ },
+ [2] = {
+ [1] = {
+ ["category"] = "Air Defence",
+ ["type"] = "generator_5i57",
+ ["y"] = 2.7889551542015,
+ ["x"] = -4.7698247930386,
+ ["heading"] = 4.0666171571468,
+ },
+ [2] = {
+ ["category"] = "Infantry",
+ ["type"] = "Infantry AK ver3",
+ ["y"] = 3.639392285097,
+ ["x"] = 1.4300755972836,
+ ["heading"] = 5.0440015382636,
+ },
+ [3] = {
+ ["category"] = "Infantry",
+ ["type"] = "Infantry AK ver3",
+ ["y"] = 3.5139072826724,
+ ["x"] = -0.32671443666074,
+ ["heading"] = 6.16101225954,
+ },
+ [4] = {
+ ["category"] = "Unarmed",
+ ["type"] = "ATMZ-5",
+ ["y"] = -6.9556010010953,
+ ["x"] = 3.007681753102,
+ ["heading"] = 4.0666171571468,
+ },
+ [5] = {
+ ["category"] = "Unarmed",
+ ["type"] = "GAZ-66",
+ ["y"] = 5.9304017026008,
+ ["x"] = 0.80323129595153,
+ ["heading"] = 6.2308254296198,
+ }
+ }
+}
+
+---@private
+---@param section RunwaySection
+function RunwayStrikeMission:AddOrUpdateRepairStatics(section)
+ if Spearhead.Util.tableLength(section.repairGroups) > 0 then
+ return
+ end
+
+ local location = { --Can randomise a little later
+ x = section.center.x,
+ y = section.center.y,
+ }
+
+ local repairGroup = Spearhead.Util.randomFromList(repairStaticConfigs)
+ for _, repairStatic in pairs(repairGroup) do
+
+ repairStatic.x = location.x + repairStatic.x
+ repairStatic.y = location.y + repairStatic.y
+
+ repairStatic.hidden = true
+ repairStatic.name = "runway_repairunit_" .. counter
+ counter = counter + 1
+ coalition.addStaticObject(country.id.RUSSIA, repairStatic)
+ table.insert(section.repairGroups, repairStatic.name)
+ end
+end
+
+
+---@private
+---@param section RunwaySection
+function RunwayStrikeMission:RemoveRepairStatics(section)
+ if Spearhead.Util.tableLength(section.repairGroups) == 0 then
+ return
+ end
+
+ for _, repairGroup in pairs(section.repairGroups) do
+ local static = StaticObject.getByName(repairGroup)
+ if static then
+ static:destroy()
+ end
+ end
+
+ section.repairGroups = {}
+end
+
+---@private
+function RunwayStrikeMission:FullRepairRunway()
+
+ for _, section in pairs(self._runwaySections) do
+ section.kilosHit = 0
+ self:RemoveRepairStatics(section)
+ end
+
+ local minX = self._runwayZone.verts[1].x
+ local minY = self._runwayZone.verts[1].y
+ local maxX = self._runwayZone.verts[1].x
+ local maxY = self._runwayZone.verts[1].y
+
+ for _, vert in pairs(self._runwayZone.verts) do
+ if vert.x < minX then
+ minX = vert.x
+ end
+ if vert.x > maxX then
+ maxX = vert.x
+ end
+ if vert.y < minY then
+ minY = vert.y
+ end
+ if vert.y > maxY then
+ maxY = vert.y
+ end
+ end
+
+ ---@type Box
+ local box = {
+ id = world.VolumeType.BOX,
+ params = {
+ min = { x = minX, z = minY, y = 0 },
+ max = { x = maxX, z = maxY, y = 10000 },
+ }
+ }
+ world.removeJunk(box)
+end
+
+---@private
+---@param runway Runway
+---@return Array
+function RunwayStrikeMission:ToSections(runway, numSections)
+
+ local sections = {}
+ local center = runway.position
+ local heading = runway.course
+ local width = runway.width
+ local length = runway.length / numSections
+
+ if heading < 0 then
+ heading = math.abs(heading)
+ else
+ heading = 0 - heading
+ end
+
+ local cosH = math.cos(heading)
+ local sinH = math.sin(heading)
+
+ for i = 0, numSections - 1 do
+ local sectionCenterOffset = (i - (numSections / 2) + 0.5) * length
+
+ ---@type Vec2
+ local sectionCenter = {
+ x = center.x + sectionCenterOffset * cosH,
+ y = center.z + sectionCenterOffset * sinH
+ }
+
+ local halfWidth = width / 2
+ local halfLength = length / 2
+
+ ---@type Array
+ local corners = {
+ {
+ x = sectionCenter.x + (-halfLength * cosH - halfWidth * sinH),
+ y = sectionCenter.y + (-halfLength * sinH + halfWidth * cosH)
+ },
+ {
+ x = sectionCenter.x + (-halfLength * cosH + halfWidth * sinH),
+ y = sectionCenter.y + (-halfLength * sinH - halfWidth * cosH)
+ },
+ {
+ x = sectionCenter.x + (halfLength * cosH + halfWidth * sinH),
+ y = sectionCenter.y + (halfLength * sinH - halfWidth * cosH)
+ },
+ {
+ x = sectionCenter.x + (halfLength * cosH - halfWidth * sinH),
+ y = sectionCenter.y + (halfLength * sinH + halfWidth * cosH)
+ }
+ }
+
+ ---@type RunwaySection
+ local section = {
+ center = sectionCenter,
+ corners = corners,
+ kilosHit = 0,
+ repairGroups = {},
+ }
+
+ table.insert(sections, section)
+ end
+
+ return sections
+end
+
+---@private
+---@param runway Runway
+---@return SpearheadTriggerZone
+function RunwayStrikeMission:RunwayToSpearheadZone(runway)
+
+ -- Calculate the 4 corner points of the runway based on heading, height, and width
+ local radHeading = runway.course
+
+ if radHeading < 0 then
+ radHeading = math.abs(radHeading)
+ else
+ radHeading = 0 - radHeading
+ end
+
+ local cosH = math.cos(radHeading)
+ local sinH = math.sin(radHeading)
+
+ local halfWidth = runway.width / 2
+ local halfHeight = runway.length / 2
+
+ ---@type Array
+ local corners = {
+ {
+ x = runway.position.x + (-halfHeight * cosH - halfWidth * sinH),
+ y = runway.position.z + (-halfHeight * sinH + halfWidth * cosH)
+ },
+ {
+ x = runway.position.x + (-halfHeight * cosH + halfWidth * sinH),
+ y = runway.position.z + (-halfHeight * sinH - halfWidth * cosH)
+ },
+ {
+ x = runway.position.x + (halfHeight * cosH + halfWidth * sinH),
+ y = runway.position.z + (halfHeight * sinH - halfWidth * cosH)
+ },
+ {
+ x = runway.position.x + (halfHeight * cosH - halfWidth * sinH),
+ y = runway.position.z + (halfHeight * sinH + halfWidth * cosH)
+ }
+ }
+
+ return {
+ location = { x = runway.position.x, y = runway.position.z },
+ radius = runway.width,
+ name = runway.Name,
+ verts = corners,
+ zone_type = "Polygon",
+ }
+end
+
+
+
+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.RunwayStrikeMission = RunwayStrikeMission
\ No newline at end of file
diff --git a/classes/stageClasses/Missions/Mission.lua b/classes/stageClasses/missions/ZoneMission.lua
similarity index 62%
rename from classes/stageClasses/Missions/Mission.lua
rename to classes/stageClasses/missions/ZoneMission.lua
index 094f1cb..8b2a9bb 100644
--- a/classes/stageClasses/Missions/Mission.lua
+++ b/classes/stageClasses/missions/ZoneMission.lua
@@ -1,32 +1,55 @@
----@class Mission : OnUnitLostListener
----@field name string
----@field missionType missionType
----@field displayMissionType string
----@field code string
----@field priority MissionPriority
----@field location Vec2?
----@field zoneName string
----@field private _state MissionState
----@field private _database Database
----@field private _logger Logger
----@field private _missionBriefing string?
+--- ZoneMission is missions that are defined by zones in the ME
+---@class ZoneMission : Mission, OnUnitLostListener
---@field private _missionGroups MissionGroups
----@field private _completeListeners Array
----@field private _missionCommandsHelper MissionCommandsHelper
-local Mission = {}
+local ZoneMission = {}
---- @class MissionCompleteListener
---- @field OnMissionComplete fun(self: any, mission:Mission)
-
---- @class MissionGroups
+--- @class MissionGroups
--- @field hasTargets boolean
--- @field groups Array
--- @field unitsAlive table>
--- @field targetsAlive table>
--- @field groupNamesPerunit table
+---@class ParsedMissionName
+---@field missionName string
+---@field type MissionType
+
+---comment
+---@param input string
+---@return ParsedMissionName?
+local function ParseZoneName(input)
+ local split_name = Spearhead.Util.split_string(input, "_")
+ local split_length = Spearhead.Util.tableLength(split_name)
+ if Spearhead.Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
+ Spearhead.AddMissionEditorWarning("Random Mission with zonename " .. input .. " not in right format")
+ return nil
+ elseif split_length < 3 then
+ Spearhead.AddMissionEditorWarning("Mission with zonename" .. input .. " not in right format")
+ return nil
+ end
+
+ ---@type MissionType
+ local parsedType = "nil"
+
+ local inputType = string.lower(split_name[2])
+ if inputType == "dead" then parsedType = "DEAD" end
+ if inputType == "strike" then parsedType = "STRIKE" end
+ if inputType == "bai" then parsedType = "BAI" end
+ if inputType == "sam" then parsedType = "SAM" end
+
+ if parsedType == "nil" then
+ Spearhead.AddMissionEditorWarning("Mission with zonename '" .. input .. "' has an unsupported type '" .. (type or "nil" ))
+ return nil
+ end
+ local name = split_name[3]
+ return {
+ missionName = name,
+ type = parsedType
+ }
+end
+
MINIMAL_UNITS_ALIVE_RATIO = 0.21
---comment
@@ -34,64 +57,32 @@ MINIMAL_UNITS_ALIVE_RATIO = 0.21
---@param priority MissionPriority
---@param database Database
---@param logger Logger
----@return Mission?
-function Mission.New(zoneName, priority, database, logger)
+---@return ZoneMission?
+function ZoneMission.new(zoneName, priority, database, logger)
+ local Mission = Spearhead.classes.stageClasses.missions.baseMissions.Mission
+ ZoneMission.__index = ZoneMission
+ setmetatable(ZoneMission, Mission)
- local function ParseZoneName(input)
- local split_name = Spearhead.Util.split_string(input, "_")
- local split_length = Spearhead.Util.tableLength(split_name)
- if Spearhead.Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
- Spearhead.AddMissionEditorWarning("Random Mission with zonename " .. input .. " not in right format")
- return nil
- elseif split_length < 3 then
- Spearhead.AddMissionEditorWarning("Mission with zonename" .. input .. " not in right format")
- return nil
- end
-
- ---@type missionType
- local parsedType = "nil"
-
- local inputType = string.lower(split_name[2])
- if inputType == "dead" then parsedType = "DEAD" end
- if inputType == "strike" then parsedType = "STRIKE" end
- if inputType == "bai" then parsedType = "BAI" end
- if inputType == "sam" then parsedType = "SAM" end
-
- if parsedType == "nil" then
- Spearhead.AddMissionEditorWarning("Mission with zonename '" .. input .. "' has an unsupported type '" .. (type or "nil" ))
- return nil
- end
- local name = split_name[3]
- return {
- missionName = name,
- type = parsedType
- }
- end
+ local self = setmetatable({}, ZoneMission)
local parsed = ParseZoneName(zoneName)
+ if not parsed then
+ logger:error("Failed to create ZoneMission " .. zoneName .. " => invalid name")
+ return nil
+ end
- if parsed == nil then return end
+ local missionBriefing = database:getMissionBriefingForMissionZone(zoneName) or "no briefing provided"
- Mission.__index = Mission
- local o = {}
- local self = setmetatable(o, Mission)
-
- self.zoneName = zoneName
- self.name = parsed.missionName
- self.missionType = parsed.type
- self.displayMissionType = self.missionType or "unknown"
- if self.missionType == "SAM" then self.displayMissionType = "DEAD" end
- self.location = database:GetLocationForMissionZone(zoneName)
- self.code = tostring(database:GetNewMissionCode())
- self.priority = priority
- self._state = "NEW"
+ local success, error = Mission.newSuper(self, zoneName, parsed.missionName, parsed.type, missionBriefing, priority, database, logger)
+ if not success then
+ logger:error("Failed to create ZoneMission " .. zoneName .. " => " .. error)
+ return nil
+ end
- self._logger = logger
- self._database = database
- self._missionCommandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(logger.LogLevel)
- self._completeListeners = {}
+ if self.missionType == "SAM" then
+ self.missionTypeDisplay = "DEAD"
+ end
- self._missionBriefing = database:getMissionBriefingForMissionZone(zoneName)
self._missionGroups = {
groups = {},
unitsAlive = {},
@@ -103,16 +94,15 @@ function Mission.New(zoneName, priority, database, logger)
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup
local groupNames = database:getGroupsForMissionZone(zoneName)
for _, groupName in pairs(groupNames) do
-
local spearheadGroup = SpearheadGroup.New(groupName)
table.insert(self._missionGroups.groups, spearheadGroup)
- local isGroupTarget =Spearhead.Util.startswith(string.lower(groupName), "tgt_")
- for _, unit in pairs(spearheadGroup:GetUnits())do
+ local isGroupTarget = Spearhead.Util.startswith(string.lower(groupName), "tgt_")
+ for _, unit in pairs(spearheadGroup:GetUnits()) do
local unitName = unit:getName()
local isUnitTarget = Spearhead.Util.startswith(string.lower(unitName), "tgt_")
- if self._missionGroups.unitsAlive[groupName] == nil then
+ if self._missionGroups.unitsAlive[groupName] == nil then
self._missionGroups.unitsAlive[groupName] = {}
end
@@ -135,109 +125,15 @@ function Mission.New(zoneName, priority, database, logger)
Spearhead.DcsUtil.DestroyGroup(groupName)
end
+ self._logger:debug("Mission " .. self.name .. " group count: " .. Spearhead.Util.tableLength(groupNames))
+
return self
end
----comment
----@return MissionState
-function Mission:GetState()
- return self._state
-end
-
-function Mission:SpawnPersistedState()
- for _, group in pairs(self._missionGroups.groups) do
- group:SpawnCorpsesOnly()
- end
-end
-
-function Mission:SpawnActive()
-
- self._logger:info("Activating " .. self.name)
-
- self._state = "ACTIVE"
- for _, group in pairs(self._missionGroups.groups) do
- group:Spawn()
- end
-
- self._missionCommandsHelper:AddMissionToCommands(self)
-
- self:StartCheckingContinuous()
-end
-
----@private
-function Mission:StartCheckingContinuous()
- ---comment
- ---@param mission Mission
- ---@param time any
- ---@return unknown
- local Check = function (mission, time)
- mission:UpdateState(true, true)
-
- if mission:GetState() == "COMPLETED" then
- return nil
- end
- return time + 30
- end
- timer.scheduleFunction(Check, self, timer.getTime() + 30)
-end
-
----@private
----@return string?
-function Mission:ToStateString()
- if self._missionGroups.hasTargets == true then
- local dead = 0
- local total = 0
- if self._missionGroups.targetsAlive then
- for _, group in pairs(self._missionGroups.targetsAlive) do
- for _, isAlive in pairs(group) do
- total = total + 1
- if isAlive == false then
- dead = dead + 1
- end
- end
- end
- end
-
- if total > 0 then
- local completionPercentage = math.floor((dead / total) * 100)
- return "Targets Destroyed: " .. completionPercentage .. "%"
- end
- else
- local dead = 0
- local total = 0
- if self._missionGroups.unitsAlive then
- for _, group in pairs(self._missionGroups.unitsAlive) do
- for _, isAlive in pairs(group) do
- total = total + 1
- if isAlive == false then
- dead = dead + 1
- end
- end
- end
- end
-
- if total > 0 then
- local completionPercentage = math.floor((dead / total) * 100)
- return "Units Destroyed: " .. completionPercentage .. "%"
- end
- end
-end
-
----comment
----@param groupId integer
-function Mission:ShowBriefing(groupId)
-
- local stateString = self:ToStateString()
-
- if self._missionBriefing == nil or self._missionBriefing == "" then self._missionBriefing = "No briefing available" end
- local text = "Mission [" .. self.code .. "] ".. self.name .. "\n \n" .. self._missionBriefing .. " \n \n" .. stateString
- trigger.action.outTextForGroup(groupId, text, 30);
-end
-
-
+---@internal
---@param checkHealth boolean
---@param messageIfDone boolean
-function Mission:UpdateState(checkHealth, messageIfDone)
+function ZoneMission:UpdateState(checkHealth, messageIfDone)
if checkHealth == nil then checkHealth = false end
if messageIfDone == false then messageIfDone = true end
@@ -294,7 +190,6 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end
if self._missionGroups.hasTargets == true then
-
local anyTargetAlive = function()
for _, units in pairs(self._missionGroups.targetsAlive) do
for _, isAlive in pairs(units) do
@@ -305,7 +200,7 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end
return false
end
-
+
if anyTargetAlive() ~= true then
self._state = "COMPLETED"
end
@@ -337,44 +232,101 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end
end
+end
+
+function ZoneMission:SpawnPersistedState()
+ for _, group in pairs(self._missionGroups.groups) do
+ group:SpawnCorpsesOnly()
+ end
+end
+
+---spawns the mission, but doesn't add
+function ZoneMission:SpawnInactive()
+ self._logger:info("PreActivating " .. self.name)
+
+ self._state = "ACTIVE"
+ for _, group in pairs(self._missionGroups.groups) do
+ group:Spawn()
+ end
+end
+
+function ZoneMission:SpawnActive()
+
+ self._logger:info("Activating " .. self.name)
+
+ self._state = "ACTIVE"
+ for _, group in pairs(self._missionGroups.groups) do
+ group:Spawn()
+ end
+
+ self._missionCommandsHelper:AddMissionToCommands(self)
+
+ self:StartCheckingContinuous()
+end
+
+
+---@private
+function ZoneMission:StartCheckingContinuous()
---comment
---@param mission Mission
- local NotifyMissionComplete = function(mission)
- mission:NotifyMissionComplete()
- return nil
+ ---@param time any
+ ---@return unknown
+ local Check = function(mission, time)
+ mission:UpdateState(true, true)
+
+ if mission:getState() == "COMPLETED" then
+ return nil
+ end
+ return time + 30
end
- if self._state == "COMPLETED" then
- timer.scheduleFunction(NotifyMissionComplete, self, timer.getTime() + 3)
+ timer.scheduleFunction(Check, self, timer.getTime() + 30)
+end
+
+---@protected
+function ZoneMission:ToStateString()
+ if self._missionGroups.hasTargets == true then
+ local dead = 0
+ local total = 0
+ if self._missionGroups.targetsAlive then
+ for _, group in pairs(self._missionGroups.targetsAlive) do
+ for _, isAlive in pairs(group) do
+ total = total + 1
+ if isAlive == false then
+ dead = dead + 1
+ end
+ end
+ end
+ end
+
+ if total > 0 then
+ local completionPercentage = math.floor((dead / total) * 100)
+ return "Targets Destroyed: " .. completionPercentage .. "%"
+ end
+ else
+ local dead = 0
+ local total = 0
+ if self._missionGroups.unitsAlive then
+ for _, group in pairs(self._missionGroups.unitsAlive) do
+ for _, isAlive in pairs(group) do
+ total = total + 1
+ if isAlive == false then
+ dead = dead + 1
+ end
+ end
+ end
+ end
+
+ if total > 0 then
+ local completionPercentage = math.floor((dead / total) * 100)
+ return "Units Destroyed: " .. completionPercentage .. "%"
+ end
end
end
----private usage advised
-function Mission:NotifyMissionComplete()
-
- self._missionCommandsHelper:RemoveMissionToCommands(self)
- self._logger:info("Mission Completed: " .. self.zoneName)
- trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed succesfully" , 20)
-
- for _, listener in pairs(self._completeListeners) do
- pcall(function()
- listener:OnMissionComplete(self)
- end)
- end
-end
-
-
----@param listener MissionCompleteListener Object that implements "OnMissionComplete(self, mission)"
-function Mission:AddMissionCompleteListener(listener)
- if type(listener) ~= "table" then
- return
- end
- table.insert(self._completeListeners, listener)
-end
-
-function Mission:OnUnitLost(object)
+function ZoneMission:OnUnitLost(object)
--[[
OnUnit lost event
- ]]--
+ ]] --
self._logger:debug("Getting on unit lost event")
local category = Object.getCategory(object)
@@ -388,7 +340,7 @@ function Mission:OnUnitLost(object)
if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then
self._missionGroups.targetsAlive[groupName][unitName] = false
end
- elseif category == Object.Category.STATIC then
+ elseif category == Object.Category.STATIC then
local name = object:getName()
self._missionGroups.unitsAlive[name][name] = false
@@ -401,9 +353,9 @@ function Mission:OnUnitLost(object)
self:UpdateState(false, true)
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.Mission = Mission
-
-
+if not Spearhead.classes.stageClasses.missions then Spearhead.classes.stageClasses.missions = {} end
+Spearhead.classes.stageClasses.missions.ZoneMission = ZoneMission
\ No newline at end of file
diff --git a/classes/stageClasses/missions/baseMissions/Mission.lua b/classes/stageClasses/missions/baseMissions/Mission.lua
new file mode 100644
index 0000000..33450e8
--- /dev/null
+++ b/classes/stageClasses/missions/baseMissions/Mission.lua
@@ -0,0 +1,147 @@
+
+
+---@class Mission
+---@field name string
+---@field zoneName string
+---@field missionType MissionType
+---@field missionTypeDisplay string
+---@field priority MissionPriority
+---@field missionBriefing string
+---@field location Vec2?
+---@field code string
+---@field protected state MissionState
+---@field getState fun(self: Mission): MissionState @Get the mission state
+---@field protected _logger Logger
+---@field protected _database Database
+---@field protected _missionCommandsHelper MissionCommandsHelper
+---@field protected _completeListeners Array
+local Mission = {}
+Mission.__index = Mission
+
+--- @class MissionCompleteListener
+--- @field OnMissionComplete fun(self: any, mission:Mission)
+
+---@protected
+---@param self Mission
+---@param zoneName string
+---@param missionName string
+---@param missionType MissionType
+---@param missionBriefing string
+---@param priority MissionPriority
+---@param database Database
+---@param logger Logger
+---@return boolean, string
+function Mission.newSuper(self, zoneName, missionName, missionType, missionBriefing, priority, database, logger)
+
+ self.zoneName = zoneName
+ self.name = missionName
+ self.missionType = missionType
+ self.priority = priority
+ self.state = "NEW"
+ self._logger = logger
+ self._database = database
+ self.missionBriefing = missionBriefing
+ self.code = tostring(database:GetNewMissionCode())
+
+ self._completeListeners = {}
+
+ self.location = database:GetLocationForMissionZone(zoneName)
+ self.missionTypeDisplay = self.missionType
+
+ self._missionCommandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(logger.LogLevel)
+
+ return true, "success"
+end
+
+---@return MissionState
+function Mission:getState()
+ return self.state
+end
+
+--region PUBLIC
+
+function Mission:SpawnPersistedState() end
+function Mission:SpawnActive() end
+
+---comment
+---@param checkHealth boolean
+---@param messageIfDone boolean
+function Mission:UpdateState(checkHealth, messageIfDone) end
+function Mission:StartCheckingContinuous() end
+
+
+---comment
+---@param groupId number
+function Mission:ShowBriefing(groupId)
+ local stateString = self:ToStateString()
+ if self._missionBriefing == nil or self._missionBriefing == "" then self._missionBriefing = "No briefing available" end
+ local text = "Mission [" ..
+ self.code .. "] " .. self.name .. "\n \n" .. self._missionBriefing .. " \n \n" .. stateString
+ trigger.action.outTextForGroup(groupId, text, 30);
+end
+
+
+---@param listener MissionCompleteListener Object that implements "OnMissionComplete(self, mission)"
+function Mission:AddMissionCompleteListener(listener)
+ if type(listener) ~= "table" then
+ return
+ end
+ table.insert(self._completeListeners, listener)
+end
+
+function Mission:NotifyMissionComplete()
+ self._missionCommandsHelper:RemoveMissionToCommands(self)
+ self._logger:info("Mission Completed: " .. self.zoneName)
+ trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed succesfully", 20)
+
+ for _, listener in pairs(self._completeListeners) do
+ pcall(function()
+ listener:OnMissionComplete(self)
+ end)
+ end
+end
+
+---endregion
+
+--region PROTECTED
+
+---@protected
+function Mission:ToStateString() return "status: in progress" end
+
+
+--endregion
+
+
+
+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
+if not Spearhead.classes.stageClasses.missions.baseMissions then Spearhead.classes.stageClasses.missions.baseMissions = {} end
+Spearhead.classes.stageClasses.missions.baseMissions.Mission = Mission
+
+
+
+
+do --aliases
+
+ --- @alias MissionPriority
+ --- | "none"
+ --- | "primary"
+ --- | "secondary"
+
+ --- @alias MissionType
+ --- | "nil"
+ --- | "STRIKE"
+ --- | "BAI"
+ --- | "DEAD"
+ --- | "SAM"
+ --- | "OCA"
+
+ --- @alias MissionState
+ --- | "NEW"
+ --- | "ACTIVE"
+ --- | "COMPLETED"
+
+end
+
+
diff --git a/config.lua b/config.lua
index e2f23f1..9475bd9 100644
--- a/config.lua
+++ b/config.lua
@@ -40,6 +40,7 @@ SpearheadConfig = {
--Will draw the active and the next stage
drawStages = true, -- default true
+ drawPreActivated = true, -- default true
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
@@ -54,6 +55,10 @@ SpearheadConfig = {
--Stage starting number
startingStage = 1,
+ -- Amount of stages that are pre-activated on top of the current active stage.
+ -- In Pre-activated the missions are not listed, but SAMs and Airbase groups are spawned.
+ preactivateStage = 1, -- default 1
+
---DEBUG logging. Consider keeping this disabled
debugEnabled = false
},
diff --git a/dev/dev_classes.lua b/dev/dev_classes.lua
index e43753a..c432cd4 100644
--- a/dev/dev_classes.lua
+++ b/dev/dev_classes.lua
@@ -6,6 +6,8 @@
local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\"
+assert(loadfile(classPath .. "_baseClasses\\Queue.lua"))()
+
assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.lua"))()
@@ -18,7 +20,10 @@ assert(loadfile(classPath .. "configuration\\CapConfig.lua"))()
assert(loadfile(classPath .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
-assert(loadfile(classPath .. "stageClasses\\Missions\\Mission.lua"))()
+assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))()
+assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))()
+assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))()
+
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.lua"))()
@@ -36,6 +41,7 @@ assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "capClasses\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
+assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
diff --git a/dev/dev_config.lua b/dev/dev_config.lua
index 7dbf2ec..a0bd89e 100644
--- a/dev/dev_config.lua
+++ b/dev/dev_config.lua
@@ -52,7 +52,7 @@ SpearheadConfig = {
maxMissionStage = 10,
--Stage starting number
- startingStage = 1,
+ startingStage = 0,
---DEBUG logging. Consider keeping this disabled
debugEnabled = true
@@ -61,7 +61,7 @@ SpearheadConfig = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
- enabled = true,
+ enabled = false,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
diff --git a/main.lua b/main.lua
index 08861fa..3a383c5 100644
--- a/main.lua
+++ b/main.lua
@@ -36,8 +36,8 @@ else
standardLogger:info("Persistence disabled")
end
-Spearhead.internal.GlobalCapManager.start(databaseManager, capConfig, stageConfig)
-Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig)
+Spearhead.classes.capClasses.GlobalCapManager.start(databaseManager, capConfig, stageConfig, defaultLogLevel)
+Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig, defaultLogLevel)
Spearhead.internal.GlobalFleetManager.start(databaseManager)
local SetStageDelayed = function(number, time)