1008 lines
44 KiB
Lua

--[[
Script: Moose_DynamicGroundBattle.lua
Written by: [F99th-TracerFacer]
Version: 1.0.3
Date: 11 November 2024
Updated: 12 November 2024
Description: This script creates a dynamic ground battle between Red and Blue coalitions
along a series of zones which can be arranged in a line or any other configuration creating a dynamic ground battle.
Capture Zone Behavior
- Zone Capture states: Captured, Guarded, Empty, Attacked, Neutral
- Zone Colors: Red, Blue, Green, Orange
Red: Captured by Red
Blue: Captured by Blue
Orange: Contested
Green: Empty
Spawning And Patrol Behavior:
- Infantry and armor groups for both sides spawn at random locations in their own zones.
- Each group then calculates the shortest distance to the nearest enemy zone and moves to that zone to patrol.
- Every ASSIGN_TASKS_SCHED seconds, the script will check the ZONE_CAPTURE states of all zones and assign tasks to groups accordingly.
- Any group NOT moving, will recieve orders to patrol the nearest enemy zone. Any unit already moving will be left alone.
- Any troops dropped off through CTLD in these zones will begin to obey these orders as well.
- Spawn frequency calculated based on the number of alive warehouses.
- Infantry can be disabled from moving patrols if desired.
- In the event of DCS assigning a ridiculous path to an object, simply stop the object and it will be reassigned a new patrol path next round.
Warehouse System & Spawn Frequencey Behavior:
1. Warehouses:
- Each side (Red and Blue) has a set of warehouses defined in the `redWarehouses` and `blueWarehouses` tables.
- The number of warehouses can be adjusted by adding or removing entries from these tables and ensuring there is a matching object in the mission editor.
2. Spawn Frequency Calculation:
- The function `CalculateSpawnFrequency` calculates the spawn frequency based on the number of alive warehouses.
- The spawn frequency is a ratio of alive warehouses to total warehouses.
- If all warehouses are alive, the spawn frequency is (100%) of the base setting.
- If half of the warehouses are alive, the spawn frequency is (50%).
- If no warehouses are alive, the spawn frequency is (0%) (no more spawns).
- So for example, if you set your spawn frequency to 300 seconds, and only 50% of your warehouses are alive, the actual spawn frequency will be 600 seconds.
- This dynamic adjustment ensures that the reinforcement rate is directly impacted by the number of operational warehouses.
3. Mark points are automatically added to the map for each warehouse.
- Include the warehouse name and a list of nearby ground units within a specified radius.
- Uppdated every `UPDATE_MARK_POINTS_SCHED` seconds.
- The maximum distance to search for units near a warehouse is defined by the `MAX_WAREHOUSE_UNIT_LIST_DISTANCE` variable.
- The mark points are displayed to all players on the map as a form of "intel" on the battlefield.
- Can be disabled by setting `ENABLE_WAREHOUSE_MARKERS` to false.
General Setup Requirements:
- The script relies on the MOOSE framework for DCS World. Ensure that the MOOSE framework is installed and running on the mission.
- Ensure that all groups and zones mentioned below are created in the mission editor. You can adjust the names of the groups and zones as needed or
add more groups and zones to the script. Ensure that the names in this script match the names in the mission editor.
Groups and Zones to be created in the editor (all LATE ACTIVATE):
- Red Infantry Groups: RedInfantry1, RedInfantry2, RedInfantry3, RedInfantry4, RedInfantry5, RedInfantry6
- Red Armor Groups: RedArmor1, RedArmor2, RedArmor3, RedArmor4, RedArmor5, RedArmor6
- Blue Infantry Groups: BlueInfantry1, BlueInfantry2, BlueInfantry3, BlueInfantry4, BlueInfantry5, BlueInfantry6
- Blue Armor Groups: BlueArmor1, BlueArmor2, BlueArmor3, BlueArmor4, BlueArmor5
- Red Zones: FrontLine1, FrontLine2, FrontLine3, FrontLine4, FrontLine5, FrontLine6
- Blue Zones: FrontLine7, FrontLine8, FrontLine9, FrontLine10, FrontLine11, FrontLine12
- Red Warehouses: RedWarehouse1-1, RedWarehouse2-1, RedWarehouse3-1, RedWarehouse4-1, RedWarehouse5-1, RedWarehouse6-1
- Blue Warehouses: BlueWarehouse1-1, BlueWarehouse2-1, BlueWarehouse3-1, BlueWarehouse4-1, BlueWarehouse5-1, BlueWarehouse6-1
- ** Note Warehouse names are based on the static "unit name" in the mission editor. **
--]]
--[[
--If you don't have command centers setup in another file, uncommnent this section below:
-- Create Command Centers and Missions for each side
-- Must have a blue unit named "BLUEHQ" and a red unit named "REDHQ" in the mission editor.
--Build Command Center and Mission for Blue
US_CC = COMMANDCENTER:New( GROUP:FindByName( "BLUEHQ" ), "USA HQ" )
US_Mission = MISSION:New( US_CC, "Insurgent Sandstorm", "Primary", "Clear the front lines of enemy activity.", coalition.side.BLUE)
US_Score = SCORING:New( "Insurgent Sandstorm - Blue" )
US_Mission:AddScoring( US_Score )
US_Mission:Start()
US_Score:SetMessagesHit(false)
US_Score:SetMessagesDestroy(false)
US_Score:SetMessagesScore(false)
--Build Command Center and Mission Red
RU_CC = COMMANDCENTER:New( GROUP:FindByName( "REDHQ" ), "Russia HQ" )
RU_Mission = MISSION:New (RU_CC, "Insurgent Sandstorm", "Primary", "Destroy U.S. and NATO forces.", coalition.side.RED)
RU_Score = SCORING:New("Insurgent Sandstorm - Red")
RU_Mission:AddScoring( RU_Score)
RU_Mission:Start()
RU_Score:SetMessagesHit(false)
RU_Score:SetMessagesDestroy(false)
RU_Score:SetMessagesScore(false)
]]
-- Infantry Patrol Settings
-- Due to some maps or locations where infantry moving is either not desired or has problems with the terrain you can disable infantry moving patrols.
-- Set to false, infantry units will spawn, and never move from their spawn location. This could be considered a defensive position and probably a good idea.
local ENABLE_CAPTURE_ZONE_MESSAGES = false -- Enable or disable attack messages when a zone is attacked.
local MOVING_INFANTRY_PATROLS = false
local ENABLE_WAREHOUSE_MARKERS = true -- Enable or disable the warehouse markers on the map.
local UPDATE_MARK_POINTS_SCHED = 60 -- Update the map markers for warehouses every 300 seconds. ENABLE_WAREHOUSE_MARKERS must be set to true for this to work.
-- Control Spawn frequency and limits of ground units.
local INIT_RED_INFANTRY = 5 -- Initial number of Red Infantry groups
local MAX_RED_INFANTRY = 25 -- Maximum number of Red Infantry groups
local SPAWN_SCHED_RED_INFANTRY = 1800 -- Spawn Red Infantry groups every 1800 seconds
local INIT_RED_ARMOR = 15 -- Initial number of Red Armor groups
local MAX_RED_ARMOR = 200 -- Maximum number of Red Armor groups
local SPAWN_SCHED_RED_ARMOR = 600 -- Spawn Red Armor groups every 300 seconds
local INIT_BLUE_INFANTRY = 5 -- Initial number of Blue Infantry groups
local MAX_BLUE_INFANTRY = 25 -- Maximum number of Blue Infantry groups
local SPAWN_SCHED_BLUE_INFANTRY = 1800 -- Spawn Blue Infantry groups every 1800 seconds
local INIT_BLUE_ARMOR = 15 -- Initial number of Blue Armor groups0
local MAX_BLUE_ARMOR = 200 -- Maximum number of Blue Armor groups
local SPAWN_SCHED_BLUE_ARMOR = 60 -- Spawn Blue Armor groups every 300 seconds
local ASSIGN_TASKS_SCHED = 900 -- Assign tasks to groups every 600 seconds. New groups added will wait this long before moving.
-- Define capture zones for each side with a visible radius.
-- These zones will be used to create capture zones for each side. The capture zones will be used to determine the state of each zone (captured, guarded, empty, attacked, neutral).
-- The zones will also be used to spawn ground units for each side.
-- The zones should be created in the mission editor and named accordingly.
-- You can add more zones as needed. The script will create capture zones for each zone and assign tasks to groups based on the zone states.
-- Maybe the zones are along a front line, or they follow a road, or they are scattered around the map. You can arrange the zones in any configuration you like.
local redZones = {
ZONE:New("FrontLine1"),
ZONE:New("FrontLine2"),
ZONE:New("FrontLine3"),
ZONE:New("FrontLine4"),
ZONE:New("FrontLine5"),
ZONE:New("FrontLine6"),
ZONE:New("FrontLine7"),
ZONE:New("FrontLine8")
}
local blueZones = {
ZONE:New("FrontLine9"),
ZONE:New("FrontLine10"),
ZONE:New("FrontLine11"),
ZONE:New("FrontLine12"),
ZONE:New("FrontLine13"),
ZONE:New("FrontLine14"),
ZONE:New("FrontLine15"),
ZONE:New("FrontLine16")
}
-- Define warehouses for each side. These warehouses will be used to calculate the spawn frequency of ground units.
-- The warehouses should be created in the mission editor and named accordingly.
local redWarehouses = {
STATIC:FindByName("RedWarehouse1-1"), -- Static units key of off unit name in mission editor rather than just the name field. weird. =\ (hours wasted! ha!)
STATIC:FindByName("RedWarehouse2-1"),
STATIC:FindByName("RedWarehouse3-1"),
STATIC:FindByName("RedWarehouse4-1"),
STATIC:FindByName("RedWarehouse5-1"),
STATIC:FindByName("RedWarehouse6-1")
}
local blueWarehouses = {
STATIC:FindByName("BlueWarehouse1-1"),
STATIC:FindByName("BlueWarehouse2-1"),
STATIC:FindByName("BlueWarehouse3-1"),
STATIC:FindByName("BlueWarehouse4-1"),
STATIC:FindByName("BlueWarehouse5-1"),
STATIC:FindByName("BlueWarehouse6-1")
}
-- Define templates for infantry and armor groups. These templates will be used to randomize the groups spawned in the zones.
-- The templates should be created in the mission editor and named accordingly.
-- You can add more templates as needed. The script will randomly select a template for each group spawned.
-- The more templates you make, the more variety you can add to the groups that are spawned.
local redInfantryTemplates = {
"RedInfantry1",
"RedInfantry2",
"RedInfantry3",
"RedInfantry4",
"RedInfantry5",
"RedInfantry6"
}
local redArmorTemplates = {
"RedArmor1",
"RedArmor2",
"RedArmor3",
"RedArmor4",
"RedArmor5",
"RedArmor6",
"RedArmor7",
"RedArmor8",
"RedArmor9",
"RedArmor10"
}
local blueInfantryTemplates = {
"BlueInfantry1",
"BlueInfantry2",
"BlueInfantry3",
"BlueInfantry4",
"BlueInfantry5",
"BlueInfantry6"
}
local blueArmorTemplates = {
"BlueArmor1",
"BlueArmor2",
"BlueArmor3",
"BlueArmor4",
"BlueArmor5"
}
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DO NOT EDIT BELOW THIS LINE
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Function to handle warehouse destruction
local function onWarehouseDestroyed(warehouseName, coalition)
local message = string.format("%s warehouse %s has been destroyed!", coalition, warehouseName)
MESSAGE:New(message, 15):ToAll()
USERSOUND:New("beeps-and-clicks.wav"):ToAll()
env.info(message)
end
-- Create an event handler class
local WarehouseEventHandler = EVENTHANDLER:New()
-- Define the event handler function
function WarehouseEventHandler:OnEventDead(EventData)
env.info("OnEventDead triggered")
if EventData then
local unitName = EventData.IniUnitName or EventData.IniDCSUnitName
if unitName then
-- Check red warehouses
for _, warehouse in ipairs(redWarehouses) do
if warehouse:GetName() == unitName then
onWarehouseDestroyed(unitName, "Red")
return
end
end
-- Check blue warehouses
for _, warehouse in ipairs(blueWarehouses) do
if warehouse:GetName() == unitName then
onWarehouseDestroyed(unitName, "Blue")
return
end
end
local notWarehouseMessage = "Destroyed unit is not a warehouse: " .. unitName
env.info(notWarehouseMessage)
else
local noUnitMessage = "No unit name available in EventData"
env.info(noUnitMessage)
end
else
env.info("EventData is nil")
end
end
-- Set up the event handler globally
WarehouseEventHandler:HandleEvent(EVENTS.Dead)
-- Function to add mark points on the map for each warehouse in the provided list
local function addMarkPoints(warehouses, coalition)
for _, warehouse in ipairs(warehouses) do
if warehouse then
local warehousePos = warehouse:GetVec3()
local details
if warehouse:IsAlive() then
if coalition == 2 then
if warehouse:GetCoalition() == 2 then
details = "Warehouse: " .. warehouse:GetName() .. "\nThis warehouse needs to be protected.\n"
else
details = "Warehouse: " .. warehouse:GetName() .. "\nThis is a primary target as it is directly supplying enemy units.\n"
end
elseif coalition == 1 then
if warehouse:GetCoalition() == 1 then
details = "Warehouse: " .. warehouse:GetName() .. "\nThis warehouse needs to be protected.\nNearby Units:\n"
else
details = "Warehouse: " .. warehouse:GetName() .. "\nThis is a primary target as it is directly supplying enemy units.\n"
end
end
else
details = "Warehouse: " .. warehouse:GetName() .. "\nTHIS TARGET HAS BEEN DESTROYED"
end
local coordinate = COORDINATE:NewFromVec3(warehousePos)
local marker = MARKER:New(coordinate, details):ToCoalition(coalition):ReadOnly()
marker:Remove(UPDATE_MARK_POINTS_SCHED)
else
env.info("addMarkPoints: Warehouse not found or is nil")
end
end
end
local function updateMarkPoints()
addMarkPoints(redWarehouses, 2) -- Blue coalition sees red warehouses as targets
addMarkPoints(blueWarehouses, 2) -- Blue coalition sees blue warehouses as needing protection
addMarkPoints(redWarehouses, 1) -- Red coalition sees red warehouses as needing protection
addMarkPoints(blueWarehouses, 1) -- Red coalition sees blue warehouses as targets
end
-- If enabled, update the mark points for the warehouses every UPDATE_MARK_POINTS_SCHED seconds.
if ENABLE_WAREHOUSE_MARKERS then
SCHEDULER:New(nil, updateMarkPoints, {}, 10, UPDATE_MARK_POINTS_SCHED)
end
-- Table to keep track of zones and their statuses
local zoneStatuses = {}
-- Function to create a capture zone
local function CreateCaptureZone(zone, coalition)
local captureZone = ZONE_CAPTURE_COALITION:New(zone, coalition)
if captureZone then
local coordinate = captureZone:GetCoordinate()
if coordinate then
env.info("Created capture zone at coordinates: " .. coordinate:ToStringLLDMS())
captureZone:Start(5, 30) -- Check every 5 seconds, capture after 30 seconds
else
env.error("Failed to get coordinates for zone: " .. zone:GetName() .. " Did you add the group to the editor?")
end
else
env.error("Failed to create capture zone for zone: " .. zone:GetName() .. " Did you add the group to the editor?")
end
return captureZone
end
-- Custom OnEnterCaptured method
--- @param Functional.Protect#ZONE_CAPTURE_COALITION self
function ZONE_CAPTURE_COALITION:OnEnterCaptured(From, Event, To)
if From ~= To then
local Coalition = self:GetCoalition()
self:E({ Coalition = Coalition })
local zoneName = self:GetZoneName()
zoneStatuses[zoneName] = { zone = self, coalition = Coalition }
if Coalition == coalition.side.BLUE then
self:Smoke(SMOKECOLOR.Blue)
self:UndrawZone()
self:DrawZone(-1, {0, 0, 1}, 2) -- Draw the zone on the map for 30 seconds, blue color, and thickness 2
--US_CC:MessageTypeToCoalition(string.format("%s has been captured by the USA", self:GetZoneName()), MESSAGE.Type.Information)
--RU_CC:MessageTypeToCoalition(string.format("%s has been captured by the USA", self:GetZoneName()), MESSAGE.Type.Information)
else
self:Smoke(SMOKECOLOR.Red)
self:UndrawZone()
self:DrawZone(-1, {1, 0, 0}, 2) -- Draw the zone on the map for 30 seconds, red color, and thickness 2
--RU_CC:MessageTypeToCoalition(string.format("%s has been captured by Russia", self:GetZoneName()), MESSAGE.Type.Information)
--US_CC:MessageTypeToCoalition(string.format("%s has been captured by Russia", self:GetZoneName()), MESSAGE.Type.Information)
end
end
end
-- Custom OnEnterGuarded method
--- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self
function ZONE_CAPTURE_COALITION:OnEnterGuarded(From, Event, To)
if From ~= To then
local Coalition = self:GetCoalition()
self:E({ Coalition = Coalition })
local zoneName = self:GetZoneName()
zoneStatuses[zoneName] = { zone = self, coalition = Coalition }
if Coalition == coalition.side.BLUE then
self:Smoke(SMOKECOLOR.Blue)
-- Draw zone DARK BLUE for guarded
self:UndrawZone()
self:DrawZone(-1, {0, 0, 0.5}, 2) -- Draw the zone on the map for 30 seconds, dark blue color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--US_CC:MessageTypeToCoalition(string.format("%s is under protection of the USA", self:GetZoneName()), MESSAGE.Type.Information)
--RU_CC:MessageTypeToCoalition(string.format("%s is under protection of the USA", self:GetZoneName()), MESSAGE.Type.Information)
end
else
self:Smoke(SMOKECOLOR.Red)
-- Draw zone DARK RED for guarded
self:UndrawZone()
self:DrawZone(-1, {0.5, 0, 0}, 2) -- Draw the zone on the map for 30 seconds, dark red color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--RU_CC:MessageTypeToCoalition(string.format("%s is under protection of Russia", self:GetZoneName()), MESSAGE.Type.Information)
--US_CC:MessageTypeToCoalition(string.format("%s is under protection of Russia", self:GetZoneName()), MESSAGE.Type.Information)
end
end
end
end
-- Custom OnEnterEmpty method
--- @param Functional.Protect#ZONE_CAPTURE_COALITION self
function ZONE_CAPTURE_COALITION:OnEnterEmpty(From, Event, To)
if From ~= To then
self:E({ Coalition = "None" })
local zoneName = self:GetZoneName()
zoneStatuses[zoneName] = { zone = self, coalition = "None" }
self:Smoke(SMOKECOLOR.Green)
self:UndrawZone()
self:DrawZone(-1, {0, 1, 0}, 2) -- Draw the zone on the map for 30 seconds, green color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--US_CC:MessageTypeToCoalition(string.format("%s is now empty", self:GetZoneName()), MESSAGE.Type.Information)
--RU_CC:MessageTypeToCoalition(string.format("%s is now empty", self:GetZoneName()), MESSAGE.Type.Information)
end
end
end
-- Custom OnEnterAttacked method
--- @param Functional.Protect#ZONE_CAPTURE_COALITION self
function ZONE_CAPTURE_COALITION:OnEnterAttacked(From, Event, To)
if From ~= To then
local Coalition = self:GetCoalition()
self:E({ Coalition = Coalition })
local zoneName = self:GetZoneName()
zoneStatuses[zoneName] = { zone = self, coalition = Coalition }
if Coalition == coalition.side.BLUE then
self:Smoke(SMOKECOLOR.Blue)
-- Draw the zone orange for contested
self:UndrawZone()
self:DrawZone(-1, {1, 0.5, 0}, 2) -- Draw the zone on the map for 30 seconds, orange color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--US_CC:MessageTypeToCoalition(string.format("%s is under attack by Russia", self:GetZoneName()), MESSAGE.Type.Information)
--RU_CC:MessageTypeToCoalition(string.format("%s is attacking the USA", self:GetZoneName()), MESSAGE.Type.Information)
end
else
self:Smoke(SMOKECOLOR.Red)
self:UndrawZone()
self:DrawZone(-1, {1, 0.5, 0}, 2) -- Draw the zone on the map for 30 seconds, orange color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--RU_CC:MessageTypeToCoalition(string.format("%s is under attack by the USA", self:GetZoneName()), MESSAGE.Type.Information)
--US_CC:MessageTypeToCoalition(string.format("%s is attacking Russia", self:GetZoneName()), MESSAGE.Type.Information)
end
end
end
end
-- Custom OnEnterNeutral method
--- @param Functional.Protect#ZONE_CAPTURE_COALITION self
function ZONE_CAPTURE_COALITION:OnEnterNeutral(From, Event, To)
if From ~= To then
self:E({ Coalition = "Neutral" })
local zoneName = self:GetZoneName()
zoneStatuses[zoneName] = { zone = self, coalition = "Neutral" }
self:Smoke(SMOKECOLOR.Green)
self:UndrawZone()
self:DrawZone(-1, {0, 1, 0}, 2) -- Draw the zone on the map for 30 seconds, green color, and thickness 2
if ENABLE_CAPTURE_ZONE_MESSAGES then
--US_CC:MessageTypeToCoalition(string.format("%s is now neutral", self:GetZoneName()), MESSAGE.Type.Information)
--RU_CC:MessageTypeToCoalition(string.format("%s is now neutral", self:GetZoneName()), MESSAGE.Type.Information)
end
end
end
-- Create capture zones for Red and Blue
local redCaptureZones = {}
local blueCaptureZones = {}
-- Iterate over all red zones to create capture zones
for _, zone in ipairs(redZones) do
-- Attempt to create a capture zone for the current red zone
local captureZone = CreateCaptureZone(zone, coalition.side.RED)
if captureZone then
-- If successful, add the capture zone to the redCaptureZones table
table.insert(redCaptureZones, captureZone)
-- Log the creation of the capture zone
env.info("Created Red capture zone: " .. zone:GetName())
-- Draw the zone on the map with infinite duration, red color, and thickness 2
zone:DrawZone(30, {1, 0, 0}, 2)
-- Initialize the zone status
zoneStatuses[zone:GetName()] = { zone = captureZone, coalition = coalition.side.RED }
else
-- If creation fails, log an error message
env.error("Failed to create Red capture zone: " .. zone:GetName())
end
end
-- Iterate over all blue zones to create capture zones
for _, zone in ipairs(blueZones) do
-- Attempt to create a capture zone for the current blue zone
local captureZone = CreateCaptureZone(zone, coalition.side.BLUE)
if captureZone then
-- If successful, add the capture zone to the blueCaptureZones table
table.insert(blueCaptureZones, captureZone)
-- Log the creation of the capture zone
env.info("Created Blue capture zone: " .. zone:GetName())
-- Draw the zone on the map with infinite duration, blue color, and thickness 2
zone:DrawZone(30, {0, 0, 1}, 2)
-- Initialize the zone status
zoneStatuses[zone:GetName()] = { zone = captureZone, coalition = coalition.side.BLUE }
else
-- If creation fails, log an error message
env.error("Failed to create Blue capture zone: " .. zone:GetName())
end
end
-- Function to handle zone capture
local function OnZoneCaptured(event)
local zone = event.zone
local coalition = event.coalition
if zone and coalition then
env.info("OnZoneCaptured: Zone " .. zone:GetName() .. " captured by coalition " .. coalition)
-- Update the zone state
if coalition == coalition.side.RED then
zoneStates[zone:GetName()] = "RED"
zone:SetCoalition(coalition.side.RED)
elseif coalition == coalition.side.BLUE then
zoneStates[zone:GetName()] = "BLUE"
zone:SetCoalition(coalition.side.BLUE)
else
zoneStates[zone:GetName()] = "NEUTRAL"
zone:SetCoalition(coalition.side.NEUTRAL)
end
else
env.error("OnZoneCaptured: Invalid zone or coalition")
end
end
-- Function to handle zone guarded events
local function OnZoneGuarded(event)
local zone = event.Zone
local coalition = event.Coalition
if coalition == coalition.side.RED then
env.info("Red is guarding zone: " .. zone:GetName())
elseif coalition == coalition.side.BLUE then
env.info("Blue is guarding zone: " .. zone:GetName())
end
end
-- Function to handle zone empty events
local function OnZoneEmpty(event)
local zone = event.Zone
env.info("Zone is empty: " .. zone:GetName())
end
-- Function to handle zone attacked events
local function OnZoneAttacked(event)
local zone = event.Zone
local attackingGroups = zone:GetGroups()
local makeup = {}
for _, group in ipairs(attackingGroups) do
local groupName = group:GetName()
local unitTypes = {}
for _, unit in ipairs(group:GetUnits()) do
local unitType = unit:GetTypeName()
unitTypes[unitType] = (unitTypes[unitType] or 0) + 1
end
table.insert(makeup, {groupName = groupName, unitTypes = unitTypes})
end
local makeupMessage = ""
for _, groupInfo in ipairs(makeup) do
makeupMessage = makeupMessage .. "Group: " .. groupInfo.groupName .. "\n"
for unitType, count in pairs(groupInfo.unitTypes) do
makeupMessage = makeupMessage .. " " .. unitType .. ": " .. count .. "\n"
end
end
local messageText = "Zone is being attacked: " .. zone:GetName() .. "\n" .. makeupMessage
env.info(messageText)
-- Announce to the player
if ENABLE_CAPTURE_ZONE_MESSAGES then
MESSAGE:New(messageText, 15):ToAll()
end
end
-- Function to handle zone neutral events
local function OnZoneNeutral(event)
local zone = event.Zone
env.info("Zone is neutral: " .. zone:GetName())
end
-- Function to check the ZONE_CAPTURE states of all zones
local function CheckZoneStates()
env.info("Checking zone states...")
local zoneStates = {}
local function processZones(zones, zoneType)
env.info("Processing " .. zoneType)
env.info("Number of zones: " .. #zones)
local allGroups = SET_GROUP:New():FilterActive():FilterStart()
for _, zone in ipairs(zones) do
if zone then
env.info("processZones: Zone object is valid")
-- Check if the zone is of the correct type
if zone.ClassName == "ZONE_CAPTURE_COALITION" then
env.info("processZones: Zone is of type ZONE_CAPTURE_COALITION")
local coalition = zone:GetCoalition()
env.info("processZones: Zone coalition: " .. tostring(coalition))
if coalition == 1 then
zoneStates[zone:GetZoneName()] = "RED"
env.info("processZones: Zone: " .. (zone:GetZoneName() or "nil") .. " State: RED")
elseif coalition == 2 then
zoneStates[zone:GetZoneName()] = "BLUE"
env.info("processZones: Zone: " .. (zone:GetZoneName() or "nil") .. " State: BLUE")
else
zoneStates[zone:GetZoneName()] = "NEUTRAL"
env.info("processZones: Zone: " .. (zone:GetZoneName() or "nil") .. " State: NEUTRAL")
end
local groupsInZone = {}
allGroups:ForEachGroup(function(group)
if group then
env.info("processZones: Checking group: " .. group:GetName())
if group.IsCompletelyInZone then
if group:IsCompletelyInZone(zone) then
table.insert(groupsInZone, group)
end
else
env.error("processZones: IsCompletelyInZone method not found in group: " .. group:GetName())
-- Log available methods on the group object
for k, v in pairs(group) do
env.info("processZones: Group method: " .. tostring(k) .. " = " .. tostring(v))
end
end
else
env.error("processZones: Invalid group")
end
end)
env.info("processZones: Number of groups in zone: " .. #groupsInZone)
else
env.error("processZones: Zone is not of type ZONE_CAPTURE_COALITION")
-- Log available methods on the zone object
for k, v in pairs(zone) do
env.info("processZones: Zone method: " .. tostring(k) .. " = " .. tostring(v))
end
end
if not zone.GetZoneName then
env.error("processZones: Missing GetZoneName method in " .. zoneType)
end
else
env.error("processZones: Invalid zone in " .. zoneType)
end
end
end
processZones(redCaptureZones, "redZones")
processZones(blueCaptureZones, "blueZones")
-- Log the zoneStates table
for zoneName, state in pairs(zoneStates) do
env.info("CheckZoneStates: Zone: " .. zoneName .. " State: " .. state)
end
return zoneStates
end
-- Function to assign tasks to groups
local function AssignTasks(group, zoneStates)
if not group or not group.GetCoalition or not group.GetCoordinate or not group.GetVelocity then
env.info("AssignTasks: Invalid group or missing methods")
return
end
local velocity = group:GetVelocityVec3()
local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
if speed > 0 then
env.info("AssignTasks: Group " .. group:GetName() .. " is already moving. No new orders sent.")
return
end
env.info("Assigning tasks to group: " .. group:GetName())
local groupCoalition = group:GetCoalition()
local groupCoordinate = group:GetCoordinate()
local closestZone = nil
local closestDistance = math.huge
env.info("Group Coalition: " .. tostring(groupCoalition))
env.info("Group Coordinate: " .. groupCoordinate:ToStringLLDMS())
for zoneName, state in pairs(zoneStates) do
env.info("Checking Zone: " .. zoneName .. " with state: " .. tostring(state))
-- Convert state to a number for comparison
local stateCoalition = (state == "RED" and 1) or (state == "BLUE" and 2) or nil
if stateCoalition and stateCoalition ~= groupCoalition then
local zone = ZONE:FindByName(zoneName)
if zone then
local zoneCoordinate = zone:GetCoordinate()
local distance = groupCoordinate:Get2DDistance(zoneCoordinate)
--env.info("Zone Coordinate: " .. zoneCoordinate:ToStringLLDMS())
--env.info("Distance to zone " .. zoneName .. ": " .. distance)
if distance < closestDistance then
closestDistance = distance
closestZone = zone
env.info("New closest zone: " .. zoneName .. " with distance: " .. distance)
end
else
env.info("AssignTasks: Zone not found - " .. zoneName)
end
else
env.info("Zone " .. zoneName .. " is already controlled by coalition: " .. tostring(state))
end
end
if closestZone then
env.info(group:GetName() .. " is moving to and patrolling zone " .. closestZone:GetName())
--MESSAGE:New(group:GetName() .. " is moving to and patrolling zone " .. closestZone:GetName(), 10):ToAll()
-- Create a patrol task using the GROUP:PatrolZones method
local patrolZones = {closestZone}
local speed = 20 -- Example speed, adjust as needed
local formation = "Cone" -- Example formation, adjust as needed
local delayMin = 30 -- Example minimum delay, adjust as needed
local delayMax = 60 -- Example maximum delay, adjust as needed
group:PatrolZones(patrolZones, speed, formation, delayMin, delayMax)
else
env.info("AssignTasks: No suitable zone found for group " .. group:GetName())
end
end
-- Function to check if a group contains infantry units
local function IsInfantryGroup(group)
env.info("IsInfantryGroup: Checking group: " .. group:GetName())
for _, unit in ipairs(group:GetUnits()) do
local unitTypeName = unit:GetTypeName()
env.info("IsInfantryGroup: Checking unit: " .. unit:GetName() .. " with type: " .. unitTypeName)
if unitTypeName:find("Infantry") or unitTypeName:find("Soldier") or unitTypeName:find("Paratrooper") then
env.info("IsInfantryGroup: Found infantry unit in group: " .. group:GetName())
return true
end
end
return false
end
-- Function to assign tasks to groups
local function AssignTasksToGroups()
env.info("AssignTasksToGroups: Starting task assignments")
local zoneStates = CheckZoneStates()
local allGroups = SET_GROUP:New():FilterActive():FilterStart()
local function processZone(zone, zoneColor)
if zone then
env.info("AssignTasksToGroups: Processing " .. zoneColor .. " zone: " .. zone:GetName())
local groupsInZone = {}
allGroups:ForEachGroup(function(group)
if group then
if group.IsCompletelyInZone then
if group:IsCompletelyInZone(zone) then
table.insert(groupsInZone, group)
end
else
env.error("AssignTasksToGroups: IsCompletelyInZone method not found in group: " .. group:GetName())
for k, v in pairs(group) do
env.info("AssignTasksToGroups: Group method: " .. tostring(k) .. " = " .. tostring(v))
end
end
else
env.error("AssignTasksToGroups: Invalid group")
end
end)
env.info("AssignTasksToGroups: Found " .. #groupsInZone .. " groups in " .. zoneColor .. " zone: " .. zone:GetName())
for _, group in ipairs(groupsInZone) do
if IsInfantryGroup(group) == true then
if MOVING_INFANTRY_PATROLS == true then
env.info("AssignTasksToGroups: Assigning tasks to infantry group: " .. group:GetName())
AssignTasks(group, zoneStates)
else
env.info("AssignTasksToGroups: Skipping infantry group: " .. group:GetName())
end
else
env.info("AssignTasksToGroups: Assigning tasks to group: " .. group:GetName())
AssignTasks(group, zoneStates)
end
end
else
env.info("AssignTasksToGroups: Invalid " .. zoneColor .. " zone")
end
end
for _, zone in ipairs(redZones) do
processZone(zone, "red")
end
for _, zone in ipairs(blueZones) do
processZone(zone, "blue")
end
env.info("AssignTasksToGroups: Task assignments completed. Running again in " .. ASSIGN_TASKS_SCHED .. " seconds.")
end
-- Function to calculate spawn frequency in seconds
local function CalculateSpawnFrequency(warehouses, baseFrequency)
local totalWarehouses = #warehouses
local aliveWarehouses = 0
for _, warehouse in ipairs(warehouses) do
local life = warehouse:GetLife()
if life and life > 0 then
aliveWarehouses = aliveWarehouses + 1
end
end
if totalWarehouses == 0 or aliveWarehouses == 0 then
return math.huge -- Stop spawning if there are no warehouses or no alive warehouses
end
local frequency = baseFrequency * (totalWarehouses / aliveWarehouses)
return frequency
end
local function CalculateSpawnFrequencyPercentage(warehouses)
local totalWarehouses = #warehouses
local aliveWarehouses = 0
for _, warehouse in ipairs(warehouses) do
local life = warehouse:GetLife()
if life and life > 0 then
aliveWarehouses = aliveWarehouses + 1
end
end
if totalWarehouses == 0 then
return 0 -- Avoid division by zero
end
local percentage = (aliveWarehouses / totalWarehouses) * 100
return math.floor(percentage)
end
-- Add event handlers for zone capture
for _, captureZone in ipairs(redCaptureZones) do
captureZone:OnEnterCaptured(OnZoneCaptured)
captureZone:OnEnterGuarded(captureZone.OnEnterGuarded)
captureZone:OnEnterEmpty(OnZoneEmpty)
captureZone:OnEnterAttacked(OnZoneAttacked)
captureZone:OnEnterNeutral(OnZoneNeutral)
end
for _, captureZone in ipairs(blueCaptureZones) do
captureZone:OnEnterCaptured(OnZoneCaptured)
captureZone:OnEnterGuarded(captureZone.OnEnterGuarded)
captureZone:OnEnterEmpty(OnZoneEmpty)
captureZone:OnEnterAttacked(OnZoneAttacked)
captureZone:OnEnterNeutral(OnZoneNeutral)
end
-- Calculate spawn frequencies
local redInfantrySpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_INFANTRY)
local redArmorSpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_ARMOR)
local blueInfantrySpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY)
local blueArmorSpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_ARMOR)
-- Calculate spawn frequency percentages
local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses, coalition.side.RED)
local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses, coalition.side.BLUE)
-- Display spawn frequency percentages to the user
MESSAGE:New("Red side spawn frequency: " .. redSpawnFrequencyPercentage .. "%", 30):ToRed()
MESSAGE:New("Blue side spawn frequency: " .. blueSpawnFrequencyPercentage .. "%", 30):ToBlue()
-- Schedule ground spawns using the calculated frequencies
redInfantrySpawn = SPAWN:New("RedInfantryGroup")
:InitRandomizeTemplate(redInfantryTemplates)
:InitRandomizeZones(redZones)
:InitLimit(INIT_RED_INFANTRY, MAX_RED_INFANTRY)
:SpawnScheduled(redInfantrySpawnFrequency, 0.5)
redArmorSpawn = SPAWN:New("RedArmorGroup")
:InitRandomizeTemplate(redArmorTemplates)
:InitRandomizeZones(redZones)
:InitLimit(INIT_RED_ARMOR, MAX_RED_ARMOR)
:SpawnScheduled(redArmorSpawnFrequency, 0.5)
blueInfantrySpawn = SPAWN:New("BlueInfantryGroup")
:InitRandomizeTemplate(blueInfantryTemplates)
:InitRandomizeZones(blueZones)
:InitLimit(INIT_BLUE_INFANTRY, MAX_BLUE_INFANTRY)
:SpawnScheduled(blueInfantrySpawnFrequency, 0.5)
blueArmorSpawn = SPAWN:New("BlueArmorGroup")
:InitRandomizeTemplate(blueArmorTemplates)
:InitRandomizeZones(blueZones)
:InitLimit(INIT_BLUE_ARMOR, MAX_BLUE_ARMOR)
:SpawnScheduled(blueArmorSpawnFrequency, 0.5)
env.info("Dynamic Ground Battle & Zone capture initialized.")
-- Function to monitor and announce warehouse status
local function MonitorWarehouses()
local blueWarehousesAlive = 0
local redWarehousesAlive = 0
for _, warehouse in ipairs(blueWarehouses) do
if warehouse:IsAlive() then
blueWarehousesAlive = blueWarehousesAlive + 1
end
end
for _, warehouse in ipairs(redWarehouses) do
if warehouse:IsAlive() then
redWarehousesAlive = redWarehousesAlive + 1
end
end
-- Debug messages to check values
env.info("MonitorWarehouses: blueWarehousesAlive = " .. blueWarehousesAlive)
env.info("MonitorWarehouses: redWarehousesAlive = " .. redWarehousesAlive)
-- Calculate spawn frequencies
local redInfantrySpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_INFANTRY)
local redArmorSpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_ARMOR)
local blueInfantrySpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY)
local blueArmorSpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_ARMOR)
-- Calculate spawn frequency percentages
local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses)
local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses)
-- Log the values
env.info("MonitorWarehouses: redInfantrySpawnFrequency = " .. redInfantrySpawnFrequency)
env.info("MonitorWarehouses: redArmorSpawnFrequency = " .. redArmorSpawnFrequency)
env.info("MonitorWarehouses: blueInfantrySpawnFrequency = " .. blueInfantrySpawnFrequency)
env.info("MonitorWarehouses: blueArmorSpawnFrequency = " .. blueArmorSpawnFrequency)
env.info("MonitorWarehouses: redSpawnFrequencyPercentage = " .. redSpawnFrequencyPercentage)
env.info("MonitorWarehouses: blueSpawnFrequencyPercentage = " .. blueSpawnFrequencyPercentage)
local msg = "[Warehouse status:]\n"
msg = msg .. "Red warehouses alive: " .. redWarehousesAlive .. " Reinforcements Capacity: " .. redSpawnFrequencyPercentage .. "%" .. "\n"
msg = msg .. "Blue warehouses alive: " .. blueWarehousesAlive .. " Reinforcements Capacity: " .. blueSpawnFrequencyPercentage .. "%" .. "\n"
MESSAGE:New(msg, 30):ToAll()
end
-- Function to check the wincondition. If either side owns all zones, mission ends.
local function checkWinCondition()
local blueOwned = true
local redOwned = true
for zoneName, owner in pairs(zoneStatuses) do
if owner ~= 1 then
redOwned = false
end
if owner ~= 2 then
blueOwned = false
end
end
if blueOwned then
MESSAGE:New("Blue side wins! They own all the capture zones.", 1800):ToAll()
USERSOUND:New("UsaTheme.ogg"):ToAll()
return true
elseif redOwned then
MESSAGE:New("Red side wins! They own all the capture zones.", 1800):ToAll()
USERSOUND:New("MotherRussia.ogg"):ToAll()
return true
end
return false
end
-- Function to toggle capture zone messages
local function ToggleCaptureZoneMessages()
ENABLE_CAPTURE_ZONE_MESSAGES = not ENABLE_CAPTURE_ZONE_MESSAGES
local status = ENABLE_CAPTURE_ZONE_MESSAGES and "enabled" or "disabled"
MESSAGE:New("Capture zone messages are now " .. status, 15):ToAll()
end
-- Timer function to periodically check the win condition
local function monitorWinCondition()
if not checkWinCondition() then
-- Schedule the next check in 60 seconds
TIMER:New(monitorWinCondition):Start(60)
end
end
-- Start monitoring the win condition
monitorWinCondition()
-- Scheduler to monitor warehouses every 120 seconds
SCHEDULER:New(nil, MonitorWarehouses, {}, 0, 120)
-- Scheduler to assign tasks to groups periodically
SCHEDULER:New(nil, AssignTasksToGroups, {}, 0, ASSIGN_TASKS_SCHED) -- Check every 600 seconds (10 minutes) - Adjust as needed
MENU_MISSION_COMMAND:New("Check Warehouse Status", missionMenu, MonitorWarehouses)
-- Add a menu item to toggle capture zone messages under the sub menu
MENU_MISSION_COMMAND:New("Toggle Capture Zone Messages", missionMenu, ToggleCaptureZoneMessages)