1 #include "LoadSaveData.h"
2 #include "Timer_Control.h"
3 #include "Types.h"
4 #include "FileMan.h"
5 #include "Soldier_Control.h"
6 #include "Overhead.h"
7 #include "Animation_Control.h"
8 #include "Points.h"
9 #include "OppList.h"
10 #include "Sound_Control.h"
11 #include "Interface.h"
12 #include "Isometric_Utils.h"
13 #include "Font_Control.h"
14 #include "AI.h"
15 #include "Message.h"
16 #include "Text.h"
17 #include "TeamTurns.h"
18 #include "Smell.h"
19 #include "Game_Clock.h"
20 #include "GameSettings.h"
21 #include "Soldier_Functions.h"
22 #include "Queen_Command.h"
23 #include "PathAI.h"
24 #include "Strategic_Turns.h"
25 #include "Lighting.h"
26 #include "Environment.h"
27 #include "Explosion_Control.h"
28 #include "Dialogue_Control.h"
29 #include "Soldier_Profile_Type.h"
30 #include "SmokeEffects.h"
31 #include "LightEffects.h"
32 #include "Meanwhile.h"
33 #include "SkillCheck.h"
34 #include "AIInternals.h"
35 #include "AIList.h"
36 #include "RenderWorld.h"
37 #include "Rotting_Corpses.h"
38 #include "Squads.h"
39 #include "Soldier_Macros.h"
40 #include "NPC.h"
41 #include "Debug.h"
42 #include "Items.h"
43 #include "Logger.h"
44
45 // for that single policy check :|
46 #include "GamePolicy.h"
47 #include "ContentManager.h"
48 #include "GameInstance.h"
49
50 #include <string_theory/format>
51 #include <string_theory/string>
52
53 #include <algorithm>
54 #include <iterator>
55
56 static SOLDIERTYPE* gOutOfTurnOrder[MAXMERCS];
57 UINT8 gubOutOfTurnPersons = 0;
58
59
LatestInterruptGuy(void)60 static inline SOLDIERTYPE* LatestInterruptGuy(void)
61 {
62 return gOutOfTurnOrder[gubOutOfTurnPersons];
63 }
64
65
66 #define REMOVE_LATEST_INTERRUPT_GUY() (DeleteFromIntList( (UINT8) (gubOutOfTurnPersons), TRUE ))
67 #define INTERRUPTS_OVER (gubOutOfTurnPersons == 1)
68
69 BOOLEAN gfHiddenInterrupt = FALSE;
70 static SOLDIERTYPE* gLastInterruptedGuy = NULL;
71
72 extern SightFlags gubSightFlags;
73
74
75 #define MIN_APS_TO_INTERRUPT 4
76
ClearIntList(void)77 void ClearIntList( void )
78 {
79 std::fill(std::begin(gOutOfTurnOrder), std::end(gOutOfTurnOrder), nullptr);
80 gubOutOfTurnPersons = 0;
81 }
82
83
StartPlayerTeamTurn(BOOLEAN fDoBattleSnd,BOOLEAN fEnteringCombatMode)84 void StartPlayerTeamTurn( BOOLEAN fDoBattleSnd, BOOLEAN fEnteringCombatMode )
85 {
86 // Start the turn of player charactors
87
88 //
89 // PATCH 1.06:
90 //
91 // make sure set properly in gTacticalStatus:
92 gTacticalStatus.ubCurrentTeam = OUR_TEAM;
93
94 InitPlayerUIBar( FALSE );
95
96
97 // Are we in combat already?
98 if ( gTacticalStatus.uiFlags & INCOMBAT )
99 {
100 PlayJA2Sample(ENDTURN_1, MIDVOLUME, 1, MIDDLEPAN);
101 }
102
103 // Check for victory conditions
104
105 // Are we in combat already?
106 if ( gTacticalStatus.uiFlags & INCOMBAT )
107 {
108 SOLDIERTYPE* sel = GetSelectedMan();
109 if (sel != NULL)
110 {
111 // Check if this guy is able to be selected....
112 if (sel->bLife < OKLIFE)
113 {
114 SelectNextAvailSoldier(sel);
115 sel = GetSelectedMan();
116 }
117
118 // Slide to selected guy...
119 if (sel != NULL)
120 {
121 SlideTo(sel, SETLOCATOR);
122
123 // Say ATTENTION SOUND...
124 if (fDoBattleSnd) DoMercBattleSound(sel, BATTLE_SOUND_ATTN1);
125
126 if ( gsInterfaceLevel == 1 )
127 {
128 gTacticalStatus.uiFlags |= SHOW_ALL_ROOFS;
129 InvalidateWorldRedundency( );
130 SetRenderFlags(RENDER_FLAG_FULL);
131 ErasePath();
132 }
133 }
134 }
135 }
136
137 // Dirty panel interface!
138 fInterfacePanelDirty = DIRTYLEVEL2;
139
140 // Adjust time now!
141 UpdateClock( );
142
143 if ( !fEnteringCombatMode )
144 {
145 CheckForEndOfCombatMode( TRUE );
146 }
147
148
149 // Signal UI done enemy's turn
150 guiPendingOverrideEvent = LU_ENDUILOCK;
151
152 // ATE: Reset killed on attack variable.. this is because sometimes timing is such
153 /// that a baddie can die and still maintain it's attacker ID
154 gTacticalStatus.fKilledEnemyOnAttack = FALSE;
155
156 // Save if we are in Dead is Dead mode
157 DoDeadIsDeadSaveIfNecessary();
158
159 HandleTacticalUI( );
160 }
161
162
FreezeInterfaceForEnemyTurn(void)163 static void FreezeInterfaceForEnemyTurn(void)
164 {
165 // Reset flags
166 gfPlotNewMovement = TRUE;
167
168 // Erase path
169 ErasePath();
170
171 // Setup locked UI
172 guiPendingOverrideEvent = LU_BEGINUILOCK;
173
174 // Remove any UI messages!
175 if (g_ui_message_overlay != NULL)
176 {
177 EndUIMessage( );
178 }
179 }
180
181
182 static void EndInterrupt(BOOLEAN fMarkInterruptOccurred);
183
184
EndTurn(UINT8 ubNextTeam)185 void EndTurn( UINT8 ubNextTeam )
186 {
187 //Check for enemy pooling (add enemies if there happens to be more than the max in the
188 //current battle. If one or more slots have freed up, we can add them now.
189
190 // Save if we are in Dead is Dead mode
191 DoDeadIsDeadSaveIfNecessary();
192
193 /*
194 if ( CheckForEndOfCombatMode( FALSE ) )
195 {
196 return;
197 }
198 */
199
200 if (INTERRUPT_QUEUED)
201 {
202 EndInterrupt( FALSE );
203 }
204 else
205 {
206 AddPossiblePendingEnemiesToBattle();
207
208 //InitEnemyUIBar( );
209
210 FreezeInterfaceForEnemyTurn();
211
212 // Loop through all mercs and set to moved
213 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
214 {
215 s->bMoved = TRUE;
216 // Cancel merc movement if continue path was not used
217 const INT16 sAPCost = PtsToMoveDirection(s, s->ubPathingData[0]);
218 if (EnoughPoints(s, sAPCost, 0, FALSE))
219 {
220 s->sFinalDestination=s->sGridNo;
221 s->fNoAPToFinishMove = 0;
222 }
223 }
224
225 gTacticalStatus.ubCurrentTeam = ubNextTeam;
226
227 BeginTeamTurn( gTacticalStatus.ubCurrentTeam );
228
229 BetweenTurnsVisibilityAdjustments();
230 }
231 }
232
EndAITurn(void)233 void EndAITurn( void )
234 {
235 if (INTERRUPT_QUEUED)
236 {
237 EndInterrupt( FALSE );
238 }
239 else
240 {
241 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
242 {
243 s->bMoved = TRUE;
244 // record old life value... for creature AI; the human AI might
245 // want to use this too at some point
246 s->bOldLife = s->bLife;
247 }
248
249 gTacticalStatus.ubCurrentTeam++;
250 BeginTeamTurn( gTacticalStatus.ubCurrentTeam );
251 }
252 }
253
EndAllAITurns(void)254 void EndAllAITurns( void )
255 {
256 // warp turn to the player's turn
257
258 if (INTERRUPT_QUEUED)
259 {
260 EndInterrupt( FALSE );
261 }
262
263 if ( gTacticalStatus.ubCurrentTeam != OUR_TEAM )
264 {
265 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
266 {
267 s->bMoved = TRUE;
268 s->uiStatusFlags &= ~SOLDIER_UNDERAICONTROL;
269 // record old life value... for creature AI; the human AI might
270 // want to use this too at some point
271 s->bOldLife = s->bLife;
272 }
273
274 gTacticalStatus.ubCurrentTeam = OUR_TEAM;
275 //BeginTeamTurn( gTacticalStatus.ubCurrentTeam );
276 }
277 }
278
279
EndTurnEvents(void)280 static void EndTurnEvents(void)
281 {
282 // HANDLE END OF TURN EVENTS
283 // handle team services like healing
284 HandleTeamServices( OUR_TEAM );
285 // handle smell and blood decay
286 DecaySmells();
287 // decay bomb timers and maybe set some off!
288 DecayBombTimers();
289
290 DecaySmokeEffects( GetWorldTotalSeconds( ) );
291 DecayLightEffects( GetWorldTotalSeconds( ) );
292
293 // decay AI warning values from corpses
294 DecayRottingCorpseAIWarnings();
295 }
296
297
BeginTeamTurn(UINT8 ubTeam)298 void BeginTeamTurn( UINT8 ubTeam )
299 {
300 while( 1 )
301 {
302 if ( ubTeam > LAST_TEAM )
303 {
304 // End turn!!
305 ubTeam = OUR_TEAM;
306 gTacticalStatus.ubCurrentTeam = OUR_TEAM;
307 EndTurnEvents();
308
309 }
310 else if (!IsTeamActive(ubTeam))
311 {
312 // inactive team, skip to the next one
313 ubTeam++;
314 gTacticalStatus.ubCurrentTeam++;
315 // skip back to the top, as we are processing another team now.
316 continue;
317 }
318
319
320 BeginLoggingForBleedMeToos( TRUE );
321
322 // decay team's public opplist
323 DecayPublicOpplist( ubTeam );
324
325 FOR_EACH_IN_TEAM(i, ubTeam)
326 {
327 SOLDIERTYPE& s = *i;
328 if (s.bLife <= 0) continue;
329 // decay personal opplist, and refresh APs and BPs
330 EVENT_BeginMercTurn(s);
331 }
332
333 if (gTacticalStatus.bBoxingState == LOST_ROUND || gTacticalStatus.bBoxingState == WON_ROUND || gTacticalStatus.bBoxingState == DISQUALIFIED )
334 {
335 // we have no business being in here any more!
336 return;
337 }
338
339 BeginLoggingForBleedMeToos( FALSE );
340
341
342
343 if (ubTeam == OUR_TEAM )
344 {
345 // ATE: Check if we are still in a valid battle...
346 // ( they could have blead to death above )
347 if ( ( gTacticalStatus.uiFlags & INCOMBAT ) )
348 {
349 StartPlayerTeamTurn( TRUE, FALSE );
350 }
351 break;
352 }
353 else
354 {
355 // Set First enemy merc to AI control
356 if ( BuildAIListForTeam( ubTeam ) )
357 {
358 SOLDIERTYPE* const s = RemoveFirstAIListEntry();
359 if (s != NULL)
360 {
361 // Dirty panel interface!
362 fInterfacePanelDirty = DIRTYLEVEL2;
363 AddTopMessage(COMPUTER_TURN_MESSAGE);
364 StartNPCAI(*s);
365 return;
366 }
367 }
368
369 // This team is dead/inactive/being skipped in boxing
370 // skip back to the top to process the next team
371 ubTeam++;
372 gTacticalStatus.ubCurrentTeam++;
373 }
374 }
375 }
376
DisplayHiddenInterrupt(SOLDIERTYPE * pSoldier)377 void DisplayHiddenInterrupt( SOLDIERTYPE * pSoldier )
378 {
379 // If the AI got an interrupt but this has been hidden from the player until this point,
380 // this code will display the interrupt
381
382 if (!gfHiddenInterrupt)
383 {
384 return;
385 }
386
387 if (pSoldier->bVisible != -1) SlideTo(pSoldier, SETLOCATOR);
388
389 guiPendingOverrideEvent = LU_BEGINUILOCK;
390
391 // Dirty panel interface!
392 fInterfacePanelDirty = DIRTYLEVEL2;
393
394 // Erase path!
395 ErasePath();
396
397 // Reset flags
398 gfPlotNewMovement = TRUE;
399
400 // Stop our guy....
401 SOLDIERTYPE* const latest = LatestInterruptGuy();
402 AdjustNoAPToFinishMove(latest, TRUE);
403 // Stop him from going to prone position if doing a turn while prone
404 latest->fTurningFromPronePosition = FALSE;
405
406 // get rid of any old overlay message
407 const MESSAGE_TYPES msg =
408 pSoldier->bTeam == MILITIA_TEAM ? MILITIA_INTERRUPT_MESSAGE:
409 COMPUTER_INTERRUPT_MESSAGE;
410 AddTopMessage(msg);
411
412 gfHiddenInterrupt = FALSE;
413 }
414
DisplayHiddenTurnbased(SOLDIERTYPE * pActingSoldier)415 void DisplayHiddenTurnbased( SOLDIERTYPE * pActingSoldier )
416 {
417 // This code should put the game in turn-based and give control to the AI-controlled soldier
418 // whose pointer has been passed in as an argument (we were in non-combat and the AI is doing
419 // something visible, i.e. making an attack)
420
421 if ( AreInMeanwhile( ) )
422 {
423 return;
424 }
425
426 if (gTacticalStatus.uiFlags & INCOMBAT)
427 {
428 // pointless call here; do nothing
429 return;
430 }
431
432 // Enter combat mode starting with this side's turn
433 gTacticalStatus.ubCurrentTeam = pActingSoldier->bTeam;
434
435 CommonEnterCombatModeCode( );
436
437 SetSoldierAsUnderAiControl( pActingSoldier );
438 SLOGD("Giving AI control to %d", pActingSoldier->ubID);
439 pActingSoldier->fTurnInProgress = TRUE;
440 gTacticalStatus.uiTimeSinceMercAIStart = GetJA2Clock();
441
442 if ( gTacticalStatus.ubTopMessageType != COMPUTER_TURN_MESSAGE)
443 {
444 // Dirty panel interface!
445 fInterfacePanelDirty = DIRTYLEVEL2;
446 AddTopMessage(COMPUTER_TURN_MESSAGE);
447 }
448
449 // freeze the user's interface
450 FreezeInterfaceForEnemyTurn();
451 }
452
453
EveryoneInInterruptListOnSameTeam(void)454 static BOOLEAN EveryoneInInterruptListOnSameTeam(void)
455 {
456 UINT8 ubLoop;
457 UINT8 ubTeam = 255;
458
459 for (ubLoop = 1; ubLoop <= gubOutOfTurnPersons; ubLoop++)
460 {
461 if ( ubTeam == 255 )
462 {
463 ubTeam = gOutOfTurnOrder[ubLoop]->bTeam;
464 }
465 else
466 {
467 if (gOutOfTurnOrder[ubLoop]->bTeam != ubTeam)
468 {
469 return( FALSE );
470 }
471 }
472 }
473 return( TRUE );
474 }
475
476
SayCloseCallQuotes(void)477 void SayCloseCallQuotes(void)
478 {
479 // report any close call quotes for us here
480 FOR_EACH_IN_TEAM(s, OUR_TEAM)
481 {
482 if (OkControllableMerc(s) &&
483 s->fCloseCall &&
484 s->bNumHitsThisTurn == 0 &&
485 !(s->usQuoteSaidExtFlags & SOLDIER_QUOTE_SAID_EXT_CLOSE_CALL) &&
486 Random(3) == 0)
487 {
488 // say close call quote!
489 TacticalCharacterDialogue(s, QUOTE_CLOSE_CALL);
490 s->usQuoteSaidExtFlags |= SOLDIER_QUOTE_SAID_EXT_CLOSE_CALL;
491 }
492 s->fCloseCall = FALSE;
493 }
494 }
495
496
497 static void DeleteFromIntList(UINT8 ubIndex, BOOLEAN fCommunicate);
498
499
StartInterrupt(void)500 static void StartInterrupt(void)
501 {
502 SOLDIERTYPE *first_interrupter = LatestInterruptGuy();
503 const INT8 bTeam = first_interrupter->bTeam;
504 SOLDIERTYPE *Interrupter = first_interrupter;
505
506 // display everyone on int queue!
507 for (INT32 cnt = gubOutOfTurnPersons; cnt > 0; --cnt)
508 {
509 SLOGD("STARTINT: Q position %d: %d", cnt, gOutOfTurnOrder[cnt]->ubID);
510 }
511
512 gTacticalStatus.fInterruptOccurred = TRUE;
513
514 FOR_EACH_SOLDIER(s)
515 {
516 s->bMovedPriorToInterrupt = s->bMoved;
517 s->bMoved = TRUE;
518 }
519
520 if (first_interrupter->bTeam == OUR_TEAM)
521 {
522 // start interrupts for everyone on our side at once
523 ST::string sTemp;
524 UINT8 ubInterrupters = 0;
525
526 // build string for display of who gets interrupt
527 while( 1 )
528 {
529 Interrupter->bMoved = FALSE;
530 SLOGD("INTERRUPT: popping %d off of the interrupt queue", Interrupter->ubID);
531
532 REMOVE_LATEST_INTERRUPT_GUY();
533 // now LatestInterruptGuy() is the guy before the previous
534 Interrupter = LatestInterruptGuy();
535
536 if (Interrupter == NULL) // previously emptied slot!
537 {
538 continue;
539 }
540 else if (Interrupter->bTeam != bTeam)
541 {
542 break;
543 }
544 }
545
546 sTemp = g_langRes->Message[ STR_INTERRUPT_FOR ];
547
548 // build string in separate loop here, want to linearly process squads...
549 for (INT32 iSquad = 0; iSquad < NUMBER_OF_SQUADS; ++iSquad)
550 {
551 FOR_EACH_IN_SQUAD(i, iSquad)
552 {
553 SOLDIERTYPE const* const s = *i;
554 if (!s->bActive) continue;
555 if (!s->bInSector) continue;
556 if (s->bMoved) continue;
557 // then this guy got an interrupt...
558 ubInterrupters++;
559 if ( ubInterrupters > 6 )
560 {
561 // flush... display string, then clear it (we could have 20 names!)
562 // add comma to end, we know we have another person after this...
563 sTemp += ", ";
564 ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, sTemp );
565 sTemp = ST::null;
566 ubInterrupters = 1;
567 }
568
569 if ( ubInterrupters > 1 )
570 {
571 sTemp += ", ";
572 }
573 sTemp += s->name;
574 }
575 }
576
577 SLOGD("INTERRUPT: starting interrupt for %d", first_interrupter->ubID);
578
579 // Select guy....
580 SelectSoldier(first_interrupter, SELSOLDIER_ACKNOWLEDGE | SELSOLDIER_FORCE_RESELECT);
581
582 // ATE; Slide to guy who got interrupted!
583 SlideTo(gLastInterruptedGuy, SETLOCATOR);
584
585 // Dirty panel interface!
586 fInterfacePanelDirty = DIRTYLEVEL2;
587 gTacticalStatus.ubCurrentTeam = first_interrupter->bTeam;
588
589 // Signal UI done enemy's turn
590 guiPendingOverrideEvent = LU_ENDUILOCK;
591 HandleTacticalUI( );
592
593 InitPlayerUIBar( TRUE );
594 //AddTopMessage(PLAYER_INTERRUPT_MESSAGE);
595
596 PlayJA2Sample(ENDTURN_1, MIDVOLUME, 1, MIDDLEPAN);
597
598 SayCloseCallQuotes();
599 }
600 else
601 {
602 // start interrupts for everyone on that side at once... and start AI with the lowest # guy
603
604 // what we do is set everyone to moved except for people with interrupts at the moment
605 /*
606 FOR_EACH_IN_TEAM(s, first_interrupter->bTeam)
607 {
608 s->bMovedPriorToInterrupt = s->bMoved;
609 s->bMoved = TRUE;
610 }
611 */
612
613 while( 1 )
614 {
615 Interrupter->bMoved = FALSE;
616
617 SLOGD("INTERRUPT: popping %d off of the interrupt queue", Interrupter->ubID);
618
619 REMOVE_LATEST_INTERRUPT_GUY();
620 // now LatestInterruptGuy() is the guy before the previous
621 Interrupter = LatestInterruptGuy();
622 if (Interrupter == NULL) // previously emptied slot!
623 {
624 continue;
625 }
626 else if (Interrupter->bTeam != bTeam)
627 {
628 break;
629 }
630 else if (Interrupter->ubID < first_interrupter->ubID)
631 {
632 first_interrupter = Interrupter;
633 }
634 }
635
636
637 // here we have to rebuilt the AI list!
638 BuildAIListForTeam( bTeam );
639
640 // set to the new first interrupter
641 SOLDIERTYPE* const pSoldier = RemoveFirstAIListEntry();
642
643 //if ( gTacticalStatus.ubCurrentTeam == OUR_TEAM)
644 if ( pSoldier->bTeam != OUR_TEAM )
645 {
646 // we're being interrupted by the computer!
647 // we delay displaying any interrupt message until the computer
648 // does something...
649 gfHiddenInterrupt = TRUE;
650 gTacticalStatus.fUnLockUIAfterHiddenInterrupt = FALSE;
651 }
652 // otherwise it's the AI interrupting another AI team
653
654 gTacticalStatus.ubCurrentTeam = pSoldier->bTeam;
655
656 SLOGD("Interrupt ( could be hidden )" );
657
658 StartNPCAI(*pSoldier);
659 }
660
661 if ( !gfHiddenInterrupt )
662 {
663 // Stop this guy....
664 SOLDIERTYPE* const latest = LatestInterruptGuy();
665 AdjustNoAPToFinishMove(latest, TRUE);
666 latest->fTurningFromPronePosition = FALSE;
667 }
668 }
669
670
EndInterrupt(BOOLEAN fMarkInterruptOccurred)671 static void EndInterrupt(BOOLEAN fMarkInterruptOccurred)
672 {
673 BOOLEAN fFound;
674 UINT8 ubMinAPsToAttack;
675
676 for (INT32 cnt = gubOutOfTurnPersons; cnt > 0; --cnt)
677 {
678 SLOGD("ENDINT: Q position %d: %d", cnt, gOutOfTurnOrder[cnt]->ubID);
679 }
680
681 // ATE: OK, now if this all happended on one frame, we may not have to stop
682 // guy from walking... so set this flag to false if so...
683 if ( fMarkInterruptOccurred )
684 {
685 // flag as true if an int occurs which ends an interrupt (int loop)
686 gTacticalStatus.fInterruptOccurred = TRUE;
687 }
688 else
689 {
690 gTacticalStatus.fInterruptOccurred = FALSE;
691 }
692
693 // Loop through all mercs and see if any passed on this interrupt
694 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
695 {
696 if (s->bInSector && !s->bMoved && s->bActionPoints == s->bIntStartAPs)
697 {
698 ubMinAPsToAttack = MinAPsToAttack(s, s->sLastTarget, FALSE);
699 if (0 < ubMinAPsToAttack && ubMinAPsToAttack <= s->bActionPoints)
700 {
701 s->bPassedLastInterrupt = TRUE;
702 }
703 }
704 }
705
706 if ( !EveryoneInInterruptListOnSameTeam() )
707 {
708 gfHiddenInterrupt = FALSE;
709
710 // resume interrupted interrupt
711 StartInterrupt();
712 }
713 else
714 {
715 SOLDIERTYPE* const interrupted = LatestInterruptGuy();
716 SLOGD("INTERRUPT: interrupt over, %d's team regains control", interrupted->ubID);
717
718 FOR_EACH_SOLDIER(s)
719 {
720 // AI guys only here...
721 if (s->bActionPoints == 0)
722 {
723 s->bMoved = TRUE;
724 }
725 else if (s->bTeam != OUR_TEAM && s->bNewSituation == IS_NEW_SITUATION)
726 {
727 s->bMoved = FALSE;
728 }
729 else
730 {
731 s->bMoved = s->bMovedPriorToInterrupt;
732 }
733 }
734
735 // change team
736 gTacticalStatus.ubCurrentTeam = interrupted->bTeam;
737 // switch appropriate messages & flags
738 if (interrupted->bTeam == OUR_TEAM)
739 {
740 // set everyone on the team to however they were set moved before the interrupt
741 // must do this before selecting soldier...
742 /*
743 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
744 {
745 s->bMoved = s->bMovedPriorToInterrupt;
746 }
747 */
748
749 ClearIntList();
750
751 // Select soldier....
752 if (interrupted->bLife < OKLIFE)
753 {
754 SelectNextAvailSoldier(interrupted);
755 }
756 else
757 {
758 SelectSoldier(interrupted, SELSOLDIER_NONE);
759 }
760
761 if (gfHiddenInterrupt)
762 {
763 // Try to make things look like nothing happened at all.
764 gfHiddenInterrupt = FALSE;
765
766 // If we can continue a move, do so!
767 SOLDIERTYPE* const sel = GetSelectedMan();
768 if (sel->fNoAPToFinishMove && interrupted->ubReasonCantFinishMove != REASON_STOPPED_SIGHT)
769 {
770 // Continue
771 AdjustNoAPToFinishMove(sel, FALSE);
772
773 if (sel->sGridNo != sel->sFinalDestination)
774 {
775 EVENT_GetNewSoldierPath(sel, sel->sFinalDestination, sel->usUIMovementMode);
776 }
777 else
778 {
779 UnSetUIBusy(interrupted);
780 }
781 }
782 else
783 {
784 UnSetUIBusy(interrupted);
785 }
786
787 if ( gTacticalStatus.fUnLockUIAfterHiddenInterrupt )
788 {
789 gTacticalStatus.fUnLockUIAfterHiddenInterrupt = FALSE;
790 UnSetUIBusy(interrupted);
791 }
792 }
793 else
794 {
795 // Signal UI done enemy's turn
796 /// ATE: This used to be ablow so it would get done for
797 // both hidden interrupts as well - NOT good because
798 // hidden interrupts should leave it locked if it was already...
799 guiPendingOverrideEvent = LU_ENDUILOCK;
800 HandleTacticalUI( );
801
802 SOLDIERTYPE* const sel = GetSelectedMan();
803 if (sel != NULL)
804 {
805 SlideTo(sel, SETLOCATOR);
806
807 // Say ATTENTION SOUND...
808 DoMercBattleSound(sel, BATTLE_SOUND_ATTN1);
809
810 if ( gsInterfaceLevel == 1 )
811 {
812 gTacticalStatus.uiFlags |= SHOW_ALL_ROOFS;
813 InvalidateWorldRedundency( );
814 SetRenderFlags(RENDER_FLAG_FULL);
815 ErasePath();
816 }
817 }
818 // 2 indicates that we're ending an interrupt and going back to
819 // normal player's turn without readjusting time left in turn (for
820 // timed turns)
821 InitPlayerUIBar( 2 );
822 }
823
824 }
825 else
826 {
827 // this could be set to true for AI-vs-AI interrupts
828 gfHiddenInterrupt = FALSE;
829
830 // Dirty panel interface!
831 fInterfacePanelDirty = DIRTYLEVEL2;
832
833 // Erase path!
834 ErasePath();
835
836 // Reset flags
837 gfPlotNewMovement = TRUE;
838
839 // restart AI with first available soldier
840 fFound = FALSE;
841
842 // rebuild list for this team if anyone on the team is still available
843 INT32 cnt = gTacticalStatus.Team[ENEMY_TEAM].bFirstID;
844 for (SOLDIERTYPE* pTempSoldier = &GetMan(cnt); cnt <= gTacticalStatus.Team[gTacticalStatus.ubCurrentTeam].bLastID; cnt++, pTempSoldier++)
845 {
846 if ( pTempSoldier->bActive && pTempSoldier->bInSector && pTempSoldier->bLife >= OKLIFE )
847 {
848 fFound = TRUE;
849 break;
850 }
851 }
852
853 if ( fFound )
854 {
855 // reset found flag because we are rebuilding the AI list
856 fFound = FALSE;
857
858 if ( BuildAIListForTeam( gTacticalStatus.ubCurrentTeam ) )
859 {
860 // now bubble up everyone left in the interrupt queue, starting
861 // at the front of the array
862 for (INT32 cnt = 1; cnt <= gubOutOfTurnPersons; ++cnt)
863 {
864 MoveToFrontOfAIList(gOutOfTurnOrder[cnt]);
865 }
866
867 SOLDIERTYPE* const s = RemoveFirstAIListEntry();
868 if (s != NULL)
869 {
870 fFound = TRUE;
871 StartNPCAI(*s);
872 }
873 }
874
875 }
876
877 AddTopMessage(COMPUTER_TURN_MESSAGE);
878
879 // Signal UI done enemy's turn
880 guiPendingOverrideEvent = LU_BEGINUILOCK;
881
882 ClearIntList();
883
884 if (!fFound) EndAITurn();
885 }
886
887 // Reset our interface!
888 fInterfacePanelDirty = DIRTYLEVEL2;
889
890 }
891 }
892
893
StandardInterruptConditionsMet(const SOLDIERTYPE * const pSoldier,const SOLDIERTYPE * const pOpponent,const INT8 bOldOppList)894 BOOLEAN StandardInterruptConditionsMet(const SOLDIERTYPE* const pSoldier, const SOLDIERTYPE* const pOpponent, const INT8 bOldOppList)
895 {
896 //UINT8 ubAniType;
897 UINT8 ubMinPtsNeeded;
898 INT8 bDir;
899
900 if ((gTacticalStatus.uiFlags & INCOMBAT) && !(gubSightFlags & SIGHT_INTERRUPT))
901 {
902 return( FALSE );
903 }
904
905 if ( gTacticalStatus.ubAttackBusyCount > 0 )
906 {
907 return( FALSE );
908 }
909
910 if (pOpponent == NULL)
911 {
912 // no opponent, so controller of 'ptr' makes the call instead
913 // ALEX
914 if (gWhoThrewRock == NULL)
915 {
916 return(FALSE);
917 }
918 }
919
920 // in non-combat allow interrupt points to be calculated freely (everyone's in control!)
921 // also allow calculation for storing in AllTeamsLookForAll
922 if ( (gTacticalStatus.uiFlags & INCOMBAT) && ( gubBestToMakeSightingSize != BEST_SIGHTING_ARRAY_SIZE_ALL_TEAMS_LOOK_FOR_ALL ) )
923 {
924 // if his team's already in control
925 if (pSoldier->bTeam == gTacticalStatus.ubCurrentTeam )
926 {
927 // CJC, July 9 1998
928 // NO ONE EVER interrupts his own team
929 return FALSE;
930 }
931 else if ( gTacticalStatus.bBoxingState != NOT_BOXING )
932 {
933 // while anything to do with boxing is going on, skip interrupts!
934 return( FALSE );
935 }
936
937 }
938
939 if ( !(pSoldier->bActive) || !(pSoldier->bInSector ) )
940 {
941 return( FALSE );
942 }
943
944 // soldiers at less than OKLIFE can't perform any actions
945 if (pSoldier->bLife < OKLIFE)
946 {
947 return(FALSE);
948 }
949
950 // soldiers out of breath are about to fall over, no interrupt
951 if (pSoldier->bBreath < OKBREATH || pSoldier->bCollapsed )
952 {
953 return(FALSE);
954 }
955
956 // if soldier doesn't have enough APs
957 if ( pSoldier->bActionPoints < MIN_APS_TO_INTERRUPT )
958 {
959 return( FALSE );
960 }
961
962 // soldiers gagging on gas are too busy about holding their cookies down...
963 if ( pSoldier->uiStatusFlags & SOLDIER_GASSED )
964 {
965 return(FALSE);
966 }
967
968 // a soldier already engaged in a life & death battle is too busy doing his
969 // best to survive to worry about "getting the jump" on additional threats
970 if (pSoldier->bUnderFire)
971 {
972 return(FALSE);
973 }
974
975 if (pSoldier->bCollapsed)
976 {
977 return( FALSE );
978 }
979
980 // don't allow neutral folks to get interrupts
981 if (pSoldier->bNeutral)
982 {
983 return( FALSE );
984 }
985
986 // no EPCs allowed to get interrupts
987 if ( AM_AN_EPC( pSoldier ) && !AM_A_ROBOT( pSoldier ) )
988 {
989 return( FALSE );
990 }
991
992
993 // don't let mercs on assignment get interrupts
994 if ( pSoldier->bTeam == OUR_TEAM && pSoldier->bAssignment >= ON_DUTY)
995 {
996 return( FALSE );
997 }
998
999
1000 // the bare minimum default is enough APs left to TURN
1001 ubMinPtsNeeded = AP_CHANGE_FACING;
1002
1003 // if the opponent is SOMEBODY
1004 if (pOpponent != NULL)
1005 {
1006 // if the soldiers are on the same side
1007 if (pSoldier->bSide == pOpponent->bSide)
1008 {
1009 // human/civilians on same side can't interrupt each other
1010 if (pSoldier->uiStatusFlags & SOLDIER_PC || IsOnCivTeam(pSoldier))
1011 {
1012 return(FALSE);
1013 }
1014 else // enemy
1015 {
1016 // enemies can interrupt EACH OTHER, but enemies and civilians on the
1017 // same side (but different teams) can't interrupt each other.
1018 if (pSoldier->bTeam != pOpponent->bTeam)
1019 {
1020 return(FALSE);
1021 }
1022 }
1023 }
1024
1025 // if the interrupted opponent is not the selected character, then the only
1026 // people eligible to win an interrupt are those on the SAME SIDE AS
1027 // the selected character, ie. his friends...
1028 if ( pOpponent->bTeam == OUR_TEAM )
1029 {
1030 const SOLDIERTYPE* const sel = GetSelectedMan();
1031 if (pOpponent != sel && pSoldier->bSide != sel->bSide)
1032 {
1033 return( FALSE );
1034 }
1035 }
1036 else
1037 {
1038 if ( !(pOpponent->uiStatusFlags & SOLDIER_UNDERAICONTROL) && (pSoldier->bSide != pOpponent->bSide))
1039 {
1040 return( FALSE );
1041 }
1042 }
1043
1044 // an non-active soldier can't interrupt a soldier who is also non-active!
1045 if ((pOpponent->bTeam != gTacticalStatus.ubCurrentTeam) && (pSoldier->bTeam != gTacticalStatus.ubCurrentTeam))
1046 {
1047 return(FALSE);
1048 }
1049
1050
1051 // if this is a "SEEING" interrupt
1052 if (pSoldier->bOppList[pOpponent->ubID] == SEEN_CURRENTLY)
1053 {
1054 // if pSoldier already saw the opponent last "look" or at least this turn
1055 if ((bOldOppList == SEEN_CURRENTLY) || (bOldOppList == SEEN_THIS_TURN))
1056 {
1057 return(FALSE); // no interrupt is possible
1058 }
1059
1060 // if the soldier is behind him and not very close, forget it
1061 bDir = atan8( pSoldier->sX, pSoldier->sY, pOpponent->sX, pOpponent->sY );
1062 if (OppositeDirection(pSoldier->bDesiredDirection) == bDir)
1063 {
1064 // directly behind; allow interrupts only within # of tiles equal to level
1065 if ( PythSpacesAway( pSoldier->sGridNo, pOpponent->sGridNo ) > EffectiveExpLevel( pSoldier ) )
1066 {
1067 return( FALSE );
1068 }
1069 }
1070
1071 // if the soldier isn't currently crouching
1072 if (!PTR_CROUCHED)
1073 {
1074 ubMinPtsNeeded = AP_CROUCH;
1075 }
1076 else
1077 {
1078 ubMinPtsNeeded = MinPtsToMove(pSoldier);
1079 }
1080 }
1081 else // this is a "HEARING" interrupt
1082 {
1083 // if the opponent can't see the "interrupter" either, OR
1084 // if the "interrupter" already has any opponents already in sight, OR
1085 // if the "interrupter" already heard the active soldier this turn
1086 if ((pOpponent->bOppList[pSoldier->ubID] != SEEN_CURRENTLY) || (pSoldier->bOppCnt > 0) || (bOldOppList == HEARD_THIS_TURN))
1087 {
1088 return(FALSE); // no interrupt is possible
1089 }
1090 }
1091 }
1092
1093
1094 // soldiers without sufficient APs to do something productive can't interrupt
1095 if (pSoldier->bActionPoints < ubMinPtsNeeded)
1096 {
1097 return(FALSE);
1098 }
1099
1100 // soldier passed on the chance to react during previous interrupt this turn
1101 if (pSoldier->bPassedLastInterrupt && !gamepolicy(multiple_interrupts))
1102 {
1103 return(FALSE);
1104 }
1105
1106
1107 #ifdef RECORDINTERRUPT
1108 // this usually starts a new series of logs, so that's why the blank line
1109 fprintf(InterruptFile, "\nStandardInterruptConditionsMet by %d vs. %d\n", pSoldier->guynum, pOpponent->ubID);
1110 #endif
1111
1112 return(TRUE);
1113 }
1114
1115
CalcInterruptDuelPts(const SOLDIERTYPE * const pSoldier,const SOLDIERTYPE * const opponent,BOOLEAN fUseWatchSpots)1116 INT8 CalcInterruptDuelPts(const SOLDIERTYPE* const pSoldier, const SOLDIERTYPE* const opponent, BOOLEAN fUseWatchSpots)
1117 {
1118 INT8 bPoints;
1119 INT8 bLightLevel;
1120
1121 // extra check to make sure neutral folks never get interrupts
1122 if (pSoldier->bNeutral)
1123 {
1124 return( NO_INTERRUPT );
1125 }
1126
1127 // BASE is one point for each experience level.
1128
1129 // Robot has interrupt points based on the controller...
1130 // Controller's interrupt points are reduced by 2 for being distracted...
1131 if ( pSoldier->uiStatusFlags & SOLDIER_ROBOT && CanRobotBeControlled( pSoldier ) )
1132 {
1133 bPoints = EffectiveExpLevel(pSoldier->robot_remote_holder) - 2;
1134 }
1135 else
1136 {
1137 bPoints = EffectiveExpLevel( pSoldier );
1138 /*
1139 if ( pSoldier->bTeam == ENEMY_TEAM )
1140 {
1141 // modify by the difficulty level setting
1142 bPoints += gbDiff[ DIFF_ENEMY_INTERRUPT_MOD ][ SoldierDifficultyLevel( pSoldier ) ];
1143 bPoints = __max( bPoints, 9 );
1144 }
1145 */
1146
1147 if ( ControllingRobot( pSoldier ) )
1148 {
1149 bPoints -= 2;
1150 }
1151 }
1152
1153 if (fUseWatchSpots)
1154 {
1155 // if this is a previously noted spot of enemies, give bonus points!
1156 bPoints += GetWatchedLocPoints(pSoldier->ubID, opponent->sGridNo, opponent->bLevel);
1157 }
1158
1159 // LOSE one point for each 2 additional opponents he currently sees, above 2
1160 if (pSoldier->bOppCnt > 2)
1161 {
1162 // subtract 1 here so there is a penalty of 1 for seeing 3 enemies
1163 bPoints -= (pSoldier->bOppCnt - 1) / 2;
1164 }
1165
1166 // LOSE one point if he's trying to interrupt only by hearing
1167 if (pSoldier->bOppList[opponent->ubID] == HEARD_THIS_TURN)
1168 {
1169 bPoints--;
1170 }
1171
1172 // if soldier is still in shock from recent injuries, that penalizes him
1173 bPoints -= pSoldier->bShock;
1174
1175 const UINT8 ubDistance = PythSpacesAway(pSoldier->sGridNo, opponent->sGridNo);
1176
1177 // if we are in combat mode - thus doing an interrupt rather than determine who gets first turn -
1178 // then give bonus
1179 if ( (gTacticalStatus.uiFlags & INCOMBAT) && (pSoldier->bTeam != gTacticalStatus.ubCurrentTeam) )
1180 {
1181 // passive player gets penalty due to range
1182 bPoints -= (ubDistance / 10);
1183 }
1184 else
1185 {
1186 // either non-combat or the player with the current turn... i.e. active...
1187 // unfortunately we can't use opplist here to record whether or not we saw this guy before, because at
1188 // this point the opplist has been updated to seen. But we can use gbSeenOpponents ...
1189
1190 // this soldier is moving, so give them a bonus for crawling or swatting at long distances
1191 if (!gbSeenOpponents[opponent->ubID][pSoldier->ubID])
1192 {
1193 if (pSoldier->usAnimState == SWATTING && ubDistance > (MaxDistanceVisible() / 2) ) // more than 1/2 sight distance
1194 {
1195 bPoints++;
1196 }
1197 else if (pSoldier->usAnimState == CRAWLING && ubDistance > (MaxDistanceVisible() / 4) ) // more than 1/4 sight distance
1198 {
1199 bPoints += ubDistance / STRAIGHT;
1200 }
1201 }
1202 }
1203
1204 // whether active or not, penalize people who are running
1205 if (pSoldier->usAnimState == RUNNING && !gbSeenOpponents[pSoldier->ubID][opponent->ubID])
1206 {
1207 bPoints -= 2;
1208 }
1209
1210 if (pSoldier->service_partner != NULL)
1211 {
1212 // distracted by being bandaged/doing bandaging
1213 bPoints -= 2;
1214 }
1215
1216 if ( HAS_SKILL_TRAIT( pSoldier, NIGHTOPS ) )
1217 {
1218 bLightLevel = LightTrueLevel(pSoldier->sGridNo, pSoldier->bLevel);
1219 if (bLightLevel > NORMAL_LIGHTLEVEL_DAY + 3)
1220 {
1221 // it's dark, give a bonus for interrupts
1222 bPoints += 1 * NUM_SKILL_TRAITS( pSoldier, NIGHTOPS );
1223 }
1224 }
1225
1226 // if he's a computer soldier
1227
1228 // CJC note: this will affect friendly AI as well...
1229
1230 if ( pSoldier->uiStatusFlags & SOLDIER_PC )
1231 {
1232 if ( pSoldier->bAssignment >= ON_DUTY )
1233 {
1234 // make sure don't get interrupts!
1235 bPoints = -10;
1236 }
1237
1238 // GAIN one point if he's previously seen the opponent
1239 // check for TRUE because -1 means we JUST saw him (always so here)
1240 if (gbSeenOpponents[pSoldier->ubID][opponent->ubID] == TRUE)
1241 {
1242 bPoints++; // seen him before, easier to react to him
1243 }
1244 }
1245 else if ( pSoldier->bTeam == ENEMY_TEAM )
1246 {
1247 // GAIN one point if he's previously seen the opponent
1248 // check for TRUE because -1 means we JUST saw him (always so here)
1249 if (gbSeenOpponents[pSoldier->ubID][opponent->ubID] == TRUE)
1250 {
1251 bPoints++; // seen him before, easier to react to him
1252 }
1253 else if (gbPublicOpplist[pSoldier->bTeam][opponent->ubID] != NOT_HEARD_OR_SEEN)
1254 {
1255 // GAIN one point if opponent has been recently radioed in by his team
1256 bPoints++;
1257 }
1258 }
1259
1260 if ( TANK( pSoldier ) )
1261 {
1262 // reduce interrupt possibilities for tanks!
1263 bPoints /= 2;
1264 }
1265
1266 if (bPoints >= AUTOMATIC_INTERRUPT)
1267 {
1268 bPoints = AUTOMATIC_INTERRUPT - 1; // hack it to one less than max so its legal
1269 }
1270 SLOGD("Calculating int pts for %d vs %d, number is %d",
1271 pSoldier->ubID, opponent->ubID, bPoints);
1272 return( bPoints );
1273 }
1274
InterruptDuel(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pOpponent)1275 BOOLEAN InterruptDuel( SOLDIERTYPE * pSoldier, SOLDIERTYPE * pOpponent)
1276 {
1277 BOOLEAN fResult = FALSE;
1278
1279 // if opponent can't currently see us and we can see them
1280 if ( pSoldier->bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY && pOpponent->bOppList[pSoldier->ubID] != SEEN_CURRENTLY )
1281 {
1282 fResult = TRUE; // we automatically interrupt
1283 // fix up our interrupt duel pts if necessary
1284 if (pSoldier->bInterruptDuelPts < pOpponent->bInterruptDuelPts)
1285 {
1286 pSoldier->bInterruptDuelPts = pOpponent->bInterruptDuelPts;
1287 }
1288 }
1289 else
1290 {
1291 // If our total points is HIGHER, then we interrupt him anyway
1292 if (pSoldier->bInterruptDuelPts > pOpponent->bInterruptDuelPts)
1293 {
1294 fResult = TRUE;
1295 }
1296 }
1297 //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, ST::format("Interrupt duel {} ({} pts) vs {} ({} pts)", pSoldier->ubID, pSoldier->bInterruptDuelPts, pOpponent->ubID, pOpponent->bInterruptDuelPts) );
1298 return( fResult );
1299 }
1300
1301
DeleteFromIntList(UINT8 ubIndex,BOOLEAN fCommunicate)1302 static void DeleteFromIntList(UINT8 ubIndex, BOOLEAN fCommunicate)
1303 {
1304 UINT8 ubLoop;
1305
1306 if ( ubIndex > gubOutOfTurnPersons)
1307 {
1308 return;
1309 }
1310 SLOGD("INTERRUPT: removing ID %d", gOutOfTurnOrder[ubIndex]->ubID);
1311
1312 // if we're NOT deleting the LAST entry in the int list
1313 if (ubIndex < gubOutOfTurnPersons)
1314 {
1315 // not the last entry, must move all those behind it over to fill the gap
1316 for (ubLoop = ubIndex; ubLoop < gubOutOfTurnPersons; ubLoop++)
1317 {
1318 gOutOfTurnOrder[ubLoop] = gOutOfTurnOrder[ubLoop + 1];
1319 }
1320 }
1321
1322 // either way, whack the last entry to NOBODY and decrement the list size
1323 gOutOfTurnOrder[gubOutOfTurnPersons] = NULL;
1324 gubOutOfTurnPersons--;
1325 }
1326
1327
AddToIntList(SOLDIERTYPE * const s,const BOOLEAN fGainControl,const BOOLEAN fCommunicate)1328 void AddToIntList(SOLDIERTYPE* const s, const BOOLEAN fGainControl, const BOOLEAN fCommunicate)
1329 {
1330 UINT8 ubLoop;
1331
1332 SLOGD("INTERRUPT: adding ID %d who %s",
1333 s->ubID, fGainControl ? "gains control" : "loses control");
1334
1335 // check whether 'who' is already anywhere on the queue after the first index
1336 // which we want to preserve so we can restore turn order
1337 for (ubLoop = 2; ubLoop <= gubOutOfTurnPersons; ubLoop++)
1338 {
1339 if (gOutOfTurnOrder[ubLoop] == s)
1340 {
1341 if (!fGainControl)
1342 {
1343 // he's LOSING control; that's it, we're done, DON'T add him to the queue again
1344 gLastInterruptedGuy = s;
1345 return;
1346 }
1347 else
1348 {
1349 // GAINING control, so delete him from this slot (because later he'll
1350 // get added to the end and we don't want him listed more than once!)
1351 DeleteFromIntList( ubLoop, FALSE );
1352 }
1353 }
1354 }
1355
1356 // increment total (making index valid) and add him to list
1357 gubOutOfTurnPersons++;
1358 gOutOfTurnOrder[gubOutOfTurnPersons] = s;
1359
1360 // if the guy is gaining control
1361 if (fGainControl)
1362 {
1363 // record his initial APs at the start of his interrupt at this time
1364 // this is not the ideal place for this, but it's the best I could do...
1365 s->bIntStartAPs = s->bActionPoints;
1366 }
1367 else
1368 {
1369 gLastInterruptedGuy = s;
1370 // turn off AI control flag if they lost control
1371 if (s->uiStatusFlags & SOLDIER_UNDERAICONTROL)
1372 {
1373 SLOGD("Taking away AI control from %d", s->ubID);
1374 s->uiStatusFlags &= ~SOLDIER_UNDERAICONTROL;
1375 }
1376 }
1377 }
1378
1379
VerifyOutOfTurnOrderArray(void)1380 static void VerifyOutOfTurnOrderArray(void)
1381 {
1382 UINT8 ubTeamHighest[ MAXTEAMS ] = { 0 };
1383 UINT8 ubTeamsInList;
1384 UINT8 ubNextIndex;
1385 UINT8 ubTeam;
1386 UINT8 ubLoop, ubLoop2;
1387 BOOLEAN fFoundLoop = FALSE;
1388
1389 for (ubLoop = 1; ubLoop <= gubOutOfTurnPersons; ubLoop++)
1390 {
1391 ubTeam = gOutOfTurnOrder[ubLoop]->bTeam;
1392 if (ubTeamHighest[ ubTeam ] > 0)
1393 {
1394 // check the other teams to see if any of them are between our last team's mention in
1395 // the array and this
1396 for (ubLoop2 = 0; ubLoop2 < MAXTEAMS; ubLoop2++)
1397 {
1398 if (ubLoop2 == ubTeam)
1399 {
1400 continue;
1401 }
1402 else
1403 {
1404 if (ubTeamHighest[ ubLoop2 ] > ubTeamHighest[ ubTeam ])
1405 {
1406 // there's a loop!! delete it!
1407 const SOLDIERTYPE* const NextInArrayOnTeam = gOutOfTurnOrder[ubLoop];
1408 ubNextIndex = ubTeamHighest[ ubTeam ] + 1;
1409
1410 while (gOutOfTurnOrder[ubNextIndex] != NextInArrayOnTeam)
1411 {
1412 // Pause them...
1413 AdjustNoAPToFinishMove(gOutOfTurnOrder[ubNextIndex], TRUE);
1414
1415 // If they were turning from prone, stop them
1416 gOutOfTurnOrder[ubNextIndex]->fTurningFromPronePosition = FALSE;
1417
1418 DeleteFromIntList( ubNextIndex, FALSE );
1419 }
1420
1421 fFoundLoop = TRUE;
1422 break;
1423 }
1424 }
1425 }
1426
1427 if (fFoundLoop)
1428 {
1429 // at this point we should restart our outside loop (ugh)
1430 fFoundLoop = FALSE;
1431 for (ubLoop2 = 0; ubLoop2 < MAXTEAMS; ubLoop2++)
1432 {
1433 ubTeamHighest[ ubLoop2 ] = 0;
1434 }
1435 ubLoop = 0;
1436 continue;
1437
1438 }
1439
1440 }
1441
1442 ubTeamHighest[ ubTeam ] = ubLoop;
1443 }
1444
1445 // Another potential problem: the player is interrupted by the enemy who is interrupted by
1446 // the militia. In this situation the enemy should just lose their interrupt.
1447 // (Or, the militia is interrupted by the enemy who is interrupted by the player.)
1448
1449 // Check for 3+ teams in the interrupt queue. If three exist then abort all interrupts (return
1450 // control to the first team)
1451 ubTeamsInList = 0;
1452 for ( ubLoop = 0; ubLoop < MAXTEAMS; ubLoop++ )
1453 {
1454 if ( ubTeamHighest[ ubLoop ] > 0 )
1455 {
1456 ubTeamsInList++;
1457 }
1458 }
1459 if ( ubTeamsInList >= 3 )
1460 {
1461 // This is bad. Loop through everyone but the first person in the INT list and remove 'em
1462 for (ubLoop = 2; ubLoop <= gubOutOfTurnPersons; )
1463 {
1464 if (gOutOfTurnOrder[ubLoop]->bTeam != gOutOfTurnOrder[1]->bTeam)
1465 {
1466 // remove!
1467
1468 // Pause them...
1469 AdjustNoAPToFinishMove(gOutOfTurnOrder[ubLoop], TRUE);
1470
1471 // If they were turning from prone, stop them
1472 gOutOfTurnOrder[ubLoop]->fTurningFromPronePosition = FALSE;
1473
1474 DeleteFromIntList( ubLoop, FALSE );
1475
1476 // since we deleted someone from the list, we want to check the same index in the
1477 // array again, hence we DON'T increment.
1478 }
1479 else
1480 {
1481 ubLoop++;
1482 }
1483 }
1484 }
1485 }
1486
1487
DoneAddingToIntList(void)1488 void DoneAddingToIntList(void)
1489 {
1490 VerifyOutOfTurnOrderArray();
1491 if (EveryoneInInterruptListOnSameTeam())
1492 {
1493 EndInterrupt(TRUE);
1494 }
1495 else
1496 {
1497 StartInterrupt();
1498 }
1499 }
1500
1501
ResolveInterruptsVs(SOLDIERTYPE * pSoldier,UINT8 ubInterruptType)1502 void ResolveInterruptsVs( SOLDIERTYPE * pSoldier, UINT8 ubInterruptType)
1503 {
1504 UINT8 ubIntCnt;
1505 SOLDIERTYPE* IntList[MAXMERCS];
1506 UINT8 ubIntDiff[MAXMERCS];
1507 UINT8 ubSmallestDiff;
1508 UINT8 ubSlot, ubSmallestSlot;
1509 BOOLEAN fIntOccurs;
1510
1511 if (gTacticalStatus.uiFlags & INCOMBAT)
1512 {
1513 ubIntCnt = 0;
1514
1515 for (UINT8 ubTeam = 0; ubTeam < MAXTEAMS; ++ubTeam)
1516 {
1517 if (IsTeamActive(ubTeam) && gTacticalStatus.Team[ubTeam].bSide != pSoldier->bSide && ubTeam != CIV_TEAM)
1518 {
1519 FOR_EACH_IN_TEAM(pOpponent, ubTeam)
1520 {
1521 if (pOpponent->bInSector && pOpponent->bLife >= OKLIFE && !pOpponent->bCollapsed)
1522 {
1523 if ( ubInterruptType == NOISEINTERRUPT )
1524 {
1525 // don't grant noise interrupts at greater than max. visible distance
1526 if ( PythSpacesAway( pSoldier->sGridNo, pOpponent->sGridNo ) > MaxDistanceVisible() )
1527 {
1528 pOpponent->bInterruptDuelPts = NO_INTERRUPT;
1529 SLOGD("Resetting int pts for %d - NOISE BEYOND SIGHT DISTANCE!?",
1530 pOpponent->ubID);
1531 continue;
1532 }
1533 }
1534 else if ( pOpponent->bOppList[pSoldier->ubID] != SEEN_CURRENTLY )
1535 {
1536 pOpponent->bInterruptDuelPts = NO_INTERRUPT;
1537 SLOGD("Resetting int pts for %d - DOESN'T SEE ON SIGHT INTERRUPT!?",
1538 pOpponent->ubID);
1539 continue;
1540 }
1541
1542 switch (pOpponent->bInterruptDuelPts)
1543 {
1544 case NO_INTERRUPT: // no interrupt possible, no duel necessary
1545 fIntOccurs = FALSE;
1546 break;
1547
1548 case AUTOMATIC_INTERRUPT: // interrupts occurs automatically
1549 pSoldier->bInterruptDuelPts = 0; // just to have a valid intDiff later
1550 fIntOccurs = TRUE;
1551 SLOGD("INTERRUPT: automatic interrupt on %d by %d",
1552 pSoldier->ubID, pOpponent->ubID);
1553 break;
1554
1555 default: // interrupt is possible, run a duel
1556 SLOGD("Calculating int duel pts for onlooker in ResolveInterruptsVs");
1557 pSoldier->bInterruptDuelPts = CalcInterruptDuelPts(pSoldier, pOpponent, TRUE);
1558 fIntOccurs = InterruptDuel(pOpponent,pSoldier);
1559 if (fIntOccurs)
1560 {
1561 SLOGD("INTERRUPT: standard interrupt on %d (%d pts) by %d (%d pts)",
1562 pSoldier->ubID, pSoldier->bInterruptDuelPts, pOpponent->ubID, pOpponent->bInterruptDuelPts);
1563 }
1564 break;
1565 }
1566
1567 if (fIntOccurs)
1568 {
1569 // remember that this opponent's scheduled to interrupt us
1570 IntList[ubIntCnt] = pOpponent;
1571
1572 // and by how much he beat us in the duel
1573 ubIntDiff[ubIntCnt] = pOpponent->bInterruptDuelPts - pSoldier->bInterruptDuelPts;
1574
1575 // increment counter of interrupts lost
1576 ubIntCnt++;
1577 }
1578 else
1579 {
1580 /*
1581 if (pOpponent->bInterruptDuelPts != NO_INTERRUPT)
1582 {
1583 ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, ST::format("{} fails to interrupt {} ({} vs {} pts)", pOpponent->ubID, pSoldier->ubID, pOpponent->bInterruptDuelPts, pSoldier->bInterruptDuelPts));
1584 }
1585 */
1586 }
1587
1588 // either way, clear out both sides' bInterruptDuelPts field to prepare next one
1589 if (pSoldier->bInterruptDuelPts != NO_INTERRUPT)
1590 {
1591 SLOGD("Resetting int pts for %d and %d",
1592 pSoldier->ubID, pOpponent->ubID);
1593 }
1594 pSoldier->bInterruptDuelPts = NO_INTERRUPT;
1595 pOpponent->bInterruptDuelPts = NO_INTERRUPT;
1596 }
1597 }
1598 }
1599 }
1600
1601 // if any interrupts are scheduled to occur (ie. I lost at least once)
1602 if (ubIntCnt)
1603 {
1604 // First add currently active character to the interrupt queue. This is
1605 // USUALLY pSoldier->guynum, but NOT always, because one enemy can
1606 // "interrupt" on another enemy's turn if he hears another team's wound
1607 // victim's screaming... the guy screaming is pSoldier here, it's not his turn!
1608 //AddToIntList(GetSelectedMan(), FALSE, TRUE);
1609
1610 if ( (gTacticalStatus.ubCurrentTeam != pSoldier->bTeam) && !(gTacticalStatus.Team[ gTacticalStatus.ubCurrentTeam ].bHuman) )
1611 {
1612 // if anyone on this team is under AI control, remove
1613 // their AI control flag and put them on the queue instead of this guy
1614 FOR_EACH_IN_TEAM(s, gTacticalStatus.ubCurrentTeam)
1615 {
1616 if (s->uiStatusFlags & SOLDIER_UNDERAICONTROL)
1617 {
1618 // this guy lost control
1619 s->uiStatusFlags &= ~SOLDIER_UNDERAICONTROL;
1620 AddToIntList(s, FALSE, TRUE);
1621 break;
1622 }
1623 }
1624
1625 }
1626 else
1627 {
1628 // this guy lost control
1629 AddToIntList(pSoldier, FALSE, TRUE);
1630 }
1631
1632 // loop once for each opponent who interrupted
1633 for (UINT8 ubLoop = 0; ubLoop < ubIntCnt; ++ubLoop)
1634 {
1635 // find the smallest intDiff still remaining in the list
1636 ubSmallestDiff = NO_INTERRUPT;
1637 ubSmallestSlot = NOBODY;
1638
1639 for (ubSlot = 0; ubSlot < ubIntCnt; ubSlot++)
1640 {
1641 if (ubIntDiff[ubSlot] < ubSmallestDiff)
1642 {
1643 ubSmallestDiff = ubIntDiff[ubSlot];
1644 ubSmallestSlot = ubSlot;
1645 }
1646 }
1647
1648 if (ubSmallestSlot < MAX_NUM_SOLDIERS)
1649 {
1650 // add this guy to everyone's interrupt queue
1651 AddToIntList(IntList[ubSmallestSlot], TRUE, TRUE);
1652 if (INTERRUPTS_OVER)
1653 {
1654 // a loop was created which removed all the people in the interrupt queue!
1655 EndInterrupt( TRUE );
1656 return;
1657 }
1658
1659 ubIntDiff[ubSmallestSlot] = NO_INTERRUPT; // mark slot as been handled
1660 }
1661 }
1662
1663 // sends off an end-of-list msg telling everyone whether to switch control,
1664 // unless it's a MOVEMENT interrupt, in which case that is delayed til later
1665 DoneAddingToIntList();
1666 }
1667 }
1668 }
1669
1670
SaveTeamTurnsToTheSaveGameFile(HWFILE const f)1671 void SaveTeamTurnsToTheSaveGameFile(HWFILE const f)
1672 {
1673 BYTE data[174];
1674 DataWriter d{data};
1675 for (size_t i = 0; i != lengthof(gOutOfTurnOrder); ++i)
1676 {
1677 INJ_SOLDIER(d, gOutOfTurnOrder[i])
1678 }
1679 INJ_U8( d, gubOutOfTurnPersons)
1680 INJ_SKIP( d, 3)
1681 INJ_SOLDIER(d, gWhoThrewRock)
1682 INJ_SKIP( d, 2)
1683 INJ_BOOL( d, gfHiddenInterrupt)
1684 INJ_SOLDIER(d, gLastInterruptedGuy)
1685 INJ_SKIP( d, 17)
1686 Assert(d.getConsumed() == lengthof(data));
1687
1688 FileWrite(f, data, sizeof(data));
1689 }
1690
1691
LoadTeamTurnsFromTheSavedGameFile(HWFILE const f)1692 void LoadTeamTurnsFromTheSavedGameFile(HWFILE const f)
1693 {
1694 BYTE data[174];
1695 FileRead(f, data, sizeof(data));
1696
1697 DataReader d{data};
1698 EXTR_SKIP(d, 1)
1699 for (size_t i = 1; i != lengthof(gOutOfTurnOrder); ++i)
1700 {
1701 EXTR_SOLDIER(d, gOutOfTurnOrder[i])
1702 }
1703 EXTR_U8( d, gubOutOfTurnPersons)
1704 EXTR_SKIP( d, 3)
1705 EXTR_SOLDIER(d, gWhoThrewRock)
1706 EXTR_SKIP( d, 2)
1707 EXTR_BOOL( d, gfHiddenInterrupt)
1708 EXTR_SOLDIER(d, gLastInterruptedGuy)
1709 EXTR_SKIP( d, 17)
1710 Assert(d.getConsumed() == lengthof(data));
1711 }
1712
1713
NPCFirstDraw(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pTargetSoldier)1714 BOOLEAN NPCFirstDraw( SOLDIERTYPE * pSoldier, SOLDIERTYPE * pTargetSoldier )
1715 {
1716 // if attacking an NPC check to see who draws first!
1717
1718 if ( pTargetSoldier->ubProfile != NO_PROFILE && pTargetSoldier->ubProfile != SLAY && pTargetSoldier->bNeutral && pTargetSoldier->bOppList[ pSoldier->ubID ] == SEEN_CURRENTLY && ( FindAIUsableObjClass( pTargetSoldier, IC_WEAPON ) != NO_SLOT ) )
1719 {
1720 UINT8 ubLargerHalf, ubSmallerHalf, ubTargetLargerHalf, ubTargetSmallerHalf;
1721
1722 // roll the dice!
1723 // e.g. if level 5, roll Random( 3 + 1 ) + 2 for result from 2 to 5
1724 // if level 4, roll Random( 2 + 1 ) + 2 for result from 2 to 4
1725 ubSmallerHalf = EffectiveExpLevel( pSoldier ) / 2;
1726 ubLargerHalf = EffectiveExpLevel( pSoldier ) - ubSmallerHalf;
1727
1728 ubTargetSmallerHalf = EffectiveExpLevel( pTargetSoldier ) / 2;
1729 ubTargetLargerHalf = EffectiveExpLevel( pTargetSoldier ) - ubTargetSmallerHalf;
1730 if ( gMercProfiles[ pTargetSoldier->ubProfile ].bApproached & gbFirstApproachFlags[ APPROACH_THREATEN - 1 ] )
1731 {
1732 // gains 1 to 2 points
1733 ubTargetSmallerHalf += 1;
1734 ubTargetLargerHalf += 1;
1735 }
1736 if ( Random( ubTargetSmallerHalf + 1) + ubTargetLargerHalf > Random( ubSmallerHalf + 1) + ubLargerHalf )
1737 {
1738 return( TRUE );
1739 }
1740 }
1741 return( FALSE );
1742 }
1743