diff --git a/changelog/snippets/balance.6179.md b/changelog/snippets/balance.6179.md new file mode 100644 index 0000000000..4d54df6735 --- /dev/null +++ b/changelog/snippets/balance.6179.md @@ -0,0 +1 @@ +- (#6179) Increase Ilshavoh's max turn rate from 75 to 90 and on the spot turn rate from 45 to 90. diff --git a/changelog/snippets/fix.6182.md b/changelog/snippets/fix.6182.md new file mode 100644 index 0000000000..8ea1466611 --- /dev/null +++ b/changelog/snippets/fix.6182.md @@ -0,0 +1 @@ +- (#6182) Enable the target bones for the Soothsayer. diff --git a/changelog/snippets/other.6181.md b/changelog/snippets/other.6181.md new file mode 100644 index 0000000000..dd115f5e1b --- /dev/null +++ b/changelog/snippets/other.6181.md @@ -0,0 +1 @@ +- (#6181) Annotate fields and functions related to bomb projectiles. diff --git a/engine/Core/Blueprints/ProjectileBlueprint.lua b/engine/Core/Blueprints/ProjectileBlueprint.lua index 152b26f494..0e0bc70c2d 100644 --- a/engine/Core/Blueprints/ProjectileBlueprint.lua +++ b/engine/Core/Blueprints/ProjectileBlueprint.lua @@ -66,7 +66,7 @@ ---@field LeadTarget boolean --- Whether projectiles should try to stay underwater. Applies only to tracking projectiles. ---@field StayUnderwater boolean ---- if the projectile is initially affected by gravity +--- if the projectile is initially affected by gravity (-4.9 ogrids/second/second) ---@field UseGravity boolean --- projectile will detonate when going above this height above ground ---@field DetonateAboveHeight number @@ -133,7 +133,8 @@ ---@field MaxZigZag number --- frequency of zig-zag directional changes, in seconds ---@field ZigZagFrequency number ---- realistic free fall ordinance type weapon +--- When true and weapon muzzle velocity is 0, the projectile's horizontal velocity is set in the direction of the target with the speed of the weapon firing the projectile. +--- Used for realistic free fall ordinance type weapons like bombs ---@field RealisticOrdinance boolean --- bombs that always drop stright down ---@field StraightDownOrdinance boolean diff --git a/engine/Core/Blueprints/WeaponBlueprint.lua b/engine/Core/Blueprints/WeaponBlueprint.lua index 6dbbbe07e8..03ed0926ce 100644 --- a/engine/Core/Blueprints/WeaponBlueprint.lua +++ b/engine/Core/Blueprints/WeaponBlueprint.lua @@ -51,7 +51,7 @@ ---@field BeamLifetime number --- if the weapon will only fire when underwater ---@field BelowWaterFireOnly? boolean ---- threshold to release point before releasing ordnance +--- Distance from bomb firing solution's position to the target's position within which the weapon will fire ---@field BombDropThreshold? number --- information about the bonuses added to the weapon when it reaches a specific veterancy level ---@field Buffs BlueprintBuff[] diff --git a/engine/Sim/Projectile.lua b/engine/Sim/Projectile.lua index eddb6057f9..8ded5c5a1e 100644 --- a/engine/Sim/Projectile.lua +++ b/engine/Sim/Projectile.lua @@ -90,7 +90,7 @@ end function Projectile:SetAcceleration(accel) end ---- Define the ballistic acceleration value, increases velocity in the current direction. +--- Set the vertical (gravitational) acceleration of the projectile. Default is -4.9, which is expected by the engine's weapon targeting and firing ---@param accel number function Projectile:SetBallisticAcceleration(accel) end diff --git a/lua/AI/AttackManager.lua b/lua/AI/AttackManager.lua index 2d7fe62cd4..60279e8935 100644 --- a/lua/AI/AttackManager.lua +++ b/lua/AI/AttackManager.lua @@ -1,43 +1,75 @@ local Utilities = import("/lua/utilities.lua") --- ATTACK MANAGER SPEC ---{ --- AttackCheckInterval = interval, --- Platoons = { --- { --- PlatoonName = string, --- AttackConditions = { function, {args} }, --- AIThread = function, -- If AMPlatoon needs a specific function --- AIName = string, -- AIs from platoon.lua --- Priority = num, --- PlatoonData = table, --- OverrideFormation = string, -- formation to use for the attack platoon --- FormCallbacks = table, -- table of functions called when an AM Platoon forms --- DestroyCallbacks = table, -- table of functions called when the platoon is destroyed --- LocationType = string, -- location from PBM -- used if you want to get units from pool --- PlatoonType = string, -- 'Air', 'Sea', 'Land' -- MUST BE SET IF UsePool IS TRUE --- UsePool = bool, -- bool to use pool or not --- }, --- }, ---} --- --- Spec for Platoons - within PlatoonData --- PlatoonData = { --- AMPlatoons = { AMPlatoonName, AMPlatoonName, etc }, --- }, +------------------------------------------------------------------------------------------------------------------------- +--- 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 +--- A quick rundown how it works in practice: +--- - In FA, it's used by the "BaseOpAI.lua", with platoons created via the BaseManager +--- - The BaseManager builds a bunch of PBM platoons, then combines them into an AM platoon +--- - The PBM platoons are defined in the many *save.lua files in "lua/AI/OpAI" +--- - Platoons containing the 'Child' name are the ones to be combined +--- - Platoons containing the 'Master' name are the ones to be converted to AM Platoons +--- - The AM is capable of forming random platoon compositions this way +--- - It's also less likely to leave units sitting at bases, since it combines existing platoons into a new one +--- - By default, the loading of platoons is handled in "ScenarioUtilities.lua", in the 'OSB' related functions +--- - AM platoons can be added in other ways, it's up to the mission maker/scripter how they do it +-------------------------------------------------------------------------------------------------------------------------- +--- An example of the Attack Manager platoon 'builder' template: +--- +--- Platoons = +--- { +--- { +--- PlatoonName = string, +--- AttackConditions = { function, {args} }, -- Literally build conditions, just with a different name, all of them need to return true in order for the platoon to be formed +--- AIThread = function, -- If AMPlatoon needs a specific function +--- AIName = string, -- AI plans from platoon.lua +--- Priority = num, +--- PlatoonData = table, +--- OverrideFormation = string, -- Formation to use for the attack platoon +--- FormCallbacks = table, -- Table of functions called when an AM Platoon forms +--- DestroyCallbacks = table, -- Table of functions called when the platoon is destroyed +--- LocationType = string, -- Location from PBM -- used if you want to get units from pool +--- PlatoonType = string, -- 'Air', 'Sea', 'Land' -- MUST BE SET IF UsePool IS TRUE +--- UsePool = bool, -- Bool to use pool or not +--- }, +--- }, + +--- Example how to pick the master platoon - within PlatoonData + --- PlatoonData = { + --- AMPlatoons = { AMPlatoonName, AMPlatoonName, etc }, + --- }, +--- Example how to set a master platoon - within PlatoonData + --- PlatoonData = { + --- AMMasterPlatoon = true, + --- }, ---@class AttackManager +---@field brain AI Brain +---@field PlatoonCount +---@field AttackCheckInterval How often the AM should attempt to form platoons +---@field Platoons Table of platoons belonging to the AI +---@field AttackManagerState Either 'ACTIVE' or 'PAUSED', used to check if the AM is active for an AI +---@field AttackManagerThread Bool, used to check if the main AM thread is already running AttackManager = ClassSimple { brain = nil, NeedSort = false, PlatoonCount = { DefaultGroupAir = 0, DefaultGroupLand = 0, DefaultGroupSea = 0, }, - + + --- Engine level initialization, usually triggered by *AIBrain:InitializeAttackManager(attackDataTable)* in the campaign AI brain + ---@param self AttackManager + ---@param brain AI Brain + ---@param attackDataTable table | See *Initialize()* below for its expected contents __init = function(self, brain, attackDataTable) self.Trash = TrashBag() self.brain = brain - self:Initialize(table) + self:Initialize(attackDataTable) end, - + + --- Unique ForkThread for this class, for easy syntax usage, and data trash handling + --- Swaps the order of params for simpler function call + ---@param self AttackManager + ---@param fn Function + ---@param ... | Further parameters for the function we want to be forked ForkThread = function(self, fn, ...) if fn then local thread = ForkThread(fn, self, unpack(arg)) @@ -47,38 +79,45 @@ AttackManager = ClassSimple { return nil end end, - + + --- The actual initialization of all necessary data, and the main Thread + ---@param self AttackManager + ---@param attackDataTable table | Optional table containing the loop check delay, default build conditions, and default platoons to load Initialize = function(self, attackDataTable) self:AddDefaultPlatoons(attackDataTable.AttackConditions) if attackDataTable then - self.AttackCheckInterval = attackDataTable.AttackCheckInterval or 13 + self.AttackCheckInterval = attackDataTable.AttackCheckInterval or 10 if attackDataTable.Platoons then self:AddPlatoonsTable(attackDataTable.Platoons) end elseif not self.AttackCheckInterval then - self.AttackCheckInterval = 13 + self.AttackCheckInterval = 10 end + self['AttackManagerState'] = 'ACTIVE' self['AttackManagerThread'] = self:ForkThread(self.AttackManagerThread) end, - + + --- The main thread that forms the AM platoons periodically + ---@param self AttackManager AttackManagerThread = function(self) - local ad = self - local alertLocation, alertLevel, tempLevel while true do - WaitSeconds(ad.AttackCheckInterval) - if ad.AttackManagerState == 'ACTIVE' and self.Platoons then + if self.AttackManagerState == 'ACTIVE' and self.Platoons then self:AttackManageAttackVectors() self:FormAttackPlatoon() end + WaitSeconds(self.AttackCheckInterval) end end, - + + --- Loads the default AM platoons, these aren't formed in coop/campaign, they were used by SC1's skirmish AIs + ---@param self AttackManager + ---@param AttackConds table | Table of conditions that return with either true or false AddDefaultPlatoons = function(self, AttackConds) - local atckCond = {} + -- Fallback condition if not AttackConds then AttackConds = { - { '/lua/editor/platooncountbuildconditions.lua', 'NumGreaterOrEqualAMPlatoons', {'default_brain', 'DefaultGroupAir', 3} }, + {'/lua/editor/MiscBuildConditions.lua', 'False', {'default_brain'}}, } end @@ -98,6 +137,9 @@ AttackManager = ClassSimple { Priority = 1, PlatoonType = 'Land', UsePool = true, + PlatoonData = { + UseFormation = 'GrowthFormation', + } }, { PlatoonName = 'DefaultGroupSea', @@ -111,13 +153,19 @@ AttackManager = ClassSimple { self:AddPlatoonsTable(platoons) end, - + + --- Adds a table of platoon builders to the AM + ---@param self AttackManager + ---@param platoons table | Table of platoon builders AddPlatoonsTable = function(self, platoons) for k,v in platoons do self:AddPlatoon(v) end end, - + + --- Adds a single platoon builder to the AM + ---@param self AttackManager + ---@param pltnTable table | table of a platoon builder instance AddPlatoon = function(self, pltnTable) if not pltnTable.AttackConditions then error('*AI WARNING: INVALID ATTACK MANAGER PLATOON LIST - Missing AttackConditions', 2) @@ -143,16 +191,27 @@ AttackManager = ClassSimple { self.NeedSort = true table.insert(self.Platoons, pltnTable) end, - + + --- Wipes the current platoon list, useful if you want to give the AI a whole new list of platoons to be formed over the old ones + ---@param self AttackManager ClearPlatoonList = function(self) self.Platoons = {} self.NeedSort = false end, - + + --- Sets the loop delay for the main platoon forming thread + ---@param self AttackManager + ---@param interval number SetAttackCheckInterval = function(self, interval) self.AttackCheckInterval = interval end, - + + --- Checks the provided conditions + --- If the first entry is "default_brain", it will be removed, this is a GPG thing ever since SC1, because the Brain param provided is always THIS AI's brain + --- My guess is that they planned to have any brain be providable, or that they moved these from C Engine side to Lua, and this is a leftover + ---@param self AttackManager + ---@param pltnInfo table | table of a platoon instance + ---@return bool CheckAttackConditions = function(self, pltnInfo) for k, v in pltnInfo.AttackConditions do if v[3][1] == "default_brain" then @@ -170,7 +229,10 @@ AttackManager = ClassSimple { end return true end, - + + ---@param self AttackManager + ---@param builderName string + ---@param priority number SetPriority = function(self, builderName, priority) for k,v in self.Platoons do if v.PlatoonName == builderName then @@ -178,10 +240,11 @@ AttackManager = ClassSimple { end end end, - + + ---@param self AttackManager SortPlatoonsViaPriority = function(self) local sortedList = {} - --Simple selection sort, this can be made faster later if we decide we need it. + -- Simple selection sort, this can be made faster later if we decide we need it. if self.Platoons then for i = 1, table.getn(self.Platoons) do local highest = 0 @@ -202,20 +265,25 @@ AttackManager = ClassSimple { return sortedList end, + --- The main function that forms the AM platoons + ---@param self AttackManager FormAttackPlatoon = function(self) - local attackForcePL = {} - local namedPlatoonList = {} local poolPlatoon = self.brain:GetPlatoonUniquelyNamed('ArmyPool') - if poolPlatoon then - table.insert(attackForcePL, poolPlatoon) - end + if self.NeedSort then self:SortPlatoonsViaPriority() end + + -- Loop through all of the AM platoons for k,v in self.Platoons do if self:CheckAttackConditions(v) then local combineList = {} local platoonList = self.brain:GetPlatoonsList() + + -- Loop through all of the platoons the AI has, check if it's part of the attack force + -- The PBM platoons are the ones set to be part of the attack force, if they have 'AMPlatoons' platoon data set, this is handled by the "PBMFormPlatoons()" function + -- If it's part of it, check the PlatoonData for the name of the 'master' platoon it belongs to, and if it matches, insert this platoon to the combineList + -- These are defined in the lua/AI/OpAI *save.lua files, containing all of the default platoons for j, platoon in platoonList do if platoon:IsPartOfAttackForce() then for i, name in platoon.PlatoonData.AMPlatoons do @@ -225,6 +293,12 @@ AttackManager = ClassSimple { end end end + + -- If the combineList is not empty, it will form the AM platoon + -- Usually platoons inside the combineList are PBM ones + -- If UsePool is true, it can cause some wonky behaviour, it can additionally grab units from the ArmyPool, but will mess up the actual unit counts defined in the platoon templates + -- This can result in platoons not forming properly, thus units sitting at their bases cluttering up + -- By default it's practically not used at all if not table.empty(combineList) or v.UsePool then local tempPlatoon if self.Platoons[k].AIName then @@ -233,30 +307,38 @@ AttackManager = ClassSimple { tempPlatoon = self.brain:CombinePlatoons(combineList) end local formation = 'GrowthFormation' - + if v.PlatoonData.OverrideFormation then tempPlatoon:SetPlatoonFormationOverride(v.PlatoonData.OverrideFormation) elseif v.PlatoonType == 'Air' and not v.UsePool then tempPlatoon:SetPlatoonFormationOverride('GrowthFormation') end - + + -- This section is only relevant if we want the AM platoon to grab from the ArmyPool, it was used for SC1's skirmish AI + -- I've added additional categories to be filtered out, we don't want land platoons to grab unassigned transports or such in case we ever decide to make use of the ArmyPool if v.UsePool then local checkCategory + -- Only T1-T3 aerial combat units if v.PlatoonType == 'Air' then - checkCategory = categories.AIR * categories.MOBILE + checkCategory = categories.AIR * categories.MOBILE - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT + -- Only T1-T3 surface combat units elseif v.PlatoonType == 'Land' then - checkCategory = categories.LAND * categories.MOBILE - categories.ENGINEER - categories.EXPERIMENTAL + checkCategory = categories.LAND * categories.MOBILE - categories.ENGINEER - categories.EXPERIMENTAL - categories.SCOUT + -- Only T1-T3 naval combat units elseif v.PlatoonType == 'Sea' then - checkCategory = categories.NAVAL * categories.MOBILE + checkCategory = categories.NAVAL * categories.MOBILE - categories.EXPERIMENTAL + -- Only T1-T3 combined-arms combat units elseif v.PlatoonType == 'Any' then - checkCategory = categories.MOBILE - categories.ENGINEER + checkCategory = categories.MOBILE - categories.ENGINEER - categories.TRANSPORTATION - categories.EXPERIMENTAL - categories.SCOUT else error('*AI WARNING: Invalid Platoon Type - ' .. v.PlatoonType, 2) break end - local poolPlatoon = self.brain:GetPlatoonUniquelyNamed('ArmyPool') + local poolUnits = poolPlatoon:GetPlatoonUnits() local addUnits = {} + + -- 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 for locNum, locData in self.brain.PBM.Locations do @@ -270,11 +352,11 @@ AttackManager = ClassSimple { break end for i,unit in poolUnits do - if Utilities.GetDistanceBetweenTwoVectors(unit:GetPosition(), location.Location) <= location.Radius - and EntityCategoryContains(checkCategory, unit) then - table.insert(addUnits, unit) + if Utilities.GetDistanceBetweenTwoVectors(unit:GetPosition(), location.Location) <= location.Radius and EntityCategoryContains(checkCategory, unit) then + table.insert(addUnits, unit) end end + -- If there's no base of origin, grab ArmyPool units from anywhere else for i,unit in poolUnits do if EntityCategoryContains(checkCategory, unit) then @@ -284,20 +366,29 @@ AttackManager = ClassSimple { end self.brain:AssignUnitsToPlatoon(tempPlatoon, addUnits, 'Attack', formation) end + + -- Set the platoon's data if v.PlatoonData then tempPlatoon:SetPlatoonData(v.PlatoonData) else tempPlatoon.PlatoonData = {} end + -- Set the platoon's name tempPlatoon.PlatoonData.PlatoonName = v.PlatoonName + + -- Set the platoon AI function if v.AIThread then tempPlatoon:ForkAIThread(import(v.AIThread[1])[v.AIThread[2]]) - end + end + + -- Add callbacks when the platoon is destroyed if v.DestroyCallbacks then for dcbNum, destroyCallback in v.DestroyCallbacks do tempPlatoon:AddDestroyCallback(import(destroyCallback[1])[destroyCallback[2]]) end end + + -- Call for the specified callbacks, since we were just formed if v.FormCallbacks then for cbNum, callback in v.FormCallbacks do if type(callback) == 'function' then @@ -311,29 +402,41 @@ AttackManager = ClassSimple { end end end, - + + --- Completely removes the AM thread + ---@param self AttackManager DestroyAttackManager = function(self) if self.AttackManagerThread then self.AttackManagerThread:Destroy() self.AttackManagerThread = nil end end, - + + --- Pauses the AM thread + ---@param self AttackManager PauseAttackManager = function(self) self.AttackManagerState = 'PAUSED' end, - + + --- Re-enables the AM thread + ---@param self AttackManager UnPauseAttackManager = function(self) self.AttackManagerState = 'ACTIVE' end, - + + --- Checks if the AttackManager has been enabled + ---@param self AttackManager + ---@return boolean IsAttackManagerActive = function(self) if self and self.AttackManagerThread and self.AttackManagerState == 'ACTIVE' then return true end return false end, - + + --- Returns with the number of platoons part of the attack force + ---@param self AttackManager + ---@return result number GetNumberAttackForcePlatoons = function(self) local platoonList = self.brain:GetPlatoonsList() local result = 0 @@ -342,19 +445,23 @@ AttackManager = ClassSimple { result = result + 1 end end - --Add in pool platoon, pool platoon is always used. + -- Add in pool platoon, pool platoon is always used. result = result + 1 return result end, - + + --- SC1's use of attack vector data, mostly on the engine side, so I can't comment on it + ---@param self AttackManager AttackManageAttackVectors = function(self) local enemyBrain = self.brain:GetCurrentEnemy() if enemyBrain then self.brain:SetUpAttackVectorsToArmy() end end, - - -- XXX: refactor this later, artifact from moving AttackManager from aibrain + + --- XXX: refactor this later, artifact from moving AttackManager from aibrain + ---@param brain AIBrain + ---@param platoon Platoon DecrementCount = function(brain, platoon) local AM = brain.AttackManager local data = platoon.PlatoonData @@ -362,4 +469,4 @@ AttackManager = ClassSimple { AM.PlatoonCount[v] = AM.PlatoonCount[v] - 1 end end -} +} \ No newline at end of file diff --git a/lua/AI/OpAI/BaseManager.lua b/lua/AI/OpAI/BaseManager.lua index f6962c6b12..8ee2caa775 100644 --- a/lua/AI/OpAI/BaseManager.lua +++ b/lua/AI/OpAI/BaseManager.lua @@ -160,6 +160,67 @@ 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 + + -- 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 Upgrade 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 + 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 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 + ---@alias Enhancement string --TODO ---@class LevelName @@ -213,7 +274,7 @@ BaseManager = ClassSimple { self.NumPermanentAssisting = 0 self.PermanentAssistCount = 0 self.PermanentAssisters = {} - self.MaximumConstructionEngineers = 2 + self.MaximumConstructionEngineers = ScenarioInfo.Options.Difficulty or 3 self.BuildingCounterData = { Default = true, @@ -222,30 +283,33 @@ BaseManager = ClassSimple { 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, + --AirAttacks = true, AirScouting = false, AntiAir = true, Artillery = true, BuildEngineers = true, CounterIntel = true, - EngineerReclaiming = true, + --EngineerReclaiming = true, Engineers = true, ExpansionBases = false, Fabrication = true, GroundDefense = true, Intel = true, - LandAttacks = true, + --LandAttacks = true, LandScouting = false, Nukes = false, Patrolling = true, - SeaAttacks = true, + --SeaAttacks = true, Shields = true, TMLs = true, Torpedos = true, Walls = true, - Custom = {}, + --Custom = {}, } self.LevelNames = {} self.OpAITable = {} @@ -1079,23 +1143,23 @@ BaseManager = ClassSimple { self:SetUnitUpgrades(upgradeTable, 'DefaultSACU', startActive) end, - ---@param self BaseManager + --- 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] - if unit and not unit.Dead then - -- Structure upgrading should take priority, so the check for unit.UnitBeingBuilt is not needed. This check is a lot more reliable to get factories to upgrade - if unit.UnitId ~= v.FinalUnit and not unit:IsBeingBuilt() and not unit:IsUnitState('Upgrading') then - self:ForkThread(self.BaseManagerUpgrade, unit, v.UnitName) - end + -- 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 - local waitTime = Random(3, 5) - WaitSeconds(waitTime) + WaitSeconds(15) end end, @@ -1236,33 +1300,25 @@ BaseManager = ClassSimple { end end, - -- Thread that will upgrade factories, radar, etc to next level - ---@param self BaseManager + --- 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) - local aiBrain = unit:GetAIBrain() - local factionIndex = aiBrain:GetFactionIndex() - local armyIndex = aiBrain:GetArmyIndex() - local upgradeID = aiBrain:FindUpgradeBP(unit.UnitId, UpgradeTemplates.StructureUpgradeTemplates[factionIndex]) - if upgradeID then - IssueToUnitClearCommands(unit) - IssueUpgrade({ unit }, upgradeID) - end - - local upgrading = true - local newUnit = false - while not unit.Dead and upgrading do - WaitSeconds(3) - upgrading = false - if unit and not unit.Dead then - if not newUnit then - newUnit = unit.UnitBeingBuilt - end - upgrading = true - end - end - ScenarioInfo.UnitNames[armyIndex][unitName] = newUnit + 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 @@ -1276,6 +1332,7 @@ BaseManager = ClassSimple { 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 @@ -1313,15 +1370,13 @@ BaseManager = ClassSimple { 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 - list[unit.buildtype].AmountWanted = list[unit.buildtype].AmountWanted + 1 -- Increment num wanted 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, - AmountNeeded = 0, AmountWanted = 1, CloseToBuilder = nil } -- add new section of build list with new unit type information + list[unit.buildtype] = { StructureType = buildList[1], StructureCategory = unit.buildtype } end break end @@ -2083,4 +2138,4 @@ function CreateBaseManager(brain, baseName, markerName, radius, levelTable) end return bManager -end +end \ No newline at end of file diff --git a/lua/AI/OpAI/BaseManagerPlatoonThreads.lua b/lua/AI/OpAI/BaseManagerPlatoonThreads.lua index f1e97d0430..be507b7e00 100644 --- a/lua/AI/OpAI/BaseManagerPlatoonThreads.lua +++ b/lua/AI/OpAI/BaseManagerPlatoonThreads.lua @@ -15,6 +15,13 @@ 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") + +-- Upvalued for performance +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 @@ -25,10 +32,11 @@ function BaseManagerEngineerPlatoonSplit(platoon) 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 isnt at capacity of engineers + -- 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) @@ -36,7 +44,7 @@ function BaseManagerEngineerPlatoonSplit(platoon) local engPlat = aiBrain:MakePlatoon('', '') aiBrain:AssignUnitsToPlatoon(engPlat, {v}, 'Support', 'None') - engPlat.PlatoonData = table.deepcopy(platoon.PlatoonData) + engPlat:SetPlatoonData(platoon.PlatoonData) v.BaseName = baseName engPlat:ForkAIThread(BaseManagerSingleEngineerPlatoon) @@ -61,6 +69,188 @@ function BaseManagerEngineerPlatoonSplit(platoon) 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 bManager = unit.Brain.BaseManagers[BaseName] + local armyIndex = unit.Brain: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 + 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 Upgrade Blueprint +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 + 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) @@ -80,27 +270,35 @@ function BaseManagerSingleRemoved(unit) bManager:SubtractCurrentEngineer() end ---- Main function for base manager engineers +--- Main function for Base Manager Engineers ---@param platoon Platoon function BaseManagerSingleEngineerPlatoon(platoon) platoon.PlatoonData.DontDisband = true local aiBrain = platoon:GetBrain() - local pData = platoon.PlatoonData - local baseName = pData.BaseName + 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) - - elseif canPermanentAssist and bManager.ConditionalBuildData.Unit and not bManager.ConditionalBuildData.Unit.Dead - and bManager.ConditionalBuildData.NeedsMoreBuilders() then + + -- 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 @@ -123,10 +321,6 @@ function BaseManagerSingleEngineerPlatoon(platoon) elseif BMBC.UnfinishedBuildingsCheck(aiBrain, baseName) then BuildUnfinishedStructures(platoon) - -- Reclaim nearby wreckage/trees/rocks/people; never do this right now dont want to destroy props and stuff - elseif false and BMBC.BaseReclaimEnabled(aiBrain, baseName) and MIBC.ReclaimablesInArea(aiBrain, baseName) then - BaseManagerReclaimThread(platoon) - -- Try to assist elseif BMBC.CategoriesBeingBuilt(aiBrain, baseName, {'MOBILE LAND', 'ALLUNITS' }) or(bManager:ConstructionNeedsAssister()) then BaseManagerAssistThread(platoon) @@ -136,7 +330,7 @@ function BaseManagerSingleEngineerPlatoon(platoon) BaseManagerEngineerPatrol(platoon) end end - WaitTicks(Random(51, 113)) + WaitTicks(75) end end @@ -146,20 +340,19 @@ end ---@return boolean function CanConditionalBuild(singleEngineerPlatoon) local aiBrain = singleEngineerPlatoon:GetBrain() - local pData = singleEngineerPlatoon.PlatoonData - local baseName = pData.BaseName + 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 -- If there's a build in progress but the unit is dead, reset the variables. - if bManager.ConditionalBuildData.Unit.Dead then + if bManager.ConditionalBuildData.Unit:BeenDestroyed() then local selectedBuild = bManager.ConditionalBuildTable[bManager.ConditionalBuildData.Index] -- If we're not supposed to retry, then remove from the conditional build list if not selectedBuild.data.Retry then - table.remove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) + TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) end bManager.ConditionalBuildData.Reset() else @@ -174,9 +367,9 @@ function CanConditionalBuild(singleEngineerPlatoon) return false end end - - -- Are there no conditional builds? - if table.empty(bManager.ConditionalBuildTable) then + + -- Are there no conditional builds? + if TableEmpty(bManager.ConditionalBuildTable) then return false end @@ -254,12 +447,12 @@ end ---@param conditionalUnit any function ConditionalBuildDied(conditionalUnit) - local aiBrain = conditionalUnit:GetAIBrain() + local aiBrain = conditionalUnit.Brain local bManager = aiBrain.BaseManagers[conditionalUnit.BaseName] local selectedBuild = conditionalUnit.ConditionalBuild - + -- Reinsert the conditional build (for one of these units) - table.insert(bManager.ConditionalBuildTable, { + TableInsert(bManager.ConditionalBuildTable, { name = selectedBuild.name, data = { MaxAssist = selectedBuild.data.MaxAssist, @@ -273,12 +466,10 @@ function ConditionalBuildDied(conditionalUnit) WaitSecondsAfterDeath = selectedBuild.data.WaitSecondsAfterDeath, }, }) - end ----@param conditionalUnit any function ConditionalBuildSuccessful(conditionalUnit) - local aiBrain = conditionalUnit:GetAIBrain() + local aiBrain = conditionalUnit.Brain local bManager = aiBrain.BaseManagers[conditionalUnit.BaseName] local selectedBuild = bManager.ConditionalBuildTable[bManager.ConditionalBuildData.Index] @@ -289,10 +480,14 @@ function ConditionalBuildSuccessful(conditionalUnit) newPlatoon:SetPlatoonData(selectedBuild.data.PlatoonData) if selectedBuild.data.PlatoonAIFunction then - newPlatoon:ForkAIThread(import(selectedBuild.data.PlatoonAIFunction[1])[selectedBuild.data.PlatoonAIFunction[2]]) + 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 end - - if selectedBuild.data.FormCallbacks then + + if selectedBuild.data.FormCallbacks then for _, callback in selectedBuild.data.FormCallbacks do if type(callback) == "function" then newPlatoon:ForkThread(callback) @@ -323,14 +518,14 @@ function ConditionalBuildSuccessful(conditionalUnit) -- Remove from the conditional build list if were not supposed to build any more if not selectedBuild.data.Amount then - table.remove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) + TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) elseif selectedBuild.data.Amount > 0 then -- Decrement the amount left to build selectedBuild.data.Amount = selectedBuild.data.Amount - 1 -- If none are left to build, remove from the build table if selectedBuild.data.Amount == 0 then - table.remove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) + TableRemove(bManager.ConditionalBuildTable, bManager.ConditionalBuildData.Index) end end @@ -342,8 +537,7 @@ end ---@param singleEngineerPlatoon Platoon function AssistConditionalBuild(singleEngineerPlatoon) local aiBrain = singleEngineerPlatoon:GetBrain() - local pData = singleEngineerPlatoon.PlatoonData - local baseName = pData.BaseName + local baseName = singleEngineerPlatoon.PlatoonData.BaseName local bManager = aiBrain.BaseManagers[baseName] local engineer = singleEngineerPlatoon:GetPlatoonUnits()[1] engineer.BaseName = baseName @@ -363,7 +557,7 @@ function AssistConditionalBuild(singleEngineerPlatoon) -- Super loop while aiBrain:PlatoonExists(singleEngineerPlatoon) do - WaitSeconds(3) + WaitTicks(30) if engineer:IsIdleState() then break @@ -378,8 +572,7 @@ end ---@param singleEngineerPlatoon Platoon function DoConditionalBuild(singleEngineerPlatoon) local aiBrain = singleEngineerPlatoon:GetBrain() - local pData = singleEngineerPlatoon.PlatoonData - local baseName = pData.BaseName + local baseName = singleEngineerPlatoon.PlatoonData.BaseName local bManager = aiBrain.BaseManagers[baseName] local engineer = singleEngineerPlatoon:GetPlatoonUnits()[1] engineer.BaseName = baseName @@ -389,13 +582,15 @@ function DoConditionalBuild(singleEngineerPlatoon) local selectedBuild = bManager.ConditionalBuildTable[buildIndex] -- Get unit plans from the scenario - local unitToBuild + local unitToBuild, unitName if type(selectedBuild.name) == 'table' then - unitToBuild = ScenarioUtils.FindUnit(selectedBuild.name[math.random(1, table.getn(selectedBuild.name))], Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('Unit with name "' .. selectedBuild.name .. '" could not be found for conditional building.') return end + unitName = table.random(selectedBuild.name) + 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 else - unitToBuild = ScenarioUtils.FindUnit(selectedBuild.name, Scenario.Armies[aiBrain.Name].Units) - if not unitToBuild then error('Unit with name "' .. selectedBuild.name .. '" could not be found for conditional building.') return end + unitName = selectedBuild.name + 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 end -- Initialize variables @@ -412,44 +607,17 @@ function DoConditionalBuild(singleEngineerPlatoon) -- Issue build orders IssueToUnitClearCommands(engineer) - local result = aiBrain:BuildStructure(engineer, unitToBuild.type, {unitToBuild.Position[1], unitToBuild.Position[3], 0}) - - -- Enter build monitoring loop - local unitInstance = false - while aiBrain:PlatoonExists(singleEngineerPlatoon) do - if not unitInstance then - unitInstance = engineer.UnitBeingBuilt - if unitInstance then - -- Store the unit - bManager.ConditionalBuildData.Unit = unitInstance - - -- If were 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 - unitInstance.KeepAlive = true - unitInstance.ConditionalBuild = selectedBuild - unitInstance.ConditionalBuildData = bManager.ConditionalBuildData - - -- register rebuild callback - TriggerFile.CreateUnitDeathTrigger(ConditionalBuildDied, unitInstance) - end - - -- Tell the unit the name of this base manager - unitInstance.BaseName = baseName - - -- Set variables so other engineers can see whats going on - bManager.ConditionalBuildData.IsInitiated = false - bManager.ConditionalBuildData.IsBuilding = true + aiBrain:BuildStructure(engineer, unitToBuild.type, {unitToBuild.Position[1], unitToBuild.Position[3], 0}) + engineer.ConditionalBuildUnitName = unitName - -- Register callbacks - TriggerFile.CreateUnitStopBeingBuiltTrigger(ConditionalBuildSuccessful, unitInstance) - end - end - if engineer:IsIdleState() then - break + -- Enter build monitoring loop, the bulk of the data assigning logic is handled in "EngineerOnStartBuild()" + repeat + WaitTicks(30) + if not aiBrain:PlatoonExists(singleEngineerPlatoon) then + return end - WaitTicks(Random(7, 13)) - end + until engineer:IsIdleState() + IssueToUnitClearCommands(engineer) TriggerFile.RemoveUnitTrigger(engineer, ConditionalBuilderDead) end @@ -483,21 +651,21 @@ function BaseManagerPatrolLocationFactoriesAI(platoon) local posTable = {} for _, fac in factories do if not fac.Dead then - table.insert(posTable, fac:GetPosition()) + TableInsert(posTable, fac:GetPosition()) end end platoon:Stop() local i = 1 - while i <= table.getn(posTable) do - local facNum = Random(1, table.getn(posTable)) + while i <= TableGetn(posTable) do + local facNum = Random(1, TableGetn(posTable)) local movePos = posTable[facNum] movePos[3] = movePos[3] + 5 platoon:Patrol(movePos) - table.remove(posTable, facNum) + TableRemove(posTable, facNum) end end @@ -528,7 +696,7 @@ function PermanentFactoryAssist(platoon) 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 + -- 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 @@ -552,7 +720,7 @@ function PermanentFactoryAssist(platoon) -- Add to the list of units that are permanently assisting in this base manager bManager.PermanentAssisters[unit] = true end - WaitTicks(Random(80, 180)) + WaitTicks(150) end end @@ -572,49 +740,39 @@ end function BaseManagerAssistThread(platoon) platoon:Stop() - local platoonUnits = platoon:GetPlatoonUnits() + local unit = platoon:GetPlatoonUnits()[1] local aiBrain = platoon:GetBrain() local bManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] local assistData = platoon.PlatoonData.Assist - local platoonPos = platoon:GetPlatoonPosition() + local platoonPos = platoon:GetPlatoonPosition() local assistee = false local assistingBool = false - local beingBuiltCategories = assistData.BeingBuiltCategories - - if not beingBuiltCategories then - beingBuiltCategories = {'MASSEXTRACTION', 'MASSPRODUCTION', 'ENERGYPRODUCTION', 'FACTORY', 'EXPERIMENTAL', 'DEFENSE', 'MOBILE LAND', 'ALLUNITS' } - end + local beingBuiltCategories = assistData.BeingBuiltCategories or {'MASSEXTRACTION', 'MASSPRODUCTION', 'ENERGYPRODUCTION', 'FACTORY', 'EXPERIMENTAL', 'DEFENSE', 'MOBILE LAND', 'ALLUNITS'} local assistRange = assistData.AssistRange or bManager.Radius local counter = 0 - local unit = platoonUnits[1] - while counter < (assistData.Time or 200) do - - -- If the engineer is assisting a construction unit that is building; break out and do nothing - if not unit:GetGuardedUnit() or - -- Check if the guarding unit is not building - (not unit:GetGuardedUnit():IsUnitState('Building') - -- Check if the base isnt constantly assisting a construction engineer - and not bManager:ConstructionNeedsAssister() - -- check if the unit being guarded is not - and not bManager:IsConstructionUnit(unit:GetGuardedUnit())) then + 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 = table.getn(v:GetGuards()) + local guardNum = TableGetn(v:GetGuards()) if not v.Dead and guardNum < lowNum then currLow = v - lowNum = table.getn(v:GetGuards()) + lowNum = TableGetn(v:GetGuards()) end if guardNum > highNum then highNum = guardNum end end - if unit:GetGuardedUnit() then - if unit:GetGuardedUnit().Dead or EntityCategoryContains(categories.FACTORY, unit:GetGuardedUnit()) or + if GuardedUnit then + if GuardedUnit.Dead or EntityCategoryContains(categories.FACTORY, GuardedUnit) or highNum > lowNum + 1 then assistee = currLow end @@ -661,24 +819,23 @@ function BaseManagerAssistThread(platoon) local guardee = assistee:GetGuardedUnit() if guardee and not guardee.Dead and EntityCategoryContains(categories.FACTORY, guardee) then local factories = AIUtils.AIReturnAssistingFactories(guardee) - table.insert(factories, assistee) - AIUtils.AIEngineersAssistFactories(aiBrain, platoonUnits, factories) + TableInsert(factories, assistee) + AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) assistingBool = true - elseif not table.empty(assistee:GetGuards()) then + elseif not TableEmpty(assistee:GetGuards()) then local factories = AIUtils.AIReturnAssistingFactories(assistee) - table.insert(factories, assistee) - AIUtils.AIEngineersAssistFactories(aiBrain, platoonUnits, factories) + TableInsert(factories, assistee) + AIUtils.AIEngineersAssistFactories(aiBrain, platoon:GetPlatoonUnits(), factories) assistingBool = true end end if assistee and not assistee.Dead then if not assistingBool then platoon:Stop() - IssueGuard(platoonUnits, assistee) + IssueGuard(platoon:GetPlatoonUnits(), assistee) end end end - local waitTime = Random(5, 20) WaitTicks(waitTime) counter = counter + waitTime @@ -690,7 +847,7 @@ end function ExpansionEngineer(platoon) platoon:Stop() - local unitCount = table.getn(platoon:GetPlatoonUnits()) + local unitCount = TableGetn(platoon:GetPlatoonUnits()) local aiBrain = platoon:GetBrain() local data = platoon.PlatoonData @@ -759,7 +916,6 @@ function ExpansionPlatoonDestroyed(brain, platoon) local aiBrain = platoon:GetBrain() local data = platoon.PlatoonData local bManager = aiBrain.BaseManagers[data.BaseName] - local eBaseName = false for num, eData in bManager.ExpansionBaseData do if eData.BaseName == data.ExpansionBase then @@ -793,103 +949,94 @@ function BaseManagerEngineerThread(platoon) platoon:Stop() local aiBrain = platoon:GetBrain() - local platoonUnits = platoon:GetPlatoonUnits() - local eng - - for _, v in platoonUnits do + 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 platoon:GetPlatoonUnits() do if not v.Dead and EntityCategoryContains(categories.CONSTRUCTION, v) then - if not eng then - eng = v + if not Engineer then + Engineer = v else IssueToUnitClearCommands(v) - IssueGuard({v}, eng) + IssueGuard({v}, Engineer) end end end - - if not eng or eng.Dead then + + -- If no Engineer was found, return + if not Engineer or Engineer.Dead then aiBrain:DisbandPlatoon(platoon) return end - -- CHOOSE APPROPRIATE BUILD FUNCTION AND SETUP BUILD VARIABLES - - if not platoon.PlatoonData.BaseName or not aiBrain.BaseManagers[platoon.PlatoonData.BaseName] then - error('*AI DEBUG: Missing Base Name or invalid base name for base manager engineer thread', 2) - end - - -- If there is a construction block use the stuff from here - local buildFunction = BuildBaseManagerStructure + 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' + } - -- BUILD BUILDINGS HERE - if eng.Dead then - aiBrain:DisbandPlatoon(platoon) - end - - local structurePriorities = platoon.PlatoonData.StructurePriorities - if not structurePriorities then - structurePriorities = {'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'} - end - - local retBool, unitName - local nameSet = false - local baseManager = aiBrain.BaseManagers[platoon.PlatoonData.BaseName] - local armyIndex = aiBrain:GetArmyIndex() - for dNum, levelData in baseManager.LevelNames do - if levelData.Priority > 0 then - for _, v in structurePriorities do - local unitType = false - if v ~= 'ALLUNITS' then - unitType = v - end - - repeat - nameSet = false - local markedUnfinished = false - retBool, unitName = buildFunction(aiBrain, eng, aiBrain.BaseManagers[platoon.PlatoonData.BaseName], levelData.Name, unitType, platoon) - if retBool then - repeat - if not nameSet then - WaitSeconds(0.1) - else - WaitSeconds(3) - end - - if not aiBrain:PlatoonExists(platoon) then - return - end - - if not markedUnfinished and eng.UnitBeingBuilt then - baseManager.UnfinishedBuildings[unitName] = true - end - - if not nameSet then - local buildingUnit = eng.UnitBeingBuilt - if unitName and buildingUnit and not buildingUnit.Dead then - nameSet = true - local armyIndex = aiBrain:GetArmyIndex() - if ScenarioInfo.UnitNames[armyIndex] and EntityCategoryContains(categories.STRUCTURE, buildingUnit) then - ScenarioInfo.UnitNames[armyIndex][unitName] = buildingUnit - end - buildingUnit.UnitName = unitName - end - end - until eng.Dead or eng:IsIdleState() - if not eng.Dead then - baseManager.UnfinishedBuildings[unitName] = nil - baseManager:DecrementUnitBuildCounter(unitName) - end - end - until not retBool + 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 - end - end - local tempPos = aiBrain.BaseManagers[platoon.PlatoonData.BaseName]:GetPosition() - platoon:MoveToLocation(tempPos, false) + 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(aiBrain.BaseManagers[platoon.PlatoonData.BaseName]:GetPosition(), false) end --- Guts of the build thing @@ -905,7 +1052,7 @@ function BuildBaseManagerStructure(aiBrain, eng, baseManager, levelName, buildin local buildTemplate = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].Template local buildList = aiBrain.BaseTemplates[baseManager.BaseName .. levelName].List - if not buildTemplate or not buildList then + if not buildTemplate or not buildList or not aiBrain:PlatoonExists(platoon) then return false end @@ -1020,7 +1167,7 @@ function PlatoonSetTargetPriorities(platoon) else local priList = {} for k, v in platoon.PlatoonData.TargetPriorities do - table.insert(priList, ParseEntityCategory(v)) + TableInsert(priList, ParseEntityCategory(v)) end local squads = { 'attack', 'support', 'scout', 'artillery' } @@ -1075,7 +1222,7 @@ function GetScoutingPath(bManager, unit) if currY == mapInfo[4] then useY = currY - 8 end - table.insert(possiblePoints, { useX, 0, useY }) + TableInsert(possiblePoints, { useX, 0, useY }) currY = currY + 48 end currX = currX + 48 @@ -1083,7 +1230,7 @@ function GetScoutingPath(bManager, unit) -- Determine which poitnts the unit can actually path to for k, v in possiblePoints do if AIUtils.CheckUnitPathingEx(v, unit:GetPosition(), unit) then - table.insert(pathablePoints, v) + TableInsert(pathablePoints, v) end end end @@ -1100,7 +1247,7 @@ function BaseManagerScoutingAI(platoon) while aiBrain:PlatoonExists(platoon) do -- Get new points every time so if the area changes we are on it local pathablePoints = GetScoutingPath(bManager, unit) - local numPoints = table.getn(pathablePoints) + local numPoints = TableGetn(pathablePoints) if numPoints > 0 then platoon:Stop() end @@ -1150,6 +1297,7 @@ function BaseManagerTMLAI(platoon) platoon:Stop() local maxRadius = unit.Blueprint.Weapon[1].MaxRadius + local unitPosition = unit:GetPosition() local simpleTargetting = true if ScenarioInfo.Options.Difficulty == 3 then @@ -1163,27 +1311,25 @@ function BaseManagerTMLAI(platoon) categories.EXPERIMENTAL, categories.ENERGYPRODUCTION, categories.STRUCTURE, - categories.TECH3 * categories.MOBILE} + categories.TECH3 * categories.MOBILE, + categories.TECH2 * categories.MOBILE + } ) while aiBrain:PlatoonExists(platoon) do if BMBC.TMLsEnabled(aiBrain, baseName) then local target = false while unit:GetTacticalSiloAmmoCount() < 1 or not target do - WaitSeconds(5) - target = false - while not target do - target = platoon:FindPrioritizedUnit('Attack', 'Enemy', true, unit:GetPosition(), maxRadius) - - if target then - break - end - - WaitSeconds(5) - - if not aiBrain:PlatoonExists(platoon) then - return - end + target = target or platoon:FindPrioritizedUnit('Attack', 'Enemy', true, unitPosition, maxRadius) + + if target and unit:GetTacticalSiloAmmoCount() >= 1 then + break + end + + WaitSeconds(5) + + if not aiBrain:PlatoonExists(platoon) then + return end end if not target.Dead then @@ -1260,24 +1406,29 @@ function AMUnlockBuildTimer(platoon) ForkThread(AMPlatoonHelperFunctions.UnlockTimer, platoon.PlatoonData.LockTimer, platoon.PlatoonData.PlatoonName) end +---- Utility function that unlocks the AttackManager platoon to be built again if the remaining units in it reach the specified Ratio +--- 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 end end + platoon.MaxUnits = count platoon.LivingUnits = count platoon.Locked = true + 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 - end - end + 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 + end + end + for _, v in platoon:GetPlatoonUnits() do if not v.Dead then v.PlatoonHandle = platoon @@ -1286,24 +1437,28 @@ function AMUnlockRatio(platoon) end end +--- Utility function that unlocks the AttackManager platoon to be built again if the remaining units in it reach the specified Ratio, with an added delay +--- 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 - for _, v in platoon:GetPlatoonUnits() do + local count = 0 + for k, v in platoon:GetPlatoonUnits() do if not v.Dead then count = count + 1 end end + platoon.MaxUnits = count platoon.LivingUnits = count platoon.Locked = true local callback = function(unit) - platoon.LivingUnits = platoon.LivingUnits - 1 - if platoon.Locked and platoon.PlatoonData.Ratio > (platoon.LivingUnits / platoon.MaxUnits) then - ForkThread(AMPlatoonHelperFunctions.UnlockTimer, platoon.PlatoonData.LockTimer, platoon.PlatoonData.PlatoonName) - platoon.Locked = false - end - end + platoon.LivingUnits = platoon.LivingUnits - 1 + if platoon.Locked and platoon.PlatoonData.Ratio > (platoon.LivingUnits / platoon.MaxUnits) then + ForkThread(AMPlatoonHelperFunctions.UnlockTimer, platoon.PlatoonData.LockTimer, platoon.PlatoonData.PlatoonName) + platoon.Locked = false + end + end + for _, v in platoon:GetPlatoonUnits() do if not v.Dead then v.PlatoonHandle = platoon diff --git a/lua/aibrains/campaign-ai.lua b/lua/aibrains/campaign-ai.lua index 17619ebbb6..62a4029c66 100644 --- a/lua/aibrains/campaign-ai.lua +++ b/lua/aibrains/campaign-ai.lua @@ -6,6 +6,10 @@ local AIBuildUnits = import("/lua/ai/aibuildunits.lua") -- upvalue scope for performance local TableGetn = table.getn +local TableInsert = table.insert +local TableRandom = table.random +local TableEmpty = table.empty +local TableRemove = table.remove local StandardBrain = import("/lua/aibrain.lua").AIBrain @@ -176,7 +180,7 @@ AIBrain = Class(StandardBrain) { if plan then return plan.EvaluatePlan(self) else - LOG('*WARNING: TRIED TO IMPORT PLAN NAME ', tostring(planName), ' BUT IT ERRORED OUT IN THE AI BRAIN.') + WARN('*WARNING: TRIED TO IMPORT PLAN NAME: ' .. tostring(planName) .. ', BUT IT ERRORED OUT IN THE AI BRAIN.') return 0 end end, @@ -205,7 +209,7 @@ AIBrain = Class(StandardBrain) { self.CurrentPlan = bestPlan end if not self.CurrentPlan then - error('*AI ERROR: Invalid plan list for army - '..self.Name, 2) + error('*AI ERROR: Invalid plan list for army - ' .. self.Name, 2) end end, @@ -255,10 +259,9 @@ AIBrain = Class(StandardBrain) { self.AttackManager:PauseAttackManager() end, - ---## AI PLATOON MANAGEMENT - ---### New PlatoonBuildManager - ---This system is meant to be able to give some data about the platoon you want and have them - ---built and formed into platoons at will. + --- AI PLATOON MANAGEMENT + --- SC1's PlatoonBuildManager, used as its base AI even for skirmish, and also for FA's campaign + --- This system is meant to be able to give some data about the platoon you want and have them built and formed into platoons at will. ---@param self CampaignAIBrain InitializePlatoonBuildManager = function(self) if not self.PBM then @@ -272,13 +275,13 @@ AIBrain = Class(StandardBrain) { Gate = {}, }, Locations = { - -- { - -- Location, - -- Radius, - -- LocType, ('MAIN', 'EXPANSION') - -- PrimaryFactories = {Air = X, Land = Y, Sea = Z} - -- UseCenterPoint, - Bool - --} + --[[{ + Location, + Radius, + LocType, ('MAIN', 'EXPANSION') + PrimaryFactories = {Air = X, Land = Y, Sea = Z} + UseCenterPoint, - Bool + }]] }, PlatoonTypes = {'Air', 'Land', 'Sea', 'Gate'}, NeedSort = { @@ -301,6 +304,10 @@ AIBrain = Class(StandardBrain) { end self.HasPlatoonList = false self:PBMSetEnabled(true) + + -- Create the global builder table where most of the builder data will be stored, I have no idea why GPG didn't define it here to begin with + -- They defined it in self:PBMAddPlatoon() for whatever reason + ScenarioInfo.BuilderTable[self.CurrentPlan] = {Air = {}, Sea = {}, Land = {}, Gate = {}} end end, @@ -346,8 +353,7 @@ AIBrain = Class(StandardBrain) { ---@param pltnTable PlatoonTable PBMAddPlatoon = function(self, pltnTable) if not pltnTable.PlatoonTemplate then - local stng = '*AI ERROR: INVALID PLATOON LIST IN '.. self.CurrentPlan.. ' - MISSING TEMPLATE. ' - error(stng, 1) + error('*AI ERROR: INVALID PLATOON LIST IN '.. self.CurrentPlan.. ' - MISSING TEMPLATE', 1) return end @@ -386,15 +392,15 @@ AIBrain = Class(StandardBrain) { end local insertTable = {BuilderName = pltnTable.BuilderName, PlatoonHandles = {}, Priority = pltnTable.Priority, LocationType = pltnTable.LocationType, PlatoonTemplate = pltnTable.PlatoonTemplate} for i = 1, num do - table.insert(insertTable.PlatoonHandles, false) + TableInsert(insertTable.PlatoonHandles, false) end - table.insert(self.PBM.Platoons[pltnTable.PlatoonType], insertTable) + TableInsert(self.PBM.Platoons[pltnTable.PlatoonType], insertTable) self.PBM.NeedSort[pltnTable.PlatoonType] = true else local insertTable = {BuilderName = pltnTable.BuilderName, PlatoonHandles = {}, Priority = pltnTable.Priority, LocationType = pltnTable.LocationType, PlatoonTemplate = pltnTable.PlatoonTemplate} for i = 1, num do - table.insert(insertTable.PlatoonHandles, false) + TableInsert(insertTable.PlatoonHandles, false) end local types = {'Air', 'Land', 'Sea'} for num, pType in types do @@ -403,7 +409,7 @@ AIBrain = Class(StandardBrain) { elseif not pltnTable.Inserted then error('AI DEBUG: BUILDER DUPLICATE NAME FOUND - ' .. pltnTable.BuilderName, 2) end - table.insert(self.PBM.Platoons[pType], insertTable) + TableInsert(self.PBM.Platoons[pType], insertTable) self.PBM.NeedSort[pType] = true end end @@ -488,18 +494,18 @@ AIBrain = Class(StandardBrain) { local gates = {} for ek, ev in factories do if EntityCategoryContains(categories.FACTORY * categories.AIR - categories.EXTERNALFACTORYUNIT, ev) and self:PBMFactoryLocationCheck(ev, v) then - table.insert(airFactories, ev) + TableInsert(airFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.LAND - categories.EXTERNALFACTORYUNIT, ev) and self:PBMFactoryLocationCheck(ev, v) then - table.insert(landFactories, ev) + TableInsert(landFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.NAVAL - categories.EXTERNALFACTORYUNIT, ev) and self:PBMFactoryLocationCheck(ev, v) then - table.insert(seaFactories, ev) + TableInsert(seaFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.GATE - categories.EXTERNALFACTORYUNIT, ev) and self:PBMFactoryLocationCheck(ev, v) then - table.insert(gates, ev) + TableInsert(gates, ev) end end local afac, lfac, sfac, gatefac - if not table.empty(airFactories) then + if not TableEmpty(airFactories) then if not v.PrimaryFactories.Air or v.PrimaryFactories.Air.Dead or v.PrimaryFactories.Air:IsUnitState('Upgrading') or self:PBMCheckHighestTechFactory(airFactories, v.PrimaryFactories.Air) then @@ -509,7 +515,7 @@ AIBrain = Class(StandardBrain) { self:PBMAssistGivenFactory(airFactories, v.PrimaryFactories.Air) end - if not table.empty(landFactories) then + if not TableEmpty(landFactories) then if not v.PrimaryFactories.Land or v.PrimaryFactories.Land.Dead or v.PrimaryFactories.Land:IsUnitState('Upgrading') or self:PBMCheckHighestTechFactory(landFactories, v.PrimaryFactories.Land) then @@ -519,7 +525,7 @@ AIBrain = Class(StandardBrain) { self:PBMAssistGivenFactory(landFactories, v.PrimaryFactories.Land) end - if not table.empty(seaFactories) then + if not TableEmpty(seaFactories) then if not v.PrimaryFactories.Sea or v.PrimaryFactories.Sea.Dead or v.PrimaryFactories.Sea:IsUnitState('Upgrading') or self:PBMCheckHighestTechFactory(seaFactories, v.PrimaryFactories.Sea) then @@ -529,7 +535,7 @@ AIBrain = Class(StandardBrain) { self:PBMAssistGivenFactory(seaFactories, v.PrimaryFactories.Sea) end - if not table.empty(gates) then + if not TableEmpty(gates) then if not v.PrimaryFactories.Gate or v.PrimaryFactories.Gate.Dead then gatefac = self:PBMGetPrimaryFactory(gates) v.PrimaryFactories.Gate = gatefac @@ -537,7 +543,7 @@ AIBrain = Class(StandardBrain) { self:PBMAssistGivenFactory(gates, v.PrimaryFactories.Gate) end - if not v.RallyPoint or table.empty(v.RallyPoint) then + if not v.RallyPoint or TableEmpty(v.RallyPoint) then self:PBMSetRallyPoint(airFactories, v, nil) self:PBMSetRallyPoint(landFactories, v, nil) self:PBMSetRallyPoint(seaFactories, v, nil, "Naval Rally Point") @@ -568,7 +574,7 @@ AIBrain = Class(StandardBrain) { ---@param markerType string ---@return boolean PBMSetRallyPoint = function(self, factories, location, rallyLoc, markerType) - if not table.empty(factories) then + if not TableEmpty(factories) then local rally local position = factories[1]:GetPosition() for facNum, facData in factories do @@ -794,7 +800,7 @@ AIBrain = Class(StandardBrain) { end if not found then - table.insert(self.PBM.Locations, spec) + TableInsert(self.PBM.Locations, spec) else error('*AI ERROR: Attempting to add a build location with a duplicate name: '..spec.LocationType, 2) return @@ -832,8 +838,6 @@ AIBrain = Class(StandardBrain) { return {v.Location[1], height, v.Location[3]} end end - elseif self.BuilderManagers[loc] then - return self.BuilderManagers[loc].FactoryManager:GetLocationCoords() end end, @@ -850,8 +854,6 @@ AIBrain = Class(StandardBrain) { return v.Radius end end - elseif self.BuilderManagers[loc] then - return self.BuilderManagers[loc].FactoryManager.Radius end return false end, @@ -882,11 +884,11 @@ AIBrain = Class(StandardBrain) { if loc.LocationType == location then local facs = {} for k, v in loc.PrimaryFactories do - table.insert(facs, v) + TableInsert(facs, v) if not v.Dead then for fNum, fac in v:GetGuards() do if EntityCategoryContains(categories.FACTORY, fac) then - table.insert(facs, fac) + TableInsert(facs, fac) end end end @@ -906,7 +908,7 @@ AIBrain = Class(StandardBrain) { PBMRemoveBuildLocation = function(self, loc, locType) for k, v in self.PBM.Locations do if (loc and v.Location == loc) or (locType and v.LocationType == locType) then - table.remove(self.PBM.Locations, k) + TableRemove(self.PBM.Locations, k) end end end, @@ -917,8 +919,7 @@ AIBrain = Class(StandardBrain) { ---@return boolean PBMSortPlatoonsViaPriority = function(self, platoonType) if platoonType ~= 'Air' and platoonType ~= 'Land' and platoonType ~= 'Sea' and platoonType ~= 'Gate' then - local strng = '*AI ERROR: TRYING TO SORT PLATOONS VIA PRIORITY BUT AN INVALID TYPE ('.. tostring(platoonType) .. ') WAS PASSED IN.' - error(strng, 2) + error('*AI ERROR: TRYING TO SORT PLATOONS VIA PRIORITY BUT AN INVALID TYPE (' .. tostring(platoonType) .. ') WAS PASSED IN.', 2) return false end local sortedList = {} @@ -934,7 +935,7 @@ AIBrain = Class(StandardBrain) { end end sortedList[i] = value - table.remove(self.PBM.Platoons[platoonType], key) + TableRemove(self.PBM.Platoons[platoonType], key) end self.PBM.Platoons[platoonType] = sortedList self.PBM.NeedSort[platoonType] = false @@ -969,7 +970,7 @@ AIBrain = Class(StandardBrain) { for _, v in poolPlat:GetPlatoonUnits() do if not v.Dead and EntityCategoryContains(categories.FACTORY - categories.MOBILE, v) then if v:IsUnitState('Building') or v:IsUnitState('Upgrading') then - table.insert(poolTransfer, v) + TableInsert(poolTransfer, v) end end end @@ -977,7 +978,7 @@ AIBrain = Class(StandardBrain) { local busyTransfer = {} for _, v in busyPlat:GetPlatoonUnits() do if not v.Dead and not v:IsUnitState('Building') and not v:IsUnitState('Upgrading') then - table.insert(busyTransfer, v) + TableInsert(busyTransfer, v) end end @@ -1127,7 +1128,7 @@ AIBrain = Class(StandardBrain) { end, - -- Main building and forming platoon thread for the Platoon Build Manager + --- Main building and forming platoon thread for the Platoon Build Manager ---@param self CampaignAIBrain PlatoonBuildManagerThread = function(self) local personality = self:GetPersonality() @@ -1144,14 +1145,14 @@ AIBrain = Class(StandardBrain) { self:PBMSetPrimaryFactories() end local platoonList = self.PBM.Platoons - -- clear the cache so we can get fresh new responses! + -- Clear the cache so we can get fresh new responses! self:PBMClearBuildConditionsCache() -- Go through the different types of platoons for typek, typev in self.PBM.PlatoonTypes do -- First go through the list of locations and see if we can build stuff there. for k, v in self.PBM.Locations do -- See if we have platoons to build in that type - if not table.empty(platoonList[typev]) then + if not TableEmpty(platoonList[typev]) then -- Sort the list of platoons via priority if self.PBM.NeedSort[typev] then self:PBMSortPlatoonsViaPriority(typev) @@ -1163,11 +1164,11 @@ AIBrain = Class(StandardBrain) { if v.PrimaryFactories[typev] then local priFac = v.PrimaryFactories[typev] local numBuildOrders = nil - if priFac and not priFac.Dead then + if not priFac.Dead then numBuildOrders = priFac:GetNumBuildOrders(categories.ALLUNITS) if numBuildOrders == 0 then local guards = priFac:GetGuards() - if guards and not table.empty(guards) then + if guards and not TableEmpty(guards) then for kg, vg in guards do numBuildOrders = numBuildOrders + vg:GetNumBuildOrders(categories.ALLUNITS) if numBuildOrders == 0 and vg:IsUnitState('Building') then @@ -1190,38 +1191,37 @@ AIBrain = Class(StandardBrain) { local globalBuilder = ScenarioInfo.BuilderTable[self.CurrentPlan][typev][vp.BuilderName] if priorityLevel and (vp.Priority ~= priorityLevel or not self.PBM.RandomSamePriority) then break - elseif (not priorityLevel or priorityLevel == vp.Priority) - and vp.Priority > 0 and globalBuilder.RequiresConstruction and + elseif (not priorityLevel or priorityLevel == vp.Priority) and vp.Priority > 0 and globalBuilder.RequiresConstruction -- The location we're looking at is an allowed location - (vp.LocationType == v.LocationType or not vp.LocationType) and + and (vp.LocationType == v.LocationType or not vp.LocationType) -- Make sure there is a handle slot available - (self:PBMHandleAvailable(vp)) then + and (self:PBMHandleAvailable(vp)) then -- Fix up the primary factories to fit the proper table required by CanBuildPlatoon local suggestedFactories = {v.PrimaryFactories[typev]} local factories = self:CanBuildPlatoon(vp.PlatoonTemplate, suggestedFactories) if factories and self:PBMCheckBuildConditions(globalBuilder.BuildConditions, armyIndex) then priorityLevel = vp.Priority for i = 1, self:PBMNumHandlesAvailable(vp) do - table.insert(possibleTemplates, {Builder = vp, Index = kp, Global = globalBuilder}) + TableInsert(possibleTemplates, {Builder = vp, Index = kp, Global = globalBuilder}) end end end end if priorityLevel then - local builderData = possibleTemplates[ Random(1, TableGetn(possibleTemplates)) ] + local builderData = TableRandom(possibleTemplates) local vp = builderData.Builder local kp = builderData.Index local globalBuilder = builderData.Global local suggestedFactories = {v.PrimaryFactories[typev]} local factories = self:CanBuildPlatoon(vp.PlatoonTemplate, suggestedFactories) vp.BuildTemplate = self:PBMBuildNumFactories(vp.PlatoonTemplate, v, typev, factories) - local template = vp.BuildTemplate - local factionIndex = self:GetFactionIndex() + local template = vp.BuildTemplate + -- Check all the requirements to build the platoon -- The Primary Factory can actually build this platoon -- The platoon build condition has been met - local ptnSize = personality:GetPlatoonSize() - -- Finally, build the platoon. + local ptnSize = personality:GetPlatoonSize() + -- Finally, build the platoon. self:BuildPlatoon(template, factories, ptnSize) self:PBMSetHandleBuilding(self.PBM.Platoons[typev][kp]) if globalBuilder.GenerateTimeOut then @@ -1240,14 +1240,14 @@ AIBrain = Class(StandardBrain) { end end end - WaitSeconds(.1) + WaitTicks(1) end - -- Do it all over again in 13 seconds. - WaitSeconds(self.PBM.BuildCheckInterval or 13) + -- Do it all over again in 10 seconds. + WaitSeconds(self.PBM.BuildCheckInterval or 10) end end, - --- ## Form platoons + --- Form platoons --- Extracted as it's own function so you can call this to try and form platoons to clean up the pool ---@param self CampaignAIBrain ---@param requireBuilding boolean `true` = platoon must have `'BUILDING'` has its handle, `false` = it'll form any platoon it can @@ -1262,7 +1262,7 @@ AIBrain = Class(StandardBrain) { numBuildOrders = location.PrimaryFactories[platoonType]:GetNumBuildOrders(categories.ALLUNITS) if numBuildOrders == 0 then local guards = location.PrimaryFactories[platoonType]:GetGuards() - if guards and not table.empty(guards) then + if guards and not TableEmpty(guards) then for kg, vg in guards do numBuildOrders = numBuildOrders + vg:GetNumBuildOrders(categories.ALLUNITS) if numBuildOrders == 0 and vg:IsUnitState('Building') then @@ -1283,45 +1283,42 @@ AIBrain = Class(StandardBrain) { -- or The platoon doesn't have a handle and either doesn't require to be building state or doesn't require construction -- all that and passes it's build condition function. if vp.Priority > 0 and (requireBuilding and self:PBMCheckHandleBuilding(vp) - and numBuildOrders and numBuildOrders == 0 - and (not vp.LocationType or vp.LocationType == location.LocationType)) + and numBuildOrders and numBuildOrders == 0 + and (not vp.LocationType or vp.LocationType == location.LocationType)) or (((self:PBMHandleAvailable(vp)) and (not requireBuilding or not globalBuilder.RequiresConstruction)) - and (not vp.LocationType or vp.LocationType == location.LocationType) - and self:PBMCheckBuildConditions(globalBuilder.BuildConditions, armyIndex)) then + and (not vp.LocationType or vp.LocationType == location.LocationType) + and self:PBMCheckBuildConditions(globalBuilder.BuildConditions, armyIndex)) then local poolPlatoon = self:GetPlatoonUniquelyNamed('ArmyPool') + local ptnSize = personality:GetPlatoonSize() local formIt = false - local template = vp.BuildTemplate - if not template then - template = vp.PlatoonTemplate - end + local template = vp.BuildTemplate or vp.PlatoonTemplate local flipTable = {} local squadNum = 3 - while squadNum <= table.getn(template) do + while squadNum <= TableGetn(template) do if template[squadNum][2] < 0 then - table.insert(flipTable, {Squad = squadNum, Value = template[squadNum][2]}) + TableInsert(flipTable, {Squad = squadNum, Value = template[squadNum][2]}) template[squadNum][2] = 1 end squadNum = squadNum + 1 end - + if location.Location and location.Radius and vp.LocationType then - formIt = poolPlatoon:CanFormPlatoon(template, personality:GetPlatoonSize(), location.Location, location.Radius) + formIt = poolPlatoon:CanFormPlatoon(template, ptnSize, location.Location, location.Radius) elseif not vp.LocationType then - formIt = poolPlatoon:CanFormPlatoon(template, personality:GetPlatoonSize()) + formIt = poolPlatoon:CanFormPlatoon(template, ptnSize) end - local ptnSize = personality:GetPlatoonSize() if formIt then local hndl if location.Location and location.Radius and vp.LocationType then - hndl = poolPlatoon:FormPlatoon(template, personality:GetPlatoonSize(), location.Location, location.Radius) + hndl = poolPlatoon:FormPlatoon(template, ptnSize, location.Location, location.Radius) self:PBMStoreHandle(hndl, vp) if vp.PlatoonTimeOutThread then vp.PlatoonTimeOutThread:Destroy() end elseif not vp.LocationType then - hndl = poolPlatoon:FormPlatoon(template, personality:GetPlatoonSize()) + hndl = poolPlatoon:FormPlatoon(template, ptnSize) self:PBMStoreHandle(hndl, vp) if vp.PlatoonTimeOutThread then vp.PlatoonTimeOutThread:Destroy() @@ -1330,29 +1327,33 @@ AIBrain = Class(StandardBrain) { hndl.PlanName = template[2] -- If we have specific AI, fork that AI thread - local pltn = self.PBM.Platoons[platoonType][kp] if globalBuilder.PlatoonAIFunction then hndl:StopAI() hndl:ForkAIThread(import(globalBuilder.PlatoonAIFunction[1])[globalBuilder.PlatoonAIFunction[2]]) end - + + -- If we have an AI from "platoon.lua", use that if globalBuilder.PlatoonAIPlan then hndl:SetAIPlan(globalBuilder.PlatoonAIPlan) end -- If we have additional threads to fork on the platoon, do that as well. + -- Note: These are platoon AI functions from "platoon.lua" if globalBuilder.PlatoonAddPlans then for papk, papv in globalBuilder.PlatoonAddPlans do hndl:ForkThread(hndl[papv]) end end - + + -- If we have additional functions to fork on the platoon, do that as well if globalBuilder.PlatoonAddFunctions then for pafk, pafv in globalBuilder.PlatoonAddFunctions do hndl:ForkThread(import(pafv[1])[pafv[2]]) end end - + + -- If we have additional behaviours to fork on the platoon, do that as well + -- Note: These are platoon AI functions from "AIBehaviors.lua" if globalBuilder.PlatoonAddBehaviors then for pafk, pafv in globalBuilder.PlatoonAddBehaviors do hndl:ForkThread(Behaviors[pafv]) @@ -1366,16 +1367,16 @@ AIBrain = Class(StandardBrain) { self.PlatoonNameCounter[vp.BuilderName] = 1 end end - + hndl:AddDestroyCallback(self.PBMPlatoonDestroyed) hndl.BuilderName = vp.BuilderName + + -- Set the platoon data + -- Also set the platoon to be part of the attack force if specified in the platoon data, used for AttackManager platoon forming if globalBuilder.PlatoonData then hndl:SetPlatoonData(globalBuilder.PlatoonData) - if globalBuilder.PlatoonData.AMPlatoons then - for _, v in globalBuilder.PlatoonData.AMPlatoons do - hndl:SetPartOfAttackForce() - break - end + if globalBuilder.PlatoonData.AMPlatoons and not TableEmpty(globalBuilder.PlatoonData.AMPlatoons) then + hndl:SetPartOfAttackForce() end end end @@ -1418,7 +1419,7 @@ AIBrain = Class(StandardBrain) { PBMBuildNumFactories = function (self, template, location, pType, factory) local retTemplate = table.deepcopy(template) local assistFacs = factory[1]:GetGuards() - table.insert(assistFacs, factory[1]) + TableInsert(assistFacs, factory[1]) local facs = {T1 = 0, T2 = 0, T3 = 0} for _, v in assistFacs do if EntityCategoryContains(categories.TECH3 * categories.FACTORY, v) then @@ -1432,7 +1433,7 @@ AIBrain = Class(StandardBrain) { -- Handle any squads with a specified build quantity local squad = 3 - while squad <= table.getn(retTemplate) do + while squad <= TableGetn(retTemplate) do if retTemplate[squad][2] > 0 then local bp = self:GetUnitBlueprint(retTemplate[squad][1]) local buildLevel = AIBuildUnits.UnitBuildCheck(bp) @@ -1458,16 +1459,16 @@ AIBrain = Class(StandardBrain) { -- Handle squads with programatic build quantity squad = 3 local remainingIds = {T1 = {}, T2 = {}, T3 = {}} - while squad <= table.getn(retTemplate) do + while squad <= TableGetn(retTemplate) do if retTemplate[squad][2] < 0 then - table.insert(remainingIds['T'..AIBuildUnits.UnitBuildCheck(self:GetUnitBlueprint(retTemplate[squad][1])) ], retTemplate[squad][1]) + TableInsert(remainingIds['T'..AIBuildUnits.UnitBuildCheck(self:GetUnitBlueprint(retTemplate[squad][1])) ], retTemplate[squad][1]) end squad = squad + 1 end local rTechLevel = 3 while rTechLevel >= 1 do for num, unitId in remainingIds['T'..rTechLevel] do - for tempRow = 3, table.getn(retTemplate) do + for tempRow = 3, TableGetn(retTemplate) do if retTemplate[tempRow][1] == unitId and retTemplate[tempRow][2] < 0 then retTemplate[tempRow][3] = 0 for fTechLevel = rTechLevel, 3 do @@ -1481,10 +1482,10 @@ AIBrain = Class(StandardBrain) { end -- Remove any IDs with 0 as a build quantity. - for i = 1, table.getn(retTemplate) do + for i = 1, TableGetn(retTemplate) do if i >= 3 then if retTemplate[i][3] == 0 then - table.remove(retTemplate, i) + TableRemove(retTemplate, i) end end end @@ -1544,13 +1545,13 @@ AIBrain = Class(StandardBrain) { local numFactories = 0 for ek, ev in factories do if EntityCategoryContains(categories.FACTORY * categories.AIR, ev) then - table.insert(airFactories, ev) + TableInsert(airFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.LAND, ev) then - table.insert(landFactories, ev) + TableInsert(landFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.NAVAL, ev) then - table.insert(seaFactories, ev) + TableInsert(seaFactories, ev) elseif EntityCategoryContains(categories.FACTORY * categories.GATE, ev) then - table.insert(gates, ev) + TableInsert(gates, ev) end end @@ -1611,7 +1612,7 @@ AIBrain = Class(StandardBrain) { if not v.LookupNumber[index] then local found = false if v[3][1] == "default_brain" then - table.remove(v[3], 1) + TableRemove(v[3], 1) end for num, bcData in self.PBM.BuildConditionsTable do @@ -1637,7 +1638,7 @@ AIBrain = Class(StandardBrain) { if not v.LookupNumber then v.LookupNumber = {} end - table.insert(self.PBM.BuildConditionsTable, v) + TableInsert(self.PBM.BuildConditionsTable, v) v.LookupNumber[index] = TableGetn(self.PBM.BuildConditionsTable) end end diff --git a/lua/editor/AMPlatoonHelperFunctions.lua b/lua/editor/AMPlatoonHelperFunctions.lua index bc432e5f4c..6f4d175983 100644 --- a/lua/editor/AMPlatoonHelperFunctions.lua +++ b/lua/editor/AMPlatoonHelperFunctions.lua @@ -6,8 +6,6 @@ ---------------------------------------------------------------------- local ScenarioFramework = import('/lua/scenarioframework.lua') --- === utility function === -- - ---@param time number ---@param name string function UnlockTimer(time, name) @@ -95,18 +93,10 @@ end ---@return boolean function ChildCountDifficulty(aiBrain, master) local counter = ScenarioFramework.AMPlatoonCounter(aiBrain, master) - local d1Num = ScenarioInfo.OSPlatoonCounter[master..'_D1'] or 1 - local d2Num = ScenarioInfo.OSPlatoonCounter[master..'_D2'] or 2 - local d3Num = ScenarioInfo.OSPlatoonCounter[master..'_D3'] or 2 - if not ScenarioInfo.Options.Difficulty or ScenarioInfo.Options.Difficulty == 1 and counter < d1Num then - return true - elseif ScenarioInfo.Options.Difficulty == 2 and counter < d2Num then - return true - elseif ScenarioInfo.Options.Difficulty == 3 and counter < d3Num then - return true - else - return false - end + local difficulty = ScenarioInfo.Options.Difficulty or 3 + local number = ScenarioInfo.OSPlatoonCounter[master..'_D'..difficulty] or difficulty + + return counter < number end --- MasterCountDifficulty = BuildCondition @@ -115,18 +105,10 @@ end ---@return boolean function MasterCountDifficulty(aiBrain, master) local counter = ScenarioFramework.AMPlatoonCounter(aiBrain, master) - local d1Num = ScenarioInfo.OSPlatoonCounter[master..'_D1'] or 1 - local d2Num = ScenarioInfo.OSPlatoonCounter[master..'_D2'] or 2 - local d3Num = ScenarioInfo.OSPlatoonCounter[master..'_D3'] or 2 - if not ScenarioInfo.Options.Difficulty or ScenarioInfo.Options.Difficulty == 1 and counter >= d1Num then - return true - elseif ScenarioInfo.Options.Difficulty == 2 and counter >= d2Num then - return true - elseif ScenarioInfo.Options.Difficulty == 3 and counter >= d3Num then - return true - else - return false - end + local difficulty = ScenarioInfo.Options.Difficulty or 3 + local number = ScenarioInfo.OSPlatoonCounter[master..'_D'..difficulty] or difficulty + + return counter >= number end -- Unused Files but moved for Mod Support diff --git a/lua/editor/BaseManagerBuildConditions.lua b/lua/editor/BaseManagerBuildConditions.lua index d5184eab57..7dad48eaab 100644 --- a/lua/editor/BaseManagerBuildConditions.lua +++ b/lua/editor/BaseManagerBuildConditions.lua @@ -94,14 +94,8 @@ end ---@param baseName string ---@return boolean function BaseManagerNeedsEngineers(aiBrain, baseName) - if not aiBrain.BaseManagers[baseName] then - return false - end - local bManager = aiBrain.BaseManagers[baseName] - if bManager.EngineerQuantity > bManager.CurrentEngineerCount then - return true - end - return false + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.EngineerQuantity > bManager.CurrentEngineerCount end ---@param aiBrain AIBrain @@ -291,7 +285,7 @@ function CategoriesBeingBuilt(aiBrain, baseName, catTable) return false end ----@param aiBrain ArmiesTable +--@param aiBrain ArmiesTable ---@param level number ---@param baseName string ---@return boolean @@ -304,17 +298,9 @@ function HighestFactoryLevel(aiBrain, level, baseName) 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 - if level == 3 then - return true - else - return false - end + return level == 3 elseif t2FacList and not table.empty(t2FacList) then - if level == 2 then - return true - else - return false - end + return level == 2 end return true end @@ -368,18 +354,16 @@ end ---@return boolean function UnfinishedBuildingsCheck(aiBrain, baseName) local bManager = aiBrain.BaseManagers[baseName] - if not bManager then - return false - end - -- Return out if the list is empty or all buildings are finished - if 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 table.empty(bManager.UnfinishedBuildings) then return false end -- Check list - local armyIndex = bManager.AIBrain:GetArmyIndex() + local armyIndex = aiBrain:GetArmyIndex() local beingBuiltList = {} - local buildingEngs = bManager.AIBrain:GetListOfUnits(categories.ENGINEER, false) + local buildingEngs = aiBrain:GetListOfUnits(categories.ENGINEER, false) for _, v in buildingEngs do local buildingUnit = v.UnitBeingBuilt if buildingUnit and buildingUnit.UnitName then @@ -420,17 +404,9 @@ function HighestFactoryLevelType(aiBrain, level, baseName, type) 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 - if level == 3 then - return true - else - return false - end + return level == 3 elseif t2FacList and not table.empty(t2FacList) then - if level == 2 then - return true - else - return false - end + return level == 2 end return true end @@ -439,81 +415,73 @@ end ---@param baseName string ---@return boolean function BaseActive(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return 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 ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function BaseReclaimEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.EngineerReclaiming + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.EngineerReclaiming end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function BasePatrollingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.Patrolling + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.Patrolling end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function BaseBuildingEngineers(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.BuildEngineers + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.BuildEngineers end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function BaseEngineersEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.Engineers + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.Engineers end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function LandScoutingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.LandScouting + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.LandScouting end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function AirScoutingEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.AirScouting + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.AirScouting end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function ExpansionBasesEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.ExpansionBases + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.ExpansionBases end ---@param aiBrain AIBrain ---@param baseName string ---@return boolean function TMLsEnabled(aiBrain, baseName) - local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.TMLs + local bManager = aiBrain.BaseManagers[baseName] + return bManager and bManager.FunctionalityStates.TMLs end ---@param aiBrain AIBrain @@ -521,8 +489,7 @@ end ---@return boolean function NukesEnabled(aiBrain, baseName) local bManager = aiBrain.BaseManagers[baseName] - if not bManager then return false end - return bManager.FunctionalityStates.Nukes + return bManager and bManager.FunctionalityStates.Nukes end --- Moved Unused Imports for mod compatibility diff --git a/lua/editor/MiscBuildConditions.lua b/lua/editor/MiscBuildConditions.lua index f5dd35334d..99567a02de 100644 --- a/lua/editor/MiscBuildConditions.lua +++ b/lua/editor/MiscBuildConditions.lua @@ -41,39 +41,28 @@ end ---@param layerPref string ---@return true | nil function IsAIBrainLayerPref(aiBrain, layerPref) - if layerPref == aiBrain.LayerPref then - return true - end + return layerPref == aiBrain.LayerPref end ---@param aiBrain AIBrain unused ---@param num number ---@return true | nil function MissionNumber(aiBrain, num) - local missionNumber = ScenarioInfo.MissionNumber - if missionNumber and missionNumber == num then - return true - end + return ScenarioInfo.MissionNumber and ScenarioInfo.MissionNumber == num end ---@param aiBrain AIBrain unused ---@param num number ---@return true | nil function MissionNumberGreaterOrEqual(aiBrain, num) - local missionNumber = ScenarioInfo.MissionNumber - if missionNumber and missionNumber >= num then - return true - end + return ScenarioInfo.MissionNumber and ScenarioInfo.MissionNumber >= num end ---@param aiBrain AIBrain unused ---@param num number ---@return true | nil function MissionNumberLessOrEqual(aiBrain, num) - local missionNumber = ScenarioInfo.MissionNumber - if missionNumber and missionNumber <= num then - return true - end + return ScenarioInfo.MissionNumber and ScenarioInfo.MissionNumber <= num end ---@param aiBrain AIBrain unused @@ -96,30 +85,21 @@ end ---@param diffLevel number ---@return true | nil function DifficultyEqual(aiBrain, diffLevel) - local difficulty = ScenarioInfo.Options.Difficulty - if difficulty and difficulty == diffLevel then - return true - end + return ScenarioInfo.Options.Difficulty and ScenarioInfo.Options.Difficulty == diffLevel end ---@param aiBrain AIBrain unused ---@param diffLevel number ---@return true | nil function DifficultyGreaterOrEqual(aiBrain, diffLevel) - local difficulty = ScenarioInfo.Options.Difficulty - if difficulty and difficulty >= diffLevel then - return true - end + return ScenarioInfo.Options.Difficulty and ScenarioInfo.Options.Difficulty >= diffLevel end ---@param aiBrain AIBrain unused ---@param diffLevel number ---@return true | nil function DifficultyLessOrEqual(aiBrain, diffLevel) - local difficulty = ScenarioInfo.Options.Difficulty - if difficulty and difficulty <= diffLevel then - return true - end + return ScenarioInfo.Options.Difficulty and ScenarioInfo.Options.Difficulty <= diffLevel end ---@param aiBrain AIBrain unused @@ -411,4 +391,4 @@ function ReclaimEnabledOnBrain(aiBrain) end -- unused imports kept for mod support -local Utils = import("/lua/utilities.lua") +local Utils = import("/lua/utilities.lua") \ No newline at end of file diff --git a/lua/editor/PlatoonCountBuildConditions.lua b/lua/editor/PlatoonCountBuildConditions.lua index a760d18d97..876e9fb0b2 100644 --- a/lua/editor/PlatoonCountBuildConditions.lua +++ b/lua/editor/PlatoonCountBuildConditions.lua @@ -1,34 +1,27 @@ ---**************************************************************************** ---** ---** File : /lua/editor/PlatoonCountBuildConditions.lua ---** Author(s): Dru Staltman, John Comes ---** ---** Summary : Generic AI Platoon Build Conditions ---** Build conditions always return true or false ---** ---** Copyright © 2005 Gas Powered Games, Inc. All rights reserved. ---**************************************************************************** +-------------------------------------------------------------------------------------------------- +-- File : /lua/editor/PlatoonCountBuildConditions.lua +-- Author(s): Dru Staltman, John Comes +-- Summary : Generic AI Platoon Build Conditions. Build conditions always return true or false +-- +-- Copyright © 2005 Gas Powered Games, Inc. All rights reserved. +-------------------------------------------------------------------------------------------------- + +--- NOTE: ScenarioInfo.VarTable and Scenarinfo.OSPlatoonCounter are both tables that are initialized in "lua/simInit.lua" +--- The build conditions were refactored with that in mind, so if either of those 2 are nil/invalid, then something has gone wrong during the initialization ---@param aiBrain AIBrain ---@param name string ---@param varName string ---@return boolean function AMPlatoonsGreaterOrEqualVarTable(aiBrain, name, varName) - local counter = 0 - local num - - if ScenarioInfo.VarTable then - if ScenarioInfo.VarTable[varName] then - num = ScenarioInfo.VarTable[varName] - if aiBrain.AttackData.AMPlatoonCount[name] then - counter = aiBrain.AttackData.AMPlatoonCount[name] - end - if counter >= num then - return true - end - end - end - return false + local counter = aiBrain.AttackData.AMPlatoonCount[name] or 0 + local num = ScenarioInfo.VarTable[varName] + + if not num then + return false + end + + return counter >= num end ---@param aiBrain AIBrain @@ -36,24 +29,14 @@ end ---@param varName string ---@return boolean function AMPlatoonsLessThanVarTable(aiBrain, name, varName) - local platoonList = aiBrain:GetPlatoonsList() - local counter = 0 - local num - - if ScenarioInfo.VarTable then - if ScenarioInfo.VarTable[varName] then - num = ScenarioInfo.VarTable[varName] - if aiBrain.AttackData.AMPlatoonCount[name] then - counter = aiBrain.AttackData.AMPlatoonCount[name] - end - if counter < num then - return true - end - end - end - - return false - + local counter = aiBrain.AttackData.AMPlatoonCount[name] or 0 + local num = ScenarioInfo.VarTable[varName] + + if not num then + return false + end + + return counter < num end ---@param aiBrain AIBrain @@ -61,20 +44,10 @@ end ---@param name2 string ---@return boolean function NumBuilderPlatoonsGreaterOrEqualNumBuilderPlatoons(aiBrain, name1, name2) - local builder1Count = 0 - local builder2Count = 0 - - if aiBrain.PlatoonNameCounter[name1] then - builder1Count = aiBrain.PlatoonNameCounter[name1] - end - if aiBrain.PlatoonNameCounter[name2] then - builder2Count = aiBrain.PlatoonNameCounter[name2] - end - if builder1Count >= builder2Count then - return true - else - return false - end + local builder1Count = aiBrain.PlatoonNameCounter[name1] or 0 + local builder2Count = aiBrain.PlatoonNameCounter[name2] or 0 + + return builder1Count >= builder2Count end ---@param aiBrain AIBrain @@ -82,20 +55,10 @@ end ---@param name2 string ---@return boolean function NumBuilderPlatoonsLessThanNumBuilderPlatoons(aiBrain, name1, name2) - local builder1Count = 0 - local builder2Count = 0 - - if aiBrain.PlatoonNameCounter[name1] then - builder1Count = aiBrain.PlatoonNameCounter[name1] - end - if aiBrain.PlatoonNameCounter[name2] then - builder2Count = aiBrain.PlatoonNameCounter[name2] - end - if builder1Count < builder2Count then - return true - else - return false - end + local builder1Count = aiBrain.PlatoonNameCounter[name1] or 0 + local builder2Count = aiBrain.PlatoonNameCounter[name2] or 0 + + return builder1Count < builder2Count end ---@param aiBrain AIBrain @@ -103,22 +66,14 @@ end ---@param varName string ---@return boolean function NumBuilderPlatoonsGreaterOrEqualVarTable(aiBrain, name, varName) - local platoonList = aiBrain:GetPlatoonsList() - local counter = 0 - local num - - if ScenarioInfo.VarTable then - if ScenarioInfo.VarTable[varName] then - num = ScenarioInfo.VarTable[varName] - if aiBrain.PlatoonNameCounter[name] then - counter = aiBrain.PlatoonNameCounter[name] - end - if counter >= num then - return true - end - end - end - return false + local counter = aiBrain.PlatoonNameCounter[name] or 0 + local num = ScenarioInfo.VarTable[varName] + + if not num then + return false + end + + return counter >= num end ---@param aiBrain AIBrain @@ -126,22 +81,14 @@ end ---@param varName string ---@return boolean function NumBuilderPlatoonsLessThanVarTable(aiBrain, name, varName) - local platoonList = aiBrain:GetPlatoonsList() - local counter = 0 - local num - - if ScenarioInfo.VarTable then - if ScenarioInfo.VarTable[varName] then - num = ScenarioInfo.VarTable[varName] - if aiBrain.PlatoonNameCounter[name] then - counter = aiBrain.PlatoonNameCounter[name] - end - if counter < num then - return true - end - end - end - return false + local counter = aiBrain.PlatoonNameCounter[name] or 0 + local num = ScenarioInfo.VarTable[varName] + + if not num then + return false + end + + return counter < num end ---@param aiBrain AIBrain @@ -149,17 +96,7 @@ end ---@param num number ---@return boolean function NumGreaterOrEqualAMPlatoons(aiBrain, name, num) - local count - if aiBrain.AttackData.AMPlatoonCount[name] then - count = aiBrain.AttackData.AMPlatoonCount[name] - else - return false - end - if count >= num then - return true - else - return false - end + return (aiBrain.AttackData.AMPlatoonCount[name] or 0) >= num end ---@param aiBrain AIBrain @@ -167,17 +104,7 @@ end ---@param num number ---@return boolean function NumGreaterAMPlatoons(aiBrain, name, num) - local count - if aiBrain.AttackData.AMPlatoonCount[name] then - count = aiBrain.AttackData.AMPlatoonCount[name] - else - return false - end - if count > num then - return true - else - return false - end + return (aiBrain.AttackData.AMPlatoonCount[name] or 0) > num end ---@param aiBrain AIBrain @@ -185,17 +112,7 @@ end ---@param num number ---@return boolean function NumLessOrEqualAMPlatoons(aiBrain, name, num) - local count - if aiBrain.AttackData.AMPlatoonCount[name] then - count = aiBrain.AttackData.AMPlatoonCount[name] - else - return true - end - if count <= num then - return true - else - return false - end + return (aiBrain.AttackData.AMPlatoonCount[name] or 0) <= num end ---@param aiBrain AIBrain @@ -203,17 +120,7 @@ end ---@param num number ---@return boolean function NumLessAMPlatoons(aiBrain, name, num) - local count - if aiBrain.AttackData.AMPlatoonCount[name] then - count = aiBrain.AttackData.AMPlatoonCount[name] - else - return true - end - if count < num then - return true - else - return false - end + return (aiBrain.AttackData.AMPlatoonCount[name] or 0) < num end ---@param aiBrain AIBrain @@ -221,20 +128,9 @@ end ---@param num number ---@return boolean function NumBuildersLessThanOSCounter(aiBrain, builderName, num) - local counter = 0 - - if ScenarioInfo.OSPlatoonCounter and ScenarioInfo.Options.Difficulty then - if ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. ScenarioInfo.Options.Difficulty] then - num = ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. ScenarioInfo.Options.Difficulty] - end - if aiBrain.PlatoonNameCounter[builderName] then - counter = aiBrain.PlatoonNameCounter[builderName] - end - if counter < num then - return true - end - end - return false + local difficulty = ScenarioInfo.Options.Difficulty or 3 + + return (aiBrain.PlatoonNameCounter[builderName] or 0) < (ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. difficulty] or num) end ---@param aiBrain AIBrain @@ -242,20 +138,9 @@ end ---@param num number ---@return boolean function NumBuildersGreaterThanEqualOSCounter(aiBrain, builderName, num) - local counter = 0 + local difficulty = ScenarioInfo.Options.Difficulty or 3 - if ScenarioInfo.OSPlatoonCounter and ScenarioInfo.Options.Difficulty then - if ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. ScenarioInfo.Options.Difficulty] then - num = ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. ScenarioInfo.Options.Difficulty] - end - if aiBrain.PlatoonNameCounter[builderName] then - counter = aiBrain.PlatoonNameCounter[builderName] - end - if counter >= num then - return true - end - end - return false + return (aiBrain.PlatoonNameCounter[builderName] or 0) >= (ScenarioInfo.OSPlatoonCounter[builderName .. '_D' .. difficulty] or num) end -- Moved Unsused Imports to bottom for mod support diff --git a/lua/editor/UnitCountBuildConditions.lua b/lua/editor/UnitCountBuildConditions.lua index 9168f8e264..c17cc3fbf0 100644 --- a/lua/editor/UnitCountBuildConditions.lua +++ b/lua/editor/UnitCountBuildConditions.lua @@ -29,10 +29,8 @@ function HaveEqualToUnitsWithCategory(aiBrain, numReq, category, idleReq) else numUnits = table.getn(aiBrain:GetListOfUnits(testCat, true)) end - if numUnits == numReq then - return true - end - return false + + return numUnits == numReq end ---@param aiBrain AIBrain @@ -51,10 +49,8 @@ function HaveGreaterThanUnitsWithCategory(aiBrain, numReq, category, idleReq) else numUnits = table.getn(aiBrain:GetListOfUnits(testCat, true)) end - if numUnits > numReq then - return true - end - return false + + return numUnits > numReq end ---@param aiBrain AIBrain @@ -73,10 +69,7 @@ function HaveLessThanUnitsWithCategory(aiBrain, numReq, category, idleReq) else numUnits = table.getn(aiBrain:GetListOfUnits(testCat, true)) end - if numUnits < numReq then - return true - end - return false + return numUnits < numReq end ---@param aiBrain AIBrain @@ -86,10 +79,7 @@ end ---@return boolean function HaveLessThanUnitsWithCategoryInArea(aiBrain, numReq, category, area) local numUnits = ScenarioFramework.NumCatUnitsInArea(category, ScenarioUtils.AreaToRect(area), aiBrain) - if numUnits < numReq then - return true - end - return false + return numUnits < numReq end ---@param aiBrain AIBrain @@ -101,17 +91,14 @@ function NumUnitsLessNearBase(aiBrain, baseName, category, num) if aiBrain.BaseTemplates[baseName].Location == nil then return false else - local unitList = aiBrain:GetUnitsAroundPoint(category,aiBrain.BaseTemplates[baseName].Location,aiBrain.BaseTemplates[baseName].Radius, 'Ally') + local unitList = aiBrain:GetUnitsAroundPoint(category, aiBrain.BaseTemplates[baseName].Location,aiBrain.BaseTemplates[baseName].Radius, 'Ally') local count = 0 - for i,unit in unitList do + for i, unit in unitList do if unit:GetAIBrain() == aiBrain then count = count + 1 end end - if count < num then - return true - end - return false + return count < num end end @@ -128,12 +115,8 @@ function HaveLessThanUnitComparison(aiBrain, category1, category2) if type(category2) == 'string' then testCat2 = ParseEntityCategory(category2) end - local numUnits1 = aiBrain:GetCurrentUnits(testCat1) - local numUnits2 = aiBrain:GetCurrentUnits(testCat2) - if numUnits1 < numUnits2 then - return true - end - return false + + return aiBrain:GetCurrentUnits(testCat1) < aiBrain:GetCurrentUnits(testCat2) end ---@param aiBrain AIBrain @@ -149,12 +132,8 @@ function HaveGreaterThanUnitComparison(aiBrain, category1, category2) if type(category2) == 'string' then testCat2 = ParseEntityCategory(category2) end - local numUnits1 = aiBrain:GetCurrentUnits(testCat1) - local numUnits2 = aiBrain:GetCurrentUnits(testCat2) - if numUnits1 > numUnits2 then - return true - end - return false + + return aiBrain:GetCurrentUnits(testCat1) > aiBrain:GetCurrentUnits(testCat2) end ---@param aiBrain AIBrain @@ -167,12 +146,8 @@ function HaveLessThanVarTableUnitsWithCategory(aiBrain, varName, category) testCat = ParseEntityCategory(category) end local numUnits = aiBrain:GetCurrentUnits(testCat) - if ScenarioInfo.VarTable[varName] then - if numUnits < ScenarioInfo.VarTable[varName] then - return true - end - end - return false + + return ScenarioInfo.VarTable[varName] and (numUnits < ScenarioInfo.VarTable[varName]) end ---@param aiBrain AIBrain @@ -185,12 +160,8 @@ function HaveGreaterThanVarTableUnitsWithCategory(aiBrain, varName, category) testCat = ParseEntityCategory(category) end local numUnits = aiBrain:GetCurrentUnits(testCat) - if ScenarioInfo.VarTable[varName] then - if numUnits > ScenarioInfo.VarTable[varName] then - return true - end - end - return false + + return ScenarioInfo.VarTable[varName] and (numUnits > ScenarioInfo.VarTable[varName]) end ---@param aiBrain AIBrain @@ -204,12 +175,8 @@ function HaveLessThanVarTableUnitsWithCategoryInArea(aiBrain, varName, category, testCat = ParseEntityCategory(category) end local numUnits = ScenarioFramework.NumCatUnitsInArea(testCat, ScenarioUtils.AreaToRect(area), aiBrain) - if ScenarioInfo.VarTable[varName] then - if numUnits < ScenarioInfo.VarTable[varName] then - return true - end - end - return false + + return ScenarioInfo.VarTable[varName] and (numUnits < ScenarioInfo.VarTable[varName]) end ---@param aiBrain AIBrain @@ -223,12 +190,8 @@ function HaveGreaterThanVarTableUnitsWithCategoryInArea(aiBrain, varName, catego testCat = ParseEntityCategory(category) end local numUnits = ScenarioFramework.NumCatUnitsInArea(testCat, ScenarioUtils.AreaToRect(area), aiBrain) - if ScenarioInfo.VarTable[varName] then - if numUnits > ScenarioInfo.VarTable[varName] then - return true - end - end - return false + + return ScenarioInfo.VarTable[varName] and (numUnits > ScenarioInfo.VarTable[varName]) end ---@param aiBrain AIBrain @@ -254,16 +217,9 @@ function HaveGreaterThanUnitsInCategoryBeingBuilt(aiBrain, numReq, category, con numUnits = aiBrain:NumCurrentlyBuilding(cat, cat + categories.CONSTRUCTION) end - if numUnits > numReq then - return true - end - return false + return numUnits > numReq end ----@param aiBrain AIBrain ----@param numunits number ----@param category EntityCategory ----@return boolean function HaveLessThanUnitsInCategoryBeingBuilt(aiBrain, numunits, category) --DUNCAN - rewritten, credit to Sorian if type(category) == 'string' then @@ -291,10 +247,7 @@ function HaveLessThanUnitsInCategoryBeingBuilt(aiBrain, numunits, category) return false end end - if numunits > numBuilding then - return true - end - return false + return numunits > numBuilding end ---@param aiBrain AIBrain @@ -496,12 +449,8 @@ function HaveUnitsWithCategoryAndAlliance(aiBrain, greater, numReq, category, al testCat = ParseEntityCategory(category) end local numUnits = aiBrain:GetNumUnitsAroundPoint(testCat, Vector(0,0,0), 100000, alliance) - if numUnits > numReq and greater then - return true - elseif numUnits < numReq and not greater then - return true - end - return false + + return (numUnits > numReq and greater) or (numUnits < numReq and not greater) end ---@param aiBrain AIBrain @@ -1543,4 +1492,4 @@ function HaveLessThanUnitsInCategoryBeingUpgraded(aiBrain, numunits, category) end function HaveGreaterThanUnitsInCategoryBeingUpgraded(aiBrain, numunits, category) return HaveUnitsInCategoryBeingUpgraded(aiBrain, numunits, category, '>') -end +end \ No newline at end of file diff --git a/lua/sim/weapons/DefaultProjectileWeapon.lua b/lua/sim/weapons/DefaultProjectileWeapon.lua index 09dcbf7976..d0e0e326dd 100644 --- a/lua/sim/weapons/DefaultProjectileWeapon.lua +++ b/lua/sim/weapons/DefaultProjectileWeapon.lua @@ -187,13 +187,14 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { proj:SetBallisticAcceleration(-self:CalculateBallisticAcceleration(proj)) end, + --- Returns the positive downwards acceleration needed for a projectile to hit its target when travelling at the same speed as the unit launching it (for bombs) ---@param self DefaultProjectileWeapon ---@param projectile Projectile ---@return number CalculateBallisticAcceleration = function(self, projectile) local launcher = projectile:GetLauncher() if not launcher then -- fail-fast - return 4.75 + return 4.9 -- Return the default gravity value if some calculations fail end local UnitGetVelocity = UnitGetVelocity @@ -225,7 +226,7 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { if self.Blueprint.MuzzleSalvoSize <= 1 then -- do the calculation but skip any cache or salvo logic if not targetPos then - return 4.75 + return 4.9 end if target and not target.IsProp then targetVelX, _, targetVelZ = UnitGetVelocity(target) @@ -233,7 +234,7 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { local targetPosX, targetPosZ = targetPos[1], targetPos[3] local distVel = VDist2(projVelX, projVelZ, targetVelX, targetVelZ) if distVel == 0 then - return 4.75 + return 4.9 end local distPos = VDist2(projPosX, projPosZ, targetPosX, targetPosZ) do @@ -243,14 +244,14 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { end end if distPos == 0 then - return 4.75 + return 4.9 end local time = distPos / distVel projPosY = projPosY - GetSurfaceHeight(targetPosX + time * targetVelX, targetPosZ + time * targetVelZ) return 200 * projPosY / (time * time) else -- otherwise, calculate & cache a couple things the first time only data = { - lastAccel = 4.75, + lastAccel = 4.9, targetPos = targetPos, } if target then @@ -283,19 +284,19 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { local GetSurfaceHeight = GetSurfaceHeight local MathSqrt = math.sqrt local spread = self.AdjustedSalvoDelay * (self.SalvoSpreadStart + self.CurrentSalvoNumber) - -- nominal acceleration is 4.75; however, bomb clusters adjust the time it takes to land + -- default gravitational acceleration is 4.9; however, bomb clusters adjust the time it takes to land -- so we convert the acceleration to time to add the spread and convert back: -- h = unitY - surfaceY => h2 = 0.5 * (unitY - surfaceHeight(unitX, unitZ)) - -- t = sqrt(2 h / a) + spread => t = sqrt(4 / 4.75 * h2) + spread + -- t = sqrt(2 h / a) + spread => t = sqrt(4 / 4.9 * h2) + spread -- a = 0.5 h / t^2 => a = h2 / t^2 local halfHeight = 0.5 * (projPosY - GetSurfaceHeight(projPosX, projPosZ)) - if halfHeight < 0.01 then return 4.75 end - local time = MathSqrt(0.842105263158 * halfHeight) + spread + if halfHeight < 0.01 then return 4.9 end + local time = MathSqrt(0.816326530612 * halfHeight) + spread -- now that we know roughly when we'll land, we can find a better guess for where -- we'll land, and thus guess the true falling height better as well halfHeight = 0.5 * (projPosY - GetSurfaceHeight(projPosX + time * projVelX, projPosX + time * projVelX)) - time = MathSqrt(0.842105263158 * halfHeight) + spread + time = MathSqrt(0.816326530612 * halfHeight) + spread local acc = halfHeight / (time * time) data.lastAccel = acc @@ -306,8 +307,8 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { -- velocity will eventually need to multiplied by 10 due to being per tick instead of per second local distVel = VDist2(projVelX, projVelZ, targetVelX, targetVelZ) if distVel == 0 then - data.lastAccel = 4.75 - return 4.75 + data.lastAccel = 4.9 + return 4.9 end local targetPosX, targetPosZ = targetPos[1], targetPos[3] @@ -324,8 +325,8 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { local time = distPos / distVel local adjustedTime = time + self.AdjustedSalvoDelay * (self.SalvoSpreadStart + self.CurrentSalvoNumber) if adjustedTime == 0 then - data.lastAccel = 4.75 - return 4.75 + data.lastAccel = 4.9 + return 4.9 end -- If we have a target, targetPos may have updated now. diff --git a/units/XRB3301/XRB3301_unit.bp b/units/XRB3301/XRB3301_unit.bp index 3bef730235..de5262b0ae 100644 --- a/units/XRB3301/XRB3301_unit.bp +++ b/units/XRB3301/XRB3301_unit.bp @@ -1,6 +1,10 @@ UnitBlueprint{ Description = "Perimeter Monitoring System", AI = { + TargetBones = { + "TargetBone03", + "TargetBone02", + }, ShowAssistRangeOnSelect = true, StagingPlatformScanRadius = 200, }, diff --git a/units/XSL0202/XSL0202_unit.bp b/units/XSL0202/XSL0202_unit.bp index dfcc7a74bb..08fb90435c 100644 --- a/units/XSL0202/XSL0202_unit.bp +++ b/units/XSL0202/XSL0202_unit.bp @@ -198,7 +198,7 @@ UnitBlueprint{ MotionType = "RULEUMT_Land", RotateOnSpot = false, TurnRadius = 2, - TurnRate = 45, + TurnRate = 90, }, SelectionSizeX = 0.55, SelectionSizeZ = 0.5,