1 #include "Font_Control.h"
2 #include "Debug.h"
3 #include "math.h"
4 #include "MapScreen.h"
5 #include "MessageBoxScreen.h"
6 #include "WorldDef.h"
7 #include "Assignments.h"
8 #include "Soldier_Control.h"
9 #include "Animation_Data.h"
10 #include "Render_Fun.h"
11 #include "Render_Dirty.h"
12 #include "MouseSystem.h"
13 #include "SysUtil.h"
14 #include "Points.h"
15 #include "Random.h"
16 #include "AI.h"
17 #include "Soldier_Ani.h"
18 #include "Overhead.h"
19 #include "Soldier_Profile.h"
20 #include "Game_Clock.h"
21 #include "Soldier_Create.h"
22 #include "Merc_Hiring.h"
23 #include "Game_Event_Hook.h"
24 #include "Message.h"
25 #include "StrategicMap.h"
26 #include "Strategic.h"
27 #include "Items.h"
28 #include "Soldier_Add.h"
29 #include "History.h"
30 #include "Squads.h"
31 #include "Strategic_Merc_Handler.h"
32 #include "Dialogue_Control.h"
33 #include "Map_Screen_Interface.h"
34 #include "Map_Screen_Interface_Map.h"
35 #include "ScreenIDs.h"
36 #include "JAScreens.h"
37 #include "Text.h"
38 #include "Merc_Contract.h"
39 #include "LaptopSave.h"
40 #include "Personnel.h"
41 #include "Auto_Resolve.h"
42 #include "Map_Screen_Interface_Bottom.h"
43 #include "Quests.h"
44 #include "Logger.h"
45
46 #include <string_theory/string>
47
48
49 #define MIN_FLIGHT_PREP_TIME 6
50
51 extern BOOLEAN gfTacticalDoHeliRun;
52 extern BOOLEAN gfFirstHeliRun;
53
54 // ATE: Globals that dictate where the mercs will land once being hired
55 // Default to start sector
56 // Saved in general saved game structure
57 INT16 g_merc_arrive_sector = START_SECTOR;
58
CreateSpecialItem(SOLDIERTYPE * const s,UINT16 item)59 void CreateSpecialItem(SOLDIERTYPE* const s, UINT16 item)
60 {
61 OBJECTTYPE o;
62 CreateItem(item, 100, &o);
63 BOOLEAN fReturn = AutoPlaceObject(s, &o, FALSE);
64 if (!fReturn) {
65 // no space, so overwrite an existing item (can happen when importing IMPs)
66 s->inv[SMALLPOCK8POS] = o;
67 }
68 }
69
HireMerc(MERC_HIRE_STRUCT & h)70 INT8 HireMerc(MERC_HIRE_STRUCT& h)
71 {
72 ProfileID const pid = h.ubProfileID;
73 MERCPROFILESTRUCT& p = GetProfile(pid);
74
75 // If we are to disregard the status of the merc
76 switch (p.bMercStatus)
77 {
78 case 0:
79 case MERC_ANNOYED_BUT_CAN_STILL_CONTACT:
80 case MERC_HIRED_BUT_NOT_ARRIVED_YET:
81 break;
82
83 default:
84 return MERC_HIRE_FAILED;
85 }
86
87 if (NumberOfMercsOnPlayerTeam() >= 18) return MERC_HIRE_OVER_20_MERCS_HIRED;
88
89 // ATE: if we are to use landing zone, update to latest value
90 // they will be updated again just before arrival...
91 if (h.fUseLandingZoneForArrival)
92 {
93 h.sSectorX = SECTORX(g_merc_arrive_sector);
94 h.sSectorY = SECTORY(g_merc_arrive_sector);
95 h.bSectorZ = 0;
96 }
97
98 SOLDIERCREATE_STRUCT MercCreateStruct;
99 MercCreateStruct = SOLDIERCREATE_STRUCT{};
100 MercCreateStruct.ubProfile = pid;
101 MercCreateStruct.sSectorX = h.sSectorX;
102 MercCreateStruct.sSectorY = h.sSectorY;
103 MercCreateStruct.bSectorZ = h.bSectorZ;
104 MercCreateStruct.bTeam = OUR_TEAM;
105 MercCreateStruct.fCopyProfileItemsOver = h.fCopyProfileItemsOver;
106 SOLDIERTYPE* const s = TacticalCreateSoldier(MercCreateStruct);
107 if (s == NULL)
108 {
109 SLOGW("TacticalCreateSoldier in HireMerc(): Failed to Add Merc");
110 return MERC_HIRE_FAILED;
111 }
112
113 if (DidGameJustStart())
114 {
115 // OK, CHECK FOR FIRST GUY, GIVE HIM SPECIAL ITEM!
116 if (s->ubID == 0)
117 {
118 // OK, give this item to our merc!
119 CreateSpecialItem(s, LETTER);
120 }
121
122 // Set insertion for first time in chopper
123 h.ubInsertionCode = INSERTION_CODE_CHOPPER;
124 }
125
126 // Record how long the merc will be gone for
127 p.bMercStatus = (UINT8)h.iTotalContractLength;
128
129 // Copy over insertion data
130 s->ubStrategicInsertionCode = h.ubInsertionCode;
131 s->usStrategicInsertionData = h.usInsertionData;
132 // ATE: Copy over value for using landing zone to soldier type
133 s->fUseLandingZoneForArrival = h.fUseLandingZoneForArrival;
134
135 // Set assignment
136 if (s->bAssignment != IN_TRANSIT) SetTimeOfAssignmentChangeForMerc(s);
137 ChangeSoldiersAssignment(s, IN_TRANSIT);
138
139 // Set the contract length
140 s->iTotalContractLength = h.iTotalContractLength;
141
142 // Reset the insurance values
143 s->iStartOfInsuranceContract = 0;
144 s->iTotalLengthOfInsuranceContract = 0;
145
146 // Store arrival time in soldier structure so map screen can display it
147 s->uiTimeSoldierWillArrive = h.uiTimeTillMercArrives;
148
149 if (DidGameJustStart())
150 {
151 // Set time of initial merc arrival in minutes
152 h.uiTimeTillMercArrives = (STARTING_TIME + FIRST_ARRIVAL_DELAY) / NUM_SEC_IN_MIN;
153
154 //set when the merc's contract is finished
155 s->iEndofContractTime = GetMidnightOfFutureDayInMinutes(s->iTotalContractLength) + GetHourWhenContractDone(s) * 60;
156 }
157 else
158 {
159 //set when the merc's contract is finished (+1 cause it takes a day for the merc to arrive)
160 s->iEndofContractTime = GetMidnightOfFutureDayInMinutes(1 + s->iTotalContractLength) + GetHourWhenContractDone(s) * 60;
161 }
162
163 // Set the time and ID of the last hired merc will arrive
164 LaptopSaveInfo.sLastHiredMerc.iIdOfMerc = pid;
165 LaptopSaveInfo.sLastHiredMerc.uiArrivalTime = h.uiTimeTillMercArrives;
166
167 // If we are trying to hire a merc that should arrive later, put the merc in the queue
168 if (h.uiTimeTillMercArrives != 0)
169 {
170 AddStrategicEvent(EVENT_DELAYED_HIRING_OF_MERC, h.uiTimeTillMercArrives, s->ubID);
171 // Specify that the merc is hired but has not arrived yet
172 p.bMercStatus = MERC_HIRED_BUT_NOT_ARRIVED_YET;
173 }
174
175 // Set the type of merc
176 if (h.bWhatKindOfMerc == MERC_TYPE__AIM_MERC)
177 {
178 s->ubWhatKindOfMercAmI = MERC_TYPE__AIM_MERC;
179
180 // Determine how much the contract is, and remember what type of contract he got
181 switch (h.iTotalContractLength)
182 {
183 case 1:
184 s->bTypeOfLastContract = CONTRACT_EXTEND_1_DAY;
185 s->iTimeCanSignElsewhere = GetWorldTotalMin();
186 break;
187
188 case 7:
189 s->bTypeOfLastContract = CONTRACT_EXTEND_1_WEEK;
190 s->iTimeCanSignElsewhere = GetWorldTotalMin();
191 break;
192
193 case 14:
194 s->bTypeOfLastContract = CONTRACT_EXTEND_2_WEEK;
195 // This fellow needs to stay the whole duration!
196 s->iTimeCanSignElsewhere = s->iEndofContractTime;
197 break;
198 }
199
200 // remember the medical deposit we PAID. The one in his profile can increase when he levels!
201 s->usMedicalDeposit = p.sMedicalDepositAmount;
202 }
203 else if (h.bWhatKindOfMerc == MERC_TYPE__MERC)
204 {
205 s->ubWhatKindOfMercAmI = MERC_TYPE__MERC;
206
207 p.iMercMercContractLength = 1;
208
209 // Set starting conditions for the merc
210 s->iStartContractTime = GetWorldDay();
211
212 AddHistoryToPlayersLog(HISTORY_HIRED_MERC_FROM_MERC, pid, GetWorldTotalMin(), -1, -1);
213 }
214 else if (h.bWhatKindOfMerc == MERC_TYPE__PLAYER_CHARACTER)
215 {
216 s->ubWhatKindOfMercAmI = MERC_TYPE__PLAYER_CHARACTER;
217 }
218 else
219 {
220 s->ubWhatKindOfMercAmI = MERC_TYPE__NPC;
221 }
222
223 // remove the merc from the Personnel screens departed list (if they have never been hired before, its ok to call it)
224 RemoveNewlyHiredMercFromPersonnelDepartedList(s->ubProfile);
225
226 gfAtLeastOneMercWasHired = TRUE;
227 return MERC_HIRE_OK;
228 }
229
230
231 static void CheckForValidArrivalSector(void);
232
233
MercArrivesCallback(SOLDIERTYPE & s)234 void MercArrivesCallback(SOLDIERTYPE& s)
235 {
236 UINT32 uiTimeOfPost;
237
238 if (!DidGameJustStart() && g_merc_arrive_sector == START_SECTOR)
239 {
240 // Mercs arriving in start sector. This sector has been deemed as the always
241 // safe sector. Seeing we don't support entry into a hostile sector (except
242 // for the beginning), we will nuke any enemies in this sector first.
243 if (gWorldSectorX != SECTORX(START_SECTOR) ||
244 gWorldSectorY != SECTORY(START_SECTOR) ||
245 gbWorldSectorZ != 0)
246 {
247 EliminateAllEnemies(SECTORX(g_merc_arrive_sector), SECTORY(g_merc_arrive_sector));
248 }
249 }
250
251 // This will update ANY soldiers currently schedules to arrive too
252 CheckForValidArrivalSector( );
253
254 // stop time compression until player restarts it
255 StopTimeCompression();
256
257 MERCPROFILESTRUCT& p = GetProfile(s.ubProfile);
258
259 // add the guy to a squad
260 AddCharacterToAnySquad(&s);
261
262 // ATE: Make sure we use global.....
263 if (s.fUseLandingZoneForArrival)
264 {
265 s.sSectorX = SECTORX(g_merc_arrive_sector);
266 s.sSectorY = SECTORY(g_merc_arrive_sector);
267 s.bSectorZ = 0;
268 }
269
270 // Add merc to sector ( if it's the current one )
271 if (gWorldSectorX == s.sSectorX && gWorldSectorY == s.sSectorY && s.bSectorZ == gbWorldSectorZ)
272 {
273 // OK, If this sector is currently loaded, and guy does not have CHOPPER insertion code....
274 // ( which means we are at beginning of game if so )
275 // Setup chopper....
276 if (s.ubStrategicInsertionCode != INSERTION_CODE_CHOPPER &&
277 SECTOR(s.sSectorX, s.sSectorY) == START_SECTOR)
278 {
279 gfTacticalDoHeliRun = TRUE;
280
281 // OK, If we are in mapscreen, get out...
282 if ( guiCurrentScreen == MAP_SCREEN )
283 {
284 // ATE: Make sure the current one is selected!
285 ChangeSelectedMapSector( gWorldSectorX, gWorldSectorY, 0 );
286
287 RequestTriggerExitFromMapscreen( MAP_EXIT_TO_TACTICAL );
288 }
289
290 s.ubStrategicInsertionCode = INSERTION_CODE_CHOPPER;
291 }
292
293 UpdateMercInSector(s, s.sSectorX, s.sSectorY, s.bSectorZ);
294 }
295 else
296 {
297 // OK, otherwise, set them in north area, so once we load again, they are here.
298 s.ubStrategicInsertionCode = INSERTION_CODE_NORTH;
299 }
300
301
302 if (s.ubStrategicInsertionCode != INSERTION_CODE_CHOPPER)
303 {
304 ScreenMsg(FONT_MCOLOR_WHITE, MSG_INTERFACE, st_format_printf(TacticalStr[MERC_HAS_ARRIVED_STR], s.name));
305
306 // ATE: He's going to say something, now that they've arrived...
307 if (!gTacticalStatus.bMercArrivingQuoteBeingUsed && !gfFirstHeliRun)
308 {
309 gTacticalStatus.bMercArrivingQuoteBeingUsed = TRUE;
310
311 TacticalCharacterDialogue(&s, QUOTE_MERC_REACHED_DESTINATION);
312
313 class DialogueEventUnsetArrivesFlag : public DialogueEvent
314 {
315 public:
316 bool Execute()
317 {
318 gTacticalStatus.bMercArrivingQuoteBeingUsed = FALSE;
319 return false;
320 }
321 };
322
323 DialogueEvent::Add(new DialogueEventUnsetArrivesFlag());
324 }
325 }
326
327 //record how long the merc will be gone for
328 p.bMercStatus = (UINT8)s.iTotalContractLength;
329
330 // remember when excatly he ARRIVED in Arulco, in case he gets fired early
331 s.uiTimeOfLastContractUpdate = GetWorldTotalMin();
332
333 //set when the merc's contract is finished
334 s.iEndofContractTime = GetMidnightOfFutureDayInMinutes(s.iTotalContractLength) + GetHourWhenContractDone(&s) * 60;
335
336 // Do initial check for bad items
337 if (s.bTeam == OUR_TEAM)
338 {
339 //ATE: Try to see if our equipment sucks!
340 if (SoldierHasWorseEquipmentThanUsedTo(&s))
341 {
342 // Randomly anytime between 9:00, and 10:00
343 uiTimeOfPost = 540 + Random( 660 );
344
345 if ( GetWorldMinutesInDay() < uiTimeOfPost )
346 {
347 AddSameDayStrategicEvent(EVENT_MERC_COMPLAIN_EQUIPMENT, uiTimeOfPost, s.ubProfile);
348 }
349 }
350 }
351
352 HandleMercArrivesQuotes(s);
353
354 fTeamPanelDirty = TRUE;
355
356 // if the currently selected sector has no one in it, select this one instead
357 if ( !CanGoToTacticalInSector( sSelMapX, sSelMapY, ( UINT8 )iCurrentMapSectorZ ) )
358 {
359 ChangeSelectedMapSector(s.sSectorX, s.sSectorY, 0);
360 }
361 }
362
363
IsMercHireable(MERCPROFILESTRUCT const & p)364 bool IsMercHireable(MERCPROFILESTRUCT const& p)
365 {
366 // If the merc has an .edt file, is not away on assignment, and isn't already
367 // hired (but not arrived yet), he is not dead and he isn't returning home
368 return
369 p.bMercStatus <= 0 &&
370 p.bMercStatus != MERC_HAS_NO_TEXT_FILE &&
371 p.bMercStatus != MERC_HIRED_BUT_NOT_ARRIVED_YET &&
372 p.bMercStatus != MERC_IS_DEAD &&
373 p.bMercStatus != MERC_RETURNING_HOME &&
374 p.bMercStatus != MERC_WORKING_ELSEWHERE &&
375 p.bMercStatus != MERC_FIRED_AS_A_POW &&
376 p.uiDayBecomesAvailable == 0;
377 }
378
379
IsMercDead(MERCPROFILESTRUCT const & p)380 bool IsMercDead(MERCPROFILESTRUCT const& p)
381 {
382 return p.bMercStatus == MERC_IS_DEAD;
383 }
384
385
HandleMercArrivesQuotes(SOLDIERTYPE & s)386 void HandleMercArrivesQuotes(SOLDIERTYPE& s)
387 {
388 // If we are approaching with helicopter, don't say any ( yet )
389 if (s.ubStrategicInsertionCode == INSERTION_CODE_CHOPPER) return;
390
391 // Player-generated characters issue a comment about arriving in Omerta.
392 if (s.ubWhatKindOfMercAmI == MERC_TYPE__PLAYER_CHARACTER &&
393 gubQuest[QUEST_DELIVER_LETTER] == QUESTINPROGRESS)
394 {
395 TacticalCharacterDialogue(&s, QUOTE_PC_DROPPED_OMERTA);
396 }
397
398 // Check to see if anyone hates this merc and will now complain
399 FOR_EACH_IN_TEAM(other, OUR_TEAM)
400 {
401 if (other->ubWhatKindOfMercAmI != MERC_TYPE__AIM_MERC) continue;
402
403 // hates the merc who has arrived and is going to gripe about it!
404 switch (WhichHated(other->ubProfile, s.ubProfile))
405 {
406 case 0: TacticalCharacterDialogue(other, QUOTE_HATED_1_ARRIVES); break;
407 case 1: TacticalCharacterDialogue(other, QUOTE_HATED_2_ARRIVES); break;
408 default: break;
409 }
410 }
411 }
412
GetMercArrivalTimeOfDay()413 UINT32 GetMercArrivalTimeOfDay( )
414 {
415 UINT32 uiCurrHour;
416 UINT32 uiMinHour;
417
418 // Pick a time...
419
420 // First get the current time of day.....
421 uiCurrHour = GetWorldHour( );
422
423 // Subtract the min time for any arrival....
424 uiMinHour = uiCurrHour + MIN_FLIGHT_PREP_TIME;
425
426 // OK, first check if we need to advance a whole day's time...
427 // See if we have missed the last flight for the day...
428 if ( ( uiCurrHour ) > 13 ) // ( > 1:00 pm - too bad )
429 {
430 // 7:30 flight....
431 return( GetMidnightOfFutureDayInMinutes( 1 ) + MERC_ARRIVE_TIME_SLOT_1 );
432 }
433
434 // Well, now we can handle flights all in one day....
435 // Find next possible flight
436 if ( uiMinHour <= 7 )
437 {
438 return( GetWorldDayInMinutes() + MERC_ARRIVE_TIME_SLOT_1 ); // 7:30 am
439 }
440 else if ( uiMinHour <= 13 )
441 {
442 return( GetWorldDayInMinutes() + MERC_ARRIVE_TIME_SLOT_2 ); // 1:30 pm
443 }
444 else
445 {
446 return( GetWorldDayInMinutes() + MERC_ARRIVE_TIME_SLOT_3 ); // 7:30 pm
447 }
448 }
449
450
UpdateAnyInTransitMercsWithGlobalArrivalSector()451 void UpdateAnyInTransitMercsWithGlobalArrivalSector( )
452 {
453 FOR_EACH_IN_TEAM(s, OUR_TEAM)
454 {
455 if (s->bAssignment == IN_TRANSIT && s->fUseLandingZoneForArrival)
456 {
457 s->sSectorX = SECTORX(g_merc_arrive_sector);
458 s->sSectorY = SECTORY(g_merc_arrive_sector);
459 s->bSectorZ = 0;
460 }
461 }
462 }
463
464
StrategicPythSpacesAway(INT16 sOrigin,INT16 sDest)465 static INT16 StrategicPythSpacesAway(INT16 sOrigin, INT16 sDest)
466 {
467 INT16 sRows,sCols,sResult;
468
469 sRows = ABS((sOrigin / MAP_WORLD_X) - (sDest / MAP_WORLD_X));
470 sCols = ABS((sOrigin % MAP_WORLD_X) - (sDest % MAP_WORLD_X));
471
472
473 // apply Pythagoras's theorem for right-handed triangle:
474 // dist^2 = rows^2 + cols^2, so use the square root to get the distance
475 sResult = (INT16)sqrt((double)((sRows * sRows) + (sCols * sCols)));
476
477 return(sResult);
478 }
479
480
481 // ATE: This function will check if the current arrival sector
482 // is valid
483 // if there are enemies present, it's invalid
484 // if so, search around for nearest non-occupied sector.
CheckForValidArrivalSector(void)485 static void CheckForValidArrivalSector(void)
486 {
487 INT16 sTop, sBottom;
488 INT16 sLeft, sRight;
489 INT16 cnt1, cnt2;
490 UINT8 ubRadius = 4;
491 INT32 leftmost;
492 INT16 sSectorGridNo, sSectorGridNo2;
493 INT32 uiRange, uiLowestRange = 999999;
494 BOOLEAN fFound = FALSE;
495 ST::string sString;
496 ST::string zShortTownIDString1;
497 ST::string zShortTownIDString2;
498
499 sSectorGridNo = SECTOR_INFO_TO_STRATEGIC_INDEX(g_merc_arrive_sector);
500
501 // Check if valid...
502 if ( !StrategicMap[ sSectorGridNo ].fEnemyControlled )
503 {
504 return;
505 }
506
507 zShortTownIDString1 = GetShortSectorString(SECTORX(g_merc_arrive_sector), SECTORY(g_merc_arrive_sector));
508
509
510 // If here - we need to do a search!
511 sTop = ubRadius;
512 sBottom = -ubRadius;
513 sLeft = - ubRadius;
514 sRight = ubRadius;
515
516 INT16 sGoodX = 0; // XXX HACK000E
517 INT16 sGoodY = 0; // XXX HACK000E
518 for( cnt1 = sBottom; cnt1 <= sTop; cnt1++ )
519 {
520 leftmost = ( ( sSectorGridNo + ( MAP_WORLD_X * cnt1 ) )/ MAP_WORLD_X ) * MAP_WORLD_X;
521
522 for( cnt2 = sLeft; cnt2 <= sRight; cnt2++ )
523 {
524 sSectorGridNo2 = sSectorGridNo + ( MAP_WORLD_X * cnt1 ) + cnt2;
525
526 if( sSectorGridNo2 >=1 && sSectorGridNo2 < ( ( MAP_WORLD_X - 1 ) * ( MAP_WORLD_X - 1 ) ) && sSectorGridNo2 >= leftmost && sSectorGridNo2 < ( leftmost + MAP_WORLD_X ) )
527 {
528 if ( !StrategicMap[ sSectorGridNo2 ].fEnemyControlled && !StrategicMap[ sSectorGridNo2 ].fEnemyAirControlled )
529 {
530 uiRange = StrategicPythSpacesAway( sSectorGridNo2, sSectorGridNo );
531
532 if ( uiRange < uiLowestRange )
533 {
534 sGoodY = cnt1;
535 sGoodX = cnt2;
536 uiLowestRange = uiRange;
537 fFound = TRUE;
538 }
539 }
540 }
541 }
542 }
543
544 if ( fFound )
545 {
546 g_merc_arrive_sector = SECTOR(SECTORX(g_merc_arrive_sector) + sGoodX, SECTORY(g_merc_arrive_sector) + sGoodY);
547
548 UpdateAnyInTransitMercsWithGlobalArrivalSector( );
549
550 zShortTownIDString2 = GetShortSectorString(SECTORX(g_merc_arrive_sector), SECTORY(g_merc_arrive_sector));
551
552 sString = st_format_printf(str_arrival_rerouted, zShortTownIDString2, zShortTownIDString1);
553
554 DoScreenIndependantMessageBox( sString, MSG_BOX_FLAG_OK, NULL );
555
556 }
557 }
558