Cas mission (#39)

* Added battlemanager and CAS missions

* Added battlemanager and CAS missions

---------

Co-authored-by: dutchie032 <dutchie032>
This commit is contained in:
2025-05-25 18:57:46 +02:00
committed by GitHub
co-authored by dutchie032 <dutchie032>
parent 61d2792524
commit e0c0f6e34b
14 changed files with 430 additions and 33 deletions
+15
View File
@@ -92,6 +92,11 @@ jobs:
git tag ${{ steps.calculate_tag.outputs.new_tag }}
git push origin ${{ steps.calculate_tag.outputs.new_tag }}
- name: Copy Release to Versioned File
run: |
cp ./output/spearhead.lua ./output/spearhead.${{ steps.calculate_tag.outputs.new_tag }}.lua
echo "Versioned file created: spearhead.${{ steps.calculate_tag.outputs.new_tag }}.lua"
- name: Create GitHub Release
uses: actions/create-release@v1
id: create_release
@@ -114,3 +119,13 @@ jobs:
asset_name: spearhead.lua
asset_content_type: application/octet-stream
- name: Upload Spearhead.lua Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/spearhead.${{steps.calculate_tag.outputs.new_tag}}.lua
asset_name: spearhead.lua
asset_content_type: application/octet-stream
+8
View File
@@ -62,6 +62,7 @@
<li><a href="#sam" class="side-nav-h3">SAM</a></li>
<li><a href="#dead" class="side-nav-h3">DEAD</a></li>
<li><a href="#bai" class="side-nav-h3">BAI</a></li>
<li><a href="#cas" class="side-nav-h3">BAI</a></li>
<li><a href="#strike" class="side-nav-h3">STRIKE</a></li>
</ul>
</li>
@@ -206,6 +207,13 @@
BAI missions involve battlefield air interdiction. These missions target enemy ground forces.
</p>
<h3 id="cas">CAS</h3>
<p>
CAS missions involve close air support for friendly ground forces. <br />
CAS mission have a special "BattleManager" module added by defualt. This will make blue and red units shoot at eachoter. <br/>
This creates a nice effect. <br/>
</p>
<h3 id="strike">STRIKE</h3>
<p>
STRIKE missions target strategic objectives, such as supply depots or command centers. <br />
Binary file not shown.
+42
View File
@@ -287,6 +287,48 @@ do -- INIT UTIL
return UTIL.getConvexHull(allpoints)
end
---Returns hull points visible from origin (not blocked by hull edges)
---@param hull Array<Vec2>
---@param origin Vec2
---@return Array<Vec2>
function UTIL.GetVisibleHullPointsFromOrigin(hull, origin)
local function segmentsIntersect(a, b, c, d)
-- Helper: returns true if segment ab intersects cd (excluding endpoints)
local function ccw(p1, p2, p3)
return (p3.y - p1.y) * (p2.x - p1.x) > (p2.y - p1.y) * (p3.x - p1.x)
end
return (ccw(a, c, d) ~= ccw(b, c, d)) and (ccw(a, b, c) ~= ccw(a, b, d))
end
local n = #hull
local visible = {}
for i = 1, n do
local p = hull[i]
local isVisible = true
-- Check against all hull edges except those incident to p
for j = 1, n do
local a = hull[j]
local b = hull[(j % n) + 1]
-- skip edges incident to p
if (a ~= p and b ~= p) then
if segmentsIntersect(origin, p, a, b) then
isVisible = false
break
end
end
end
if isVisible then
table.insert(visible, p)
end
end
return visible
end
end
Spearhead.Util = UTIL
+30 -11
View File
@@ -56,7 +56,8 @@
---@field buildingKilos number?
---@class MissionZoneData
---@field Groups Array<string>
---@field RedGroups Array<string>
---@field BlueGroups Array<string>
---@class MissionData
---@field Groups Array<string>
@@ -423,7 +424,14 @@ function Database.New(Logger)
local missions = 0
for _, data in pairs(self._tables.MissionZoneData) do
missions = missions + 1
for _, groupName in pairs(data.Groups) do
for _, groupName in pairs(data.RedGroups) do
local group = Group.getByName(groupName)
if group then
totalUnits = totalUnits + group:getInitialSize()
end
end
for _, groupName in pairs(data.BlueGroups) do
local group = Group.getByName(groupName)
if group then
totalUnits = totalUnits + group:getInitialSize()
@@ -645,13 +653,19 @@ function Database:loadMissionzoneUnits()
local all_groups = getAvailableGroups()
for _, missionZoneName in pairs(self._tables.MissionZones) do
self._tables.MissionZoneData[missionZoneName] = {
Groups = {}
RedGroups = {},
BlueGroups = {},
}
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
for _, groupName in pairs(groups) do
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif group and group:getCoalition() == coalition.side.BLUE then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
is_group_taken[groupName] = true
table.insert(self._tables.MissionZoneData[missionZoneName].Groups, groupName)
end
end
end
@@ -661,12 +675,19 @@ function Database:loadRandomMissionzoneUnits()
local all_groups = getAvailableGroups()
for _, missionZoneName in pairs(self._tables.RandomMissionZones) do
self._tables.MissionZoneData[missionZoneName] = {
Groups = {}
RedGroups = {},
BlueGroups = {},
}
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
for _, groupName in pairs(groups) do
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif group and group:getCoalition() == coalition.side.BLUE then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
is_group_taken[groupName] = true
table.insert(self._tables.MissionZoneData[missionZoneName].Groups, groupName)
end
end
end
@@ -885,11 +906,9 @@ function Database:getRandomMissionsForStage(stagename)
return stageZone.RandomMissionZones
end
---@return Array<string>
function Database:getGroupsForMissionZone(missionZoneName)
local missionZoneData = self._tables.MissionZoneData[missionZoneName]
if not missionZoneData then return {} end
return missionZoneData.Groups
---@return MissionZoneData?
function Database:getMissionDataForZone(missionZoneName)
return self._tables.MissionZoneData[missionZoneName]
end
---@param stageName string
+49 -6
View File
@@ -6,13 +6,10 @@
---@field private _isStatic boolean
---@field private _isSpawned boolean
local SpearheadGroup = {}
SpearheadGroup.__index = SpearheadGroup
function SpearheadGroup.New(groupName)
SpearheadGroup.__index = SpearheadGroup
local o = {}
local self = setmetatable(o, SpearheadGroup)
local self = setmetatable({}, SpearheadGroup)
self._isStatic = Spearhead.DcsUtil.IsGroupStatic(groupName) == true
self.groupName = groupName
@@ -132,7 +129,7 @@ end
---comment
---@return table result list of objects
function SpearheadGroup:GetUnits()
function SpearheadGroup:GetObjects()
local result = {}
if self._isStatic == true then
@@ -150,6 +147,24 @@ function SpearheadGroup:GetUnits()
return result
end
---comment
---@return Array<Unit> result list of objects
function SpearheadGroup:GetAsUnits()
if self._isStatic == true then
return {}
end
local result = {}
local group = Group.getByName(self.groupName)
if not group then return {} end
for _, unit in pairs(group:getUnits()) do
table.insert(result, unit)
end
return result
end
---@return Array<Vec3>
function SpearheadGroup:GetAllUnitPositions()
@@ -167,7 +182,35 @@ function SpearheadGroup:GetAllUnitPositions()
end
end
return result
end
function SpearheadGroup:SetInvisible()
local group = Group.getByName(self.groupName)
if group then
local setInvisible = {
id = 'SetInvisible',
params = {
value = true
}
}
group:getController():setCommand(setInvisible)
end
end
function SpearheadGroup:SetVisible()
local group = Group.getByName(self.groupName)
if group then
local setInvisible = {
id = 'SetInvisible',
params = {
value = false
}
}
group:getController():setCommand(setInvisible)
end
end
if not Spearhead.classes then Spearhead.classes = {} end
@@ -53,7 +53,7 @@ function BlueSam.New(database, logger, zoneName)
end
for _, unit in pairs(SpearheadGroup:GetUnits()) do
for _, unit in pairs(SpearheadGroup:GetObjects()) do
if SpearheadGroup:GetCoalition() == 1 then
table.insert(blueUnitsPos, unit:getPoint())
elseif SpearheadGroup:GetCoalition() == 2 then
@@ -50,7 +50,7 @@ function StageBase.New(databaseManager, logger, airbaseName)
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._red_groups, shGroup)
for _, unit in pairs(shGroup:GetUnits()) do
for _, unit in pairs(shGroup:GetObjects()) do
redUnitsPos[unit:getName()] = unit:getPoint()
end
@@ -61,7 +61,7 @@ function StageBase.New(databaseManager, logger, airbaseName)
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._blue_groups, shGroup)
for _, unit in pairs(shGroup:GetUnits()) do
for _, unit in pairs(shGroup:GetObjects()) do
blueUnitsPos[unit:getName()] = unit:getPoint()
end
@@ -0,0 +1,232 @@
---@class BattleManager
---@field private _name string
---@field private _logger Logger
---@field private _redGroups Array<SpearheadGroup>
---@field private _blueGroups Array<SpearheadGroup>
---@field private _redShootAtPoints Array<Vec2>
---@field private _blueShootAtPoints Array<Vec2>
---@field private _isActive boolean
local BattleManager = {}
BattleManager.__index = BattleManager
---@param redGroups Array<SpearheadGroup>
---@param blueGroups Array<SpearheadGroup>
---@param name string
---@param logLevel LogLevel
---@return BattleManager
function BattleManager.New(redGroups, blueGroups, name, logLevel)
local self = setmetatable({}, BattleManager)
self._isActive = false
self._name = name
self._logger = Spearhead.LoggerTemplate.new("BattleManager_" .. name, logLevel)
self._redGroups = redGroups
self._blueGroups = blueGroups
self._logger:debug("BattleManager created with name: " .. self._name
.. ", red groups: " .. #self._redGroups
.. ", blue groups: " .. #self._blueGroups)
return self
end
---@param self BattleManager
---@param time number
local function CheckTask(self, time)
local interval = self:Update()
if not interval then return end
return time + interval
end
function BattleManager:Start()
self._logger:info("BattleManager started: " .. self._name)
self._isActive = true
self:SetAllInvisible()
timer.scheduleFunction(CheckTask, self, timer.getTime() + 5)
end
function BattleManager:Stop()
self._isActive = false
self:SetAllVisible()
end
---@private
function BattleManager:SetAllInvisible()
for _, group in pairs(self._redGroups) do
group:SetInvisible()
end
for _, group in pairs(self._blueGroups) do
group:SetInvisible()
end
end
---@private
function BattleManager:SetAllVisible()
for _, group in pairs(self._redGroups) do
group:SetVisible()
end
for _, group in pairs(self._blueGroups) do
group:SetVisible()
end
end
---comment
---@return number?
function BattleManager:Update()
if self._isActive == false then
return nil
end
self._logger:debug("BattleManager Update called for " .. self._name)
local shootChance = 1 -- Adjust this value to control the shooting probability (0.0 to 1.0)
self:LetUnitsShoot(self._redGroups, self._blueGroups)
self:LetUnitsShoot(self._blueGroups, self._redGroups)
return math.random(4, 10) -- Return a random interval between 5 and 10 seconds for the next update
end
---@private
---@param fromGroups Array<SpearheadGroup>
---@param targetGroups Array<SpearheadGroup>
---@return Array<Vec2>
function BattleManager:CalculateShootAtPoints(fromGroups, targetGroups)
---@type Array<Vec2>
local result = {}
for _, group in pairs(targetGroups) do
---@type Array<Vec2>
local points = {}
self._logger:debug("Processing red group: " .. group.groupName)
local groupUnits = group:GetAsUnits()
for _, unit in pairs(group:GetAsUnits()) do
local pos = unit:getPoint()
table.insert(points, {x = pos.x, y = pos.z})
end
local hull = Spearhead.Util.getConvexHull(points)
local enlargedHull = Spearhead.Util.enlargeConvexHull(hull, 30) -- Enlarge the hull by 50 meters
local randomGroup = Spearhead.Util.randomFromList(fromGroups) --[[@as SpearheadGroup]]
if randomGroup then
local randomUnit = Spearhead.Util.randomFromList(randomGroup:GetObjects()) --[[@as Object]]
local pos = randomUnit:getPoint()
---@type Vec2
local vec2 = {x = pos.x, y = pos.z}
local shootPoints = Spearhead.Util.GetVisibleHullPointsFromOrigin(enlargedHull, vec2)
for _, point in pairs(shootPoints) do
table.insert(result, point)
end
end
end
return result
end
---@private
---@param groups Array<SpearheadGroup>
---@param targetGroups Array<SpearheadGroup>
function BattleManager:LetUnitsShoot(groups, targetGroups)
local shootChance = math.random(3, 7) / 10
for _, group in pairs(groups) do
local units = group:GetAsUnits()
for _, unit in pairs(units) do
if self:IsUnitApplicable(unit) == true then
if math.random() <= shootChance then
local unitPos = unit:getPoint()
local point = self:GetRandomPoint({x = unitPos.x, y = unitPos.z }, targetGroups)
if point then
local qty = 1
local ammo = unit:getAmmo()
if ammo and ammo[1] then
qty = math.ceil((ammo[1].count or 1) / 50)
end
local shootTask = {
id = "FireAtPoint",
params = {
point = point,
radius = 1,
expendQty = qty,
expendQtyEnabled = true,
counterbattaryRadius = math.random(5, 10)
}
}
self._logger:debug("Red unit " .. unit:getName() .. " will shoot " .. qty .. " rounds at point: " .. tostring(point))
local controller = unit:getController()
if controller then
controller:setTask(shootTask)
end
end
end
end
end
end
end
---@private
---@param unit Unit
---@return boolean
function BattleManager:IsUnitApplicable(unit)
if not unit or not unit:isExist() then
return false
end
if
unit:hasAttribute("AAA") == true
or unit:hasAttribute("Air Defence") == true
or unit:hasAttribute("Mobile AAA") == true
then
return false
end
return true
end
---@private
---@param origin Vec2
---@param groups Array<SpearheadGroup>
---@return Vec2?
function BattleManager:GetRandomPoint(origin, groups)
local group = Spearhead.Util.randomFromList(groups) --[[@as SpearheadGroup]]
if not group then return nil end
local points = {}
local groupUnits = group:GetAsUnits()
for _, unit in pairs(group:GetAsUnits()) do
local pos = unit:getPoint()
table.insert(points, {x = pos.x, y = pos.z})
end
local hull = Spearhead.Util.getConvexHull(points)
local enlargedHull = Spearhead.Util.enlargeConvexHull(hull, 30)
local shootPoints = Spearhead.Util.GetVisibleHullPointsFromOrigin(enlargedHull, origin)
return Spearhead.Util.randomFromList(shootPoints) --[[@as Vec2]]
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
Spearhead.classes.stageClasses.helpers.BattleManager = BattleManager
@@ -1,3 +1,4 @@
---@class MissionCommandsHelper
---@field missionsByCode table<string, Mission> @table of missions by their code
---@field enabledByCode table<string, boolean> @table of enabled missions by their code
@@ -31,6 +32,7 @@ function MissionCommandsHelper.getOrCreate(logLevel)
instance.pinnedByGroup = {}
instance.lastUpdate = 0
instance._supplyHubGroups = {}
instance._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logLevel)
---comment
@@ -1,3 +1,6 @@
env.info("Spearhead SupplyUnitsTracker loaded")
---@class SupplyUnitsTracker
---@field private _supplyUnitsByName table<string, Unit>
---@field private _cargoInUnits table<string, table<CrateType, number>>
@@ -404,7 +407,6 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit)
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+44 -12
View File
@@ -5,11 +5,13 @@
---@field private _dependencies table<string, boolean>
---@field private _completeAtIndex number
---@field private _parentStage Stage
---@field private _battleManager? BattleManager
local ZoneMission = {}
--- @class MissionGroups
--- @field hasTargets boolean
--- @field groups Array<SpearheadGroup>
--- @field redGroups Array<SpearheadGroup>
--- @field blueGroups Array<SpearheadGroup>
--- @field unitsAlive table<string, table<string, boolean>>
--- @field targetsAlive table<string, table<string, boolean>>
--- @field groupNamesPerunit table<string,string>
@@ -38,6 +40,7 @@ local function ParseZoneName(input)
local inputType = string.lower(split_name[2])
if inputType == "dead" then parsedType = "DEAD" end
if inputType == "strike" then parsedType = "STRIKE" end
if inputType == "cas" then parsedType = "CAS" end
if inputType == "bai" then parsedType = "BAI" end
if inputType == "sam" then parsedType = "SAM" end
@@ -91,7 +94,8 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage)
end
self._missionGroups = {
groups = {},
redGroups = {},
blueGroups = {},
unitsAlive = {},
targetsAlive = {},
hasTargets = false,
@@ -107,7 +111,7 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage)
end
local completeAtIndex = database:getMissionCompleteAt(zoneName)
if completeAtIndex == nil and self.missionType == "BAI" then
if completeAtIndex == nil and self.missionType == "BAI" or self.missionType == "CAS" then
self._completeAtIndex = 0.8
elseif completeAtIndex == nil then
self._completeAtIndex = 1
@@ -118,13 +122,24 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage)
self._logger:debug("Complete at index " .. self.zoneName .. ": " .. self._completeAtIndex)
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup
local groupNames = database:getGroupsForMissionZone(zoneName)
for _, groupName in pairs(groupNames) do
local missionData = database:getMissionDataForZone(zoneName)
if not missionData then return end
for _, groupName in pairs(missionData.BlueGroups) do
local spearheadGroup = SpearheadGroup.New(groupName)
table.insert(self._missionGroups.groups, spearheadGroup)
if spearheadGroup then
table.insert(self._missionGroups.blueGroups, spearheadGroup)
end
spearheadGroup:Destroy()
end
for _, groupName in pairs(missionData.RedGroups) do
local spearheadGroup = SpearheadGroup.New(groupName)
table.insert(self._missionGroups.redGroups, spearheadGroup)
local isGroupTarget = Spearhead.Util.startswith(string.lower(groupName), "tgt_")
for _, unit in pairs(spearheadGroup:GetUnits()) do
for _, unit in pairs(spearheadGroup:GetObjects()) do
local unitName = unit:getName()
local isUnitTarget = Spearhead.Util.startswith(string.lower(unitName), "tgt_")
@@ -148,10 +163,15 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage)
Spearhead.Events.addOnUnitLostEventListener(unitName, self)
end
Spearhead.DcsUtil.DestroyGroup(groupName)
spearheadGroup:Destroy()
end
self._logger:debug("Mission " .. self.name .. " group count: " .. Spearhead.Util.tableLength(groupNames))
if self.missionType == "CAS" then
self._battleManager = Spearhead.classes.stageClasses.helpers.BattleManager.New(self._missionGroups.redGroups, self._missionGroups.blueGroups, self.zoneName, self._logger.LogLevel)
end
self._logger:debug("Mission " .. self.name .. " group count: " .. Spearhead.Util.tableLength(missionData.RedGroups))
return self
end
@@ -291,10 +311,14 @@ function ZoneMission:UpdateState(checkHealth, messageIfDone)
self._state = "COMPLETED"
end
end
if self._state == "COMPLETED" and self._battleManager then
self._battleManager:Stop()
end
end
function ZoneMission:SpawnPersistedState()
for _, group in pairs(self._missionGroups.groups) do
for _, group in pairs(self._missionGroups.redGroups) do
group:Spawn()
end
end
@@ -303,7 +327,7 @@ end
function ZoneMission:SpawnInactive()
self._logger:info("PreActivating " .. self.name)
for _, group in pairs(self._missionGroups.groups) do
for _, group in pairs(self._missionGroups.redGroups) do
group:Spawn()
end
end
@@ -323,10 +347,18 @@ function ZoneMission:SpawnActive()
end
self._state = "ACTIVE"
for _, group in pairs(self._missionGroups.groups) do
for _, group in pairs(self._missionGroups.redGroups) do
group:Spawn()
end
for _, group in pairs(self._missionGroups.blueGroups) do
group:Spawn()
end
if self._battleManager then
self._battleManager:Start()
end
self._missionCommandsHelper:AddMissionToCommands(self)
self:StartCheckingContinuous()
@@ -154,6 +154,7 @@ do --aliases
--- @alias MissionType
--- | "nil"
--- | "STRIKE"
--- | "CAS"
--- | "BAI"
--- | "DEAD"
--- | "SAM"
+1
View File
@@ -37,6 +37,7 @@ assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyUnitsTracker.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\BattleManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()