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