@@ -44,6 +44,18 @@ public class McvManagerBotModuleInfo : ConditionalTraitInfo
4444 [ Desc ( "Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?" ) ]
4545 public readonly bool RestrictMCVDeploymentFallbackToBase = true ;
4646
47+ [ Desc ( "Cash threshold to trigger expansion (building a new MCV)." ) ]
48+ public readonly int ExpansionCashThreshold = 1000 ;
49+
50+ [ Desc ( "Maximum number of bases (construction yards) allowed." ) ]
51+ public readonly int MaxBaseCount = 2 ;
52+
53+ [ Desc ( "Minimum distance from existing bases for a new expansion." ) ]
54+ public readonly int MinimumExpansionDistance = 30 ;
55+
56+ [ Desc ( "Maximum distance from existing bases for a new expansion." ) ]
57+ public readonly int MaximumExpansionDistance = 50 ;
58+
4759 public override object Create ( ActorInitializer init ) { return new McvManagerBotModule ( init . Self , this ) ; }
4860 }
4961
@@ -66,6 +78,9 @@ public CPos GetRandomBaseCenter()
6678
6779 IBotPositionsUpdated [ ] notifyPositionsUpdated ;
6880 IBotRequestUnitProduction [ ] requestUnitProduction ;
81+ PlayerResources playerResources ;
82+ IResourceLayer resourceLayer ;
83+ TechTree techTree ;
6984
7085 CPos initialBaseCenter ;
7186 int scanInterval ;
@@ -85,6 +100,9 @@ protected override void Created(Actor self)
85100 {
86101 notifyPositionsUpdated = self . Owner . PlayerActor . TraitsImplementing < IBotPositionsUpdated > ( ) . ToArray ( ) ;
87102 requestUnitProduction = self . Owner . PlayerActor . TraitsImplementing < IBotRequestUnitProduction > ( ) . ToArray ( ) ;
103+ playerResources = self . Owner . PlayerActor . Trait < PlayerResources > ( ) ;
104+ resourceLayer = self . World . WorldActor . TraitOrDefault < IResourceLayer > ( ) ;
105+ techTree = self . Owner . PlayerActor . TraitOrDefault < TechTree > ( ) ;
88106 }
89107
90108 protected override void TraitEnabled ( Actor self )
@@ -131,9 +149,36 @@ bool ShouldBuildMCV()
131149 if ( ! allowedToBuildMCV )
132150 return false ;
133151
134- // Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it).
135- return AIUtils . CountActorByCommonName ( constructionYards ) < Info . MinimumConstructionYardCount &&
136- AIUtils . CountActorByCommonName ( mcvFactories ) > 0 ;
152+ var constructionYardCount = AIUtils . CountActorByCommonName ( constructionYards ) ;
153+
154+ // Recovery Mode: Build MCV if we don't have any construction yards (and we have a factory to build it).
155+ if ( constructionYardCount < Info . MinimumConstructionYardCount && AIUtils . CountActorByCommonName ( mcvFactories ) > 0 )
156+ return true ;
157+
158+ // Expansion Mode: Build MCV if we have excess cash and haven't reached max base count.
159+ var activeMcvs = AIUtils . CountActorByCommonName ( mcvs ) ;
160+
161+ // Count MCVs currently in production queue (to avoid double ordering)
162+ var mcvsInProduction = world . ActorsWithTrait < ProductionQueue > ( )
163+ . Where ( a => a . Actor . Owner == player )
164+ . SelectMany ( a => a . Trait . AllQueued ( ) )
165+ . Count ( i => Info . McvTypes . Contains ( i . Item ) ) ;
166+
167+ if ( constructionYardCount + activeMcvs + mcvsInProduction < Info . MaxBaseCount )
168+ {
169+ // Check if we can build any MCV (prerequisites met)
170+ foreach ( var mcvType in Info . McvTypes )
171+ {
172+ if ( ! world . Map . Rules . Actors . TryGetValue ( mcvType , out var actorInfo ) )
173+ continue ;
174+
175+ var buildable = actorInfo . TraitInfoOrDefault < BuildableInfo > ( ) ;
176+ if ( buildable != null && techTree != null && techTree . HasPrerequisites ( buildable . Prerequisites ) )
177+ return true ;
178+ }
179+ }
180+
181+ return false ;
137182 }
138183
139184 void DeployMcvs ( IBot bot , bool chooseLocation )
@@ -151,12 +196,16 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
151196 if ( move )
152197 {
153198 // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base!
199+ var baseCount = AIUtils . CountActorByCommonName ( constructionYards ) ;
154200 var restrictToBase =
155201 Info . RestrictMCVDeploymentFallbackToBase &&
156- AIUtils . CountActorByCommonName ( constructionYards ) > 0 ;
202+ baseCount > 0 ;
203+
204+ // If we are expanding (have at least one base), we should look for a spot further away
205+ var isExpansion = baseCount > 0 ;
157206
158207 var transformsInfo = mcv . Info . TraitInfo < TransformsInfo > ( ) ;
159- var desiredLocation = ChooseMcvDeployLocation ( transformsInfo . IntoActor , transformsInfo . Offset , restrictToBase ) ;
208+ var desiredLocation = ChooseMcvDeployLocation ( transformsInfo . IntoActor , transformsInfo . Offset , restrictToBase , isExpansion ) ;
160209 if ( desiredLocation == null )
161210 return ;
162211
@@ -175,7 +224,7 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
175224 bot . QueueOrder ( new Order ( "DeployTransform" , mcv , true ) ) ;
176225 }
177226
178- CPos ? ChooseMcvDeployLocation ( string actorType , CVec offset , bool distanceToBaseIsImportant )
227+ CPos ? ChooseMcvDeployLocation ( string actorType , CVec offset , bool distanceToBaseIsImportant , bool isExpansion )
179228 {
180229 var actorInfo = world . Map . Rules . Actors [ actorType ] ;
181230 var bi = actorInfo . TraitInfoOrDefault < BuildingInfo > ( ) ;
@@ -201,9 +250,36 @@ void DeployMcv(IBot bot, Actor mcv, bool move)
201250 }
202251
203252 var baseCenter = GetRandomBaseCenter ( ) ;
253+ var targetCenter = baseCenter ;
254+ var minRange = Info . MinBaseRadius ;
255+ var maxRange = distanceToBaseIsImportant ? Info . MaxBaseRadius : world . Map . Grid . MaximumTileSearchRange ;
256+
257+ if ( isExpansion && resourceLayer != null )
258+ {
259+ // Find a resource patch that is far enough from existing bases
260+ var existingBases = constructionYards . Actors . Select ( a => a . Location ) . ToList ( ) ;
261+ var maxSearchRadius = System . Math . Min ( Info . MaximumExpansionDistance , world . Map . Grid . MaximumTileSearchRange ) ;
262+ var minSearchRadius = System . Math . Min ( Info . MinimumExpansionDistance , maxSearchRadius ) ;
263+
264+ var potentialResourceTiles = world . Map . FindTilesInAnnulus ( baseCenter , minSearchRadius , maxSearchRadius )
265+ . Where ( c => resourceLayer . GetResource ( c ) . Type != null )
266+ . Shuffle ( world . LocalRandom )
267+ . Take ( 20 ) ;
268+
269+ foreach ( var tile in potentialResourceTiles )
270+ {
271+ // Ensure this tile is far from ALL existing bases
272+ if ( existingBases . All ( baseLoc => ( tile - baseLoc ) . LengthSquared >= Info . MinimumExpansionDistance * Info . MinimumExpansionDistance ) )
273+ {
274+ targetCenter = tile ;
275+ minRange = 2 ; // Close to resources
276+ maxRange = 10 ;
277+ break ;
278+ }
279+ }
280+ }
204281
205- return FindPos ( baseCenter , baseCenter , Info . MinBaseRadius ,
206- distanceToBaseIsImportant ? Info . MaxBaseRadius : world . Map . Grid . MaximumTileSearchRange ) ;
282+ return FindPos ( targetCenter , targetCenter , minRange , maxRange ) ;
207283 }
208284
209285 List < MiniYamlNode > IGameSaveTraitData . IssueTraitData ( Actor self )
0 commit comments