1 #include "Directories.h"
2 #include "Interface_Control.h"
3 #include "Interface_Panels.h"
4 #include "LoadSaveMercProfile.h"
5 #include "MapScreen.h"
6 #include "Merc_Hiring.h"
7 #include "Debug.h"
8 #include "math.h"
9 #include "WorldDef.h"
10 #include "Soldier_Control.h"
11 #include "Animation_Data.h"
12 #include "Render_Fun.h"
13 #include "Render_Dirty.h"
14 #include "MouseSystem.h"
15 #include "Interface.h"
16 #include "SysUtil.h"
17 #include "FileMan.h"
18 #include "Points.h"
19 #include "Random.h"
20 #include "AI.h"
21 #include "Soldier_Ani.h"
22 #include "Overhead.h"
23 #include "Soldier_Profile.h"
24 #include "Game_Clock.h"
25 #include "Assignments.h"
26 #include "Dialogue_Control.h"
27 #include "Soldier_Create.h"
28 #include "Soldier_Add.h"
29 #include "OppList.h"
30 #include "Weapons.h"
31 #include "Strategic_Town_Loyalty.h"
32 #include "Squads.h"
33 #include "Tactical_Save.h"
34 #include "Quests.h"
35 #include "AIM.h"
36 #include "Interface_Dialogue.h"
37 #include "GameSettings.h"
38 #include "Interface_Utils.h"
39 #include "StrategicMap.h"
40 #include "Game_Event_Hook.h"
41 #include "Map_Information.h"
42 #include "History.h"
43 #include "Personnel.h"
44 #include "Environment.h"
45 #include "Items.h"
46 #include "GameRes.h"
47 #include "Faces.h"
48 
49 #include "ContentManager.h"
50 #include "GameInstance.h"
51 #include "content/ContentMercs.h"
52 #include "WeaponModels.h"
53 #include "MercProfile.h"
54 
55 extern BOOLEAN gfProfileDataLoaded;
56 
57 
58 BOOLEAN	gfPotentialTeamChangeDuringDeath = FALSE;
59 
60 
61 MERCPROFILESTRUCT gMercProfiles[ NUM_PROFILES ];
62 
63 INT8 gbSkillTraitBonus[NUM_SKILLTRAITS] =
64 {
65 	0, //NO_SKILLTRAIT
66 	25, //LOCKPICKING
67 	15, //HANDTOHAND
68 	15, //ELECTRONICS
69 	15, //NIGHTOPS
70 	12, //THROWING
71 	15, //TEACHING
72 	15, //HEAVY_WEAPS
73 	0, //AUTO_WEAPS
74 	15, //STEALTHY
75 	0, //AMBIDEXT
76 	0, //THIEF // UNUSED!
77 	30, //MARTIALARTS
78 	30, //KNIFING
79 	15, //ONROOF
80 	0, //CAMOUFLAGED
81 };
82 
83 
84 UINT8 gubNumTerrorists = 0;
85 
86 struct TerroristInfo
87 {
88 	ProfileID profile;
89 	UINT8     sectors[5];
90 };
91 
92 static TerroristInfo const g_terrorist_infos[] =
93 {
94 	{DRUGGIST, { 0,       0,       0,       0,       0      }}, // Elgin, preplaced
95 	{SLAY,     { SEC_F9,  SEC_I14, SEC_G1,  SEC_G2,  SEC_G8 }}, // Slay
96 	{ANNIE,    { SEC_I14, SEC_C6,  SEC_B2,  SEC_L11, SEC_G8 }}, // Matron
97 	{CHRIS,    { SEC_G1,  SEC_F9,  SEC_L11, SEC_G8,  SEC_G2 }}, // Imposter
98 	{TIFFANY,  { SEC_I14, SEC_G2,  SEC_H14, SEC_C6,  SEC_B2 }}, // Tiffany
99 	{T_REX,    { SEC_F9,  SEC_H14, SEC_H2,  SEC_G1,  SEC_B2 }}  // Rexall
100 };
101 
102 INT16 gsRobotGridNo;
103 
104 
105 struct AssassinInfo
106 {
107 	ProfileID profile;
108 	UINT8     towns[5];
109 };
110 
111 static AssassinInfo const g_assassin_info[] =
112 {
113 	{ JIM,    { CAMBRIA,  DRASSEN, ALMA,    BALIME,  GRUMM   } },
114 	{ JACK,   { CHITZENA, ESTONI,  ALMA,    BALIME,  GRUMM   } },
115 	{ OLAF,   { DRASSEN,  ESTONI,  ALMA,    CAMBRIA, BALIME  } },
116 	{ RAY,    { CAMBRIA,  OMERTA,  BALIME,  GRUMM,   DRASSEN } },
117 	{ OLGA,   { CHITZENA, OMERTA,  CAMBRIA, ALMA,    GRUMM   } },
118 	{ TYRONE, { CAMBRIA,  BALIME,  ALMA,    GRUMM,   DRASSEN } }
119 };
120 
121 
122 static INT16 CalcMedicalDeposit(MERCPROFILESTRUCT const&);
123 static void DecideActiveTerrorists();
124 static void StartSomeMercsOnAssignment(void);
125 
126 
LoadMercProfiles()127 void LoadMercProfiles()
128 {
129 	{ AutoSGPFile f(GCM->openGameResForReading(BINARYDATADIR "/prof.dat"));
130 		LoadRawMercProfiles(f, NUM_PROFILES, gMercProfiles, getDataFilesEncodingCorrector());
131 		for (UINT32 i = 0; i != NUM_PROFILES; ++i)
132 		{
133 			MERCPROFILESTRUCT& p = gMercProfiles[i];
134 
135 			// // dumping std inventory
136 			// printf("%03d/%s\n", i, p.zNickname.c_str());
137 			// FOR_EACH(UINT16, k, p.inv)
138 			// {
139 			//   const ItemModel *item = GCM->getItem(*k);
140 			//   printf("  %s\n", item->getInternalName().c_str());
141 			// }
142 
143 			// If the dialogue exists for the merc, allow the merc to be hired
144 			p.bMercStatus = Content::canMercBeHired(GCM, i) ? 0 : MERC_HAS_NO_TEXT_FILE;
145 
146 			p.sMedicalDepositAmount = p.bMedicalDeposit ? CalcMedicalDeposit(p) : 0;
147 
148 			// ATE: New, face display independent of ID num now, default is the
149 			// profile ID
150 			p.ubFaceIndex = i;
151 
152 			if (!gGameOptions.fGunNut)
153 			{
154 				// CJC: replace guns in profile if they aren't available
155 				FOR_EACH(UINT16, k, p.inv)
156 				{
157 					const ItemModel *item = GCM->getItem(*k);
158 					if (!item->isGun() || !item->isInBigGunList()) continue;
159 
160 					const WeaponModel *oldWeapon = item->asWeapon();
161 					const WeaponModel *newWeapon = GCM->getWeaponByName(oldWeapon->getStandardReplacement());
162 
163 					*k = newWeapon->getItemIndex();
164 
165 					// Search through inventory and replace ammo accordingly
166 					FOR_EACH(UINT16, l, p.inv)
167 					{
168 						UINT16 const ammo = *l;
169 						if (!(GCM->getItem(ammo)->isAmmo())) continue;
170 						UINT16 const new_ammo = FindReplacementMagazineIfNecessary(oldWeapon, ammo, newWeapon);
171 						if (new_ammo == NOTHING) continue;
172 						// Found a new magazine, replace
173 						*l = new_ammo;
174 					}
175 				}
176 			}
177 
178 			// Calculate inital attractiveness for the merc's initial gun and armour.
179 			// Calculate the optional gear cost.
180 			p.bMainGunAttractiveness = -1;
181 			p.bArmourAttractiveness  = -1;
182 			p.usOptionalGearCost     =  0;
183 			FOR_EACH(UINT16 const, k, p.inv)
184 			{
185 				UINT16 const item_id = *k;
186 				if (item_id == NOTHING) continue;
187 				const ItemModel * item = GCM->getItem(item_id);
188 
189 				if (item->isGun())    p.bMainGunAttractiveness = GCM->getWeapon(item_id)->ubDeadliness;
190 				if (item->isArmour()) p.bArmourAttractiveness  = Armour[item->getClassIndex()].ubProtection;
191 
192 				p.usOptionalGearCost += item->getPrice();
193 			}
194 
195 			// These variables to get loaded in
196 			p.fUseProfileInsertionInfo = FALSE;
197 			p.sGridNo                  = 0;
198 
199 			// ARM: this is also being done inside the profile editor, but put it here
200 			// too, so this project's code makes sense
201 			p.bHatedCount[0]    = p.bHatedTime[0];
202 			p.bHatedCount[1]    = p.bHatedTime[1];
203 			p.bLearnToHateCount = p.bLearnToHateTime;
204 			p.bLearnToLikeCount = p.bLearnToLikeTime;
205 		}
206 	}
207 
208 	DecideActiveTerrorists();
209 
210 	// Initialize mercs' status
211 	StartSomeMercsOnAssignment();
212 
213 	gfProfileDataLoaded = TRUE;
214 
215 	// no better place..heh?.. will load faces for profiles that are 'extern'.....won't have soldiertype instances
216 	PreloadExternalNPCFaces();
217 
218 	LoadCarPortraitValues();
219 }
220 
221 
222 #define MAX_ADDITIONAL_TERRORISTS 4
223 
224 
225 // One terrorist is always Elgin. Determine how many more terrorists - 2 to 4
226 // more.
DecideActiveTerrorists()227 static void DecideActiveTerrorists()
228 {
229 	// Using this stochastic process(!), the chances for terrorists are:
230 	// EASY:    3,  9%    4, 42%    5, 49%
231 	// MEDIUM:  3, 25%    4, 50%    5, 25%
232 	// HARD:    3, 49%    4, 42%    5,  9%
233 	UINT32 chance;
234 	switch (gGameOptions.ubDifficultyLevel)
235 	{
236 		case DIF_LEVEL_EASY: chance = 70; break;
237 		default:             chance = 50; break;
238 		case DIF_LEVEL_HARD: chance = 30; break;
239 	}
240 	UINT8 n_additional_terrorists = 2; // Add at least 2 more.
241 	for (UINT8 n = MAX_ADDITIONAL_TERRORISTS - n_additional_terrorists; n != 0; --n)
242 	{
243 		if (Chance(chance)) ++n_additional_terrorists;
244 	}
245 
246 	UINT8 terrorist_placement[MAX_ADDITIONAL_TERRORISTS];
247 	for (UINT8 n_terrorists_added = 0; n_terrorists_added != n_additional_terrorists;)
248 	{
249 		FOR_EACH(TerroristInfo const, i, g_terrorist_infos)
250 		{
251 			if (n_terrorists_added == n_additional_terrorists) break;
252 
253 			TerroristInfo const& t = *i;
254 			MERCPROFILESTRUCT&   p = GetProfile(t.profile);
255 			// Random 40% chance of adding this terrorist if not yet placed.
256 			if (p.sSectorX != 0)   continue;
257 			if (Random(100) >= 40) continue;
258 
259 			// Since there are 5 spots per terrorist and a maximum of 5 terrorists, we
260 			// are guaranteed to be able to find a spot for each terrorist since there
261 			// aren't enough other terrorists to use up all the spots for any one
262 			// terrorist
263 pick_sector:
264 			// Pick a random spot, see if it's already been used by another terrorist.
265 			UINT8 const sector = t.sectors[Random(lengthof(t.sectors))];
266 			for (UINT8 k = 0; k != n_terrorists_added; ++k)
267 			{
268 				if (terrorist_placement[k] == sector) goto pick_sector;
269 			}
270 
271 			// Place terrorist.
272 			p.sSectorX = SECTORX(sector);
273 			p.sSectorY = SECTORY(sector);
274 			p.bSectorZ = 0;
275 			terrorist_placement[n_terrorists_added++] = sector;
276 		}
277 	}
278 
279 	// Set total terrorists outstanding in Carmen's info byte.
280 	GetProfile(CARMEN).bNPCData = 1 + n_additional_terrorists;
281 	// Store total terrorists.
282 	gubNumTerrorists = 1 + n_additional_terrorists;
283 }
284 
285 
MakeRemainingTerroristsTougher()286 void MakeRemainingTerroristsTougher()
287 {
288 	UINT8 n_remaining_terrorists = 0;
289 	FOR_EACH(TerroristInfo const, i, g_terrorist_infos)
290 	{
291 		ProfileID         const  pid = i->profile;
292 		MERCPROFILESTRUCT const& p   = GetProfile(pid);
293 		if (p.bMercStatus == MERC_IS_DEAD || p.sSectorX == 0 || p.sSectorY == 0) continue;
294 		// Slay on player's team, doesn't count towards remaining terrorists
295 		if (pid == SLAY && FindSoldierByProfileIDOnPlayerTeam(SLAY)) continue;
296 		++n_remaining_terrorists;
297 	}
298 
299 	UINT8 remaining_difficulty = 60 / gubNumTerrorists * (gubNumTerrorists - n_remaining_terrorists);
300 
301 	switch (gGameOptions.ubDifficultyLevel)
302 	{
303 		case DIF_LEVEL_MEDIUM: remaining_difficulty = remaining_difficulty * 13 / 10; break;
304 		case DIF_LEVEL_HARD:   remaining_difficulty = remaining_difficulty * 16 / 10; break;
305 		default: break;
306 	}
307 
308 	UINT16 old_item;
309 	UINT16 new_item;
310 	if (remaining_difficulty < 14)
311 	{
312 		// nothing
313 		return;
314 	}
315 	else if (remaining_difficulty < 28)
316 	{
317 		// mini grenade
318 		old_item = NOTHING;
319 		new_item = MINI_GRENADE;
320 	}
321 	else if (remaining_difficulty < 42)
322 	{
323 		// hand grenade
324 		old_item = MINI_GRENADE;
325 		new_item = HAND_GRENADE;
326 	}
327 	else if (remaining_difficulty < 56)
328 	{
329 		// mustard
330 		old_item = HAND_GRENADE;
331 		new_item = MUSTARD_GRENADE;
332 	}
333 	else if (remaining_difficulty < 70)
334 	{
335 		// LAW
336 		old_item = MUSTARD_GRENADE;
337 		new_item = ROCKET_LAUNCHER;
338 	}
339 	else
340 	{
341 		// LAW and hand grenade
342 		old_item = NOTHING;
343 		new_item = HAND_GRENADE;
344 	}
345 
346 	OBJECTTYPE Object;
347 	DeleteObj(&Object);
348 	Object.usItem     = new_item;
349 	Object.bStatus[0] = 100;
350 
351 	FOR_EACH(TerroristInfo const, i, g_terrorist_infos)
352 	{
353 		ProfileID         const  pid = i->profile;
354 		MERCPROFILESTRUCT const& p   = GetProfile(pid);
355 		if (p.bMercStatus == MERC_IS_DEAD || p.sSectorX == 0 || p.sSectorY == 0) continue;
356 		// Slay on player's team, doesn't count towards remaining terrorists
357 		if (pid == SLAY && FindSoldierByProfileIDOnPlayerTeam(SLAY)) continue;
358 
359 		if (old_item != NOTHING)
360 		{
361 			RemoveObjectFromSoldierProfile(pid, old_item);
362 		}
363 		PlaceObjectInSoldierProfile(pid, &Object);
364 	}
365 }
366 
367 
DecideOnAssassin()368 void DecideOnAssassin()
369 {
370 	ProfileID   assassins[lengthof(g_assassin_info)];
371 	UINT8       n    = 0;
372 	UINT8 const town = GetTownIdForSector(SECTOR(gWorldSectorX, gWorldSectorY));
373 	FOR_EACH(AssassinInfo const, i, g_assassin_info)
374 	{
375 		AssassinInfo      const  a = *i;
376 		MERCPROFILESTRUCT const& p = GetProfile(a.profile);
377 		// Make sure alive and not placed already.
378 		if (p.bMercStatus == MERC_IS_DEAD)      continue;
379 		if (p.sSectorX != 0 || p.sSectorY != 0) continue;
380 		// Check this merc to see if the town is a possibility.
381 		FOR_EACH(UINT8 const, k, a.towns)
382 		{
383 			if (*k != town) continue;
384 			assassins[n++] = a.profile;
385 			break;
386 		}
387 	}
388 
389 	if (n == 0) return;
390 	ProfileID const    pid = assassins[Random(n)];
391 	MERCPROFILESTRUCT& p   = GetProfile(pid);
392 	p.sSectorX             = gWorldSectorX;
393 	p.sSectorY             = gWorldSectorY;
394 	AddStrategicEvent(EVENT_REMOVE_ASSASSIN, GetWorldTotalMin() + 60 * (3 + Random(3)), pid);
395 }
396 
397 
MakeRemainingAssassinsTougher()398 void MakeRemainingAssassinsTougher()
399 {
400 	UINT8 n_remaining_assassins = 0;
401 	FOR_EACH(AssassinInfo const, i, g_assassin_info)
402 	{
403 		if (GetProfile(i->profile).bMercStatus == MERC_IS_DEAD) continue;
404 		++n_remaining_assassins;
405 	}
406 
407 	size_t const n_assassins = lengthof(g_assassin_info);
408 	UINT8        difficulty  = 60 / n_assassins * (n_assassins - n_remaining_assassins);
409 	switch (gGameOptions.ubDifficultyLevel)
410 	{
411 		case DIF_LEVEL_MEDIUM: difficulty = difficulty * 13 / 10; break;
412 		case DIF_LEVEL_HARD:   difficulty = difficulty * 16 / 10; break;
413 		default: break;
414 	}
415 
416 	UINT16 new_item;
417 	UINT16 old_item;
418 	if (difficulty < 14)
419 	{
420 		// Nothing
421 		return;
422 	}
423 	else if (difficulty < 28)
424 	{
425 		// Mini grenade
426 		old_item = NOTHING;
427 		new_item = MINI_GRENADE;
428 	}
429 	else if (difficulty < 42)
430 	{
431 		// Hand grenade
432 		old_item = MINI_GRENADE;
433 		new_item = HAND_GRENADE;
434 	}
435 	else if (difficulty < 56)
436 	{
437 		// Mustard grenade
438 		old_item = HAND_GRENADE;
439 		new_item = MUSTARD_GRENADE;
440 	}
441 	else if (difficulty < 70)
442 	{
443 		// LAW
444 		old_item = MUSTARD_GRENADE;
445 		new_item = ROCKET_LAUNCHER;
446 	}
447 	else
448 	{
449 		// LAW and hand grenade
450 		old_item = NOTHING;
451 		new_item = HAND_GRENADE;
452 	}
453 
454 	OBJECTTYPE o;
455 	CreateItem(new_item, 100, &o);
456 	FOR_EACH(AssassinInfo const, i, g_assassin_info)
457 	{
458 		ProfileID const pid = i->profile;
459 		if (GetProfile(pid).bMercStatus == MERC_IS_DEAD) continue;
460 		if (old_item != NOTHING)
461 		{
462 			RemoveObjectFromSoldierProfile(pid, old_item);
463 		}
464 		PlaceObjectInSoldierProfile(pid, &o);
465 	}
466 }
467 
468 
StartSomeMercsOnAssignment(void)469 static void StartSomeMercsOnAssignment(void)
470 {
471 	UINT32 uiChance;
472 
473 	// some randomly picked A.I.M. mercs will start off "on assignment" at the beginning of each new game
474 	for (auto profile : GCM->listMercProfiles())
475 	{
476 		if (!profile->isAIMMerc() && !profile->isMERCMerc()) continue;
477 
478 		MERCPROFILESTRUCT& p = profile->getStruct();
479 
480 		// calc chance to start on assignment
481 		uiChance = 5 * p.bExpLevel;
482 
483 		if (Random(100) < uiChance)
484 		{
485 			p.bMercStatus = MERC_WORKING_ELSEWHERE;
486 			p.uiDayBecomesAvailable = 1 + Random(6 + p.bExpLevel / 2); // 1-(6 to 11) days
487 		}
488 		else
489 		{
490 			p.bMercStatus           = MERC_OK;
491 			p.uiDayBecomesAvailable = 0;
492 		}
493 
494 		p.uiPrecedentQuoteSaid   = 0;
495 		p.ubDaysOfMoraleHangover = 0;
496 	}
497 }
498 
499 
SetProfileFaceData(ProfileID const pid,UINT8 const face_idx,UINT16 const eyes_x,UINT16 const eyes_y,UINT16 const mouth_x,UINT16 const mouth_y)500 void SetProfileFaceData(ProfileID const pid, UINT8 const face_idx, UINT16 const eyes_x, UINT16 const eyes_y, UINT16 const mouth_x, UINT16 const mouth_y)
501 {
502 	MERCPROFILESTRUCT& p = GetProfile(pid);
503 	p.ubFaceIndex = face_idx;
504 	p.usEyesX     = eyes_x;
505 	p.usEyesY     = eyes_y;
506 	p.usMouthX    = mouth_x;
507 	p.usMouthY    = mouth_y;
508 }
509 
510 
CalcCompetence(MERCPROFILESTRUCT const & p)511 static UINT16 CalcCompetence(MERCPROFILESTRUCT const& p)
512 {
513 	UINT32 uiStats, uiSkills, uiActionPoints, uiSpecialSkills;
514 	UINT16 usCompetence;
515 
516 
517 	// count life twice 'cause it's also hit points
518 	// mental skills are halved 'cause they're actually not that important within the game
519 	uiStats = ((2 * p.bLifeMax) + p.bStrength + p.bAgility + p.bDexterity + ((p.bLeadership + p.bWisdom) / 2)) / 3;
520 
521 	// marksmanship is very important, count it double
522 	uiSkills = (UINT32) ((2 * (pow((double)p.bMarksmanship, 3) / 10000)) +
523 				1.5 *	(pow((double)p.bMedical, 3) / 10000) +
524 				(pow((double)p.bMechanical, 3) / 10000) +
525 				(pow((double)p.bExplosive, 3) / 10000));
526 
527 	// action points
528 	uiActionPoints = 5 + (((10 * p.bExpLevel +
529 				3 * p.bAgility  +
530 				2 * p.bLifeMax  +
531 				2 * p.bDexterity) + 20) / 40);
532 
533 
534 	// count how many he has, don't care what they are
535 	uiSpecialSkills = ((p.bSkillTrait != 0) ? 1 : 0) + ((p.bSkillTrait2 != 0) ? 1 : 0);
536 
537 	usCompetence = (UINT16) ((pow(p.bExpLevel, 0.2) * uiStats * uiSkills * (uiActionPoints - 6) * (1 + (0.05 * (FLOAT)uiSpecialSkills))) / 1000);
538 
539 	// this currently varies from about 10 (Flo) to 1200 (Gus)
540 	return(usCompetence);
541 }
542 
543 
CalcMedicalDeposit(MERCPROFILESTRUCT const & p)544 static INT16 CalcMedicalDeposit(MERCPROFILESTRUCT const& p)
545 {
546 	UINT16 usDeposit;
547 
548 	// this rounds off to the nearest hundred
549 	usDeposit = (5 * CalcCompetence(p) + 50) / 100 * 100;
550 
551 	return(usDeposit);
552 }
553 
554 
FindSoldierByProfileID(const ProfileID pid)555 SOLDIERTYPE* FindSoldierByProfileID(const ProfileID pid)
556 {
557 	FOR_EACH_SOLDIER(s)
558 	{
559 		if (s->ubProfile == pid) return s;
560 	}
561 	return NULL;
562 }
563 
564 
FindSoldierByProfileIDOnPlayerTeam(const ProfileID pid)565 SOLDIERTYPE* FindSoldierByProfileIDOnPlayerTeam(const ProfileID pid)
566 {
567 	FOR_EACH_IN_TEAM(s, OUR_TEAM)
568 	{
569 		if (s->ubProfile == pid) return s;
570 	}
571 	return NULL;
572 }
573 
574 
ChangeSoldierTeam(SOLDIERTYPE * const old_s,UINT8 const team)575 SOLDIERTYPE* ChangeSoldierTeam(SOLDIERTYPE* const old_s, UINT8 const team)
576 {
577 	if (gfInTalkPanel) DeleteTalkingMenu();
578 
579 	GridNo const old_gridno = old_s->sGridNo;
580 
581 	// At the low level check if this guy is in inv panel, else remove.
582 	if (gsCurInterfacePanel == SM_PANEL && gpSMCurrentMerc == old_s)
583 	{
584 		SetCurrentInterfacePanel(TEAM_PANEL);
585 	}
586 
587 	// Remove him from the game.
588 	InternalTacticalRemoveSoldier(*old_s, FALSE);
589 
590 	// Create a new one.
591 	SOLDIERCREATE_STRUCT c;
592 	c = SOLDIERCREATE_STRUCT{};
593 	c.bTeam            = team;
594 	c.ubProfile        = old_s->ubProfile;
595 	c.bBodyType        = old_s->ubBodyType;
596 	c.sSectorX         = old_s->sSectorX;
597 	c.sSectorY         = old_s->sSectorY;
598 	c.bSectorZ         = old_s->bSectorZ;
599 	c.sInsertionGridNo = old_s->sGridNo; // XXX always NOWHERE due to InternalTacticalRemoveSoldier() above
600 	c.bDirection       = old_s->bDirection;
601 	if (old_s->uiStatusFlags & SOLDIER_VEHICLE)
602 	{
603 		c.ubProfile          = NO_PROFILE;
604 		c.fUseGivenVehicle   = TRUE;
605 		c.bUseGivenVehicleID = old_s->bVehicleID;
606 	}
607 	SOLDIERTYPE* const new_s = TacticalCreateSoldier(c);
608 	if (!new_s) return 0;
609 
610 	// Copy vital stats back.
611 	new_s->bLife                   = old_s->bLife;
612 	new_s->bLifeMax                = old_s->bLifeMax;
613 	new_s->bAgility                = old_s->bAgility;
614 	new_s->bLeadership             = old_s->bLeadership;
615 	new_s->bDexterity              = old_s->bDexterity;
616 	new_s->bStrength               = old_s->bStrength;
617 	new_s->bWisdom                 = old_s->bWisdom;
618 	new_s->bExpLevel               = old_s->bExpLevel;
619 	new_s->bMarksmanship           = old_s->bMarksmanship;
620 	new_s->bMedical                = old_s->bMedical;
621 	new_s->bMechanical             = old_s->bMechanical;
622 	new_s->bExplosive              = old_s->bExplosive;
623 	new_s->bLastRenderVisibleValue = old_s->bLastRenderVisibleValue;
624 	new_s->bVisible                = old_s->bVisible;
625 	new_s->bCamo                   = old_s->bCamo;
626 	new_s->bLevel                  = old_s->bLevel;
627 	if(old_s->bLevel == SECOND_LEVEL) SetSoldierHeight(new_s, SECOND_LEVEL_Z_OFFSET);
628 	if (new_s->bCamo != 0) CreateSoldierPalettes(*new_s);
629 
630 	if (team == OUR_TEAM) new_s->bVisible = 1;
631 
632 	// Copy over any items.
633 	for (UINT32 i = 0; i != NUM_INV_SLOTS; ++i)
634 	{
635 		new_s->inv[i] = old_s->inv[i];
636 	}
637 
638 	// Loop through all active merc slots, change any attacker's target if they
639 	// were once on this guy.
640 	FOR_EACH_MERC(i)
641 	{
642 		SOLDIERTYPE& s = **i;
643 		if (s.target == old_s) s.target = new_s;
644 	}
645 
646 	new_s->sInsertionGridNo = old_gridno;
647 
648 	if (gfPotentialTeamChangeDuringDeath)
649 	{
650 		HandleCheckForDeathCommonCode(old_s);
651 	}
652 
653 	if (gfWorldLoaded && old_s->bInSector)
654 	{
655 		AddSoldierToSectorNoCalculateDirectionUseAnimation(new_s, old_s->usAnimState, old_s->usAniCode);
656 		HandleSight(*new_s, SIGHT_LOOK | SIGHT_RADIO);
657 	}
658 
659 	if (new_s->ubProfile != NO_PROFILE)
660 	{
661 		UINT8& misc_flags = GetProfile(new_s->ubProfile).ubMiscFlags;
662 		misc_flags = team == OUR_TEAM ?
663 			misc_flags |  PROFILE_MISC_FLAG_RECRUITED :
664 			misc_flags & ~PROFILE_MISC_FLAG_RECRUITED;
665 	}
666 
667 	return new_s;
668 }
669 
670 
RecruitRPC(UINT8 ubCharNum)671 BOOLEAN RecruitRPC( UINT8 ubCharNum )
672 {
673 	SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubCharNum);
674 	if (!pSoldier)
675 	{
676 		return( FALSE );
677 	}
678 
679 	// OK, set recruit flag..
680 	gMercProfiles[ ubCharNum ].ubMiscFlags |= PROFILE_MISC_FLAG_RECRUITED;
681 
682 	// Add this guy to our team!
683 	SOLDIERTYPE* const pNewSoldier = ChangeSoldierTeam(pSoldier, OUR_TEAM);
684 
685 	// handle set up any RPC's that will leave us in time
686 	if ( ubCharNum == SLAY )
687 	{
688 		// slay will leave in a week
689 		pNewSoldier->iEndofContractTime = GetWorldTotalMin() + ( 7 * 24 * 60 );
690 
691 		KickOutWheelchair( pNewSoldier );
692 	}
693 	else if ( ubCharNum == DYNAMO && gubQuest[ QUEST_FREE_DYNAMO ] == QUESTINPROGRESS )
694 	{
695 		EndQuest( QUEST_FREE_DYNAMO, pSoldier->sSectorX, pSoldier->sSectorY );
696 	}
697 	// handle town loyalty adjustment
698 	HandleTownLoyaltyForNPCRecruitment( pNewSoldier );
699 
700 	// Try putting them into the current squad
701 	if (!AddCharacterToSquad(pNewSoldier, CurrentSquad()))
702 	{
703 		AddCharacterToAnySquad( pNewSoldier );
704 	}
705 
706 	ResetDeadSquadMemberList( pNewSoldier->bAssignment );
707 
708 	DirtyMercPanelInterface( pNewSoldier, DIRTYLEVEL2 );
709 
710 	if ( pNewSoldier->inv[ HANDPOS ].usItem == NOTHING )
711 	{
712 		// empty handed - swap in first available weapon
713 		INT8		bSlot;
714 
715 		bSlot = FindObjClass( pNewSoldier, IC_WEAPON );
716 		if ( bSlot != NO_SLOT )
717 		{
718 			if ( GCM->getItem(pNewSoldier->inv[ bSlot ].usItem)->isTwoHanded() )
719 			{
720 				if ( bSlot != SECONDHANDPOS && pNewSoldier->inv[ SECONDHANDPOS ].usItem != NOTHING )
721 				{
722 					// need to move second hand item out first
723 					AutoPlaceObject( pNewSoldier, &(pNewSoldier->inv[ SECONDHANDPOS ]), FALSE );
724 				}
725 			}
726 			// swap item to hand
727 			SwapObjs( &(pNewSoldier->inv[ bSlot ]), &(pNewSoldier->inv[ HANDPOS ]) );
728 		}
729 	}
730 
731 	if ( ubCharNum == IRA )
732 	{
733 		// trigger 0th PCscript line
734 		TriggerNPCRecord( IRA, 0 );
735 	}
736 
737 	// Set whatkind of merc am i
738 	pNewSoldier->ubWhatKindOfMercAmI = MERC_TYPE__NPC;
739 
740 
741 	//
742 	//add a history log that tells the user that a npc has joined the team
743 	//
744 	// ( pass in pNewSoldier->sSectorX cause if its invalid, -1, n/a will appear as the sector in the history log )
745 	AddHistoryToPlayersLog( HISTORY_RPC_JOINED_TEAM, pNewSoldier->ubProfile, GetWorldTotalMin(), pNewSoldier->sSectorX, pNewSoldier->sSectorY );
746 
747 
748 	//remove the merc from the Personnel screens departed list ( if they have never been hired before, its ok to call it )
749 	RemoveNewlyHiredMercFromPersonnelDepartedList( pSoldier->ubProfile );
750 
751 	return( TRUE );
752 }
753 
RecruitEPC(UINT8 ubCharNum)754 BOOLEAN RecruitEPC( UINT8 ubCharNum )
755 {
756 	SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubCharNum);
757 	if (!pSoldier)
758 	{
759 		return( FALSE );
760 	}
761 
762 	// OK, set recruit flag..
763 	gMercProfiles[ ubCharNum ].ubMiscFlags |= PROFILE_MISC_FLAG_EPCACTIVE;
764 
765 	gMercProfiles[ ubCharNum ].ubMiscFlags3 &= ~PROFILE_MISC_FLAG3_PERMANENT_INSERTION_CODE;
766 
767 	// Add this guy to our team!
768 	SOLDIERTYPE* const pNewSoldier = ChangeSoldierTeam( pSoldier, OUR_TEAM );
769 	pNewSoldier->ubWhatKindOfMercAmI = MERC_TYPE__EPC;
770 
771 	// Try putting them into the current squad
772 	if (!AddCharacterToSquad(pNewSoldier, CurrentSquad()))
773 	{
774 		AddCharacterToAnySquad( pNewSoldier );
775 	}
776 
777 	ResetDeadSquadMemberList( pNewSoldier->bAssignment );
778 
779 	DirtyMercPanelInterface( pNewSoldier, DIRTYLEVEL2 );
780 	// Make the interface panel dirty..
781 	// This will dirty the panel next frame...
782 	gfRerenderInterfaceFromHelpText = TRUE;
783 
784 
785 	// If we are a robot, look to update controller....
786 	if ( pNewSoldier->uiStatusFlags & SOLDIER_ROBOT )
787 	{
788 		UpdateRobotControllerGivenRobot( pNewSoldier );
789 	}
790 
791 	// Set whatkind of merc am i
792 	pNewSoldier->ubWhatKindOfMercAmI = MERC_TYPE__EPC;
793 
794 	UpdateTeamPanelAssignments( );
795 
796 	return( TRUE );
797 }
798 
799 
UnRecruitEPC(ProfileID const pid)800 BOOLEAN UnRecruitEPC(ProfileID const pid)
801 {
802 	SOLDIERTYPE* const s = FindSoldierByProfileID(pid);
803 	if (!s) return FALSE;
804 	if (s->ubWhatKindOfMercAmI != MERC_TYPE__EPC) return FALSE;
805 
806 	if (s->bAssignment < ON_DUTY) ResetDeadSquadMemberList(s->bAssignment);
807 
808 	MERCPROFILESTRUCT& p = GetProfile(pid);
809 
810 	// OK, UN set recruit flag..
811 	p.ubMiscFlags &= ~PROFILE_MISC_FLAG_EPCACTIVE;
812 
813 	// update sector values to current
814 
815 	// check to see if this person should disappear from the map after this
816 	if ((pid == JOHN || pid == MARY) &&
817 			s->sSectorX == 13            &&
818 			s->sSectorY == MAP_ROW_B     &&
819 			s->bSectorZ == 0)
820 	{
821 		p.sSectorX = 0;
822 		p.sSectorY = 0;
823 		p.bSectorZ = 0;
824 	}
825 	else
826 	{
827 		p.sSectorX = s->sSectorX;
828 		p.sSectorY = s->sSectorY;
829 		p.bSectorZ = s->bSectorZ;
830 	}
831 
832 	// how do we decide whether or not to set this?
833 	p.fUseProfileInsertionInfo  = TRUE;
834 	p.ubMiscFlags3             |= PROFILE_MISC_FLAG3_PERMANENT_INSERTION_CODE;
835 
836 	ChangeSoldierTeam(s, CIV_TEAM);
837 	UpdateTeamPanelAssignments();
838 	return TRUE;
839 }
840 
841 
WhichBuddy(UINT8 ubCharNum,UINT8 ubBuddy)842 INT8 WhichBuddy( UINT8 ubCharNum, UINT8 ubBuddy )
843 {
844 	if (ubCharNum == NO_PROFILE)
845 	{
846 		return -1;
847 	}
848 
849 	MERCPROFILESTRUCT const& p = GetProfile(ubCharNum);
850 	for (INT8 bLoop = 0; bLoop < 3; bLoop++)
851 	{
852 		if (p.bBuddy[bLoop] == ubBuddy)
853 		{
854 			return( bLoop );
855 		}
856 	}
857 	return( -1 );
858 }
859 
WhichHated(UINT8 ubCharNum,UINT8 ubHated)860 INT8 WhichHated( UINT8 ubCharNum, UINT8 ubHated )
861 {
862 	INT8								bLoop;
863 
864 	MERCPROFILESTRUCT const& p = GetProfile(ubCharNum);
865 
866 	for (bLoop = 0; bLoop < 3; bLoop++)
867 	{
868 		if (p.bHated[bLoop] == ubHated)
869 		{
870 			return( bLoop );
871 		}
872 	}
873 	return( -1 );
874 }
875 
876 
GetFirstBuddyOnTeam(MERCPROFILESTRUCT const & p)877 INT8 GetFirstBuddyOnTeam(MERCPROFILESTRUCT const& p)
878 {
879 	for (INT i = 0; i != 3; ++i)
880 	{
881 		INT8 const buddy = p.bBuddy[i];
882 		if (buddy < 0)                     continue;
883 		if (!IsMercOnTeam(buddy))          continue;
884 		if (IsMercDead(GetProfile(buddy))) continue;
885 		return buddy;
886 	}
887 	return -1;
888 }
889 
890 
IsProfileATerrorist(ProfileID const pid)891 bool IsProfileATerrorist(ProfileID const pid)
892 {
893 	FOR_EACH(TerroristInfo const, i, g_terrorist_infos)
894 	{
895 		if (i->profile == pid) return true;
896 	}
897 	return false;
898 }
899 
900 
IsProfileAHeadMiner(UINT8 ubProfile)901 BOOLEAN IsProfileAHeadMiner(UINT8 ubProfile)
902 {
903 	switch (ubProfile)
904 	{
905 		case FRED:
906 		case MATT:
907 		case OSWALD:
908 		case CALVIN:
909 		case CARL:   return TRUE;
910 		default:     return FALSE;
911 	}
912 }
913 
914 
UpdateSoldierPointerDataIntoProfile()915 void UpdateSoldierPointerDataIntoProfile()
916 {
917 	FOR_EACH_MERC(i)
918 	{
919 		SOLDIERTYPE const& s = **i;
920 		if (s.ubProfile == NO_PROFILE) continue;
921 		// If we are above player mercs
922 		if (MercProfile(s.ubProfile).isPlayerMerc()) continue;
923 
924 		MERCPROFILESTRUCT& p = GetProfile(s.ubProfile);
925 		p.bLife         = s.bLife;
926 		p.bLifeMax      = s.bLifeMax;
927 		p.bAgility      = s.bAgility;
928 		p.bLeadership   = s.bLeadership;
929 		p.bDexterity    = s.bDexterity;
930 		p.bStrength     = s.bStrength;
931 		p.bWisdom       = s.bWisdom;
932 		p.bExpLevel     = s.bExpLevel;
933 		p.bMarksmanship = s.bMarksmanship;
934 		p.bMedical      = s.bMedical;
935 		p.bMechanical   = s.bMechanical;
936 		p.bExplosive    = s.bExplosive;
937 	}
938 }
939 
SwapLarrysProfiles(SOLDIERTYPE * const s)940 SOLDIERTYPE* SwapLarrysProfiles(SOLDIERTYPE* const s)
941 {
942 	const ProfileID src_id = s->ubProfile;
943 	ProfileID       dst_id;
944 	switch (src_id)
945 	{
946 		case LARRY_NORMAL: dst_id = LARRY_DRUNK;  break;
947 		case LARRY_DRUNK:  dst_id = LARRY_NORMAL; break;
948 		default:           return s; // I don't think so!
949 	}
950 
951 	MERCPROFILESTRUCT const& src = GetProfile(src_id);
952 	MERCPROFILESTRUCT&       dst = GetProfile(dst_id);
953 
954 	dst.ubMiscFlags2                = src.ubMiscFlags2;
955 	dst.ubMiscFlags                 = src.ubMiscFlags;
956 	dst.sSectorX                    = src.sSectorX;
957 	dst.sSectorY                    = src.sSectorY;
958 	dst.uiDayBecomesAvailable       = src.uiDayBecomesAvailable;
959 	dst.usKills                     = src.usKills;
960 	dst.usAssists                   = src.usAssists;
961 	dst.usShotsFired                = src.usShotsFired;
962 	dst.usShotsHit                  = src.usShotsHit;
963 	dst.usBattlesFought             = src.usBattlesFought;
964 	dst.usTimesWounded              = src.usTimesWounded;
965 	dst.usTotalDaysServed           = src.usTotalDaysServed;
966 	dst.fUseProfileInsertionInfo    = src.fUseProfileInsertionInfo;
967 	dst.sGridNo                     = src.sGridNo;
968 	dst.ubQuoteActionID             = src.ubQuoteActionID;
969 	dst.ubLastQuoteSaid             = src.ubLastQuoteSaid;
970 	dst.ubStrategicInsertionCode    = src.ubStrategicInsertionCode;
971 	dst.bMercStatus                 = src.bMercStatus;
972 	dst.bSectorZ                    = src.bSectorZ;
973 	dst.usStrategicInsertionData    = src.usStrategicInsertionData;
974 	dst.ubMiscFlags3                = src.ubMiscFlags3;
975 	dst.ubDaysOfMoraleHangover      = src.ubDaysOfMoraleHangover;
976 	dst.ubNumTimesDrugUseInLifetime = src.ubNumTimesDrugUseInLifetime;
977 	dst.uiPrecedentQuoteSaid        = src.uiPrecedentQuoteSaid;
978 	dst.sPreCombatGridNo            = src.sPreCombatGridNo;
979 
980 // CJC: this is causing problems so just skip the transfer of exp...
981 /*
982 	dst.sLifeGain         = src.sLifeGain;
983 	dst.sAgilityGain      = src.sAgilityGain;
984 	dst.sDexterityGain    = src.sDexterityGain;
985 	dst.sStrengthGain     = src.sStrengthGain;
986 	dst.sLeadershipGain   = src.sLeadershipGain;
987 	dst.sWisdomGain       = src.sWisdomGain;
988 	dst.sExpLevelGain     = src.sExpLevelGain;
989 	dst.sMarksmanshipGain = src.sMarksmanshipGain;
990 	dst.sMedicalGain      = src.sMedicalGain;
991 	dst.sMechanicGain     = src.sMechanicGain;
992 	dst.sExplosivesGain   = src.sExplosivesGain;
993 
994 	dst.bLifeDelta         = src.bLifeDelta;
995 	dst.bAgilityDelta      = src.bAgilityDelta;
996 	dst.bDexterityDelta    = src.bDexterityDelta;
997 	dst.bStrengthDelta     = src.bStrengthDelta;
998 	dst.bLeadershipDelta   = src.bLeadershipDelta;
999 	dst.bWisdomDelta       = src.bWisdomDelta;
1000 	dst.bExpLevelDelta     = src.bExpLevelDelta;
1001 	dst.bMarksmanshipDelta = src.bMarksmanshipDelta;
1002 	dst.bMedicalDelta      = src.bMedicalDelta;
1003 	dst.bMechanicDelta     = src.bMechanicDelta;
1004 	dst.bExplosivesDelta   = src.bExplosivesDelta;
1005 */
1006 
1007 	memcpy(dst.bInvStatus, src.bInvStatus, sizeof(dst.bInvStatus));
1008 	memcpy(dst.bInvNumber, src.bInvStatus, sizeof(dst.bInvNumber));
1009 	memcpy(dst.inv,        src.inv,        sizeof(dst.inv));
1010 
1011 	DeleteSoldierFace(s);
1012 	s->ubProfile = dst_id;
1013 	InitSoldierFace(*s);
1014 
1015 	s->bStrength     = dst.bStrength     + dst.bStrengthDelta;
1016 	s->bDexterity    = dst.bDexterity    + dst.bDexterityDelta;
1017 	s->bAgility      = dst.bAgility      + dst.bAgilityDelta;
1018 	s->bWisdom       = dst.bWisdom       + dst.bWisdomDelta;
1019 	s->bExpLevel     = dst.bExpLevel     + dst.bExpLevelDelta;
1020 	s->bLeadership   = dst.bLeadership   + dst.bLeadershipDelta;
1021 	s->bMarksmanship = dst.bMarksmanship + dst.bMarksmanshipDelta;
1022 	s->bMechanical   = dst.bMechanical   + dst.bMechanicDelta;
1023 	s->bMedical      = dst.bMedical      + dst.bMedicalDelta;
1024 	s->bExplosive    = dst.bExplosive    + dst.bExplosivesDelta;
1025 
1026 	if (s->ubProfile == LARRY_DRUNK)
1027 	{
1028 		SetFactTrue(FACT_LARRY_CHANGED);
1029 	}
1030 	else
1031 	{
1032 		SetFactFalse(FACT_LARRY_CHANGED);
1033 	}
1034 
1035 	DirtyMercPanelInterface(s, DIRTYLEVEL2);
1036 
1037 	return s;
1038 }
1039 
1040 
DoesNPCOwnBuilding(SOLDIERTYPE * pSoldier,INT16 sGridNo)1041 BOOLEAN DoesNPCOwnBuilding( SOLDIERTYPE *pSoldier, INT16 sGridNo )
1042 {
1043 	UINT8 ubRoomInfo;
1044 
1045 	// Get room info
1046 	ubRoomInfo = gubWorldRoomInfo[ sGridNo ];
1047 
1048 	if ( ubRoomInfo == NO_ROOM )
1049 	{
1050 		return( FALSE );
1051 	}
1052 
1053 	// Are we an NPC?
1054 	if ( pSoldier->bTeam != CIV_TEAM )
1055 	{
1056 		return( FALSE );
1057 	}
1058 
1059 	// OK, check both ranges
1060 	if ( ubRoomInfo >= gMercProfiles[ pSoldier->ubProfile ].ubRoomRangeStart[ 0 ] &&
1061 			ubRoomInfo <= gMercProfiles[ pSoldier->ubProfile ].ubRoomRangeEnd[ 0 ] )
1062 	{
1063 		return( TRUE );
1064 	}
1065 
1066 	if ( ubRoomInfo >= gMercProfiles[ pSoldier->ubProfile ].ubRoomRangeStart[ 1 ] &&
1067 			ubRoomInfo <= gMercProfiles[ pSoldier->ubProfile ].ubRoomRangeEnd[ 1 ] )
1068 	{
1069 		return( TRUE );
1070 	}
1071 
1072 	return( FALSE );
1073 }
1074 
IsProfileIdAnAimOrMERCMerc(UINT8 ubProfileID)1075 BOOLEAN IsProfileIdAnAimOrMERCMerc(UINT8 ubProfileID)
1076 {
1077 	// AIM: ubProfileID < BIFF
1078 	// MERC: ubProfileID >= BIFF && ubProfileID <= BUBBA
1079 	MercProfile p(ubProfileID);
1080 	return p.isAIMMerc() || p.isMERCMerc();
1081 }
1082