1 #include "Strategic_Movement.h"
2 #include "Animation_Data.h"
3 #include "Auto_Resolve.h"
4 #include "BloodCatSpawnsModel.h"
5 #include "Campaign.h"
6 #include "ContentManager.h"
7 #include "Dialogue_Control.h"
8 #include "Faces.h"
9 #include "FileMan.h"
10 #include "Font_Control.h"
11 #include "Game_Clock.h"
12 #include "Game_Events.h"
13 #include "GameInstance.h"
14 #include "GameSettings.h"
15 #include "Inventory_Choosing.h"
16 #include "Items.h"
17 #include "JAScreens.h"
18 #include "LoadSaveData.h"
19 #include "Map_Information.h"
20 #include "Map_Screen_Helicopter.h"
21 #include "Map_Screen_Interface.h"
22 #include "Map_Screen_Interface_Border.h"
23 #include "Map_Screen_Interface_Bottom.h"
24 #include "MapScreen.h"
25 #include "Meanwhile.h"
26 #include "MercProfile.h"
27 #include "Message.h"
28 #include "Music_Control.h"
29 #include "Overhead.h"
30 #include "Player_Command.h"
31 #include "PreBattle_Interface.h"
32 #include "Queen_Command.h"
33 #include "Quests.h"
34 #include "Soldier_Macros.h"
35 #include "Squads.h"
36 #include "Strategic.h"
37 #include "StrategicMap_Secrets.h"
38 #include "Strategic_AI.h"
39 #include "Strategic_Pathing.h"
40 #include "Tactical_Save.h"
41 #include "Text.h"
42 #include "Town_Militia.h"
43 #include "Video.h"
44 #include <algorithm>
45 #include <iterator>
46 #include <stdexcept>
47 #include <string_theory/format>
48 #include <string_theory/string>
49
50
51 // the delay for a group about to arrive
52 #define ABOUT_TO_ARRIVE_DELAY 5
53
54 GROUP *gpGroupList;
55
56 static GROUP* gpPendingSimultaneousGroup = NULL;
57
58 extern BOOLEAN gubNumAwareBattles;
59 extern INT8 SquadMovementGroups[ ];
60 extern INT8 gubVehicleMovementGroups[ ];
61
62 BOOLEAN gfDelayAutoResolveStart = FALSE;
63
64
65 static BOOLEAN gfRandomizingPatrolGroup = FALSE;
66
67 UINT8 gubNumGroupsArrivedSimultaneously = 0;
68
69 //Doesn't require text localization. This is for debug strings only.
70 static const char* const gszTerrain[NUM_TRAVTERRAIN_TYPES] =
71 {
72 "TOWN",
73 "ROAD",
74 "PLAINS",
75 "SAND",
76 "SPARSE",
77 "DENSE",
78 "SWAMP",
79 "WATER",
80 "HILLS",
81 "GROUNDBARRIER",
82 "NS_RIVER",
83 "EW_RIVER",
84 "EDGEOFWORLD"
85 };
86
87 BOOLEAN gfUndergroundTacticalTraversal = FALSE;
88
89 // remembers which player group is the Continue/Stop prompt about? No need to save as long as you can't save while prompt ON
90 static GROUP* gpGroupPrompting = NULL;
91
92 static UINT32 uniqueIDMask[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
93
94
95 static GROUP* gpInitPrebattleGroup = NULL;
96
97 // waiting for input from user
98 static BOOLEAN gfWaitingForInput = FALSE;
99
100
101 static UINT8 AddGroupToList(GROUP* pGroup);
102
103
104 //Player grouping functions
105 //.........................
106 //Creates a new player group, returning the unique ID of that group. This is the first
107 //step before adding waypoints and members to the player group.
CreateNewPlayerGroupDepartingFromSector(UINT8 const ubSectorX,UINT8 const ubSectorY)108 GROUP* CreateNewPlayerGroupDepartingFromSector(UINT8 const ubSectorX, UINT8 const ubSectorY)
109 {
110 AssertMsg( ubSectorX >= 1 && ubSectorX <= 16, String( "CreateNewPlayerGroup with out of range sectorX value of %d", ubSectorX ) );
111 AssertMsg( ubSectorY >= 1 && ubSectorY <= 16, String( "CreateNewPlayerGroup with out of range sectorY value of %d", ubSectorY ) );
112 GROUP* const pNew = new GROUP{};
113 pNew->pPlayerList = NULL;
114 pNew->pWaypoints = NULL;
115 pNew->ubSectorX = pNew->ubNextX = ubSectorX;
116 pNew->ubSectorY = pNew->ubNextY = ubSectorY;
117 pNew->ubOriginalSector = (UINT8)SECTOR( ubSectorX, ubSectorY );
118 pNew->fPlayer = TRUE;
119 pNew->ubMoveType = ONE_WAY;
120 pNew->ubNextWaypointID = 0;
121 pNew->ubTransportationMask = FOOT;
122 pNew->fVehicle = FALSE;
123 pNew->ubCreatedSectorID = pNew->ubOriginalSector;
124 pNew->ubSectorIDOfLastReassignment = 255;
125
126 AddGroupToList(pNew);
127 return pNew;
128 }
129
130
CreateNewVehicleGroupDepartingFromSector(UINT8 const ubSectorX,UINT8 const ubSectorY)131 GROUP* CreateNewVehicleGroupDepartingFromSector(UINT8 const ubSectorX, UINT8 const ubSectorY)
132 {
133 AssertMsg( ubSectorX >= 1 && ubSectorX <= 16, String( "CreateNewVehicleGroup with out of range sectorX value of %d", ubSectorX ) );
134 AssertMsg( ubSectorY >= 1 && ubSectorY <= 16, String( "CreateNewVehicleGroup with out of range sectorY value of %d", ubSectorY ) );
135 GROUP* const pNew = new GROUP{};
136 pNew->pWaypoints = NULL;
137 pNew->ubSectorX = pNew->ubNextX = ubSectorX;
138 pNew->ubSectorY = pNew->ubNextY = ubSectorY;
139 pNew->ubOriginalSector = (UINT8)SECTOR( ubSectorX, ubSectorY );
140 pNew->ubMoveType = ONE_WAY;
141 pNew->ubNextWaypointID = 0;
142 pNew->fVehicle = TRUE;
143 pNew->fPlayer = TRUE;
144 pNew->pPlayerList = NULL;
145 pNew->ubCreatedSectorID = pNew->ubOriginalSector;
146 pNew->ubSectorIDOfLastReassignment = 255;
147
148 // get the type
149 pNew->ubTransportationMask = CAR;
150
151 AddGroupToList(pNew);
152 return pNew;
153 }
154
155
AddPlayerToGroup(GROUP & g,SOLDIERTYPE & s)156 void AddPlayerToGroup(GROUP& g, SOLDIERTYPE& s)
157 {
158 AssertMsg(g.fPlayer, "Attempting AddPlayerToGroup() on an ENEMY group!");
159
160 PLAYERGROUP* const p = new PLAYERGROUP{};
161 p->pSoldier = &s;
162 p->next = 0;
163
164 s.ubGroupID = g.ubGroupID;
165
166 PLAYERGROUP* i = g.pPlayerList;
167 if (!i)
168 {
169 g.pPlayerList = p;
170 g.ubGroupSize = 1;
171 g.ubPrevX = s.ubPrevSectorID % 16 + 1;
172 g.ubPrevY = s.ubPrevSectorID / 16 + 1;
173 g.ubSectorX = s.sSectorX;
174 g.ubSectorY = s.sSectorY;
175 g.ubSectorZ = s.bSectorZ;
176 }
177 else
178 {
179 for (; i->next; i = i->next)
180 {
181 AssertMsg(i->pSoldier->ubProfile != s.ubProfile, String("Attempting to add an already existing merc to group (ubProfile=%d).", s.ubProfile));
182 }
183 i->next = p;
184
185 ++g.ubGroupSize;
186 }
187 }
188
189
190 static void CancelEmptyPersistentGroupMovement(GROUP&);
191
192
RemovePlayerFromPGroup(GROUP & g,SOLDIERTYPE & s)193 void RemovePlayerFromPGroup(GROUP& g, SOLDIERTYPE& s)
194 {
195 AssertMsg(g.fPlayer, "Attempting RemovePlayerFromGroup() on an ENEMY group!");
196
197 for (PLAYERGROUP** i = &g.pPlayerList; *i; i = &(*i)->next)
198 {
199 PLAYERGROUP* const p = *i;
200 if (p->pSoldier != &s) continue;
201
202 *i = p->next;
203 delete p;
204
205 s.ubPrevSectorID = SECTOR(g.ubPrevX, g.ubPrevY);
206 s.ubGroupID = 0;
207
208 if (--g.ubGroupSize == 0)
209 {
210 if (!g.fPersistant)
211 {
212 RemoveGroup(g);
213 }
214 else
215 {
216 CancelEmptyPersistentGroupMovement(g);
217 }
218 }
219 break;
220 }
221 }
222
223
RemovePlayerFromGroup(SOLDIERTYPE & s)224 void RemovePlayerFromGroup(SOLDIERTYPE& s)
225 {
226 GROUP* const pGroup = GetGroup(s.ubGroupID);
227
228 //KM : August 6, 1999 Patch fix
229 // Because the release build has no assertions, it was still possible for the group to be null,
230 // causing a crash. Instead of crashing, it'll simply return false.
231 if (!pGroup) return;
232 //end
233
234 AssertMsg(pGroup, String("Attempting to RemovePlayerFromGroup( %d, %d ) from non-existant group", s.ubGroupID, s.ubProfile));
235
236 RemovePlayerFromPGroup(*pGroup, s);
237 }
238
239
240 static void SetLocationOfAllPlayerSoldiersInGroup(GROUP const&, INT16 x, INT16 y, INT8 z);
241
242
GroupReversingDirectionsBetweenSectors(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY,BOOLEAN fBuildingWaypoints)243 BOOLEAN GroupReversingDirectionsBetweenSectors( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY, BOOLEAN fBuildingWaypoints )
244 {
245 // if we're not between sectors, or we are but we're continuing in the same direction as before
246 if ( !GroupBetweenSectorsAndSectorXYIsInDifferentDirection( pGroup, ubSectorX, ubSectorY ) )
247 {
248 // then there's no need to reverse directions
249 return FALSE;
250 }
251
252 //The new direction is reversed, so we have to go back to the sector we just left.
253
254 //Search for the arrival event, and kill it!
255 DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
256
257 //Adjust the information in the group to reflect the new movement.
258 pGroup->ubPrevX = pGroup->ubNextX;
259 pGroup->ubPrevY = pGroup->ubNextY;
260 pGroup->ubNextX = pGroup->ubSectorX;
261 pGroup->ubNextY = pGroup->ubSectorY;
262 pGroup->ubSectorX = pGroup->ubPrevX;
263 pGroup->ubSectorY = pGroup->ubPrevY;
264
265 if( pGroup->fPlayer )
266 {
267 // ARM: because we've changed the group's ubSectoryX and ubSectorY, we must now also go and change the sSectorX and
268 // sSectorY of all the soldiers in this group so that they stay in synch. Otherwise pathing and movement problems
269 // will result since the group is in one place while the merc is in another...
270 SetLocationOfAllPlayerSoldiersInGroup(*pGroup, pGroup->ubSectorX, pGroup->ubSectorY, 0);
271 }
272
273
274 // IMPORTANT: The traverse time doesn't change just because we reverse directions! It takes the same time no matter
275 // which direction you're going in! This becomes critical in case the player reverse directions again before moving!
276
277 // The time it takes to arrive there will be exactly the amount of time we have been moving away from it.
278 pGroup->setArrivalTime(pGroup->uiTraverseTime - pGroup->uiArrivalTime + GetWorldTotalMin() * 2);
279
280 // if they're not already there
281 if( pGroup->uiArrivalTime > GetWorldTotalMin() )
282 {
283 //Post the replacement event to move back to the previous sector!
284 AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID );
285
286 if( pGroup->fPlayer )
287 {
288 if( ( pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY ) > GetWorldTotalMin( ) )
289 {
290 // Post the about to arrive event
291 AddStrategicEvent( EVENT_GROUP_ABOUT_TO_ARRIVE, pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, pGroup->ubGroupID );
292 }
293 }
294 }
295 else
296 {
297 // IMPORTANT: this can't be called during RebuildWayPointsForGroupPath(), since it will clear the mercpath
298 // prematurely by assuming the mercs are now at their final destination when only the first waypoint is in place!!!
299 // To handle this situation, RebuildWayPointsForGroupPath() will issue it's own call after it's ready for it.
300 if ( !fBuildingWaypoints )
301 {
302 // never really left. Must set check for battle TRUE in order for HandleNonCombatGroupArrival() to run!
303 GroupArrivedAtSector(*pGroup, TRUE, TRUE);
304 }
305 }
306
307
308 return TRUE;
309 }
310
311
312
GroupBetweenSectorsAndSectorXYIsInDifferentDirection(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY)313 BOOLEAN GroupBetweenSectorsAndSectorXYIsInDifferentDirection( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY )
314 {
315 INT32 currDX, currDY, newDX, newDY;
316 UINT8 ubNumUnalignedAxes = 0;
317
318
319 if( !pGroup->fBetweenSectors )
320 return( FALSE );
321
322
323 // Determine the direction the group is currently traveling in
324 currDX = pGroup->ubNextX - pGroup->ubSectorX;
325 currDY = pGroup->ubNextY - pGroup->ubSectorY;
326
327 //Determine the direction the group would need to travel in to reach the given sector
328 newDX = ubSectorX - pGroup->ubSectorX;
329 newDY = ubSectorY - pGroup->ubSectorY;
330
331 // clip the new dx/dy values to +/- 1
332 if( newDX )
333 {
334 ubNumUnalignedAxes++;
335 newDX /= ABS( newDX );
336 }
337 if( newDY )
338 {
339 ubNumUnalignedAxes++;
340 newDY /= ABS( newDY );
341 }
342
343 // error checking
344 if( ubNumUnalignedAxes > 1 )
345 {
346 AssertMsg( FALSE, String( "Checking a diagonal move for direction change, groupID %d. AM-0", pGroup->ubGroupID ) );
347 return FALSE;
348 }
349
350 // Compare the dx/dy's. If they're exactly the same, group is travelling in the same direction as before, so we're not
351 // changing directions.
352 // Note that 90-degree orthogonal changes are considered changing direction, as well as the full 180-degree reversal.
353 // That's because the party must return to the previous sector in each of those cases, too.
354 if( currDX == newDX && currDY == newDY )
355 return( FALSE );
356
357
358 // yes, we're between sectors, and we'd be changing direction to go to the given sector
359 return( TRUE );
360 }
361
362
363 static void InitiateGroupMovementToNextSector(GROUP* pGroup);
364
365
366 /* Appends a waypoint to the end of the list. Waypoint MUST be on the same
367 * horizontal or vertical level as the last waypoint added. */
AddWaypointToPGroup(GROUP * const g,UINT8 const x,UINT8 const y)368 BOOLEAN AddWaypointToPGroup(GROUP* const g, UINT8 const x, UINT8 const y) // Same, but overloaded
369 {
370 AssertMsg(1 <= x && x <= 16, String("AddWaypointToPGroup with out of range sectorX value of %d", x));
371 AssertMsg(1 <= y && y <= 16, String("AddWaypointToPGroup with out of range sectorY value of %d", y));
372
373 if (!g) return FALSE;
374
375 /* At this point, we have the group, and a valid coordinate. Now we must
376 * determine that this waypoint will be aligned exclusively to either the x or
377 * y axis of the last waypoint in the list. */
378 UINT8 n_aligned_axes = 0;
379 bool reversing_direction = FALSE;
380 WAYPOINT* wp = g->pWaypoints;
381 if (!wp)
382 {
383 if (GroupReversingDirectionsBetweenSectors(g, x, y, TRUE))
384 {
385 if (g->fPlayer)
386 { /* Because we reversed, we must add the new current sector back at the
387 * head of everyone's mercpath */
388 AddSectorToFrontOfMercPathForAllSoldiersInGroup(g, g->ubSectorX, g->ubSectorY);
389 }
390
391 /* Very special case that requiring specific coding. Check out the
392 * comments at the above function for more information. */
393 reversing_direction = TRUE;
394 // ARM: Kris - new rulez. Must still fall through and add a waypoint anyway!!!
395 }
396 else
397 { // No waypoints, so compare against the current location.
398 if (g->ubSectorX == x) ++n_aligned_axes;
399 if (g->ubSectorY == y) ++n_aligned_axes;
400 }
401 }
402 else
403 { //we do have a waypoint list, so go to the last entry
404 while (wp->next)
405 {
406 wp = wp->next;
407 }
408 // Now, we are pointing to the last waypoint in the list
409 if (wp->x == x) ++n_aligned_axes;
410 if (wp->y == y) ++n_aligned_axes;
411 }
412
413 if (!reversing_direction)
414 {
415 if (n_aligned_axes == 0)
416 {
417 AssertMsg(FALSE, String("Invalid DIAGONAL waypoint being added for groupID %d. AM-0", g->ubGroupID));
418 return FALSE;
419 }
420
421 if (n_aligned_axes >= 2)
422 {
423 AssertMsg(FALSE, String("Invalid IDENTICAL waypoint being added for groupID %d. AM-0", g->ubGroupID));
424 return FALSE;
425 }
426
427 // Has to be different in exactly 1 axis to be a valid new waypoint
428 Assert(n_aligned_axes == 1);
429 }
430
431 WAYPOINT* const new_wp = new WAYPOINT{};
432 new_wp->x = x;
433 new_wp->y = y;
434 new_wp->next = 0;
435
436 if (wp)
437 { // Add the waypoint to the end of the list
438 wp->next = new_wp;
439 }
440 else
441 { // We are adding the first waypoint.
442 g->pWaypoints = new_wp;
443
444 /* Important: The first waypoint added actually initiates the group's
445 * movement to the next sector. */
446 /* Don't do this if we have reversed directions! In that case, the required
447 * work has already been done back there */
448 if (!reversing_direction)
449 { /* We need to calculate the next sector the group is moving to and post an
450 * event for it. */
451 InitiateGroupMovementToNextSector(g);
452 }
453 }
454
455 if (g->fPlayer)
456 { // Also, nuke any previous "tactical traversal" information.
457 CFOR_EACH_PLAYER_IN_GROUP(curr, g)
458 {
459 curr->pSoldier->ubStrategicInsertionCode = 0;
460 }
461 }
462
463 return TRUE;
464 }
465
466
467 // NOTE: This does NOT expect a strategic sector ID
AddWaypointIDToPGroup(GROUP * pGroup,UINT8 ubSectorID)468 BOOLEAN AddWaypointIDToPGroup( GROUP *pGroup, UINT8 ubSectorID )
469 {
470 UINT8 ubSectorX, ubSectorY;
471 ubSectorX = SECTORX( ubSectorID );
472 ubSectorY = SECTORY( ubSectorID );
473 return AddWaypointToPGroup( pGroup, ubSectorX, ubSectorY );
474 }
475
476
AddWaypointStrategicIDToPGroup(GROUP * pGroup,UINT32 uiSectorID)477 BOOLEAN AddWaypointStrategicIDToPGroup( GROUP *pGroup, UINT32 uiSectorID )
478 {
479 UINT8 ubSectorX, ubSectorY;
480 ubSectorX = ( UINT8 ) GET_X_FROM_STRATEGIC_INDEX( uiSectorID );
481 ubSectorY = ( UINT8 ) GET_Y_FROM_STRATEGIC_INDEX( uiSectorID );
482 return AddWaypointToPGroup( pGroup, ubSectorX, ubSectorY );
483 }
484
485
486 //Enemy grouping functions -- private use by the strategic AI.
487 //............................................................
CreateNewEnemyGroupDepartingFromSector(UINT32 uiSector,UINT8 ubNumAdmins,UINT8 ubNumTroops,UINT8 ubNumElites)488 GROUP* CreateNewEnemyGroupDepartingFromSector( UINT32 uiSector, UINT8 ubNumAdmins, UINT8 ubNumTroops, UINT8 ubNumElites )
489 {
490 AssertMsg( uiSector <= 255, String( "CreateNewEnemyGroup with out of range value of %d", uiSector ) );
491 GROUP* const pNew = new GROUP{};
492 pNew->pEnemyGroup = new ENEMYGROUP{};
493 pNew->pWaypoints = NULL;
494 pNew->ubSectorX = (UINT8)SECTORX( uiSector );
495 pNew->ubSectorY = (UINT8)SECTORY( uiSector );
496 pNew->ubOriginalSector = (UINT8)uiSector;
497 pNew->fPlayer = FALSE;
498 pNew->ubMoveType = CIRCULAR;
499 pNew->ubNextWaypointID = 0;
500 pNew->pEnemyGroup->ubNumAdmins = ubNumAdmins;
501 pNew->pEnemyGroup->ubNumTroops = ubNumTroops;
502 pNew->pEnemyGroup->ubNumElites = ubNumElites;
503 pNew->ubGroupSize = (UINT8)(ubNumTroops + ubNumElites);
504 pNew->ubTransportationMask = FOOT;
505 pNew->fVehicle = FALSE;
506 pNew->ubCreatedSectorID = pNew->ubOriginalSector;
507 pNew->ubSectorIDOfLastReassignment = 255;
508
509 AddGroupToList(pNew);
510 return pNew;
511 }
512
513 //INTERNAL LIST MANIPULATION FUNCTIONS
514
515 //When adding any new group to the list, this is what must be done:
516 //1) Find the first unused ID (unique)
517 //2) Assign that ID to the new group
518 //3) Insert the group at the end of the list.
AddGroupToList(GROUP * const g)519 static UINT8 AddGroupToList(GROUP* const g)
520 {
521 // First, find a unique ID
522 for (UINT8 id = 0; ++id;)
523 {
524 const UINT32 index = id / 32;
525 const UINT32 bit = id % 32;
526 const UINT32 mask = 1 << bit;
527 if (uniqueIDMask[index] & mask) continue;
528
529 // Found a free id
530 g->ubGroupID = id;
531 uniqueIDMask[index] |= mask;
532
533 // Append group to list
534 GROUP** i = &gpGroupList;
535 while (*i != NULL) i = &(*i)->next;
536 *i = g;
537
538 return id;
539 }
540 throw std::runtime_error("Out of group IDs");
541 }
542
543
544 /* Destroys the waypoint list, detaches group from list, then deallocated the
545 * memory for the group */
RemoveGroupFromList(GROUP * const g)546 static void RemoveGroupFromList(GROUP* const g)
547 {
548 for (GROUP** i = &gpGroupList; *i != NULL; i = &(*i)->next)
549 {
550 if (*i != g) continue;
551
552 // Found the group, so now remove it.
553 *i = g->next;
554
555 // Clear the unique group ID
556 const UINT32 index = g->ubGroupID / 32;
557 const UINT32 bit = g->ubGroupID % 32;
558 const UINT32 mask = 1 << bit;
559 Assert(uniqueIDMask[index] & mask);
560 uniqueIDMask[index] &= ~mask;
561
562 delete g;
563 return;
564 }
565 SLOGA("Trying to remove a strategic group that isn't in the list!");
566 }
567
568
GetGroup(UINT8 ubGroupID)569 GROUP* GetGroup( UINT8 ubGroupID )
570 {
571 FOR_EACH_GROUP(curr)
572 {
573 if( curr->ubGroupID == ubGroupID )
574 return curr;
575 }
576 return NULL;
577 }
578
579
580 class CharacterDialogueEventBeginPrebattleInterface : public CharacterDialogueEvent
581 {
582 public:
CharacterDialogueEventBeginPrebattleInterface(SOLDIERTYPE & soldier,GROUP * const initiating_battle_group)583 CharacterDialogueEventBeginPrebattleInterface(SOLDIERTYPE& soldier, GROUP* const initiating_battle_group) :
584 CharacterDialogueEvent(soldier),
585 initiating_battle_group_(initiating_battle_group)
586 {}
587
Execute()588 bool Execute()
589 {
590 if (!MayExecute()) return true;
591
592 SOLDIERTYPE const& s = soldier_;
593 ExecuteCharacterDialogue(s.ubProfile, QUOTE_ENEMY_PRESENCE, s.face, DIALOGUE_TACTICAL_UI, TRUE, false);
594
595 // Setup face with data!
596 FACETYPE& f = *gpCurrentTalkingFace;
597 f.uiFlags |= FACE_TRIGGER_PREBATTLE_INT;
598 f.u.initiating_battle.group = initiating_battle_group_;
599
600 return false;
601 }
602
603 private:
604 GROUP* const initiating_battle_group_;
605 };
606
607
HandleImportantPBIQuote(SOLDIERTYPE & s,GROUP * const initiating_battle_group)608 static void HandleImportantPBIQuote(SOLDIERTYPE& s, GROUP* const initiating_battle_group)
609 {
610 // Wake merc up for THIS quote
611 bool const asleep = s.fMercAsleep;
612 if (asleep) MakeCharacterDialogueEventSleep(s, false);
613 DialogueEvent::Add(new CharacterDialogueEventBeginPrebattleInterface(s, initiating_battle_group));
614 if (asleep) MakeCharacterDialogueEventSleep(s, true);
615 }
616
617
618 //If this is called, we are setting the game up to bring up the prebattle interface. Before doing so,
619 //one of the involved mercs will pipe up. When he is finished, we automatically go into the mapscreen,
620 //regardless of the mode we are in.
PrepareForPreBattleInterface(GROUP * pPlayerDialogGroup,GROUP * pInitiatingBattleGroup)621 static void PrepareForPreBattleInterface(GROUP* pPlayerDialogGroup, GROUP* pInitiatingBattleGroup)
622 {
623 // ATE; Changed alogrithm here...
624 // We first loop through the group and save ubID's ov valid guys to talk....
625 // ( Can't if sleeping, unconscious, and EPC, etc....
626 UINT8 ubNumMercs = 0;
627
628 if( fDisableMapInterfaceDueToBattle )
629 {
630 SLOGA("fDisableMapInterfaceDueToBattle is set before attempting to bring up PBI.\
631 Please send PRIOR save if possible and details on anything that just happened before this battle." );
632 return;
633 }
634
635 // Pipe up with quote...
636 AssertMsg( pPlayerDialogGroup, "Didn't get a player dialog group for prebattle interface." );
637
638 AssertMsg(pPlayerDialogGroup->pPlayerList, String( "Player group %d doesn't have *any* players in it! (Finding dialog group)", pPlayerDialogGroup->ubGroupID));
639
640 SOLDIERTYPE* mercs_in_group[20];
641 CFOR_EACH_PLAYER_IN_GROUP(pPlayer, pPlayerDialogGroup)
642 {
643 SOLDIERTYPE* const pSoldier = pPlayer->pSoldier;
644
645 if (pSoldier->bLife >= OKLIFE && !IsMechanical(*pSoldier) && !AM_AN_EPC(pSoldier))
646 {
647 mercs_in_group[ubNumMercs++] = pSoldier;
648 }
649 }
650
651 //Set music
652 SetMusicMode( MUSIC_TACTICAL_ENEMYPRESENT );
653
654 if( (gfTacticalTraversal && pInitiatingBattleGroup == gpTacticalTraversalGroup) ||
655 (pInitiatingBattleGroup && !pInitiatingBattleGroup->fPlayer &&
656 pInitiatingBattleGroup->ubSectorX == gWorldSectorX &&
657 pInitiatingBattleGroup->ubSectorY == gWorldSectorY && !gbWorldSectorZ) )
658 { // At least say quote....
659 if ( ubNumMercs > 0 )
660 {
661 if( pPlayerDialogGroup->uiFlags & GROUPFLAG_JUST_RETREATED_FROM_BATTLE )
662 {
663 gfCantRetreatInPBI = TRUE;
664 }
665
666 SOLDIERTYPE* const chosen = mercs_in_group[Random(ubNumMercs)];
667 gpTacticalTraversalChosenSoldier = chosen;
668
669 if( !gfTacticalTraversal )
670 {
671 HandleImportantPBIQuote(*chosen, pInitiatingBattleGroup);
672 }
673
674 InterruptTime();
675 PauseGame();
676 LockPauseState(LOCK_PAUSE_PREBATTLE_CURRENT_SQUAD);
677
678 if( !gfTacticalTraversal )
679 fDisableMapInterfaceDueToBattle = TRUE;
680 }
681 return;
682 }
683
684
685 // Randomly pick a valid merc from the list we have created!
686 if ( ubNumMercs > 0 )
687 {
688 if( pPlayerDialogGroup->uiFlags & GROUPFLAG_JUST_RETREATED_FROM_BATTLE )
689 {
690 gfCantRetreatInPBI = TRUE;
691 }
692
693 SOLDIERTYPE* const chosen = mercs_in_group[Random(ubNumMercs)];
694 HandleImportantPBIQuote(*chosen, pInitiatingBattleGroup);
695 InterruptTime();
696 PauseGame();
697 LockPauseState(LOCK_PAUSE_PREBATTLE);
698
699 // disable exit from mapscreen and what not until face done talking
700 fDisableMapInterfaceDueToBattle = TRUE;
701 }
702 else
703 {
704 // ATE: What if we have unconscious guys, etc....
705 // We MUST start combat, but donot play quote...
706 InitPreBattleInterface(pInitiatingBattleGroup, true);
707 }
708 }
709
710
711 static void HandleOtherGroupsArrivingSimultaneously(UINT8 x, UINT8 y, UINT8 z);
712 static void NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX, UINT8 ubSectorY);
713 static BOOLEAN TestForBloodcatAmbush(GROUP const*);
714 static void TriggerPrebattleInterface(MessageBoxReturnValue);
715 static BOOLEAN PossibleToCoordinateSimultaneousGroupArrivals(GROUP* pFirstGroup);
716
717
CheckConditionsForBattle(GROUP * pGroup)718 static BOOLEAN CheckConditionsForBattle(GROUP* pGroup)
719 {
720 GROUP *pPlayerDialogGroup = NULL;
721 SOLDIERTYPE *pSoldier;
722 BOOLEAN fBattlePending = FALSE;
723 BOOLEAN fAliveMerc = FALSE;
724 BOOLEAN fMilitiaPresent = FALSE;
725 BOOLEAN fCombatAbleMerc = FALSE;
726 BOOLEAN fBloodCatAmbush = FALSE;
727
728 if( gfWorldLoaded )
729 { //look for people arriving in the currently loaded sector. This handles reinforcements.
730 const GROUP* const curr = FindPlayerMovementGroupInSector(gWorldSectorX, gWorldSectorY);
731 if( !gbWorldSectorZ && PlayerMercsInSector( (UINT8)gWorldSectorX, (UINT8)gWorldSectorY, gbWorldSectorZ ) &&
732 pGroup->ubSectorX == gWorldSectorX && pGroup->ubSectorY == gWorldSectorY &&
733 curr )
734 { //Reinforcements have arrived!
735 if( gTacticalStatus.fEnemyInSector )
736 {
737 HandleArrivalOfReinforcements( pGroup );
738 return( TRUE );
739 }
740 }
741 }
742
743 if( !DidGameJustStart() )
744 {
745 gubEnemyEncounterCode = NO_ENCOUNTER_CODE;
746 }
747
748 HandleOtherGroupsArrivingSimultaneously( pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ );
749
750 FOR_EACH_PLAYER_GROUP(i)
751 {
752 GROUP& g = *i;
753 if (g.ubGroupSize)
754 {
755 if (!g.fBetweenSectors)
756 {
757 if (g.ubSectorX == pGroup->ubSectorX && g.ubSectorY == pGroup->ubSectorY && !g.ubSectorZ)
758 {
759 if (!GroupHasInTransitDeadOrPOWMercs(g) &&
760 (!IsGroupTheHelicopterGroup(g) || !fHelicopterIsAirBorne) &&
761 (!g.fVehicle || DoesVehicleGroupHaveAnyPassengers(g)))
762 {
763 //Now, a player group is in this sector. Determine if the group contains any mercs that can fight.
764 //Vehicles, EPCs and the robot doesn't count. Mercs below OKLIFE do.
765 CFOR_EACH_PLAYER_IN_GROUP(pPlayer, &g)
766 {
767 pSoldier = pPlayer->pSoldier;
768 if( !(pSoldier->uiStatusFlags & SOLDIER_VEHICLE) )
769 {
770 if( !AM_A_ROBOT( pSoldier ) &&
771 !AM_AN_EPC( pSoldier ) &&
772 pSoldier->bLife >= OKLIFE )
773 {
774 fCombatAbleMerc = TRUE;
775 }
776 if( pSoldier->bLife > 0 )
777 {
778 fAliveMerc = TRUE;
779 }
780 }
781 }
782 if( !pPlayerDialogGroup && fCombatAbleMerc )
783 {
784 pPlayerDialogGroup = &g;
785 }
786 if( fCombatAbleMerc )
787 {
788 break;
789 }
790 }
791 }
792 }
793 }
794 }
795
796 if( pGroup->fPlayer )
797 {
798 pPlayerDialogGroup = pGroup;
799
800 if( NumEnemiesInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
801 {
802 fBattlePending = TRUE;
803 }
804
805 if( pGroup->uiFlags & GROUPFLAG_HIGH_POTENTIAL_FOR_AMBUSH && fBattlePending )
806 { //This group has just arrived in a new sector from an adjacent sector that he retreated from
807 //If this battle is an encounter type battle, then there is a 90% chance that the battle will
808 //become an ambush scenario.
809 gfHighPotentialForAmbush = TRUE;
810 }
811
812 //If there are bloodcats in this sector, then it internally checks and handles it
813 if( TestForBloodcatAmbush( pGroup ) )
814 {
815 fBloodCatAmbush = TRUE;
816 fBattlePending = TRUE;
817 }
818
819 if( fBattlePending && (!fBloodCatAmbush || gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE) )
820 {
821 if( PossibleToCoordinateSimultaneousGroupArrivals( pGroup ) )
822 {
823 return FALSE;
824 }
825 }
826 }
827 else
828 {
829 if( CountAllMilitiaInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
830 {
831 fMilitiaPresent = TRUE;
832 fBattlePending = TRUE;
833 }
834 if( fAliveMerc )
835 {
836 fBattlePending = TRUE;
837 }
838 }
839
840 if( !fAliveMerc && !fMilitiaPresent )
841 { //empty vehicle, everyone dead, don't care. Enemies don't care.
842 return FALSE;
843 }
844
845 if( fBattlePending )
846 { //A battle is pending, but the player's could be all unconcious or dead.
847 //Go through every group until we find at least one concious merc. The looping will determine
848 //if there are any live mercs and/or concious ones. If there are no concious mercs, but alive ones,
849 //then we will go straight to autoresolve, where the enemy will likely annihilate them or capture them.
850 //If there are no alive mercs, then there is nothing anybody can do. The enemy will completely ignore
851 //this, and continue on.
852
853 if( gubNumGroupsArrivedSimultaneously )
854 { //Because this is a battle case, clear all the group flags
855 FOR_EACH_GROUP(curr)
856 {
857 if (gubNumGroupsArrivedSimultaneously == 0) break;
858 if( curr->uiFlags & GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY )
859 {
860 curr->uiFlags &= ~GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY;
861 gubNumGroupsArrivedSimultaneously--;
862 }
863 }
864 }
865
866 gpInitPrebattleGroup = pGroup;
867
868 if( gubEnemyEncounterCode == BLOODCAT_AMBUSH_CODE || gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE )
869 {
870 NotifyPlayerOfBloodcatBattle( pGroup->ubSectorX, pGroup->ubSectorY );
871 return TRUE;
872 }
873
874 if( !fCombatAbleMerc )
875 { //Prepare for instant autoresolve.
876 gfDelayAutoResolveStart = TRUE;
877 gfUsePersistantPBI = TRUE;
878 if( fMilitiaPresent )
879 {
880 NotifyPlayerOfInvasionByEnemyForces( pGroup->ubSectorX, pGroup->ubSectorY, 0, TriggerPrebattleInterface );
881 }
882 else
883 {
884 ST::string pSectorStr = GetSectorIDString(pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ, TRUE);
885 ST::string str = st_format_printf(gpStrategicString[ STR_DIALOG_ENEMIES_ATTACK_UNCONCIOUSMERCS ], pSectorStr);
886 DoScreenIndependantMessageBox( str, MSG_BOX_FLAG_OK, TriggerPrebattleInterface );
887 }
888 }
889
890 if( pPlayerDialogGroup )
891 {
892 PrepareForPreBattleInterface( pPlayerDialogGroup, pGroup );
893 }
894 return TRUE;
895 }
896 return FALSE;
897 }
898
899
900 class DialogueEventTriggerPrebattleInterface : public DialogueEvent
901 {
902 public:
DialogueEventTriggerPrebattleInterface(GROUP * const init_prebattle_group)903 DialogueEventTriggerPrebattleInterface(GROUP* const init_prebattle_group) :
904 init_prebattle_group_(init_prebattle_group)
905 {}
906
Execute()907 bool Execute()
908 {
909 UnLockPauseState();
910 InitPreBattleInterface(init_prebattle_group_, true);
911 return false;
912 }
913
914 private:
915 GROUP* const init_prebattle_group_;
916 };
917
918
TriggerPrebattleInterface(MessageBoxReturnValue const ubResult)919 static void TriggerPrebattleInterface(MessageBoxReturnValue const ubResult)
920 {
921 StopTimeCompression();
922 DialogueEvent::Add(new DialogueEventTriggerPrebattleInterface(gpInitPrebattleGroup));
923 gpInitPrebattleGroup = NULL;
924 }
925
926
927 //This will get called after a battle is auto-resolved or automatically after arriving
928 //at the next sector during a move and the area is clear.
CalculateNextMoveIntention(GROUP * pGroup)929 void CalculateNextMoveIntention( GROUP *pGroup )
930 {
931 INT32 i;
932 WAYPOINT *wp;
933
934 Assert( pGroup );
935
936 //TEMP: Ignore resting...
937
938 //Should be surely an enemy group that has just made a new decision to go elsewhere!
939 if( pGroup->fBetweenSectors )
940 {
941 return;
942 }
943
944 if( !pGroup->pWaypoints )
945 {
946 return;
947 }
948
949 //Determine if we are at a waypoint.
950 i = pGroup->ubNextWaypointID;
951 wp = pGroup->pWaypoints;
952 while( i-- )
953 { //Traverse through the waypoint list to the next waypoint ID
954 Assert( wp );
955 wp = wp->next;
956 }
957 Assert( wp );
958
959 //We have the next waypoint, now check if we are actually there.
960 if( pGroup->ubSectorX == wp->x && pGroup->ubSectorY == wp->y )
961 { //We have reached the next waypoint, so now determine what the next waypoint is.
962 switch( pGroup->ubMoveType )
963 {
964 case ONE_WAY:
965 if( !wp->next )
966 { //No more waypoints, so we've reached the destination.
967 return;
968 }
969 //Advance destination to next waypoint ID
970 pGroup->ubNextWaypointID++;
971 break;
972 case CIRCULAR:
973 wp = wp->next;
974 if( !wp )
975 { //reached the end of the patrol route. Set to the first waypoint in list, indefinately.
976 //NOTE: If the last waypoint isn't exclusively aligned to the x or y axis of the first
977 // waypoint, there will be an assertion failure inside the waypoint movement code.
978 pGroup->ubNextWaypointID = 0;
979 }
980 else
981 pGroup->ubNextWaypointID++;
982 break;
983 case ENDTOEND_FORWARDS:
984 wp = wp->next;
985 if( !wp )
986 {
987 AssertMsg( pGroup->ubNextWaypointID, "EndToEnd patrol group needs more than one waypoint!" );
988 pGroup->ubNextWaypointID--;
989 pGroup->ubMoveType = ENDTOEND_BACKWARDS;
990 }
991 else
992 pGroup->ubNextWaypointID++;
993 break;
994 case ENDTOEND_BACKWARDS:
995 if( !pGroup->ubNextWaypointID )
996 {
997 pGroup->ubNextWaypointID++;
998 pGroup->ubMoveType = ENDTOEND_FORWARDS;
999 }
1000 else
1001 pGroup->ubNextWaypointID--;
1002 break;
1003 }
1004 }
1005 InitiateGroupMovementToNextSector( pGroup );
1006 }
1007
1008
1009 /* Based on how long movement took, mercs gain a bit of life experience for
1010 * travelling */
AwardExperienceForTravelling(GROUP & g)1011 static void AwardExperienceForTravelling(GROUP& g)
1012 {
1013 UINT32 const traverse_time = g.uiTraverseTime;
1014 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1015 {
1016 if (!i->pSoldier) continue;
1017 SOLDIERTYPE& s = *i->pSoldier;
1018 if (IsMechanical(s)) continue;
1019 if (AM_AN_EPC(&s)) continue;
1020
1021 if (s.bLifeMax < 100)
1022 {
1023 /* Amount was originally based on getting 100 - bLifeMax points for 12
1024 * hours of travel (720), but changed to flat rate since StatChange makes
1025 * roll vs 100-lifemax as well */
1026 UINT32 const points = traverse_time / (450 / (100 - s.bLifeMax));
1027 if (points > 0) StatChange(s, HEALTHAMT, (UINT8)points, FROM_SUCCESS);
1028 }
1029
1030 if (s.bStrength < 100)
1031 {
1032 UINT32 const carried_percent = CalculateCarriedWeight(&s);
1033 if (carried_percent > 50)
1034 {
1035 UINT32 const points = traverse_time / (450 / (100 - s.bStrength));
1036 StatChange(s, STRAMT, points * (carried_percent - 50) / 100, FROM_SUCCESS);
1037 }
1038 }
1039 }
1040 }
1041
1042
AddCorpsesToBloodcatLair(INT16 sSectorX,INT16 sSectorY)1043 static void AddCorpsesToBloodcatLair(INT16 sSectorX, INT16 sSectorY)
1044 {
1045 ROTTING_CORPSE_DEFINITION Corpse;
1046 Corpse = ROTTING_CORPSE_DEFINITION{};
1047
1048 // Setup some values!
1049 Corpse.ubBodyType = REGMALE;
1050 Corpse.sHeightAdjustment = 0;
1051 Corpse.bVisible = TRUE;
1052
1053 Corpse.HeadPal = "BROWNHEAD";
1054 Corpse.VestPal = "YELLOWVEST";
1055 Corpse.SkinPal = "PINKSKIN";
1056 Corpse.PantsPal = "GREENPANTS";
1057
1058
1059 Corpse.bDirection = (INT8)Random(8);
1060
1061 // Set time of death
1062 // Make sure they will be rotting!
1063 Corpse.uiTimeOfDeath = GetWorldTotalMin( ) - ( 2 * NUM_SEC_IN_DAY / 60 );
1064 // Set type
1065 Corpse.ubType = (UINT8)SMERC_JFK;
1066 Corpse.usFlags = ROTTING_CORPSE_FIND_SWEETSPOT_FROM_GRIDNO;
1067
1068 // 1st gridno
1069 Corpse.sGridNo = 14319;
1070 //Add the rotting corpse info to the sectors unloaded rotting corpse file
1071 AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1072
1073 // 2nd gridno
1074 Corpse.sGridNo = 9835;
1075 //Add the rotting corpse info to the sectors unloaded rotting corpse file
1076 AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1077
1078
1079 // 3rd gridno
1080 Corpse.sGridNo = 11262;
1081 //Add the rotting corpse info to the sectors unloaded rotting corpse file
1082 AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1083 }
1084
1085
1086 static void HandleNonCombatGroupArrival(GROUP&, bool main_group, bool never_left);
1087 static void ReportVehicleOutOfGas(VEHICLETYPE const&, UINT8 x, UINT8 y);
1088 static void SpendVehicleFuel(SOLDIERTYPE&, INT16 fuel_spent);
1089 static INT16 VehicleFuelRemaining(SOLDIERTYPE const&);
1090
1091
GroupArrivedAtSector(GROUP & g,BOOLEAN const check_for_battle,BOOLEAN const never_left)1092 void GroupArrivedAtSector(GROUP& g, BOOLEAN const check_for_battle, BOOLEAN const never_left)
1093 {
1094 gfWaitingForInput = FALSE;
1095
1096 if (g.fPlayer)
1097 {
1098 // Set the fact we have visited the sector
1099 if (PLAYERGROUP const* curr = g.pPlayerList)
1100 {
1101 INT8 const assignment = curr->pSoldier->bAssignment;
1102 if (assignment < ON_DUTY) ResetDeadSquadMemberList(assignment);
1103 }
1104
1105 if (g.fVehicle)
1106 {
1107 VEHICLETYPE const& v = GetVehicleFromMvtGroup(g);
1108 if (!IsHelicopter(v) && !g.pPlayerList)
1109 { /* Nobody here, better just get out now. With vehicles, arriving empty
1110 * is probably ok, since passengers might have been killed but vehicle
1111 * lived. */
1112 return;
1113 }
1114 }
1115 else
1116 {
1117 if (!g.pPlayerList)
1118 { // Nobody here, better just get out now
1119 SLOGA("Player group %d arrived in sector empty.", g.ubGroupID);
1120 return;
1121 }
1122 }
1123 }
1124
1125 UINT8 const x = g.ubNextX;
1126 UINT8 const y = g.ubNextY;
1127 UINT8 const z = g.ubSectorZ;
1128
1129 // Check for exception cases which
1130 bool const exception_queue =
1131 gTacticalStatus.bBoxingState != NOT_BOXING &&
1132 !g.fPlayer &&
1133 x == 5 && y == 4 && z == 0;
1134
1135 /* First check if the group arriving is going to queue another battle.
1136 * NOTE: We can't have more than one battle ongoing at a time. */
1137 if (exception_queue ||
1138 (check_for_battle && gTacticalStatus.fEnemyInSector && FindPlayerMovementGroupInSector(gWorldSectorX, gWorldSectorY) && (x != gWorldSectorX || y != gWorldSectorY || gbWorldSectorZ > 0)) ||
1139 AreInMeanwhile() ||
1140 /* KM: Aug 11, 1999 -- Patch fix: Added additional checks to prevent a 2nd
1141 * battle in the case where the player is involved in a potential battle
1142 * with bloodcats/civilians */
1143 (check_for_battle && HostileCiviliansPresent()) ||
1144 (check_for_battle && HostileBloodcatsPresent()))
1145 { /* Queue battle! Delay arrival by a random value ranging from 3-5 minutes,
1146 * so it doesn't get the player too suspicious after it happens to him a few
1147 * times, which, by the way, is a rare occurrence. */
1148 if (AreInMeanwhile())
1149 { /* Tack on only 1 minute if we are in a meanwhile scene. This effectively
1150 * prevents any battle from occurring while inside a meanwhile scene. */
1151 ++g.uiArrivalTime;
1152 }
1153 else
1154 {
1155 g.uiArrivalTime += Random(3) + 3;
1156 }
1157
1158 if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
1159 SLOGA("Failed to add movement event.");
1160
1161 if (g.fPlayer && g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1162 {
1163 AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
1164 }
1165 return;
1166 }
1167
1168 // Update the position of the group
1169 g.ubPrevX = g.ubSectorX;
1170 g.ubPrevY = g.ubSectorY;
1171 g.ubSectorX = x;
1172 g.ubSectorY = y;
1173 g.ubNextX = 0;
1174 g.ubNextY = 0;
1175
1176 if (g.fPlayer)
1177 {
1178 // Award life 'experience' for traveling, based on travel time.
1179 if (!g.fVehicle)
1180 { // Gotta be walking to get tougher
1181 AwardExperienceForTravelling(g);
1182 }
1183 else if (!IsGroupTheHelicopterGroup(g))
1184 {
1185 VEHICLETYPE const& v = GetVehicleFromMvtGroup(g);
1186 SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1187
1188 SpendVehicleFuel(vs, g.uiTraverseTime * 6);
1189
1190 if (VehicleFuelRemaining(vs) == 0)
1191 {
1192 ReportVehicleOutOfGas(v, x, y);
1193 // Nuke the group's path, so they don't continue moving.
1194 ClearMercPathsAndWaypointsForAllInGroup(g);
1195 }
1196 }
1197 }
1198
1199 g.uiTraverseTime = 0;
1200 g.setArrivalTime(0);
1201 g.fBetweenSectors = FALSE;
1202 fMapPanelDirty = TRUE;
1203 fMapScreenBottomDirty = TRUE;
1204
1205 bool group_destroyed = false;
1206 if (g.fPlayer)
1207 {
1208 // If this is the last sector along player group's movement path (no more waypoints)
1209 if (GroupAtFinalDestination(&g))
1210 { // Clear their strategic movement (mercpaths and waypoints)
1211 ClearMercPathsAndWaypointsForAllInGroup(g);
1212 }
1213
1214 // If on surface
1215 if (z == 0)
1216 {
1217 // check for discovering secret locations
1218 if (GetMapSecretBySectorID(SECTOR(x, y)))
1219 {
1220 SetSectorSecretAsFound(SECTOR(x, y));
1221 }
1222 }
1223
1224 UINT8 insertion_direction;
1225 UINT8 strategic_insertion_code;
1226 if (x < g.ubPrevX)
1227 {
1228 insertion_direction = SOUTHWEST;
1229 strategic_insertion_code = INSERTION_CODE_EAST;
1230 }
1231 else if (x > g.ubPrevX)
1232 {
1233 insertion_direction = NORTHEAST;
1234 strategic_insertion_code = INSERTION_CODE_WEST;
1235 }
1236 else if (y < g.ubPrevY)
1237 {
1238 insertion_direction = NORTHWEST;
1239 strategic_insertion_code = INSERTION_CODE_SOUTH;
1240 }
1241 else if (y > g.ubPrevY)
1242 {
1243 insertion_direction = SOUTHEAST;
1244 strategic_insertion_code = INSERTION_CODE_NORTH;
1245 }
1246 else
1247 {
1248 SLOGA("GroupArrivedAtSector: group arrives in sector where it already has been");
1249 return;
1250 }
1251
1252 bool const here = x == gWorldSectorX && y == gWorldSectorY && z == gbWorldSectorZ;
1253 ST::string who;
1254 if (!g.fVehicle)
1255 {
1256 // non-vehicle player group
1257 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1258 {
1259 SOLDIERTYPE& s = *i->pSoldier;
1260 s.fBetweenSectors = FALSE;
1261 s.sSectorX = x;
1262 s.sSectorY = y;
1263 s.bSectorZ = z;
1264 s.ubPrevSectorID = SECTOR(g.ubPrevX, g.ubPrevY);
1265 s.ubInsertionDirection = insertion_direction;
1266
1267 // don't override if a tactical traversal
1268 if (s.ubStrategicInsertionCode != INSERTION_CODE_PRIMARY_EDGEINDEX &&
1269 s.ubStrategicInsertionCode != INSERTION_CODE_SECONDARY_EDGEINDEX)
1270 {
1271 s.ubStrategicInsertionCode = strategic_insertion_code;
1272 }
1273
1274 // Remove head from their mapscreen path list
1275 if (s.pMercPath) s.pMercPath = RemoveHeadFromStrategicPath(s.pMercPath);
1276
1277 /* ATE: Check if this sector is currently loaded, if so, add them to the
1278 * tactical engine */
1279 if (here) UpdateMercInSector(s, x, y, z);
1280 }
1281
1282 // If there's anybody in the group
1283 if (g.pPlayerList)
1284 {
1285 SOLDIERTYPE const& s = *g.pPlayerList->pSoldier;
1286 who =
1287 s.bAssignment >= ON_DUTY ? s.name : // A loner
1288 pAssignmentStrings[s.bAssignment]; // Squad
1289 }
1290 }
1291 else
1292 { // Vehicle player group
1293 VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
1294 // Remove head from vehicle's mapscreen path list
1295 if (v.pMercPath) v.pMercPath = RemoveHeadFromStrategicPath(v.pMercPath);
1296
1297 // Update vehicle position
1298 SetVehicleSectorValues(v, x, y);
1299 v.fBetweenSectors = FALSE;
1300
1301 if (!IsHelicopter(v))
1302 {
1303 SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1304 vs.fBetweenSectors = FALSE;
1305 vs.sSectorX = x;
1306 vs.sSectorY = y;
1307 vs.bSectorZ = z;
1308 vs.ubInsertionDirection = insertion_direction;
1309 vs.ubStrategicInsertionCode = strategic_insertion_code;
1310
1311 // If this sector is currently loaded, add vehicle to the tactical engine
1312 if (here) UpdateMercInSector(vs, x, y, z);
1313
1314 // Set directions of insertion
1315 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1316 {
1317 SOLDIERTYPE& s = *i->pSoldier;
1318 s.fBetweenSectors = FALSE;
1319 s.sSectorX = x;
1320 s.sSectorY = y;
1321 s.bSectorZ = z;
1322 s.ubInsertionDirection = insertion_direction;
1323 s.ubStrategicInsertionCode = strategic_insertion_code;
1324
1325 // If this sector is currently loaded, add passenger to the tactical engine
1326 if (here) UpdateMercInSector(s, x, y, z);
1327 }
1328 }
1329 else
1330 {
1331 if (HandleHeliEnteringSector(v.sSectorX, v.sSectorY))
1332 { // Helicopter destroyed
1333 group_destroyed = true;
1334 }
1335 }
1336
1337 if (!group_destroyed) who = pVehicleStrings[v.ubVehicleType];
1338 }
1339
1340 if (!who.empty())
1341 { /* Don't print any messages when arriving underground (there's no delay
1342 * involved) or if we never left (cancel) */
1343 if (GroupAtFinalDestination(&g) && z == 0 && !never_left)
1344 {
1345 ScreenMsg(FONT_MCOLOR_DKRED, MSG_INTERFACE, st_format_printf(pMessageStrings[MSG_ARRIVE], who, pMapVertIndex[y], pMapHortIndex[x]));
1346 }
1347 }
1348
1349 if (!group_destroyed)
1350 {
1351 // On foot, or in a vehicle other than the chopper
1352 if (!g.fVehicle || !IsGroupTheHelicopterGroup(g))
1353 {
1354 // ATE: Add a few corpse to the bloodcat lair
1355 auto spawns = GCM->getBloodCatSpawnsOfSector( SECTOR(x, y) );
1356 if ( spawns != NULL && spawns->isLair &&
1357 !GetSectorFlagStatus(x, y, z, SF_ALREADY_VISITED))
1358 {
1359 AddCorpsesToBloodcatLair(x, y);
1360 }
1361
1362 // Mark the sector as visited already
1363 SetSectorFlag(x, y, z, SF_ALREADY_VISITED);
1364 }
1365 }
1366
1367 // Update character info
1368 fTeamPanelDirty = TRUE;
1369 fCharacterInfoPanelDirty = TRUE;
1370 }
1371
1372 if (!group_destroyed)
1373 {
1374 /* Determine if a battle should start. If a battle does start, or get's
1375 * delayed, then we will keep the group in memory including all waypoints,
1376 * until after the battle is resolved. At that point, we will continue the
1377 * processing. */
1378 if (check_for_battle && !CheckConditionsForBattle(&g) && !gfWaitingForInput)
1379 {
1380 HandleNonCombatGroupArrival(g, true, never_left);
1381
1382 if (gubNumGroupsArrivedSimultaneously != 0)
1383 {
1384 FOR_EACH_GROUP_SAFE(i)
1385 {
1386 GROUP& g = *i;
1387 if (!(g.uiFlags & GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY)) continue;
1388 --gubNumGroupsArrivedSimultaneously;
1389 HandleNonCombatGroupArrival(g, false, false);
1390 if (gubNumGroupsArrivedSimultaneously == 0) break;
1391 }
1392 }
1393 }
1394 else
1395 { // Handle cases for pre-battle conditions
1396 g.uiFlags = 0;
1397 if (gubNumAwareBattles != 0)
1398 { /* When the AI is looking for the players, and a battle is initiated,
1399 * then decrement the value, otherwise the queen will continue searching
1400 * to infinity. */
1401 --gubNumAwareBattles;
1402 }
1403 }
1404 }
1405 gfWaitingForInput = FALSE;
1406 }
1407
1408
HandleNonCombatGroupArrival(GROUP & g,bool const main_group,bool const never_left)1409 static void HandleNonCombatGroupArrival(GROUP& g, bool const main_group, bool const never_left)
1410 {
1411 if (StrategicAILookForAdjacentGroups(&g))
1412 { /* The routine actually just deleted the enemy group (player's don't get
1413 * deleted), so we are done! */
1414 return;
1415 }
1416
1417 if (g.fPlayer)
1418 { // The group will always exist after the AI was processed.
1419
1420 bool const is_heli_group = g.fVehicle && IsGroupTheHelicopterGroup(g);
1421 if (!is_heli_group)
1422 { // Take control of sector
1423 SetThisSectorAsPlayerControlled(g.ubSectorX, g.ubSectorY, g.ubSectorZ, FALSE);
1424 }
1425
1426 // If this is the last sector along their movement path (no more waypoints)
1427 if (GroupAtFinalDestination(&g))
1428 {
1429 // If currently selected sector has nobody in it
1430 if (PlayerMercsInSector(sSelMapX, sSelMapY, iCurrentMapSectorZ) == 0)
1431 { // Make this sector strategically selected
1432 ChangeSelectedMapSector(g.ubSectorX, g.ubSectorY, g.ubSectorZ);
1433 }
1434
1435 if (!is_heli_group) // Else Skyrider speaks for heli movement
1436 {
1437 StopTimeCompression();
1438
1439 // If traversing tactically, or we never left (just canceling), don't do this
1440 if (!gfTacticalTraversal && !never_left)
1441 {
1442 RandomMercInGroupSaysQuote(g, QUOTE_MERC_REACHED_DESTINATION);
1443 }
1444 }
1445 }
1446 /* Look for NPCs to stop for, anyone is too tired to keep going, if all OK
1447 * rebuild waypoints & continue movement
1448 * NOTE: Only the main group (first group arriving) will stop for NPCs, it's
1449 * just too much hassle to stop them all */
1450 PlayerGroupArrivedSafelyInSector(g, main_group);
1451 }
1452 else
1453 {
1454 if (!g.fDebugGroup)
1455 {
1456 CalculateNextMoveIntention(&g);
1457 }
1458 else
1459 {
1460 RemoveGroup(g);
1461 }
1462 }
1463
1464 g.uiFlags = 0; // Clear the non-persistant flags
1465 }
1466
1467
1468 /* Because a battle is about to start, we need to go through the event list and
1469 * look for other groups that may arrive at the same time -- enemies or players,
1470 * and blindly add them to the sector without checking for battle conditions, as
1471 * it has already determined that a new battle is about to start. */
HandleOtherGroupsArrivingSimultaneously(UINT8 const x,UINT8 const y,UINT8 const z)1472 static void HandleOtherGroupsArrivingSimultaneously(UINT8 const x, UINT8 const y, UINT8 const z)
1473 {
1474 UINT32 const now = GetWorldTotalSeconds();
1475 gubNumGroupsArrivedSimultaneously = 0;
1476 restart:
1477 for (STRATEGICEVENT* i = gpEventList; i && i->uiTimeStamp <= now; i = i->next)
1478 {
1479 if (i->ubCallbackID != EVENT_GROUP_ARRIVAL) continue;
1480 if (i->ubFlags & SEF_DELETION_PENDING) continue;
1481
1482 GROUP& g = *GetGroup((UINT8)i->uiParam);
1483 if (g.ubNextX != x || g.ubNextY != y || g.ubSectorZ != z) continue;
1484 if (!g.fBetweenSectors) continue;
1485
1486 GroupArrivedAtSector(g, FALSE, FALSE);
1487 g.uiFlags |= GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY;
1488 ++gubNumGroupsArrivedSimultaneously;
1489 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1490 goto restart;
1491 }
1492 }
1493
1494
1495 static void DelayEnemyGroupsIfPathsCross(GROUP& player_group);
1496
1497
1498 /* The user has just approved to plan a simultaneous arrival. So we will
1499 * syncronize all of the involved groups so that they arrive at the same time
1500 * (which is the time the final group would arrive). */
PrepareGroupsForSimultaneousArrival()1501 static void PrepareGroupsForSimultaneousArrival()
1502 {
1503 GROUP& first_group = *gpPendingSimultaneousGroup;
1504
1505 /* For all of the groups that haven't arrived yet, determine which one is
1506 * going to take the longest. */
1507 UINT32 latest_arrival_time = 0;
1508 FOR_EACH_PLAYER_GROUP(i)
1509 {
1510 GROUP& g = *i;
1511 if (&g == &first_group) continue;
1512 if (!g.fBetweenSectors) continue;
1513 if (g.ubNextX != first_group.ubSectorX) continue;
1514 if (g.ubNextY != first_group.ubSectorY) continue;
1515 if (IsGroupTheHelicopterGroup(g)) continue;
1516 latest_arrival_time = MAX(g.uiArrivalTime, latest_arrival_time);
1517 g.uiFlags |= GROUPFLAG_SIMULTANEOUSARRIVAL_APPROVED | GROUPFLAG_MARKER;
1518 }
1519
1520 /* Now, go through the list again, and reset their arrival event to the latest
1521 * arrival time. */
1522 FOR_EACH_GROUP(i)
1523 {
1524 GROUP& g = *i;
1525 if (!(g.uiFlags & GROUPFLAG_MARKER)) continue;
1526
1527 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1528
1529 /* NOTE: This can cause the arrival time to be > GetWorldTotalMin() +
1530 * TraverseTime, so keep that in mind if you have any code that uses these 3
1531 * values to figure out how far along its route a group is! */
1532 g.setArrivalTime(latest_arrival_time);
1533 AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID);
1534
1535 if (g.fPlayer && g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1536 {
1537 AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
1538 }
1539
1540 DelayEnemyGroupsIfPathsCross(g);
1541 g.uiFlags &= ~GROUPFLAG_MARKER;
1542 }
1543
1544 /* We still have the first group that has arrived. Because they are set up to
1545 * be in the destination sector, we will "warp" them back to the last sector,
1546 * and also setup a new arrival time for them. */
1547 first_group.ubNextX = first_group.ubSectorX;
1548 first_group.ubNextY = first_group.ubSectorY;
1549 first_group.ubSectorX = first_group.ubPrevX;
1550 first_group.ubSectorY = first_group.ubPrevY;
1551 first_group.setArrivalTime(latest_arrival_time);
1552 first_group.fBetweenSectors = TRUE;
1553
1554 if (first_group.fVehicle)
1555 {
1556 VEHICLETYPE& v = GetVehicleFromMvtGroup(first_group);
1557 v.fBetweenSectors = TRUE;
1558
1559 if (!IsHelicopter(v))
1560 {
1561 SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1562 vs.fBetweenSectors = TRUE;
1563 }
1564 }
1565
1566 AddStrategicEvent(EVENT_GROUP_ARRIVAL, first_group.uiArrivalTime, first_group.ubGroupID);
1567
1568 if (first_group.fPlayer && first_group.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1569 {
1570 AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, first_group.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, first_group.ubGroupID);
1571 }
1572 DelayEnemyGroupsIfPathsCross(first_group);
1573 }
1574
1575
1576 static void PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue);
1577
1578
1579 /* See if there are other groups OTW. If so, and if we haven't asked the user
1580 * yet to plan a simultaneous attack, do so now, and readjust the groups
1581 * accordingly. If it is possible to do so, then we will set up the gui, and
1582 * postpone the prebattle interface. */
PossibleToCoordinateSimultaneousGroupArrivals(GROUP * const first_group)1583 static BOOLEAN PossibleToCoordinateSimultaneousGroupArrivals(GROUP* const first_group)
1584 {
1585 // If the user has already been asked, then don't ask the question again!
1586 if (first_group->uiFlags & (GROUPFLAG_SIMULTANEOUSARRIVAL_APPROVED | GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED)) return FALSE;
1587 if (IsGroupTheHelicopterGroup(*first_group)) return FALSE;
1588
1589 /* Count the number of groups that are scheduled to arrive in the same sector
1590 * and are currently adjacent to the sector in question. */
1591 UINT8 n_nearby_groups = 0;
1592 FOR_EACH_PLAYER_GROUP(i)
1593 {
1594 GROUP& g = *i;
1595 if (&g == first_group) continue;
1596 if (!g.fBetweenSectors) continue;
1597 if (g.ubNextX != first_group->ubSectorX) continue;
1598 if (g.ubNextY != first_group->ubSectorY) continue;
1599 if (g.uiFlags & GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED) continue;
1600 if (IsGroupTheHelicopterGroup(g)) continue;
1601 g.uiFlags |= GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED;
1602 ++n_nearby_groups;
1603 }
1604
1605 if (n_nearby_groups == 0) return FALSE;
1606
1607 // Postpone the battle until the user answers the dialog.
1608 InterruptTime();
1609 PauseGame();
1610 LockPauseState(LOCK_PAUSE_SIMULTANEOUS_ARRIVAL);
1611 gpPendingSimultaneousGroup = first_group;
1612
1613 ST::string pStr =
1614 n_nearby_groups == 1 ? gpStrategicString[STR_DETECTED_SINGULAR] :
1615 gpStrategicString[STR_DETECTED_PLURAL];
1616 ST::string enemy_type =
1617 gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE ? gpStrategicString[STR_PB_BLOODCATS] :
1618 gpStrategicString[STR_PB_ENEMIES];
1619 /* header, sector, singular/plural str, confirmation string.
1620 * Ex: Enemies have been detected in sector J9 and another squad is about to
1621 * arrive. Do you wish to coordinate a simultaneous arrival? */
1622 ST::string str = st_format_printf(pStr, enemy_type, 'A' + first_group->ubSectorY - 1, first_group->ubSectorX);
1623 str += ST::format(" {}", gpStrategicString[STR_COORDINATE]);
1624 DoMapMessageBox(MSG_BOX_BASIC_STYLE, str, guiCurrentScreen, MSG_BOX_FLAG_YESNO, PlanSimultaneousGroupArrivalCallback);
1625 gfWaitingForInput = TRUE;
1626 return TRUE;
1627 }
1628
1629
PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue const bMessageValue)1630 static void PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue const bMessageValue)
1631 {
1632 if( bMessageValue == MSG_BOX_RETURN_YES )
1633 {
1634 PrepareGroupsForSimultaneousArrival();
1635 }
1636 else
1637 {
1638 PrepareForPreBattleInterface( gpPendingSimultaneousGroup, gpPendingSimultaneousGroup );
1639 }
1640 UnLockPauseState();
1641 UnPauseGame();
1642 }
1643
1644
DelayEnemyGroupsIfPathsCross(GROUP & player_group)1645 static void DelayEnemyGroupsIfPathsCross(GROUP& player_group)
1646 {
1647 FOR_EACH_ENEMY_GROUP(i)
1648 {
1649 GROUP& g = *i;
1650 // Check to see if this group will arrive in next sector before the player group.
1651 if (g.uiArrivalTime >= player_group.uiArrivalTime) continue;
1652 // Check to see if enemy group will cross paths with player group.
1653 if (g.ubNextX != player_group.ubSectorX) continue;
1654 if (g.ubNextY != player_group.ubSectorY) continue;
1655 if (g.ubSectorX != player_group.ubNextX) continue;
1656 if (g.ubSectorY != player_group.ubNextY) continue;
1657
1658 /* The enemy group will cross paths with the player, so find and delete the
1659 * arrival event and repost it in the future (like a minute or so after the
1660 * player arrives) */
1661 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1662
1663 /* NOTE: This can cause the arrival time to be > GetWorldTotalMin() +
1664 * TraverseTime, so keep that in mind if you have any code that uses these 3
1665 * values to figure out how far along its route a group is! */
1666 g.setArrivalTime(player_group.uiArrivalTime + 1 + Random(10));
1667 if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
1668 SLOGA("Failed to add movement event.");
1669 }
1670 }
1671
1672
1673 //Calculates and posts an event to move the group to the next sector.
InitiateGroupMovementToNextSector(GROUP * pGroup)1674 static void InitiateGroupMovementToNextSector(GROUP* pGroup)
1675 {
1676 INT32 dx, dy;
1677 INT32 i;
1678 UINT8 ubDirection;
1679 UINT8 ubSector;
1680 WAYPOINT *wp;
1681 UINT32 uiSleepMinutes = 0;
1682
1683 Assert( pGroup );
1684 i = pGroup->ubNextWaypointID;
1685 wp = pGroup->pWaypoints;
1686 while( i-- )
1687 { //Traverse through the waypoint list to the next waypoint ID
1688 Assert( wp );
1689 wp = wp->next;
1690 }
1691 Assert( wp );
1692 //We now have the correct waypoint.
1693 //Analyse the group and determine which direction it will move from the current sector.
1694 dx = wp->x - pGroup->ubSectorX;
1695 dy = wp->y - pGroup->ubSectorY;
1696 if( dx && dy )
1697 { //Can't move diagonally!
1698 SLOGA("Attempting to move to waypoint in a diagonal direction from sector %d,%d to sector %d,%d",
1699 pGroup->ubSectorX, pGroup->ubSectorY, wp->x, wp->y );
1700 }
1701 //Clip dx/dy value so that the move is for only one sector.
1702 if( dx >= 1 )
1703 {
1704 ubDirection = EAST_STRATEGIC_MOVE;
1705 dx = 1;
1706 }
1707 else if( dy >= 1 )
1708 {
1709 ubDirection = SOUTH_STRATEGIC_MOVE;
1710 dy = 1;
1711 }
1712 else if( dx <= -1 )
1713 {
1714 ubDirection = WEST_STRATEGIC_MOVE;
1715 dx = -1;
1716 }
1717 else if( dy <= -1 )
1718 {
1719 ubDirection = NORTH_STRATEGIC_MOVE;
1720 dy = -1;
1721 }
1722 else
1723 {
1724 SLOGA("InitiateGroupMovementToNextSector: Attempting to move to waypoint %d, %d that you are already at!", wp->x, wp->y);
1725 return;
1726 }
1727 //All conditions for moving to the next waypoint are now good.
1728 pGroup->ubNextX = (UINT8)( dx + pGroup->ubSectorX );
1729 pGroup->ubNextY = (UINT8)( dy + pGroup->ubSectorY );
1730 //Calc time to get to next waypoint...
1731 ubSector = (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
1732 if( !pGroup->ubSectorZ )
1733 {
1734 BOOLEAN fCalcRegularTime = TRUE;
1735 if( !pGroup->fPlayer )
1736 { //Determine if the enemy group is "sleeping". If so, then simply delay their arrival time by the amount of time
1737 //they are going to be sleeping for.
1738 if( GetWorldHour() >= 21 || GetWorldHour() <= 4 )
1739 { //It is definitely night time.
1740 if( Chance( 67 ) )
1741 { //2 in 3 chance of going to sleep.
1742 pGroup->uiTraverseTime = GetSectorMvtTimeForGroup( ubSector, ubDirection, pGroup );
1743 uiSleepMinutes = 360 + Random( 121 ); //6-8 hours sleep
1744 fCalcRegularTime = FALSE;
1745 }
1746 }
1747 }
1748 if( fCalcRegularTime )
1749 {
1750 pGroup->uiTraverseTime = GetSectorMvtTimeForGroup( ubSector, ubDirection, pGroup );
1751 }
1752 }
1753 else
1754 {
1755 pGroup->uiTraverseTime = 1;
1756 }
1757
1758 AssertMsg(pGroup->uiTraverseTime != TRAVERSE_TIME_IMPOSSIBLE, String("Group %d (%s) attempting illegal move from %c%d to %c%d (%s).",
1759 pGroup->ubGroupID, ( pGroup->fPlayer ) ? "Player" : "AI",
1760 pGroup->ubSectorY+'A', pGroup->ubSectorX, pGroup->ubNextY+'A', pGroup->ubNextX,
1761 gszTerrain[SectorInfo[ubSector].ubTraversability[ubDirection]] ) );
1762
1763 // add sleep, if any
1764 pGroup->uiTraverseTime += uiSleepMinutes;
1765
1766 if( gfTacticalTraversal && gpTacticalTraversalGroup == pGroup )
1767 {
1768 if( gfUndergroundTacticalTraversal )
1769 { //underground movement between sectors takes 1 minute.
1770 pGroup->uiTraverseTime = 1;
1771 }
1772 else
1773 { //strategic movement between town sectors takes 5 minutes.
1774 pGroup->uiTraverseTime = 5;
1775 }
1776 }
1777
1778 // if group isn't already between sectors
1779 if ( !pGroup->fBetweenSectors )
1780 {
1781 // put group between sectors
1782 pGroup->fBetweenSectors = TRUE;
1783 // and set it's arrival time
1784 pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
1785 }
1786 // NOTE: if the group is already between sectors, DON'T MESS WITH ITS ARRIVAL TIME! THAT'S NOT OUR JOB HERE!!!
1787
1788
1789 // special override for AI patrol initialization only
1790 if( gfRandomizingPatrolGroup )
1791 { //We're initializing the patrol group, so randomize the enemy groups to have extremely quick and varying
1792 //arrival times so that their initial positions aren't easily determined.
1793 pGroup->uiTraverseTime = 1 + Random( pGroup->uiTraverseTime - 1 );
1794 pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
1795 }
1796
1797
1798 if (pGroup->fVehicle)
1799 {
1800 // vehicle, set fact it is between sectors too
1801 VEHICLETYPE& v = GetVehicleFromMvtGroup(*pGroup);
1802 v.fBetweenSectors = TRUE;
1803
1804 if (!IsHelicopter(v))
1805 {
1806 SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1807 vs.fBetweenSectors = TRUE;
1808 RemoveSoldierFromTacticalSector(vs);
1809 }
1810 }
1811
1812 //Post the event!
1813 if( !AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID ) )
1814 SLOGA("Failed to add movement event.");
1815
1816 //For the case of player groups, we need to update the information of the soldiers.
1817 if( pGroup->fPlayer )
1818 {
1819 if( pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin( ) )
1820 {
1821 AddStrategicEvent( EVENT_GROUP_ABOUT_TO_ARRIVE, pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, pGroup->ubGroupID );
1822 }
1823
1824 CFOR_EACH_PLAYER_IN_GROUP(curr, pGroup)
1825 {
1826 SOLDIERTYPE& s = *curr->pSoldier;
1827 s.fBetweenSectors = TRUE;
1828 RemoveSoldierFromTacticalSector(s);
1829 }
1830 CheckAndHandleUnloadingOfCurrentWorld();
1831
1832 //If an enemy group will be crossing paths with the player group, delay the enemy group's arrival time so that
1833 //the player will always encounter that group.
1834 if( !pGroup->ubSectorZ )
1835 {
1836 DelayEnemyGroupsIfPathsCross(*pGroup);
1837 }
1838 }
1839 }
1840
1841
RemoveGroupWaypoints(GROUP & g)1842 void RemoveGroupWaypoints(GROUP& g)
1843 {
1844 // If there aren't any waypoints to delete, then return
1845 if (!g.pWaypoints) return;
1846
1847 // Remove all of the waypoints
1848 for (WAYPOINT* i = g.pWaypoints; i;)
1849 {
1850 WAYPOINT* const del = i;
1851 i = i->next;
1852 delete del;
1853 }
1854
1855 g.ubNextWaypointID = 0;
1856 g.pWaypoints = 0;
1857 }
1858
1859
1860 // Set this groups previous sector values
SetGroupPrevSectors(GROUP & g,UINT8 const x,UINT8 const y)1861 static void SetGroupPrevSectors(GROUP& g, UINT8 const x, UINT8 const y)
1862 {
1863 g.ubPrevX = x;
1864 g.ubPrevY = y;
1865 }
1866
1867
1868 static BOOLEAN gfRemovingAllGroups = FALSE;
1869
1870
RemoveGroup(GROUP & g)1871 void RemoveGroup(GROUP& g)
1872 {
1873 if (g.fPersistant && !gfRemovingAllGroups)
1874 {
1875 CancelEmptyPersistentGroupMovement(g);
1876 return;
1877 DoScreenIndependantMessageBox("Strategic Info Warning: Attempting to delete a persistant group.", MSG_BOX_FLAG_OK, NULL);
1878 }
1879
1880 RemoveGroupWaypoints(g);
1881
1882 // Remove the arrival event if applicable.
1883 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1884
1885 // Determine what type of group we have (because it requires different methods)
1886 if (g.fPlayer)
1887 {
1888 while (g.pPlayerList)
1889 {
1890 PLAYERGROUP* const pPlayer = g.pPlayerList;
1891 g.pPlayerList = g.pPlayerList->next;
1892 delete pPlayer;
1893 }
1894 }
1895 else
1896 {
1897 RemoveGroupFromStrategicAILists(g);
1898 delete g.pEnemyGroup;
1899 }
1900
1901 RemoveGroupFromList(&g);
1902
1903 /* safety check: if this group is the BattleGroup, invalid the pointer */
1904 if(gpBattleGroup == &g)
1905 {
1906 gpBattleGroup = 0;
1907 }
1908 }
1909
1910
RemoveAllGroups()1911 void RemoveAllGroups()
1912 {
1913 gfRemovingAllGroups = TRUE;
1914 while( gpGroupList )
1915 {
1916 RemoveGroup(*gpGroupList);
1917 }
1918 gfRemovingAllGroups = FALSE;
1919 }
1920
1921
SetGroupSectorValue(INT16 const x,INT16 const y,INT16 const z,GROUP & g)1922 void SetGroupSectorValue(INT16 const x, INT16 const y, INT16 const z, GROUP& g)
1923 {
1924 RemoveGroupWaypoints(g);
1925
1926 // Set sector x and y to passed values
1927 g.ubSectorX = x;
1928 g.ubSectorY = y;
1929 g.ubNextX = x;
1930 g.ubNextY = y;
1931 g.ubSectorZ = z;
1932 g.fBetweenSectors = FALSE;
1933
1934 // Set next sectors same as current
1935 g.ubOriginalSector = SECTOR(x, y);
1936 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1937
1938 // Set all of the mercs in the group so that they are in the new sector, too.
1939 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1940 {
1941 SOLDIERTYPE& s = *i->pSoldier;
1942 s.sSectorX = x;
1943 s.sSectorY = y;
1944 s.bSectorZ = z;
1945 s.fBetweenSectors = FALSE;
1946 }
1947
1948 CheckAndHandleUnloadingOfCurrentWorld();
1949 }
1950
1951
SetEnemyGroupSector(GROUP & g,UINT8 const sector_id)1952 void SetEnemyGroupSector(GROUP& g, UINT8 const sector_id)
1953 {
1954 // Make sure it is valid
1955 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1956
1957 if (!gfRandomizingPatrolGroup) RemoveGroupWaypoints(g);
1958
1959 g.ubSectorX = g.ubNextX = SECTORX(sector_id);
1960 g.ubSectorY = g.ubNextY = SECTORY(sector_id);
1961 g.ubSectorZ = 0;
1962 g.fBetweenSectors = FALSE;
1963 }
1964
1965
1966 // Set groups next sector x,y value, used ONLY for teleporting groups
SetGroupNextSectorValue(INT16 const x,INT16 const y,GROUP & g)1967 static void SetGroupNextSectorValue(INT16 const x, INT16 const y, GROUP& g)
1968 {
1969 RemoveGroupWaypoints(g);
1970 // Set sector x and y to passed values
1971 g.ubNextX = x;
1972 g.ubNextY = y;
1973 g.fBetweenSectors = FALSE;
1974 // Set next sectors same as current
1975 g.ubOriginalSector = SECTOR(g.ubSectorX, g.ubSectorY);
1976 }
1977
1978
CalculateTravelTimeOfGroup(GROUP const * const pGroup)1979 INT32 CalculateTravelTimeOfGroup(GROUP const* const pGroup)
1980 {
1981 INT32 iDelta;
1982 UINT32 uiEtaTime = 0;
1983 WAYPOINT *pNode = NULL;
1984 WAYPOINT pCurrent, pDest;
1985
1986 // check if valid group
1987 if( pGroup == NULL )
1988 {
1989 // return current time
1990 return( uiEtaTime );
1991 }
1992
1993 // set up next node
1994 pNode = pGroup-> pWaypoints;
1995
1996 // now get the delta in current sector and next sector
1997 iDelta = ( INT32 )( SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) - SECTOR( pGroup->ubNextX, pGroup->ubNextY ) );
1998
1999 if( iDelta == 0 )
2000 {
2001 // not going anywhere...return current time
2002 return( uiEtaTime );
2003 }
2004
2005
2006 // if already on the road
2007 if ( pGroup->fBetweenSectors )
2008 {
2009 // to get travel time to the first sector, use the arrival time, this way it accounts for delays due to simul. arrival
2010 if ( pGroup->uiArrivalTime >= GetWorldTotalMin( ) )
2011 {
2012 uiEtaTime += ( pGroup->uiArrivalTime - GetWorldTotalMin( ) );
2013 }
2014
2015 // first waypoint is NEXT sector
2016 pCurrent.x = pGroup->ubNextX;
2017 pCurrent.y = pGroup->ubNextY;
2018 }
2019 else
2020 {
2021 // first waypoint is CURRENT sector
2022 pCurrent.x = pGroup->ubSectorX;
2023 pCurrent.y = pGroup->ubSectorY;
2024 }
2025
2026 while( pNode )
2027 {
2028 pDest.x = pNode->x;
2029 pDest.y = pNode->y;
2030
2031 // update eta time by the path between these 2 waypts
2032 uiEtaTime += FindTravelTimeBetweenWaypoints( &pCurrent, &pDest, pGroup );
2033
2034 pCurrent.x = pNode->x;
2035 pCurrent.y = pNode->y;
2036
2037 // next waypt
2038 pNode = pNode->next;
2039 }
2040
2041 return( uiEtaTime );
2042 }
2043
2044
FindTravelTimeBetweenWaypoints(WAYPOINT const * const pSource,WAYPOINT const * const pDest,GROUP const * const pGroup)2045 INT32 FindTravelTimeBetweenWaypoints(WAYPOINT const* const pSource, WAYPOINT const* const pDest, GROUP const * const pGroup)
2046 {
2047 UINT8 ubStart=0, ubEnd = 0;
2048 INT32 iDelta = 0;
2049 INT32 iCurrentCostInTime = 0;
2050 UINT8 ubCurrentSector = 0;
2051 UINT8 ubDirection;
2052 INT32 iThisCostInTime;
2053
2054
2055 // find travel time between waypoints
2056 if( !pSource || !pDest )
2057 {
2058 // no change
2059 return( iCurrentCostInTime );
2060 }
2061
2062 // get start and end setor values
2063 ubStart = SECTOR( pSource->x, pSource->y );
2064 ubEnd = SECTOR( pDest->x, pDest->y );
2065
2066 // are we in fact moving?
2067 if( ubStart == ubEnd )
2068 {
2069 // no
2070 return( iCurrentCostInTime );
2071 }
2072
2073 iDelta = ( INT32 )( ubEnd - ubStart );
2074
2075 // which direction are we moving?
2076 if( iDelta > 0 )
2077 {
2078 if( iDelta % ( SOUTH_MOVE - 2 ) == 0 )
2079 {
2080 iDelta = ( SOUTH_MOVE - 2 ) ;
2081 ubDirection = SOUTH_STRATEGIC_MOVE;
2082 }
2083 else
2084 {
2085 iDelta = EAST_MOVE;
2086 ubDirection = EAST_STRATEGIC_MOVE;
2087 }
2088 }
2089 else
2090 {
2091 if( iDelta % ( NORTH_MOVE + 2 ) == 0 )
2092 {
2093 iDelta = ( NORTH_MOVE + 2 );
2094 ubDirection = NORTH_STRATEGIC_MOVE;
2095 }
2096 else
2097 {
2098 iDelta = WEST_MOVE;
2099 ubDirection = WEST_STRATEGIC_MOVE;
2100 }
2101 }
2102
2103 for( ubCurrentSector = ubStart; ubCurrentSector != ubEnd; ubCurrentSector += ( INT8 ) iDelta )
2104 {
2105 // find diff between current and next
2106 iThisCostInTime = GetSectorMvtTimeForGroup( ubCurrentSector, ubDirection, pGroup );
2107
2108 AssertMsg(iThisCostInTime != static_cast<INT32>(TRAVERSE_TIME_IMPOSSIBLE), String("Group %d (%s) attempting illegal move from sector %d, dir %d (%s).",
2109 pGroup->ubGroupID, ( pGroup->fPlayer ) ? "Player" : "AI",
2110 ubCurrentSector, ubDirection,
2111 gszTerrain[SectorInfo[ubCurrentSector].ubTraversability[ubDirection]] ) );
2112
2113 // accumulate it
2114 iCurrentCostInTime += iThisCostInTime;
2115 }
2116
2117 return( iCurrentCostInTime );
2118 }
2119
2120
2121 #define FOOT_TRAVEL_TIME 89
2122 #define CAR_TRAVEL_TIME 30
2123 #define TRUCK_TRAVEL_TIME 32
2124 #define TRACKED_TRAVEL_TIME 46
2125 #define AIR_TRAVEL_TIME 10
2126
2127
2128 // Changes: direction contains the strategic move value, not the delta value.
GetSectorMvtTimeForGroup(UINT8 const ubSector,UINT8 const direction,GROUP const * const g)2129 INT32 GetSectorMvtTimeForGroup(UINT8 const ubSector, UINT8 const direction, GROUP const* const g)
2130 {
2131 /* Determine the group's method(s) of transportation. If more than one, we
2132 * will always use the highest time. */
2133 UINT8 const transport_mask = g->ubTransportationMask;
2134 UINT8 const traverse_type = SectorInfo[ubSector].ubTraversability[direction];
2135 INT32 best_traverse_time = 1000000;
2136
2137 if (traverse_type == EDGEOFWORLD) return TRAVERSE_TIME_IMPOSSIBLE;
2138
2139 /* ARM: Made air-only travel take its normal time per sector even through
2140 * towns. Because Skyrider charges by the sector, not by flying time, it's
2141 * annoying when his default route detours through a town to save time, but
2142 * costs extra money. This isn't exactly unrealistic, since the chopper
2143 * shouldn't be faster flying over a town anyway. Not that other kinds of
2144 * travel should be either - but the towns represents a kind of warping of our
2145 * space-time scale as it is. */
2146 if (traverse_type == TOWN && transport_mask != AIR)
2147 return 5; // Very fast, and vehicle types don't matter.
2148
2149 if (transport_mask & FOOT)
2150 {
2151 UINT8 traverse_mod;
2152 switch (traverse_type)
2153 {
2154 case ROAD: traverse_mod = 100; break;
2155 case PLAINS: traverse_mod = 85; break;
2156 case SAND: traverse_mod = 50; break;
2157 case SPARSE: traverse_mod = 70; break;
2158 case DENSE: traverse_mod = 60; break;
2159 case SWAMP: traverse_mod = 35; break;
2160 case WATER: traverse_mod = 25; break;
2161 case HILLS: traverse_mod = 50; break;
2162 case NS_RIVER: traverse_mod = 25; break;
2163 case EW_RIVER: traverse_mod = 25; break;
2164 default: return TRAVERSE_TIME_IMPOSSIBLE;
2165 }
2166 INT32 const traverse_time = FOOT_TRAVEL_TIME * 100 / traverse_mod;
2167 if (best_traverse_time > traverse_time)
2168 best_traverse_time = traverse_time;
2169
2170 if (g->fPlayer)
2171 {
2172 INT32 highest_encumbrance = 100;
2173 CFOR_EACH_PLAYER_IN_GROUP(curr, g)
2174 {
2175 SOLDIERTYPE const* const s = curr->pSoldier;
2176 if (s->bAssignment == VEHICLE) continue;
2177 /* Soldier is on foot and travelling. Factor encumbrance into movement
2178 * rate. */
2179 INT32 const encumbrance = CalculateCarriedWeight(s);
2180 if (highest_encumbrance < encumbrance)
2181 {
2182 highest_encumbrance = encumbrance;
2183 }
2184 }
2185 best_traverse_time = best_traverse_time * highest_encumbrance / 100;
2186 }
2187 }
2188
2189 if (transport_mask & CAR)
2190 {
2191 UINT8 traverse_mod;
2192 switch (traverse_type)
2193 {
2194 case ROAD: traverse_mod = 100; break;
2195 default: return TRAVERSE_TIME_IMPOSSIBLE;
2196 }
2197 INT32 const traverse_time = CAR_TRAVEL_TIME * 100 / traverse_mod;
2198 if (best_traverse_time > traverse_time)
2199 best_traverse_time = traverse_time;
2200 }
2201
2202 if (transport_mask & TRUCK)
2203 {
2204 UINT8 traverse_mod;
2205 switch (traverse_type)
2206 {
2207 case ROAD: traverse_mod = 100; break;
2208 case PLAINS: traverse_mod = 75; break;
2209 case SPARSE: traverse_mod = 60; break;
2210 case HILLS: traverse_mod = 50; break;
2211 default: return TRAVERSE_TIME_IMPOSSIBLE;
2212 }
2213 INT32 const traverse_time = TRUCK_TRAVEL_TIME * 100 / traverse_mod;
2214 if (best_traverse_time > traverse_time)
2215 best_traverse_time = traverse_time;
2216 }
2217
2218 if (transport_mask & TRACKED)
2219 {
2220 UINT8 traverse_mod;
2221 switch (traverse_type)
2222 {
2223 case ROAD: traverse_mod = 100; break;
2224 case PLAINS: traverse_mod = 100; break;
2225 case SAND: traverse_mod = 70; break;
2226 case SPARSE: traverse_mod = 60; break;
2227 case HILLS: traverse_mod = 60; break;
2228 case NS_RIVER: traverse_mod = 20; break;
2229 case EW_RIVER: traverse_mod = 20; break;
2230 case WATER: traverse_mod = 10; break;
2231 default: return TRAVERSE_TIME_IMPOSSIBLE;
2232 }
2233 INT32 const traverse_time = TRACKED_TRAVEL_TIME * 100 / traverse_mod;
2234 if (best_traverse_time > traverse_time)
2235 best_traverse_time = traverse_time;
2236 }
2237
2238 if (transport_mask & AIR)
2239 {
2240 INT32 const traverse_time = AIR_TRAVEL_TIME;
2241 if (best_traverse_time > traverse_time)
2242 best_traverse_time = traverse_time;
2243 }
2244
2245 return best_traverse_time;
2246 }
2247
2248
2249 // Counts the number of live mercs in any given sector.
PlayerMercsInSector(UINT8 const x,UINT8 const y,UINT8 const z)2250 UINT8 PlayerMercsInSector(UINT8 const x, UINT8 const y, UINT8 const z)
2251 {
2252 UINT8 n_mercs = 0;
2253 CFOR_EACH_PLAYER_GROUP(g)
2254 {
2255 if (g->fBetweenSectors) continue;
2256 if (g->ubSectorX != x || g->ubSectorY != y || g->ubSectorZ != z) continue;
2257 /* We have a group, make sure that it isn't a group containing only dead
2258 * members. */
2259 CFOR_EACH_PLAYER_IN_GROUP(p, g)
2260 {
2261 SOLDIERTYPE const* const s = p->pSoldier;
2262 if (s->bLife == 0) continue;
2263 // Robots count as mercs here, because they can fight, but vehicles don't
2264 if (s->uiStatusFlags & SOLDIER_VEHICLE) continue;
2265 n_mercs++;
2266 }
2267 }
2268 return n_mercs;
2269 }
2270
2271
PlayerGroupsInSector(UINT8 const x,UINT8 const y,UINT8 const z)2272 UINT8 PlayerGroupsInSector(UINT8 const x, UINT8 const y, UINT8 const z)
2273 {
2274 UINT8 n_groups = 0;
2275 CFOR_EACH_PLAYER_GROUP(g)
2276 {
2277 if (g->fBetweenSectors) continue;
2278 if (g->ubSectorX != x || g->ubSectorY != y || g->ubSectorZ != z) continue;
2279 /* We have a group, make sure that it isn't a group containing only dead
2280 * members. */
2281 CFOR_EACH_PLAYER_IN_GROUP(p, g)
2282 {
2283 if (p->pSoldier->bLife == 0) continue;
2284 ++n_groups;
2285 break;
2286 }
2287 }
2288 return n_groups;
2289 }
2290
2291
2292 // is the player group with this id in motion?
PlayerIDGroupInMotion(UINT8 const id)2293 bool PlayerIDGroupInMotion(UINT8 const id)
2294 {
2295 GROUP* const g = GetGroup(id);
2296 return g && PlayerGroupInMotion(g);
2297 }
2298
2299
2300 // is the player group in motion?
PlayerGroupInMotion(GROUP const * const pGroup)2301 BOOLEAN PlayerGroupInMotion(GROUP const* const pGroup)
2302 {
2303 return( pGroup -> fBetweenSectors );
2304 }
2305
2306
2307 /* Add this group to the current battle fray!
2308 * NOTE: For enemies, only MAX_STRATEGIC_TEAM_SIZE at a time can be in a battle,
2309 * so if it ever gets past that, god help the player, but we'll have to insert
2310 * them as those slots free up. */
HandleArrivalOfReinforcements(GROUP const * const g)2311 void HandleArrivalOfReinforcements(GROUP const* const g)
2312 {
2313 if (g->fPlayer)
2314 { /* We don't have to worry about filling up the player slots, because it is
2315 * impossible to have more player's in the game than the number of slots
2316 * available for the player. */
2317
2318 /* First, determine which entrypoint to use, based on the travel direction
2319 * of the group */
2320 UINT8 const x = g->ubSectorX;
2321 UINT8 const y = g->ubSectorY;
2322 InsertionCode const strategic_insertion_code =
2323 x < g->ubPrevX ? INSERTION_CODE_EAST :
2324 x > g->ubPrevX ? INSERTION_CODE_WEST :
2325 y < g->ubPrevY ? INSERTION_CODE_SOUTH :
2326 y > g->ubPrevY ? INSERTION_CODE_NORTH :
2327 throw std::logic_error("reinforcements come from same sector");
2328
2329 bool first = true;
2330 CFOR_EACH_PLAYER_IN_GROUP(p, g)
2331 {
2332 SOLDIERTYPE& s = *p->pSoldier;
2333 s.ubStrategicInsertionCode = strategic_insertion_code;
2334 UpdateMercInSector(s, x, y, 0);
2335
2336 // Do arrives quote
2337 if (first) TacticalCharacterDialogue(&s, QUOTE_MERC_REACHED_DESTINATION);
2338 first = false;
2339 }
2340 ScreenMsg(FONT_YELLOW, MSG_INTERFACE, g_langRes->Message[STR_PLAYER_REINFORCEMENTS]);
2341 }
2342 else
2343 {
2344 gfPendingEnemies = TRUE;
2345 ResetMortarsOnTeamCount();
2346 AddPossiblePendingEnemiesToBattle();
2347 }
2348 }
2349
2350
PlayersBetweenTheseSectors(INT16 const sec_src,INT16 const sec_dst,INT32 * const n_enter,INT32 * const n_exit,BOOLEAN * const about_to_arrive_enter)2351 BOOLEAN PlayersBetweenTheseSectors(INT16 const sec_src, INT16 const sec_dst, INT32* const n_enter, INT32* const n_exit, BOOLEAN* const about_to_arrive_enter)
2352 {
2353 *n_enter = 0;
2354 *n_exit = 0;
2355 *about_to_arrive_enter = FALSE;
2356
2357 GROUP const* const bg = gpBattleGroup;
2358 INT16 const sec_battle = bg ? SECTOR(bg->ubSectorX, bg->ubSectorY) : -1;
2359
2360 /* Get number of characters entering/existing between these two sectors.
2361 * Special conditions during pre-battle interface to return where this
2362 * function is used to show potential retreating directions instead! */
2363
2364 CFOR_EACH_PLAYER_GROUP(i)
2365 {
2366 GROUP const& g = *i;
2367 bool const is_heli_group = IsGroupTheHelicopterGroup(g);
2368
2369 /* If this group is aboard the helicopter and we're showing the airspace
2370 * layer, don't count any mercs aboard the chopper, because the chopper icon
2371 * itself serves the function of showing the location/size of this group. */
2372 if (is_heli_group && fShowAircraftFlag) continue;
2373
2374 /* If only showing retreat paths, ignore groups not in the battle sector.
2375 * If NOT showing retreat paths, ignore groups not between sectors. */
2376 if (gfDisplayPotentialRetreatPaths ? sec_battle != sec_src : !g.fBetweenSectors) continue;
2377
2378 UINT8 n_mercs = g.ubGroupSize;
2379 if (n_mercs == 0) // Skip empty persistent groups
2380 {
2381 Assert(g.fPersistant);
2382 continue;
2383 }
2384
2385 INT16 const sec_prev = IS_VALID_SECTOR(g.ubPrevX, g.ubPrevY) ? SECTOR(g.ubPrevX, g.ubPrevY) : -1;
2386 INT16 const sec_cur = SECTOR(g.ubSectorX, g.ubSectorY);
2387 INT16 const sec_next = IS_VALID_SECTOR(g.ubNextX, g.ubNextY) ? SECTOR(g.ubNextX, g.ubNextY) : -1;
2388
2389 bool const may_retreat_from_battle =
2390 sec_battle == sec_src && sec_cur == sec_src && sec_prev == sec_dst;
2391
2392 bool const retreating_from_battle =
2393 sec_battle == sec_dst && sec_cur == sec_dst && sec_prev == sec_src;
2394
2395 if (may_retreat_from_battle || (sec_cur == sec_src && sec_next == sec_dst))
2396 {
2397 // If it's a valid vehicle, but not the helicopter (which can fly empty)
2398 if (g.fVehicle && !is_heli_group)
2399 { // subtract 1, we don't wanna count the vehicle itself for purposes of showing a number on the map
2400 n_mercs--;
2401 }
2402
2403 *n_enter += n_mercs;
2404
2405 if (may_retreat_from_battle ||
2406 g.uiArrivalTime - GetWorldTotalMin() <= ABOUT_TO_ARRIVE_DELAY)
2407 {
2408 *about_to_arrive_enter = TRUE;
2409 }
2410 }
2411 else if (retreating_from_battle || (sec_cur == sec_dst && sec_next == sec_src))
2412 {
2413 // If it's a valid vehicle, but not the helicopter (which can fly empty)
2414 if (g.fVehicle && !is_heli_group)
2415 { // subtract 1, we don't wanna count the vehicle itself for purposes of showing a number on the map
2416 n_mercs--;
2417 }
2418
2419 *n_exit += n_mercs;
2420 }
2421 }
2422
2423 // if there was actually anyone leaving this sector and entering next
2424 return *n_enter > 0;
2425 }
2426
2427
MoveAllGroupsInCurrentSectorToSector(UINT8 const x,UINT8 const y,UINT8 const z)2428 void MoveAllGroupsInCurrentSectorToSector(UINT8 const x, UINT8 const y, UINT8 const z)
2429 {
2430 FOR_EACH_PLAYER_GROUP(g)
2431 {
2432 if (g->ubSectorX != gWorldSectorX) continue;
2433 if (g->ubSectorY != gWorldSectorY) continue;
2434 if (g->ubSectorZ != gbWorldSectorZ) continue;
2435 if (g->fBetweenSectors) continue;
2436
2437 // This player group is in the currently loaded sector
2438 g->ubSectorX = x;
2439 g->ubSectorY = y;
2440 g->ubSectorZ = z;
2441 CFOR_EACH_PLAYER_IN_GROUP(p, g)
2442 {
2443 p->pSoldier->sSectorX = x;
2444 p->pSoldier->sSectorY = y;
2445 p->pSoldier->bSectorZ = z;
2446 p->pSoldier->fBetweenSectors = FALSE;
2447 }
2448 }
2449 CheckAndHandleUnloadingOfCurrentWorld();
2450 }
2451
2452
2453 static void SaveEnemyGroupStruct(HWFILE, GROUP const&);
2454 static void SavePlayerGroupList(HWFILE, GROUP const*);
2455 static void SaveWayPointList(HWFILE, GROUP const*);
2456
2457
SaveStrategicMovementGroupsToSaveGameFile(HWFILE const f)2458 void SaveStrategicMovementGroupsToSaveGameFile(HWFILE const f)
2459 {
2460 // Save the number of movement groups to the saved game file
2461 UINT32 uiNumberOfGroups = 0;
2462 CFOR_EACH_GROUP(g) ++uiNumberOfGroups;
2463 FileWrite(f, &uiNumberOfGroups, sizeof(UINT32));
2464
2465 CFOR_EACH_GROUP(g)
2466 {
2467 BYTE data[84];
2468 DataWriter d{data};
2469 INJ_BOOL(d, g->fDebugGroup)
2470 INJ_BOOL(d, g->fPlayer)
2471 INJ_BOOL(d, g->fVehicle)
2472 INJ_BOOL(d, g->fPersistant)
2473 INJ_U8(d, g->ubGroupID)
2474 INJ_U8(d, g->ubGroupSize)
2475 INJ_U8(d, g->ubSectorX)
2476 INJ_U8(d, g->ubSectorY)
2477 INJ_U8(d, g->ubSectorZ)
2478 INJ_U8(d, g->ubNextX)
2479 INJ_U8(d, g->ubNextY)
2480 INJ_U8(d, g->ubPrevX)
2481 INJ_U8(d, g->ubPrevY)
2482 INJ_U8(d, g->ubOriginalSector)
2483 INJ_BOOL(d, g->fBetweenSectors)
2484 INJ_U8(d, g->ubMoveType)
2485 INJ_U8(d, g->ubNextWaypointID)
2486 INJ_SKIP(d, 3)
2487 INJ_U32(d, g->uiArrivalTime)
2488 INJ_U32(d, g->uiTraverseTime)
2489 INJ_SKIP(d, 8)
2490 INJ_U8(d, g->ubTransportationMask)
2491 INJ_SKIP(d, 3)
2492 INJ_U32(d, g->uiFlags)
2493 INJ_U8(d, g->ubCreatedSectorID)
2494 INJ_U8(d, g->ubSectorIDOfLastReassignment)
2495 INJ_SKIP(d, 38)
2496 Assert(d.getConsumed() == lengthof(data));
2497
2498 FileWrite(f, data, sizeof(data));
2499
2500 // Save the linked list, for the current type of group
2501 if (g->fPlayer)
2502 {
2503 if (g->ubGroupSize) SavePlayerGroupList(f, g);
2504 }
2505 else
2506 {
2507 SaveEnemyGroupStruct(f, *g);
2508 }
2509
2510 SaveWayPointList(f, g);
2511 }
2512
2513 // Save the unique id mask
2514 FileWrite(f, uniqueIDMask, sizeof(uniqueIDMask));
2515 }
2516
2517
2518 static void LoadEnemyGroupStructFromSavedGame(HWFILE, GROUP&);
2519 static void LoadPlayerGroupList(HWFILE, GROUP*);
2520 static void LoadWayPointList(HWFILE, GROUP*);
2521
2522
LoadStrategicMovementGroupsFromSavedGameFile(HWFILE const f)2523 void LoadStrategicMovementGroupsFromSavedGameFile(HWFILE const f)
2524 {
2525 Assert(gpGroupList == NULL);
2526
2527 // Load the number of nodes in the list
2528 UINT32 uiNumberOfGroups;
2529 FileRead(f, &uiNumberOfGroups, sizeof(UINT32));
2530
2531 //loop through all the nodes and add them to the LL
2532 GROUP** anchor = &gpGroupList;
2533 for (UINT32 i = uiNumberOfGroups; i != 0; --i)
2534 {
2535 GROUP* const g = new GROUP{};
2536
2537 BYTE data[84];
2538 FileRead(f, data, sizeof(data));
2539
2540 DataReader d{data};
2541 EXTR_BOOL(d, g->fDebugGroup)
2542 EXTR_BOOL(d, g->fPlayer)
2543 EXTR_BOOL(d, g->fVehicle)
2544 EXTR_BOOL(d, g->fPersistant)
2545 EXTR_U8(d, g->ubGroupID)
2546 EXTR_U8(d, g->ubGroupSize)
2547 EXTR_U8(d, g->ubSectorX)
2548 EXTR_U8(d, g->ubSectorY)
2549 EXTR_U8(d, g->ubSectorZ)
2550 EXTR_U8(d, g->ubNextX)
2551 EXTR_U8(d, g->ubNextY)
2552 EXTR_U8(d, g->ubPrevX)
2553 EXTR_U8(d, g->ubPrevY)
2554 EXTR_U8(d, g->ubOriginalSector)
2555 EXTR_BOOL(d, g->fBetweenSectors)
2556 EXTR_U8(d, g->ubMoveType)
2557 EXTR_U8(d, g->ubNextWaypointID)
2558 EXTR_SKIP(d, 3)
2559 EXTR_U32(d, g->uiArrivalTime)
2560 EXTR_U32(d, g->uiTraverseTime)
2561 EXTR_SKIP(d, 8)
2562 EXTR_U8(d, g->ubTransportationMask)
2563 EXTR_SKIP(d, 3)
2564 EXTR_U32(d, g->uiFlags)
2565 EXTR_U8(d, g->ubCreatedSectorID)
2566 EXTR_U8(d, g->ubSectorIDOfLastReassignment)
2567 EXTR_SKIP(d, 38)
2568 Assert(d.getConsumed() == lengthof(data));
2569
2570 if (g->fPlayer)
2571 {
2572 // If there is a player list, add it
2573 if (g->ubGroupSize) LoadPlayerGroupList(f, g);
2574 }
2575 else // Else it's an enemy group
2576 {
2577 LoadEnemyGroupStructFromSavedGame(f, *g);
2578 }
2579
2580 LoadWayPointList(f, g);
2581
2582 // Add the node to the list
2583 *anchor = g;
2584 anchor = &g->next;
2585 }
2586
2587 //@@@ TEMP!
2588 //Rebuild the uniqueIDMask as a very old bug broke the uniqueID assignments in extremely rare cases.
2589 std::fill(std::begin(uniqueIDMask), std::end(uniqueIDMask), 0);
2590 CFOR_EACH_GROUP(g)
2591 {
2592 const UINT32 index = g->ubGroupID / 32;
2593 const UINT32 bit = g->ubGroupID % 32;
2594 const UINT32 mask = 1 << bit;
2595 uniqueIDMask[index] += mask;
2596 }
2597
2598 // Skip over saved unique id mask
2599 FileSeek(f, 32, FILE_SEEK_FROM_CURRENT);
2600 }
2601
2602
2603 // Saves the Player's group list to the saved game file
SavePlayerGroupList(HWFILE const f,GROUP const * const g)2604 static void SavePlayerGroupList(HWFILE const f, GROUP const* const g)
2605 {
2606 // Save the number of nodes in the list
2607 UINT32 uiNumberOfNodesInList = 0;
2608 CFOR_EACH_PLAYER_IN_GROUP(p, g) ++uiNumberOfNodesInList;
2609 FileWrite(f, &uiNumberOfNodesInList, sizeof(UINT32));
2610
2611 // Loop through and save only the players profile id
2612 CFOR_EACH_PLAYER_IN_GROUP(p, g)
2613 {
2614 // Save the ubProfile ID for this node
2615 const UINT32 uiProfileID = p->pSoldier->ubProfile;
2616 FileWrite(f, &uiProfileID, sizeof(UINT32));
2617 }
2618 }
2619
2620
2621 //Loads the LL for the playerlist from the savegame file
LoadPlayerGroupList(HWFILE const f,GROUP * const g)2622 static void LoadPlayerGroupList(HWFILE const f, GROUP* const g)
2623 {
2624 // Load the number of nodes in the player list
2625 UINT32 node_count;
2626 FileRead(f, &node_count, sizeof(UINT32));
2627
2628 PLAYERGROUP** anchor = &g->pPlayerList;
2629 for (UINT32 i = node_count; i != 0; --i)
2630 {
2631 PLAYERGROUP* const pg = new PLAYERGROUP{};
2632
2633 UINT32 profile_id;
2634 FileRead(f, &profile_id, sizeof(UINT32));
2635
2636 SOLDIERTYPE* const s = FindSoldierByProfileIDOnPlayerTeam(profile_id);
2637 //Should never happen
2638 //Assert(s != NULL);
2639 pg->pSoldier = s;
2640 pg->next = NULL;
2641
2642 *anchor = pg;
2643 anchor = &pg->next;
2644 }
2645 }
2646
2647
2648 // Saves the enemy group struct to the saved game file
SaveEnemyGroupStruct(HWFILE const f,GROUP const & g)2649 static void SaveEnemyGroupStruct(HWFILE const f, GROUP const& g)
2650 {
2651 BYTE data[29];
2652 DataWriter d{data};
2653 ENEMYGROUP const& eg = *g.pEnemyGroup;
2654 INJ_U8( d, eg.ubNumTroops)
2655 INJ_U8( d, eg.ubNumElites)
2656 INJ_U8( d, eg.ubNumAdmins)
2657 INJ_SKIP(d, 1)
2658 INJ_U8( d, eg.ubPendingReinforcements)
2659 INJ_U8( d, eg.ubAdminsInBattle)
2660 INJ_U8( d, eg.ubIntention)
2661 INJ_U8( d, eg.ubTroopsInBattle)
2662 INJ_U8( d, eg.ubElitesInBattle)
2663 INJ_SKIP(d, 20)
2664 Assert(d.getConsumed() == lengthof(data));
2665
2666 FileWrite(f, data, sizeof(data));
2667 }
2668
2669
2670 // Loads the enemy group struct from the saved game file
LoadEnemyGroupStructFromSavedGame(HWFILE const f,GROUP & g)2671 static void LoadEnemyGroupStructFromSavedGame(HWFILE const f, GROUP& g)
2672 {
2673 BYTE data[29];
2674 FileRead(f, data, sizeof(data));
2675
2676 ENEMYGROUP* const eg = new ENEMYGROUP{};
2677 DataReader d{data};
2678 EXTR_U8( d, eg->ubNumTroops)
2679 EXTR_U8( d, eg->ubNumElites)
2680 EXTR_U8( d, eg->ubNumAdmins)
2681 EXTR_SKIP(d, 1)
2682 EXTR_U8( d, eg->ubPendingReinforcements)
2683 EXTR_U8( d, eg->ubAdminsInBattle)
2684 EXTR_U8( d, eg->ubIntention)
2685 EXTR_U8( d, eg->ubTroopsInBattle)
2686 EXTR_U8( d, eg->ubElitesInBattle)
2687 EXTR_SKIP(d, 20)
2688 Assert(d.getConsumed() == lengthof(data));
2689
2690 g.pEnemyGroup = eg;
2691 }
2692
2693
SaveWayPointList(HWFILE const f,GROUP const * const g)2694 static void SaveWayPointList(HWFILE const f, GROUP const* const g)
2695 {
2696 // Save the number of waypoints
2697 UINT32 uiNumberOfWayPoints = 0;
2698 for (const WAYPOINT* w = g->pWaypoints; w != NULL; w = w->next)
2699 {
2700 ++uiNumberOfWayPoints;
2701 }
2702 FileWrite(f, &uiNumberOfWayPoints, sizeof(UINT32));
2703
2704 for (const WAYPOINT* w = g->pWaypoints; w != NULL; w = w->next)
2705 {
2706 BYTE data[8];
2707 DataWriter d{data};
2708 INJ_U8( d, w->x)
2709 INJ_U8( d, w->y)
2710 INJ_SKIP(d, 6)
2711 Assert(d.getConsumed() == lengthof(data));
2712
2713 FileWrite(f, data, sizeof(data));
2714 }
2715 }
2716
2717
LoadWayPointList(HWFILE const f,GROUP * const g)2718 static void LoadWayPointList(HWFILE const f, GROUP* const g)
2719 {
2720 // Load the number of waypoints
2721 UINT32 uiNumberOfWayPoints;
2722 FileRead(f, &uiNumberOfWayPoints, sizeof(UINT32));
2723
2724 WAYPOINT** anchor = &g->pWaypoints;
2725 for (UINT32 i = uiNumberOfWayPoints; i != 0; --i)
2726 {
2727 WAYPOINT* const w = new WAYPOINT{};
2728
2729 BYTE data[8];
2730 FileRead(f, data, sizeof(data));
2731
2732 DataReader d{data};
2733 EXTR_U8( d, w->x)
2734 EXTR_U8( d, w->y)
2735 EXTR_SKIP(d, 6)
2736 Assert(d.getConsumed() == lengthof(data));
2737
2738 // Add the node to the list
2739 *anchor = w;
2740 anchor = &w->next;
2741 }
2742 *anchor = NULL;
2743 }
2744
2745
CalculateGroupRetreatSector(GROUP * pGroup)2746 void CalculateGroupRetreatSector( GROUP *pGroup )
2747 {
2748 SECTORINFO *pSector;
2749 UINT32 uiSectorID;
2750
2751 uiSectorID = SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
2752 pSector = &SectorInfo[ uiSectorID ];
2753
2754 if( pSector->ubTraversability[ NORTH_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2755 pSector->ubTraversability[ NORTH_STRATEGIC_MOVE ] != EDGEOFWORLD )
2756 {
2757 pGroup->ubPrevX = pGroup->ubSectorX;
2758 pGroup->ubPrevY = pGroup->ubSectorY - 1;
2759 }
2760 else if( pSector->ubTraversability[ EAST_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2761 pSector->ubTraversability[ EAST_STRATEGIC_MOVE ] != EDGEOFWORLD )
2762 {
2763 pGroup->ubPrevX = pGroup->ubSectorX + 1;
2764 pGroup->ubPrevY = pGroup->ubSectorY;
2765 }
2766 else if( pSector->ubTraversability[ WEST_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2767 pSector->ubTraversability[ WEST_STRATEGIC_MOVE ] != EDGEOFWORLD )
2768 {
2769 pGroup->ubPrevX = pGroup->ubSectorX - 1;
2770 pGroup->ubPrevY = pGroup->ubSectorY;
2771 }
2772 else if( pSector->ubTraversability[ SOUTH_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2773 pSector->ubTraversability[ SOUTH_STRATEGIC_MOVE ] != EDGEOFWORLD )
2774 {
2775 pGroup->ubPrevX = pGroup->ubSectorX;
2776 pGroup->ubPrevY = pGroup->ubSectorY + 1;
2777 }
2778 else
2779 {
2780 SLOGA("Player group cannot retreat from sector %c%d ", pGroup->ubSectorY+'A'-1, pGroup->ubSectorX);
2781 return;
2782 }
2783 if( pGroup->fPlayer )
2784 { //update the previous sector for the mercs
2785 CFOR_EACH_PLAYER_IN_GROUP(pPlayer, pGroup)
2786 {
2787 pPlayer->pSoldier->ubPrevSectorID = (UINT8)SECTOR( pGroup->ubPrevX, pGroup->ubPrevY );
2788 }
2789 }
2790 }
2791
2792 //Called when all checks have been made for the group (if possible to retreat, etc.) This function
2793 //blindly determines where to move the group.
RetreatGroupToPreviousSector(GROUP & g)2794 void RetreatGroupToPreviousSector(GROUP& g)
2795 {
2796 AssertMsg(!g.fBetweenSectors, "Can't retreat a group when between sectors!");
2797
2798 UINT8 direction = 255;
2799 if (g.ubPrevX != 16 || g.ubPrevY != 16)
2800 { // Group has a previous sector
2801 g.ubNextX = g.ubPrevX;
2802 g.ubNextY = g.ubPrevY;
2803
2804 // Determine the correct direction
2805 INT32 const dx = g.ubNextX - g.ubSectorX;
2806 INT32 const dy = g.ubNextY - g.ubSectorY;
2807 if (dx == 0 && dy == -1) direction = NORTH_STRATEGIC_MOVE;
2808 else if (dx == 1 && dy == 0) direction = EAST_STRATEGIC_MOVE;
2809 else if (dx == 0 && dy == 1) direction = SOUTH_STRATEGIC_MOVE;
2810 else if (dx == -1 && dy == 0) direction = WEST_STRATEGIC_MOVE;
2811 else
2812 {
2813 throw std::runtime_error(ST::format("Player group attempting illegal retreat from {}{} to {}{}.", (char)(g.ubSectorY + 'A' - 1), g.ubSectorX, (char)(g.ubNextY + 'A' - 1), g.ubNextX).to_std_string());
2814 }
2815 }
2816 else
2817 { // Group doesn't have a previous sector. Create one, then recurse
2818 CalculateGroupRetreatSector(&g);
2819 RetreatGroupToPreviousSector(g);
2820 // XXX direction is invalid, causes out-of-bounds access below
2821 }
2822
2823 // Calc time to get to next waypoint
2824 UINT8 const sector = SECTOR(g.ubSectorX, g.ubSectorY);
2825 g.uiTraverseTime = GetSectorMvtTimeForGroup(sector, direction, &g);
2826 AssertMsg(g.uiTraverseTime != TRAVERSE_TIME_IMPOSSIBLE, String("Group %d (%s) attempting illegal move from %c%d to %c%d (%s).", g.ubGroupID, g.fPlayer ? "Player" : "AI", g.ubSectorY + 'A', g.ubSectorX, g.ubNextY + 'A', g.ubNextX, gszTerrain[SectorInfo[sector].ubTraversability[direction]]));
2827
2828 // Because we are in the strategic layer, don't make the arrival instantaneous (towns)
2829 if (g.uiTraverseTime == 0) g.uiTraverseTime = 5;
2830
2831 g.setArrivalTime(GetWorldTotalMin() + g.uiTraverseTime);
2832 g.fBetweenSectors = TRUE;
2833 g.uiFlags |= GROUPFLAG_JUST_RETREATED_FROM_BATTLE;
2834
2835 if (g.fVehicle)
2836 { // Vehicle, set fact it is between sectors too
2837 VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
2838 v.fBetweenSectors = TRUE;
2839 }
2840
2841 if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
2842 SLOGA("Failed to add movement event.");
2843
2844 // For the case of player groups, we need to update the information of the soldiers.
2845 if (g.fPlayer)
2846 {
2847 if (g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
2848 {
2849 AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
2850 }
2851
2852 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
2853 {
2854 SOLDIERTYPE& s = *i->pSoldier;
2855 s.fBetweenSectors = TRUE;
2856 RemoveSoldierFromTacticalSector(s);
2857 }
2858 }
2859 }
2860
2861
FindEnemyMovementGroupInSector(const UINT8 ubSectorX,const UINT8 ubSectorY)2862 GROUP* FindEnemyMovementGroupInSector(const UINT8 ubSectorX, const UINT8 ubSectorY)
2863 {
2864 FOR_EACH_ENEMY_GROUP(g)
2865 {
2866 if (g->ubSectorX == ubSectorX &&
2867 g->ubSectorY == ubSectorY &&
2868 g->ubSectorZ == 0)
2869 {
2870 return g;
2871 }
2872 }
2873 return NULL;
2874 }
2875
2876
FindPlayerMovementGroupInSector(const UINT8 x,const UINT8 y)2877 GROUP* FindPlayerMovementGroupInSector(const UINT8 x, const UINT8 y)
2878 {
2879 FOR_EACH_PLAYER_GROUP(i)
2880 {
2881 GROUP& g = *i;
2882 // NOTE: These checks must always match the INVOLVED group checks in PBI!!!
2883 if (g.ubGroupSize != 0 &&
2884 !g.fBetweenSectors &&
2885 g.ubSectorX == x &&
2886 g.ubSectorY == y &&
2887 g.ubSectorZ == 0 &&
2888 !GroupHasInTransitDeadOrPOWMercs(g) &&
2889 (!IsGroupTheHelicopterGroup(g) || !fHelicopterIsAirBorne))
2890 {
2891 return &g;
2892 }
2893 }
2894 return NULL;
2895 }
2896
2897
GroupAtFinalDestination(const GROUP * const pGroup)2898 BOOLEAN GroupAtFinalDestination(const GROUP* const pGroup)
2899 {
2900 WAYPOINT *wp;
2901
2902 if( pGroup->ubMoveType != ONE_WAY )
2903 return FALSE; //Group will continue to patrol, hence never stops.
2904
2905 //Determine if we are at the final waypoint.
2906 wp = GetFinalWaypoint( pGroup );
2907
2908 if( !wp )
2909 { //no waypoints, so the group is at it's destination. This happens when
2910 //an enemy group is created in the destination sector (which is legal for
2911 //staging groups which always stop adjacent to their real sector destination)
2912 return TRUE;
2913 }
2914
2915 // if we're there
2916 if( ( pGroup->ubSectorX == wp->x ) && ( pGroup->ubSectorY == wp->y ) )
2917 {
2918 return TRUE;
2919 }
2920
2921 return FALSE;
2922 }
2923
2924
GetFinalWaypoint(const GROUP * const pGroup)2925 WAYPOINT* GetFinalWaypoint(const GROUP* const pGroup)
2926 {
2927 WAYPOINT *wp;
2928
2929 Assert( pGroup );
2930
2931 //Make sure they're on a one way route, otherwise this request is illegal
2932 Assert( pGroup->ubMoveType == ONE_WAY );
2933
2934 wp = pGroup->pWaypoints;
2935 if( wp )
2936 {
2937 while( wp->next )
2938 {
2939 wp = wp->next;
2940 }
2941 }
2942
2943 return( wp );
2944 }
2945
2946
2947 static void ResetMovementForEnemyGroup(GROUP* pGroup);
2948
2949
2950 //The sector supplied resets ALL enemy groups in the sector specified. See comments in
2951 //ResetMovementForEnemyGroup() for more details on what the resetting does.
ResetMovementForEnemyGroupsInLocation(UINT8 ubSectorX,UINT8 ubSectorY)2952 void ResetMovementForEnemyGroupsInLocation( UINT8 ubSectorX, UINT8 ubSectorY )
2953 {
2954 INT16 sSectorX, sSectorY, sSectorZ;
2955
2956 GetCurrentBattleSectorXYZ( &sSectorX, &sSectorY, &sSectorZ );
2957 FOR_EACH_GROUP_SAFE(pGroup)
2958 {
2959 if( !pGroup->fPlayer )
2960 {
2961 if( pGroup->ubSectorX == sSectorX && pGroup->ubSectorY == sSectorY )
2962 {
2963 ResetMovementForEnemyGroup( pGroup );
2964 }
2965 }
2966 }
2967 }
2968
2969
2970 //This function is used to reset the location of the enemy group if they are
2971 //currently between sectors. If they were 50% of the way from sector A10 to A11,
2972 //then after this function is called, then that group would be 0% of the way from
2973 //sector A10 to A11. In no way does this function effect the strategic path for
2974 //the group.
ResetMovementForEnemyGroup(GROUP * pGroup)2975 static void ResetMovementForEnemyGroup(GROUP* pGroup)
2976 {
2977 //Validate that the group is an enemy group and that it is moving.
2978 if( pGroup->fPlayer )
2979 {
2980 return;
2981 }
2982 if( !pGroup->fBetweenSectors || !pGroup->ubNextX || !pGroup->ubNextY )
2983 { //Reset the group's assignment by moving it to the group's original sector as it's pending group.
2984 RepollSAIGroup( pGroup );
2985 return;
2986 }
2987
2988 //Cancel the event that is posted.
2989 DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
2990
2991 //Calculate the new arrival time (all data pertaining to movement should be valid)
2992 if( pGroup->uiTraverseTime > 400 )
2993 { //The group was likely sleeping which makes for extremely long arrival times. Shorten it
2994 //arbitrarily. Doesn't really matter if this isn't accurate.
2995 pGroup->uiTraverseTime = 90;
2996 }
2997 pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
2998
2999 //Add a new event
3000 AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID );
3001 }
3002
3003
UpdatePersistantGroupsFromOldSave(UINT32 uiSavedGameVersion)3004 void UpdatePersistantGroupsFromOldSave( UINT32 uiSavedGameVersion )
3005 {
3006 INT32 cnt;
3007 BOOLEAN fDoChange = FALSE;
3008
3009 // ATE: If saved game is < 61, we need to do something better!
3010 if( uiSavedGameVersion < 61 )
3011 {
3012 for( cnt = 0; cnt < 55; cnt++ )
3013 {
3014 // create mvt groups
3015 GROUP* const pGroup = GetGroup(cnt);
3016 if( pGroup != NULL && pGroup->fPlayer )
3017 {
3018 pGroup->fPersistant = TRUE;
3019 }
3020 }
3021
3022 fDoChange = TRUE;
3023 }
3024 else if( uiSavedGameVersion < 63 )
3025 {
3026 for( cnt = 0; cnt < NUMBER_OF_SQUADS; cnt++ )
3027 {
3028 // create mvt groups
3029 GROUP* const pGroup = GetGroup(SquadMovementGroups[cnt]);
3030 if ( pGroup != NULL )
3031 {
3032 pGroup->fPersistant = TRUE;
3033 }
3034 }
3035
3036 for( cnt = 0; cnt < MAX_VEHICLES; cnt++ )
3037 {
3038 GROUP* const pGroup = GetGroup(gubVehicleMovementGroups[cnt]);
3039 if ( pGroup != NULL )
3040 {
3041 pGroup->fPersistant = TRUE;
3042 }
3043 }
3044
3045 fDoChange = TRUE;
3046 }
3047
3048 if ( fDoChange )
3049 {
3050 //Remove all empty groups
3051 FOR_EACH_GROUP_SAFE(i)
3052 {
3053 GROUP& g = *i;
3054 if (g.ubGroupSize == 0 && !g.fPersistant) RemoveGroup(g);
3055 }
3056 }
3057 }
3058
3059 //Determines if any particular group WILL be moving through a given sector given it's current
3060 //position in the route and the pGroup->ubMoveType must be ONE_WAY. If the group is currently
3061 //IN the sector, or just left the sector, it will return FALSE.
GroupWillMoveThroughSector(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY)3062 BOOLEAN GroupWillMoveThroughSector( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY )
3063 {
3064 WAYPOINT *wp;
3065 INT32 i, dx, dy;
3066 UINT8 ubOrigX, ubOrigY;
3067
3068 Assert( pGroup );
3069 AssertMsg( pGroup->ubMoveType == ONE_WAY, String( "GroupWillMoveThroughSector() -- Attempting to test group with an invalid move type. ubGroupID: %d, ubMoveType: %d, sector: %c%d -- KM:0",
3070 pGroup->ubGroupID, pGroup->ubMoveType, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX ) );
3071
3072 //Preserve the original sector values, as we will be temporarily modifying the group's ubSectorX/Y values
3073 //as we traverse the waypoints.
3074 ubOrigX = pGroup->ubSectorX;
3075 ubOrigY = pGroup->ubSectorY;
3076
3077 i = pGroup->ubNextWaypointID;
3078 wp = pGroup->pWaypoints;
3079
3080 if( !wp )
3081 { //This is a floating group!?
3082 return FALSE;
3083 }
3084 while( i-- )
3085 { //Traverse through the waypoint list to the next waypoint ID
3086 Assert( wp );
3087 wp = wp->next;
3088 }
3089 Assert( wp );
3090
3091
3092 while( wp )
3093 {
3094 while( pGroup->ubSectorX != wp->x || pGroup->ubSectorY != wp->y )
3095 {
3096 //We now have the correct waypoint.
3097 //Analyse the group and determine which direction it will move from the current sector.
3098 dx = wp->x - pGroup->ubSectorX;
3099 dy = wp->y - pGroup->ubSectorY;
3100 if( dx && dy )
3101 { //Can't move diagonally!
3102 SLOGA("GroupWillMoveThroughSector() -- Attempting to process waypoint in a diagonal direction from sector %c%d to sector %c%d for group at sector %c%d",
3103 pGroup->ubSectorY + 'A', pGroup->ubSectorX, wp->y + 'A' - 1, wp->x, ubOrigY + 'A' - 1, ubOrigX);
3104 pGroup->ubSectorX = ubOrigX;
3105 pGroup->ubSectorY = ubOrigY;
3106 return TRUE;
3107 }
3108 if( !dx && !dy ) //Can't move to position currently at!
3109 {
3110 SLOGA("GroupWillMoveThroughSector() -- Attempting to process same waypoint at %c%d for group at %c%d",
3111 wp->y + 'A' - 1, wp->x, ubOrigY + 'A' - 1, ubOrigX);
3112 pGroup->ubSectorX = ubOrigX;
3113 pGroup->ubSectorY = ubOrigY;
3114 return TRUE;
3115 }
3116 //Clip dx/dy value so that the move is for only one sector.
3117 if( dx >= 1 )
3118 {
3119 dx = 1;
3120 }
3121 else if( dy >= 1 )
3122 {
3123 dy = 1;
3124 }
3125 else if( dx <= -1 )
3126 {
3127 dx = -1;
3128 }
3129 else if( dy <= -1 )
3130 {
3131 dy = -1;
3132 }
3133 //Advance the sector value
3134 pGroup->ubSectorX = (UINT8)( dx + pGroup->ubSectorX );
3135 pGroup->ubSectorY = (UINT8)( dy + pGroup->ubSectorY );
3136 //Check to see if it the sector we are checking to see if this group will be moving through.
3137 if( pGroup->ubSectorX == ubSectorX && pGroup->ubSectorY == ubSectorY )
3138 {
3139 pGroup->ubSectorX = ubOrigX;
3140 pGroup->ubSectorY = ubOrigY;
3141 return TRUE;
3142 }
3143 }
3144 //Advance to the next waypoint.
3145 wp = wp->next;
3146 }
3147 pGroup->ubSectorX = ubOrigX;
3148 pGroup->ubSectorY = ubOrigY;
3149 return FALSE;
3150 }
3151
VehicleFuelRemaining(SOLDIERTYPE const & vs)3152 static INT16 VehicleFuelRemaining(SOLDIERTYPE const& vs)
3153 {
3154 Assert(vs.uiStatusFlags & SOLDIER_VEHICLE);
3155 return vs.sBreathRed;
3156 }
3157
3158
SpendVehicleFuel(SOLDIERTYPE & vs,INT16 const fuel_spent)3159 static void SpendVehicleFuel(SOLDIERTYPE& vs, INT16 const fuel_spent)
3160 {
3161 Assert(vs.uiStatusFlags & SOLDIER_VEHICLE);
3162 vs.sBreathRed = MAX(0, vs.sBreathRed - fuel_spent);
3163 vs.bBreath = (vs.sBreathRed + 99) / 100;
3164 }
3165
3166
AddFuelToVehicle(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pVehicle)3167 void AddFuelToVehicle(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pVehicle)
3168 {
3169 OBJECTTYPE *pItem;
3170 INT16 sFuelNeeded, sFuelAvailable, sFuelAdded;
3171 pItem = &pSoldier->inv[ HANDPOS ];
3172 if( pItem->usItem != GAS_CAN )
3173 {
3174 return;
3175 }
3176 //Soldier has gas can, so now add gas to vehicle while removing gas from the gas can.
3177 //A gas can with 100 status translate to 50% of a fillup.
3178 if( pVehicle->sBreathRed == 10000 )
3179 { //Message for vehicle full?
3180 return;
3181 }
3182 // pItem->bStatus is always present, no need to check for it
3183 //Fill 'er up.
3184 sFuelNeeded = 10000 - pVehicle->sBreathRed;
3185 sFuelAvailable = pItem->bStatus[0] * 50;
3186 sFuelAdded = MIN( sFuelNeeded, sFuelAvailable );
3187 //Add to vehicle
3188 pVehicle->sBreathRed += sFuelAdded;
3189 pVehicle->bBreath = (INT8)(pVehicle->sBreathRed / 100);
3190 //Subtract from item
3191 pItem->bStatus[0] = (INT8)(pItem->bStatus[0] - sFuelAdded / 50);
3192 if( !pItem->bStatus[0] )
3193 { //Gas can is empty, so toast the item.
3194 DeleteObj( pItem );
3195 }
3196 }
3197
3198
ReportVehicleOutOfGas(VEHICLETYPE const & v,UINT8 const x,UINT8 const y)3199 static void ReportVehicleOutOfGas(VEHICLETYPE const& v, UINT8 const x, UINT8 const y)
3200 {
3201 // Report that the vehicle that just arrived is out of gas
3202 ST::string str = st_format_printf(gzLateLocalizedString[STR_LATE_05], pVehicleStrings[v.ubVehicleType], y + 'A' - 1, x);
3203 DoScreenIndependantMessageBox(str, MSG_BOX_FLAG_OK, 0);
3204 }
3205
3206
SetLocationOfAllPlayerSoldiersInGroup(GROUP const & g,INT16 const x,INT16 const y,INT8 const z)3207 static void SetLocationOfAllPlayerSoldiersInGroup(GROUP const& g, INT16 const x, INT16 const y, INT8 const z)
3208 {
3209 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
3210 {
3211 if (!i->pSoldier) continue;
3212 SOLDIERTYPE& s = *i->pSoldier;
3213 s.sSectorX = x;
3214 s.sSectorY = y;
3215 s.bSectorZ = z;
3216 }
3217
3218 if (g.fVehicle)
3219 {
3220 VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
3221 v.sSectorX = x;
3222 v.sSectorY = y;
3223 v.sSectorZ = z;
3224
3225 if (!IsHelicopter(v))
3226 {
3227 SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
3228 /* These are apparently unnecessary, since vehicles are part of the
3229 * pPlayerList in a vehicle group. Oh well. */
3230 vs.sSectorX = x;
3231 vs.sSectorY = y;
3232 vs.bSectorZ = z;
3233 }
3234 }
3235 }
3236
3237
RandomizePatrolGroupLocation(GROUP * pGroup)3238 void RandomizePatrolGroupLocation( GROUP *pGroup )
3239 { //Make sure this is an enemy patrol group
3240 WAYPOINT *wp;
3241 UINT8 ubMaxWaypointID = 0;
3242 UINT8 ubTotalWaypoints;
3243 UINT8 ubChosen;
3244 UINT8 ubSectorID;
3245
3246 //return; //disabled for now
3247
3248 Assert( !pGroup->fPlayer );
3249 Assert( pGroup->ubMoveType == ENDTOEND_FORWARDS );
3250 Assert( pGroup->pEnemyGroup->ubIntention == PATROL );
3251
3252 //Search for the event, and kill it (if it exists)!
3253 DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
3254
3255 //count the group's waypoints
3256 wp = pGroup->pWaypoints;
3257 while( wp )
3258 {
3259 if( wp->next )
3260 {
3261 ubMaxWaypointID++;
3262 }
3263 wp = wp->next;
3264 }
3265 //double it (they go back and forth) -- it's using zero based indices, so you have to add one to get the number of actual
3266 //waypoints in one direction.
3267 ubTotalWaypoints = (UINT8)((ubMaxWaypointID) * 2);
3268
3269 //pick the waypoint they start at
3270 ubChosen = (UINT8)Random( ubTotalWaypoints );
3271
3272 if( ubChosen >= ubMaxWaypointID )
3273 { //They chose a waypoint going in the reverse direction, so translate it
3274 //to an actual waypointID and switch directions.
3275 pGroup->ubMoveType = ENDTOEND_BACKWARDS;
3276 pGroup->ubNextWaypointID = ubChosen - ubMaxWaypointID;
3277 ubChosen = pGroup->ubNextWaypointID + 1;
3278 }
3279 else
3280 {
3281 pGroup->ubMoveType = ENDTOEND_FORWARDS;
3282 pGroup->ubNextWaypointID = ubChosen + 1;
3283 }
3284
3285 //Traverse through the waypoint list again, to extract the location they are at.
3286 wp = pGroup->pWaypoints;
3287 while( wp && ubChosen )
3288 {
3289 ubChosen--;
3290 wp = wp->next;
3291 }
3292
3293 //logic error if this fails. We should have a null value for ubChosen
3294 Assert( !ubChosen );
3295 Assert( wp );
3296
3297 //Move the group to the location of this chosen waypoint.
3298 ubSectorID = (UINT8)SECTOR( wp->x, wp->y );
3299
3300 //Set up this global var to randomize the arrival time of the group from
3301 //1 minute to actual traverse time between the sectors.
3302 gfRandomizingPatrolGroup = TRUE;
3303
3304 SetEnemyGroupSector(*pGroup, ubSectorID);
3305 InitiateGroupMovementToNextSector( pGroup );
3306
3307 //Immediately turn off the flag once finished.
3308 gfRandomizingPatrolGroup = FALSE;
3309
3310 }
3311
3312
3313 //Whenever a player group arrives in a sector, and if bloodcats exist in the sector,
3314 //roll the dice to see if this will become an ambush random encounter.
TestForBloodcatAmbush(GROUP const * const pGroup)3315 static BOOLEAN TestForBloodcatAmbush(GROUP const* const pGroup)
3316 {
3317 SECTORINFO *pSector;
3318 INT32 iHoursElapsed;
3319 UINT8 ubSectorID;
3320 UINT8 ubChance;
3321 INT8 bDifficultyMaxCats;
3322 INT8 bProgressMaxCats;
3323 INT8 bNumMercMaxCats;
3324 BOOLEAN fAlreadyAmbushed = FALSE;
3325
3326 if( pGroup->ubSectorZ )
3327 { //no ambushes underground (no bloodcats either)
3328 return FALSE;
3329 }
3330
3331 ubSectorID = (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
3332 pSector = &SectorInfo[ ubSectorID ];
3333
3334 ubChance = 5 * gGameOptions.ubDifficultyLevel;
3335
3336 bool bIsLair = false, bIsArena = false;
3337 auto spawns = GCM->getBloodCatSpawnsOfSector( ubSectorID );
3338 if (spawns != NULL) {
3339 bIsLair = spawns->isLair; // SEC_I16
3340 bIsArena = spawns->isArena; // SEC_N5
3341 }
3342
3343 iHoursElapsed = (GetWorldTotalMin() - pSector->uiTimeCurrentSectorWasLastLoaded) / 60;
3344 if( bIsLair || bIsArena )
3345 { //These are special maps -- we use all placements.
3346 if( pSector->bBloodCats == -1 )
3347 {
3348 pSector->bBloodCats = pSector->bBloodCatPlacements;
3349 }
3350 else if( pSector->bBloodCats > 0 && pSector->bBloodCats < pSector->bBloodCatPlacements )
3351 { //Slowly have them recuperate if we haven't been here for a long time. The population will
3352 //come back up to the maximum if left long enough.
3353 INT32 iBloodCatDiff;
3354 iBloodCatDiff = pSector->bBloodCatPlacements - pSector->bBloodCats;
3355 pSector->bBloodCats += (INT8)MIN( iHoursElapsed / 18, iBloodCatDiff );
3356 }
3357 //Once 0, the bloodcats will never recupe.
3358 }
3359 else if( pSector->bBloodCats == -1 )
3360 { //If we haven't been ambushed by bloodcats yet...
3361 if( gfAutoAmbush || PreChance( ubChance ) )
3362 {
3363 //randomly choose from 5-8, 7-10, 9-12 bloodcats based on easy, normal, and hard, respectively
3364 bDifficultyMaxCats = (INT8)( Random( 4 ) + gGameOptions.ubDifficultyLevel*2 + 3 );
3365
3366 //maximum of 3 bloodcats or 1 for every 6%, 5%, 4% progress based on easy, normal, and hard, respectively
3367 bProgressMaxCats = (INT8)MAX( CurrentPlayerProgressPercentage() / (7 - gGameOptions.ubDifficultyLevel), 3 );
3368
3369 //make sure bloodcats don't outnumber mercs by a factor greater than 2
3370 bNumMercMaxCats = (INT8)(PlayerMercsInSector( pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ ) * 2);
3371
3372 //choose the lowest number of cats calculated by difficulty and progress.
3373 pSector->bBloodCats = (INT8)MIN( bDifficultyMaxCats, bProgressMaxCats );
3374
3375 if( gGameOptions.ubDifficultyLevel != DIF_LEVEL_HARD )
3376 { //if not hard difficulty, ensure cats never outnumber mercs by a factor of 2 (min 3 bloodcats)
3377 pSector->bBloodCats = (INT8)MIN( pSector->bBloodCats, bNumMercMaxCats );
3378 pSector->bBloodCats = (INT8)MAX( pSector->bBloodCats, 3 );
3379 }
3380
3381 //ensure that there aren't more bloodcats than placements
3382 pSector->bBloodCats = (INT8)MIN( pSector->bBloodCats, pSector->bBloodCatPlacements );
3383 }
3384 }
3385 else if( !bIsLair )
3386 {
3387 if( !gfAutoAmbush && PreChance( 95 ) )
3388 { //already ambushed here. But 5% chance of getting ambushed again!
3389 fAlreadyAmbushed = TRUE;
3390 }
3391 }
3392
3393 if( !fAlreadyAmbushed && !bIsArena && pSector->bBloodCats > 0 &&
3394 !pGroup->fVehicle && !NumEnemiesInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
3395 {
3396 if( !bIsLair || !gubFact[ FACT_PLAYER_KNOWS_ABOUT_BLOODCAT_LAIR ] )
3397 {
3398 gubEnemyEncounterCode = BLOODCAT_AMBUSH_CODE;
3399 }
3400 else
3401 {
3402 gubEnemyEncounterCode = ENTERING_BLOODCAT_LAIR_CODE;
3403 }
3404 return TRUE;
3405 }
3406 else
3407 {
3408 gubEnemyEncounterCode = NO_ENCOUNTER_CODE;
3409 return FALSE;
3410 }
3411 }
3412
3413
NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX,UINT8 ubSectorY)3414 static void NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX, UINT8 ubSectorY)
3415 {
3416 ST::string str;
3417 ST::string zTempString;
3418 if( gubEnemyEncounterCode == BLOODCAT_AMBUSH_CODE )
3419 {
3420 zTempString = GetSectorIDString(ubSectorX, ubSectorY, 0, TRUE);
3421 str = st_format_printf(pMapErrorString[ 12 ], zTempString);
3422 }
3423 else if( gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE )
3424 {
3425 str = pMapErrorString[ 13 ];
3426 }
3427
3428 if( guiCurrentScreen == MAP_SCREEN )
3429 { //Force render mapscreen (need to update the position of the group before the dialog appears.
3430 fMapPanelDirty = TRUE;
3431 MapScreenHandle();
3432 InvalidateScreen();
3433 RefreshScreen();
3434 }
3435
3436 gfUsePersistantPBI = TRUE;
3437 DoScreenIndependantMessageBox( str, MSG_BOX_FLAG_OK, TriggerPrebattleInterface );
3438 }
3439
3440
PlaceGroupInSector(GROUP & g,INT16 const prev_x,INT16 const prev_y,INT16 const next_x,INT16 const next_y,INT8 const z,bool const check_for_battle)3441 void PlaceGroupInSector(GROUP& g, INT16 const prev_x, INT16 const prev_y, INT16 const next_x, INT16 const next_y, INT8 const z, bool const check_for_battle)
3442 {
3443 ClearMercPathsAndWaypointsForAllInGroup(g);
3444 // Change where they are and where they're going
3445 SetGroupPrevSectors(g, prev_x, prev_y);
3446 SetGroupSectorValue(prev_x, prev_y, z, g);
3447 SetGroupNextSectorValue(next_x, next_y, g);
3448 // Call arrive event
3449 GroupArrivedAtSector(g, check_for_battle, FALSE);
3450 }
3451
setArrivalTime(UINT32 arrival_time)3452 void GROUP::setArrivalTime(UINT32 arrival_time)
3453 {
3454 /* Please centralize all changes to the arrival times of groups through here,
3455 * especially the helicopter group! */
3456
3457 /* If this group is the helicopter group, we have to make sure that its
3458 * arrival time is never greater than the sum of the current time and its
3459 * traverse time, because those 3 values are used to plot its map position!
3460 * Because of this the chopper groups must NEVER be delayed for any reason -
3461 * it gets excluded from simultaneous arrival logic */
3462
3463 /* Also note that non-chopper groups can currently be delayed such that this
3464 * assetion would fail - enemy groups by DelayEnemyGroupsIfPathsCross(), and
3465 * player groups via PrepareGroupsForSimultaneousArrival(). So we skip the
3466 * assert. */
3467
3468 if (IsGroupTheHelicopterGroup(*this))
3469 {
3470 // Make sure it's valid (NOTE: the correct traverse time must be set first!)
3471 UINT32 const now = GetWorldTotalMin();
3472 if (arrival_time > now + this->uiTraverseTime)
3473 {
3474 AssertMsg(FALSE, String( "setArrivalTime: Setting invalid arrival time %d for group %d, WorldTime = %d, TraverseTime = %d", arrival_time, this->ubGroupID, now, this->uiTraverseTime));
3475 // Fix it if assertions are disabled
3476 arrival_time = now + this->uiTraverseTime;
3477 }
3478 }
3479
3480 this->uiArrivalTime = arrival_time;
3481 }
3482
3483
3484 // Non-persistent groups should be simply removed instead
CancelEmptyPersistentGroupMovement(GROUP & g)3485 static void CancelEmptyPersistentGroupMovement(GROUP& g)
3486 {
3487 Assert(g.ubGroupSize == 0);
3488 Assert(g.fPersistant);
3489
3490 /* Don't do this for vehicle groups - the chopper can keep flying empty, while
3491 * other vehicles still exist and teleport to nearest sector instead */
3492 if (g.fVehicle) return;
3493
3494 // Prevent it from arriving empty
3495 DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
3496
3497 RemoveGroupWaypoints(g);
3498 g.uiTraverseTime = 0;
3499 g.setArrivalTime(0);
3500 g.fBetweenSectors = FALSE;
3501 g.ubPrevX = 0;
3502 g.ubPrevY = 0;
3503 g.ubSectorX = 0;
3504 g.ubSectorY = 0;
3505 g.ubNextX = 0;
3506 g.ubNextY = 0;
3507 }
3508
3509
3510 static bool HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP&);
3511
3512
3513 // look for NPCs to stop for, anyone is too tired to keep going, if all OK rebuild waypoints & continue movement
PlayerGroupArrivedSafelyInSector(GROUP & g,BOOLEAN const fCheckForNPCs)3514 void PlayerGroupArrivedSafelyInSector(GROUP& g, BOOLEAN const fCheckForNPCs)
3515 {
3516 BOOLEAN fPlayerPrompted = FALSE;
3517
3518 Assert(g.fPlayer);
3519
3520 // if we haven't already checked for NPCs, and the group isn't empty
3521 if (fCheckForNPCs && HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(g))
3522 {
3523 // wait for player to answer/confirm prompt before doing anything else
3524 fPlayerPrompted = TRUE;
3525 }
3526
3527 // if we're not prompting the player
3528 if ( !fPlayerPrompted )
3529 {
3530 // and we're not at the end of our road
3531 if (!GroupAtFinalDestination(&g))
3532 {
3533 if (AnyMercInGroupCantContinueMoving(g))
3534 {
3535 // stop: clear their strategic movement (mercpaths and waypoints)
3536 ClearMercPathsAndWaypointsForAllInGroup(g);
3537
3538 // NOTE: Of course, it would be better if they continued onwards once everyone was ready to go again, in which
3539 // case we'd want to preserve the plotted path, but since the player can mess with the squads, etc.
3540 // in the mean-time, that just seemed to risky to try to support. They could get into a fight and be too
3541 // injured to move, etc. Basically, we'd have run a complete CanCharacterMoveInStrategic(0 check on all of them.
3542 // It's a wish list task for AM...
3543
3544 // stop time so player can react if group was already on the move and suddenly halts
3545 StopTimeCompression();
3546 }
3547 else
3548 {
3549 // continue onwards: rebuild way points, initiate movement
3550 RebuildWayPointsForGroupPath(GetGroupMercPathPtr(g), g);
3551 }
3552 }
3553 }
3554 }
3555
3556
3557 static void HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue);
3558
3559
HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP & g)3560 static bool HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP& g)
3561 {
3562 Assert(g.fPlayer);
3563
3564 // Nobody in the group (perfectly legal with the chopper).
3565 if (!g.pPlayerList) return false;
3566
3567 // Chopper doesn't stop for NPCs.
3568 if (IsGroupTheHelicopterGroup(g)) return false;
3569
3570 /* If we're already in the middle of a prompt (possible with simultaneously
3571 * group arrivals!), don't try to prompt again. */
3572 if (gpGroupPrompting) return false;
3573
3574 INT16 const x = g.ubSectorX;
3575 INT16 const y = g.ubSectorY;
3576 INT8 const z = g.ubSectorZ;
3577
3578 // Check for profiled NPCs in sector.
3579 if (!WildernessSectorWithAllProfiledNPCsNotSpokenWith(x, y, z)) return false;
3580
3581 // Store the group pointer for use by the callback function.
3582 gpGroupPrompting = &g;
3583
3584 // Build string for squad.
3585 ST::string sector_name = GetSectorIDString(x, y, z, FALSE);
3586 ST::string msg = st_format_printf(pLandMarkInSectorString, g.pPlayerList->pSoldier->bAssignment + 1, sector_name);
3587
3588 MessageBoxFlags const flags =
3589 GroupAtFinalDestination(&g) ? MSG_BOX_FLAG_OK :
3590 MSG_BOX_FLAG_CONTINUESTOP;
3591 DoScreenIndependantMessageBox(msg, flags, HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback);
3592 // wait, we're prompting the player
3593 return true;
3594 }
3595
3596
WildernessSectorWithAllProfiledNPCsNotSpokenWith(INT16 const x,INT16 const y,INT8 const z)3597 bool WildernessSectorWithAllProfiledNPCsNotSpokenWith(INT16 const x, INT16 const y, INT8 const z)
3598 {
3599 bool found_somebody = false;
3600
3601 // Don't do this for underground sectors.
3602 if (z != 0) return false;
3603
3604 // Skip towns/pseudo-towns (anything that shows up on the map as being special).
3605 if (StrategicMap[CALCULATE_STRATEGIC_INDEX(x, y)].bNameId != BLANK_SECTOR) return false;
3606
3607 // Skip SAM sites.
3608 if (IsThisSectorASAMSector(x, y, z)) return false;
3609
3610 for (const MercProfile* profile : GCM->listMercProfiles())
3611 {
3612 // Skip player mercs
3613 if (profile->isPlayerMerc()) continue;
3614
3615 MERCPROFILESTRUCT const& p = profile->getStruct();
3616
3617 // Skip dead.
3618 if (p.bMercStatus == MERC_IS_DEAD) continue;
3619 if (p.bLife <= 0) continue;
3620 // Skip vehicles.
3621 if (profile->isVehicle()) continue;
3622
3623 // In this sector?
3624 if (p.sSectorX != x || p.sSectorY != y || p.bSectorZ != z) continue;
3625
3626 if (p.ubLastDateSpokenTo != 0 ||
3627 p.ubMiscFlags & (PROFILE_MISC_FLAG_RECRUITED | PROFILE_MISC_FLAG_EPCACTIVE))
3628 { /* Already spoke to this guy, don't prompt about this sector again,
3629 * regardless of status of other NPCs here. Although Hamous wanders
3630 * around, he never shares the same wilderness sector as other important
3631 * NPCs. */
3632 return false;
3633 }
3634 /* We haven't talked to him yet and he's not currently recruired/escorted by
3635 * player. This is a guy we need to stop for. */
3636 found_somebody = true;
3637 }
3638 return found_somebody;
3639 }
3640
3641
HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue const exit_value)3642 static void HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue const exit_value)
3643 {
3644 Assert(gpGroupPrompting);
3645 GROUP& g = *gpGroupPrompting;
3646 gpGroupPrompting = 0;
3647 switch (exit_value)
3648 {
3649 case MSG_BOX_RETURN_YES:
3650 case MSG_BOX_RETURN_OK:
3651 // NPCs now checked, continue moving if appropriate
3652 PlayerGroupArrivedSafelyInSector(g, FALSE);
3653 break;
3654
3655 case MSG_BOX_RETURN_NO:
3656 // Stop here
3657 ClearMercPathsAndWaypointsForAllInGroup(g);
3658 ChangeSelectedMapSector(g.ubSectorX, g.ubSectorY, g.ubSectorZ);
3659 StopTimeCompression();
3660 break;
3661 default:
3662 break;
3663 }
3664 fMapPanelDirty = TRUE;
3665 fMapScreenBottomDirty = TRUE;
3666 }
3667
3668
DoesPlayerExistInPGroup(GROUP const & g,SOLDIERTYPE const & s)3669 bool DoesPlayerExistInPGroup(GROUP const& g, SOLDIERTYPE const& s)
3670 {
3671 CFOR_EACH_PLAYER_IN_GROUP(curr, &g)
3672 {
3673 if (curr->pSoldier == &s) return true;
3674 }
3675 return false;
3676 }
3677
3678
GroupHasInTransitDeadOrPOWMercs(GROUP const & g)3679 bool GroupHasInTransitDeadOrPOWMercs(GROUP const& g)
3680 {
3681 CFOR_EACH_PLAYER_IN_GROUP(i, &g)
3682 {
3683 if (!i->pSoldier) continue;
3684 switch (i->pSoldier->bAssignment)
3685 {
3686 case IN_TRANSIT:
3687 case ASSIGNMENT_POW:
3688 case ASSIGNMENT_DEAD:
3689 return true;
3690 }
3691 }
3692 return false;
3693 }
3694
3695