1 #include "Font.h"
2 #include "AI.h"
3 #include "Debug_Pages.h"
4 #include "Isometric_Utils.h"
5 #include "Overhead.h"
6 #include "Event_Pump.h"
7 #include "Random.h"
8 #include "Overhead_Types.h"
9 #include "OppList.h"
10 #include "AI.h"
11 #include "Font_Control.h"
12 #include "Soldier_Find.h"
13 #include "Animation_Control.h"
14 #include "LOS.h"
15 #include "FOV.h"
16 #include "Dialogue_Control.h"
17 #include "Lighting.h"
18 #include "Environment.h"
19 #include "Points.h"
20 #include "Interface_Dialogue.h"
21 #include "Message.h"
22 #include "Soldier_Profile.h"
23 #include "Structure.h"
24 #include "TeamTurns.h"
25 #include "Interactive_Tiles.h"
26 #include "Render_Fun.h"
27 #include "Text.h"
28 #include "TileDef.h"
29 #include "Timer_Control.h"
30 #include "Soldier_Macros.h"
31 #include "Soldier_Functions.h"
32 #include "Handle_UI.h"
33 #include "Queen_Command.h"
34 #include "Keys.h"
35 #include "Campaign.h"
36 #include "Soldier_Init_List.h"
37 #include "Music_Control.h"
38 #include "ContentMusic.h"
39 #include "StrategicMap.h"
40 #include "Quests.h"
41 #include "Meanwhile.h"
42 #include "WorldMan.h"
43 #include "SkillCheck.h"
44 #include "Smell.h"
45 #include "GameSettings.h"
46 #include "Game_Clock.h"
47 #include "Civ_Quotes.h"
48 #include "Sound_Control.h"
49 #include "Drugs_And_Alcohol.h"
50 #include "Debug.h"
51 #include "Items.h"
52 #include "GameRes.h"
53 #include "MercProfile.h"
54 
55 #include "ContentManager.h"
56 #include "GameInstance.h"
57 
58 #include <string_theory/format>
59 #include <string_theory/string>
60 
61 #include <algorithm>
62 #include <iterator>
63 
64 #define WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
65 
66 
67 static const BOOLEAN gbShowEnemies = FALSE; // XXX seems to be a debug switch
68 
69 // for ManLooksForMan()
70 #define MANLOOKSFOROTHERTEAMS			0
71 #define OTHERTEAMSLOOKFORMAN			1
72 #define VERIFYANDDECAYOPPLIST			2
73 #define HANDLESTEPPEDLOOKAT			3
74 #define LOOKANDLISTEN				4
75 #define UPDATEPUBLIC				5
76 #define CALLER_UNKNOWN				6
77 
78 // this variable is a flag used in HandleSight to determine whether (while in non-combat RT)
79 // someone has just been seen, EITHER THE MOVER OR SOMEONE THE MOVER SEES
80 static BOOLEAN gfPlayerTeamSawCreatures = FALSE;
81 BOOLEAN gfPlayerTeamSawJoey = FALSE;
82 BOOLEAN gfMikeShouldSayHi = FALSE;
83 
84 static SOLDIERTYPE* gBestToMakeSighting[BEST_SIGHTING_ARRAY_SIZE];
85 UINT8 gubBestToMakeSightingSize = 0;
86 //BOOLEAN gfHumanSawSomeoneInRealtime;
87 
88 static BOOLEAN gfDelayResolvingBestSightingDueToDoor = FALSE;
89 
90 #define SHOULD_BECOME_HOSTILE_SIZE 32
91 
92 static SOLDIERTYPE* gShouldBecomeHostileOrSayQuote[SHOULD_BECOME_HOSTILE_SIZE];
93 static UINT8 gubNumShouldBecomeHostileOrSayQuote;
94 
95 // NB this ID is set for someone opening a door
96 SOLDIERTYPE* gInterruptProvoker = NULL;
97 
98 INT8 gbPublicOpplist[MAXTEAMS][TOTAL_SOLDIERS];
99 INT8 gbSeenOpponents[TOTAL_SOLDIERS][TOTAL_SOLDIERS];
100 INT16 gsLastKnownOppLoc[TOTAL_SOLDIERS][TOTAL_SOLDIERS]; // merc vs. merc
101 INT8 gbLastKnownOppLevel[TOTAL_SOLDIERS][TOTAL_SOLDIERS];
102 INT16 gsPublicLastKnownOppLoc[MAXTEAMS][TOTAL_SOLDIERS]; // team vs. merc
103 INT8 gbPublicLastKnownOppLevel[MAXTEAMS][TOTAL_SOLDIERS];
104 UINT8 gubPublicNoiseVolume[MAXTEAMS];
105 INT16 gsPublicNoiseGridno[MAXTEAMS];
106 INT8	gbPublicNoiseLevel[MAXTEAMS];
107 
108 UINT8 gubKnowledgeValue[10][10] =
109 {
110 	//   P E R S O N A L   O P P L I S T  //
111 	// -4   -3   -2   -1   0   1   2   3   4   5   //
112 	{   0,   1,   2,   3,  0,  5,  4,  3,  2,  1}, // -4
113 	{   0,   0,   1,   2,  0,  4,  3,  2,  1,  0}, // -3    O
114 	{   0,   0,   0,   1,  0,  3,  2,  1,  0,  0}, // -2  P P
115 	{   0,   0,   0,   0,  0,  2,  1,  0,  0,  0}, // -1  U P
116 	{   0,   1,   2,   3,  0,  5,  4,  3,  2,  1}, //  0  B L
117 	{   0,   0,   0,   0,  0,  0,  0,  0,  0,  0}, //  1  L I
118 	{   0,   0,   0,   0,  0,  1,  0,  0,  0,  0}, //  2  I S
119 	{   0,   0,   0,   1,  0,  2,  1,  0,  0,  0}, //  3  C T
120 	{   0,   0,   1,   2,  0,  3,  2,  1,  0,  0}, //  4
121 	{   0,   1,   2,   3,  0,  4,  3,  2,  1,  0}  //  5
122 
123 	/*
124 	//   P E R S O N A L   O P P L I S T  //
125 	// -3   -2   -1   0   1   2   3   4   //
126 	{   0,   1,   2,  0,  4,  3,  2,  1   }, // -3    O
127 	{   0,   0,   1,  0,  3,  2,  1,  0   }, // -2  P P
128 	{   0,   0,   0,  0,  2,  1,  0,  0   }, // -1  U P
129 	{   1,   2,   3,  0,  5,  4,  3,  2   }, //  0  B L
130 	{   0,   0,   0,  0,  0,  0,  0,  0   }, //  1  L I
131 	{   0,   0,   0,  0,  1,  0,  0,  0   }, //  2  I S
132 	{   0,   0,   1,  0,  2,  1,  0,  0   }, //  3  C T
133 	{   0,   1,   2,  0,  3,  2,  1,  0   }  //  4
134 	*/
135 };
136 
137 #define MAX_WATCHED_LOC_POINTS			4
138 #define WATCHED_LOC_RADIUS			1
139 
140 INT16 gsWatchedLoc[ TOTAL_SOLDIERS ][ NUM_WATCHED_LOCS ];
141 INT8 gbWatchedLocLevel[ TOTAL_SOLDIERS ][ NUM_WATCHED_LOCS ];
142 UINT8 gubWatchedLocPoints[ TOTAL_SOLDIERS ][ NUM_WATCHED_LOCS ];
143 BOOLEAN gfWatchedLocReset[ TOTAL_SOLDIERS ][ NUM_WATCHED_LOCS ];
144 static BOOLEAN gfWatchedLocHasBeenIncremented[TOTAL_SOLDIERS][NUM_WATCHED_LOCS];
145 
146 static const INT8 gbLookDistance[8][8] =
147 {
148 	//  LOOKER DIR       LOOKEE DIR
149 	//                 NORTH  | NORTHEAST |  EAST   | SOUTHEAST |  SOUTH  | SOUTHWEST |  WEST   | NORTHWEST
150 	/* NORTH     */ { STRAIGHT,      ANGLE,     SIDE,    SBEHIND,   BEHIND,    SBEHIND,     SIDE,     ANGLE },
151 	/* NORTHEAST */ { ANGLE,      STRAIGHT,    ANGLE,       SIDE,  SBEHIND,     BEHIND,  SBEHIND,      SIDE },
152 	/* EAST      */ { SIDE,          ANGLE, STRAIGHT,      ANGLE,     SIDE,    SBEHIND,   BEHIND,   SBEHIND },
153 	/* SOUTHEAST */ { SBEHIND,        SIDE,    ANGLE,   STRAIGHT,    ANGLE,       SIDE,  SBEHIND,    BEHIND },
154 	/* SOUTH     */ { BEHIND,      SBEHIND,     SIDE,      ANGLE, STRAIGHT,      ANGLE,     SIDE,   SBEHIND },
155 	/* SOUTHWEST */ { SBEHIND,      BEHIND,  SBEHIND,       SIDE,    ANGLE,   STRAIGHT,    ANGLE,      SIDE },
156 	/* WEST      */ { SIDE,        SBEHIND,   BEHIND,    SBEHIND,     SIDE,      ANGLE, STRAIGHT,     ANGLE },
157 	/* NORTHWEST */ { ANGLE,          SIDE,  SBEHIND,     BEHIND,  SBEHIND,       SIDE,    ANGLE,  STRAIGHT }
158 };
159 
160 const SOLDIERTYPE* gWhoThrewRock = NULL;
161 
162 #define NIGHTSIGHTGOGGLES_BONUS		2
163 #define UVGOGGLES_BONUS			4
164 
165 // % values of sighting distance at various light levels
166 
167 INT8 gbLightSighting[1][16] =
168 {{
169 	// human
170 	80, // brightest
171 	86,
172 	93,
173 	100, // normal daylight, 3
174 	94,
175 	88,
176 	82,
177 	76,
178 	70, // mid-dawn, 8
179 	64,
180 	58,
181 	51,
182 	43, // normal nighttime, 12 (11 tiles)
183 	30,
184 	17,
185 	9
186 }};
187 /*
188 {{
189 	// human
190 	80, // brightest
191 	86,
192 	93,
193 	100, // normal daylight, 3
194 	93,
195 	86,
196 	79,
197 	72,
198 	65, // mid-dawn, 8
199 	58,
200 	53,
201 	43, // normal nighttime, 11  (11 tiles)
202 	35,
203 	26,
204 	17,
205 	9
206 }};*/
207 
208 SightFlags gubSightFlags;
209 
210 #define DECAY_OPPLIST_VALUE( value )\
211 {\
212 	if ( (value) >= SEEN_THIS_TURN)\
213 	{\
214 		(value)++;\
215 		if ( (value) > OLDEST_SEEN_VALUE )\
216 		{\
217 			(value) = NOT_HEARD_OR_SEEN;\
218 		}\
219 	}\
220 	else\
221 	{\
222 		if ( (value) <= HEARD_THIS_TURN)\
223 		{\
224 			(value)--;\
225 			if ( (value) < OLDEST_HEARD_VALUE)\
226 			{\
227 				(value) = NOT_HEARD_OR_SEEN;\
228 			}\
229 		}\
230 	}\
231 }
232 
233 
AdjustMaxSightRangeForEnvEffects(INT8 bLightLevel,INT16 sDistVisible)234 INT16 AdjustMaxSightRangeForEnvEffects(INT8 bLightLevel, INT16 sDistVisible)
235 {
236 	INT16 sNewDist = 0;
237 
238 	sNewDist = sDistVisible * gbLightSighting[ 0 ][ bLightLevel ] / 100;
239 
240 	// Adjust it based on weather...
241 	if ( guiEnvWeather & ( WEATHER_FORECAST_SHOWERS | WEATHER_FORECAST_THUNDERSHOWERS ) )
242 	{
243 		sNewDist = sNewDist * 70 / 100;
244 	}
245 
246 	return( sNewDist );
247 }
248 
249 
SwapBestSightingPositions(INT8 bPos1,INT8 bPos2)250 static void SwapBestSightingPositions(INT8 bPos1, INT8 bPos2)
251 {
252 	SOLDIERTYPE* const temp = gBestToMakeSighting[bPos1];
253 	gBestToMakeSighting[bPos1] = gBestToMakeSighting[bPos2];
254 	gBestToMakeSighting[bPos2] = temp;
255 }
256 
257 
ReevaluateBestSightingPosition(SOLDIERTYPE * pSoldier,INT8 bInterruptDuelPts)258 static void ReevaluateBestSightingPosition(SOLDIERTYPE* pSoldier, INT8 bInterruptDuelPts)
259 {
260 	UINT8 ubLoop, ubLoop2;
261 	BOOLEAN fFound = FALSE;
262 	BOOLEAN fPointsGotLower = FALSE;
263 
264 	if ( bInterruptDuelPts == NO_INTERRUPT )
265 	{
266 		return;
267 	}
268 
269 	if ( !( pSoldier->uiStatusFlags & SOLDIER_MONSTER ) )
270 	{
271 		//gfHumanSawSomeoneInRealtime = TRUE;
272 	}
273 
274 	if ( (pSoldier->bInterruptDuelPts != NO_INTERRUPT) && (bInterruptDuelPts < pSoldier->bInterruptDuelPts) )
275 	{
276 		fPointsGotLower = TRUE;
277 	}
278 
279 	if ( fPointsGotLower )
280 	{
281 		// loop to end of array less 1 entry since we can't swap the last entry out of the array
282 		for ( ubLoop = 0; ubLoop < gubBestToMakeSightingSize - 1; ubLoop++ )
283 		{
284 			if (pSoldier == gBestToMakeSighting[ubLoop])
285 			{
286 				fFound = TRUE;
287 				break;
288 			}
289 		}
290 
291 		// this guy has fewer interrupt pts vs another enemy!  reduce position unless in last place
292 		if (fFound)
293 		{
294 			// set new points
295 			SLOGD(
296 				"RBSP: reducing points for %d to %d",
297 				pSoldier->ubID, bInterruptDuelPts);
298 			pSoldier->bInterruptDuelPts = bInterruptDuelPts;
299 
300 			// must percolate him down
301 			for ( ubLoop2 = ubLoop + 1; ubLoop2 < gubBestToMakeSightingSize; ubLoop2++ )
302 			{
303 				if (gBestToMakeSighting[ubLoop2] != NULL && gBestToMakeSighting[ubLoop2 - 1]->bInterruptDuelPts < gBestToMakeSighting[ubLoop2]->bInterruptDuelPts)
304 				{
305 					SwapBestSightingPositions( (UINT8) (ubLoop2 - 1), ubLoop2 );
306 				}
307 				else
308 				{
309 					break;
310 				}
311 			}
312 		}
313 		else if (pSoldier == gBestToMakeSighting[gubBestToMakeSightingSize - 1])
314 		{
315 			// in list but can't be bumped down... set his new points
316 			SLOGD(
317 				"RBSP: reduced points for last individual %d to %d",
318 				pSoldier->ubID, bInterruptDuelPts);
319 			pSoldier->bInterruptDuelPts = bInterruptDuelPts;
320 		}
321 	}
322 	else
323 	{
324 		// loop through whole array
325 		for ( ubLoop = 0; ubLoop < gubBestToMakeSightingSize; ubLoop++ )
326 		{
327 			if (pSoldier == gBestToMakeSighting[ubLoop])
328 			{
329 				fFound = TRUE;
330 				break;
331 			}
332 		}
333 
334 		if (!fFound)
335 		{
336 			for ( ubLoop = 0; ubLoop < gubBestToMakeSightingSize; ubLoop++ )
337 			{
338 				if (gBestToMakeSighting[ubLoop] == NULL || bInterruptDuelPts > gBestToMakeSighting[ubLoop]->bInterruptDuelPts)
339 				{
340 					if (gBestToMakeSighting[gubBestToMakeSightingSize - 1] !=  NULL)
341 					{
342 						gBestToMakeSighting[gubBestToMakeSightingSize - 1]->bInterruptDuelPts = NO_INTERRUPT;
343 						SLOGD(
344 							"RBSP: resetting points for %d to zilch",
345 							pSoldier->ubID);
346 					}
347 
348 					// set new points
349 					SLOGD("RBSP: setting points for %d to %d",
350 								pSoldier->ubID, bInterruptDuelPts);
351 					pSoldier->bInterruptDuelPts = bInterruptDuelPts;
352 
353 					// insert here!
354 					for ( ubLoop2 = gubBestToMakeSightingSize - 1; ubLoop2 > ubLoop; ubLoop2-- )
355 					{
356 						gBestToMakeSighting[ubLoop2] = gBestToMakeSighting[ubLoop2 - 1];
357 					}
358 					gBestToMakeSighting[ubLoop] = pSoldier;
359 					break;
360 				}
361 			}
362 		}
363 		// else points didn't get lower, so do nothing (because we want to leave each merc with
364 		// as low int points as possible)
365 	}
366 
367 	for ( ubLoop = 0; ubLoop < BEST_SIGHTING_ARRAY_SIZE; ubLoop++ )
368 	{
369 		if (gBestToMakeSighting[ubLoop] != NULL)
370 		{
371 			SLOGD("RBSP entry %d: %d (%d pts)",
372 				ubLoop, gBestToMakeSighting[ubLoop]->ubID,
373 				gBestToMakeSighting[ubLoop]->bInterruptDuelPts);
374 		}
375 	}
376 }
377 
378 
HandleBestSightingPositionInRealtime(void)379 static void HandleBestSightingPositionInRealtime(void)
380 {
381 	// This function is called for handling interrupts when opening a door in non-combat or
382 	// just sighting in non-combat, deciding who gets the first turn
383 
384 	if ( gfDelayResolvingBestSightingDueToDoor )
385 	{
386 		SLOGD("HBSPIR: skipping due to door flag" );
387 		return;
388 	}
389 
390 	if (gBestToMakeSighting[0] != NULL)
391 	{
392 		SLOGD("HBSPIR called and there is someone in the list" );
393 
394 		//if (gfHumanSawSomeoneInRealtime)
395 		{
396 			if (gBestToMakeSighting[1] == NULL)
397 			{
398 				// award turn
399 				EnterCombatMode(gBestToMakeSighting[0]->bTeam);
400 			}
401 			else
402 			{
403 				// if 1st and 2nd on same team, or 1st and 3rd on same team, or there IS no 3rd, award turn to 1st
404 				if (gBestToMakeSighting[0]->bTeam == gBestToMakeSighting[1]->bTeam ||
405 					gBestToMakeSighting[2] == NULL ||
406 					gBestToMakeSighting[0]->bTeam == gBestToMakeSighting[2]->bTeam)
407 				{
408 					EnterCombatMode(gBestToMakeSighting[0]->bTeam);
409 				}
410 				else // give turn to 2nd best but interrupt to 1st
411 				{
412 					SLOGD(
413 						"Entering combat mode: turn for 2nd best, int for best" );
414 
415 					EnterCombatMode(gBestToMakeSighting[1]->bTeam);
416 					// 2nd guy loses control
417 					AddToIntList(gBestToMakeSighting[1], FALSE, TRUE);
418 					// 1st guy gains control
419 					AddToIntList(gBestToMakeSighting[0], TRUE,  TRUE);
420 					DoneAddingToIntList();
421 				}
422 			}
423 		}
424 
425 		for (UINT8 ubLoop = 0; ubLoop < BEST_SIGHTING_ARRAY_SIZE; ++ubLoop)
426 		{
427 			if (gBestToMakeSighting[ubLoop] != NULL)
428 			{
429 				gBestToMakeSighting[ubLoop]->bInterruptDuelPts = NO_INTERRUPT;
430 				SLOGD("RBSP: done, resetting points for %d to zilch",
431 					gBestToMakeSighting[ubLoop]->ubID);
432 			}
433 		}
434 
435 #if defined FORCE_ASSERTS_ON
436 		FOR_EACH_MERC(i)
437 		{
438 			SOLDIERTYPE* const s = *i;
439 			AssertMsg(s->bInterruptDuelPts == NO_INTERRUPT,
440 					String("%s (%d) still has interrupt pts!", s->name.c_str(), s->ubID));
441 		}
442 #endif
443 	}
444 }
445 
446 
HandleBestSightingPositionInTurnbased(void)447 static void HandleBestSightingPositionInTurnbased(void)
448 {
449 	// This function is called for handling interrupts when opening a door in turnbased
450 	BOOLEAN fOk = FALSE;
451 
452 	if (gBestToMakeSighting[0] != NULL)
453 	{
454 		if (gBestToMakeSighting[0]->bTeam != gTacticalStatus.ubCurrentTeam)
455 		{
456 			// interrupt!
457 			UINT8 ubLoop;
458 			for (ubLoop = 0; ubLoop < gubBestToMakeSightingSize; ++ubLoop)
459 			{
460 				if (gBestToMakeSighting[ubLoop] == NULL)
461 				{
462 					// If nobody, do nothing (for now) - abort!
463 					if (gInterruptProvoker == NULL) return;
464 
465 					// use this guy as the "interrupted" fellow
466 					gBestToMakeSighting[ubLoop] = gInterruptProvoker;
467 					fOk = TRUE;
468 					break;
469 				}
470 				else if (gBestToMakeSighting[ubLoop]->bTeam == gTacticalStatus.ubCurrentTeam)
471 				{
472 					fOk = TRUE;
473 					break;
474 				}
475 			}
476 
477 			if ( fOk )
478 			{
479 				// this is the guy who gets "interrupted"; all else before him interrupted him
480 				AddToIntList(gBestToMakeSighting[ubLoop], FALSE, TRUE);
481 				for (UINT8 ubLoop2 = 0; ubLoop2 < ubLoop; ++ubLoop2)
482 				{
483 					AddToIntList(gBestToMakeSighting[ubLoop2], TRUE, TRUE);
484 				}
485 				DoneAddingToIntList();
486 			}
487 
488 		}
489 		for (UINT8 ubLoop = 0; ubLoop < BEST_SIGHTING_ARRAY_SIZE; ++ubLoop)
490 		{
491 			if (gBestToMakeSighting[ubLoop] != NULL)
492 			{
493 				gBestToMakeSighting[ubLoop]->bInterruptDuelPts = NO_INTERRUPT;
494 				SLOGD("RBSP (TB): done, resetting points for %d to zilch",
495 							gBestToMakeSighting[ubLoop]->ubID);
496 			}
497 		}
498 
499 #if defined FORCE_ASSERTS_ON
500 		FOR_EACH_MERC(i)
501 		{
502 			SOLDIERTYPE* const s = *i;
503 			AssertMsg(s->bInterruptDuelPts == NO_INTERRUPT, String("%s (%d) still has interrupt pts!", s->name.c_str(), s->ubID));
504 		}
505 #endif
506 	}
507 }
508 
509 
InitSightArrays()510 static void InitSightArrays()
511 {
512 	FOR_EACH(SOLDIERTYPE*, i, gBestToMakeSighting) *i = 0;
513 }
514 
515 
AddToShouldBecomeHostileOrSayQuoteList(SOLDIERTYPE * const s)516 void AddToShouldBecomeHostileOrSayQuoteList(SOLDIERTYPE* const s)
517 {
518 	UINT8 ubLoop;
519 
520 	Assert( gubNumShouldBecomeHostileOrSayQuote < SHOULD_BECOME_HOSTILE_SIZE );
521 
522 	if (s->bLife < OKLIFE) return;
523 
524 	// make sure not already in list
525 	for ( ubLoop = 0; ubLoop < gubNumShouldBecomeHostileOrSayQuote; ubLoop++ )
526 	{
527 		if (gShouldBecomeHostileOrSayQuote[ubLoop] == s) return;
528 	}
529 
530 	gShouldBecomeHostileOrSayQuote[gubNumShouldBecomeHostileOrSayQuote++] = s;
531 }
532 
533 
SelectSpeakerFromHostileOrSayQuoteList(void)534 static SOLDIERTYPE* SelectSpeakerFromHostileOrSayQuoteList(void)
535 {
536 	SOLDIERTYPE* speaker_list[SHOULD_BECOME_HOSTILE_SIZE];
537 	UINT8 ubLoop, ubNumProfiles = 0;
538 
539 	for ( ubLoop = 0; ubLoop < gubNumShouldBecomeHostileOrSayQuote; ubLoop++ )
540 	{
541 		SOLDIERTYPE* const pSoldier = gShouldBecomeHostileOrSayQuote[ubLoop];
542 		if ( pSoldier->ubProfile != NO_PROFILE )
543 		{
544 
545 			// make sure person can say quote!!!!
546 			gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags2 |= PROFILE_MISC_FLAG2_NEEDS_TO_SAY_HOSTILE_QUOTE;
547 
548 			if ( NPCHasUnusedHostileRecord( pSoldier->ubProfile, APPROACH_DECLARATION_OF_HOSTILITY ) )
549 			{
550 				speaker_list[ubNumProfiles++] = pSoldier;
551 			}
552 			else
553 			{
554 				// turn flag off again
555 				gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags2 &= ~PROFILE_MISC_FLAG2_NEEDS_TO_SAY_HOSTILE_QUOTE;
556 			}
557 
558 		}
559 	}
560 
561 	return ubNumProfiles == 0 ? NULL : speaker_list[Random(ubNumProfiles)];
562 }
563 
564 
CheckHostileOrSayQuoteList(void)565 void CheckHostileOrSayQuoteList( void )
566 {
567 	if ( gubNumShouldBecomeHostileOrSayQuote == 0 || !DialogueQueueIsEmpty() ||
568 		gfInTalkPanel || gfWaitingForTriggerTimer )
569 	{
570 		return;
571 	}
572 	else
573 	{
574 		UINT8 ubLoop;
575 
576 		SOLDIERTYPE* const speaker = SelectSpeakerFromHostileOrSayQuoteList();
577 		if (speaker == NULL)
578 		{
579 			// make sure everyone on this list is hostile
580 			for ( ubLoop = 0; ubLoop < gubNumShouldBecomeHostileOrSayQuote; ubLoop++ )
581 			{
582 				SOLDIERTYPE* const pSoldier = gShouldBecomeHostileOrSayQuote[ubLoop];
583 				if ( pSoldier->bNeutral )
584 				{
585 					MakeCivHostile( pSoldier, 2 );
586 					// make civ group, if any, hostile
587 					if ( pSoldier->bTeam == CIV_TEAM && pSoldier->ubCivilianGroup != NON_CIV_GROUP && gTacticalStatus.fCivGroupHostile[ pSoldier->ubCivilianGroup ] == CIV_GROUP_WILL_BECOME_HOSTILE )
588 					{
589 						gTacticalStatus.fCivGroupHostile[ pSoldier->ubCivilianGroup ] = CIV_GROUP_HOSTILE;
590 					}
591 				}
592 			}
593 
594 			// unpause all AI
595 			UnPauseAI();
596 			// reset the list
597 			std::fill(std::begin(gShouldBecomeHostileOrSayQuote), std::end(gShouldBecomeHostileOrSayQuote), nullptr);
598 			gubNumShouldBecomeHostileOrSayQuote = 0;
599 			//and return/go into combat
600 			if ( !(gTacticalStatus.uiFlags & INCOMBAT ) )
601 			{
602 				EnterCombatMode( CIV_TEAM );
603 			}
604 		}
605 		else
606 		{
607 			// pause all AI
608 			PauseAIUntilManuallyUnpaused();
609 			// stop everyone?
610 
611 			// We want to make this guy visible to the player.
612 			if (speaker->bVisible != TRUE)
613 			{
614 				gbPublicOpplist[OUR_TEAM][speaker->ubID] = HEARD_THIS_TURN;
615 				HandleSight(*speaker, SIGHT_LOOK | SIGHT_RADIO);
616 			}
617 			// trigger hater
618 			TriggerNPCWithIHateYouQuote(speaker->ubProfile);
619 		}
620 	}
621 }
622 
623 
624 static void ManLooksForOtherTeams(SOLDIERTYPE* pSoldier);
625 static void OurTeamRadiosRandomlyAbout(SOLDIERTYPE* about);
626 static void OtherTeamsLookForMan(SOLDIERTYPE* pOpponent);
627 
628 
HandleSight(SOLDIERTYPE & s,SightFlags const sight_flags)629 void HandleSight(SOLDIERTYPE& s, SightFlags const sight_flags)
630 {
631 	if (!s.bActive)                     return;
632 	if (!s.bInSector)                   return;
633 	if (s.uiStatusFlags & SOLDIER_DEAD) return;
634 
635 	gubSightFlags = sight_flags;
636 
637 	if (gubBestToMakeSightingSize != BEST_SIGHTING_ARRAY_SIZE_ALL_TEAMS_LOOK_FOR_ALL)
638 	{
639 		// If this is not being called as a result of all teams look for all, reset
640 		// array size
641 		if (gTacticalStatus.uiFlags & INCOMBAT)
642 		{
643 			// NB the incombat size is 0
644 			gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_INCOMBAT;
645 		}
646 		else
647 		{
648 			gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_NONCOMBAT;
649 		}
650 
651 		InitSightArrays();
652 	}
653 
654 	for (UINT32 i = 0; i != NUM_WATCHED_LOCS; ++i)
655 	{
656 		gfWatchedLocHasBeenIncremented[s.ubID][i] = FALSE;
657 	}
658 
659 	gfPlayerTeamSawCreatures = FALSE;
660 
661 	// store new situation value
662 	INT8 const temp_new_situation = s.bNewSituation;
663 	s.bNewSituation = FALSE;
664 
665 	// If we've been told to make this soldier look (& others look back at him)
666 	if (sight_flags & SIGHT_LOOK)
667 	{
668 		// If this soldier's under our control and well enough to look
669 		if (s.bLife >= OKLIFE)
670 		{
671 			// He looks for all other soldiers not on his own team
672 			ManLooksForOtherTeams(&s);
673 		}
674 
675 		// All soldiers under our control but not on ptr's team look for him
676 		OtherTeamsLookForMan(&s);
677 	}
678 
679 	// If we've been told that interrupts are possible as a result of sighting
680 	if ((gTacticalStatus.uiFlags & INCOMBAT) &&
681 		sight_flags & SIGHT_INTERRUPT)
682 	{
683 		ResolveInterruptsVs(&s, SIGHTINTERRUPT);
684 	}
685 
686 	if (gubBestToMakeSightingSize == BEST_SIGHTING_ARRAY_SIZE_NONCOMBAT)
687 	{
688 		HandleBestSightingPositionInRealtime();
689 	}
690 
691 	if (s.bNewSituation && !(s.uiStatusFlags & SOLDIER_PC))
692 	{
693 		HaultSoldierFromSighting(&s, TRUE);
694 	}
695 	s.bNewSituation = __max(s.bNewSituation, temp_new_situation);
696 
697 	// If we've been told to radio the results
698 	if (sight_flags & SIGHT_RADIO)
699 	{
700 		if (s.uiStatusFlags & SOLDIER_PC)
701 		{
702 			// update our team's public knowledge
703 			RadioSightings(&s, EVERYBODY, s.bTeam);
704 
705 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
706 			RadioSightings(&s, EVERYBODY, MILITIA_TEAM);
707 #endif
708 
709 			// If it's our local player's merc, revealing roofs and looking for items
710 			// is handled here, too
711 			if (IsOnOurTeam(s)) RevealRoofsAndItems(&s, TRUE);
712 		}
713 		else if (gGameOptions.ubDifficultyLevel >= DIF_LEVEL_MEDIUM)
714 		{
715 			// Unless in easy mode allow alerted enemies to radio
716 			// Don't allow admins to radio
717 			if (s.bTeam == ENEMY_TEAM &&
718 				gTacticalStatus.Team[ENEMY_TEAM].bAwareOfOpposition &&
719 				s.ubSoldierClass != SOLDIER_CLASS_ADMINISTRATOR)
720 			{
721 				RadioSightings(&s, EVERYBODY, s.bTeam);
722 			}
723 		}
724 
725 		s.bNewOppCnt = 0;
726 
727 		// If this soldier's NOT on our team (MAY be under our control, though!)
728 		if (!IsOnOurTeam(s)) OurTeamRadiosRandomlyAbout(&s); // radio about him only
729 
730 		// All non-humans under our control would now radio, if they were allowed to
731 		// radio automatically (but they're not). So just nuke new opp cnt.
732 		// NEW: under LOCALOPPLIST, humans on other teams now also radio in here
733 		FOR_EACH_MERC(i)
734 		{
735 			SOLDIERTYPE& them = **i;
736 			if (them.bLife < OKLIFE) continue;
737 
738 			// if this merc is on the same team as the target soldier
739 			if (them.bTeam == s.bTeam) continue; // he doesn't look (he ALWAYS knows about him)
740 
741 			// Other human team's merc report sightings to their teams now
742 			if (them.uiStatusFlags & SOLDIER_PC)
743 			{
744 				// Temporary for opplist synching - disable random order radioing
745 				// Exclude our own team, we've already done them, randomly
746 				if (them.bTeam != OUR_TEAM)
747 					RadioSightings(&them, &s, them.bTeam);
748 			}
749 			else if (gGameOptions.ubDifficultyLevel >= DIF_LEVEL_MEDIUM)
750 			{
751 				// Unless in easy mode allow alerted enemies to radio
752 				// Don't allow admins to radio
753 				if (them.bTeam == ENEMY_TEAM &&
754 					gTacticalStatus.Team[ENEMY_TEAM].bAwareOfOpposition &&
755 					them.ubSoldierClass != SOLDIER_CLASS_ADMINISTRATOR)
756 				{
757 					RadioSightings(&them, EVERYBODY, them.bTeam);
758 				}
759 			}
760 
761 			them.bNewOppCnt = 0;
762 		}
763 	}
764 
765 	// CJC August 13 2002: At the end of handling sight, reset sight flags to
766 	// allow interrupts in case an audio cue should cause someone to see an enemy
767 	gubSightFlags |= SIGHT_INTERRUPT;
768 }
769 
770 
771 // All mercs on our local team check if they should radio about him
OurTeamRadiosRandomlyAbout(SOLDIERTYPE * const about)772 static void OurTeamRadiosRandomlyAbout(SOLDIERTYPE* const about)
773 {
774 	// make a list of all of our team's mercs
775 	UINT radio_cnt = 0;
776 	SOLDIERTYPE* radio_men[20];
777 	FOR_EACH_IN_TEAM(s, OUR_TEAM)
778 	{
779 		// if this merc is in this sector, and well enough to look, then put him on
780 		// our list
781 		if (s->bInSector && s->bLife >= OKLIFE)
782 			radio_men[radio_cnt++] = s;
783 	}
784 
785 	// Now RANDOMLY handle each of the mercs on our list, until none remain (this
786 	// is all being done ONLY so that the mercs in the earliest merc slots do not
787 	// arbitrarily get the bulk of the sighting speech quote action, while the
788 	// later ones almost never pipe up, and is NOT strictly necessary, but a nice
789 	// improvement over original JA)
790 	for (; radio_cnt > 0; --radio_cnt)
791 	{
792 		// Pick a merc from one of the remaining slots at random
793 		const UINT chosen_idx = Random(radio_cnt);
794 		SOLDIERTYPE* const chosen = radio_men[chosen_idx];
795 
796 		// Handle radioing for that merc
797 		RadioSightings(chosen, about, chosen->bTeam);
798 		chosen->bNewOppCnt = 0;
799 
800 		// Move the contents of the last slot into the one just handled
801 		radio_men[chosen_idx] = radio_men[radio_cnt - 1];
802 	}
803 }
804 
805 
TeamNoLongerSeesMan(const UINT8 ubTeam,SOLDIERTYPE * const pOpponent,const SOLDIERTYPE * const exclude,const INT8 bIteration)806 static INT16 TeamNoLongerSeesMan(const UINT8 ubTeam, SOLDIERTYPE* const pOpponent, const SOLDIERTYPE* const exclude, const INT8 bIteration)
807 {
808 	// look for all mercs on the same team, check opplists for this soldier
809 	CFOR_EACH_IN_TEAM(pMate, ubTeam)
810 	{
811 		// if this "teammate" is me, myself, or I (whom we want to exclude)
812 		if (pMate == exclude)
813 			continue; // skip to next teammate, I KNOW I don't see him...
814 
815 		// if this merc is not on the same team
816 		if (pMate->bTeam != ubTeam)
817 			continue; // skip him, he's no teammate at all!
818 
819 		// if this merc is at base, on assignment, dead, unconscious
820 		if (!pMate->bInSector || pMate->bLife < OKLIFE)
821 			continue; // next merc
822 
823 		// if this teammate currently sees this opponent
824 		if (pMate->bOppList[pOpponent->ubID] == SEEN_CURRENTLY)
825 			return(FALSE); // that's all I need to know, get out of here
826 	}
827 
828 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
829 	if ( bIteration == 0 )
830 	{
831 		if (ubTeam == OUR_TEAM && IsTeamActive(MILITIA_TEAM))
832 		{
833 			// check militia team as well
834 			return TeamNoLongerSeesMan(MILITIA_TEAM, pOpponent, exclude, 1);
835 		}
836 		else if (ubTeam == MILITIA_TEAM && IsTeamActive(OUR_TEAM))
837 		{
838 			// check player team as well
839 			return TeamNoLongerSeesMan(OUR_TEAM, pOpponent, exclude, 1);
840 		}
841 	}
842 #endif
843 
844 	// none of my friends is currently seeing the guy, so return success
845 	return(TRUE);
846 }
847 
848 
DistanceSmellable(const SOLDIERTYPE * const pSubject)849 static INT16 DistanceSmellable(const SOLDIERTYPE* const pSubject)
850 {
851 	INT16 sDistVisible = STRAIGHT; // as a base
852 
853 	sDistVisible *= 2;
854 	//In hypothetical non-turnbased mode:
855 	//sDistVisible += 3;
856 
857 	if (pSubject)
858 	{
859 		if (pSubject->uiStatusFlags & SOLDIER_MONSTER)
860 		{
861 			// trying to smell a friend; change nothing
862 		}
863 		else
864 		{
865 			// smelling a human or animal; if they are coated with monster smell, distance shrinks
866 			sDistVisible = sDistVisible * (pSubject->bNormalSmell - pSubject->bMonsterSmell) /
867 					NORMAL_HUMAN_SMELL_STRENGTH;
868 			if (sDistVisible < 0)
869 			{
870 				sDistVisible = 0;
871 			}
872 		}
873 	}
874 	return( sDistVisible );
875 }
876 
MaxDistanceVisible(void)877 INT16 MaxDistanceVisible( void )
878 {
879 	return( STRAIGHT * 2 );
880 }
881 
882 
DistanceVisible(const SOLDIERTYPE * pSoldier,INT8 bFacingDir,INT8 bSubjectDir,INT16 sSubjectGridNo,INT8 bLevel)883 INT16 DistanceVisible(const SOLDIERTYPE* pSoldier, INT8 bFacingDir, INT8 bSubjectDir, INT16 sSubjectGridNo, INT8 bLevel)
884 {
885 	INT16 sDistVisible;
886 	INT8  bLightLevel;
887 
888 	// IMPORTANT! WhoIsThere2 can return a null-pointer for grid calcs
889 	const SOLDIERTYPE* const pSubject = WhoIsThere2(sSubjectGridNo, bLevel);
890 	// muzzleflash can only considered if there is actually a soldier on the grid_no
891 	BOOLEAN const hasMuzzleFlash = pSubject && pSubject->fMuzzleFlash;
892 
893 	if (pSoldier->uiStatusFlags & SOLDIER_MONSTER)
894 	{
895 		if ( !pSubject )
896 		{
897 			return( FALSE );
898 		}
899 		return DistanceSmellable(pSubject);
900 	}
901 
902 	if (pSoldier->bBlindedCounter > 0)
903 	{
904 		// we're bliiiiiiiiind!!!
905 		return( 0 );
906 	}
907 
908 	if ( bFacingDir == DIRECTION_IRRELEVANT && TANK( pSoldier ) )
909 	{
910 		// always calculate direction for tanks so we have something to work with
911 		bFacingDir = pSoldier->bDesiredDirection;
912 		bSubjectDir = (INT8) GetDirectionToGridNoFromGridNo( pSoldier->sGridNo, sSubjectGridNo );
913 		//bSubjectDir = atan8(pSoldier->sX,pSoldier->sY,pOpponent->sX,pOpponent->sY);
914 	}
915 
916 	if ( !TANK( pSoldier ) && ( bFacingDir == DIRECTION_IRRELEVANT || (pSoldier->uiStatusFlags & SOLDIER_ROBOT) || hasMuzzleFlash ) )
917 	{
918 		sDistVisible = MaxDistanceVisible();
919 	}
920 	else
921 	{
922 
923 		if (pSoldier->sGridNo == sSubjectGridNo)
924 		{
925 			// looking up or down or two people accidentally in same tile... don't want it to be 0!
926 			sDistVisible = MaxDistanceVisible();
927 		}
928 		else
929 		{
930 			sDistVisible = gbLookDistance[bFacingDir][bSubjectDir];
931 
932 			if ( sDistVisible == ANGLE && (pSoldier->bTeam == OUR_TEAM ||
933 				pSoldier->bAlertStatus >= STATUS_RED ) )
934 			{
935 				sDistVisible = STRAIGHT;
936 			}
937 
938 			sDistVisible *= 2;
939 
940 			if ( pSoldier->usAnimState == RUNNING )
941 			{
942 				if ( gbLookDistance[bFacingDir][bSubjectDir] != STRAIGHT )
943 				{
944 					// reduce sight when we're not looking in that direction...
945 					// (20%?)
946 					sDistVisible = (sDistVisible * 8) / 10;
947 				}
948 			}
949 		}
950 	}
951 
952 	if (pSoldier->bLevel != bLevel)
953 	{
954 		// add two tiles distance to visibility to/from roofs
955 		sDistVisible += 2;
956 	}
957 
958 	// now reduce based on light level; SHADE_MIN is the define for the
959 	// highest number the light can be
960 	bLightLevel = LightTrueLevel(sSubjectGridNo, bLevel);
961 
962 	if (hasMuzzleFlash)
963 		bLightLevel = MIN(bLightLevel, NORMAL_LIGHTLEVEL_DAY);
964 	sDistVisible = AdjustMaxSightRangeForEnvEffects(bLightLevel, sDistVisible);
965 
966 	// if we wanted to simulate desert-blindness, we'd bump up the light level
967 	// under certain conditions (daytime in the desert, for instance)
968 	if (bLightLevel < NORMAL_LIGHTLEVEL_DAY)
969 	{
970 		// greater than normal daylight level; check for sun goggles
971 		if (IsWearingHeadGear(*pSoldier, SUNGOGGLES))
972 		{
973 			// increase sighting distance by up to 2 tiles
974 			sDistVisible++;
975 			if (bLightLevel < NORMAL_LIGHTLEVEL_DAY - 1)
976 			{
977 				sDistVisible++;
978 			}
979 		}
980 	}
981 	else if (bLightLevel > NORMAL_LIGHTLEVEL_DAY + 5)
982 	{
983 		if (IsWearingHeadGear(*pSoldier, NIGHTGOGGLES) || IsWearingHeadGear(*pSoldier, UVGOGGLES) ||
984 			pSoldier->ubBodyType == BLOODCAT || AM_A_ROBOT(pSoldier))
985 		{
986 			if (IsWearingHeadGear(*pSoldier, NIGHTGOGGLES) || AM_A_ROBOT(pSoldier))
987 			{
988 				if (bLightLevel > NORMAL_LIGHTLEVEL_NIGHT)
989 				{
990 					// when it gets really dark, light-intensification goggles become less effective
991 					if ( bLightLevel < NORMAL_LIGHTLEVEL_NIGHT + 3 )
992 					{
993 						sDistVisible += (NIGHTSIGHTGOGGLES_BONUS / 2);
994 					}
995 					// else no help at all!
996 				}
997 				else
998 				{
999 					sDistVisible += NIGHTSIGHTGOGGLES_BONUS;
1000 				}
1001 
1002 			}
1003 			// UV goggles only function above ground... ditto for bloodcats
1004 			else if ( gbWorldSectorZ == 0 )
1005 			{
1006 				sDistVisible += UVGOGGLES_BONUS;
1007 			}
1008 
1009 		}
1010 
1011 		// give one step better vision for people with nightops
1012 		sDistVisible += 1 * NUM_SKILL_TRAITS(pSoldier, NIGHTOPS);
1013 	}
1014 
1015 
1016 	// let tanks see and be seen further (at night)
1017 	if ( (TANK( pSoldier ) && sDistVisible > 0) || (pSubject && TANK( pSubject ) ) )
1018 	{
1019 		sDistVisible = __max( sDistVisible + 5, MaxDistanceVisible() );
1020 	}
1021 
1022 	if ( gpWorldLevelData[ pSoldier->sGridNo ].ubExtFlags[ bLevel ] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS) )
1023 	{
1024 		if (!IsWearingHeadGear(*pSoldier, GASMASK))
1025 		{
1026 			// in gas without a gas mask; reduce max distance visible to 2 tiles at most
1027 			sDistVisible = __min( sDistVisible, 2 );
1028 		}
1029 	}
1030 
1031 	return(sDistVisible);
1032 }
1033 
1034 
1035 static void HandleManNoLongerSeen(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent, INT8* pPersOL, INT8* pbPublOL);
1036 static void DecideTrueVisibility(SOLDIERTYPE* pSoldier);
1037 
1038 
EndMuzzleFlash(SOLDIERTYPE * pSoldier)1039 void EndMuzzleFlash( SOLDIERTYPE * pSoldier )
1040 {
1041 	pSoldier->fMuzzleFlash = FALSE;
1042 
1043 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
1044 	if ( pSoldier->bTeam != OUR_TEAM && pSoldier->bTeam != MILITIA_TEAM )
1045 #else
1046 	if ( pSoldier->bTeam != OUR_TEAM )
1047 #endif
1048 	{
1049 		pSoldier->bVisible = 0; // indeterminate state
1050 	}
1051 
1052 	FOR_EACH_MERC(i)
1053 	{
1054 		SOLDIERTYPE* const pOtherSoldier = *i;
1055 		if ( pOtherSoldier->bOppList[ pSoldier->ubID ] == SEEN_CURRENTLY )
1056 		{
1057 			if ( pOtherSoldier->sGridNo != NOWHERE )
1058 			{
1059 				if ( PythSpacesAway( pOtherSoldier->sGridNo, pSoldier->sGridNo ) > DistanceVisible( pOtherSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, pSoldier->sGridNo, pSoldier->bLevel ) )
1060 				{
1061 					// if this guy can no longer see us, change to seen this turn
1062 					HandleManNoLongerSeen(pOtherSoldier, pSoldier,
1063 								&(pOtherSoldier->bOppList[pSoldier->ubID]),
1064 								&(gbPublicOpplist[pOtherSoldier->bTeam][pSoldier->ubID]));
1065 				}
1066 				// else this person is still seen, if the looker is on our side or the militia the person should stay visible
1067 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
1068 				else if ( pOtherSoldier->bTeam == OUR_TEAM || pOtherSoldier->bTeam == MILITIA_TEAM )
1069 #else
1070 				else if ( pOtherSoldier->bTeam == OUR_TEAM )
1071 #endif
1072 				{
1073 					pSoldier->bVisible = TRUE; // yes, still seen
1074 				}
1075 			}
1076 		}
1077 	}
1078 	DecideTrueVisibility(pSoldier);
1079 }
1080 
1081 
TurnOffEveryonesMuzzleFlashes(void)1082 void TurnOffEveryonesMuzzleFlashes(void)
1083 {
1084 	FOR_EACH_MERC(i)
1085 	{
1086 		SOLDIERTYPE* const s = *i;
1087 		if (s->fMuzzleFlash) EndMuzzleFlash(s);
1088 	}
1089 }
1090 
1091 
TurnOffTeamsMuzzleFlashes(UINT8 ubTeam)1092 void TurnOffTeamsMuzzleFlashes( UINT8 ubTeam )
1093 {
1094 	FOR_EACH_IN_TEAM(s, ubTeam)
1095 	{
1096 		if (s->fMuzzleFlash) EndMuzzleFlash(s);
1097 	}
1098 }
1099 
1100 
DecideHearing(const SOLDIERTYPE * pSoldier)1101 static INT8 DecideHearing(const SOLDIERTYPE* pSoldier)
1102 {
1103 	// calculate the hearing value for the merc...
1104 
1105 	INT8 bSlot;
1106 	INT8 bHearing;
1107 
1108 	if ( TANK( pSoldier ) )
1109 	{
1110 		return( -5 );
1111 	}
1112 	else if ( pSoldier->uiStatusFlags & SOLDIER_MONSTER )
1113 	{
1114 		return( -10 );
1115 	}
1116 
1117 	bHearing = 0;
1118 
1119 	if (pSoldier->bExpLevel > 3)
1120 	{
1121 		bHearing++;
1122 	}
1123 
1124 	// sharper hearing generally
1125 	bHearing += 1 * NUM_SKILL_TRAITS(pSoldier, NIGHTOPS);
1126 
1127 	bSlot = FindObj( pSoldier, EXTENDEDEAR );
1128 	if ( bSlot == HEAD1POS || bSlot == HEAD2POS)
1129 	{
1130 		// at 81-100% adds +5, at 61-80% adds +4, at 41-60% adds +3, etc.
1131 		bHearing += pSoldier->inv[bSlot].bStatus[0] / 20 + 1;
1132 	}
1133 
1134 	// adjust for dark conditions
1135 	switch ( ubAmbientLightLevel )
1136 	{
1137 		case 8:
1138 		case 9:
1139 			bHearing += 1;
1140 			break;
1141 		case 10:
1142 			bHearing += 2;
1143 			break;
1144 		case 11:
1145 		case 12:
1146 		case 13:
1147 		case 14:
1148 		case 15:
1149 			bHearing += 3;
1150 			// yet another bonus for nighttime
1151 			bHearing += 1 * NUM_SKILL_TRAITS(pSoldier, NIGHTOPS);
1152 			break;
1153 		default:
1154 			break;
1155 	}
1156 
1157 	return( bHearing );
1158 }
1159 
InitOpplistForDoorOpening(void)1160 void InitOpplistForDoorOpening( void )
1161 {
1162 	// this is called before generating a noise for opening a door so that
1163 	// the results of hearing the noise are lumped in with the results from AllTeamsLookForAll
1164 	gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_ALL_TEAMS_LOOK_FOR_ALL;
1165 	gfDelayResolvingBestSightingDueToDoor = TRUE; // will be turned off in allteamslookforall
1166 	SLOGD("HBSPIR: setting door flag on" );
1167 	// must init sight arrays here
1168 	InitSightArrays();
1169 }
1170 
1171 
AllTeamsLookForAll(UINT8 ubAllowInterrupts)1172 void AllTeamsLookForAll(UINT8 ubAllowInterrupts)
1173 {
1174 	if( ( gTacticalStatus.uiFlags & LOADING_SAVED_GAME ) )
1175 	{
1176 		return;
1177 	}
1178 
1179 	if (ubAllowInterrupts || !(gTacticalStatus.uiFlags & INCOMBAT) )
1180 	{
1181 		gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_ALL_TEAMS_LOOK_FOR_ALL;
1182 		if ( gfDelayResolvingBestSightingDueToDoor )
1183 		{
1184 			// turn off flag now, and skip init of sight arrays
1185 			SLOGD("HBSPIR: turning door flag off" );
1186 			gfDelayResolvingBestSightingDueToDoor = FALSE;
1187 		}
1188 		else
1189 		{
1190 			InitSightArrays();
1191 		}
1192 	}
1193 
1194 	FOR_EACH_MERC(i)
1195 	{
1196 		SOLDIERTYPE& s = **i;
1197 		if (s.bLife >= OKLIFE) HandleSight(s, SIGHT_LOOK); // no radio or interrupts yet
1198 	}
1199 
1200 	// the player team now radios about all sightings
1201 	FOR_EACH_IN_TEAM(i, OUR_TEAM)
1202 	{
1203 		HandleSight(*i, SIGHT_RADIO); // looking was done above
1204 	}
1205 
1206 	if ( !(gTacticalStatus.uiFlags & INCOMBAT) )
1207 	{
1208 		// decide who should get first turn
1209 		HandleBestSightingPositionInRealtime();
1210 		// this could have made us switch to combat mode
1211 		if ( (gTacticalStatus.uiFlags & INCOMBAT) )
1212 		{
1213 			gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_INCOMBAT;
1214 		}
1215 		else
1216 		{
1217 			gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_NONCOMBAT;
1218 		}
1219 	}
1220 	else if ( ubAllowInterrupts )
1221 	{
1222 		HandleBestSightingPositionInTurnbased();
1223 		// reset sighting size to 0
1224 		gubBestToMakeSightingSize = BEST_SIGHTING_ARRAY_SIZE_INCOMBAT;
1225 	}
1226 
1227 	// reset interrupt only guynum which may have been used
1228 	gInterruptProvoker = NULL;
1229 }
1230 
1231 
1232 static INT16 ManLooksForMan(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent, UINT8 ubCaller);
1233 
1234 
ManLooksForOtherTeams(SOLDIERTYPE * pSoldier)1235 static void ManLooksForOtherTeams(SOLDIERTYPE* pSoldier)
1236 {
1237 	SLOGD("MANLOOKSFOROTHERTEAMS ID %d(%s) team %d side %d",
1238 		pSoldier->ubID, pSoldier->name.c_str(), pSoldier->bTeam, pSoldier->bSide);
1239 
1240 	// one soldier (pSoldier) looks for every soldier on another team (pOpponent)
1241 	FOR_EACH_MERC(i)
1242 	{
1243 		SOLDIERTYPE* const pOpponent = *i;
1244 		if (pOpponent->bLife)
1245 		{
1246 			// and if he's on another team...
1247 			if (pSoldier->bTeam != pOpponent->bTeam)
1248 			{
1249 
1250 				// use both sides actual x,y co-ordinates (neither side's moving)
1251 				// if he sees this opponent...
1252 				ManLooksForMan(pSoldier,pOpponent,MANLOOKSFOROTHERTEAMS);
1253 
1254 				// OK, We now want to , if in non-combat, set visiblity to 0 if not visible still....
1255 				// This allows us to walk away from buddy and have them disappear instantly
1256 				if (!(gTacticalStatus.uiFlags & INCOMBAT))
1257 				{
1258 					if ( pOpponent->bVisible == 0)
1259 					{
1260 						pOpponent->bVisible = -1;
1261 					}
1262 				}
1263 
1264 			}
1265 		}
1266 	}
1267 }
1268 
1269 
1270 static void RemoveOneOpponent(SOLDIERTYPE* pSoldier);
1271 
1272 
HandleManNoLongerSeen(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pOpponent,INT8 * pPersOL,INT8 * pbPublOL)1273 static void HandleManNoLongerSeen(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent, INT8* pPersOL, INT8* pbPublOL)
1274 {
1275 	// if neither side is neutral AND
1276 	// if this soldier is an opponent (fights for different side)
1277 	if (!CONSIDERED_NEUTRAL( pOpponent, pSoldier ) && !CONSIDERED_NEUTRAL( pSoldier, pOpponent ) &&
1278 		(pSoldier->bSide != pOpponent->bSide))
1279 	{
1280 		RemoveOneOpponent(pSoldier);
1281 	}
1282 
1283 	// change personal opplist to indicate "seen this turn"
1284 	// don't use UpdatePersonal() here, because we're changing to a *lower*
1285 	// opplist value (which UpdatePersonal ignores) and we're not updating
1286 	// the lastKnown gridno at all, we're keeping it at its previous value
1287 
1288 	*pPersOL = SEEN_THIS_TURN;
1289 
1290 	if ( (pSoldier->ubCivilianGroup == KINGPIN_CIV_GROUP) && (pOpponent->bTeam == OUR_TEAM ) )
1291 	{
1292 		UINT8 const ubRoom = GetRoom(pOpponent->sGridNo);
1293 		if (IN_BROTHEL(ubRoom) && IN_BROTHEL_GUARD_ROOM(ubRoom))
1294 		{
1295 			// unauthorized!
1296 			// make guard run to block guard room
1297 			CancelAIAction(pSoldier);
1298 			RESETTIMECOUNTER( pSoldier->AICounter, 0 );
1299 			pSoldier->bNextAction = AI_ACTION_RUN;
1300 			pSoldier->usNextActionData = 13250;
1301 		}
1302 	}
1303 
1304 	// if opponent was seen publicly last time
1305 	if (*pbPublOL == SEEN_CURRENTLY)
1306 	{
1307 		// check if I was the only one who was seeing this guy (exlude ourselves)
1308 		// THIS MUST HAPPEN EVEN FOR ENEMIES, TO MAKE THEIR PUBLIC opplist DECAY!
1309 		if (TeamNoLongerSeesMan(pSoldier->bTeam, pOpponent, pSoldier, 0))
1310 		{
1311 			SLOGD("TeamNoLongerSeesMan: ID %d(%s) to ID %d",
1312 				pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID);
1313 
1314 			// don't use UpdatePublic() here, because we're changing to a *lower*
1315 			// opplist value (which UpdatePublic ignores) and we're not updating
1316 			// the lastKnown gridno at all, we're keeping it at its previous value
1317 			*pbPublOL = SEEN_THIS_TURN;
1318 
1319 			// ATE: Set visiblity to 0
1320 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
1321 			if ( (pSoldier->bTeam == OUR_TEAM || pSoldier->bTeam == MILITIA_TEAM) && !(pOpponent->bTeam == OUR_TEAM || pOpponent->bTeam == MILITIA_TEAM ) )
1322 #else
1323 			if ( pSoldier->bTeam == OUR_TEAM && pOpponent->bTeam != OUR_TEAM )
1324 #endif
1325 			{
1326 				pOpponent->bVisible = 0;
1327 			}
1328 		}
1329 	}
1330 	else
1331 		SLOGD("ManLooksForMan: ID %d(%s) to ID %d Personally seen, public %d",
1332 			pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID, *pbPublOL);
1333 
1334 	// if we had only seen the guy for an instant and now lost sight of him
1335 	if (gbSeenOpponents[pSoldier->ubID][pOpponent->ubID] == -1)
1336 		// we can't leave it -1, because InterruptDuel() uses the special -1
1337 		// value to know if we're only JUST seen the guy and screw up otherwise
1338 		// it's enough to know we have seen him before
1339 		gbSeenOpponents[pSoldier->ubID][pOpponent->ubID] = TRUE;
1340 
1341 }
1342 
1343 
1344 static void ManSeesMan(SOLDIERTYPE& s, SOLDIERTYPE& opponent, UINT8 caller2);
1345 
1346 
ManLooksForMan(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pOpponent,UINT8 ubCaller)1347 static INT16 ManLooksForMan(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent, UINT8 ubCaller)
1348 {
1349 	INT8 bDir,bAware = FALSE,bSuccess = FALSE;
1350 	INT16 sDistVisible,sDistAway;
1351 	INT8  *pPersOL,*pbPublOL;
1352 
1353 
1354 	/*
1355 	if (ptr->guynum >= MAX_NUM_SOLDIERS)
1356 	{
1357 		return(success);
1358 	}
1359 
1360 	if (oppPtr->guynum >= MAX_NUM_SOLDIERS)
1361 	{
1362 		return(success);
1363 	}*/
1364 
1365 	if (pSoldier == pOpponent)
1366 	{
1367 		SLOGD(ST::format("ManLooksForMan - Looking for self - ID {}({})",
1368 			pSoldier->ubID, pSoldier->name));
1369 		return(FALSE);
1370 	}
1371 
1372 	// if we're somehow looking while inactive, at base, dead or dying
1373 	if (!pSoldier->bActive || !pSoldier->bInSector || (pSoldier->bLife < OKLIFE))
1374 	{
1375 		SLOGD("ManLooksForMan - WE are inactive/dead etc ID %d(%s)to ID %d",
1376 			pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID);
1377 		return(FALSE);
1378 	}
1379 
1380 
1381 
1382 	// if we're somehow looking for a guy who is inactive, at base, or already dead
1383 	if (!pOpponent->bActive || !pOpponent->bInSector || pOpponent->bLife <= 0 ||
1384 		pOpponent->sGridNo == NOWHERE )
1385 	{
1386 		return(FALSE);
1387 	}
1388 
1389 
1390 	// if he's looking for a guy who is on the same team
1391 	if (pSoldier->bTeam == pOpponent->bTeam)
1392 	{
1393 		SLOGD("ManLooksForMan - SAME TEAM ID %d(%s)to ID %d",
1394 			pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID);
1395 		return(FALSE);
1396 	}
1397 
1398 	if (pSoldier->bLife < OKLIFE || pSoldier->fMercAsleep)
1399 	{
1400 		return( FALSE );
1401 	}
1402 
1403 	// NEED TO CHANGE THIS
1404 	/*
1405 	// don't allow unconscious persons to look, but COLLAPSED, etc. is OK
1406 	if (ptr->anitype[ptr->anim] == UNCONSCIOUS)
1407 		return(success);*/
1408 
1409 	if (pSoldier->ubBodyType == LARVAE_MONSTER || (pSoldier->uiStatusFlags & SOLDIER_VEHICLE &&
1410 		pSoldier->bTeam == OUR_TEAM))
1411 	{
1412 		// don't do sight for these
1413 		return( FALSE );
1414 	}
1415 
1416 
1417 	/*
1418 	if (ptrProjected)
1419 	{
1420 		// use looker's PROJECTED x,y co-ordinates (those of his next gridno)
1421 		fromX = ptr->destx;
1422 		fromY = ptr->desty;
1423 		fromGridno = ExtMen[ptr->guynum].nextGridno;
1424 	}
1425 	else
1426 	{
1427 		// use looker's ACTUAL x,y co-ordinates (those of gridno he's in now)
1428 		fromX = ptr->x;
1429 		fromY = ptr->y;
1430 		fromGridno = ptr->sGridNo;
1431 	}
1432 
1433 
1434 	if (oppPtrProjected)
1435 	{
1436 		// use target's PROJECTED x,y co-ordinates (those of his next gridno)
1437 		toX = oppPtr->destx;
1438 		toY = oppPtr->desty;
1439 		toGridno = ExtMen[oppPtr->guynum].nextGridno;
1440 	}
1441 	else
1442 	{
1443 		// use target's ACTUAL x,y co-ordinates (those of gridno he's in now)
1444 		toX = oppPtr->x;
1445 		toY = oppPtr->y;
1446 		toGridno = oppPtr->gridno;
1447 	}*/
1448 
1449 	pPersOL = &(pSoldier->bOppList[pOpponent->ubID]);
1450 	pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][pOpponent->ubID]);
1451 
1452 
1453 	// if soldier is known about (SEEN or HEARD within last few turns)
1454 	if (*pPersOL || *pbPublOL)
1455 	{
1456 		bAware = TRUE;
1457 
1458 		// then we look for him full viewing distance in EVERY direction
1459 		sDistVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, 0, pOpponent->sGridNo,
1460 						pOpponent->bLevel );
1461 	}
1462 	else // soldier is not currently known about
1463 	{
1464 		// distance we "see" then depends on the direction he is located from us
1465 		bDir = atan8(pSoldier->sX,pSoldier->sY,pOpponent->sX,pOpponent->sY);
1466 		// BIG NOTE: must use desdir instead of direction, since in a projected
1467 		// situation, the direction may still be changing if it's one of the first
1468 		// few animation steps when this guy's turn to do his stepped look comes up
1469 		sDistVisible = DistanceVisible(pSoldier,pSoldier->bDesiredDirection,bDir, pOpponent->sGridNo,
1470 						pOpponent->bLevel);
1471 	}
1472 
1473 	// calculate how many spaces away soldier is (using Pythagoras' theorem)
1474 	sDistAway = PythSpacesAway(pSoldier->sGridNo,pOpponent->sGridNo);
1475 	SLOGD("MANLOOKSFORMAN: ID %d(%s) to ID %d: sDistAway %d sDistVisible %d",
1476 		pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID, sDistAway, sDistVisible);
1477 
1478 	// if we see close enough to see the soldier
1479 	if (sDistAway <= sDistVisible)
1480 	{
1481 		// and we can trace a line of sight to his x,y coordinates
1482 		// must use the REAL opplist value here since we may or may not know of him
1483 		if (SoldierToSoldierLineOfSightTest(pSoldier,pOpponent,(UINT8)sDistVisible,bAware))
1484 		{
1485 			ManSeesMan(*pSoldier, *pOpponent, ubCaller);
1486 			bSuccess = TRUE;
1487 		}
1488 		else
1489 		{
1490 			SLOGD("FAILED LINEOFSIGHT: ID %d (%s)to ID %d Personally %d, public %d",
1491 				pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID, *pPersOL, *pbPublOL);
1492 		}
1493 	}
1494 
1495 	// if soldier seen personally LAST time could not be seen THIS time
1496 	if (!bSuccess && (*pPersOL == SEEN_CURRENTLY))
1497 	{
1498 		HandleManNoLongerSeen( pSoldier, pOpponent, pPersOL, pbPublOL );
1499 	}
1500 	else
1501 	{
1502 		if (!bSuccess)
1503 		{
1504 			SLOGD(
1505 				"NO LONGER VISIBLE ID %d (%s)to ID %d Personally %d, public %d success: %d",
1506 				pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID, *pPersOL, *pbPublOL, bSuccess);
1507 
1508 
1509 			// we didn't see the opponent, but since we didn't last time, we should be
1510 			//if (*pbPublOL)
1511 				//pOpponent->bVisible = TRUE;
1512 		}
1513 		else
1514 		{
1515 			SLOGD(
1516 				"COOL. STILL VISIBLE ID %d (%s)to ID %d Personally %d, public %d success: %d",
1517 				pSoldier->ubID, pSoldier->name.c_str(), pOpponent->ubID, *pPersOL, *pbPublOL, bSuccess);
1518 		}
1519 	}
1520 	return(bSuccess);
1521 }
1522 
1523 
1524 static void IncrementWatchedLoc(const SOLDIERTYPE* watcher, INT16 sGridNo, INT8 bLevel);
1525 static void SetWatchedLocAsUsed(UINT8 ubID, INT16 sGridNo, INT8 bLevel);
1526 static void MakeBloodcatsHostile(void);
1527 static void AddOneOpponent(SOLDIERTYPE* pSoldier);
1528 static void UpdatePersonal(SOLDIERTYPE* pSoldier, UINT8 ubID, INT8 bNewOpplist, INT16 sGridno, INT8 bLevel);
1529 
1530 
ManSeesMan(SOLDIERTYPE & s,SOLDIERTYPE & opponent,UINT8 const caller2)1531 static void ManSeesMan(SOLDIERTYPE& s, SOLDIERTYPE& opponent, UINT8 const caller2)
1532 {
1533 	if (s.ubID        >= MAX_NUM_SOLDIERS) return;
1534 	if (opponent.ubID >= MAX_NUM_SOLDIERS) return;
1535 
1536 	// if we're somehow looking while inactive, at base, dying or already dead
1537 	if (!s.bActive)       return;
1538 	if (!s.bInSector)     return;
1539 	if (s.bLife < OKLIFE) return;
1540 
1541 	// if we're somehow seeing a guy who is inactive, at base, or already dead
1542 	if (!opponent.bActive)   return;
1543 	if (!opponent.bInSector) return;
1544 	if (opponent.bLife <= 0) return;
1545 
1546 	// If we're somehow seeing a guy who is on the same team
1547 	if (s.bTeam == opponent.bTeam) return;
1548 
1549 	INT8   const old_opp_list = s.bOppList[opponent.ubID];
1550 	GridNo const opp_gridno   = opponent.sGridNo;
1551 	INT8   const opp_level    = opponent.bLevel;
1552 
1553 	bool new_opponent = false;
1554 	// If we're seeing a guy we didn't see on our last chance to look for him
1555 	if (s.bOppList[opponent.ubID] != SEEN_CURRENTLY)
1556 	{
1557 		if (opponent.bTeam == OUR_TEAM)
1558 		{
1559 			if (s.ubProfile != NO_PROFILE)
1560 			{
1561 				if (s.bTeam == CIV_TEAM)
1562 				{
1563 					bool not_added_to_list = true;
1564 					// If this person doing the sighting is a member of a civ group that
1565 					// hates us but this fact hasn't been revealed, change the side of
1566 					// these people now. This will make them non-neutral so AddOneOpponent
1567 					// will be called, and the guy will say his "I hate you" quote
1568 					if (s.bNeutral)
1569 					{
1570 						if (s.ubCivilianGroup != NON_CIV_GROUP &&
1571 							gTacticalStatus.fCivGroupHostile[s.ubCivilianGroup] >= CIV_GROUP_WILL_BECOME_HOSTILE)
1572 						{
1573 							AddToShouldBecomeHostileOrSayQuoteList(&s);
1574 							not_added_to_list = false;
1575 						}
1576 					}
1577 					else if (NPCHasUnusedRecordWithGivenApproach(s.ubProfile, APPROACH_DECLARATION_OF_HOSTILITY))
1578 					{
1579 						// only add if have something to say
1580 						AddToShouldBecomeHostileOrSayQuoteList(&s);
1581 						not_added_to_list = false;
1582 					}
1583 
1584 					if (not_added_to_list)
1585 					{
1586 						switch (s.ubProfile)
1587 						{
1588 							case CARMEN:
1589 								// Carmen goes to war (against Slay)
1590 								if (opponent.ubProfile == SLAY && s.bNeutral)
1591 								{
1592 									s.bAttitude = ATTACKSLAYONLY;
1593 									TriggerNPCRecord(s.ubProfile, 28);
1594 								}
1595 								break;
1596 
1597 							case ELDIN:
1598 								if (s.bNeutral)
1599 								{
1600 									// If player is in behind the ropes of the museum display or
1601 									// if alarm has gone off (status red)
1602 									UINT8 const room = GetRoom(opponent.sGridNo);
1603 									if ((!CheckFact(FACT_MUSEUM_OPEN, 0) &&
1604 										22 <= room && room <= 41) ||
1605 										CheckFact(FACT_MUSEUM_ALARM_WENT_OFF, 0) ||
1606 										room == 39 ||
1607 										room == 40 ||
1608 										FindObj(&opponent, CHALICE) != NO_SLOT)
1609 									{
1610 										SetFactTrue(FACT_MUSEUM_ALARM_WENT_OFF);
1611 										AddToShouldBecomeHostileOrSayQuoteList(&s);
1612 									}
1613 								}
1614 								break;
1615 
1616 							case JIM:
1617 							case JACK:
1618 							case OLAF:
1619 							case RAY:
1620 							case OLGA:
1621 							case TYRONE:
1622 								// change orders, reset action!
1623 								if (s.bOrders != SEEKENEMY)
1624 								{
1625 									s.bOrders = SEEKENEMY;
1626 									if (s.bOppCnt == 0)
1627 									{
1628 										// Didn't see anyone before!
1629 										CancelAIAction(&s);
1630 										SetNewSituation(&s);
1631 									}
1632 								}
1633 								break;
1634 
1635 							case ANGEL:
1636 								if (opponent.ubProfile == MARIA)
1637 								{
1638 									if (CheckFact(FACT_MARIA_ESCORTED_AT_LEATHER_SHOP, MARIA) == TRUE)
1639 									{
1640 										// She was rescued! yay!
1641 										TriggerNPCRecord(ANGEL, 12);
1642 									}
1643 								}
1644 								else if (CheckFact(FACT_ANGEL_LEFT_DEED, ANGEL) == TRUE &&
1645 									!CheckFact(FACT_ANGEL_MENTIONED_DEED, ANGEL))
1646 								{
1647 									CancelAIAction(&s);
1648 									s.sAbsoluteFinalDestination = NOWHERE;
1649 									EVENT_StopMerc(&s);
1650 									TriggerNPCRecord(ANGEL, 20);
1651 								}
1652 								break;
1653 
1654 							case JOE:
1655 							case ELLIOT:
1656 							{
1657 								MERCPROFILESTRUCT& p = GetProfile(s.ubProfile);
1658 								if (!(p.ubMiscFlags2 & PROFILE_MISC_FLAG2_SAID_FIRSTSEEN_QUOTE) &&
1659 									!AreInMeanwhile())
1660 								{
1661 									TriggerNPCRecord(s.ubProfile, 4);
1662 									p.ubMiscFlags2 |= PROFILE_MISC_FLAG2_SAID_FIRSTSEEN_QUOTE;
1663 								}
1664 								break;
1665 							}
1666 
1667 							default:
1668 								break;
1669 						}
1670 					}
1671 				}
1672 				else
1673 				{
1674 					switch (s.ubProfile)
1675 					{
1676 						case IGGY:
1677 						{
1678 							MERCPROFILESTRUCT& iggy = GetProfile(s.ubProfile);
1679 							if (!(iggy.ubMiscFlags2 & PROFILE_MISC_FLAG2_SAID_FIRSTSEEN_QUOTE))
1680 							{
1681 								TriggerNPCRecord(s.ubProfile, 9);
1682 								iggy.ubMiscFlags2 |= PROFILE_MISC_FLAG2_SAID_FIRSTSEEN_QUOTE;
1683 								gbPublicOpplist[OUR_TEAM][s.ubID] = HEARD_THIS_TURN;
1684 							}
1685 							break;
1686 						}
1687 					}
1688 				}
1689 			}
1690 			else if (s.bTeam == CIV_TEAM)
1691 			{
1692 				if (s.ubCivilianGroup != NON_CIV_GROUP &&
1693 					gTacticalStatus.fCivGroupHostile[s.ubCivilianGroup] >= CIV_GROUP_WILL_BECOME_HOSTILE && s.bNeutral)
1694 				{
1695 					AddToShouldBecomeHostileOrSayQuoteList(&s);
1696 				}
1697 				else if (s.ubCivilianGroup == KINGPIN_CIV_GROUP)
1698 				{
1699 					// Generic kingpin goon
1700 
1701 					// Check to see if we are looking at Maria or unauthorized personnel in the brothel
1702 					if (opponent.ubProfile == MARIA)
1703 					{
1704 						MakeCivHostile(&s, 2);
1705 						if (!(gTacticalStatus.uiFlags & INCOMBAT))
1706 						{
1707 							EnterCombatMode(s.bTeam);
1708 						}
1709 						SetFactTrue(FACT_MARIA_ESCAPE_NOTICED);
1710 					}
1711 					else
1712 					{
1713 						// JA2 Gold: only go hostile if see player IN guard room
1714 						UINT8 const room = GetRoom(opponent.sGridNo);
1715 						if (IN_BROTHEL_GUARD_ROOM(room))
1716 						{
1717 							// Unauthorized
1718 							MakeCivHostile(&s, 2);
1719 							if (!(gTacticalStatus.uiFlags & INCOMBAT))
1720 							{
1721 								EnterCombatMode(s.bTeam);
1722 							}
1723 						}
1724 					}
1725 				}
1726 				else if (s.ubCivilianGroup == HICKS_CIV_GROUP &&
1727 					!CheckFact(FACT_HICKS_MARRIED_PLAYER_MERC, 0))
1728 				{
1729 					// if before 6:05 or after 22:00, make hostile and enter combat
1730 					UINT32 const uiTime = GetWorldMinutesInDay();
1731 					if (uiTime < 365 || 1320 < uiTime)
1732 					{
1733 						// get off our farm!
1734 						MakeCivHostile(&s, 2);
1735 						if (!(gTacticalStatus.uiFlags & INCOMBAT))
1736 						{
1737 							EnterCombatMode(s.bTeam);
1738 							LocateSoldier(&s, TRUE);
1739 							INT16 sX;
1740 							INT16 sY;
1741 							GetSoldierScreenPos(&s, &sX, &sY);
1742 							BeginCivQuote(&s, CIV_QUOTE_HICKS_SEE_US_AT_NIGHT, 0, sX, sY);
1743 						}
1744 					}
1745 				}
1746 			}
1747 		}
1748 		else if (s.bTeam == OUR_TEAM)
1749 		{
1750 			switch (opponent.ubProfile)
1751 			{
1752 				case MIKE:
1753 					if (s.ubWhatKindOfMercAmI == MERC_TYPE__AIM_MERC &&
1754 						!(s.usQuoteSaidExtFlags & SOLDIER_QUOTE_SAID_EXT_MIKE))
1755 					{
1756 						if (gfMikeShouldSayHi == FALSE) gfMikeShouldSayHi = TRUE;
1757 						TacticalCharacterDialogue(&s, QUOTE_AIM_SEEN_MIKE);
1758 						s.usQuoteSaidExtFlags |= SOLDIER_QUOTE_SAID_EXT_MIKE;
1759 					}
1760 					break;
1761 
1762 				case JOEY:
1763 					if (!gfPlayerTeamSawJoey)
1764 					{
1765 						TacticalCharacterDialogue(&s, QUOTE_SPOTTED_JOEY);
1766 						gfPlayerTeamSawJoey = TRUE;
1767 					}
1768 					break;
1769 			}
1770 		}
1771 
1772 		// As soon as a bloodcat sees someone, it becomes hostile. This is safe to
1773 		// do here because we haven't made this new person someone we've seen yet
1774 		// (so we are assured we won't count 'em twice for oppcnt purposes)
1775 		if (s.ubBodyType == BLOODCAT)
1776 		{
1777 			bool do_roar = false;
1778 			if (s.bNeutral)
1779 			{
1780 				MakeBloodcatsHostile();
1781 				do_roar = true;
1782 			}
1783 			else if (s.bOppCnt == 0 && Random(2) == 0)
1784 			{
1785 				do_roar = true;
1786 			}
1787 			if (do_roar) PlayJA2Sample(BLOODCAT_ROAR, HIGHVOLUME, 1, MIDDLEPAN);
1788 		}
1789 		else if (opponent.ubBodyType == BLOODCAT && opponent.bNeutral)
1790 		{
1791 			MakeBloodcatsHostile();
1792 		}
1793 
1794 		// if both of us are not neutral, AND
1795 		// if this man is actually a true opponent (we're not on the same side)
1796 		if (!CONSIDERED_NEUTRAL(&opponent, &s) &&
1797 			!CONSIDERED_NEUTRAL(&s, &opponent) &&
1798 			s.bSide != opponent.bSide)
1799 		{
1800 			AddOneOpponent(&s);
1801 			SLOGD("ManSeesMan: ID %d(%s) to ID %d NEW TO ME",
1802 				s.ubID, s.name.c_str(), opponent.ubID);
1803 
1804 			// if we also haven't seen him earlier this turn
1805 			if (s.bOppList[opponent.ubID] != SEEN_THIS_TURN)
1806 			{
1807 				new_opponent = true;
1808 				++s.bNewOppCnt; // increment looker's NEW opponent count
1809 
1810 				IncrementWatchedLoc(&s, opponent.sGridNo, opponent.bLevel);
1811 
1812 				if (s.bTeam == OUR_TEAM &&
1813 					opponent.bTeam == ENEMY_TEAM &&
1814 					!CheckFact(FACT_FIRST_BATTLE_FOUGHT, 0))
1815 				{
1816 					SetFactTrue(FACT_FIRST_BATTLE_BEING_FOUGHT);
1817 				}
1818 			}
1819 			else
1820 			{
1821 				SetWatchedLocAsUsed(s.ubID, opponent.sGridNo, opponent.bLevel);
1822 			}
1823 
1824 			// we already know the soldier isn't SEEN_CURRENTLY,
1825 			// now check if he is really "NEW" ie. not expected to be there
1826 
1827 			// if the looker hasn't seen this opponent at all earlier this turn, OR
1828 			// if the opponent is not where the looker last thought him to be
1829 			if (s.bOppList[opponent.ubID] != SEEN_THIS_TURN ||
1830 				gsLastKnownOppLoc[s.ubID][opponent.ubID] != opp_gridno)
1831 			{
1832 				SetNewSituation(&s);  // force the looker to re-evaluate
1833 			}
1834 			else
1835 			{
1836 				// if we in a non-combat movement decision, presumably this is not
1837 				// something we were quite expecting, so make a new decision.  For
1838 				// other (combat) movement decisions, we took his position into account
1839 				// when we made it, so don't make us think again & slow things down.
1840 				switch (s.bAction)
1841 				{
1842 					case AI_ACTION_RANDOM_PATROL:
1843 					case AI_ACTION_SEEK_OPPONENT:
1844 					case AI_ACTION_SEEK_FRIEND:
1845 					case AI_ACTION_POINT_PATROL:
1846 					case AI_ACTION_LEAVE_WATER_GAS:
1847 					case AI_ACTION_SEEK_NOISE:
1848 						SetNewSituation(&s);  // force the looker to re-evaluate
1849 						break;
1850 				}
1851 			}
1852 		}
1853 	}
1854 	else
1855 	{
1856 		SLOGD("ManSeesMan: ID %d(%s) to ID %d ALREADYSEENCURRENTLY",
1857 			s.ubID, s.name.c_str(), opponent.ubID);
1858 	}
1859 	// Remember that the soldier is currently seen and his new location
1860 	UpdatePersonal(&s, opponent.ubID, SEEN_CURRENTLY, opp_gridno, opp_level);
1861 
1862 	if (caller2 == MANLOOKSFOROTHERTEAMS ||
1863 		caller2 == OTHERTEAMSLOOKFORMAN  ||
1864 		caller2 == CALLER_UNKNOWN) // unknown->hearing
1865 	{
1866 		if (gubBestToMakeSightingSize != BEST_SIGHTING_ARRAY_SIZE_INCOMBAT &&
1867 			gTacticalStatus.bBoxingState == NOT_BOXING &&
1868 			new_opponent)
1869 		{
1870 			if (gTacticalStatus.uiFlags & INCOMBAT)
1871 			{
1872 				// Presumably a door opening, we do require standard interrupt
1873 				// conditions
1874 				if (StandardInterruptConditionsMet(&s, &opponent, old_opp_list))
1875 				{
1876 					ReevaluateBestSightingPosition(&s, CalcInterruptDuelPts(&s, &opponent, TRUE));
1877 				}
1878 			}
1879 			else if (s.bTeam != OUR_TEAM || opponent.bLife >= OKLIFE)
1880 			{
1881 				// Require the enemy not to be dying if we are the sighter; in other
1882 				// words, always add for AI guys, and always add for people with life >=
1883 				// OKLIFE
1884 				ReevaluateBestSightingPosition(&s, CalcInterruptDuelPts(&s, &opponent, TRUE));
1885 			}
1886 		}
1887 	}
1888 
1889 	// if this man has never seen this opponent before in this sector
1890 	INT8& seen = gbSeenOpponents[s.ubID][opponent.ubID];
1891 	if (seen == FALSE)
1892 	{
1893 		// Remember that he is just seeing him now for the first time (-1)
1894 		seen = -1;
1895 	}
1896 	else
1897 	{
1898 		// Man is seeing an opponent AGAIN whom he has seen at least once before
1899 		seen = TRUE;
1900 	}
1901 
1902 	// if looker is on local team, and the enemy was invisible or "maybe"
1903 	// visible just prior to this
1904 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
1905 	if ((IsOnOurTeam(s) || s.bTeam == MILITIA_TEAM) && opponent.bVisible <= 0)
1906 #else
1907 	if (IsOnOurTeam(s) && opponent.bVisible <= 0)
1908 #endif
1909 	{
1910 		// If opponent was truly invisible, not just turned off temporarily (FALSE)
1911 		// then locate to him and set his locator flag
1912 		bool const do_locate = opponent.bVisible == -1;
1913 
1914 		// Make opponent visible (to us). Must do this BEFORE the locate since it
1915 		// checks for visibility
1916 		opponent.bVisible = TRUE;
1917 
1918 		// ATE: Cancel any fading going on
1919 		// ATE: Added for fade in
1920 		if (opponent.fBeginFade == 1 || opponent.fBeginFade == 2)
1921 		{
1922 			opponent.fBeginFade = FALSE;
1923 
1924 			MAP_ELEMENT& me = gpWorldLevelData[opponent.sGridNo];
1925 			opponent.ubFadeLevel = opponent.bLevel > 0 && me.pRoofHead ? me.pRoofHead->ubShadeLevel :
1926 						me.pLandHead->ubShadeLevel;
1927 
1928 			// Set levelnode shade level
1929 			if (opponent.pLevelNode)
1930 			{
1931 				opponent.pLevelNode->ubShadeLevel = opponent.ubFadeLevel;
1932 			}
1933 		}
1934 		SLOGD("ID %d (%s) MAKING %d VISIBLE",
1935 			s.ubID, s.name.c_str(), opponent.ubID);
1936 
1937 		if (do_locate)
1938 		{
1939 			// Change his anim speed
1940 			SetSoldierAniSpeed(&opponent);
1941 
1942 			if ((gTacticalStatus.uiFlags & INCOMBAT || gTacticalStatus.fVirginSector) &&
1943 				!opponent.bNeutral &&
1944 				s.bSide != opponent.bSide)
1945 			{
1946 				SlideTo(&opponent, SETLOCATOR);
1947 			}
1948 		}
1949 	}
1950 	else if (!IsOnOurTeam(s))
1951 	{
1952 		// ATE: Check stance, change to threatending
1953 		ReevaluateEnemyStance(&s, s.usAnimState);
1954 	}
1955 }
1956 
1957 
DecideTrueVisibility(SOLDIERTYPE * pSoldier)1958 static void DecideTrueVisibility(SOLDIERTYPE* pSoldier)
1959 {
1960 	// if his visibility is still in the special "limbo" state (FALSE)
1961 	if (pSoldier->bVisible == FALSE)
1962 	{
1963 		// then none of our team's merc turned him visible,
1964 		// therefore he now becomes truly invisible
1965 		pSoldier->bVisible = -1;
1966 
1967 		// Don;t adjust anim speed here, it's done once fade is over!
1968 	}
1969 }
1970 
1971 
OtherTeamsLookForMan(SOLDIERTYPE * pOpponent)1972 static void OtherTeamsLookForMan(SOLDIERTYPE* pOpponent)
1973 {
1974 	INT8 bOldOppList;
1975 
1976 	// if the guy we're looking for is NOT on our team AND is currently visible
1977 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
1978 	if ((pOpponent->bTeam != OUR_TEAM && pOpponent->bTeam != MILITIA_TEAM) &&
1979 		(pOpponent->bVisible >= 0 && pOpponent->bVisible < 2) && pOpponent->bLife)
1980 #else
1981 	if ((pOpponent->bTeam != OUR_TEAM) && (pOpponent->bVisible >= 0 &&
1982 		pOpponent->bVisible < 2) && pOpponent->bLife)
1983 #endif
1984 	{
1985 		// assume he's no longer visible, until one of our mercs sees him again
1986 		pOpponent->bVisible = 0;
1987 	}
1988 		SLOGD("OTHERTEAMSLOOKFORMAN ID %d(%s) team %d side %d",
1989 			pOpponent->ubID, pOpponent->name.c_str(), pOpponent->bTeam, pOpponent->bSide);
1990 
1991 	// all soldiers not on oppPtr's team now look for him
1992 	FOR_EACH_MERC(i)
1993 	{
1994 		SOLDIERTYPE* const pSoldier = *i;
1995 
1996 		// if this merc is active, in this sector, and well enough to look
1997 		if (pSoldier->bLife >= OKLIFE  && (pSoldier->ubBodyType != LARVAE_MONSTER))
1998 		{
1999 			// if this merc is on the same team as the target soldier
2000 			if (pSoldier->bTeam == pOpponent->bTeam)
2001 			{
2002 				continue; // he doesn't look (he ALWAYS knows about him)
2003 			}
2004 
2005 			bOldOppList = pSoldier->bOppList[pOpponent->ubID];
2006 
2007 			// this merc looks for the soldier in question
2008 			// use both sides actual x,y co-ordinates (neither side's moving)
2009 			if (ManLooksForMan(pSoldier,pOpponent,OTHERTEAMSLOOKFORMAN))
2010 			{
2011 				// if a new opponent is seen (which must be oppPtr himself)
2012 				//if ((gTacticalStatus.uiFlags & INCOMBAT) && pSoldier->bNewOppCnt)
2013 				// Calc interrupt points in non-combat because we might get an interrupt or be interrupted
2014 				// on our first turn
2015 
2016 				// if doing regular in-combat sighting (not on opening doors!)
2017 				if ( gubBestToMakeSightingSize == BEST_SIGHTING_ARRAY_SIZE_INCOMBAT )
2018 				{
2019 					if ((gTacticalStatus.uiFlags & INCOMBAT) && pSoldier->bNewOppCnt)
2020 					{
2021 						// as long as viewer meets minimum interrupt conditions
2022 						if (gubSightFlags & SIGHT_INTERRUPT && StandardInterruptConditionsMet(pSoldier, pOpponent, bOldOppList))
2023 						{
2024 							// calculate the interrupt duel points
2025 							pSoldier->bInterruptDuelPts = CalcInterruptDuelPts(pSoldier, pOpponent, TRUE);
2026 							SLOGD(
2027 								"Calculating int duel pts in OtherTeamsLookForMan, %d has %d points",
2028 								pSoldier->ubID, pSoldier->bInterruptDuelPts);
2029 						}
2030 						else
2031 						{
2032 							pSoldier->bInterruptDuelPts = NO_INTERRUPT;
2033 						}
2034 					}
2035 				}
2036 			}
2037 		}
2038 	}
2039 
2040 
2041 	// if he's not on our team
2042 	if (pOpponent->bTeam != OUR_TEAM)
2043 	{
2044 		DecideTrueVisibility(pOpponent);
2045 	}
2046 }
2047 
2048 
AddOneOpponent(SOLDIERTYPE * pSoldier)2049 static void AddOneOpponent(SOLDIERTYPE* pSoldier)
2050 {
2051 	INT8 bOldOppCnt = pSoldier->bOppCnt;
2052 
2053 	pSoldier->bOppCnt++;
2054 
2055 	if (!bOldOppCnt)
2056 	{
2057 		// if we hadn't known about opponents being here for sure prior to this
2058 		if (pSoldier->ubBodyType == LARVAE_MONSTER)
2059 		{
2060 			// never become aware of you!
2061 			return;
2062 		}
2063 
2064 		if (pSoldier->bAlertStatus < STATUS_RED)
2065 		{
2066 			CheckForChangingOrders(pSoldier);
2067 		}
2068 
2069 		pSoldier->bAlertStatus = STATUS_BLACK; // force black AI status right away
2070 
2071 		if (pSoldier->uiStatusFlags & SOLDIER_MONSTER)
2072 		{
2073 			pSoldier->ubCaller = NOBODY;
2074 			pSoldier->bCallPriority = 0;
2075 		}
2076 	}
2077 
2078 	if (pSoldier->bTeam == OUR_TEAM)
2079 	{
2080 		// adding an opponent for player; reset # of turns that we haven't seen an enemy
2081 		gTacticalStatus.bConsNumTurnsNotSeen = 0;
2082 	}
2083 
2084 }
2085 
2086 
RemoveOneOpponent(SOLDIERTYPE * pSoldier)2087 static void RemoveOneOpponent(SOLDIERTYPE* pSoldier)
2088 {
2089 	pSoldier->bOppCnt--;
2090 
2091 	if ( pSoldier->bOppCnt < 0 )
2092 	{
2093 		SLOGD("Oppcnt for %d (%s) tried to go below 0",
2094 			pSoldier->ubID, pSoldier->name.c_str());
2095 		pSoldier->bOppCnt = 0;
2096 	}
2097 
2098 	// if no opponents remain in sight, drop status to RED (but NOT newSit.!)
2099 	if (!pSoldier->bOppCnt)
2100 		pSoldier->bAlertStatus = STATUS_RED;
2101 }
2102 
2103 
2104 static void UpdatePublic(UINT8 ubTeam, SOLDIERTYPE* s, INT8 bNewOpplist, INT16 sGridno, INT8 bLevel);
2105 static void ResetLastKnownLocs(SOLDIERTYPE const&);
2106 
2107 
RemoveManAsTarget(SOLDIERTYPE * pSoldier)2108 void RemoveManAsTarget(SOLDIERTYPE *pSoldier)
2109 {
2110 
2111 	SOLDIERTYPE *pOpponent;
2112 	UINT8 ubTarget,ubLoop;
2113 
2114 
2115 	ubTarget = pSoldier->ubID;
2116 
2117 	// clean up the public opponent lists and locations
2118 	for (ubLoop = 0; ubLoop < MAXTEAMS; ubLoop++)
2119 	// never causes any additional looks
2120 	UpdatePublic(ubLoop, pSoldier, NOT_HEARD_OR_SEEN, NOWHERE, 0);
2121 
2122 	// clean up all opponent's opplists
2123 	for (ubLoop = 0; ubLoop < guiNumMercSlots; ubLoop++)
2124 	{
2125 		pOpponent = MercSlots[ ubLoop ];
2126 
2127 		// if the target is active, a true opponent and currently seen by this merc
2128 		if (pOpponent)
2129 		{
2130 			// check to see if OPPONENT considers US neutral
2131 			if ((pOpponent->bOppList[ubTarget] == SEEN_CURRENTLY) && !pOpponent->bNeutral &&
2132 				!CONSIDERED_NEUTRAL(pOpponent, pSoldier) && (pSoldier->bSide != pOpponent->bSide))
2133 			{
2134 				RemoveOneOpponent(pOpponent);
2135 			}
2136 			UpdatePersonal(pOpponent, ubTarget, NOT_HEARD_OR_SEEN,NOWHERE,0);
2137 			gbSeenOpponents[ubLoop][ubTarget] = FALSE;
2138 		}
2139 	}
2140 
2141 	ResetLastKnownLocs(*pSoldier);
2142 
2143 	TacticalTeamType* const tt = &gTacticalStatus.Team[pSoldier->bTeam];
2144 	if (tt->last_merc_to_radio == pSoldier) tt->last_merc_to_radio = NULL;
2145 }
2146 
2147 
UpdatePublic(const UINT8 ubTeam,SOLDIERTYPE * const s,const INT8 bNewOpplist,const INT16 sGridno,const INT8 bLevel)2148 static void UpdatePublic(const UINT8 ubTeam, SOLDIERTYPE* const s, const INT8 bNewOpplist, const INT16 sGridno, const INT8 bLevel)
2149 {
2150 	UINT8 ubTeamMustLookAgain = FALSE;
2151 
2152 	INT8* const pbPublOL = &gbPublicOpplist[ubTeam][s->ubID];
2153 
2154 	// if new opplist is more up-to-date, or we are just wiping it for some reason
2155 	if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][bNewOpplist - OLDEST_HEARD_VALUE] > 0) ||
2156 		(bNewOpplist == NOT_HEARD_OR_SEEN))
2157 	{
2158 		// if this team is becoming aware of a soldier it wasn't previously aware of
2159 		if ((bNewOpplist != NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
2160 			ubTeamMustLookAgain = TRUE;
2161 
2162 		// change the public opplist *BEFORE* anyone looks again or we'll recurse!
2163 		*pbPublOL = bNewOpplist;
2164 	}
2165 
2166 
2167 	// always update the gridno, no matter what
2168 	gsPublicLastKnownOppLoc[ubTeam][s->ubID] = sGridno;
2169 	gbPublicLastKnownOppLevel[ubTeam][s->ubID] = bLevel;
2170 
2171 	// if team has been told about a guy the team was completely unaware of
2172 	if (ubTeamMustLookAgain)
2173 	{
2174 		// then everyone on team who's not aware of guynum must look for him
2175 		FOR_EACH_IN_TEAM(pSoldier, ubTeam)
2176 		{
2177 			// if this soldier is active, in this sector, and well enough to look
2178 			if (pSoldier->bInSector && pSoldier->bLife >= OKLIFE && !(pSoldier->uiStatusFlags & SOLDIER_GASSED))
2179 			{
2180 				// if soldier isn't aware of guynum, give him another chance to see
2181 				if (pSoldier->bOppList[s->ubID] == NOT_HEARD_OR_SEEN)
2182 				{
2183 					if (ManLooksForMan(pSoldier, s, UPDATEPUBLIC))
2184 					{
2185 						// then he actually saw guynum because of our new public knowledge
2186 						// whether successful or not, whack newOppCnt.  Since this is a
2187 						// delayed reaction to a radio call, there's no chance of interrupt!
2188 						pSoldier->bNewOppCnt = 0;
2189 					}
2190 				}
2191 			}
2192 		}
2193 	}
2194 }
2195 
2196 
UpdatePersonal(SOLDIERTYPE * pSoldier,UINT8 ubID,INT8 bNewOpplist,INT16 sGridno,INT8 bLevel)2197 static void UpdatePersonal(SOLDIERTYPE* pSoldier, UINT8 ubID, INT8 bNewOpplist, INT16 sGridno, INT8 bLevel)
2198 {
2199 	// if new opplist is more up-to-date, or we are just wiping it for some reason
2200 	if ((gubKnowledgeValue[pSoldier->bOppList[ubID] - OLDEST_HEARD_VALUE][bNewOpplist - OLDEST_HEARD_VALUE] > 0) ||
2201 		(bNewOpplist == NOT_HEARD_OR_SEEN))
2202 	{
2203 		pSoldier->bOppList[ubID] = bNewOpplist;
2204 	}
2205 
2206 	// always update the gridno, no matter what
2207 	gsLastKnownOppLoc[pSoldier->ubID][ubID] = sGridno;
2208 	gbLastKnownOppLevel[pSoldier->ubID][ubID] = bLevel;
2209 }
2210 
2211 
ResetLastKnownLocs(SOLDIERTYPE const & s)2212 static void ResetLastKnownLocs(SOLDIERTYPE const& s)
2213 {
2214 	FOR_EACH_MERC(i)
2215 	{
2216 		const SoldierID tgt_id = (*i)->ubID;
2217 		gsLastKnownOppLoc[s.ubID][tgt_id] = NOWHERE;
2218 		// IAN added this June 14/97
2219 		gsPublicLastKnownOppLoc[s.bTeam][tgt_id] = NOWHERE;
2220 	}
2221 }
2222 
2223 
2224 /*
2225 // INITIALIZATION STUFF
2226 -------------------------
2227 // Upon loading a scenario, call these:
2228 InitOpponentKnowledgeSystem();
2229 
2230 // loop through all soldiers and for each soldier call
2231 InitSoldierOpplist(pSoldier);
2232 
2233 // call this once
2234 AllTeamsLookForAll(NO_INTERRUPTS); // no interrupts permitted this early
2235 
2236 
2237 // for each additional soldier created, call
2238 InitSoldierOpplist(pSoldier);
2239 HandleSight(pSoldier,SIGHT_LOOK);
2240 
2241 
2242 
2243 MOVEMENT STUFF
2244 -----------------
2245 // whenever new tile is reached, call
2246 HandleSight(pSoldier,SIGHT_LOOK);*/
2247 
2248 
InitOpponentKnowledgeSystem(void)2249 void InitOpponentKnowledgeSystem(void)
2250 {
2251 	INT32 iTeam, cnt, cnt2;
2252 
2253 	for (auto& i : gbSeenOpponents)
2254 	{
2255 		std::fill(std::begin(i), std::end(i), 0);
2256 	}
2257 	for (auto& i : gbPublicOpplist)
2258 	{
2259 		std::fill(std::begin(i), std::end(i), NOT_HEARD_OR_SEEN);
2260 	}
2261 
2262 	for (iTeam=0; iTeam < MAXTEAMS; iTeam++)
2263 	{
2264 		gubPublicNoiseVolume[iTeam] = 0;
2265 		gsPublicNoiseGridno[iTeam] = NOWHERE;
2266 		gbPublicNoiseLevel[iTeam] = 0;
2267 		for (cnt = 0; cnt < MAX_NUM_SOLDIERS; cnt++)
2268 		{
2269 			gsPublicLastKnownOppLoc[ iTeam ][ cnt ] = NOWHERE;
2270 		}
2271 	}
2272 
2273 	// initialize public last known locations for all teams
2274 	for (cnt = 0; cnt < MAX_NUM_SOLDIERS; cnt++)
2275 	{
2276 		for (cnt2 = 0; cnt2 < NUM_WATCHED_LOCS; cnt2++ )
2277 		{
2278 			gsWatchedLoc[ cnt ][ cnt2 ] = NOWHERE;
2279 			gubWatchedLocPoints[ cnt ][ cnt2 ] = 0;
2280 			gfWatchedLocReset[ cnt ][ cnt2 ] = FALSE;
2281 		}
2282 	}
2283 
2284 	for ( cnt = 0; cnt < SHOULD_BECOME_HOSTILE_SIZE; cnt++ )
2285 	{
2286 		gShouldBecomeHostileOrSayQuote[cnt] = NULL;
2287 	}
2288 
2289 	gubNumShouldBecomeHostileOrSayQuote = 0;
2290 }
2291 
2292 
2293 
InitSoldierOppList(SOLDIERTYPE & s)2294 void InitSoldierOppList(SOLDIERTYPE& s)
2295 {
2296 	std::fill(std::begin(s.bOppList), std::end(s.bOppList), NOT_HEARD_OR_SEEN);
2297 	s.bOppCnt = 0;
2298 	ResetLastKnownLocs(s);
2299 	std::fill_n(gbSeenOpponents[s.ubID], MAXMERCS, 0);
2300 }
2301 
2302 
BetweenTurnsVisibilityAdjustments()2303 void BetweenTurnsVisibilityAdjustments()
2304 {
2305 	// Make all soldiers on other teams, that are no longer seen, not visible
2306 	FOR_EACH_SOLDIER(i)
2307 	{
2308 		SOLDIERTYPE& s = *i;
2309 		if (!s.bInSector)            continue;
2310 		if (s.bLife == 0)            continue;
2311 		if (IsOnOurTeam(s))          continue;
2312 #ifdef WE_SEE_WHAT_MILITIA_SEES_AND_VICE_VERSA
2313 		if (s.bTeam == MILITIA_TEAM) continue;
2314 #endif
2315 		// Check if anyone on our team currently sees him (exclude NOBODY)
2316 		if (!TeamNoLongerSeesMan(OUR_TEAM, &s, 0, 0)) continue;
2317 		// then our team has lost sight of him
2318 		s.bVisible = -1; // Make him fully invisible
2319 	}
2320 }
2321 
2322 
SaySeenQuote(SOLDIERTYPE * pSoldier,BOOLEAN fSeenCreature,BOOLEAN fVirginSector)2323 static void SaySeenQuote(SOLDIERTYPE* pSoldier, BOOLEAN fSeenCreature, BOOLEAN fVirginSector)
2324 {
2325 	UINT8 ubNumEnemies = 0;
2326 	UINT8 ubNumAllies = 0;
2327 
2328 	if ( AreInMeanwhile( ) )
2329 	{
2330 		return;
2331 	}
2332 
2333 	// Check out for our under large fire quote
2334 	if ( !(pSoldier->usQuoteSaidFlags & SOLDIER_QUOTE_SAID_IN_SHIT ) )
2335 	{
2336 		// Get total enemies.
2337 		// Loop through all mercs in sector and count # of enemies
2338 		FOR_EACH_MERC(i)
2339 		{
2340 			const SOLDIERTYPE* const t = *i;
2341 			if (OK_ENEMY_MERC(t)) ++ubNumEnemies;
2342 		}
2343 
2344 		// OK, after this, check our guys
2345 		FOR_EACH_MERC(i)
2346 		{
2347 			const SOLDIERTYPE* const t = *i;
2348 			if (!OK_ENEMY_MERC(t) && t->bOppCnt >= ubNumEnemies / 2) ++ubNumAllies;
2349 		}
2350 
2351 		// now check!
2352 		if ( ( pSoldier->bOppCnt - ubNumAllies ) > 2 )
2353 		{
2354 			// Say quote!
2355 			TacticalCharacterDialogue( pSoldier, QUOTE_IN_TROUBLE_SLASH_IN_BATTLE );
2356 
2357 			pSoldier->usQuoteSaidFlags |= SOLDIER_QUOTE_SAID_IN_SHIT;
2358 
2359 			return;
2360 		}
2361 
2362 	}
2363 
2364 
2365 	if ( fSeenCreature == 1 )
2366 	{
2367 
2368 		// Is this our first time seeing them?
2369 		if ( gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags & PROFILE_MISC_FLAG_HAVESEENCREATURE )
2370 		{
2371 			// Are there multiplaes and we have not said this quote during this battle?
2372 			if ( !(	pSoldier->usQuoteSaidFlags & SOLDIER_QUOTE_SAID_MULTIPLE_CREATURES ) )
2373 			{
2374 				// Check for multiples!
2375 				ubNumEnemies = 0;
2376 
2377 				// Get total enemies.
2378 				// Loop through all mercs in sector and count # of enemies
2379 				FOR_EACH_MERC(i)
2380 				{
2381 					const SOLDIERTYPE* const t = *i;
2382 					if (OK_ENEMY_MERC(t) &&
2383 						t->uiStatusFlags & SOLDIER_MONSTER &&
2384 						pSoldier->bOppList[t->ubID] == SEEN_CURRENTLY)
2385 					{
2386 						++ubNumEnemies;
2387 					}
2388 				}
2389 
2390 				if ( ubNumEnemies > 2 )
2391 				{
2392 					// Yes, set flag
2393 					pSoldier->usQuoteSaidFlags |= SOLDIER_QUOTE_SAID_MULTIPLE_CREATURES;
2394 
2395 					// Say quote
2396 					TacticalCharacterDialogue( pSoldier, QUOTE_ATTACKED_BY_MULTIPLE_CREATURES );
2397 				}
2398 				else
2399 				{
2400 					TacticalCharacterDialogue( pSoldier, QUOTE_SEE_CREATURE );
2401 				}
2402 			}
2403 			else
2404 			{
2405 				TacticalCharacterDialogue( pSoldier, QUOTE_SEE_CREATURE );
2406 			}
2407 		}
2408 		else
2409 		{
2410 			// Yes, set flag
2411 			gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags |= PROFILE_MISC_FLAG_HAVESEENCREATURE;
2412 
2413 			TacticalCharacterDialogue( pSoldier, QUOTE_FIRSTTIME_GAME_SEE_CREATURE );
2414 		}
2415 	}
2416 	// 2 is for bloodcat...
2417 	else if ( fSeenCreature == 2 )
2418 	{
2419 		TacticalCharacterDialogue( pSoldier, QUOTE_SPOTTED_BLOODCAT );
2420 	}
2421 	else
2422 	{
2423 		if ( fVirginSector )
2424 		{
2425 			// First time we've seen a guy this sector
2426 			TacticalCharacterDialogue( pSoldier, QUOTE_SEE_ENEMY_VARIATION );
2427 		}
2428 		else
2429 		{
2430 			if(isEnglishVersion())
2431 			{
2432 				if ( Random( 100 ) < 30 )
2433 				{
2434 					DoMercBattleSound( pSoldier, BATTLE_SOUND_ENEMY );
2435 				}
2436 				else
2437 				{
2438 					TacticalCharacterDialogue( pSoldier, QUOTE_SEE_ENEMY );
2439 				}
2440 			}
2441 			else
2442 			{
2443 				TacticalCharacterDialogue( pSoldier, QUOTE_SEE_ENEMY );
2444 			}
2445 		}
2446 	}
2447 }
2448 
2449 
OurTeamSeesSomeone(SOLDIERTYPE * pSoldier,INT8 bNumReRevealed,INT8 bNumNewEnemies)2450 static void OurTeamSeesSomeone(SOLDIERTYPE* pSoldier, INT8 bNumReRevealed, INT8 bNumNewEnemies)
2451 {
2452 	if ( gTacticalStatus.fVirginSector )
2453 	{
2454 		// If we are in NPC dialogue now... stop!
2455 		DeleteTalkingMenu( );
2456 
2457 		// Say quote!
2458 		SaySeenQuote( pSoldier, gfPlayerTeamSawCreatures, TRUE);
2459 
2460 		HaultSoldierFromSighting( pSoldier, TRUE );
2461 
2462 		// Set virgin sector to false....
2463 		gTacticalStatus.fVirginSector = FALSE;
2464 	}
2465 	else
2466 	{
2467 		if ( pSoldier->bTeam == OUR_TEAM )
2468 		{
2469 			// STOP IF WE WERE MOVING....
2470 			/// Speek up!
2471 			if ( bNumReRevealed > 0 && bNumNewEnemies == 0 )
2472 			{
2473 				DoMercBattleSound( pSoldier, BATTLE_SOUND_CURSE1 );
2474 			}
2475 			else
2476 			{
2477 				SaySeenQuote( pSoldier, gfPlayerTeamSawCreatures, FALSE);
2478 			}
2479 
2480 			HaultSoldierFromSighting( pSoldier, TRUE );
2481 
2482 			if ( gTacticalStatus.fEnemySightingOnTheirTurn )
2483 			{
2484 				// Locate to our guy, then slide to enemy
2485 				LocateSoldier(pSoldier, SETLOCATOR);
2486 
2487 				// Now slide to other guy....
2488 				SlideTo(gTacticalStatus.enemy_sighting_on_their_turn_enemy, SETLOCATOR);
2489 			}
2490 
2491 			// Unset User's turn UI
2492 			UnSetUIBusy(pSoldier);
2493 		}
2494 	}
2495 
2496 	// OK, check what music mode we are in, change to battle if we're in battle
2497 	// If we are in combat....
2498 	if ( ( gTacticalStatus.uiFlags & INCOMBAT ) )
2499 	{
2500 		// If we are NOT in any music mode...
2501 		if ( gubMusicMode == MUSIC_NONE )
2502 		{
2503 			SetMusicMode( MUSIC_TACTICAL_BATTLE );
2504 		}
2505 	}
2506 }
2507 
2508 
RadioSightings(SOLDIERTYPE * const pSoldier,SOLDIERTYPE * const about,UINT8 ubTeamToRadioTo)2509 void RadioSightings(SOLDIERTYPE* const pSoldier, SOLDIERTYPE* const about, UINT8 ubTeamToRadioTo)
2510 {
2511 	INT32   iLoop;
2512 	UINT8   start,end,revealedEnemies = 0,unknownEnemies = 0;
2513 	//UINT8   oppIsCivilian;
2514 	INT8    *pPersOL,*pbPublOL; //,dayQuote;
2515 	BOOLEAN fContactSeen;
2516 	BOOLEAN fSawCreatureForFirstTime = FALSE;
2517 
2518 	SLOGD("RADIO SIGHTINGS: for %d about %d",
2519 		pSoldier->ubID, SOLDIER2ID(about));
2520 
2521 	gTacticalStatus.Team[pSoldier->bTeam].last_merc_to_radio = pSoldier;
2522 
2523 	// who are we radioing about?
2524 	if (about == EVERYBODY)
2525 	{
2526 		start = 0;
2527 		end   = MAXMERCS;
2528 	}
2529 	else
2530 	{
2531 		start = about->ubID;
2532 		end   = start + 1;
2533 	}
2534 
2535 	// hang a pointer to the start of our this guy's personal opplist
2536 	pPersOL = &(pSoldier->bOppList[start]);
2537 
2538 	// hang a pointer to the start of this guy's opponents in the public opplist
2539 	pbPublOL = &(gbPublicOpplist[ubTeamToRadioTo][start]);
2540 
2541 	SOLDIERTYPE* pOpponent = &GetMan(start);
2542 
2543 	// loop through every one of this guy's opponents
2544 	for (iLoop = start; iLoop < end; iLoop++,pOpponent++,pPersOL++,pbPublOL++)
2545 	{
2546 		fContactSeen = FALSE;
2547 		SLOGD("RS: checking %d", pOpponent->ubID);
2548 
2549 
2550 		// make sure this merc is active, here & still alive (unconscious OK)
2551 		if (!pOpponent->bActive || !pOpponent->bInSector || !pOpponent->bLife)
2552 		{
2553 			SLOGD("RS: inactive/notInSector/life %d",
2554 				pOpponent->ubID);
2555 			continue; // skip to the next merc
2556 		}
2557 
2558 		// if these two mercs are on the same SIDE, then they're NOT opponents
2559 		// NEW: Apr. 21 '96: must allow ALL non-humans to get radioed about
2560 		if ((pSoldier->bSide == pOpponent->bSide) && (pOpponent->uiStatusFlags & SOLDIER_PC))
2561 		{
2562 			SLOGD("RS: same side %d", pSoldier->bSide);
2563 			continue; // skip to the next merc
2564 		}
2565 
2566 		// if we personally don't know a thing about this opponent
2567 		if (*pPersOL == NOT_HEARD_OR_SEEN)
2568 		{
2569 			SLOGD("RS: not heard or seen");
2570 			continue; // skip to the next opponent
2571 		}
2572 
2573 		// if personal knowledge is NOT more up to date and NOT the same as public
2574 		if ((!gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pPersOL - OLDEST_HEARD_VALUE]) &&
2575 			(*pbPublOL != *pPersOL))
2576 		{
2577 			SLOGD("RS: no new knowledge per %d pub %d",
2578 				*pPersOL, *pbPublOL);
2579 			continue; // skip to the next opponent
2580 		}
2581 		SLOGD("RS: made it!");
2582 
2583 		// if it's our merc, and he currently sees this opponent
2584 		if (IsOnOurTeam(*pSoldier) &&
2585 			*pPersOL == SEEN_CURRENTLY &&
2586 			pOpponent->bSide != pSoldier->bSide &&
2587 			!pOpponent->bNeutral)
2588 		{
2589 			// don't care whether and how many new enemies are seen if everyone visible
2590 			// and he's healthy enough to be a threat (so is worth talking about)
2591 
2592 			// do the following if we're radioing to our own team; if radioing to militia
2593 			// then alert them instead
2594 			if ( ubTeamToRadioTo != MILITIA_TEAM )
2595 			{
2596 				if (!gbShowEnemies && (pOpponent->bLife >= OKLIFE))
2597 				{
2598 					// if this enemy has not been publicly seen or heard recently
2599 					if (*pbPublOL == NOT_HEARD_OR_SEEN)
2600 					{
2601 						// chalk up another "unknown" enemy
2602 						unknownEnemies++;
2603 
2604 						fContactSeen = TRUE;
2605 
2606 						// now the important part: does this enemy see him/her back?
2607 						if (pOpponent->bOppList[pSoldier->ubID] != SEEN_CURRENTLY)
2608 						{
2609 							// EXPERIENCE GAIN (10): Discovered a new enemy without being seen
2610 							StatChange(*pSoldier, EXPERAMT, 10, FROM_SUCCESS);
2611 						}
2612 					}
2613 					else
2614 					{
2615 
2616 						// if he has publicly not been seen now, or anytime during this turn
2617 						if ((*pbPublOL != SEEN_CURRENTLY) && (*pbPublOL != SEEN_THIS_TURN))
2618 						{
2619 							// chalk up another "revealed" enemy
2620 							revealedEnemies++;
2621 							fContactSeen = TRUE;
2622 						}
2623 					}
2624 
2625 					if ( fContactSeen )
2626 					{
2627 						if ( pSoldier->bTeam == OUR_TEAM )
2628 						{
2629 							if ( gTacticalStatus.ubCurrentTeam != OUR_TEAM )
2630 							{
2631 								// Save some stuff!
2632 								if (gTacticalStatus.fEnemySightingOnTheirTurn)
2633 								{
2634 									// this has already come up so turn OFF the
2635 									// pause-all-anims flag for the previous
2636 									// person and set it for this next person
2637 									gTacticalStatus.enemy_sighting_on_their_turn_enemy->fPauseAllAnimation = FALSE;
2638 								}
2639 								else
2640 								{
2641 									gTacticalStatus.fEnemySightingOnTheirTurn = TRUE;
2642 								}
2643 								gTacticalStatus.enemy_sighting_on_their_turn_enemy = pOpponent;
2644 								gTacticalStatus.uiTimeSinceDemoOn = GetJA2Clock( );
2645 
2646 								pOpponent->fPauseAllAnimation = TRUE;
2647 
2648 							}
2649 						}
2650 
2651 						if ( pOpponent->uiStatusFlags & SOLDIER_MONSTER )
2652 						{
2653 							gfPlayerTeamSawCreatures = TRUE;
2654 						}
2655 
2656 						// ATE: Added for bloodcat...
2657 						if ( pOpponent->ubBodyType == BLOODCAT )
2658 						{
2659 							// 2 is for bloodcat
2660 							gfPlayerTeamSawCreatures = 2;
2661 						}
2662 
2663 					}
2664 
2665 					if ( pOpponent->uiStatusFlags & SOLDIER_MONSTER )
2666 					{
2667 						if ( !(gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags & PROFILE_MISC_FLAG_HAVESEENCREATURE) )
2668 						{
2669 							fSawCreatureForFirstTime = TRUE;
2670 						}
2671 					}
2672 
2673 				}
2674 			}
2675 			else
2676 			{
2677 				// radioing to militia that we saw someone! alert them!
2678 				if (IsTeamActive(MILITIA_TEAM) &&
2679 					!gTacticalStatus.Team[MILITIA_TEAM].bAwareOfOpposition)
2680 				{
2681 					HandleInitialRedAlert(MILITIA_TEAM);
2682 				}
2683 			}
2684 		} // end of our team's merc sees new opponent
2685 
2686 		// IF WE'RE HERE, OUR PERSONAL INFORMATION IS AT LEAST AS UP-TO-DATE
2687 		// AS THE PUBLIC KNOWLEDGE, SO WE WILL REPLACE THE PUBLIC KNOWLEDGE
2688 		SLOGD("UPDATE PUBLIC: soldier %d SEEING soldier %d",
2689 			pSoldier->ubID, pOpponent->ubID);
2690 		UpdatePublic(ubTeamToRadioTo, pOpponent, *pPersOL,
2691 				gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID],
2692 				gbLastKnownOppLevel[pSoldier->ubID][pOpponent->ubID]);
2693 	}
2694 
2695 
2696 	// if soldier heard a misc noise more important that his team's public one
2697 	if (pSoldier->ubNoiseVolume > gubPublicNoiseVolume[ubTeamToRadioTo])
2698 	{
2699 		// replace the soldier's team's public noise with his
2700 		gsPublicNoiseGridno[ubTeamToRadioTo] 	= pSoldier->sNoiseGridno;
2701 		gbPublicNoiseLevel[ubTeamToRadioTo] 	= pSoldier->bNoiseLevel;
2702 		gubPublicNoiseVolume[ubTeamToRadioTo] 	= pSoldier->ubNoiseVolume;
2703 	}
2704 
2705 
2706 	// if this soldier is on the local team
2707 	if (IsOnOurTeam(*pSoldier))
2708 	{
2709 		// don't trigger sighting quotes or stop merc's movement if everyone visible
2710 		//if (!(gTacticalStatus.uiFlags & SHOW_ALL_MERCS))
2711 		{
2712 			// if we've revealed any enemies, or seen any previously unknown enemies
2713 			if (revealedEnemies || unknownEnemies)
2714 			{
2715 				// First check for a virgin map and set to false if we see our first guy....
2716 				// Only if this guy is an ememy!
2717 
2718 				OurTeamSeesSomeone( pSoldier, revealedEnemies, unknownEnemies );
2719 			}
2720 			else if (fSawCreatureForFirstTime)
2721 			{
2722 				gMercProfiles[ pSoldier->ubProfile ].ubMiscFlags |= PROFILE_MISC_FLAG_HAVESEENCREATURE;
2723 				TacticalCharacterDialogue( pSoldier, QUOTE_FIRSTTIME_GAME_SEE_CREATURE );
2724 			}
2725 
2726 		}
2727 	}
2728 }
2729 
2730 
2731 
DebugSoldierPage1()2732 void DebugSoldierPage1()
2733 {
2734 	INT32 const h = DEBUG_PAGE_LINE_HEIGHT;
2735 
2736 	const SOLDIERTYPE* const s = FindSoldierFromMouse();
2737 	if (s != NULL)
2738 	{
2739 		MPageHeader(ST::format("DEBUG SOLDIER PAGE ONE, GRIDNO {}", s->sGridNo));
2740 
2741 		INT32 y = DEBUG_PAGE_START_Y;
2742 
2743 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ID:",   s->ubID);
2744 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "TEAM:", s->bTeam);
2745 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "SIDE:", s->bSide);
2746 
2747 		MHeader(DEBUG_PAGE_FIRST_COLUMN, y +=  h, "STATUS FLAGS:");
2748 		MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y, ST::format("{x}", s->uiStatusFlags));
2749 
2750 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "HUMAN:",    gTacticalStatus.Team[s->bTeam].bHuman);
2751 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "APs:",      s->bActionPoints);
2752 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Breath:",   s->bBreath);
2753 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Life:",     s->bLife);
2754 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "LifeMax:",  s->bLifeMax);
2755 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Bleeding:", s->bBleeding);
2756 
2757 		y = DEBUG_PAGE_START_Y;
2758 
2759 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Agility:",               s->bAgility,      EffectiveAgility(s));
2760 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Dexterity:",             s->bDexterity,    EffectiveDexterity(s));
2761 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Strength:",              s->bStrength);
2762 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Wisdom:",                s->bWisdom,       EffectiveWisdom(s));
2763 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Exp Lvl:",               s->bExpLevel,     EffectiveExpLevel(s));
2764 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Mrksmnship",             s->bMarksmanship, EffectiveMarksmanship(s));
2765 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Mechanical:",            s->bMechanical);
2766 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Explosive:",             s->bExplosive);
2767 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Medical:",               s->bMedical);
2768 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Drug Effects:",          s->bDrugEffect[0]);
2769 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Drug Side Effects:",     s->bDrugSideEffect[0]);
2770 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Booze Effects:",         s->bDrugEffect[1]);
2771 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Hangover Side Effects:", s->bDrugSideEffect[1]);
2772 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "AI has Keys:",           s->bHasKeys);
2773 	}
2774 	else if (GetMouseMapPos() != NOWHERE)
2775 	{
2776 		MPageHeader("DEBUG LAND PAGE ONE");
2777 	}
2778 }
2779 
DebugSoldierPage2()2780 void DebugSoldierPage2()
2781 {
2782 	static const char* const gzDirectionStr[] =
2783 	{
2784 		"NORTHEAST",
2785 		"EAST",
2786 		"SOUTHEAST",
2787 		"SOUTH",
2788 		"SOUTHWEST",
2789 		"WEST",
2790 		"NORTHWEST",
2791 		"NORTH"
2792 	};
2793 
2794 	INT32 const h = DEBUG_PAGE_LINE_HEIGHT;
2795 
2796 	const SOLDIERTYPE* const s = FindSoldierFromMouse();
2797 	if (s != NULL)
2798 	{
2799 		MPageHeader(ST::format("DEBUG SOLDIER PAGE TWO, GRIDNO {}", s->sGridNo));
2800 
2801 		INT32 y = DEBUG_PAGE_START_Y;
2802 
2803 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ID:", s->ubID);
2804 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Body Type:", s->ubBodyType);
2805 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Opp Cnt:", s->bOppCnt);
2806 
2807 		ST::string opp_header;
2808 		INT8    const* opp_list = s->bOppList;
2809 		if (s->bTeam == OUR_TEAM || s->bTeam == MILITIA_TEAM)	// look at 8 to 15 opplist entries
2810 		{
2811 			opp_header = "Opplist B:";
2812 			opp_list += 20;
2813 		}
2814 		else	// team 1 - enemies so look at first 8 (0-7) opplist entries
2815 		{
2816 			opp_header = "OppList A:";
2817 		}
2818 		MHeader(DEBUG_PAGE_FIRST_COLUMN, y += h, opp_header);
2819 		MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y, ST::format("{} {} {} {} {} {} {} {}",
2820 			opp_list[0], opp_list[1], opp_list[2], opp_list[3],
2821 			opp_list[4], opp_list[5], opp_list[6], opp_list[7])
2822 		);
2823 
2824 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Visible:",     s->bVisible);
2825 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Direction:",   gzDirectionStr[s->bDirection]);
2826 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "DesDirection", gzDirectionStr[s->bDesiredDirection]);
2827 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "GridNo:",      s->sGridNo);
2828 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Dest:",        s->sFinalDestination);
2829 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Path Size:",   s->ubPathDataSize);
2830 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Path Index:",  s->ubPathIndex);
2831 
2832 		MHeader(DEBUG_PAGE_FIRST_COLUMN, y += h, "First 3 Steps:");
2833 		MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y, ST::format("{} {} {}",
2834 			s->ubPathingData[0],
2835 			s->ubPathingData[1],
2836 			s->ubPathingData[2])
2837 		);
2838 
2839 		MHeader(DEBUG_PAGE_FIRST_COLUMN, y += h, "Next 3 Steps:");
2840 		MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y, ST::format("{} {} {}",
2841 			s->ubPathingData[s->ubPathIndex],
2842 			s->ubPathingData[s->ubPathIndex + 1],
2843 			s->ubPathingData[s->ubPathIndex + 2])
2844 		);
2845 
2846 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "FlashInd:",    s->fFlashLocator);
2847 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ShowInd:",     s->fShowLocator);
2848 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Main hand:",   ShortItemNames[s->inv[HANDPOS].usItem]);
2849 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Second hand:", ShortItemNames[s->inv[SECONDHANDPOS].usItem]);
2850 
2851 		const GridNo map_pos = GetMouseMapPos();
2852 		if (map_pos != NOWHERE)
2853 		{
2854 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "CurrGridNo:", map_pos);
2855 		}
2856 	}
2857 	else
2858 	{
2859 		const GridNo map_pos = GetMouseMapPos();
2860 		if (map_pos == NOWHERE) return;
2861 
2862 		MPageHeader("DEBUG LAND PAGE TWO");
2863 
2864 		INT32                    y  = DEBUG_PAGE_START_Y;
2865 		MAP_ELEMENT const* const me = &gpWorldLevelData[map_pos];
2866 
2867 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Land Raised:", me->sHeight);
2868 
2869 		LEVELNODE const* const land_head = me->pLandHead;
2870 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Land Node:", land_head);
2871 		if (land_head != NULL)
2872 		{
2873 			UINT16 const idx = land_head->usIndex;
2874 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Land Node:", idx);
2875 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Full Land:", gTileDatabase[idx].ubFullTile);
2876 		}
2877 
2878 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Land St Node:", me->pLandStart);
2879 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "GRIDNO:",       map_pos);
2880 
2881 		if (me->uiFlags & MAPELEMENT_MOVEMENT_RESERVED)
2882 		{
2883 			MPrint(  DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y += h, ST::format("Merc: {}",  me->ubReservedSoldierID));
2884 			MPrint( DEBUG_PAGE_FIRST_COLUMN, y,      "RESERVED MOVEMENT FLAG ON:");
2885 		}
2886 
2887 		LEVELNODE const* const node = GetCurInteractiveTile();
2888 		if (node != NULL)
2889 		{
2890 			MPrint(  DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y += h, ST::format("Tile: {}", node->usIndex));
2891 			MPrint( DEBUG_PAGE_FIRST_COLUMN, y,      "ON INT TILE");
2892 		}
2893 
2894 		if (me->uiFlags & MAPELEMENT_REVEALED)
2895 		{
2896 			MPrint(DEBUG_PAGE_FIRST_COLUMN, y += h, "REVEALED");
2897 		}
2898 
2899 		if (me->uiFlags & MAPELEMENT_RAISE_LAND_START)
2900 		{
2901 			MPrint(DEBUG_PAGE_FIRST_COLUMN, y += h, "Land Raise Start");
2902 		}
2903 
2904 		if (me->uiFlags & MAPELEMENT_RAISE_LAND_END)
2905 		{
2906 			MPrint(DEBUG_PAGE_FIRST_COLUMN, y += h, "Raise Land End");
2907 		}
2908 
2909 		if (gubWorldRoomInfo[map_pos] != NO_ROOM)
2910 		{
2911 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Room Number", gubWorldRoomInfo[map_pos]);
2912 		}
2913 
2914 		if (me->ubExtFlags[0] & MAPELEMENT_EXT_NOBURN_STRUCT)
2915 		{
2916 			MPrint(0, y += h, "Don't Use Burn Through For Soldier");
2917 		}
2918 	}
2919 }
2920 
2921 
DebugSoldierPage3()2922 void DebugSoldierPage3()
2923 {
2924 	static const char* const gzAlertStr[] =
2925 	{
2926 		"GREEN",
2927 		"YELLOW",
2928 		"RED",
2929 		"BLACK"
2930 	};
2931 
2932 	INT32 const h = DEBUG_PAGE_LINE_HEIGHT;
2933 
2934 	const SOLDIERTYPE* const s = FindSoldierFromMouse();
2935 	if (s != NULL)
2936 	{
2937 		MPageHeader(ST::format("DEBUG SOLDIER PAGE THREE, GRIDNO {}", s->sGridNo));
2938 
2939 		INT32 y = DEBUG_PAGE_START_Y;
2940 
2941 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ID:",     s->ubID);
2942 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Action:", gzActionStr[s->bAction]);
2943 
2944 		if (s->uiStatusFlags & SOLDIER_ENEMY)
2945 		{
2946 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Alert:", gzAlertStr[s->bAlertStatus]);
2947 		}
2948 
2949 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Action Data:", s->usActionData);
2950 
2951 		if (s->uiStatusFlags & SOLDIER_ENEMY)
2952 		{
2953 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "AIMorale", s->bAIMorale);
2954 		}
2955 		else
2956 		{
2957 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Morale", s->bMorale);
2958 		}
2959 
2960 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Delayed Movement:", s->fDelayedMovement);
2961 
2962 		if (gubWatchedLocPoints[s->ubID][0] > 0)
2963 		{
2964 			MPrint(DEBUG_PAGE_FIRST_COLUMN, y += h, ST::format("Watch {}/{} for {} pts",
2965 				gsWatchedLoc[s->ubID][0],
2966 				gbWatchedLocLevel[s->ubID][0],
2967 				gubWatchedLocPoints[s->ubID][0])
2968 			);
2969 		}
2970 
2971 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ActionInProg:", s->bActionInProgress);
2972 
2973 		if (gubWatchedLocPoints[s->ubID][1] > 0)
2974 		{
2975 			MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y += h, ST::format("Watch {}/{} for {} pts",
2976 				gsWatchedLoc[s->ubID][1],
2977 				gbWatchedLocLevel[s->ubID][1],
2978 				gubWatchedLocPoints[s->ubID][1])
2979 			);
2980 		}
2981 
2982 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Last Action:", gzActionStr[s->bLastAction]);
2983 
2984 		if (gubWatchedLocPoints[s->ubID][2] > 0)
2985 		{
2986 			MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y += h, ST::format("Watch {}/{} for {} pts",
2987 				gsWatchedLoc[s->ubID][2],
2988 				gbWatchedLocLevel[s->ubID][2],
2989 				gubWatchedLocPoints[s->ubID][2])
2990 			);
2991 		}
2992 
2993 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Animation:",   gAnimControl[s->usAnimState].zAnimStr);
2994 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Getting Hit:", s->fGettingHit);
2995 
2996 		if (s->ubCivilianGroup != 0)
2997 		{
2998 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Civ group", s->ubCivilianGroup);
2999 		}
3000 
3001 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Suppress pts:",       s->ubSuppressionPoints);
3002 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Attacker ID:",        SOLDIER2ID(s->attacker));
3003 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "EndAINotCalled:",     s->fTurnInProgress);
3004 
3005 		y = DEBUG_PAGE_START_Y;
3006 
3007 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "PrevAnimation:",      gAnimControl[s->usOldAniState].zAnimStr);
3008 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "PrevAniCode:",        gusAnimInst[s->usOldAniState][s->sOldAniCode]);
3009 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "GridNo:",             s->sGridNo);
3010 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "AniCode:",            gusAnimInst[s->usAnimState][s->usAniCode]);
3011 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "No APS To fin Move:", s->fNoAPToFinishMove);
3012 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Bullets out:",        s->bBulletsLeft);
3013 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "Anim non-int:",       s->fInNonintAnim);
3014 		MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "RT Anim non-int:",    s->fRTInNonintAnim);
3015 
3016 		// OPINION OF SELECTED MERC
3017 		const SOLDIERTYPE* const sel = GetSelectedMan();
3018 		if (sel != NULL && s->ubProfile != NO_PROFILE)
3019 		{
3020 			MercProfile const p = MercProfile(s->ubProfile);
3021 			if (p.isPlayerMerc() || p.isRPC())
3022 			{
3023 				MPrintStat(DEBUG_PAGE_SECOND_COLUMN, y += h, "NPC Opinion:", GetProfile(s->ubProfile).bMercOpinion[sel->ubProfile]);
3024 			}
3025 		}
3026 	}
3027 	else
3028 	{
3029 		const GridNo map_pos = GetMouseMapPos();
3030 		if (map_pos == NOWHERE) return;
3031 
3032 		MPageHeader("DEBUG LAND PAGE THREE");
3033 
3034 		INT32 y = DEBUG_PAGE_START_Y;
3035 
3036 		// OK, display door information here.....
3037 		DOOR_STATUS const* const door = GetDoorStatus(map_pos);
3038 		if (door == NULL)
3039 		{
3040 			MHeader(DEBUG_PAGE_FIRST_COLUMN, y += h, "No Door Status");
3041 		}
3042 		else
3043 		{
3044 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Door Status Found:", map_pos);
3045 
3046 			ST::string door_state =
3047 				door->ubFlags & DOOR_OPEN ? "OPEN" : "CLOSED";
3048 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Actual Status:", door_state);
3049 
3050 			ST::string perceived_state =
3051 				door->ubFlags & DOOR_PERCEIVED_NOTSET ? "NOT SET" :
3052 				door->ubFlags & DOOR_PERCEIVED_OPEN   ? "OPEN"    :
3053 									"CLOSED";
3054 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Perceived Status:", perceived_state);
3055 		}
3056 
3057 		//Find struct data and se what it says......
3058 		STRUCTURE const* const structure = FindStructure(map_pos, STRUCTURE_ANYDOOR);
3059 		if (structure == NULL)
3060 		{
3061 			MHeader(DEBUG_PAGE_FIRST_COLUMN, y += h, "No Door Struct Data");
3062 		}
3063 		else
3064 		{
3065 			ST::string structure_state =
3066 				structure->fFlags & STRUCTURE_OPEN ? "OPEN" : "CLOSED";
3067 			MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "State:", structure_state);
3068 		}
3069 	}
3070 }
3071 
3072 
AppendAttachmentCode(UINT16 item,ST::string & str)3073 static void AppendAttachmentCode(UINT16 item, ST::string& str)
3074 {
3075 	switch (item)
3076 	{
3077 		case SILENCER:    str += " Sil"; break;
3078 		case SNIPERSCOPE: str += " Scp"; break;
3079 		case BIPOD:       str += " Bip"; break;
3080 		case LASERSCOPE:  str += " Las"; break;
3081 	}
3082 }
3083 
3084 
WriteQuantityAndAttachments(OBJECTTYPE const * o,INT32 const y)3085 static void WriteQuantityAndAttachments(OBJECTTYPE const* o, INT32 const y)
3086 {
3087 	//100%  Qty: 2  Attach:
3088 	//100%  Qty: 2
3089 	//100%  Attach:
3090 	//100%
3091 	if (GCM->getItem(o->usItem)->getItemClass() == IC_AMMO)
3092 	{
3093 		if (o->ubNumberOfObjects > 1)
3094 		{
3095 			ST::string str = ST::format("Clips:  {}  ({}", o->ubNumberOfObjects, o->bStatus[0]);
3096 			for (UINT8 i = 1; i < o->ubNumberOfObjects; ++i)
3097 			{
3098 				ST::string temp = ST::format(", {}", o->bStatus[0]);
3099 				str += temp;
3100 			}
3101 			str += ")";
3102 			MPrint(DEBUG_PAGE_SECOND_COLUMN, y, str);
3103 		}
3104 		else
3105 		{
3106 			MPrint(DEBUG_PAGE_SECOND_COLUMN, y, ST::format("{} rounds", o->bStatus[0]));
3107 		}
3108 	}
3109 	else
3110 	{
3111 		// Build attachment string
3112 		ST::string attachments;
3113 		if (o->usAttachItem[0] ||
3114 				o->usAttachItem[1] ||
3115 				o->usAttachItem[2] ||
3116 				o->usAttachItem[3])
3117 		{
3118 			attachments = "  (";
3119 			AppendAttachmentCode(o->usAttachItem[0], attachments);
3120 			AppendAttachmentCode(o->usAttachItem[1], attachments);
3121 			AppendAttachmentCode(o->usAttachItem[2], attachments);
3122 			AppendAttachmentCode(o->usAttachItem[3], attachments);
3123 			attachments += " )";
3124 		}
3125 		else
3126 		{
3127 			attachments = ST::null;
3128 		}
3129 
3130 		if (o->ubNumberOfObjects > 1)
3131 		{
3132 			//everything
3133 			MPrint(DEBUG_PAGE_SECOND_COLUMN, y, ST::format("{}%  Qty:  {}{}", o->bStatus[0], o->ubNumberOfObjects, attachments));
3134 		}
3135 		else
3136 		{
3137 			//condition and attachments
3138 			MPrint(DEBUG_PAGE_SECOND_COLUMN, y, ST::format("{}%{}", o->bStatus[0], attachments));
3139 		}
3140 	}
3141 }
3142 
3143 
PrintItem(INT32 y,const ST::string & header,const OBJECTTYPE * o)3144 static void PrintItem(INT32 y, const ST::string& header, const OBJECTTYPE* o)
3145 {
3146 	MHeader(DEBUG_PAGE_FIRST_COLUMN, y, header);
3147 	if (!o->usItem) return;
3148 	MPrint(DEBUG_PAGE_FIRST_COLUMN+DEBUG_PAGE_LABEL_WIDTH, y, ST::format("{}", ShortItemNames[o->usItem]));
3149 	WriteQuantityAndAttachments(o, y);
3150 }
3151 
3152 
DebugSoldierPage4()3153 void DebugSoldierPage4()
3154 {
3155 	const SOLDIERTYPE* const s = FindSoldierFromMouse();
3156 
3157 	INT32 const h = DEBUG_PAGE_LINE_HEIGHT;
3158 
3159 	if (s != NULL)
3160 	{
3161 		MPageHeader(ST::format("DEBUG SOLDIER PAGE FOUR, GRIDNO {}", s->sGridNo));
3162 
3163 		INT32 y = DEBUG_PAGE_START_Y;
3164 
3165 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "Exp. Level:", s->bExpLevel);
3166 		ST::string sclass;
3167 		switch (s->ubSoldierClass)
3168 		{
3169 			case SOLDIER_CLASS_ADMINISTRATOR:
3170 				sclass = "(Administrator)";
3171 				break;
3172 			case SOLDIER_CLASS_ELITE:
3173 				sclass = "(Army Elite)";
3174 				break;
3175 			case SOLDIER_CLASS_ARMY:
3176 				sclass = "(Army Troop)";
3177 				break;
3178 			case SOLDIER_CLASS_CREATURE:
3179 				sclass = "(Creature)";
3180 				break;
3181 			case SOLDIER_CLASS_GREEN_MILITIA:
3182 				sclass = "(Green Militia)";
3183 				break;
3184 			case SOLDIER_CLASS_REG_MILITIA:
3185 				sclass = "(Reg Militia)";
3186 				break;
3187 			case SOLDIER_CLASS_ELITE_MILITIA:
3188 				sclass = "(Elite Militia)";
3189 				break;
3190 			case SOLDIER_CLASS_MINER:
3191 				sclass = "(Miner)";
3192 				break;
3193 			// don't care (don't write anything)
3194 			default:
3195 				sclass = ST::null;
3196 				break;
3197 		}
3198 		if (!sclass.empty())
3199 			MPrint(DEBUG_PAGE_SECOND_COLUMN, y, ST::format("{}", sclass));
3200 
3201 		if (s->bTeam != OUR_TEAM)
3202 		{
3203 			ST::string orders;
3204 			switch (s->bOrders)
3205 			{
3206 				case STATIONARY:  orders = "STATIONARY";    break;
3207 				case ONGUARD:     orders = "ON GUARD";      break;
3208 				case ONCALL:      orders = "ON CALL";       break;
3209 				case SEEKENEMY:   orders = "SEEK ENEMY";    break;
3210 				case CLOSEPATROL: orders = "CLOSE PATROL";  break;
3211 				case FARPATROL:   orders = "FAR PATROL";    break;
3212 				case POINTPATROL: orders = "POINT PATROL";  break;
3213 				case RNDPTPATROL: orders = "RND PT PATROL"; break;
3214 				default:          orders = "UNKNOWN";       break;
3215 			}
3216 			ST::string attitude;
3217 			switch (s->bAttitude)
3218 			{
3219 				case DEFENSIVE:   attitude = "DEFENSIVE";    break;
3220 				case BRAVESOLO:   attitude = "BRAVE SOLO";   break;
3221 				case BRAVEAID:    attitude = "BRAVE AID";    break;
3222 				case AGGRESSIVE:  attitude = "AGGRESSIVE";   break;
3223 				case CUNNINGSOLO: attitude = "CUNNING SOLO"; break;
3224 				case CUNNINGAID:  attitude = "CUNNING AID";  break;
3225 				default:          attitude = "UNKNOWN";      break;
3226 			}
3227 			SOLDIERINITNODE const* const node = FindSoldierInitNodeBySoldier(*s);
3228 			y += h;
3229 			if (node)
3230 			{
3231 				MPrint(DEBUG_PAGE_FIRST_COLUMN, y, ST::format("{}, {}, REL EQUIP: {}, REL ATTR: {}",
3232 					orders, attitude,
3233 					node->pBasicPlacement->bRelativeEquipmentLevel,
3234 					node->pBasicPlacement->bRelativeAttributeLevel)
3235 				);
3236 			}
3237 			else
3238 			{
3239 				MPrint(DEBUG_PAGE_FIRST_COLUMN, y, ST::format("{}, {}", orders, attitude));
3240 			}
3241 		}
3242 
3243 		MPrintStat(DEBUG_PAGE_FIRST_COLUMN, y += h, "ID:", s->ubID);
3244 
3245 		PrintItem(y += h, "HELMETPOS:",     &s->inv[HELMETPOS]);
3246 		PrintItem(y += h, "VESTPOS:",       &s->inv[VESTPOS]);
3247 		PrintItem(y += h, "LEGPOS:",        &s->inv[LEGPOS]);
3248 		PrintItem(y += h, "HEAD1POS:",      &s->inv[HEAD1POS]);
3249 		PrintItem(y += h, "HEAD2POS:",      &s->inv[HEAD2POS]);
3250 		PrintItem(y += h, "HANDPOS:",       &s->inv[HANDPOS]);
3251 		PrintItem(y += h, "SECONDHANDPOS:", &s->inv[SECONDHANDPOS]);
3252 		PrintItem(y += h, "BIGPOCK1POS:",   &s->inv[BIGPOCK1POS]);
3253 		PrintItem(y += h, "BIGPOCK2POS:",   &s->inv[BIGPOCK2POS]);
3254 		PrintItem(y += h, "BIGPOCK3POS:",   &s->inv[BIGPOCK3POS]);
3255 		PrintItem(y += h, "BIGPOCK4POS:",   &s->inv[BIGPOCK4POS]);
3256 		PrintItem(y += h, "SMALLPOCK1POS:", &s->inv[SMALLPOCK1POS]);
3257 		PrintItem(y += h, "SMALLPOCK2POS:", &s->inv[SMALLPOCK2POS]);
3258 		PrintItem(y += h, "SMALLPOCK3POS:", &s->inv[SMALLPOCK3POS]);
3259 		PrintItem(y += h, "SMALLPOCK4POS:", &s->inv[SMALLPOCK4POS]);
3260 		PrintItem(y += h, "SMALLPOCK5POS:", &s->inv[SMALLPOCK5POS]);
3261 		PrintItem(y += h, "SMALLPOCK6POS:", &s->inv[SMALLPOCK6POS]);
3262 		PrintItem(y += h, "SMALLPOCK7POS:", &s->inv[SMALLPOCK7POS]);
3263 		PrintItem(y += h, "SMALLPOCK8POS:", &s->inv[SMALLPOCK8POS]);
3264 	}
3265 	else
3266 	{
3267 		MPageHeader("DEBUG LAND PAGE FOUR");
3268 	}
3269 }
3270 
3271 
3272 //
3273 // Noise stuff
3274 //
3275 
MovementNoise(SOLDIERTYPE const * const pSoldier)3276 UINT8 MovementNoise(SOLDIERTYPE const * const pSoldier)
3277 {
3278 	constexpr UINT8 MAX_MOVEMENT_NOISE = 9;
3279 
3280 	if ( pSoldier->bTeam == ENEMY_TEAM )
3281 	{
3282 		return( (UINT8) (MAX_MOVEMENT_NOISE - PreRandom( 2 )) );
3283 	}
3284 
3285 	INT32 iStealthSkill = 20 + 4 * EffectiveExpLevel( pSoldier ) + ((EffectiveDexterity( pSoldier ) * 4) / 10); // 24-100
3286 
3287 	// big bonus for those "extra stealthy" mercs
3288 	if ( pSoldier->ubBodyType == BLOODCAT )
3289 	{
3290 		iStealthSkill += 50;
3291 	}
3292 	else
3293 	{
3294 		iStealthSkill += 25 * NUM_SKILL_TRAITS(pSoldier, STEALTHY);
3295 	}
3296 
3297 	UINT8 const ubBandaged = pSoldier->bLifeMax - pSoldier->bLife - pSoldier->bBleeding;
3298 	UINT8 const ubEffLife = pSoldier->bLife + (ubBandaged / 2);
3299 
3300 	// IF "SNEAKER'S" "EFFECTIVE LIFE" IS AT LESS THAN 50
3301 	if (ubEffLife < 50)
3302 	{
3303 		// reduce effective stealth skill by up to 50% for low life
3304 		iStealthSkill -= (iStealthSkill * (50 - ubEffLife)) / 100;
3305 	}
3306 
3307 	// if breath is below 50%
3308 	if (pSoldier->bBreath < 50)
3309 	{
3310 		// reduce effective stealth skill by up to 50%
3311 		iStealthSkill -= (iStealthSkill * (50 - pSoldier->bBreath)) / 100;
3312 	}
3313 
3314 	// if sneaker is moving through water
3315 	// DeepWater() must be checked first because Water() matches all types of water
3316 	if (DeepWater(pSoldier->sGridNo))
3317 	{
3318 		iStealthSkill -= 20;
3319 	}
3320 	else if (Water(pSoldier->sGridNo))
3321 	{
3322 		iStealthSkill -= 10;
3323 	}
3324 
3325 	// minus 3 percent per bonus AP from adrenaline
3326 	iStealthSkill -= 3 * pSoldier->bDrugEffect[ DRUG_TYPE_ADRENALINE ];
3327 
3328 	iStealthSkill = std::max(iStealthSkill, 0);
3329 
3330 	UINT8 ubVolume;
3331 	if (!pSoldier->bStealthMode)	// REGULAR movement
3332 	{
3333 		UINT8 ubMaxVolume = MAX_MOVEMENT_NOISE - (iStealthSkill / 16); // 9 - (0 to 9) => 0 to 9
3334 
3335 		if (Water(pSoldier->sGridNo))
3336 		{
3337 			ubMaxVolume++; // in water, can be even louder
3338 		}
3339 		switch (pSoldier->usAnimState)
3340 		{
3341 			case CRAWLING:
3342 				ubMaxVolume = ubMaxVolume <= 2 ? 0 : ubMaxVolume - 2;
3343 				break;
3344 			case SWATTING:
3345 				ubMaxVolume = ubMaxVolume <= 1 ? 0 : ubMaxVolume - 1;
3346 				break;
3347 			case RUNNING:
3348 				ubMaxVolume += 3;
3349 				break;
3350 		}
3351 
3352 		if (ubMaxVolume < 2)
3353 		{
3354 			ubVolume = ubMaxVolume;
3355 		}
3356 		else
3357 		{
3358 			ubVolume = 1 + (UINT8) PreRandom(ubMaxVolume); // actual volume is 1 to max volume
3359 		}
3360 	}
3361 	else // in STEALTH mode
3362 	{
3363 		INT32 iRoll = (INT32) PreRandom(100);	// roll them bones!
3364 
3365 		if (iRoll >= iStealthSkill)   // v1.13 modification: give a second chance!
3366 		{
3367 			iRoll = (INT32) PreRandom(100);
3368 		}
3369 
3370 		if (iRoll < iStealthSkill)
3371 		{
3372 			ubVolume = 0;	// made it, stayed quiet moving through this tile
3373 		}
3374 		else	// OOPS!
3375 		{
3376 			ubVolume = 1 + ((iRoll - iStealthSkill + 1) / 16);	// volume is 1 - 7 ...
3377 			switch (pSoldier->usAnimState)
3378 			{
3379 				case CRAWLING:
3380 					ubVolume = ubVolume <= 2 ? 0 : ubVolume - 2;
3381 					break;
3382 				case SWATTING:
3383 					ubVolume -= 1;
3384 					break;
3385 				case RUNNING:
3386 					ubVolume += 3;
3387 					break;
3388 			}
3389 		}
3390 	}
3391 
3392 	return( ubVolume );
3393 }
3394 
DoorOpeningNoise(SOLDIERTYPE * pSoldier)3395 UINT8 DoorOpeningNoise( SOLDIERTYPE *pSoldier )
3396 {
3397 	INT16 sGridNo;
3398 	DOOR_STATUS *pDoorStatus;
3399 	UINT8 ubDoorNoise;
3400 
3401 	// door being opened gridno is always the pending-action-data2 value
3402 	sGridNo = pSoldier->sPendingActionData2;
3403 	pDoorStatus = GetDoorStatus( sGridNo );
3404 
3405 	if ( pDoorStatus && pDoorStatus->ubFlags & DOOR_HAS_TIN_CAN )
3406 	{
3407 		// double noise possible!
3408 		ubDoorNoise = DOOR_NOISE_VOLUME * 3;
3409 	}
3410 	else
3411 	{
3412 		ubDoorNoise = DOOR_NOISE_VOLUME;
3413 	}
3414 	if ( MovementNoise( pSoldier ) )
3415 	{
3416 		// failed any stealth checks
3417 		return( ubDoorNoise );
3418 	}
3419 	else
3420 	{
3421 		// succeeded in being stealthy!
3422 		return( 0 );
3423 	}
3424 }
3425 
3426 
MakeNoise(SOLDIERTYPE * const noise_maker,INT16 const sGridNo,INT8 const bLevel,UINT8 const ubVolume,NoiseKind const ubNoiseType)3427 void MakeNoise(SOLDIERTYPE* const noise_maker, INT16 const sGridNo, INT8 const bLevel, UINT8 const ubVolume, NoiseKind const ubNoiseType)
3428 {
3429 	if ( gTacticalStatus.ubAttackBusyCount )
3430 	{
3431 		// delay these events until the attack is over!
3432 		EV_S_NOISE SNoise;
3433 		SNoise.ubNoiseMaker = SOLDIER2ID(noise_maker);
3434 		SNoise.sGridNo      = sGridNo;
3435 		SNoise.bLevel       = bLevel;
3436 		SNoise.ubVolume     = ubVolume;
3437 		SNoise.ubNoiseType  = ubNoiseType;
3438 		AddGameEvent(SNoise, DEMAND_EVENT_DELAY);
3439 	}
3440 	else
3441 	{
3442 		OurNoise(noise_maker, sGridNo, bLevel, ubVolume, ubNoiseType);
3443 	}
3444 }
3445 
3446 
3447 static void ProcessNoise(SOLDIERTYPE* noise_maker, INT16 sGridNo, INT8 bLevel, UINT8 ubBaseVolume, NoiseKind);
3448 
3449 
OurNoise(SOLDIERTYPE * const noise_maker,INT16 const sGridNo,INT8 const bLevel,UINT8 const ubVolume,NoiseKind const ubNoiseType)3450 void OurNoise(SOLDIERTYPE* const noise_maker, INT16 const sGridNo, INT8 const bLevel, UINT8 const ubVolume, NoiseKind const ubNoiseType)
3451 {
3452 #ifdef BYPASSNOISE
3453 	return;
3454 #endif
3455 
3456 	// see if anyone actually hears this noise, sees noise_maker, etc.
3457 	ProcessNoise(noise_maker, sGridNo, bLevel, ubVolume, ubNoiseType);
3458 
3459 	if (noise_maker != NULL &&
3460 		(gTacticalStatus.uiFlags & INCOMBAT) &&
3461 		!gfDelayResolvingBestSightingDueToDoor)
3462 	{
3463 		// interrupts are possible, resolve them now (we're in control here)
3464 		// (you can't interrupt NOBODY, even if you hear the noise)
3465 		ResolveInterruptsVs(noise_maker, NOISEINTERRUPT);
3466 	}
3467 }
3468 
3469 
3470 static void HearNoise(SOLDIERTYPE* pSoldier, SOLDIERTYPE* noise_maker, UINT16 sGridNo, INT8 bLevel, UINT8 ubVolume, NoiseKind, BOOLEAN* ubSeen);
3471 static UINT8 CalcEffVolume(const SOLDIERTYPE* pSoldier, INT16 sGridNo, INT8 bLevel, NoiseKind, UINT8 ubBaseVolume, UINT8 bCheckTerrain, UINT8 ubTerrType1, UINT8 ubTerrType2);
3472 static void TellPlayerAboutNoise(SOLDIERTYPE* pSoldier, const SOLDIERTYPE* noise_maker, INT16 sGridNo, INT8 bLevel, UINT8 ubVolume, NoiseKind, UINT8 ubNoiseDir);
3473 
3474 
ProcessNoise(SOLDIERTYPE * const noise_maker,INT16 const sGridNo,INT8 const bLevel,UINT8 const ubBaseVolume,NoiseKind const ubNoiseType)3475 static void ProcessNoise(SOLDIERTYPE* const noise_maker, INT16 const sGridNo, INT8 const bLevel, UINT8 const ubBaseVolume, NoiseKind const ubNoiseType)
3476 {
3477 	UINT8 ubLoudestEffVolume, ubEffVolume;
3478 	//UINT8 ubPlayVolume;
3479 	INT8 bCheckTerrain = FALSE;
3480 	UINT8 ubNoiseDir        = (UINT8)-1; // XXX HACK000E probably ubLoudestNoiseDir should be used
3481 	UINT8 ubLoudestNoiseDir = (UINT8)-1; // XXX HACK000E
3482 
3483 	// if the base volume itself was negligible
3484 	if (!ubBaseVolume)
3485 		return;
3486 
3487 
3488 	// EXPLOSIONs are special, because they COULD be caused by a merc who is
3489 	// no longer alive (but he placed the bomb or flaky grenade in the past).
3490 	// Later noiseMaker gets whacked to NOBODY anyway, so that's OK.  So a
3491 	// dead noiseMaker is only used here to decide WHICH soldiers HearNoise().
3492 
3493 	// if noise is made by a person, AND it's not noise from an explosion
3494 	if (noise_maker != NULL && ubNoiseType != NOISE_EXPLOSION)
3495 	{
3496 		// inactive/not in sector/dead soldiers, shouldn't be making noise!
3497 		if (!noise_maker->bActive ||
3498 			!noise_maker->bInSector ||
3499 			noise_maker->uiStatusFlags & SOLDIER_DEAD)
3500 		{
3501 			return;
3502 		}
3503 
3504 		// if he's out of life, and this isn't just his "dying scream" which is OK
3505 		if (noise_maker->bLife == 0 && ubNoiseType != NOISE_SCREAM)
3506 		{
3507 			return;
3508 		}
3509 	}
3510 
3511 
3512 	// DETERMINE THE TERRAIN TYPE OF THE GRIDNO WHERE NOISE IS COMING FROM
3513 
3514 	const UINT8 ubSourceTerrType = gpWorldLevelData[sGridNo].ubTerrainID;
3515 
3516 	// if we have now somehow obtained a valid terrain type
3517 	if ((ubSourceTerrType >= FLAT_GROUND) || (ubSourceTerrType <= DEEP_WATER))
3518 	{
3519 		bCheckTerrain = TRUE;
3520 	}
3521 	// else give up trying to get terrain type, just assume sound isn't muffled
3522 
3523 
3524 	// DETERMINE THE *PERCEIVED* SOURCE OF THE NOISE
3525 	SOLDIERTYPE* source;
3526 	switch (ubNoiseType)
3527 	{
3528 		// for noise generated by an OBJECT shot/thrown/dropped by the noiseMaker
3529 		case NOISE_ROCK_IMPACT:
3530 			gWhoThrewRock = noise_maker;
3531 			/* FALLTHROUGH */
3532 		case NOISE_BULLET_IMPACT:
3533 		case NOISE_GRENADE_IMPACT:
3534 		case NOISE_EXPLOSION:
3535 			// the source of the noise is not at all obvious, so hide it from
3536 			// the listener and maintain noiseMaker's cover by making source NOBODY
3537 			source = NULL;
3538 			break;
3539 
3540 		default:
3541 			// normal situation: the noiseMaker is obviously the source of the noise
3542 			source = noise_maker;
3543 			break;
3544 	}
3545 
3546 	// LOOP THROUGH EACH TEAM
3547 	for (UINT8 bTeam = 0; bTeam < MAXTEAMS; ++bTeam)
3548 	{
3549 		// skip any inactive teams
3550 		if (!IsTeamActive(bTeam)) continue;
3551 
3552 		// if a the noise maker is a person, not just NOBODY
3553 		if (noise_maker != NULL)
3554 		{
3555 			// if this team is the same TEAM as the noise maker's
3556 			// (for now, assume we will report noises by unknown source on same SIDE)
3557 			// OR, if the noise maker is currently in sight to this HUMAN team
3558 
3559 			// CJC: changed to if the side is the same side as the noise maker's!
3560 			// CJC: changed back!
3561 
3562 			if (bTeam == noise_maker->bTeam) continue;
3563 
3564 			if (gTacticalStatus.Team[bTeam].bHuman &&
3565 					gbPublicOpplist[bTeam][noise_maker->ubID] == SEEN_CURRENTLY)
3566 			{
3567 				continue;
3568 			}
3569 		}
3570 
3571 		// tell player about noise if enemies are present
3572 		BOOLEAN bTellPlayer = gTacticalStatus.fEnemyInSector && ( !(gTacticalStatus.uiFlags & INCOMBAT) || (gTacticalStatus.ubCurrentTeam) );
3573 
3574 		switch (ubNoiseType)
3575 		{
3576 			case NOISE_GUNFIRE:
3577 			case NOISE_BULLET_IMPACT:
3578 			case NOISE_ROCK_IMPACT:
3579 			case NOISE_GRENADE_IMPACT:
3580 				// It's noise caused by a projectile.  If the projectile was seen by
3581 				// the local player while in flight (PublicBullet), then don't bother
3582 				// giving him a message about the noise it made, he's obviously aware.
3583 				if (1 /*PublicBullet*/)
3584 				{
3585 					bTellPlayer = FALSE;
3586 				}
3587 
3588 				break;
3589 
3590 			case NOISE_EXPLOSION:
3591 				// if center of explosion is in visual range of team, don't report
3592 				// noise, because the player is already watching the thing go BOOM!
3593 				if (TeamMemberNear(bTeam,sGridNo,STRAIGHT))
3594 				{
3595 					bTellPlayer = FALSE;
3596 				}
3597 				break;
3598 
3599 			case NOISE_SILENT_ALARM:
3600 				bTellPlayer = FALSE;
3601 				break;
3602 			default:
3603 				break;
3604 		}
3605 
3606 		// if noise was made by a person
3607 		if (noise_maker != NULL)
3608 		{
3609 			switch (gbPublicOpplist[bTeam][noise_maker->ubID])
3610 			{
3611 				case SEEN_CURRENTLY:
3612 				case SEEN_THIS_TURN:
3613 				case HEARD_THIS_TURN:
3614 					// If noisemaker has been *PUBLICLY* SEEN OR HEARD during THIS TURN,
3615 					// then don't bother reporting any noise made by him to the player
3616 					bTellPlayer = FALSE;
3617 					break;
3618 
3619 				default:
3620 					if (noise_maker->bVisible == TRUE && bTeam == OUR_TEAM)
3621 					{
3622 						SLOGD(
3623 							"Handling noise from person not currently seen in player's public opplist");
3624 					}
3625 					break;
3626 			}
3627 
3628 			if (noise_maker->bLife == 0)
3629 			{
3630 				// this guy is dead (just dying) so don't report to player
3631 				bTellPlayer = FALSE;
3632 			}
3633 		}
3634 
3635 		// refresh flags for this new team
3636 		BOOLEAN bHeard = FALSE;
3637 		BOOLEAN bSeen  = FALSE;
3638 		ubLoudestEffVolume = 0;
3639 		SOLDIERTYPE* heard_loudest_by = NULL;
3640 
3641 		// All mercs on this team check if they are eligible to hear this noise
3642 		FOR_EACH_IN_TEAM(pSoldier, bTeam)
3643 		{
3644 			// if this "listener" is in no condition to care
3645 			if (!pSoldier->bInSector || pSoldier->uiStatusFlags & SOLDIER_DEAD ||
3646 				(pSoldier->bLife < OKLIFE) || pSoldier->ubBodyType == LARVAE_MONSTER)
3647 			{
3648 				continue; // skip him!
3649 			}
3650 
3651 			if ( pSoldier->uiStatusFlags & SOLDIER_VEHICLE && pSoldier->bTeam == OUR_TEAM  )
3652 			{
3653 				continue; // skip
3654 			}
3655 
3656 			if ( bTeam == OUR_TEAM && pSoldier->bAssignment == ASSIGNMENT_POW )
3657 			{
3658 				// POWs should not be processed for noise
3659 				continue;
3660 			}
3661 
3662 			// if a the noise maker is a person, not just NOBODY
3663 			if (noise_maker != NULL)
3664 			{
3665 				// if this listener can see this noise maker
3666 				if (pSoldier->bOppList[noise_maker->ubID] == SEEN_CURRENTLY)
3667 				{
3668 					// civilians care about gunshots even if they come from someone they can see
3669 					if ( !( pSoldier->bNeutral && ubNoiseType == NOISE_GUNFIRE ) )
3670 					{
3671 						continue; // then who cares whether he can also hear the guy?
3672 					}
3673 				}
3674 
3675 				// screen out allied militia from hearing us
3676 				switch (noise_maker->bTeam)
3677 				{
3678 					case OUR_TEAM:
3679 						// if the listener is militia and still on our side, ignore noise from us
3680 						if ( pSoldier->bTeam == MILITIA_TEAM && pSoldier->bSide == 0 )
3681 						{
3682 							continue;
3683 						}
3684 						break;
3685 					case ENEMY_TEAM:
3686 						switch( pSoldier->ubProfile )
3687 						{
3688 							case WARDEN:
3689 							case GENERAL:
3690 							case SERGEANT:
3691 							case CONRAD:
3692 								// ignore soldier team
3693 								continue;
3694 							default:
3695 								break;
3696 						}
3697 						break;
3698 					case MILITIA_TEAM:
3699 						// if the noisemaker is militia and still on our side, ignore
3700 						// noise if we're listening
3701 						if (pSoldier->bTeam == OUR_TEAM && noise_maker->bSide == 0)
3702 						{
3703 							continue;
3704 						}
3705 						break;
3706 				}
3707 
3708 				if ( gWorldSectorX == 5 && gWorldSectorY == MAP_ROW_N )
3709 				{
3710 					// in the bloodcat arena sector, skip noises between army & bloodcats
3711 					if (pSoldier->bTeam == ENEMY_TEAM && noise_maker->bTeam == CREATURE_TEAM)
3712 					{
3713 						continue;
3714 					}
3715 					if (pSoldier->bTeam == CREATURE_TEAM && noise_maker->bTeam == ENEMY_TEAM)
3716 					{
3717 						continue;
3718 					}
3719 				}
3720 
3721 
3722 			}
3723 			else
3724 			{
3725 				// screen out allied militia from hearing us
3726 				if (noise_maker == NULL && pSoldier->bTeam == MILITIA_TEAM && pSoldier->bSide == 0)
3727 				{
3728 					continue;
3729 				}
3730 			}
3731 
3732 			if ((pSoldier->bTeam == CIV_TEAM) && (ubNoiseType == NOISE_GUNFIRE ||
3733 				ubNoiseType == NOISE_EXPLOSION))
3734 			{
3735 				pSoldier->ubMiscSoldierFlags |= SOLDIER_MISC_HEARD_GUNSHOT;
3736 			}
3737 
3738 			// Can the listener hear noise of that volume given his circumstances?
3739 			ubEffVolume = CalcEffVolume(pSoldier, sGridNo, bLevel, ubNoiseType,
3740 							ubBaseVolume, bCheckTerrain,
3741 							pSoldier->bOverTerrainType, ubSourceTerrType);
3742 
3743 			if (ubEffVolume > 0)
3744 			{
3745 				// ALL RIGHT!  Passed all the tests, this listener hears this noise!!!
3746 				HearNoise(pSoldier, source, sGridNo, bLevel, ubEffVolume, ubNoiseType, &bSeen);
3747 
3748 				bHeard = TRUE;
3749 
3750 				ubNoiseDir = GetDirectionToGridNoFromGridNo(pSoldier->sGridNo, sGridNo);
3751 
3752 				// check the 'noise heard & reported' bit for that soldier & direction
3753 				if ( ubNoiseType != NOISE_MOVEMENT || bTeam != OUR_TEAM ||
3754 					(pSoldier->bInterruptDuelPts != NO_INTERRUPT) ||
3755 					!(pSoldier->ubMovementNoiseHeard & (1 << ubNoiseDir) ) )
3756 				{
3757 					if (ubEffVolume > ubLoudestEffVolume)
3758 					{
3759 						ubLoudestEffVolume = ubEffVolume;
3760 						heard_loudest_by = pSoldier;
3761 						ubLoudestNoiseDir = ubNoiseDir;
3762 					}
3763 				}
3764 
3765 			}
3766 			else
3767 			{
3768 				ubEffVolume = 0;
3769 			}
3770 		}
3771 
3772 
3773 		// if the noise was heard at all
3774 		if (bHeard)
3775 		{
3776 			// and we're doing our team
3777 			if (bTeam == OUR_TEAM)
3778 			{
3779 				// if we are to tell the player about this type of noise
3780 				if (bTellPlayer && heard_loudest_by != NULL)
3781 				{
3782 					// the merc that heard it the LOUDEST is the one to comment
3783 					// should add level to this function call
3784 					TellPlayerAboutNoise(heard_loudest_by, noise_maker, sGridNo, bLevel, ubLoudestEffVolume, ubNoiseType, ubLoudestNoiseDir);
3785 
3786 					if ( ubNoiseType == NOISE_MOVEMENT)
3787 					{
3788 						heard_loudest_by->ubMovementNoiseHeard |= 1 << ubNoiseDir;
3789 					}
3790 
3791 				}
3792 				//if ( !(pSoldier->ubMovementNoiseHeard & (1 << ubNoiseDir) ) )
3793 			}
3794 #ifdef REPORTTHEIRNOISE
3795 			else // debugging: report noise heard by other team's soldiers
3796 			{
3797 				if (bTellPlayer)
3798 				{
3799 					TellPlayerAboutNoise(heard_loudest_by, noise_maker, sGridNo, bLevel, ubLoudestEffVolume, ubNoiseType, ubLoudestNoiseDir);
3800 				}
3801 			}
3802 #endif
3803 		}
3804 
3805 		// if the listening team is human-controlled AND
3806 		// the noise's source is another soldier
3807 		// (computer-controlled teams don't radio or automatically report NOISE)
3808 		if (source != NULL && gTacticalStatus.Team[bTeam].bHuman)
3809 		{
3810 			// if noise_maker was seen by at least one member of this team
3811 			if (bSeen)
3812 			{
3813 				// this team is now allowed to report sightings and set Public flags
3814 				OurTeamRadiosRandomlyAbout(source);
3815 			}
3816 			else // not seen
3817 			{
3818 				if (bHeard)
3819 				{
3820 					// mark noise maker as having been PUBLICLY heard THIS TURN
3821 					UpdatePublic(bTeam, source, HEARD_THIS_TURN, sGridNo, bLevel);
3822 				}
3823 			}
3824 		}
3825 	}
3826 
3827 	gWhoThrewRock = NULL;
3828 }
3829 
3830 
CalcEffVolume(SOLDIERTYPE const * const pSoldier,INT16 const sGridNo,INT8 const bLevel,NoiseKind const ubNoiseType,UINT8 const ubBaseVolume,UINT8 const bCheckTerrain,UINT8 const ubTerrType1,UINT8 const ubTerrType2)3831 static UINT8 CalcEffVolume(SOLDIERTYPE const* const pSoldier, INT16 const sGridNo, INT8 const bLevel, NoiseKind const ubNoiseType, UINT8 const ubBaseVolume, UINT8 const bCheckTerrain, UINT8 const ubTerrType1, UINT8 const ubTerrType2)
3832 {
3833 	INT32 iEffVolume, iDistance;
3834 
3835 	if (IsWearingHeadGear(*pSoldier, WALKMAN))
3836 	{
3837 		return( 0 );
3838 	}
3839 
3840 	if ( gTacticalStatus.uiFlags & INCOMBAT )
3841 	{
3842 		// ATE: Funny things happen to ABC stuff if bNewSituation set....
3843 		if ( gTacticalStatus.ubCurrentTeam == pSoldier->bTeam )
3844 		{
3845 			return( 0 );
3846 		}
3847 	}
3848 
3849 	// adjust default noise volume by listener's hearing capability
3850 	iEffVolume = (INT32) ubBaseVolume + (INT32) DecideHearing( pSoldier );
3851 
3852 
3853 	// effective volume reduced by listener's number of opponents in sight
3854 	iEffVolume -= pSoldier->bOppCnt;
3855 
3856 
3857 	// calculate the distance (in adjusted pixels) between the source of the
3858 	// noise (gridno) and the location of the would-be listener (pSoldier->gridno)
3859 	iDistance = (INT32) PythSpacesAway( pSoldier->sGridNo, sGridNo );
3860 
3861 	// effective volume fades over distance beyond 1 tile away
3862 	iEffVolume -= (iDistance - 1);
3863 
3864 	/*
3865 	if (pSoldier->bTeam == CIV_TEAM && pSoldier->ubBodyType != CROW )
3866 	{
3867 		if (pSoldier->ubCivilianGroup == 0 && pSoldier->ubProfile == NO_PROFILE)
3868 		{
3869 			// nameless civs reduce effective volume by 2 for gunshots etc
3870 			// (double the reduction due to distance)
3871 			// so that they don't cower from attacks that are really far away
3872 			switch (ubNoiseType)
3873 			{
3874 				case NOISE_GUNFIRE:
3875 				case NOISE_BULLET_IMPACT:
3876 				case NOISE_GRENADE_IMPACT:
3877 				case NOISE_EXPLOSION:
3878 					iEffVolume -= iDistance;
3879 					break;
3880 				default:
3881 					break;
3882 			}
3883 		}
3884 		else if (pSoldier->bNeutral)
3885 		{
3886 			// NPCs and people in groups ignore attack noises unless they are no longer neutral
3887 			switch (ubNoiseType)
3888 			{
3889 				case NOISE_GUNFIRE:
3890 				case NOISE_BULLET_IMPACT:
3891 				case NOISE_GRENADE_IMPACT:
3892 				case NOISE_EXPLOSION:
3893 					iEffVolume = 0;
3894 					break;
3895 				default:
3896 					break;
3897 			}
3898 		}
3899 	}*/
3900 
3901 	if (pSoldier->usAnimState == RUNNING)
3902 	{
3903 		iEffVolume -= 5;
3904 	}
3905 
3906 #if 0 /* XXX Something is very wrong here... */
3907 	if (pSoldier->bAssignment == SLEEPING )
3908 	{
3909 		// decrease effective volume since we're asleep!
3910 		iEffVolume -= 5;
3911 	}
3912 #endif
3913 
3914 	// check for floor/roof difference
3915 	if (bLevel > pSoldier->bLevel)
3916 	{
3917 		// sound is amplified by roof
3918 		iEffVolume += 5;
3919 	}
3920 	else if (bLevel < pSoldier->bLevel)
3921 	{
3922 		// sound is muffled
3923 		iEffVolume -= 5;
3924 	}
3925 
3926 	// if we still have a chance of hearing this, and the terrain types are known
3927 	if (iEffVolume > 0)
3928 	{
3929 		if (bCheckTerrain)
3930 		{
3931 			// if, between noise and listener, one is outside and one is inside
3932 
3933 			// NOTE: This is a pretty dumb way of doing things, since it won't detect
3934 			// the presence of walls between 2 spots both inside or both outside, but
3935 			// given our current system it's the best that we can do
3936 
3937 			if (((ubTerrType1 == FLAT_FLOOR) && (ubTerrType2 != FLAT_FLOOR)) ||
3938 				((ubTerrType1 != FLAT_FLOOR) && (ubTerrType2 == FLAT_FLOOR)))
3939 			{
3940 				// sound is muffled, reduce the effective volume of the noise
3941 				iEffVolume -= 5;
3942 			}
3943 		}
3944 
3945 	}
3946 	if (iEffVolume > 0)
3947 	{
3948 		return( (UINT8) iEffVolume );
3949 	}
3950 	else
3951 	{
3952 		return( 0 );
3953 	}
3954 }
3955 
3956 
HearNoise(SOLDIERTYPE * const pSoldier,SOLDIERTYPE * const noise_maker,UINT16 const sGridNo,INT8 const bLevel,UINT8 const ubVolume,NoiseKind const ubNoiseType,BOOLEAN * const ubSeen)3957 static void HearNoise(SOLDIERTYPE* const pSoldier, SOLDIERTYPE* const noise_maker, UINT16 const sGridNo, INT8 const bLevel, UINT8 const ubVolume, NoiseKind const ubNoiseType, BOOLEAN* const ubSeen)
3958 {
3959 	INT16   sNoiseX, sNoiseY;
3960 	INT8    bHadToTurn = FALSE, bSourceSeen = FALSE;
3961 	INT8    bOldOpplist;
3962 	INT16   sDistVisible;
3963 	INT8    bDirection;
3964 	BOOLEAN fMuzzleFlash = FALSE;
3965 
3966 	SLOGD("%d hears noise from %d (%d/%d) volume %d",
3967 		pSoldier->ubID, SOLDIER2ID(noise_maker), sGridNo, bLevel, ubVolume);
3968 
3969 
3970 	if ( pSoldier->ubBodyType == CROW )
3971 	{
3972 		CrowsFlyAway( pSoldier->bTeam );
3973 		return;
3974 	}
3975 
3976 	// "Turn head" towards the source of the noise and try to see what's there
3977 
3978 	// don't use DistanceVisible here, but use maximum visibility distance
3979 	// in as straight line instead.  Represents guy "turning just his head"
3980 
3981 	// CJC 97/10: CHANGE!  Since STRAIGHT can not reliably be used as a
3982 	// max sighting distance (varies based on realtime/turnbased), call
3983 	// the function with the new DIRECTION_IRRELEVANT define
3984 
3985 	// is he close enough to see that gridno if he turns his head?
3986 
3987 	// ignore muzzle flashes when turning head to see noise
3988 	if (ubNoiseType == NOISE_GUNFIRE && noise_maker != NULL && noise_maker->fMuzzleFlash)
3989 	{
3990 		sNoiseX = CenterX(sGridNo);
3991 		sNoiseY = CenterY(sGridNo);
3992 		bDirection = atan8(pSoldier->sX,pSoldier->sY,sNoiseX,sNoiseY);
3993 		if (pSoldier->bDirection != bDirection &&
3994 			pSoldier->bDirection != OneCDirection(bDirection) &&
3995 			pSoldier->bDirection != OneCCDirection(bDirection))
3996 		{
3997 			// temporarily turn off muzzle flash so DistanceVisible can be calculated without it
3998 			noise_maker->fMuzzleFlash = FALSE;
3999 			fMuzzleFlash = TRUE;
4000 		}
4001 	}
4002 
4003 	sDistVisible = DistanceVisible( pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sGridNo, bLevel );
4004 
4005 	if ( fMuzzleFlash )
4006 	{
4007 		// turn flash on again
4008 		noise_maker->fMuzzleFlash = TRUE;
4009 	}
4010 
4011 	if (PythSpacesAway(pSoldier->sGridNo,sGridNo) <= sDistVisible )
4012 	{
4013 		// just use the XXadjustedXX center of the gridno
4014 		sNoiseX = CenterX(sGridNo);
4015 		sNoiseY = CenterY(sGridNo);
4016 
4017 		if (pSoldier->bDirection != atan8(pSoldier->sX,pSoldier->sY,sNoiseX,sNoiseY))
4018 		{
4019 			bHadToTurn = TRUE;
4020 		}
4021 		else
4022 		{
4023 			bHadToTurn = FALSE;
4024 		}
4025 
4026 		// and we can trace a line of sight to his x,y coordinates?
4027 		// (taking into account we are definitely aware of this guy now)
4028 
4029 		// skip LOS check if we had to turn and we're a tank.  sorry Mr Tank, no looking out of the sides for you!
4030 		if ( !( bHadToTurn && TANK( pSoldier ) ) )
4031 		{
4032 			if ( SoldierTo3DLocationLineOfSightTest( pSoldier, sGridNo, bLevel, 0, (UINT8) sDistVisible, TRUE ) )
4033 			{
4034 				// he can actually see the spot where the noise came from!
4035 				bSourceSeen = TRUE;
4036 
4037 				// if this sounds like a door opening/closing (could also be a crate)
4038 				if (ubNoiseType == NOISE_CREAKING)
4039 				{
4040 					// then look around and update ALL doors that have secretly changed
4041 					//LookForDoors(pSoldier,AWARE);
4042 				}
4043 			}
4044 		}
4045 	}
4046 
4047 	// if noise is made by a person
4048 	if (noise_maker != NULL)
4049 	{
4050 		bOldOpplist = pSoldier->bOppList[noise_maker->ubID];
4051 
4052 		// WE ALREADY KNOW THAT HE'S ON ANOTHER TEAM, AND HE'S NOT BEING SEEN
4053 		// ProcessNoise() ALREADY DID THAT WORK FOR US
4054 
4055 		if (bSourceSeen)
4056 		{
4057 			ManSeesMan(*pSoldier, *noise_maker, CALLER_UNKNOWN);
4058 
4059 			// if it's an AI soldier, he is not allowed to automatically radio any
4060 			// noise heard, but manSeesMan has set his newOppCnt, so clear it here
4061 			if (!(pSoldier->uiStatusFlags & SOLDIER_PC))
4062 			{
4063 				pSoldier->bNewOppCnt = 0;
4064 			}
4065 
4066 			*ubSeen = TRUE;
4067 			// RadioSightings() must only be called later on by ProcessNoise() itself
4068 			// because we want the soldier who heard noise the LOUDEST to report it
4069 
4070 			if ( pSoldier->bNeutral )
4071 			{
4072 				// could be a civilian watching us shoot at an enemy
4073 				if (((ubNoiseType == NOISE_GUNFIRE) || (ubNoiseType == NOISE_BULLET_IMPACT)) &&
4074 					(ubVolume >= 3))
4075 				{
4076 					// if status is only GREEN or YELLOW
4077 					if (pSoldier->bAlertStatus < STATUS_RED)
4078 					{
4079 						// then this soldier goes to status RED, has proof of enemy presence
4080 						pSoldier->bAlertStatus = STATUS_RED;
4081 						CheckForChangingOrders(pSoldier);
4082 					}
4083 				}
4084 			}
4085 
4086 		}
4087 		else // noise maker still can't be seen
4088 		{
4089 			SetNewSituation( pSoldier ); // re-evaluate situation
4090 
4091 			// if noise type was unmistakably that of gunfire
4092 			if (((ubNoiseType == NOISE_GUNFIRE) || (ubNoiseType == NOISE_BULLET_IMPACT)) && (ubVolume >= 3))
4093 			{
4094 				// if status is only GREEN or YELLOW
4095 				if (pSoldier->bAlertStatus < STATUS_RED)
4096 				{
4097 					// then this soldier goes to status RED, has proof of enemy presence
4098 					pSoldier->bAlertStatus = STATUS_RED;
4099 					CheckForChangingOrders(pSoldier);
4100 				}
4101 			}
4102 
4103 			// remember that the soldier has been heard and his new location
4104 			UpdatePersonal(pSoldier, noise_maker->ubID ,HEARD_THIS_TURN, sGridNo, bLevel);
4105 
4106 			// Public info is not set unless EVERYONE on the team fails to see the
4107 			// ubnoisemaker, leaving the 'seen' flag FALSE.  See ProcessNoise().
4108 
4109 			// CJC: set the noise gridno for the soldier, if appropriate - this is what is looked at by the AI!
4110 			if (ubVolume >= pSoldier->ubNoiseVolume)
4111 			{
4112 				// yes it is, so remember this noise INSTEAD (old noise is forgotten)
4113 				pSoldier->sNoiseGridno = sGridNo;
4114 				pSoldier->bNoiseLevel = bLevel;
4115 
4116 				// no matter how loud noise was, don't remember it for than 12 turns!
4117 				if (ubVolume < MAX_MISC_NOISE_DURATION)
4118 				{
4119 					pSoldier->ubNoiseVolume = ubVolume;
4120 				}
4121 				else
4122 				{
4123 					pSoldier->ubNoiseVolume = MAX_MISC_NOISE_DURATION;
4124 				}
4125 
4126 				SetNewSituation( pSoldier );  // force a fresh AI decision to be made
4127 			}
4128 
4129 		}
4130 
4131 		if ( pSoldier->fAIFlags & AI_ASLEEP )
4132 		{
4133 			switch( ubNoiseType )
4134 			{
4135 				case NOISE_BULLET_IMPACT:
4136 				case NOISE_GUNFIRE:
4137 				case NOISE_EXPLOSION:
4138 				case NOISE_SCREAM:
4139 				case NOISE_WINDOW_SMASHING:
4140 				case NOISE_DOOR_SMASHING:
4141 					// WAKE UP!
4142 					pSoldier->fAIFlags &= (~AI_ASLEEP);
4143 					break;
4144 				default:
4145 					break;
4146 			}
4147 		}
4148 
4149 		// FIRST REQUIRE MUTUAL HOSTILES!
4150 		if (!CONSIDERED_NEUTRAL(noise_maker, pSoldier) && !CONSIDERED_NEUTRAL(pSoldier, noise_maker) &&
4151 			pSoldier->bSide != noise_maker->bSide)
4152 		{
4153 			// regardless of whether the noisemaker (who's not NOBODY) was seen or not,
4154 			// as long as listener meets minimum interrupt conditions
4155 			if ( gfDelayResolvingBestSightingDueToDoor)
4156 			{
4157 				if (bSourceSeen && (!(gTacticalStatus.uiFlags & INCOMBAT) ||
4158 					(gubSightFlags & SIGHTINTERRUPT &&
4159 					StandardInterruptConditionsMet(pSoldier, noise_maker, bOldOpplist))))
4160 				{
4161 					// we should be adding this to the array for the AllTeamLookForAll to handle
4162 					// since this is a door opening noise, add a bonus equal to half the door volume
4163 					UINT8 ubPoints;
4164 
4165 					ubPoints = CalcInterruptDuelPts(pSoldier, noise_maker, TRUE);
4166 					if ( ubPoints != NO_INTERRUPT )
4167 					{
4168 						// require the enemy not to be dying if we are the sighter; in other words,
4169 						// always add for AI guys, and always add for people with life >= OKLIFE
4170 						if (pSoldier->bTeam != OUR_TEAM || noise_maker->bLife >= OKLIFE)
4171 						{
4172 							ReevaluateBestSightingPosition( pSoldier, (UINT8) (ubPoints + (ubVolume / 2)) );
4173 						}
4174 					}
4175 				}
4176 			}
4177 			else
4178 			{
4179 				if (gTacticalStatus.uiFlags & INCOMBAT)
4180 				{
4181 					if (StandardInterruptConditionsMet(pSoldier, noise_maker, bOldOpplist))
4182 					{
4183 						// he gets a chance to interrupt the noisemaker
4184 						pSoldier->bInterruptDuelPts = CalcInterruptDuelPts(pSoldier, noise_maker, TRUE);
4185 						SLOGD(
4186 							"Calculating int duel pts in noise code, %d has %d points",
4187 							pSoldier->ubID, pSoldier->bInterruptDuelPts);
4188 					}
4189 					else
4190 					{
4191 						pSoldier->bInterruptDuelPts = NO_INTERRUPT;
4192 					}
4193 				}
4194 				else if ( bSourceSeen )
4195 				{
4196 					// seen source, in realtime, so check for sighting stuff
4197 					HandleBestSightingPositionInRealtime();
4198 				}
4199 			}
4200 
4201 		}
4202 	}
4203 	else   // noise made by NOBODY
4204 	{
4205 		// if noise type was unmistakably that of an explosion (seen or not) or alarm
4206 		if (!(pSoldier->uiStatusFlags & SOLDIER_PC))
4207 		{
4208 			if ( ( ubNoiseType == NOISE_EXPLOSION || ubNoiseType == NOISE_SILENT_ALARM ) && (ubVolume >= 3) )
4209 			{
4210 				if ( ubNoiseType == NOISE_SILENT_ALARM )
4211 				{
4212 					WearGasMaskIfAvailable( pSoldier );
4213 				}
4214 				// if status is only GREEN or YELLOW
4215 				if (pSoldier->bAlertStatus < STATUS_RED)
4216 				{
4217 					// then this soldier goes to status RED, has proof of enemy presence
4218 					pSoldier->bAlertStatus = STATUS_RED;
4219 					CheckForChangingOrders(pSoldier);
4220 				}
4221 			}
4222 		}
4223 		// if the source of the noise can't be seen,
4224 		// OR if it's a rock and the listener had to turn so that by the time he
4225 		// looked all his saw was a bunch of rocks lying still
4226 		if (!bSourceSeen || ((ubNoiseType == NOISE_ROCK_IMPACT) && (bHadToTurn) ) ||
4227 			ubNoiseType == NOISE_SILENT_ALARM )
4228 		{
4229 			// check if the effective volume of this new noise is greater than or at
4230 			// least equal to the volume of the currently noticed noise stored
4231 			if (ubVolume >= pSoldier->ubNoiseVolume)
4232 			{
4233 				// yes it is, so remember this noise INSTEAD (old noise is forgotten)
4234 				pSoldier->sNoiseGridno = sGridNo;
4235 				pSoldier->bNoiseLevel = bLevel;
4236 
4237 				// no matter how loud noise was, don't remember it for than 12 turns!
4238 				if (ubVolume < MAX_MISC_NOISE_DURATION)
4239 				{
4240 					pSoldier->ubNoiseVolume = ubVolume;
4241 				}
4242 				else
4243 				{
4244 					pSoldier->ubNoiseVolume = MAX_MISC_NOISE_DURATION;
4245 				}
4246 
4247 				SetNewSituation( pSoldier ); // force a fresh AI decision to be made
4248 			}
4249 		}
4250 		else
4251 		// if listener sees the source of the noise, AND it's either a grenade,
4252 		//  or it's a rock that he watched land (didn't need to turn)
4253 		{
4254 			SetNewSituation( pSoldier );  // re-evaluate situation
4255 
4256 			// if status is only GREEN or YELLOW
4257 			if (pSoldier->bAlertStatus < STATUS_RED)
4258 			{
4259 				// then this soldier goes to status RED, has proof of enemy presence
4260 				pSoldier->bAlertStatus = STATUS_RED;
4261 				CheckForChangingOrders(pSoldier);
4262 			}
4263 		}
4264 
4265 		if ( gubBestToMakeSightingSize == BEST_SIGHTING_ARRAY_SIZE_INCOMBAT )
4266 		{
4267 			// if the noise heard was the fall of a rock
4268 			if ((gTacticalStatus.uiFlags & INCOMBAT) && ubNoiseType == NOISE_ROCK_IMPACT)
4269 			{
4270 				// give every ELIGIBLE listener an automatic interrupt, since it's
4271 				// reasonable to assume the guy throwing wants to wait for their reaction!
4272 				if (StandardInterruptConditionsMet(pSoldier, NULL, FALSE))
4273 				{
4274 					pSoldier->bInterruptDuelPts = AUTOMATIC_INTERRUPT; // force automatic interrupt
4275 					SLOGD(
4276 						"Calculating int duel pts in noise code, %d has %d points",
4277 						pSoldier->ubID, pSoldier->bInterruptDuelPts);
4278 				}
4279 				else
4280 				{
4281 					pSoldier->bInterruptDuelPts = NO_INTERRUPT;
4282 				}
4283 			}
4284 		}
4285 	}
4286 }
4287 
4288 
TellPlayerAboutNoise(SOLDIERTYPE * const s,SOLDIERTYPE const * const noise_maker,INT16 const sGridNo,INT8 const level,UINT8 const volume,NoiseKind const noise_type,UINT8 const noise_dir)4289 static void TellPlayerAboutNoise(SOLDIERTYPE* const s, SOLDIERTYPE const* const noise_maker, INT16 const sGridNo, INT8 const level, UINT8 const volume, NoiseKind const noise_type, UINT8 const noise_dir)
4290 {
4291 	// CJC: tweaked the noise categories upwards a bit because our movement noises
4292 	// can be louder now.
4293 	UINT8 const volume_idx =
4294 		volume <  4 ? 0 : // 1-3:  faint noise
4295 		volume <  8 ? 1 : // 4-7:  definite noise
4296 		volume < 12 ? 2 : // 8-11: loud noise
4297 		3;                // 12+:  very loud noise
4298 
4299 	if (noise_maker && s->bTeam == OUR_TEAM && s->bTeam == noise_maker->bTeam)
4300 	{
4301 		SLOGE("%s (%d) heard noise from %s (%d), noise at %dL%d, type %d",
4302 			s->name.c_str(), s->ubID, noise_maker->name.c_str(), noise_maker->ubID, sGridNo, level, noise_type);
4303 	}
4304 
4305 	// display a message about a noise, e.g. Sidney hears a loud splash from/to?
4306 	// the north.
4307 	ST::string direction =
4308 		level      == s->bLevel            ||
4309 		noise_type == NOISE_EXPLOSION      ||
4310 		noise_type == NOISE_SCREAM         ||
4311 		noise_type == NOISE_ROCK_IMPACT    ||
4312 		noise_type == NOISE_GRENADE_IMPACT ? pDirectionStr[noise_dir] :
4313 		level      >  s->bLevel            ? gzLateLocalizedString[STR_LATE_06] : // From above
4314 		gzLateLocalizedString[STR_LATE_07];                                       // From below
4315 	ScreenMsg(MSG_FONT_YELLOW, MSG_INTERFACE, st_format_printf(pNewNoiseStr[noise_type], s->name, pNoiseVolStr[volume_idx], direction));
4316 
4317 	// If the sound was faint, say something
4318 	if (volume_idx == 0 &&
4319 		!AreInMeanwhile() &&
4320 		!(gTacticalStatus.uiFlags & ENGAGED_IN_CONV) &&
4321 		s->ubTurnsUntilCanSayHeardNoise == 0)
4322 	{
4323 		TacticalCharacterDialogue(s, QUOTE_HEARD_SOMETHING);
4324 		s->ubTurnsUntilCanSayHeardNoise = gTacticalStatus.uiFlags & INCOMBAT ? 2 : 5;
4325 	}
4326 }
4327 
4328 
VerifyAndDecayOpplist(SOLDIERTYPE * pSoldier)4329 void VerifyAndDecayOpplist(SOLDIERTYPE *pSoldier)
4330 {
4331 	INT8 *pPersOL; // pointer into soldier's opponent list
4332 
4333 	// reduce all seen/known opponent's turn counters by 1 (towards 0)
4334 	// 1) verify accuracy of the opplist by testing sight vs known opponents
4335 	// 2) increment opplist value if opponent is known but not currenly seen
4336 	// 3) forget about known opponents who haven't been noticed in some time
4337 
4338 	// if soldier is unconscious, make sure his opplist is wiped out & bail out
4339 	if (pSoldier->bLife < OKLIFE)
4340 	{
4341 		std::fill(std::begin(pSoldier->bOppList), std::end(pSoldier->bOppList), NOT_HEARD_OR_SEEN);
4342 		pSoldier->bOppCnt = 0;
4343 		return;
4344 	}
4345 
4346 	// if any new opponents were seen earlier and not yet radioed
4347 	if (pSoldier->bNewOppCnt)
4348 	{
4349 		if (pSoldier->uiStatusFlags & SOLDIER_PC)
4350 		{
4351 			RadioSightings(pSoldier,EVERYBODY,pSoldier->bTeam);
4352 		}
4353 
4354 		pSoldier->bNewOppCnt = 0;
4355 	}
4356 
4357 	// man looks for each of his opponents WHO ARE ALREADY KNOWN TO HIM
4358 	FOR_EACH_MERC(i)
4359 	{
4360 		SOLDIERTYPE* const pOpponent = *i;
4361 		// if this merc is active, here, and alive
4362 		if (pOpponent->bLife)
4363 		{
4364 			// if this merc is on the same team, he's no opponent, so skip him
4365 			if (pSoldier->bTeam == pOpponent->bTeam)
4366 			{
4367 				continue;
4368 			}
4369 
4370 			pPersOL = pSoldier->bOppList + pOpponent->ubID;
4371 
4372 			// if this opponent is "known" in any way (seen or heard recently)
4373 			if (*pPersOL != NOT_HEARD_OR_SEEN)
4374 			{
4375 				// use both sides actual x,y co-ordinates (neither side's moving)
4376 				ManLooksForMan(pSoldier,pOpponent,VERIFYANDDECAYOPPLIST);
4377 
4378 				// decay opplist value if necessary
4379 				DECAY_OPPLIST_VALUE( *pPersOL );
4380 				/*
4381 				// if opponent was SEEN recently but is NOT visible right now
4382 				if (*pPersOL >= SEEN_THIS_TURN)
4383 				{
4384 					(*pPersOL)++; // increment #turns it's been since last seen
4385 
4386 					// if it's now been longer than the maximum we care to remember
4387 					if (*pPersOL > SEEN_2_TURNS_AGO)
4388 						*pPersOL = 0; // forget that we knew this guy
4389 				}
4390 				else
4391 				{
4392 					// if opponent was merely HEARD recently, not actually seen
4393 					if (*pPersOL <= HEARD_THIS_TURN)
4394 					{
4395 						(*pPersOL)--; // increment #turns it's been since last heard
4396 
4397 						// if it's now been longer than the maximum we care to remember
4398 						if (*pPersOL < HEARD_2_TURNS_AGO)
4399 							*pPersOL = 0; // forget that we knew this guy
4400 					}
4401 				}*/
4402 			}
4403 
4404 		}
4405 	}
4406 
4407 
4408 	// if any new opponents were seen
4409 	if (pSoldier->bNewOppCnt)
4410 	{
4411 		// turns out this is NOT an error!  If this guy was gassed last time he
4412 		// looked, his sight limit was 2 tiles, and now he may no longer be gassed
4413 		// and thus he sees opponents much further away for the first time!
4414 		// - Always happens if you STUNGRENADE an opponent by surprise...
4415 		if (pSoldier->uiStatusFlags & SOLDIER_PC)
4416 			RadioSightings(pSoldier,EVERYBODY,pSoldier->bTeam);
4417 
4418 		pSoldier->bNewOppCnt = 0;
4419 	}
4420 }
4421 
DecayIndividualOpplist(SOLDIERTYPE * pSoldier)4422 void DecayIndividualOpplist(SOLDIERTYPE *pSoldier)
4423 {
4424 	INT8 *pPersOL; // pointer into soldier's opponent list
4425 
4426 	// reduce all currently seen opponent's turn counters by 1 (towards 0)
4427 
4428 	// if soldier is unconscious, make sure his opplist is wiped out & bail out
4429 	if (pSoldier->bLife < OKLIFE)
4430 	{
4431 		// must make sure that public opplist is kept to match...
4432 		FOR_EACH_SOLDIER(tgt)
4433 		{
4434 			if (pSoldier->bOppList[tgt->ubID] == SEEN_CURRENTLY)
4435 			{
4436 				HandleManNoLongerSeen(pSoldier, tgt, &pSoldier->bOppList[tgt->ubID], &gbPublicOpplist[pSoldier->bTeam][tgt->ubID]);
4437 			}
4438 		}
4439 
4440 		std::fill(std::begin(pSoldier->bOppList), std::end(pSoldier->bOppList), NOT_HEARD_OR_SEEN);
4441 		pSoldier->bOppCnt = 0;
4442 		return;
4443 	}
4444 
4445 	// man looks for each of his opponents WHO IS CURRENTLY SEEN
4446 	FOR_EACH_MERC(i)
4447 	{
4448 		const SOLDIERTYPE* const pOpponent = *i;
4449 		// if this merc is active, here, and alive
4450 		if (pOpponent->bLife)
4451 		{
4452 			// if this merc is on the same team, he's no opponent, so skip him
4453 			if (pSoldier->bTeam == pOpponent->bTeam)
4454 			{
4455 				continue;
4456 			}
4457 
4458 			pPersOL = pSoldier->bOppList + pOpponent->ubID;
4459 
4460 			// if this opponent is seen currently
4461 			if (*pPersOL == SEEN_CURRENTLY)
4462 			{
4463 				// they are NOT visible now!
4464 				(*pPersOL)++;
4465 				if (!CONSIDERED_NEUTRAL(pOpponent, pSoldier) &&
4466 					!CONSIDERED_NEUTRAL(pSoldier, pOpponent) &&
4467 					(pSoldier->bSide != pOpponent->bSide))
4468 				{
4469 					RemoveOneOpponent(pSoldier);
4470 				}
4471 
4472 			}
4473 		}
4474 	}
4475 }
4476 
4477 
4478 
VerifyPublicOpplistDueToDeath(SOLDIERTYPE * pSoldier)4479 void VerifyPublicOpplistDueToDeath(SOLDIERTYPE *pSoldier)
4480 {
4481 	// OK, someone died. Anyone that the deceased ALONE saw has to decay
4482 	// immediately in the Public Opplist.
4483 
4484 	// If deceased didn't see ANYONE, don't bother
4485 	if (pSoldier->bOppCnt == 0)
4486 	{
4487 		return;
4488 	}
4489 
4490 
4491 	// Deceased looks for each of his opponents who is "seen currently"
4492 	FOR_EACH_MERC(i)
4493 	{
4494 		SOLDIERTYPE* const pOpponent = *i;
4495 
4496 		// first, initialize flag since this will be a "new" opponent
4497 		BOOLEAN bOpponentStillSeen = FALSE;
4498 
4499 		// if this opponent is active, here, and alive
4500 		if (pOpponent->bLife)
4501 		{
4502 			// if this opponent is on the same team, he's no opponent, so skip him
4503 			if (pSoldier->bTeam == pOpponent->bTeam)
4504 			{
4505 				continue;
4506 			}
4507 
4508 			// point to what the deceased's personal opplist value is
4509 			const INT8* const pPersOL = pSoldier->bOppList + pOpponent->ubID;
4510 
4511 			// if this opponent was CURRENTLY SEEN by the deceased (before his
4512 			// untimely demise)
4513 			if (*pPersOL == SEEN_CURRENTLY)
4514 			{
4515 				// then we need to know if any teammates ALSO see this opponent, so loop through
4516 				// trying to find ONE witness to the death...
4517 				FOR_EACH_MERC(j)
4518 				{
4519 					const SOLDIERTYPE* const pTeamMate = *j;
4520 					// if this teammate is active, here, and alive
4521 					if (pTeamMate->bLife)
4522 					{
4523 						// if this opponent is NOT on the same team, then skip him
4524 						if (pTeamMate->bTeam != pSoldier->bTeam)
4525 						{
4526 							continue;
4527 						}
4528 
4529 						// point to what the teammate's personal opplist value is
4530 						const INT8* const pMatePersOL = pTeamMate->bOppList + pOpponent->ubID;
4531 
4532 						// test to see if this value is "seen currently"
4533 						if (*pMatePersOL == SEEN_CURRENTLY)
4534 						{
4535 							// this opponent HAS been verified!
4536 							bOpponentStillSeen = TRUE;
4537 
4538 							// we can stop looking for other witnesses now
4539 							break;
4540 						}
4541 					}
4542 				}
4543 			}
4544 
4545 			// if no witnesses for this opponent, then decay the Public Opplist
4546 			if ( !bOpponentStillSeen )
4547 			{
4548 				DECAY_OPPLIST_VALUE( gbPublicOpplist[pSoldier->bTeam][pOpponent->ubID] );
4549 			}
4550 		}
4551 	}
4552 }
4553 
4554 
4555 static void DecayWatchedLocs(INT8 bTeam);
4556 
4557 
DecayPublicOpplist(INT8 bTeam)4558 void DecayPublicOpplist(INT8 bTeam)
4559 {
4560 	INT8 bNoPubliclyKnownOpponents = TRUE;
4561 	INT8 *pbPublOL;
4562 
4563 	// decay the team's public noise volume, forget public noise gridno if <= 0
4564 	// used to be -1 per turn but that's not fast enough!
4565 	if (gubPublicNoiseVolume[bTeam] > 0)
4566 	{
4567 		if ( gTacticalStatus.uiFlags & INCOMBAT )
4568 		{
4569 			gubPublicNoiseVolume[bTeam] = (UINT8) ( (UINT32) (gubPublicNoiseVolume[bTeam] * 7) / 10 );
4570 		}
4571 		else
4572 		{
4573 			gubPublicNoiseVolume[bTeam] = gubPublicNoiseVolume[bTeam] / 2;
4574 		}
4575 
4576 		if (gubPublicNoiseVolume[bTeam] <= 0)
4577 		{
4578 			gsPublicNoiseGridno[bTeam] = NOWHERE;
4579 		}
4580 	}
4581 
4582 	// decay the team's Public Opplist
4583 	FOR_EACH_MERC(i)
4584 	{
4585 		SOLDIERTYPE* const pSoldier = *i;
4586 		// for every living soldier on ANOTHER team
4587 		if (pSoldier->bLife && pSoldier->bTeam != bTeam)
4588 		{
4589 			// hang a pointer to the byte holding team's public opplist for this merc
4590 			pbPublOL = &gbPublicOpplist[bTeam][pSoldier->ubID];
4591 
4592 			if (*pbPublOL == NOT_HEARD_OR_SEEN)
4593 			{
4594 				continue;
4595 			}
4596 
4597 			// well, that make this a "publicly known opponent", so nuke that flag
4598 			bNoPubliclyKnownOpponents = FALSE;
4599 
4600 			// if this person has been SEEN recently, but is not currently visible
4601 			if (*pbPublOL >= SEEN_THIS_TURN)
4602 			{
4603 				(*pbPublOL)++; // increment how long it's been
4604 			}
4605 			else
4606 			{
4607 				// if this person has been only HEARD recently
4608 				if (*pbPublOL <= HEARD_THIS_TURN)
4609 				{
4610 					(*pbPublOL)--; // increment how long it's been
4611 				}
4612 			}
4613 
4614 			// if it's been longer than the maximum we care to remember
4615 			if ((*pbPublOL > OLDEST_SEEN_VALUE) || (*pbPublOL < OLDEST_HEARD_VALUE))
4616 			{
4617 				// forget about him,
4618 				// and also forget where he was last seen (it's been too long)
4619 				// this is mainly so POINT_PATROL guys don't SEEK_OPPONENTs forever
4620 				UpdatePublic(bTeam, pSoldier, NOT_HEARD_OR_SEEN, NOWHERE, 0);
4621 			}
4622 		}
4623 	}
4624 
4625 	// if all opponents are publicly unknown (NOT_HEARD_OR_SEEN)
4626 	if (bNoPubliclyKnownOpponents)
4627 	{
4628 		// forget about the last radio alert (ie. throw away who made the call)
4629 		// this is mainly so POINT_PATROL guys don't SEEK_FRIEND forever after
4630 		gTacticalStatus.Team[bTeam].last_merc_to_radio = NULL;
4631 	}
4632 
4633 	// decay watched locs as well
4634 	DecayWatchedLocs( bTeam );
4635 }
4636 
4637 // bit of a misnomer; this is now decay all opplists
NonCombatDecayPublicOpplist(UINT32 uiTime)4638 void NonCombatDecayPublicOpplist( UINT32 uiTime )
4639 {
4640 	if ( uiTime - gTacticalStatus.uiTimeSinceLastOpplistDecay >= TIME_BETWEEN_RT_OPPLIST_DECAYS)
4641 	{
4642 		// decay!
4643 		FOR_EACH_MERC(i) VerifyAndDecayOpplist(*i);
4644 
4645 		for (UINT32 cnt = 0; cnt < MAXTEAMS; ++cnt)
4646 		{
4647 			if (IsTeamActive(cnt)) DecayPublicOpplist((INT8)cnt);
4648 		}
4649 		// update time
4650 		gTacticalStatus.uiTimeSinceLastOpplistDecay = uiTime;
4651 	}
4652 }
4653 
RecalculateOppCntsDueToNoLongerNeutral(SOLDIERTYPE * pSoldier)4654 void RecalculateOppCntsDueToNoLongerNeutral( SOLDIERTYPE * pSoldier )
4655 {
4656 	pSoldier->bOppCnt = 0;
4657 
4658 	if (!pSoldier->bNeutral)
4659 	{
4660 		FOR_EACH_MERC(i)
4661 		{
4662 			SOLDIERTYPE* const pOpponent = *i;
4663 			// for every living soldier on ANOTHER team
4664 			if (pOpponent->bLife &&
4665 				!pOpponent->bNeutral &&
4666 				pOpponent->bTeam != pSoldier->bTeam &&
4667 				!CONSIDERED_NEUTRAL(pOpponent, pSoldier) &&
4668 				!CONSIDERED_NEUTRAL(pSoldier, pOpponent) &&
4669 				pSoldier->bSide != pOpponent->bSide)
4670 			{
4671 				if ( pSoldier->bOppList[pOpponent->ubID] == SEEN_CURRENTLY )
4672 				{
4673 					AddOneOpponent( pSoldier );
4674 				}
4675 				if ( pOpponent->bOppList[pSoldier->ubID] == SEEN_CURRENTLY )
4676 				{
4677 					// have to add to opponent's oppcount as well since we just became non-neutral
4678 					AddOneOpponent( pOpponent );
4679 				}
4680 			}
4681 		}
4682 	}
4683 }
4684 
RecalculateOppCntsDueToBecomingNeutral(SOLDIERTYPE * pSoldier)4685 void RecalculateOppCntsDueToBecomingNeutral( SOLDIERTYPE * pSoldier )
4686 {
4687 	if (pSoldier->bNeutral)
4688 	{
4689 		pSoldier->bOppCnt = 0;
4690 
4691 		FOR_EACH_MERC(i)
4692 		{
4693 			SOLDIERTYPE* const pOpponent = *i;
4694 			// for every living soldier on ANOTHER team
4695 			if (pOpponent->bLife &&
4696 				!pOpponent->bNeutral &&
4697 				pOpponent->bTeam != pSoldier->bTeam &&
4698 				!CONSIDERED_NEUTRAL(pSoldier, pOpponent) &&
4699 				pSoldier->bSide != pOpponent->bSide)
4700 			{
4701 				if ( pOpponent->bOppList[pSoldier->ubID] == SEEN_CURRENTLY )
4702 				{
4703 					// have to rem from opponent's oppcount as well since we just became neutral
4704 					RemoveOneOpponent( pOpponent );
4705 				}
4706 			}
4707 		}
4708 	}
4709 }
4710 
NoticeUnseenAttacker(SOLDIERTYPE * pAttacker,SOLDIERTYPE * pDefender,INT8 bReason)4711 void NoticeUnseenAttacker( SOLDIERTYPE * pAttacker, SOLDIERTYPE * pDefender, INT8 bReason )
4712 {
4713 	INT8    bOldOppList;
4714 	UINT8   ubTileSightLimit;
4715 	BOOLEAN fSeesAttacker = FALSE;
4716 	INT8    bDirection;
4717 	BOOLEAN	fMuzzleFlash = FALSE;
4718 
4719 	if ( !(gTacticalStatus.uiFlags & INCOMBAT) )
4720 	{
4721 		return;
4722 	}
4723 
4724 	if ( pAttacker->usAttackingWeapon == DART_GUN )
4725 	{
4726 		// rarely noticed
4727 		if ( SkillCheck( pDefender, NOTICE_DART_CHECK, 0 ) < 0)
4728 		{
4729 			return;
4730 		}
4731 	}
4732 
4733 	// do we need to do checks for life/breath here?
4734 
4735 	if ( pDefender->ubBodyType == LARVAE_MONSTER || (pDefender->uiStatusFlags & SOLDIER_VEHICLE &&
4736 		pDefender->bTeam == OUR_TEAM) )
4737 	{
4738 		return;
4739 	}
4740 
4741 	bOldOppList = pDefender->bOppList[ pAttacker->ubID ];
4742 	if ( PythSpacesAway( pAttacker->sGridNo, pDefender->sGridNo ) <= MaxDistanceVisible() )
4743 	{
4744 		// check LOS, considering we are now aware of the attacker
4745 		// ignore muzzle flashes when must turning head
4746 		if ( pAttacker->fMuzzleFlash )
4747 		{
4748 			bDirection = atan8( pDefender->sX,pDefender->sY, pAttacker->sX, pAttacker->sY );
4749 			if (pDefender->bDirection != bDirection &&
4750 				pDefender->bDirection != OneCDirection(bDirection) &&
4751 				pDefender->bDirection != OneCCDirection(bDirection))
4752 			{
4753 				// temporarily turn off muzzle flash so DistanceVisible can be calculated without it
4754 				pAttacker->fMuzzleFlash = FALSE;
4755 				fMuzzleFlash = TRUE;
4756 			}
4757 		}
4758 
4759 		ubTileSightLimit = (UINT8) DistanceVisible( pDefender, DIRECTION_IRRELEVANT, 0, pAttacker->sGridNo, pAttacker->bLevel );
4760 		if (SoldierToSoldierLineOfSightTest( pDefender, pAttacker, ubTileSightLimit, TRUE ) != 0)
4761 		{
4762 			fSeesAttacker = TRUE;
4763 		}
4764 		if ( fMuzzleFlash )
4765 		{
4766 			pAttacker->fMuzzleFlash = TRUE;
4767 		}
4768 	}
4769 
4770 	if (fSeesAttacker)
4771 	{
4772 		ManSeesMan(*pDefender, *pAttacker, CALLER_UNKNOWN);
4773 
4774 		// newOppCnt not needed here (no radioing), must get reset right away
4775 		// CJC: Huh? well, leave it in for now
4776 		pDefender->bNewOppCnt = 0;
4777 
4778 
4779 		if (pDefender->bTeam == OUR_TEAM)
4780 		{
4781 			// EXPERIENCE GAIN (5): Victim notices/sees a previously UNSEEN attacker
4782 			StatChange(*pDefender, EXPERAMT, 5, FROM_SUCCESS);
4783 
4784 			// mark attacker as being SEEN right now
4785 			RadioSightings(pDefender, pAttacker, pDefender->bTeam);
4786 		}
4787 		// NOTE: ENEMIES DON'T REPORT A SIGHTING PUBLICLY UNTIL THEY RADIO IT IN!
4788 		else
4789 		{
4790 			// go to threatening stance
4791 			ReevaluateEnemyStance( pDefender, pDefender->usAnimState );
4792 		}
4793 	}
4794 	else // victim NOTICED the attack, but CAN'T SEE the actual attacker
4795 	{
4796 		SetNewSituation( pDefender ); // re-evaluate situation
4797 
4798 		// if victim's alert status is only GREEN or YELLOW
4799 		if (pDefender->bAlertStatus < STATUS_RED)
4800 		{
4801 			// then this soldier goes to status RED, has proof of enemy presence
4802 			pDefender->bAlertStatus = STATUS_RED;
4803 			CheckForChangingOrders( pDefender );
4804 		}
4805 
4806 		UpdatePersonal( pDefender, pAttacker->ubID, HEARD_THIS_TURN, pAttacker->sGridNo, pAttacker->bLevel );
4807 
4808 		// if the victim is a human-controlled soldier, instantly report publicly
4809 		if (pDefender->uiStatusFlags & SOLDIER_PC)
4810 		{
4811 			// mark attacker as having been PUBLICLY heard THIS TURN & remember where
4812 			UpdatePublic(pDefender->bTeam, pAttacker, HEARD_THIS_TURN, pAttacker->sGridNo, pAttacker->bLevel);
4813 		}
4814 	}
4815 
4816 	if (StandardInterruptConditionsMet(pDefender, pAttacker, bOldOppList))
4817 	{
4818 		SLOGD(
4819 			"INTERRUPT: NoticeUnseenAttacker, standard\
4820 			conditions are met; defender %d, attacker %d\n\
4821 			Calculating int duel pts for defender in NUA",
4822 			pDefender->ubID, pAttacker->ubID);
4823 
4824 		// calculate the interrupt duel points
4825 		pDefender->bInterruptDuelPts = CalcInterruptDuelPts(pDefender, pAttacker, FALSE);
4826 	}
4827 	else
4828 	{
4829 		pDefender->bInterruptDuelPts = NO_INTERRUPT;
4830 	}
4831 
4832 	// say quote
4833 
4834 	if (pDefender->bInterruptDuelPts != NO_INTERRUPT)
4835 	{
4836 		// check for possible interrupt and handle control change if it happens
4837 		// this code is basically ResolveInterruptsVs for 1 man only...
4838 
4839 		// calculate active soldier's dueling pts for the upcoming interrupt duel
4840 		SLOGD("Calculating int duel pts for attacker in NUA");
4841 		pAttacker->bInterruptDuelPts = CalcInterruptDuelPts(pAttacker, pDefender, FALSE);
4842 		if ( InterruptDuel( pDefender, pAttacker ) )
4843 		{
4844 			SLOGD(
4845 				"INTERRUPT: NoticeUnseenAttacker, defender\
4846 				pts %d, attacker pts %d, defender gets interrupt",
4847 				pDefender->bInterruptDuelPts, pAttacker->bInterruptDuelPts);
4848 			AddToIntList(pAttacker, FALSE, TRUE);
4849 			AddToIntList(pDefender, TRUE,  TRUE);
4850 			DoneAddingToIntList();
4851 		}
4852 		// either way, clear out both sides' duelPts fields to prepare next duel
4853 		SLOGD("Resetting int pts for %d and %d in NUA",
4854 			pDefender->ubID, pAttacker->ubID );
4855 		pDefender->bInterruptDuelPts = NO_INTERRUPT;
4856 		pAttacker->bInterruptDuelPts = NO_INTERRUPT;
4857 	}
4858 }
4859 
4860 
CheckForAlertWhenEnemyDies(SOLDIERTYPE * pDyingSoldier)4861 void CheckForAlertWhenEnemyDies(SOLDIERTYPE* pDyingSoldier)
4862 {
4863 	INT8  bDir;
4864 	INT16 sDistAway, sDistVisible;
4865 
4866 	FOR_EACH_IN_TEAM(pSoldier, pDyingSoldier->bTeam)
4867 	{
4868 		if (pSoldier->bInSector && pSoldier != pDyingSoldier && pSoldier->bLife >= OKLIFE &&
4869 			pSoldier->bAlertStatus < STATUS_RED)
4870 		{
4871 			// this guy might have seen the man die
4872 
4873 			// distance we "see" then depends on the direction he is located from us
4874 			bDir = atan8(pSoldier->sX,pSoldier->sY,pDyingSoldier->sX,pDyingSoldier->sY);
4875 			sDistVisible = DistanceVisible(pSoldier, pSoldier->bDesiredDirection, bDir,
4876 							pDyingSoldier->sGridNo, pDyingSoldier->bLevel);
4877 			sDistAway = PythSpacesAway( pSoldier->sGridNo, pDyingSoldier->sGridNo );
4878 
4879 			// if we see close enough to see the soldier
4880 			if (sDistAway <= sDistVisible)
4881 			{
4882 				// and we can trace a line of sight to his x,y coordinates
4883 				// assume enemies are always aware of their buddies...
4884 				if (SoldierTo3DLocationLineOfSightTest(pSoldier, pDyingSoldier->sGridNo,
4885 									pDyingSoldier->bLevel, 0,
4886 									(UINT8) sDistVisible, TRUE))
4887 				{
4888 					pSoldier->bAlertStatus = STATUS_RED;
4889 					CheckForChangingOrders( pSoldier );
4890 				}
4891 			}
4892 		}
4893 	}
4894 }
4895 
4896 
MercSeesCreature(SOLDIERTYPE const & s)4897 bool MercSeesCreature(SOLDIERTYPE const& s)
4898 {
4899 	if (s.bOppCnt <= 0) return false;
4900 	CFOR_EACH_IN_TEAM(i, CREATURE_TEAM)
4901 	{
4902 		SOLDIERTYPE const& tgt = *i;
4903 		if (!(tgt.uiStatusFlags & SOLDIER_MONSTER)) continue;
4904 		if (!s.bOppList[tgt.ubID])                  continue;
4905 		return true;
4906 	}
4907 	return false;
4908 }
4909 
4910 
FindUnusedWatchedLoc(UINT8 ubID)4911 static INT8 FindUnusedWatchedLoc(UINT8 ubID)
4912 {
4913 	INT8 bLoop;
4914 
4915 	for ( bLoop = 0; bLoop < NUM_WATCHED_LOCS; bLoop++ )
4916 	{
4917 		if ( gsWatchedLoc[ ubID ][ bLoop ] == NOWHERE )
4918 		{
4919 			return( bLoop );
4920 		}
4921 	}
4922 	return( -1 );
4923 }
4924 
4925 
FindWatchedLocWithLessThanXPointsLeft(UINT8 ubID,UINT8 ubPointLimit)4926 static INT8 FindWatchedLocWithLessThanXPointsLeft(UINT8 ubID, UINT8 ubPointLimit)
4927 {
4928 	INT8 bLoop;
4929 
4930 	for ( bLoop = 0; bLoop < NUM_WATCHED_LOCS; bLoop++ )
4931 	{
4932 		if ( gsWatchedLoc[ ubID ][ bLoop ] != NOWHERE && gubWatchedLocPoints[ ubID ][ bLoop ] <= ubPointLimit )
4933 		{
4934 			return( bLoop );
4935 		}
4936 	}
4937 	return( -1 );
4938 }
4939 
4940 
FindWatchedLoc(UINT8 ubID,INT16 sGridNo,INT8 bLevel)4941 static INT8 FindWatchedLoc(UINT8 ubID, INT16 sGridNo, INT8 bLevel)
4942 {
4943 	INT8 bLoop;
4944 
4945 	for ( bLoop = 0; bLoop < NUM_WATCHED_LOCS; bLoop++ )
4946 	{
4947 		if ( gsWatchedLoc[ ubID ][ bLoop ] != NOWHERE &&  gbWatchedLocLevel[ ubID ][ bLoop ] == bLevel )
4948 		{
4949 			if ( SpacesAway( gsWatchedLoc[ ubID ][ bLoop ], sGridNo ) <= WATCHED_LOC_RADIUS )
4950 			{
4951 				return( bLoop );
4952 			}
4953 		}
4954 	}
4955 	return( -1 );
4956 }
4957 
GetWatchedLocPoints(UINT8 ubID,INT16 sGridNo,INT8 bLevel)4958 INT8 GetWatchedLocPoints( UINT8 ubID, INT16 sGridNo, INT8 bLevel )
4959 {
4960 	INT8 bLoc;
4961 
4962 	bLoc = FindWatchedLoc( ubID, sGridNo, bLevel );
4963 	if (bLoc != -1)
4964 	{
4965 		if (gubWatchedLocPoints[ ubID ][ bLoc ] > 1)
4966 		{
4967 			SLOGD("Soldier %d getting %d points for interrupt in watched location", ubID, gubWatchedLocPoints[ ubID ][ bLoc ] - 1 );
4968 		}
4969 		// one loc point is worth nothing, so return number minus 1
4970 
4971 		// experiment with 1 loc point being worth 1 point
4972 		return( gubWatchedLocPoints[ ubID ][ bLoc ] );
4973 	}
4974 
4975 	return( 0 );
4976 }
4977 
4978 
GetHighestVisibleWatchedLoc(const SOLDIERTYPE * const s)4979 INT8 GetHighestVisibleWatchedLoc(const SOLDIERTYPE* const s)
4980 {
4981 	const INT16* const loc_id = gsWatchedLoc[s->ubID];
4982 	const UINT8* const pts_id = gubWatchedLocPoints[s->ubID];
4983 	const INT8*  const lvl_id = gbWatchedLocLevel[s->ubID];
4984 	INT8 bHighestLoc    = -1;
4985 	INT8 bHighestPoints =  0;
4986 	for (INT8 bLoop = 0; bLoop < NUM_WATCHED_LOCS; ++bLoop)
4987 	{
4988 		const INT16 loc = loc_id[bLoop];
4989 		if (loc == NOWHERE) continue;
4990 
4991 		const UINT8 pts = pts_id[bLoop];
4992 		if (pts <= bHighestPoints) continue;
4993 
4994 		const INT8  lvl          = lvl_id[bLoop];
4995 		const INT16 sDistVisible = DistanceVisible(s, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, loc, lvl);
4996 		// look at standing height
4997 		if (SoldierTo3DLocationLineOfSightTest(s, loc, lvl, 3, sDistVisible, TRUE))
4998 		{
4999 			bHighestLoc    = bLoop;
5000 			bHighestPoints = pts;
5001 		}
5002 	}
5003 	return bHighestLoc;
5004 }
5005 
5006 
GetHighestWatchedLocPoints(const SOLDIERTYPE * const s)5007 INT8 GetHighestWatchedLocPoints(const SOLDIERTYPE* const s)
5008 {
5009 	const INT16* const loc_id = gsWatchedLoc[s->ubID];
5010 	const UINT8* const pts_id = gubWatchedLocPoints[s->ubID];
5011 	INT8 bHighestPoints = 0;
5012 	for (INT8 bLoop = 0; bLoop < NUM_WATCHED_LOCS; ++bLoop)
5013 	{
5014 		if (loc_id[bLoop] == NOWHERE) continue;
5015 
5016 		const UINT8 pts = pts_id[bLoop];
5017 		if (pts > bHighestPoints) bHighestPoints = pts;
5018 	}
5019 	return bHighestPoints;
5020 }
5021 
5022 
CommunicateWatchedLoc(const SOLDIERTYPE * const watcher,const INT16 sGridNo,const INT8 bLevel,const UINT8 ubPoints)5023 static void CommunicateWatchedLoc(const SOLDIERTYPE* const watcher, const INT16 sGridNo, const INT8 bLevel, const UINT8 ubPoints)
5024 {
5025 	INT8 bLoopPoint, bPoint;
5026 
5027 	const INT8 bTeam = watcher->bTeam;
5028 
5029 	CFOR_EACH_IN_TEAM(s, bTeam)
5030 	{
5031 		if (s == watcher) continue;
5032 		if (!s->bInSector || s->bLife < OKLIFE)
5033 		{
5034 			continue;
5035 		}
5036 		const SoldierID ubLoop = s->ubID;
5037 		bLoopPoint = FindWatchedLoc( ubLoop, sGridNo, bLevel );
5038 		if ( bLoopPoint == -1 )
5039 		{
5040 			// add this as a watched point
5041 			bPoint = FindUnusedWatchedLoc( ubLoop );
5042 			if (bPoint == -1)
5043 			{
5044 				// if we have a point with only 1 point left, replace it
5045 				bPoint = FindWatchedLocWithLessThanXPointsLeft( ubLoop, ubPoints );
5046 			}
5047 			if (bPoint != -1)
5048 			{
5049 				gsWatchedLoc[ ubLoop ][ bPoint ] = sGridNo;
5050 				gbWatchedLocLevel[ ubLoop ][ bPoint ] = bLevel;
5051 				gubWatchedLocPoints[ ubLoop ][ bPoint ] = ubPoints;
5052 				gfWatchedLocReset[ ubLoop ][ bPoint ] = FALSE;
5053 				gfWatchedLocHasBeenIncremented[ ubLoop ][ bPoint ] = TRUE;
5054 			}
5055 			// else no points available!
5056 		}
5057 		else
5058 		{
5059 			// increment to max
5060 			gubWatchedLocPoints[ ubLoop ][ bLoopPoint ] = __max( gubWatchedLocPoints[ ubLoop ][ bLoopPoint ], ubPoints );
5061 
5062 			gfWatchedLocReset[ ubLoop ][ bLoopPoint ] = FALSE;
5063 			gfWatchedLocHasBeenIncremented[ ubLoop ][ bLoopPoint ] = TRUE;
5064 		}
5065 	}
5066 }
5067 
5068 
IncrementWatchedLoc(const SOLDIERTYPE * const watcher,const INT16 sGridNo,const INT8 bLevel)5069 static void IncrementWatchedLoc(const SOLDIERTYPE* const watcher, const INT16 sGridNo, const INT8 bLevel)
5070 {
5071 	const SoldierID ubID = watcher->ubID;
5072 	INT8  bPoint;
5073 
5074 	bPoint = FindWatchedLoc( ubID, sGridNo, bLevel );
5075 	if (bPoint == -1)
5076 	{
5077 		// try adding point
5078 		bPoint = FindUnusedWatchedLoc( ubID );
5079 		if (bPoint == -1)
5080 		{
5081 			// if we have a point with only 1 point left, replace it
5082 			bPoint = FindWatchedLocWithLessThanXPointsLeft( ubID, 1 );
5083 		}
5084 
5085 		if (bPoint != -1)
5086 		{
5087 			gsWatchedLoc[ ubID ][ bPoint ] = sGridNo;
5088 			gbWatchedLocLevel[ ubID ][ bPoint ] = bLevel;
5089 			gubWatchedLocPoints[ ubID ][ bPoint ] = 1;
5090 			gfWatchedLocReset[ ubID ][ bPoint ] = FALSE;
5091 			gfWatchedLocHasBeenIncremented[ ubID ][ bPoint ] = TRUE;
5092 
5093 			CommunicateWatchedLoc(watcher, sGridNo, bLevel, 1);
5094 		}
5095 		// otherwise abort; no points available
5096 	}
5097 	else
5098 	{
5099 		if ( !gfWatchedLocHasBeenIncremented[ ubID ][ bPoint ] && gubWatchedLocPoints[ ubID ][ bPoint ] < MAX_WATCHED_LOC_POINTS )
5100 		{
5101 			gubWatchedLocPoints[ ubID ][ bPoint ]++;
5102 			CommunicateWatchedLoc(watcher, sGridNo, bLevel, gubWatchedLocPoints[ubID][bPoint]);
5103 		}
5104 		gfWatchedLocReset[ ubID ][ bPoint ] = FALSE;
5105 		gfWatchedLocHasBeenIncremented[ ubID ][ bPoint ] = TRUE;
5106 	}
5107 }
5108 
5109 
SetWatchedLocAsUsed(UINT8 ubID,INT16 sGridNo,INT8 bLevel)5110 static void SetWatchedLocAsUsed(UINT8 ubID, INT16 sGridNo, INT8 bLevel)
5111 {
5112 	INT8 bPoint;
5113 
5114 	bPoint = FindWatchedLoc( ubID, sGridNo, bLevel );
5115 	if (bPoint != -1)
5116 	{
5117 		gfWatchedLocReset[ ubID ][ bPoint ] = FALSE;
5118 	}
5119 }
5120 
5121 
WatchedLocLocationIsEmpty(INT16 sGridNo,INT8 bLevel,INT8 bTeam)5122 static BOOLEAN WatchedLocLocationIsEmpty(INT16 sGridNo, INT8 bLevel, INT8 bTeam)
5123 {
5124 	// look to see if there is anyone near the watched loc who is not on this team
5125 	INT16 sTempGridNo, sX, sY;
5126 
5127 	for ( sY = -WATCHED_LOC_RADIUS; sY <= WATCHED_LOC_RADIUS; sY++ )
5128 	{
5129 		for ( sX = -WATCHED_LOC_RADIUS; sX <= WATCHED_LOC_RADIUS; sX++ )
5130 		{
5131 			sTempGridNo = sGridNo + sX + sY * WORLD_ROWS;
5132 			if ( sTempGridNo < 0 || sTempGridNo >= WORLD_MAX )
5133 			{
5134 				continue;
5135 			}
5136 			const SOLDIERTYPE* const tgt = WhoIsThere2(sTempGridNo, bLevel);
5137 			if (tgt != NULL && tgt->bTeam != bTeam) return FALSE;
5138 		}
5139 	}
5140 	return( TRUE );
5141 }
5142 
5143 
DecayWatchedLocs(INT8 bTeam)5144 static void DecayWatchedLocs(INT8 bTeam)
5145 {
5146 	UINT8 cnt, cnt2;
5147 
5148 	// loop through all soldiers
5149 	for ( cnt = gTacticalStatus.Team[ bTeam ].bFirstID; cnt <= gTacticalStatus.Team[ bTeam ].bLastID; cnt++ )
5150 	{
5151 		// for each watched location
5152 		for ( cnt2 = 0; cnt2 < NUM_WATCHED_LOCS; cnt2++ )
5153 		{
5154 			if (gsWatchedLoc[cnt][cnt2] != NOWHERE &&
5155 				WatchedLocLocationIsEmpty(gsWatchedLoc[cnt][cnt2], gbWatchedLocLevel[cnt][cnt2], bTeam))
5156 			{
5157 				// if the reset flag is still set, then we should decay this point
5158 				if (gfWatchedLocReset[ cnt ][ cnt2 ])
5159 				{
5160 					// turn flag off again
5161 					gfWatchedLocReset[ cnt ][ cnt2 ] = FALSE;
5162 
5163 					// halve points
5164 					gubWatchedLocPoints[ cnt ][ cnt2 ] /= 2;
5165 					// if points have reached 0, then reset the location
5166 					if (gubWatchedLocPoints[ cnt ][ cnt2 ] == 0)
5167 					{
5168 						gsWatchedLoc[ cnt ][ cnt2 ] = NOWHERE;
5169 					}
5170 				}
5171 				else
5172 				{
5173 					// flag was false so set to true (will be reset if new people seen there next turn)
5174 					gfWatchedLocReset[ cnt ][ cnt2 ] = TRUE;
5175 				}
5176 			}
5177 		}
5178 	}
5179 }
5180 
5181 
MakeBloodcatsHostile(void)5182 static void MakeBloodcatsHostile(void)
5183 {
5184 	FOR_EACH_IN_TEAM(s, CREATURE_TEAM)
5185 	{
5186 		if (s->ubBodyType == BLOODCAT && s->bInSector && s->bLife > 0)
5187 		{
5188 			SetSoldierNonNeutral(s);
5189 			RecalculateOppCntsDueToNoLongerNeutral(s);
5190 			if (gTacticalStatus.uiFlags & INCOMBAT)
5191 			{
5192 				CheckForPotentialAddToBattleIncrement(s);
5193 			}
5194 		}
5195 	}
5196 }
5197