diff --git a/changelog/snippets/other.7271.md b/changelog/snippets/other.7271.md new file mode 100644 index 00000000000..9fe4e678b94 --- /dev/null +++ b/changelog/snippets/other.7271.md @@ -0,0 +1 @@ +- Refactor and annotate the BaseManager class and other files and functions used by the campaign platoons. (#7271) diff --git a/engine/Sim/CAiBrain.lua b/engine/Sim/CAiBrain.lua index 2239ea581a9..d71ed0de1af 100644 --- a/engine/Sim/CAiBrain.lua +++ b/engine/Sim/CAiBrain.lua @@ -29,7 +29,7 @@ end --- Assigns units to a platoon ---@param platoon moho.platoon_methods | string Either a reference to a platoon, or the unique name of the platoon ---@param units Unit[] ----@param squad PlatoonSquads +---@param squad PlatoonSquadType ---@param formation UnitFormations ---@return UnitFormations #Returns the name of the formation function CAiBrain:AssignUnitsToPlatoon(platoon, units, squad, formation) @@ -406,12 +406,18 @@ end --- Returns platoon by unique name. --- ----@see moho.platoon_methods.UniquelyNamePlatoon To set the name. +---@see moho.platoon_methods.UniquelyNamePlatoon # To set the name. ---@param name string ---@return Platoon? function CAiBrain:GetPlatoonUniquelyNamed(name) end +--- `ArmyPool` is created by engine and contains all units for the army +---@param name 'ArmyPool' +---@return Platoon +function CAiBrain:GetPlatoonUniquelyNamed(name) +end + --- Returns brain's platoons ---@return Platoon[] function CAiBrain:GetPlatoonsList() @@ -509,7 +515,7 @@ end --- TODO. ---@param platoon Platoon ----@param squad PlatoonSquads +---@param squad PlatoonSquadType ---@param alliance AllianceType ---@param compareType 'LeastDefended'|'Closest'|'Furthest'|'HighestValue' ---@param category EntityCategory @@ -519,7 +525,7 @@ end --- TODO. ---@param platoon Platoon ----@param squad PlatoonSquads +---@param squad PlatoonSquadType ---@param alliance AllianceType ---@param compareType 'LeastDefended'|'Closest'|'Furthest'|'HighestValue' ---@param category EntityCategory diff --git a/lua/AI/AttackManager.lua b/lua/AI/AttackManager.lua index 77134c684c1..18c550d47dc 100644 --- a/lua/AI/AttackManager.lua +++ b/lua/AI/AttackManager.lua @@ -3,6 +3,12 @@ local Utilities = import("/lua/utilities.lua") local pairs, ipairs = pairs, ipairs local iscallable = iscallable +local airCheckCategories = categories.AIR * categories.MOBILE - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT +local landCheckCategories = categories.LAND * categories.MOBILE - categories.ENGINEER - categories.EXPERIMENTAL - categories.SCOUT +local navalCheckCategories = categories.NAVAL * categories.MOBILE - categories.EXPERIMENTAL +local anyCheckCategories = categories.MOBILE - categories.ENGINEER - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT + + ------------------------------------------------------------------------------------------------------------------------- --- This is the AttackManager class that is used for campaign/coop --- Vanilla Supreme Commander (referred to as *SC1* from now on) used this along with the PBM for its skirmish AI as well @@ -58,6 +64,7 @@ local iscallable = iscallable ---@field DestroyCallbacks FileFunctionRef[]|nil Table of functions called when the platoon is destroyed ---@field LocationType string Location from PBM -- used if you want to get units from pool ---@field PlatoonType "Air" | "Gate" | "Land" | "Sea" | "Any" MUST BE SET IF UsePool IS `true` +---@field PlatoonAddFunctions FileFunctionRef[] ---@field UsePool boolean Bool to use pool or not. Defaults to `false` @@ -72,7 +79,6 @@ local iscallable = iscallable ---@field AttackManagerState AttackManagerState Either 'ACTIVE' or 'PAUSED', used to check if the AM is active for an AI ---@field AMFormThread thread|nil AttackManager = ClassSimple { - brain = nil, NeedSort = false, PlatoonCount = { DefaultGroupAir = 0, DefaultGroupLand = 0, DefaultGroupSea = 0, }, @@ -240,7 +246,7 @@ AttackManager = ClassSimple { CheckAttackConditions = function(self, pltnInfo) for _, v in pairs(pltnInfo.AttackConditions) do if iscallable(v[1]) then - if not v[1](self.brain, unpack(v[2])) then + if not v[1](self.brain, unpack(v[2]--[[@as table]])) then return false end else @@ -330,16 +336,16 @@ AttackManager = ClassSimple { local checkCategory -- Only T1-T3 aerial combat units if v.PlatoonType == 'Air' then - checkCategory = categories.AIR * categories.MOBILE - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT + checkCategory = airCheckCategories -- Only T1-T3 surface combat units elseif v.PlatoonType == 'Land' then - checkCategory = categories.LAND * categories.MOBILE - categories.ENGINEER - categories.EXPERIMENTAL - categories.SCOUT + checkCategory = landCheckCategories -- Only T1-T3 naval combat units elseif v.PlatoonType == 'Sea' then - checkCategory = categories.NAVAL * categories.MOBILE - categories.EXPERIMENTAL + checkCategory = navalCheckCategories -- Only T1-T3 combined-arms combat units elseif v.PlatoonType == 'Any' then - checkCategory = categories.MOBILE - categories.ENGINEER - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT + checkCategory = anyCheckCategories else error('*AI WARNING: Invalid Platoon Type - ' .. v.PlatoonType, 2) end @@ -349,7 +355,7 @@ AttackManager = ClassSimple { -- If the AM platoon has a base of origin, it will only grab ArmyPool units from near it if v.LocationType then - local location = false + local location for locNum, locData in self.brain.PBM.Locations do if v.LocationType == locData.LocationType then location = locData diff --git a/lua/AI/OpAI/BaseManager.lua b/lua/AI/OpAI/BaseManager.lua index 12eb87dd4d1..0c4a1c8e568 100644 --- a/lua/AI/OpAI/BaseManager.lua +++ b/lua/AI/OpAI/BaseManager.lua @@ -4,83 +4,9 @@ ---- Copyright © 2005 Gas Powered Games, Inc. All rights reserved. ---------------------------------------------------------------------- ----@alias SaveFile "AirAttacks" | "AirScout" | "BasicLandAttack" | "BomberEscort" | "HeavyLandAttack" | "LandAssualt" | "LeftoverCleanup" | "LightAirAttack" | "NavalAttacks" | "NavalFleet" - --- types commonly used in repository - ----@class FunctionName: string - ----@class BuildCondition ----@field [1] FileName ----@field [2] FunctionName ----@field [3] table - ----@class FileFunctionRef ----@field [1] FileName ----@field [2] FunctionName - ----@class BuildGroup ----@field Name string ----@field Priority number - --- types used by AddOpAI - ----@class MasterPlatoonFunction ----@field [1] FileName ----@field [2] FunctionName - ----@class PlatoonData ----@field TransportReturn Marker # Location for transports to return to ----@field PatrolChains Marker[] # Selection of patrol chains to guide the constructed units ----@field PatrolChain Marker # Patrol chain to guide the construced units ----@field AttackChain Marker # Attack chain to guide the constructed units ----@field LandingChain Marker # Landing chain to guide the transports carrying the constructed units ----@field Area Area # An area, use depends on master platoon function ----@field Location Marker # A location, use depends on master platoon function ----@field BaseName string # Name of the `BaseManager` the platoon belongs to ----@field NumBuilding integer # Specific to `BaseManager` engineer platoons - ----@class AddOpAIData ----@field MasterPlatoonFunction FileFunctionRef # Behavior of instances upon completion ----@field PlatoonData PlatoonData # Parameters of the master platoon function ----@field Priority number # Priority over other builders - --- types used by AddUnitAI - ----@class AddUnitAIData ----@field Amount number # Number of engineers that can assist building ----@field KeepAlive boolean # ?? ----@field BuildCondition BuildCondition[] # Build conditions that must be met before building can start, can be empty ----@field PlatoonAIFunction FileFunctionRef # A { file, function } reference to the platoon AI function ----@field MaxAssist number # Number of engineers that can assist construction ----@field Retry boolean # Flag that allows the AI to retry ----@field PlatoonData PlatoonData # Parameters of the platoon AI function - --- types used by the BaseManager - ----@class BaseEngineerCount ----@field [1] integer Maximum number of engineers ----@field [2] integer Engineers permanenly assisting factories, has to be <= max number - ----@class EngineerDifficultyCount ----@field [1] integer Number of engineers for Easy difficulty ----@field [2] integer Number of engineers for Medium difficulty ----@field [3] integer Number of engineers for Hard difficulty - ----@class BaseEngineerDifficultyCount ----@field [1] EngineerDifficultyCount Maximum number of engineers ----@field [2] EngineerDifficultyCount Engineers permanenly assisting factories, has to be <= max number - ----@class BmBaseTemplates Base template specific for the BaseManager ----@field Template table ----@field List table ----@field UnitNames table ----@field BuildCounter table - local AIUtils = import("/lua/ai/aiutilities.lua") local ScenarioUtils = import("/lua/sim/scenarioutilities.lua") - local StructureTemplates = import("/lua/buildingtemplates.lua") local UpgradeTemplates = import("/lua/upgradetemplates.lua") local Buff = import("/lua/sim/buff.lua") @@ -88,10 +14,13 @@ local Buff = import("/lua/sim/buff.lua") local BaseOpAI = import("/lua/ai/opai/baseopai.lua") local ReactiveAI = import("/lua/ai/opai/reactiveai.lua") local NavalOpAI = import("/lua/ai/opai/navalopai.lua") +local ConditionalBuildManager = import("/lua/ai/opai/conditionalbuildmanager.lua").ConditionalBuildManager local BMBC = '/lua/editor/basemanagerbuildconditions.lua' local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' +local sortDownByPriority = sort_down_by("Priority") + -- Default rebuild numbers for buildings based on type; -1 is infinite local BuildingCounterDefaultValues = { -- Difficulty 1 @@ -174,1348 +103,162 @@ local BuildingCounterDefaultValues = { }, } ---- Failsafe callback function when a structure marked for needing an upgrade starts building something ---- If that 'something' is the upgrade itself, create a callback for the upgrade ----@param unit Unit ----@param unitBeingBuilt Unit -function FailSafeStructureOnStartBuild(unit, unitBeingBuilt) - -- If we are in the upgrading state, then it's the upgrade we want under normal circumstances. - -- We don't use different upgrades paths for coop, only that of the original SCFA (no Support Factory upgrade paths whatsoever) - -- If you decide to mess around with AI armies in cheat mode, and order a newly added upgrade path instead anyway, then any mishaps happening afterwards is on you! - if unit:IsUnitState('Upgrading') then - unitBeingBuilt.UnitName = unit.UnitName - unitBeingBuilt.BaseName = unit.BaseName +---@alias FunctionName string - -- Add callback when the upgrade is finished - if not unitBeingBuilt.AddedFinishedCallback then - unitBeingBuilt:AddUnitCallback(FailSafeUpgradeOnStopBeingBuilt, 'OnStopBeingBuilt') - unitBeingBuilt.AddedFinishedCallback = true - end - end -end +---@class FileFunctionRef +---@field [1] FileName # Path to the file +---@field [2] FunctionName # Function in the file to call ---- Failsafe function that will upgrade factories, radar, etc. to next level ----@param unit Unit ----@param upgradeID UnitId Blueprint -function FailSafeUpgradeBaseManagerStructure(unit, upgradeID) - -- Add callback when the structure starts building something - if not unit.AddedUpgradeCallback then - unit:AddOnStartBuildCallback(FailSafeStructureOnStartBuild) - unit.AddedUpgradeCallback = true - end +---@class BuildCondition: FileFunctionRef +---@field [3] table # List of params to pass into the build condition function - IssueUpgrade({unit}, upgradeID) - unit.SetToUpgrade = true -end +---@class PlatoonData +---@field TransportReturn? MarkerName # Location for transports to return to +---@field PatrolChains? MarkerName[] # Selection of patrol chains to guide the constructed units +---@field PatrolChain? MarkerName # Patrol chain to guide the construced units +---@field AttackChain? MarkerName # Attack chain to guide the constructed units +---@field LandingChain? MarkerName # Landing chain to guide the transports carrying the constructed units +---@field Area? AreaName # An area, use depends on master platoon function +---@field Location? MarkerName # A location, use depends on master platoon function +---@field BaseName? string # Name of the `BaseManager` the platoon belongs to +---@field NumBuilding? integer # Specific to `BaseManager` engineer platoons ---- Failsafe callback function when a structure upgrade is finished building ---- Updates the ScenarioInfo.UnitNames table with the new unit, and upgrades further if needed ----@param unit Unit -function FailSafeUpgradeOnStopBeingBuilt(unit) - local aiBrain = unit.Brain --[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[unit.BaseName] +---@class AddOpAIData +---@field MasterPlatoonFunction FileFunctionRef # Behavior of instances upon completion +---@field PlatoonData? PlatoonData # Parameters of the master platoon function +---@field Priority? integer # Priority over other builders. Defaults to the template priority + +---@class AddUnitAIData +---@field Amount? integer # How many instances of this unit to build. Defaults to `1`. +---@field KeepAlive? boolean # Rebuild the unit after death, defaults to `false` +---@field BuildCondition? BuildCondition[] # Build conditions that must be met before building can start, can be empty +---@field PlatoonAIFunction? FileFunctionRef|function # A { file, function } reference to the platoon AI function +---@field FormCallbacks? FileFunctionRef[]|function[] # A list of callbacks to be executed when the platoon is formed +---@field MaxAssist? integer # Number of engineers that can assist construction. Defaults to `1`. +---@field Retry? boolean # Retry construction of the unit, if it dies unfinished. Defaults to `false`. +---@field PlatoonData? PlatoonData # Parameters of the platoon AI function +---@field WaitSecondsAfterDeath? integer # Time to wait after conditional build's death before starting a new one. - if bManager then - local armyIndex = aiBrain:GetArmyIndex() - ScenarioInfo.UnitNames[armyIndex][unit.UnitName] = unit +-- types used by the BaseManager - local factionIndex = aiBrain:GetFactionIndex() - local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) +---@class BuildGroup +---@field Name string +---@field Priority number - -- Check if our structure can even upgrade to begin with - if upgradeID then - -- Check if the BM is supposed to upgrade this structure further - for index, structure in bManager.UpgradeTable do - -- If the names match, and the IDs don't, we need to upgrade - if unit.UnitName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then - FailSafeUpgradeBaseManagerStructure(unit, upgradeID) - end - end - end - end -end +---@class BaseEngineerCount +---@field [1] integer Maximum number of engineers +---@field [2] integer Engineers permanenly assisting factories, has to be <= max number + +---@class EngineerDifficultyCount +---@field [1] integer Number of engineers for Easy difficulty +---@field [2] integer Number of engineers for Medium difficulty +---@field [3] integer Number of engineers for Hard difficulty + +---@class BaseEngineerDifficultyCount +---@field [1] EngineerDifficultyCount Maximum number of engineers +---@field [2] EngineerDifficultyCount Engineers permanenly assisting factories, has to be <= max number + +---@class BmLevelName +---@field Name string +---@field Priority integer ----@alias BaseFunctionalityState "AirScouting" | "AntiAir" | "Artillery" | "BuildEngineers" | "CounterIntel" | "Engineers" | "ExpansionBases" | "Fabrication" | "GroundDefense" | "Intel" | "LandScouting" | "Nukes" | "Patrolling" | "Shields" | "TMLs" | "Torpedos" | "Walls" +---@class BmBaseTemplateList +---@field StructureType string Structure template name, e.g. `"T1LandFactory"` +---@field StructureCategory BlueprintId + +---@class BmBuildCounter +---@field BuildingID BlueprintId +---@field BuildingType string +---@field Position Vector +---@field UnitName string Name of the unit from the map editor +---@field Counter integer `-1` for infinite, else number of times this structure can be built + +---@class BmBaseTemplates Base template specific for the BaseManager +---@field Template table<{[1]: table<{[1]: string}>, [2]: Vector}> +---@field List table +---@field UnitNames table> # Table of unit names from the editor, indexed by position X and then by position Z +---@field BuildCounter table> + +---@class BMFunctionalityState +---@field AirScouting boolean +---@field AntiAir boolean +---@field Artillery boolean +---@field BuildEngineers boolean +---@field CounterIntel boolean +---@field Engineers boolean +---@field EngineerReclaiming boolean +---@field ExpansionBases boolean +---@field Fabrication boolean +---@field GroundDefense boolean +---@field Intel boolean +---@field LandScouting boolean +---@field Nukes boolean +---@field Patrolling boolean +---@field Shields boolean +---@field TMLs boolean +---@field Torpedos boolean +---@field Walls boolean ---@class ExpansionBaseData ----@field BaseName string Name of the base manager to expand to ----@field Engineers integer Number of engineers to sent to the expansion. Engineers are set, only when there's not enough. ----@field IncomingEngineers integer Number of engineers on the way to the expansion +---@field BaseName string # Name of the base manager to expand to +---@field Engineers integer # Number of engineers to sent to the expansion. Engineers are set, only when there's not enough. +---@field IncomingEngineers integer # Number of engineers on the way to the expansion +---@field TransportPlatoon? boolean # Is never set ---@alias Enhancement string --TODO ----@alias LevelName BuildGroup - ----@class ConditionalBuildData ----@field DecrementAssisting function # Decreases `NumAssisting` by one ----@field IncrementAssisting function # Increases `NumAssisting` by one ----@field Index integer # Stores the index of the current conditional being built ----@field IsBuilding boolean # True if a conditional build is going on, else false ----@field IsInitiated boolean # True if a unit has been issued the build command but has not yet begun building ----@field MainBuilder Unit|nil # nil if there is currently not a main conditional builder, else the unit building ----@field MaxAssisting integer # Maximum units to assist the current conditional build ----@field NeedsMoreBuilders fun(): boolean # Checks if more assisters are needed ----@field NumAssisting integer # Number of engies assisting the conditional build ----@field Reset function # Resets this table to the initial values ----@field Unit Unit|nil # The actual unit being constructed currently ----@field WaitSecondsAfterDeath integer|nil # Time to wait after conditional build's death before starting a new one. - +---@class ConditionalBuildEntry +---@field name string|string[] Name of the unit group to build, or a table of unit names to build +---@field data AddUnitAIData + +--- Manager to maintain a base, build, and dispatch attacks. Used mainly in campaign missions. +--- +--- To create a new base: +--- 1. Initialize the base, calling one of the initialize functions. +--- 2. Start the base. The base can spawn structures or start empty and build everyting. +--- 3. Optionally set engineers counts +--- 4. Add OpAI platoons with attacks ---@class BaseManager ---@field Active boolean ---@field AIBrain CampaignAIBrain ---@field BaseName string ----@field ConditionalBuildData ConditionalBuildData ----@field ConditionalBuildTable table[] --TODO +---@field ConditionalBuildManager ConditionalBuildManager +---@field ConditionalBuildTable ConditionalBuildEntry[] ---@field ConstructionEngineers Unit[] ----@field CurrentEngineerCount integer ----@field EngineerBuildRateBuff string|nil Name of the buff to apply to engineers ----@field EngineerQuantity integer +---@field CurrentEngineerCount integer # Number of currently active engineers +---@field ConstructionAssistBool boolean +---@field EngineerBuildRateBuff string? # Name of the buff to apply to engineers +---@field EngineerQuantity integer # Max number of engineers the base is allowed to use ---@field EngineersBuilding integer +---@field DefaultEngineerPatrolChain string? # Patrol chains used by base engineers +---@field DefaultAirScoutPatrolChain string? # Patrol chains for air scouting, if not set, random route is generated for each scouting platoon. +---@field DefaultLandScoutPatrolChain string? # Patrol chains for land scouting, if not set, random route is generated for each scouting platoon. ---@field ExpansionBaseData ExpansionBaseData[] ----@field FactoryBuildRateBuff string|nil Name of the buff to apply to factories ----@field FunctionalityStates table +---@field FactoryBuildRateBuff string? # Name of the buff to apply to factories +---@field FunctionalityStates BMFunctionalityState ---@field Initialized boolean ----@field LevelNames LevelName[] ----@field MaximumConstructionEngineers integer ----@field NumPermanentAssisting integer ----@field OpAITable table ----@field PermanentAssistCount integer +---@field BuildTable table +---@field LevelNames BmLevelName[] +---@field MaximumConstructionEngineers integer # Number of engineers that can start construction projects. Defaults to `Difficulty` or `3` +---@field NumPermanentAssisting integer # Number of engineer that is currently permanently assisting factories +---@field OpAITable table +---@field PermanentAssistCount integer # Number of engineers that should permanently assist factories ---@field PermanentAssisters table ---@field Position Vector ---@field Radius number ---@field Trash TrashBag ---@field UnfinishedBuildings table ----@field UnitUpgrades table ----@field UpgradeTable table -BaseManager = ClassSimple { - - --- Introduces all the relevant fields to the base manager, internally called by the engine - ---@param self BaseManager # An instance of the BaseManager class - ---@return nil - Create = function(self) - self.Trash = TrashBag() - - self.Active = false - self.DefaultEngineerPatrolChain = false - self.DefaultAirScoutPatrolChain = false - self.DefaultLandScoutPatrolChain = false - self.Initialized = false - self.ConstructionAssistBool = false - - self.FactoryBuildRateBuff = nil - self.EngineerBuildRateBuff = nil - - self.CurrentEngineerCount = 0 - self.EngineerQuantity = 0 - self.EngineersBuilding = 0 - self.NumPermanentAssisting = 0 - self.PermanentAssistCount = 0 - self.PermanentAssisters = {} - self.MaximumConstructionEngineers = ScenarioInfo.Options.Difficulty or 3 - - self.BuildingCounterData = { - Default = true, - } - - self.BuildTable = {} - self.ConstructionEngineers = {} - self.ExpansionBaseData = {} - - -- Commented out unused states, these were only found here throughout the FAF repo - -- We can re-enable them if corresponding functionalities are created, but right now there are none - self.FunctionalityStates = { - --AirAttacks = true, - AirScouting = false, - AntiAir = true, - Artillery = true, - BuildEngineers = true, - CounterIntel = true, - --EngineerReclaiming = true, - Engineers = true, - ExpansionBases = false, - Fabrication = true, - GroundDefense = true, - Intel = true, - --LandAttacks = true, - LandScouting = false, - Nukes = false, - Patrolling = true, - --SeaAttacks = true, - Shields = true, - TMLs = true, - Torpedos = true, - Walls = true, - - --Custom = {}, - } - self.LevelNames = {} - self.OpAITable = {} - self.UnfinishedBuildings = {} - self.UnfinishedEngineers = {} - self.UnitUpgrades = { - DefaultACU = {}, - DefaultSACU = {}, - Shields = {}, - } - self.UpgradeTable = {} - - -- This table stores data about conditional builds (for experimentals etc...) - self.ConditionalBuildTable = {} -- Used to build an op unit once the conditions are met - - self.ConditionalBuildData = { - IsInitiated = false, - IsBuilding = false, - NumAssisting = 0, - MaxAssisting = 1, - Index = 0, - - IncrementAssisting = function() - self.ConditionalBuildData.NumAssisting = self.ConditionalBuildData.NumAssisting + 1 - end, - DecrementAssisting = function() - self.ConditionalBuildData.NumAssisting = self.ConditionalBuildData.NumAssisting - 1 - end, - - Reset = function() - self.ConditionalBuildData.IsInitiated = false - self.ConditionalBuildData.IsBuilding = false - self.ConditionalBuildData.NumAssisting = 0 - self.ConditionalBuildData.MaxAssisting = 1 - self.ConditionalBuildData.Unit = nil - self.ConditionalBuildData.MainBuilder = nil - self.ConditionalBuildData.Index = 0 - self.ConditionalBuildData.WaitSecondsAfterDeath = nil - end, - - NeedsMoreBuilders = function() - return self.ConditionalBuildData.IsBuilding and - (self.ConditionalBuildData.NumAssisting < self.ConditionalBuildData.MaxAssisting) - end, - } - end, - - --- Initialises the base manager. - ---@see See the functions StartNonZeroBase, StartDifficultyBase, StartBase or StartEmptyBase to the initial state of the base - ---@param self BaseManager # An instance of the BaseManager class - ---@param brain CampaignAIBrain # An instance of the Brain class that we're managing a base for - ---@param baseName string # Name reference to a unit group as defined in the map that represnts the base, usually appended with _D1, _D2 or _D3 - ---@param markerName MarkerName # Name reference to a marker as defined in the map that represents the center of the base - ---@param radius number # Radius of the base - any structure that is within this distance to the center of the base is considered part of the base - ---@param levelTable any # A table of { { string, Priority } } that represents the priority of various sections of the base - ---@param diffultySeparate any # Flag that indicates we have a base that expands based on difficulty - ---@return nil - Initialize = function(self, brain, baseName, markerName, radius, levelTable, diffultySeparate) - self.Active = true - if self.Initialized then - error('*AI ERROR: BaseManager named "' .. baseName .. '" has already been initialized', 2) - end - - self.Initialized = true - if not brain.BaseManagers then - brain.BaseManagers = {} - brain:PBMRemoveBuildLocation(nil, 'MAIN') -- Remove main since we dont use it in ops much - end - - brain.BaseManagers[baseName] = self -- Store base in table, index by name of base - self.AIBrain = brain - self.Position = ScenarioUtils.MarkerToPosition(markerName) - self.BaseName = baseName - self.Radius = radius - for groupName, priority in levelTable do - if not diffultySeparate then - self:AddBuildGroup(groupName, priority, false, true) -- Do not spawn units, do not sort - else - self:AddBuildGroupDifficulty(groupName, priority, false, true) -- Do not spawn units, do not sort - end - end - - self.AIBrain:PBMAddBuildLocation(markerName, radius, baseName) -- Add base to PBM - self:LoadDefaultBaseCDRs() -- ACU things - self:LoadDefaultBaseSupportCDRs() -- sACU things - self:LoadDefaultBaseEngineers() -- All other Engs - self:LoadDefaultScoutingPlatoons() -- Load in default scouts - self:LoadDefaultBaseTMLs() -- TMLs - self:LoadDefaultBaseNukes() -- Nukes - self:SortGroupNames() -- Force sort since no sorting when adding groups earlier - self:ForkThread(self.UpgradeCheckThread) -- Start the thread to see if any buildings need upgrades - - -- Check for a default chains for engineers' patrol and scouting - if Scenario.Chains[baseName .. '_EngineerChain'] then - self:SetDefaultEngineerPatrolChain(baseName .. '_EngineerChain') - end - - if Scenario.Chains[baseName .. '_AirScoutChain'] then - self:SetDefaultAirScoutPatrolChain(baseName .. '_AirScoutChain') - end - - if Scenario.Chains[baseName .. '_LandScoutChain'] then - self:SetDefaultLandScoutPatrolChain(baseName .. '_LandScoutChain') - end - end, - - --- Checks whether this base manager has been initialised, note - throws an error. - ---@param self BaseManager # An instance of the BaseManager class - ---@return nil - InitializedCheck = function(self) - if not self.Initialized then - error('*AI ERROR: BaseManager named "' .. self.BaseName .. '" is not inialized', 2) - end - end, - - --- Enables or disables the base entirely, it may take a while before all base functionality is stopped - ---@param self BaseManager # An instance of the BaseManager class - ---@param status boolean # Flag that indicates whether the base should be active - ---@return nil - BaseActive = function(self, status) - self.Active = status - end, - - --- Initialises the base manager using the _D1, _D2 and _D3 difficulty tables. - ---@see See the functions StartNonZeroBase, StartDifficultyBase, StartBase or StartEmptyBase to the initial state of the base - ---@param self BaseManager # An instance of the BaseManager class - ---@param brain CampaignAIBrain # An instance of the Brain class that we're managing a base for - ---@param baseName string # Name reference to a unit group as defined in the map that represnts the base, usually appended with _D1, _D2 or _D3 - ---@param markerName Marker # Name reference to a marker as defined in the map that represents the center of the base - ---@param radius number # Radius of the base - any structure that is within this distance to the center of the base is considered part of the base - ---@param levelTable table # A table of { { string, Priority } } that represents the priority of various sections of the base - ---@return nil - InitializeDifficultyTables = function(self, brain, baseName, markerName, radius, levelTable) - self:Initialize(brain, baseName, markerName, radius, levelTable, true) - end, - - -- Auto trashbags all threads on a base manager - - --- Allocates a thread running the function where the base manager is prepended as the first argument. The thread is inserted in the trashbag of the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param fn function # A function to run on the forked thread - ---@param ... unknown # Parameters of the function where the base manager is prepended as the first argument - ---@return thread? # An instance of the Thread class - ForkThread = function(self, fn, ...) - if fn then - local thread = ForkThread(fn, self, unpack(arg)) - self.Trash:Add(thread) - return thread - else - return nil - end - end, - - --- Instructs the base to attempt to build a specific unit group as defined in the map. These are usually experimentals - ---@param self BaseManager # An instance of the BaseManager class - ---@param sUnitName string # Name reference to a unit group as defined in the map - ---@param bRetry boolean # Whether or not we should retry after failing to build - ---@param nNumEngineers number # Number of engineers that can assist building - ---@param tPlatoonAIFunction FileFunctionRef # A { file, function } reference to the platoon AI function - ---@param tPlatoonData PlatoonData # Parameters of the platoon AI function - ---@param fCondition BuildCondition[] # Build conditions that must be met before building can start, can be empty - ---@param bKeepAlive boolean # ?? - ---@return nil - ConditionalBuild = function(self, sUnitName, bRetry, nNumEngineers, tPlatoonAIFunction, tPlatoonData, fCondition, bKeepAlive) - if type(fCondition) ~= 'function' then error('Parameter fCondition must be a function.') return end - - table.insert(self.ConditionalBuildTable, - { - name = sUnitName, - data = - { - MaxAssist = nNumEngineers, - BuildCondition = fCondition, - PlatoonAIFunction = tPlatoonAIFunction, - PlatoonData = tPlatoonData, - Retry = bRetry, - KeepAlive = bKeepAlive, - Amount = 1, - }, - }) - end, - - --- Instructs the base to attempt to build a specific unit group as defined in the map. These are usually experimentals. - ---@see Functionally the same as ConditionalBuild - ---@param self BaseManager # An instance of the BaseManager class - ---@param unit string # Name reference to a unit group as defined in the map - ---@param data AddUnitAIData # Parameters that describe the build conditions, the platoon function and the data of the platoon function - ---@return boolean|OpAI # True or false to indicate success - AddUnitAI = function(self, unit, data) - return self:AddOpAI(unit, data) - end, - - --- Attaches an OpAI instance to the base manager that uses the base to build platoons. - ---@param self BaseManager # An instance of the BaseManager class - ---@param ptype SaveFile | string # Save file that is used to find child quantities - ---@param name string | AddUnitAIData # A name set by you to allow you to retrieve the returned AI instance - ---@param data AddOpAIData? # Parameters that describe the build conditions, the platoon function and the data of the platoon function - ---@return OpAI | boolean # An instance of the OpAI class or false - AddOpAI = function(self, ptype, name, data) - if not self.AIBrain then - error('*AI ERROR: No AI Brain for base manager') - return false - end - - -- If it's a table of unit names, or a single unit name - if (type(ptype) == 'table' and not ptype.Platoons) - or (type(ptype) == 'string' and ScenarioUtils.FindUnit(ptype, Scenario.Armies[self.AIBrain.Name].Units)) then - table.insert(self.ConditionalBuildTable, - { - name = ptype, - data = name, - }) - return true - end - ---@cast name -AddUnitAIData - if not self:CheckOpAIName(name) then return false end - - self.OpAITable[name] = BaseOpAI.CreateOpAI(self.AIBrain, self.BaseName, ptype, name, data) - - return self.OpAITable[name] - end, - - --- Retrieves a previously made OpAI instance - ---@param self BaseManager # An instance of the BaseManager class - ---@param name string # A name previously set by you to attach an OpAI instance to the base manager - ---@return OpAI | boolean # An instance of the OpAI class or false - GetOpAI = function(self, name) - if self.OpAITable[name] then - return self.OpAITable[name] - else - return false - end - end, - - --- Checks whether the intended OpAI name is unique. Only useful when hooking / modding an existing mission - ---@param self BaseManager # An instance of the BaseManager class - ---@param name string # A name to check - ---@return boolean # True of unique, otherwise false - CheckOpAIName = function(self, name) - if self.OpAITable[name] then - error('*AI ERROR: Duplicate OpAI name: ' .. name .. ' - for base manager: ' .. self.BaseName) - return false - end - - return true - end, - - ---@param self BaseManager - ---@param triggeringType string - ---@param reactionType string - ---@param name string - ---@param data any - ---@return boolean|OpAI - AddReactiveAI = function(self, triggeringType, reactionType, name, data) - self:InitializedCheck() - self.AIBrain:PBMEnableRandomSamePriority() - - if not self:CheckOpAIName(name) then return false end - - self.OpAITable[name] = ReactiveAI.CreateReactiveAI(self.AIBrain, self.BaseName, triggeringType, reactionType, - name, data) - - return self.OpAITable[name] - end, - - -- Add generated naval AI. Uses different OpAI type because it generates platoon data - ---@param self BaseManager - ---@param name string - ---@param data any - ---@return boolean|OpAI - AddNavalAI = function(self, name, data) - if not self.AIBrain then - error('*AI ERROR: No AI Brain for base manager') - return false - end - - if not self:CheckOpAIName(name) then return false end - - self.OpAITable[name] = NavalOpAI.CreateNavalAI(self.AIBrain, self.BaseName, name, data) - return self.OpAITable[name] - end, - - --- Adds a build group to the base manager that it needs to maintain - ---@param self BaseManager # An instance of the BaseManager class - ---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to build - ---@param priority number # Priority that indicates how important this build group is in comparison to others - ---@param spawn? boolean # `true` to spawn the group right awaz, `false` to let the BaseManager build it. - ---@param initial? boolean # Initial group don't triggers sorting of the groups by priority. It is used only internally when the base manager is first initialized with multiple groups - ---@return nil - AddBuildGroup = function(self, groupName, priority, spawn, initial) - -- Make sure the group exists - if not self:FindGroup(groupName) then - table.insert(self.LevelNames, { Name = groupName, Priority = priority }) - - -- Setup the brain base template for use in the base manager (Don't create so we can get a unitnames table) - self.AIBrain.BaseTemplates[self.BaseName .. groupName] = { Template = {}, List = {}, UnitNames = {}, - BuildCounter = {} } - - -- Now that we have a group name find it and add data - self:AddToBuildingTemplate(groupName, self.BaseName .. groupName) - - -- Spawn with SpawnGroup so we can track number of times this unit has existed - if spawn then - self:SpawnGroup(groupName) - end - - if not initial then - self:SortGroupNames() - end - else - error('*AI DEBUG: Group Name - ' .. groupName .. ' already exists in Base Manager group data', 2) - end - end, - - --- Adds a build group based based on difficult to the base manager that it needs to maintain - ---@param self BaseManager # An instance of the BaseManager class - ---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to build, appends the _D1, _D2 or _D3 to indicate difficulty - ---@param priority number # Priority that indicates how important this build group is in comparison to others - ---@param spawn? boolean # `true` to spawn the group right awaz, `false` to let the BaseManager build it. - ---@param initial? boolean # Initial group don't triggers sorting of the groups by priority. It is used only internally when the base manager is first initialized with multiple groups - AddBuildGroupDifficulty = function(self, groupName, priority, spawn, initial) - groupName = groupName .. '_D' .. ScenarioInfo.Options.Difficulty - self:AddBuildGroup(groupName, priority, spawn, initial) - end, - - --- Removes a build group from the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to be removed - ClearGroupTemplate = function(self, groupName) - self.AIBrain.BaseTemplates[self.BaseName .. groupName] = { Template = {}, List = {}, UnitNames = {}, - BuildCounter = {} } - end, - - --- Finds a build group from the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to be removed - ---@return BuildGroup | boolean # The build group in linked to the unit group or false - FindGroup = function(self, groupName) - for num, data in self.LevelNames do - if data.Name == groupName then - return data - end - end - return false - end, - - --- Retrieves the center of the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@return Vector # A { x, y, z } array-based table - GetPosition = function(self) - return self.Position - end, - - --- Retrieves the radius of the base manager, which is used to search for factories and engineers - ---@param self BaseManager # An instance of the BaseManager class - ---@return number - GetRadius = function(self) - return self.Radius - end, - - --- Defines the radius of the base manager, which is used to search for factories and engineers - ---@param self BaseManager # An instance of the BaseManager class - ---@param rad number # New radius of the base manager - ---@return nil - SetRadius = function(self, rad) - self.Radius = rad - end, - - --------------------------------------------------------------------------- - -- Functions for tracking the number of engineers working in a base manager - --------------------------------------------------------------------------- - - --- Add to the engineer count, useful when gifting the base engineers. - ---@param self BaseManager # An instance of the BaseManager class - ---@param num? integer # Amount to add to the engineer count. Default to 1. - AddCurrentEngineer = function(self, num) - self.CurrentEngineerCount = self.CurrentEngineerCount + (num or 1) - end, - - --- Subtract from the engineer count - ---@param self BaseManager # An instance of the BaseManager class - ---@param num? integer # Amount to subtract from the engineer count. Default to 1. - SubtractCurrentEngineer = function(self, num) - self.CurrentEngineerCount = self.CurrentEngineerCount - (num or 1) - end, - - --- Retrieve the engineer count - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer # Number of active engineers - GetCurrentEngineerCount = function(self) - return self.CurrentEngineerCount - end, - - --- Retrieve the maximum number of engineers, the base manager won't build more engineers than this - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer # Maximum number of engineers for this base manager - GetMaximumEngineers = function(self) - return self.EngineerQuantity - end, - - --- Add an engineer to the engineer pool of the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param unit Unit # Engineer to add - AddConstructionEngineer = function(self, unit) - table.insert(self.ConstructionEngineers, unit) - end, - - --- Remove an engineer from the engineer pool of the base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param unit Unit # Engineer to remove - RemoveConstructionEngineer = function(self, unit) - for k, v in self.ConstructionEngineers do - if v.EntityId == unit.EntityId then - table.remove(self.ConstructionEngineers, k) - break - end - end - end, - - --- Defines the maximum number of construction engineers - ---@param self BaseManager # An instance of the BaseManager class - ---@param num integer # New maximum number of construction engineers - SetMaximumConstructionEngineers = function(self, num) - self.MaximumConstructionEngineers = num - end, - - --- Retrieves the maximum number of construction engineers - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer # Maximum number of construction engineers - GetConstructionEngineerMaximum = function(self) - return self.MaximumConstructionEngineers - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer - GetConstructionEngineerCount = function(self) - return table.getn(self.ConstructionEngineers) - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@param bool boolean - SetConstructionAlwaysAssist = function(self, bool) - self.ConstructionAssistBool = bool - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return boolean - ConstructionAlwaysAssist = function(self) - return self.ConstructionAssistBool - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return boolean - ConstructionNeedsAssister = function(self) - if not self:ConstructionAlwaysAssist() or self:GetConstructionEngineerCount() == 0 then - return false - end - return true - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@param unit Unit - ---@return boolean - IsConstructionUnit = function(self, unit) - if not unit or unit.Dead then - return false - end - - for k, v in self.ConstructionEngineers do - if v.EntityId == unit.EntityId then - return true - end - end - - return false - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@param num integer - SetPermanentAssistCount = function(self, num) - if num > self.EngineerQuantity then - error('*Base Manager Error: More permanent assisters than total engineers') - end - self.PermanentAssistCount = num - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer - GetPermanentAssistCount = function(self) - return self.PermanentAssistCount - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@param num integer - SetNumPermanentAssisting = function(self, num) - self.NumPermanentAssisting = num - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer - IncrementPermanentAssisting = function(self) - self.NumPermanentAssisting = self.NumPermanentAssisting + 1 - return self.NumPermanentAssisting - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer - DecrementPermanentAssisting = function(self) - self.NumPermanentAssisting = self.NumPermanentAssisting - 1 - return self.NumPermanentAssisting - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer - GetNumPermanentAssisting = function(self) - return self.NumPermanentAssisting - end, - - ---comment - ---@param self BaseManager # An instance of the BaseManager class - ---@return boolean - NeedPermanentFactoryAssist = function(self) - if table.getn(self:GetAllBaseFactories()) >= 1 and - self:GetPermanentAssistCount() > self:GetNumPermanentAssisting() then - return true - end - return false - end, - - ---@param self BaseManager - ---@param count number - SetEngineerCountByDifficulty = function(self, count) - end, - - ---@param self BaseManager - ---@param count number - SetEngineerCountAlt = function(self, count) - end, - - ---Sets the maximum number of engineers operating in the base, this number includes commander if its spawned. - ---@param self BaseManager # An instance of the BaseManager class - ---@param count BaseEngineerDifficultyCount | BaseEngineerCount | integer If we have a table, we have various possible ways of counting engineers - --- {tNum1, tNum2, tNum3} - This is a difficulty defined total number of engs - --- {{tNum1, tNum2, tNum3,}, {aNum1, aNum2, aNum3}} - This is a difficulty defined total and permanent assisters - --- {tNum, aNum} - This is a single defined total with permanent assist - --- num - this is the number of total engineers - SetEngineerCount = function(self, count) - if type(count) == 'table' then - -- Table of tables means set the permanent assist count with total count - if type(count[1]) == 'table' then - self:SetTotalEngineerCount(count[1][ScenarioInfo.Options.Difficulty]) - self:SetPermanentAssistCount(count[2][ScenarioInfo.Options.Difficulty]) - -- Table with 3 entries is a dificulty table - elseif table.getn(count) == 3 then - self:SetTotalEngineerCount(count[ScenarioInfo.Options.Difficulty]) - -- Table with 2 entries means first is total engs, 2nd is num permanent assisting - elseif table.getn(count) == 2 then - self:SetTotalEngineerCount(count[1]) - self:SetPermanentAssistCount(count[2]) - -- Unknown number of entries - else - error('*Base Manager Error: Unknown number of entries passed to SetEngineerCount') - end - else - self:SetTotalEngineerCount(count--[[@as integer]]) - end - end, - - --- Defines the total engineer count of this base manager - ---@param self BaseManager # An instance of the BaseManager class - ---@param num integer # - SetTotalEngineerCount = function(self, num) - self.EngineerQuantity = num - ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] = num - end, - - --- Retrieves the amount of engineers that are building - ---@param self BaseManager # An instance of the BaseManager class - ---@return integer # Amount of engineers that are building - GetEngineersBuilding = function(self) - return self.EngineersBuilding - end, - - --- Adds or subtracts from the number of engineers that are building - ---@param self BaseManager # An instance of the BaseManager class - ---@param count integer # Amount to add or subtract - SetEngineersBuilding = function(self, count) - self.EngineersBuilding = self.EngineersBuilding + count - end, - - --- Defines the number of support command units this base manager should maintain - ---@param self BaseManager # An instance of the BaseManager class - ---@param count number # Number of support command units - SetSupportACUCount = function(self, count) - ScenarioInfo.VarTable[self.BaseName .. '_sACUNumber'] = count - end, - - --- Defines the factory build rate buff that is applied to all factories - ---@param self BaseManager # An instance of the BaseManager class - ---@param buffName string # Name of a buff instance - SetFactoryBuildRateBuff = function(self, buffName) - self.FactoryBuildRateBuff = buffName - end, - - --- Defines the engineer build rate buff that is applied to all engineers - ---@param self BaseManager # An instance of the BaseManager class - ---@param buffName string # Name of a buff instance - SetEngineerBuildRateBuff = function(self, buffName) - self.EngineerBuildRateBuff = buffName - end, - - --------------------------------------------------- - -- Get/Set of default chains for base funcitonality - --------------------------------------------------- - ---@param self BaseManager - ---@return any - GetDefaultEngineerPatrolChain = function(self) - return self.DefaultEngineerPatrolChain - end, - - ---@param self BaseManager - ---@param chainName string - ---@return boolean - SetDefaultEngineerPatrolChain = function(self, chainName) - self.DefaultEngineerPatrolChain = chainName - return true - end, - - ---@param self BaseManager - ---@return any - GetDefaultAirScoutPatrolChain = function(self) - return self.DefaultAirScoutPatrolChain - end, - ---@param self BaseManager - ---@param chainName string - ---@return boolean - SetDefaultAirScoutPatrolChain = function(self, chainName) - self.DefaultAirScoutPatrolChain = chainName - return true - end, - - ---@param self BaseManager - ---@return any - GetDefaultLandScoutPatrolChain = function(self) - return self.DefaultLandScoutPatrolChain - end, - - ---@param self BaseManager - ---@param chainName string - ---@return boolean - SetDefaultLandScoutPatrolChain = function(self, chainName) - self.DefaultLandScoutPatrolChain = chainName - return true - end, - - --- Returns all factories working at a base manager - ---@param self BaseManager - ---@param category? EntityCategory Filter only this category factories - ---@return any - GetAllBaseFactories = function(self, category) - if not category then - return self.AIBrain:PBMGetAllFactories(self.BaseName) - end - - -- Filter factories by category passed in - local retFacs = {} - for k, v in self.AIBrain:PBMGetAllFactories(self.BaseName) do - if EntityCategoryContains(category, v) then - table.insert(retFacs, v) - end - end - - return retFacs - end, - - --- Add in the ability for an expansion base to move out and help another base manager at another location - --- Functionality should mean that you simply specifiy the name of the base and it will then send out an - --- engineer to build it. You can also specify the number of engineers you would like to support with - --- baseData is a field that does nothing currently. If we ever need more data (transports maybe) it would - --- be housed there. - ---@param self BaseManager - ---@param baseName string - ---@param engQuantity number - ---@param baseData any - AddExpansionBase = function(self, baseName, engQuantity, baseData) - - table.insert(self.ExpansionBaseData, - { BaseName = baseName, Engineers = (engQuantity or 1), IncomingEngineers = 0 }) - self.FunctionalityStates.ExpansionBases = true - if baseData then - -- Setup base here - end - end, - - ----------------------------------------------- - -- Base Manager Unit Upgrade Level functions -- - ----------------------------------------------- - - ---Set what type of upgrades you want on what types of units. Applies to only ACU and SACU right now. - ---@param self BaseManager - ---@param upgradeTable table List of enhancements `{'ResourceEnhancement', 'T3Engineering'}` - ---@param unitName "DefaultACU"|"DefaultSACU" - ---@param startActive? boolean If true, it adds the enhancements to the existing units around the base right away. - SetUnitUpgrades = function(self, upgradeTable, unitName, startActive) - if not unitName then - error('*AI Debug: No unit name given for unit upgrades: Base named - ' .. self.BaseName, 2) - else - self.UnitUpgrades[unitName] = upgradeTable - end - - if startActive then - local units = {} - if unitName == 'DefaultACU' then - for k, v in AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.COMMAND, self.Position, self.Radius) do - table.insert(units, v) - end - elseif unitName == 'DefaultSACU' then - for k, v in AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.SUBCOMMANDER, self.Position, - self.Radius) do - table.insert(units, v) - end - end - - for num, unit in units do - for uNum, upgrade in upgradeTable do - unit:CreateEnhancement(upgrade) - end - end - end - end, - - --- Determines if a specific unit needs upgrades, returns name of upgrade if needed - --- Works with up to 3-level enhancement paths - --- TODO: Make a check that can deal with any number of prerequisites, like a 4-5-6 level enhancement path, example: ('Shield -> 'ShieldHeavy' -> 'ShieldVeryHeavy' ->'ShieldUltraHeavy' -> 'ShieldUltraBigHeavy') - ---@param self BaseManager - ---@param unit Unit - ---@param unitType? string - ---@return string|boolean - UnitNeedsUpgrade = function(self, unit, unitType) - if unit.Dead then - return false - end - - -- Find appropriate data about unit upgrade info - local upgradeTable - if unitType then - upgradeTable = self.UnitUpgrades[unitType] - else - upgradeTable = self.UnitUpgrades[unit.UnitName] - end - - if not upgradeTable then - return false - end - - local allEnhancements = unit:GetBlueprint().Enhancements - if not allEnhancements then - return false - end - - for _, upgradeName in upgradeTable do - -- Find the upgrade in the unit's bp - local bpUpgrade = allEnhancements[upgradeName] - if bpUpgrade then - if not unit:HasEnhancement(upgradeName) then - -- Check if we already have an enhancement on the slot our desired enhancement wants to occupy - if SimUnitEnhancements and SimUnitEnhancements[unit.EntityId] and SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] then - -- Account for 3-level enhancements, like the Cybran ACU's recent *Stealth -> Self-Repair -> Cloak* enhancement path, if we want 'Cloak', check for 'Stealth' - -- Check for the prerequisite's prerequisite, and return it - if bpUpgrade.Prerequisite and allEnhancements[bpUpgrade.Prerequisite].Prerequisite and (SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] == allEnhancements[bpUpgrade.Prerequisite].Prerequisite) then - return bpUpgrade.Prerequisite - -- If it's a direct prerequisite enhancement, return upgrade name - elseif bpUpgrade.Prerequisite and (SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] == bpUpgrade.Prerequisite) then - return upgradeName - -- It's not a prerequisite, remove the enhancement - else - return SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] .. 'Remove' - end - -- Check if our desired enhancement's prerequisite has any prerequisites, and return its name (Prerequisiteception) - elseif bpUpgrade.Prerequisite and allEnhancements[bpUpgrade.Prerequisite].Prerequisite and not unit:HasEnhancement(allEnhancements[bpUpgrade.Prerequisite].Prerequisite) then - return allEnhancements[bpUpgrade.Prerequisite].Prerequisite - -- Check if our desired enhancement has any prerequisites, and return its name - elseif bpUpgrade.Prerequisite and not unit:HasEnhancement(bpUpgrade.Prerequisite) then - return bpUpgrade.Prerequisite - -- No requirement and no enhancement occupying our desired slot, return the upgrade name - else - return upgradeName - end - end - else - error('*Base Manager Error: ' .. self.BaseName .. ', enhancement: ' .. upgradeName .. ' was not found in the unit\'s bp.') - end - end - - return false - end, - - ---@param self BaseManager - ---@param upgradeTable table - ---@param startActive? boolean - SetACUUpgrades = function(self, upgradeTable, startActive) - self:SetUnitUpgrades(upgradeTable, 'DefaultACU', startActive) - end, - - ---@param self BaseManager - ---@param upgradeTable table - ---@param startActive? boolean - SetSACUUpgrades = function(self, upgradeTable, startActive) - self:SetUnitUpgrades(upgradeTable, 'DefaultSACU', startActive) - end, - - --- Failsafe thread that will periodically loop through existing units that have been converted to lower tech level units so they can be built (ie. HQ factories) - --- If their unit IDs don't match the one set in the save.lua file, a failsafe function will be called to check if they are idle, so an upgrade can be started - ---@param self BaseManager - UpgradeCheckThread = function(self) - local armyIndex = self.AIBrain:GetArmyIndex() - while true do - if self.Active then - for k, v in self.UpgradeTable do - local unit = ScenarioInfo.UnitNames[armyIndex][v.UnitName] - -- Check if the structure exists, and needs to upgrade - if unit and not unit.Dead and unit.UnitId ~= v.FinalUnit then - --self:ForkThread(self.BaseManagerUpgrade, unit, v.UnitName) - self:BaseManagerUpgrade(unit, v.UnitName) - end - end - end - WaitSeconds(15) - end - end, - - --- Sort build groups by priority - ---@param self BaseManager - SortGroupNames = function(self) - local sortedList = {} - for i = 1, table.getn(self.LevelNames) do - local highest, highPos - for num, data in self.LevelNames do - if not highest or data.Priority > highest.Priority then - highest = data - highPos = num - end - end - sortedList[i] = highest - table.remove(self.LevelNames, highPos) - end - self.LevelNames = sortedList - end, - - --- Sets a group's priority - ---@param self BaseManager - ---@param groupName string - ---@param priority number - SetGroupPriority = function(self, groupName, priority) - for num, data in self.LevelNames do - if data.Name == groupName then - data.Priority = priority - break - end - end - self:SortGroupNames() - end, - - --- Spawns a group, tracks number of times it has been built, gives nuke and anti-nukes ammo - ---@param self BaseManager - ---@param groupName string - ---@param uncapturable? boolean - ---@param balance? boolean - SpawnGroup = function(self, groupName, uncapturable, balance) - local unitGroup = ScenarioUtils.CreateArmyGroup(self.AIBrain.Name, groupName, nil, balance) - - for _, v in unitGroup do - if self.FactoryBuildRateBuff then - Buff.ApplyBuff(v, self.FactoryBuildRateBuff) - end - if self.EngineerBuildRateBuff then - Buff.ApplyBuff(v, self.EngineerBuildRateBuff) - end - if uncapturable then - v:SetCapturable(false) - v:SetReclaimable(false) - end - if EntityCategoryContains(categories.SILO, v) then - if ScenarioInfo.Options.Difficulty == 1 then - v:GiveNukeSiloAmmo(1) - v:GiveTacticalSiloAmmo(1) - else - v:GiveNukeSiloAmmo(2) - v:GiveTacticalSiloAmmo(2) - end - end - end - end, - - --- If we want a group in the base manager to be wreckage, use this function - ---@param self BaseManager - ---@param groupName string - SpawnGroupAsWreckage = function(self, groupName) - local unitGroup = ScenarioUtils.CreateArmyGroup(self.AIBrain.Name, groupName, true) - end, - - --- Sets Engineer Count, spawns in all groups that have priority greater than zero - ---@param self BaseManager - ---@param engineerNumber? integer|table Defaults to 0 - ---@param uncapturable? boolean - StartNonZeroBase = function(self, engineerNumber, uncapturable) - if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then - self:SetEngineerCount(0) - elseif engineerNumber then - self:SetEngineerCount(engineerNumber) - end - - for num, data in self.LevelNames do - if data.Priority and data.Priority > 0 then - if ScenarioInfo.LoadBalance and ScenarioInfo.LoadBalance.Enabled then - table.insert(ScenarioInfo.LoadBalance.SpawnGroups, { self, data.Name, uncapturable }) - else - self:SpawnGroup(data.Name, uncapturable) - end - end - end - end, - - ---@param self BaseManager - ---@param groupNames string[] - ---@param engineerNumber? integer Defaults to 0 - ---@param uncapturable? boolean - StartDifficultyBase = function(self, groupNames, engineerNumber, uncapturable) - local newNames = {} - for k, v in groupNames do - table.insert(newNames, v .. '_D' .. ScenarioInfo.Options.Difficulty) - end - self:StartBase(newNames, engineerNumber, uncapturable) - end, - - -- Sets engineer count, spawns in all groups passed in in groupNames table - ---@param self BaseManager - ---@param groupNames table - ---@param engineerNumber? integer Defaults to 0 - ---@param uncapturable? boolean - StartBase = function(self, groupNames, engineerNumber, uncapturable) - if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then - self:SetEngineerCount(0) - elseif engineerNumber then - self:SetEngineerCount(engineerNumber) - end - - for num, name in groupNames do - local group = self:FindGroup(name) - if not group then - error('*AI DEBUG: Unable to create group - ' .. name .. ' - Data does not exist in Base Manager', 2) - else - self:SpawnGroup(group.Name, uncapturable) - end - end - end, - - -- Sets engineer count and spawns in no groups - ---@param self BaseManager - ---@param engineerNumber? integer Defaults to 1 - StartEmptyBase = function(self, engineerNumber) - if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then - self:SetEngineerCount(1) - elseif engineerNumber then - self:SetEngineerCount(engineerNumber) - end - end, - - --- Failsafe function that will upgrade factories, radar, etc. to next level if the initial upgrade order executed via build callbacks failed somehow - ---@param self BaseManager - ---@param unit Unit - ---@param unitName string - BaseManagerUpgrade = function(self, unit, unitName) - -- If we were set to upgrade, and we're being built, or busy building something, return - if unit.SetToUpgrade and (unit:IsUnitState('Upgrading') or unit:IsUnitState('Building') or unit:IsUnitState('BeingBuilt') or unit:GetNumBuildOrders(categories.ALLUNITS) > 0) then - return - end - - local aiBrain = self.AIBrain - local factionIndex = aiBrain:GetFactionIndex() - local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) - - if upgradeID then - FailSafeUpgradeBaseManagerStructure(unit, upgradeID) - else - WARN("BM Failsafe upgrade error: Couldn't find valid upgrade ID for unit named: " .. tostring(unitName) .. ", part of: " .. tostring(unit.BaseName)) - end - end, - - ---@param self BaseManager - ---@param buildingType string - ---@return boolean - CheckStructureBuildable = function(self, buildingType) - if self.BuildTable[buildingType] == false then - return false - end - - return true - end, - - --- The following "template" variables were removed due to them not being used at all: AmountNeeded, AmountWanted, CloseToBuilder - ---@param self BaseManager - ---@param groupName string - ---@param addName string - AddToBuildingTemplate = function(self, groupName, addName) - local tblUnit = ScenarioUtils.AssembleArmyGroup(self.AIBrain.Name, groupName) - local factionIndex = self.AIBrain:GetFactionIndex() - local template = self.AIBrain.BaseTemplates[addName].Template - local list = self.AIBrain.BaseTemplates[addName].List - local unitNames = self.AIBrain.BaseTemplates[addName].UnitNames - local buildCounter = self.AIBrain.BaseTemplates[addName].BuildCounter - if not tblUnit then - error('*AI DEBUG - Group: ' .. tostring(groupName) .. ' not found for Army: ' .. tostring(self.AIBrain.Name), 2) - else - -- Convert building to the proper type to be built if needed (ex: T2 and T3 factories to T1) - for i, unit in tblUnit do - for k, unitId in StructureTemplates.RebuildStructuresTemplate[factionIndex] do - if unit.type == unitId[1] then - table.insert(self.UpgradeTable, { FinalUnit = unit.type, UnitName = i, }) - unit.buildtype = unitId[2] - break - end - end - if not unit.buildtype then - unit.buildtype = unit.type - end - end - for i, unit in tblUnit do - self:StoreStructureName(i, unit, unitNames) - for j, buildList in StructureTemplates.BuildingTemplates[factionIndex] do -- BuildList[1] is type ("T1LandFactory"); buildList[2] is unitId (ueb0101) - local unitPos = { unit.Position[1], unit.Position[3], 0 } - if unit.buildtype == buildList[2] and buildList[1] ~= 'T3Sonar' then -- If unit to be built is the same id as the buildList unit it needs to be added - self:StoreBuildCounter(buildCounter, buildList[1], buildList[2], unitPos, i) - - local inserted = false - for k, section in template do -- Check each section of the template for the right type - if section[1][1] == buildList[1] then - table.insert(section, unitPos) -- Add position of new unit if found - inserted = true - break - end - end - if not inserted then -- If section doesn't exist create new one - table.insert(template, { { buildList[1] }, unitPos }) -- add new build type to list with new unit - list[unit.buildtype] = { StructureType = buildList[1], StructureCategory = unit.buildtype } - end - break - end - end - end - end - end, - - ---@param self BaseManager - ---@param unitName string - ---@param unitData table - ---@param namesTable table - StoreStructureName = function(self, unitName, unitData, namesTable) - if not namesTable[ unitData.Position[1] ] then - namesTable[ unitData.Position[1] ] = {} - end - namesTable[ unitData.Position[1] ][ unitData.Position[3] ] = unitName - end, - - ---@param self BaseManager - ---@param buildCounter table - ---@param buildingType string - ---@param buildingId number - ---@param unitPos Vector - ---@param unitName string - StoreBuildCounter = function(self, buildCounter, buildingType, buildingId, unitPos, unitName) - if not buildCounter[ unitPos[1] ] then - buildCounter[ unitPos[1] ] = {} - end - buildCounter[ unitPos[1] ][ unitPos[2] ] = { - BuildingID = buildingId, - BuildingType = buildingType, - Position = unitPos, - UnitName = unitName, - } - if self.BuildingCounterData.Default then - buildCounter[ unitPos[1] ][ unitPos[2] ].Counter = self:BuildingCounterDifficultyDefault(buildingType) - end - end, - - ---@param self BaseManager - ---@param buildingType string - ---@return any - BuildingCounterDifficultyDefault = function(self, buildingType) - local diff = ScenarioInfo.Options.Difficulty - if not diff then diff = 1 end - for k, v in BuildingCounterDefaultValues[diff] do - if buildingType == k then - return v - end - end - - return BuildingCounterDefaultValues[diff].Default - end, - - ---@param self BaseManager - ---@param location Vector - ---@param buildCounter table - ---@return boolean - CheckUnitBuildCounter = function(self, location, buildCounter) - for xVal, xData in buildCounter do - if xVal == location[1] then - for yVal, yData in xData do - if yVal == location[2] then - if yData.Counter > 0 or yData.Counter == -1 then - return true - else - return false - end - end - end - end - end - - return false - end, - - ---@param self BaseManager - ---@param unitName string - ---@return boolean - DecrementUnitBuildCounter = function(self, unitName) - for levelNum, levelData in self.LevelNames do - for firstNum, firstData in self.AIBrain.BaseTemplates[self.BaseName .. levelData.Name].BuildCounter do - for secondNum, secondData in firstData do - if secondData.UnitName == unitName then - if secondData.Counter > 0 then - secondData.Counter = secondData.Counter - 1 - end - return true - end - end - end - end - - return false - end, - - -- Enable/Disable functionality of base parts through functions - ---@param self BaseManager - ---@param actType string - ---@param val boolean - SetActive = function(self, actType, val) - if self.ActivationFunctions[actType .. 'Active'] then - self.ActivationFunctions[actType .. 'Active'](self, val) - else - error('*AI DEBUG: Invalid Activation type type - ' .. actType, 2) - end - end, - +---@field UnitUpgrades table +---@field UpgradeTable {UnitName: string, FinalUnit: BlueprintId}[] +---@field BuildingCounterData {["Default"]: true} +---@overload fun(): BaseManager +local BM = { ActivationFunctions = { - ShieldsActive = function(self, val) - local shields = AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.SHIELD * categories.STRUCTURE, - self.Position, self.Radius) + ---@param bm BaseManager + ---@param val boolean + ShieldsActive = function(bm, val) + local shields = AIUtils.GetOwnUnitsAroundPoint(bm.AIBrain, categories.SHIELD * categories.STRUCTURE, + bm.Position, bm.Radius) for k, v in shields do if val then v:OnScriptBitSet(0) -- If turning on shields @@ -1523,12 +266,13 @@ BaseManager = ClassSimple { v:OnScriptBitClear(0) -- If turning off shields end end - self.FunctionalityStates.Shields = val + bm.FunctionalityStates.Shields = val end, - - FabricationActive = function(self, val) - local fabs = AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.MASSFABRICATION * categories.STRUCTURE, - self.Position, self.Radius) + ---@param bm BaseManager + ---@param val boolean + FabricationActive = function(bm, val) + local fabs = AIUtils.GetOwnUnitsAroundPoint(bm.AIBrain, categories.MASSFABRICATION * categories.STRUCTURE, + bm.Position, bm.Radius) for k, v in fabs do if val then v:OnScriptBitClear(4) -- If turning on @@ -1536,13 +280,14 @@ BaseManager = ClassSimple { v:OnScriptBitSet(4) -- If turning off end end - self.FunctionalityStates.Fabrication = val + bm.FunctionalityStates.Fabrication = val end, - - IntelActive = function(self, val) - local intelUnits = AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, - (categories.RADAR + categories.SONAR + categories.OMNI) * categories.STRUCTURE, self.Position, - self.Radius) + ---@param bm BaseManager + ---@param val boolean + IntelActive = function(bm, val) + local intelUnits = AIUtils.GetOwnUnitsAroundPoint(bm.AIBrain, + (categories.RADAR + categories.SONAR + categories.OMNI) * categories.STRUCTURE, bm.Position, + bm.Radius) for k, v in intelUnits do if val then v:OnScriptBitClear(3) -- If turning on @@ -1550,12 +295,13 @@ BaseManager = ClassSimple { v:OnScriptBitSet(3) -- If turning off end end - self.FunctionalityStates.Intel = val + bm.FunctionalityStates.Intel = val end, - - CounterIntelActive = function(self, val) - local intelUnits = AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, - categories.COUNTERINTELLIGENCE * categories.STRUCTURE, self.Position, self.Radius) + ---@param bm BaseManager + ---@param val boolean + CounterIntelActive = function(bm, val) + local intelUnits = AIUtils.GetOwnUnitsAroundPoint(bm.AIBrain, + categories.COUNTERINTELLIGENCE * categories.STRUCTURE, bm.Position, bm.Radius) for k, v in intelUnits do if val then v:OnScriptBitClear(3) -- If turning on intel @@ -1563,566 +309,1849 @@ BaseManager = ClassSimple { v:OnScriptBitSet(2) -- If turning off intel end end - self.FunctionalityStates.CounterIntel = val + bm.FunctionalityStates.CounterIntel = val end, - - TMLActive = function(self, val) - self.FunctionalityStates.TMLs = val + ---@param bm BaseManager + ---@param val boolean + TMLActive = function(bm, val) + bm.FunctionalityStates.TMLs = val end, - - NukeActive = function(self, val) - self.FunctionalityStates.Nukes = val + ---@param bm BaseManager + ---@param val boolean + NukeActive = function(bm, val) + bm.FunctionalityStates.Nukes = val end, - - PatrolActive = function(self, val) - self.FunctionalityStates.Patrolling = val + ---@param bm BaseManager + ---@param val boolean + PatrolActive = function(bm, val) + bm.FunctionalityStates.Patrolling = val end, - - ReclaimActive = function(self, val) - self.FunctionalityStates.EngineerReclaiming = val + ---@param bm BaseManager + ---@param val boolean + ReclaimActive = function(bm, val) + bm.FunctionalityStates.EngineerReclaiming = val end, - - LandScoutingActive = function(self, val) - self.FunctionalityStates.LandScouting = val + ---@param bm BaseManager + ---@param val boolean + LandScoutingActive = function(bm, val) + bm.FunctionalityStates.LandScouting = val end, - - AirScoutingActive = function(self, val) - self.FunctionalityStates.AirScouting = val + ---@param bm BaseManager + ---@param val boolean + AirScoutingActive = function(bm, val) + bm.FunctionalityStates.AirScouting = val + end, + }, + BuildFunctions = { + ---@param bm BaseManager + ---@param val boolean + BuildEngineers = function(bm, val) + bm.FunctionalityStates.BuildEngineers = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildAntiAir = function(bm, val) + bm.BuildTable['T1AADefense'] = val + bm.BuildTable['T2AADefense'] = val + bm.BuildTable['T3AADefense'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildGroundDefense = function(bm, val) + bm.BuildTable['T1GroundDefense'] = val + bm.BuildTable['T2GroundDefense'] = val + bm.BuildTable['T3GroundDefense'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildTorpedo = function(bm, val) + bm.BuildTable['T1NavalDefense'] = val + bm.BuildTable['T2NavalDefense'] = val + bm.BuildTable['T3NavalDefense'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildAirFactories = function(bm, val) + bm.BuildTable['T1AirFactory'] = val + bm.BuildTable['T2AirFactory'] = val + bm.BuildTable['T3AirFactory'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildLandFactories = function(bm, val) + bm.BuildTable['T1LandFactory'] = val + bm.BuildTable['T2LandFactory'] = val + bm.BuildTable['T3LandFactory'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildSeaFactories = function(bm, val) + bm.BuildTable['T1Seafactory'] = val + bm.BuildTable['T2Seafactory'] = val + bm.BuildTable['T3Seafactory'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildFactories = function(bm, val) + bm.BuildFunctions['BuildAirFactories'](bm, val) + bm.BuildFunctions['BuildSeaFactories'](bm, val) + bm.BuildFunctions['BuildLandFactories'](bm, val) + end, + ---@param bm BaseManager + ---@param val boolean + BuildMissileDefense = function(bm, val) + bm.BuildTable['T1StrategicMissileDefense'] = val + bm.BuildTable['T2StrategicMissileDefense'] = val + bm.BuildTable['T3StrategicMissileDefense'] = val + bm.BuildTable['T1MissileDefense'] = val + bm.BuildTable['T2MissileDefense'] = val + bm.BuildTable['T3MissileDefense'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildShields = function(bm, val) + bm.BuildTable['T3ShieldDefense'] = val + bm.BuildTable['T2ShieldDefense'] = val + bm.BuildTable['T1ShieldDefense'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildArtillery = function(bm, val) + bm.BuildTable['T3Artillery'] = val + bm.BuildTable['T2Artillery'] = val + bm.BuildTable['T1Artillery'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildExperimentals = function(bm, val) + bm.BuildTable['T4LandExperimental1'] = val + bm.BuildTable['T4LandExperimental2'] = val + bm.BuildTable['T4AirExperimental1'] = val + bm.BuildTable['T4SeaExperimental1'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildWalls = function(bm, val) + bm.BuildTable['Wall'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildDefenses = function(bm, val) + bm.BuildFunctions['BuildAntiAir'](bm, val) + bm.BuildFunctions['BuildGroundDefense'](bm, val) + bm.BuildFunctions['BuildTorpedo'](bm, val) + bm.BuildFunctions['BuildArtillery'](bm, val) + bm.BuildFunctions['BuildShields'](bm, val) + bm.BuildFunctions['BuildWalls'](bm, val) + end, + ---@param bm BaseManager + ---@param val boolean + BuildJammers = function(bm, val) + bm.BuildTable['T1RadarJammer'] = val + bm.BuildTable['T2RadarJammer'] = val + bm.BuildTable['T3RadarJammer'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildRadar = function(bm, val) + bm.BuildTable['T3Radar'] = val + bm.BuildTable['T2Radar'] = val + bm.BuildTable['T1Radar'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildSonar = function(bm, val) + bm.BuildTable['T3Sonar'] = val + bm.BuildTable['T2Sonar'] = val + bm.BuildTable['T1Sonar'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildIntel = function(bm, val) + bm.BuildFunctions['BuildSonar'](bm, val) + bm.BuildFunctions['BuildRadar'](bm, val) + bm.BuildFunctions['BuildJammers'](bm, val) + end, + ---@param bm BaseManager + ---@param val boolean + BuildMissiles = function(bm, val) + bm.BuildTable['T3StrategicMissile'] = val + bm.BuildTable['T2StrategicMissile'] = val + bm.BuildTable['T1StrategicMissile'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildFabrication = function(bm, val) + bm.BuildTable['T3MassCreation'] = val + bm.BuildTable['T2MassCreation'] = val + bm.BuildTable['T1MassCreation'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildAirStaging = function(bm, val) + bm.BuildTable['T1AirStagingPlatform'] = val + bm.BuildTable['T2AirStagingPlatform'] = val + bm.BuildTable['T3AirStagingPlatform'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildMassExtraction = function(bm, val) + bm.BuildTable['T1Resource'] = val + bm.BuildTable['T2Resource'] = val + bm.BuildTable['T3Resource'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildEnergyProduction = function(bm, val) + bm.BuildTable['T1EnergyProduction'] = val + bm.BuildTable['T2EnergyProduction'] = val + bm.BuildTable['T3EnergyProduction'] = val + bm.BuildTable['T1HydroCarbon'] = val + bm.BuildTable['T2HydroCarbon'] = val + bm.BuildTable['T3HydroCarbon'] = val + end, + ---@param bm BaseManager + ---@param val boolean + BuildStorage = function(bm, val) + bm.BuildTable['MassStorage'] = val + bm.BuildTable['EnergyStorage'] = val end, }, +} +--- Introduces all the relevant fields to the base manager +function BM:Create() + self.Trash = TrashBag() + + self.Active = false + self.Initialized = false + self.ConstructionAssistBool = false + + self.FactoryBuildRateBuff = nil + self.EngineerBuildRateBuff = nil + + self.CurrentEngineerCount = 0 + self.EngineerQuantity = 0 + self.EngineersBuilding = 0 + self.NumPermanentAssisting = 0 + self.PermanentAssistCount = 0 + self.PermanentAssisters = {} + self.MaximumConstructionEngineers = ScenarioInfo.Options.Difficulty or 3 + + self.BuildingCounterData = { + Default = true, + } + + self.BuildTable = {} + self.ConstructionEngineers = {} + self.ExpansionBaseData = {} + + -- Commented out unused states, these were only found here throughout the FAF repo + -- We can re-enable them if corresponding functionalities are created, but right now there are none + self.FunctionalityStates = { + --AirAttacks = true, + AirScouting = false, + AntiAir = true, + Artillery = true, + BuildEngineers = true, + CounterIntel = true, + EngineerReclaiming = false, + Engineers = true, + ExpansionBases = false, + Fabrication = true, + GroundDefense = true, + Intel = true, + --LandAttacks = true, + LandScouting = false, + Nukes = false, + Patrolling = true, + --SeaAttacks = true, + Shields = true, + TMLs = true, + Torpedos = true, + Walls = true, + + --Custom = {}, + } + self.LevelNames = {} + self.OpAITable = {} + self.UnfinishedBuildings = {} + self.UnfinishedEngineers = {} + self.UnitUpgrades = { + DefaultACU = {}, + DefaultSACU = {}, + Shields = {}, + } + self.UpgradeTable = {} + + -- This table stores data about conditional builds (for experimentals etc...) + self.ConditionalBuildTable = {} -- Used to build an op unit once the conditions are met + + self.ConditionalBuildManager = ConditionalBuildManager() +end + +--- Initialises the base manager. Sets up the base template from provided structure groups. +---@see BaseManager.StartNonZeroBase # to start the base and spawn `levelTable` groups. +---@see BaseManager.StartDifficultyBase # to start the base and spawn other non-maintained groups based on difficulty +---@see BaseManager.StartBase # to start the base and spawn other non-maintained groups +---@see BaseManager.StartEmptyBase # to start the base empty +---@param brain CampaignAIBrain +---@param baseName string # Unique name for the base +---@param position MarkerName | Vector # The base will manage factories and engineers around this position +---@param radius integer # Distance from the `position` to define the base area. +---@param levelTable table # Name of the structure groups and their priorities for the base to maintain. +---@param diffultySeparate? boolean # If `true` the groups in `levelTable` will be appended by `_D1` difficulty string. +function BM:Initialize(brain, baseName, position, radius, levelTable, diffultySeparate) + self.Active = true + if self.Initialized then + error('*AI ERROR: BaseManager named "' .. baseName .. '" has already been initialized', 2) + end + + self.Initialized = true + if not brain.BaseManagers then + brain.BaseManagers = {} + brain:PBMRemoveBuildLocation(nil, 'MAIN') -- Remove main since we dont use it in ops much + end - -- Enable/Disable building of buildings and stuff - ---@param self BaseManager - ---@param buildType string - ---@param val boolean - SetBuild = function(self, buildType, val) - if not self.Active then - return + brain.BaseManagers[baseName] = self -- Store base in table, index by name of base + self.AIBrain = brain + ---@type Vector + local pos + if type(position) == "string" then + pos = ScenarioUtils.MarkerToPosition(position) + else + pos = position + end + self.Position = Vector(pos[1], pos[2], pos[3]) + self.BaseName = baseName + self.Radius = radius + for groupName, priority in levelTable do + if not diffultySeparate then + self:AddBuildGroup(groupName, priority, false, true) -- Do not spawn units, do not sort + else + self:AddBuildGroupDifficulty(groupName, priority, false, true) -- Do not spawn units, do not sort end - if self.BuildFunctions['Build' .. buildType] then - self.BuildFunctions['Build' .. buildType](self, val) + end + + self.AIBrain:PBMAddBuildLocation(position, radius, baseName) -- Add base to PBM + self:LoadDefaultBaseCDRs() -- ACU things + self:LoadDefaultBaseSupportCDRs() -- sACU things + self:LoadDefaultBaseEngineers() -- All other Engs + self:LoadDefaultScoutingPlatoons() -- Load in default scouts + self:LoadDefaultBaseTMLs() -- TMLs + self:LoadDefaultBaseNukes() -- Nukes + self:SortGroupNames() -- Force sort since no sorting when adding groups earlier + self:ForkThread(self.UpgradeCheckThread) -- Start the thread to see if any buildings need upgrades + + local chains = Scenario.Chains + -- Check for a default chains for engineers' patrol and scouting + + local chainName = baseName .. '_EngineerChain' + if chains[chainName] then + self:SetDefaultEngineerPatrolChain(chainName) + end + chainName = baseName .. '_AirScoutChain' + if chains[chainName] then + self:SetDefaultAirScoutPatrolChain(chainName) + end + chainName = baseName .. '_LandScoutChain' + if chains[chainName] then + self:SetDefaultLandScoutPatrolChain(chainName) + end +end + +--- Throws an error if the manager isnt initialized yet. +---@protected +function BM:InitializedCheck() + if not self.Initialized then + error('*AI ERROR: BaseManager named "' .. self.BaseName .. '" is not inialized', 2) + end +end + +--- Enables or disables the base entirely, it may take a while before all base functionality is stopped +---@param status boolean # Flag that indicates whether the base should be active +function BM:BaseActive(status) + self.Active = status +end + +--- Initialises the base manager. Sets up the base template from provided structure groups +--- using the _D1, _D2 and _D3 difficulty table for groups in `levelTable`. +---@see BaseManager.StartNonZeroBase # to start the base and spawn `levelTable` groups. +---@see BaseManager.StartDifficultyBase # to start the base and spawn other non-maintained groups based on difficulty +---@see BaseManager.StartBase # to start the base and spawn other non-maintained groups +---@see BaseManager.StartEmptyBase # to start the base empty +---@param brain CampaignAIBrain +---@param baseName string # Unique name for the base +---@param position MarkerName|Vector # The base will manage factories and engineers around this position +---@param radius integer # Distance from the `position` to define the base area. +---@param levelTable table # Name of the structure groups and their priorities for the base to maintain. +function BM:InitializeDifficultyTables(brain, baseName, position, radius, levelTable) + self:Initialize(brain, baseName, position, radius, levelTable, true) +end + +--- Allocates a thread running the function where the base manager is prepended as the first argument. The thread is inserted in the trashbag of the base manager +---@param fn function # A function to run on the forked thread +---@param ... unknown # Parameters of the function where the base manager is prepended as the first argument +---@return thread? # An instance of the Thread class +function BM:ForkThread(fn, ...) + if fn then + local thread = ForkThread(fn, self, unpack(arg)) + self.Trash:Add(thread) + return thread + else + return nil + end +end + +--- Instructs the base to attempt to build a specific unit group as defined in the map. These are usually experimentals +---@param sUnitName string # Name reference to a unit group as defined in the map +---@param bRetry boolean # Whether or not we should retry after failing to build +---@param nNumEngineers number # Number of engineers that can assist building +---@param tPlatoonAIFunction FileFunctionRef # A { file, function } reference to the platoon AI function +---@param tPlatoonData PlatoonData # Parameters of the platoon AI function +---@param fCondition BuildCondition[] # Build conditions that must be met before building can start, can be empty +---@param bKeepAlive boolean # ?? +---@return nil +function BM:ConditionalBuild(sUnitName, bRetry, nNumEngineers, tPlatoonAIFunction, tPlatoonData, fCondition, bKeepAlive) + if type(fCondition) ~= 'function' then + error('Parameter fCondition must be a function.') + return + end + + table.insert(self.ConditionalBuildTable, { + name = sUnitName, + data = { + MaxAssist = nNumEngineers, + BuildCondition = fCondition, + PlatoonAIFunction = tPlatoonAIFunction, + PlatoonData = tPlatoonData, + Retry = bRetry, + KeepAlive = bKeepAlive, + Amount = 1, + }, + }) +end + +--- Instructs the base to attempt to build a specific unit group as defined in the map. These are usually experimentals. +--- +---@see BaseManager.ConditionalBuild # - similar functionality +---@param unit string # Name reference to a unit group as defined in the map +---@param data AddUnitAIData # Parameters that describe the build conditions, the platoon function and the data of the platoon function +---@return boolean # True when the AI was created +function BM:AddUnitAI(unit, data) + return self:AddOpAI(unit, data) --[[@as boolean]] +end + +--- Attaches an OpAI instance to the base manager that uses the base to build platoons. +---@param ptype OpAIPlatoonTpFile | string # Save file that is used to find child quantities +---@param name string # A name set by you to allow you to retrieve the returned AI instance +---@param data AddOpAIData? # Parameters that describe the build conditions, the platoon function and the data of the platoon function +---@return OpAI # An instance of the OpAI class or false +---@overload fun(self: BaseManager, ptype: string[], data: AddUnitAIData): boolean +function BM:AddOpAI(ptype, name, data) + if not self.AIBrain then + error('*AI ERROR: No AI Brain for base manager') + end + + -- If it's a table of unit names, or a single unit name + if (type(ptype) == 'table' and not ptype.Platoons) + or (type(ptype) == 'string' and ScenarioUtils.FindUnit(ptype, Scenario.Armies[self.AIBrain.Name].Units)) then + table.insert(self.ConditionalBuildTable, + { + name = ptype, + data = name, + }) + return true + end + + self:CheckOpAIName(name) + + local opai = BaseOpAI.CreateOpAI(self.AIBrain, self.BaseName, ptype, name, data) + self.OpAITable[name] = opai + + return opai +end + +--- Retrieves a previously made OpAI instance +---@param name string # A name previously set by you to attach an OpAI instance to the base manager +---@return OpAI|NavalOpAI|ReactiveAI # An instance of the OpAI class or false +function BM:GetOpAI(name) + return self.OpAITable[name] +end + +--- Checks whether the intended OpAI name is unique. +--- +--- Throws an error if the name is in use. +---@protected +---@param name string # A name to check +function BM:CheckOpAIName(name) + if self.OpAITable[name] then + error('*AI ERROR: Duplicate OpAI name: ' .. name .. ' - for base manager: ' .. self.BaseName) + end +end + +--- AI that is built with a predefined platoon template as response to the provided trigger type +--- +---@see OpAI.SetChildActive # to disable certain children +---@param trigger ReactAITriggerEvent +---@param reaction ReactAIReactionType +---@param name string # Unique name for the AI platoon +---@param data? table # Optional data to overwrite the default platoon builder values +---@return ReactiveAI +function BM:AddReactiveAI(trigger, reaction, name, data) + self:InitializedCheck() + self.AIBrain:PBMEnableRandomSamePriority() + + self:CheckOpAIName(name) + + local opai = ReactiveAI.CreateReactiveAI(self.AIBrain, self.BaseName, trigger, reaction, name, data) + self.OpAITable[name] = opai + + return opai +end + +-- Add generated naval AI. Uses different OpAI type because it generates platoon data +---@param name string +---@param data NavalOpAIData +---@return NavalOpAI +function BM:AddNavalAI(name, data) + if not self.AIBrain then + error('*AI ERROR: No AI Brain for base manager') + end + + self:CheckOpAIName(name) + + local opai = NavalOpAI.CreateNavalAI(self.AIBrain, self.BaseName, name, data) + self.OpAITable[name] = opai + + return opai +end + +--- Adds a build group to the base manager that it needs to maintain +---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to build +---@param priority number # Priority that indicates how important this build group is in comparison to others +---@param spawn? boolean # `true` to spawn the group right awaz, `false` to let the BaseManager build it. +---@param initial? boolean # Initial group don't triggers sorting of the groups by priority. It is used only internally when the base manager is first initialized with multiple groups +function BM:AddBuildGroup(groupName, priority, spawn, initial) + -- Make sure the group exists + if self:HasGroup(groupName) then + error('*AI DEBUG: Group Name - ' .. groupName .. ' already exists in Base Manager group data', 2) + end + + table.insert(self.LevelNames, { Name = groupName, Priority = priority }) + + local name = self.BaseName .. groupName + -- Setup the brain base template for use in the base manager (Don't create so we can get a unitnames table) + self.AIBrain.BaseTemplates[name] = { + Template = {}, + List = {}, + UnitNames = {}, + BuildCounter = {} + } + + -- Now that we have a group name find it and add data + self:AddToBuildingTemplate(groupName, name) + + -- Spawn with SpawnGroup so we can track number of times this unit has existed + if spawn then + self:SpawnGroup(groupName) + end + + if not initial then + self:SortGroupNames() + end +end + +--- Adds a build group based based on difficult to the base manager that it needs to maintain +---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to build, appends the _D1, _D2 or _D3 to indicate difficulty +---@param priority number # Priority that indicates how important this build group is in comparison to others +---@param spawn? boolean # `true` to spawn the group right awaz, `false` to let the BaseManager build it. +---@param initial? boolean # Initial group don't triggers sorting of the groups by priority. It is used only internally when the base manager is first initialized with multiple groups +function BM:AddBuildGroupDifficulty(groupName, priority, spawn, initial) + groupName = groupName .. '_D' .. ScenarioInfo.Options.Difficulty + self:AddBuildGroup(groupName, priority, spawn, initial) +end + +--- Removes a build group from the base manager +---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to be removed +function BM:ClearGroupTemplate(groupName) + self.AIBrain.BaseTemplates[self.BaseName .. groupName] = { + Template = {}, + List = {}, + UnitNames = {}, + BuildCounter = {} + } +end + +--- Checks if a build group exists in the base manager +---@param groupName string +---@return boolean +function BM:HasGroup(groupName) + for _, data in self.LevelNames do + if data.Name == groupName then + return true + end + end + return false +end + +--- Finds a build group from the base manager +---@protected +---@param groupName string # Name reference to a unit group as defined in the map that represents the unit group to be removed +---@return BmLevelName? # The build group in linked to the unit group or nil +function BM:FindGroup(groupName) + for _, data in self.LevelNames do + if data.Name == groupName then + return data + end + end +end + +--- Retrieves the center of the base manager +---@return Vector # A { x, y, z } array-based table +function BM:GetPosition() + return self.Position +end + +--- Retrieves the radius of the base manager, which is used to search for factories and engineers +---@return number +function BM:GetRadius() + return self.Radius +end + +--- Defines the radius of the base manager, which is used to search for factories and engineers +---@param rad number +function BM:SetRadius(rad) + self.Radius = rad +end + +--#region - Functions for tracking the number of engineers working in a base manager + +--- Add to the engineer count, useful when gifting the base engineers. +---@param num? integer # Amount to add to the engineer count. Default to 1. +function BM:AddCurrentEngineer(num) + self.CurrentEngineerCount = self.CurrentEngineerCount + (num or 1) +end + +--- Subtract from the engineer count +---@param num? integer # Amount to subtract from the engineer count. Default to 1. +function BM:SubtractCurrentEngineer(num) + self.CurrentEngineerCount = self.CurrentEngineerCount - (num or 1) +end + +--- Retrieve the engineer count +---@return integer # Number of active engineers +function BM:GetCurrentEngineerCount() + return self.CurrentEngineerCount +end + +--- Retrieve the maximum number of engineers, the base manager won't build more engineers than this +---@return integer # Maximum number of engineers for this base manager +function BM:GetMaximumEngineers() + return self.EngineerQuantity +end + +--- Add an engineer to the engineer pool of the base manager +---@param unit Unit # Engineer to add +function BM:AddConstructionEngineer(unit) + table.insert(self.ConstructionEngineers, unit) +end + +--- Remove an engineer from the engineer pool of the base manager +---@param unit Unit # Engineer to remove +function BM:RemoveConstructionEngineer(unit) + for k, v in self.ConstructionEngineers do + if v.EntityId == unit.EntityId then + table.remove(self.ConstructionEngineers, k) + break + end + end +end + +--- Sets the number of engineers that can start new construction projects +---@param num integer +function BM:SetMaximumConstructionEngineers(num) + self.MaximumConstructionEngineers = num +end + +--- Returns the number of engineers that can start new construction projects +---@return integer +function BM:GetConstructionEngineerMaximum() + return self.MaximumConstructionEngineers +end + +---@return integer +function BM:GetConstructionEngineerCount() + return table.getn(self.ConstructionEngineers) +end + +---@param bool boolean +function BM:SetConstructionAlwaysAssist(bool) + self.ConstructionAssistBool = bool +end + +---@return boolean +function BM:ConstructionAlwaysAssist() + return self.ConstructionAssistBool +end + +--- Returns true when assisting construction is allowed in the base manager and the base has construction engineers +---@return boolean +function BM:ConstructionNeedsAssister() + if not self:ConstructionAlwaysAssist() or self:GetConstructionEngineerCount() == 0 then + return false + end + return true +end + +--- Checks if the unit is registered as a constuction engineer for base building. +---@param unit Unit +---@return boolean +function BM:IsConstructionUnit(unit) + if not unit or unit.Dead then + return false + end + + for k, v in self.ConstructionEngineers do + if v.EntityId == unit.EntityId then + return true + end + end + + return false +end + +---@param num integer +function BM:SetPermanentAssistCount(num) + if num > self.EngineerQuantity then + error('*Base Manager Error: More permanent assisters than total engineers') + end + self.PermanentAssistCount = num +end + +---@return integer +function BM:GetPermanentAssistCount() + return self.PermanentAssistCount +end + +---@param num integer +function BM:SetNumPermanentAssisting(num) + self.NumPermanentAssisting = num +end + +---@return integer +function BM:IncrementPermanentAssisting() + self.NumPermanentAssisting = self.NumPermanentAssisting + 1 + return self.NumPermanentAssisting +end + +---@return integer +function BM:DecrementPermanentAssisting() + self.NumPermanentAssisting = self.NumPermanentAssisting - 1 + return self.NumPermanentAssisting +end + +---@return integer +function BM:GetNumPermanentAssisting() + return self.NumPermanentAssisting +end + +---@return boolean +function BM:NeedPermanentFactoryAssist() + if table.getn(self:GetAllBaseFactories()) >= 1 and + self:GetPermanentAssistCount() > self:GetNumPermanentAssisting() then + return true + end + return false +end + +--- Sets the maximum number of engineers operating in the base, this number includes commander if its spawned. +---@param count BaseEngineerDifficultyCount | BaseEngineerCount | integer # If we have a table, we have various possible ways of counting engineers +--- {tNum1, tNum2, tNum3} - This is a difficulty defined total number of engs +--- +--- {{tNum1, tNum2, tNum3,}, {aNum1, aNum2, aNum3}} - This is a difficulty defined total and permanent assisters +--- +--- {tNum, aNum} - This is a single defined total with permanent assist +--- +--- num - this is the number of total engineers +function BM:SetEngineerCount(count) + if type(count) == 'table' then + -- Table of tables means set the permanent assist count with total count + if type(count[1]) == 'table' then + self:SetTotalEngineerCount(count[1][ScenarioInfo.Options.Difficulty]) + self:SetPermanentAssistCount(count[2][ScenarioInfo.Options.Difficulty]) + -- Table with 3 entries is a dificulty table + elseif table.getn(count) == 3 then + self:SetTotalEngineerCount(count[ScenarioInfo.Options.Difficulty]) + -- Table with 2 entries means first is total engs, 2nd is num permanent assisting + elseif table.getn(count) == 2 then + self:SetTotalEngineerCount(count[1] --[[@as integer]]) + self:SetPermanentAssistCount(count[2] --[[@as integer]]) + -- Unknown number of entries else - error('*AI DEBUG: Invalid build type - ' .. buildType, 2) + error('*Base Manager Error: Unknown number of entries passed to SetEngineerCount') + end + else + self:SetTotalEngineerCount(count --[[@as integer]]) + end +end + +--- Defines the total engineer count of this base manager +---@param num integer +function BM:SetTotalEngineerCount(num) + self.EngineerQuantity = num + ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] = num +end + +--- Retrieves the amount of engineers that are building +---@return integer +function BM:GetEngineersBuilding() + return self.EngineersBuilding +end + +--- Adds or subtracts from the number of engineers that are building +---@param count integer # Amount to add or subtract +function BM:SetEngineersBuilding(count) + self.EngineersBuilding = self.EngineersBuilding + count +end + +--- Defines the number of support command units this base manager should maintain +---@param count integer +function BM:SetSupportACUCount(count) + ScenarioInfo.VarTable[self.BaseName .. '_sACUNumber'] = count +end + +--- Defines the factory build rate buff that is applied to all factories +---@param buffName string # Name of a buff instance +function BM:SetFactoryBuildRateBuff(buffName) + self.FactoryBuildRateBuff = buffName +end + +--- Defines the engineer build rate buff that is applied to all engineers +---@param buffName string # Name of a buff instance +function BM:SetEngineerBuildRateBuff(buffName) + self.EngineerBuildRateBuff = buffName +end + +--#endregion +--#region - Get/Set of default chains for base funcitonality + +---@return string? +function BM:GetDefaultEngineerPatrolChain() + return self.DefaultEngineerPatrolChain +end + +---@param chainName string +---@return boolean +function BM:SetDefaultEngineerPatrolChain(chainName) + self.DefaultEngineerPatrolChain = chainName + return true +end + +---@return string? +function BM:GetDefaultAirScoutPatrolChain() + return self.DefaultAirScoutPatrolChain +end + +---@param chainName string +---@return boolean +function BM:SetDefaultAirScoutPatrolChain(chainName) + self.DefaultAirScoutPatrolChain = chainName + return true +end + +---@return string? +function BM:GetDefaultLandScoutPatrolChain() + return self.DefaultLandScoutPatrolChain +end + +---@param chainName string +---@return boolean +function BM:SetDefaultLandScoutPatrolChain(chainName) + self.DefaultLandScoutPatrolChain = chainName + return true +end + +--- Returns all factories working at a base manager +---@param category? EntityCategory # Filter only this category factories +---@return FactoryUnit[] factories # All factories working at this base manager, filtered by category if provided +function BM:GetAllBaseFactories(category) + if not category then + return self.AIBrain:PBMGetAllFactories(self.BaseName) + end + + -- Filter factories by category passed in + local retFacs = {} + for k, v in self.AIBrain:PBMGetAllFactories(self.BaseName) do + if EntityCategoryContains(category, v) then + table.insert(retFacs, v) end - end, - - -- Disable all buildings - ---@param self BaseManager - ---@param val boolean - SetBuildAllStructures = function(self, val) - for k, v in self.BuildFunctions do - if k ~= 'BuildEngineers' then - v(self, val) + end + + return retFacs +end + +--- Add in the ability for an expansion base to move out and help another base manager at another location +--- Functionality should mean that you simply specifiy the name of the base and it will then send out an +--- engineer to build it. You can also specify the number of engineers you would like to support with +--- +--- baseData is a field that does nothing currently. If we ever need more data (transports maybe) it would +--- be housed there. +---@param baseName string +---@param engQuantity? number # Defaults to `1` +---@param baseData? any +function BM:AddExpansionBase(baseName, engQuantity, baseData) + table.insert(self.ExpansionBaseData, { + BaseName = baseName, + Engineers = engQuantity or 1, + IncomingEngineers = 0 + }) + + self.FunctionalityStates.ExpansionBases = true + if baseData then + -- Setup base here + end +end + +--#endregion +--#region - Base Manager Unit Upgrade Level functions -- + +--- Set what type of upgrades you want on what types of units. The list of upgrades can be changed later. +--- +--- Specify only the final upgrades if the enhancements has any prerequisites. +--- +--- Applies to only ACU and SACU right now. +---@param upgradeTable Enhancement[] # List of enhancements `{'ResourceEnhancement', 'T3Engineering'}` +---@param unitName "DefaultACU"|"DefaultSACU" +---@param startActive? boolean # If true, it adds the enhancements to the existing units around the base right away. +function BM:SetUnitUpgrades(upgradeTable, unitName, startActive) + if not unitName then + error('*AI Debug: No unit name given for unit upgrades: Base named - ' .. self.BaseName, 2) + else + self.UnitUpgrades[unitName] = upgradeTable + end + + if startActive then + local units = {} + if unitName == 'DefaultACU' then + for k, v in AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.COMMAND, self.Position, self.Radius) do + table.insert(units, v) + end + elseif unitName == 'DefaultSACU' then + for k, v in AIUtils.GetOwnUnitsAroundPoint(self.AIBrain, categories.SUBCOMMANDER, self.Position, + self.Radius) do + table.insert(units, v) end end - end, - BuildFunctions = { - BuildEngineers = function(self, val) - self.FunctionalityStates.BuildEngineers = val - end, + for num, unit in units do + for uNum, upgrade in upgradeTable do + unit:CreateEnhancement(upgrade) + end + end + end +end - BuildAntiAir = function(self, val) - self.BuildTable['T1AADefense'] = val - self.BuildTable['T2AADefense'] = val - self.BuildTable['T3AADefense'] = val - end, +--- Determines if a specific unit needs upgrades, returns name of upgrade if needed +--- +--- Works with up to 3-level enhancement paths +---@param unit Unit +---@param unitType? string +---@return string|boolean +function BM:UnitNeedsUpgrade(unit, unitType) + -- TODO: Make a check that can deal with any number of prerequisites, + -- like a 4-5-6 level enhancement path, example: + -- ('Shield -> 'ShieldHeavy' -> 'ShieldVeryHeavy' ->'ShieldUltraHeavy' -> 'ShieldUltraBigHeavy') + if unit.Dead then + return false + end - BuildGroundDefense = function(self, val) - self.BuildTable['T1GroundDefense'] = val - self.BuildTable['T2GroundDefense'] = val - self.BuildTable['T3GroundDefense'] = val - end, + -- Find appropriate data about unit upgrade info + local upgradeTable + if unitType then + upgradeTable = self.UnitUpgrades[unitType] + else + upgradeTable = self.UnitUpgrades[unit.UnitName] + end - BuildTorpedo = function(self, val) - self.BuildTable['T1NavalDefense'] = val - self.BuildTable['T2NavalDefense'] = val - self.BuildTable['T3NavalDefense'] = val - end, + if not upgradeTable then + return false + end - BuildAirFactories = function(self, val) - self.BuildTable['T1AirFactory'] = val - self.BuildTable['T2AirFactory'] = val - self.BuildTable['T3AirFactory'] = val - end, + local allEnhancements = unit:GetBlueprint().Enhancements + if not allEnhancements then + return false + end - BuildLandFactories = function(self, val) - self.BuildTable['T1LandFactory'] = val - self.BuildTable['T2LandFactory'] = val - self.BuildTable['T3LandFactory'] = val - end, + for _, upgradeName in upgradeTable do + -- Find the upgrade in the unit's bp + local bpUpgrade = allEnhancements[upgradeName] + if bpUpgrade then + if not unit:HasEnhancement(upgradeName) then + -- Check if we already have an enhancement on the slot our desired enhancement wants to occupy + if SimUnitEnhancements and SimUnitEnhancements[unit.EntityId] and SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] then + -- Account for 3-level enhancements, like the Cybran ACU's recent *Stealth -> Self-Repair -> Cloak* enhancement path, if we want 'Cloak', check for 'Stealth' + -- Check for the prerequisite's prerequisite, and return it + if bpUpgrade.Prerequisite and allEnhancements[bpUpgrade.Prerequisite].Prerequisite and (SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] == allEnhancements[bpUpgrade.Prerequisite].Prerequisite) then + return bpUpgrade.Prerequisite + -- If it's a direct prerequisite enhancement, return upgrade name + elseif bpUpgrade.Prerequisite and (SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] == bpUpgrade.Prerequisite) then + return upgradeName + -- It's not a prerequisite, remove the enhancement + else + return SimUnitEnhancements[unit.EntityId][bpUpgrade.Slot] .. 'Remove' + end + -- Check if our desired enhancement's prerequisite has any prerequisites, and return its name (Prerequisiteception) + elseif bpUpgrade.Prerequisite and allEnhancements[bpUpgrade.Prerequisite].Prerequisite and not unit:HasEnhancement(allEnhancements[bpUpgrade.Prerequisite].Prerequisite) then + return allEnhancements[bpUpgrade.Prerequisite].Prerequisite + -- Check if our desired enhancement has any prerequisites, and return its name + elseif bpUpgrade.Prerequisite and not unit:HasEnhancement(bpUpgrade.Prerequisite) then + return bpUpgrade.Prerequisite + -- No requirement and no enhancement occupying our desired slot, return the upgrade name + else + return upgradeName + end + end + else + error('*Base Manager Error: ' .. self.BaseName .. ', enhancement: ' .. upgradeName .. ' was not found in the unit\'s bp.') + end + end - BuildSeaFactories = function(self, val) - self.BuildTable['T1Seafactory'] = val - self.BuildTable['T2Seafactory'] = val - self.BuildTable['T3Seafactory'] = val - end, + return false +end - BuildFactories = function(self, val) - self.BuildFunctions['BuildAirFactories'](self, val) - self.BuildFunctions['BuildSeaFactories'](self, val) - self.BuildFunctions['BuildLandFactories'](self, val) - end, +---@see BaseManager.SetUnitUpgrades # for details +---@param upgradeTable Enhancement[] +---@param startActive? boolean +function BM:SetACUUpgrades(upgradeTable, startActive) + self:SetUnitUpgrades(upgradeTable, 'DefaultACU', startActive) +end - BuildMissileDefense = function(self, val) - self.BuildTable['T1StrategicMissileDefense'] = val - self.BuildTable['T2StrategicMissileDefense'] = val - self.BuildTable['T3StrategicMissileDefense'] = val - self.BuildTable['T1MissileDefense'] = val - self.BuildTable['T2MissileDefense'] = val - self.BuildTable['T3MissileDefense'] = val - end, +---@see BaseManager.SetUnitUpgrades # for details +---@param upgradeTable Enhancement[] +---@param startActive? boolean +function BM:SetSACUUpgrades(upgradeTable, startActive) + self:SetUnitUpgrades(upgradeTable, 'DefaultSACU', startActive) +end - BuildShields = function(self, val) - self.BuildTable['T3ShieldDefense'] = val - self.BuildTable['T2ShieldDefense'] = val - self.BuildTable['T1ShieldDefense'] = val - end, +--- Failsafe thread that will periodically loop through existing units that have been converted to lower tech level units so they can be built (ie. HQ factories) +--- If their unit IDs don't match the one set in the save.lua file, a failsafe function will be called to check if they are idle, so an upgrade can be started +---@protected +function BM:UpgradeCheckThread() + local armyIndex = self.AIBrain:GetArmyIndex() + local unitNames = ScenarioInfo.UnitNames[armyIndex] + + while true do + if self.Active then + for k, v in pairs(self.UpgradeTable) do + local unit = unitNames[v.UnitName] + -- Check if the structure exists, and needs to upgrade + if unit and not unit.Dead and unit.UnitId ~= v.FinalUnit then + --self:ForkThread(self.BaseManagerUpgrade, unit, v.UnitName) + self:BaseManagerUpgrade(unit, v.UnitName) + end + end + end + WaitSeconds(15) + end +end - BuildArtillery = function(self, val) - self.BuildTable['T3Artillery'] = val - self.BuildTable['T2Artillery'] = val - self.BuildTable['T1Artillery'] = val - end, +--- Sort build groups by priority +---@protected +function BM:SortGroupNames() + table.sort(self.LevelNames, sortDownByPriority) +end - BuildExperimentals = function(self, val) - self.BuildTable['T4LandExperimental1'] = val - self.BuildTable['T4LandExperimental2'] = val - self.BuildTable['T4AirExperimental1'] = val - self.BuildTable['T4SeaExperimental1'] = val - end, +--- Changes the priority of the group previously added to the manager. +---@param groupName string +---@param priority number +function BM:SetGroupPriority(groupName, priority) + for num, data in self.LevelNames do + if data.Name == groupName then + data.Priority = priority + break + end + end + self:SortGroupNames() +end - BuildWalls = function(self, val) - self.BuildTable['Wall'] = val - end, +--- Spawns a group, gives nuke and anti-nukes ammo and applies buffs. +--- +--- The base will not try to rebuild anything from this group. +---@param groupName string +---@param uncapturable? boolean +---@param balance? boolean +function BM:SpawnGroup(groupName, uncapturable, balance) + local unitGroup = ScenarioUtils.CreateArmyGroup(self.AIBrain.Name, groupName, nil, balance) + ---@cast unitGroup -nil + for _, v in unitGroup do + if self.FactoryBuildRateBuff then + Buff.ApplyBuff(v, self.FactoryBuildRateBuff) + end + if self.EngineerBuildRateBuff then + Buff.ApplyBuff(v, self.EngineerBuildRateBuff) + end + if uncapturable then + v:SetCapturable(false) + v:SetReclaimable(false) + end + if EntityCategoryContains(categories.SILO, v) then + if ScenarioInfo.Options.Difficulty == 1 then + v:GiveNukeSiloAmmo(1) + v:GiveTacticalSiloAmmo(1) + else + v:GiveNukeSiloAmmo(2) + v:GiveTacticalSiloAmmo(2) + end + end + end +end - BuildDefenses = function(self, val) - self.BuildFunctions['BuildAntiAir'](self, val) - self.BuildFunctions['BuildGroundDefense'](self, val) - self.BuildFunctions['BuildTorpedo'](self, val) - self.BuildFunctions['BuildArtillery'](self, val) - self.BuildFunctions['BuildShields'](self, val) - self.BuildFunctions['BuildWalls'](self, val) - end, +--- Spawn a unit group as wreckages. +--- +--- The base will not try to rebuild anything from this group. +---@param groupName string +function BM:SpawnGroupAsWreckage(groupName) + ScenarioUtils.CreateArmyGroup(self.AIBrain.Name, groupName, true) +end - BuildJammers = function(self, val) - self.BuildTable['T1RadarJammer'] = val - self.BuildTable['T2RadarJammer'] = val - self.BuildTable['T3RadarJammer'] = val - end, +--- Starts the base. Sets Engineer Count, spawns in all groups that have priority greater than zero +---@param engineerNumber? BaseEngineerDifficultyCount | BaseEngineerCount | integer # Defaults to 0 +---@param uncapturable? boolean +function BM:StartNonZeroBase(engineerNumber, uncapturable) + if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then + self:SetEngineerCount(0) + elseif engineerNumber then + self:SetEngineerCount(engineerNumber) + end - BuildRadar = function(self, val) - self.BuildTable['T3Radar'] = val - self.BuildTable['T2Radar'] = val - self.BuildTable['T1Radar'] = val - end, + for num, data in self.LevelNames do + if data.Priority and data.Priority > 0 then + if ScenarioInfo.LoadBalance and ScenarioInfo.LoadBalance.Enabled then + table.insert(ScenarioInfo.LoadBalance.SpawnGroups, { self, data.Name, uncapturable }) + else + self:SpawnGroup(data.Name, uncapturable) + end + end + end +end - BuildSonar = function(self, val) - self.BuildTable['T3Sonar'] = val - self.BuildTable['T2Sonar'] = val - self.BuildTable['T1Sonar'] = val - end, +--- Starts the base. Sets engineer count, spawns in all groups in `groupNames` based on difficulty. +--- +--- The manager will not maintain any structures from these groups. +---@param groupNames GroupName[] +---@param engineerNumber? BaseEngineerDifficultyCount | BaseEngineerCount | integer # Defaults to 0 +---@param uncapturable? boolean +function BM:StartDifficultyBase(groupNames, engineerNumber, uncapturable) + local newNames = {} + for k, v in groupNames do + table.insert(newNames, v .. '_D' .. ScenarioInfo.Options.Difficulty) + end + self:StartBase(newNames, engineerNumber, uncapturable) +end - BuildIntel = function(self, val) - self.BuildFunctions['BuildSonar'](self, val) - self.BuildFunctions['BuildRadar'](self, val) - self.BuildFunctions['BuildJammers'](self, val) - end, +--- Starts the base. Sets engineer count, spawns in all groups in `groupNames`. +--- +--- The manager will not maintain any structures from these groups. +---@param groupNames GroupName[] +---@param engineerNumber? BaseEngineerDifficultyCount | BaseEngineerCount | integer # Defaults to 0 +---@param uncapturable? boolean +function BM:StartBase(groupNames, engineerNumber, uncapturable) + if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then + self:SetEngineerCount(0) + elseif engineerNumber then + self:SetEngineerCount(engineerNumber) + end - BuildMissiles = function(self, val) - self.BuildTable['T3StrategicMissile'] = val - self.BuildTable['T2StrategicMissile'] = val - self.BuildTable['T1StrategicMissile'] = val - end, + for num, name in groupNames do + local group = self:FindGroup(name) + if not group then + error('*AI DEBUG: Unable to create group - ' .. name .. ' - Data does not exist in Base Manager', 2) + else + self:SpawnGroup(group.Name, uncapturable) + end + end +end - BuildFabrication = function(self, val) - self.BuildTable['T3MassCreation'] = val - self.BuildTable['T2MassCreation'] = val - self.BuildTable['T1MassCreation'] = val - end, +--- Starts the base. Sets engineer count and spawns in no groups. +--- +--- Use this if you want engineers to build the base. +---@param engineerNumber? BaseEngineerDifficultyCount | BaseEngineerCount | integer # Defaults to 1 +function BM:StartEmptyBase(engineerNumber) + if not engineerNumber and not ScenarioInfo.VarTable[self.BaseName .. '_EngineerNumber'] then + self:SetEngineerCount(1) + elseif engineerNumber then + self:SetEngineerCount(engineerNumber) + end +end - BuildAirStaging = function(self, val) - self.BuildTable['T1AirStagingPlatform'] = val - self.BuildTable['T2AirStagingPlatform'] = val - self.BuildTable['T3AirStagingPlatform'] = val - end, +--- Failsafe function that will upgrade factories, radar, etc. to next level if the initial upgrade order executed via build callbacks failed somehow +---@protected +---@param unit Unit +---@param unitName string +function BM:BaseManagerUpgrade(unit, unitName) + -- If we were set to upgrade, and we're being built, or busy building something, return + if unit.SetToUpgrade and (unit:IsUnitState('Upgrading') or unit:IsUnitState('Building') or unit:IsUnitState('BeingBuilt') or unit:GetNumBuildOrders(categories.ALLUNITS) > 0) then + return + end - BuildMassExtraction = function(self, val) - self.BuildTable['T1Resource'] = val - self.BuildTable['T2Resource'] = val - self.BuildTable['T3Resource'] = val - end, + local aiBrain = self.AIBrain + local factionIndex = aiBrain:GetFactionIndex() + local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) + + if upgradeID then + FailSafeUpgradeBaseManagerStructure(unit, upgradeID) + else + WARN("BM Failsafe upgrade error: Couldn't find valid upgrade ID for unit named: " .. tostring(unitName) .. ", part of: " .. tostring(unit.BaseName)) + for k, v in pairs(self.UpgradeTable) do + if v.UnitName == unitName then + table.remove(self.UpgradeTable, k) + return + end + end + end +end - BuildEnergyProduction = function(self, val) - self.BuildTable['T1EnergyProduction'] = val - self.BuildTable['T2EnergyProduction'] = val - self.BuildTable['T3EnergyProduction'] = val - self.BuildTable['T1HydroCarbon'] = val - self.BuildTable['T2HydroCarbon'] = val - self.BuildTable['T3HydroCarbon'] = val - end, +---@param buildingType string +---@return boolean +function BM:CheckStructureBuildable(buildingType) + if self.BuildTable[buildingType] == false then + return false + end - BuildStorage = function(self, val) - self.BuildTable['MassStorage'] = val - self.BuildTable['EnergyStorage'] = val - end, - }, + return true +end + +--- The following "template" variables were removed due to them not being used at all: AmountNeeded, AmountWanted, CloseToBuilder +---@protected +---@param groupName string +---@param addName string +function BM:AddToBuildingTemplate(groupName, addName) + local tblUnit = ScenarioUtils.AssembleArmyGroup(self.AIBrain.Name, groupName) + local factionIndex = self.AIBrain:GetFactionIndex() + local template = self.AIBrain.BaseTemplates[addName].Template + local list = self.AIBrain.BaseTemplates[addName].List + local unitNames = self.AIBrain.BaseTemplates[addName].UnitNames + local buildCounter = self.AIBrain.BaseTemplates[addName].BuildCounter + if not tblUnit then + error('*AI DEBUG - Group: ' .. tostring(groupName) .. ' not found for Army: ' .. tostring(self.AIBrain.Name), 2) + end + + for name, unit in pairs(tblUnit) do + -- Convert building to the proper type to be built if needed (ex: T2 and T3 factories to T1) + for _, unitId in pairs(StructureTemplates.RebuildStructuresTemplate[factionIndex]) do + if unit.type == unitId[1] then + table.insert(self.UpgradeTable, { FinalUnit = unit.type, UnitName = name, }) + unit.buildtype = unitId[2] + break + end + end + if not unit.buildtype then + unit.buildtype = unit.type + end + + self:StoreStructureName(name, unit, unitNames) + for _, buildList in pairs(StructureTemplates.BuildingTemplates[factionIndex]) do -- BuildList[1] is type ("T1LandFactory"); buildList[2] is unitId (ueb0101) + local structureType = buildList[1] + -- Skip T3 sonars as they are mobile and built by the conditional build + if structureType == 'T3Sonar' then continue end + + local structureBpId = buildList[2] + if unit.buildtype ~= structureBpId then continue end + + -- If unit to be built is the same id as the buildList unit it needs to be added + local unitPos = { unit.Position[1], unit.Position[3], 0 } + self:StoreBuildCounter(buildCounter, structureType, structureBpId, unitPos, name) + + local inserted = false + for k, section in pairs(template) do -- Check each section of the template for the right type + if section[1][1] == structureType then + table.insert(section, unitPos) -- Add position of new unit if found + inserted = true + break + end + end + if not inserted then -- If section doesn't exist create new one + table.insert(template, { { structureType }, unitPos }) -- add new build type to list with new unit + list[unit.buildtype] = { StructureType = structureType, StructureCategory = unit.buildtype } + end + break + end + end +end + +---@protected +---@param unitName string +---@param unitData table +---@param namesTable table +function BM:StoreStructureName(unitName, unitData, namesTable) + if not namesTable[unitData.Position[1]] then + namesTable[unitData.Position[1]] = {} + end + namesTable[unitData.Position[1]][unitData.Position[3]] = unitName +end - ------------------------------------- - -- Default builders for base managers - ------------------------------------- - - ---@param self BaseManager - LoadDefaultBaseEngineers = function(self) - local defaultBuilder - -- The Engineer AI Thread - for i = 1, 3 do - defaultBuilder = { - BuilderName = 'T' .. i .. 'BaseManager_EngineersWork_' .. self.BaseName, - PlatoonTemplate = self:CreateEngineerPlatoonTemplate(i), - Priority = 1, - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerEngineerPlatoonSplit' }, - BuildConditions = { - { BMBC, 'BaseManagerNeedsEngineers', { self.BaseName } }, - { BMBC, 'BaseActive', { self.BaseName } }, - }, - PlatoonData = { - BaseName = self.BaseName, - }, - PlatoonType = 'Any', - RequiresConstruction = false, - LocationType = self.BaseName, - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) +---@protected +---@param buildCounter table +---@param buildingType string +---@param buildingId BlueprintId +---@param unitPos Vector +---@param unitName string +function BM:StoreBuildCounter(buildCounter, buildingType, buildingId, unitPos, unitName) + if not buildCounter[unitPos[1]] then + buildCounter[unitPos[1]] = {} + end + buildCounter[unitPos[1]][unitPos[2]] = { + BuildingID = buildingId, + BuildingType = buildingType, + Position = unitPos, + UnitName = unitName, + } + if self.BuildingCounterData.Default then + buildCounter[unitPos[1]][unitPos[2]].Counter = self:BuildingCounterDifficultyDefault(buildingType) + end +end + +---@protected +---@param buildingType string +---@return any +function BM:BuildingCounterDifficultyDefault(buildingType) + ---@type integer + local diff = ScenarioInfo.Options.Difficulty + if not diff then diff = 1 end + for k, v in BuildingCounterDefaultValues[diff] do + if buildingType == k then + return v end + end + + return BuildingCounterDefaultValues[diff].Default +end - -- Disband platoons - engineers built here - for i = 1, 3 do - for j = 1, 5 do - for num, pType in { 'Air', 'Land', 'Sea' } do - defaultBuilder = { - BuilderName = 'T' .. i .. 'BaseManagerEngineerDisband_' .. j .. 'Count_' .. self.BaseName, - PlatoonAIPlan = 'DisbandAI', - PlatoonTemplate = self:CreateEngineerPlatoonTemplate(i, j), - Priority = 300 * i, - PlatoonType = pType, - RequiresConstruction = true, - LocationType = self.BaseName, - PlatoonData = { - NumBuilding = j, - BaseName = self.BaseName, - }, - BuildConditions = { - { BMBC, 'BaseEngineersEnabled', { self.BaseName } }, - { BMBC, 'BaseBuildingEngineers', { self.BaseName } }, - { BMBC, 'HighestFactoryLevel', { i, self.BaseName } }, - { BMBC, 'FactoryCountAndNeed', { i, j, pType, self.BaseName } }, - { BMBC, 'BaseActive', { self.BaseName } }, - }, - PlatoonBuildCallbacks = { { BMBC, 'BaseManagerEngineersStarted' }, }, - InstanceCount = 3, - BuildTimeOut = 10, -- Timeout really fast because they dont need to really finish - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) +--- Checks if the unit can be re/build based on the rebuild difficulty counter. +---@param location Vector +---@param buildCounter table> +---@return boolean +function BM:CheckUnitBuildCounter(location, buildCounter) + local xData = buildCounter[location[1]] + if xData then + local yData = xData[location[2]] + if yData and (yData.Counter > 0 or yData.Counter == -1) then + return true + end + end + + return false +end + +---@param unitName string +---@return boolean +function BM:DecrementUnitBuildCounter(unitName) + for _, levelData in pairs(self.LevelNames) do + for _, firstData in pairs(self.AIBrain.BaseTemplates[self.BaseName .. levelData.Name].BuildCounter) do + for _, secondData in pairs(firstData) do + if secondData.UnitName == unitName then + if secondData.Counter > 0 then + secondData.Counter = secondData.Counter - 1 + end + return true end end end - end, - - ---@param self BaseManager - LoadDefaultBaseCDRs = function(self) - -- CDR Build - local defaultBuilder = { - BuilderName = 'BaseManager_CDRPlatoon_' .. self.BaseName, - PlatoonTemplate = self:CreateCommanderPlatoonTemplate(), + end + + return false +end + +---@alias BaseManagerFunctionality +---| "Shields" +---| "Fabrication" +---| "Intel" +---| "CounterIntel" +---| "TML" +---| "Nuke" +---| "Patrol" +---| "Reclaim" +---| "LandScouting" +---| "AirScouting" + +--- Toggle functionality of base parts through functions +---@param actType BaseManagerFunctionality +---@param val boolean +function BM:SetActive(actType, val) + local fnName = actType .. 'Active' + local fn = self.ActivationFunctions[fnName] + if not fn then + error('*AI DEBUG: Invalid Activation type type - ' .. actType, 2) + end + fn(self, val) +end + +---@alias BaseManagerBuildFunctionality +---| 'Engineers' +---| 'AntiAir' +---| 'GroundDefense' +---| 'Torpedo' +---| 'AirFactories' +---| 'LandFactories' +---| 'SeaFactories' +---| 'Factories' +---| 'MissileDefense' +---| 'Shields' +---| 'Artillery' +---| 'Experimentals' +---| 'Walls' +---| 'Defenses' +---| 'Jammers' +---| 'Radar' +---| 'Sonar' +---| 'Intel' +---| 'Missiles' +---| 'Fabrication' +---| 'AirStaging' +---| 'MassExtraction' +---| 'EnergyProduction' +---| 'Storage' + +--- Toggle what structures can the base build. All are enabled by default. +---@param buildType BaseManagerBuildFunctionality +---@param val boolean +function BM:SetBuild(buildType, val) + if not self.Active then return end + + local fnName = 'Build' .. buildType + local fn = self.BuildFunctions[fnName] + if not fn then + error('*AI DEBUG: Invalid build type - ' .. buildType, 2) + end + fn(self, val) +end + +--- Toggle building of all structures in the base +---@param val boolean +function BM:SetBuildAllStructures(val) + for k, v in self.BuildFunctions do + if k ~= 'BuildEngineers' then + v(self, val) + end + end +end + +--#endregion +--#region - Default builders for base managers + +---@protected +function BM:LoadDefaultBaseEngineers() + local defaultBuilder + -- The Engineer AI Thread + for i = 1, 3 do + defaultBuilder = { + BuilderName = 'T' .. i .. 'BaseManager_EngineersWork_' .. self.BaseName, + PlatoonTemplate = self:CreateEngineerPlatoonTemplate(i), Priority = 1, - PlatoonType = 'Any', - RequiresConstruction = false, - LocationType = self.BaseName, - PlatoonAddFunctions = { - -- {'/lua/ai/opai/OpBehaviors.lua', 'CDROverchargeBehavior'}, -- TODO: Re-add once it doesnt interfere with BM engineer thread - { BMPT, 'UnitUpgradeBehavior' }, - }, - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerSingleEngineerPlatoon' }, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerEngineerPlatoonSplit' }, BuildConditions = { - { BMBC, 'BaseActive', { self.BaseName } }, + { BMBC, 'BaseManagerNeedsEngineers', { self.BaseName } }, + { BMBC, 'BaseActive', { self.BaseName } }, }, PlatoonData = { BaseName = self.BaseName, }, - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) - end, - - ---@param self BaseManager - LoadDefaultBaseSupportCDRs = function(self) - -- sCDR Build - local defaultBuilder = { - BuilderName = 'BaseManager_sCDRPlatoon_' .. self.BaseName, - PlatoonTemplate = self:CreateSupportCommanderPlatoonTemplate(), - Priority = 1, PlatoonType = 'Any', RequiresConstruction = false, LocationType = self.BaseName, - PlatoonAddFunctions = { - { BMPT, 'UnitUpgradeBehavior' }, - }, - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerSingleEngineerPlatoon' }, - BuildConditions = { - { BMBC, 'BaseActive', { self.BaseName } }, - }, - PlatoonData = { - BaseName = self.BaseName, - }, } self.AIBrain:PBMAddPlatoon(defaultBuilder) + end + + -- Disband platoons - engineers built here + for i = 1, 3 do + for j = 1, 5 do + for num, pType in { 'Air', 'Land', 'Sea' } do + defaultBuilder = { + BuilderName = 'T' .. i .. 'BaseManagerEngineerDisband_' .. j .. 'Count_' .. self.BaseName, + PlatoonAIPlan = 'DisbandAI', + PlatoonTemplate = self:CreateEngineerPlatoonTemplate(i, j), + Priority = 300 * i, + PlatoonType = pType, + RequiresConstruction = true, + LocationType = self.BaseName, + PlatoonData = { + NumBuilding = j, + BaseName = self.BaseName, + }, + BuildConditions = { + { BMBC, 'BaseEngineersEnabled', { self.BaseName } }, + { BMBC, 'BaseBuildingEngineers', { self.BaseName } }, + { BMBC, 'HighestFactoryLevel', { i, self.BaseName } }, + { BMBC, 'FactoryCountAndNeed', { i, j, pType, self.BaseName } }, + { BMBC, 'BaseActive', { self.BaseName } }, + }, + PlatoonBuildCallbacks = { { BMBC, 'BaseManagerEngineersStarted' }, }, + InstanceCount = 3, + BuildTimeOut = 10, -- Timeout really fast because they dont need to really finish + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) + end + end + end +end + +---@protected +function BM:LoadDefaultBaseCDRs() + -- CDR Build + local defaultBuilder = { + BuilderName = 'BaseManager_CDRPlatoon_' .. self.BaseName, + PlatoonTemplate = self:CreateCommanderPlatoonTemplate(), + Priority = 1, + PlatoonType = 'Any', + RequiresConstruction = false, + LocationType = self.BaseName, + PlatoonAddFunctions = { + -- {'/lua/ai/opai/OpBehaviors.lua', 'CDROverchargeBehavior'}, -- TODO: Re-add once it doesnt interfere with BM engineer thread + { BMPT, 'UnitUpgradeBehavior' }, + }, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerSingleEngineerPlatoon' }, + BuildConditions = { + { BMBC, 'BaseActive', { self.BaseName } }, + }, + PlatoonData = { + BaseName = self.BaseName, + }, + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) +end + +---@protected +function BM:LoadDefaultBaseSupportCDRs() + -- sCDR Build + local defaultBuilder = { + BuilderName = 'BaseManager_sCDRPlatoon_' .. self.BaseName, + PlatoonTemplate = self:CreateSupportCommanderPlatoonTemplate(), + Priority = 1, + PlatoonType = 'Any', + RequiresConstruction = false, + LocationType = self.BaseName, + PlatoonAddFunctions = { + { BMPT, 'UnitUpgradeBehavior' }, + }, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerSingleEngineerPlatoon' }, + BuildConditions = { + { BMBC, 'BaseActive', { self.BaseName } }, + }, + PlatoonData = { + BaseName = self.BaseName, + }, + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) + + -- Disband platoon + defaultBuilder = { + BuilderName = 'BaseManager_sACUDisband_' .. self.BaseName, + PlatoonAIPlan = 'DisbandAI', + PlatoonTemplate = self:CreateSupportCommanderPlatoonTemplate(), + Priority = 900, + PlatoonType = 'Gate', + RequiresConstruction = true, + LocationType = self.BaseName, + BuildConditions = { + { BMBC, 'BaseEngineersEnabled', { self.BaseName } }, + { BMBC, 'NumUnitsLessNearBase', + { self.BaseName, ParseEntityCategory('SUBCOMMANDER'), self.BaseName .. '_sACUNumber' } }, + { BMBC, 'BaseActive', { self.BaseName } }, + }, + InstanceCount = 2, + BuildTimeOut = 10, -- Timeout really fast because they dont need to really finish + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) +end - -- Disband platoon +---@protected +function BM:LoadDefaultScoutingPlatoons() + -- Land Scouts + local defaultBuilder = { + BuilderName = 'BaseManager_LandScout_' .. self.BaseName, + PlatoonTemplate = self:CreateLandScoutPlatoon(), + Priority = 500, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerScoutingAI' }, + BuildConditions = { + { BMBC, 'LandScoutingEnabled', { self.BaseName, } }, + { BMBC, 'BaseActive', { self.BaseName } }, + }, + PlatoonData = { + BaseName = self.BaseName, + }, + PlatoonType = 'Land', + RequiresConstruction = true, + LocationType = self.BaseName, + InstanceCount = 1, + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) + + -- T1-T3 Air Scouts + for i = 1, 3 do defaultBuilder = { - BuilderName = 'BaseManager_sACUDisband_' .. self.BaseName, - PlatoonAIPlan = 'DisbandAI', - PlatoonTemplate = self:CreateSupportCommanderPlatoonTemplate(), - Priority = 900, - PlatoonType = 'Gate', - RequiresConstruction = true, - LocationType = self.BaseName, - BuildConditions = { - { BMBC, 'BaseEngineersEnabled', { self.BaseName } }, - { BMBC, 'NumUnitsLessNearBase', - { self.BaseName, ParseEntityCategory('SUBCOMMANDER'), self.BaseName .. '_sACUNumber' } }, - { BMBC, 'BaseActive', { self.BaseName } }, - }, - InstanceCount = 2, - BuildTimeOut = 10, -- Timeout really fast because they dont need to really finish - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) - end, - - ---@param self BaseManager - LoadDefaultScoutingPlatoons = function(self) - -- Land Scouts - local defaultBuilder = { - BuilderName = 'BaseManager_LandScout_' .. self.BaseName, - PlatoonTemplate = self:CreateLandScoutPlatoon(), - Priority = 500, + BuilderName = 'BaseManager_T' .. i .. 'AirScout_' .. self.BaseName, + PlatoonTemplate = self:CreateAirScoutPlatoon(i), + Priority = 250 + (i * 250), -- 500, 750, 1000 for T1-3 PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerScoutingAI' }, BuildConditions = { - { BMBC, 'LandScoutingEnabled', { self.BaseName, } }, - { BMBC, 'BaseActive', { self.BaseName } }, + { BMBC, 'HighestFactoryLevelType', { i, self.BaseName, 'Air' } }, + { BMBC, 'AirScoutingEnabled', { self.BaseName, } }, + { BMBC, 'BaseActive', { self.BaseName } }, }, PlatoonData = { BaseName = self.BaseName, }, - PlatoonType = 'Land', + PlatoonType = 'Air', RequiresConstruction = true, LocationType = self.BaseName, InstanceCount = 1, } self.AIBrain:PBMAddPlatoon(defaultBuilder) + end +end - -- T1-T3 Air Scouts - for i = 1, 3 do - defaultBuilder = { - BuilderName = 'BaseManager_T' .. i ..'AirScout_' .. self.BaseName, - PlatoonTemplate = self:CreateAirScoutPlatoon(i), - Priority = 250 + (i * 250), -- 500, 750, 1000 for T1-3 - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerScoutingAI' }, - BuildConditions = { - { BMBC, 'HighestFactoryLevelType', { i, self.BaseName, 'Air' } }, - { BMBC, 'AirScoutingEnabled', { self.BaseName, } }, - { BMBC, 'BaseActive', { self.BaseName } }, - }, - PlatoonData = { - BaseName = self.BaseName, - }, - PlatoonType = 'Air', - RequiresConstruction = true, - LocationType = self.BaseName, - InstanceCount = 1, - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) - end - end, - - ---@param self BaseManager - LoadDefaultBaseTMLs = function(self) - local defaultBuilder = { - BuilderName = 'BaseManager_TMLPlatoon_' .. self.BaseName, - PlatoonTemplate = self:CreateTMLPlatoonTemplate(), - Priority = 300, - PlatoonType = 'Any', - RequiresConstruction = false, - LocationType = self.BaseName, - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerTMLPlatoon' }, - BuildConditions = { - { BMBC, 'BaseActive', { self.BaseName } }, - { BMBC, 'TMLsEnabled', { self.BaseName } }, - }, - PlatoonData = { - BaseName = self.BaseName, - }, - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) - end, - - ---@param self BaseManager - LoadDefaultBaseNukes = function(self) - local defaultBuilder = { - BuilderName = 'BaseManager_NukePlatoon_' .. self.BaseName, - PlatoonTemplate = self:CreateNukePlatoonTemplate(), - Priority = 400, - PlatoonType = 'Any', - RequiresConstruction = false, - LocationType = self.BaseName, - PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerNukePlatoon' }, - BuildConditions = { - { BMBC, 'BaseActive', { self.BaseName } }, - { BMBC, 'NukesEnabled', { self.BaseName } }, - }, - PlatoonData = { - BaseName = self.BaseName, - }, - } - self.AIBrain:PBMAddPlatoon(defaultBuilder) - end, - - ---@param self BaseManager - ---@return PlatoonTemplate - CreateTMLPlatoonTemplate = function(self) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'TMLTemplate', - 'NoPlan', - { 'ueb2108', 1, 1, 'Attack', 'None' }, - } - template = ScenarioUtils.FactionConvert(template, faction) - - return template - end, - - ---@param self BaseManager - ---@return PlatoonTemplate - CreateNukePlatoonTemplate = function(self) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'NukeTemplate', - 'NoPlan', - { 'ueb2305', 1, 1, 'Attack', 'None' }, - } - template = ScenarioUtils.FactionConvert(template, faction) - - return template - end, - - ---@param self BaseManager - ---@return PlatoonTemplate - CreateLandScoutPlatoon = function(self) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'LandScoutTemplate', - 'NoPlan', - { 'uel0101', -1, 1, 'Scout', 'None' }, - } - template = ScenarioUtils.FactionConvert(template, faction) - - return template - end, - - ---@param self BaseManager - ---@param techLevel number - ---@return PlatoonTemplate - CreateAirScoutPlatoon = function(self, techLevel) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'AirScoutTemplate', - 'NoPlan', - { 'uea', -1, 1, 'Scout', 'None' }, - } +---@protected +function BM:LoadDefaultBaseTMLs() + local defaultBuilder = { + BuilderName = 'BaseManager_TMLPlatoon_' .. self.BaseName, + PlatoonTemplate = self:CreateTMLPlatoonTemplate(), + Priority = 300, + PlatoonType = 'Any', + RequiresConstruction = false, + LocationType = self.BaseName, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerTMLPlatoon' }, + BuildConditions = { + { BMBC, 'BaseActive', { self.BaseName } }, + { BMBC, 'TMLsEnabled', { self.BaseName } }, + }, + PlatoonData = { + BaseName = self.BaseName, + }, + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) +end - if techLevel == 3 then - template[3][1] = template[3][1] .. '0302' - else - template[3][1] = template[3][1] .. '0101' - end +---@protected +function BM:LoadDefaultBaseNukes() + local defaultBuilder = { + BuilderName = 'BaseManager_NukePlatoon_' .. self.BaseName, + PlatoonTemplate = self:CreateNukePlatoonTemplate(), + Priority = 400, + PlatoonType = 'Any', + RequiresConstruction = false, + LocationType = self.BaseName, + PlatoonAIFunction = { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'BaseManagerNukePlatoon' }, + BuildConditions = { + { BMBC, 'BaseActive', { self.BaseName } }, + { BMBC, 'NukesEnabled', { self.BaseName } }, + }, + PlatoonData = { + BaseName = self.BaseName, + }, + } + self.AIBrain:PBMAddPlatoon(defaultBuilder) +end - template = ScenarioUtils.FactionConvert(template, faction) +---@protected +---@return PlatoonTemplate +function BM:CreateTMLPlatoonTemplate() + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'TMLTemplate', + 'NoPlan', + { 'ueb2108', 1, 1, 'Attack', 'None' }, + } + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end - return template - end, +---@protected +---@return PlatoonTemplate +function BM:CreateNukePlatoonTemplate() + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'NukeTemplate', + 'NoPlan', + { 'ueb2305', 1, 1, 'Attack', 'None' }, + } + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end - ---@param self BaseManager - ---@return PlatoonTemplate - CreateCommanderPlatoonTemplate = function(self) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'CommanderTemplate', - 'NoPlan', - { 'uel0001', 1, 1, 'Support', 'None' }, - } - template = ScenarioUtils.FactionConvert(template, faction) - - return template - end, - - ---@param self BaseManager - ---@return PlatoonTemplate - CreateSupportCommanderPlatoonTemplate = function(self) - local faction = self.AIBrain:GetFactionIndex() - local template = { - 'CommanderTemplate', - 'NoPlan', - { 'uel0301', 1, 1, 'Support', 'None' }, - } - template = ScenarioUtils.FactionConvert(template, faction) - - return template - end, - - ---@param self BaseManager - ---@param techLevel number - ---@param platoonSize? number Defaults to 5 - ---@return PlatoonTemplate - CreateEngineerPlatoonTemplate = function(self, techLevel, platoonSize) - local faction = self.AIBrain:GetFactionIndex() - local size = platoonSize or 5 - local template = { - 'EngineerThing', - 'NoPlan', - { 'uel', 1, size, 'Support', 'None' }, - } +---@protected +---@return PlatoonTemplate +function BM:CreateLandScoutPlatoon() + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'LandScoutTemplate', + 'NoPlan', + { 'uel0101', -1, 1, 'Scout', 'None' }, + } + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end - if techLevel == 1 then - template[3][1] = template[3][1] .. '0105' - elseif techLevel == 2 then - template[3][1] = template[3][1] .. '0208' - else - template[3][1] = template[3][1] .. '0309' - end +---@protected +---@param techLevel number +---@return PlatoonTemplate +function BM:CreateAirScoutPlatoon(techLevel) + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'AirScoutTemplate', + 'NoPlan', + { 'uea', -1, 1, 'Scout', 'None' }, + } + + if techLevel == 3 then + template[3][1] = template[3][1] .. '0302' + else + template[3][1] = template[3][1] .. '0101' + end - template = ScenarioUtils.FactionConvert(template, faction) + template = ScenarioUtils.FactionConvert(template, faction) - return template - end, -} + return template +end + +---@protected +---@return PlatoonTemplate +function BM:CreateCommanderPlatoonTemplate() + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'CommanderTemplate', + 'NoPlan', + { 'uel0001', 1, 1, 'Support', 'None' }, + } + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end + +---@protected +---@return PlatoonTemplate +function BM:CreateSupportCommanderPlatoonTemplate() + local faction = self.AIBrain:GetFactionIndex() + local template = { + 'CommanderTemplate', + 'NoPlan', + { 'uel0301', 1, 1, 'Support', 'None' }, + } + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end + +---@protected +---@param techLevel number +---@param platoonSize? number # Defaults to 5 +---@return PlatoonTemplate +function BM:CreateEngineerPlatoonTemplate(techLevel, platoonSize) + local faction = self.AIBrain:GetFactionIndex() + local size = platoonSize or 5 + local template = { + 'EngineerThing', + 'NoPlan', + { 'uel', 1, size, 'Support', 'None' }, + } + + if techLevel == 1 then + template[3][1] = template[3][1] .. '0105' + elseif techLevel == 2 then + template[3][1] = template[3][1] .. '0208' + else + template[3][1] = template[3][1] .. '0309' + end + + template = ScenarioUtils.FactionConvert(template, faction) + + return template +end + +BaseManager = ClassSimple(BM) --- Prepares a base manager, note that you still need to call one of the Start functions --- If no params are provided, the base manager is only created, but not initialized ---@param brain AIBrain ---@param baseName string ----@param markerName Marker +---@param position MarkerName|Vector ---@param radius number ---@param levelTable table ---@return BaseManager ---@overload fun(): BaseManager -function CreateBaseManager(brain, baseName, markerName, radius, levelTable) - - ---@type BaseManager +function CreateBaseManager(brain, baseName, position, radius, levelTable) local bManager = BaseManager() bManager:Create() - if brain and baseName and markerName and radius then - bManager:Initialize(brain--[[@as CampaignAIBrain]], baseName, markerName, radius, levelTable) + if brain and baseName and position and radius then + bManager:Initialize(brain --[[@as CampaignAIBrain]], baseName, position, radius, levelTable) end return bManager -end \ No newline at end of file +end + +--- Failsafe callback function when a structure marked for needing an upgrade starts building something +--- If that 'something' is the upgrade itself, create a callback for the upgrade +---@param unit Unit +---@param unitBeingBuilt Unit +function FailSafeStructureOnStartBuild(unit, unitBeingBuilt) + -- If we are in the upgrading state, then it's the upgrade we want under normal circumstances. + -- We don't use different upgrades paths for coop, only that of the original SCFA (no Support Factory upgrade paths whatsoever) + -- If you decide to mess around with AI armies in cheat mode, and order a newly added upgrade path instead anyway, then any mishaps happening afterwards is on you! + if unit:IsUnitState('Upgrading') then + unitBeingBuilt.UnitName = unit.UnitName + unitBeingBuilt.BaseName = unit.BaseName + + -- Add callback when the upgrade is finished + if not unitBeingBuilt.AddedFinishedCallback then + unitBeingBuilt:AddUnitCallback(FailSafeUpgradeOnStopBeingBuilt, 'OnStopBeingBuilt') + unitBeingBuilt.AddedFinishedCallback = true + end + end +end + +--- Failsafe function that will upgrade factories, radar, etc. to next level +---@param unit Unit +---@param upgradeID UnitId # Blueprint +function FailSafeUpgradeBaseManagerStructure(unit, upgradeID) + -- Add callback when the structure starts building something + if not unit.AddedUpgradeCallback then + unit:AddOnStartBuildCallback(FailSafeStructureOnStartBuild) + unit.AddedUpgradeCallback = true + end + + IssueUpgrade({ unit }, upgradeID) + unit.SetToUpgrade = true +end + +--- Failsafe callback function when a structure upgrade is finished building +--- Updates the ScenarioInfo.UnitNames table with the new unit, and upgrades further if needed +---@param unit Unit +function FailSafeUpgradeOnStopBeingBuilt(unit) + local aiBrain = unit.Brain --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[unit.BaseName] + + if bManager then + local armyIndex = aiBrain:GetArmyIndex() + ScenarioInfo.UnitNames[armyIndex][unit.UnitName] = unit + + local factionIndex = aiBrain:GetFactionIndex() + local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) + + -- Check if our structure can even upgrade to begin with + if upgradeID then + -- Check if the BM is supposed to upgrade this structure further + for index, structure in pairs(bManager.UpgradeTable) do + -- If the names match, and the IDs don't, we need to upgrade + if unit.UnitName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then + FailSafeUpgradeBaseManagerStructure(unit, upgradeID) + end + end + end + end +end + +--#endregion diff --git a/lua/AI/OpAI/BaseManagerPlatoonThreads.lua b/lua/AI/OpAI/BaseManagerPlatoonThreads.lua index c8ad75377ff..12cfdea381a 100644 --- a/lua/AI/OpAI/BaseManagerPlatoonThreads.lua +++ b/lua/AI/OpAI/BaseManagerPlatoonThreads.lua @@ -14,363 +14,104 @@ local SUtils = import("/lua/ai/sorianutilities.lua") local TriggerFile = import("/lua/scenariotriggers.lua") local Buff = import("/lua/sim/buff.lua") local BMBC = import("/lua/editor/basemanagerbuildconditions.lua") -local MIBC = import("/lua/editor/miscbuildconditions.lua") local UpgradeTemplates = import("/lua/upgradetemplates.lua") +local Utilities = import("/lua/utilities.lua") -- Upvalued for performance +local pairs, ipairs = pairs, ipairs +local type, unpack = type, unpack local TableEmpty = table.empty local TableInsert = table.insert local TableRemove = table.remove local TableGetn = table.getn - ---- Split the platoon into single unit platoons ----@param platoon Platoon -function BaseManagerEngineerPlatoonSplit(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local units = platoon:GetPlatoonUnits() - local baseName = platoon.PlatoonData.BaseName - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then - aiBrain:DisbandPlatoon(platoon) - return - end - for _, v in units do - if not v.Dead then - -- Make sure current base manager isn't at capacity of engineers - if EntityCategoryContains(categories.ENGINEER, v) and bManager.EngineerQuantity > bManager.CurrentEngineerCount then - if bManager.EngineerBuildRateBuff then - Buff.ApplyBuff(v, bManager.EngineerBuildRateBuff) - end - - local engPlat = aiBrain:MakePlatoon('', '') - aiBrain:AssignUnitToPlatoon(engPlat, v, 'Support', 'None') - engPlat:SetPlatoonData(platoon.PlatoonData) - v.BaseName = baseName - engPlat:ForkAIThread(BaseManagerSingleEngineerPlatoon) - - -- If engineer is not a commander or sub-commander, increment number of units working for the base - -- set up death trigger for the engineer - if not EntityCategoryContains(categories.COMMAND + categories.SUBCOMMANDER, v) then - bManager:AddCurrentEngineer() - - -- Only add death callback if it hasnt been set yet - if not v.Subtracted then - TriggerFile.CreateUnitDestroyedTrigger(BaseManagerSingleDestroyed, v) - end - - -- If the base is building engineers, subtract one from the amount being built - if bManager:GetEngineersBuilding() > 0 then - bManager:SetEngineersBuilding(-1) - end - end - end - end - end - aiBrain:DisbandPlatoon(platoon) -end - ---- Callback function when an engineering unit starts building something ---- If that 'something' is a named unit the Engineer was told to build, then it caches the necessary data on the named unit ---- Also resets the unit name we cached on the Engineer, so if any mishaps happen, the Engineer can just build something else afterwards ---- We handle cases of BuildingUnitName and ConditionalUnitName, the former is for Base Manager structures, the later is for Conditional Builds (ie. experimentals) ---- Handles special case where a guarding Engineer initiates the construction INSTEAD of our primary Engineer ---- Also handles BM upgrades for instant structure upgrading, this is to avoid issues with PBM factory assigning ----@param unit Unit ----@param unitBeingBuilt Unit -function EngineerOnStartBuild(unit, unitBeingBuilt) - -- Normally the Engineer we issued the build order for starts the initial construction, however if it has guarding Engineers, they might do so instead - -- We don't queue several build related orders in the campaign enviroment, so if the Engineer that initialized construction is currently guarding another, it's the ONLY order it has right now - -- If we are guarding something, overwrite the origin unit to that, we don't copy over any data, simply treat this Engineer as the primary one, and reset data on the actual primary Engineer - local Guardee = unit:GetGuardedUnit() - if Guardee and not Guardee.Dead then - unit = Guardee - end - - -- We cached the name of the unit we want to build, and the Engineer's BaseManager, 1st when we ordered the Engineer to build, 2nd when the Engineer's platoon was formed - local StructureUnitName = unit.BuildingUnitName - local ExperimentalUnitName = unit.ConditionalBuildUnitName - local BaseName = unit.BaseName - local aiBrain = unit.Brain--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[BaseName] - local armyIndex = aiBrain:GetArmyIndex() - - -- First check if we were told to build a BM structure, if not, check if it's a CB - if StructureUnitName and bManager then - -- Cache names to the structure - unitBeingBuilt.UnitName = StructureUnitName - unitBeingBuilt.BaseName = BaseName - - -- Flag the structure as unfinished so the BM can maintain its construction - bManager.UnfinishedBuildings[StructureUnitName] = true - - -- Update the global UnitNames table so the BM knows the structure exists - ScenarioInfo.UnitNames[armyIndex][StructureUnitName] = unitBeingBuilt - - -- Reset the cached unit name, so a new one can be picked right after - unit.BuildingUnitName = nil - - -- Register callbacks - if not unitBeingBuilt.AddedCompletionCallback then - unitBeingBuilt:AddUnitCallback(InitialStructureBuildSuccessful, 'OnStopBeingBuilt') - unitBeingBuilt.AddedCompletionCallback = true - end - elseif ExperimentalUnitName and bManager then - -- Restore the index saved in the CanConditionalBuild call - local buildIndex = bManager.ConditionalBuildData.Index - local selectedBuild = bManager.ConditionalBuildTable[buildIndex] - - -- Store the unit - bManager.ConditionalBuildData.Unit = unitBeingBuilt - - -- If we're supposed to keep a certain number of these guys in the field, store the info on him so he can reinsert himself in the conditional build table when he bites it. - if selectedBuild.data.KeepAlive then - unitBeingBuilt.KeepAlive = true - unitBeingBuilt.ConditionalBuild = selectedBuild - unitBeingBuilt.ConditionalBuildData = bManager.ConditionalBuildData - - -- Register rebuild callback - TriggerFile.CreateUnitDestroyedTrigger(ConditionalBuildDied, unitBeingBuilt) - end - - -- Cache names to the unit - unitBeingBuilt.UnitName = ExperimentalUnitName - unitBeingBuilt.BaseName = BaseName - - -- Set variables so other Engineers can see what's going on - bManager.ConditionalBuildData.IsInitiated = false - bManager.ConditionalBuildData.IsBuilding = true - - -- Reset the cached unit name, so a new one can be picked right after - unit.ConditionalBuildUnitName = nil - - -- Register callbacks - if not unitBeingBuilt.AddedCompletionCallback then - unitBeingBuilt:AddUnitCallback(ConditionalBuildSuccessful, 'OnStopBeingBuilt') - unitBeingBuilt.AddedCompletionCallback = true - end - end -end - ---- Callback function when an engineering unit failed to build something, this is called in several cases ---- Resets the unit name we assigned to the Engineer, so in case it's still alive, it can start building something else ----@param unit Unit -function EngineerOnFailedToBuild(unit) - unit.ConditionalBuildUnitName = nil - unit.BuildingUnitName = nil -end - ---- Callback function when a structure is initially finished building ---- Marks the unit as finished for the unit's BaseManager, so Engineers won't try to finish it (infinite loop of repair orders on a full health unit), and decrements the amount of times it can be rebuilt ----@param unit Unit -function InitialStructureBuildSuccessful(unit) - local aiBrain = unit.Brain--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[unit.BaseName] - - if bManager then - local StructureName = unit.UnitName - - if bManager.UnfinishedBuildings[StructureName] then - bManager.UnfinishedBuildings[StructureName] = nil - bManager:DecrementUnitBuildCounter(StructureName) - end - - local factionIndex = aiBrain:GetFactionIndex() - local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) - - -- Check if our structure can even upgrade to begin with - if upgradeID then - -- Check if the BM is supposed to upgrade this structure - for index, structure in bManager.UpgradeTable do - -- If the names match, and the IDs don't, we need to upgrade - if StructureName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then - UpgradeBaseManagerStructure(unit, upgradeID) - end - end - end - end -end - ---- Function that will upgrade factories, radar, etc. to next level ----@param unit Unit ----@param upgradeID BlueprintId -function UpgradeBaseManagerStructure(unit, upgradeID) - -- Add callback when the structure starts building something - if not unit.AddedUpgradeCallback then - unit:AddOnStartBuildCallback(StructureOnStartBuild) - unit.AddedUpgradeCallback = true - end - - IssueUpgrade({unit}, upgradeID) - unit.SetToUpgrade = true -end - ---- Callback function when a structure marked for needing an upgrade starts building something ---- If that 'something' is the upgrade itself, create a callback for the upgrade ----@param unit Unit ----@param unitBeingBuilt Unit -function StructureOnStartBuild(unit, unitBeingBuilt) - -- If we are in the upgrading state, then it's the upgrade we want under normal circumstances. - -- We don't use different upgrades paths for coop, only that of the original SCFA (no Support Factory upgrade paths whatsoever) - -- If you decide to mess around with AI armies in cheat mode, and order a newly added upgrade path instead anyway, then any mishaps happening afterwards is on you! - if unit:IsUnitState('Upgrading') then - --LOG('Structure building upgrade named: ' .. tostring(unit.UnitName)) - unitBeingBuilt.UnitName = unit.UnitName - unitBeingBuilt.BaseName = unit.BaseName - - -- Add callback when the upgrade is finished - if not unitBeingBuilt.AddedFinishedCallback then - unitBeingBuilt:AddUnitCallback(UpgradeOnStopBeingBuilt, 'OnStopBeingBuilt') - unitBeingBuilt.AddedFinishedCallback = true - end - end -end - ---- Callback function when a structure upgrade is finished building ---- Updates the ScenarioInfo.UnitNames table with the new unit, and upgrades further if needed ----@param unit Unit -function UpgradeOnStopBeingBuilt(unit) - local aiBrain = unit.Brain--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[unit.BaseName] - if bManager then - --LOG('Structure finished upgrade named: ' .. tostring(unit.UnitName)) - local armyIndex = aiBrain:GetArmyIndex() - ScenarioInfo.UnitNames[armyIndex][unit.UnitName] = unit - - local factionIndex = aiBrain:GetFactionIndex() - local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) - - -- Check if our structure can even upgrade to begin with - if upgradeID then - -- Check if the BM is supposed to upgrade this structure further - for index, structure in bManager.UpgradeTable do - -- If the names match, and the IDs don't, we need to upgrade - if unit.UnitName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then - UpgradeBaseManagerStructure(unit, upgradeID) - end - end - end - end -end - ---- Death callback when units die to decrease counter ----@param unit Unit -function BaseManagerSingleDestroyed(unit) - if not unit.Subtracted then - unit.Subtracted = true - local aiBrain = unit:GetAIBrain()--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[unit.BaseName] - bManager:SubtractCurrentEngineer() - end -end +local ForkThread = ForkThread +local EntityCategoryContains, ParseEntityCategory = EntityCategoryContains, ParseEntityCategory +local WaitTicks, WaitSeconds = WaitTicks, WaitSeconds +local IssueToUnitMove = IssueToUnitMove +local IssueToUnitClearCommands = IssueToUnitClearCommands +local IssueToUnitStop = IssueToUnitStop +local IssueGuard = IssueGuard +local IssueNuke = IssueNuke +local IssueRepair = IssueRepair +local IssueScript = IssueScript +local IssueTactical = IssueTactical +local IssueUpgrade = IssueUpgrade + +local categoriesENGINEER = categories.ENGINEER +local categoriesFACTORY = categories.FACTORY +local categoriesCONSTRUCTION = categories.CONSTRUCTION +local categoriesAIR = categories.AIR +local categoriesLAND = categories.LAND +local categoriesCOMMAND = categories.COMMAND +local categoriesSUBCOMMANDER = categories.SUBCOMMANDER +local categoriesSTRUCTURE = categories.STRUCTURE + +---@class BMAssistPlatoonData +---@field Time integer|nil Min time in ticks the engineer will try to find and assist things. After this time it can find something else to do. +---@field AssistRange integer|nil Range in which the engineer can find things to assist. Defaults to base manager's `Radius` +---@field BeingBuiltCategories string[]|nil + +--- Possible data the engineer platoon can have +---@class BMEngineerPlatoonData +---@field BaseName string Name of the base manager +---@field Assist BMAssistPlatoonData|nil --- Callback when unit is removed from base manager ---@param unit Unit function BaseManagerSingleRemoved(unit) - local aiBrain = unit:GetAIBrain()--[[@as CampaignAIBrain]] + local aiBrain = unit:GetAIBrain() --[[@as CampaignAIBrain]] local bManager = aiBrain.BaseManagers[unit.BaseName] bManager:SubtractCurrentEngineer() end ---- Main function for Base Manager Engineers ----@param platoon Platoon -function BaseManagerSingleEngineerPlatoon(platoon) - platoon.PlatoonData.DontDisband = true - - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local baseName = platoon.PlatoonData.BaseName - local bManager = aiBrain.BaseManagers[baseName] - local unit = platoon:GetPlatoonUnits()[1] - local canPermanentAssist = EntityCategoryContains(categories.ENGINEER - (categories.COMMAND + categories.SUBCOMMANDER), unit) - local commandUnit = EntityCategoryContains(categories.COMMAND + categories.SUBCOMMANDER, unit) - unit.BaseName = baseName - - -- Add build callbacks for these Engineers - if not unit.AddedBuildCallback then - -- The universal function doesn't work for OnStartBuild, unit.unitBeingBuilt is only accessable after the DoUnitCallback has been executed - unit:AddOnStartBuildCallback(EngineerOnStartBuild) - unit:AddUnitCallback(EngineerOnFailedToBuild, "OnFailedToBuild") - unit.AddedBuildCallback = true - end - - while aiBrain:PlatoonExists(platoon) do - if BMBC.BaseEngineersEnabled(aiBrain, baseName) then - -- Move to expansion base - if not commandUnit and BMBC.ExpansionBasesEnabled(aiBrain, baseName) and BMBC.ExpansionBasesNeedEngineers(aiBrain, baseName) then - ExpansionEngineer(platoon) - - -- Assist a conditional builder under construction - elseif canPermanentAssist and bManager.ConditionalBuildData.Unit and not bManager.ConditionalBuildData.Unit.Dead and bManager.ConditionalBuildData.NeedsMoreBuilders() then - AssistConditionalBuild(platoon) - - -- If we can do a conditional build here, then do it - elseif canPermanentAssist and CanConditionalBuild(platoon) then - DoConditionalBuild(platoon) +--#region Conditional Builds - -- Try to build buildings - elseif BMBC.NeedAnyStructure(aiBrain, baseName) and bManager:GetConstructionEngineerCount() < bManager:GetConstructionEngineerMaximum() then - bManager:AddConstructionEngineer(unit) - TriggerFile.CreateUnitDestroyedTrigger(ConstructionUnitDeath, unit) - BaseManagerEngineerThread(platoon) - bManager:RemoveConstructionEngineer(unit) - - -- Permanent Assist - Assist factories until the unit dies - elseif canPermanentAssist and bManager:NeedPermanentFactoryAssist() then - bManager:IncrementPermanentAssisting() - PermanentFactoryAssist(platoon) - - -- Finish unfinished buildings - elseif BMBC.UnfinishedBuildingsCheck(aiBrain, baseName) then - BuildUnfinishedStructures(platoon) - - -- Try to assist - elseif BMBC.CategoriesBeingBuilt(aiBrain, baseName, {'MOBILE LAND', 'ALLUNITS' }) or(bManager:ConstructionNeedsAssister()) then - BaseManagerAssistThread(platoon) - - -- Try to patrol - elseif BMBC.BasePatrollingEnabled(aiBrain, baseName) and not unit:IsUnitState('Patrolling') then - BaseManagerEngineerPatrol(platoon) - end - end - WaitTicks(75) - end -end +---@class ConditionalBuildUnit: Unit +---@field KeepAlive boolean|nil # Flag that indicates the unit should be rebuilt if it +---@field ConditionalBuild ConditionalBuildEntry # The conditional build entry that spawned this unit +---@field ConditionalBuildManager ConditionalBuildManager # The conditional build data that spawned this unit +---@field UnitName string Unit name from the editor save.lua file +---@field BaseName string Name of the BaseManager that spawned this unit +---@field AddedCompletionCallback boolean|nil --- If there is a conditional build that this engineer can tackle, then this function will return true ---- and the base managers ConditionalBuildData.Index will have the index of ConditionalBuildTable stored in it +--- and the base managers ConditionalBuildManager.Index will have the index of ConditionalBuildTable stored in it ---@param singleEngineerPlatoon Platoon ---@return boolean function CanConditionalBuild(singleEngineerPlatoon) - local aiBrain = singleEngineerPlatoon:GetBrain()--[[@as CampaignAIBrain]] - local baseName = singleEngineerPlatoon.PlatoonData.BaseName + local aiBrain = singleEngineerPlatoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = singleEngineerPlatoon.PlatoonData.BaseName local bManager = aiBrain.BaseManagers[baseName] local engineer = singleEngineerPlatoon:GetPlatoonUnits()[1] engineer.BaseName = baseName -- Is there a build in progress? - if bManager.ConditionalBuildData.IsBuilding then + local cbManager = bManager.ConditionalBuildManager + local cbTable = bManager.ConditionalBuildTable + if cbManager.IsBuilding then -- If there's a build in progress but the unit is dead, reset the variables. - if bManager.ConditionalBuildData.Unit:BeenDestroyed() then - local selectedBuild = bManager.ConditionalBuildTable[bManager.ConditionalBuildData.Index] + if cbManager.Unit:BeenDestroyed() then + local selectedBuild = cbTable[cbManager.Index] -- If we're not supposed to retry, then remove from the conditional build list if not selectedBuild.data.Retry then - TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) + TableRemove(cbTable, cbManager.Index) end - bManager.ConditionalBuildData.Reset() + cbManager:Reset() else return false end end -- Is there a build being initiated (unit is moving to start the build)? - if bManager.ConditionalBuildData.IsInitiated then + if cbManager.IsInitiated then -- Is the initiator is still alive? (If the initiator is dead it means he died before the build was started and we can ignore the IsInitiated flag) - if bManager.ConditionalBuildData.MainBuilder and not bManager.ConditionalBuildData.MainBuilder.Dead then + if cbManager.MainBuilder and not cbManager.MainBuilder.Dead then return false end end - -- Are there no conditional builds? - if TableEmpty(bManager.ConditionalBuildTable) then + -- Are there no conditional builds? + if TableEmpty(cbTable) then return false end @@ -378,19 +119,20 @@ function CanConditionalBuild(singleEngineerPlatoon) local buildIndex = 0 -- Go through the list of conditional builds and see if any of the conditions are met - table.foreachi(bManager.ConditionalBuildTable, function(index, build) + table.foreachi(cbTable, function(index, build) + ---@cast index +integer, -string if buildIndex ~= 0 then return end -- Check if this engineer can build this particular structure if type(build.name) == 'table' then --table of units to build at random for i, unitName in build.name do local unitToBuild = ScenarioUtils.FindUnit(unitName, Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('*CONDITIONAL BUILD ERROR: No unit exists with name ' ..unitName) end + if not unitToBuild then error('*CONDITIONAL BUILD ERROR: No unit exists with name ' .. unitName) end if not engineer:CanBuild(unitToBuild.type) then return end end else local unitToBuild = ScenarioUtils.FindUnit(build.name, Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('*CONDITIONAL BUILD ERROR: No unit exists with name ' ..build.name) end + if not unitToBuild then error('*CONDITIONAL BUILD ERROR: No unit exists with name ' .. build.name) end if not engineer:CanBuild(unitToBuild.type) then return end end @@ -408,7 +150,7 @@ function CanConditionalBuild(singleEngineerPlatoon) -- Condition is true. buildIndex = index - -- If Conditions is an old-style condition table... + -- If Conditions is an old-style condition table... else local conditionsMet = true table.foreachi(Conditions, function(idx, cond) @@ -424,7 +166,6 @@ function CanConditionalBuild(singleEngineerPlatoon) -- Condition is true. buildIndex = index - end end) @@ -432,7 +173,7 @@ function CanConditionalBuild(singleEngineerPlatoon) if buildIndex == 0 then return false end -- Save index for use - bManager.ConditionalBuildData.Index = buildIndex + cbManager.Index = buildIndex return true end @@ -440,22 +181,22 @@ end --- Called when a unit helping on a conditional build bites it ---@param engineer Unit function ConditionalBuilderDead(engineer) - local aiBrain = engineer:GetAIBrain()--[[@as CampaignAIBrain]] + local aiBrain = engineer:GetAIBrain() --[[@as CampaignAIBrain]] local bManager = aiBrain.BaseManagers[engineer.BaseName] - bManager.ConditionalBuildData.DecrementAssisting() + bManager.ConditionalBuildManager:DecrementAssisting() end ----@param conditionalUnit any +---@param conditionalUnit ConditionalBuildUnit function ConditionalBuildDied(conditionalUnit) - local aiBrain = conditionalUnit.Brain + local aiBrain = conditionalUnit.Brain --[[@as CampaignAIBrain]] local bManager = aiBrain.BaseManagers[conditionalUnit.BaseName] local selectedBuild = conditionalUnit.ConditionalBuild -- Reinsert the conditional build (for one of these units) TableInsert(bManager.ConditionalBuildTable, { name = selectedBuild.name, - data = { + data = { MaxAssist = selectedBuild.data.MaxAssist, BuildCondition = selectedBuild.data.BuildCondition, PlatoonAIFunction = selectedBuild.data.PlatoonAIFunction, @@ -469,27 +210,36 @@ function ConditionalBuildDied(conditionalUnit) }) end +--- Unlocks conditional build `name` after `seconds` for rebuilding after death. +---@param name string +---@param seconds integer +local function unlockConditionalBuildThread(name, seconds) + WaitSeconds(seconds) + ScenarioInfo.ConditionalBuildLocks[name] = false +end + +---@param conditionalUnit ConditionalBuildUnit function ConditionalBuildSuccessful(conditionalUnit) - local aiBrain = conditionalUnit.Brain + local aiBrain = conditionalUnit.Brain --[[@as CampaignAIBrain]] local bManager = aiBrain.BaseManagers[conditionalUnit.BaseName] - local selectedBuild = bManager.ConditionalBuildTable[bManager.ConditionalBuildData.Index] - + local selectedBuild = bManager.ConditionalBuildTable[bManager.ConditionalBuildManager.Index] + local selectedData = selectedBuild.data -- Assign AI local newPlatoon = aiBrain:MakePlatoon('', '') aiBrain:AssignUnitToPlatoon(newPlatoon, conditionalUnit, 'Attack', 'None') newPlatoon:StopAI() - newPlatoon:SetPlatoonData(selectedBuild.data.PlatoonData) - - if selectedBuild.data.PlatoonAIFunction then - if type (selectedBuild.data.PlatoonAIFunction) == "function" then - newPlatoon:ForkAIThread(selectedBuild.data.PlatoonAIFunction) - else - newPlatoon:ForkAIThread(import(selectedBuild.data.PlatoonAIFunction[1])[selectedBuild.data.PlatoonAIFunction[2]]) - end + newPlatoon:SetPlatoonData(selectedData.PlatoonData or {}) + + if selectedData.PlatoonAIFunction then + if type(selectedData.PlatoonAIFunction) == "function" then + newPlatoon:ForkAIThread(selectedData.PlatoonAIFunction --[[@as function]]) + else + newPlatoon:ForkAIThread(import(selectedData.PlatoonAIFunction[1])[selectedData.PlatoonAIFunction[2]]) + end end - if selectedBuild.data.FormCallbacks then - for _, callback in selectedBuild.data.FormCallbacks do + if selectedData.FormCallbacks then + for _, callback in pairs(selectedData.FormCallbacks) do if type(callback) == "function" then newPlatoon:ForkThread(callback) else @@ -499,62 +249,55 @@ function ConditionalBuildSuccessful(conditionalUnit) end -- Set up a death wait thing for it to rebuild - if bManager.ConditionalBuildData.WaitSecondsAfterDeath then + local cbManager = bManager.ConditionalBuildManager + local waitTime = cbManager.WaitSecondsAfterDeath + if waitTime then -- If were supposed to wait a certain amount of time before building the unit again, handle that here. + ---@type table ScenarioInfo.ConditionalBuildLocks = ScenarioInfo.ConditionalBuildLocks or {} - ScenarioInfo.ConditionalBuildLocks[selectedBuild.name] = true + local name = selectedBuild.name + ScenarioInfo.ConditionalBuildLocks[name] = true - local waitTime = bManager.ConditionalBuildData.WaitSecondsAfterDeath - - -- Register death callback TriggerFile.CreateUnitDestroyedTrigger(function(unit) - ForkThread(function() - WaitSeconds(waitTime) - ScenarioInfo.ConditionalBuildLocks[selectedBuild.name] = false - end) - end, - conditionalUnit - ) + ForkThread(unlockConditionalBuildThread, name, waitTime) + end, conditionalUnit) end -- Remove from the conditional build list if were not supposed to build any more - if not selectedBuild.data.Amount then - TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) - elseif selectedBuild.data.Amount > 0 then + if not selectedData.Amount then + TableRemove(bManager.ConditionalBuildTable, cbManager.Index) + elseif selectedData.Amount > 0 then -- Decrement the amount left to build - selectedBuild.data.Amount = selectedBuild.data.Amount - 1 + selectedData.Amount = selectedData.Amount - 1 -- If none are left to build, remove from the build table - if selectedBuild.data.Amount == 0 then - TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) + if selectedData.Amount == 0 then + TableRemove(bManager.ConditionalBuildTable, cbManager.Index) end end -- Reset conditional build variables - bManager.ConditionalBuildData.Reset() + cbManager:Reset() end --- Called if there is a conditional build in progress that can be assisted ---@param singleEngineerPlatoon Platoon function AssistConditionalBuild(singleEngineerPlatoon) - local aiBrain = singleEngineerPlatoon:GetBrain()--[[@as CampaignAIBrain]] + local aiBrain = singleEngineerPlatoon:GetBrain() --[[@as CampaignAIBrain]] local baseName = singleEngineerPlatoon.PlatoonData.BaseName local bManager = aiBrain.BaseManagers[baseName] local engineer = singleEngineerPlatoon:GetPlatoonUnits()[1] engineer.BaseName = baseName - -- Restore the index saved in the CanConditionalBuild call - local buildIndex = bManager.ConditionalBuildData.Index + local cbManager = bManager.ConditionalBuildManager + cbManager:IncrementAssisting() -- Register death callback TriggerFile.CreateUnitDestroyedTrigger(ConditionalBuilderDead, engineer) - -- Increment number of units assisting - bManager.ConditionalBuildData.IncrementAssisting() - -- Give orders to repair the unit IssueToUnitClearCommands(engineer) - IssueRepair({engineer}, bManager.ConditionalBuildData.Unit) + IssueRepair({ engineer }, cbManager.Unit) -- Super loop while aiBrain:PlatoonExists(singleEngineerPlatoon) do @@ -572,49 +315,49 @@ end --- Called if there is a conditional build available to start ---@param singleEngineerPlatoon Platoon function DoConditionalBuild(singleEngineerPlatoon) - local aiBrain = singleEngineerPlatoon:GetBrain()--[[@as CampaignAIBrain]] + local aiBrain = singleEngineerPlatoon:GetBrain() --[[@as CampaignAIBrain]] local baseName = singleEngineerPlatoon.PlatoonData.BaseName local bManager = aiBrain.BaseManagers[baseName] local engineer = singleEngineerPlatoon:GetPlatoonUnits()[1] engineer.BaseName = baseName + local cbManager = bManager.ConditionalBuildManager -- Restore the index saved in the CanConditionalBuild call - local buildIndex = bManager.ConditionalBuildData.Index + local buildIndex = cbManager.Index local selectedBuild = bManager.ConditionalBuildTable[buildIndex] -- Get unit plans from the scenario local unitToBuild, unitName if type(selectedBuild.name) == 'table' then - unitName = table.random(selectedBuild.name) + unitName = table.random(selectedBuild.name --[[@as string[] ]]) unitToBuild = ScenarioUtils.FindUnit(unitName, Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('Unit with name "' .. unitName .. '" could not be found for conditional building.') return end + if not unitToBuild then + error('Unit with name "' .. unitName .. '" could not be found for conditional building.') + return + end else - unitName = selectedBuild.name + unitName = selectedBuild.name --[[@as string]] unitToBuild = ScenarioUtils.FindUnit(unitName, Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('Unit with name "' .. unitName .. '" could not be found for conditional building.') return end + if not unitToBuild then + error('Unit with name "' .. unitName .. '" could not be found for conditional building.') + return + end end - -- Initialize variables - bManager.ConditionalBuildData.MainBuilder = engineer - bManager.ConditionalBuildData.NumAssisting = 1 - bManager.ConditionalBuildData.MaxAssisting = selectedBuild.data.MaxAssist or 1 - bManager.ConditionalBuildData.Unit = nil - bManager.ConditionalBuildData.IsInitiated = true -- Prevents other engineers from trying to start their own builds - bManager.ConditionalBuildData.IsBuilding = false - bManager.ConditionalBuildData.WaitSecondsAfterDeath = selectedBuild.data.WaitSecondsAfterDeath + cbManager:OnUnitConstructionRequested(engineer, selectedBuild.data) -- Register death callback TriggerFile.CreateUnitDestroyedTrigger(ConditionalBuilderDead, engineer) -- Issue build orders IssueToUnitClearCommands(engineer) - aiBrain:BuildStructure(engineer, unitToBuild.type, {unitToBuild.Position[1], unitToBuild.Position[3], 0}) - engineer.ConditionalBuildUnitName = unitName + aiBrain:BuildStructure(engineer, unitToBuild.type, { unitToBuild.Position[1], unitToBuild.Position[3], 0 }) + engineer.ConditionalBuildUnitName = unitName -- Enter build monitoring loop, the bulk of the data assigning logic is handled in "EngineerOnStartBuild()" - repeat - WaitTicks(30) - if not aiBrain:PlatoonExists(singleEngineerPlatoon) then + repeat + WaitTicks(30) + if not aiBrain:PlatoonExists(singleEngineerPlatoon) then return end until engineer:IsIdleState() @@ -623,244 +366,520 @@ function DoConditionalBuild(singleEngineerPlatoon) TriggerFile.RemoveUnitTrigger(engineer, ConditionalBuilderDead) end ----@param platoon Platoon -function BaseManagerEngineerPatrol(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] - local chain = bManager:GetDefaultEngineerPatrolChain() +--#endregion - -- Use the default chain or generate random one from factories - if chain then - platoon.PlatoonData.PatrolChain = chain - ScenarioPlatoonAI.PatrolThread(platoon) - else - BaseManagerPatrolLocationFactoriesAI(platoon) - end +--#region Engineer Building Threads + +--- When a unit that was constructing dies +---@param unit Unit +function ConstructionUnitDeath(unit) + local aiBrain = unit:GetAIBrain() --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[unit.BaseName] + bManager:RemoveConstructionEngineer(unit) end ----@param platoon Platoon -function BaseManagerPatrolLocationFactoriesAI(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local baseName = platoon.PlatoonData.BaseName - local bManager = aiBrain.BaseManagers[baseName] +---@class Unit +---@field package Subtracted? true - local factories = aiBrain:PBMGetAllFactories(baseName) - if not factories then - return +--- Death callback when units die to decrease counter +---@param unit Unit +function BaseManagerSingleDestroyed(unit) + if not unit.Subtracted then + unit.Subtracted = true + local aiBrain = unit:GetAIBrain() --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[unit.BaseName] + bManager:SubtractCurrentEngineer() end +end - local posTable = {} - for _, fac in factories do - if not fac.Dead then - TableInsert(posTable, fac:GetPosition()) +--- Callback function when a structure marked for needing an upgrade starts building something +--- If that 'something' is the upgrade itself, create a callback for the upgrade +---@param unit Unit +---@param unitBeingBuilt Unit +function StructureOnStartBuild(unit, unitBeingBuilt) + -- If we are in the upgrading state, then it's the upgrade we want under normal circumstances. + -- We don't use different upgrades paths for coop, only that of the original SCFA (no Support Factory upgrade paths whatsoever) + -- If you decide to mess around with AI armies in cheat mode, and order a newly added upgrade path instead anyway, then any mishaps happening afterwards is on you! + if unit:IsUnitState('Upgrading') then + --LOG('Structure building upgrade named: ' .. tostring(unit.UnitName)) + unitBeingBuilt.UnitName = unit.UnitName + unitBeingBuilt.BaseName = unit.BaseName + + -- Add callback when the upgrade is finished + if not unitBeingBuilt.AddedFinishedCallback then + unitBeingBuilt:AddUnitCallback(UpgradeOnStopBeingBuilt, 'OnStopBeingBuilt') + unitBeingBuilt.AddedFinishedCallback = true end end +end - platoon:Stop() - - local i = 1 - while i <= TableGetn(posTable) do - local facNum = Random(1, TableGetn(posTable))--[[@as integer]] - local movePos = posTable[facNum] - movePos[3] = movePos[3] + 5 +--- Function that will upgrade factories, radar, etc. to next level +---@param unit Unit +---@param upgradeID BlueprintId +function UpgradeBaseManagerStructure(unit, upgradeID) + -- Add callback when the structure starts building something + if not unit.AddedUpgradeCallback then + unit:AddOnStartBuildCallback(StructureOnStartBuild) + unit.AddedUpgradeCallback = true + end - platoon:Patrol(movePos) + IssueUpgrade({ unit }, upgradeID) + unit.SetToUpgrade = true +end - TableRemove(posTable, facNum) +--- Callback function when a structure upgrade is finished building +--- Updates the ScenarioInfo.UnitNames table with the new unit, and upgrades further if needed +---@param unit Unit +function UpgradeOnStopBeingBuilt(unit) + local aiBrain = unit.Brain --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[unit.BaseName] + if bManager then + --LOG('Structure finished upgrade named: ' .. tostring(unit.UnitName)) + local armyIndex = aiBrain:GetArmyIndex() + ScenarioInfo.UnitNames[armyIndex][unit.UnitName] = unit + + local factionIndex = aiBrain:GetFactionIndex() + local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) + + -- Check if our structure can even upgrade to begin with + if upgradeID then + -- Check if the BM is supposed to upgrade this structure further + for index, structure in bManager.UpgradeTable do + -- If the names match, and the IDs don't, we need to upgrade + if unit.UnitName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then + UpgradeBaseManagerStructure(unit, upgradeID) + end + end + end end end ---- When a unit that was constructing dies +--- Callback function when a structure is initially finished building +--- Marks the unit as finished for the unit's BaseManager, so Engineers won't try to finish it (infinite loop of repair orders on a full health unit), and decrements the amount of times it can be rebuilt ---@param unit Unit -function ConstructionUnitDeath(unit) - local aiBrain = unit:GetAIBrain()--[[@as CampaignAIBrain]] +function InitialStructureBuildSuccessful(unit) + local aiBrain = unit.Brain --[[@as CampaignAIBrain]] local bManager = aiBrain.BaseManagers[unit.BaseName] - bManager:RemoveConstructionEngineer(unit) -end ----@param platoon Platoon -function PermanentFactoryAssist(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] - local assistFac = false - local unit = platoon:GetPlatoonUnits()[1] + if bManager then + local StructureName = unit.UnitName - TriggerFile.CreateUnitDestroyedTrigger(PermanentAssisterDead, unit) - while aiBrain:PlatoonExists(platoon) do - -- Get all factories in the base manager - local facs = bManager:GetAllBaseFactories() + if bManager.UnfinishedBuildings[StructureName] then + bManager.UnfinishedBuildings[StructureName] = nil + bManager:DecrementUnitBuildCounter(StructureName) + end - -- Determine the number of guards on all factories - local high, highFac, low, lowFac - for _, v in facs do - if not v.Dead then - local guards = v:GetGuards() - local numGuards = 0 - for gNum, gUnit in guards do - -- Make sure this guy is a permanent assister and not a transient assister - if not gUnit.Dead and not EntityCategoryContains(categories.FACTORY, gUnit) and bManager.PermanentAssisters[gUnit] then - numGuards = numGuards + 1 - end - end - if not high or numGuards > high then - high = numGuards - highFac = v - end - if not low or numGuards < low then - low = numGuards - lowFac = v + local factionIndex = aiBrain:GetFactionIndex() + local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) + + -- Check if our structure can even upgrade to begin with + if upgradeID then + -- Check if the BM is supposed to upgrade this structure + for index, structure in bManager.UpgradeTable do + -- If the names match, and the IDs don't, we need to upgrade + if StructureName == structure.UnitName and unit.UnitId ~= structure.FinalUnit and not unit.SetToUpgrade then + UpgradeBaseManagerStructure(unit, upgradeID) end end end - -- If we don't have a factory or our factory is dead, or the disparity between factories is more than 1, reorganize engineers. - if ((not assistFac or assistFac.Dead) and lowFac) or (high and low and lowFac and high > low + 1 and highFac == unit:GetGuardedUnit()) then - assistFac = lowFac - platoon:Stop() - IssueGuard({unit}, lowFac) - - -- Add to the list of units that are permanently assisting in this base manager - bManager.PermanentAssisters[unit] = true - end - WaitTicks(150) end end +--- Callback function when an engineering unit starts building something +--- If that 'something' is a named unit the Engineer was told to build, then it caches the necessary data on the named unit +--- Also resets the unit name we cached on the Engineer, so if any mishaps happen, the Engineer can just build something else afterwards +--- We handle cases of BuildingUnitName and ConditionalUnitName, the former is for Base Manager structures, the later is for Conditional Builds (ie. experimentals) +--- Handles special case where a guarding Engineer initiates the construction INSTEAD of our primary Engineer +--- Also handles BM upgrades for instant structure upgrading, this is to avoid issues with PBM factory assigning ---@param unit Unit -function PermanentAssisterDead(unit) - local aiBrain = unit:GetAIBrain()--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[unit.BaseName] - if bManager then - bManager:DecrementPermanentAssisting() +---@param unitBeingBuilt Unit +function EngineerOnStartBuild(unit, unitBeingBuilt) + -- Normally the Engineer we issued the build order for starts the initial construction, however if it has guarding Engineers, they might do so instead + -- We don't queue several build related orders in the campaign enviroment, so if the Engineer that initialized construction is currently guarding another, it's the ONLY order it has right now + -- If we are guarding something, overwrite the origin unit to that, we don't copy over any data, simply treat this Engineer as the primary one, and reset data on the actual primary Engineer + local Guardee = unit:GetGuardedUnit() + if Guardee and not Guardee.Dead then + unit = Guardee + end - -- Remove from permanent assister list - bManager.PermanentAssisters[unit] = nil + -- We cached the name of the unit we want to build, and the Engineer's BaseManager, 1st when we ordered the Engineer to build, 2nd when the Engineer's platoon was formed + local StructureUnitName = unit.BuildingUnitName + local ExperimentalUnitName = unit.ConditionalBuildUnitName + local BaseName = unit.BaseName + local aiBrain = unit.Brain --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[BaseName] + local armyIndex = aiBrain:GetArmyIndex() + + -- First check if we were told to build a BM structure, if not, check if it's a CB + if StructureUnitName and bManager then + -- Cache names to the structure + unitBeingBuilt.UnitName = StructureUnitName + unitBeingBuilt.BaseName = BaseName + + -- Flag the structure as unfinished so the BM can maintain its construction + bManager.UnfinishedBuildings[StructureUnitName] = true + + -- Update the global UnitNames table so the BM knows the structure exists + ScenarioInfo.UnitNames[armyIndex][StructureUnitName] = unitBeingBuilt + + -- Reset the cached unit name, so a new one can be picked right after + unit.BuildingUnitName = nil + + -- Register callbacks + if not unitBeingBuilt.AddedCompletionCallback then + unitBeingBuilt:AddUnitCallback(InitialStructureBuildSuccessful, 'OnStopBeingBuilt') + unitBeingBuilt.AddedCompletionCallback = true + end + elseif ExperimentalUnitName and bManager then + -- Restore the index saved in the CanConditionalBuild call + local cbManager = bManager.ConditionalBuildManager + local buildIndex = cbManager.Index + local selectedBuild = bManager.ConditionalBuildTable[buildIndex] + + cbManager:OnUnitConstructionStarted(unitBeingBuilt) + + -- If we're supposed to keep a certain number of these guys in the field, store the info on him so he can reinsert himself in the conditional build table when he bites it. + if selectedBuild.data.KeepAlive then + unitBeingBuilt.KeepAlive = true + unitBeingBuilt.ConditionalBuild = selectedBuild + unitBeingBuilt.ConditionalBuildManager = cbManager + + -- Register rebuild callback + TriggerFile.CreateUnitDestroyedTrigger(ConditionalBuildDied, unitBeingBuilt) + end + + -- Cache names to the unit + unitBeingBuilt.UnitName = ExperimentalUnitName + unitBeingBuilt.BaseName = BaseName + + -- Reset the cached unit name, so a new one can be picked right after + unit.ConditionalBuildUnitName = nil + + -- Register callbacks + if not unitBeingBuilt.AddedCompletionCallback then + unitBeingBuilt:AddUnitCallback(ConditionalBuildSuccessful, 'OnStopBeingBuilt') + unitBeingBuilt.AddedCompletionCallback = true + end end end ---- Assist units that are building structures and units +--- Callback function when an engineering unit failed to build something, this is called in several cases +--- Resets the unit name we assigned to the Engineer, so in case it's still alive, it can start building something else +---@param unit Unit +function EngineerOnFailedToBuild(unit) + unit.ConditionalBuildUnitName = nil + unit.BuildingUnitName = nil +end + +--- Guts of the build thing +---@param aiBrain CampaignAIBrain +---@param eng Unit +---@param baseManager BaseManager +---@param levelName string +---@param buildingType? string ---@param platoon Platoon -function BaseManagerAssistThread(platoon) - platoon:Stop() +---@return boolean `True` if a structure was issued to be built, `false` if not +---@return string|nil `UnitName` of the structure that was issued to be built, or `nil` if no structure was issued +function BuildBaseManagerStructure(aiBrain, eng, baseManager, levelName, buildingType, platoon) + local bmLevelName = baseManager.BaseName .. levelName + local buildTemplate = aiBrain.BaseTemplates[bmLevelName].Template + local buildList = aiBrain.BaseTemplates[bmLevelName].List - local unit = platoon:GetPlatoonUnits()[1] - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] - local assistData = platoon.PlatoonData.Assist - local platoonPos = platoon:GetPlatoonPosition() - local assistee = false - local assistingBool = false - local beingBuiltCategories = assistData.BeingBuiltCategories or {'MASSEXTRACTION', 'MASSPRODUCTION', 'ENERGYPRODUCTION', 'FACTORY', 'EXPERIMENTAL', 'DEFENSE', 'MOBILE LAND', 'ALLUNITS'} + if not buildTemplate or not buildList or not aiBrain:PlatoonExists(platoon) then + return false + end - local assistRange = assistData.AssistRange or bManager.Radius - local counter = 0 - local waitTime = 30 - while counter < (assistData.Time or 150) and aiBrain:PlatoonExists(platoon) do - local GuardedUnit = unit:GetGuardedUnit() - -- If the engineer is assisting a construction unit that is building, or we don't need assisters, or we are a construction unit, break out and do nothing - if not GuardedUnit or (not GuardedUnit:IsUnitState('Building') and not bManager:ConstructionNeedsAssister() and not bManager:IsConstructionUnit(GuardedUnit)) then - if bManager:ConstructionNeedsAssister() then - local consUnits = bManager.ConstructionEngineers - local lowNum = 100000 - local highNum = 0 - local currLow = false - for _, v in consUnits do - local guardNum = TableGetn(v:GetGuards()) - if not v.Dead and guardNum < lowNum then - currLow = v - lowNum = TableGetn(v:GetGuards()) - end - if guardNum > highNum then - highNum = guardNum - end - end - if GuardedUnit then - if GuardedUnit.Dead or EntityCategoryContains(categories.FACTORY, GuardedUnit) or - highNum > lowNum + 1 then - assistee = currLow - end - else - assistee = currLow + local namesTable = aiBrain.BaseTemplates[bmLevelName].UnitNames + local buildCounter = aiBrain.BaseTemplates[bmLevelName].BuildCounter + for _, entry in pairs(buildTemplate) do + ---@type string + local structureType = entry[1][1] + -- Check if type (ex. T1AirFactory) is correct + if (not buildingType or buildingType == 'ALLUNITS' or buildingType == structureType) and baseManager:CheckStructureBuildable(structureType) then + ---@type BlueprintId|nil + local category + for catName, catData in pairs(buildList) do + if catData.StructureType == structureType then + category = catData.StructureCategory + break end end - -- Find valid unit to assist - -- Get all units building stuff - TODO get list with point and radius; get list of units with state - if not assistee then - local unitsBuilding = aiBrain:GetListOfUnits(categories.CONSTRUCTION, false) - -- Iterate through being built categories - for catNum, buildeeCat in beingBuiltCategories do - local buildCat = ParseEntityCategory(buildeeCat) - for unitNum, constructionUnit in unitsBuilding do - -- Check if the unit is actually building something - if not constructionUnit.Dead and constructionUnit:IsUnitState('Building') then - -- Check to make sure unit being built is of proper category - local buildingUnit = constructionUnit.UnitBeingBuilt - if buildingUnit and not buildingUnit.Dead and EntityCategoryContains(buildCat, buildingUnit) then - -- If the unit building is a factory make sure its in the right PBM Location Type - if not EntityCategoryContains(categories.FACTORY, constructionUnit) or aiBrain:PBMFactoryLocationCheck(constructionUnit, platoon.PlatoonData.BaseName) then - -- make sure unit is within valid assist range - local unitPos = constructionUnit:GetPosition() - if unitPos and platoonPos and VDist2(platoonPos[1], platoonPos[3], unitPos[1], unitPos[3]) < assistRange then - assistee = constructionUnit - break - end - end - end - end - end - -- If we have found a valid unit to assist break off - if assistee then - break + + if not (category and eng:CanBuild(category)) then + continue + end + + -- Iterate through the build locations to find the closest structure to our engineer position. + local engPos = eng:GetPosition() + local closest, closestPosition + for i = 2, TableGetn(entry) do + ---@type Vector + local pos = entry[i] + local position = Vector(pos[1], 0, pos[2]) + if aiBrain:CanBuildStructureAt(category, position) and baseManager:CheckUnitBuildCounter(pos, buildCounter) then + if not closest or Utilities.XZDistanceTwoVectorsSquared(engPos, position) < Utilities.XZDistanceTwoVectorsSquared(engPos, closestPosition) then + closest = pos + closestPosition = position end end end - -- If the unit to be assisted is a factory, assist whatever it is assisting or is assisting it - -- Makes sure all factories have someone helping out to load balance better - if assistee and not assistee.Dead and EntityCategoryContains(categories.FACTORY, assistee) then - platoon:Stop() - local guardee = assistee:GetGuardedUnit() - if guardee and not guardee.Dead and EntityCategoryContains(categories.FACTORY, guardee) then - local factories = AIUtils.AIReturnAssistingFactories(guardee) - TableInsert(factories, assistee) - AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) - assistingBool = true - elseif not TableEmpty(assistee:GetGuards()) then - local factories = AIUtils.AIReturnAssistingFactories(assistee) - TableInsert(factories, assistee) - AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) - assistingBool = true + if not closest then + continue + end + + -- Removed transport call as the pathing check was creating problems with base manager rebuilding + -- TODO: develop system where base managers more easily rebuild in far away or hard to reach locations + -- and TransportUnitsToLocation(platoon, {closest[1], 0, closest[2]}) then + IssueToUnitClearCommands(eng) + aiBrain:BuildStructure(eng, category, closest, false) + + local unitName + if namesTable[closest[1]][closest[2]] then + unitName = namesTable[closest[1]][closest[2]] + end + + return true, unitName + end + end + return false, nil +end + +-- Default order of structures types for building +DefaultStructurePriorities = { + 'T3Resource', + 'T2Resource', + 'T1Resource', + 'T3EnergyProduction', + 'T2EnergyProduction', + 'T1EnergyProduction', + 'T3MassCreation', + 'T2EngineerSupport', + 'T3SupportLandFactory', + 'T3SupportAirFactory', + 'T3SupportSeaFactory', + 'T2SupportLandFactory', + 'T2SupportAirFactory', + 'T2SupportSeaFactory', + 'T1LandFactory', + 'T1AirFactory', + 'T1SeaFactory', + 'T4LandExperimental1', + 'T4LandExperimental2', + 'T4AirExperimental1', + 'T4SeaExperimental1', + 'T3ShieldDefense', + 'T2ShieldDefense', + 'T3StrategicMissileDefense', + 'T3Radar', + 'T2Radar', + 'T1Radar', + 'T3AADefense', + 'T3GroundDefense', + 'T3NavalDefense', + 'T2AADefense', + 'T2MissileDefense', + 'T2GroundDefense', + 'T2NavalDefense', + 'ALLUNITS' +} + +--- Engineer build structures +---@param platoon Platoon +function BaseManagerEngineerThread(platoon) + platoon:Stop() + + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] + + -- Handle case of invalid data + if not platoon.PlatoonData.BaseName or not baseManager then + error('*AI DEBUG: Missing Base Name or invalid base name for base manager engineer thread', 2) + end + + -- Grab the Engineer + local Engineer + + for _, v in pairs(platoon:GetPlatoonUnits()) do + if not v.Dead and EntityCategoryContains(categoriesCONSTRUCTION, v) then + if not Engineer then + Engineer = v + else + IssueToUnitClearCommands(v) + IssueGuard({ v }, Engineer) + end + end + end + + -- If no Engineer was found, return + if not Engineer or Engineer.Dead then + aiBrain:DisbandPlatoon(platoon) + return + end + + local StructurePriorities = platoon.PlatoonData.StructurePriorities or DefaultStructurePriorities + + local StructureFound, UnitName + + -- The idea is to search for a structure (or any unit, but 99% it's a structure) based on the priorities set above that needs to be built + -- If we found one, and the build order could be issued (see 'BuildBaseManagerStructure()' for that), we wait until our engineer finishes its task, then check for the next structure + -- The original iteration uses the same dual 'for' loop, however if a structure of a higher priority got destroyed, the engineer would ignore that until all remaining categories were exhausted + -- In this case we check through the priorities every single time + -- Ie., our Engineer half-way done building all walls, and a T3 resource structure is destroyed during that, our engineer will rebuild said resource structure, and return to finishing the walls + while aiBrain:PlatoonExists(platoon) do + -- Assume we found no structure at the start of each loop + StructureFound = false + + -- Loop through each build group + for _, levelData in ipairs(baseManager.LevelNames) do + -- Loop through each priority category if the current build group has a higher build priority than 0 + if levelData.Priority <= 0 then continue end + + for _, v in ipairs(StructurePriorities) do + local UnitType + if v ~= 'ALLUNITS' then + UnitType = v + end + + -- Check each structure + StructureFound, UnitName = BuildBaseManagerStructure(aiBrain, Engineer, baseManager, levelData.Name, UnitType, platoon) + if StructureFound and UnitName then + Engineer.BuildingUnitName = UnitName + -- Break out, and do so again for the other 'for' loop + break end end - if assistee and not assistee.Dead then - if not assistingBool then - platoon:Stop() - IssueGuard(platoon:GetPlatoonUnits(), assistee) + + if StructureFound and UnitName then + break + end + end + + -- We've went through the structure list, wait until the engineer is idle or dead, + -- We gotta wait at least once under any circumstances, so I'm not using a "while" loop here, because this function can freeze the sim if say, an T1 Aeon Bomber stuns our engineer unit + repeat + WaitTicks(11) + if not aiBrain:PlatoonExists(platoon) then + return + end + until Engineer.Dead or Engineer:IsIdleState() + + -- Break out if we couldn't find a structure to build + if not StructureFound then + break + end + end + + platoon:MoveToLocation(baseManager:GetPosition(), false) +end + +--- Finish building structures that werent finshed +---@param platoon Platoon +function BuildUnfinishedStructures(platoon) + platoon:Stop() + + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local platoonUnits = platoon:GetPlatoonUnits() + local armyIndex = aiBrain:GetArmyIndex() + local eng = platoonUnits[1] + local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] + + if not eng or eng.Dead then + aiBrain:DisbandPlatoon(platoon) + return + end + + -- Otherwise help build whatever needs us + local unfinishedBuildings = false + repeat + unfinishedBuildings = false + local beingBuiltList = {} + local buildingEngs = aiBrain:GetListOfUnits(categoriesENGINEER, false) + -- Find all engineers building structures + for k, v in buildingEngs do + local buildingUnit = v.UnitBeingBuilt + if buildingUnit and buildingUnit.UnitName then + beingBuiltList[buildingUnit.UnitName] = true + end + end + -- Check all unfinished buildings to see if they need someone workin on them + for unitName, _ in pairs(bManager.UnfinishedBuildings) do + local building = ScenarioInfo.UnitNames[armyIndex][unitName] + if building and not building.Dead then + if not beingBuiltList[unitName] then + unfinishedBuildings = true + IssueToUnitClearCommands(eng) + IssueRepair({ eng }, building) + repeat + WaitSeconds(3) + if not aiBrain:PlatoonExists(platoon) then + return + end + until eng:IsIdleState() + bManager.UnfinishedBuildings[unitName] = nil end end end - WaitTicks(waitTime) + until not unfinishedBuildings +end - counter = counter + waitTime +--#endregion + +--#region Engineer Expansion + +---@param brain AIBrain +---@param platoon Platoon +function ExpansionPlatoonDestroyed(brain, platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local data = platoon.PlatoonData + local bManager = aiBrain.BaseManagers[data.BaseName] + + for _, eData in pairs(bManager.ExpansionBaseData) do + if eData.BaseName == data.ExpansionBase then + eData.IncomingEngineers = eData.IncomingEngineers - 1 + return + end end end ---- New base expansion +--- Move a unit to a new location +---@param platoon Platoon +---@param finalLocation Vector +---@return PlatoonCommand|boolean +function TransportUnitsToLocation(platoon, finalLocation) + local units = platoon:GetPlatoonUnits() + local unit = units[1] + if AIUtils.CheckUnitPathingEx(finalLocation, unit:GetPosition(), unit) then + local cmd = platoon:MoveToLocation(finalLocation, false) + return cmd + end + + if not AIUtils.GetTransports(platoon) then + return false + end + AIUtils.UseTransports(units, platoon:GetSquadUnits('Scout') --[[@as AirTransport[] ]], finalLocation) + + return true +end + +--- Find an expansion base that needs engineers, and send them there. +--- Once they arrive, disband the platoon to the expansion base manager can grab them. ---@param platoon Platoon function ExpansionEngineer(platoon) platoon:Stop() - local unitCount = TableGetn(platoon:GetPlatoonUnits()) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] local data = platoon.PlatoonData + local bManager = aiBrain.BaseManagers[data.BaseName] - if not (data.BaseName and aiBrain.BaseManagers[data.BaseName] and aiBrain.BaseManagers[data.BaseName].ExpansionBaseData) then + if not (data.BaseName and bManager and bManager.ExpansionBaseData) then aiBrain:DisbandPlatoon(platoon) return end - local bManager = aiBrain.BaseManagers[data.BaseName] + ---@type PlatoonCommand|boolean local cmd = false - for num, eData in bManager.ExpansionBaseData do + for _, eData in ipairs(bManager.ExpansionBaseData) do -- Find out what expansion base needs engineers if BMBC.NumEngiesInExpansionBase(aiBrain, data.BaseName, eData.BaseName) then if data.ExpansionBase ~= eData.BaseName then @@ -868,17 +887,19 @@ function ExpansionEngineer(platoon) end -- Tracks engineers that are on the way to the expansion base - eData.IncomingEngineers = eData.IncomingEngineers + unitCount + eData.IncomingEngineers = eData.IncomingEngineers + TableGetn(platoon:GetPlatoonUnits()) -- Remove engieneer from IncomingEngineers if it dies on the way platoon:AddDestroyCallback(ExpansionPlatoonDestroyed) - if eData.TransportPlatoon or VDist3(platoon:GetPlatoonPosition(), aiBrain.BaseManagers[eData.BaseName]:GetPosition()) > 250 then - cmd = TransportUnitsToLocation(platoon, aiBrain.BaseManagers[eData.BaseName]:GetPosition()) + local expansionPos = aiBrain.BaseManagers[eData.BaseName]:GetPosition() + local currentPos = platoon:GetPlatoonPosition() --[[@as Vector]] + if eData.TransportPlatoon or VDist3(currentPos, expansionPos) > 250 then + cmd = TransportUnitsToLocation(platoon, expansionPos) end if not cmd then - cmd = platoon:MoveToLocation(aiBrain.BaseManagers[eData.BaseName]:GetPosition(), false) + cmd = platoon:MoveToLocation(expansionPos, false) end break end @@ -890,7 +911,7 @@ function ExpansionEngineer(platoon) end if cmd and type(cmd) ~= 'boolean' then - while platoon:IsCommandsActive(cmd) do + while platoon:IsCommandsActive(cmd --[[@as PlatoonCommand]]) do WaitSeconds(5) if not aiBrain:PlatoonExists(platoon) then return @@ -898,9 +919,10 @@ function ExpansionEngineer(platoon) end end - for num, eData in bManager.ExpansionBaseData do + for _, eData in pairs(bManager.ExpansionBaseData) do if eData.BaseName == data.ExpansionBase then eData.IncomingEngineers = eData.IncomingEngineers - 1 + break end end @@ -912,246 +934,372 @@ function ExpansionEngineer(platoon) end end ----@param brain AIBrain +--#endregion + +--#region Patrol and Assist + +--- Sets the platoon on a patrol route created from base factories positions. ---@param platoon Platoon -function ExpansionPlatoonDestroyed(brain, platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local data = platoon.PlatoonData - local bManager = aiBrain.BaseManagers[data.BaseName] +function BaseManagerPatrolLocationFactoriesAI(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = platoon.PlatoonData.BaseName + local bManager = aiBrain.BaseManagers[baseName] - for num, eData in bManager.ExpansionBaseData do - if eData.BaseName == data.ExpansionBase then - eData.IncomingEngineers = eData.IncomingEngineers - 1 + local factories = aiBrain:PBMGetAllFactories(baseName) + if not factories then + return + end + + local posTable = {} + for _, fac in factories do + if not fac.Dead then + TableInsert(posTable, fac:GetPosition()) end end + + platoon:Stop() + + local i = 1 + while i <= TableGetn(posTable) do + local facNum = Random(1, TableGetn(posTable)) --[[@as integer]] + local movePos = posTable[facNum] + movePos[3] = movePos[3] + 5 + + platoon:Patrol(movePos) + + TableRemove(posTable, facNum) + end end ---- Move a unit to a new location +--- Patrols the engineer using the default BaseManager patrol chain or around the base factories. ---@param platoon Platoon ----@param finalLocation Vector ----@return PlatoonCommand|boolean -function TransportUnitsToLocation(platoon, finalLocation) - local units = platoon:GetPlatoonUnits() - if AIUtils.CheckUnitPathingEx(finalLocation, units[1]:GetPosition(), units[1]) then - local cmd = platoon:MoveToLocation(finalLocation, false) - return cmd +function BaseManagerEngineerPatrol(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] + local chain = bManager:GetDefaultEngineerPatrolChain() + + -- Use the default chain or generate random one from factories + if chain then + platoon.PlatoonData.PatrolChain = chain + ScenarioPlatoonAI.PatrolThread(platoon) + else + BaseManagerPatrolLocationFactoriesAI(platoon) end +end - if not AIUtils.GetTransports(platoon) then - return false +---@param unit Unit +function PermanentAssisterDead(unit) + local aiBrain = unit:GetAIBrain() --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[unit.BaseName] + if bManager then + bManager:DecrementPermanentAssisting() + + -- Remove from permanent assister list + bManager.PermanentAssisters[unit] = nil end - AIUtils.UseTransports(units, platoon:GetSquadUnits('Scout'), finalLocation) +end - return true +---@param platoon Platoon +function PermanentFactoryAssist(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] + ---@type FactoryUnit|nil + local assistFac + local unit = platoon:GetPlatoonUnits()[1] + + TriggerFile.CreateUnitDestroyedTrigger(PermanentAssisterDead, unit) + while aiBrain:PlatoonExists(platoon) do + -- Get all factories in the base manager + local facs = bManager:GetAllBaseFactories() + + -- Determine the number of guards on all factories + local high, highFac, low, lowFac + for _, v in pairs(facs) do + if not v.Dead then + local guards = v:GetGuards() + local numGuards = 0 + for _, gUnit in pairs(guards) do + -- Make sure this guy is a permanent assister and not a transient assister + if not gUnit.Dead and not EntityCategoryContains(categoriesFACTORY, gUnit) and bManager.PermanentAssisters[gUnit] then + numGuards = numGuards + 1 + end + end + if not high or numGuards > high then + high = numGuards + highFac = v + end + if not low or numGuards < low then + low = numGuards + lowFac = v + end + end + end + -- If we don't have a factory or our factory is dead, or the disparity between factories is more than 1, reorganize engineers. + if ((not assistFac or assistFac.Dead) and lowFac) or (high and low and lowFac and high > low + 1 and highFac == unit:GetGuardedUnit()) then + assistFac = lowFac + platoon:Stop() + IssueGuard({ unit }, lowFac) + + -- Add to the list of units that are permanently assisting in this base manager + bManager.PermanentAssisters[unit] = true + end + WaitTicks(150) + end end ---- Engineer build structures +-- Default order of structure types for assisting +DefaultAssistCategories = { + 'MASSEXTRACTION', + 'MASSPRODUCTION', + 'ENERGYPRODUCTION', + 'FACTORY', + 'EXPERIMENTAL', + 'DEFENSE', + 'MOBILE LAND', + 'ALLUNITS' +} + +--- Assist construction projects in the base +--- +--- Assist order: +--- - Other engineers building structures +--- - Factories producting units ---@param platoon Platoon -function BaseManagerEngineerThread(platoon) +function BaseManagerAssistThread(platoon) platoon:Stop() - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local baseManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] + local engineer = platoon:GetPlatoonUnits()[1] + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = platoon.PlatoonData.BaseName + local bManager = aiBrain.BaseManagers[baseName] + ---@type BMAssistPlatoonData + local assistData = platoon.PlatoonData.Assist + local platoonPos = platoon:GetPlatoonPosition() --[[@as Vector]] + ---@type Unit|nil + local assistee + local assistingBool = false + local beingBuiltCategories = assistData.BeingBuiltCategories or DefaultAssistCategories + + local assistRange = assistData.AssistRange or bManager.Radius + -- Faster to compare with squared value + local assistRangeSqr = assistRange * assistRange + local counter = 0 + local waitTime = 30 + + while counter < (assistData.Time or 150) and aiBrain:PlatoonExists(platoon) do + local guardedUnit = engineer:GetGuardedUnit() + -- If the engineer is assisting a construction unit that is building, or we don't need assisters, or we are a construction unit, break out and do nothing + if not guardedUnit or (not guardedUnit:IsUnitState('Building') and not bManager:ConstructionNeedsAssister() and not bManager:IsConstructionUnit(guardedUnit)) then + -- 1) Try to find a constructions engineer to assist to + if bManager:ConstructionNeedsAssister() then + local lowNum = 100000 + local highNum = 0 + ---@type Unit|nil + local currLow + for _, eng in pairs(bManager.ConstructionEngineers) do + local guardNum = TableGetn(eng:GetGuards()) + if not eng.Dead and guardNum < lowNum then + currLow = eng + lowNum = guardNum + end + if guardNum > highNum then + highNum = guardNum + end + end + if guardedUnit then + if guardedUnit.Dead or EntityCategoryContains(categoriesFACTORY, guardedUnit) or highNum > lowNum + 1 then + assistee = currLow + end + else + assistee = currLow + end + end + -- 2) Find any valid unit to assist. Get all units building stuff - TODO get list with point and radius; get list of units with state + if not assistee then + local unitsBuilding = aiBrain:GetListOfUnits(categoriesCONSTRUCTION, false) + + for _, buildeeCat in ipairs(beingBuiltCategories) do + local buildCat = ParseEntityCategory(buildeeCat) + for _, unit in pairs(unitsBuilding) do + if unit.Dead or not unit:IsUnitState('Building') then continue end + + -- Check to make sure unit being built is of proper category + local buildingUnit = unit.UnitBeingBuilt + if not buildingUnit or buildingUnit.Dead or not EntityCategoryContains(buildCat, buildingUnit) then continue end + + -- If the unit building is a factory make sure its in the right PBM Location Type + if not EntityCategoryContains(categoriesFACTORY, unit) or aiBrain:PBMFactoryLocationCheck(unit, baseName) then + -- make sure unit is within valid assist range + local unitPos = unit:GetPosition() + if Utilities.XZDistanceTwoVectorsSquared(platoonPos, unitPos) < assistRangeSqr then + assistee = unit + break + end + end + end - -- Handle case of invalid data - if not platoon.PlatoonData.BaseName or not baseManager then - error('*AI DEBUG: Missing Base Name or invalid base name for base manager engineer thread', 2) - end + if assistee then + break + end + end + end - -- Grab the Engineer - local Engineer + -- If the unit to be assisted is a factory, assist whatever it is assisting or is assisting it + -- Makes sure all factories have someone helping out to load balance better + if assistee and not assistee.Dead then + if EntityCategoryContains(categoriesFACTORY, assistee) then + platoon:Stop() + local guardee = assistee:GetGuardedUnit() + if guardee and not guardee.Dead and EntityCategoryContains(categoriesFACTORY, guardee) then + local factories = AIUtils.AIReturnAssistingFactories(guardee) + TableInsert(factories, assistee) + AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) + assistingBool = true + elseif not TableEmpty(assistee:GetGuards()) then + local factories = AIUtils.AIReturnAssistingFactories(assistee) + TableInsert(factories, assistee) + AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) + assistingBool = true + end + end - for _, v in platoon:GetPlatoonUnits() do - if not v.Dead and EntityCategoryContains(categories.CONSTRUCTION, v) then - if not Engineer then - Engineer = v - else - IssueToUnitClearCommands(v) - IssueGuard({v}, Engineer) + if not assistingBool then + platoon:Stop() + IssueGuard(platoon:GetPlatoonUnits(), assistee) + end end end - end + WaitTicks(waitTime) - -- If no Engineer was found, return - if not Engineer or Engineer.Dead then - aiBrain:DisbandPlatoon(platoon) - return + counter = counter + waitTime end +end - local StructurePriorities = platoon.PlatoonData.StructurePriorities or - { - 'T3Resource', 'T2Resource', 'T1Resource', 'T3EnergyProduction', 'T2EnergyProduction', 'T1EnergyProduction', 'T3MassCreation', - 'T2EngineerSupport', 'T3SupportLandFactory', 'T3SupportAirFactory', 'T3SupportSeaFactory', 'T2SupportLandFactory', 'T2SupportAirFactory', 'T2SupportSeaFactory', 'T1LandFactory', 'T1AirFactory', 'T1SeaFactory', - 'T4LandExperimental1', 'T4LandExperimental2', 'T4AirExperimental1', 'T4SeaExperimental1', - 'T3ShieldDefense', 'T2ShieldDefense', 'T3StrategicMissileDefense', 'T3Radar', 'T2Radar', 'T1Radar', - 'T3AADefense', 'T3GroundDefense', 'T3NavalDefense', 'T2AADefense', 'T2MissileDefense', 'T2GroundDefense', 'T2NavalDefense', 'ALLUNITS' - } - - local StructureFound, UnitName - - -- The idea is to search for a structure (or any unit, but 99% it's a structure) based on the priorities set above that needs to be built - -- If we found one, and the build order could be issued (see 'BuildBaseManagerStructure()' for that), we wait until our engineer finishes its task, then check for the next structure - -- The original iteration uses the same dual 'for' loop, however if a structure of a higher priority got destroyed, the engineer would ignore that until all remaining categories were exhausted - -- In this case we check through the priorities every single time - -- Ie., our Engineer half-way done building all walls, and a T3 resource structure is destroyed during that, our engineer will rebuild said resource structure, and return to finishing the walls - while aiBrain:PlatoonExists(platoon) do - -- Assume we found no structure at the start of each loop - StructureFound = false - - -- Loop through each build group - for index, levelData in baseManager.LevelNames do - -- Loop through each priority category if the current build group has a higher build priority than 0 - if levelData.Priority > 0 then - for k, v in StructurePriorities do - local UnitType = false - if v ~= 'ALLUNITS' then - UnitType = v - end - - -- Check each structure - StructureFound, UnitName = BuildBaseManagerStructure(aiBrain, Engineer, baseManager, levelData.Name, UnitType, platoon) - if StructureFound and UnitName then - Engineer.BuildingUnitName = UnitName - -- Break out, and do so again for the other 'for' loop - break - end - end - end - - if StructureFound and UnitName then - break - end - end - - -- We've went through the structure list, wait until the engineer is idle or dead, - -- We gotta wait at least once under any circumstances, so I'm not using a "while" loop here, because this function can freeze the sim if say, an T1 Aeon Bomber stuns our engineer unit - repeat - WaitTicks(20) - if not aiBrain:PlatoonExists(platoon) then - return - end - until Engineer.Dead or Engineer:IsIdleState() +--#endregion - -- Break out if we couldn't find a structure to build - if not StructureFound then - break - end - end +--#region Main Engineer Platoon Thread - platoon:MoveToLocation(aiBrain.BaseManagers[platoon.PlatoonData.BaseName]:GetPosition(), false) -end +local commanderCategory = categoriesCOMMAND + categoriesSUBCOMMANDER +local permAssisterCategory = categoriesENGINEER - commanderCategory ---- Guts of the build thing ----@param aiBrain CampaignAIBrain ----@param eng Unit ----@param baseManager BaseManager ----@param levelName string ----@param buildingType string +--- Main function for Base Manager Engineers ---@param platoon Platoon ----@return boolean ----@return string|nil -function BuildBaseManagerStructure(aiBrain, eng, baseManager, levelName, buildingType, platoon) - local buildTemplate = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].Template - local buildList = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].List +function BaseManagerSingleEngineerPlatoon(platoon) + platoon.PlatoonData.DontDisband = true - if not buildTemplate or not buildList or not aiBrain:PlatoonExists(platoon) then - return false + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = platoon.PlatoonData.BaseName + local bManager = aiBrain.BaseManagers[baseName] + local cbManager = bManager.ConditionalBuildManager + local unit = platoon:GetPlatoonUnits()[1] + local canPermanentAssist = EntityCategoryContains(permAssisterCategory, unit) + local commandUnit = EntityCategoryContains(commanderCategory, unit) + unit.BaseName = baseName + + -- Add build callbacks for these Engineers + if not unit.AddedBuildCallback then + -- The universal function doesn't work for OnStartBuild, unit.unitBeingBuilt is only accessable after the DoUnitCallback has been executed + unit:AddOnStartBuildCallback(EngineerOnStartBuild) + unit:AddUnitCallback(EngineerOnFailedToBuild, "OnFailedToBuild") + unit.AddedBuildCallback = true end - local namesTable = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].UnitNames - local buildCounter = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].BuildCounter - for K, v in buildTemplate do - -- Check if type (ex. T1AirFactory) is correct - if (not buildingType or buildingType == 'ALLUNITS' or buildingType == v[1][1]) and baseManager:CheckStructureBuildable(v[1][1]) then - local category - for catName, catData in buildList do - if catData.StructureType == v[1][1] then - category = catData.StructureCategory - break - end - end - if category and eng:CanBuild(category) then - local engineerPos = eng:GetPosition() - local closest = false - -- Iterate through build locations - for num, location in v do - -- Check if it can be built and then build - if num > 1 and aiBrain:CanBuildStructureAt(category, {location[1], 0, location[2]}) and baseManager:CheckUnitBuildCounter(location, buildCounter) then - if not closest or VDist2(location[1], location[2], engineerPos[1], engineerPos[3]) < VDist2(closest[1], closest[2], engineerPos[1], engineerPos[3]) then - closest = location - end - end - end + while aiBrain:PlatoonExists(platoon) do + if BMBC.BaseEngineersEnabled(aiBrain, baseName) then + -- Move to expansion base + if not commandUnit and BMBC.ExpansionBasesEnabled(aiBrain, baseName) and BMBC.ExpansionBasesNeedEngineers(aiBrain, baseName) then + ExpansionEngineer(platoon) - if closest then - -- Removed transport call as the pathing check was creating problems with base manager rebuilding - -- TODO: develop system where base managers more easily rebuild in far away or hard to reach locations - -- and TransportUnitsToLocation(platoon, {closest[1], 0, closest[2]}) then - IssueToUnitClearCommands(eng) - aiBrain:BuildStructure(eng, category, closest, false) + -- Assist a conditional builder under construction + elseif canPermanentAssist and cbManager:NeedsMoreBuilders() then + AssistConditionalBuild(platoon) - local unitName - if namesTable[closest[1]][closest[2]] then - unitName = namesTable[closest[1]][closest[2]] - end + -- If we can do a conditional build here, then do it + elseif canPermanentAssist and CanConditionalBuild(platoon) then + DoConditionalBuild(platoon) - return true, unitName - end + -- Try to build buildings + elseif BMBC.NeedAnyStructure(aiBrain, baseName) and bManager:GetConstructionEngineerCount() < bManager:GetConstructionEngineerMaximum() then + bManager:AddConstructionEngineer(unit) + TriggerFile.CreateUnitDestroyedTrigger(ConstructionUnitDeath, unit) + BaseManagerEngineerThread(platoon) + bManager:RemoveConstructionEngineer(unit) + + -- Permanent Assist - Assist factories until the unit dies + elseif canPermanentAssist and bManager:NeedPermanentFactoryAssist() then + bManager:IncrementPermanentAssisting() + PermanentFactoryAssist(platoon) + + -- Finish unfinished buildings + elseif BMBC.UnfinishedBuildingsCheck(aiBrain, baseName) then + BuildUnfinishedStructures(platoon) + + -- Try to assist + elseif BMBC.CategoriesBeingBuilt(aiBrain, baseName, { 'MOBILE LAND', 'ALLUNITS' }) or (bManager:ConstructionNeedsAssister()) then + BaseManagerAssistThread(platoon) + + -- Try to patrol + elseif BMBC.BasePatrollingEnabled(aiBrain, baseName) and not unit:IsUnitState('Patrolling') then + BaseManagerEngineerPatrol(platoon) end end + WaitTicks(75) end - return false end ---- Finish building structures that werent finshed +--- Split the engineers platoon into single unit platoons +--- +--- Used by the BaseManager platoon builders ---@param platoon Platoon -function BuildUnfinishedStructures(platoon) - platoon:Stop() - - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local platoonUnits = platoon:GetPlatoonUnits() - local armyIndex = aiBrain:GetArmyIndex() - local eng = platoonUnits[1] - local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] - - if not eng or eng.Dead then +function BaseManagerEngineerPlatoonSplit(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local units = platoon:GetPlatoonUnits() + local baseName = platoon.PlatoonData.BaseName + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then aiBrain:DisbandPlatoon(platoon) return end + for _, engineer in pairs(units) do + if engineer.Dead or not EntityCategoryContains(categoriesENGINEER, engineer) then + continue + -- Make sure current base manager isn't at capacity of engineers + elseif bManager.CurrentEngineerCount >= bManager.EngineerQuantity then + continue + end - -- Otherwise help build whatever needs us - local unfinishedBuildings = false - repeat - unfinishedBuildings = false - local beingBuiltList = {} - local buildingEngs = aiBrain:GetListOfUnits(categories.ENGINEER, false) - -- Find all engineers building structures - for k, v in buildingEngs do - local buildingUnit = v.UnitBeingBuilt - if buildingUnit and buildingUnit.UnitName then - beingBuiltList[buildingUnit.UnitName] = true - end + if bManager.EngineerBuildRateBuff then + Buff.ApplyBuff(engineer, bManager.EngineerBuildRateBuff) end - -- Check all unfinished buildings to see if they need someone workin on them - for unitName, _ in bManager.UnfinishedBuildings do - if ScenarioInfo.UnitNames[armyIndex][unitName] and not ScenarioInfo.UnitNames[armyIndex][unitName].Dead then - if not beingBuiltList[unitName] then - unfinishedBuildings = true - IssueToUnitClearCommands(eng) - IssueRepair({eng}, ScenarioInfo.UnitNames[armyIndex][unitName]) - repeat - WaitSeconds(3) - if not aiBrain:PlatoonExists(platoon) then - return - end - until eng:IsIdleState() - bManager.UnfinishedBuildings[unitName] = nil - end + + local engPlat = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(engPlat, engineer, 'Support', 'None') + engPlat:SetPlatoonData(platoon.PlatoonData) + engineer.BaseName = baseName + engPlat:ForkAIThread(BaseManagerSingleEngineerPlatoon) + + -- If engineer is not a commander or sub-commander, increment number of units working for the base + -- set up death trigger for the engineer + if not EntityCategoryContains(commanderCategory, engineer) then + bManager:AddCurrentEngineer() + + -- Only add death callback if it hasnt been set yet + if not engineer.Subtracted then + TriggerFile.CreateUnitDestroyedTrigger(BaseManagerSingleDestroyed, engineer) + end + + -- If the base is building engineers, subtract one from the amount being built + if bManager:GetEngineersBuilding() > 0 then + bManager:SetEngineersBuilding(-1) end end - until not unfinishedBuildings + end + aiBrain:DisbandPlatoon(platoon) end +--#endregion + +---@see OpAI.SetTargettingPriorities ---@param platoon Platoon function PlatoonSetTargetPriorities(platoon) if platoon.PlatoonData.CategoryPriorities then @@ -1172,7 +1320,7 @@ function PlatoonSetTargetPriorities(platoon) TableInsert(priList, ParseEntityCategory(v)) end - local squads = { 'attack', 'support', 'scout', 'artillery' } + local squads = { 'Attack', 'Support', 'Scout', 'Artillery' } for k, v in squads do platoon:SetPrioritizedTargetList(v, priList) end @@ -1183,24 +1331,18 @@ end ---@param unit Unit ---@return Vector[] function GetScoutingPath(bManager, unit) - local mapInfo = {} - if not ScenarioInfo.MapData.PlayableRect then - mapInfo[1] = 0 - mapInfo[2] = 0 - mapInfo[3] = ScenarioInfo.size[1] - mapInfo[4] = ScenarioInfo.size[2] - else - mapInfo = ScenarioInfo.MapData.PlayableRect - end + local mapInfo = ScenarioInfo.MapData.PlayableRect or { + 0, 0, ScenarioInfo.size[1], ScenarioInfo.size[2] + } local pathablePoints = {} local possiblePoints = {} - if EntityCategoryContains(categories.AIR, unit) and bManager:GetDefaultAirScoutPatrolChain() then + if EntityCategoryContains(categoriesAIR, unit) and bManager:GetDefaultAirScoutPatrolChain() then -- Do air thing - pathablePoints = ScenarioUtils.ChainToPositions(bManager:GetDefaultAirScoutPatrolChain()) - elseif EntityCategoryContains(categories.LAND, unit) and bManager:GetDefaultLandScoutPatrolChain() then + pathablePoints = ScenarioUtils.ChainToPositions(bManager:GetDefaultAirScoutPatrolChain() --[[@as string]]) + elseif EntityCategoryContains(categoriesLAND, unit) and bManager:GetDefaultLandScoutPatrolChain() then -- Do land thing - pathablePoints = ScenarioUtils.ChainToPositions(bManager:GetDefaultLandScoutPatrolChain()) + pathablePoints = ScenarioUtils.ChainToPositions(bManager:GetDefaultLandScoutPatrolChain() --[[@as string]]) else local startX = mapInfo[1] local startY = mapInfo[2] @@ -1242,7 +1384,7 @@ end ---@param platoon Platoon function BaseManagerScoutingAI(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] local unit = platoon:GetPlatoonUnits()[1] local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] @@ -1265,40 +1407,26 @@ function BaseManagerScoutingAI(platoon) end end ---- Assigns the units of the given platoon into new single unit platoons, and sets the 'BaseManagerTMLAI' as their platoon AI function ---- Also copies over the platoon data, which we require to determine if the unit's BaseManager is allowed to use the TML ----@param platoon Platoon -function BaseManagerTMLPlatoon(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local TMLs = platoon:GetPlatoonUnits() - - if not aiBrain.BaseManagers[platoon.PlatoonData.BaseName] then - aiBrain:DisbandPlatoon(platoon) - end - - for _, launcher in TMLs do - if not launcher.Dead then - local launcherPlatoon = aiBrain:MakePlatoon('', '') - aiBrain:AssignUnitToPlatoon(launcherPlatoon, launcher, 'Attack', 'None') - launcherPlatoon.PlatoonData = table.deepcopy(platoon.PlatoonData) - launcherPlatoon:ForkAIThread(BaseManagerTMLAI) - end - end - - aiBrain:DisbandPlatoon(platoon) -end +DefaultTMLPlatoonPriorities = { + categories.COMMAND, + categories.EXPERIMENTAL, + categories.ENERGYPRODUCTION, + categories.STRUCTURE, + categories.TECH3 * categories.MOBILE, + categories.TECH2 * categories.MOBILE +} ---@param platoon Platoon function BaseManagerTMLAI(platoon) - local aiBrain = platoon:GetBrain() - local baseName = platoon.PlatoonData.BaseName + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = platoon.PlatoonData.BaseName local unit = platoon:GetPlatoonUnits()[1] if not unit then return end platoon:Stop() - local maxRadius = unit.Blueprint.Weapon[1].MaxRadius - local unitPosition = unit:GetPosition() + local maxRadius = unit.Blueprint.Weapon[1].MaxRadius + local unitPosition = unit:GetPosition() local simpleTargetting = true if ScenarioInfo.Options.Difficulty == 3 then @@ -1307,39 +1435,31 @@ function BaseManagerTMLAI(platoon) unit:SetAutoMode(true) - platoon:SetPrioritizedTargetList('Attack', { - categories.COMMAND, - categories.EXPERIMENTAL, - categories.ENERGYPRODUCTION, - categories.STRUCTURE, - categories.TECH3 * categories.MOBILE, - categories.TECH2 * categories.MOBILE - } - ) + platoon:SetPrioritizedTargetList('Attack', DefaultTMLPlatoonPriorities) while aiBrain:PlatoonExists(platoon) do if BMBC.TMLsEnabled(aiBrain, baseName) then - local target = false + local target while unit:GetTacticalSiloAmmoCount() < 1 or not target do - target = target or platoon:FindPrioritizedUnit('Attack', 'Enemy', true, unitPosition, maxRadius) + target = target or platoon:FindPrioritizedUnit('Attack', 'Enemy', true, unitPosition, maxRadius) - if target and unit:GetTacticalSiloAmmoCount() >= 1 then - break - end + if target and unit:GetTacticalSiloAmmoCount() >= 1 then + break + end - WaitSeconds(5) + WaitSeconds(5) - if not aiBrain:PlatoonExists(platoon) then + if not aiBrain:PlatoonExists(platoon) then return end end if not target.Dead then - if EntityCategoryContains(categories.STRUCTURE, target) or simpleTargetting then - IssueTactical({unit}, target) + if EntityCategoryContains(categoriesSTRUCTURE, target) or simpleTargetting then + IssueTactical({ unit }, target) else local targPos = SUtils.LeadTarget(platoon, target) if targPos then - IssueTactical({unit}, targPos) + IssueTactical({ unit }, targPos) end end end @@ -1348,58 +1468,83 @@ function BaseManagerTMLAI(platoon) end end ---- Assigns the units of the given platoon into new single unit platoons, and sets the 'BaseManagerNukeAI' as their platoon AI function ---- Also copies over the platoon data, which we require to determine if the unit's BaseManager is allowed to use the SML +--- Assigns the units of the given platoon into new single unit platoons, and sets the 'BaseManagerTMLAI' as their platoon AI function +--- Also copies over the platoon data, which we require to determine if the unit's BaseManager is allowed to use the TML ---@param platoon Platoon -function BaseManagerNukePlatoon(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local SMLs = platoon:GetPlatoonUnits() +function BaseManagerTMLPlatoon(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local TMLs = platoon:GetPlatoonUnits() - if not aiBrain.BaseManagers[platoon.PlatoonData.BaseName] then + if not aiBrain.BaseManagers[platoon.PlatoonData.BaseName] then aiBrain:DisbandPlatoon(platoon) + return end - for _, silo in SMLs do - if not silo.Dead then - local siloPlatoon = aiBrain:MakePlatoon('', '') - aiBrain:AssignUnitToPlatoon(siloPlatoon, silo, 'Support', 'None') - siloPlatoon.PlatoonData = table.deepcopy(platoon.PlatoonData) - siloPlatoon:ForkAIThread(BaseManagerNukeAI) - end - end + for _, launcher in TMLs do + if not launcher.Dead then + local launcherPlatoon = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(launcherPlatoon, launcher, 'Attack', 'None') + launcherPlatoon.PlatoonData = table.deepcopy(platoon.PlatoonData) + launcherPlatoon:ForkAIThread(BaseManagerTMLAI) + end + end - aiBrain:DisbandPlatoon(platoon) + aiBrain:DisbandPlatoon(platoon) end ---@param platoon Platoon function BaseManagerNukeAI(platoon) - local aiBrain = platoon:GetBrain()--[[@as CampaignAIBrain]] - local baseName = platoon.PlatoonData.BaseName + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local baseName = platoon.PlatoonData.BaseName local unit = platoon:GetPlatoonUnits()[1] - if not unit then return end + if not unit then return end - platoon:Stop() + platoon:Stop() - unit:SetAutoMode(true) + unit:SetAutoMode(true) while aiBrain:PlatoonExists(platoon) do - if BMBC.NukesEnabled(aiBrain, baseName) then - while unit:GetNukeSiloAmmoCount() < 1 do - WaitSeconds(15) - if not aiBrain:PlatoonExists(platoon) then - return - end - end - - nukePos = AIBehaviors.GetHighestThreatClusterLocation(aiBrain, unit) - if nukePos then - IssueNuke({unit}, nukePos) - WaitSeconds(15) - IssueToUnitClearCommands(unit) - end - end - WaitSeconds(10) + if BMBC.NukesEnabled(aiBrain, baseName) then + while unit:GetNukeSiloAmmoCount() < 1 do + WaitSeconds(15) + if not aiBrain:PlatoonExists(platoon) then + return + end + end + + local nukePos = AIBehaviors.GetHighestThreatClusterLocation(aiBrain, unit) + if nukePos then + IssueNuke({ unit }, nukePos) + WaitSeconds(15) + IssueToUnitClearCommands(unit) + end + end + WaitSeconds(10) + end +end + +--- Assigns the units of the given platoon into new single unit platoons, and sets the 'BaseManagerNukeAI' as their platoon AI function +--- Also copies over the platoon data, which we require to determine if the unit's BaseManager is allowed to use the SML +---@param platoon Platoon +function BaseManagerNukePlatoon(platoon) + local aiBrain = platoon:GetBrain() --[[@as CampaignAIBrain]] + local SMLs = platoon:GetPlatoonUnits() + + if not aiBrain.BaseManagers[platoon.PlatoonData.BaseName] then + aiBrain:DisbandPlatoon(platoon) + return + end + + for _, silo in SMLs do + if not silo.Dead then + local siloPlatoon = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(siloPlatoon, silo, 'Support', 'None') + siloPlatoon.PlatoonData = table.deepcopy(platoon.PlatoonData) + siloPlatoon:ForkAIThread(BaseManagerNukeAI) + end end + + aiBrain:DisbandPlatoon(platoon) end ---@param platoon Platoon @@ -1411,7 +1556,7 @@ end --- Ie., if the platoon has 7 out of the 10 original units alive, and the ratio is 0.8, the AI can build this platoon again ---@param platoon Platoon function AMUnlockRatio(platoon) - local count = 0 + local count = 0 for k, v in platoon:GetPlatoonUnits() do if not v.Dead then count = count + 1 @@ -1425,8 +1570,8 @@ function AMUnlockRatio(platoon) local callback = function(unit) platoon.LivingUnits = platoon.LivingUnits - 1 if platoon.Locked and platoon.PlatoonData.Ratio > (platoon.LivingUnits / platoon.MaxUnits) then - ScenarioInfo.AMLockTable[platoon.PlatoonData.PlatoonName] = false - platoon.Locked = false + ScenarioInfo.AMLockTable[platoon.PlatoonData.PlatoonName] = false + platoon.Locked = false end end @@ -1442,7 +1587,7 @@ end --- Ie., if the platoon has 7 out of the 10 original units alive, and the ratio is 0.8, the AI can build this platoon again after the specified time has passsed ---@param platoon Platoon function AMUnlockRatioTimer(platoon) - local count = 0 + local count = 0 for k, v in platoon:GetPlatoonUnits() do if not v.Dead then count = count + 1 @@ -1468,7 +1613,7 @@ function AMUnlockRatioTimer(platoon) end end ----@param aiBrain AIBrain +---@param aiBrain CampaignAIBrain ---@param location Vector ---@return any function ClosestPBMLocation(aiBrain, location) @@ -1486,70 +1631,68 @@ function ClosestPBMLocation(aiBrain, location) return closest end ----@param platoon Platoon -function UnitUpgradeBehavior(platoon) - local unit = platoon:GetPlatoonUnits()[1] - if not unit.UpgradeThread then - if platoon.PlatoonData and not unit.CDRData then - unit.CDRData = platoon.PlatoonData - end - unit.UpgradeThread = unit:ForkThread(UnitUpgradeThread) - end -end - ---@param unit Unit function UnitUpgradeThread(unit) - local aiBrain = unit:GetAIBrain()--[[@as CampaignAIBrain]] - local bManager - - if unit.CDRData.BaseName and aiBrain.BaseManagers[unit.CDRData.BaseName] then - bManager = aiBrain.BaseManagers[unit.CDRData.BaseName] - end + local aiBrain = unit:GetAIBrain() --[[@as CampaignAIBrain]] + local baseName = unit.CDRData and unit.CDRData.BaseName + if not baseName then return end + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return end -- Determine the type of unit local unitType - if EntityCategoryContains(categories.COMMAND, unit) then + if EntityCategoryContains(categoriesCOMMAND, unit) then unitType = 'DefaultACU' - elseif EntityCategoryContains(categories.SUBCOMMANDER, unit) then + elseif EntityCategoryContains(categoriesSUBCOMMANDER, unit) then unitType = 'DefaultSACU' end while not unit.Dead do - if not bManager then - bManager = aiBrain.BaseManagers[unit.PlatoonData.BaseName] - end - if bManager then - local upgradeName = bManager:UnitNeedsUpgrade(unit, unitType) + local upgradeName = bManager:UnitNeedsUpgrade(unit, unitType) - if upgradeName and not unit:IsUnitState('Building') then - -- Remove the unit from the builders list - if bManager:IsConstructionUnit(unit) then - bManager:RemoveConstructionEngineer(unit) - end + if upgradeName and not unit:IsUnitState('Building') then + -- Remove the unit from the builders list + if bManager:IsConstructionUnit(unit) then + bManager:RemoveConstructionEngineer(unit) + end - local platoon = aiBrain:MakePlatoon('', '') - aiBrain:AssignUnitToPlatoon(platoon, unit, 'support', 'none') + local platoon = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(platoon, unit, 'Support', 'NoFormation') - local order = { - TaskName = "EnhanceTask", - Enhancement = upgradeName - } - IssueStop({unit}) - IssueToUnitClearCommands(unit) - IssueScript({unit}, order) + local order = { + TaskName = "EnhanceTask", + Enhancement = upgradeName + } + IssueToUnitStop(unit) + IssueToUnitClearCommands(unit) + IssueScript({ unit }, order) - repeat - WaitSeconds(3) - if unit.Dead then - return - end - until unit:IsIdleState() - aiBrain:DisbandPlatoon(platoon) - end + repeat + WaitSeconds(3) + if unit.Dead then + return + end + until unit:IsIdleState() + + -- Walk back to the base, so the unit can't be grabbed by the base again. + IssueToUnitMove(unit, bManager.Position) + aiBrain:DisbandPlatoon(platoon) end WaitSeconds(5) end end +---@param platoon Platoon +function UnitUpgradeBehavior(platoon) + local unit = platoon:GetPlatoonUnits()[1] + if not unit.UpgradeThread then + if platoon.PlatoonData and not unit.CDRData then + unit.CDRData = platoon.PlatoonData + end + unit.UpgradeThread = unit:ForkThread(UnitUpgradeThread) + end +end + -- kept for mod compatibility, as they may depend on these local AIAttackUtils = import("/lua/ai/aiattackutilities.lua") +local MIBC = import("/lua/editor/miscbuildconditions.lua") diff --git a/lua/AI/OpAI/BaseOpAI.lua b/lua/AI/OpAI/BaseOpAI.lua index f34d184969c..f1c11fa2ba6 100644 --- a/lua/AI/OpAI/BaseOpAI.lua +++ b/lua/AI/OpAI/BaseOpAI.lua @@ -15,6 +15,50 @@ local BMBC = '/lua/editor/basemanagerbuildconditions.lua' local MIBC = '/lua/editor/miscbuildconditions.lua' local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' +local pairs, ipairs = pairs, ipairs +local type, unpack = type, unpack +local mathFloor = math.floor +local stringFind = string.find +local stringLen = string.len +local stringSub = string.sub +local tableInsert = table.insert +local tableRemove = table.remove +local tableEmpty = table.empty +local tableGetn = table.getn + +-- Used by the build conditions that dont need any args +local emptyTable = {} +local platoonTypes = { 'Air', 'Land', 'Sea' } +local targetCommanderLastPriorities = { + categories.EXPERIMENTAL, + categories.STRUCTURE * categories.DEFENSE, + categories.STRUCTURE * categories.ECONOMIC, + categories.MOBILE - categories.COMMAND, + categories.ALLUNITS - categories.COMMAND, + categories.COMMAND, +} +local targetCommanderNeverPriorities = { + categories.EXPERIMENTAL, + categories.STRUCTURE * categories.DEFENSE, + categories.STRUCTURE * categories.ECONOMIC, + categories.MOBILE - categories.COMMAND, + categories.ALLUNITS - categories.COMMAND, +} +local defTargetPriorities = { 'COMMAND', 'MOBILE', 'STRUCTURE DEFENSE', 'ALLUNITS' } + +--- Platoon template file with pre-generated platoon templates and builders +---@alias OpAIPlatoonTpFile +---| "AirAttacks" +---| "AirScout" +---| "BasicLandAttack" +---| "BomberEscort" +---| "HeavyLandAttack" +---| "LandAssualt" +---| "LeftoverCleanup" +---| "LightAirAttack" +---| "NavalAttacks" +---| "NavalFleet" + ---@alias OpAIChildType ---| "Bombers" # T1 Bombers ---| "Interceptors" # T1 Interceptors @@ -35,6 +79,7 @@ local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' ---| "LightArtillery" # T1 Arty ---| "MobileAntiAir" # T1 MAA ---| "HeavyTanks" # T2 Tanks +---| "RangeBots" # T2 Cybran/UEF bots ---| "AmphibiousTanks" # T2 Amphibious tanks ---| "MobileShields" # T2/T3 Mobile shield ---| "MobileStealth" # T2 Stealth field @@ -63,6 +108,9 @@ local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' ---| "T3Submarines" # T3 Sera submarine ---| "TorpedoBoats" # T2 UEF Torpedo boat ---| "BattleCruisers" +---| "T1" # T1 ships in NavalOpAI from generated templates +---| "T2" # T2 ships in NavalOpAI from generated templates +---| "T3" # T3 ships in NavalOpAI from generated templates ---| "All" ---@alias OpAILockType @@ -73,16 +121,23 @@ local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' ---| "RatioTimer" # Combination of `DeathTimer` and `DeathRatio` ---@class OpAILockData ----@field Ratio number|nil Dead units ratio of the platoon that will trigger rebuild ----@field LockTimer integer|nil The platoon can be rebuild after this amount of seconds +---@field Ratio? number Dead units ratio of the platoon that will trigger rebuild +---@field LockTimer? integer The platoon can be rebuild after this amount of seconds ---@class OpAIChildrenName ---@field BuilderName string ---@field ChildrenType OpAIChildType[] ---@class OpAIChildHandle ----@field ChildName string ----@field ChildBuilder table +---@field ChildName string Builder name +---@field ChildBuilder PBMPlatoonBuilder + +---@class ChildMonitorFnData: FileFunctionRef +---@field [3] table + +---@class OpAINewChildMonitorData +---@field DirectFunction? fun(): boolean Return `false` to disable the child +---@field FunctionInfo? ChildMonitorFnData `fun(aiBrain, ...): boolean` Return `false` to disable the child ---@class OpAI ---@field Trash TrashBag @@ -90,870 +145,863 @@ local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' ---@field LocationType string Name of the base this AI belongs to ---@field MasterName string ---@field GlobalVarName string `name`_`BuilderType` ----@field BuilderType SaveFile | string Save file that is used to find child quantities ----@field MasterData table +---@field BuilderType OpAIPlatoonTpFile | GeneratedScenario Save file that is used to find child quantities +---@field MasterData AMBuilder ---@field ChildrenHandles OpAIChildHandle[] ----@field ChildMonitorHandle any ----@field ChildMonitorData table +--- Periodically runs functions in `OpAI.ChildMonitorData` to toggle children +--- +--- Childrens are enabled by default, unless one of the functions return `false` +---@field ChildMonitorHandle? thread +---@field ChildMonitorData? table ---@field ChildrenNames OpAIChildrenName[] ----@field EnabledTypes table ----@field PreCreateFinished boolean +---@field EnabledTypes table ChildType is enabled by default when added. +---@field private PreCreateFinished boolean ---@overload fun(): OpAI -OpAI = ClassSimple { - ---Set up variables local to this OpAI instance - ---@param self OpAI - PreCreate = function(self) - if self.PreCreateFinished then return end - - self.Trash = TrashBag() - - self.PreCreateFinished = true - end, - - ---@param self OpAI - ---@param force? boolean - ---@return boolean - FindMaster = function(self, force) - if self.MasterData and not force then - return true - end - for k,v in self.AIBrain.AttackData.Platoons do - if v.PlatoonName == self.MasterName then - self.MasterData = v - return true - end - end - return false - end, +local BaseOpAI = {} +--- Set up variables local to this OpAI instance +---@private +function BaseOpAI:PreCreate() + if self.PreCreateFinished then return end + + self.Trash = TrashBag() + self.PreCreateFinished = true +end - ---@param self OpAI - ---@param force? boolean - ---@return boolean - FindChildren = function(self, force) - if self.ChildrenHandles and not table.empty(self.ChildrenHandles) and not force then +---@private +---@param force? boolean +---@return boolean +function BaseOpAI:FindMaster(force) + if self.MasterData and not force then + return true + end + for k, v in self.AIBrain.AttackData.Platoons do + if v.PlatoonName == self.MasterName then + self.MasterData = v return true end - self.ChildrenHandles = {} - local types = { 'Air', 'Land', 'Sea' } - for _, currType in types do - for name,builder in ScenarioInfo.BuilderTable[self.AIBrain.CurrentPlan][currType] do - if self:ChildNameCheck(name) then - table.insert(self.ChildrenHandles, { ChildName=name, ChildBuilder=builder }) - end - end - end + end + return false +end + +---@private +---@param force? boolean +---@return boolean +function BaseOpAI:FindChildren(force) + if self.ChildrenHandles and not tableEmpty(self.ChildrenHandles) and not force then return true - end, - - ---@param self OpAI - ---@param typeTable OpAIChildType[] - AddChildType = function(self,typeTable) - if typeTable then - for _, tName in typeTable do - if self.EnabledTypes[tName] == nil then - self.EnabledTypes[tName] = true - end + end + self.ChildrenHandles = {} + local builderTable = ScenarioInfo.BuilderTable[self.AIBrain.CurrentPlan] + for _, currType in platoonTypes do + for name, builder in builderTable[currType] do + if self:ChildNameCheck(name) then + tableInsert(self.ChildrenHandles, { ChildName = name, ChildBuilder = builder }) end end - end, + end + return true +end - ---@param self OpAI - ---@param name string - ---@return boolean - ChildNameCheck = function(self,name) - for _, v in self.ChildrenNames do - local found = string.find(v.BuilderName, name .. '_', 1, true) - if v.BuilderName == name or found then - return true - end +---@private +---@param typeTable OpAIChildType[] +function BaseOpAI:AddChildType(typeTable) + if not typeTable then return end + + for _, tName in typeTable do + if self.EnabledTypes[tName] == nil then + self.EnabledTypes[tName] = true end - return false - end, + end +end - ---@param self OpAI - ---@param number integer - ---@param childType? any - SetChildCount = function(self, number, childType) - if not childType then - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_D'..ScenarioInfo.Options.Difficulty] = number - else - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_'..childType..'_D'..ScenarioInfo.Options.Difficulty] = number - end - end, - - ---@param self OpAI - ---@param diffTable integer[] - SetChildCountDiffTable = function(self, diffTable) - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_D1'] = diffTable[1] - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_D2'] = diffTable[2] - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_D3'] = diffTable[3] - end, - - ---@param self OpAI - ---@param functionInfo any - ---@param childType any - SetChildrenPlatoonAI = function(self, functionInfo, childType) - if not self:FindChildren() then - error('*AI DEBUG: No children for OpAI found') - end - for k,v in self.ChildrenHandles do - v.ChildBuilder.PlatoonAIFunction = functionInfo - end - end, - - ---Overrides the default platoon formation - ---@param self OpAI - ---@param formationName UnitFormations - SetFormation = function(self, formationName) - if not self:FindMaster() then - return +---@private +---@param name string +---@return boolean +function BaseOpAI:ChildNameCheck(name) + local searchFor = name .. '_' + for _, v in self.ChildrenNames do + local found = stringFind(v.BuilderName, searchFor, 1, true) + if v.BuilderName == name or found then + return true end - self.MasterData.PlatoonData.OverrideFormation = formationName - end, - - ---@param self OpAI - ---@param funcName string - ---@param bool boolean - SetFunctionStatus = function(self, funcName, bool) - ScenarioInfo.OSPlatoonCounter[self.MasterName..'_' .. funcName] = bool - end, - - ---TODO: make a system out of this. Derive functionality per override per OpAI type - ---@param self OpAI - ---@param functionData any - MasterPlatoonFunctionalityChange = function(self, functionData) - if functionData[2] == 'LandAssaultWithTransports' then - self:SetFunctionStatus('Transports', true) - end - end, - - ---@param self OpAI - ---@param cat any - ---@return boolean - TargetCommanderLast = function(self, cat) - return self:SetTargettingPriorities( - { - categories.EXPERIMENTAL, - categories.STRUCTURE * categories.DEFENSE, - categories.STRUCTURE * categories.ECONOMIC, - categories.MOBILE - categories.COMMAND, - categories.ALLUNITS - categories.COMMAND, - categories.COMMAND, - - } - , cat) - end, - - ---@param self OpAI - ---@param cat any - ---@return boolean - TargetCommanderNever = function(self, cat) - return self:SetTargettingPriorities( - { - categories.EXPERIMENTAL, - categories.STRUCTURE * categories.DEFENSE, - categories.STRUCTURE * categories.ECONOMIC, - categories.MOBILE - categories.COMMAND, - categories.ALLUNITS - categories.COMMAND, - } - , cat) - end, - - ---categories is an optional parameter specifying a subset of the platoon we wish to set target priorities for. - ---@param self OpAI - ---@param priTable string[] - ---@param categories? EntityCategory - ---@return boolean - SetTargettingPriorities = function(self, priTable, categories) - if not self:FindMaster() then - return false - end - - local priList = { unpack(priTable) } - local defList = { 'COMMAND', 'MOBILE', 'STRUCTURE DEFENSE', 'ALLUNITS',} - - if categories then - --save the priorities for this category. - if not self.MasterData.PlatoonData.CategoryPriorities then self.MasterData.PlatoonData.CategoryPriorities = {} end - - --NOTE: This should probably be a table.deepcopy if we're going to alter the original table in the future. - - self.MasterData.PlatoonData.CategoryPriorities[categories] = priList + end + return false +end - else - for i,v in defList do - table.insert(priList, v) - end +--- Sets the number of child platoons to build. +--- +--- Once the desired amount it reached, they are combined into a master platoon. +---@param number integer +---@param childType? any +function BaseOpAI:SetChildCount(number, childType) + if not childType then + ScenarioInfo.OSPlatoonCounter[self.MasterName .. '_D' .. ScenarioInfo.Options.Difficulty] = number + else + ScenarioInfo.OSPlatoonCounter[self.MasterName .. '_' .. childType .. '_D' .. ScenarioInfo.Options.Difficulty] = number + end +end - self.MasterData.PlatoonData.TargetPriorities = {} +--- Sets the number of child platoons to build. +--- +--- Once the desired amount it reached, they are combined into a master platoon. +---@param diffTable {[1]: integer, [2]: integer, [3]: integer} count to set for Easy, Medium, Hard difficulty +function BaseOpAI:SetChildCountDiffTable(diffTable) + local platoonCounter = ScenarioInfo.OSPlatoonCounter + platoonCounter[self.MasterName .. '_D1'] = diffTable[1] + platoonCounter[self.MasterName .. '_D2'] = diffTable[2] + platoonCounter[self.MasterName .. '_D3'] = diffTable[3] +end - for i,v in priList do - table.insert(self.MasterData.PlatoonData.TargetPriorities, v) - end +--- Changes the children platoons' AI function +---@param functionInfo FileFunctionRef +---@param childType any Unused +function BaseOpAI:SetChildrenPlatoonAI(functionInfo, childType) + if not self:FindChildren() then + error('*AI DEBUG: No children for OpAI found') + end + for k, v in self.ChildrenHandles do + v.ChildBuilder.PlatoonAIFunction = functionInfo + end +end - --for k,v in priTable do - -- table.insert(self.MasterData.PlatoonData.TargetPriorities, v) - --end - --for k,v in defaultPri do - -- table.insert(self.MasterData.PlatoonData.TargetPriorities, v) - --end +--- Overrides the default platoon formation +---@param formationName UnitFormations +function BaseOpAI:SetFormation(formationName) + if not self:FindMaster() then return end + self.MasterData.PlatoonData.OverrideFormation = formationName +end - end +---@private +---@param funcName string +---@param bool boolean +function BaseOpAI:SetFunctionStatus(funcName, bool) + ScenarioInfo.OSPlatoonCounter[self.MasterName .. '_' .. funcName] = bool +end - table.insert(self.MasterData.PlatoonAddFunctions, { '/lua/ai/opai/BaseManagerPlatoonThreads.lua', 'PlatoonSetTargetPriorities' }) +--- TODO: make a system out of this. Derive functionality per override per OpAI type +---@private +---@param functionData any +function BaseOpAI:MasterPlatoonFunctionalityChange(functionData) + if functionData[2] == 'LandAssaultWithTransports' then + self:SetFunctionStatus('Transports', true) + end +end - return true - end, - - ---@param self OpAI - ---@param childrenData any - ---```childData = { - --- { 'LightTanks', 'LightBots' }, - --- { - --- Function or function table, - --- Function or function table, - --- Function or function table, - --- }, - ---} - AddChildrenMonitor = function(self, childrenData) - for k,v in childrenData do - self:AddChildMonitor(v) - end - end, - - ---@param self OpAI - ---@param childData any - AddChildMonitor = function(self, childData) - -- add children and functions to the child table in self - -- ChildMonitorData - for tNum,tName in childData[1] do - if self.EnabledTypes[tName] == nil then - error('*AI DEBUG: Invalid child type - ' .. tName .. ' - in OpAI type - ' .. self.BuilderType, 2) - end - if not self.ChildMonitorData[tName] then - self.ChildMonitorData[tName] = {} - end - end - for fNum,fData in childData[2] do - if type(fData) == 'table' then - -- Check function data - for tNum,tName in childData[1] do - table.insert(self.ChildMonitorData[tName], { FunctionInfo = fData }) - end - elseif type(fData) == 'function' then - for tNum,tName in childData[1] do - table.insert(self.ChildMonitorData[tName], { DirectFunction = fData }) - end - end - end +--- Sets the the whole platoon or only units matching categories to target commanders after all other units. +---@param cat? EntityCategory Specifies a subset of the platoon to set target priorities for +---@return boolean +function BaseOpAI:TargetCommanderLast(cat) + return self:SetTargettingPriorities(targetCommanderLastPriorities, cat) +end - -- run the check once and enable/disable as needed - for tNum,tName in childData[1] do - self:ChildMonitorCheck(tName, self.ChildMonitorData[tName]) - end +--- Sets the the whole platoon or only units matching categories to not target commanders. +---@param cat? EntityCategory Specifies a subset of the platoon to set target priorities for +---@return boolean +function BaseOpAI:TargetCommanderNever(cat) + return self:SetTargettingPriorities(targetCommanderNeverPriorities, cat) +end - -- start thread if not already started - if not self.ChildMonitorHandle then - self.ChildMonitorHandle = ForkThread(self.ChildMonitorThread, self) - self.Trash:Add(self.ChildMonitorHandle) +--- Sets the target priorities for the whole platoon or only units matching categories +---@param priTable string[]|EntityCategory[] +---@param cat? EntityCategory specifying a subset of the platoon we wish to set target priorities for +---@return boolean +function BaseOpAI:SetTargettingPriorities(priTable, cat) + if not self:FindMaster() then return false end + + local priList = { unpack(priTable) } + + if cat then + --save the priorities for this category. + if not self.MasterData.PlatoonData.CategoryPriorities then self.MasterData.PlatoonData.CategoryPriorities = {} end + + --NOTE: This should probably be a table.deepcopy if we're going to alter the original table in the future. + self.MasterData.PlatoonData.CategoryPriorities[cat] = priList + else + for _, v in ipairs(defTargetPriorities) do + tableInsert(priList, v) end - end, - ---@param self OpAI - ChildMonitorThread = function(self) - while true do - -- Iterate through list enabling/disabling children types as needed. - for name,data in self.ChildMonitorData do - self:ChildMonitorCheck(name, data) - end - WaitSeconds(7) + local priorities = {} + for _, v in ipairs(priList) do + tableInsert(priorities, v) end - end, - ---@param self OpAI - ---@param childName any - ---@param childData any - ChildMonitorCheck = function(self, childName, childData) - for k,v in childData do - if v.DirectFunction and not v.DirectFunction() then - self:SetChildActive(childName, false) - return - elseif v.FunctionInfo then - if v.FunctionInfo[3][1] == "default_brain" then - table.remove(v.FunctionInfo[3], 1) - end - if not import(v.FunctionInfo[1])[v.FunctionInfo[2]](self.AIBrain, unpack(v.FunctionInfo[3])) then - self:SetChildActive(childName, false) - return + self.MasterData.PlatoonData.TargetPriorities = priorities + end + + tableInsert(self.MasterData.PlatoonAddFunctions, { BMPT, 'PlatoonSetTargetPriorities' }) + + return true +end + +---@class OpAINewChildMonitorData +---@field [1] OpAIChildType[] +---@field [2] (ChildMonitorFnData|fun(): boolean)[] + +---@param childrenData OpAINewChildMonitorData[] +--- ``` +--- childrenData = { +--- { 'LightTanks', 'LightBots' }, +--- { +--- Function or function table, +--- Function or function table, +--- Function or function table, +--- }, +--- } +--- ``` +function BaseOpAI:AddChildrenMonitor(childrenData) + for _, v in childrenData do + self:AddChildMonitor(v) + end +end + +--- Adds a function that will be run periodically to toggle the child. +--- +--- The child stays enabled, unless the function returns `false`. +---@param childData OpAINewChildMonitorData[] +function BaseOpAI:AddChildMonitor(childData) + -- add children and functions to the child table in self + -- ChildMonitorData + ---@type OpAIChildType[] + local children = childData[1] + ---@type (fun(): boolean | ChildMonitorFnData)[] + local functions = childData[2] + for _, childName in children do + if self.EnabledTypes[childName] == nil then + error('*AI DEBUG: Invalid child type - ' .. childName .. ' - in OpAI type - ' .. self.BuilderType, 2) + end + + local monitorData = self.ChildMonitorData[childName] + if not monitorData then + monitorData = {} + self.ChildMonitorData[childName] = monitorData + end + + for _, fnData in functions do + if type(fnData) == 'table' then + if fnData[3][1] == "default_brain" then + tableRemove(fnData[3], 1) end + tableInsert(monitorData, { FunctionInfo = fnData }) + elseif type(fnData) == 'function' then + tableInsert(monitorData, { DirectFunction = fnData }) end end - self:SetChildActive(childName, true) - end, - ---@param self OpAI - ---@param childrenType OpAIChildType[]|OpAIChildType - ---@param quantity integer|integer[] - SetChildQuantity = function(self, childrenType, quantity) - if not self:FindChildren() or not self:FindMaster() then - return - end - self:SetChildActive('All', false) - if type(childrenType) == 'table' then - self:SetChildrenActive(childrenType) - else - self:SetChildActive(childrenType, true) - end - self:SetChildCount(1) - self:KeepChildren(childrenType) - self:OverrideTemplateSize(quantity) - end, + -- run the check once and enable/disable as needed + self:ChildMonitorCheck(childName, monitorData) + end + + -- start thread if not already started + if not self.ChildMonitorHandle then + self.ChildMonitorHandle = ForkThread(self.ChildMonitorThread, self) + self.Trash:Add(self.ChildMonitorHandle) + end +end + +--- Runs periodically to toggle children enabled. +--- +--- Children are enabled unless one of the conditions return `false`. +---@private +function BaseOpAI:ChildMonitorThread() + local monitorData = self.ChildMonitorData + ---@cast monitorData -nil + while true do + -- Iterate through list enabling/disabling children types as needed. + for name, data in pairs(monitorData) do + self:ChildMonitorCheck(name, data) + end + WaitSeconds(7) + end +end - ---@param self OpAI - ---@param childrenType any - RemoveChildren = function(self, childrenType) - if not self:FindChildren() then +---@private +---@param name OpAIChildType +---@param data OpAINewChildMonitorData[] +function BaseOpAI:ChildMonitorCheck(name, data) + for _, v in data do + local fn = v.DirectFunction + local fnInfo = v.FunctionInfo + if fn and not fn() then + self:SetChildActive(name, false) return + elseif fnInfo then + if not import(fnInfo[1])[fnInfo[2]](self.AIBrain, unpack(fnInfo[3])) then + self:SetChildActive(name, false) + return + end end - local removeTable = {} - if type(childrenType) == 'table' then - removeTable = childrenType - else - table.insert(removeTable, childrenType) - end + end + self:SetChildActive(name, true) +end - for k,v in self.ChildrenNames do - if v.ChildrenType then - local found = false - for cNum, cName in v.ChildrenType do - for num,name in removeTable do - if (cName == name) then - found = true - break - end - end - if found then +--- Overrides the the template size of `childrenType` to `quantity` and disables all other childs. +--- +--- Child platoon template **has to match all** `childrenType` to be set. +--- +--- Possible combinations are: +--- - `'HeavyTanks', 6` - Sets the tanks count to 6 +--- - `{'HeavyTanks', 'LightTanks', 'LightArtillery'}, 9` - Sets each to 3 +--- - `{'HeavyTanks', 'LightArtillery'}, {10, 2}` - Sets tanks count to 10, art to 2 +---@param childrenType OpAIChildType[]|OpAIChildType +---@param quantity integer|integer[] +function BaseOpAI:SetChildQuantity(childrenType, quantity) + if not self:FindChildren() or not self:FindMaster() then return end + + self:SetChildActive('All', false) + if type(childrenType) == 'table' then + self:SetChildrenActive(childrenType) + else + self:SetChildActive(childrenType, true) + end + self:SetChildCount(1) + self:KeepChildren(childrenType) + self:OverrideTemplateSize(quantity) +end + +---@param childrenType OpAIChildType|OpAIChildType[] +function BaseOpAI:RemoveChildren(childrenType) + if not self:FindChildren() then return end + + local removeTable = {} + if type(childrenType) == 'table' then + removeTable = childrenType + else + tableInsert(removeTable, childrenType) + end + + local childrenNames = self.ChildrenNames + local childrenHandles = self.ChildrenHandles + for k, v in childrenNames do + if v.ChildrenType then + local found = false + for cNum, cName in v.ChildrenType do + for num, name in removeTable do + if (cName == name) then + found = true break end end - - -- Remove the builder if found then - for num,child in self.ChildrenHandles do - if child.ChildBuilder.BuilderName == v.BuilderName then - self.ChildrenHandles[num] = nil - end + break + end + end + + -- Remove the builder + if found then + for num, child in childrenHandles do + if child.ChildBuilder.BuilderName == v.BuilderName then + childrenHandles[num] = nil end - self.ChildrenNames[k] = nil end + childrenNames[k] = nil end end - end, + end +end - ---@param self OpAI - ---@param childrenType OpAIChildType[]|OpAIChildType - KeepChildren = function(self, childrenType) - if not self:FindChildren() then - return - end - local keepTable = {} - if type(childrenType) == 'table' then - keepTable = childrenType - else - table.insert(keepTable, childrenType) - end +---@param childrenType OpAIChildType[]|OpAIChildType +function BaseOpAI:KeepChildren(childrenType) + if not self:FindChildren() then return end + + ---@type OpAIChildType[] + local keepTable + if type(childrenType) == 'table' then + keepTable = childrenType + else + keepTable = {} + tableInsert(keepTable, childrenType) + end + + local childrenNames = self.ChildrenNames + local childrenHandles = self.ChildrenHandles + for k, v in childrenNames do + if v.ChildrenType then + -- Child must have all children type to be kept + local found + for _, cName in v.ChildrenType do + found = false + for _, name in keepTable do + if (cName == name) then + found = true + break + end + end - for k, v in self.ChildrenNames do - if v.ChildrenType then - -- Child must have all children type to be kept - local found - for _, cName in v.ChildrenType do + -- This child was not found; break out so we can remove + if not found then + break + end + end + + -- All keeptable children must be found to be kept as well. + if found then + for _, name in keepTable do found = false - for _, name in keepTable do - if (cName == name) then + for _, cName in v.ChildrenType do + -- child name found; move to the next + if cName == name then found = true break end end - -- This child was not found; break out so we can remove + -- Child not found; break to remove if not found then break end end + end - -- All keeptable children must be found to be kept as well. - if found then - for _, name in keepTable do - found = false - for _, cName in v.ChildrenType do - -- child name found; move to the next - if cName == name then - found = true - break - end - end - - -- Child not found; break to remove - if not found then - break - end - end - end - - -- Remove the builder - if not found then - self.AIBrain:PBMRemoveBuilder(v.BuilderName) - for num,child in self.ChildrenHandles do - if child.ChildBuilder.BuilderName == v.BuilderName then - self.ChildrenHandles[num] = nil - end + -- Remove the builder + if not found then + self.AIBrain:PBMRemoveBuilder(v.BuilderName) + for num, child in childrenHandles do + if child.ChildBuilder.BuilderName == v.BuilderName then + childrenHandles[num] = nil end - self.ChildrenNames[k] = nil end + childrenNames[k] = nil end end - end, - - ---@param self OpAI - ---@param quantity integer|integer[] - OverrideTemplateSize = function(self, quantity) - for _, v in self.ChildrenHandles do - if type(quantity) == 'table' then - for sNum,sData in v.ChildBuilder.PlatoonTemplate do - if sNum >= 3 then - sData[2] = 1 - sData[3] = quantity[sNum - 2] or 1 - end + end +end + +---@protected +---@param quantity integer|integer[] +function BaseOpAI:OverrideTemplateSize(quantity) + local quantityType = type(quantity) + for _, v in self.ChildrenHandles do + if quantityType == 'table' then + for sNum, sData in v.ChildBuilder.PlatoonTemplate do + if sNum >= 3 then + sData[2] = 1 + sData[3] = quantity[sNum - 2] or 1 end - else - local overrideNum = math.floor(quantity / (table.getn(v.ChildBuilder.PlatoonTemplate) - 2)) - for sNum,sData in v.ChildBuilder.PlatoonTemplate do - if sNum >= 3 then - sData[2] = 1 - sData[3] = overrideNum - end + end + else + local overrideNum = mathFloor(quantity / (tableGetn(v.ChildBuilder.PlatoonTemplate) - 2)) + for sNum, sData in v.ChildBuilder.PlatoonTemplate do + if sNum >= 3 then + sData[2] = 1 + sData[3] = overrideNum end end end - end, - - ---Build conditions for PBM; Attack Conditions for AM Platoons - ---@param self OpAI - ---@param fileName FileName - ---@param funcName string - ---@param parameters table Array with parameters that will be passed to the build condition function - ---@param bName? string - ---@return boolean - AddBuildCondition = function(self, fileName, funcName, parameters, bName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - -- Remove `"default_brain"` param from the condition. It's a remnant of GPG code that got refactored, - -- as the AIBrain is always the first param passed to the build condition - if parameters[1] == "default_brain" then - table.remove(parameters, 1) - end - for _, v in self.ChildrenHandles do - local found + end +end - if bName and v.ChildBuilder.BuilderName then - found = string.find(bName, v.ChildBuilder.BuilderName .. '_', 1, true) - end +--- Build conditions for PBM; Attack Conditions for AM Platoons +---@param fileName FileName +---@param funcName string +---@param parameters table Array with parameters that will be passed to the build condition function +---@param bName? string +---@return boolean +function BaseOpAI:AddBuildCondition(fileName, funcName, parameters, bName) + if not self:FindChildren() or not self:FindMaster() then return false end + + -- Remove `"default_brain"` param from the condition. It's a remnant of GPG code that got refactored, + -- as the AIBrain is always the first param passed to the build condition + if parameters[1] == "default_brain" then + tableRemove(parameters, 1) + end + for _, v in self.ChildrenHandles do + local found + + if bName and v.ChildBuilder.BuilderName then + found = stringFind(bName, v.ChildBuilder.BuilderName .. '_', 1, true) + end + + if not bName or bName == v.ChildBuilder.BuilderName or found then + tableInsert(v.ChildBuilder.BuildConditions, { fileName, funcName, parameters }) + end + end + if not bName or bName == self.MasterName then + tableInsert(self.MasterData.AttackConditions, { fileName, funcName, parameters }) + end + return true +end - if not bName or bName == v.ChildBuilder.BuilderName or found then - table.insert(v.ChildBuilder.BuildConditions, { fileName, funcName, parameters }) - end - end - if not bName or bName == self.MasterName then - table.insert(self.MasterData.AttackConditions, { fileName, funcName, parameters }) - end - return true - end, - - ---@param self OpAI - ---@param funcName string - ---@param bName? string - ---@return boolean - RemoveBuildCondition = function(self, funcName, bName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - for _, v in self.ChildrenHandles do - if not bName or bName == v.ChildBuilder.BuilderName then - for num,bc in v.ChildBuilder.BuildConditions do - if bc[2] == funcName then - v.ChildBuilder.BuildConditions[num] = nil - end +---@param funcName string +---@param bName? string +---@return boolean +function BaseOpAI:RemoveBuildCondition(funcName, bName) + if not self:FindChildren() or not self:FindMaster() then return false end + + for _, v in self.ChildrenHandles do + local builder = v.ChildBuilder + if not bName or bName == builder.BuilderName then + local buildConditions = builder.BuildConditions + if not buildConditions then continue end + -- remove all entries with this fn name + for i = tableGetn(buildConditions), 1, -1 do + local bc = buildConditions[i] + if bc[2] == funcName then + tableRemove(buildConditions, i) end end end - if not bName or bName == self.MasterName then - for num,ac in self.MasterData.AttackConditions do - if ac[2] == funcName then - self.MasterData.AttackConditions[num] = nil - end + end + if not bName or bName == self.MasterName then + local attackConditions = self.MasterData.AttackConditions + -- remove all entries with this fn name + for i = tableGetn(attackConditions), 1, -1 do + local ac = attackConditions[i] + if ac[2] == funcName then + tableRemove(attackConditions, i) end end - return true - end, - - ---Add Functions for PBM Platoons; FormCallbacks for AM Platoons - ---@param self OpAI - ---@param fileName fun(self: Platoon) | FileName - ---@param funcName? string - ---@param bName? string - ---@return boolean - AddAddFunction = function(self, fileName, funcName, bName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - for _, v in self.ChildrenHandles do - if not bName or bName == v.ChildBuilder.BuilderName then - table.insert(v.ChildBuilder.PlatoonAddFunctions, { fileName, funcName }) - end - end - if not bName or bName == self.MasterName then - if type(fileName) == 'function' then - table.insert(self.MasterData.FormCallbacks, fileName) - else - table.insert(self.MasterData.FormCallbacks, { fileName, funcName }) - end - end - return true - end, - - ---Adds a function to run when the platoon is formed. - ---@param self OpAI - ---@param filename fun(self: Platoon) | FileName - ---@param funcName? string - ---@param builderName? string - AddFormCallback = function(self, filename, funcName, builderName) - builderName = builderName or self.MasterName - self:AddAddFunction(filename, funcName, builderName) - end, - - ---Remove Functions for PBM Platoons; FormCallbacks for AM Platoons - ---@param self OpAI - ---@param funcName string - ---@param builderName? string - ---@return boolean - RemoveAddFunction = function(self, funcName, builderName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - for k,v in self.ChildrenHandles do - if not builderName or builderName == v.ChildBuilder.BuilderName then - for num,bc in v.ChildBuilder.PlatoonAddFunctions do - if bc[2] == funcName then - v.ChildBuilder.PlatoonAddFunctions[num] = nil - end - end - end + end + return true +end + +--- Add Functions for PBM Platoons; FormCallbacks for AM Platoons +---@protected +---@param fileName fun(self: Platoon) | FileName +---@param funcName? string +---@param bName? string +---@return boolean +function BaseOpAI:AddAddFunction(fileName, funcName, bName) + if not self:FindChildren() or not self:FindMaster() then return false end + + for _, v in self.ChildrenHandles do + if not bName or bName == v.ChildBuilder.BuilderName then + tableInsert(v.ChildBuilder.PlatoonAddFunctions, { fileName, funcName }) + end + end + if not bName or bName == self.MasterName then + if type(fileName) == 'function' then + tableInsert(self.MasterData.FormCallbacks, fileName) + else + tableInsert(self.MasterData.FormCallbacks, { fileName, funcName }) end - if not builderName or builderName == self.MasterName then - for num,ac in self.MasterData.FormCallbacks do - if ac[2] == funcName then - self.MasterData.FormCallbacks[num] = nil + end + return true +end + +--- Adds a function to run when the platoon is formed. +---@param filename fun(self: Platoon) | FileName +---@param funcName? string +---@param builderName? string +function BaseOpAI:AddFormCallback(filename, funcName, builderName) + builderName = builderName or self.MasterName + self:AddAddFunction(filename, funcName, builderName) +end + +--- Remove Functions for PBM Platoons; FormCallbacks for AM Platoons +---@protected +---@param funcName string +---@param builderName? string +---@return boolean +function BaseOpAI:RemoveAddFunction(funcName, builderName) + if not self:FindChildren() or not self:FindMaster() then return false end + + for k, v in self.ChildrenHandles do + if not builderName or builderName == v.ChildBuilder.BuilderName then + for num, bc in v.ChildBuilder.PlatoonAddFunctions do + if bc[2] == funcName then + v.ChildBuilder.PlatoonAddFunctions[num] = nil end end end - return true - end, - - ---@param self OpAI - ---@param funcName string - ---@param builderName? string - RemoveFormCallback = function(self, funcName, builderName) - self:RemoveAddFunction(funcName, builderName) - end, - - ---Add Build Callback for PBM Platoons; Death Callback for AM Platoons - ---@param self OpAI - ---@param fileName FileName - ---@param funcName string - ---@param builderName? string - ---@return boolean - AddBuildCallback = function(self, fileName, funcName, builderName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - for k,v in self.ChildrenHandles do - if not builderName or builderName == v.ChildBuilder.BuilderName then - table.insert(v.ChildBuilder.PlatoonBuildCallbacks, { fileName, funcName }) + end + if not builderName or builderName == self.MasterName then + for num, ac in self.MasterData.FormCallbacks do + if ac[2] == funcName then + self.MasterData.FormCallbacks[num] = nil end end - if not builderName or builderName == self.MasterName then - table.insert(self.MasterData.DestroyCallbacks, { fileName, funcName }) - end - return true - end, - - ---@param self OpAI - ---@param fileName FileName - ---@param funcName string - ---@param builderName? string - AddDestroyCallback = function(self, fileName, funcName, builderName) - self:AddBuildCallback(fileName, funcName, builderName) - end, - - ---@param self OpAI - ---@param funcName string - ---@param builderName? string - ---@return boolean - RemoveBuildCallback = function(self, funcName, builderName) - if not self:FindChildren() or not self:FindMaster() then - return false - end - for k,v in self.ChildrenHandles do - if not builderName or builderName == v.ChildBuilder.BuilderName then - for num,bc in v.ChildBuilder.PlatoonBuildCallbacks do - if bc[2] == funcName then - v.ChildBuilder.PlatoonBuildCallbacks[num] = nil - end - end - end - end - if not builderName or builderName == self.MasterName then - for num,ac in self.MasterData.FormCallbacks do - if ac[2] == funcName then - self.MasterData.FormCallbacks[num] = nil + end + return true +end + +---@param funcName string +---@param builderName? string +function BaseOpAI:RemoveFormCallback(funcName, builderName) + self:RemoveAddFunction(funcName, builderName) +end + +--- Add Build Callback for PBM Platoons; Death Callback for AM Platoons +---@param fileName FileName +---@param funcName string +---@param builderName? string +---@return boolean +function BaseOpAI:AddBuildCallback(fileName, funcName, builderName) + if not self:FindChildren() or not self:FindMaster() then return false end + + for k, v in self.ChildrenHandles do + if not builderName or builderName == v.ChildBuilder.BuilderName then + tableInsert(v.ChildBuilder.PlatoonBuildCallbacks, { fileName, funcName }) + end + end + if not builderName or builderName == self.MasterName then + tableInsert(self.MasterData.DestroyCallbacks, { fileName, funcName }) + end + return true +end + +---@param fileName FileName +---@param funcName string +---@param builderName? string +function BaseOpAI:AddDestroyCallback(fileName, funcName, builderName) + self:AddBuildCallback(fileName, funcName, builderName) +end + +---@param funcName string +---@param builderName? string +---@return boolean +function BaseOpAI:RemoveBuildCallback(funcName, builderName) + if not self:FindChildren() or not self:FindMaster() then return false end + + for k, v in self.ChildrenHandles do + if not builderName or builderName == v.ChildBuilder.BuilderName then + for num, bc in v.ChildBuilder.PlatoonBuildCallbacks do + if bc[2] == funcName then + v.ChildBuilder.PlatoonBuildCallbacks[num] = nil end end end - return true - end, - - ---@param self OpAI - ---@param funcName string - ---@param builderName? string - RemoveDestroyCallback = function(self, funcName, builderName) - self:RemoveBuildCallback(funcName, builderName) - end, - - ---@param self OpAI - ---@param val boolean - ---@return boolean - MasterUsePool = function(self, val) - if not self:FindMaster() then - return false - end - self.MasterData.UsePool = val - return true - end, - - ---Changed the default (once all units die) rebuild logic for this AI - ---@param self OpAI - ---@param lockType OpAILockType - ---@param lockData? OpAILockData - SetLockingStyle = function(self, lockType, lockData) - if not(lockType == 'None' or lockType == 'DeathTimer' or lockType == 'BuildTimer' or lockType == 'DeathRatio' or lockType == 'RatioTimer') then - error('*AI ERROR: Error adding lock style: valid types are "DeathTimer", "BuildTimer", "DeathRatio", "RatioTimer", or "None"', 2) - end - self:RemoveBuildCondition('AMCheckPlatoonLock') - if lockType ~= 'None' then - self:AddBuildCondition('/lua/editor/amplatoonhelperfunctions.lua', 'AMCheckPlatoonLock', {self.MasterName}) - self:RemoveDestroyCallback('AMUnlockPlatoon', self.MasterName) - self:RemoveFormCallback('AMUnlockBuildTimer', self.MasterName) - self:RemoveFormCallback('AMUnlockRatio', self.MasterName) - if lockType == 'DeathTimer' then - if not lockData or not lockData.LockTimer then - error('*AI DEBUG: Death Timers require the data LockTimer', 2) - end - self:AddDestroyCallback('/lua/editor/amplatoonhelperfunctions.lua', 'AMUnlockPlatoon', self.MasterName) - self.MasterData.PlatoonData.LockTimer = lockData.LockTimer - elseif lockType == 'BuildTimer' then - if not lockData or not lockData.LockTimer then - error('*AI DEBUG: Build Timers require the data LockTimer', 2) - end - self:AddFormCallback(BMPT, 'AMUnlockBuildTimer', self.MasterName) - self.MasterData.PlatoonData.LockTimer = lockData.LockTimer - elseif lockType == 'DeathRatio' then - if not lockData or not lockData.Ratio then - error('*AI DEBUG: Death Ratio unlocking requires the data Ratio', 2) - end - self:AddFormCallback(BMPT, 'AMUnlockRatio', self.MasterName) - self.MasterData.PlatoonData.Ratio = lockData.Ratio - elseif lockType == 'RatioTimer' then - if not lockData or not lockData.Ratio or not lockData.LockTimer then - error('*AI DEBUG: RatioTimer unlocking requires the data "Ratio" and "LockTimer"',2) - end - self:AddFormCallback(BMPT, 'AMUnlockRatioTimer', self.MasterName) - self.MasterData.PlatoonData.LockTimer = lockData.LockTimer - self.MasterData.PlatoonData.Ratio = lockData.Ratio + end + if not builderName or builderName == self.MasterName then + for num, ac in self.MasterData.DestroyCallbacks do + if ac[2] == funcName then + self.MasterData.DestroyCallbacks[num] = nil end end - end, + end + return true +end - ---@param self OpAI - ---@param childrenTypes OpAIChildType[] - SetChildrenActive = function(self, childrenTypes) - if not self:FindChildren() then - return - end +---@see OpAI.RemoveBuildCallback # identical functionality +---@param funcName string +---@param builderName? string +function BaseOpAI:RemoveDestroyCallback(funcName, builderName) + self:RemoveBuildCallback(funcName, builderName) +end - for _, v in childrenTypes do - self:SetChildActive(v, true) - end - end, +---@param val boolean +---@return boolean +function BaseOpAI:MasterUsePool(val) + if not self:FindMaster() then return false end - ---@param self OpAI - ---@param cType OpAIChildType - ---@param val boolean - SetChildActive = function(self, cType, val) - if not self:FindChildren() then - return - end + self.MasterData.UsePool = val + return true +end - -- check against self.EnabledTypes +--- Changes the default (once all units die) rebuild condition for this AI platoon. +---@param lockType OpAILockType +---@param lockData? OpAILockData +function BaseOpAI:SetLockingStyle(lockType, lockData) + if not (lockType == 'None' or lockType == 'DeathTimer' or lockType == 'BuildTimer' or lockType == 'DeathRatio' or lockType == 'RatioTimer') then + error('*AI ERROR: Error adding lock style: valid types are "DeathTimer", "BuildTimer", "DeathRatio", "RatioTimer", or "None"', 2) + end + self:RemoveBuildCondition('AMCheckPlatoonLock') + if lockType == 'None' then return end + + self:AddBuildCondition('/lua/editor/amplatoonhelperfunctions.lua', 'AMCheckPlatoonLock', { self.MasterName }) + self:RemoveDestroyCallback('AMUnlockPlatoon', self.MasterName) + self:RemoveFormCallback('AMUnlockBuildTimer', self.MasterName) + self:RemoveFormCallback('AMUnlockRatio', self.MasterName) + if lockType == 'DeathTimer' then + if not lockData or not lockData.LockTimer then + error('*AI DEBUG: Death Timers require the data LockTimer', 2) + end + self:AddDestroyCallback('/lua/editor/amplatoonhelperfunctions.lua', 'AMUnlockPlatoon', self.MasterName) + self.MasterData.PlatoonData.LockTimer = lockData.LockTimer + elseif lockType == 'BuildTimer' then + if not lockData or not lockData.LockTimer then + error('*AI DEBUG: Build Timers require the data LockTimer', 2) + end + self:AddFormCallback(BMPT, 'AMUnlockBuildTimer', self.MasterName) + self.MasterData.PlatoonData.LockTimer = lockData.LockTimer + elseif lockType == 'DeathRatio' then + if not lockData or not lockData.Ratio then + error('*AI DEBUG: Death Ratio unlocking requires the data Ratio', 2) + end + self:AddFormCallback(BMPT, 'AMUnlockRatio', self.MasterName) + self.MasterData.PlatoonData.Ratio = lockData.Ratio + elseif lockType == 'RatioTimer' then + if not lockData or not lockData.Ratio or not lockData.LockTimer then + error('*AI DEBUG: RatioTimer unlocking requires the data "Ratio" and "LockTimer"', 2) + end + self:AddFormCallback(BMPT, 'AMUnlockRatioTimer', self.MasterName) + self.MasterData.PlatoonData.LockTimer = lockData.LockTimer + self.MasterData.PlatoonData.Ratio = lockData.Ratio + end +end - if cType ~= 'All' then - self.EnabledTypes[cType] = val - else - for k, _ in self.EnabledTypes do - self.EnabledTypes[k] = val - end +---@param childrenTypes OpAIChildType[] +function BaseOpAI:SetChildrenActive(childrenTypes) + if not self:FindChildren() then return end + + for _, v in childrenTypes do + self:SetChildActive(v, true) + end +end + +---@param cType OpAIChildType +---@param val boolean +function BaseOpAI:SetChildActive(cType, val) + if not self:FindChildren() then return end + + -- check against self.EnabledTypes + local enabledTypes = self.EnabledTypes + + if cType ~= 'All' then + enabledTypes[cType] = val + else + for k, _ in enabledTypes do + enabledTypes[k] = val end + end - -- Loop through children - for _, v in self.ChildrenNames do - -- Make sure this child has children types - if not v.ChildrenType then continue end + -- Loop through children + for _, v in self.ChildrenNames do + -- Make sure this child has children types + local childrenType = v.ChildrenType + if not childrenType then continue end - -- We don't want to change by default - local change = false - -- if the type is 'All' or we find that this builder has this child type, we may want to change - for _, cName in v.ChildrenType do - if (cName == cType) or (cType == 'All') then - change = true - break - end + -- We don't want to change by default + local change = false + -- if the type is 'All' or we find that this builder has this child type, we may want to change + for _, cName in childrenType do + if (cName == cType) or (cType == 'All') then + change = true + break end + end - -- Need to change the children here - if not change then continue end + -- Need to change the children here + if not change then continue end - -- make sure that this builder's enabled types are all active - local changeVal = true - for _, cName in v.ChildrenType do - -- This child type is not enabled, we'll want to disable this child type - if not self.EnabledTypes[cName] then - changeVal = false - break - end - end - if changeVal then - if not self:AddBuildCondition(MIBC, 'True', {}, v.BuilderName) or - not self:RemoveBuildCondition('False', v.BuilderName) then - error('*AI ERROR: Error Adding build condition',2) - end - else - if not self:AddBuildCondition(MIBC, 'False', {}, v.BuilderName) or - not self:RemoveBuildCondition('True', v.BuilderName) then - error('*AI ERROR: Error Adding build condition',2) - end + -- make sure that this builder's enabled types are all active + local changeVal = true + for _, cName in childrenType do + -- This child type is not enabled, we'll want to disable this child type + if not enabledTypes[cName] then + changeVal = false + break end end - end, - - ---@param self OpAI - ---@param brain CampaignAIBrain - ---@param location string Name of the base - ---@param builderType SaveFile | string Save file that is used to find child quantities - ---@param name string A name set by you to allow you to retrieve the returned AI instance - ---@param builderData AddOpAIData? - Create = function(self, brain, location, builderType, name, builderData) - if not self.PreCreateFinished then - self:PreCreate() - end - -- local tables to this class instance - self.ChildMonitorData = {} - self.ChildrenNames = {} - self.EnabledTypes = {} - - -- Store off local instances of some variables - self.AIBrain = brain - self.LocationType = location - self.BuilderType = builderType - if type(self.BuilderType) == 'string' then - self.GlobalVarName = name .. '_' .. self.BuilderType + if changeVal then + if not self:AddBuildCondition(MIBC, 'True', emptyTable, v.BuilderName) or + not self:RemoveBuildCondition('False', v.BuilderName) then + error('*AI ERROR: Error Adding build condition', 2) + end else - self.GlobalVarName = name .. '_' .. self.BuilderType.Name + if not self:AddBuildCondition(MIBC, 'False', emptyTable, v.BuilderName) or + not self:RemoveBuildCondition('True', v.BuilderName) then + error('*AI ERROR: Error Adding build condition', 2) + end end + end +end - -- Load all the platoon data info in the formation desired - local platoonData = {} - if not builderData then - platoonData.Priority = 0 - platoonData.PlatoonData = {} +--- Sets up all the variables and loads platoon builders. +--- +--- **This function is called automatically if the OpAI is added through BaseManager** +---@see BaseManager.AddOpAI +---@param brain CampaignAIBrain +---@param location string Name of the base +---@param builderType OpAIPlatoonTpFile | GeneratedScenario Save file that is used to find child quantities +---@param name string A name set by you to allow you to retrieve the returned AI instance +---@param builderData AddOpAIData? +function BaseOpAI:Create(brain, location, builderType, name, builderData) + if not self.PreCreateFinished then self:PreCreate() end + -- local tables to this class instance + self.ChildMonitorData = {} + self.ChildrenNames = {} + self.EnabledTypes = {} + + -- Store off local instances of some variables + self.AIBrain = brain + self.LocationType = location + self.BuilderType = builderType + ---@type string + local builderTypeName + if type(self.BuilderType) == 'string' then + self.GlobalVarName = name .. '_' .. self.BuilderType + builderTypeName = self.BuilderType --[[@as string]] + else + self.GlobalVarName = name .. '_' .. self.BuilderType.Name + builderTypeName = self.BuilderType.Name + end + + -- Load all the platoon data info in the formation desired + local data = {} + if not builderData then + data.Priority = 0 + data.PlatoonData = {} + else + -- Set PlatoonData + if builderData.PlatoonData then + data.PlatoonData = builderData.PlatoonData else - -- Set PlatoonData - if builderData.PlatoonData then - platoonData.PlatoonData = builderData.PlatoonData - else - platoonData.PlatoonData = {} - end - -- Set priority - if builderData.Priority then - platoonData.Priority = builderData.Priority - else - platoonData.Priority = 0 - end + data.PlatoonData = {} end - platoonData.LocationType = location - - local builders = false - local saveFile + -- Set priority + if builderData.Priority then + data.Priority = builderData.Priority + else + data.Priority = 0 + end + end + data.LocationType = location - if type(self.BuilderType) == "string" then --BuilderType is old-school - ScenarioUtils.LoadOSB('OSB_' .. self.BuilderType .. '_' .. name, brain.Name, platoonData) + ---@type table? + local builders + ---@type {Scenario: Scenario} + local saveFile - local fileName = '/lua/ai/OpAI/' .. self.BuilderType .. '_save.lua' - saveFile = import(fileName) + if type(self.BuilderType) == "string" then --BuilderType is old-school + ScenarioUtils.LoadOSB('OSB_' .. builderTypeName .. '_' .. name, brain.Name, data) - builders = saveFile.Scenario.Armies['ARMY_1'].PlatoonBuilders.Builders - self.MasterName = 'OSB_Master_' .. self.BuilderType .. '_' .. brain.Name .. '_' .. name - else --If BuilderType is a table (was pregenerated) + local fileName = '/lua/ai/OpAI/' .. builderTypeName .. '_save.lua' + saveFile = import(fileName) - ScenarioUtils.LoadOSB(builderType, brain.Name, platoonData) - saveFile = {Scenario = builderType} + self.MasterName = 'OSB_Master_' .. builderTypeName .. '_' .. brain.Name .. '_' .. name + else --If BuilderType is a table (was pregenerated) + ScenarioUtils.LoadOSB(builderType, brain.Name, data) + saveFile = { Scenario = builderType --[[@as GeneratedScenario]] } - --self.MasterName = 'OSB_Master_' .. saveFile.Scenario.Name .. '_' .. brain.Name .. '_' .. name - self.MasterName = 'OSB_Master_' .. saveFile.Scenario.Name .. '_' .. brain.Name - end + --self.MasterName = 'OSB_Master_' .. saveFile.Scenario.Name .. '_' .. brain.Name .. '_' .. name + self.MasterName = 'OSB_Master_' .. saveFile.Scenario.Name --[[@as string]] .. '_' .. brain.Name + end - builders = saveFile.Scenario.Armies['ARMY_1'].PlatoonBuilders.Builders + builders = saveFile.Scenario.Armies['ARMY_1'].PlatoonBuilders.Builders - if not builders then - error('*OpAI ERROR: No OpAI Global named: '..self.BuilderType, 2) - end - for k,v in builders do - if string.sub(k, 1, 10) == 'OSB_Child_' then - local startCheck = 11 - if type(self.BuilderType) == "string" then - startCheck = startCheck + 1 + string.len(self.BuilderType) - else - startCheck = startCheck + 1 + string.len(self.BuilderType.Name) - end - local cType = string.sub(k,startCheck) - - table.insert(self.ChildrenNames, { BuilderName = k..'_'..brain.Name..'_'..name, ChildrenType = v.ChildrenType }) - self:AddChildType(v.ChildrenType) + if not builders then + error('*OpAI ERROR: No OpAI Global named: ' .. builderTypeName, 2) + end + for k, v in builders do + if stringSub(k, 1, 10) == 'OSB_Child_' then + local startCheck = 11 + if type(self.BuilderType) == "string" then + startCheck = startCheck + 1 + stringLen(builderTypeName --[[@as string]]) + else + startCheck = startCheck + 1 + stringLen(builderTypeName) end + local cType = stringSub(k, startCheck) + + tableInsert(self.ChildrenNames, { BuilderName = k .. '_' .. brain.Name .. '_' .. name, ChildrenType = v.ChildrenType }) + self:AddChildType(v.ChildrenType) end - if builderData and builderData.MasterPlatoonFunction then - if self:FindMaster() then - self.MasterData.AIThread = builderData.MasterPlatoonFunction - self:MasterPlatoonFunctionalityChange(builderData.MasterPlatoonFunction) - end + end + if builderData and builderData.MasterPlatoonFunction then + if self:FindMaster() then + self.MasterData.AIThread = builderData.MasterPlatoonFunction + self:MasterPlatoonFunctionalityChange(builderData.MasterPlatoonFunction) end + end + + self:AddBuildCondition(BMBC, 'BaseActive', { location }) +end + +OpAI = ClassSimple(BaseOpAI) - self:AddBuildCondition(BMBC, 'BaseActive', { location }) - end, -} ---@param brain CampaignAIBrain ---@param location string ----@param builderType SaveFile | string +---@param builderType OpAIPlatoonTpFile | string ---@param name string ---@param builderData AddOpAIData? ---@return OpAI diff --git a/lua/AI/OpAI/ConditionalBuildManager.lua b/lua/AI/OpAI/ConditionalBuildManager.lua new file mode 100644 index 00000000000..2c81c8b0d14 --- /dev/null +++ b/lua/AI/OpAI/ConditionalBuildManager.lua @@ -0,0 +1,75 @@ +--- Helper class that manages construction projects of mobile units by engineers in the BaseManager. +--- +--- Used for building units that can't be produced from factories and must be built by engineers. +--- - experimentals +--- - sonars +--- - any other mobile unit that the engineers can build. +---@class ConditionalBuildManager +---@field Index integer # Index of the conditional build entry from the base manager that is being built +---@field IsBuilding boolean # Is currently producing unit? +--- True when the engineer is starting a new build, +--- prevents other engineers from starting their own builds +---@field IsInitiated boolean +---@field MainBuilder? Unit # Engineer that was issued to build the required unit. +---@field MaxAssisting integer # Max engineers that can assist the construction +---@field NumAssisting integer # Number of engies currently assisting +---@field Unit? Unit # The actual unit being constructed currently +---@field WaitSecondsAfterDeath? integer # Time to wait after conditional build's death before starting a new one. +---@overload fun(): ConditionalBuildManager +local CBM = {} +function CBM:__init() + self.IsInitiated = false + self.IsBuilding = false + self.NumAssisting = 0 + self.MaxAssisting = 1 + self.Index = 0 +end + +--- Store the engineer that was issued to start the construction and update the values so other +--- engineers won't try to start their own. +---@param builder Unit +---@param data AddUnitAIData +function CBM:OnUnitConstructionRequested(builder, data) + self.MainBuilder = builder + self.NumAssisting = 1 + self.MaxAssisting = data.MaxAssist or 1 + self.Unit = nil + self.IsInitiated = true + self.IsBuilding = false + self.WaitSecondsAfterDeath = data.WaitSecondsAfterDeath +end + +--- Stores the unit and sets variables so other engineers can see what's going on +---@param unitBeingbuilt Unit +function CBM:OnUnitConstructionStarted(unitBeingbuilt) + self.IsInitiated = false + self.IsBuilding = true + self.Unit = unitBeingbuilt +end + +function CBM:IncrementAssisting() + self.NumAssisting = self.NumAssisting + 1 +end + +function CBM:DecrementAssisting() + self.NumAssisting = self.NumAssisting - 1 +end + +--- Resets the stored values so it can be reused for another build project +function CBM:Reset() + self.IsInitiated = false + self.IsBuilding = false + self.NumAssisting = 0 + self.MaxAssisting = 1 + self.Unit = nil + self.MainBuilder = nil + self.Index = 0 + self.WaitSecondsAfterDeath = nil +end + +---@return boolean? +function CBM:NeedsMoreBuilders() + return self.IsBuilding and self.Unit and not self.Unit.Dead and (self.NumAssisting < self.MaxAssisting) +end + +ConditionalBuildManager = ClassSimple(CBM) diff --git a/lua/AI/OpAI/GenerateNaval.lua b/lua/AI/OpAI/GenerateNaval.lua index 49f2dec59ae..f88b91ee45d 100644 --- a/lua/AI/OpAI/GenerateNaval.lua +++ b/lua/AI/OpAI/GenerateNaval.lua @@ -9,226 +9,360 @@ local ScenarioFramework = import("/lua/scenarioframework.lua") +local SPAI = '/lua/ScenarioPlatoonAI.lua' + +local unpack = unpack +local mathFloor = math.floor +local mathMod = math.mod +local tableInsert = table.insert +local tableFind = table.find + +--- Generated data with platoon templates and builders for OpAI +---@class GeneratedScenario: Scenario +---@field Name string + +---@alias NavalOpAIChildType +---| 'Frigate' +---| 'Destroyer' +---| 'Battleship' +---| 'T3Submarine' +---| 'T2Submarine' +---| 'Submarine' +---| 'Cruiser' +---| 'LightBoat' # Aeon AA boat +---| 'Utility' # Shield, stealth boats +---| 'Fatty' # T3 Battlecruiser / Missile ship +---| 'Carrier' # Aircraft carriers +---| 'NukeSubmarine' + +---@alias NavalOpAIGenType +---| 'CORE' # Firagates, Destroyers, Battleships +---| 'SUBS' # T1 subs, T2 sub-hunters +---| 'LIGHT' # Aeon AA boat +---| 'CRUISERS' # Cruisers +---| 'UTILITY' # Shield, stealth boats +---| 'CARRIERS' # Aircraft carriers +---| 'FATTIES' # T3 Battlecruiser / Missile ship +---| 'NUKESUBS' # T3 nuke subs + +---@alias NavalOpAIGenConversion +---| 'FRIGATES_PER_DESTROYER' # How many ships must exist before we converting to next tier. Defaults to `5` +---| 'DESTROYERS_PER_BATTLESHIP' # How many ships must exist before we converting to next tier. Defaults to `5` +---| 'CORE_TO_SUBS' # How many core ships must be places before we convert to this type. Defaults to `2`. +---| 'CORE_TO_CRUISERS' # How many core ships must be places before we convert to this type. Defaults to `4`. +---| 'CORE_TO_FATTIES' # How many core ships must be places before we convert to this type. Defaults to `3`. +---| 'CORE_TO_CARRIERS' # How many core ships must be places before we convert to this type. Defaults to `3`. +---| 'CORE_TO_LIGHT' # How many core ships must be places before we convert to this type. Defaults to `2`. +---| 'CORE_TO_UTILITY' # How many core ships must be places before we convert to this type. Defaults to `2`. +---| 'CORE_TO_NUKESUBS' # How many core ships must be places before we convert to this type. Defaults to `3`. + +---@class NavalOpAIGeneratorData +--- The starting number of virtual frigate units allocated to the very first platoon template (Tier 1, Wave 1) +--- +--- Sets the baseline strength and size for the weakest naval wave. Higher values result in larger starting fleets +--- +--- If not provided, it will be calculated from the number of naval factories in the base +---@field MaxFrigates? integer +--- The target number of virtual frigate units allocated to the final platoon template (Tier 3, final wave) +--- +--- Controls the maximum overall scaling and final size of the endgame naval force +--- +--- If not provided, it will be calculated from the number of naval factories in the base +---@field MinFrigates? integer +--- Used to calculate `MaxFrigates` based on number of naval factories in the base. +--- +--- Used only if `MaxFrigates` is not specified. +--- +--- Defaults to `1` +---@field MaxMultiplier? integer +--- Used to calculate `MinFrigates` based on number of naval factories in the base. +--- +--- Used only if `MinFrigates` is not specified. +--- +--- Defaults to `MaxMultiplier` +---@field MinMultiplier? integer +--- The number of distinct platoon build waves generated within each of the 3 tech tiers (T1, T2, and T3) +--- +--- Determines total progression steps Nx3. +--- It controls the growth smooth steps: higher values create more granular progression with smaller unit increments +--- between waves,while lower values cause steeper jumps in fleet composition +--- +--- Defaults to `1` +---@field NumLevels? integer +--- If specified, only these types will be used to generate the template +--- +--- `Frigates` can't be disabled. +---@field EnabledTypes? NavalOpAIChildType[] +--- Disables certain childs from being added during during template generation. +--- +--- Core ships can't be disabled. +---@field DisableTypes? table +--- Overrides default conversions for generating the platoon. +--- +---@see NavalOpAIGenConversion +---@field Overrides? table + --To make life easier with factions not having identical naval units and such. -local TIERS = -{ +local TIERS = { --Tier 1 naval units - { - CORE = {U='ues0103', C='urs0103', A='uas0103', S='xss0103'}, - SUBS = {U='ues0203', C='urs0203', A='uas0203', S='xss0203'}, - LIGHT = { A='uas0102', }, + { -- UEF AEON CYBRAN SERA + CORE = { 'ues0103', 'uas0103', 'urs0103', 'xss0103' }, + SUBS = { 'ues0203', 'uas0203', 'urs0203', 'xss0203' }, + LIGHT = { nil , 'uas0102', nil , nil }, }, --Tier 2 naval units { - CORE = {U='ues0201', C='urs0201', A='uas0201', S='xss0201'}, - SUBS = {U='xes0102', C='xrs0204', A='xas0204', S='xss0203'}, --note seraphim have no T2 sub hunter - CRUISERS = {U='ues0202', C='urs0202', A='uas0202', S='xss0202'}, - UTILITY = {U='xes0205', C='xrs0205', }, + CORE = { 'ues0201', 'uas0201', 'urs0201', 'xss0201' }, + SUBS = { 'xes0102', 'xas0204', 'xrs0204', 'xss0203' }, --note seraphim have no T2 sub hunter + CRUISERS = { 'ues0202', 'uas0202', 'urs0202', 'xss0202' }, + UTILITY = { 'xes0205', nil , 'xrs0205', nil }, }, --Tier 3 naval units { - CORE = {U='ues0302', C='urs0302', A='uas0302', S='xss0302'}, - SUBS = {U='xes0102', C='xrs0204', A='xas0204', S='xss0304'}, - CRUISERS = {U='ues0202', C='urs0202', A='uas0202', S='xss0202'}, - CARRIERS = { C='urs0303', A='uas0303', S='xss0303'}, - FATTIES = {U='xes0307', A='xas0306', }, - UTILITY = {U='xes0205', C='xrs0205', }, - NUKESUBS = {U='ues0304', C='urs0304', A='uas0304', }, + CORE = { 'ues0302', 'uas0302', 'urs0302', 'xss0302' }, + SUBS = { 'xes0102', 'xas0204', 'xrs0204', 'xss0304' }, + CRUISERS = { 'ues0202', 'uas0202', 'urs0202', 'xss0202' }, + CARRIERS = { nil , 'uas0303', 'urs0303', 'xss0303' }, + FATTIES = { 'xes0307', 'xas0306', nil , nil }, + UTILITY = { 'xes0205', nil , 'xrs0205', nil }, + NUKESUBS = { 'ues0304', 'uas0304', 'urs0304', nil }, }, } local BasePriority = 700 -local Conversions = -{ ---How many ships must exist before we convert them to one of the next-tier core ship +--- How many ships must exist before we convert them to one of the next-tier core ship +--- +--- How many core ships (frigates, destroyers, battleships) must be in a platoon before we include one of these unit types. +local Conversions = { FRIGATES_PER_DESTROYER = 5, DESTROYERS_PER_BATTLESHIP = 5, - ---How many core ships (frigates, destroyers, battleships) must be in a platoon before we include one of these unit types. CORE_TO_SUBS = 2, CORE_TO_CRUISERS = 4, CORE_TO_FATTIES = 3, CORE_TO_CARRIERS = 3, - CORE_TO_LIGHT = 0.5, + CORE_TO_LIGHT = 2, CORE_TO_UTILITY = 2, CORE_TO_NUKESUBS = 3, } -function IsEnabledType(unitType, data) - for _, v in data.EnabledTypes do - if unitType == v then - return true - end - end - return false +--- Returns true if `enabledTypes` is not specified or when it contains `unitType` +---@param unitType NavalOpAIChildType +---@param enabledTypes? NavalOpAIChildType[] +---@return boolean +local function isEnabledType(unitType, enabledTypes) + -- Everything is enabled by default + if not enabledTypes then return true end + return tableFind(enabledTypes, unitType) ~= nil end -function GenerateNavalOSB(name, levelsPerTier, minFrigates, maxFrigates, faction, data) - if data.Overrides then - for k, v in data.Overrides do - Conversions[k] = v - end - end +---@param name NavalOpAIGenConversion +---@param overrides? table +---@return number +local function getConversion(name, overrides) + return (overrides and overrides[name]) or Conversions[name] +end - local allEnabled = true - if data.EnabledTypes then - allEnabled = false - end +---@param name string +---@param levelsPerTier integer +---@param minFrigates integer +---@param maxFrigates integer +---@param faction integer 1=UEF, 2=Aeon, 3=Cybran, 4=Seraphim +---@param data NavalOpAIGeneratorData +---@return GeneratedScenario +function GenerateNavalOSB(name, levelsPerTier, minFrigates, maxFrigates, faction, data) + local enabledTypes = data.EnabledTypes + local overrides = data.Overrides + + local builders = {} + local Scenario = { + Name = name, + Platoons = { + OST_BLANK_TEMPLATE = { 'OST_BLANK_TEMPLATE', '' }, + }, + Armies = { + ARMY_1 = { + PlatoonBuilders = { + Builders = builders, + }, + }, + }, + } + local masterPlatoonName = 'OSB_Master_' .. name - local Scenario = { Platoons = {}, Armies = { ARMY_1 = { PlatoonBuilders = { Builders = { } } } }, Name = name } local levels = levelsPerTier * 3 --Frigate increment per level - local frigInc = (maxFrigates-minFrigates)/(levels-1) + local frigInc = (maxFrigates - minFrigates) / (levels - 1) --Accumulators local frigAcc = minFrigates local destAcc = 0 local battAcc = 0 - Scenario.Platoons['OST_BLANK_TEMPLATE'] = {'OST_BLANK_TEMPLATE', ''} + local TIER1, TIER2, TIER3 = TIERS[1], TIERS[2], TIERS[3] --Build the stuff for level = 1, levels do --Types of naval units in the platoon - local tier = math.floor((level-1)/levelsPerTier) + 1 - local waveLevel = math.mod(level-1, levelsPerTier) + 1 - local template = 'OST_' .. name .. '_' .. tostring(tier) .. '-' .. tostring(waveLevel) .. '_Template' + local tier = mathFloor((level - 1) / levelsPerTier) + 1 + local waveLevel = mathMod(level - 1, levelsPerTier) + 1 + local tpName = 'OST_' .. name .. '_' .. tier .. '-' .. waveLevel .. '_Template' - local children = {'T' .. tostring(tier)} --, 'L' .. tostring(level)} + ---@type OpAIChildType[] + local children = { 'T' .. tier } --, 'L' .. tostring(level)} - Scenario.Platoons[template] = {template, ''} + local template = { tpName, '' } + Scenario.Platoons[tpName] = template - -------------------------------------------- + -------------------------------- -- Generate the platoon template - -------------------------------------------- + -------------------------------- --If we're tier 2 or higher, convert frigates to destroyers at the going rate - while tier >= 2 and frigAcc >= Conversions.FRIGATES_PER_DESTROYER do - frigAcc = frigAcc - Conversions.FRIGATES_PER_DESTROYER + while tier >= 2 and frigAcc >= getConversion('FRIGATES_PER_DESTROYER', overrides) do + frigAcc = frigAcc - getConversion('FRIGATES_PER_DESTROYER', overrides) destAcc = destAcc + 1 end --If we're tier 3 or higher, convert destroyers to battleships at the going rate - while tier >= 3 and destAcc >= Conversions.DESTROYERS_PER_BATTLESHIP do - destAcc = destAcc - Conversions.DESTROYERS_PER_BATTLESHIP + while tier >= 3 and destAcc >= getConversion('DESTROYERS_PER_BATTLESHIP', overrides) do + destAcc = destAcc - getConversion('DESTROYERS_PER_BATTLESHIP', overrides) battAcc = battAcc + 1 end - local numFrigates = math.floor(frigAcc) - local numDestroyers = math.floor(destAcc) - local numBattleships = math.floor(battAcc) + local numFrigates = mathFloor(frigAcc) + local numDestroyers = mathFloor(destAcc) + local numBattleships = mathFloor(battAcc) if numFrigates > 0 then - table.insert(Scenario.Platoons[template], {TIERS[1].CORE[faction], 1, numFrigates, 'attack', 'None'}) - table.insert(children, 'Frigate') + tableInsert(template, { TIER1.CORE[faction], 1, numFrigates, 'Attack', 'None' }) + tableInsert(children, 'Frigate') end - if (allEnabled or IsEnabledType('Destroyer', data)) and numDestroyers > 0 then - table.insert(Scenario.Platoons[template], {TIERS[2].CORE[faction], 1, numDestroyers, 'attack', 'None'}) - table.insert(children, 'Destroyer') + if isEnabledType('Destroyer', enabledTypes) and numDestroyers > 0 then + tableInsert(template, { TIER2.CORE[faction], 1, numDestroyers, 'Attack', 'None' }) + tableInsert(children, 'Destroyer') end - if (allEnabled or IsEnabledType('Battleship', data)) and numBattleships > 0 then - table.insert(Scenario.Platoons[template], {TIERS[3].CORE[faction], 1, numBattleships, 'attack', 'None'}) - table.insert(children, 'Battleship') + if isEnabledType('Battleship', enabledTypes) and numBattleships > 0 then + tableInsert(template, { TIER3.CORE[faction], 1, numBattleships, 'Attack', 'None' }) + tableInsert(children, 'Battleship') end -- Do submarines. - local numSubmarines = 0 - if (allEnabled or IsEnabledType('Submarine', data)) then - if tier == 1 then numSubmarines = math.floor(numFrigates / Conversions.CORE_TO_SUBS) - elseif tier >= 2 then numSubmarines = math.floor(numDestroyers / Conversions.CORE_TO_SUBS) - elseif tier >= 3 then numSubmarines = math.floor(numBattleships / Conversions.CORE_TO_SUBS) end - if numSubmarines > 0 then + if isEnabledType('Submarine', enabledTypes) then + local numSubmarines = 0 + if tier >= 3 then + numSubmarines = mathFloor(numBattleships / getConversion('CORE_TO_SUBS', overrides)) + elseif tier >= 2 then + numSubmarines = mathFloor(numDestroyers / getConversion('CORE_TO_SUBS', overrides)) + else + numSubmarines = mathFloor(numFrigates / getConversion('CORE_TO_SUBS', overrides)) + end + if numSubmarines > 0 then local placed = false if tier == 3 and not data.DisableTypes['T3Submarine'] then - table.insert(Scenario.Platoons[template], {TIERS[3].SUBS[faction], 1, numSubmarines, 'guard', 'None'}) - table.insert(children, 'T3Submarine') + tableInsert(template, { TIER3.SUBS[faction], 1, numSubmarines, 'Guard', 'None' }) + tableInsert(children, 'T3Submarine') placed = true elseif tier >= 2 and not placed and not data.DisableTypes['T2Submarine'] then - table.insert(Scenario.Platoons[template], {TIERS[2].SUBS[faction], 1, numSubmarines, 'guard', 'None'}) - table.insert(children, 'T2Submarine') + tableInsert(template, { TIER2.SUBS[faction], 1, numSubmarines, 'Guard', 'None' }) + tableInsert(children, 'T2Submarine') elseif not placed and not data.DisableTypes['Submarine'] then - table.insert(Scenario.Platoons[template], {TIERS[1].SUBS[faction], 1, numSubmarines, 'guard', 'None'}) - table.insert(children, 'Submarine') + tableInsert(template, { TIER1.SUBS[faction], 1, numSubmarines, 'Guard', 'None' }) + tableInsert(children, 'Submarine') end end end -- Do cruisers. - local numCruisers = 0 - if (allEnabled or IsEnabledType('Cruiser', data)) then - if tier == 2 then numCruisers = math.floor(numDestroyers / Conversions.CORE_TO_CRUISERS) - elseif tier >= 3 then numCruisers = math.floor(numBattleships / Conversions.CORE_TO_CRUISERS) end + if isEnabledType('Cruiser', enabledTypes) then + local numCruisers = 0 + if tier == 2 then + numCruisers = mathFloor(numDestroyers / getConversion('CORE_TO_CRUISERS', overrides)) + elseif tier >= 3 then + numCruisers = mathFloor(numBattleships / getConversion('CORE_TO_CRUISERS', overrides)) + end + if numCruisers > 0 then - table.insert(Scenario.Platoons[template], {TIERS[tier].CRUISERS[faction], 1, numCruisers, 'guard', 'None'}) - table.insert(children, 'Cruiser') + tableInsert(template, { TIERS[tier].CRUISERS[faction], 1, numCruisers, 'Guard', 'None' }) + tableInsert(children, 'Cruiser') end end -- Do light T1 boats only at T1. Note not every faction has light T1 boats - local numLight = 0 - if (allEnabled or IsEnabledType('LightBoat', data)) then - if tier == 1 and TIERS[1].LIGHT[faction] then numLight = math.floor(numFrigates / Conversions.CORE_TO_LIGHT) end + if isEnabledType('LightBoat', enabledTypes) then + local numLight = 0 + if tier == 1 and TIER1.LIGHT[faction] then + numLight = mathFloor(numFrigates / getConversion('CORE_TO_LIGHT', overrides)) + end + if numLight > 0 then - table.insert(Scenario.Platoons[template], {TIERS[1].LIGHT[faction], 1, numLight, 'guard', 'None'}) - table.insert(children, 'LightBoat') + tableInsert(template, { TIER1.LIGHT[faction], 1, numLight, 'Guard', 'None' }) + tableInsert(children, 'LightBoat') end end -- Do T2 utility boats. Cybran = stealth boat, UEF = shields. Note not every faction has a T2 utility boat. - local numUtility = 0 - if (allEnabled or IsEnabledType('Utility', data)) then - if tier == 2 and TIERS[2].UTILITY[faction] then numUtility = math.floor(numDestroyers / Conversions.CORE_TO_UTILITY) - elseif tier >= 3 and TIERS[3].UTILITY[faction] then numUtility = math.floor(numBattleships / Conversions.CORE_TO_UTILITY) end + if isEnabledType('Utility', enabledTypes) then + local numUtility = 0 + if tier == 2 and TIER2.UTILITY[faction] then + numUtility = mathFloor(numDestroyers / getConversion('CORE_TO_UTILITY', overrides)) + elseif tier >= 3 and TIER3.UTILITY[faction] then + numUtility = mathFloor(numBattleships / getConversion('CORE_TO_UTILITY', overrides)) + end + if numUtility > 0 then - table.insert(Scenario.Platoons[template], {TIERS[3].UTILITY[faction], 1, numUtility, 'guard', 'None'}) - table.insert(children, 'Utility') + tableInsert(template, { TIER3.UTILITY[faction], 1, numUtility, 'Guard', 'None' }) + tableInsert(children, 'Utility') end end -- Do T3 fatties. battlecruiser, missile ship. Note Aeon and Cybran have no fatties. - local numFatties = 0 - if (allEnabled or IsEnabledType('Fatty', data)) then - if tier >= 3 and TIERS[3].FATTIES[faction] then numFatties = math.floor(numBattleships / Conversions.CORE_TO_FATTIES) end + if isEnabledType('Fatty', enabledTypes) then + local numFatties = 0 + if tier >= 3 and TIER3.FATTIES[faction] then + numFatties = mathFloor(numBattleships / getConversion('CORE_TO_FATTIES', overrides)) + end + if numFatties > 0 then - table.insert(Scenario.Platoons[template], {TIERS[3].FATTIES[faction], 1, numFatties, 'guard', 'None'}) - table.insert(children, 'Fatty') + tableInsert(template, { TIER3.FATTIES[faction], 1, numFatties, 'Guard', 'None' }) + tableInsert(children, 'Fatty') end end -- Do T3 carriers. Note UEF has no T3 carrier. - local numCarriers = 0 - if (allEnabled or IsEnabledType('Carrier', data)) then - if tier >= 3 and TIERS[3].CARRIERS[faction] then numCarriers = math.floor(numBattleships / Conversions.CORE_TO_CARRIERS) end + if isEnabledType('Carrier', enabledTypes) then + local numCarriers = 0 + if tier >= 3 and TIER3.CARRIERS[faction] then + numCarriers = mathFloor(numBattleships / getConversion('CORE_TO_CARRIERS', overrides)) + end + if numCarriers > 0 then - table.insert(Scenario.Platoons[template], {TIERS[3].CARRIERS[faction], 1, numCarriers, 'guard', 'None'}) - table.insert(children, 'Carrier') + tableInsert(template, { TIER3.CARRIERS[faction], 1, numCarriers, 'Guard', 'None' }) + tableInsert(children, 'Carrier') end end -- Do nuke subs, only if allowed. Note Seraphim has no nuke sub. - local numNukeSubs = 0 - if (not allEnabled and IsEnabledType('NukeSubmarine', data)) then - if tier >= 3 and TIERS[3].NUKESUBS[faction] then numNukeSubs = math.floor(numBattleships / Conversions.CORE_TO_NUKESUBS) end + if (enabledTypes and isEnabledType('NukeSubmarine', enabledTypes)) then + local numNukeSubs = 0 + if tier >= 3 and TIER3.NUKESUBS[faction] then + numNukeSubs = mathFloor(numBattleships / getConversion('CORE_TO_NUKESUBS', overrides)) + end + if numNukeSubs > 0 then - table.insert(Scenario.Platoons[template], {TIERS[3].NUKESUBS[faction], 1, numNukeSubs, 'guard', 'None'}) - table.insert(children, 'NukeSubmarine') + tableInsert(template, { TIER3.NUKESUBS[faction], 1, numNukeSubs, 'Guard', 'None' }) + tableInsert(children, 'NukeSubmarine') end end - -------------------------------------------- -- Create the child platoon builder - -------------------------------------------- - - Scenario.Armies.ARMY_1.PlatoonBuilders.Builders['OSB_Child_' .. name .. '_' .. tostring(tier) .. '-' .. tostring(waveLevel)] = - { - PlatoonAIFunction = {'/lua/ScenarioPlatoonAI.lua','DefaultOSBasePatrol',{'default_platoon'},{'default_platoon'}}, - PlatoonTemplate = template, - --Priority = nPriority+level, + builders['OSB_Child_' .. name .. '_' .. tier .. '-' .. waveLevel] = { + PlatoonAIFunction = { SPAI, 'DefaultOSBasePatrol', { 'default_platoon' }, { 'default_platoon' } }, + PlatoonTemplate = tpName, Priority = BasePriority + tier, InstanceCount = 1, LocationType = 'MAIN', @@ -238,20 +372,22 @@ function GenerateNavalOSB(name, levelsPerTier, minFrigates, maxFrigates, faction BuildConditions = { { '/lua/editor/amplatoonhelperfunctions.lua', 'AMCheckPlatoonLock', - {'default_master'}, - {'default_master'} + { 'default_master' }, + { 'default_master' } }, { '/lua/ai/opai/GenerateNaval.lua', 'ChildShouldBuild', - {'default_master'}, - {'default_master'}, + { 'default_master' }, + { 'default_master' }, }, }, PlatoonData = { { - type = 5, name = 'AMPlatoons', value = { - {type = 2, name = 'String_0', value = 'OSB_Master_' .. name}, - {type = 2, name = 'APPEND_FleetChildren', value = 'OSB_Master_' .. name}, + type = 5, + name = 'AMPlatoons', + value = { + { type = 2, name = 'String_0', value = masterPlatoonName }, + { type = 2, name = 'APPEND_FleetChildren', value = masterPlatoonName }, } }, }, @@ -259,92 +395,71 @@ function GenerateNavalOSB(name, levelsPerTier, minFrigates, maxFrigates, faction ChildrenType = { unpack(children) }, } - -------------------------------------------- --Increment the number of frigates and continue - -------------------------------------------- frigAcc = frigAcc + frigInc end - -------------------------------------------- -- Create the master platoon builder - -------------------------------------------- - Scenario.Armies.ARMY_1.PlatoonBuilders.Builders['OSB_Master_' .. name] = - { + + builders[masterPlatoonName] = { PlatoonTemplate = 'OST_BLANK_TEMPLATE', Priority = BasePriority + 1 + levels, InstanceCount = 1, LocationType = 'MAIN', BuildTimeOut = -1, PlatoonType = 'Sea', - RequiresConstruction = true, - PlatoonAIFunction = {'/lua/ScenarioPlatoonAI.lua','DefaultOSBasePatrol',{'default_platoon'},{'default_platoon'}}, + RequiresConstruction = false, + PlatoonAIFunction = { SPAI, 'DefaultOSBasePatrol', { 'default_platoon' }, { 'default_platoon' } }, BuildConditions = { { '/lua/editor/amplatoonhelperfunctions.lua', 'AMCheckPlatoonLock', - {'default_master'}, - {'default_master'} + { 'default_master' }, + { 'default_master' } }, { - '/lua/ai/opai/GenerateNaval.lua', 'FleetIsBuilt', - {'default_master'}, - {'default_master'}, + '/lua/ai/opai/generatenaval.lua', 'FleetIsBuilt', + { 'default_master' }, + { 'default_master' }, }, }, PlatoonBuildCallbacks = { { '/lua/editor/amplatoonhelperfunctions.lua', 'AMUnlockPlatoon', - {'default_platoon'}, - {'default_platoon'} + { 'default_platoon' }, + { 'default_platoon' } }, }, PlatoonAddFunctions = { { '/lua/editor/amplatoonhelperfunctions.lua', 'AMLockPlatoon', - {'default_platoon'}, - {'default_platoon'} + { 'default_platoon' }, + { 'default_platoon' } }, }, PlatoonData = { - {type = 3, name = 'AMMasterPlatoon', value = true}, - {type = 3, name = 'UsePool', value = false}, + { type = 3, name = 'AMMasterPlatoon', value = true }, + { type = 3, name = 'UsePool', value = false }, }, } return Scenario end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- function: FleetIsBuilt = BuildCondition doc = "Please work function docs." ----- ----- parameter 0: string aiBrain = "default_brain" ----- parameter 1: string master = "default_master" ----- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---@param aiBrain CampaignAIBrain +---@param master string +---@return boolean function FleetIsBuilt(aiBrain, master) - local fleetCounter = ScenarioFramework.AMPlatoonCounter(aiBrain, master..'_FleetChildren') + local fleetCounter = ScenarioFramework.AMPlatoonCounter(aiBrain, master .. '_FleetChildren') - if fleetCounter >= 1 then - return true - else - return false - end + return fleetCounter >= 1 end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- function: ChildShouldBuild = BuildCondition doc = "Please work function docs." ----- ----- parameter 0: string aiBrain = "default_brain" ----- parameter 1: string master = "default_master" ----- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---@param aiBrain CampaignAIBrain +---@param master string +---@return boolean function ChildShouldBuild(aiBrain, master) - local fleetCounter = ScenarioFramework.AMPlatoonCounter(aiBrain, master..'_FleetChildren') + local fleetCounter = ScenarioFramework.AMPlatoonCounter(aiBrain, master .. '_FleetChildren') - if fleetCounter < 1 then - return true - else - return false - end + return fleetCounter < 1 end diff --git a/lua/AI/OpAI/NavalOpAI.lua b/lua/AI/OpAI/NavalOpAI.lua index 89ac3965df2..c426ec2641d 100644 --- a/lua/AI/OpAI/NavalOpAI.lua +++ b/lua/AI/OpAI/NavalOpAI.lua @@ -8,46 +8,50 @@ local OpAI = import("/lua/ai/opai/baseopai.lua").OpAI local GenerateNavalOSB = import("/lua/ai/opai/generatenaval.lua") +local mathMax = math.max +local tableGetn = table.getn +local categoriesNaval = categories.NAVAL + +---@class NavalOpAIData: AddOpAIData, NavalOpAIGeneratorData + ---@class NavalOpAI : OpAI -NavalOpAI = Class(OpAI) { - ---@param self AIBrain - ---@param brain AIBrain - ---@param location string - ---@param name string - ---@param data number - Create = function(self, brain, location, name, data) - local bManager = brain.BaseManagers[location] - - local numLevels = data.NumLevels or 1 - - local maxMultiplier = data.MaxMultiplier or 1 - local minMultiplier = data.MinMultiplier or maxMultiplier - - local maxFrigates = data.MaxFrigates or ( (table.getn(bManager:GetAllBaseFactories(categories.NAVAL)) or 1) * maxMultiplier ) - if maxFrigates == 0 then maxFrigates = 1 end - local minFrigates = data.MinFrigates or ( (table.getn(bManager:GetAllBaseFactories(categories.NAVAL)) or 1) * minMultiplier ) - if minFrigates == 0 then minFrigates = 1 end - - local faction = 'S' - local brainFaction = brain:GetFactionIndex() - if brainFaction == 1 then faction = 'U' - elseif brainFaction == 2 then faction = 'A' - elseif brainFaction == 3 then faction = 'C' - end - - local navalTable = GenerateNavalOSB.GenerateNavalOSB(name, numLevels, minFrigates, maxFrigates, faction, data) - - -- Create the actual OpAI thing here passing in the rebuilt data - OpAI.Create(self, brain, location, navalTable, name .. '_' .. location .. '_NavalAI', data) +---@overload fun(): NavalOpAI +local NavalAI = {} +---@param brain CampaignAIBrain +---@param location string +---@param name string +---@param data NavalOpAIData +function NavalAI:Create(brain, location, name, data) + local bManager = brain.BaseManagers[location] + + local numLevels = data.NumLevels or 1 + + local maxMultiplier = data.MaxMultiplier or 1 + local minMultiplier = data.MinMultiplier or maxMultiplier + + local numFactories = tableGetn(bManager:GetAllBaseFactories(categoriesNaval)) + local base = mathMax(numFactories, 1) + + local maxFrigates = data.MaxFrigates or (base * maxMultiplier) + if maxFrigates == 0 then maxFrigates = 1 end + local minFrigates = data.MinFrigates or (base * minMultiplier) + if minFrigates == 0 then minFrigates = 1 end + + local faction = brain:GetFactionIndex() + + local navalTable = GenerateNavalOSB.GenerateNavalOSB(name, numLevels, minFrigates, maxFrigates, faction, data) + + -- Create the actual OpAI thing here passing in the rebuilt data + OpAI.Create(self, brain, location, navalTable, name .. '_' .. location .. '_NavalAI', data) +end - end, -} +NavalOpAI = Class(OpAI --[[@as fa-class]])(NavalAI) ----@param brain AIBrain +---@param brain CampaignAIBrain ---@param location string ---@param name string ----@param data number ----@return unknown +---@param data table +---@return NavalOpAI function CreateNavalAI(brain, location, name, data) local navalAI = NavalOpAI() navalAI:Create(brain, location, name, data) @@ -63,4 +67,4 @@ local ScenarioPlatoonAI = import("/lua/scenarioplatoonai.lua") local UCBC = '/lua/editor/unitcountbuildconditions.lua' local BMBC = '/lua/editor/basemanagerbuildconditions.lua' local MIBC = '/lua/editor/miscbuildconditions.lua' -local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' \ No newline at end of file +local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' diff --git a/lua/AI/OpAI/OpBehaviors.lua b/lua/AI/OpAI/OpBehaviors.lua index 642647592f2..016151aff4b 100644 --- a/lua/AI/OpAI/OpBehaviors.lua +++ b/lua/AI/OpAI/OpBehaviors.lua @@ -4,245 +4,311 @@ -- Copyright © 2005 Gas Powered Games, Inc. All rights reserved. --------------------------------------------------------------------- --- Platoon Lua Module -- local Utilities = import("/lua/utilities.lua") local ScenarioUtils = import("/lua/sim/scenarioutilities.lua") --- ===== CDR ADD BEHAVIORS ===== -- ----@param platoon Platoon +local XZDist = Utilities.XZDistanceTwoVectors +local DistEnts = Utilities.GetDistanceBetweenTwoEntities + +local ipairs = ipairs +local Vector = Vector +local WaitSeconds = WaitSeconds + +---@class PlatoonCDRData +---@field CDRData OpAICDRData + +---@class OpAICDRData +---@field LeashPosition? MarkerName Name of the marker to return to +---@field LeashRadius? integer Distance to trigger return back to `LeashPosition` + +---@class OpAIACUUnit: CommandUnit +---@field Fighting boolean ACU is fighting enemy units +---@field Running boolean ACU is running away due to low HP +---@field GivingUp boolean Not implemented yet +---@field Leashing boolean ACU got too far from the base position and is being moved back +---@field Cornered boolean Not implemented yet +---@field CDRData? OpAICDRData +---@field UnitBeingBuiltBehavior? Unit Reference to the unit commander was building in case it goes fight +---@field OverchargeThread? thread +---@field LeashThread? thread +---@field RunAwayThread? thread + +-- Target priorities for the commander OC / fight +CDRTargetPriorities = { + categories.COMMAND, + categories.EXPERIMENTAL, + categories.TECH3 * categories.INDIRECTFIRE, + categories.TECH3 * categories.MOBILE, + categories.TECH2 * categories.INDIRECTFIRE, + categories.MOBILE * categories.TECH2, + categories.TECH1 * categories.INDIRECTFIRE, + categories.TECH1 * categories.MOBILE, categories.ALLUNITS +} + +--- Sets up threads for ACU fighting +--- - actively OC-ing and attacking nearby units. +--- - returning to base if got too far during fight +--- - return to base if on low HP +--- - finish construction projects if they were abandonned for fighting +---@see PlatoonCDRData For required platoon data. +---@param platoon Platoon | PlatoonCDRData function CDROverchargeBehavior(platoon) - local cdr = platoon:GetPlatoonUnits()[1] + local cdr = platoon:GetPlatoonUnits()[1] --[[@as OpAIACUUnit]] if platoon.CDRData then cdr.CDRData = platoon.CDRData end if not cdr.OverchargeThread then - cdr.OverchargeThread = cdr:ForkThread( CDROverChargeThread ) + cdr.OverchargeThread = cdr:ForkThread(CDROverChargeThread) end if not cdr.LeashThread then - cdr.LeashThread = cdr:ForkThread( CDRLeashThread ) + cdr.LeashThread = cdr:ForkThread(CDRLeashThread) end if not cdr.RunAwayThread then - cdr.RunAwayThread = cdr:ForkThread( CDRRunAwayThread ) + cdr.RunAwayThread = cdr:ForkThread(CDRRunAwayThread) end end ----@param cdr CommandUnit -function CDROverChargeThread( cdr ) +---@param cdr OpAIACUUnit +function CDROverChargeThread(cdr) local aiBrain = cdr:GetAIBrain() + local data = cdr.CDRData local weapBPs = cdr:GetBlueprint().Weapon + ---@cast weapBPs -nil + ---@type WeaponBlueprint local weapon - for k,v in weapBPs do + for _, v in weapBPs do if v.Label == 'OverCharge' then weapon = v break end end - local beingBuilt = false + local plat - local startX, startZ = aiBrain:GetArmyStartPos() - if cdr.CDRData and cdr.CDRData.LeashPosition then - local tempPos = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) - startX = tempPos[1] - startZ = tempPos[3] + ---@type Vector + local startPos + if data and data.LeashPosition then + startPos = ScenarioUtils.MarkerToPosition(data.LeashPosition) + else + local locX, locZ = aiBrain:GetArmyStartPos() + startPos = Vector(locX, 0, locZ) end - cdr.UnitBeingBuiltBehavior = false - while not cdr:IsDead() do + + local scanCategories = categories.LAND - categories.SCOUT + + cdr.UnitBeingBuiltBehavior = nil + while not cdr.Dead do if not cdr.Running and not cdr.GivingUp and not cdr.Leashing then local cdrPos = cdr:GetPosition() - local numUnits = aiBrain:GetNumUnitsAroundPoint( categories.LAND - categories.SCOUT, cdrPos, ( weapon.MaxRadius * 2 ), 'Enemy' ) + local numUnits = aiBrain:GetNumUnitsAroundPoint(scanCategories, cdrPos, (weapon.MaxRadius * 2), 'Enemy') local overCharging = false + if numUnits > 0 then cdr.Fighting = true - if cdr:GetUnitBeingBuilt() then + if cdr.UnitBeingBuilt then --LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR was building something') - cdr.UnitBeingBuiltBehavior = cdr:GetUnitBeingBuilt() + cdr.UnitBeingBuiltBehavior = cdr.UnitBeingBuilt end - plat = aiBrain:MakePlatoon( '', '' ) - aiBrain:AssignUnitToPlatoon( plat, cdr, 'support', 'None' ) + + plat = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(plat, cdr, 'Support', 'None') plat:Stop() - local priList = { categories.COMMAND, categories.EXPERIMENTAL, categories.TECH3 * categories.INDIRECTFIRE, - categories.TECH3 * categories.MOBILE, categories.TECH2 * categories.INDIRECTFIRE, categories.MOBILE * categories.TECH2, - categories.TECH1 * categories.INDIRECTFIRE, categories.TECH1 * categories.MOBILE, categories.ALLUNITS } + + local priList = CDRTargetPriorities + ---@type Unit|nil local target local continueFighting = true local counter = 0 + repeat overCharging = false - if counter >= 5 or not target or target:IsDead() or Utilities.GetDistanceBetweenTwoEntities(cdr, target) > ( weapon.MaxRadius * 3 ) then + if counter >= 5 or not target or target.Dead or DistEnts(cdr, target) > (weapon.MaxRadius * 3) then counter = 0 - for k,v in priList do - target = plat:FindClosestUnit( 'Support', 'Enemy', true, v ) - if target and Utilities.GetDistanceBetweenTwoEntities(cdr, target) < (weapon.MaxRadius * 2 ) then + for _, v in ipairs(priList) do + target = plat:FindClosestUnit('Support', 'Enemy', true, v) + if target and DistEnts(cdr, target) < (weapon.MaxRadius * 2) then break end - target = false + target = nil end + if target then - if aiBrain:GetEconomyStored('ENERGY') >= weapon.EnergyRequired and target and not target:IsDead() then + if aiBrain:GetEconomyStored('ENERGY') >= weapon.EnergyRequired and not target.Dead then overCharging = true IssueToUnitClearCommands(cdr) - IssueOverCharge( {cdr}, target ) - elseif not target:IsDead() then + IssueOverCharge({ cdr }, target) + elseif not target.Dead then local tarPos = target:GetPosition() IssueToUnitClearCommands(cdr) - IssueToUnitMove(cdr, tarPos ) - if cdr.CDRData and cdr.CDRData.LeashPosition then - local tempPos = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) - startX = tempPos[1] - startZ = tempPos[3] - end - IssueToUnitMove(cdr, {startX, 0, startZ} ) + IssueToUnitMove(cdr, tarPos) + IssueToUnitMove(cdr, startPos) end end end + if overCharging then - while target and not target:IsDead() and not cdr:IsDead() and counter <= 5 do + while target and not target.Dead and not cdr.Dead and counter <= 5 do WaitTicks(6) counter = counter + .5 end else - WaitTicks(51) + WaitSeconds(5) counter = counter + 5 end + if cdr.Dead then return end + cdrPos = cdr:GetPosition() - if cdr:IsDead() then - return - end - if ( aiBrain:GetNumUnitsAroundPoint( categories.LAND - categories.SCOUT, cdrPos, ( weapon.MaxRadius * 2 ), 'Enemy' ) <= 1 ) then + if aiBrain:GetNumUnitsAroundPoint(scanCategories, cdrPos, weapon.MaxRadius * 2, 'Enemy') <= 1 then continueFighting = false end until not continueFighting or not aiBrain:PlatoonExists(plat) - if not cdr:IsDead() then + + if not cdr.Dead then if cdr.CDRData and cdr.CDRData.LeashPosition then - local tempPos = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) - startX = tempPos[1] - startZ = tempPos[3] + startPos = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) end + cdr.Fighting = false if overCharging then - IssueToUnitMove(cdr, {startX, 0, startZ} ) + IssueToUnitMove(cdr, startPos) end end if aiBrain:PlatoonExists(plat) then - cdr:ForkThread( CDRRepairBuildingUnit, plat ) + cdr:ForkThread(CDRRepairBuildingUnit, plat) end end end - WaitTicks(31) + WaitSeconds(3) end end ----@param cdr CommandUnit +--- Finishes the constructioin of the unit the ACU was previously building +---@param cdr OpAIACUUnit ---@param plat Platoon -function CDRRepairBuildingUnit( cdr, plat ) +function CDRRepairBuildingUnit(cdr, plat) local aiBrain = cdr:GetAIBrain() - if cdr.UnitBeingBuiltBehavior and not cdr.UnitBeingBuiltBehavior:BeenDestroyed() then + local targetUnit = cdr.UnitBeingBuiltBehavior + + if targetUnit and not targetUnit:BeenDestroyed() and targetUnit:GetFractionComplete() < 1 then IssueToUnitClearCommands(cdr) - IssueRepair( {cdr}, cdr.UnitBeingBuiltBehavior ) + IssueRepair({ cdr }, targetUnit) + repeat - WaitTicks(11) + WaitSeconds(1) if cdr.Fighting or cdr.Running or cdr.GivingUp or cdr.Leashing or cdr.Cornered then return end until cdr:IsIdleState() - cdr.UnitBeingBuiltBehavior = false + + cdr.UnitBeingBuiltBehavior = nil end - if not cdr:IsDead() and not ( cdr.Fighting or cdr.Running or cdr.GivingUp or cdr.Leashing or cdr.Cornered ) then + + if not cdr.Dead and not (cdr.Fighting or cdr.Running or cdr.GivingUp or cdr.Leashing or cdr.Cornered) then local pool = aiBrain:GetPlatoonUniquelyNamed('ArmyPool') - aiBrain:AssignUnitToPlatoon( pool, cdr, 'Unassigned', 'None' ) + aiBrain:AssignUnitToPlatoon(pool, cdr, 'Unassigned', 'NoFormation') end end ----@param cdr CommandUnit +--- Moves the commander back to the base position if it get's too far while fighting. +---@param cdr OpAIACUUnit function CDRLeashThread(cdr) + local data = cdr.CDRData + local rad = data and data.LeashRadius -- if no radius specified return out of function - local rad - if cdr.CDRData and cdr.CDRData.LeashRadius then - rad = cdr.CDRData.LeashRadius - else - return - end + if not rad then return end local aiBrain = cdr:GetAIBrain() - local locX, locZ = aiBrain:GetArmyStartPos() - local loc = { locX, 0, locZ } - if cdr.CDRData and cdr.CDRData.LeashPosition then - loc = ScenarioUtils.MarkerToPosition( cdr.CDRData.LeashPosition ) + ---@type Vector + local pos + if data and data.LeashPosition then + pos = ScenarioUtils.MarkerToPosition(data.LeashPosition) + else + local locX, locZ = aiBrain:GetArmyStartPos() + pos = Vector(locX, 0, locZ) end - while not cdr:IsDead() do - if cdr.CDRData and cdr.CDRData.LeashPosition then - loc = ScenarioUtils.MarkerToPosition( cdr.CDRData.LeashPosition ) - end + + while not cdr.Dead do if not cdr.GivingUp and not cdr.Running then local cdrPos = cdr:GetPosition() - if VDist2( cdrPos[1], cdrPos[3], loc[1], loc[3] ) > rad then - local plat = aiBrain:MakePlatoon( '', '' ) - aiBrain:AssignUnitToPlatoon( plat, cdr, 'support', 'none' ) + + if XZDist(cdrPos, pos) > rad then + local plat = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(plat, cdr, 'Support', 'NoFormation') plat:Stop() - plat:MoveToLocation( loc, false ) + plat:MoveToLocation(pos, false) cdr.Leashing = true --LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR AI ACTIVATE - Commander leashing to MAIN' ) - WaitTicks(101) - if not cdr:IsDead() then + WaitSeconds(10) + + if not cdr.Dead then cdr.Leashing = false if aiBrain:PlatoonExists(plat) then - cdr:ForkThread( CDRRepairBuildingUnit, plat ) + cdr:ForkThread(CDRRepairBuildingUnit, plat) end end end end - WaitTicks(51) + WaitSeconds(5) end end ----@param cdr CommandUnit -function CDRRunAwayThread( cdr ) +---@param cdr OpAIACUUnit +function CDRRunAwayThread(cdr) local aiBrain = cdr:GetAIBrain() - local runSpotX, runSpotZ = aiBrain:GetArmyStartPos() - local runSpot = { runSpotX, 0, runSpotZ } - if cdr.CDRData and cdr.CDRData.LeashPosition then - runSpot = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) + local data = cdr.CDRData + ---@type Vector + local runSpot + if data and data.LeashPosition then + runSpot = ScenarioUtils.MarkerToPosition(data.LeashPosition) + else + local locX, locZ = aiBrain:GetArmyStartPos() + runSpot = Vector(locX, 0, locZ) end - while not cdr:IsDead() do - if cdr:GetHealthPercent() < .4 and not cdr.GivingUp then - if cdr.CDRData and cdr.CDRData.LeashPosition then - runSpot = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) - end + + local hpThreshold = 0.4 + + while not cdr.Dead do + if cdr:GetHealthPercent() < hpThreshold and not cdr.GivingUp then --LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR AI ACTIVATE - CDR RUNNING AWAY' ) local cdrPos = cdr:GetPosition() - local nmeAir = aiBrain:GetUnitsAroundPoint( categories.AIR, cdrPos, 25, 'Enemy' ) - local nmeLand = aiBrain:GetUnitsAroundPoint( categories.LAND, cdrPos, 25, 'Enemy' ) - local nmeHardcore = aiBrain:GetUnitsAroundPoint( categories.EXPERIMENTAL, cdrPos, 25, 'Enemy' ) - if ( Utilities.XZDistanceTwoVectors( cdrPos, runSpot) > 15 ) and ( nmeAir > 3 or nmeLand > 3 or nmeHardcore > 0 ) then + local nmeAir = aiBrain:GetNumUnitsAroundPoint(categories.AIR, cdrPos, 25, 'Enemy') + local nmeLand = aiBrain:GetNumUnitsAroundPoint(categories.LAND, cdrPos, 25, 'Enemy') + local nmeHardcore = aiBrain:GetNumUnitsAroundPoint(categories.EXPERIMENTAL, cdrPos, 25, 'Enemy') + + if (XZDist(cdrPos, runSpot) > 15) and (nmeAir > 3 or nmeLand > 3 or nmeHardcore > 0) then if cdr:IsUnitState('Building') then - cdr.UnitBeingBuiltBehavior = cdr:GetUnitBeingBuilt() + cdr.UnitBeingBuiltBehavior = cdr.UnitBeingBuilt end + cdr.Running = true - local plat = aiBrain:MakePlatoon( '', '' ) - aiBrain:AssignUnitToPlatoon( plat, cdr, 'support', 'None' ) + local plat = aiBrain:MakePlatoon('', '') + aiBrain:AssignUnitToPlatoon(plat, cdr, 'Support', 'NoFormation') + repeat plat:Stop() - cmd = plat:MoveToLocation( runSpot, false ) - WaitTicks(31) - if not cdr:IsDead() then + plat:MoveToLocation(runSpot, false) + + WaitSeconds(3) + + if not cdr.Dead then cdrPos = cdr:GetPosition() - nmeAir = aiBrain:GetUnitsAroundPoint( categories.AIR, cdrPos, 25, 'Enemy' ) - nmeLand = aiBrain:GetUnitsAroundPoint( categories.LAND, cdrPos, 25, 'Enemy' ) - nmeHardcore = aiBrain:GetUnitsAroundPoint( categories.EXPERIMENTAL, cdrPos, 25, 'Enemy' ) + nmeAir = aiBrain:GetNumUnitsAroundPoint(categories.AIR, cdrPos, 25, 'Enemy') + nmeLand = aiBrain:GetNumUnitsAroundPoint(categories.LAND, cdrPos, 25, 'Enemy') + nmeHardcore = aiBrain:GetNumUnitsAroundPoint(categories.EXPERIMENTAL, cdrPos, 25, 'Enemy') if cdr.CDRData and cdr.CDRData.LeashPosition then runSpot = ScenarioUtils.MarkerToPosition(cdr.CDRData.LeashPosition) end end - until cdr:IsDead() or ( Utilities.XZDistanceTwoVectors( cdrPos, runSpot) > 15 ) or ( nmeAir < 2 and nmeLand < 2 and nmeHardcore == 0 ) or cdr:GetHealthPercent() > .4 + until cdr.Dead or (XZDist(cdrPos, runSpot) > 15) or (nmeAir < 2 and nmeLand < 2 and nmeHardcore == 0) or cdr:GetHealthPercent() > hpThreshold --LOG('*AI DEBUG: ARMY ' .. aiBrain:GetArmyIndex() .. ': CDR AI DEACTIVATE - Run away no more!') cdr.Running = false cdr.Cornered = false - if not cdr:IsDead() and not cdr.GivingUp and aiBrain:PlatoonExists(plat) then - cdr:ForkThread( CDRRepairBuildingUnit, plat ) + if not cdr.Dead and not cdr.GivingUp and aiBrain:PlatoonExists(plat) then + cdr:ForkThread(CDRRepairBuildingUnit, plat) end end end - WaitTicks(31) + + WaitSeconds(3) end -end \ No newline at end of file +end diff --git a/lua/AI/OpAI/ReactiveAI.lua b/lua/AI/OpAI/ReactiveAI.lua index f9c05e74f5f..7ed58ee754d 100644 --- a/lua/AI/OpAI/ReactiveAI.lua +++ b/lua/AI/OpAI/ReactiveAI.lua @@ -9,91 +9,59 @@ local OpAI = import("/lua/ai/opai/baseopai.lua").OpAI local UCBC = '/lua/editor/unitcountbuildconditions.lua' local BMBC = '/lua/editor/basemanagerbuildconditions.lua' local MIBC = '/lua/editor/miscbuildconditions.lua' +local OAUCBC = '/lua/editor/otherarmyunitcountbuildconditions.lua' local BMPT = '/lua/ai/opai/basemanagerplatoonthreads.lua' ---[[ -Types usable in ReactiveAI - -ReactionTypes: - AirRetaliation - - ** Following not implemented yet ** - Horde - Combined - Pinpoint - - -TriggeringEventType - ExperimentalLand - ExperimentalAir - ExperimentalNaval - Nuke - HLRA - MassedAir - -]]-- - - +local unpack = unpack + +---@class ReactAIBuilderTypeBase +---@field OpAI OpAIPlatoonTpFile +---@field Children OpAIChildType[] +---@field Priority integer Higher number = Higher priority, 0 gets never built +---@field ChildCount integer How many child platoons to build and combine +---@field PlatoonAIFunction FileFunctionRef +---@field PlatoonData table Any data required for the PlatoonAIFunction +---@field TriggeringBuildConditions BuildCondition[] + +---@alias ReactAITriggerEvent #What triggers this AI from being built and dispatched +---| 'ExperimentalLand' # Land experimental was detected +---| 'ExperimentalAir' # Air experimental was detected +---| 'ExperimentalNaval' # Naval experimental was detected +---| 'Nuke' # Nuke was detected +---| 'HLRA' # Heavy long range artillery, nukes, satellites, experimental structures +---| 'MassedAir' # Mass of T2 and T3 air units was detected + +---@alias ReactAIReactionType #How the AI reacts when the conditions are met +---| 'AirRetaliation' # Send air units + +-- ** Following not implemented yet ** +-- | 'Horde' +-- | 'Combined' +-- | 'Pinpoint' + TrackingCategories = { - ExperimentalAir = { categories.EXPERIMENTAL * categories.AIR, }, + ExperimentalAir = { categories.EXPERIMENTAL * categories.AIR }, ExperimentalLand = { categories.uel0401, (categories.EXPERIMENTAL * categories.LAND * categories.MOBILE) }, - ExperimentalNaval = { categories.EXPERIMENTAL * categories.NAVAL, }, - Nuke = { categories.NUKE, }, - HLRA = { categories.ueb2401, (categories.STRATEGIC * categories.TECH3) + (categories.EXPERIMENTAL * categories.STRUCTURE), }, - MassedAir = { categories.AIR * categories.MOBILE * ( categories.TECH2 + categories.TECH3 ), }, + ExperimentalNaval = { categories.EXPERIMENTAL * categories.NAVAL }, + Nuke = { categories.NUKE }, + HLRA = { categories.ueb2401, (categories.STRATEGIC * categories.TECH3) + (categories.EXPERIMENTAL * categories.STRUCTURE) }, + MassedAir = { categories.AIR * categories.MOBILE * (categories.TECH2 + categories.TECH3) }, } +--- AI that is built with a predefined platoon template as response to the provided trigger type +--- +---@see ReactAITriggerEvent for list of available events +---@see ReactAIReactionType for the types of reactive AIs +---@see OpAI.SetChildActive to disable certain children ---@class ReactiveAI : OpAI -ReactiveAI = Class(OpAI) { - - ---@param self OpAI - ---@param brain AIBrain - ---@param location string - ---@param triggeringEventType any - ---@param reactionType any - ---@param name string - ---@param data any - Create = function(self, brain, location, triggeringEventType, reactionType, name, data) - -- With the actionType and responseType, we must create a builderType with proper builderData to create - -- the OpAI - - -- We figure out what type of builder to add based on our action and response - local builderType = self:GetBuilderType( triggeringEventType, reactionType ) - - -- At this point we need to combine the passed in data with our own data to create the OpAI - local builderData = { - MasterPlatoonFunction = builderType.PlatoonAIFunction, - PlatoonData = builderType.PlatoonData, - Priority = builderType.Priority, - } - if data then - for k,v in data do - builderData[k] = v - end - end - - -- Create the actual OpAI thing here passing in the rebuilt data - OpAI.Create(self, brain, location, builderType.OpAI, name .. '_' .. location .. '_ReactiveAI', builderData) - - -- add in children count, child locks, etc - self:SetChildCount(builderType.ChildCount) - - -- Activate only the children listed - self:SetChildActive('All', false) - for k,v in builderType.Children do - self:SetChildActive(v, true) - end - for k,v in builderType.TriggeringBuildConditions do - self:AddBuildCondition( unpack(v) ) - end - end, - +---@field ReactionData table> +---@overload fun(): ReactiveAI +local ReactOpAI = { ReactionData = { - -- This uses purely air to respond. It is the easiest to implement and has the least chance of breaking AirRetaliation = { - ExperimentalAir = { - OpAI = 'AirAttacks', - Children = { 'AirSuperiority', 'CombatFighters', 'Interceptors', }, + ExperimentalAir = { + OpAI = 'AirAttacks', + Children = { 'AirSuperiority', 'CombatFighters', 'Interceptors' }, Priority = 1200, ChildCount = 4, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -101,13 +69,12 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.ExperimentalAir, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 1, TrackingCategories.ExperimentalAir, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 1, TrackingCategories.ExperimentalAir, '>=' } }, }, }, - ExperimentalLand = { - OpAI = 'AirAttacks', - Children = { 'HeavyGunships', 'Gunships', 'Bombers', 'CombatFighters', }, + ExperimentalLand = { + OpAI = 'AirAttacks', + Children = { 'HeavyGunships', 'Gunships', 'Bombers', 'CombatFighters' }, Priority = 1200, ChildCount = 3, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -115,13 +82,12 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.ExperimentalLand, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 1, TrackingCategories.ExperimentalLand, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 1, TrackingCategories.ExperimentalLand, '>=' } }, }, }, - ExperimentalNaval = { - OpAI = 'AirAttacks', - Children = { 'TorpedoBombers', 'HeavyTorpedoBombers', }, + ExperimentalNaval = { + OpAI = 'AirAttacks', + Children = { 'TorpedoBombers', 'HeavyTorpedoBombers' }, Priority = 1200, ChildCount = 3, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -129,13 +95,12 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.ExperimentalNaval, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 1, TrackingCategories.ExperimentalNaval, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 1, TrackingCategories.ExperimentalNaval, '>=' } }, }, }, - Nuke = { - OpAI = 'AirAttacks', - Children = { 'StrategicBombers', 'HeavyGunships', 'Gunships', 'Bombers', }, + Nuke = { + OpAI = 'AirAttacks', + Children = { 'StratBombers', 'HeavyGunships', 'Gunships', 'Bombers' }, ChildCount = 1, Priority = 1200, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -143,13 +108,12 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.Nuke, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 1, TrackingCategories.Nuke, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 1, TrackingCategories.Nuke, '>=' } }, }, }, - HLRA = { - OpAI = 'AirAttacks', - Children = { 'StrategicBombers', 'HeavyGunships', 'Gunships', 'Bombers', }, + HLRA = { + OpAI = 'AirAttacks', + Children = { 'StratBombers', 'HeavyGunships', 'Gunships', 'Bombers' }, ChildCount = 1, Priority = 1200, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -157,13 +121,12 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.HLRA, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 1, TrackingCategories.HLRA, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 1, TrackingCategories.HLRA, '>=' } }, }, - }, - MassedAir = { - OpAI = 'AirAttacks', - Children = { 'AirSuperiority', 'CombatFighters', 'Interceptors', }, + }, + MassedAir = { + OpAI = 'AirAttacks', + Children = { 'AirSuperiority', 'CombatFighters', 'Interceptors' }, ChildCount = 4, Priority = 1200, PlatoonAIFunction = { '/lua/ScenarioPlatoonAI.lua', 'CategoryHunterPlatoonAI' }, @@ -171,50 +134,80 @@ ReactiveAI = Class(OpAI) { CategoryList = TrackingCategories.MassedAir, }, TriggeringBuildConditions = { - { '/lua/editor/OtherArmyUnitCountBuildConditions.lua', 'FocusBrainBeingBuiltOrActiveCategoryCompare', - { 40, TrackingCategories.MassedAir, '>=' } }, + { OAUCBC, 'FocusBrainBeingBuiltOrActiveCategoryCompare', { 40, TrackingCategories.MassedAir, '>=' } }, }, }, }, - -- End of AirRetaliation block }, +} +---@param brain CampaignAIBrain +---@param location string Name of the base +---@param trigger ReactAITriggerEvent +---@param reaction ReactAIReactionType +---@param name string Unique name of the AI platoon +---@param data? table Optional data to overrite the default platoon builder values +function ReactOpAI:Create(brain, location, trigger, reaction, name, data) + -- With the actionType and responseType, we must create a builderType with proper builderData to create + -- the OpAI + + -- We figure out what type of builder to add based on our action and response + local builderType = self:GetBuilderType(trigger, reaction) + + -- At this point we need to combine the passed in data with our own data to create the OpAI + local builderData = { + MasterPlatoonFunction = builderType.PlatoonAIFunction, + PlatoonData = builderType.PlatoonData, + Priority = builderType.Priority, + } + if data then + for k, v in data do + builderData[k] = v + end + end - ---@param self OpAI - ---@param builderData any - ---@param typeData any - ---@param builderType any - ---@param triggeringEventType any - ---@param reactionType any - GetBuilderData = function( self, builderData, typeData, builderType, triggeringEventType, reactionType ) - end, - - ---@param self OpAI - ---@param triggeringEventType any - ---@param reactionType any - ---@return any - GetBuilderType = function( self, triggeringEventType, reactionType ) - local retData = self.ReactionData[reactionType][triggeringEventType] - if not retData then - if not self.ReactionData[reactionType] then - error( '*AI ERROR: Invalid reaction Type for ReactiveAI - ' .. reactionType, 2 ) - end - error( '*AI ERROR: Invalid triggeringEventType for ReactiveAI - ' .. triggeringEventType, 2 ) + -- Create the actual OpAI thing here passing in the rebuilt data + OpAI.Create(self, brain, location, builderType.OpAI, name .. '_' .. location .. '_ReactiveAI', builderData) + + -- add in children count, child locks, etc + self:SetChildCount(builderType.ChildCount) + + -- Activate only the children listed + self:SetChildActive('All', false) + for _, v in builderType.Children do + self:SetChildActive(v, true) + end + + for _, v in builderType.TriggeringBuildConditions do + self:AddBuildCondition(unpack(v)) + end +end + +---@protected +---@param trigger ReactAITriggerEvent +---@param reaction ReactAIReactionType +---@return ReactAIBuilderTypeBase +function ReactOpAI:GetBuilderType(trigger, reaction) + local retData = self.ReactionData[reaction][trigger] + if not retData then + if not self.ReactionData[reaction] then + error('*AI ERROR: Invalid reaction Type for ReactiveAI - ' .. reaction, 2) end - return retData - end, + error('*AI ERROR: Invalid triggeringEvent for ReactiveAI - ' .. trigger, 2) + end + return retData +end - ReactionDatas = {}, -} +ReactiveAI = Class(OpAI --[[@as fa-class]])(ReactOpAI) ----@param brain AIBrain ----@param location string ----@param triggeringEventType any ----@param reactionType any ----@param name string ----@param data any ----@return any -function CreateReactiveAI(brain, location, triggeringEventType, reactionType, name, data) +---@param brain CampaignAIBrain +---@param location string Name of the base +---@param trigger ReactAITriggerEvent +---@param reaction ReactAIReactionType +---@param name string Unique name of the AI platoon +---@param data? table Optional data to overrite the default platoon builder values +---@return ReactiveAI +function CreateReactiveAI(brain, location, trigger, reaction, name, data) local reactAI = ReactiveAI() - reactAI:Create(brain, location, triggeringEventType, reactionType, name, data) + reactAI:Create(brain, location, trigger, reaction, name, data) return reactAI end diff --git a/lua/AI/aiutilities.lua b/lua/AI/aiutilities.lua index 19ce2b1fbee..7336123d5d0 100644 --- a/lua/AI/aiutilities.lua +++ b/lua/AI/aiutilities.lua @@ -1455,7 +1455,7 @@ end ---@param aiBrain AIBrain ---@param platoon Platoon ----@param squad PlatoonSquads +---@param squad PlatoonSquadType ---@param maxRange number ---@param atkPri table ---@param enemyBrain? AIBrain diff --git a/lua/ScenarioFramework.lua b/lua/ScenarioFramework.lua index 747ef88a293..f93076eae88 100644 --- a/lua/ScenarioFramework.lua +++ b/lua/ScenarioFramework.lua @@ -437,7 +437,7 @@ CreateUnitNearTypeTrigger = TriggerFile.CreateUnitNearTypeTrigger --- Orders a platoon to move along a route ---@param platoon Platoon ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonMoveRoute(platoon, route, squad) for _, node in route do if type(node) == 'string' then @@ -454,7 +454,7 @@ end --- Orders platoon to patrol a route ---@param platoon Platoon ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonPatrolRoute(platoon, route, squad) for _, node in route do if type(node) == 'string' then @@ -471,7 +471,7 @@ end --- Orders a platoon to attack-move along a route ---@param platoon Platoon ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonAttackRoute(platoon, route, squad) for _, node in route do if type(node) == 'string' then @@ -488,7 +488,7 @@ end --- Orders a platoon to move along a chain ---@param platoon Platoon ---@param chain ChainName ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonMoveChain(platoon, chain, squad) PlatoonMoveRoute(platoon, ScenarioUtils.ChainToPositions(chain), squad) end @@ -496,7 +496,7 @@ end --- Orders a platoon to patrol along a chain ---@param platoon Platoon ---@param chain ChainName ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonPatrolChain(platoon, chain, squad) PlatoonPatrolRoute(platoon, ScenarioUtils.ChainToPositions(chain), squad) end @@ -504,7 +504,7 @@ end --- Orders a platoon to attack-move through a chain ---@param platoon Platoon ---@param chain ChainName ----@param squad? PlatoonSquads +---@param squad? PlatoonSquadType function PlatoonAttackChain(platoon, chain, squad) PlatoonAttackRoute(platoon, ScenarioUtils.ChainToPositions(chain), squad) end diff --git a/lua/aibrain.lua b/lua/aibrain.lua index ad68b403902..36b5a1c97a4 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -653,7 +653,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM ---@param self AIBrain ---@param platoon Platoon | string Either a reference to a platoon, or the unique name of the platoon ---@param unit Unit - ---@param squad PlatoonSquads + ---@param squad PlatoonSquadType ---@param formation UnitFormations ---@return UnitFormations #Returns the name of the formation AssignUnitToPlatoon = function(self, platoon, unit, squad, formation) diff --git a/lua/aibrains/campaign-ai.lua b/lua/aibrains/campaign-ai.lua index dd4da441f2b..c3a2bc9a466 100644 --- a/lua/aibrains/campaign-ai.lua +++ b/lua/aibrains/campaign-ai.lua @@ -366,9 +366,7 @@ AIBrain = Class(StandardBrain) { -- TURNING OFF AI POOL PLATOON, I MAY JUST REMOVE THAT PLATOON FUNCTIONALITY LATER local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - poolPlatoon:TurnOffPoolAI() - end + poolPlatoon:TurnOffPoolAI() self.HasPlatoonList = false self:PBMSetEnabled(true) diff --git a/lua/aibrains/easy-ai.lua b/lua/aibrains/easy-ai.lua index 470d2972fde..dc3d3b4488d 100644 --- a/lua/aibrains/easy-ai.lua +++ b/lua/aibrains/easy-ai.lua @@ -246,7 +246,7 @@ AIBrain = Class(StandardBrain, EconomyComponent) { ---@param self EasyAIBrain ---@param platoon AIPlatoon ---@param units Unit[] - ---@param squad PlatoonSquads + ---@param squad PlatoonSquadType ---@param formation UnitFormations AssignUnitsToPlatoon = function(self, platoon, units, squad, formation) StandardBrain.AssignUnitsToPlatoon(self, platoon, units, squad, formation) diff --git a/lua/aibrains/medium-ai.lua b/lua/aibrains/medium-ai.lua index 65fd7a6b158..6a99fc35b2d 100644 --- a/lua/aibrains/medium-ai.lua +++ b/lua/aibrains/medium-ai.lua @@ -334,10 +334,8 @@ AIBrain = Class(StandardBrain, EconomyComponent) { -- TURNING OFF AI POOL PLATOON, I MAY JUST REMOVE THAT PLATOON FUNCTIONALITY LATER local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - poolPlatoon.ArmyPool = true - poolPlatoon:TurnOffPoolAI() - end + poolPlatoon.ArmyPool = true + poolPlatoon:TurnOffPoolAI() -- Stores handles to all builders for quick iteration and updates to all self.BuilderHandles = {} diff --git a/lua/aibrains/rush-ai.lua b/lua/aibrains/rush-ai.lua index 12d7c13153f..6ac1c3785cc 100644 --- a/lua/aibrains/rush-ai.lua +++ b/lua/aibrains/rush-ai.lua @@ -334,10 +334,8 @@ AIBrain = Class(StandardBrain, EconomyComponent) { -- TURNING OFF AI POOL PLATOON, I MAY JUST REMOVE THAT PLATOON FUNCTIONALITY LATER local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - poolPlatoon.ArmyPool = true - poolPlatoon:TurnOffPoolAI() - end + poolPlatoon.ArmyPool = true + poolPlatoon:TurnOffPoolAI() -- Stores handles to all builders for quick iteration and updates to all self.BuilderHandles = {} diff --git a/lua/aibrains/tech-ai.lua b/lua/aibrains/tech-ai.lua index 0e1246db01d..83c242a0cca 100644 --- a/lua/aibrains/tech-ai.lua +++ b/lua/aibrains/tech-ai.lua @@ -334,10 +334,8 @@ AIBrain = Class(StandardBrain, EconomyComponent) { -- TURNING OFF AI POOL PLATOON, I MAY JUST REMOVE THAT PLATOON FUNCTIONALITY LATER local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - poolPlatoon.ArmyPool = true - poolPlatoon:TurnOffPoolAI() - end + poolPlatoon.ArmyPool = true + poolPlatoon:TurnOffPoolAI() -- Stores handles to all builders for quick iteration and updates to all self.BuilderHandles = {} diff --git a/lua/aibrains/turtle-ai.lua b/lua/aibrains/turtle-ai.lua index 766e883fad9..12d01249c21 100644 --- a/lua/aibrains/turtle-ai.lua +++ b/lua/aibrains/turtle-ai.lua @@ -334,10 +334,8 @@ AIBrain = Class(StandardBrain, EconomyComponent) { -- TURNING OFF AI POOL PLATOON, I MAY JUST REMOVE THAT PLATOON FUNCTIONALITY LATER local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - poolPlatoon.ArmyPool = true - poolPlatoon:TurnOffPoolAI() - end + poolPlatoon.ArmyPool = true + poolPlatoon:TurnOffPoolAI() -- Stores handles to all builders for quick iteration and updates to all self.BuilderHandles = {} diff --git a/lua/editor/BaseManagerBuildConditions.lua b/lua/editor/BaseManagerBuildConditions.lua index 57512ea6322..06c140a6633 100644 --- a/lua/editor/BaseManagerBuildConditions.lua +++ b/lua/editor/BaseManagerBuildConditions.lua @@ -7,47 +7,55 @@ ----------------------------------------------------------------- local AIUtils = import("/lua/ai/aiutilities.lua") +local Utilities = import("/lua/utilities.lua") ----NeedAnyStructure = BuildCondition +local pairs = pairs +local tableEmpty = table.empty +local tableGetn = table.getn +local EntityCategoryContains, ParseEntityCategory = EntityCategoryContains, ParseEntityCategory + +--Cached categories +local t2factories = categories.FACTORY * categories.TECH2 +local t3factories = categories.FACTORY * categories.TECH3 + +--- Checks the base template of the base manager to see if any structure is not built. ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function NeedAnyStructure(aiBrain, baseName) + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end - if not aiBrain.BaseManagers[baseName] then - return false - end + for _, data in pairs(bManager.LevelNames) do + if data.Priority <= 0 then continue end - local bManager = aiBrain.BaseManagers[baseName] - for dNum, data in bManager.LevelNames do - if data.Priority > 0 then - local buildTemplate = aiBrain.BaseTemplates[baseName .. data.Name].Template - local buildList = aiBrain.BaseTemplates[baseName .. data.Name].List - local buildCounter = aiBrain.BaseTemplates[baseName .. data.Name].BuildCounter - - if not buildTemplate or not buildList then - return false + local levelName = baseName .. data.Name + local buildTemplate = aiBrain.BaseTemplates[levelName].Template + local buildList = aiBrain.BaseTemplates[levelName].List + local buildCounter = aiBrain.BaseTemplates[levelName].BuildCounter + + if not buildTemplate or not buildList then continue end + + for _, entry in pairs(buildTemplate) do + local structureType = entry[1][1] + if not bManager:CheckStructureBuildable(structureType) then continue end + + ---@type BlueprintId|nil + local category + for catName, catData in pairs(buildList) do + if catData.StructureType == structureType then + category = catData.StructureCategory + break + end end - for _, v in buildTemplate do - if bManager:CheckStructureBuildable(v[1][1]) then - -- Get the building to build - local category - for catName, catData in buildList do - if catData.StructureType == v[1][1] then - category = catData.StructureCategory - break - end - end - -- Iterate through build locations - for num, location in v do - if category and num > 1 then - -- Check if it can be built and then build - if aiBrain:CanBuildStructureAt(category, {location[1], 0, location[2]}) - and bManager:CheckUnitBuildCounter(location, buildCounter) then - return true - end - end - end + if not category then continue end + + -- Iterate through build locations + for i = 2, tableGetn(entry) do + ---@type Vector + local pos = entry[i] + if aiBrain:CanBuildStructureAt(category, { pos[1], 0, pos[2] }) and bManager:CheckUnitBuildCounter(pos, buildCounter) then + return true end end end @@ -55,69 +63,64 @@ function NeedAnyStructure(aiBrain, baseName) return false end ----NumUnitsLessNearBase = BuildCondition +--- Compares number of `category` units belonging to `aiBrain` that are in within the base radius to `varName` ---@param aiBrain CampaignAIBrain ---@param baseName string ---@param category EntityCategory ----@param varName string +---@param varName? string|integer If `nil` compares to `BaseManager.EngineerQuantity`, `string` to variable in `ScenarioInfo.VarTable`, `integer` to found unit count. ---@return boolean function NumUnitsLessNearBase(aiBrain, baseName, category, varName) - if aiBrain.BaseManagers[baseName] == nil then - return false - else - local base = aiBrain.BaseManagers[baseName] - local unitList = aiBrain:GetUnitsAroundPoint(category, base:GetPosition(), base.Radius, 'Ally') - local count = 0 - for i, unit in unitList do - if unit:GetAIBrain() == aiBrain then - count = count + 1 - end + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end + + local unitList = aiBrain:GetUnitsAroundPoint(category, bManager.Position, bManager.Radius, 'Ally') + local count = 0 + for _, unit in pairs(unitList) do + if unit:GetAIBrain() == aiBrain then + count = count + 1 end - if not varName then - if count < base.EngineerQuantity then - return true - end - elseif type(varName) == 'string' then - if count < ScenarioInfo.VarTable[varName] then - return true - end - else - if count < varName then - return true - end + end + if not varName then + if count < bManager.EngineerQuantity then + return true + end + elseif type(varName) == 'string' then + if count < ScenarioInfo.VarTable[varName] then + return true + end + else + if count < varName then + return true end - return false end + return false end +--- Returns `true` when number of active engineers in the base is less than set maximum. ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function BaseManagerNeedsEngineers(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.EngineerQuantity > bManager.CurrentEngineerCount + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.EngineerQuantity > bManager.CurrentEngineerCount end +--- Returns `true` when any of the expansions has less engineers than set in the expansion data. ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function ExpansionBasesNeedEngineers(aiBrain, baseName) - if not aiBrain.BaseManagers[baseName] then - return false - end local bManager = aiBrain.BaseManagers[baseName] - if not bManager.ExpansionBaseData then - return false - end - for num, eData in bManager.ExpansionBaseData do - local eBaseName = eData.BaseName - local base = aiBrain.BaseManagers[eBaseName] - if base and base:GetPosition() and base.Radius then - local count = base.CurrentEngineerCount - count = count + eData.IncomingEngineers - if count < eData.Engineers then - return true - end + if not (bManager and bManager.ExpansionBaseData) then return false end + + for _, eData in pairs(bManager.ExpansionBaseData) do + local ebManager = aiBrain.BaseManagers[eData.BaseName] + if not ebManager then continue end + + local count = ebManager.CurrentEngineerCount + count = count + eData.IncomingEngineers + if count < eData.Engineers then + return true end end return false @@ -129,69 +132,66 @@ end ---@param eBaseName string ---@return boolean function NumEngiesInExpansionBase(aiBrain, baseName, eBaseName) - if not aiBrain.BaseManagers[baseName] or not aiBrain.BaseManagers[eBaseName] then - return false - end local bManager = aiBrain.BaseManagers[baseName] - if not bManager.ExpansionBaseData then - return false - end - for num, eData in bManager.ExpansionBaseData do - if eData.BaseName == eBaseName then - local base = aiBrain.BaseManagers[eBaseName] - if base and base:GetPosition() and base.Radius then - local count = base.CurrentEngineerCount - count = count + eData.IncomingEngineers - if count < eData.Engineers then - return true - end - end + local ebManager = aiBrain.BaseManagers[eBaseName] + if not (bManager and ebManager and bManager.ExpansionBaseData) then return false end + + for _, eData in pairs(bManager.ExpansionBaseData) do + if eData.BaseName ~= eBaseName then continue end + + local count = ebManager.CurrentEngineerCount + count = count + eData.IncomingEngineers + if count < eData.Engineers then + return true end end return false end +--- Currently unsed, as ACU is treated as engineer in the base manager. ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function CDRInPoolNeedAnyStructure(aiBrain, baseName) - if not aiBrain.BaseManagers[baseName] then - return false - end + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end + local pool = aiBrain:GetPlatoonUniquelyNamed('ArmyPool') - local cdrUnit = false + ---@type ACUUnit|nil + local cdrUnit for _, v in pool:GetPlatoonUnits() do if not v.Dead and EntityCategoryContains(categories.COMMAND, v) then cdrUnit = v end end - if not cdrUnit then - return false - end - for dNum, data in aiBrain.BaseManagers[baseName].LevelNames do - if data.Priority > 0 then - local buildTemplate = aiBrain.BaseTemplates[baseName .. data.Name].Template - local buildList = aiBrain.BaseTemplates[baseName .. data.Name].List - if not buildTemplate or not buildList then - return false - end - for _, v in buildTemplate do - -- Get the building to build - local category - for catName, catData in buildList do - if catData.StructureType == v[1][1] then - category = catData.StructureCategory - break - end + + if not cdrUnit then return false end + + for _, data in pairs(bManager.LevelNames) do + if data.Priority <= 0 then continue end + + local levelName = baseName .. data.Name + local buildTemplate = aiBrain.BaseTemplates[levelName].Template + local buildList = aiBrain.BaseTemplates[levelName].List + if not buildTemplate or not buildList then return false end + + for _, entry in pairs(buildTemplate) do + -- Get the building to build + local category + for catName, catData in buildList do + if catData.StructureType == entry[1][1] then + category = catData.StructureCategory + break end - if category and cdrUnit:CanBuild(category) then - -- Iterate through build locations - for num, location in v do - -- Check if it can be built and then build - if num > 1 and aiBrain:CanBuildStructureAt(category, {location[1], 0, location[2]}) then - return true - end - end + end + if not category or not cdrUnit:CanBuild(category) then continue end + + -- Iterate through build locations + for i = 2, tableGetn(entry) do + ---@type Vector + local pos = entry[i] + if aiBrain:CanBuildStructureAt(category, { pos[1], 0, pos[2] }) then + return true end end end @@ -199,50 +199,50 @@ function CDRInPoolNeedAnyStructure(aiBrain, baseName) return false end +--- Currently unsed, as sACU is treated as engineer in the base manager. ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function SubCDRInPoolNeedAnyStructure(aiBrain, baseName) - if not aiBrain.BaseManagers[baseName] then - return false - end + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end local pool = aiBrain:GetPlatoonUniquelyNamed('ArmyPool') - local cdrUnit = false + ---@type ACUUnit|nil + local cdrUnit for _, v in pool:GetPlatoonUnits() do if not v.Dead and EntityCategoryContains(categories.SUBCOMMANDER, v) then cdrUnit = v end end - if not cdrUnit then - return false - end + if not cdrUnit then return false end - for dNum, data in aiBrain.BaseManagers[baseName].LevelNames do - if data.Priority > 0 then - local buildTemplate = aiBrain.BaseTemplates[baseName .. data.Name].Template - local buildList = aiBrain.BaseTemplates[baseName .. data.Name].List - if not buildTemplate or not buildList then - return false - end - for _, v in buildTemplate do - -- Get the building to build - local category - for catName, catData in buildList do - if catData.StructureType == v[1][1] then - category = catData.StructureCategory - break - end + for _, data in pairs(bManager.LevelNames) do + if data.Priority <= 0 then continue end + + local levelName = baseName .. data.Name + local buildTemplate = aiBrain.BaseTemplates[levelName].Template + local buildList = aiBrain.BaseTemplates[levelName].List + if not buildTemplate or not buildList then return false end + + for _, entry in pairs(buildTemplate) do + -- Get the building to build + local category + for catName, catData in buildList do + if catData.StructureType == entry[1][1] then + category = catData.StructureCategory + break end - if category and cdrUnit:CanBuild(category) then - -- Iterate through build locations - for num, location in v do - -- Check if it can be built and then build - if num > 1 and aiBrain:CanBuildStructureAt(category, {location[1], 0, location[2]}) then - return true - end - end + end + if not category or not cdrUnit:CanBuild(category) then continue end + + -- Iterate through build locations + for i = 2, tableGetn(entry) do + ---@type Vector + local pos = entry[i] + if aiBrain:CanBuildStructureAt(category, { pos[1], 0, pos[2] }) then + return true end end end @@ -250,58 +250,94 @@ function SubCDRInPoolNeedAnyStructure(aiBrain, baseName) return false end +--- Returns `true` if the base construction units are building any of the passed `catTable` units. ---@param aiBrain CampaignAIBrain ---@param baseName string ----@param catTable string +---@param catTable string[] List of category strings to test, e.g. `{'MOBILE LAND', 'ALLUNITS' }` ---@return boolean function CategoriesBeingBuilt(aiBrain, baseName, catTable) - if not aiBrain.BaseManagers[baseName] then - return false - end + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end - local basePos = aiBrain.BaseManagers[baseName]:GetPosition() - local baseRad = aiBrain.BaseManagers[baseName].Radius - if not basePos or not baseRad then - return false - end + local basePos = bManager.Position + local baseRad = bManager.Radius + -- Faster to compare squared distances + local baseRadSquared = baseRad * baseRad local unitsBuilding = aiBrain:GetListOfUnits(categories.CONSTRUCTION, false) - for unitNum, unit in unitsBuilding do - if not unit.Dead and unit:IsUnitState('Building') then - local buildingUnit = unit.UnitBeingBuilt - if buildingUnit and not buildingUnit.Dead then - for catNum, buildeeCat in catTable do - local buildCat = ParseEntityCategory(buildeeCat) - if EntityCategoryContains(buildCat, buildingUnit) then - local unitPos = unit:GetPosition() - if unitPos and VDist2(basePos[1], basePos[3], unitPos[1], unitPos[3]) < baseRad then - return true - end - end - end + for _, unit in pairs(unitsBuilding) do + if unit.Dead or not unit:IsUnitState('Building') then continue end + + local buildingUnit = unit.UnitBeingBuilt + if not buildingUnit or buildingUnit.Dead then continue end + + for _, buildeeCat in pairs(catTable) do + local buildCat = ParseEntityCategory(buildeeCat) + if not EntityCategoryContains(buildCat, buildingUnit) then continue end + + local unitPos = unit:GetPosition() + if unitPos and Utilities.XZDistanceTwoVectorsSquared(unitPos, basePos) < baseRadSquared then + return true end end end return false end ---@param aiBrain ArmiesTable ----@param level number +--- Checks if the base has any T3 / T2 factories based on `level`. +--- +--- Always returns true for when `level` is `1` +---@param aiBrain CampaignAIBrain +---@param level number Factory tech level ---@param baseName string ---@return boolean function HighestFactoryLevel(aiBrain, level, baseName) local bManager = aiBrain.BaseManagers[baseName] - if not bManager then - return false + if not bManager then return false end + + local position = bManager.Position + local radius = bManager.Radius + local t3fac = AIUtils.GetOwnUnitsAroundPoint(aiBrain, t3factories, position, radius) + local t2fac = AIUtils.GetOwnUnitsAroundPoint(aiBrain, t2factories, position, radius) + if table.getn(t3fac) > 0 then + return level == 3 + elseif table.getn(t2fac) > 0 then + return level == 2 + end + + return true +end + +--- Returns true when the highest tier factory matches `level` +--- Example: level = 2, type = "Land". Platoon wont be built if the base has T3 land factory. +---@param aiBrain CampaignAIBrain +---@param level number +---@param baseName string +---@param type "Air"|"Land"|"Sea" +---@return boolean +function HighestFactoryLevelType(aiBrain, level, baseName, type) + local bManager = aiBrain.BaseManagers[baseName] + if not bManager then return false end + + local position = bManager.Position + local radius = bManager.Radius + local catCheck + if type == 'Air' then + catCheck = categories.AIR + elseif type == 'Land' then + catCheck = categories.LAND + elseif type == 'Sea' then + catCheck = categories.NAVAL end - local t3FacList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, categories.FACTORY * categories.TECH3, bManager:GetPosition(), bManager.Radius) - local t2FacList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, categories.FACTORY * categories.TECH2, bManager:GetPosition(), bManager.Radius) - if t3FacList and not table.empty(t3FacList) then - return level == 3 - elseif t2FacList and not table.empty(t2FacList) then + local t3fac = AIUtils.GetOwnUnitsAroundPoint(aiBrain, t3factories * catCheck, position, radius) + local t2fac = AIUtils.GetOwnUnitsAroundPoint(aiBrain, t2factories * catCheck, position, radius) + if table.getn(t3fac) > 0 then + return level == 3 + elseif table.getn(t2fac) > 0 then return level == 2 end + return true end @@ -313,13 +349,11 @@ end ---@return boolean function FactoryCountAndNeed(aiBrain, techLevel, engQuantity, pType, baseName) local bManager = aiBrain.BaseManagers[baseName] - if not bManager then - return false - end + if not bManager then return false end - local facCat = ParseEntityCategory('FACTORY * TECH'..techLevel) - local facList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, facCat, bManager:GetPosition(), bManager.Radius) - local typeCount = {Air = 0, Land = 0, Sea = 0, } + local facCat = ParseEntityCategory('FACTORY * TECH' .. techLevel) + local facList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, facCat, bManager.Position, bManager.Radius) + local typeCount = { Air = 0, Land = 0, Sea = 0, } for k, v in facList do if EntityCategoryContains(categories.AIR, v) then typeCount['Air'] = typeCount['Air'] + 1 @@ -354,9 +388,9 @@ end ---@return boolean function UnfinishedBuildingsCheck(aiBrain, baseName) local bManager = aiBrain.BaseManagers[baseName] - - -- Return if the BaseManager doesn't exist, or the list is empty, or all buildings are finished - if not bManager or table.empty(bManager.UnfinishedBuildings) then + + -- Return if the BaseManager doesn't exist, or the list is empty, or all buildings are finished + if not bManager or tableEmpty(bManager.UnfinishedBuildings) then return false end @@ -372,7 +406,8 @@ function UnfinishedBuildingsCheck(aiBrain, baseName) end for unitName, _ in bManager.UnfinishedBuildings do - if ScenarioInfo.UnitNames[armyIndex][unitName] and not ScenarioInfo.UnitNames[armyIndex][unitName].Dead then + local unit = ScenarioInfo.UnitNames[armyIndex][unitName] + if unit and not unit.Dead then if not beingBuiltList[unitName] then return true end @@ -381,42 +416,12 @@ function UnfinishedBuildingsCheck(aiBrain, baseName) return false end ----@param aiBrain CampaignAIBrain ----@param level number ----@param baseName string ----@param type string ----@return boolean -function HighestFactoryLevelType(aiBrain, level, baseName, type) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then - return false - end - - local catCheck - if type == 'Air' then - catCheck = categories.AIR - elseif type == 'Land' then - catCheck = categories.LAND - elseif type == 'Sea' then - catCheck = categories.NAVAL - end - - local t3FacList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, categories.FACTORY * categories.TECH3 * catCheck, bManager:GetPosition(), bManager.Radius) - local t2FacList = AIUtils.GetOwnUnitsAroundPoint(aiBrain, categories.FACTORY * categories.TECH2 * catCheck, bManager:GetPosition(), bManager.Radius) - if t3FacList and not table.empty(t3FacList) then - return level == 3 - elseif t2FacList and not table.empty(t2FacList) then - return level == 2 - end - return true -end - ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function BaseActive(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.Active + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.Active end --- Deprecated, it was supposed to be a condition for an unfinished reclaim function/thread @@ -424,7 +429,7 @@ end ---@param baseName string ---@return boolean function BaseReclaimEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] + local bManager = aiBrain.BaseManagers[baseName] return bManager and bManager.FunctionalityStates.EngineerReclaiming end @@ -432,7 +437,7 @@ end ---@param baseName string ---@return boolean function BasePatrollingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] + local bManager = aiBrain.BaseManagers[baseName] return bManager and bManager.FunctionalityStates.Patrolling end @@ -440,47 +445,47 @@ end ---@param baseName string ---@return boolean function BaseBuildingEngineers(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.BuildEngineers + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.BuildEngineers end ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function BaseEngineersEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.Engineers + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.Engineers end ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function LandScoutingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.LandScouting + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.LandScouting end ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function AirScoutingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.AirScouting + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.AirScouting end ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function ExpansionBasesEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.ExpansionBases + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.ExpansionBases end ---@param aiBrain CampaignAIBrain ---@param baseName string ---@return boolean function TMLsEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] + local bManager = aiBrain.BaseManagers[baseName] return bManager and bManager.FunctionalityStates.TMLs end @@ -489,9 +494,10 @@ end ---@return boolean function NukesEnabled(aiBrain, baseName) local bManager = aiBrain.BaseManagers[baseName] - return bManager and bManager.FunctionalityStates.Nukes + return bManager and bManager.FunctionalityStates.Nukes end ---- Moved Unused Imports for mod compatibility +-- Moved Unused Imports for mod compatibility + local ScenarioFramework = import("/lua/scenarioframework.lua") -local ScenarioUtils = import("/lua/sim/scenarioutilities.lua") \ No newline at end of file +local ScenarioUtils = import("/lua/sim/scenarioutilities.lua") diff --git a/lua/platoon.lua b/lua/platoon.lua index 21759aa8af4..97bf24b6bf0 100644 --- a/lua/platoon.lua +++ b/lua/platoon.lua @@ -46,13 +46,13 @@ local mobileNavalCategories = categories.MOBILE * categories.NAVAL --for sorian AI local SUtils = import("/lua/ai/sorianutilities.lua") ----@alias PlatoonSquads 'Attack' | 'Artillery' | 'Guard' | 'Scout' | 'Support' | 'Unassigned' +---@alias PlatoonSquads PlatoonSquadType ---@class PlatoonSquadTemplate ---@field [1] UnitId ---@field [2] integer Min, negative number will set it to `Min` * NumAvailableFactories, each time the platoon is set to build ---@field [3] integer Max ----@field [4] PlatoonSquads +---@field [4] PlatoonSquadType ---@field [5] UnitFormations ---Platoon Template can have any number of squad templates, starting from index 3. diff --git a/lua/sim/ScenarioUtilities.lua b/lua/sim/ScenarioUtilities.lua index 42a37402aaa..68d9cc5773f 100644 --- a/lua/sim/ScenarioUtilities.lua +++ b/lua/sim/ScenarioUtilities.lua @@ -1100,7 +1100,7 @@ function CreatePlatoons(strArmy, tblNode, tblResult, platoonList, currPlatoon, t end ---@type PlatoonSquadTemplate local squadTp = currTemplate[i] - ---@type BlueprintId, integer, integer, PlatoonSquads, UnitFormations + ---@type BlueprintId, integer, integer, PlatoonSquadType, UnitFormations local bpId, min, max, squad, formation = unpack(squadTp) if tblData.type == bpId and platoon.squadCounter[i] < max then platoon.squadCounter[i] = platoon.squadCounter[i] + 1 diff --git a/lua/system/GlobalPlatoonTemplate.lua b/lua/system/GlobalPlatoonTemplate.lua index 049c8101ef0..58823e2cf73 100644 --- a/lua/system/GlobalPlatoonTemplate.lua +++ b/lua/system/GlobalPlatoonTemplate.lua @@ -8,7 +8,7 @@ ---@field [1] EntityCategory | string ---@field [2] number Minimal number of units ---@field [3] number Maximum number of units ----@field [4] PlatoonSquads Squad the units belong to +---@field [4] PlatoonSquadType Squad the units belong to ---@field [5] UnitFormations Formation to apply ---@class PlatoonTemplatePlanSpec