Feature/mission refactor (#26)

* Refactored Mission classes to add Missions other than trigger zone missions. (eg. Runway Strikes, Resupply etc.)
* Runway strike initial POC
Co-authored-by: dutchie032 <dutchie032>
This commit is contained in:
2025-05-07 13:18:45 +02:00
committed by GitHub
parent 31f99686f6
commit a2f62d797b
30 changed files with 1580 additions and 547 deletions
Vendored
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
**/.DS_Store
+10 -1
View File
@@ -6,5 +6,14 @@
], ],
"Lua.diagnostics.globals": [ "Lua.diagnostics.globals": [
"lfs" "lfs"
] ],
"maptz.regionfolder": {
"[lua]" : {
"foldEnd": "--endregion",
"foldEndRegex": "[\\s]*--endregion",
"foldStart": "--region [NAME]",
"foldStartRegex": "[\\s]*--region[\\s]*(.*)",
"disableFolding": false
}
}
} }
+16 -1
View File
@@ -292,10 +292,25 @@
Missions can be randomized using the <span class="inline-lua"><span class="lua-variable">RANDOMMISSION</span></span> prefix. <br /> Missions can be randomized using the <span class="inline-lua"><span class="lua-variable">RANDOMMISSION</span></span> prefix. <br />
Spearhead will pick one random mission from zones with the same name. Spearhead will pick one random mission from zones with the same name.
</p> </p>
<h2>Runway Bombing</h2>
<p>
Runway bombing can be a very effective OCA tactic. With Spearhead we've tried adding as much logic and detail to it so it will feel as engaging as possible. <br/>
Firstly, the runway is split up in 5 sections and only sections 2, 3 and 4 are then actually tracked. <br/>
<span style="font-family: Consolas, Monaco, 'Lucida Console', monospace;">
+-----------+-----------+-----------+-----------+-----------+ <br/>
| Section 1 | Section 2 | Section 3 | Section 4 | Section 5 | <br/>
+-----------+-----------+-----------+-----------+-----------+ <br/>
</span>
</p>
</div> </div>
</div> </div>
<div style="clear: both;"></div>
</main> </main>
<footer> <footer>
<p>&copy; 2025 Spearhead Project</p> <p>&copy; 2025 Spearhead Project</p>
Binary file not shown.
+50
View File
@@ -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<any>
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
+48 -15
View File
@@ -10,6 +10,8 @@
---@field groupsByName table<string, CapGroup> ---@field groupsByName table<string, CapGroup>
---@field PrimaryGroups Array<CapGroup> ---@field PrimaryGroups Array<CapGroup>
---@field BackupGroups Array<CapGroup> ---@field BackupGroups Array<CapGroup>
---@field private runwayBombingTracker RunwayBombingTracker
---@field private runwayStrikeMissions table<string, RunwayStrikeMission>
local CapBase = {} local CapBase = {}
@@ -23,14 +25,17 @@ end
---@param logger table ---@param logger table
---@param capConfig table ---@param capConfig table
---@param stageConfig table ---@param stageConfig table
---@param runwayBombingTracker RunwayBombingTracker
---@return CapBase ---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig) function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker)
CapBase.__index = CapBase CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]] local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
self.groupNames = database:getCapGroupsAtAirbase(airbaseName) self.groupNames = database:getCapGroupsAtAirbase(airbaseName)
self.database = database self.database = database
self.runwayBombingTracker = runwayBombingTracker
self.runwayStrikeMissions = {}
self.airbaseName = airbaseName self.airbaseName = airbaseName
self.logger = logger self.logger = logger
@@ -44,7 +49,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig)
self.BackupGroups = {} self.BackupGroups = {}
for key, name in pairs(self.groupNames) do 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 if capGroup then
self.groupsByName[name] = capGroup self.groupsByName[name] = capGroup
@@ -57,21 +62,40 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig)
capGroup:AddOnStateUpdatedListener(self) capGroup:AddOnStateUpdatedListener(self)
end end
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) Spearhead.Events.AddStageNumberChangedListener(self)
return self return self
end 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) 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. 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 if capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT
or capGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION
or capGroup.state == Spearhead.internal.CapGroup.GroupState.REARMING or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.REARMING
then then
return return
end end
@@ -84,7 +108,7 @@ function CapBase:SpawnIfApplicable()
local targetStage = capGroup:GetTargetZone(self.activeStage) 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() capGroup:SpawnOnTheRamp()
end end
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 --Count back up groups that are active or reassign to the new zone if that's needed
for _, backupGroup in pairs(self.BackupGroups) do 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) local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
if supposedTargetStage then if supposedTargetStage then
if supposedTargetStage ~= backupGroup.assignedStageNumber then if supposedTargetStage ~= backupGroup.assignedStageNumber then
@@ -113,7 +137,7 @@ function CapBase:CheckAndScheduleCAP()
else else
backupGroup:SendRTBAndDespawn() backupGroup:SendRTBAndDespawn()
end 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() backupGroup:SendRTB()
end end
end end
@@ -133,12 +157,12 @@ function CapBase:CheckAndScheduleCAP()
requiredPerStage[supposedTargetStage] = requiredPerStage[supposedTargetStage] + 1 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 if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
primaryGroup:SendToStage(supposedTargetStage) primaryGroup:SendToStage(supposedTargetStage)
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
end 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 supposedTargetStage ~= primaryGroup.assignedStageNumber then
if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
primaryGroup:SendToStage(supposedTargetStage) primaryGroup:SendToStage(supposedTargetStage)
@@ -148,7 +172,7 @@ function CapBase:CheckAndScheduleCAP()
end end
end end
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 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() primaryGroup:SendRTB()
end end
else else
@@ -157,7 +181,7 @@ function CapBase:CheckAndScheduleCAP()
end end
for _, backupGroup in pairs(self.BackupGroups) do 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) local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
if supposedTargetStage then if supposedTargetStage then
if countPerStage[supposedTargetStage] == nil then if countPerStage[supposedTargetStage] == nil then
@@ -177,6 +201,13 @@ function CapBase:CheckAndScheduleCAP()
function CapBase:OnStageNumberChanged(number) function CapBase:OnStageNumberChanged(number)
self.activeStage = number self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then
for _, mission in pairs(self.runwayStrikeMissions) do
mission:SpawnActive()
end
end
self:SpawnIfApplicable() self:SpawnIfApplicable()
timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5) timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5)
end end
@@ -195,5 +226,7 @@ function CapBase:IsBaseActiveWhenStageIsActive(stageNumber)
end end
if not Spearhead.internal then Spearhead.internal = {} end if not Spearhead then Spearhead = {} end
Spearhead.internal.CapAirbase = CapBase if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
Spearhead.classes.capClasses.CapAirbase = CapBase
+9 -2
View File
@@ -261,6 +261,11 @@ function CapGroup:SendToStage(stageZoneNumber)
local controller = group:getController() local controller = group:getController()
local capPoints = self.database:getCapRouteInZone(stageZoneNumber, self.airbaseName) 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 altitude = math.random(self.capConfig:getMinAlt(), self.capConfig:getMaxAlt())
local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed()) local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed())
local attackHelos = false local attackHelos = false
@@ -418,5 +423,7 @@ function CapGroup:OnUnitLost(initiatorUnit)
end end
if not Spearhead.internal then Spearhead.internal = {} end if not Spearhead then Spearhead = {} end
Spearhead.internal.CapGroup = CapGroup if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
Spearhead.classes.capClasses.CapGroup = CapGroup
+15 -8
View File
@@ -9,12 +9,15 @@ do
---comment ---comment
---@param database Database ---@param database Database
---@param capConfig any ---@param capConfig table
---@param stageConfig any ---@param stageConfig StageConfig
function GlobalCapManager.start(database, capConfig, stageConfig) ---@param logLevel LogLevel
function GlobalCapManager.start(database, capConfig, stageConfig, logLevel)
if initiated == true then return end 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() local zones = database:getStagezoneNames()
if zones then if zones then
@@ -27,8 +30,8 @@ do
if airbaseNames then if airbaseNames then
for _, airbaseName in pairs(airbaseNames) do for _, airbaseName in pairs(airbaseNames) do
if airbaseName then if airbaseName then
local airbaseSpecificLogger = Spearhead.LoggerTemplate.new("CAP_" .. airbaseName, capConfig.logLevel) local airbaseSpecificLogger = Spearhead.LoggerTemplate.new("CAP_" .. airbaseName, logLevel)
local airbase = Spearhead.internal.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig) local airbase = Spearhead.classes.capClasses.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker)
if airbase then if airbase then
table.insert(airbasesPerStage[stageName], airbase) table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase allAirbasesByName[airbaseName] = airbase
@@ -61,5 +64,9 @@ do
end end
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
@@ -0,0 +1,129 @@
---@class RunwayBombingTracker : OnWeaponFiredListener
---@field trackedRunways table<Runway, RunwayStrikeMission>
---@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
+5 -7
View File
@@ -2,10 +2,11 @@
--- @class StageConfig --- @class StageConfig
--- @field isEnabled boolean --- @field isEnabled boolean
--- @field isDrawStagesEnabled boolean --- @field isDrawStagesEnabled boolean
--- @field isDrawPreActivatedEnabled boolean
--- @field isAutoStages boolean --- @field isAutoStages boolean
--- @field startingStage integer --- @field startingStage integer
--- @field maxMissionsPerStage integer --- @field maxMissionsPerStage integer
--- @field logLevel LogLevel --- @field AmountPreactivateStage integer
local StageConfig = {}; local StageConfig = {};
@@ -24,13 +25,10 @@ function StageConfig:new()
isAutoStages = SpearheadConfig.StageConfig.autoStages or true, isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1, startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10, 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 }) setmetatable(o, { __index = self })
return o; return o;
-19
View File
@@ -3,23 +3,6 @@
do -- mission aliases do -- mission aliases
--- @alias MissionPriority
--- | "none"
--- | "primary"
--- | "secondary"
--- @alias missionType
--- | "nil"
--- | "STRIKE"
--- | "BAI"
--- | "DEAD"
--- | "SAM"
--- @alias MissionState
--- | "NEW"
--- | "ACTIVE"
--- | "COMPLETED"
--- @alias LogLevel --- @alias LogLevel
--- | "DEBUG" --- | "DEBUG"
--- | "INFO" --- | "INFO"
@@ -27,8 +10,6 @@ do -- mission aliases
--- | "ERROR" --- | "ERROR"
--- | "NONE" --- | "NONE"
---@class Array<T>: { [integer]: T } ---@class Array<T>: { [integer]: T }
--- @class Position --- @class Position
+11 -6
View File
@@ -1,5 +1,10 @@
local FleetGroup = {} local FleetGroup = {}
---comment
---@param fleetGroupName string
---@param database Database
---@param logger Logger
---@return nil
function FleetGroup:new(fleetGroupName, database, logger) function FleetGroup:new(fleetGroupName, database, logger)
local o = {} local o = {}
@@ -30,10 +35,10 @@ function FleetGroup:new(fleetGroupName, database, logger)
Spearhead.AddMissionEditorWarning( Spearhead.AddMissionEditorWarning(
"CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName) "CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName)
else else
---@param zone SpearheadTriggerZone
---@return Vec2, Vec2
local function GetTwoFurthestPoints(zone) 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 biggest = nil
local biggestA = zone.verts[1] local biggestA = zone.verts[1]
@@ -43,7 +48,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
for ii = i + 1, 4 do for ii = i + 1, 4 do
local a = zone.verts[i] local a = zone.verts[i]
local b = zone.verts[ii] local b = zone.verts[ii]
local dist = getDist(a, b) local dist = Spearhead.Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then if biggest == nil or dist > biggest then
biggestA = a biggestA = a
@@ -52,7 +57,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
end end
end 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 end
local function getMinMaxStage(namePart) local function getMinMaxStage(namePart)
@@ -90,7 +95,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
for i = first, second do for i = first, second do
o.targetZonePerStage[tostring(i)] = zoneName o.targetZonePerStage[tostring(i)] = zoneName
end 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 else
Spearhead.AddMissionEditorWarning("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName) Spearhead.AddMissionEditorWarning("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName)
end end
+242 -183
View File
@@ -82,7 +82,6 @@ do -- INIT UTIL
---@param ignoreCase boolean? ---@param ignoreCase boolean?
---@return boolean ---@return boolean
UTIL.startswith = function(str, findable, ignoreCase) UTIL.startswith = function(str, findable, ignoreCase)
if ignoreCase == true then if ignoreCase == true then
return string.lower(str):find('^' .. string.lower(findable)) ~= nil return string.lower(str):find('^' .. string.lower(findable)) ~= nil
end end
@@ -122,8 +121,8 @@ do -- INIT UTIL
end end
---comment ---comment
---@param a Vec2 DCS Point vector {x, z , y} ---@param a Vec2
---@param b Vec2 DCS Point vector {x, z , y} ---@param b Vec2
---@return number ---@return number
function UTIL.VectorDistance2d(a, b) function UTIL.VectorDistance2d(a, b)
return math.sqrt((b.x - a.x) ^ 2 + (b.y - a.y) ^ 2) 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 return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5
end end
local function isInComplexPolygon(polygon, x, y)
---comment local function getEdges(poly)
---@param polygon table of pairs { x, z } local result = {}
---@param x number X location for i = 1, #poly do
---@param z number Y location local point1 = poly[i]
---@return boolean local point2Index = i + 1
function UTIL.IsPointInPolygon(polygon, x, z) if point2Index > #poly then point2Index = 1 end
local function isInComplexPolygon(polygon, x, z) local point2 = poly[point2Index]
local function getEdges(poly) local edge = { x1 = point1.x, z1 = point1.y, x2 = point2.x, z2 = point2.y }
local result = {} table.insert(result, edge)
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
end end
return result
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
end 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 end
---comment ---comment
---@param points table points { x, z } ---@param polygon Array<Vec2> of pairs { x, y }
---@return table hullPoints { x, z } ---@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<Vec2> points
---@return Array<Vec2> hullPoints
function UTIL.getConvexHull(points) function UTIL.getConvexHull(points)
if #points == 0 then if #points == 0 then
return {} return {}
end end
local function ccw(a,b,c) ---comment
return (b.z - a.z) * (c.x - a.x) > (b.x - a.x) * (c.z - a.z) ---@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 end
table.sort(points, function(left,right) table.sort(points, function(left, right)
return left.z < right.z return left.y < right.y
end) end)
local hull = {} local hull = {}
-- lower hull -- lower hull
for _,point in pairs(points) do for _, point in pairs(points) do
while #hull >= 2 and not ccw(hull[#hull-1], hull[#hull], point) do while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull,#hull) table.remove(hull, #hull)
end end
table.insert(hull,point) table.insert(hull, point)
end end
-- upper hull -- upper hull
local t = #hull + 1 local t = #hull + 1
for i=#points, 1, -1 do for i = #points, 1, -1 do
local point = points[i] local point = points[i]
while #hull >= t and not ccw(hull[#hull-1], hull[#hull], point) do while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull,#hull) table.remove(hull, #hull)
end end
table.insert(hull,point) table.insert(hull, point)
end end
table.remove(hull,#hull) table.remove(hull, #hull)
return hull return hull
end end
---@param points Array<Vec2>
function UTIL.enlargeConvexHull(points, meters) function UTIL.enlargeConvexHull(points, meters)
---@type Array<Vec2>
local allpoints = {}
local allpoints = {}
for _, point in pairs(points) do for _, point in pairs(points) do
table.insert(allpoints, point) 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 }) allpoints[#allpoints + 1] = point
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] = { x = point.x + meters, y = point.y, }
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] = { x = point.x - meters, y = point.y, }
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] = { 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 end
return UTIL.getConvexHull(allpoints) return UTIL.getConvexHull(allpoints)
@@ -262,12 +284,25 @@ do -- INIT DCS_UTIL
zone_type, zone_type,
x, x,
z, y,
radius radius
verts, verts,
} }
]] -- ]] --
---@alias SpearheadTriggerZoneType
---| "Cilinder"
---| "Polygon"
---@class SpearheadTriggerZone
---@field name string
---@field location Vec2
---@field radius number
---@field verts Array<Vec2>
---@field zone_type SpearheadTriggerZoneType
---@type Array<SpearheadTriggerZone>
DCS_UTIL.__trigger_zones = {} DCS_UTIL.__trigger_zones = {}
end end
@@ -293,18 +328,9 @@ do -- INIT DCS_UTIL
} }
DCS_UTIL.__airbaseNamesById = {} DCS_UTIL.__airbaseNamesById = {}
--[[
zone = {
name,
zone_type,
x,
z,
radius
verts,
} ---@type table<string, SpearheadTriggerZone>
]] -- DCS_UTIL.__airbaseZonesByName = {}
DCS_UTIL.__airbaseZonesById = {}
DCS_UTIL.__airportsStartingCoalition = {} DCS_UTIL.__airportsStartingCoalition = {}
DCS_UTIL.__warehouseStartingCoalition = {} DCS_UTIL.__warehouseStartingCoalition = {}
@@ -362,21 +388,25 @@ do -- INIT DCS_UTIL
do --init trigger zones do --init trigger zones
for i, trigger_zone in pairs(env.mission.triggers.zones) do for i, trigger_zone in pairs(env.mission.triggers.zones) do
-- reorder verts as they are not ordered correctly in the ME -- reorder verts as they are not ordered correctly in the ME
verts = {} local verts = {}
if Spearhead.Util.tableLength(trigger_zone.verticies) >=4 then 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[4].x, y = 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[3].x, y = 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[2].x, y = trigger_zone.verticies[2].y })
table.insert(verts, { x = trigger_zone.verticies[1].x , z = trigger_zone.verticies[1].y }) table.insert(verts, { x = trigger_zone.verticies[1].x, y = trigger_zone.verticies[1].y })
end end
local zoneType = "Cilinder"
if trigger_zone.type == DCS_UTIL.ZoneType.Polygon then
zoneType = "Polygon"
end
---@type SpearheadTriggerZone
local zone = { local zone = {
name = trigger_zone.name, name = trigger_zone.name,
zone_type = trigger_zone.type, zone_type = zoneType,
x = trigger_zone.x, location = { x = trigger_zone.x, y = trigger_zone.y },
z = trigger_zone.y,
radius = trigger_zone.radius, radius = trigger_zone.radius,
verts = verts verts = verts
} }
@@ -408,35 +438,36 @@ do -- INIT DCS_UTIL
end end
end end
do -- fill airbaseNames and zones do -- fill airbaseNames and zones
local airbases = world.getAirbases() local airbases = world.getAirbases()
if airbases then if airbases then
for _, airbase in pairs(airbases) do for _, airbase in pairs(airbases) do
local name = airbase:getName() local name = airbase:getName()
local id = tostring(airbase:getID())
if name and id then DCS_UTIL.__airbaseNamesById[tostring(airbase:getID())] = name
DCS_UTIL.__airbaseNamesById[id] = name
if name then
local relevantPoints = {} local relevantPoints = {}
for _, x in pairs(airbase:getRunways()) do for _, x in pairs(airbase:getRunways()) do
if x.position and x.position.x and x.position.z then 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
end end
for _, x in pairs(airbase:getParking()) do for _, x in pairs(airbase:getParking()) do
if x.vTerminalPos and x.vTerminalPos.x and x.vTerminalPos.z then 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
end end
local points = UTIL.getConvexHull(relevantPoints) local points = UTIL.getConvexHull(relevantPoints)
local enlargedPoints = UTIL.enlargeConvexHull(points, 750) local enlargedPoints = UTIL.enlargeConvexHull(points, 750)
DCS_UTIL.__airbaseZonesById[id] = { DCS_UTIL.__airbaseZonesByName[name] = {
name = 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 verts = enlargedPoints
} }
end end
@@ -484,7 +515,7 @@ do -- INIT DCS_UTIL
end end
---destroy the given group ---destroy the given group
---@param groupName string ---@param groupName string
function DCS_UTIL.DestroyGroup(groupName) function DCS_UTIL.DestroyGroup(groupName)
if DCS_UTIL.IsGroupStatic(groupName) then if DCS_UTIL.IsGroupStatic(groupName) then
local object = StaticObject.getByName(groupName) local object = StaticObject.getByName(groupName)
@@ -514,13 +545,14 @@ do -- INIT DCS_UTIL
end end
end end
--- takes a list of units and returns all the units that are in any of the zones --- takes a list of units and returns all the units that are in any of the zones
---@param unit_names table unit names ---@param unit_names table unit names
---@param zone_names table zone names ---@param zone_names table zone names
---@return table unit list of objects { unit = UNIT, zone_name = zoneName} ---@return table unit list of objects { unit = UNIT, zone_name = zoneName}
function DCS_UTIL.getUnitsInZones(unit_names, zone_names) function DCS_UTIL.getUnitsInZones(unit_names, zone_names)
local units = {} local units = {}
---@type Array<SpearheadTriggerZone>
local zones = {} local zones = {}
for k = 1, #unit_names do for k = 1, #unit_names do
@@ -544,14 +576,9 @@ do -- INIT DCS_UTIL
local lCat = Object.getCategory(lUnit) local lCat = Object.getCategory(lUnit)
for zone_name, zone in pairs(zones) do 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 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 local isInZone = UTIL.is3dPointInZone(unit_pos, zone)
if UTIL.IsPointInPolygon(zone.verts, unit_pos.x, unit_pos.z) == true then if isInZone == true then
in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name } 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
end end
end end
end end
@@ -569,22 +596,13 @@ do -- INIT DCS_UTIL
return {} return {}
end end
-- MAP Just for mapping sake return DCS_UTIL.areGroupsInCustomZone(group_names, zone)
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)
end end
--- takes a x, y poistion and checks if it is inside any of the zones --- takes a x, y poistion and checks if it is inside any of the zones
---@param group_names table North South position ---@param group_names Array<string> North South position
---@param zone table { x, z, zonetype, radius, verts } ---@param zone SpearheadTriggerZone
---@return table groupnames list of groups that are in the zone ---@return Array<string> groupnames list of groups that are in the zone
function DCS_UTIL.areGroupsInCustomZone(group_names, zone) function DCS_UTIL.areGroupsInCustomZone(group_names, zone)
local units = {} local units = {}
if Spearhead.Util.tableLength(group_names) < 1 then return {} end if Spearhead.Util.tableLength(group_names) < 1 then return {} end
@@ -606,14 +624,9 @@ do -- INIT DCS_UTIL
local result_groups = {} local result_groups = {}
for _, entry in pairs(units) do for _, entry in pairs(units) do
local pos = entry.unit:getPoint() local pos = entry.unit:getPoint()
if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then local isInZone = UTIL.is3dPointInZone(pos, zone)
if UTIL.IsPointInPolygon(zone.verts, pos.x, pos.z) == true then if isInZone == true then
table.insert(result_groups, entry.groupname) 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
end end
end end
return result_groups return result_groups
@@ -625,6 +638,7 @@ do -- INIT DCS_UTIL
---@param zone_names table zone names ---@param zone_names table zone names
---@return table zones list of objects { zone_name = zoneName} ---@return table zones list of objects { zone_name = zoneName}
function DCS_UTIL.isPositionInZones(x, z, zone_names) function DCS_UTIL.isPositionInZones(x, z, zone_names)
---@type Array<SpearheadTriggerZone>
local zones = {} local zones = {}
for index, zone_name in pairs(zone_names) do for index, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name] local zone = DCS_UTIL.__trigger_zones[zone_name]
@@ -635,14 +649,8 @@ do -- INIT DCS_UTIL
local result_zones = {} local result_zones = {}
for zone_name, zone in pairs(zones) do for zone_name, zone in pairs(zones) do
if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then if UTIL.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then
if UTIL.IsPointInPolygon(zone.verts, x, z) == true then result_zones[#result_zones + 1] = zone.name
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
end end
end end
return result_zones return result_zones
@@ -655,14 +663,8 @@ do -- INIT DCS_UTIL
---@return boolean result ---@return boolean result
function DCS_UTIL.isPositionInZone(x, z, zone_name) function DCS_UTIL.isPositionInZone(x, z, zone_name)
local zone = DCS_UTIL.__trigger_zones[zone_name] local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then if UTIL.is3dPointInZone({ x = x, y = 0, z = z }, zone) then
if UTIL.IsPointInPolygon(zone.verts, x, z) == true then return true
return true
end
else
if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
return true
end
end end
return false return false
end end
@@ -672,58 +674,29 @@ do -- INIT DCS_UTIL
---@param parent_zone_name string ---@param parent_zone_name string
---@return boolean result ---@return boolean result
function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name) function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name)
local zoneA = DCS_UTIL.__trigger_zones[zone_name] local zoneA = DCS_UTIL.__trigger_zones[zone_name] --[[@as SpearheadTriggerZone]]
local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name] if zoneA == nil then return false end
local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name] --[[@as SpearheadTriggerZone]]
if zoneB.zone_type == DCS_UTIL.ZoneType.Polygon and zoneB.verts then if zoneB == nil then return false end
if UTIL.IsPointInPolygon(zoneB.verts, zoneA.x, zoneA.z) == true then return UTIL.is3dPointInZone({ x = zoneA.location.x, y = 0, z = zoneA.location.y }, zoneB)
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
end end
---comment ---comment
---@param zone_name any ---@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) function DCS_UTIL.getZoneByName(zone_name)
if zone_name == nil then return nil end if zone_name == nil then return nil end
return DCS_UTIL.__trigger_zones[zone_name] return DCS_UTIL.__trigger_zones[zone_name]
end end
---comment ---comment
---@param airbaseId any ---@param airbaseName string
---@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.getAirbaseZoneById(airbaseId) function DCS_UTIL.getAirbaseZoneByName(airbaseName)
local string = tostring(airbaseId)
if string == nil then return nil end if string == nil then return nil end
return DCS_UTIL.__airbaseZonesById[string] return DCS_UTIL.__airbaseZonesByName[string]
end end
---maps the category name to the DCS group category ---maps the category name to the DCS group category
---@param input string the name ---@param input string the name
---@return integer? ---@return integer?
@@ -790,7 +763,7 @@ do -- INIT DCS_UTIL
---@return Array<Unit> units ---@return Array<Unit> units
function DCS_UTIL.getAllPlayerUnits() function DCS_UTIL.getAllPlayerUnits()
local units = {} local units = {}
for i = 0,2 do for i = 0, 2 do
local players = coalition.getPlayers(i) local players = coalition.getPlayers(i)
for key, unit in pairs(players) do for key, unit in pairs(players) do
units[#units + 1] = unit units[#units + 1] = unit
@@ -809,7 +782,7 @@ do -- INIT DCS_UTIL
---get base from id ---get base from id
---@param baseId number ---@param baseId number
---@return table? table ---@return Airbase? table
function DCS_UTIL.getAirbaseById(baseId) function DCS_UTIL.getAirbaseById(baseId)
local name = DCS_UTIL.getAirbaseName(baseId) local name = DCS_UTIL.getAirbaseName(baseId)
if name == nil then return nil end if name == nil then return nil end
@@ -817,7 +790,7 @@ do -- INIT DCS_UTIL
end end
---Get the starting coalition of a farp or airbase ---Get the starting coalition of a farp or airbase
---@param airbase Airbase ---@param airbase Airbase
---@return number? coalition ---@return number? coalition
function DCS_UTIL.getStartingCoalition(airbase) function DCS_UTIL.getStartingCoalition(airbase)
if airbase == nil then if airbase == nil then
@@ -865,6 +838,92 @@ do -- INIT DCS_UTIL
end end
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 --- spawns the units as specified in the mission file itself
--- location and route can be nil and will then use default route --- location and route can be nil and will then use default route
---@param groupName string ---@param groupName string
@@ -930,15 +989,15 @@ do -- INIT DCS_UTIL
end end
end end
end end
return false return false
end end
---comment ---comment
---@param groupId number ---@param groupId number
---@return Group? ---@return Group?
function DCS_UTIL.GetPlayerGroupByGroupID(groupId) function DCS_UTIL.GetPlayerGroupByGroupID(groupId)
for i = 0,2 do for i = 0, 2 do
local players = coalition.getPlayers(i) local players = coalition.getPlayers(i)
for key, unit in pairs(players) do for key, unit in pairs(players) do
if unit and unit:isExist() == true then if unit and unit:isExist() == true then
+65 -25
View File
@@ -101,7 +101,7 @@ function Database.New(Logger)
local zone_name = zone_data.name local zone_name = zone_data.name
---@type Vec2 ---@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, "_") local split_string = Spearhead.Util.split_string(zone_name, "_")
table.insert(self._tables.AllZoneNames, 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 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 else
local function getDist(a, b)
return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
end
local biggest = nil local biggest = nil
local biggestA = nil local biggestA = nil
local biggestB = nil local biggestB = nil
@@ -325,7 +322,7 @@ function Database.New(Logger)
for ii = i + 1, 4 do for ii = i + 1, 4 do
local a = zone.verts[i] local a = zone.verts[i]
local b = zone.verts[ii] local b = zone.verts[ii]
local dist = getDist(a, b) local dist = Spearhead.Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then if biggest == nil or dist > biggest then
biggestA = a biggestA = a
@@ -338,8 +335,8 @@ function Database.New(Logger)
if biggestA and biggestB then if biggestA and biggestB then
table.insert(capData.routes, table.insert(capData.routes,
{ {
point1 = { x = biggestA.x, z = biggestA.z }, point1 = { x = biggestA.x, z = biggestA.y },
point2 = { x = biggestB.x, z = biggestB.z } point2 = { x = biggestB.x, z = biggestB.y }
}) })
end end
end end
@@ -348,9 +345,24 @@ function Database.New(Logger)
end end
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("initiated the database with amount of zones: ")
self._logger:info("Stages: " .. Spearhead.Util.tableLength(self._tables.StageZones)) self._logger:info("Stages: " .. Spearhead.Util.tableLength(self._tables.StageZones))
self._logger:info("Total Missions: " .. Spearhead.Util.tableLength(self._tables.MissionZoneData)) 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("Random Missions: " .. Spearhead.Util.tableLength(self._tables.RandomMissionZones))
self._logger:info("Farps: " .. Spearhead.Util.tableLength(self._tables.AllFarpZones)) self._logger:info("Farps: " .. Spearhead.Util.tableLength(self._tables.AllFarpZones))
self._logger:info("Airbases: " .. Spearhead.Util.tableLength(self._tables.AirbaseDataPerAirfield)) self._logger:info("Airbases: " .. Spearhead.Util.tableLength(self._tables.AirbaseDataPerAirfield))
@@ -393,9 +405,21 @@ function Database:loadCapUnits()
local all_groups = getAvailableCAPGroups() local all_groups = getAvailableCAPGroups()
local airbases = world.getAirbases() local airbases = world.getAirbases()
for _, airbase in pairs(airbases) do for _, airbase in pairs(airbases) do
local baseId = airbase:getID()
local point = airbase:getPoint() 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 baseData = self:getOrCreateAirbaseData(airbase:getName())
local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, zone) local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, zone)
@@ -480,9 +504,19 @@ function Database:loadAirbaseGroups()
if base then if base then
local basedata = self:getOrCreateAirbaseData(baseName) local basedata = self:getOrCreateAirbaseData(baseName)
local baseId = base:getID()
local point = base:getPoint() 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 if airbaseZone and base:getDesc().category == Airbase.Category.AIRDROME then
local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, airbaseZone) local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, airbaseZone)
@@ -554,7 +588,11 @@ function Database:GetLocationForMissionZone(missionZoneName)
return self._tables.MissionZonesLocations[missionZoneName] return self._tables.MissionZonesLocations[missionZoneName]
end 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 stageNumber = tostring(stageNumber) or "nothing"
local routeData = self._tables.CapDataPerStageNumber[stageNumber] local routeData = self._tables.CapDataPerStageNumber[stageNumber]
if routeData then if routeData then
@@ -576,24 +614,24 @@ function Database:getCapRouteInZone(stageNumber, baseId)
local aY = pC.z + vY / magV * radius; local aY = pC.z + vY / magV * radius;
return { x = aX, z = aY } return { x = aX, z = aY }
end end
local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or "none"
"none"
local stagezone = Spearhead.DcsUtil.getZoneByName(stageZoneName) local stagezone = Spearhead.DcsUtil.getZoneByName(stageZoneName)
if stagezone then if stagezone then
local base = Spearhead.DcsUtil.getAirbaseById(baseId)
---@type Airbase?
local base = Airbase.getByName(baseName)
if base then if base then
local closest = nil local closest = nil
if stagezone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then 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()) base:getPoint())
else else
local function getDist(a, b)
return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
end
local closestDistance = -1 local closestDistance = -1
for _, vert in pairs(stagezone.verts) do 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 if closestDistance == -1 or distance < closestDistance then
closestDistance = distance closestDistance = distance
closest = vert closest = vert
@@ -602,17 +640,19 @@ function Database:getCapRouteInZone(stageNumber, baseId)
end end
if math.random(1, 2) % 2 == 0 then 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 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 end
end end
return nil
end end
end end
---@return table result a list of stage zone names ---@return Array<string> result a list of stage zone names
function Database:getStagezoneNames() function Database:getStagezoneNames()
return self._tables.StageZoneNames return self._tables.StageZoneNames
end end
+38 -1
View File
@@ -73,7 +73,35 @@ do
end end
end end
---@class OnWeaponFiredListener
---@field OnWeaponFired fun(self:OnWeaponFiredListener, unit:Unit?, weapon:Weapon?, target:Object?)
---@type Array<OnWeaponFiredListener>
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 = {} local onLandEventListeners = {}
---Add an event listener to a specific unit ---Add an event listener to a specific unit
@@ -311,6 +339,15 @@ do
end 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 if event.id == world.event.S_EVENT_MISSION_END then
Spearhead.classes.persistence.Persistence.UpdateNow() Spearhead.classes.persistence.Persistence.UpdateNow()
end end
+4 -5
View File
@@ -212,12 +212,11 @@ do --setup route util
---@param durationOnStation number ---@param durationOnStation number
---@param attackHelos boolean ---@param attackHelos boolean
---@param deviationDistance number ---@param deviationDistance number
---@return table route ---@return table? route
ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed, ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance)
durationOnStation, attackHelos, deviationDistance)
local baseName = Spearhead.DcsUtil.getAirbaseName(airdromeId) local baseName = Spearhead.DcsUtil.getAirbaseName(airdromeId)
if baseName == nil then if baseName == nil then
return {} return nil
end end
durationOnStation = durationOnStation or 1800 durationOnStation = durationOnStation or 1800
@@ -228,7 +227,7 @@ do --setup route util
local base = Airbase.getByName(baseName) local base = Airbase.getByName(baseName)
if base == nil then if base == nil then
return {} return nil
end end
local additionalFlyOverTasks = { local additionalFlyOverTasks = {
+6 -5
View File
@@ -19,10 +19,11 @@ GlobalStageManager = {}
---comment ---comment
---@param database Database ---@param database Database
---@param stageConfig StageConfig ---@param stageConfig StageConfig
---@param logLevel LogLevel
---@return nil ---@return nil
function GlobalStageManager:NewAndStart(database, stageConfig) function GlobalStageManager:NewAndStart(database, stageConfig, logLevel)
local logger = Spearhead.LoggerTemplate.new("StageManager", stageConfig.logLevel) local logger = Spearhead.LoggerTemplate.new("StageManager", logLevel)
logger:info("Using Stage Log Level: " .. stageConfig.logLevel) logger:info("Using Stage Log Level: " .. logLevel)
local o = {} local o = {}
setmetatable(o, { __index = self }) setmetatable(o, { __index = self })
@@ -122,7 +123,7 @@ function GlobalStageManager:NewAndStart(database, stageConfig)
end end
local stageDisplayName = split[3] 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 if valid == true and orderNumber then
---@type StageInitData ---@type StageInitData
@@ -175,7 +176,7 @@ function GlobalStageManager:NewAndStart(database, stageConfig)
end end
if valid == true then if valid == true then
local stagelogger = Spearhead.LoggerTemplate.new(stageName, stageConfig.logLevel) local stagelogger = Spearhead.LoggerTemplate.new(stageName, logLevel)
---@type WaitingStageInitData ---@type WaitingStageInitData
local initData = { local initData = {
@@ -57,7 +57,6 @@ function BlueSam.New(database, logger, zoneName)
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Spearhead.Util.VectorDistance3d(blueUnitPos, redUnitPos) local distance = Spearhead.Util.VectorDistance3d(blueUnitPos, redUnitPos)
env.info("distance: " .. tostring(distance))
if distance <= cleanup_distance then if distance <= cleanup_distance then
self._cleanupUnits[redUnitName] = true self._cleanupUnits[redUnitName] = true
end end
@@ -91,4 +90,4 @@ if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
if Spearhead.classes.stageClasses.SpecialZones == nil then Spearhead.classes.stageClasses.SpecialZones = {} 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
+51 -43
View File
@@ -6,8 +6,8 @@
--- @class StageData --- @class StageData
--- @field missionsByCode table<string, Mission> --- @field missionsByCode table<string, Mission>
--- @field missions Array<Mission> --- @field missions Array<ZoneMission>
--- @field sams Array<Mission> --- @field sams Array<ZoneMission>
--- @field blueSams Array<BlueSam> --- @field blueSams Array<BlueSam>
--- @field airbases Array<StageBase> --- @field airbases Array<StageBase>
--- @field miscGroups Array<SpearheadGroup> --- @field miscGroups Array<SpearheadGroup>
@@ -24,7 +24,7 @@
--- @class Stage : MissionCompleteListener, OnStageChangedListener --- @class Stage : MissionCompleteListener, OnStageChangedListener
--- @field zoneName string --- @field zoneName string
--- @field stageName string --- @field stageName string?
--- @field stageNumber number --- @field stageNumber number
--- @field protected _isActive boolean --- @field protected _isActive boolean
--- @field protected _isComplete boolean --- @field protected _isComplete boolean
@@ -47,6 +47,14 @@ Stage.__index = Stage
local stageDrawingId = 100 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 ---comment
---@param database Database ---@param database Database
---@param stageConfig StageConfig ---@param stageConfig StageConfig
@@ -83,7 +91,12 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
self._activeStage = -99 self._activeStage = -99
self._preActivated = false self._preActivated = false
self._stageConfig = stageConfig or {} 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._spawnedGroups = {}
self._missionPriority = missionPriority self._missionPriority = missionPriority
self._stageCompleteListeners = {} self._stageCompleteListeners = {}
@@ -108,8 +121,9 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
do -- load tables do -- load tables
local missionZones = database:getMissionsForStage(self.zoneName) 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 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 if mission then
self._db.missionsByCode[mission.code] = mission self._db.missionsByCode[mission.code] = mission
if mission.missionType == "SAM" then if mission.missionType == "SAM" then
@@ -125,7 +139,7 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
---@type table<string, Array<Mission>> ---@type table<string, Array<Mission>>
local randomMissionByName = {} local randomMissionByName = {}
for _, missionZoneName in pairs(randomMissionNames) do 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 mission then
if randomMissionByName[mission.name] == nil then if randomMissionByName[mission.name] == nil then
randomMissionByName[mission.name] = {} randomMissionByName[mission.name] = {}
@@ -200,14 +214,14 @@ function Stage:IsComplete()
if self._isComplete == true then return true end if self._isComplete == true then return true end
for i, mission in pairs(self._db.sams) do for i, mission in pairs(self._db.sams) do
local state = mission:GetState() local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then if state == "ACTIVE" or state == "NEW" then
return false return false
end end
end end
for i, mission in pairs(self._db.missions) do for i, mission in pairs(self._db.missions) do
local state = mission:GetState() local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then if state == "ACTIVE" or state == "NEW" then
return false return false
end end
@@ -231,7 +245,7 @@ function Stage:CheckAndUpdateSelf()
local availableMissions = {} local availableMissions = {}
for _, mission in pairs(dbTables.missionsByCode) do for _, mission in pairs(dbTables.missionsByCode) do
local state = mission:GetState() local state = mission:getState()
if state == "ACTIVE" then if state == "ACTIVE" then
activeCount = activeCount + 1 activeCount = activeCount + 1
@@ -266,7 +280,7 @@ end
---private use only ---private use only
function Stage:NotifyComplete() 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 for _, listener in pairs(self._stageCompleteListeners) do
pcall(function() pcall(function()
@@ -285,12 +299,13 @@ function Stage:AddStageCompleteListener(listener)
end end
---Activates all SAMS, Airbase units etc all at once. ---Activates all SAMS, Airbase units etc all at once.
function Stage:PreActivate() ---@param draw boolean
function Stage:PreActivate(draw)
if self._preActivated == false then if self._preActivated == false then
self._preActivated = true self._preActivated = true
for key, mission in pairs(self._db.sams) do for key, mission in pairs(self._db.sams) do
if mission then if mission then
mission:SpawnActive() mission:SpawnInactive()
end end
end end
@@ -298,36 +313,25 @@ function Stage:PreActivate()
airbase:ActivateRedStage() airbase:ActivateRedStage()
end end
end end
end
---@param stageColor StageColor if draw == true then
function Stage:MarkStage(stageColor) self:MarkStage(Stage.StageColors.RED_PREACTIVE)
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 }
end end
local zone = Spearhead.DcsUtil.getZoneByName(self.zoneName) end
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
trigger.action.setMarkupColorFill(self._stageDrawingId, fillColor) ---@param stageColor DrawColor
trigger.action.setMarkupColor(self._stageDrawingId, line) 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
end end
@@ -335,10 +339,10 @@ function Stage:ActivateStage()
self._isActive = true; self._isActive = true;
pcall(function() pcall(function()
self:MarkStage("RED") self:MarkStage(Stage.StageColors.RED_ACTIVE)
end) end)
self:PreActivate() self:PreActivate(false)
self._logger:debug("Activating Misc groups for zone. Count: " .. Spearhead.Util.tableLength(self._db.miscGroups)) self._logger:debug("Activating Misc groups for zone. Count: " .. Spearhead.Util.tableLength(self._db.miscGroups))
for _, miscGroup in pairs(self._db.miscGroups) do for _, miscGroup in pairs(self._db.miscGroups) do
@@ -371,8 +375,12 @@ function Stage:OnStageNumberChanged(number)
local previousActive = self._activeStage local previousActive = self._activeStage
self._activeStage = number 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 end
if number == self.stageNumber then if number == self.stageNumber then
@@ -434,7 +442,7 @@ function Stage:ActivateBlueStage()
---@param self Stage ---@param self Stage
local ActivateBlueAsync = function(self) local ActivateBlueAsync = function(self)
pcall(function() pcall(function()
self:MarkStage("BLUE") self:MarkStage(Stage.StageColors.BLUE)
end) end)
self:ActivateBlueGroups() self:ActivateBlueGroups()
+7 -5
View File
@@ -1,10 +1,8 @@
---@class ExtraStage : Stage ---@class ExtraStage : Stage
local ExtraStage = {} local ExtraStage = {}
ExtraStage.__index = ExtraStage ExtraStage.__index = ExtraStage
local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
setmetatable(ExtraStage, Stage)
---comment ---comment
---@param database Database ---@param database Database
@@ -14,11 +12,15 @@ setmetatable(ExtraStage, Stage)
---@return ExtraStage ---@return ExtraStage
function ExtraStage.New(database, stageConfig, logger, initData) 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]] local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
self:superNew(database, stageConfig, logger, initData, "secondary") self:superNew(database, stageConfig, logger, initData, "secondary")
self.OnPostBlueActivated = function (selfStage) self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage("GRAY")
selfStage:MarkStage(Stage.StageColors.GRAY)
end end
self.OnPostStageComplete = function (selfStage) self.OnPostStageComplete = function (selfStage)
@@ -35,7 +37,7 @@ function ExtraStage:OnStageNumberChanged(number)
self._activeStage = number self._activeStage = number
if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate() self:PreActivate(false)
end end
if number == self.stageNumber then if number == self.stageNumber then
+4 -3
View File
@@ -27,9 +27,10 @@ function WaitingStage.New(database, stageConfig, logger, initData)
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
self._startTime = nil self._startTime = nil
self.CheckContinuousAsync = function (self, time) self.CheckContinuousAsync = function (selfA, time)
if self:IsComplete() then
self:NotifyComplete() if selfA:IsComplete() == true then
selfA:NotifyComplete()
return nil return nil
end end
@@ -114,7 +114,7 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
end end
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 end
---Primary missions ---Primary missions
@@ -218,7 +218,6 @@ function MissionCommandsHelper:addMissionCommands(groupId, mission)
end end
if path then if path then
self._logger:debug("Registering command: [" .. mission.code .. "]" .. mission.name)
local missionFolderName = "[" .. mission.code .. "]" .. mission.name local missionFolderName = "[" .. mission.code .. "]" .. mission.name
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path) missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName) table.insert(path, missionFolderName)
@@ -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<RunwaySection>
---@field private _minKilosForDamage number
---@field private _repairInProgress boolean
local RunwayStrikeMission = {}
---@class RunwaySection
---@field center Vec2
---@field corners Array<Vec2>
---@field kilosHit number
---@field repairGroups Array<string>
---@field drawID number?
---@param runway Runway
---@param database Database
---@param logger Logger
---@param runwayBombingTracker RunwayBombingTracker
---@return RunwayStrikeMission?
function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBombingTracker)
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<Array<StaticSpawn>>
local repairStaticConfigs = {
[1] = {
[1] = {
["category"] = "Unarmed",
["type"] = "ZIL-135",
["y"] = -4,
["x"] = 10,
["heading"] = 6.2133721370998,
},
[2] = {
["category"] = "Unarmed",
["type"] = "Tigr_233036",
["y"] = 0,
["x"] = 10,
["heading"] = 6.2133721370998,
},
[3] = {
["category"] = "Unarmed",
["type"] = "Infantry AK ver3",
["y"] = 10,
["x"] = 7,
["heading"] = 4.4331363000656,
},
[4] = {
["category"] = "Unarmed",
["type"] = "Infantry AK ver3",
["y"] = 12,
["x"] = 1,
["heading"] = 4.4331363000656,
},
[5] = {
["category"] = "Unarmed",
["type"] = "Infantry AK ver3",
["y"] = -10,
["x"] = -2,
["heading"] = 4.1538836197465,
},
[6] = {
["category"] = "Unarmed",
["type"] = "CV_59_Large_Forklift",
["y"] = -11,
["x"] = 0,
["heading"] = 1.535889741755,
},
[7] = {
["category"] = "Unarmed",
["type"] = "ZiL-131 APA-80",
["y"] = 11,
["x"] = 5,
["heading"] = 0.62831853071796,
},
[8] = {
["category"] = "Unarmed",
["type"] = "CV_59_NS60",
["y"] = -4,
["x"] = -15,
["heading"] = 0.62831853071796,
}
},
[2] = {
[1] = {
["category"] = "Air Defence",
["type"] = "generator_5i57",
["y"] = 2.7889551542015,
["x"] = -4.7698247930386,
["heading"] = 4.0666171571468,
},
[2] = {
["category"] = "Infantry",
["type"] = "Infantry AK ver3",
["y"] = 3.639392285097,
["x"] = 1.4300755972836,
["heading"] = 5.0440015382636,
},
[3] = {
["category"] = "Infantry",
["type"] = "Infantry AK ver3",
["y"] = 3.5139072826724,
["x"] = -0.32671443666074,
["heading"] = 6.16101225954,
},
[4] = {
["category"] = "Unarmed",
["type"] = "ATMZ-5",
["y"] = -6.9556010010953,
["x"] = 3.007681753102,
["heading"] = 4.0666171571468,
},
[5] = {
["category"] = "Unarmed",
["type"] = "GAZ-66",
["y"] = 5.9304017026008,
["x"] = 0.80323129595153,
["heading"] = 6.2308254296198,
}
}
}
---@private
---@param section RunwaySection
function RunwayStrikeMission:AddOrUpdateRepairStatics(section)
if 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<RunwaySection>
function RunwayStrikeMission:ToSections(runway, numSections)
local sections = {}
local center = runway.position
local heading = runway.course
local width = runway.width
local length = runway.length / numSections
if heading < 0 then
heading = math.abs(heading)
else
heading = 0 - heading
end
local cosH = math.cos(heading)
local sinH = math.sin(heading)
for i = 0, numSections - 1 do
local sectionCenterOffset = (i - (numSections / 2) + 0.5) * length
---@type Vec2
local sectionCenter = {
x = center.x + sectionCenterOffset * cosH,
y = center.z + sectionCenterOffset * sinH
}
local halfWidth = width / 2
local halfLength = length / 2
---@type Array<Vec2>
local corners = {
{
x = sectionCenter.x + (-halfLength * cosH - halfWidth * sinH),
y = sectionCenter.y + (-halfLength * sinH + halfWidth * cosH)
},
{
x = sectionCenter.x + (-halfLength * cosH + halfWidth * sinH),
y = sectionCenter.y + (-halfLength * sinH - halfWidth * cosH)
},
{
x = sectionCenter.x + (halfLength * cosH + halfWidth * sinH),
y = sectionCenter.y + (halfLength * sinH - halfWidth * cosH)
},
{
x = sectionCenter.x + (halfLength * cosH - halfWidth * sinH),
y = sectionCenter.y + (halfLength * sinH + halfWidth * cosH)
}
}
---@type RunwaySection
local section = {
center = sectionCenter,
corners = corners,
kilosHit = 0,
repairGroups = {},
}
table.insert(sections, section)
end
return sections
end
---@private
---@param runway Runway
---@return SpearheadTriggerZone
function RunwayStrikeMission:RunwayToSpearheadZone(runway)
-- Calculate the 4 corner points of the runway based on heading, height, and width
local radHeading = runway.course
if radHeading < 0 then
radHeading = math.abs(radHeading)
else
radHeading = 0 - radHeading
end
local cosH = math.cos(radHeading)
local sinH = math.sin(radHeading)
local halfWidth = runway.width / 2
local halfHeight = runway.length / 2
---@type Array<Vec2>
local corners = {
{
x = runway.position.x + (-halfHeight * cosH - halfWidth * sinH),
y = runway.position.z + (-halfHeight * sinH + halfWidth * cosH)
},
{
x = runway.position.x + (-halfHeight * cosH + halfWidth * sinH),
y = runway.position.z + (-halfHeight * sinH - halfWidth * cosH)
},
{
x = runway.position.x + (halfHeight * cosH + halfWidth * sinH),
y = runway.position.z + (halfHeight * sinH - halfWidth * cosH)
},
{
x = runway.position.x + (halfHeight * cosH - halfWidth * sinH),
y = runway.position.z + (halfHeight * sinH + halfWidth * cosH)
}
}
return {
location = { x = runway.position.x, y = runway.position.z },
radius = runway.width,
name = runway.Name,
verts = corners,
zone_type = "Polygon",
}
end
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
@@ -1,32 +1,55 @@
---@class Mission : OnUnitLostListener --- ZoneMission is missions that are defined by zones in the ME
---@field name string ---@class ZoneMission : Mission, OnUnitLostListener
---@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?
---@field private _missionGroups MissionGroups ---@field private _missionGroups MissionGroups
---@field private _completeListeners Array<MissionCompleteListener> local ZoneMission = {}
---@field private _missionCommandsHelper MissionCommandsHelper
local Mission = {}
--- @class MissionCompleteListener --- @class MissionGroups
--- @field OnMissionComplete fun(self: any, mission:Mission)
--- @class MissionGroups
--- @field hasTargets boolean --- @field hasTargets boolean
--- @field groups Array<SpearheadGroup> --- @field groups Array<SpearheadGroup>
--- @field unitsAlive table<string, table<string, boolean>> --- @field unitsAlive table<string, table<string, boolean>>
--- @field targetsAlive table<string, table<string, boolean>> --- @field targetsAlive table<string, table<string, boolean>>
--- @field groupNamesPerunit table<string,string> --- @field groupNamesPerunit table<string,string>
---@class ParsedMissionName
---@field missionName string
---@field type MissionType
---comment
---@param input string
---@return ParsedMissionName?
local function ParseZoneName(input)
local split_name = 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 MINIMAL_UNITS_ALIVE_RATIO = 0.21
---comment ---comment
@@ -34,64 +57,32 @@ MINIMAL_UNITS_ALIVE_RATIO = 0.21
---@param priority MissionPriority ---@param priority MissionPriority
---@param database Database ---@param database Database
---@param logger Logger ---@param logger Logger
---@return Mission? ---@return ZoneMission?
function Mission.New(zoneName, priority, database, logger) 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 self = setmetatable({}, ZoneMission)
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 parsed = ParseZoneName(zoneName) 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 success, error = Mission.newSuper(self, zoneName, parsed.missionName, parsed.type, missionBriefing, priority, database, logger)
local o = {} if not success then
local self = setmetatable(o, Mission) logger:error("Failed to create ZoneMission " .. zoneName .. " => " .. error)
return nil
self.zoneName = zoneName end
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"
self._logger = logger if self.missionType == "SAM" then
self._database = database self.missionTypeDisplay = "DEAD"
self._missionCommandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(logger.LogLevel) end
self._completeListeners = {}
self._missionBriefing = database:getMissionBriefingForMissionZone(zoneName)
self._missionGroups = { self._missionGroups = {
groups = {}, groups = {},
unitsAlive = {}, unitsAlive = {},
@@ -103,16 +94,15 @@ function Mission.New(zoneName, priority, database, logger)
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup
local groupNames = database:getGroupsForMissionZone(zoneName) local groupNames = database:getGroupsForMissionZone(zoneName)
for _, groupName in pairs(groupNames) do for _, groupName in pairs(groupNames) do
local spearheadGroup = SpearheadGroup.New(groupName) local spearheadGroup = SpearheadGroup.New(groupName)
table.insert(self._missionGroups.groups, spearheadGroup) table.insert(self._missionGroups.groups, spearheadGroup)
local isGroupTarget =Spearhead.Util.startswith(string.lower(groupName), "tgt_") local isGroupTarget = Spearhead.Util.startswith(string.lower(groupName), "tgt_")
for _, unit in pairs(spearheadGroup:GetUnits())do for _, unit in pairs(spearheadGroup:GetUnits()) do
local unitName = unit:getName() local unitName = unit:getName()
local isUnitTarget = Spearhead.Util.startswith(string.lower(unitName), "tgt_") 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] = {} self._missionGroups.unitsAlive[groupName] = {}
end end
@@ -135,109 +125,15 @@ function Mission.New(zoneName, priority, database, logger)
Spearhead.DcsUtil.DestroyGroup(groupName) Spearhead.DcsUtil.DestroyGroup(groupName)
end end
self._logger:debug("Mission " .. self.name .. " group count: " .. Spearhead.Util.tableLength(groupNames))
return self return self
end end
---comment ---@internal
---@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
---@param checkHealth boolean ---@param checkHealth boolean
---@param messageIfDone boolean ---@param messageIfDone boolean
function Mission:UpdateState(checkHealth, messageIfDone) function ZoneMission:UpdateState(checkHealth, messageIfDone)
if checkHealth == nil then checkHealth = false end if checkHealth == nil then checkHealth = false end
if messageIfDone == false then messageIfDone = true end if messageIfDone == false then messageIfDone = true end
@@ -294,7 +190,6 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end end
if self._missionGroups.hasTargets == true then if self._missionGroups.hasTargets == true then
local anyTargetAlive = function() local anyTargetAlive = function()
for _, units in pairs(self._missionGroups.targetsAlive) do for _, units in pairs(self._missionGroups.targetsAlive) do
for _, isAlive in pairs(units) do for _, isAlive in pairs(units) do
@@ -305,7 +200,7 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end end
return false return false
end end
if anyTargetAlive() ~= true then if anyTargetAlive() ~= true then
self._state = "COMPLETED" self._state = "COMPLETED"
end end
@@ -337,44 +232,101 @@ function Mission:UpdateState(checkHealth, messageIfDone)
end end
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 ---comment
---@param mission Mission ---@param mission Mission
local NotifyMissionComplete = function(mission) ---@param time any
mission:NotifyMissionComplete() ---@return unknown
return nil local Check = function(mission, time)
mission:UpdateState(true, true)
if mission:getState() == "COMPLETED" then
return nil
end
return time + 30
end end
if self._state == "COMPLETED" then timer.scheduleFunction(Check, self, timer.getTime() + 30)
timer.scheduleFunction(NotifyMissionComplete, self, timer.getTime() + 3) 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
end end
---private usage advised function ZoneMission:OnUnitLost(object)
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)
--[[ --[[
OnUnit lost event OnUnit lost event
]]-- ]] --
self._logger:debug("Getting on unit lost event") self._logger:debug("Getting on unit lost event")
local category = Object.getCategory(object) 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 if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then
self._missionGroups.targetsAlive[groupName][unitName] = false self._missionGroups.targetsAlive[groupName][unitName] = false
end end
elseif category == Object.Category.STATIC then elseif category == Object.Category.STATIC then
local name = object:getName() local name = object:getName()
self._missionGroups.unitsAlive[name][name] = false self._missionGroups.unitsAlive[name][name] = false
@@ -401,9 +353,9 @@ function Mission:OnUnitLost(object)
self:UpdateState(false, true) self:UpdateState(false, true)
end end
if not Spearhead.classes then Spearhead.classes = {} end if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Missions then Spearhead.classes.stageClasses.Missions = {} end if not Spearhead.classes.stageClasses.missions then Spearhead.classes.stageClasses.missions = {} end
Spearhead.classes.stageClasses.Missions.Mission = Mission Spearhead.classes.stageClasses.missions.ZoneMission = ZoneMission
@@ -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<MissionCompleteListener>
local Mission = {}
Mission.__index = Mission
--- @class MissionCompleteListener
--- @field OnMissionComplete fun(self: any, mission:Mission)
---@protected
---@param self Mission
---@param zoneName string
---@param missionName string
---@param missionType MissionType
---@param missionBriefing string
---@param priority MissionPriority
---@param database Database
---@param logger Logger
---@return boolean, string
function Mission.newSuper(self, zoneName, missionName, missionType, missionBriefing, priority, database, logger)
self.zoneName = zoneName
self.name = missionName
self.missionType = missionType
self.priority = priority
self.state = "NEW"
self._logger = logger
self._database = database
self.missionBriefing = missionBriefing
self.code = tostring(database:GetNewMissionCode())
self._completeListeners = {}
self.location = database:GetLocationForMissionZone(zoneName)
self.missionTypeDisplay = self.missionType
self._missionCommandsHelper = 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
+5
View File
@@ -40,6 +40,7 @@ SpearheadConfig = {
--Will draw the active and the next stage --Will draw the active and the next stage
drawStages = true, -- default true drawStages = true, -- default true
drawPreActivated = true, -- default true
--AutoStages will continue to the next stage automatically on completion of the missions within the stage. --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. -- 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 --Stage starting number
startingStage = 1, 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 ---DEBUG logging. Consider keeping this disabled
debugEnabled = false debugEnabled = false
}, },
+7 -1
View File
@@ -6,6 +6,8 @@
local basePath = "C:\\Repos\\DCS\\Spearhead\\" local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\" local classPath = basePath .. "classes\\"
assert(loadfile(classPath .. "_baseClasses\\Queue.lua"))()
assert(loadfile(classPath .. "spearhead_base.lua"))() assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))() assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.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 .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.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\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.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\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))() assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))() assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))() assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
+2 -2
View File
@@ -52,7 +52,7 @@ SpearheadConfig = {
maxMissionStage = 10, maxMissionStage = 10,
--Stage starting number --Stage starting number
startingStage = 1, startingStage = 0,
---DEBUG logging. Consider keeping this disabled ---DEBUG logging. Consider keeping this disabled
debugEnabled = true debugEnabled = true
@@ -61,7 +61,7 @@ SpearheadConfig = {
--- io and lfs cannot be sanitized in the MissionScripting.lua --- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead --- enables or disables the persistence logic in spearhead
enabled = true, enabled = false,
--- sets the directory where the persistence file is stored --- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used. --- if nil then lfs.writedir() will be used.
+2 -2
View File
@@ -36,8 +36,8 @@ else
standardLogger:info("Persistence disabled") standardLogger:info("Persistence disabled")
end end
Spearhead.internal.GlobalCapManager.start(databaseManager, capConfig, stageConfig) Spearhead.classes.capClasses.GlobalCapManager.start(databaseManager, capConfig, stageConfig, defaultLogLevel)
Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig) Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig, defaultLogLevel)
Spearhead.internal.GlobalFleetManager.start(databaseManager) Spearhead.internal.GlobalFleetManager.start(databaseManager)
local SetStageDelayed = function(number, time) local SetStageDelayed = function(number, time)