mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Stability and Design Improvements
Robust Spawner Initialization: I've added validation to ensure that spawners are only initialized if their required unit templates and warehouses are present in the mission. This prevents the script from attempting to run spawning logic that is destined to fail, making the system more resilient to mission editor configuration errors. Clearer Tasking Logic: The task assignment logic is now more explicit and follows a clear order of priorities for units: Defend Assigned Zone: A unit assigned as a defender will always patrol its home zone. Reinforce Contested Zone: If a zone is under attack, any available non-defenders will automatically assist in its defense. Fill Garrison: If a zone needs defenders, an available unit will be elected to become a permanent defender. Attack Enemy: If a unit has no defensive tasks, it will be assigned to patrol towards the nearest enemy-held zone.
This commit is contained in:
parent
924757919f
commit
f76d741588
Binary file not shown.
Binary file not shown.
@ -285,14 +285,11 @@ local zoneGarrisons = {}
|
|||||||
-- Structure: groupGarrisonAssignments[groupName] = zoneName (or nil if not a defender)
|
-- Structure: groupGarrisonAssignments[groupName] = zoneName (or nil if not a defender)
|
||||||
local groupGarrisonAssignments = {}
|
local groupGarrisonAssignments = {}
|
||||||
|
|
||||||
-- Reusable SET_GROUP to prevent memory leaks from repeated creation
|
-- Reusable SET_GROUP to prevent repeated creation within a single function call
|
||||||
local cachedAllGroups = nil
|
|
||||||
local function getAllGroups()
|
local function getAllGroups()
|
||||||
if not cachedAllGroups then
|
-- This must be called every time to get a fresh list of all groups, including newly spawned ones.
|
||||||
cachedAllGroups = SET_GROUP:New():FilterActive():FilterStart()
|
-- The :FilterActive() ensures we only work with groups that are currently alive.
|
||||||
env.info("[DGB PLUGIN] Created cached SET_GROUP for performance")
|
return SET_GROUP:New():FilterActive()
|
||||||
end
|
|
||||||
return cachedAllGroups
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Function to get zones controlled by a specific coalition
|
-- Function to get zones controlled by a specific coalition
|
||||||
@ -560,89 +557,7 @@ local function TryDefenderRotation(group, zone)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function AssignTasks(group, currentZoneCapture)
|
local function AssignTasks(group, currentZoneCapture)
|
||||||
if not group or not group.GetCoalition or not group.GetCoordinate or not group.GetVelocityVec3 then
|
-- This function is no longer needed as its logic has been integrated into AssignTasksToGroups
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- GARRISON SYSTEM: Defenders never leave their zone
|
|
||||||
if IsDefender(group) then
|
|
||||||
local assignedZoneName = groupGarrisonAssignments[group:GetName()]
|
|
||||||
if assignedZoneName then
|
|
||||||
-- Find the zone object
|
|
||||||
for idx, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
||||||
local zone = zoneCapture:GetZone()
|
|
||||||
if zone and zone:GetName() == assignedZoneName then
|
|
||||||
-- Keep patrolling home zone
|
|
||||||
group:PatrolZones({zone}, 20, "Cone", 30, 60)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- If we get here, the defender's zone was lost or not found, but they still stay put
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Don't reassign if already moving
|
|
||||||
local velocity = group:GetVelocityVec3()
|
|
||||||
local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
|
|
||||||
if speed > 0.5 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local groupCoalition = group:GetCoalition()
|
|
||||||
local groupCoordinate = group:GetCoordinate()
|
|
||||||
local currentZone = currentZoneCapture and currentZoneCapture:GetZone() or nil
|
|
||||||
|
|
||||||
-- GARRISON SYSTEM: Check if current zone needs defenders
|
|
||||||
if currentZoneCapture and currentZone and currentZoneCapture:GetCoalition() == groupCoalition then
|
|
||||||
local zoneName = currentZone:GetName()
|
|
||||||
|
|
||||||
-- Try to elect as defender if zone needs one
|
|
||||||
if ZoneNeedsDefenders(zoneName) then
|
|
||||||
if ElectDefender(group, currentZone, "zone under-garrisoned") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Try rotation if enabled
|
|
||||||
if TryDefenderRotation(group, currentZone) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If the zone is under attack, all units help defend (even non-defenders)
|
|
||||||
local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil
|
|
||||||
if zoneState == "Attacked" then
|
|
||||||
env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", group:GetName(), currentZone:GetName()))
|
|
||||||
group:PatrolZones({ currentZone }, 20, "Cone", 30, 60)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local closestZone = nil
|
|
||||||
local closestDistance = math.huge
|
|
||||||
|
|
||||||
-- Find nearest enemy zone
|
|
||||||
for idx, zoneCapture in ipairs(zoneCaptureObjects) do
|
|
||||||
local zoneCoalition = zoneCapture:GetCoalition()
|
|
||||||
|
|
||||||
if zoneCoalition ~= groupCoalition and zoneCoalition ~= coalition.side.NEUTRAL then
|
|
||||||
local zone = zoneCapture:GetZone()
|
|
||||||
if zone then
|
|
||||||
local zoneCoordinate = zone:GetCoordinate()
|
|
||||||
local distance = groupCoordinate:Get2DDistance(zoneCoordinate)
|
|
||||||
|
|
||||||
if distance < closestDistance then
|
|
||||||
closestDistance = distance
|
|
||||||
closestZone = zone
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if closestZone then
|
|
||||||
env.info(string.format("[DGB PLUGIN] %s patrolling %s", group:GetName(), closestZone:GetName()))
|
|
||||||
group:PatrolZones({closestZone}, 20, "Cone", 30, 60)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Function to assign tasks to all groups
|
-- Function to assign tasks to all groups
|
||||||
@ -651,43 +566,120 @@ local function AssignTasksToGroups()
|
|||||||
local allGroups = getAllGroups()
|
local allGroups = getAllGroups()
|
||||||
local tasksAssigned = 0
|
local tasksAssigned = 0
|
||||||
local defendersActive = 0
|
local defendersActive = 0
|
||||||
|
local mobileAssigned = 0
|
||||||
|
|
||||||
|
-- Create a quick lookup table for zone objects by name
|
||||||
|
local zoneLookup = {}
|
||||||
|
for _, zc in ipairs(zoneCaptureObjects) do
|
||||||
|
local zone = zc:GetZone()
|
||||||
|
if zone then
|
||||||
|
zoneLookup[zone:GetName()] = { zone = zone, capture = zc }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
allGroups:ForEachGroup(function(group)
|
allGroups:ForEachGroup(function(group)
|
||||||
if group and group:IsAlive() then
|
if not group or not group:IsAlive() then return end
|
||||||
-- Check if group is in a friendly zone
|
|
||||||
local groupCoalition = group:GetCoalition()
|
local groupName = group:GetName()
|
||||||
local inFriendlyZone = false
|
local groupCoalition = group:GetCoalition()
|
||||||
local currentZoneCapture = nil
|
|
||||||
|
-- 1. HANDLE DEFENDERS
|
||||||
for idx, zoneCapture in ipairs(zoneCaptureObjects) do
|
if IsDefender(group) then
|
||||||
if zoneCapture:GetCoalition() == groupCoalition then
|
defendersActive = defendersActive + 1
|
||||||
local zone = zoneCapture:GetZone()
|
local assignedZoneName = groupGarrisonAssignments[groupName]
|
||||||
if zone and group:IsCompletelyInZone(zone) then
|
local zoneInfo = zoneLookup[assignedZoneName]
|
||||||
inFriendlyZone = true
|
if zoneInfo then
|
||||||
currentZoneCapture = zoneCapture
|
-- Ensure defender patrols its assigned zone
|
||||||
break
|
group:PatrolZones({ zoneInfo.zone }, 20, "Cone", 30, 60)
|
||||||
|
tasksAssigned = tasksAssigned + 1
|
||||||
|
end
|
||||||
|
return -- Defenders do not get any other tasks
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. HANDLE MOBILE FORCES (NON-DEFENDERS)
|
||||||
|
|
||||||
|
-- Skip infantry if movement is disabled
|
||||||
|
if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't reassign if already moving
|
||||||
|
local velocity = group:GetVelocityVec3()
|
||||||
|
local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
|
||||||
|
if speed > 0.5 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find which zone the group is in
|
||||||
|
local currentZone = nil
|
||||||
|
local currentZoneCapture = nil
|
||||||
|
for _, zc in ipairs(zoneCaptureObjects) do
|
||||||
|
local zone = zc:GetZone()
|
||||||
|
if zone and group:IsCompletelyInZone(zone) then
|
||||||
|
currentZone = zone
|
||||||
|
currentZoneCapture = zc
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only assign tasks to groups inside a friendly zone
|
||||||
|
if not currentZone or currentZoneCapture:GetCoalition() ~= groupCoalition then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local zoneName = currentZone:GetName()
|
||||||
|
|
||||||
|
-- 3. DEFENDER ELECTION & ROTATION
|
||||||
|
-- Try to elect as defender if zone needs one
|
||||||
|
if ZoneNeedsDefenders(zoneName) then
|
||||||
|
if ElectDefender(group, currentZone, "zone under-garrisoned") then
|
||||||
|
tasksAssigned = tasksAssigned + 1
|
||||||
|
return -- Now a defender, task is set
|
||||||
|
end
|
||||||
|
elseif TryDefenderRotation(group, currentZone) then
|
||||||
|
tasksAssigned = tasksAssigned + 1
|
||||||
|
return -- Rotated in as a defender, task is set
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4. DEFEND CONTESTED ZONE
|
||||||
|
-- If the zone is under attack, all non-defenders should help defend it
|
||||||
|
local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil
|
||||||
|
if zoneState == "Attacked" then
|
||||||
|
env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", groupName, zoneName))
|
||||||
|
group:PatrolZones({ currentZone }, 20, "Cone", 30, 60)
|
||||||
|
tasksAssigned = tasksAssigned + 1
|
||||||
|
mobileAssigned = mobileAssigned + 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5. PATROL TO NEAREST ENEMY ZONE
|
||||||
|
local closestEnemyZone = nil
|
||||||
|
local closestDistance = math.huge
|
||||||
|
local groupCoordinate = group:GetCoordinate()
|
||||||
|
|
||||||
|
for _, zc in ipairs(zoneCaptureObjects) do
|
||||||
|
local zoneCoalition = zc:GetCoalition()
|
||||||
|
if zoneCoalition ~= groupCoalition and zoneCoalition ~= coalition.side.NEUTRAL then
|
||||||
|
local zone = zc:GetZone()
|
||||||
|
if zone then
|
||||||
|
local distance = groupCoordinate:Get2DDistance(zone:GetCoordinate())
|
||||||
|
if distance < closestDistance then
|
||||||
|
closestDistance = distance
|
||||||
|
closestEnemyZone = zone
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
if inFriendlyZone then
|
|
||||||
-- Skip infantry if movement is disabled (unless they're defenders)
|
if closestEnemyZone then
|
||||||
if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS and not IsDefender(group) then
|
env.info(string.format("[DGB PLUGIN] %s patrolling towards enemy zone %s", groupName, closestEnemyZone:GetName()))
|
||||||
return
|
group:PatrolZones({ closestEnemyZone }, 20, "Cone", 30, 60)
|
||||||
end
|
tasksAssigned = tasksAssigned + 1
|
||||||
|
mobileAssigned = mobileAssigned + 1
|
||||||
-- Count defenders
|
|
||||||
if IsDefender(group) then
|
|
||||||
defendersActive = defendersActive + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
AssignTasks(group, currentZoneCapture)
|
|
||||||
tasksAssigned = tasksAssigned + 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked (%d defenders).", tasksAssigned, defendersActive))
|
env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked (%d defenders, %d mobile).", tasksAssigned, defendersActive, mobileAssigned))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Function to monitor and announce warehouse status
|
-- Function to monitor and announce warehouse status
|
||||||
@ -941,65 +933,67 @@ blueArmorSpawn = SPAWN:New(BLUE_ARMOR_SPAWN_GROUP)
|
|||||||
:InitRandomizeTemplate(blueArmorTemplates)
|
:InitRandomizeTemplate(blueArmorTemplates)
|
||||||
:InitLimit(INIT_BLUE_ARMOR, MAX_BLUE_ARMOR)
|
:InitLimit(INIT_BLUE_ARMOR, MAX_BLUE_ARMOR)
|
||||||
|
|
||||||
-- Helper to schedule spawns per category so each uses its intended cadence.
|
-- Helper to schedule spawns per category. This is a self-rescheduling function.
|
||||||
local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar)
|
local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar)
|
||||||
local lastSpawnTime = 0
|
local function spawnAndReschedule()
|
||||||
local checkInterval = 10 -- Check every 10 seconds if it's time to spawn
|
-- Calculate the next spawn interval first
|
||||||
|
|
||||||
local function spawnCheck()
|
|
||||||
local currentTime = timer.getTime()
|
|
||||||
local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar)
|
local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar)
|
||||||
|
|
||||||
if not spawnInterval then
|
if not spawnInterval then
|
||||||
-- No warehouses alive - use recheck delay
|
-- No warehouses. Pause spawning and check again after the delay.
|
||||||
spawnInterval = NO_WAREHOUSE_RECHECK_DELAY
|
env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses). Rechecking in %ds.", label, NO_WAREHOUSE_RECHECK_DELAY))
|
||||||
if currentTime - lastSpawnTime >= spawnInterval then
|
SCHEDULER:New(nil, spawnAndReschedule, {}, NO_WAREHOUSE_RECHECK_DELAY)
|
||||||
env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive, will recheck)", label))
|
|
||||||
lastSpawnTime = currentTime
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if enough time has passed to spawn
|
-- Get friendly zones
|
||||||
if currentTime - lastSpawnTime >= spawnInterval then
|
local friendlyZones = getZonesFn()
|
||||||
local friendlyZones = getZonesFn()
|
if #friendlyZones > 0 then
|
||||||
local zonesAvailable = #friendlyZones
|
local chosenZone = friendlyZones[math.random(#friendlyZones)]
|
||||||
|
local spawnedGroup = spawnObject:SpawnInZone(chosenZone, false)
|
||||||
if zonesAvailable > 0 then
|
|
||||||
local chosenZone = friendlyZones[math.random(zonesAvailable)]
|
-- Post-spawn logic: if the group was created, check if it should become a defender
|
||||||
local spawnedGroup = spawnObject:SpawnInZone(chosenZone, false)
|
if spawnedGroup then
|
||||||
|
-- Use a short delay to ensure the group object is fully initialized before we work with it
|
||||||
-- Check if the spawn zone needs defenders and auto-elect if so
|
SCHEDULER:New(nil, function()
|
||||||
if spawnedGroup then
|
local grp = GROUP:FindByName(spawnedGroup:GetName())
|
||||||
local zoneName = chosenZone:GetName()
|
if grp and grp:IsAlive() then
|
||||||
if ZoneNeedsDefenders(zoneName) then
|
local zoneName = chosenZone:GetName()
|
||||||
SCHEDULER:New(nil, function()
|
-- If the zone needs defenders, this new unit is the perfect candidate
|
||||||
local grp = GROUP:FindByName(spawnedGroup:GetName())
|
if ZoneNeedsDefenders(zoneName) then
|
||||||
if grp and grp:IsAlive() then
|
ElectDefender(grp, chosenZone, "spawned in under-garrisoned zone")
|
||||||
ElectDefender(grp, chosenZone, "spawn in under-garrisoned zone")
|
end
|
||||||
end
|
|
||||||
end, {}, 2) -- Delay 2 seconds to ensure group is fully initialized
|
|
||||||
end
|
end
|
||||||
end
|
end, {}, 2) -- 2-second delay
|
||||||
|
|
||||||
lastSpawnTime = currentTime
|
|
||||||
else
|
|
||||||
env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label))
|
|
||||||
lastSpawnTime = currentTime -- Reset timer even if no zones available
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones).", label))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Schedule the next run
|
||||||
|
SCHEDULER:New(nil, spawnAndReschedule, {}, spawnInterval)
|
||||||
|
env.info(string.format("[DGB PLUGIN] Next %s spawn scheduled in %d seconds.", label, math.floor(spawnInterval)))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Single scheduler that runs continuously at fixed check interval
|
-- Kick off the first spawn with a random delay to stagger the different spawners
|
||||||
local scheduler = SCHEDULER:New(nil, spawnCheck, {}, math.random(5, 15), checkInterval)
|
local initialDelay = math.random(5, 15)
|
||||||
return scheduler
|
SCHEDULER:New(nil, spawnAndReschedule, {}, initialDelay)
|
||||||
|
env.info(string.format("[DGB PLUGIN] %s spawner initialized. First check in %d seconds.", label, initialDelay))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Schedule spawns (each spawner now runs at its own configured cadence)
|
-- Schedule spawns (each spawner now runs at its own configured cadence)
|
||||||
ScheduleSpawner(redInfantrySpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_INFANTRY, "Red Infantry", RED_INFANTRY_CADENCE_SCALAR)
|
if redInfantryValid and redWarehousesValid then
|
||||||
ScheduleSpawner(redArmorSpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_ARMOR, "Red Armor", RED_ARMOR_CADENCE_SCALAR)
|
ScheduleSpawner(redInfantrySpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_INFANTRY, "Red Infantry", RED_INFANTRY_CADENCE_SCALAR)
|
||||||
ScheduleSpawner(blueInfantrySpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, "Blue Infantry", BLUE_INFANTRY_CADENCE_SCALAR)
|
end
|
||||||
ScheduleSpawner(blueArmorSpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, "Blue Armor", BLUE_ARMOR_CADENCE_SCALAR)
|
if redArmorValid and redWarehousesValid then
|
||||||
|
ScheduleSpawner(redArmorSpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_ARMOR, "Red Armor", RED_ARMOR_CADENCE_SCALAR)
|
||||||
|
end
|
||||||
|
if blueInfantryValid and blueWarehousesValid then
|
||||||
|
ScheduleSpawner(blueInfantrySpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, "Blue Infantry", BLUE_INFANTRY_CADENCE_SCALAR)
|
||||||
|
end
|
||||||
|
if blueArmorValid and blueWarehousesValid then
|
||||||
|
ScheduleSpawner(blueArmorSpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, "Blue Armor", BLUE_ARMOR_CADENCE_SCALAR)
|
||||||
|
end
|
||||||
|
|
||||||
-- Schedule warehouse marker updates
|
-- Schedule warehouse marker updates
|
||||||
if ENABLE_WAREHOUSE_MARKERS then
|
if ENABLE_WAREHOUSE_MARKERS then
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user