1 #include "Strategic_AI.h"
2 #include "ArmyCompositionModel.h"
3 #include "CacheSectorsModel.h"
4 #include "Campaign.h"
5 #include "Campaign_Init.h"
6 #include "Campaign_Types.h"
7 #include "ContentManager.h"
8 #include "Debug.h"
9 #include "Facts.h"
10 #include "FileMan.h"
11 #include "Game_Clock.h"
12 #include "Game_Event_Hook.h"
13 #include "Game_Init.h"
14 #include "GameInstance.h"
15 #include "GameSettings.h"
16 #include "Interface_Dialogue.h"
17 #include "Logger.h"
18 #include "Map_Information.h"
19 #include "MapScreen.h"
20 #include "Overhead.h"
21 #include "Overhead_Types.h"
22 #include "Player_Command.h"
23 #include "PreBattle_Interface.h"
24 #include "Queen_Command.h"
25 #include "Quests.h"
26 #include "Random.h"
27 #include "SAM_Sites.h"
28 #include "Scheduling.h"
29 #include "SGPFile.h"
30 #include "Soldier_Control.h"
31 #include "Soldier_Profile.h"
32 #include "Soldier_Tile.h"
33 #include "Strategic_Movement.h"
34 #include "Strategic_Movement_Costs.h"
35 #include "Strategic_Pathing.h"
36 #include "StrategicAIPolicy.h"
37 #include "StrategicMap.h"
38 #include "Town_Militia.h"
39 #include <vector>
40
41 #define SAI_VERSION 29
42
43 /*
44 STRATEGIC AI -- UNDERLYING PHILOSOPHY
45 The most fundamental part of the strategic AI which takes from reality and gives to gameplay is the manner
46 the queen attempts to take her towns back. Finances and owning mines are the most important way
47 to win the game. As the player takes more mines over, the queen will focus more on quality and defense. In
48 the beginning of the game, she will focus more on offense than mid-game or end-game.
49
50 REALITY
51 The queen owns the entire country, and the player starts the game with a small lump of cash, enough to hire
52 some mercenaries for about a week. In that week, the queen may not notice what is going on, and the player
53 would believably take over one of the towns before she could feasibly react. As soon as her military was
54 aware of the situation, she would likely proceed to send 300-400 troops to annihilate the opposition, and the
55 game would be over relatively quickly. If the player was a prodigy, and managed to hold the town against such
56 a major assault, he would probably lose in the long run being forced into a defensive position and running out
57 of money quickly while the queen could continue to pump out the troops. On the other hand, if the player
58 somehow managed to take over most of the mines, he would be able to casually walk over the queen eventually
59 just from the sheer income allowing him to purchase several of the best mercs. That would have the effect of
60 making the game impossibly difficult in the beginning of the game, and a joke at the end (this is very much
61 like Master Of Orion II on the more difficult settings )
62
63 GAMEPLAY
64 Because we want the game to be like a normal game and make it fun, we need to make the game easy in the
65 beginning and harder at the end. In order to accomplish this, I feel that pure income shouldn't be the factor
66 for the queen, because she would likely crucify a would-be leader in his early days. So, in the beginning of
67 the game, the forces would already be situated with the majority of forces being the administrators in the towns,
68 and army troops and elites in the more important sectors. Restricting the queen's offensive
69 abilities using a distance penalty would mean that the furthest sectors from the queen's palace would be
70 much easier to defend because she would only be allowed to send x number of troops. As you get closer to the
71 queen, she would be allowed to send larger forces to attack those towns in question. Also, to further
72 increase the games difficulty as the campaign progresses in the player's favor, we could also increase the
73 quality of the queen's troops based purely on the peek progress percentage. This is calculated using a formula
74 that determines how well the player is doing by combining loyalty of towns owned, income generated, etc. So,
75 in the beginning of the game, the quality is at the worst, but once you capture your first mines/towns, it
76 permanently increase the queen's quality rating, effectively bumping up the stakes. By the time you capture
77 four or five mines, the queen is going to focus more (but not completely) on quality defense as she prepares
78 for your final onslaught. This quality rating will augment the experience level, equipment rating, and/or
79 attribute ratings of the queen's troops. I would maintain a table of these enhancements based on the current
80 quality rating hooking into the difficulty all along.
81
82 //EXPLANATION OF THE WEIGHT SYSTEM:
83 The strategic AI has two types of groups: garrisons and patrol groups. Each of these groups contain
84 information of it's needs, mainly desired population. If the current population is greater than the
85 desired population, and the group will get a negative weight assigned to it, which means that it is willing
86 to give up troops to areas that need them more. On the other hand, if a group has less than the desired population,
87 then the weight will be positive, meaning they are requesting reinforcements.
88
89 The weight generated will range between -100 and +100. The calculated weight is modified by the priority
90 of the group. If the priority of the group is high, they
91 */
92
93 BOOLEAN gfAutoAIAware = FALSE;
94
95 //Saved vars
96 BOOLEAN gfExtraElites = 0; //Set when queen compositions are augmented with bonus elites.
97 INT32 giForcePercentage = 0; //Modifies the starting group sizes relative by percentage
98 INT32 giArmyAlertness = 0; //The chance the group will spot an adjacent player/militia
99 INT32 giArmyAlertnessDecay = 0; //How much the spotting chance decreases when spot check succeeds
100 UINT8 gubNumAwareBattles = 0; //When non-zero, this means the queen is very aware and searching for players. Every time
101 //there is an enemy initiated battle, this counter decrements until zero. Until that point,
102 //all adjacent sector checks automatically succeed.
103 BOOLEAN gfQueenAIAwake = FALSE; //This flag turns on/off the strategic decisions. If it's off, no reinforcements
104 //or assaults will happen.
105 //@@@Alex, this flag is ONLY set by the first meanwhile scene which calls an action. If this
106 //action isn't called, the AI will never turn on. It is completely dependant on this action. It can
107 //be toggled at will in the AIViewer for testing purposes.
108 INT32 giReinforcementPool = 0; //How many troops the queen has in reserve in noman's land. These guys are spawned as needed in P3.
109 INT32 giReinforcementPoints = 0; //the entire army's capacity to provide reinforcements.
110 INT32 giRequestPoints = 0; //the entire army's need for reinforcements.
111 UINT8 gubSAIVersion = SAI_VERSION; //Used for adding new features to be saved.
112 UINT8 gubQueenPriorityPhase = 0; //Defines how far into defence the queen is -- abstractly related to defcon index ranging from 0-10.
113 //10 is the most defensive
114 //Used for authorizing the use of the first battle meanwhile scene AFTER the battle is complete. This is the case used when
115 //the player attacks a town, and is set once militia are sent to investigate.
116 BOOLEAN gfFirstBattleMeanwhileScenePending = FALSE;
117
118 //After the first battle meanwhile scene is finished, this flag is set, and the queen orders patrol groups to immediately fortify all towns.
119 BOOLEAN gfMassFortificationOrdered = FALSE;
120
121 UINT8 gubMinEnemyGroupSize = 0;
122 UINT8 gubHoursGracePeriod = 0;
123 UINT16 gusPlayerBattleVictories = 0;
124 BOOLEAN gfUseAlternateQueenPosition = FALSE;
125
126 //padding for generic globals
127 #define SAI_PADDING_BYTES 97
128 INT8 gbPadding[SAI_PADDING_BYTES]; // XXX HACK000B
129 //patrol group info plus padding
130 std::vector<PATROL_GROUP> gPatrolGroup;
131 //army composition info plus padding
132 std::vector<ARMY_COMPOSITION> gArmyComp;
133 //garrison info plus padding
134 std::vector<GARRISON_GROUP> gGarrisonGroup;
135
136 extern UINT8 gubNumGroupsArrivedSimultaneously;
137
138 //This refers to the number of force points that are *saved* for the AI to use. This is basically an array of each
139 //group. When the queen wants to send forces to attack a town that is defended, the initial number of forces that
140 //she would send would be considered too weak. So, instead, she will send that force to the sector's adjacent sector,
141 //and stage, while
142 UINT8 *gubGarrisonReinforcementsDenied = NULL;
143 UINT8 *gubPatrolReinforcementsDenied = NULL;
144
145 //Unsaved vars
146 BOOLEAN gfDisplayStrategicAILogs = FALSE;
147
148 extern INT16 sWorldSectorLocationOfFirstBattle;
149
150 //returns the number of reinforcements permitted to be sent. Will increased if the denied counter is non-zero.
GarrisonReinforcementsRequested(INT32 iGarrisonID,UINT8 * pubExtraReinforcements)151 static INT32 GarrisonReinforcementsRequested(INT32 iGarrisonID, UINT8* pubExtraReinforcements)
152 {
153 INT32 iReinforcementsRequested;
154 INT32 iExistingForces;
155 SECTORINFO *pSector;
156
157 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
158 iExistingForces = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
159 iReinforcementsRequested = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation - iExistingForces;
160
161 //Record how many of the reinforcements are additionally provided due to being denied in the past. This will grow
162 //until it is finally excepted or an absolute max is made.
163 *pubExtraReinforcements = (UINT8)(gubGarrisonReinforcementsDenied[ iGarrisonID ] / (6 - gGameOptions.ubDifficultyLevel));
164 //Make sure the number of extra reinforcements don't bump the force size past the max of MAX_STRATEGIC_TEAM_SIZE.
165 *pubExtraReinforcements = (UINT8)MIN( (INT32)*pubExtraReinforcements, MIN( (INT32)(*pubExtraReinforcements), MAX_STRATEGIC_TEAM_SIZE - iReinforcementsRequested ) );
166
167 iReinforcementsRequested = MIN( MAX_STRATEGIC_TEAM_SIZE, iReinforcementsRequested );
168
169 return iReinforcementsRequested;
170 }
171
172
PatrolReinforcementsRequested(PATROL_GROUP const * const pg)173 static INT32 PatrolReinforcementsRequested(PATROL_GROUP const* const pg)
174 {
175 GROUP* const g = GetGroup(pg->ubGroupID);
176 INT32 size = pg->bSize;
177 if (g) size -= g->ubGroupSize;
178 return size;
179 }
180
181
ReinforcementsAvailable(INT32 iGarrisonID)182 static INT32 ReinforcementsAvailable(INT32 iGarrisonID)
183 {
184 SECTORINFO *pSector;
185 INT32 iReinforcementsAvailable;
186
187 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
188 iReinforcementsAvailable = pSector->ubNumTroops + pSector->ubNumElites + pSector->ubNumAdmins;
189 iReinforcementsAvailable -= gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation;
190
191 switch( gGarrisonGroup[ iGarrisonID ].ubComposition )
192 {
193 case LEVEL1_DEFENCE:
194 case LEVEL2_DEFENCE:
195 case LEVEL3_DEFENCE:
196 case ALMA_DEFENCE:
197 case ALMA_MINE:
198 //Legal spawning locations
199 break;
200 default:
201 //No other sector permitted to send surplus troops
202 return 0;
203 }
204
205 return iReinforcementsAvailable;
206 }
207
208
PlayerForceTooStrong(UINT8 ubSectorID,UINT16 usOffensePoints,UINT16 * pusDefencePoints)209 static BOOLEAN PlayerForceTooStrong(UINT8 ubSectorID, UINT16 usOffensePoints, UINT16* pusDefencePoints)
210 {
211 SECTORINFO *pSector;
212 UINT8 ubSectorX, ubSectorY;
213
214 ubSectorX = (UINT8)SECTORX( ubSectorID );
215 ubSectorY = (UINT8)SECTORY( ubSectorID );
216 pSector = &SectorInfo[ ubSectorID ];
217
218 *pusDefencePoints = pSector->ubNumberOfCivsAtLevel[ GREEN_MILITIA ] * 1 +
219 pSector->ubNumberOfCivsAtLevel[ REGULAR_MILITIA ] * 2 +
220 pSector->ubNumberOfCivsAtLevel[ ELITE_MILITIA ] * 3 +
221 PlayerMercsInSector( ubSectorX, ubSectorY, 0 ) * 5;
222 if( *pusDefencePoints > usOffensePoints )
223 {
224 return TRUE;
225 }
226 return FALSE;
227 }
228
229
230 static void SendReinforcementsForGarrison(INT32 iDstGarrisonID, UINT16 usDefencePoints, GROUP** pOptionalGroup);
231
232
RequestAttackOnSector(UINT8 ubSectorID,UINT16 usDefencePoints)233 static void RequestAttackOnSector(UINT8 ubSectorID, UINT16 usDefencePoints)
234 {
235 UINT32 i;
236 for( i = 0; i < gGarrisonGroup.size(); i++ )
237 {
238 if( gGarrisonGroup[ i ].ubSectorID == ubSectorID && !gGarrisonGroup[ i ].ubPendingGroupID )
239 {
240 SLOGD("An attack has been requested in sector %c%d.",
241 SECTORY( ubSectorID ) + 'A' - 1, SECTORX( ubSectorID ) );
242 SendReinforcementsForGarrison( i, usDefencePoints, NULL );
243 return;
244 }
245 }
246 }
247
248
AdjacentSectorIsImportantAndUndefended(UINT8 const sector_id)249 static bool AdjacentSectorIsImportantAndUndefended(UINT8 const sector_id)
250 {
251 switch (StrategicMap[SECTOR_INFO_TO_STRATEGIC_INDEX(sector_id)].bNameId)
252 {
253 case OMERTA:
254 case SAN_MONA:
255 case ESTONI:
256 // These towns aren't important.
257 return false;
258 }
259 SECTORINFO const& si = SectorInfo[sector_id];
260 return
261 si.ubNumTroops == 0 &&
262 si.ubNumElites == 0 &&
263 si.ubNumAdmins == 0 &&
264 si.ubTraversability[THROUGH_STRATEGIC_MOVE] == TOWN &&
265 !PlayerSectorDefended(sector_id);
266 }
267
268
269 static void ReassignAIGroup(GROUP** pGroup);
270
271
ValidateGroup(GROUP * pGroup)272 static void ValidateGroup(GROUP* pGroup)
273 {
274 if( !pGroup->ubSectorX || !pGroup->ubSectorY || pGroup->ubSectorX > 16 || pGroup->ubSectorY > 16 )
275 {
276 if( gTacticalStatus.uiFlags & LOADING_SAVED_GAME )
277 {
278 SLOGE( "Internal error (invalid enemy group #%d location at %c%d, destination %c%d). Please send PRIOR save file and Debug Log.",
279 pGroup->ubGroupID, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX, pGroup->ubNextY + 'A' - 1, pGroup->ubNextX );
280 RemoveGroupFromStrategicAILists(*pGroup);
281 RemoveGroup(*pGroup);
282 return;
283 }
284 }
285 if( !pGroup->ubNextX || !pGroup->ubNextY )
286 {
287 if( !pGroup->fPlayer && pGroup->pEnemyGroup->ubIntention != STAGING
288 && pGroup->pEnemyGroup->ubIntention != REINFORCEMENTS )
289 {
290 SLOGE( "Internal error (floating group). Please send PRIOR save file and Debug Log." );
291 if( gTacticalStatus.uiFlags & LOADING_SAVED_GAME )
292 {
293 RemoveGroupFromStrategicAILists(*pGroup);
294 ReassignAIGroup( &pGroup );
295 return;
296 }
297 }
298 }
299 if( pGroup->pEnemyGroup->ubNumAdmins + pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites != pGroup->ubGroupSize ||
300 pGroup->ubGroupSize > MAX_STRATEGIC_TEAM_SIZE )
301 {
302 SLOGE( "Internal error (bad group populations). Please send PRIOR save file and Debug Log." );
303 }
304 }
305
306
ValidateLargeGroup(GROUP * pGroup)307 static void ValidateLargeGroup(GROUP* pGroup)
308 {
309 if( pGroup->ubGroupSize > 25 )
310 {
311 SLOGW( "warning: Enemy group containing %d soldiers\n\
312 (%d admins, %d troops, %d elites) in sector %c%d. This message is a temporary test message\n\
313 to evaluate a potential problems with very large enemy groups.",
314 pGroup->ubGroupSize, pGroup->pEnemyGroup->ubNumAdmins, pGroup->pEnemyGroup->ubNumTroops, pGroup->pEnemyGroup->ubNumElites,
315 pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX );
316 }
317 }
318
319
InitStrategicAI()320 void InitStrategicAI()
321 {
322 gfExtraElites = FALSE;
323 gubNumAwareBattles = 0;
324 gfQueenAIAwake = FALSE;
325 giReinforcementPoints = 0;
326 giRequestPoints = 0;
327 gubSAIVersion = SAI_VERSION;
328 gubQueenPriorityPhase = 0;
329 gfFirstBattleMeanwhileScenePending = FALSE;
330 gfMassFortificationOrdered = FALSE;
331 gusPlayerBattleVictories = 0;
332 gfUseAlternateQueenPosition = FALSE;
333
334 // 475 is 7:55am in minutes since midnight, the time the game starts on day 1
335 UINT32 evaluate_time = 475;
336 UINT8 const difficulty = gGameOptions.ubDifficultyLevel;
337
338 giReinforcementPool = saipolicy(queens_pool_of_troops);
339 giForcePercentage = saipolicy(initial_garrison_percentages);
340 giArmyAlertness = saipolicy(enemy_starting_alert_level);
341 giArmyAlertnessDecay = saipolicy(enemy_starting_alert_decay);
342 gubMinEnemyGroupSize = saipolicy(min_enemy_group_size);
343 gubHoursGracePeriod = saipolicy(grace_period_in_hours);
344 evaluate_time += saipolicy(time_evaluate_in_minutes) + Random(saipolicy(time_evaluate_variance));
345
346 AddStrategicEvent(EVENT_EVALUATE_QUEEN_SITUATION, evaluate_time, 0);
347
348 //Initialize the sectorinfo structure so all sectors don't point to a garrisonID.
349 FOR_EACH(SECTORINFO, i, SectorInfo)
350 {
351 i->ubGarrisonID = NO_GARRISON;
352 }
353
354 /* Copy over the original army composition as it does get modified during the
355 * campaign. This bulletproofs starting the game over again. */
356 gArmyComp = GCM->getArmyCompositions();
357
358 // Eliminate more perimeter defenses on the easier levels.
359 switch (difficulty)
360 {
361 case DIF_LEVEL_EASY:
362 gArmyComp[LEVEL2_DEFENCE].bDesiredPopulation = 0;
363 gArmyComp[LEVEL2_DEFENCE].bStartPopulation = 0;
364 /* FALLTHROUGH */
365 case DIF_LEVEL_MEDIUM:
366 gArmyComp[LEVEL3_DEFENCE].bDesiredPopulation = 0;
367 gArmyComp[LEVEL3_DEFENCE].bStartPopulation = 0;
368 break;
369 }
370
371 // Initialize the patrol group definitions
372 gPatrolGroup = GCM->getPatrolGroups();
373 gubPatrolReinforcementsDenied = new UINT8[gPatrolGroup.size()]{};
374
375 // Initialize the garrison group definitions
376 auto origGarrisonGroups = GCM->getGarrisonGroups();
377 auto iGarrisonArraySize = origGarrisonGroups.size();
378 gGarrisonGroup = origGarrisonGroups;
379
380 gubGarrisonReinforcementsDenied = new UINT8[iGarrisonArraySize]{};
381
382 // Modify initial force sizes?
383 INT32 const force_percentage = giForcePercentage;
384 if (force_percentage != 100)
385 { /* The initial force sizes are being modified, so go through each of the
386 * army compositions and adjust them accordingly. */
387 for (size_t i = 0; i != gArmyComp.size(); ++i)
388 {
389 ARMY_COMPOSITION& a = gArmyComp[i];
390 if (i != QUEEN_DEFENCE)
391 {
392 a.bDesiredPopulation = MIN(MAX_STRATEGIC_TEAM_SIZE, a.bDesiredPopulation * force_percentage / 100);
393 if (a.bStartPopulation != MAX_STRATEGIC_TEAM_SIZE)
394 { /* If the value is MAX_STRATEGIC_TEAM_SIZE, then that means the
395 * particular sector is a spawning location. Don't modify the value if
396 * it is MAX_STRATEGIC_TEAM_SIZE. Everything else is game. */
397 a.bStartPopulation = MIN(MAX_STRATEGIC_TEAM_SIZE, a.bStartPopulation * force_percentage / 100);
398 }
399 }
400 else
401 {
402 a.bDesiredPopulation = MIN(32, a.bDesiredPopulation * force_percentage / 100);
403 a.bStartPopulation = a.bDesiredPopulation;
404 }
405 }
406 for (size_t i = 0; i != gPatrolGroup.size(); ++i)
407 { // Force modified range within 1 - MAX_STRATEGIC_TEAM_SIZE.
408 INT8& size = gPatrolGroup[i].bSize;
409 size = MAX(gubMinEnemyGroupSize, MIN(MAX_STRATEGIC_TEAM_SIZE, size * force_percentage / 100));
410 }
411 }
412
413 /* Initialize the garrisons based on the initial sizes (all variances are plus
414 * or minus 1). */
415 for (UINT32 i = 0; i != iGarrisonArraySize; ++i)
416 {
417 GARRISON_GROUP& gg = gGarrisonGroup[i];
418 SECTORINFO& si = SectorInfo[gg.ubSectorID];
419 si.ubGarrisonID = i;
420 ARMY_COMPOSITION const& ac = gArmyComp[gg.ubComposition];
421 INT32 start_pop = ac.bStartPopulation;
422 INT32 const desired_pop = ac.bDesiredPopulation;
423 INT32 const iPriority = ac.bPriority;
424 INT32 const elite_chance = ac.bElitePercentage;
425 INT32 const troop_chance = ac.bTroopPercentage + elite_chance;
426 INT32 const admin_chance = ac.bAdminPercentage;
427
428 switch (gg.ubComposition)
429 {
430 case ROADBLOCK:
431 si.uiFlags |= SF_ENEMY_AMBUSH_LOCATION;
432 start_pop = Chance(20) ? ac.bDesiredPopulation : 0;
433 break;
434
435 case SANMONA_SMALL:
436 start_pop = 0; // Not appropriate until Kingpin is killed.
437 break;
438 }
439
440 if (start_pop != 0)
441 {
442 if (gg.ubSectorID != SEC_P3)
443 {
444 // if population is less than maximum
445 if (start_pop != MAX_STRATEGIC_TEAM_SIZE)
446 {
447 // then vary it a bit (+/- 25%)
448 start_pop = start_pop * (100 + Random(51) - 25) / 100;
449 }
450
451 start_pop = MAX(gubMinEnemyGroupSize, MIN(MAX_STRATEGIC_TEAM_SIZE, start_pop));
452 }
453
454 if (admin_chance != 0)
455 {
456 si.ubNumAdmins = admin_chance * start_pop / 100;
457 }
458 else for (INT32 cnt = start_pop; cnt != 0; --cnt)
459 { // For each soldier randomly determine the type.
460 INT32 const roll = Random(100);
461 if (roll < elite_chance)
462 {
463 ++si.ubNumElites;
464 }
465 else if (roll < troop_chance)
466 {
467 ++si.ubNumTroops;
468 }
469 }
470
471 switch (gg.ubComposition)
472 {
473 case CAMBRIA_DEFENCE:
474 case CAMBRIA_MINE:
475 case ALMA_MINE:
476 case GRUMM_MINE:
477 // Fill up extra start slots with troops
478 start_pop -= si.ubNumAdmins;
479 si.ubNumTroops = start_pop;
480 break;
481
482 case DRASSEN_AIRPORT:
483 case DRASSEN_DEFENCE:
484 case DRASSEN_MINE:
485 si.ubNumAdmins = MAX(5, si.ubNumAdmins);
486 break;
487
488 case TIXA_PRISON:
489 si.ubNumAdmins = MAX(8, si.ubNumAdmins);
490 break;
491 }
492 }
493
494 if (admin_chance != 0 && si.ubNumAdmins < gubMinEnemyGroupSize)
495 {
496 si.ubNumAdmins = gubMinEnemyGroupSize;
497 }
498
499 /* Calculate weight (range is -20 to +20 before multiplier). The multiplier
500 * of 3 brings it to a range of -96 to +96 which is close enough to a
501 * plus/minus 100%. The resultant percentage is then converted based on the
502 * priority. */
503 INT32 weight = (desired_pop - start_pop) * 3;
504 if (weight > 0)
505 { // Modify it by its priority.
506 // Generates a value between 2 and 100
507 weight = weight * iPriority / 96;
508 weight = MAX(weight, 2);
509 giRequestPoints += weight;
510 }
511 else if (weight < 0)
512 { // Modify it by its reverse priority
513 // Generates a value between -2 and -100
514 weight = weight * (100 - iPriority) / 96;
515 weight = MIN(weight, -2);
516 giReinforcementPoints -= weight;
517 }
518 gg.bWeight = weight;
519
520 /* Post an event which allows them to check adjacent sectors periodically.
521 * Spread them out so that they process at different times. */
522 AddPeriodStrategicEventWithOffset(EVENT_CHECK_ENEMY_CONTROLLED_SECTOR, 140 - 20 * difficulty + Random(4), 475 + i, gg.ubSectorID);
523 }
524
525 // Initialize each of the patrol groups
526 for (size_t i = 0; i != gPatrolGroup.size(); ++i)
527 {
528 PATROL_GROUP& pg = gPatrolGroup[i];
529 UINT8 n_troops = pg.bSize + Random(3) - 1;
530 n_troops = MAX(gubMinEnemyGroupSize, MIN(MAX_STRATEGIC_TEAM_SIZE, n_troops));
531 /* Note on adding patrol groups: The patrol group can't actually start on
532 * the first waypoint, so we set it to the second way point for
533 * initialization, and then add the waypoints from 0 up */
534 GROUP& g = *CreateNewEnemyGroupDepartingFromSector(pg.ubSectorID[1], 0, n_troops, 0);
535 ENEMYGROUP& eg = *g.pEnemyGroup;
536
537 if (i == 3 || i == 4)
538 { /* Special case: Two patrol groups are administrator groups -- rest are
539 * troops */
540 eg.ubNumAdmins = eg.ubNumTroops;
541 eg.ubNumTroops = 0;
542 }
543 pg.ubGroupID = g.ubGroupID;
544 eg.ubIntention = PATROL;
545 g.ubMoveType = ENDTOEND_FORWARDS;
546 FOR_EACH(UINT8 const, i, pg.ubSectorID)
547 {
548 if (*i == 0) break;
549 AddWaypointIDToPGroup(&g, *i);
550 }
551 RandomizePatrolGroupLocation(&g);
552 ValidateGroup(&g);
553 }
554
555 /* Choose one cache map out of five possible maps. Select the sector randomly,
556 * set up the flags to use the alternate map, then place 8-12 regular troops
557 * there (no AI though). Changing MAX_STRATEGIC_TEAM_SIZE may require changes
558 * to to the defending force here. */
559 auto model = GCM->getCacheSectors();
560 INT16 sSectorID = model->pickSector();
561 if (sSectorID >= 0)
562 {
563 SECTORINFO &si = SectorInfo[sSectorID];
564 si.uiFlags |= SF_USE_ALTERNATE_MAP;
565 si.ubNumTroops = model->getNumTroops(difficulty);
566 STLOGD("Weapon cache is at {}", SECTOR_SHORT_STRING(sSectorID));
567 }
568 }
569
570
KillStrategicAI()571 void KillStrategicAI()
572 {
573 gPatrolGroup.clear();
574 gGarrisonGroup.clear();
575
576 if( gubPatrolReinforcementsDenied )
577 {
578 delete[] gubPatrolReinforcementsDenied;
579 gubPatrolReinforcementsDenied = NULL;
580 }
581 if( gubGarrisonReinforcementsDenied )
582 {
583 delete[] gubGarrisonReinforcementsDenied;
584 gubGarrisonReinforcementsDenied = NULL;
585 }
586 DeleteAllStrategicEventsOfType( EVENT_EVALUATE_QUEEN_SITUATION );
587 }
588
OkayForEnemyToMoveThroughSector(UINT8 ubSectorID)589 BOOLEAN OkayForEnemyToMoveThroughSector( UINT8 ubSectorID )
590 {
591 SECTORINFO *pSector;
592 pSector = &SectorInfo[ ubSectorID ];
593 if( pSector->uiTimeLastPlayerLiberated && pSector->uiTimeLastPlayerLiberated + (gubHoursGracePeriod * 3600) > GetWorldTotalSeconds() )
594 {
595 return FALSE;
596 }
597 return TRUE;
598 }
599
600
EnemyPermittedToAttackSector(GROUP ** pGroup,UINT8 ubSectorID)601 static BOOLEAN EnemyPermittedToAttackSector(GROUP** pGroup, UINT8 ubSectorID)
602 {
603 SECTORINFO *pSector;
604 BOOLEAN fPermittedToAttack = TRUE;
605
606 pSector = &SectorInfo[ ubSectorID ];
607 fPermittedToAttack = OkayForEnemyToMoveThroughSector( ubSectorID );
608 if( pGroup && *pGroup && pSector->ubGarrisonID != NO_GARRISON )
609 {
610 if( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID )
611 {
612 GROUP *pPendingGroup;
613 pPendingGroup = GetGroup( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID );
614 if( pPendingGroup == *pGroup )
615 {
616 if( fPermittedToAttack )
617 {
618 if( GroupAtFinalDestination( *pGroup ) )
619 { //High priority reinforcements have arrived. This overrides most other situations.
620 return TRUE;
621 }
622 }
623 else
624 { //Reassign the group
625 ReassignAIGroup( pGroup );
626 }
627 }
628 }
629 }
630 if( !fPermittedToAttack )
631 {
632 return FALSE;
633 }
634 //If Hill-billies are alive, then enemy won't attack the sector.
635 switch( ubSectorID )
636 {
637 case SEC_F10:
638 //Hill-billy farm -- not until hill billies are dead.
639 if (CheckFact(FACT_HILLBILLIES_KILLED, FALSE))
640 return FALSE;
641 break;
642 case SEC_A9:
643 case SEC_A10:
644 //Omerta -- not until Day 2 at 7:45AM.
645 if( GetWorldTotalMin() < 3345 )
646 return FALSE;
647 break;
648 case SEC_B13:
649 case SEC_C13:
650 case SEC_D13:
651 //Drassen -- not until Day 3 at 6:30AM.
652 if( GetWorldTotalMin() < 4710 )
653 return FALSE;
654 break;
655 case SEC_C5:
656 case SEC_C6:
657 case SEC_D5:
658 //San Mona -- not until Kingpin is dead.
659 return CheckFact(FACT_KINGPIN_DEAD, 0);
660
661 case SEC_G1:
662 if( PlayerSectorDefended( SEC_G2 ) && (PlayerSectorDefended( SEC_H1 ) || PlayerSectorDefended( SEC_H2 )) )
663 {
664 return FALSE;
665 }
666 break;
667 case SEC_H2:
668 if( PlayerSectorDefended( SEC_H2 ) && (PlayerSectorDefended( SEC_G1 ) || PlayerSectorDefended( SEC_G2 )) )
669 {
670 return FALSE;
671 }
672 break;
673 }
674 return TRUE;
675 }
676
677
678 enum SAIMOVECODE
679 {
680 DIRECT,
681 EVASIVE,
682 STAGE
683 };
684 static void MoveSAIGroupToSector(GROUP**, UINT8 sector, SAIMOVECODE, UINT8 intention);
685
686
HandlePlayerGroupNoticedByPatrolGroup(const GROUP * const pPlayerGroup,GROUP * pEnemyGroup)687 static BOOLEAN HandlePlayerGroupNoticedByPatrolGroup(const GROUP* const pPlayerGroup, GROUP* pEnemyGroup)
688 {
689 UINT16 usDefencePoints;
690 UINT16 usOffensePoints;
691
692 UINT8 const ubSectorID = SECTOR(pPlayerGroup->ubSectorX, pPlayerGroup->ubSectorY);
693 usOffensePoints = pEnemyGroup->pEnemyGroup->ubNumAdmins * 2 +
694 pEnemyGroup->pEnemyGroup->ubNumTroops * 4 +
695 pEnemyGroup->pEnemyGroup->ubNumElites * 6;
696
697 const UINT8 playerSector = pPlayerGroup->ubNextX
698 ? SECTOR(pPlayerGroup->ubNextX, pPlayerGroup->ubNextY)
699 : SECTOR(pPlayerGroup->ubSectorX, pPlayerGroup->ubSectorY);
700
701 if(PlayerForceTooStrong(ubSectorID, usOffensePoints, &usDefencePoints)
702 || SectorInfo[playerSector].ubGarrisonID != NO_GARRISON)
703 {
704 RequestAttackOnSector(ubSectorID, usDefencePoints);
705 return FALSE;
706 }
707 //For now, automatically attack.
708 if( pPlayerGroup->ubNextX )
709 {
710 MoveSAIGroupToSector( &pEnemyGroup, playerSector, DIRECT, PURSUIT );
711
712 SLOGD("Enemy group at %c%d detected player group at %c%d and is moving to intercept them at %c%d.",
713 pEnemyGroup->ubSectorY + 'A' - 1, pEnemyGroup->ubSectorX,
714 pPlayerGroup->ubSectorY + 'A' - 1, pPlayerGroup->ubSectorX,
715 pPlayerGroup->ubNextY + 'A' - 1, pPlayerGroup->ubNextX );
716 }
717 else
718 {
719 MoveSAIGroupToSector( &pEnemyGroup, playerSector, DIRECT, PURSUIT );
720
721 SLOGD("Enemy group at %c%d detected player group at %c%d and is moving to intercept them at %c%d.",
722 pEnemyGroup->ubSectorY + 'A' - 1, pEnemyGroup->ubSectorX,
723 pPlayerGroup->ubSectorY + 'A' - 1, pPlayerGroup->ubSectorX,
724 pPlayerGroup->ubSectorY + 'A' - 1, pPlayerGroup->ubSectorX );
725 }
726 return TRUE;
727 }
728
729
730 static void ConvertGroupTroopsToComposition(GROUP* pGroup, INT32 iCompositionID);
731 static void RemoveSoldiersFromGarrisonBasedOnComposition(INT32 iGarrisonID, UINT8 ubSize);
732
733
HandlePlayerGroupNoticedByGarrison(const GROUP * const pPlayerGroup,const UINT8 ubSectorID)734 static void HandlePlayerGroupNoticedByGarrison(const GROUP* const pPlayerGroup, const UINT8 ubSectorID)
735 {
736 SECTORINFO *pSector;
737 GROUP *pGroup;
738 INT32 iReinforcementsApproved;
739 UINT16 usOffensePoints, usDefencePoints;
740 UINT8 ubEnemies;
741 pSector = &SectorInfo[ ubSectorID ];
742 //First check to see if the player is at his final destination.
743 if( !GroupAtFinalDestination( pPlayerGroup ) )
744 {
745 return;
746 }
747 usOffensePoints = pSector->ubNumAdmins * 2 +
748 pSector->ubNumTroops * 4 +
749 pSector->ubNumElites * 6;
750 if( PlayerForceTooStrong( ubSectorID, usOffensePoints, &usDefencePoints ) )
751 {
752 RequestAttackOnSector( ubSectorID, usDefencePoints );
753 return;
754 }
755
756 if( pSector->ubGarrisonID != NO_GARRISON )
757 {
758 //Decide whether or not they will attack them with some of the troops.
759 ubEnemies = (UINT8)(pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites);
760 iReinforcementsApproved = (ubEnemies - gArmyComp[ gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bDesiredPopulation / 2);
761 if( iReinforcementsApproved*2 > pPlayerGroup->ubGroupSize*3 && iReinforcementsApproved > gubMinEnemyGroupSize )
762 { //Then enemy's available outnumber the player by at least 3:2, so attack them.
763 pGroup = CreateNewEnemyGroupDepartingFromSector( ubSectorID, 0, (UINT8)iReinforcementsApproved, 0 );
764
765 ConvertGroupTroopsToComposition( pGroup, gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition );
766
767 MoveSAIGroupToSector( &pGroup, (UINT8)SECTOR( pPlayerGroup->ubSectorX, pPlayerGroup->ubSectorY ), DIRECT, REINFORCEMENTS );
768
769 RemoveSoldiersFromGarrisonBasedOnComposition( pSector->ubGarrisonID, pGroup->ubGroupSize );
770
771 if( pSector->ubNumTroops + pSector->ubNumElites + pSector->ubNumAdmins > MAX_STRATEGIC_TEAM_SIZE )
772 {
773 SLOGE("Sector %c%d now has %d enemies (max %d).",
774 gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID / 16 + 'A' , gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID % 16,
775 pSector->ubNumTroops + pSector->ubNumElites + pSector->ubNumAdmins, MAX_STRATEGIC_TEAM_SIZE );
776 }
777
778 SLOGD("Enemy garrison at %c%d detected stopped player group at %c%d and is sending %d troops to attack.",
779 gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID / 16 + 'A' , gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID % 16,
780 pPlayerGroup->ubSectorY + 'A' - 1, pPlayerGroup->ubSectorX,
781 pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumTroops );
782 }
783 }
784 }
785
786
HandleMilitiaNoticedByPatrolGroup(UINT8 ubSectorID,GROUP * pEnemyGroup)787 static BOOLEAN HandleMilitiaNoticedByPatrolGroup(UINT8 ubSectorID, GROUP* pEnemyGroup)
788 {
789 //For now, automatically attack.
790 UINT16 usOffensePoints, usDefencePoints;
791 UINT8 ubSectorX = (UINT8)(ubSectorID % 16) + 1;
792 UINT8 ubSectorY = (UINT8)(ubSectorID / 16) + 1;
793 usOffensePoints = pEnemyGroup->pEnemyGroup->ubNumAdmins * 2 +
794 pEnemyGroup->pEnemyGroup->ubNumTroops * 4 +
795 pEnemyGroup->pEnemyGroup->ubNumElites * 6;
796 if( PlayerForceTooStrong( ubSectorID, usOffensePoints, &usDefencePoints ) )
797 {
798 RequestAttackOnSector( ubSectorID, usDefencePoints );
799 return FALSE;
800 }
801
802 MoveSAIGroupToSector( &pEnemyGroup, (UINT8)SECTOR( ubSectorX, ubSectorY ), DIRECT, REINFORCEMENTS );
803 SLOGD("Enemy group at %c%d detected militia at %c%d and is moving to attack them.",
804 pEnemyGroup->ubSectorY + 'A' - 1, pEnemyGroup->ubSectorX,
805 ubSectorY + 'A' - 1, ubSectorX );
806 return FALSE;
807 }
808
809
AttemptToNoticeEmptySectorSucceeds(void)810 static BOOLEAN AttemptToNoticeEmptySectorSucceeds(void)
811 {
812 if( gubNumAwareBattles || gfAutoAIAware )
813 { //The queen is in high-alert and is searching for players. All adjacent checks will automatically succeed.
814 return TRUE;
815 }
816 if( DayTime() )
817 { //Day time chances are normal
818 if( Chance( giArmyAlertness ) )
819 {
820 giArmyAlertness -= giArmyAlertnessDecay;
821 //Minimum alertness should always be at least 0.
822 giArmyAlertness = MAX( 0, giArmyAlertness );
823 return TRUE;
824 }
825 giArmyAlertness++;
826 return FALSE;
827 }
828 //Night time chances are one third of normal.
829 if( Chance( giArmyAlertness/3 ) )
830 {
831 giArmyAlertness -= giArmyAlertnessDecay;
832 //Minimum alertness should always be at least 0.
833 giArmyAlertness = MAX( 0, giArmyAlertness );
834 return TRUE;
835 }
836 if( Chance( 33 ) )
837 {
838 giArmyAlertness++;
839 }
840 return FALSE;
841 }
842
843
844 //Calling the function assumes that a player group is found to be adjacent to an enemy group.
845 //This uses the alertness rating to emulate the chance that the group will notice. If it does
846 //notice, then the alertness drops accordingly to simulate a period of time where the enemy would
847 //not notice as much. If it fails, the alertness gradually increases until it succeeds.
AttemptToNoticeAdjacentGroupSucceeds(void)848 static BOOLEAN AttemptToNoticeAdjacentGroupSucceeds(void)
849 {
850 if( gubNumAwareBattles || gfAutoAIAware )
851 { //The queen is in high-alert and is searching for players. All adjacent checks will automatically succeed.
852 return TRUE;
853 }
854 if( DayTime() )
855 { //Day time chances are normal
856 if( Chance( giArmyAlertness ) )
857 {
858 giArmyAlertness -= giArmyAlertnessDecay;
859 //Minimum alertness should always be at least 0.
860 giArmyAlertness = MAX( 0, giArmyAlertness );
861 return TRUE;
862 }
863 giArmyAlertness++;
864 return FALSE;
865 }
866 //Night time chances are one third of normal.
867 if( Chance( giArmyAlertness/3 ) )
868 {
869 giArmyAlertness -= giArmyAlertnessDecay;
870 //Minimum alertness should always be at least 0.
871 giArmyAlertness = MAX( 0, giArmyAlertness );
872 return TRUE;
873 }
874 if( Chance( 33 ) )
875 {
876 giArmyAlertness++;
877 }
878 return FALSE;
879 }
880
881
HandleEmptySectorNoticedByPatrolGroup(GROUP * pGroup,UINT8 ubEmptySectorID)882 static BOOLEAN HandleEmptySectorNoticedByPatrolGroup(GROUP* pGroup, UINT8 ubEmptySectorID)
883 {
884 UINT8 ubGarrisonID;
885 UINT8 ubSectorX = (UINT8)(ubEmptySectorID % 16) + 1;
886 UINT8 ubSectorY = (UINT8)(ubEmptySectorID / 16) + 1;
887
888 ubGarrisonID = SectorInfo[ ubEmptySectorID ].ubGarrisonID;
889 if( ubGarrisonID != NO_GARRISON )
890 {
891 if( gGarrisonGroup[ ubGarrisonID ].ubPendingGroupID )
892 {
893 return FALSE;
894 }
895 }
896 else
897 {
898 return FALSE;
899 }
900
901 //Clear the patrol group's previous orders.
902 RemoveGroupFromStrategicAILists(*pGroup);
903
904 gGarrisonGroup[ ubGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
905 MoveSAIGroupToSector( &pGroup, (UINT8)SECTOR( ubSectorX, ubSectorY ), DIRECT, REINFORCEMENTS );
906 SLOGD("Enemy group at %c%d detected undefended sector at %c%d and is moving to retake it.",
907 pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX,
908 ubSectorY + 'A' - 1, ubSectorX );
909 return TRUE;
910 }
911
912
HandleEmptySectorNoticedByGarrison(UINT8 ubGarrisonSectorID,UINT8 ubEmptySectorID)913 static void HandleEmptySectorNoticedByGarrison(UINT8 ubGarrisonSectorID, UINT8 ubEmptySectorID)
914 {
915 SECTORINFO *pSector;
916 GROUP *pGroup;
917 UINT8 ubAvailableTroops;
918 UINT8 ubSrcGarrisonID = 255, ubDstGarrisonID = 255;
919
920 //Make sure that the destination sector doesn't already have a pending group.
921 pSector = &SectorInfo[ ubEmptySectorID ];
922
923 ubSrcGarrisonID = SectorInfo[ ubGarrisonSectorID ].ubGarrisonID;
924 ubDstGarrisonID = SectorInfo[ ubEmptySectorID ].ubGarrisonID;
925
926 if( ubSrcGarrisonID == NO_GARRISON || ubDstGarrisonID == NO_GARRISON )
927 { //Bad logic
928 return;
929 }
930
931 if( gGarrisonGroup[ ubDstGarrisonID ].ubPendingGroupID )
932 { //A group is already on-route, so don't send anybody from here.
933 return;
934 }
935
936 //An opportunity has arisen, where the enemy has noticed an important sector that is undefended.
937 pSector = &SectorInfo[ ubGarrisonSectorID ];
938 ubAvailableTroops = pSector->ubNumTroops + pSector->ubNumElites + pSector->ubNumAdmins;
939
940 if( ubAvailableTroops >= gubMinEnemyGroupSize * 2 )
941 { //split group into two groups, and move one of the groups to the next sector.
942 pGroup = CreateNewEnemyGroupDepartingFromSector( ubGarrisonSectorID, 0, (UINT8)(ubAvailableTroops / 2), 0 );
943 ConvertGroupTroopsToComposition( pGroup, gGarrisonGroup[ ubDstGarrisonID ].ubComposition );
944 RemoveSoldiersFromGarrisonBasedOnComposition( ubSrcGarrisonID, pGroup->ubGroupSize );
945 gGarrisonGroup[ ubDstGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
946 MoveSAIGroupToSector( &pGroup, ubEmptySectorID, DIRECT, REINFORCEMENTS );
947 }
948 }
949
950
ReinforcementsApproved(INT32 iGarrisonID,UINT16 * pusDefencePoints)951 static BOOLEAN ReinforcementsApproved(INT32 iGarrisonID, UINT16* pusDefencePoints)
952 {
953 SECTORINFO *pSector;
954 UINT16 usOffensePoints;
955 UINT8 ubSectorX, ubSectorY;
956
957 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
958 ubSectorX = (UINT8)SECTORX( gGarrisonGroup[ iGarrisonID ].ubSectorID );
959 ubSectorY = (UINT8)SECTORY( gGarrisonGroup[ iGarrisonID ].ubSectorID );
960
961 *pusDefencePoints = pSector->ubNumberOfCivsAtLevel[ GREEN_MILITIA ] * 1 +
962 pSector->ubNumberOfCivsAtLevel[ REGULAR_MILITIA ] * 2 +
963 pSector->ubNumberOfCivsAtLevel[ ELITE_MILITIA ] * 3 +
964 PlayerMercsInSector( ubSectorX, ubSectorY, 0 ) * 4;
965 usOffensePoints = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bAdminPercentage * 2 +
966 gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bTroopPercentage * 3 +
967 gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bElitePercentage * 4 +
968 gubGarrisonReinforcementsDenied[ iGarrisonID ];
969 usOffensePoints = usOffensePoints * gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation / 100;
970
971 if( usOffensePoints > *pusDefencePoints )
972 {
973 return TRUE;
974 }
975 //Before returning false, determine if reinforcements have been denied repeatedly. If so, then
976 //we might send an augmented force to take it back.
977 if( gubGarrisonReinforcementsDenied[ iGarrisonID ] + usOffensePoints > *pusDefencePoints )
978 {
979 SLOGD("Sector %c%d will now recieve an %d extra troops due to multiple denials for reinforcements in the past for strong player presence.",
980 ubSectorY + 'A' - 1, ubSectorX, gubGarrisonReinforcementsDenied[ iGarrisonID ] / 3 );
981 return TRUE;
982 }
983 //Reinforcements will have to wait. For now, increase the reinforcements denied. The amount increase is 20 percent
984 //of the garrison's priority.
985 gubGarrisonReinforcementsDenied[ iGarrisonID ] += (UINT8)(gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bPriority / 2);
986
987 return FALSE;
988 }
989
990
991 static void EliminateSurplusTroopsForGarrison(GROUP* pGroup, SECTORINFO* pSector);
992 static void RecalculateGarrisonWeight(INT32 iGarrisonID);
993 static void RecalculatePatrolWeight(PATROL_GROUP&);
994
995
996 //if the group has arrived in a sector, and doesn't have any particular orders, then
997 //send him back where they came from.
998 //RETURNS TRUE if the group is deleted or told to move somewhere else.
999 //This is important as the calling function will need
1000 //to abort processing of the group for obvious reasons.
EvaluateGroupSituation(GROUP * pGroup)1001 static BOOLEAN EvaluateGroupSituation(GROUP* pGroup)
1002 {
1003 SECTORINFO *pSector;
1004 GROUP *pPatrolGroup;
1005 UINT32 i;
1006
1007 if( !gfQueenAIAwake )
1008 {
1009 return FALSE;
1010 }
1011 Assert( !pGroup->fPlayer );
1012 if( pGroup->pEnemyGroup->ubIntention == PURSUIT )
1013 { //Lost the player group that he was going to attack. Return to original position.
1014 ReassignAIGroup( &pGroup );
1015 return TRUE;
1016 }
1017 else if( pGroup->pEnemyGroup->ubIntention == REINFORCEMENTS )
1018 { //The group has arrived at the location where he is supposed to reinforce.
1019 //Step 1 -- Check for matching garrison location
1020 for( i = 0; i < gGarrisonGroup.size(); i++ )
1021 {
1022 if( gGarrisonGroup[ i ].ubSectorID == SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) &&
1023 gGarrisonGroup[ i ].ubPendingGroupID == pGroup->ubGroupID )
1024 {
1025 pSector = &SectorInfo[ SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) ];
1026
1027 if( gGarrisonGroup[ i ].ubSectorID != SEC_P3 )
1028 {
1029 EliminateSurplusTroopsForGarrison( pGroup, pSector );
1030 pSector->ubNumAdmins = (UINT8)(pSector->ubNumAdmins + pGroup->pEnemyGroup->ubNumAdmins);
1031 pSector->ubNumTroops = (UINT8)(pSector->ubNumTroops + pGroup->pEnemyGroup->ubNumTroops);
1032 pSector->ubNumElites = (UINT8)(pSector->ubNumElites + pGroup->pEnemyGroup->ubNumElites);
1033
1034 SLOGD("%d reinforcements have arrived to garrison sector %c%d",
1035 pGroup->pEnemyGroup->ubNumAdmins + pGroup->pEnemyGroup->ubNumTroops +
1036 pGroup->pEnemyGroup->ubNumElites, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX );
1037 if( IsThisSectorASAMSector( pGroup->ubSectorX, pGroup->ubSectorY, 0 ) )
1038 {
1039 StrategicMap[ pGroup->ubSectorX + pGroup->ubSectorY * MAP_WORLD_X ].bSAMCondition = 100;
1040 UpdateSAMDoneRepair( pGroup->ubSectorX, pGroup->ubSectorY, 0 );
1041 }
1042 }
1043 else
1044 { //The group was sent back to the queen's palace (probably because they couldn't be reassigned
1045 //anywhere else, but it is possible that the queen's sector is requesting the reinforcements. In
1046 //any case, if the queen's sector is less than full strength, fill it up first, then
1047 //simply add the rest to the global pool.
1048 if( pSector->ubNumElites < MAX_STRATEGIC_TEAM_SIZE )
1049 {
1050 if( pSector->ubNumElites + pGroup->ubGroupSize >= MAX_STRATEGIC_TEAM_SIZE )
1051 { //Fill up the queen's guards, then apply the rest to the reinforcement pool
1052 giReinforcementPool += MAX_STRATEGIC_TEAM_SIZE - pSector->ubNumElites;
1053 pSector->ubNumElites = MAX_STRATEGIC_TEAM_SIZE;
1054 SLOGD("%d reinforcements have arrived to garrison queen's sector. The excess troops will be relocated to the reinforcement pool.",
1055 pGroup->ubGroupSize );
1056 }
1057 else
1058 { //Add all the troops to the queen's guard.
1059 pSector->ubNumElites += pGroup->ubGroupSize;
1060 SLOGD(ST::format("{} reinforcements have arrived to garrison queen's sector ({}{}).",
1061 pGroup->ubGroupSize, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX));
1062 }
1063 }
1064 else
1065 { //Add all the troops to the reinforcement pool as the queen's guard is at full strength.
1066 giReinforcementPool += pGroup->ubGroupSize;
1067 SLOGD(ST::format("{} reinforcements have arrived at queen's sector ({}{}) and have been added to the reinforcement pool.",
1068 pGroup->ubGroupSize, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX));
1069 }
1070 }
1071
1072 SetThisSectorAsEnemyControlled(pGroup->ubSectorX, pGroup->ubSectorY, 0);
1073 RemoveGroup(*pGroup);
1074 RecalculateGarrisonWeight( i );
1075
1076 return TRUE;
1077 }
1078 }
1079 //Step 2 -- Check for Patrol groups matching waypoint index.
1080 for( i = 0; i < gPatrolGroup.size(); i++ )
1081 {
1082 if( gPatrolGroup[ i ].ubSectorID[ 1 ] == SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) &&
1083 gPatrolGroup[ i ].ubPendingGroupID == pGroup->ubGroupID )
1084 {
1085 gPatrolGroup[ i ].ubPendingGroupID = 0;
1086 if( gPatrolGroup[ i ].ubGroupID && gPatrolGroup[ i ].ubGroupID != pGroup->ubGroupID )
1087 { //cheat, and warp our reinforcements to them!
1088 pPatrolGroup = GetGroup( gPatrolGroup[ i ].ubGroupID );
1089 pPatrolGroup->pEnemyGroup->ubNumTroops += pGroup->pEnemyGroup->ubNumTroops;
1090 pPatrolGroup->pEnemyGroup->ubNumElites += pGroup->pEnemyGroup->ubNumElites;
1091 pPatrolGroup->pEnemyGroup->ubNumAdmins += pGroup->pEnemyGroup->ubNumAdmins;
1092 pPatrolGroup->ubGroupSize += (UINT8)(pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins );
1093 SLOGD("%d reinforcements have joined patrol group at sector %c%d (new size: %d)",
1094 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1095 pPatrolGroup->ubSectorY + 'A' - 1, pPatrolGroup->ubSectorX, pPatrolGroup->ubGroupSize );
1096 if( pPatrolGroup->ubGroupSize > MAX_STRATEGIC_TEAM_SIZE )
1097 {
1098 UINT8 ubCut;
1099 SLOGE( "Patrol group #%d in %c%d received too many reinforcements from group #%d that was created in %c%d. Size truncated from %d to %d.\n\
1100 Please send Strategic Decisions.txt and PRIOR save.",
1101 pPatrolGroup->ubGroupID, pPatrolGroup->ubSectorY + 'A' - 1, pPatrolGroup->ubSectorX,
1102 pGroup->ubGroupID, SECTORY( pGroup->ubCreatedSectorID ) + 'A' - 1, SECTORX( pGroup->ubCreatedSectorID ),
1103 pPatrolGroup->ubGroupSize, MAX_STRATEGIC_TEAM_SIZE );
1104 //truncate the group size.
1105 ubCut = pPatrolGroup->ubGroupSize - MAX_STRATEGIC_TEAM_SIZE;
1106 while( ubCut-- )
1107 {
1108 if( pGroup->pEnemyGroup->ubNumAdmins )
1109 {
1110 pGroup->pEnemyGroup->ubNumAdmins--;
1111 pPatrolGroup->pEnemyGroup->ubNumAdmins--;
1112 }
1113 else if( pGroup->pEnemyGroup->ubNumTroops )
1114 {
1115 pGroup->pEnemyGroup->ubNumTroops--;
1116 pPatrolGroup->pEnemyGroup->ubNumTroops--;
1117 }
1118 else if( pGroup->pEnemyGroup->ubNumElites )
1119 {
1120 pGroup->pEnemyGroup->ubNumElites--;
1121 pPatrolGroup->pEnemyGroup->ubNumElites--;
1122 }
1123 }
1124 pPatrolGroup->ubGroupSize = MAX_STRATEGIC_TEAM_SIZE;
1125 Assert( pPatrolGroup->pEnemyGroup->ubNumAdmins +
1126 pPatrolGroup->pEnemyGroup->ubNumTroops +
1127 pPatrolGroup->pEnemyGroup->ubNumElites == MAX_STRATEGIC_TEAM_SIZE );
1128 }
1129 RemoveGroup(*pGroup);
1130 RecalculatePatrolWeight(gPatrolGroup[i]);
1131 ValidateLargeGroup( pPatrolGroup );
1132 }
1133 else
1134 { //the reinforcements have become the new patrol group (even if same group)
1135 gPatrolGroup[ i ].ubGroupID = pGroup->ubGroupID;
1136 pGroup->pEnemyGroup->ubIntention = PATROL;
1137 pGroup->ubMoveType = ENDTOEND_FORWARDS;
1138 RemoveGroupWaypoints(*pGroup);
1139 AddWaypointIDToPGroup( pGroup, gPatrolGroup[ i ].ubSectorID[ 0 ] );
1140 AddWaypointIDToPGroup( pGroup, gPatrolGroup[ i ].ubSectorID[ 1 ] );
1141 if( gPatrolGroup[ i ].ubSectorID[ 2 ] )
1142 { //Add optional waypoints if included.
1143 AddWaypointIDToPGroup( pGroup, gPatrolGroup[ i ].ubSectorID[ 2 ] );
1144 if( gPatrolGroup[ i ].ubSectorID[ 3 ] )
1145 AddWaypointIDToPGroup( pGroup, gPatrolGroup[ i ].ubSectorID[ 3 ] );
1146 }
1147 SLOGD("%d soldiers have arrived to patrol area near sector %c%d",
1148 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1149 pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX );
1150 RecalculatePatrolWeight(gPatrolGroup[i]);
1151 }
1152 return TRUE;
1153 }
1154 }
1155 }
1156 else
1157 { //This is a floating group at his final destination...
1158 if( pGroup->pEnemyGroup->ubIntention != STAGING && pGroup->pEnemyGroup->ubIntention != REINFORCEMENTS )
1159 {
1160 ReassignAIGroup( &pGroup );
1161 return TRUE;
1162 }
1163 }
1164 return FALSE;
1165 }
1166
1167
EnemyNoticesPlayerArrival(GROUP const & pg,UINT8 const x,UINT8 const y)1168 static bool EnemyNoticesPlayerArrival(GROUP const& pg, UINT8 const x, UINT8 const y)
1169 {
1170 GROUP* const eg = FindEnemyMovementGroupInSector(x, y);
1171 if (eg && AttemptToNoticeAdjacentGroupSucceeds())
1172 {
1173 HandlePlayerGroupNoticedByPatrolGroup(&pg, eg);
1174 return true;
1175 }
1176
1177 SECTORINFO const& s = SectorInfo[SECTOR(x, y)];
1178 UINT8 const n_enemies = s.ubNumAdmins + s.ubNumTroops + s.ubNumElites;
1179 if (n_enemies && s.ubGarrisonID != NO_GARRISON && AttemptToNoticeAdjacentGroupSucceeds())
1180 {
1181 HandlePlayerGroupNoticedByGarrison(&pg, SECTOR(x, y));
1182 return true;
1183 }
1184
1185 return false;
1186 }
1187
1188
1189 static void SendGroupToPool(GROUP** pGroup);
1190
1191
1192 //returns TRUE if the group was deleted.
StrategicAILookForAdjacentGroups(GROUP * pGroup)1193 BOOLEAN StrategicAILookForAdjacentGroups( GROUP *pGroup )
1194 {
1195 UINT8 ubSectorID;
1196 if( !gfQueenAIAwake )
1197 { //The queen isn't aware the player's presence yet, so she is oblivious to any situations.
1198
1199 if( !pGroup->fPlayer )
1200 {
1201 //Exception case!
1202 //In the beginning of the game, a group is sent to A9 after the first battle. If you leave A9, when they arrive,
1203 //they will stay there indefinately because the AI isn't awake. What we do, is if this is a group in A9, then
1204 //send them home.
1205 if( GroupAtFinalDestination( pGroup ) )
1206 {
1207 //Wake up the queen now, if she hasn't woken up already.
1208 WakeUpQueen();
1209 if( (pGroup->ubSectorX == 9 && pGroup->ubSectorY == 1) ||
1210 (pGroup->ubSectorX == 3 && pGroup->ubSectorY == 16) )
1211 {
1212 SendGroupToPool( &pGroup );
1213 if( !pGroup )
1214 { //Group was transferred to the pool
1215 return TRUE;
1216 }
1217 }
1218 }
1219 }
1220
1221 if( !gfQueenAIAwake )
1222 {
1223 return FALSE;
1224 }
1225 }
1226 if( !pGroup->fPlayer )
1227 { //The enemy group has arrived at a new sector and now controls it.
1228 //Look in each of the four directions, and the alertness rating will
1229 //determine the chance to detect any players that may exist in that sector.
1230 GROUP* pEnemyGroup = pGroup;
1231 if( GroupAtFinalDestination( pEnemyGroup ) )
1232 {
1233 return EvaluateGroupSituation( pEnemyGroup );
1234 }
1235 ubSectorID = (UINT8)SECTOR( pEnemyGroup->ubSectorX, pEnemyGroup->ubSectorY );
1236 if( pEnemyGroup && pEnemyGroup->ubSectorY > 1 && EnemyPermittedToAttackSector( &pEnemyGroup, (UINT8)(ubSectorID - 16) ) )
1237 {
1238 GROUP* const pPlayerGroup = FindPlayerMovementGroupInSector(pEnemyGroup->ubSectorX, pEnemyGroup->ubSectorY - 1);
1239 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1240 {
1241 return HandlePlayerGroupNoticedByPatrolGroup( pPlayerGroup, pEnemyGroup );
1242 }
1243 else if( CountAllMilitiaInSector( pEnemyGroup->ubSectorX, (UINT8)(pEnemyGroup->ubSectorY-1) ) &&
1244 AttemptToNoticeAdjacentGroupSucceeds() )
1245 {
1246 return HandleMilitiaNoticedByPatrolGroup( (UINT8)SECTOR( pEnemyGroup->ubSectorX, pEnemyGroup->ubSectorY-1 ), pEnemyGroup );
1247 }
1248 else if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID-16) ) && AttemptToNoticeEmptySectorSucceeds() )
1249 {
1250 return HandleEmptySectorNoticedByPatrolGroup( pEnemyGroup, (UINT8)(ubSectorID-16) );
1251 }
1252 }
1253 if( pEnemyGroup && pEnemyGroup->ubSectorX > 1 && EnemyPermittedToAttackSector( &pEnemyGroup, (UINT8)(ubSectorID - 1) ) )
1254 {
1255 GROUP* const pPlayerGroup = FindPlayerMovementGroupInSector(pEnemyGroup->ubSectorX - 1, pEnemyGroup->ubSectorY);
1256 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1257 {
1258 return HandlePlayerGroupNoticedByPatrolGroup( pPlayerGroup, pEnemyGroup );
1259 }
1260 else if( CountAllMilitiaInSector( (UINT8)(pEnemyGroup->ubSectorX-1), pEnemyGroup->ubSectorY ) &&
1261 AttemptToNoticeAdjacentGroupSucceeds() )
1262 {
1263 return HandleMilitiaNoticedByPatrolGroup( (UINT8)SECTOR( pEnemyGroup->ubSectorX-1, pEnemyGroup->ubSectorY ), pEnemyGroup );
1264 }
1265 else if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID-1) ) && AttemptToNoticeEmptySectorSucceeds() )
1266 {
1267 return HandleEmptySectorNoticedByPatrolGroup( pEnemyGroup, (UINT8)(ubSectorID-1) );
1268 }
1269 }
1270 if( pEnemyGroup && pEnemyGroup->ubSectorY < 16 && EnemyPermittedToAttackSector( &pEnemyGroup, (UINT8)(ubSectorID + 16) ) )
1271 {
1272 GROUP* const pPlayerGroup = FindPlayerMovementGroupInSector(pEnemyGroup->ubSectorX, pEnemyGroup->ubSectorY + 1);
1273 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1274 {
1275 return HandlePlayerGroupNoticedByPatrolGroup( pPlayerGroup, pEnemyGroup );
1276 }
1277 else if( CountAllMilitiaInSector( pEnemyGroup->ubSectorX, (UINT8)(pEnemyGroup->ubSectorY+1) ) &&
1278 AttemptToNoticeAdjacentGroupSucceeds() )
1279 {
1280 return HandleMilitiaNoticedByPatrolGroup( (UINT8)SECTOR( pEnemyGroup->ubSectorX, pEnemyGroup->ubSectorY+1 ), pEnemyGroup );
1281 }
1282 else if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID+16) ) && AttemptToNoticeEmptySectorSucceeds() )
1283 {
1284 return HandleEmptySectorNoticedByPatrolGroup( pEnemyGroup, (UINT8)(ubSectorID+16) );
1285 }
1286 }
1287 if( pEnemyGroup && pEnemyGroup->ubSectorX < 16 && EnemyPermittedToAttackSector( &pEnemyGroup, (UINT8)(ubSectorID + 1) ) )
1288 {
1289 GROUP* const pPlayerGroup = FindPlayerMovementGroupInSector(pEnemyGroup->ubSectorX + 1, pEnemyGroup->ubSectorY);
1290 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1291 {
1292 return HandlePlayerGroupNoticedByPatrolGroup( pPlayerGroup, pEnemyGroup );
1293 }
1294 else if( CountAllMilitiaInSector( (UINT8)(pEnemyGroup->ubSectorX+1), pEnemyGroup->ubSectorY ) &&
1295 AttemptToNoticeAdjacentGroupSucceeds() )
1296 {
1297 return HandleMilitiaNoticedByPatrolGroup( (UINT8)SECTOR( pEnemyGroup->ubSectorX+1, pEnemyGroup->ubSectorY ), pEnemyGroup );
1298 }
1299 else if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID+1) ) && AttemptToNoticeEmptySectorSucceeds() )
1300 {
1301 return HandleEmptySectorNoticedByPatrolGroup( pEnemyGroup, (UINT8)(ubSectorID+1) );
1302 }
1303 }
1304 if( !pEnemyGroup )
1305 { //group deleted.
1306 return TRUE;
1307 }
1308 }
1309 else
1310 { /* The player group has arrived at a new sector and now controls it. Look
1311 * in each of the four directions, and the enemy alertness rating will
1312 * determine if the enemy notices that the player is here. Additionally,
1313 * there are also stationary enemy groups that may also notice the player's
1314 * new presence.
1315 * NOTE: Always returns false because it is the player group that we are
1316 * handling. We don't mess with the player group here! */
1317 GROUP const& pg = *pGroup;
1318 if (pg.ubSectorZ != 0) return FALSE;
1319 UINT8 const x = pg.ubSectorX;
1320 UINT8 const y = pg.ubSectorY;
1321 if (!EnemyPermittedToAttackSector(0, SECTOR(x, y))) return FALSE;
1322 if (y > 1 && EnemyNoticesPlayerArrival(pg, x, y - 1)) return FALSE;
1323 if (x < 16 && EnemyNoticesPlayerArrival(pg, x + 1, y)) return FALSE;
1324 if (y < 16 && EnemyNoticesPlayerArrival(pg, x, y + 1)) return FALSE;
1325 if (x > 1 && EnemyNoticesPlayerArrival(pg, x - 1, y)) return FALSE;
1326 }
1327 return FALSE;
1328 }
1329
1330
1331 //This is called periodically for each enemy occupied sector containing garrisons.
CheckEnemyControlledSector(UINT8 ubSectorID)1332 void CheckEnemyControlledSector( UINT8 ubSectorID )
1333 {
1334 SECTORINFO *pSector;
1335 UINT8 ubSectorX, ubSectorY;
1336 if( !gfQueenAIAwake )
1337 {
1338 return;
1339 }
1340 //First, determine if the sector is still owned by the enemy.
1341 pSector = &SectorInfo[ ubSectorID ];
1342 if( pSector->ubGarrisonID != NO_GARRISON )
1343 {
1344 if( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID )
1345 { //Look for a staging group.
1346 GROUP *pGroup;
1347 pGroup = GetGroup( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID );
1348 if( pGroup )
1349 { //We have a staging group
1350 if( GroupAtFinalDestination( pGroup ) )
1351 {
1352 if( pGroup->pEnemyGroup->ubPendingReinforcements )
1353 {
1354 if( pGroup->pEnemyGroup->ubPendingReinforcements > 4 )
1355 {
1356 UINT8 ubNum = (UINT8)(3 + Random( 3 ));
1357 pGroup->pEnemyGroup->ubNumTroops += ubNum;
1358 pGroup->ubGroupSize += ubNum;
1359 pGroup->pEnemyGroup->ubPendingReinforcements -= ubNum;
1360 RecalculateGroupWeight(*pGroup);
1361 ValidateLargeGroup( pGroup );
1362 }
1363 else
1364 {
1365 pGroup->pEnemyGroup->ubNumTroops += pGroup->pEnemyGroup->ubPendingReinforcements;
1366 pGroup->ubGroupSize += pGroup->pEnemyGroup->ubPendingReinforcements;
1367 pGroup->pEnemyGroup->ubPendingReinforcements = 0;
1368 ValidateLargeGroup( pGroup );
1369 }
1370 }
1371 else if( SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) != gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID )
1372 {
1373 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ pSector->ubGarrisonID ].ubSectorID, DIRECT, pGroup->pEnemyGroup->ubIntention );
1374 }
1375 }
1376 //else the group is on route to stage hopefully...
1377 }
1378 }
1379 }
1380 if( pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites )
1381 {
1382
1383 //The sector is still controlled, so look around to see if there are any players nearby.
1384 ubSectorX = (UINT8)SECTORX( ubSectorID );
1385 ubSectorY = (UINT8)SECTORY( ubSectorID );
1386 if( ubSectorY > 1 && EnemyPermittedToAttackSector( NULL, (UINT8)(ubSectorID - 16) ) )
1387 {
1388 /*
1389 pPlayerGroup = FindPlayerMovementGroupInSector(bSectorX, ubSectorY - 1);
1390 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1391 {
1392 HandlePlayerGroupNoticedByGarrison( pPlayerGroup, ubSectorID );
1393 return;
1394 }
1395 else
1396 */
1397 if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID-16) ) && AttemptToNoticeEmptySectorSucceeds() )
1398 {
1399 HandleEmptySectorNoticedByGarrison( ubSectorID, (UINT8)(ubSectorID-16) );
1400 return;
1401 }
1402 }
1403 if( ubSectorX < 16 && EnemyPermittedToAttackSector( NULL, (UINT8)(ubSectorID + 1) ) )
1404 {
1405 /*
1406 pPlayerGroup = FindPlayerMovementGroupInSector(ubSectorX + 1, ubSectorY);
1407 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1408 {
1409 HandlePlayerGroupNoticedByGarrison( pPlayerGroup, ubSectorID );
1410 return;
1411 }
1412 else
1413 */
1414 if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID+1) ) && AttemptToNoticeEmptySectorSucceeds() )
1415 {
1416 HandleEmptySectorNoticedByGarrison( ubSectorID, (UINT8)(ubSectorID+1) );
1417 return;
1418 }
1419 }
1420 if( ubSectorY < 16 && EnemyPermittedToAttackSector( NULL, (UINT8)(ubSectorID + 16) ) )
1421 {
1422 /*
1423 pPlayerGroup = FindPlayerMovementGroupInSector(ubSectorX, ubSectorY + 1);
1424 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1425 {
1426 HandlePlayerGroupNoticedByGarrison( pPlayerGroup, ubSectorID );
1427 return;
1428 }
1429 else
1430 */
1431 if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID+16) ) && AttemptToNoticeEmptySectorSucceeds() )
1432 {
1433 HandleEmptySectorNoticedByGarrison( ubSectorID, (UINT8)(ubSectorID+16) );
1434 return;
1435 }
1436 }
1437 if( ubSectorX > 1 && EnemyPermittedToAttackSector( NULL, (UINT8)(ubSectorID - 1) ) )
1438 {
1439 /*
1440 pPlayerGroup = FindPlayerMovementGroupInSector(ubSectorX - 1, ubSectorY);
1441 if( pPlayerGroup && AttemptToNoticeAdjacentGroupSucceeds() )
1442 {
1443 HandlePlayerGroupNoticedByGarrison( pPlayerGroup, ubSectorID );
1444 return;
1445 }
1446 else
1447 */
1448 if( AdjacentSectorIsImportantAndUndefended( (UINT8)(ubSectorID-1) ) && AttemptToNoticeEmptySectorSucceeds() )
1449 {
1450 HandleEmptySectorNoticedByGarrison( ubSectorID, (UINT8)(ubSectorID-1) );
1451 return;
1452 }
1453 }
1454 }
1455 }
1456
1457
RemoveGroupFromStrategicAILists(GROUP const & g)1458 void RemoveGroupFromStrategicAILists(GROUP const& g)
1459 {
1460 UINT8 const group_id = g.ubGroupID;
1461 for (size_t i = 0; i < gPatrolGroup.size(); ++i)
1462 {
1463 PATROL_GROUP& pg = gPatrolGroup[i];
1464 if (pg.ubGroupID == group_id)
1465 { // Patrol group was destroyed
1466 pg.ubGroupID = 0;
1467 RecalculatePatrolWeight(pg);
1468 return;
1469 }
1470 if (pg.ubPendingGroupID == group_id)
1471 { // Group never arrived to reinforce
1472 pg.ubPendingGroupID = 0;
1473 return;
1474 }
1475 }
1476 for (size_t i = 0; i < gGarrisonGroup.size(); ++i)
1477 {
1478 GARRISON_GROUP& gg = gGarrisonGroup[i];
1479 if (gg.ubPendingGroupID == group_id)
1480 { // Group never arrived to reinforce
1481 gg.ubPendingGroupID = 0;
1482 return;
1483 }
1484 }
1485 }
1486
1487
1488 /* Recalculates a group's weight based on any changes.
1489 * @@@Alex, this is possibly missing in some areas. It is hard to ensure it is
1490 * everywhere with all the changes I've made. I'm sure you could probably find
1491 * some missing calls. */
RecalculatePatrolWeight(PATROL_GROUP & p)1492 static void RecalculatePatrolWeight(PATROL_GROUP& p)
1493 {
1494 // First, remove the previous weight from the applicable field
1495 INT32 const prev_weight = p.bWeight;
1496 if (prev_weight > 0) giRequestPoints -= prev_weight;
1497
1498 INT32 need_population;
1499 if (p.ubGroupID != 0)
1500 {
1501 need_population = p.bSize - GetGroup(p.ubGroupID)->ubGroupSize;
1502 if (need_population < 0)
1503 {
1504 p.bWeight = 0;
1505 return;
1506 }
1507 }
1508 else
1509 {
1510 need_population = p.bSize;
1511 }
1512 INT32 weight = need_population * 3 * p.bPriority / 96;
1513 weight = MIN(weight, 2);
1514 p.bWeight = weight;
1515 giRequestPoints += weight;
1516 }
1517
1518
1519 //Recalculates a group's weight based on any changes.
1520 //@@@Alex, this is possibly missing in some areas. It is hard to ensure it is
1521 //everywhere with all the changes I've made. I'm sure you could probably find some missing calls.
RecalculateGarrisonWeight(INT32 iGarrisonID)1522 static void RecalculateGarrisonWeight(INT32 iGarrisonID)
1523 {
1524 SECTORINFO *pSector;
1525 INT32 iWeight, iPrevWeight;
1526 INT32 iDesiredPop, iCurrentPop, iPriority;
1527
1528 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
1529 iDesiredPop = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation;
1530 iCurrentPop = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
1531 iPriority = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bPriority;
1532
1533 //First, remove the previous weight from the applicable field.
1534 iPrevWeight = gGarrisonGroup[ iGarrisonID ].bWeight;
1535 if( iPrevWeight > 0 )
1536 giRequestPoints -= iPrevWeight;
1537 else if( iPrevWeight < 0 )
1538 giReinforcementPoints += iPrevWeight;
1539
1540 //Calculate weight (range is -20 to +20 before multiplier).
1541 //The multiplier of 3 brings it to a range of -96 to +96 which is
1542 //close enough to a plus/minus 100%. The resultant percentage is then
1543 //converted based on the priority.
1544 iWeight = (iDesiredPop - iCurrentPop) * 3;
1545 if( iWeight > 0 )
1546 { //modify it by it's priority.
1547 //generates a value between 2 and 100
1548 iWeight = iWeight * iPriority / 96;
1549 iWeight = MAX( iWeight, 2 );
1550 giRequestPoints += iWeight;
1551 }
1552 else if( iWeight < 0 )
1553 { //modify it by it's reverse priority
1554 //generates a value between -2 and -100
1555 iWeight = iWeight * (100-iPriority) / 96;
1556 iWeight = MIN( iWeight, -2 );
1557 giReinforcementPoints -= (INT8)iWeight;
1558 }
1559
1560 gGarrisonGroup[ iGarrisonID ].bWeight = (INT8)iWeight;
1561 }
1562
RecalculateSectorWeight(UINT8 ubSectorID)1563 void RecalculateSectorWeight( UINT8 ubSectorID )
1564 {
1565 for( size_t i = 0; i < gGarrisonGroup.size(); i++ )
1566 {
1567 if( gGarrisonGroup[ i ].ubSectorID == ubSectorID )
1568 {
1569 RecalculateGarrisonWeight( i );
1570 return;
1571 }
1572 }
1573 }
1574
1575
1576 static void TagSAIGroupWithGracePeriod(GROUP const&);
1577
1578
RecalculateGroupWeight(GROUP const & g)1579 void RecalculateGroupWeight(GROUP const& g)
1580 {
1581 for (size_t i = 0; i != gPatrolGroup.size(); ++i)
1582 {
1583 PATROL_GROUP& p = gPatrolGroup[i];
1584 if (p.ubGroupID != g.ubGroupID) continue;
1585 if (g.ubGroupSize == 0)
1586 {
1587 TagSAIGroupWithGracePeriod(g);
1588 p.ubGroupID = 0;
1589 }
1590 RecalculatePatrolWeight(p);
1591 return;
1592 }
1593 }
1594
1595
1596 static BOOLEAN GarrisonCanProvideMinimumReinforcements(INT32 iGarrisonID);
1597
1598
ChooseSuitableGarrisonToProvideReinforcements(INT32 iDstGarrisonID,INT32 iReinforcementsRequested)1599 static INT32 ChooseSuitableGarrisonToProvideReinforcements(INT32 iDstGarrisonID, INT32 iReinforcementsRequested)
1600 {
1601 UINT32 iSrcGarrisonID, iBestGarrisonID = NO_GARRISON;
1602 INT32 iReinforcementsAvailable;
1603 UINT32 i, iRandom, iWeight;
1604 INT8 bBestWeight;
1605 UINT8 ubSectorID;
1606 size_t iGarrisonArraySize = gGarrisonGroup.size();
1607
1608 //Check to see if we could send reinforcements from Alma. Only Drassen/Cambria get preferred
1609 //service from Alma, due to it's proximity and Alma's purpose as a forward military base.
1610 ubSectorID = gGarrisonGroup[ iDstGarrisonID ].ubSectorID;
1611 switch( ubSectorID )
1612 {
1613 case SEC_B13: case SEC_C13: case SEC_D13: case SEC_D15: //Drassen + nearby SAM site
1614 case SEC_F8: case SEC_F9: case SEC_G8: case SEC_G9: case SEC_H8: //Cambria
1615 //reinforcements will be primarily sent from Alma whenever possible.
1616
1617 //find which the first sector that contains Alma soldiers.
1618 for( i = 0; i < gGarrisonGroup.size(); i++ )
1619 {
1620 if( gGarrisonGroup[ i ].ubComposition == ALMA_DEFENCE )
1621 break;
1622 }
1623 iSrcGarrisonID = i;
1624 //which of these 4 Alma garrisons have the most reinforcements available? It is
1625 //possible that none of these garrisons can provide any reinforcements.
1626 bBestWeight = 0;
1627 for( i = iSrcGarrisonID; i < iSrcGarrisonID + 4; i++ )
1628 {
1629 RecalculateGarrisonWeight( i );
1630 if( bBestWeight > gGarrisonGroup[ i ].bWeight && GarrisonCanProvideMinimumReinforcements( i ) )
1631 {
1632 bBestWeight = gGarrisonGroup[ i ].bWeight;
1633 iBestGarrisonID = i;
1634 }
1635 }
1636 //If we can provide reinforcements from Alma, then make sure that it can provide at least 67% of
1637 //the requested reinforcements.
1638 if( bBestWeight < 0 )
1639 {
1640 iReinforcementsAvailable = ReinforcementsAvailable( iBestGarrisonID );
1641 if( iReinforcementsAvailable * 100 >= iReinforcementsRequested * 67 )
1642 { //This is the approved group to provide the reinforcements.
1643 return iBestGarrisonID;
1644 }
1645 }
1646 break;
1647 }
1648
1649 //The Alma case either wasn't applicable or failed to have the right reinforcements. Do a general weighted search.
1650 iRandom = Random( giReinforcementPoints );
1651 for( iSrcGarrisonID = 0; iSrcGarrisonID < iGarrisonArraySize; iSrcGarrisonID++ )
1652 { //go through the garrisons
1653 RecalculateGarrisonWeight( iSrcGarrisonID );
1654 iWeight = -gGarrisonGroup[ iSrcGarrisonID ].bWeight;
1655 if( iWeight > 0 )
1656 { //if group is able to provide reinforcements.
1657 if( iRandom < iWeight && GarrisonCanProvideMinimumReinforcements( iSrcGarrisonID ) )
1658 {
1659 iReinforcementsAvailable = ReinforcementsAvailable( iSrcGarrisonID );
1660 if( iReinforcementsAvailable * 100 >= iReinforcementsRequested * 67 )
1661 { //This is the approved group to provide the reinforcements.
1662 return iSrcGarrisonID;
1663 }
1664 }
1665 iRandom -= iWeight;
1666 }
1667 }
1668
1669 //So far we have failed on all accounts. Now, simply process all the garrisons, and return the first garrison that can
1670 //provide the reinforcements.
1671 for( iSrcGarrisonID = 0; iSrcGarrisonID < iGarrisonArraySize; iSrcGarrisonID++ )
1672 { //go through the garrisons
1673 RecalculateGarrisonWeight( iSrcGarrisonID );
1674 iWeight = -gGarrisonGroup[ iSrcGarrisonID ].bWeight;
1675 if( iWeight > 0 && GarrisonCanProvideMinimumReinforcements( iSrcGarrisonID ) )
1676 { //if group is able to provide reinforcements.
1677 iReinforcementsAvailable = ReinforcementsAvailable( iSrcGarrisonID );
1678 if( iReinforcementsAvailable * 100 >= iReinforcementsRequested * 67 )
1679 { //This is the approved group to provide the reinforcements.
1680 return iSrcGarrisonID;
1681 }
1682 }
1683 }
1684
1685 //Well, if we get this far, the queen must be low on troops. Send whatever we can.
1686 iRandom = Random( giReinforcementPoints );
1687 for( iSrcGarrisonID = 0; iSrcGarrisonID < iGarrisonArraySize; iSrcGarrisonID++ )
1688 { //go through the garrisons
1689 RecalculateGarrisonWeight( iSrcGarrisonID );
1690 iWeight = -gGarrisonGroup[ iSrcGarrisonID ].bWeight;
1691 if( iWeight > 0 && GarrisonCanProvideMinimumReinforcements( iSrcGarrisonID ) )
1692 { //if group is able to provide reinforcements.
1693 if( iRandom < iWeight )
1694 {
1695 iReinforcementsAvailable = ReinforcementsAvailable( iSrcGarrisonID );
1696 return iSrcGarrisonID;
1697 }
1698 iRandom -= iWeight;
1699 }
1700 }
1701
1702 //Failed completely.
1703 return -1;
1704 }
1705
1706
SendReinforcementsForGarrison(INT32 iDstGarrisonID,UINT16 usDefencePoints,GROUP ** pOptionalGroup)1707 static void SendReinforcementsForGarrison(INT32 iDstGarrisonID, UINT16 usDefencePoints, GROUP** pOptionalGroup)
1708 {
1709 INT32 iChance, iRandom, iSrcGarrisonID;
1710 INT32 iMaxReinforcementsAllowed, iReinforcementsAvailable, iReinforcementsRequested, iReinforcementsApproved;
1711 GROUP *pGroup;
1712 UINT8 ubSrcSectorX, ubSrcSectorY, ubDstSectorX, ubDstSectorY;
1713 UINT8 ubNumExtraReinforcements;
1714 UINT8 ubGroupSize;
1715 BOOLEAN fLimitMaxTroopsAllowable = FALSE;
1716
1717 //Determine how many units the garrison needs.
1718 iReinforcementsRequested = GarrisonReinforcementsRequested( iDstGarrisonID, &ubNumExtraReinforcements );
1719
1720 //The maximum number of reinforcements can't be offsetted past a certain point based on the
1721 //priority of the garrison.
1722 iMaxReinforcementsAllowed = //from 1 to 3 times the desired size of the normal force.
1723 gArmyComp[ gGarrisonGroup[ iDstGarrisonID ].ubComposition ].bDesiredPopulation +
1724 gArmyComp[ gGarrisonGroup[ iDstGarrisonID ].ubComposition ].bDesiredPopulation *
1725 gArmyComp[ gGarrisonGroup[ iDstGarrisonID ].ubComposition ].bPriority / 50;
1726
1727 if( iReinforcementsRequested + ubNumExtraReinforcements > iMaxReinforcementsAllowed )
1728 { //adjust the extra reinforcements so that it doesn't exceed the maximum allowed.
1729 fLimitMaxTroopsAllowable = TRUE;
1730 ubNumExtraReinforcements = (UINT8)(iMaxReinforcementsAllowed - iReinforcementsRequested);
1731 }
1732
1733 iReinforcementsRequested += ubNumExtraReinforcements;
1734
1735 if( iReinforcementsRequested <= 0 )
1736 {
1737 return;
1738 }
1739
1740 ubDstSectorX = (UINT8)SECTORX( gGarrisonGroup[ iDstGarrisonID ].ubSectorID );
1741 ubDstSectorY = (UINT8)SECTORY( gGarrisonGroup[ iDstGarrisonID ].ubSectorID );
1742
1743 if( pOptionalGroup && *pOptionalGroup )
1744 { //This group will provide the reinforcements
1745 pGroup = *pOptionalGroup;
1746
1747 SLOGD("%d troops have been reassigned from %c%d to garrison sector %c%d",
1748 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1749 pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX,
1750 ubDstSectorY + 'A' - 1, ubDstSectorX );
1751
1752 gGarrisonGroup[ iDstGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
1753 ConvertGroupTroopsToComposition( pGroup, gGarrisonGroup[ iDstGarrisonID ].ubComposition );
1754 MoveSAIGroupToSector( pOptionalGroup, gGarrisonGroup[ iDstGarrisonID ].ubSectorID, STAGE, REINFORCEMENTS );
1755 return;
1756 }
1757 iRandom = Random( giReinforcementPoints + giReinforcementPool );
1758 if( iRandom < giReinforcementPool )
1759 { //use the pool and send the requested amount from SECTOR P3 (queen's palace)
1760 QUEEN_POOL:
1761
1762 //KM : Sep 9, 1999
1763 //If the player owns sector P3, any troops that spawned there were causing serious problems, seeing battle checks
1764 //were not performed!
1765 if( !StrategicMap[ CALCULATE_STRATEGIC_INDEX( 3, 16 ) ].fEnemyControlled )
1766 { //Queen can no longer send reinforcements from the palace if she doesn't control it!
1767 return;
1768 }
1769
1770
1771 if( !giReinforcementPool )
1772 {
1773 return;
1774 }
1775 iReinforcementsApproved = MIN( iReinforcementsRequested, giReinforcementPool );
1776
1777 if( iReinforcementsApproved * 3 < usDefencePoints )
1778 { //The enemy force that would be sent would likely be decimated by the player forces.
1779 gubGarrisonReinforcementsDenied[ iDstGarrisonID ] += (UINT8)(gArmyComp[ gGarrisonGroup[ iDstGarrisonID ].ubComposition ].bPriority / 2);
1780 return;
1781 }
1782 else
1783 {
1784 //The force is strong enough to be able to take the sector.
1785 gubGarrisonReinforcementsDenied[ iDstGarrisonID ] = 0;
1786 }
1787
1788 //The chance she will send them is related with the strength difference between the
1789 //player's force and the queen's.
1790 if( ubNumExtraReinforcements && fLimitMaxTroopsAllowable && iReinforcementsApproved == iMaxReinforcementsAllowed )
1791 {
1792 iChance = (iReinforcementsApproved + ubNumExtraReinforcements) * 100 / usDefencePoints;
1793 if( !Chance( iChance ) )
1794 {
1795 return;
1796 }
1797 }
1798
1799
1800 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, (UINT8)iReinforcementsApproved, 0 );
1801 ConvertGroupTroopsToComposition( pGroup, gGarrisonGroup[ iDstGarrisonID ].ubComposition );
1802 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
1803 giReinforcementPool -= iReinforcementsApproved;
1804 pGroup->ubMoveType = ONE_WAY;
1805 gGarrisonGroup[ iDstGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
1806
1807 ubGroupSize = (UINT8)(pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins);
1808
1809 if( ubNumExtraReinforcements )
1810 {
1811 SLOGD("%d troops have been sent from palace to stage assault near sector %c%d",
1812 ubGroupSize, ubDstSectorY + 'A' - 1, ubDstSectorX );
1813 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iDstGarrisonID ].ubSectorID, STAGE, STAGING );
1814 }
1815 else
1816 {
1817 SLOGD("%d troops have been sent from palace to garrison sector %c%d",
1818 ubGroupSize, ubDstSectorY + 'A' - 1, ubDstSectorX );
1819 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iDstGarrisonID ].ubSectorID, STAGE, REINFORCEMENTS );
1820 }
1821 return;
1822 }
1823 else
1824 {
1825 iSrcGarrisonID = ChooseSuitableGarrisonToProvideReinforcements( iDstGarrisonID, iReinforcementsRequested );
1826 if( iSrcGarrisonID == -1 )
1827 {
1828 goto QUEEN_POOL;
1829 }
1830
1831 ubSrcSectorX = (gGarrisonGroup[ iSrcGarrisonID ].ubSectorID % 16) + 1;
1832 ubSrcSectorY = (gGarrisonGroup[ iSrcGarrisonID ].ubSectorID / 16) + 1;
1833 if( ubSrcSectorX != gWorldSectorX || ubSrcSectorY != gWorldSectorY || gbWorldSectorZ > 0 )
1834 { //The reinforcements aren't coming from the currently loaded sector!
1835 iReinforcementsAvailable = ReinforcementsAvailable( iSrcGarrisonID );
1836 if( iReinforcementsAvailable <= 0)
1837 {
1838 SLOGE( "Attempting to send reinforcements from a garrison that doesn't have any! -- KM:0 (with prior saved game and Debug Log)" );
1839 return;
1840 }
1841 //Send the lowest of the two: number requested or number available
1842
1843 iReinforcementsApproved = MIN( iReinforcementsRequested, iReinforcementsAvailable );
1844 if( iReinforcementsApproved > iMaxReinforcementsAllowed - ubNumExtraReinforcements )
1845 { //The force isn't strong enough, but the queen isn't willing to apply extra resources
1846 iReinforcementsApproved = iMaxReinforcementsAllowed - ubNumExtraReinforcements;
1847 }
1848 else if( (iReinforcementsApproved + ubNumExtraReinforcements) * 3 < usDefencePoints )
1849 { //The enemy force that would be sent would likely be decimated by the player forces.
1850 gubGarrisonReinforcementsDenied[ iDstGarrisonID ] += (UINT8)(gArmyComp[ gGarrisonGroup[ iDstGarrisonID ].ubComposition ].bPriority / 2);
1851 return;
1852 }
1853 else
1854 {
1855 //The force is strong enough to be able to take the sector.
1856 gubGarrisonReinforcementsDenied[ iDstGarrisonID ] = 0;
1857 }
1858
1859 //The chance she will send them is related with the strength difference between the
1860 //player's force and the queen's.
1861 if( iReinforcementsApproved + ubNumExtraReinforcements == iMaxReinforcementsAllowed && usDefencePoints )
1862 {
1863 iChance = (iReinforcementsApproved + ubNumExtraReinforcements) * 100 / usDefencePoints;
1864 if( !Chance( iChance ) )
1865 {
1866 return;
1867 }
1868 }
1869
1870 pGroup = CreateNewEnemyGroupDepartingFromSector( gGarrisonGroup[ iSrcGarrisonID ].ubSectorID, 0, (UINT8)iReinforcementsApproved, 0 );
1871 ConvertGroupTroopsToComposition( pGroup, gGarrisonGroup[ iDstGarrisonID ].ubComposition );
1872 RemoveSoldiersFromGarrisonBasedOnComposition( iSrcGarrisonID, pGroup->ubGroupSize );
1873 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
1874 pGroup->ubMoveType = ONE_WAY;
1875 gGarrisonGroup[ iDstGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
1876 ubGroupSize = (UINT8)( pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins );
1877
1878 if( ubNumExtraReinforcements )
1879 {
1880 pGroup->pEnemyGroup->ubPendingReinforcements = ubNumExtraReinforcements;
1881 SLOGD("%d troops have been sent from sector %c%d to stage assault near sector %c%d",
1882 ubGroupSize, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX, ubDstSectorY + 'A' - 1, ubDstSectorX );
1883
1884 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iDstGarrisonID ].ubSectorID, STAGE, STAGING );
1885 }
1886 else
1887 {
1888 SLOGD("%d troops have been sent from sector %c%d to garrison sector %c%d",
1889 ubGroupSize, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX, ubDstSectorY + 'A' - 1, ubDstSectorX );
1890
1891 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iDstGarrisonID ].ubSectorID, STAGE, REINFORCEMENTS );
1892 }
1893 SLOGD("%d troops have been sent from garrison sector %c%d to patrol area near sector %c%d",
1894 ubGroupSize, ubSrcSectorY + 'A' - 1, ubSrcSectorX, ubDstSectorY + 'A' - 1, ubDstSectorX );
1895
1896 return;
1897 }
1898 }
1899 }
1900
1901
SendReinforcementsForPatrol(INT32 iPatrolID,GROUP ** pOptionalGroup)1902 static void SendReinforcementsForPatrol(INT32 iPatrolID, GROUP** pOptionalGroup)
1903 {
1904 GROUP *pGroup;
1905 INT32 iRandom, iWeight;
1906 UINT32 iSrcGarrisonID;
1907 INT32 iReinforcementsAvailable, iReinforcementsRequested, iReinforcementsApproved;
1908 UINT8 ubSrcSectorX, ubSrcSectorY;
1909
1910 PATROL_GROUP* const pg = &gPatrolGroup[iPatrolID];
1911
1912 //Determine how many units the patrol group needs.
1913 iReinforcementsRequested = PatrolReinforcementsRequested(pg);
1914
1915 if( iReinforcementsRequested <= 0)
1916 return;
1917
1918 UINT8 const ubDstSectorX = (pg->ubSectorID[1] % 16) + 1;
1919 UINT8 const ubDstSectorY = (pg->ubSectorID[1] / 16) + 1;
1920
1921 if( pOptionalGroup && *pOptionalGroup )
1922 { //This group will provide the reinforcements
1923 pGroup = *pOptionalGroup;
1924 pg->ubPendingGroupID = pGroup->ubGroupID;
1925
1926 SLOGD("%d troops have been reassigned from %c%d to reinforce patrol group covering sector %c%d",
1927 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1928 pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX,
1929 ubDstSectorY + 'A' - 1, ubDstSectorX );
1930
1931 MoveSAIGroupToSector(pOptionalGroup, pg->ubSectorID[1], EVASIVE, REINFORCEMENTS);
1932 return;
1933 }
1934 iRandom = Random( giReinforcementPoints + giReinforcementPool );
1935 if( iRandom < giReinforcementPool )
1936 { //use the pool and send the requested amount from SECTOR P3 (queen's palace)
1937 iReinforcementsApproved = MIN( iReinforcementsRequested, giReinforcementPool );
1938 if( !iReinforcementsApproved )
1939 {
1940 AssertMsg(FALSE, "Trying to create empty reinforcements group!");
1941 return;
1942 }
1943 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, (UINT8)iReinforcementsApproved, 0 );
1944 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
1945 giReinforcementPool -= iReinforcementsApproved;
1946 pg->ubPendingGroupID = pGroup->ubGroupID;
1947
1948 SLOGD("%d troops have been sent from palace to patrol area near sector %c%d",
1949 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1950 ubDstSectorY + 'A' - 1, ubDstSectorX );
1951
1952 MoveSAIGroupToSector(&pGroup, pg->ubSectorID[1], EVASIVE, REINFORCEMENTS);
1953 return;
1954 }
1955 else
1956 {
1957 iRandom -= giReinforcementPool;
1958 for( iSrcGarrisonID = 0; iSrcGarrisonID < gGarrisonGroup.size(); iSrcGarrisonID++ )
1959 { //go through the garrisons
1960 RecalculateGarrisonWeight( iSrcGarrisonID );
1961 iWeight = -gGarrisonGroup[ iSrcGarrisonID ].bWeight;
1962 if( iWeight > 0 )
1963 { //if group is able to provide reinforcements.
1964 if( iRandom < iWeight )
1965 { //This is the group that gets the reinforcements!
1966 ubSrcSectorX = (UINT8)SECTORX(gGarrisonGroup[ iSrcGarrisonID ].ubSectorID );
1967 ubSrcSectorY = (UINT8)SECTORY(gGarrisonGroup[ iSrcGarrisonID ].ubSectorID );
1968 if( ubSrcSectorX != gWorldSectorX || ubSrcSectorY != gWorldSectorY || gbWorldSectorZ > 0 )
1969 { //The reinforcements aren't coming from the currently loaded sector!
1970 iReinforcementsAvailable = ReinforcementsAvailable( iSrcGarrisonID );
1971 //Send the lowest of the two: number requested or number available
1972 iReinforcementsApproved = MIN( iReinforcementsRequested, iReinforcementsAvailable );
1973 pGroup = CreateNewEnemyGroupDepartingFromSector( gGarrisonGroup[ iSrcGarrisonID ].ubSectorID, 0, (UINT8)iReinforcementsApproved, 0 );
1974 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
1975 pg->ubPendingGroupID = pGroup->ubGroupID;
1976
1977 RemoveSoldiersFromGarrisonBasedOnComposition( iSrcGarrisonID, pGroup->ubGroupSize );
1978
1979 SLOGD("%d troops have been sent from garrison sector %c%d to patrol area near sector %c%d",
1980 pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins,
1981 ubSrcSectorY + 'A' - 1, ubSrcSectorX,
1982 ubDstSectorY + 'A' - 1, ubDstSectorX );
1983
1984 MoveSAIGroupToSector(&pGroup, pg->ubSectorID[1], EVASIVE, REINFORCEMENTS);
1985 return;
1986 }
1987 }
1988 iRandom -= iWeight;
1989 }
1990 }
1991 }
1992 }
1993
1994
1995 static void EvolveQueenPriorityPhase(BOOLEAN fForceChange);
1996 static BOOLEAN GarrisonRequestingMinimumReinforcements(INT32 iGarrisonID);
1997 static BOOLEAN PatrolRequestingMinimumReinforcements(INT32 iPatrolID);
1998 static void UpgradeAdminsToTroops();
1999
2000
2001 //Periodically does a general poll and check on each of the groups and garrisons, determines
2002 //reinforcements, new patrol groups, planned assaults, etc.
EvaluateQueenSituation()2003 void EvaluateQueenSituation()
2004 {
2005 UINT32 i, iRandom;
2006 UINT32 iWeight;
2007 UINT32 uiOffset;
2008 UINT16 usDefencePoints;
2009 INT32 iSumOfAllWeights = 0;
2010
2011 // figure out how long it shall be before we call this again
2012
2013 // The more work to do there is (request points the queen's army is asking for), the more often she will make decisions
2014 // This can increase the decision intervals by up to 500 extra minutes (> 8 hrs)
2015 uiOffset = MAX( 100 - giRequestPoints, 0);
2016 uiOffset = uiOffset + Random( uiOffset * 4 );
2017 uiOffset += saipolicy(time_evaluate_in_minutes) + Random(saipolicy(time_evaluate_variance));
2018
2019 if( !giReinforcementPool )
2020 { //Queen has run out of reinforcements. Simulate recruiting and training new troops
2021 uiOffset *= 10;
2022 giReinforcementPool += 30;
2023 AddStrategicEvent( EVENT_EVALUATE_QUEEN_SITUATION, GetWorldTotalMin() + uiOffset, 0 );
2024 return;
2025 }
2026
2027 //Re-post the event
2028 AddStrategicEvent( EVENT_EVALUATE_QUEEN_SITUATION, GetWorldTotalMin() + uiOffset, 0 );
2029
2030 // if the queen hasn't been alerted to player's presence yet
2031 if( !gfQueenAIAwake )
2032 { //no decisions can be made yet.
2033 return;
2034 }
2035
2036 // Adjust queen's disposition based on player's progress
2037 EvolveQueenPriorityPhase( FALSE );
2038
2039 // Gradually promote any remaining admins into troops
2040 UpgradeAdminsToTroops();
2041
2042 if( ( giRequestPoints <= 0 ) || ( ( giReinforcementPoints <= 0 ) && ( giReinforcementPool <= 0 ) ) )
2043 { //we either have no reinforcements or request for reinforcements.
2044 return;
2045 }
2046
2047 //now randomly choose who gets the reinforcements.
2048 // giRequestPoints is the combined sum of all the individual weights of all garrisons and patrols requesting reinforcements
2049 iRandom = Random( giRequestPoints );
2050
2051 //go through garrisons first
2052 for( i = 0; i < gGarrisonGroup.size(); i++ )
2053 {
2054 RecalculateGarrisonWeight( i );
2055 iWeight = gGarrisonGroup[ i ].bWeight;
2056 if( iWeight > 0 )
2057 { //if group is requesting reinforcements.
2058
2059 iSumOfAllWeights += iWeight; // debug only!
2060
2061 if( iRandom < iWeight && !gGarrisonGroup[ i ].ubPendingGroupID &&
2062 EnemyPermittedToAttackSector( NULL, gGarrisonGroup[ i ].ubSectorID ) &&
2063 GarrisonRequestingMinimumReinforcements( i ) )
2064 { //This is the group that gets the reinforcements!
2065 if( ReinforcementsApproved( i, &usDefencePoints ) )
2066 {
2067 SendReinforcementsForGarrison( i, usDefencePoints, NULL );
2068 }
2069 else
2070 {
2071 SLOGD("Reinforcements were denied to go to %c%d because player forces too strong.",
2072 SECTORY( gGarrisonGroup[ i ].ubSectorID ) + 'A' - 1, SECTORX( gGarrisonGroup[ i ].ubSectorID ) );
2073 }
2074 return;
2075 }
2076 iRandom -= iWeight;
2077 }
2078 }
2079
2080 //go through the patrol groups
2081 for( i = 0; i < gPatrolGroup.size(); i++ )
2082 {
2083 RecalculatePatrolWeight(gPatrolGroup[i]);
2084 iWeight = gPatrolGroup[ i ].bWeight;
2085 if( iWeight > 0 )
2086 {
2087 iSumOfAllWeights += iWeight; // debug only!
2088
2089 if( iRandom < iWeight && !gPatrolGroup[ i ].ubPendingGroupID && PatrolRequestingMinimumReinforcements( i ) )
2090 { //This is the group that gets the reinforcements!
2091 SendReinforcementsForPatrol( i, NULL );
2092 return;
2093 }
2094 iRandom -= iWeight;
2095 }
2096 }
2097 }
2098
2099
SaveStrategicAI(HWFILE const hFile)2100 void SaveStrategicAI(HWFILE const hFile)
2101 {
2102 GARRISON_GROUP gEmptyGarrisonGroup;
2103 PATROL_GROUP gEmptyPatrolGroup;
2104 ARMY_COMPOSITION gEmptyArmyComp;
2105 INT32 iPatrolArraySize = static_cast<INT32>(gPatrolGroup.size());
2106 INT32 iGarrisonArraySize = static_cast<INT32>(gGarrisonGroup.size());
2107 size_t i;
2108
2109 gEmptyPatrolGroup = PATROL_GROUP{};
2110 gEmptyArmyComp = ARMY_COMPOSITION{};
2111 gEmptyGarrisonGroup = GARRISON_GROUP{};
2112
2113 FileSeek(hFile, 3, FILE_SEEK_FROM_CURRENT);
2114 FileWrite(hFile, &gfExtraElites, 1);
2115 FileWrite(hFile, &iGarrisonArraySize, 4);
2116 FileWrite(hFile, &iPatrolArraySize, 4);
2117 FileWrite(hFile, &giReinforcementPool, 4);
2118 FileWrite(hFile, &giForcePercentage, 4);
2119 FileWrite(hFile, &giArmyAlertness, 4);
2120 FileWrite(hFile, &giArmyAlertnessDecay, 4);
2121 FileWrite(hFile, &gfQueenAIAwake, 1);
2122 FileWrite(hFile, &giReinforcementPoints, 4);
2123 FileWrite(hFile, &giRequestPoints, 4);
2124 FileWrite(hFile, &gubNumAwareBattles, 1);
2125 FileWrite(hFile, &gubSAIVersion, 1);
2126 FileWrite(hFile, &gubQueenPriorityPhase, 1);
2127 FileWrite(hFile, &gfFirstBattleMeanwhileScenePending, 1);
2128 FileWrite(hFile, &gfMassFortificationOrdered, 1);
2129 FileWrite(hFile, &gubMinEnemyGroupSize, 1);
2130 FileWrite(hFile, &gubHoursGracePeriod, 1);
2131 FileWrite(hFile, &gusPlayerBattleVictories, 2);
2132 FileWrite(hFile, &gfUseAlternateQueenPosition, 1);
2133 FileWrite(hFile, gbPadding, SAI_PADDING_BYTES);
2134 //Save the army composition (which does get modified)
2135 FileWrite(hFile, gArmyComp.data(), gArmyComp.size() * sizeof(ARMY_COMPOSITION));
2136 i = SAVED_ARMY_COMPOSITIONS - gArmyComp.size();
2137 while( i-- )
2138 {
2139 FileWrite(hFile, &gEmptyArmyComp, sizeof(ARMY_COMPOSITION));
2140 }
2141 //Save the patrol group definitions
2142 if (!gPatrolGroup.empty()) FileWrite(hFile, gPatrolGroup.data(), gPatrolGroup.size() * sizeof(PATROL_GROUP));
2143 i = SAVED_PATROL_GROUPS - gPatrolGroup.size();
2144 while( i-- )
2145 {
2146 FileWrite(hFile, &gEmptyPatrolGroup, sizeof(PATROL_GROUP));
2147 }
2148 //Save the garrison information!
2149 if (!gGarrisonGroup.empty()) FileWrite(hFile, gGarrisonGroup.data(), gGarrisonGroup.size() * sizeof(GARRISON_GROUP));
2150 i = SAVED_GARRISON_GROUPS - gGarrisonGroup.size();
2151 while( i-- )
2152 {
2153 FileWrite(hFile, &gEmptyGarrisonGroup, sizeof(GARRISON_GROUP));
2154 }
2155
2156 FileWrite(hFile, gubPatrolReinforcementsDenied, gPatrolGroup.size());
2157
2158 FileWrite(hFile, gubGarrisonReinforcementsDenied, gGarrisonGroup.size());
2159 }
2160
2161
2162 static void ReinitializeUnvisitedGarrisons(void);
2163
2164
LoadStrategicAI(HWFILE const hFile)2165 void LoadStrategicAI(HWFILE const hFile)
2166 {
2167 size_t i;
2168 UINT8 ubSAIVersion;
2169 UINT32 iPatrolArraySize, iGarrisonArraySize;
2170
2171 FileSeek(hFile, 3, FILE_SEEK_FROM_CURRENT);
2172 FileRead(hFile, &gfExtraElites, 1);
2173 FileRead(hFile, &iGarrisonArraySize, 4);
2174 FileRead(hFile, &iPatrolArraySize, 4);
2175 FileRead(hFile, &giReinforcementPool, 4);
2176 FileRead(hFile, &giForcePercentage, 4);
2177 FileRead(hFile, &giArmyAlertness, 4);
2178 FileRead(hFile, &giArmyAlertnessDecay, 4);
2179 FileRead(hFile, &gfQueenAIAwake, 1);
2180 FileRead(hFile, &giReinforcementPoints, 4);
2181 FileRead(hFile, &giRequestPoints, 4);
2182 FileRead(hFile, &gubNumAwareBattles, 1);
2183 FileRead(hFile, &ubSAIVersion, 1);
2184 FileRead(hFile, &gubQueenPriorityPhase, 1);
2185 FileRead(hFile, &gfFirstBattleMeanwhileScenePending, 1);
2186 FileRead(hFile, &gfMassFortificationOrdered, 1);
2187 FileRead(hFile, &gubMinEnemyGroupSize, 1);
2188 FileRead(hFile, &gubHoursGracePeriod, 1);
2189 FileRead(hFile, &gusPlayerBattleVictories, 2);
2190 FileRead(hFile, &gfUseAlternateQueenPosition, 1);
2191 FileRead(hFile, gbPadding, SAI_PADDING_BYTES);
2192 //Restore the army composition
2193 gArmyComp.clear();
2194 gArmyComp.assign(SAVED_ARMY_COMPOSITIONS, ARMY_COMPOSITION{});
2195 FileRead(hFile, gArmyComp.data(), SAVED_ARMY_COMPOSITIONS * sizeof(ARMY_COMPOSITION)); // read everything first, will discard what we don't need when we have also the Garrison Groups
2196
2197 //Restore the patrol group definitions
2198 if (iPatrolArraySize != GCM->getPatrolGroups().size())
2199 {
2200 STLOGW("Number of Patrol Groups in save ({}) is different from definition ({}). Save might not work properly.", iPatrolArraySize, GCM->getPatrolGroups().size());
2201 }
2202 auto buffPG = new PATROL_GROUP[SAVED_PATROL_GROUPS]{};
2203 FileRead(hFile, buffPG, SAVED_PATROL_GROUPS * sizeof(PATROL_GROUP));
2204 gPatrolGroup = std::vector<PATROL_GROUP>(buffPG, buffPG + iPatrolArraySize);
2205 delete[] buffPG;
2206
2207 gubSAIVersion = SAI_VERSION;
2208 //Load the garrison information!
2209 if (iGarrisonArraySize != GCM->getGarrisonGroups().size())
2210 {
2211 STLOGW("Number of Garrison Groups in save ({}) is different from definition ({}). Save might not work properly.", iGarrisonArraySize, GCM->getGarrisonGroups().size());
2212 }
2213 auto buffGG = new GARRISON_GROUP[SAVED_GARRISON_GROUPS]{};
2214 FileRead(hFile, buffGG, SAVED_GARRISON_GROUPS * sizeof(GARRISON_GROUP));
2215 gGarrisonGroup = std::vector<GARRISON_GROUP>(buffGG, buffGG + iGarrisonArraySize);
2216 delete[] buffGG;
2217
2218 // resize gArmyComp, ensuring all army compositions referenced by Garrison Groups exist
2219 size_t numArmyCompositions = NUM_ARMY_COMPOSITIONS;
2220 for (auto gGroup : gGarrisonGroup)
2221 {
2222 numArmyCompositions = std::max<size_t>(numArmyCompositions, gGroup.ubComposition + 1);
2223 }
2224 gArmyComp.resize(numArmyCompositions);
2225 if (gArmyComp.size() != GCM->getArmyCompositions().size())
2226 {
2227 STLOGW("Number of Army Compositions in save ({}) is different from definition ({}). Save might not work properly.", gArmyComp.size(), GCM->getArmyCompositions().size());
2228 }
2229 ArmyCompositionModel::validateLoadedData(gArmyComp);
2230
2231 //Load the list of reinforcement patrol points.
2232 if( gubPatrolReinforcementsDenied )
2233 {
2234 delete[] gubPatrolReinforcementsDenied;
2235 gubPatrolReinforcementsDenied = NULL;
2236 }
2237 gubPatrolReinforcementsDenied = new UINT8[iPatrolArraySize]{};
2238 FileRead(hFile, gubPatrolReinforcementsDenied, iPatrolArraySize);
2239
2240 //Load the list of reinforcement garrison points.
2241 if( gubGarrisonReinforcementsDenied )
2242 {
2243 delete[] gubGarrisonReinforcementsDenied;
2244 gubGarrisonReinforcementsDenied = NULL;
2245 }
2246 gubGarrisonReinforcementsDenied = new UINT8[iGarrisonArraySize]{};
2247 FileRead(hFile, gubGarrisonReinforcementsDenied, iGarrisonArraySize);
2248
2249 if( ubSAIVersion < 6 )
2250 { //Reinitialize the costs since they have changed.
2251
2252 //Recreate the compositions
2253 gArmyComp = GCM->getArmyCompositions();
2254 EvolveQueenPriorityPhase( TRUE );
2255
2256 //Recreate the patrol desired sizes
2257 auto origPatrolGroup = GCM->getPatrolGroups();
2258 for( i = 0; i < gPatrolGroup.size(); i++ )
2259 {
2260 gPatrolGroup[ i ].bSize = origPatrolGroup[ i ].bSize;
2261 }
2262 }
2263 if( ubSAIVersion < 7 )
2264 {
2265 BuildUndergroundSectorInfoList();
2266 }
2267 if( ubSAIVersion < 8 )
2268 {
2269 ReinitializeUnvisitedGarrisons();
2270 }
2271 if( ubSAIVersion < 10 )
2272 {
2273 for( i = 0; i < gPatrolGroup.size(); i++ )
2274 {
2275 if( gPatrolGroup[ i ].bSize >= 16 )
2276 {
2277 gPatrolGroup[ i ].bSize = 10;
2278 }
2279 }
2280 FOR_EACH_ENEMY_GROUP(pGroup)
2281 {
2282 if (pGroup->ubGroupSize >= 16)
2283 { //accident in patrol groups being too large
2284 UINT8 ubGetRidOfXTroops = pGroup->ubGroupSize - 10;
2285 if( gbWorldSectorZ || pGroup->ubSectorX != gWorldSectorX || pGroup->ubSectorY != gWorldSectorY )
2286 { //don't modify groups in the currently loaded sector.
2287 if( pGroup->pEnemyGroup->ubNumTroops >= ubGetRidOfXTroops )
2288 {
2289 pGroup->pEnemyGroup->ubNumTroops -= ubGetRidOfXTroops;
2290 pGroup->ubGroupSize -= ubGetRidOfXTroops;
2291 }
2292 }
2293 }
2294 }
2295 }
2296 if( ubSAIVersion < 13 )
2297 {
2298 for( i = 0; i < 255; i++ )
2299 {
2300 SectorInfo[ i ].bBloodCatPlacements = 0;
2301 SectorInfo[ i ].bBloodCats = -1;
2302 }
2303 InitBloodCatSectors();
2304 //This info is used to clean up the two new codes inserted into the
2305 //middle of the enumeration for battle codes.
2306 if( gubEnemyEncounterCode > CREATURE_ATTACK_CODE )
2307 {
2308 gubEnemyEncounterCode += 2;
2309 }
2310 if( gubExplicitEnemyEncounterCode > CREATURE_ATTACK_CODE )
2311 {
2312 gubExplicitEnemyEncounterCode += 2;
2313 }
2314
2315 }
2316 if( ubSAIVersion < 14 )
2317 {
2318 UNDERGROUND_SECTORINFO *pSector;
2319 pSector = FindUnderGroundSector( 4, 11, 1 );
2320 if( pSector->ubNumTroops + pSector->ubNumElites > 20 )
2321 {
2322 pSector->ubNumTroops -= 2;
2323 }
2324 pSector = FindUnderGroundSector( 3, 15, 1 );
2325 if( pSector->ubNumTroops + pSector->ubNumElites > 20 )
2326 {
2327 pSector->ubNumTroops -= 2;
2328 }
2329 }
2330 if( ubSAIVersion < 16 )
2331 {
2332 UNDERGROUND_SECTORINFO *pSector;
2333 pSector = FindUnderGroundSector( 3, 15, 1 );
2334 if( pSector )
2335 {
2336 pSector->ubAdjacentSectors |= SOUTH_ADJACENT_SECTOR;
2337 }
2338 pSector = FindUnderGroundSector( 3, 16, 1 );
2339 if( pSector )
2340 {
2341 pSector->ubAdjacentSectors |= NORTH_ADJACENT_SECTOR;
2342 }
2343 }
2344 if( ubSAIVersion < 17 )
2345 { //Patch all groups that have this flag set
2346 gubNumGroupsArrivedSimultaneously = 0;
2347 {
2348 FOR_EACH_GROUP(pGroup)
2349 {
2350 if( pGroup->uiFlags & GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY )
2351 {
2352 pGroup->uiFlags &= ~GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY;
2353 }
2354 }
2355 }
2356 }
2357 if( ubSAIVersion < 18 )
2358 { //adjust down the number of bloodcats based on difficulty in the two special bloodcat levels
2359 switch( gGameOptions.ubDifficultyLevel )
2360 {
2361 case DIF_LEVEL_EASY: //50%
2362 SectorInfo[ SEC_I16 ].bBloodCatPlacements = 14;
2363 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 13;
2364 SectorInfo[ SEC_I16 ].bBloodCats = 14;
2365 SectorInfo[ SEC_N5 ].bBloodCats = 13;
2366 break;
2367 case DIF_LEVEL_MEDIUM: //75%
2368 SectorInfo[ SEC_I16 ].bBloodCatPlacements = 19;
2369 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 18;
2370 SectorInfo[ SEC_I16 ].bBloodCats = 19;
2371 SectorInfo[ SEC_N5 ].bBloodCats = 18;
2372 break;
2373 case DIF_LEVEL_HARD: //100%
2374 SectorInfo[ SEC_I16 ].bBloodCatPlacements = 26;
2375 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 25;
2376 SectorInfo[ SEC_I16 ].bBloodCats = 26;
2377 SectorInfo[ SEC_N5 ].bBloodCats = 25;
2378 break;
2379 }
2380 }
2381 if( ubSAIVersion < 19 )
2382 {
2383 //Clear the garrison in C5
2384 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_C5 ].ubGarrisonID ].ubComposition ].bPriority = 0;
2385 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_C5 ].ubGarrisonID ].ubComposition ].bDesiredPopulation = 0;
2386 }
2387 if( ubSAIVersion < 20 )
2388 {
2389 gArmyComp[ QUEEN_DEFENCE ].bDesiredPopulation = 32;
2390 SectorInfo[ SEC_P3 ].ubNumElites = 32;
2391 }
2392 if( ubSAIVersion < 21 )
2393 {
2394 FOR_EACH_GROUP(pGroup) pGroup->uiFlags = 0;
2395 }
2396 if( ubSAIVersion < 22 )
2397 { //adjust down the number of bloodcats based on difficulty in the two special bloodcat levels
2398 switch( gGameOptions.ubDifficultyLevel )
2399 {
2400 case DIF_LEVEL_EASY: //50%
2401 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 8;
2402 SectorInfo[ SEC_N5 ].bBloodCats = 10;
2403 break;
2404 case DIF_LEVEL_MEDIUM: //75%
2405 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 8;
2406 SectorInfo[ SEC_N5 ].bBloodCats = 10;
2407 break;
2408 case DIF_LEVEL_HARD: //100%
2409 SectorInfo[ SEC_N5 ].bBloodCatPlacements = 8;
2410 SectorInfo[ SEC_N5 ].bBloodCats = 10;
2411 break;
2412 }
2413 }
2414 if( ubSAIVersion < 23 )
2415 {
2416 if( gWorldSectorX != 3 || gWorldSectorY != 16 || !gbWorldSectorZ )
2417 {
2418 SectorInfo[ SEC_P3 ].ubNumElites = 32;
2419 }
2420 }
2421 if( ubSAIVersion < 24 )
2422 {
2423 //If the queen has escaped to the basement, do not use the profile insertion info
2424 //when we finally go down there, otherwise she will end up in the wrong spot, possibly inside
2425 //the walls.
2426 if( !gubFact[ FACT_QUEEN_DEAD ] && gMercProfiles[ QUEEN ].bSectorZ == 1 )
2427 {
2428 if( gbWorldSectorZ != 1 || gWorldSectorX != 16 || gWorldSectorY != 3 )
2429 { //We aren't in the basement sector
2430 gMercProfiles[ QUEEN ].fUseProfileInsertionInfo = FALSE;
2431 }
2432 else
2433 { //We are in the basement sector, relocate queen to proper position.
2434 FOR_EACH_IN_TEAM(i, CIV_TEAM)
2435 {
2436 SOLDIERTYPE& s = *i;
2437 if (s.ubProfile != QUEEN) continue;
2438 // Found queen, relocate her to 16866
2439 BumpAnyExistingMerc(16866);
2440 TeleportSoldier(s, 16866, true);
2441 break;
2442 }
2443 }
2444 }
2445 }
2446 if( ubSAIVersion < 25 )
2447 {
2448 if( gubFact[ FACT_SKYRIDER_CLOSE_TO_CHOPPER ] )
2449 {
2450 gMercProfiles[ SKYRIDER ].fUseProfileInsertionInfo = FALSE;
2451 }
2452 }
2453
2454 //KM : July 21, 1999 patch fix
2455 if( ubSAIVersion < 26 )
2456 {
2457 INT32 i;
2458 for( i = 0; i < 255; i++ )
2459 {
2460 if( SectorInfo[ i ].ubNumberOfCivsAtLevel[ GREEN_MILITIA ] +
2461 SectorInfo[ i ].ubNumberOfCivsAtLevel[ REGULAR_MILITIA ] +
2462 SectorInfo[ i ].ubNumberOfCivsAtLevel[ ELITE_MILITIA ] > 20 )
2463 {
2464 SectorInfo[ i ].ubNumberOfCivsAtLevel[ GREEN_MILITIA ] = 0;
2465 SectorInfo[ i ].ubNumberOfCivsAtLevel[ REGULAR_MILITIA ] = 20;
2466 SectorInfo[ i ].ubNumberOfCivsAtLevel[ ELITE_MILITIA ] = 0;
2467 }
2468 }
2469 }
2470
2471 //KM : August 4, 1999 patch fix
2472 // This addresses the problem of not having any soldiers in sector N7 when playing the game under easy difficulty.
2473 // If captured and interrogated, the player would find no soldiers defending the sector. This changes the composition
2474 // so that it will always be there, and adds the soldiers accordingly if the sector isn't loaded when the update is made.
2475 if( ubSAIVersion < 27 )
2476 {
2477 if( gGameOptions.ubDifficultyLevel == DIF_LEVEL_EASY )
2478 {
2479 if( gWorldSectorX != 7 || gWorldSectorY != 14 || gbWorldSectorZ )
2480 {
2481 INT32 cnt, iRandom;
2482 INT32 iEliteChance, iTroopChance, iAdminChance;
2483 INT32 iStartPop;
2484 SECTORINFO *pSector = NULL;
2485
2486 //Change the garrison composition to LEVEL1_DEFENCE from LEVEL2_DEFENCE
2487 pSector = &SectorInfo[ SEC_N7 ];
2488 gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition = LEVEL1_DEFENCE;
2489
2490 iStartPop = gArmyComp[ gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bStartPopulation;
2491 iEliteChance = gArmyComp[ gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bElitePercentage;
2492 iTroopChance = gArmyComp[ gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bTroopPercentage + iEliteChance;
2493 iAdminChance = gArmyComp[ gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bAdminPercentage;
2494
2495 if( iStartPop )
2496 {
2497 // if population is less than maximum
2498 if( iStartPop != MAX_STRATEGIC_TEAM_SIZE )
2499 {
2500 // then vary it a bit (+/- 25%)
2501 iStartPop = iStartPop * ( 100 + ( Random ( 51 ) - 25 ) ) / 100;
2502 }
2503
2504 iStartPop = MAX( gubMinEnemyGroupSize, MIN( MAX_STRATEGIC_TEAM_SIZE, iStartPop ) );
2505 cnt = iStartPop;
2506
2507 if( iAdminChance )
2508 {
2509 pSector->ubNumAdmins = iAdminChance * iStartPop / 100;
2510 }
2511 else while( cnt-- )
2512 { //for each person, randomly determine the types of each soldier.
2513 {
2514 iRandom = Random( 100 );
2515 if( iRandom < iEliteChance )
2516 {
2517 pSector->ubNumElites++;
2518 }
2519 else if( iRandom < iTroopChance )
2520 {
2521 pSector->ubNumTroops++;
2522 }
2523 }
2524 }
2525 }
2526 }
2527 }
2528 }
2529
2530 if( ubSAIVersion < 28 )
2531 {
2532 if( !StrategicMap[ CALCULATE_STRATEGIC_INDEX( 3, 16 ) ].fEnemyControlled )
2533 { //Eliminate all enemy groups in this sector, because the player owns the sector, and it is not
2534 //possible for them to spawn there!
2535 FOR_EACH_GROUP_SAFE(i)
2536 {
2537 GROUP& g = *i;
2538 if (g.fPlayer) continue;
2539 if (g.ubSectorX != 3) continue;
2540 if (g.ubSectorY != 16) continue;
2541 if (g.ubPrevX != 0) continue;
2542 if (g.ubPrevY != 0) continue;
2543 RemoveGroupFromStrategicAILists(g);
2544 RemoveGroup(g);
2545 }
2546 }
2547 }
2548 if( ubSAIVersion < 29 )
2549 {
2550 InitStrategicMovementCosts();
2551 }
2552
2553 //KM : Aug 11, 1999 -- Patch fix: Blindly update the airspace control. There is a bug somewhere
2554 // that is failing to keep this information up to date, and I failed to find it. At least this
2555 // will patch saves.
2556 UpdateAirspaceControl( );
2557
2558 EvolveQueenPriorityPhase( TRUE );
2559
2560 //Count and correct the floating groups
2561 FOR_EACH_GROUP_SAFE(pGroup)
2562 {
2563 if( !pGroup->fPlayer )
2564 {
2565 if( !pGroup->fBetweenSectors )
2566 {
2567 if( pGroup->ubSectorX != gWorldSectorX ||
2568 pGroup->ubSectorY != gWorldSectorY ||
2569 gbWorldSectorZ )
2570 {
2571 RepollSAIGroup( pGroup );
2572 ValidateGroup( pGroup );
2573 }
2574 }
2575 }
2576 }
2577 SLOGD("Restoring saved game at Day %02d, %02d:%02d ", GetWorldDay(), GetWorldHour(), GetWorldMinutesInDay()%60 );
2578
2579 //Update the version number to the most current.
2580 gubSAIVersion = SAI_VERSION;
2581 }
2582
2583
2584 //As the player's progress changes in the game, the queen will adjust her priorities accordingly.
2585 //Basically, increasing priorities and numbers for sectors she owns, and lowering them.
2586 //@@@Alex, this is tweakable. My philosophies could be incorrect. It might be better if instead of lowering
2587 //priorities and numbers for towns the queen has lost, to instead lower the priority but increase the numbers so
2588 //she would send larger attack forces. This is questionable.
EvolveQueenPriorityPhase(BOOLEAN fForceChange)2589 static void EvolveQueenPriorityPhase(BOOLEAN fForceChange)
2590 {
2591 INT32 index, num, iFactor;
2592 INT32 iChange, iNew, iNumSoldiers, iNumPromotions;
2593 SECTORINFO *pSector;
2594 std::vector<UINT8> ubOwned;
2595 std::vector<UINT8> ubTotal;
2596 UINT8 ubNewPhase;
2597 ubNewPhase = CurrentPlayerProgressPercentage() / 10;
2598 auto origArmyComp = GCM->getArmyCompositions();
2599
2600 if( !fForceChange && ubNewPhase == gubQueenPriorityPhase )
2601 {
2602 return;
2603 }
2604
2605 if( gubQueenPriorityPhase > ubNewPhase )
2606 {
2607 SLOGD("The queen's defence priority has decreased from %d0%% to %d0%%.", gubQueenPriorityPhase, ubNewPhase );
2608 }
2609 else if( gubQueenPriorityPhase < ubNewPhase )
2610 {
2611 SLOGD("The queen's defence priority has increased from %d0%% to %d0%%.", gubQueenPriorityPhase, ubNewPhase );
2612 }
2613 else
2614 {
2615 SLOGD("The queen's defence priority is the same (%d0%%), but has been forced to update.", gubQueenPriorityPhase );
2616 }
2617
2618 gubQueenPriorityPhase = ubNewPhase;
2619
2620 //The phase value refers to the deviation percentage she will apply to original garrison values.
2621 //All sector values are evaluated to see how many of those sectors are enemy controlled. If they
2622 //are controlled by her, the desired number will be increased as well as the priority. On the other
2623 //hand, if she doesn't own those sectors, the values will be decreased instead. All values are based off of
2624 //the originals.
2625 std::fill_n(std::back_inserter(ubOwned), gArmyComp.size(), 0);
2626 std::fill_n(std::back_inserter(ubTotal), gArmyComp.size(), 0);
2627
2628 //Record the values required to calculate the percentage of each composition type that the queen controls.
2629 for( size_t i = 0; i < gGarrisonGroup.size(); i++ )
2630 {
2631 index = gGarrisonGroup[ i ].ubComposition;
2632 if( StrategicMap[ SECTOR_INFO_TO_STRATEGIC_INDEX( gGarrisonGroup[ i ].ubSectorID ) ].fEnemyControlled )
2633 {
2634 ubOwned[ index ]++;
2635 }
2636 ubTotal[ index ]++;
2637 }
2638
2639 //Go through the *majority* of compositions and modify the priority/desired values.
2640 for( size_t i = 0; i < gArmyComp.size(); i++ )
2641 {
2642 switch( i )
2643 {
2644 case QUEEN_DEFENCE:
2645 case MEDUNA_DEFENCE:
2646 case MEDUNA_SAMSITE:
2647 case LEVEL1_DEFENCE:
2648 case LEVEL2_DEFENCE:
2649 case LEVEL3_DEFENCE:
2650 case OMERTA_WELCOME_WAGON:
2651 case ROADBLOCK:
2652 //case SANMONA_SMALL:
2653 //don't consider these compositions
2654 continue;
2655 }
2656 //If the queen owns them ALL, then she gets the maximum defensive bonus. If she owns NONE,
2657 //then she gets a maximum defensive penalty. Everything else lies in the middle. The legal
2658 //range is +-50.
2659 if( ubTotal[ i ] )
2660 {
2661 iFactor = (ubOwned[ i ] * 100 / ubTotal[ i ]) - 50;
2662 }
2663 else
2664 {
2665 iFactor = -50;
2666 }
2667 iFactor = iFactor * gubQueenPriorityPhase / 10;
2668
2669 //modify priority by + or - 25% of original
2670 if( gArmyComp[ i ].bPriority )
2671 {
2672 num = origArmyComp[ i ].bPriority+ iFactor / 2;
2673 num = MIN( MAX( 0, num ), 100 );
2674 gArmyComp[ i ].bPriority = (INT8)num;
2675 }
2676
2677 //modify desired population by + or - 50% of original population
2678 num = origArmyComp[ i ].bDesiredPopulation * (100 + iFactor) / 100;
2679 num = MIN( MAX( 6, num ), MAX_STRATEGIC_TEAM_SIZE );
2680 gArmyComp[ i ].bDesiredPopulation = (INT8)num;
2681
2682 //if gfExtraElites is set, then augment the composition sizes
2683 if( gfExtraElites && iFactor >= 15 && gArmyComp[ i ].bElitePercentage )
2684 {
2685 iChange = gGameOptions.ubDifficultyLevel * 5;
2686
2687 //increase elite % (max 100)
2688 iNew = gArmyComp[ i ].bElitePercentage + iChange;
2689 iNew = (INT8)MIN( 100, iNew );
2690 gArmyComp[ i ].bElitePercentage = (INT8)iNew;
2691
2692 //decrease troop % (min 0)
2693 iNew = gArmyComp[ i ].bTroopPercentage - iChange;
2694 iNew = (INT8)MAX( 0, iNew );
2695 gArmyComp[ i ].bTroopPercentage = (INT8)iNew;
2696 }
2697 }
2698 if( gfExtraElites )
2699 {
2700 //Turn off the flag so that this doesn't happen everytime this function is called!
2701 gfExtraElites = FALSE;
2702
2703 for( UINT32 i = 0; i < gGarrisonGroup.size(); i++ )
2704 {
2705 //if we are dealing with extra elites, then augment elite compositions (but only if they exist in the sector).
2706 //If the queen still owns the town by more than 65% (iFactor >= 15), then upgrade troops to elites in those sectors.
2707 index = gGarrisonGroup[ i ].ubComposition;
2708 switch( index )
2709 {
2710 case QUEEN_DEFENCE:
2711 case MEDUNA_DEFENCE:
2712 case MEDUNA_SAMSITE:
2713 case LEVEL1_DEFENCE:
2714 case LEVEL2_DEFENCE:
2715 case LEVEL3_DEFENCE:
2716 case OMERTA_WELCOME_WAGON:
2717 case ROADBLOCK:
2718 //case SANMONA_SMALL:
2719 //don't consider these compositions
2720 continue;
2721 }
2722 pSector = &SectorInfo[ gGarrisonGroup[ i ].ubSectorID ];
2723 if( ubTotal[ index ] )
2724 {
2725 iFactor = (ubOwned[ index ] * 100 / ubTotal[ index ]) - 50;
2726 }
2727 else
2728 {
2729 iFactor = -50;
2730 }
2731 if( iFactor >= 15 )
2732 { //Make the actual elites in sector match the new garrison percentage
2733 if( !gfWorldLoaded || gbWorldSectorZ ||
2734 gWorldSectorX != SECTORX( gGarrisonGroup[ i ].ubSectorID ) ||
2735 gWorldSectorY != SECTORY( gGarrisonGroup[ i ].ubSectorID ) )
2736 { //Also make sure the sector isn't currently loaded!
2737 iNumSoldiers = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
2738 iNumPromotions = gArmyComp[ index ].bElitePercentage * iNumSoldiers / 100 - pSector->ubNumElites;
2739
2740 if( iNumPromotions > 0 )
2741 {
2742 while( iNumPromotions-- )
2743 {
2744 if( pSector->ubNumAdmins )
2745 {
2746 pSector->ubNumAdmins--;
2747 }
2748 else if( pSector->ubNumTroops )
2749 {
2750 pSector->ubNumTroops--;
2751 }
2752 else
2753 {
2754 SLOGA("EvolveQueenPriorityPhase: more promotions than soldiers");
2755 }
2756 pSector->ubNumElites++;
2757 }
2758 Assert( iNumSoldiers == pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites );
2759 }
2760 }
2761 }
2762 }
2763 }
2764 //Recalculate all of the weights.
2765 for( size_t i = 0; i < gGarrisonGroup.size(); i++ )
2766 {
2767 RecalculateGarrisonWeight( i );
2768 }
2769 }
2770
2771
2772 static void RequestHighPriorityGarrisonReinforcements(INT32 iGarrisonID, UINT8 ubSoldiersRequested);
2773
2774
ExecuteStrategicAIAction(UINT16 usActionCode,INT16 sSectorX,INT16 sSectorY)2775 void ExecuteStrategicAIAction( UINT16 usActionCode, INT16 sSectorX, INT16 sSectorY )
2776 {
2777 GROUP *pGroup, *pPendingGroup = NULL;
2778 SECTORINFO *pSector;
2779 UINT8 ubSectorID;
2780 UINT8 ubNumSoldiers;
2781 switch( usActionCode )
2782 {
2783 case STRATEGIC_AI_ACTION_WAKE_QUEEN:
2784 WakeUpQueen();
2785 break;
2786
2787 case STRATEGIC_AI_ACTION_QUEEN_DEAD:
2788 gfQueenAIAwake = FALSE;
2789 break;
2790
2791 case STRATEGIC_AI_ACTION_KINGPIN_DEAD:
2792 //Immediate send a small garrison to C5 (to discourage access to Tony the dealer)
2793 /*
2794 for( i = 0; i < gGarrisonGroup.size(); i++ )
2795 {
2796 if( gGarrisonGroup[ i ].ubComposition == SANMONA_SMALL )
2797 {
2798 //Setup the composition so from now on the queen will consider this an important sector
2799 //to hold.
2800 gArmyComp[ gGarrisonGroup[ i ].ubComposition ].bPriority = 65;
2801 gArmyComp[ gGarrisonGroup[ i ].ubComposition ].bTroopPercentage = 100;
2802 gArmyComp[ gGarrisonGroup[ i ].ubComposition ].bDesiredPopulation = 5;
2803 RequestHighPriorityGarrisonReinforcements( i, (UINT8)(2 + Random( 4 )) ); //send 2-5 soldiers now.
2804 break;
2805 }
2806 }
2807 */
2808 break;
2809 case NPC_ACTION_SEND_SOLDIERS_TO_DRASSEN:
2810 //Send 6, 9, or 12 troops (based on difficulty) one of the Drassen sectors. If nobody is there when they arrive,
2811 //those troops will get reassigned.
2812
2813 if( Chance( 50 ) )
2814 {
2815 ubSectorID = SEC_D13;
2816 }
2817 else if( Chance( 60 ) )
2818 {
2819 ubSectorID = SEC_B13;
2820 }
2821 else
2822 {
2823 ubSectorID = SEC_C13;
2824 }
2825 ubNumSoldiers = (UINT8)(3 + gGameOptions.ubDifficultyLevel * 3);
2826 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, ubNumSoldiers, 0 );
2827
2828 if( !gGarrisonGroup[ SectorInfo[ ubSectorID ].ubGarrisonID ].ubPendingGroupID )
2829 {
2830 pGroup->pEnemyGroup->ubIntention = STAGE;
2831 gGarrisonGroup[ SectorInfo[ ubSectorID ].ubGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
2832 }
2833 else
2834 { //this should never happen (but if it did, then this is the best way to deal with it).
2835 pGroup->pEnemyGroup->ubIntention = PURSUIT;
2836 }
2837 giReinforcementPool -= ubNumSoldiers;
2838 giReinforcementPool = MAX( giReinforcementPool, 0 );
2839
2840 MoveSAIGroupToSector( &pGroup, ubSectorID, EVASIVE, pGroup->pEnemyGroup->ubIntention );
2841
2842 break;
2843 case NPC_ACTION_SEND_SOLDIERS_TO_BATTLE_LOCATION:
2844
2845 //Send 4, 8, or 12 troops (based on difficulty) to the location of the first battle. If nobody is there when they arrive,
2846 //those troops will get reassigned.
2847 ubSectorID = (UINT8)STRATEGIC_INDEX_TO_SECTOR_INFO( sWorldSectorLocationOfFirstBattle );
2848 pSector = &SectorInfo[ ubSectorID ];
2849 ubNumSoldiers = (UINT8)(gGameOptions.ubDifficultyLevel * 4);
2850 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, ubNumSoldiers, 0 );
2851 giReinforcementPool -= ubNumSoldiers;
2852 giReinforcementPool = MAX( giReinforcementPool, 0 );
2853
2854 //Determine if the battle location actually has a garrison assignment. If so, and the following
2855 //checks succeed, the enemies will be sent to attack and reinforce that sector. Otherwise, the
2856 //enemies will simply check it out, then leave.
2857 if( pSector->ubGarrisonID != NO_GARRISON )
2858 { //sector has a garrison
2859 if( !NumEnemiesInSector( (INT16)SECTORX( ubSectorID ), (INT16)SECTORY( ubSectorID ) ) )
2860 { //no enemies are here
2861 if( gArmyComp[ !gGarrisonGroup[ pSector->ubGarrisonID ].ubComposition ].bPriority )
2862 { //the garrison is important
2863 if( !gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID )
2864 { //the garrison doesn't have reinforcements already on route.
2865 gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
2866 MoveSAIGroupToSector( &pGroup, ubSectorID, STAGE, REINFORCEMENTS );
2867 break;
2868 }
2869 }
2870 }
2871 }
2872 else
2873 {
2874 MoveSAIGroupToSector( &pGroup, ubSectorID, EVASIVE, PURSUIT );
2875 }
2876
2877 break;
2878 case NPC_ACTION_SEND_SOLDIERS_TO_OMERTA:
2879 ubNumSoldiers = (UINT8)(gGameOptions.ubDifficultyLevel * 6); //6, 12, or 18 based on difficulty.
2880 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, ubNumSoldiers, (UINT8)(ubNumSoldiers/7) ); //add 1 elite to normal, and 2 for hard
2881 ubNumSoldiers = (UINT8)(ubNumSoldiers + ubNumSoldiers / 7);
2882 giReinforcementPool -= ubNumSoldiers;
2883 giReinforcementPool = MAX( giReinforcementPool, 0 );
2884 if( PlayerMercsInSector( 9, 1, 1 ) && !PlayerMercsInSector( 10, 1, 1 ) && !PlayerMercsInSector( 10, 1, 2 ) )
2885 { //send to A9 (if mercs in A9, but not in A10 or A10 basement)
2886 ubSectorID = SEC_A9;
2887 }
2888 else
2889 { //send to A10
2890 ubSectorID = SEC_A10;
2891 }
2892
2893 MoveSAIGroupToSector( &pGroup, ubSectorID, EVASIVE, PURSUIT );
2894
2895 ValidateGroup( pGroup );
2896 break;
2897 case NPC_ACTION_SEND_TROOPS_TO_SAM:
2898 ubSectorID = (UINT8)SECTOR( sSectorX, sSectorY );
2899 ubNumSoldiers = (UINT8)( 3 + gGameOptions.ubDifficultyLevel + HighestPlayerProgressPercentage() / 15 );
2900 giReinforcementPool -= ubNumSoldiers;
2901 giReinforcementPool = MAX( giReinforcementPool, 0 );
2902 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, 0, ubNumSoldiers );
2903 MoveSAIGroupToSector( &pGroup, ubSectorID, STAGE, REINFORCEMENTS );
2904
2905 if( gGarrisonGroup[ SectorInfo[ ubSectorID ].ubGarrisonID ].ubPendingGroupID )
2906 { //Clear the pending group's assignment.
2907 pPendingGroup = GetGroup( gGarrisonGroup[ SectorInfo[ ubSectorID ].ubGarrisonID ].ubPendingGroupID );
2908 Assert( pPendingGroup );
2909 RemoveGroupFromStrategicAILists(*pPendingGroup);
2910 }
2911 //Assign the elite squad to attack the SAM site
2912 pGroup->pEnemyGroup->ubIntention = REINFORCEMENTS;
2913 gGarrisonGroup[ SectorInfo[ ubSectorID ].ubGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
2914
2915 if( pPendingGroup )
2916 { //Reassign the pending group
2917 ReassignAIGroup( &pPendingGroup );
2918 }
2919
2920 break;
2921 case NPC_ACTION_ADD_MORE_ELITES:
2922 gfExtraElites = TRUE;
2923 EvolveQueenPriorityPhase( TRUE );
2924 break;
2925 case NPC_ACTION_GIVE_KNOWLEDGE_OF_ALL_MERCS:
2926 //temporarily make the queen's forces more aware (high alert)
2927 gubNumAwareBattles = saipolicy(num_aware_battles);
2928 break;
2929 default:
2930 SLOGD("QueenAI failed to handle action code %d.", usActionCode );
2931 break;
2932 }
2933 }
2934
2935 static UINT8 RedirectEnemyGroupsMovingThroughSector(UINT8 ubSectorX, UINT8 ubSectorY);
2936
2937
StrategicHandleQueenLosingControlOfSector(INT16 sSectorX,INT16 sSectorY,INT16 sSectorZ)2938 void StrategicHandleQueenLosingControlOfSector( INT16 sSectorX, INT16 sSectorY, INT16 sSectorZ )
2939 {
2940 SECTORINFO *pSector;
2941 UINT8 ubSectorID;
2942 if( sSectorZ )
2943 { //The queen doesn't care about anything happening under the ground.
2944 return;
2945 }
2946
2947 if( StrategicMap[ sSectorX + sSectorY * MAP_WORLD_X ].fEnemyControlled )
2948 { //If the sector doesn't belong to the player, then we shouldn't be calling this function!
2949 SLOGE( "StrategicHandleQueenLosingControlOfSector() was called for a sector that is internally considered to be enemy controlled." );
2950 return;
2951 }
2952
2953 ubSectorID = SECTOR( sSectorX, sSectorY );
2954 pSector = &SectorInfo[ ubSectorID ];
2955
2956 //Keep track of victories and wake up the queen after x number of battles.
2957 gusPlayerBattleVictories++;
2958 if( gusPlayerBattleVictories == 5 - gGameOptions.ubDifficultyLevel )
2959 { //4 victories for easy, 3 for normal, 2 for hard
2960 WakeUpQueen();
2961 }
2962
2963 if( pSector->ubGarrisonID == NO_GARRISON )
2964 { //Queen doesn't care if the sector lost wasn't a garrison sector.
2965 return;
2966 }
2967 else
2968 { //check to see if there are any pending reinforcements. If so, then cancel their orders and have them
2969 //reassigned, so the player doesn't get pestered. This is a feature that *dumbs* down the AI, and is done
2970 //for the sake of gameplay. We don't want the game to be tedious.
2971 if( !pSector->uiTimeLastPlayerLiberated )
2972 {
2973 pSector->uiTimeLastPlayerLiberated = GetWorldTotalSeconds();
2974 }
2975 else
2976 { //convert hours to seconds and subtract up to half of it randomly "seconds - (hours*3600 / 2)"
2977 pSector->uiTimeLastPlayerLiberated = GetWorldTotalSeconds() - Random( gubHoursGracePeriod * 1800 );
2978 }
2979 if( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID )
2980 {
2981 GROUP *pGroup;
2982 pGroup = GetGroup( gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID );
2983 if( pGroup )
2984 {
2985 ReassignAIGroup( &pGroup );
2986 }
2987 gGarrisonGroup[ pSector->ubGarrisonID ].ubPendingGroupID = 0;
2988 }
2989 }
2990
2991 //If there are any enemy groups that will be moving through this sector due, they will have to repath which
2992 //will cause them to avoid the sector. Returns the number of redirected groups.
2993 RedirectEnemyGroupsMovingThroughSector( (UINT8)sSectorX, (UINT8)sSectorY );
2994 }
2995
2996
SectorDistance(UINT8 ubSectorID1,UINT8 ubSectorID2)2997 static UINT8 SectorDistance(UINT8 ubSectorID1, UINT8 ubSectorID2)
2998 {
2999 UINT8 ubSectorX1, ubSectorX2, ubSectorY1, ubSectorY2;
3000 UINT8 ubDist;
3001 ubSectorX1 = (UINT8)SECTORX( ubSectorID1 );
3002 ubSectorX2 = (UINT8)SECTORX( ubSectorID2 );
3003 ubSectorY1 = (UINT8)SECTORY( ubSectorID1 );
3004 ubSectorY2 = (UINT8)SECTORY( ubSectorID2 );
3005
3006 ubDist = (UINT8)( ABS( ubSectorX1 - ubSectorX2 ) + ABS( ubSectorY1 - ubSectorY2 ) );
3007
3008 return ubDist;
3009 }
3010
3011
RequestHighPriorityGarrisonReinforcements(size_t iGarrisonID,UINT8 ubSoldiersRequested)3012 static void RequestHighPriorityGarrisonReinforcements(size_t iGarrisonID, UINT8 ubSoldiersRequested)
3013 {
3014 size_t i, iBestIndex;
3015 GROUP *pGroup;
3016 UINT8 ubBestDist, ubDist;
3017 UINT8 ubDstSectorX, ubDstSectorY;
3018 //AssertMsg( gPatrolGroup.size() == GCM->getPatrolGroups().size(), "Strategic AI -- Patrol group definition mismatch." );
3019 ubBestDist = 255;
3020 iBestIndex = -1;
3021 for( i = 0; i < gPatrolGroup.size(); i++ )
3022 {
3023 if( gPatrolGroup[ i ].ubGroupID )
3024 {
3025 pGroup = GetGroup( gPatrolGroup[ i ].ubGroupID );
3026 if( pGroup && pGroup->ubGroupSize >= ubSoldiersRequested )
3027 {
3028 ubDist = SectorDistance( (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ), gGarrisonGroup[ iGarrisonID ].ubSectorID );
3029 if( ubDist < ubBestDist )
3030 {
3031 ubBestDist = ubDist;
3032 iBestIndex = i;
3033 }
3034 }
3035 }
3036 }
3037 ubDstSectorX = (UINT8)SECTORX( gGarrisonGroup[ iGarrisonID ].ubSectorID );
3038 ubDstSectorY = (UINT8)SECTORY( gGarrisonGroup[ iGarrisonID ].ubSectorID );
3039 if( iBestIndex != (size_t)-1 )
3040 { //Send the group to the garrison
3041 pGroup = GetGroup( gPatrolGroup[ iBestIndex ].ubGroupID );
3042 if( pGroup->ubGroupSize > ubSoldiersRequested && pGroup->ubGroupSize - ubSoldiersRequested >= gubMinEnemyGroupSize )
3043 { //Split the group, and send to location
3044 GROUP *pNewGroup;
3045 pNewGroup = CreateNewEnemyGroupDepartingFromSector( (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ), 0, 0, 0 );
3046 //Transfer the troops from group to new group
3047 if( pGroup->pEnemyGroup->ubNumTroops >= ubSoldiersRequested )
3048 { //All of them are troops, so do it in one shot.
3049 pGroup->pEnemyGroup->ubNumTroops -= ubSoldiersRequested;
3050 pGroup->ubGroupSize -= ubSoldiersRequested;
3051 pNewGroup->pEnemyGroup->ubNumTroops = ubSoldiersRequested;
3052 pNewGroup->ubGroupSize += ubSoldiersRequested;
3053 ValidateLargeGroup( pGroup );
3054 ValidateLargeGroup( pNewGroup );
3055 }
3056 else while( ubSoldiersRequested )
3057 { //There aren't enough troops, so transfer other types when we run out of troops, prioritizing admins, then elites.
3058 if( pGroup->pEnemyGroup->ubNumTroops )
3059 {
3060 pGroup->pEnemyGroup->ubNumTroops--;
3061 pGroup->ubGroupSize--;
3062 pNewGroup->pEnemyGroup->ubNumTroops++;
3063 pNewGroup->ubGroupSize++;
3064 ubSoldiersRequested--;
3065 ValidateLargeGroup( pGroup );
3066 ValidateLargeGroup( pNewGroup );
3067 }
3068 else if( pGroup->pEnemyGroup->ubNumAdmins )
3069 {
3070 pGroup->pEnemyGroup->ubNumAdmins--;
3071 pGroup->ubGroupSize--;
3072 pNewGroup->pEnemyGroup->ubNumAdmins++;
3073 pNewGroup->ubGroupSize++;
3074 ubSoldiersRequested--;
3075 ValidateLargeGroup( pGroup );
3076 ValidateLargeGroup( pNewGroup );
3077 }
3078 else if( pGroup->pEnemyGroup->ubNumElites )
3079 {
3080 pGroup->pEnemyGroup->ubNumElites--;
3081 pGroup->ubGroupSize--;
3082 pNewGroup->pEnemyGroup->ubNumElites++;
3083 pNewGroup->ubGroupSize++;
3084 ubSoldiersRequested--;
3085 ValidateLargeGroup( pGroup );
3086 ValidateLargeGroup( pNewGroup );
3087 }
3088 else
3089 {
3090 SLOGA("Strategic AI group transfer error." );
3091 return;
3092 }
3093 }
3094 pNewGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
3095 gGarrisonGroup[ iGarrisonID ].ubPendingGroupID = pNewGroup->ubGroupID;
3096 RecalculatePatrolWeight(gPatrolGroup[iBestIndex]);
3097
3098 MoveSAIGroupToSector( &pNewGroup, gGarrisonGroup[ iGarrisonID ].ubSectorID, EVASIVE, REINFORCEMENTS );
3099 }
3100 else
3101 { //Send the whole group and kill it's patrol assignment.
3102 gPatrolGroup[ iBestIndex ].ubGroupID = 0;
3103 gGarrisonGroup[ iGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
3104 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
3105 RecalculatePatrolWeight(gPatrolGroup[iBestIndex]);
3106 //The ONLY case where the group is told to move somewhere else when they could be BETWEEN sectors. The movegroup functions
3107 //don't work if this is the case. Teleporting them to their previous sector is the best and easiest way to deal with this.
3108 SetEnemyGroupSector(*pGroup, SECTOR(pGroup->ubSectorX, pGroup->ubSectorY));
3109
3110 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iGarrisonID ].ubSectorID, EVASIVE, REINFORCEMENTS );
3111 ValidateGroup( pGroup );
3112 }
3113 }
3114 else
3115 { //There are no groups that have enough troops. Send a new force from the palace instead.
3116 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_P3, 0, ubSoldiersRequested, 0 );
3117 pGroup->ubMoveType = ONE_WAY;
3118 pGroup->pEnemyGroup->ubIntention = REINFORCEMENTS;
3119 gGarrisonGroup[ iGarrisonID ].ubPendingGroupID = pGroup->ubGroupID;
3120 pGroup->ubOriginalSector = (UINT8)SECTOR( ubDstSectorX, ubDstSectorY );
3121 giReinforcementPool -= (INT32)ubSoldiersRequested;
3122
3123 MoveSAIGroupToSector( &pGroup, gGarrisonGroup[ iGarrisonID ].ubSectorID, EVASIVE, REINFORCEMENTS );
3124 ValidateGroup( pGroup );
3125 }
3126 }
3127
3128
3129 static void MassFortifyTowns(void);
3130
3131
WakeUpQueen()3132 void WakeUpQueen()
3133 {
3134 gfQueenAIAwake = TRUE;
3135 if( !gfMassFortificationOrdered )
3136 {
3137 gfMassFortificationOrdered = TRUE;
3138 MassFortifyTowns();
3139 }
3140 }
3141
3142
3143 //Simply orders all garrisons to take troops from the patrol groups and send the closest troops from them. Any garrison,
3144 //whom there request isn't fulfilled (due to lack of troops), will recieve their reinforcements from the queen (P3).
MassFortifyTowns(void)3145 static void MassFortifyTowns(void)
3146 {
3147 size_t i;
3148 SECTORINFO *pSector;
3149 GROUP *pGroup;
3150 UINT8 ubNumTroops, ubDesiredTroops;
3151 for( i = 0; i < gGarrisonGroup.size(); i++ )
3152 {
3153 pSector = &SectorInfo[ gGarrisonGroup[ i ].ubSectorID ];
3154 ubNumTroops = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
3155 ubDesiredTroops = (UINT8)gArmyComp[ gGarrisonGroup[ i ].ubComposition ].bDesiredPopulation;
3156 if( ubNumTroops < ubDesiredTroops )
3157 {
3158 if( !gGarrisonGroup[ i ].ubPendingGroupID &&
3159 gGarrisonGroup[ i ].ubComposition != ROADBLOCK &&
3160 EnemyPermittedToAttackSector( NULL, gGarrisonGroup[ i ].ubSectorID ) )
3161 {
3162 RequestHighPriorityGarrisonReinforcements( i, (UINT8)(ubDesiredTroops - ubNumTroops) );
3163 }
3164 }
3165 }
3166 //Convert the garrison sitting in Omerta (if alive), and reassign them
3167 pSector = &SectorInfo[ SEC_A9 ];
3168 if( pSector->ubNumTroops )
3169 {
3170 pGroup = CreateNewEnemyGroupDepartingFromSector( SEC_A9, 0, pSector->ubNumTroops, 0 );
3171 pSector->ubNumTroops = 0;
3172 pGroup->pEnemyGroup->ubIntention = PATROL;
3173 pGroup->ubMoveType = ONE_WAY;
3174 ReassignAIGroup( &pGroup );
3175 ValidateGroup( pGroup );
3176 RecalculateSectorWeight( SEC_A9 );
3177 }
3178 }
3179
StrategicHandleMineThatRanOut(UINT8 ubSectorID)3180 void StrategicHandleMineThatRanOut( UINT8 ubSectorID )
3181 {
3182 switch( ubSectorID )
3183 {
3184 case SEC_B2:
3185 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_A2 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3186 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_B2 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3187 break;
3188 case SEC_D13:
3189 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_B13 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3190 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_C13 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3191 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_D13 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3192 break;
3193 case SEC_H8:
3194 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_F8 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3195 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_F9 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3196 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_G8 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3197 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_G9 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3198 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_H8 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3199 break;
3200 case SEC_I14:
3201 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_H13 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3202 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_H14 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3203 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_I13 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3204 gArmyComp[ gGarrisonGroup[ SectorInfo[ SEC_I14 ].ubGarrisonID ].ubComposition ].bPriority /= 4;
3205 break;
3206 }
3207 }
3208
3209
GarrisonCanProvideMinimumReinforcements(INT32 iGarrisonID)3210 static BOOLEAN GarrisonCanProvideMinimumReinforcements(INT32 iGarrisonID)
3211 {
3212 INT32 iAvailable;
3213 INT32 iDesired;
3214 SECTORINFO *pSector;
3215 UINT8 ubSectorX, ubSectorY;
3216
3217 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
3218
3219 iAvailable = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
3220 iDesired = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation;
3221
3222 if( iAvailable - iDesired >= gubMinEnemyGroupSize )
3223 {
3224 //Do a more expensive check first to determine if there is a player presence here (combat in progress)
3225 //If so, do not provide reinforcements from here.
3226 ubSectorX = (UINT8)SECTORX( gGarrisonGroup[ iGarrisonID ].ubSectorID );
3227 ubSectorY = (UINT8)SECTORY( gGarrisonGroup[ iGarrisonID ].ubSectorID );
3228 if( PlayerMercsInSector( ubSectorX, ubSectorY, 0 ) || CountAllMilitiaInSector( ubSectorX, ubSectorY ) )
3229 {
3230 return FALSE;
3231 }
3232 return TRUE;
3233 }
3234 return FALSE;
3235 }
3236
3237
GarrisonRequestingMinimumReinforcements(INT32 iGarrisonID)3238 static BOOLEAN GarrisonRequestingMinimumReinforcements(INT32 iGarrisonID)
3239 {
3240 INT32 iAvailable;
3241 INT32 iDesired;
3242 SECTORINFO *pSector;
3243
3244 if( gGarrisonGroup[ iGarrisonID ].ubPendingGroupID )
3245 {
3246 return FALSE;
3247 }
3248
3249 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
3250 iAvailable = pSector->ubNumAdmins + pSector->ubNumTroops + pSector->ubNumElites;
3251 iDesired = gArmyComp[ gGarrisonGroup[ iGarrisonID ].ubComposition ].bDesiredPopulation;
3252
3253 if( iDesired - iAvailable >= gubMinEnemyGroupSize )
3254 {
3255 return TRUE;
3256 }
3257 return FALSE;
3258 }
3259
3260
3261 static BOOLEAN PermittedToFillPatrolGroup(INT32 iPatrolID);
3262
3263
PatrolRequestingMinimumReinforcements(INT32 iPatrolID)3264 static BOOLEAN PatrolRequestingMinimumReinforcements(INT32 iPatrolID)
3265 {
3266 GROUP *pGroup;
3267
3268 if( gPatrolGroup[ iPatrolID ].ubPendingGroupID )
3269 {
3270 return FALSE;
3271 }
3272 if( !PermittedToFillPatrolGroup( iPatrolID ) )
3273 { //if the group was defeated, it won't be considered for reinforcements again for several days
3274 return FALSE;
3275 }
3276 pGroup = GetGroup( gPatrolGroup[ iPatrolID ].ubGroupID );
3277 if( pGroup )
3278 {
3279 if( gPatrolGroup[ iPatrolID ].bSize - pGroup->ubGroupSize >= gubMinEnemyGroupSize )
3280 {
3281 return TRUE;
3282 }
3283 }
3284 return FALSE;
3285 }
3286
3287
EliminateSurplusTroopsForGarrison(GROUP * pGroup,SECTORINFO * pSector)3288 static void EliminateSurplusTroopsForGarrison(GROUP* pGroup, SECTORINFO* pSector)
3289 {
3290 INT32 iTotal;
3291 iTotal = pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites + pGroup->pEnemyGroup->ubNumAdmins +
3292 pSector->ubNumTroops + pSector->ubNumElites + pSector->ubNumAdmins;
3293 if( iTotal <= MAX_STRATEGIC_TEAM_SIZE )
3294 {
3295 return;
3296 }
3297 iTotal -= MAX_STRATEGIC_TEAM_SIZE;
3298 while( iTotal )
3299 {
3300 if( pGroup->pEnemyGroup->ubNumAdmins )
3301 {
3302 if( pGroup->pEnemyGroup->ubNumAdmins < iTotal )
3303 {
3304 iTotal -= pGroup->pEnemyGroup->ubNumAdmins;
3305 pGroup->pEnemyGroup->ubNumAdmins = 0;
3306 }
3307 else
3308 {
3309 pGroup->pEnemyGroup->ubNumAdmins -= (UINT8)iTotal;
3310 iTotal = 0;
3311 }
3312 }
3313 else if( pSector->ubNumAdmins )
3314 {
3315 if( pSector->ubNumAdmins < iTotal )
3316 {
3317 iTotal -= pSector->ubNumAdmins;
3318 pSector->ubNumAdmins = 0;
3319 }
3320 else
3321 {
3322 pSector->ubNumAdmins -= (UINT8)iTotal;
3323 iTotal = 0;
3324 }
3325 }
3326 else if( pGroup->pEnemyGroup->ubNumTroops )
3327 {
3328 if( pGroup->pEnemyGroup->ubNumTroops < iTotal )
3329 {
3330 iTotal -= pGroup->pEnemyGroup->ubNumTroops;
3331 pGroup->pEnemyGroup->ubNumTroops = 0;
3332 }
3333 else
3334 {
3335 pGroup->pEnemyGroup->ubNumTroops -= (UINT8)iTotal;
3336 iTotal = 0;
3337 }
3338 }
3339 else if( pSector->ubNumTroops )
3340 {
3341 if( pSector->ubNumTroops < iTotal )
3342 {
3343 iTotal -= pSector->ubNumTroops;
3344 pSector->ubNumTroops = 0;
3345 }
3346 else
3347 {
3348 pSector->ubNumTroops -= (UINT8)iTotal;
3349 iTotal = 0;
3350 }
3351 }
3352 else if( pGroup->pEnemyGroup->ubNumElites )
3353 {
3354 if( pGroup->pEnemyGroup->ubNumElites < iTotal )
3355 {
3356 iTotal -= pGroup->pEnemyGroup->ubNumElites;
3357 pGroup->pEnemyGroup->ubNumElites = 0;
3358 }
3359 else
3360 {
3361 pGroup->pEnemyGroup->ubNumElites -= (UINT8)iTotal;
3362 iTotal = 0;
3363 }
3364 }
3365 else if( pSector->ubNumElites )
3366 {
3367 if( pSector->ubNumElites < iTotal )
3368 {
3369 iTotal -= pSector->ubNumElites;
3370 pSector->ubNumElites = 0;
3371 }
3372 else
3373 {
3374 pSector->ubNumElites -= (UINT8)iTotal;
3375 iTotal = 0;
3376 }
3377 }
3378 }
3379 }
3380
3381
3382 /* Once Queen is awake, she'll gradually begin replacing admins with regular
3383 * troops. This is mainly to keep player from fighting many more admins once
3384 * they are no longer any challenge for him. Eventually all admins will vanish
3385 * off map. */
UpgradeAdminsToTroops()3386 static void UpgradeAdminsToTroops()
3387 {
3388 /* On normal, AI evaluates approximately every 10 hrs. There are about
3389 * 130 administrators seeded on the map. Some of these will be killed by the
3390 * player. */
3391
3392 INT32 const min_priority = 100 - 10 * HighestPlayerProgressPercentage();
3393
3394 // Check all garrisons for administrators.
3395 UINT const cur = GetWorldSector();
3396 //GARRISON_GROUP const* const end = gGarrisonGroup + gGarrisonGroup.size();
3397 for (auto i = gGarrisonGroup.begin(); i != gGarrisonGroup.end(); ++i)
3398 {
3399 GARRISON_GROUP const& g = *i;
3400
3401 // Skip sector if it's currently loaded, we'll never upgrade guys in those.
3402 if (cur == g.ubSectorID) continue;
3403
3404 SECTORINFO& sector = SectorInfo[g.ubSectorID];
3405 if (sector.ubNumAdmins == 0) continue; // No admins in garrison.
3406
3407 INT8 const priority = gArmyComp[g.ubComposition].bPriority;
3408
3409 /* Highest priority sectors are upgraded first. Each 1% of progress lowers
3410 * the priority threshold required to start triggering upgrades by 10%. */
3411 if (priority <= min_priority) continue;
3412
3413 for (UINT8 n_to_check = sector.ubNumAdmins; n_to_check != 0; --n_to_check)
3414 {
3415 /* Chance to upgrade at each check is random and is dependent on the
3416 * garrison's priority. */
3417 if (!Chance(priority)) continue;
3418 --sector.ubNumAdmins;
3419 ++sector.ubNumTroops;
3420 }
3421 }
3422
3423 // Check all moving enemy groups for administrators.
3424 FOR_EACH_ENEMY_GROUP(i)
3425 {
3426 GROUP const& g = *i;
3427 if (g.ubGroupSize == 0) continue;
3428 if (g.fVehicle) continue;
3429
3430 // Skip sector if it's currently loaded, we'll never upgrade guys in those.
3431 if (g.ubSectorX == gWorldSectorX && g.ubSectorY == gWorldSectorY) continue;
3432
3433 Assert(g.pEnemyGroup);
3434 ENEMYGROUP& eg = *g.pEnemyGroup;
3435 if (eg.ubNumAdmins == 0) continue; // No admins in group.
3436
3437 INT8 priority;
3438 if (eg.ubIntention == PATROL)
3439 { // Use that patrol's priority.
3440 size_t const patrol_id = FindPatrolGroupIndexForGroupID(g.ubGroupID);
3441 Assert(patrol_id != (size_t)-1);
3442 priority = gPatrolGroup[patrol_id].bPriority;
3443 }
3444 else
3445 { // Use a default priority.
3446 priority = 50;
3447 }
3448
3449 /* Highest priority groups are upgraded first. Each 1% of progress lowers
3450 * the priority threshold required to start triggering upgrades by 10%. */
3451 if (priority <= min_priority) continue;
3452
3453 for (UINT8 n_to_check = eg.ubNumAdmins; n_to_check != 0; --n_to_check)
3454 {
3455 /* Chance to upgrade at each check is random and is dependent on the
3456 * group's priority. */
3457 if (!Chance(priority)) continue;
3458 --eg.ubNumAdmins;
3459 ++eg.ubNumTroops;
3460 }
3461 }
3462 }
3463
3464
FindPatrolGroupIndexForGroupID(UINT8 ubGroupID)3465 size_t FindPatrolGroupIndexForGroupID( UINT8 ubGroupID )
3466 {
3467 for( size_t sPatrolIndex = 0; sPatrolIndex < gPatrolGroup.size(); sPatrolIndex++ )
3468 {
3469 if ( gPatrolGroup[ sPatrolIndex ].ubGroupID == ubGroupID )
3470 {
3471 // found it
3472 return( sPatrolIndex );
3473 }
3474 }
3475
3476 // not there!
3477 return( -1 );
3478 }
3479
3480
FindPatrolGroupIndexForGroupIDPending(UINT8 ubGroupID)3481 size_t FindPatrolGroupIndexForGroupIDPending( UINT8 ubGroupID )
3482 {
3483 for( size_t sPatrolIndex = 0; sPatrolIndex < gPatrolGroup.size(); sPatrolIndex++ )
3484 {
3485 if ( gPatrolGroup[ sPatrolIndex ].ubPendingGroupID == ubGroupID )
3486 {
3487 // found it
3488 return( sPatrolIndex );
3489 }
3490 }
3491
3492 // not there!
3493 return( -1 );
3494 }
3495
3496
FindGarrisonIndexForGroupIDPending(UINT8 ubGroupID)3497 size_t FindGarrisonIndexForGroupIDPending( UINT8 ubGroupID )
3498 {
3499 for( size_t sGarrisonIndex = 0; sGarrisonIndex < gGarrisonGroup.size(); sGarrisonIndex++ )
3500 {
3501 if ( gGarrisonGroup[ sGarrisonIndex ].ubPendingGroupID == ubGroupID )
3502 {
3503 // found it
3504 return( sGarrisonIndex );
3505 }
3506 }
3507
3508 // not there!
3509 return( -1 );
3510 }
3511
3512
TransferGroupToPool(GROUP ** pGroup)3513 static void TransferGroupToPool(GROUP** pGroup)
3514 {
3515 giReinforcementPool += (*pGroup)->ubGroupSize;
3516 RemoveGroup(**pGroup);
3517 *pGroup = NULL;
3518 }
3519
3520
3521 //NOTE: Make sure you call SetEnemyGroupSector() first if the group is between sectors!! See example in ReassignAIGroup()...
SendGroupToPool(GROUP ** pGroup)3522 static void SendGroupToPool(GROUP** pGroup)
3523 {
3524 if( (*pGroup)->ubSectorX == 3 && (*pGroup)->ubSectorY == 16 )
3525 {
3526 TransferGroupToPool( pGroup );
3527 }
3528 else
3529 {
3530 (*pGroup)->ubSectorIDOfLastReassignment = (UINT8)SECTOR( (*pGroup)->ubSectorX, (*pGroup)->ubSectorY );
3531 MoveSAIGroupToSector( pGroup, SEC_P3, EVASIVE, REINFORCEMENTS );
3532 }
3533 }
3534
3535
ReassignAIGroup(GROUP ** pGroup)3536 static void ReassignAIGroup(GROUP** pGroup)
3537 {
3538 UINT32 i, iRandom;
3539 UINT32 iWeight;
3540 UINT16 usDefencePoints;
3541 size_t iReloopLastIndex = -1;
3542 UINT8 ubSectorID;
3543
3544 ubSectorID = (UINT8)SECTOR( (*pGroup)->ubSectorX, (*pGroup)->ubSectorY );
3545
3546 (*pGroup)->ubSectorIDOfLastReassignment = ubSectorID;
3547
3548 RemoveGroupFromStrategicAILists(**pGroup);
3549
3550 //First thing to do, is teleport the group to be AT the sector he is currently moving from. Otherwise, the
3551 //strategic pathing can break if the group is between sectors upon reassignment.
3552 SetEnemyGroupSector(**pGroup, ubSectorID);
3553
3554 if( giRequestPoints <= 0 )
3555 { //we have no request for reinforcements, so send the group to Meduna for reassignment in the pool.
3556 SendGroupToPool( pGroup );
3557 return;
3558 }
3559
3560 //now randomly choose who gets the reinforcements.
3561 // giRequestPoints is the combined sum of all the individual weights of all garrisons and patrols requesting reinforcements
3562 iRandom = Random( giRequestPoints );
3563
3564 //go through garrisons first and begin considering where the random value dictates. If that garrison doesn't require
3565 //reinforcements, it'll continue on considering all subsequent garrisons till the end of the array. If it fails at that
3566 //point, it'll restart the loop at zero, and consider all garrisons to the index that was first considered by the random value.
3567 for( i = 0; i < gGarrisonGroup.size(); i++ )
3568 {
3569 RecalculateGarrisonWeight( i );
3570 iWeight = gGarrisonGroup[ i ].bWeight;
3571 if( iWeight > 0 )
3572 { //if group is requesting reinforcements.
3573 if( iRandom < iWeight )
3574 {
3575 if( !gGarrisonGroup[ i ].ubPendingGroupID &&
3576 EnemyPermittedToAttackSector( NULL, gGarrisonGroup[ i ].ubSectorID ) &&
3577 GarrisonRequestingMinimumReinforcements( i ) )
3578 { //This is the group that gets the reinforcements!
3579 if( ReinforcementsApproved( i, &usDefencePoints ) )
3580 {
3581 SendReinforcementsForGarrison( i, usDefencePoints, pGroup );
3582 return;
3583 }
3584 }
3585 if( iReloopLastIndex == (size_t)-1 )
3586 { //go to the next garrison and clear the iRandom value so it attempts to use all subsequent groups.
3587 iReloopLastIndex = i - 1;
3588 iRandom = 0;
3589 }
3590 }
3591 //Decrease the iRandom value until it hits 0. When that happens, all garrisons will get considered until
3592 //we either have a match or process all of the garrisons.
3593 iRandom -= iWeight;
3594 }
3595 }
3596 if( iReloopLastIndex >= 0 )
3597 { //Process the loop again to the point where the original random slot started considering, and consider
3598 //all of the garrisons. If this fails, all patrol groups will be considered next.
3599 for( i = 0; i <= iReloopLastIndex; i++ )
3600 {
3601 RecalculateGarrisonWeight( i );
3602 iWeight = gGarrisonGroup[ i ].bWeight;
3603 if( iWeight > 0 )
3604 { //if group is requesting reinforcements.
3605 if( !gGarrisonGroup[ i ].ubPendingGroupID &&
3606 EnemyPermittedToAttackSector( NULL, gGarrisonGroup[ i ].ubSectorID ) &&
3607 GarrisonRequestingMinimumReinforcements( i ) )
3608 { //This is the group that gets the reinforcements!
3609 if( ReinforcementsApproved( i, &usDefencePoints ) )
3610 {
3611 SendReinforcementsForGarrison( i, usDefencePoints, pGroup );
3612 return;
3613 }
3614 }
3615 }
3616 }
3617 }
3618 if( iReloopLastIndex == (size_t)-1 )
3619 {
3620 //go through the patrol groups
3621 for( i = 0; i < gPatrolGroup.size(); i++ )
3622 {
3623 RecalculatePatrolWeight(gPatrolGroup[i]);
3624 iWeight = gPatrolGroup[ i ].bWeight;
3625 if( iWeight > 0 )
3626 {
3627 if( iRandom < iWeight )
3628 {
3629 if( !gPatrolGroup[ i ].ubPendingGroupID && PatrolRequestingMinimumReinforcements( i ) )
3630 { //This is the group that gets the reinforcements!
3631 SendReinforcementsForPatrol( i, pGroup );
3632 return;
3633 }
3634 }
3635 if( iReloopLastIndex == (size_t)-1 )
3636 {
3637 iReloopLastIndex = i - 1;
3638 iRandom = 0;
3639 }
3640 iRandom -= iWeight;
3641 }
3642 }
3643 }
3644 else
3645 {
3646 iReloopLastIndex = gPatrolGroup.size() - 1;
3647 }
3648
3649 for( i = 0; i <= iReloopLastIndex; i++ )
3650 {
3651 RecalculatePatrolWeight(gPatrolGroup[i]);
3652 iWeight = gPatrolGroup[ i ].bWeight;
3653 if( iWeight > 0 )
3654 {
3655 if( !gPatrolGroup[ i ].ubPendingGroupID && PatrolRequestingMinimumReinforcements( i ) )
3656 { //This is the group that gets the reinforcements!
3657 SendReinforcementsForPatrol( i, pGroup );
3658 return;
3659 }
3660 }
3661 }
3662 TransferGroupToPool( pGroup );
3663 }
3664
3665
3666 /* When an enemy AI group is eliminated by the player, apply a grace period in
3667 * which the group isn't allowed to be filled for several days. */
TagSAIGroupWithGracePeriod(GROUP const & g)3668 static void TagSAIGroupWithGracePeriod(GROUP const& g)
3669 {
3670 size_t const patrol_id = FindPatrolGroupIndexForGroupID(g.ubGroupID);
3671 if (patrol_id == (size_t)-1) return;
3672
3673 UINT32 grace_period = saipolicy(patrol_grace_period_in_days);
3674 gPatrolGroup[patrol_id].bFillPermittedAfterDayMod100 = (GetWorldDay() + grace_period) % 100;
3675 }
3676
3677
PermittedToFillPatrolGroup(INT32 iPatrolID)3678 static BOOLEAN PermittedToFillPatrolGroup(INT32 iPatrolID)
3679 {
3680 INT32 iDay;
3681 INT32 iDayAllowed;
3682 iDay = GetWorldDay();
3683 iDayAllowed = gPatrolGroup[ iPatrolID ].bFillPermittedAfterDayMod100 + (iDay / 100) * 100;
3684 return iDay >= iDayAllowed;
3685 }
3686
RepollSAIGroup(GROUP * pGroup)3687 void RepollSAIGroup( GROUP *pGroup )
3688 {
3689 UINT32 i;
3690 Assert( !pGroup->fPlayer );
3691 if( GroupAtFinalDestination( pGroup ) )
3692 {
3693 EvaluateGroupSituation( pGroup );
3694 return;
3695 }
3696 for( i = 0; i < gPatrolGroup.size(); i++ )
3697 {
3698 if( gPatrolGroup[ i ].ubGroupID == pGroup->ubGroupID )
3699 {
3700 RecalculatePatrolWeight(gPatrolGroup[i]); //in case there are any dead enemies
3701 CalculateNextMoveIntention( pGroup );
3702 return;
3703 }
3704 }
3705 for( i = 0; i < gGarrisonGroup.size(); i++ )
3706 {
3707 //KM : August 6, 1999 Patch fix
3708 // Ack, wasn't checking for the matching group to garrison
3709 if( gGarrisonGroup[ i ].ubPendingGroupID == pGroup->ubGroupID )
3710 //end
3711 {
3712 RecalculateGarrisonWeight( i ); //in case there are any dead enemies
3713 CalculateNextMoveIntention( pGroup );
3714 return;
3715 }
3716 }
3717 }
3718
3719
CalcNumTroopsBasedOnComposition(UINT8 * pubNumTroops,UINT8 * pubNumElites,UINT8 ubTotal,INT32 iCompositionID)3720 static void CalcNumTroopsBasedOnComposition(UINT8* pubNumTroops, UINT8* pubNumElites, UINT8 ubTotal, INT32 iCompositionID)
3721 {
3722 *pubNumTroops = gArmyComp[ iCompositionID ].bTroopPercentage * ubTotal / 100;
3723 *pubNumElites = gArmyComp[ iCompositionID ].bElitePercentage * ubTotal / 100;
3724
3725 //Due to low roundoff, it is highly possible that we will be short one soldier.
3726 while( *pubNumTroops + *pubNumElites < ubTotal )
3727 {
3728 if( Chance( gArmyComp[ iCompositionID ].bTroopPercentage ) )
3729 {
3730 (*pubNumTroops)++;
3731 }
3732 else
3733 {
3734 (*pubNumElites)++;
3735 }
3736 }
3737 Assert( *pubNumTroops + *pubNumElites == ubTotal );
3738 }
3739
3740
ConvertGroupTroopsToComposition(GROUP * pGroup,INT32 iCompositionID)3741 static void ConvertGroupTroopsToComposition(GROUP* pGroup, INT32 iCompositionID)
3742 {
3743 Assert( pGroup );
3744 Assert( !pGroup->fPlayer );
3745 CalcNumTroopsBasedOnComposition( &pGroup->pEnemyGroup->ubNumTroops, &pGroup->pEnemyGroup->ubNumElites, pGroup->ubGroupSize, iCompositionID );
3746 pGroup->pEnemyGroup->ubNumAdmins = 0;
3747 pGroup->ubGroupSize = pGroup->pEnemyGroup->ubNumTroops + pGroup->pEnemyGroup->ubNumElites;
3748 ValidateLargeGroup( pGroup );
3749 }
3750
3751
RemoveSoldiersFromGarrisonBasedOnComposition(INT32 iGarrisonID,UINT8 ubSize)3752 static void RemoveSoldiersFromGarrisonBasedOnComposition(INT32 iGarrisonID, UINT8 ubSize)
3753 {
3754 SECTORINFO *pSector;
3755 INT32 iCompositionID;
3756 UINT8 ubNumTroops, ubNumElites;
3757
3758 iCompositionID = gGarrisonGroup[ iGarrisonID ].ubComposition;
3759
3760 CalcNumTroopsBasedOnComposition( &ubNumTroops, &ubNumElites, ubSize, iCompositionID );
3761 pSector = &SectorInfo[ gGarrisonGroup[ iGarrisonID ].ubSectorID ];
3762 //if there are administrators in this sector, remove them first.
3763
3764 while( ubSize && pSector->ubNumAdmins )
3765 {
3766 pSector->ubNumAdmins--;
3767 ubSize--;
3768 if( ubNumTroops )
3769 {
3770 ubNumTroops--;
3771 }
3772 else
3773 {
3774 ubNumElites--;
3775 }
3776 }
3777 //No administrators are left.
3778
3779 //Eliminate the troops
3780 while( ubNumTroops )
3781 {
3782 if( pSector->ubNumTroops )
3783 {
3784 pSector->ubNumTroops--;
3785 }
3786 else if( pSector->ubNumElites )
3787 {
3788 pSector->ubNumElites--;
3789 }
3790 else
3791 {
3792 SLOGA("RemoveSoldiersFromGarrisonBasedOnComposition: trying to eliminate more troops than present");
3793 }
3794 ubNumTroops--;
3795 }
3796
3797 //Eliminate the elites
3798 while( ubNumElites )
3799 {
3800 if( pSector->ubNumElites )
3801 {
3802 pSector->ubNumElites--;
3803 }
3804 else if( pSector->ubNumTroops )
3805 {
3806 pSector->ubNumTroops--;
3807 }
3808 else
3809 {
3810 SLOGA("RemoveSoldiersFromGarrisonBasedOnComposition: trying to eliminate more elites than present");
3811 }
3812 ubNumElites--;
3813 }
3814
3815 RecalculateGarrisonWeight( iGarrisonID );
3816 }
3817
3818
MoveSAIGroupToSector(GROUP ** const pGroup,UINT8 const sector,SAIMOVECODE const move_code,UINT8 const intention)3819 static void MoveSAIGroupToSector(GROUP** const pGroup, UINT8 const sector, SAIMOVECODE const move_code, UINT8 const intention)
3820 {
3821 UINT8 const dst_x = SECTORX(sector);
3822 UINT8 const dst_y = SECTORY(sector);
3823 GROUP& g = **pGroup;
3824
3825 if (g.fBetweenSectors) SetEnemyGroupSector(g, SECTOR(g.ubSectorX, g.ubSectorY));
3826
3827 g.pEnemyGroup->ubIntention = intention;
3828 g.ubMoveType = ONE_WAY;
3829
3830 /* Make sure that the group isn't moving into a garrison sector. These sectors
3831 * should be using ASSAULT intentions! */
3832 Assert(intention != PURSUIT || SectorInfo[sector].ubGarrisonID == NO_GARRISON);
3833
3834 /* If the destination sector is the current location. Instead of causing code
3835 * logic problems, simply process them as if they just arrived. */
3836 if (g.ubSectorX == dst_x && g.ubSectorY == dst_y && EvaluateGroupSituation(&g))
3837 { // The group was deleted.
3838 *pGroup = 0;
3839 return;
3840 }
3841
3842 UINT8 const x = g.ubSectorX;
3843 UINT8 const y = g.ubSectorY;
3844 switch (move_code)
3845 {
3846 case STAGE: MoveGroupFromSectorToSectorButAvoidPlayerInfluencedSectorsAndStopOneSectorBeforeEnd(g, x, y, dst_x, dst_y); break;
3847 case EVASIVE: MoveGroupFromSectorToSectorButAvoidPlayerInfluencedSectors( g, x, y, dst_x, dst_y); break;
3848 case DIRECT: MoveGroupFromSectorToSector( g, x, y, dst_x, dst_y); break;
3849 }
3850 /* Make sure that the group is moving. If this fails, then the pathing may
3851 * have failed for some reason. */
3852 ValidateGroup(&g);
3853 }
3854
3855
3856 //If there are any enemy groups that will be moving through this sector due, they will have to repath which
3857 //will cause them to avoid the sector. Returns the number of redirected groups.
RedirectEnemyGroupsMovingThroughSector(UINT8 ubSectorX,UINT8 ubSectorY)3858 static UINT8 RedirectEnemyGroupsMovingThroughSector(UINT8 ubSectorX, UINT8 ubSectorY)
3859 {
3860 UINT8 ubNumGroupsRedirected = 0;
3861 WAYPOINT *pWaypoint;
3862 UINT8 ubDestSectorID;
3863 FOR_EACH_ENEMY_GROUP(pGroup)
3864 {
3865 if (pGroup->ubMoveType == ONE_WAY)
3866 { //check the waypoint list
3867 if( GroupWillMoveThroughSector( pGroup, ubSectorX, ubSectorY ) )
3868 {
3869 //extract the group's destination.
3870 pWaypoint = GetFinalWaypoint( pGroup );
3871 Assert( pWaypoint );
3872 ubDestSectorID = (UINT8)SECTOR( pWaypoint->x, pWaypoint->y );
3873 SetEnemyGroupSector(*pGroup, SECTOR(pGroup->ubSectorX, pGroup->ubSectorY));
3874 MoveSAIGroupToSector( &pGroup, ubDestSectorID, EVASIVE, pGroup->pEnemyGroup->ubIntention );
3875 ubNumGroupsRedirected++;
3876 }
3877 }
3878 }
3879 if( ubNumGroupsRedirected )
3880 {
3881 SLOGD("Test message for new feature: %d enemy groups were redirected away from moving through sector %c%d. Please don't report unless this number is greater than 5.",
3882 ubNumGroupsRedirected, ubSectorY + 'A' - 1, ubSectorX );
3883 }
3884 return ubNumGroupsRedirected;
3885 }
3886
3887
3888 //when the SAI compositions change, it is necessary to call this function upon version load,
3889 //to reflect the changes of the compositions to the sector that haven't been visited yet.
ReinitializeUnvisitedGarrisons(void)3890 static void ReinitializeUnvisitedGarrisons(void)
3891 {
3892 SECTORINFO *pSector;
3893 ARMY_COMPOSITION *pArmyComp;
3894 GROUP *pGroup;
3895 UINT32 i, cnt, iEliteChance, iAdminChance;
3896
3897 //Recreate the compositions
3898 gArmyComp = GCM->getArmyCompositions();
3899 EvolveQueenPriorityPhase( TRUE );
3900
3901 //Go through each unvisited sector and recreate the garrison forces based on
3902 //the desired population.
3903 for( i = 0; i < gGarrisonGroup.size(); i++ )
3904 {
3905 if( gGarrisonGroup[ i ].ubComposition >= LEVEL1_DEFENCE && gGarrisonGroup[ i ].ubComposition <= LEVEL3_DEFENCE )
3906 { //These 3 compositions make up the perimeter around Meduna. The existance of these are based on the
3907 //difficulty level, and we don't want to reset these anyways, due to the fact that many of the reinforcements
3908 //come from these sectors, and it could potentially add upwards of 150 extra troops which would seriously
3909 //unbalance the difficulty.
3910 continue;
3911 }
3912 pSector = &SectorInfo[ gGarrisonGroup[ i ].ubSectorID ];
3913 pArmyComp = &gArmyComp[ gGarrisonGroup[ i ].ubComposition ];
3914 if( !(pSector->uiFlags & SF_ALREADY_VISITED) )
3915 {
3916 pSector->ubNumAdmins = 0;
3917 pSector->ubNumTroops = 0;
3918 pSector->ubNumElites = 0;
3919 if( gfQueenAIAwake )
3920 {
3921 cnt = pArmyComp->bDesiredPopulation;
3922 }
3923 else
3924 {
3925 cnt = pArmyComp->bStartPopulation;
3926 }
3927
3928 if( gGarrisonGroup[ i ].ubPendingGroupID )
3929 { //if the garrison has reinforcements on route, then subtract the number of
3930 //reinforcements from the value we reset the size of the garrison. This is to
3931 //prevent overfilling the group.
3932 pGroup = GetGroup( gGarrisonGroup[ i ].ubPendingGroupID );
3933 if( pGroup )
3934 {
3935 cnt -= pGroup->ubGroupSize;
3936 cnt = MAX( cnt, 0 );
3937 }
3938 }
3939
3940 iEliteChance = pArmyComp->bElitePercentage;
3941 iAdminChance = pArmyComp->bAdminPercentage;
3942 if( iAdminChance && !gfQueenAIAwake && cnt )
3943 {
3944 pSector->ubNumAdmins = iAdminChance * cnt / 100;
3945 }
3946 else while( cnt-- )
3947 { //for each person, randomly determine the types of each soldier.
3948 if( Chance( iEliteChance ) )
3949 {
3950 pSector->ubNumElites++;
3951 }
3952 else
3953 {
3954 pSector->ubNumTroops++;
3955 }
3956 }
3957 }
3958 }
3959 }
3960
3961
3962 #ifdef WITH_UNITTESTS
3963 #include "gtest/gtest.h"
3964
TEST(StrategicAI,asserts)3965 TEST(StrategicAI, asserts)
3966 {
3967 EXPECT_EQ(sizeof(ARMY_COMPOSITION), 20u);
3968 EXPECT_EQ(sizeof(PATROL_GROUP), 20u);
3969 EXPECT_EQ(sizeof(GARRISON_GROUP), 14u);
3970 }
3971
3972 #endif
3973