1 #include "NPC.h"
2 #include "AI.h"
3 #include "Animation_Control.h"
4 #include "Arms_Dealer.h"
5 #include "Arms_Dealer_Init.h"
6 #include "Buffer.h"
7 #include "ContentManager.h"
8 #include "Dialogue_Control.h"
9 #include "Directories.h"
10 #include "Faces.h"
11 #include "FileMan.h"
12 #include "Font_Control.h"
13 #include "Game_Clock.h"
14 #include "GameInstance.h"
15 #include "GameRes.h"
16 #include "Interface_Dialogue.h"
17 #include "Interface_Items.h"
18 #include "Isometric_Utils.h"
19 #include "Items.h"
20 #include "LoadSaveData.h"
21 #include "Meanwhile.h"
22 #include "MercProfile.h"
23 #include "Message.h"
24 #include "OppList.h"
25 #include "Overhead.h"
26 #include "Quests.h"
27 #include "QuestText.h"
28 #include "Render_Fun.h"
29 #include "Scheduling.h"
30 #include "SkillCheck.h"
31 #include "Soldier_Add.h"
32 #include "Soldier_Macros.h"
33 #include "Soldier_Tile.h"
34 #include "Strategic_Town_Loyalty.h"
35 #include "Tactical_Save.h"
36 #include "Text.h"
37 #include "Timer_Control.h"
38 #include "WeaponModels.h"
39 #include <string_theory/format>
40 #include <string_theory/string>
41
42 #define NUM_NPC_QUOTE_RECORDS 50
43 #define NUM_CIVQUOTE_SECTORS 20
44 #define MINERS_CIV_QUOTE_INDEX 16
45
46 static const INT16 gsCivQuoteSector[NUM_CIVQUOTE_SECTORS][2] =
47 {
48 { 2, MAP_ROW_A },
49 { 2, MAP_ROW_B },
50 { 13, MAP_ROW_B },
51 { 13, MAP_ROW_C },
52 { 13, MAP_ROW_D },
53 { 8, MAP_ROW_F },
54 { 9, MAP_ROW_F },
55 { 8, MAP_ROW_G },
56 { 9, MAP_ROW_G },
57 { 1, MAP_ROW_H },
58
59 { 2, MAP_ROW_H },
60 { 3, MAP_ROW_H },
61 { 8, MAP_ROW_H },
62 { 13, MAP_ROW_H },
63 { 14, MAP_ROW_I },
64 { 11, MAP_ROW_L },
65 { 12, MAP_ROW_L },
66 { 0, 0 }, // THIS ONE USED NOW - FOR bSectorZ > 0.....
67 { 0, 0 },
68 { 0, 0 },
69 };
70
71 #define NO_FACT (MAX_FACTS - 1)
72 #define NO_QUEST 255
73 #define QUEST_NOT_STARTED_NUM 100
74 #define QUEST_DONE_NUM 200
75 #define NO_QUOTE 255
76 #define IRRELEVANT 255
77 #define MUST_BE_NEW_DAY 254
78 #define NO_MOVE 65535
79 #define INITIATING_FACTOR 30
80
81 #define TURN_FLAG_ON(a, b) ((a) |= (b))
82 #define TURN_FLAG_OFF(a, b) ((a) &= ~(b))
83 #define CHECK_FLAG(a, b) ((a) & (b))
84
85 #define QUOTE_FLAG_SAID 0x0001
86 #define QUOTE_FLAG_ERASE_ONCE_SAID 0x0002
87 #define QUOTE_FLAG_SAY_ONCE_PER_CONVO 0x0004
88
89 #define TURN_UI_OFF 65000
90 #define TURN_UI_ON 65001
91 #define SPECIAL_TURN_UI_OFF 65002
92 #define SPECIAL_TURN_UI_ON 65003
93
94 #define LARGE_AMOUNT_MONEY 1000
95
96 #define ACCEPT_ANY_ITEM 1000
97 #define ANY_RIFLE 1001
98
99 #define NUM_REAL_APPROACHES APPROACH_RECRUIT
100
101
102 enum StandardQuoteIDs
103 {
104 QUOTE_INTRO = 0,
105 QUOTE_SUBS_INTRO,
106 QUOTE_FRIENDLY_DEFAULT1,
107 QUOTE_FRIENDLY_DEFAULT2,
108 QUOTE_GIVEITEM_NO,
109 QUOTE_DIRECT_DEFAULT,
110 QUOTE_THREATEN_DEFAULT,
111 QUOTE_RECRUIT_NO,
112 QUOTE_BYE,
113 QUOTE_GETLOST
114 };
115
116
117 struct NPCQuoteInfo
118 {
119 UINT32 ubIdentifier;
120
121 UINT16 fFlags;
122
123 // conditions
124 union
125 {
126 INT16 sRequiredItem; // item NPC must have to say quote
127 INT16 sRequiredGridno; // location for NPC req'd to say quote
128 };
129 UINT16 usFactMustBeTrue; // ...before saying quote
130 UINT16 usFactMustBeFalse; // ...before saying quote
131 UINT8 ubQuest; // quest must be current to say quote
132 UINT8 ubFirstDay; // first day quote can be said
133 UINT8 ubLastDay; // last day quote can be said
134 UINT8 ubApproachRequired; // must use this approach to generate quote
135 UINT8 ubOpinionRequired; // opinion needed for this quote
136
137 // quote to say (if any)
138 UINT8 ubQuoteNum; // this is the quote to say
139 UINT8 ubNumQuotes; // total # of quotes to say
140
141 // actions
142 UINT8 ubStartQuest;
143 UINT8 ubEndQuest;
144 UINT8 ubTriggerNPC;
145 UINT8 ubTriggerNPCRec;
146 UINT16 usSetFactTrue;
147 UINT16 usGiftItem; // item NPC gives to merc after saying quote
148 UINT16 usGoToGridno;
149 INT16 sActionData; // special action value
150 };
151
152 static NPCQuoteInfo* gpNPCQuoteInfoArray[NUM_PROFILES];
153 static NPCQuoteInfo* gpBackupNPCQuoteInfoArray[NUM_PROFILES];
154 static NPCQuoteInfo* gpCivQuoteInfoArray[NUM_CIVQUOTE_SECTORS];
155
156 // Warning: cheap hack approaching
157 static BOOLEAN gfTriedToLoadQuoteInfoArray[NUM_PROFILES];
158
159 INT8 const gbFirstApproachFlags[] = { 0x01, 0x02, 0x04, 0x08 };
160
161
162 static UINT8 const gubAlternateNPCFileNumsForQueenMeanwhiles[] = { 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176 };
163 static UINT8 const gubAlternateNPCFileNumsForElliotMeanwhiles[] = { 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196 };
164
ExtractNPCQuoteInfoArrayFromFile(HWFILE const f)165 static NPCQuoteInfo* ExtractNPCQuoteInfoArrayFromFile(HWFILE const f)
166 {
167 SGP::Buffer<NPCQuoteInfo> buf(NUM_NPC_QUOTE_RECORDS);
168 for (NPCQuoteInfo* i = buf; i != buf + NUM_NPC_QUOTE_RECORDS; ++i)
169 {
170 BYTE data[32];
171 FileRead(f, data, sizeof(data));
172
173 DataReader d{data};
174 if(isRussianVersion())
175 {
176 EXTR_U32( d, i->ubIdentifier);
177 }
178 else
179 {
180 i->ubIdentifier = 0;
181 }
182 EXTR_U16( d, i->fFlags)
183 EXTR_I16( d, i->sRequiredItem)
184 EXTR_U16( d, i->usFactMustBeTrue)
185 EXTR_U16( d, i->usFactMustBeFalse)
186 EXTR_U8( d, i->ubQuest)
187 EXTR_U8( d, i->ubFirstDay)
188 EXTR_U8( d, i->ubLastDay)
189 EXTR_U8( d, i->ubApproachRequired)
190 EXTR_U8( d, i->ubOpinionRequired)
191 EXTR_U8( d, i->ubQuoteNum)
192 EXTR_U8( d, i->ubNumQuotes)
193 EXTR_U8( d, i->ubStartQuest)
194 EXTR_U8( d, i->ubEndQuest)
195 EXTR_U8( d, i->ubTriggerNPC)
196 EXTR_U8( d, i->ubTriggerNPCRec)
197 EXTR_SKIP(d, 1)
198 EXTR_U16( d, i->usSetFactTrue)
199 EXTR_U16( d, i->usGiftItem)
200 EXTR_U16( d, i->usGoToGridno)
201 EXTR_I16( d, i->sActionData)
202 if(!isRussianVersion())
203 {
204 EXTR_SKIP(d, 4);
205 }
206 Assert(d.getConsumed() == lengthof(data));
207 }
208 return buf.Release();
209 }
210
211
ConditionalExtractNPCQuoteInfoArrayFromFile(HWFILE const f,NPCQuoteInfo * & q)212 static void ConditionalExtractNPCQuoteInfoArrayFromFile(HWFILE const f, NPCQuoteInfo*& q)
213 {
214 UINT8 present;
215 FileRead(f, &present, sizeof(present));
216 FreeNullArray(q);
217 if (!present) return;
218 q = ExtractNPCQuoteInfoArrayFromFile(f);
219 }
220
221
ConditionalInjectNPCQuoteInfoArrayIntoFile(HWFILE const f,NPCQuoteInfo const * const q)222 static void ConditionalInjectNPCQuoteInfoArrayIntoFile(HWFILE const f, NPCQuoteInfo const* const q)
223 {
224 if (!q)
225 {
226 static UINT8 const zero = 0;
227 FileWrite(f, &zero, sizeof(zero));
228 return;
229 }
230
231 static UINT8 const one = 1;
232 FileWrite(f, &one, sizeof(one));
233
234 for (NPCQuoteInfo const* i = q; i != q + NUM_NPC_QUOTE_RECORDS; ++i)
235 {
236 BYTE data[32];
237 DataWriter d{data};
238 if(isRussianVersion())
239 {
240 INJ_U32( d, i->ubIdentifier);
241 }
242 INJ_U16( d, i->fFlags)
243 INJ_I16( d, i->sRequiredItem)
244 INJ_U16( d, i->usFactMustBeTrue)
245 INJ_U16( d, i->usFactMustBeFalse)
246 INJ_U8( d, i->ubQuest)
247 INJ_U8( d, i->ubFirstDay)
248 INJ_U8( d, i->ubLastDay)
249 INJ_U8( d, i->ubApproachRequired)
250 INJ_U8( d, i->ubOpinionRequired)
251 INJ_U8( d, i->ubQuoteNum)
252 INJ_U8( d, i->ubNumQuotes)
253 INJ_U8( d, i->ubStartQuest)
254 INJ_U8( d, i->ubEndQuest)
255 INJ_U8( d, i->ubTriggerNPC)
256 INJ_U8( d, i->ubTriggerNPCRec)
257 INJ_SKIP(d, 1)
258 INJ_U16( d, i->usSetFactTrue)
259 INJ_U16( d, i->usGiftItem)
260 INJ_U16( d, i->usGoToGridno)
261 INJ_I16( d, i->sActionData)
262 if(!isRussianVersion())
263 {
264 INJ_SKIP(d, 4);
265 }
266 Assert(d.getConsumed() == lengthof(data));
267 FileWrite(f, data, sizeof(data));
268 }
269 }
270
271
272 //
273 // NPC QUOTE LOW LEVEL ROUTINES
274 //
275
276
LoadQuoteFile(UINT8 ubNPC)277 static NPCQuoteInfo* LoadQuoteFile(UINT8 ubNPC)
278 try
279 {
280 ST::string zFileName;
281 MercProfile profile(ubNPC);
282
283 if ( ubNPC == PETER || ubNPC == ALBERTO || ubNPC == CARLO )
284 {
285 // use a copy of Herve's data file instead!
286 zFileName = ST::format("{}/{03d}.npc", NPCDATADIR, HERVE);
287 }
288 else if (profile.isPlayerMerc() || (profile.isRPC() && profile.isRecruited()))
289 {
290 zFileName = ST::format("{}/000.npc", NPCDATADIR);
291 }
292 else
293 {
294 zFileName = ST::format("{}/{03d}.npc", NPCDATADIR, ubNPC);
295 }
296
297 // ATE: Put some stuff i here to use a different NPC file if we are in a meanwhile.....
298 if ( AreInMeanwhile( ) )
299 {
300 // If we are the queen....
301 if ( ubNPC == QUEEN )
302 {
303 zFileName = ST::format("{}/{03d}.npc", NPCDATADIR, gubAlternateNPCFileNumsForQueenMeanwhiles[GetMeanwhileID()]);
304 }
305
306 // If we are elliot....
307 if ( ubNPC == ELLIOT )
308 {
309 zFileName = ST::format("{}/{03d}.npc", NPCDATADIR, gubAlternateNPCFileNumsForElliotMeanwhiles[GetMeanwhileID()]);
310 }
311
312 }
313
314 AutoSGPFile f(GCM->openGameResForReading(zFileName));
315 return ExtractNPCQuoteInfoArrayFromFile(f);
316 }
317 catch (const std::exception& e)
318 {
319 STLOGE("caught exception: {}", e.what());
320 return 0;
321 }
322 catch (...) { return 0; }
323
324
RevertToOriginalQuoteFile(UINT8 ubNPC)325 static void RevertToOriginalQuoteFile(UINT8 ubNPC)
326 {
327 if ( gpBackupNPCQuoteInfoArray[ ubNPC ] && gpNPCQuoteInfoArray[ubNPC] )
328 {
329 delete[] gpNPCQuoteInfoArray[ubNPC];
330 gpNPCQuoteInfoArray[ubNPC] = gpBackupNPCQuoteInfoArray[ubNPC];
331 gpBackupNPCQuoteInfoArray[ubNPC] = NULL;
332 }
333 }
334
335
BackupOriginalQuoteFile(UINT8 ubNPC)336 static void BackupOriginalQuoteFile(UINT8 ubNPC)
337 {
338 gpBackupNPCQuoteInfoArray[ ubNPC ] = gpNPCQuoteInfoArray[ ubNPC ];
339 gpNPCQuoteInfoArray[ubNPC] = NULL;
340 }
341
342
EnsureQuoteFileLoaded(UINT8 const ubNPC)343 static NPCQuoteInfo* EnsureQuoteFileLoaded(UINT8 const ubNPC)
344 {
345 if (ubNPC == ROBOT) return 0;
346
347 NPCQuoteInfo*& q = gpNPCQuoteInfoArray[ubNPC];
348 bool load_file = !q;
349
350 if (MercProfile(ubNPC).isRPC())
351 {
352 if (GetProfile(ubNPC).ubMiscFlags & PROFILE_MISC_FLAG_RECRUITED)
353 { // recruited
354 if (!gpBackupNPCQuoteInfoArray[ubNPC])
355 {
356 // no backup stored of current script, so need to backup
357 load_file = true;
358 // set pointer to back up script!
359 BackupOriginalQuoteFile(ubNPC);
360 }
361 // else have backup, are recruited, nothing special
362 }
363 else
364 { // not recruited
365 if (gpBackupNPCQuoteInfoArray[ubNPC])
366 {
367 // backup stored, restore backup
368 RevertToOriginalQuoteFile(ubNPC);
369 }
370 // else are no backup, nothing special
371 }
372 }
373
374 if (load_file)
375 {
376 q = LoadQuoteFile(ubNPC);
377 if (!q)
378 {
379 if (!gfTriedToLoadQuoteInfoArray[ubNPC]) // don't report the error a second time
380 {
381 SLOGE("NPC needs NPC file: %d.", ubNPC );
382 gfTriedToLoadQuoteInfoArray[ubNPC] = TRUE;
383 }
384 // error message at this point!
385 }
386 }
387
388 return q;
389 }
390
391
ReloadQuoteFile(UINT8 const ubNPC)392 bool ReloadQuoteFile(UINT8 const ubNPC)
393 {
394 FreeNullArray(gpNPCQuoteInfoArray[ubNPC]);
395 FreeNullArray(gpBackupNPCQuoteInfoArray[ubNPC]);
396 return EnsureQuoteFileLoaded(ubNPC);
397 }
398
399
ReloadQuoteFileIfLoaded(UINT8 const ubNPC)400 static bool ReloadQuoteFileIfLoaded(UINT8 const ubNPC)
401 {
402 NPCQuoteInfo*& q = gpNPCQuoteInfoArray[ubNPC];
403 if (!q) return TRUE;
404 FreeNullArray(q);
405 return EnsureQuoteFileLoaded(ubNPC);
406 }
407
408
RefreshNPCScriptRecord(UINT8 const ubNPC,UINT8 const record)409 static void RefreshNPCScriptRecord(UINT8 const ubNPC, UINT8 const record)
410 {
411 if (ubNPC == NO_PROFILE)
412 {
413 // loop through all PCs, and refresh their copy of this record
414 for (const MercProfile* p : GCM->listMercProfiles())
415 {
416 ProfileID ubMercID = p->getID();
417 if (p->isPlayerMerc() ||
418 (p->isRPC() && p->isRecruited() && gpBackupNPCQuoteInfoArray[ubMercID]))
419 {
420 RefreshNPCScriptRecord(ubMercID, record);
421 }
422 }
423 return;
424 }
425
426 NPCQuoteInfo* const quotes = gpNPCQuoteInfoArray[ubNPC];
427 if (!quotes) return;
428
429 NPCQuoteInfo& q = quotes[record];
430 // already used? so we don't have to refresh!
431 if (CHECK_FLAG(q.fFlags, QUOTE_FLAG_SAID)) return;
432
433 SGP::Buffer<NPCQuoteInfo> new_quotes(LoadQuoteFile(ubNPC));
434 if (!new_quotes) return;
435 q = new_quotes[record];
436 }
437
438
439 //
440 // CIV QUOTE LOW LEVEL ROUTINES
441 //
442
443
LoadCivQuoteFile(UINT8 const idx)444 static NPCQuoteInfo* LoadCivQuoteFile(UINT8 const idx)
445 {
446 char const* filename;
447 char buf[255];
448 if (idx == MINERS_CIV_QUOTE_INDEX)
449 {
450 filename = NPCDATADIR "/miners.npc";
451 }
452 else
453 {
454 sprintf(buf, NPCDATADIR "/%c%d.npc", 'A' + gsCivQuoteSector[idx][1] - 1, gsCivQuoteSector[idx][0]);
455 filename = buf;
456 }
457 AutoSGPFile f(GCM->openGameResForReading(filename));
458 return ExtractNPCQuoteInfoArrayFromFile(f);
459 }
460
461
EnsureCivQuoteFileLoaded(UINT8 const idx)462 static NPCQuoteInfo* EnsureCivQuoteFileLoaded(UINT8 const idx)
463 try
464 {
465 NPCQuoteInfo*& q = gpCivQuoteInfoArray[idx];
466 if (!q) q = LoadCivQuoteFile(idx);
467 return q;
468 }
469 catch (...) { return 0; }
470
471
ReloadCivQuoteFileIfLoaded(UINT8 const idx)472 static bool ReloadCivQuoteFileIfLoaded(UINT8 const idx)
473 try
474 {
475 NPCQuoteInfo*& q = gpCivQuoteInfoArray[idx];
476 if (!q) return true;
477 FreeNullArray(q);
478 q = LoadCivQuoteFile(idx);
479 return true;
480 }
481 catch (...) { return false; }
482
483
ShutdownNPCQuotes()484 void ShutdownNPCQuotes()
485 {
486 FOR_EACH(NPCQuoteInfo*, i, gpNPCQuoteInfoArray) FreeNullArray(*i);
487 FOR_EACH(NPCQuoteInfo*, i, gpBackupNPCQuoteInfoArray) FreeNullArray(*i);
488 FOR_EACH(NPCQuoteInfo*, i, gpCivQuoteInfoArray) FreeNullArray(*i);
489 }
490
491
492 //
493 // GENERAL LOW LEVEL ROUTINES
494 //
495
ReloadAllQuoteFiles(void)496 void ReloadAllQuoteFiles(void)
497 {
498 for (const MercProfile* p : GCM->listMercProfiles())
499 {
500 if (!p->isNPCorRPC()) continue;
501
502 // zap backup if any
503 FreeNullArray(gpBackupNPCQuoteInfoArray[p->getID()]);
504 ReloadQuoteFileIfLoaded(p->getID());
505 }
506 // reload all civ quote files
507 for (UINT8 ubLoop = 0; ubLoop < NUM_CIVQUOTE_SECTORS; ubLoop++ )
508 {
509 ReloadCivQuoteFileIfLoaded( ubLoop );
510 }
511 }
512
513
514 //
515 // THE REST
516 //
517
518
SetQuoteRecordAsUsed(UINT8 ubNPC,UINT8 ubRecord)519 void SetQuoteRecordAsUsed( UINT8 ubNPC, UINT8 ubRecord )
520 {
521 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
522 if (!quotes) return;
523 quotes[ubRecord].fFlags |= QUOTE_FLAG_SAID;
524 }
525
526
CalcThreateningEffectiveness(UINT8 const ubMerc)527 static INT32 CalcThreateningEffectiveness(UINT8 const ubMerc)
528 {
529 // effective threat is 1/3 strength, 1/3 weapon deadliness, 1/3 leadership
530
531 SOLDIERTYPE* const s = FindSoldierByProfileIDOnPlayerTeam(ubMerc);
532 if (!s) return 0;
533
534 UINT16 const item_idx = s->inv[HANDPOS].usItem;
535 INT32 deadliness =
536 GCM->getItem(item_idx)->isWeapon() ? GCM->getWeapon(item_idx)->ubDeadliness :
537 0;
538
539 if (deadliness == 0) deadliness = -30; // penalize!
540
541 INT32 const strength = EffectiveStrength(s);
542 return (EffectiveLeadership(s) + strength + deadliness) / 2;
543 }
544
545
CalcDesireToTalk(UINT8 const ubNPC,UINT8 const ubMerc,Approach const bApproach)546 UINT8 CalcDesireToTalk(UINT8 const ubNPC, UINT8 const ubMerc, Approach const bApproach)
547 {
548 INT32 iWillingness;
549 INT32 iPersonalVal, iTownVal, iApproachVal;
550 INT32 iEffectiveLeadership;
551
552 MERCPROFILESTRUCT const& pNPCProfile = GetProfile(ubNPC);
553 MERCPROFILESTRUCT const& pMercProfile = GetProfile(ubMerc);
554
555 iPersonalVal = 50 + pNPCProfile.bMercOpinion[ubMerc];
556
557 // ARM: NOTE - for towns which don't use loyalty (San Mona, Estoni, Tixa, Orta )
558 // loyalty will always remain 0 (this was OKed by Ian)
559 iTownVal = gTownLoyalty[pNPCProfile.bTown].ubRating;
560 iTownVal = iTownVal * pNPCProfile.bTownAttachment / 100;
561
562 if (bApproach == NPC_INITIATING_CONV || bApproach == APPROACH_GIVINGITEM )
563 {
564 iApproachVal = 100;
565 }
566 else if ( bApproach == APPROACH_THREATEN )
567 {
568 iEffectiveLeadership = CalcThreateningEffectiveness(ubMerc) * pMercProfile.usApproachFactor[bApproach - 1] / 100;
569 iApproachVal = pNPCProfile.ubApproachVal[bApproach - 1] * iEffectiveLeadership / 50;
570 }
571 else
572 {
573 iEffectiveLeadership = (INT32)pMercProfile.bLeadership * pMercProfile.usApproachFactor[bApproach - 1] / 100;
574 iApproachVal = pNPCProfile.ubApproachVal[bApproach - 1] * iEffectiveLeadership / 50;
575 }
576 // NB if town attachment is less than 100% then we should make personal value proportionately more important!
577 if (pNPCProfile.bTownAttachment < 100)
578 {
579 iPersonalVal = iPersonalVal * (100 + (100 - pNPCProfile.bTownAttachment) ) / 100;
580 }
581 iWillingness = (iPersonalVal / 2 + iTownVal / 2) * iApproachVal / 100;
582
583 if (bApproach == NPC_INITIATING_CONV)
584 {
585 iWillingness -= INITIATING_FACTOR;
586 }
587
588 if (iWillingness < 0)
589 {
590 iWillingness = 0;
591 }
592
593 return( (UINT8) iWillingness );
594 }
595
596
ApproachedForFirstTime(MERCPROFILESTRUCT & pNPCProfile,Approach const bApproach)597 static void ApproachedForFirstTime(MERCPROFILESTRUCT& pNPCProfile, Approach const bApproach)
598 {
599 UINT8 ubLoop;
600 UINT32 uiTemp;
601
602 pNPCProfile.bApproached |= gbFirstApproachFlags[bApproach - 1];
603 for (ubLoop = 1; ubLoop <= NUM_REAL_APPROACHES; ubLoop++)
604 {
605 uiTemp = (UINT32) pNPCProfile.ubApproachVal[ubLoop - 1] * (UINT32) pNPCProfile.ubApproachMod[bApproach - 1][ubLoop - 1] / 100;
606 if (uiTemp > 255)
607 {
608 uiTemp = 255;
609 }
610 pNPCProfile.ubApproachVal[ubLoop-1] = (UINT8) uiTemp;
611 }
612 }
613
614
615 static UINT8 NPCConsiderQuote(UINT8 ubNPC, UINT8 ubMerc, Approach, UINT8 ubQuoteNum, UINT8 ubTalkDesire, NPCQuoteInfo* pNPCQuoteInfoArray);
616
617
NPCConsiderTalking(UINT8 const ubNPC,UINT8 const ubMerc,Approach const approach,UINT8 const record,NPCQuoteInfo * const pNPCQuoteInfoArray,NPCQuoteInfo ** const ppResultQuoteInfo,UINT8 * const pubQuoteNum)618 static UINT8 NPCConsiderTalking(UINT8 const ubNPC, UINT8 const ubMerc, Approach const approach, UINT8 const record, NPCQuoteInfo* const pNPCQuoteInfoArray, NPCQuoteInfo** const ppResultQuoteInfo, UINT8* const pubQuoteNum)
619 {
620 // This function returns the opinion level required of the "most difficult" quote
621 // that the NPC is willing to say to the merc. It can also provide the quote #.
622 SOLDIERTYPE const* const s = FindSoldierByProfileID(ubNPC);
623 if (!s) return 0;
624
625 if (ppResultQuoteInfo) *ppResultQuoteInfo = 0;
626 if (pubQuoteNum) *pubQuoteNum = 0;
627
628 UINT8 talk_desire = 0;
629 if (approach <= NUM_REAL_APPROACHES)
630 {
631 MERCPROFILESTRUCT& p = GetProfile(ubNPC);
632 // What's our willingness to divulge?
633 talk_desire = CalcDesireToTalk(ubNPC, ubMerc, approach);
634 if (approach < NUM_REAL_APPROACHES && !(p.bApproached & gbFirstApproachFlags[approach - 1]))
635 {
636 ApproachedForFirstTime(p, approach);
637 }
638 }
639 else if (ubNPC == PABLO && approach == APPROACH_SECTOR_NOT_SAFE) // for Pablo, consider as threaten
640 {
641 MERCPROFILESTRUCT& p = GetProfile(ubNPC);
642 // What's our willingness to divulge?
643 talk_desire = CalcDesireToTalk(ubNPC, ubMerc, APPROACH_THREATEN);
644 if (p.bApproached & gbFirstApproachFlags[APPROACH_THREATEN - 1])
645 {
646 ApproachedForFirstTime(p, APPROACH_THREATEN);
647 }
648 }
649
650 UINT8 first_quote_record;
651 UINT8 last_quote_record;
652 switch (approach)
653 {
654 case TRIGGER_NPC:
655 first_quote_record = record;
656 last_quote_record = record;
657 break;
658
659 default:
660 first_quote_record = 0;
661 last_quote_record = NUM_NPC_QUOTE_RECORDS - 1;
662 break;
663 }
664
665 bool fQuoteFound = false;
666 UINT8 ubHighestOpinionRequired = 0;
667 NPCQuoteInfo* pNPCQuoteInfo = 0;
668 UINT8 ubQuote = 0;
669 for (UINT8 i = first_quote_record; i <= last_quote_record; ++i)
670 {
671 pNPCQuoteInfo = &pNPCQuoteInfoArray[i];
672
673 // Check if we have the item / are in right spot
674 if (pNPCQuoteInfo->sRequiredItem > 0)
675 {
676 if (FindObjectInSoldierProfile(GetProfile(ubNPC), pNPCQuoteInfo->sRequiredItem) == NO_SLOT)
677 {
678 continue;
679 }
680 }
681 else if (pNPCQuoteInfo->sRequiredGridno < 0)
682 {
683 if (s->sGridNo != -pNPCQuoteInfo->sRequiredGridno)
684 {
685 continue;
686 }
687 }
688
689 if (!NPCConsiderQuote(ubNPC, ubMerc, approach, i, talk_desire, pNPCQuoteInfoArray)) continue;
690
691 if (approach != NPC_INITIATING_CONV)
692 {
693 // we do have a quote to say, and we want to say this one right away!
694 if (ppResultQuoteInfo) *ppResultQuoteInfo = pNPCQuoteInfo;
695 if (pubQuoteNum) *pubQuoteNum = i;
696 return pNPCQuoteInfo->ubOpinionRequired;
697 }
698
699 // want to find the quote with the highest required opinion rating that we're willing
700 // to say
701 if (pNPCQuoteInfo->ubOpinionRequired > ubHighestOpinionRequired)
702 {
703 fQuoteFound = true;
704 ubHighestOpinionRequired = pNPCQuoteInfo->ubOpinionRequired;
705 ubQuote = pNPCQuoteInfo->ubQuoteNum;
706 }
707 }
708
709 // Whew, checked them all. If we found a quote, return the appropriate values.
710 if (fQuoteFound)
711 {
712 if (ppResultQuoteInfo) *ppResultQuoteInfo = pNPCQuoteInfo;
713 if (pubQuoteNum) *pubQuoteNum = ubQuote;
714 return ubHighestOpinionRequired;
715 }
716 else
717 {
718 if (ppResultQuoteInfo) *ppResultQuoteInfo = 0;
719 if (pubQuoteNum) *pubQuoteNum = 0;
720 return 0;
721 }
722 }
723
724
UseQuote(NPCQuoteInfo * const quotes,NPCQuoteInfo ** const quote,UINT8 * const quote_id,UINT8 const quote_to_use)725 static UINT8 UseQuote(NPCQuoteInfo* const quotes, NPCQuoteInfo** const quote, UINT8* const quote_id, UINT8 const quote_to_use)
726 {
727 *quote = "es[quote_to_use];
728 *quote_id = quote_to_use;
729 return quotes[quote_to_use].ubOpinionRequired;
730 }
731
732
733 static UINT8 HandleNPCBeingGivenMoneyByPlayer(UINT8 ubNPC, UINT32 uiMoneyAmount);
734
735
NPCConsiderReceivingItemFromMerc(UINT8 const ubNPC,UINT8 const ubMerc,OBJECTTYPE const * const o,NPCQuoteInfo * const pNPCQuoteInfoArray,NPCQuoteInfo ** const ppResultQuoteInfo,UINT8 * const pubQuoteNum)736 static UINT8 NPCConsiderReceivingItemFromMerc(UINT8 const ubNPC, UINT8 const ubMerc, OBJECTTYPE const* const o, NPCQuoteInfo* const pNPCQuoteInfoArray, NPCQuoteInfo** const ppResultQuoteInfo, UINT8* const pubQuoteNum)
737 {
738 // This function returns the opinion level required of the "most difficult" quote
739 // that the NPC is willing to say to the merc. It can also provide the quote #.
740 *ppResultQuoteInfo = 0;
741 *pubQuoteNum = 0;
742
743 // don't accept any items when we are the player's enemy
744 if (CheckFact(FACT_NPC_IS_ENEMY, ubNPC) && ubNPC != JOE) return 0;
745
746 // How much do we want to talk with this merc?
747 UINT8 const ubTalkDesire = CalcDesireToTalk(ubNPC, ubMerc, APPROACH_GIVINGITEM);
748
749 UINT16 item_to_consider = o->usItem;
750 if (GCM->getItem(item_to_consider)->getItemClass() == IC_GUN && item_to_consider != ROCKET_LAUNCHER)
751 {
752 UINT8 const weapon_class = GCM->getWeapon(item_to_consider)->ubWeaponClass;
753 if (weapon_class == RIFLECLASS || weapon_class == MGCLASS)
754 {
755 item_to_consider = ANY_RIFLE; // treat all rifles the same
756 }
757 }
758 switch (item_to_consider)
759 {
760 case HEAD_2:
761 case HEAD_3:
762 //case HEAD_4: // NOT Slay's head; it's different
763 case HEAD_5:
764 case HEAD_6:
765 case HEAD_7:
766 // all treated the same in the NPC code
767 item_to_consider = HEAD_2;
768 break;
769
770 case MONEY:
771 case SILVER:
772 case GOLD:
773 {
774 Fact const fact =
775 o->uiMoneyAmount < LARGE_AMOUNT_MONEY ? FACT_SMALL_AMOUNT_OF_MONEY :
776 FACT_LARGE_AMOUNT_OF_MONEY;
777 SetFactTrue(fact);
778 item_to_consider = MONEY;
779 break;
780 }
781
782 case WINE:
783 case BEER:
784 item_to_consider = ALCOHOL;
785 break;
786
787 default:
788 break;
789 }
790
791 if (o->bStatus[0] < 50)
792 {
793 SetFactTrue(FACT_ITEM_POOR_CONDITION);
794 }
795 else
796 {
797 SetFactFalse(FACT_ITEM_POOR_CONDITION);
798 }
799
800 for (UINT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
801 {
802 NPCQuoteInfo& q = pNPCQuoteInfoArray[i];
803
804 // First see if we want that item....
805 INT16 const req_item = q.sRequiredItem;
806 if (req_item <= 0) continue;
807 if (req_item != item_to_consider && req_item != ACCEPT_ANY_ITEM) continue;
808
809 // Now see if everyhting else is OK
810 if (!NPCConsiderQuote(ubNPC, ubMerc, APPROACH_GIVINGITEM, i, ubTalkDesire, pNPCQuoteInfoArray)) continue;
811
812 switch (ubNPC)
813 {
814 case DARREN:
815 if (item_to_consider == MONEY && q.sActionData == NPC_ACTION_DARREN_GIVEN_CASH)
816 {
817 if (o->uiMoneyAmount < 1000)
818 {
819 // refuse, bet too low - record 15
820 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 15);
821 }
822 else if (o->uiMoneyAmount > 5000)
823 {
824 // refuse, bet too high - record 16
825 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 16);
826 }
827 else
828 {
829 // record amount of bet
830 gMercProfiles[DARREN].iBalance = o->uiMoneyAmount;
831 SetFactFalse(FACT_DARREN_EXPECTING_MONEY);
832
833 // if never fought before, use record 17
834 // if fought before, today, use record 31
835 // else use record 18
836 if (!(gpNPCQuoteInfoArray[DARREN][17].fFlags & QUOTE_FLAG_SAID)) // record 17 not used
837 {
838 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 17);
839 }
840 else
841 {
842 // find Kingpin, if he's in his house, invoke the script to move him to the bar
843 UINT8 id = 31;
844 SOLDIERTYPE const* const kingpin = FindSoldierByProfileID(KINGPIN);
845 if (kingpin)
846 {
847 UINT8 const room = GetRoom(kingpin->sGridNo);
848 // first boxer, bring kingpin over
849 if (IN_KINGPIN_HOUSE(room)) id = 18;
850 }
851
852 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, id);
853 }
854 }
855 }
856 break;
857
858 case ANGEL:
859 if (item_to_consider == MONEY && q.sActionData == NPC_ACTION_ANGEL_GIVEN_CASH)
860 {
861 if (o->uiMoneyAmount < GCM->getItem(LEATHER_JACKET_W_KEVLAR)->getPrice())
862 { // refuse, bet too low - record 8
863 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 8);
864 }
865 else if (o->uiMoneyAmount > GCM->getItem(LEATHER_JACKET_W_KEVLAR)->getPrice())
866 { // refuse, bet too high - record 9
867 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 9);
868 }
869 else
870 { // accept - record 10
871 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 10);
872 }
873 }
874 break;
875
876 case MADAME:
877 if (item_to_consider == MONEY)
878 {
879 if (GetProfile(ubMerc).bSex == FEMALE)
880 {
881 // say quote about not catering to women!
882 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 5);
883 }
884
885 switch (o->uiMoneyAmount)
886 {
887 case 100:
888 case 200: // Carla
889 if (!CheckFact(FACT_CARLA_AVAILABLE, 0)) goto madame_default;
890 gMercProfiles[MADAME].bNPCData += (INT8)(o->uiMoneyAmount / 100);
891 TriggerNPCRecord(MADAME, 16);
892 break;
893
894 case 500:
895 case 1000: // Cindy
896 if (!CheckFact(FACT_CINDY_AVAILABLE, 0)) goto madame_default;
897 gMercProfiles[MADAME].bNPCData += (INT8)(o->uiMoneyAmount / 500);
898 TriggerNPCRecord(MADAME, 17);
899 break;
900
901 case 300:
902 case 600: // Bambi
903 if (!CheckFact(FACT_BAMBI_AVAILABLE, 0)) goto madame_default;
904 gMercProfiles[MADAME].bNPCData += (INT8)(o->uiMoneyAmount / 300);
905 TriggerNPCRecord(MADAME, 18);
906 break;
907
908 case 400:
909 case 800: // Maria
910 if (gubQuest[QUEST_RESCUE_MARIA] != QUESTINPROGRESS) goto madame_default;
911 gMercProfiles[MADAME].bNPCData += (INT8)(o->uiMoneyAmount / 400);
912 TriggerNPCRecord(MADAME, 19);
913 break;
914
915 default:
916 madame_default:
917 // play quotes 39-42 (plus 44 if quest 22 on) plus 43 if >1 PC
918 // and return money
919 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, 25);
920 }
921 }
922 break;
923
924 case JOE:
925 if (item_to_consider == MONEY && q.sActionData != NPC_ACTION_JOE_GIVEN_CASH)
926 {
927 break;
928 }
929 goto check_give_money;
930
931 case GERARD:
932 if (item_to_consider == MONEY && q.sActionData != NPC_ACTION_GERARD_GIVEN_CASH)
933 {
934 break;
935 }
936 goto check_give_money;
937
938 case STEVE:
939 case VINCE:
940 case WALTER:
941 case FRANK:
942 check_give_money:
943 if (item_to_consider == MONEY)
944 {
945 if (ubNPC == VINCE || ubNPC == STEVE)
946 {
947 if (!CheckFact(FACT_VINCE_EXPECTING_MONEY, ubNPC) &&
948 gMercProfiles[ubNPC].iBalance < 0 &&
949 q.sActionData != NPC_ACTION_DONT_ACCEPT_ITEM)
950 {
951 MERCPROFILESTRUCT& p = GetProfile(ubNPC);
952 // increment balance
953 p.iBalance += (INT32)o->uiMoneyAmount;
954 p.uiTotalCostToDate += o->uiMoneyAmount;
955 if (p.iBalance > 0) p.iBalance = 0;
956 ScreenMsg(FONT_YELLOW, MSG_INTERFACE, st_format_printf(TacticalStr[BALANCE_OWED_STR], p.zNickname, -p.iBalance));
957 }
958 else if (!CheckFact(FACT_VINCE_EXPECTING_MONEY, ubNPC) &&
959 q.sActionData != NPC_ACTION_DONT_ACCEPT_ITEM)
960 {
961 // just accept cash!
962 if (ubNPC == VINCE)
963 {
964 *ppResultQuoteInfo = &pNPCQuoteInfoArray[8];
965 }
966 else
967 {
968 *ppResultQuoteInfo = &pNPCQuoteInfoArray[7];
969 }
970 return (*ppResultQuoteInfo)->ubOpinionRequired;
971 }
972 else
973 {
974 // handle the player giving NPC some money
975 UINT8 const quote_id = HandleNPCBeingGivenMoneyByPlayer(ubNPC, o->uiMoneyAmount);
976 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, quote_id);
977 }
978 }
979 else
980 {
981 // handle the player giving NPC some money
982 UINT8 const quote_id = HandleNPCBeingGivenMoneyByPlayer(ubNPC, o->uiMoneyAmount);
983 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, quote_id);
984 }
985 }
986 break;
987
988 case KINGPIN:
989 if (item_to_consider == MONEY && gubQuest[QUEST_KINGPIN_MONEY] == QUESTINPROGRESS)
990 {
991 UINT8 const quote_id = HandleNPCBeingGivenMoneyByPlayer(ubNPC, o->uiMoneyAmount);
992 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, quote_id);
993 }
994 break;
995
996 default:
997 if (item_to_consider == MONEY && (ubNPC == SKYRIDER || (ubNPC != NO_PROFILE && MercProfile(ubNPC).isRPC())))
998 {
999 MERCPROFILESTRUCT& p = GetProfile(ubNPC);
1000 if (p.iBalance < 0 && q.sActionData != NPC_ACTION_DONT_ACCEPT_ITEM)
1001 {
1002 // increment balance
1003 p.iBalance += (INT32)o->uiMoneyAmount;
1004 p.uiTotalCostToDate += o->uiMoneyAmount;
1005 if (p.iBalance > 0) p.iBalance = 0;
1006 ScreenMsg(FONT_YELLOW, MSG_INTERFACE, st_format_printf(TacticalStr[BALANCE_OWED_STR], p.zNickname, -p.iBalance));
1007 }
1008 }
1009 break;
1010 }
1011 // This is great!
1012 // Return desire value
1013 return UseQuote(pNPCQuoteInfoArray, ppResultQuoteInfo, pubQuoteNum, i);
1014 }
1015
1016 return 0;
1017 }
1018
1019
1020 // handle money being npc being
HandleNPCBeingGivenMoneyByPlayer(UINT8 const ubNPC,UINT32 const uiMoneyAmount)1021 static UINT8 HandleNPCBeingGivenMoneyByPlayer(UINT8 const ubNPC, UINT32 const uiMoneyAmount)
1022 {
1023 UINT8 quote_id=0; // Wmaybe-uninitialized
1024 switch( ubNPC )
1025 {
1026 // handle for STEVE and VINCE
1027 case STEVE:
1028 case VINCE:
1029 {
1030 INT32 iCost;
1031
1032 iCost = (INT32) CalcMedicalCost( ubNPC );
1033
1034 // check amount of money
1035 if ( (INT32)uiMoneyAmount + giHospitalTempBalance + giHospitalRefund >= iCost )
1036 {
1037 // enough cash, check how much help is needed
1038 if( CheckFact( FACT_WOUNDED_MERCS_NEARBY , ubNPC) )
1039 {
1040 quote_id = 26;
1041 }
1042 else if( CheckFact( FACT_ONE_WOUNDED_MERC_NEARBY, ubNPC ) )
1043 {
1044 quote_id = 25;
1045 }
1046
1047 if ( giHospitalRefund > 0 )
1048 {
1049 giHospitalRefund = __max( 0, giHospitalRefund - iCost + uiMoneyAmount );
1050 }
1051 giHospitalTempBalance = 0;
1052 }
1053 else
1054 {
1055 ST::string sTempString = ST::format("${}", iCost - uiMoneyAmount - giHospitalTempBalance);
1056
1057 // not enough cash
1058 ScreenMsg( FONT_MCOLOR_LTYELLOW,
1059 MSG_INTERFACE,
1060 st_format_printf(g_langRes->Message[ STR_NEED_TO_GIVE_MONEY ],
1061 gMercProfiles[ ubNPC ].zNickname,
1062 sTempString) );
1063 quote_id = 27;
1064 giHospitalTempBalance += uiMoneyAmount;
1065 }
1066 }
1067 break;
1068 case KINGPIN:
1069 if ( (INT32) uiMoneyAmount < -gMercProfiles[ KINGPIN ].iBalance )
1070 {
1071 quote_id = 9;
1072 }
1073 else
1074 {
1075 quote_id = 10;
1076 }
1077 gMercProfiles[ KINGPIN ].iBalance += (INT32) uiMoneyAmount;
1078 break;
1079 case WALTER:
1080 if ( gMercProfiles[ WALTER ].iBalance == 0 )
1081 {
1082 quote_id = 12;
1083 }
1084 else
1085 {
1086 quote_id = 13;
1087 }
1088 gMercProfiles[ WALTER ].iBalance += uiMoneyAmount;
1089 break;
1090 case FRANK:
1091 gArmsDealerStatus[ ARMS_DEALER_FRANK ].uiArmsDealersCash += uiMoneyAmount;
1092 quote_id = 0;
1093 break;
1094
1095 case GERARD:
1096 gMercProfiles[ GERARD ].iBalance += uiMoneyAmount;
1097 if ( (gMercProfiles[ GERARD ].iBalance) >= 10000 )
1098 {
1099 quote_id = 12;
1100 }
1101 else
1102 {
1103 quote_id = 11;
1104 }
1105 break;
1106 case JOE:
1107 gMercProfiles[ JOE ].iBalance += uiMoneyAmount;
1108 if ( (gMercProfiles[ JOE ].iBalance) >= 10000 )
1109 {
1110 quote_id = 7;
1111 }
1112 else
1113 {
1114 quote_id = 6;
1115 }
1116 break;
1117
1118 default:
1119 quote_id = 0;
1120 }
1121 return quote_id;
1122 }
1123
1124
NPCConsiderQuote(UINT8 const ubNPC,UINT8 const ubMerc,Approach const ubApproach,UINT8 const ubQuoteNum,UINT8 const ubTalkDesire,NPCQuoteInfo * const pNPCQuoteInfoArray)1125 static UINT8 NPCConsiderQuote(UINT8 const ubNPC, UINT8 const ubMerc, Approach const ubApproach, UINT8 const ubQuoteNum, UINT8 const ubTalkDesire, NPCQuoteInfo* const pNPCQuoteInfoArray)
1126 {
1127 //This function looks at a quote and determines if conditions for it have been met.
1128 // Returns 0 if none , 1 if one is found
1129 NPCQuoteInfo * pNPCQuoteInfo;
1130 UINT32 uiDay;
1131 BOOLEAN fTrue;
1132
1133 MERCPROFILESTRUCT const* const pNPCProfile = ubNPC != NO_PROFILE ? &GetProfile(ubNPC) : 0;
1134
1135 // How much do we want to talk with this merc?
1136 uiDay = GetWorldDay();
1137
1138 pNPCQuoteInfo = &(pNPCQuoteInfoArray[ubQuoteNum]);
1139
1140 if (ubApproach != NPC_INITIATING_CONV && ubMerc != NO_PROFILE)
1141 {
1142 SLOGD("New Approach for NPC ID: %d '%s' against Merc: %d '%s'\n\
1143 \tTesting Record #: %d",
1144 ubNPC, GetProfile(ubNPC).zNickname.c_str(), ubMerc, GetProfile(ubMerc).zNickname.c_str(), ubQuoteNum);
1145 }
1146
1147 if (CHECK_FLAG( pNPCQuoteInfo->fFlags, QUOTE_FLAG_SAID ))
1148 {
1149 if (ubApproach != NPC_INITIATING_CONV)
1150 {
1151 SLOGD("Quote Already Said, leaving");
1152 }
1153 // skip quotes already said
1154 return( FALSE );
1155 }
1156
1157 // if the quote is quest-specific, is the player on that quest?
1158 if (pNPCQuoteInfo->ubQuest != NO_QUEST)
1159 {
1160 if (pNPCQuoteInfo->ubQuest > QUEST_DONE_NUM && (pNPCQuoteInfo->ubQuest - QUEST_DONE_NUM) < MAX_QUESTS)
1161 {
1162 if (gubQuest[pNPCQuoteInfo->ubQuest - QUEST_DONE_NUM] != QUESTDONE)
1163 {
1164 return( FALSE );
1165 }
1166 }
1167 else if (pNPCQuoteInfo->ubQuest > QUEST_NOT_STARTED_NUM && (pNPCQuoteInfo->ubQuest - QUEST_NOT_STARTED_NUM) < MAX_QUESTS)
1168 {
1169 if (gubQuest[pNPCQuoteInfo->ubQuest - QUEST_NOT_STARTED_NUM] != QUESTNOTSTARTED)
1170 {
1171 return( FALSE );
1172 }
1173 }
1174 else if (pNPCQuoteInfo->ubQuest < MAX_QUESTS)
1175 {
1176 if (gubQuest[pNPCQuoteInfo->ubQuest] != QUESTINPROGRESS)
1177 {
1178 return( FALSE );
1179 }
1180 }
1181 else
1182 {
1183 ST::string err = ST::format("invalid quest index: {}", pNPCQuoteInfo->ubQuest);
1184 throw std::runtime_error(err.to_std_string());
1185 }
1186 }
1187
1188 // if there are facts to be checked, check them
1189 if (pNPCQuoteInfo->usFactMustBeTrue != NO_FACT)
1190 {
1191 fTrue = CheckFact((Fact)pNPCQuoteInfo->usFactMustBeTrue, ubNPC);
1192 if (ubApproach != NPC_INITIATING_CONV)
1193 {
1194 SLOGD("Fact (%d:'%s') Must be True, status is %s",
1195 pNPCQuoteInfo->usFactMustBeTrue, FactDescText[pNPCQuoteInfo->usFactMustBeTrue].c_str(),
1196 fTrue ? "True" : "False, returning");
1197 }
1198 if (!fTrue) return FALSE;
1199 }
1200
1201 if (pNPCQuoteInfo->usFactMustBeFalse != NO_FACT)
1202 {
1203 fTrue = CheckFact((Fact)pNPCQuoteInfo->usFactMustBeFalse, ubNPC);
1204 if (ubApproach != NPC_INITIATING_CONV)
1205 {
1206 SLOGD("Fact(%d:'%s') Must be False status is %s",
1207 pNPCQuoteInfo->usFactMustBeFalse, FactDescText[pNPCQuoteInfo->usFactMustBeFalse].c_str(),
1208 (fTrue == TRUE) ? "True, return" : "FALSE" );
1209 }
1210 if (fTrue) return( FALSE );
1211 }
1212
1213 // check for required approach
1214 // since the "I hate you" code triggers the record, triggering has to work properly
1215 // with the other value that is stored!
1216 if ( pNPCQuoteInfo->ubApproachRequired || !(ubApproach == APPROACH_FRIENDLY || ubApproach == APPROACH_DIRECT || ubApproach == TRIGGER_NPC ) )
1217 {
1218 if (ubApproach != NPC_INITIATING_CONV)
1219 {
1220 SLOGD("Approach Taken(%d) must equal required Approach(%d) = %s",
1221 ubApproach, pNPCQuoteInfo->ubApproachRequired,
1222 (ubApproach != pNPCQuoteInfo->ubApproachRequired) ? "TRUE, return" : "FALSE" );
1223 }
1224 if ( pNPCQuoteInfo->ubApproachRequired == APPROACH_ONE_OF_FOUR_STANDARD )
1225 {
1226 // friendly to recruit will match
1227 if ( ubApproach < APPROACH_FRIENDLY || ubApproach > APPROACH_RECRUIT )
1228 {
1229 return( FALSE );
1230 }
1231 }
1232 else if ( pNPCQuoteInfo->ubApproachRequired == APPROACH_FRIENDLY_DIRECT_OR_RECRUIT )
1233 {
1234 if ( ubApproach != APPROACH_FRIENDLY && ubApproach != APPROACH_DIRECT && ubApproach != APPROACH_RECRUIT )
1235 {
1236 return( FALSE );
1237 }
1238 }
1239 else if (ubApproach != pNPCQuoteInfo->ubApproachRequired)
1240 {
1241 return( FALSE );
1242 }
1243 }
1244
1245 // check time constraints on the quotes
1246 if (pNPCProfile != NULL && pNPCQuoteInfo->ubFirstDay == MUST_BE_NEW_DAY)
1247 {
1248 if (ubApproach != NPC_INITIATING_CONV)
1249 {
1250 SLOGD("Time constraints. Current Day(%d) must <= Day last spoken too (%d) : %s",
1251 uiDay, pNPCProfile->ubLastDateSpokenTo,
1252 (uiDay <= pNPCProfile->ubLastDateSpokenTo) ? "TRUE, return" : "FALSE" );
1253 }
1254
1255 if (uiDay <= pNPCProfile->ubLastDateSpokenTo)
1256 {
1257 // too early!
1258 return( FALSE );
1259 }
1260 }
1261 else if (uiDay < pNPCQuoteInfo->ubFirstDay)
1262 {
1263 if (ubApproach != NPC_INITIATING_CONV)
1264 {
1265 SLOGD("Current Day(%d) is before Required first day(%d) = %s",
1266 uiDay, pNPCQuoteInfo->ubFirstDay,
1267 (uiDay < pNPCQuoteInfo->ubFirstDay) ? "False, returning" : "True" );
1268 }
1269 // too early!
1270 return( FALSE );
1271 }
1272
1273 if (uiDay > pNPCQuoteInfo->ubLastDay && uiDay < 255 )
1274 {
1275 if (ubApproach != NPC_INITIATING_CONV)
1276 {
1277 SLOGD("Current Day(%d) is after Required first day(%d) = %s",
1278 uiDay, pNPCQuoteInfo->ubFirstDay,
1279 (uiDay > pNPCQuoteInfo->ubLastDay) ? "TRUE, returning" : "FALSE" );
1280 }
1281 // too late!
1282 return( FALSE );
1283 }
1284
1285 // check opinion required
1286 if ((pNPCQuoteInfo->ubOpinionRequired != IRRELEVANT) && (ubApproach != TRIGGER_NPC))
1287 {
1288 if (ubApproach != NPC_INITIATING_CONV)
1289 {
1290 SLOGD("Opinion Required. Talk Desire (%d), Opinion Required(%d) : %s",
1291 ubTalkDesire, pNPCQuoteInfo->ubOpinionRequired,
1292 (ubTalkDesire < pNPCQuoteInfo->ubOpinionRequired) ? "False, return" : "False, continue" );
1293 }
1294
1295 if (ubTalkDesire < pNPCQuoteInfo->ubOpinionRequired )
1296 {
1297 return( FALSE );
1298 }
1299 }
1300
1301 if (ubApproach != NPC_INITIATING_CONV)
1302 {
1303 SLOGD("Return the quote opinion value! = TRUE");
1304 }
1305 // Return the quote opinion value!
1306 return( TRUE );
1307
1308 }
1309
1310
ReplaceLocationInNPCData(NPCQuoteInfo * pNPCQuoteInfoArray,INT16 sOldGridNo,INT16 sNewGridNo)1311 static void ReplaceLocationInNPCData(NPCQuoteInfo* pNPCQuoteInfoArray, INT16 sOldGridNo, INT16 sNewGridNo)
1312 {
1313 UINT8 ubFirstQuoteRecord, ubLastQuoteRecord, ubLoop;
1314 NPCQuoteInfo * pNPCQuoteInfo;
1315
1316 ubFirstQuoteRecord = 0;
1317 ubLastQuoteRecord = NUM_NPC_QUOTE_RECORDS - 1;
1318 for (ubLoop = ubFirstQuoteRecord; ubLoop <= ubLastQuoteRecord; ubLoop++)
1319 {
1320 pNPCQuoteInfo = &(pNPCQuoteInfoArray[ ubLoop ]);
1321 if (sOldGridNo == -pNPCQuoteInfo->sRequiredGridno)
1322 {
1323 pNPCQuoteInfo->sRequiredGridno = -sNewGridNo;
1324 }
1325 if (sOldGridNo == pNPCQuoteInfo->usGoToGridno)
1326 {
1327 pNPCQuoteInfo->usGoToGridno = sNewGridNo;
1328 }
1329 }
1330 }
1331
1332
ReplaceLocationInNPCDataFromProfileID(UINT8 const ubNPC,INT16 const sOldGridNo,INT16 const sNewGridNo)1333 void ReplaceLocationInNPCDataFromProfileID(UINT8 const ubNPC, INT16 const sOldGridNo, INT16 const sNewGridNo)
1334 {
1335 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
1336 if (!quotes) return; // error
1337 ReplaceLocationInNPCData(quotes, sOldGridNo, sNewGridNo);
1338 }
1339
1340
ResetOncePerConvoRecords(NPCQuoteInfo * pNPCQuoteInfoArray)1341 static void ResetOncePerConvoRecords(NPCQuoteInfo* pNPCQuoteInfoArray)
1342 {
1343 UINT8 ubLoop;
1344
1345 for ( ubLoop = 0; ubLoop < NUM_NPC_QUOTE_RECORDS; ubLoop++ )
1346 {
1347 if ( CHECK_FLAG(pNPCQuoteInfoArray[ubLoop].fFlags, QUOTE_FLAG_SAY_ONCE_PER_CONVO) )
1348 {
1349 TURN_FLAG_OFF( pNPCQuoteInfoArray[ubLoop].fFlags, QUOTE_FLAG_SAID );
1350 }
1351 }
1352 }
1353
1354
ResetOncePerConvoRecordsForNPC(UINT8 ubNPC)1355 void ResetOncePerConvoRecordsForNPC( UINT8 ubNPC )
1356 {
1357 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
1358 if (!quotes) return; // error
1359 ResetOncePerConvoRecords(quotes);
1360 }
1361
1362
ResetOncePerConvoRecordsForAllNPCsInLoadedSector(void)1363 void ResetOncePerConvoRecordsForAllNPCsInLoadedSector( void )
1364 {
1365 if ( gWorldSectorX == 0 || gWorldSectorY == 0 )
1366 {
1367 return;
1368 }
1369
1370 for (const MercProfile* p : GCM->listMercProfiles())
1371 {
1372 // only RPCs and NPCs
1373 if (!p->isNPCorRPC()) continue;
1374
1375 ProfileID ubMercID = p->getID();
1376 if ( gMercProfiles[ubMercID].sSectorX == gWorldSectorX &&
1377 gMercProfiles[ubMercID].sSectorY == gWorldSectorY &&
1378 gMercProfiles[ubMercID].bSectorZ == gbWorldSectorZ &&
1379 gpNPCQuoteInfoArray[ubMercID] != NULL )
1380 {
1381 ResetOncePerConvoRecordsForNPC(ubMercID);
1382 }
1383 }
1384 }
1385
1386
NPCClosePanel()1387 static void NPCClosePanel()
1388 {
1389 DialogueEvent::Add(new DialogueEventCallback<DeleteTalkingMenu>());
1390 }
1391
1392
TalkingMenuGiveItem(ProfileID const npc,OBJECTTYPE * const object,INT8 const inv_pos)1393 static void TalkingMenuGiveItem(ProfileID const npc, OBJECTTYPE* const object, INT8 const inv_pos)
1394 {
1395 class DialogueEventGiveItem : public DialogueEvent
1396 {
1397 public:
1398 DialogueEventGiveItem(ProfileID const npc, OBJECTTYPE* const object, INT8 const inv_pos) :
1399 object_(object),
1400 npc_(npc),
1401 inv_pos_(inv_pos)
1402 {}
1403
1404 bool Execute()
1405 {
1406 HandleNPCItemGiven(npc_, object_, inv_pos_);
1407 return false;
1408 }
1409
1410 private:
1411 OBJECTTYPE* const object_;
1412 ProfileID const npc_;
1413 INT8 const inv_pos_;
1414 };
1415
1416 DialogueEvent::Add(new DialogueEventGiveItem(npc, object, inv_pos));
1417 }
1418
1419
ReturnItemToPlayer(ProfileID const merc,OBJECTTYPE * const o)1420 static void ReturnItemToPlayer(ProfileID const merc, OBJECTTYPE* const o)
1421 {
1422 if (!o) return;
1423
1424 SOLDIERTYPE* const s = FindSoldierByProfileID(merc);
1425
1426 // Try to auto place object and then if it fails, put into cursor
1427 if (!AutoPlaceObject(s, o, FALSE))
1428 {
1429 InternalBeginItemPointer(s, o, NO_SLOT);
1430 }
1431 DirtyMercPanelInterface(s, DIRTYLEVEL2);
1432 }
1433
1434
1435 static void TriggerClosestMercWhoCanSeeNPC(UINT8 ubNPC, NPCQuoteInfo* pQuotePtr);
1436
1437
ConverseFull(UINT8 const ubNPC,UINT8 const ubMerc,Approach bApproach,UINT8 const approach_record,OBJECTTYPE * const o)1438 void ConverseFull(UINT8 const ubNPC, UINT8 const ubMerc, Approach bApproach, UINT8 const approach_record, OBJECTTYPE* const o)
1439 {
1440 static INT32 giNPCSpecialReferenceCount = 0;
1441
1442 NPCQuoteInfo QuoteInfo;
1443 NPCQuoteInfo * pQuotePtr = &(QuoteInfo);
1444 UINT8 ubLoop, ubQuoteNum, ubRecordNum;
1445 UINT32 uiDay;
1446
1447 Assert((bApproach == APPROACH_GIVINGITEM) == (o != 0));
1448
1449 SOLDIERTYPE* const pNPC = FindSoldierByProfileID(ubNPC);
1450 if ( pNPC )
1451 {
1452 // set delay for civ AI movement
1453 pNPC->uiTimeSinceLastSpoke = GetJA2Clock();
1454
1455 if (!CheckFact(FACT_CURRENT_SECTOR_IS_SAFE, ubNPC))
1456 {
1457 if ( bApproach != TRIGGER_NPC && bApproach != APPROACH_GIVEFIRSTAID && bApproach != APPROACH_DECLARATION_OF_HOSTILITY && bApproach != APPROACH_ENEMY_NPC_QUOTE )
1458 {
1459 if ( NPCHasUnusedRecordWithGivenApproach( ubNPC, APPROACH_SECTOR_NOT_SAFE ) )
1460 {
1461 // override with sector-not-safe approach
1462 bApproach = APPROACH_SECTOR_NOT_SAFE;
1463 }
1464 }
1465 }
1466
1467 // make sure civ is awake now
1468 pNPC->fAIFlags &= (~AI_ASLEEP);
1469 }
1470
1471 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubNPC);
1472 if (!pNPCQuoteInfoArray)
1473 { // error!!!
1474 ReturnItemToPlayer(ubMerc, o);
1475 return;
1476 }
1477
1478 MERCPROFILESTRUCT& p = GetProfile(ubNPC);
1479 switch( bApproach )
1480 {
1481 case NPC_INITIAL_QUOTE:
1482 // reset stuff
1483 ResetOncePerConvoRecords( pNPCQuoteInfoArray );
1484
1485 // CHEAP HACK
1486 // Since we don't have CONDITIONAL once-per-convo refreshes, do this in code
1487 // NB fact 281 is 'Darren has explained boxing rules'
1488 if (ubNPC == DARREN && !CheckFact(FACT_281, DARREN))
1489 {
1490 TURN_FLAG_OFF( pNPCQuoteInfoArray[11].fFlags, QUOTE_FLAG_SAID );
1491 }
1492
1493 // turn the NPC to face us
1494 // this '1' value is a dummy....
1495 NPCDoAction( ubNPC, NPC_ACTION_TURN_TO_FACE_NEAREST_MERC, 1 );
1496
1497 if (p.ubLastDateSpokenTo > 0)
1498 {
1499 uiDay = GetWorldDay();
1500 if (uiDay > p.ubLastDateSpokenTo)
1501 {
1502 NPCConsiderTalking( ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1503 if (pQuotePtr != NULL)
1504 {
1505 // converse using this approach instead!
1506 ReturnItemToPlayer(ubMerc, o);
1507 Converse(ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE);
1508 return;
1509 }
1510 // subsequent times approached intro
1511 ubQuoteNum = QUOTE_SUBS_INTRO;
1512 }
1513 else
1514 {
1515 // say nothing!
1516 ReturnItemToPlayer(ubMerc, o);
1517 return;
1518 }
1519 }
1520 else
1521 {
1522 // try special initial quote first
1523 NPCConsiderTalking( ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1524 if (pQuotePtr != NULL)
1525 {
1526 // converse using this approach instead!
1527 ReturnItemToPlayer(ubMerc, o);
1528 Converse(ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE);
1529 return;
1530 }
1531
1532 NPCConsiderTalking( ubNPC, ubMerc, APPROACH_INITIAL_QUOTE, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1533 if (pQuotePtr != NULL)
1534 {
1535 // converse using this approach instead!
1536 ReturnItemToPlayer(ubMerc, o);
1537 Converse(ubNPC, ubMerc, APPROACH_INITIAL_QUOTE);
1538 return;
1539 }
1540
1541 // first time approached intro
1542 ubQuoteNum = QUOTE_INTRO;
1543 }
1544 TalkingMenuDialogue( ubQuoteNum );
1545 p.ubLastQuoteSaid = ubQuoteNum;
1546 p.bLastQuoteSaidWasSpecial = FALSE;
1547 break;
1548 case NPC_WHOAREYOU:
1549 ubQuoteNum = QUOTE_INTRO;
1550 TalkingMenuDialogue( ubQuoteNum );
1551 // For now, DO NOT remember for 'Come again?'
1552 break;
1553 case APPROACH_REPEAT:
1554 if (p.ubLastQuoteSaid == NO_QUOTE)
1555 {
1556 // this should never occur now!
1557 TalkingMenuDialogue( QUOTE_INTRO );
1558 }
1559 else
1560 {
1561 if (p.bLastQuoteSaidWasSpecial)
1562 {
1563 pQuotePtr = &(pNPCQuoteInfoArray[p.ubLastQuoteSaid]);
1564 // say quote and following consecutive quotes
1565 for (ubLoop = 0; ubLoop < pQuotePtr->ubNumQuotes; ubLoop++)
1566 {
1567 // say quote #(pQuotePtr->ubQuoteNum + ubLoop)
1568 TalkingMenuDialogue( (UINT8)( pQuotePtr->ubQuoteNum + ubLoop ) );
1569 }
1570 }
1571 else
1572 {
1573 TalkingMenuDialogue(p.ubLastQuoteSaid);
1574 }
1575 }
1576 break;
1577 default:
1578 switch( bApproach )
1579 {
1580 case APPROACH_GIVINGITEM:
1581 // first start by triggering any introduction quote if there is one...
1582 if (p.ubLastDateSpokenTo > 0)
1583 {
1584 uiDay = GetWorldDay();
1585 if (uiDay > p.ubLastDateSpokenTo)
1586 {
1587 NPCConsiderTalking( ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1588 if (pQuotePtr != NULL)
1589 {
1590 // converse using this approach instead!
1591 Converse(ubNPC, ubMerc, APPROACH_SPECIAL_INITIAL_QUOTE);
1592
1593 if ( ubNPC == DARREN )
1594 {
1595 // then we have to make this give attempt fail
1596 ReturnItemToPlayer(ubMerc, o);
1597 return;
1598 }
1599 }
1600 }
1601 }
1602 else
1603 {
1604 NPCConsiderTalking( ubNPC, ubMerc, APPROACH_INITIAL_QUOTE, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1605 if (pQuotePtr != NULL)
1606 {
1607 // converse using this approach instead!
1608 Converse(ubNPC, ubMerc, APPROACH_INITIAL_QUOTE);
1609 }
1610 }
1611
1612 // If we are approaching because we want to give an item, do something different
1613 NPCConsiderReceivingItemFromMerc(ubNPC, ubMerc, o, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum);
1614 break;
1615 case TRIGGER_NPC:
1616 // if triggering, pass in the approach data as the record to consider
1617 STLOGD("Handling trigger {}/{} at {}", gMercProfiles[ubNPC].zNickname.c_str(), approach_record, GetJA2Clock());
1618 NPCConsiderTalking(ubNPC, ubMerc, bApproach, approach_record, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum);
1619 break;
1620 default:
1621 NPCConsiderTalking( ubNPC, ubMerc, bApproach, 0, pNPCQuoteInfoArray, &pQuotePtr, &ubRecordNum );
1622 break;
1623 }
1624 if (pQuotePtr == NULL)
1625 {
1626 // say random everyday quote
1627 // do NOT set last quote said!
1628 switch( bApproach )
1629 {
1630 case APPROACH_FRIENDLY:
1631 if (p.bFriendlyOrDirectDefaultResponseUsedRecently)
1632 {
1633 ubQuoteNum = QUOTE_GETLOST;
1634 }
1635 else
1636 {
1637 ubQuoteNum = QUOTE_FRIENDLY_DEFAULT1 + (UINT8) Random( 2 );
1638 p.bFriendlyOrDirectDefaultResponseUsedRecently = TRUE;
1639 }
1640 break;
1641 case APPROACH_DIRECT:
1642 if (p.bFriendlyOrDirectDefaultResponseUsedRecently)
1643 {
1644 ubQuoteNum = QUOTE_GETLOST;
1645 }
1646 else
1647 {
1648 ubQuoteNum = QUOTE_DIRECT_DEFAULT;
1649 p.bFriendlyOrDirectDefaultResponseUsedRecently = TRUE;
1650 }
1651 break;
1652 case APPROACH_THREATEN:
1653 if (p.bThreatenDefaultResponseUsedRecently)
1654 {
1655 ubQuoteNum = QUOTE_GETLOST;
1656 }
1657 else
1658 {
1659 ubQuoteNum = QUOTE_THREATEN_DEFAULT;
1660 p.bThreatenDefaultResponseUsedRecently = TRUE;
1661 }
1662 break;
1663 case APPROACH_RECRUIT:
1664 if (p.bRecruitDefaultResponseUsedRecently)
1665 {
1666 ubQuoteNum = QUOTE_GETLOST;
1667 }
1668 else
1669 {
1670 ubQuoteNum = QUOTE_RECRUIT_NO;
1671 p.bRecruitDefaultResponseUsedRecently = TRUE;
1672 }
1673 break;
1674 case APPROACH_GIVINGITEM:
1675 ubQuoteNum = QUOTE_GIVEITEM_NO;
1676
1677 /*
1678 CC - now handled below
1679 */
1680 break;
1681 case TRIGGER_NPC:
1682 // trigger did not succeed - abort!!
1683 return;
1684 default:
1685 ubQuoteNum = QUOTE_INTRO;
1686 break;
1687 }
1688 TalkingMenuDialogue( ubQuoteNum );
1689 p.ubLastQuoteSaid = ubQuoteNum;
1690 p.bLastQuoteSaidWasSpecial = FALSE;
1691 if (ubQuoteNum == QUOTE_GETLOST)
1692 {
1693 if (ubNPC == 70 || ubNPC == 120)
1694 {
1695 // becomes an enemy
1696 NPCDoAction( ubNPC, NPC_ACTION_BECOME_ENEMY, 0 );
1697 }
1698 // close panel at end of speech
1699 NPCClosePanel();
1700 }
1701 else if ( ubQuoteNum == QUOTE_GIVEITEM_NO )
1702 {
1703 // close panel at end of speech
1704 NPCClosePanel();
1705 if ( pNPC )
1706 {
1707 switch( ubNPC )
1708 {
1709 case JIM:
1710 case JACK:
1711 case OLAF:
1712 case RAY:
1713 case OLGA:
1714 case TYRONE:
1715 // Start combat etc
1716 CancelAIAction(pNPC);
1717 AddToShouldBecomeHostileOrSayQuoteList(pNPC);
1718 default:
1719 break;
1720 }
1721 }
1722 }
1723 }
1724 else
1725 {
1726 // turn before speech?
1727 if ( pQuotePtr->sActionData <= -NPC_ACTION_TURN_TO_FACE_NEAREST_MERC )
1728 {
1729 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
1730 ZEROTIMECOUNTER( pSoldier->AICounter );
1731 if (pSoldier->bNextAction == AI_ACTION_WAIT)
1732 {
1733 pSoldier->bNextAction = AI_ACTION_NONE;
1734 pSoldier->usNextActionData = 0;
1735 }
1736 NPCDoAction( ubNPC, (UINT16) -(pQuotePtr->sActionData), ubRecordNum );
1737 }
1738 if (pQuotePtr->ubQuoteNum != NO_QUOTE)
1739 {
1740 // say quote and following consecutive quotes
1741 for (ubLoop = 0; ubLoop < pQuotePtr->ubNumQuotes; ubLoop++)
1742 {
1743 TalkingMenuDialogue( (UINT8)( pQuotePtr->ubQuoteNum + ubLoop ) );
1744 }
1745 p.ubLastQuoteSaid = ubRecordNum;
1746 p.bLastQuoteSaidWasSpecial = TRUE;
1747 }
1748 // set to "said" if we should do so
1749 if (pQuotePtr->fFlags & QUOTE_FLAG_ERASE_ONCE_SAID || pQuotePtr->fFlags & QUOTE_FLAG_SAY_ONCE_PER_CONVO)
1750 {
1751 TURN_FLAG_ON( pQuotePtr->fFlags, QUOTE_FLAG_SAID );
1752 }
1753
1754 // Carry out implications (actions) of this record
1755
1756 // Give NPC item if appropriate
1757 if (bApproach == APPROACH_GIVINGITEM )
1758 {
1759 if ( pQuotePtr->sActionData != NPC_ACTION_DONT_ACCEPT_ITEM )
1760 {
1761 PlaceObjectInSoldierProfile(ubNPC, o);
1762
1763 // Find the GIVER....
1764 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubMerc);
1765
1766 // Is this one of us?
1767 if ( pSoldier->bTeam == OUR_TEAM )
1768 {
1769 INT8 const bSlot = FindExactObj(pSoldier, o);
1770 if (bSlot != NO_SLOT)
1771 {
1772 RemoveObjs(&pSoldier->inv[bSlot], o->ubNumberOfObjects);
1773 DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
1774 }
1775 }
1776 else
1777 {
1778 RemoveObjectFromSoldierProfile(ubMerc, o->usItem);
1779 }
1780 }
1781 // CC: now handled below
1782 /*
1783 else
1784 {
1785 // ATE: Here, put back into inventory or place on ground....
1786 {
1787 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubMerc);
1788
1789 // Try to auto place object and then if it fails, put into cursor
1790 if (!AutoPlaceObject(pSoldier, o, FALSE))
1791 {
1792 InternalBeginItemPointer(pSoldier, o, NO_SLOT);
1793 }
1794 DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
1795
1796 }
1797 }
1798 */
1799 }
1800 else if (bApproach == APPROACH_RECRUIT)
1801 {
1802 // the guy just joined our party
1803 }
1804
1805 // Set things
1806 if (pQuotePtr->usSetFactTrue != NO_FACT)
1807 {
1808 SetFactTrue((Fact)pQuotePtr->usSetFactTrue);
1809 }
1810 if (pQuotePtr->ubEndQuest != NO_QUEST)
1811 {
1812 EndQuest( pQuotePtr->ubEndQuest, gWorldSectorX, gWorldSectorY );
1813 }
1814 if (pQuotePtr->ubStartQuest != NO_QUEST)
1815 {
1816 StartQuest( pQuotePtr->ubStartQuest, gWorldSectorX, gWorldSectorY );
1817 }
1818
1819 // Give item to merc?
1820 if ( pQuotePtr->usGiftItem >= TURN_UI_OFF )
1821 {
1822 switch ( pQuotePtr->usGiftItem )
1823 {
1824 case TURN_UI_OFF:
1825 if ( !(gTacticalStatus.uiFlags & INCOMBAT) )
1826 {
1827 gTacticalStatus.uiFlags |= ENGAGED_IN_CONV;
1828 // Increment refrence count...
1829 giNPCReferenceCount = 1;
1830 }
1831 break;
1832 case TURN_UI_ON:
1833 // while the special ref count is set, ignore standard off
1834 if ( giNPCSpecialReferenceCount == 0 )
1835 {
1836 gTacticalStatus.uiFlags &= ~ENGAGED_IN_CONV;
1837 // Decrement refrence count...
1838 giNPCReferenceCount = 0;
1839 }
1840 break;
1841 case SPECIAL_TURN_UI_OFF:
1842 if ( !(gTacticalStatus.uiFlags & INCOMBAT) )
1843 {
1844
1845 gTacticalStatus.uiFlags |= ENGAGED_IN_CONV;
1846 // Increment refrence count...
1847 giNPCReferenceCount = 1;
1848 if ( giNPCSpecialReferenceCount < 0 )
1849 {
1850 // ???
1851 giNPCSpecialReferenceCount = 0;
1852 }
1853 // increment SPECIAL reference count
1854 giNPCSpecialReferenceCount += 1;
1855 }
1856 break;
1857 case SPECIAL_TURN_UI_ON:
1858 // Decrement SPECIAL reference count
1859 giNPCSpecialReferenceCount -= 1;
1860 // if special count is now 0, turn reactivate UI
1861 if ( giNPCSpecialReferenceCount == 0 )
1862 {
1863 gTacticalStatus.uiFlags &= ~ENGAGED_IN_CONV;
1864 giNPCReferenceCount = 0;
1865 }
1866 else if ( giNPCSpecialReferenceCount < 0 )
1867 {
1868 // ???
1869 giNPCSpecialReferenceCount = 0;
1870 }
1871 break;
1872 }
1873 }
1874 else if ( pQuotePtr->usGiftItem != 0 )
1875 {
1876 {
1877 INT8 bInvPos;
1878
1879 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
1880
1881 // Look for item....
1882 bInvPos = FindObj( pSoldier, pQuotePtr->usGiftItem );
1883
1884 AssertMsg( bInvPos != NO_SLOT, "NPC.C: Gift item does not exist in NPC." );
1885
1886 TalkingMenuGiveItem( ubNPC, &(pSoldier->inv[ bInvPos ] ), bInvPos );
1887 }
1888 }
1889 // Action before movement?
1890 if ( pQuotePtr->sActionData < 0 && pQuotePtr->sActionData > -NPC_ACTION_TURN_TO_FACE_NEAREST_MERC )
1891 {
1892 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
1893 ZEROTIMECOUNTER( pSoldier->AICounter );
1894 if (pSoldier->bNextAction == AI_ACTION_WAIT)
1895 {
1896 pSoldier->bNextAction = AI_ACTION_NONE;
1897 pSoldier->usNextActionData = 0;
1898 }
1899 NPCDoAction( ubNPC, (UINT16) -(pQuotePtr->sActionData), ubRecordNum );
1900 }
1901 else if ( pQuotePtr->usGoToGridno == NO_MOVE && pQuotePtr->sActionData > 0 )
1902 {
1903 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
1904 ZEROTIMECOUNTER( pSoldier->AICounter );
1905 if (pSoldier->bNextAction == AI_ACTION_WAIT)
1906 {
1907 pSoldier->bNextAction = AI_ACTION_NONE;
1908 pSoldier->usNextActionData = 0;
1909 }
1910 NPCDoAction( ubNPC, (UINT16) (pQuotePtr->sActionData), ubRecordNum );
1911 }
1912
1913 // Movement?
1914 if ( pQuotePtr->usGoToGridno != NO_MOVE )
1915 {
1916 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
1917
1918 // stupid hack CC
1919 if (pSoldier && ubNPC == KYLE)
1920 {
1921 // make sure he has keys
1922 pSoldier->bHasKeys = TRUE;
1923 }
1924 if (pSoldier && pSoldier->sGridNo == pQuotePtr->usGoToGridno )
1925 {
1926 // search for quotes to trigger immediately!
1927 pSoldier->ubQuoteRecord = ubRecordNum + 1; // add 1 so that the value is guaranteed nonzero
1928 NPCReachedDestination( pSoldier, TRUE );
1929 }
1930 else
1931 {
1932 // turn off cowering
1933 if ( pNPC->uiStatusFlags & SOLDIER_COWERING)
1934 {
1935 //pNPC->uiStatusFlags &= ~SOLDIER_COWERING;
1936 EVENT_InitNewSoldierAnim( pNPC, STANDING, 0 , FALSE );
1937 }
1938
1939 pSoldier->ubQuoteRecord = ubRecordNum + 1; // add 1 so that the value is guaranteed nonzero
1940
1941 if (pQuotePtr->sActionData == NPC_ACTION_TELEPORT_NPC)
1942 {
1943 BumpAnyExistingMerc( pQuotePtr->usGoToGridno );
1944 TeleportSoldier(*pSoldier, pQuotePtr->usGoToGridno, false);
1945 // search for quotes to trigger immediately!
1946 NPCReachedDestination( pSoldier, FALSE );
1947 }
1948 else
1949 {
1950 NPCGotoGridNo( ubNPC, pQuotePtr->usGoToGridno, ubRecordNum );
1951 }
1952 }
1953 }
1954
1955 // Trigger other NPC?
1956 // ATE: Do all triggers last!
1957 if ( pQuotePtr->ubTriggerNPC != IRRELEVANT )
1958 {
1959 // Check for special NPC trigger codes
1960 if ( pQuotePtr->ubTriggerNPC == 0 )
1961 {
1962 TriggerClosestMercWhoCanSeeNPC( ubNPC, pQuotePtr );
1963 }
1964 else if ( pQuotePtr->ubTriggerNPC == 1 )
1965 {
1966 // trigger self
1967 TriggerNPCRecord( ubNPC, pQuotePtr->ubTriggerNPCRec );
1968 }
1969 else
1970 {
1971 TriggerNPCRecord( pQuotePtr->ubTriggerNPC, pQuotePtr->ubTriggerNPCRec );
1972 }
1973 }
1974
1975 // Ian says it is okay to take this out!
1976 /*
1977 if (bApproach == APPROACH_ENEMY_NPC_QUOTE)
1978 {
1979 NPCClosePanel();
1980 }
1981 */
1982
1983 }
1984 break;
1985 }
1986
1987 // Set last day spoken!
1988 switch( bApproach )
1989 {
1990 case APPROACH_FRIENDLY:
1991 case APPROACH_DIRECT:
1992 case APPROACH_THREATEN:
1993 case APPROACH_RECRUIT:
1994 case NPC_INITIATING_CONV:
1995 case NPC_INITIAL_QUOTE:
1996 case APPROACH_SPECIAL_INITIAL_QUOTE:
1997 case APPROACH_DECLARATION_OF_HOSTILITY:
1998 case APPROACH_INITIAL_QUOTE:
1999 case APPROACH_GIVINGITEM:
2000 p.ubLastDateSpokenTo = (UINT8)GetWorldDay();
2001 break;
2002 default:
2003 break;
2004 }
2005
2006 /* If the approach was changed, always return the item. Otherwise check to see
2007 * if the record in question specified refusal */
2008 if (bApproach != APPROACH_GIVINGITEM || !pQuotePtr || pQuotePtr->sActionData == NPC_ACTION_DONT_ACCEPT_ITEM)
2009 {
2010 ReturnItemToPlayer(ubMerc, o);
2011 }
2012 }
2013
2014
Converse(UINT8 const ubNPC,UINT8 const ubMerc,Approach const bApproach)2015 void Converse(UINT8 const ubNPC, UINT8 const ubMerc, Approach const bApproach)
2016 {
2017 ConverseFull(ubNPC, ubMerc, bApproach, 0, 0);
2018 }
2019
2020
NPCConsiderInitiatingConv(const SOLDIERTYPE * const pNPC)2021 INT16 NPCConsiderInitiatingConv(const SOLDIERTYPE* const pNPC)
2022 {
2023 UINT8 const ubNPC = pNPC->ubProfile;
2024 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubNPC);
2025 if (!pNPCQuoteInfoArray) return NOWHERE; // error
2026
2027 const INT16 sMyGridNo = pNPC->sGridNo;
2028 INT16 sDesiredMercDist = 100;
2029 UINT8 ubDesiredMerc = NOBODY;
2030 UINT8 ubHighestTalkDesire = 0;
2031 SOLDIERTYPE* pDesiredMerc = NULL; // XXX HACK000E
2032 // loop through all mercs
2033 for (UINT8 ubMerc = 0; ubMerc < guiNumMercSlots; ++ubMerc)
2034 {
2035 const SOLDIERTYPE* const pMerc = MercSlots[ubMerc];
2036 if (pMerc == NULL) continue;
2037
2038 // only look for mercs on the side of the player
2039 if (pMerc->bSide != OUR_TEAM) continue;
2040
2041 // only look for active mercs
2042 if (pMerc->bAssignment >= ON_DUTY) continue;
2043
2044 // if they're not visible, don't think about it
2045 if (pNPC->bOppList[ubMerc] != SEEN_CURRENTLY) continue;
2046
2047 /* what's the opinion required for the highest-opinion quote that we would
2048 * say to this merc */
2049 const UINT8 ubTalkDesire = NPCConsiderTalking(pNPC->ubProfile, pMerc->ubProfile, NPC_INITIATING_CONV, 0, pNPCQuoteInfoArray, NULL, NULL);
2050 if (ubTalkDesire == 0) continue;
2051
2052 if (ubTalkDesire > ubHighestTalkDesire)
2053 {
2054 ubHighestTalkDesire = ubTalkDesire;
2055 ubDesiredMerc = ubMerc;
2056 pDesiredMerc = &GetMan(ubMerc);
2057 sDesiredMercDist = PythSpacesAway(sMyGridNo, pDesiredMerc->sGridNo);
2058 }
2059 else if (ubTalkDesire == ubHighestTalkDesire)
2060 {
2061 INT16 const sDist = PythSpacesAway(sMyGridNo, GetMan(ubMerc).sGridNo);
2062 if (sDist < sDesiredMercDist)
2063 {
2064 // we can say the same thing to this merc, and they're closer!
2065 ubDesiredMerc = ubMerc;
2066 sDesiredMercDist = sDist;
2067 }
2068 }
2069 }
2070
2071 return ubDesiredMerc == NOBODY ? NOWHERE : pDesiredMerc->sGridNo;
2072 }
2073
2074
NPCReachedDestination(SOLDIERTYPE * pNPC,BOOLEAN fAlreadyThere)2075 void NPCReachedDestination( SOLDIERTYPE * pNPC, BOOLEAN fAlreadyThere )
2076 {
2077 // perform action or whatever after reaching our destination
2078 UINT8 ubNPC;
2079 NPCQuoteInfo * pQuotePtr;
2080 UINT8 ubLoop;
2081 UINT8 ubQuoteRecord;
2082
2083 if ( pNPC->ubQuoteRecord == 0 )
2084 {
2085 ubQuoteRecord = 0;
2086 }
2087 else
2088 {
2089 ubQuoteRecord = (UINT8) (pNPC->ubQuoteRecord - 1);
2090 }
2091
2092 // Clear values!
2093 pNPC->ubQuoteRecord = 0;
2094 if (pNPC->bTeam == OUR_TEAM)
2095 {
2096 // the "under ai control" flag was set temporarily; better turn it off now
2097 pNPC->uiStatusFlags &= (~SOLDIER_PCUNDERAICONTROL);
2098 // make damn sure the AI_HANDLE_EVERY_FRAME flag is turned off
2099 pNPC->fAIFlags &= (AI_HANDLE_EVERY_FRAME);
2100 }
2101
2102 ubNPC = pNPC->ubProfile;
2103 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubNPC);
2104 if (!pNPCQuoteInfoArray) return; // error
2105
2106 pQuotePtr = &(pNPCQuoteInfoArray[ubQuoteRecord]);
2107 // either we are supposed to consider a new quote record
2108 // (indicated by a negative gridno in the has-item field)
2109 // or an action to perform once we reached this gridno
2110
2111 if ( pNPC->sGridNo == pQuotePtr->usGoToGridno )
2112 {
2113 // check for an after-move action
2114 if ( pQuotePtr->sActionData > 0)
2115 {
2116 NPCDoAction( ubNPC, (UINT16) pQuotePtr->sActionData, ubQuoteRecord );
2117 }
2118 }
2119
2120 for ( ubLoop = 0; ubLoop < NUM_NPC_QUOTE_RECORDS; ubLoop++ )
2121 {
2122 pQuotePtr = &(pNPCQuoteInfoArray[ubLoop]);
2123 if ( pNPC->sGridNo == -(pQuotePtr->sRequiredGridno ) )
2124 {
2125 if ( NPCConsiderQuote( ubNPC, 0, TRIGGER_NPC, ubLoop, 0, pNPCQuoteInfoArray ) )
2126 {
2127 if (fAlreadyThere)
2128 {
2129 TriggerNPCRecord( ubNPC, ubLoop );
2130 }
2131 else
2132 {
2133 // trigger this quote
2134 TriggerNPCRecordImmediately( ubNPC, ubLoop );
2135 }
2136 return;
2137 }
2138 }
2139 }
2140 }
2141
2142
2143 // Trigger an NPC record
NPCTriggerNPC(UINT8 const npc,UINT8 const record,Approach const approach,bool const show_dialogue_menu)2144 static void NPCTriggerNPC(UINT8 const npc, UINT8 const record, Approach const approach, bool const show_dialogue_menu)
2145 {
2146 class DialogueEventTriggerNPC : public DialogueEvent
2147 {
2148 public:
2149 DialogueEventTriggerNPC(ProfileID const npc, UINT8 const record, Approach const approach, bool const show_dialogue_menu) :
2150 npc_(npc),
2151 record_(record),
2152 show_dialogue_menu_(show_dialogue_menu),
2153 approach_(approach)
2154 {}
2155
2156 bool Execute()
2157 {
2158 HandleNPCTriggerNPC(npc_, record_, show_dialogue_menu_, approach_);
2159 return false;
2160 }
2161
2162 private:
2163 ProfileID const npc_;
2164 UINT8 const record_;
2165 bool const show_dialogue_menu_;
2166 Approach const approach_;
2167 };
2168
2169 DialogueEvent::Add(new DialogueEventTriggerNPC(npc, record, approach, show_dialogue_menu));
2170 }
2171
2172
TriggerNPCRecord(UINT8 const ubTriggerNPC,UINT8 const record)2173 void TriggerNPCRecord(UINT8 const ubTriggerNPC, UINT8 const record)
2174 {
2175 // Check if we have a quote to trigger...
2176
2177 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubTriggerNPC);
2178 if (!quotes) return; // error
2179 NPCQuoteInfo const& q = quotes[record];
2180 bool const display_dialogue = q.ubQuoteNum != IRRELEVANT;
2181
2182 if (NPCConsiderQuote(ubTriggerNPC, 0, TRIGGER_NPC, record, 0, quotes))
2183 {
2184 NPCTriggerNPC(ubTriggerNPC, record, TRIGGER_NPC, display_dialogue);
2185 }
2186 else
2187 { // don't do anything
2188 SLOGW("trigger of %d, record %d cannot proceed, possible error", ubTriggerNPC, record);
2189 }
2190 }
2191
2192
TriggerNPCRecordImmediately(UINT8 const ubTriggerNPC,UINT8 const record)2193 void TriggerNPCRecordImmediately(UINT8 const ubTriggerNPC, UINT8 const record)
2194 {
2195 // Check if we have a quote to trigger...
2196
2197 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubTriggerNPC);
2198 if (!quotes) return; // error
2199 NPCQuoteInfo const& q = quotes[record];
2200 bool const display_dialogue = q.ubQuoteNum != IRRELEVANT;
2201
2202 if (NPCConsiderQuote(ubTriggerNPC, 0, TRIGGER_NPC, record, 0, quotes))
2203 { // trigger IMMEDIATELY
2204 HandleNPCTriggerNPC(ubTriggerNPC, record, display_dialogue, TRIGGER_NPC);
2205 }
2206 else
2207 { // don't do anything
2208 SLOGW("trigger of %d, record %d cannot proceed, possible error", ubTriggerNPC, record);
2209 }
2210 }
2211
2212
PCsNearNPC(UINT8 ubNPC)2213 void PCsNearNPC( UINT8 ubNPC )
2214 {
2215 UINT8 ubLoop;
2216 NPCQuoteInfo * pQuotePtr;
2217
2218 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubNPC);
2219 if (!pNPCQuoteInfoArray) return; // error
2220
2221 // see what this triggers...
2222 SetFactTrue( FACT_PC_NEAR );
2223
2224 // Clear values!
2225 // Get value for NPC
2226 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
2227 pSoldier->ubQuoteRecord = 0;
2228
2229 for ( ubLoop = 0; ubLoop < NUM_NPC_QUOTE_RECORDS; ubLoop++ )
2230 {
2231 pQuotePtr = &(pNPCQuoteInfoArray[ubLoop]);
2232 if ( pSoldier->sGridNo == -(pQuotePtr->sRequiredGridno ) )
2233 {
2234 if ( NPCConsiderQuote( ubNPC, 0, TRIGGER_NPC, ubLoop, 0, pNPCQuoteInfoArray ) )
2235 {
2236 // trigger this quote IMMEDIATELY!
2237 TriggerNPCRecordImmediately( ubNPC, ubLoop );
2238 break;
2239 }
2240 }
2241 }
2242
2243 // reset fact
2244 SetFactFalse( FACT_PC_NEAR );
2245 }
2246
PCDoesFirstAidOnNPC(UINT8 ubNPC)2247 BOOLEAN PCDoesFirstAidOnNPC( UINT8 ubNPC )
2248 {
2249 UINT8 ubLoop;
2250 NPCQuoteInfo * pQuotePtr;
2251
2252 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubNPC);
2253 if (!pNPCQuoteInfoArray) return FALSE; // error
2254
2255 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
2256 // Clear values!
2257 pSoldier->ubQuoteRecord = 0;
2258
2259 // Set flag...
2260 gMercProfiles[ ubNPC ].ubMiscFlags2 |= PROFILE_MISC_FLAG2_BANDAGED_TODAY;
2261
2262 for ( ubLoop = 0; ubLoop < NUM_NPC_QUOTE_RECORDS; ubLoop++ )
2263 {
2264 pQuotePtr = &(pNPCQuoteInfoArray[ubLoop]);
2265 if ( pQuotePtr->ubApproachRequired == APPROACH_GIVEFIRSTAID )
2266 {
2267 if ( NPCConsiderQuote( ubNPC, 0, TRIGGER_NPC, ubLoop, 0, pNPCQuoteInfoArray ) )
2268 {
2269 // trigger this quote IMMEDIATELY!
2270 TriggerNPCRecordImmediately( ubNPC, ubLoop );
2271 return( TRUE );
2272 }
2273 }
2274 }
2275 return( FALSE );
2276 }
2277
2278
TriggerClosestMercWhoCanSeeNPC(UINT8 ubNPC,NPCQuoteInfo * pQuotePtr)2279 static void TriggerClosestMercWhoCanSeeNPC(UINT8 ubNPC, NPCQuoteInfo* pQuotePtr)
2280 {
2281 // Loop through all mercs, gather closest mercs who can see and trigger one!
2282 UINT8 ubNumMercs = 0;
2283
2284 const SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
2285
2286 // Loop through all our guys and randomly say one from someone in our sector
2287 SOLDIERTYPE* mercs_in_sector[40];
2288 FOR_EACH_IN_TEAM(s, OUR_TEAM)
2289 {
2290 // Add guy if he's a candidate...
2291 if (OkControllableMerc(s) &&
2292 s->bOppList[pSoldier->ubID] == SEEN_CURRENTLY)
2293 {
2294 mercs_in_sector[ubNumMercs++] = s;
2295 }
2296 }
2297
2298 // If we are > 0
2299 if ( ubNumMercs > 0 )
2300 {
2301 SOLDIERTYPE* const chosen = mercs_in_sector[Random(ubNumMercs)];
2302
2303 // Post action to close panel
2304 NPCClosePanel( );
2305
2306 // If 64, do something special
2307 if ( pQuotePtr->ubTriggerNPCRec == QUOTE_RESPONSE_TO_MIGUEL_SLASH_QUOTE_MERC_OR_RPC_LETGO )
2308 {
2309 class CharacterDialogueEventPCTriggerNPC : public CharacterDialogueEvent
2310 {
2311 public:
2312 CharacterDialogueEventPCTriggerNPC(SOLDIERTYPE& s) : CharacterDialogueEvent(s) {}
2313
2314 bool Execute()
2315 {
2316 if (!MayExecute()) return true;
2317
2318 SOLDIERTYPE const& s = soldier_;
2319 ExecuteCharacterDialogue(s.ubProfile, QUOTE_RESPONSE_TO_MIGUEL_SLASH_QUOTE_MERC_OR_RPC_LETGO, s.face, DIALOGUE_TACTICAL_UI, TRUE, false);
2320
2321 // Setup face with data!
2322 FACETYPE& f = *gpCurrentTalkingFace;
2323 f.uiFlags |= FACE_PCTRIGGER_NPC;
2324 f.u.trigger.npc = MIGUEL;
2325 f.u.trigger.record = 6;
2326
2327 return false;
2328 }
2329 };
2330
2331 DialogueEvent::Add(new CharacterDialogueEventPCTriggerNPC(*chosen));
2332 ++giNPCReferenceCount;
2333 }
2334 else
2335 {
2336 TacticalCharacterDialogue(chosen, pQuotePtr->ubTriggerNPCRec);
2337 }
2338 }
2339
2340 }
2341
TriggerNPCWithIHateYouQuote(UINT8 ubTriggerNPC)2342 BOOLEAN TriggerNPCWithIHateYouQuote( UINT8 ubTriggerNPC )
2343 {
2344 UINT8 ubLoop;
2345
2346 NPCQuoteInfo* const pNPCQuoteInfoArray = EnsureQuoteFileLoaded(ubTriggerNPC);
2347 if (!pNPCQuoteInfoArray) return FALSE; // error
2348
2349 for ( ubLoop = 0; ubLoop < NUM_NPC_QUOTE_RECORDS; ubLoop++ )
2350 {
2351 if ( NPCConsiderQuote( ubTriggerNPC, 0, APPROACH_DECLARATION_OF_HOSTILITY, ubLoop, 0, pNPCQuoteInfoArray ) )
2352 {
2353 // trigger this quote!
2354 NPCTriggerNPC(ubTriggerNPC, ubLoop, APPROACH_DECLARATION_OF_HOSTILITY, true);
2355 gMercProfiles[ ubTriggerNPC ].ubMiscFlags |= PROFILE_MISC_FLAG_SAID_HOSTILE_QUOTE;
2356 return( TRUE );
2357 }
2358 }
2359 return( FALSE );
2360
2361 }
2362
2363
NPCHasUnusedRecordWithGivenApproach(UINT8 const ubNPC,Approach const approach)2364 BOOLEAN NPCHasUnusedRecordWithGivenApproach(UINT8 const ubNPC, Approach const approach)
2365 {
2366 // Check if we have a quote that could be used
2367
2368 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2369 if (!quotes) return FALSE; // error
2370
2371 for (UINT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
2372 {
2373 if (!NPCConsiderQuote(ubNPC, 0, approach, i, 0, quotes)) continue;
2374 return TRUE;
2375 }
2376 return FALSE;
2377 }
2378
2379
NPCHasUnusedHostileRecord(UINT8 const ubNPC,Approach const approach)2380 BOOLEAN NPCHasUnusedHostileRecord(UINT8 const ubNPC, Approach const approach)
2381 {
2382 /* this is just like the standard check BUT we must skip any records using
2383 * fact 289 and print debug msg for any records which can't be marked as used
2384 */
2385 // Check if we have a quote that could be used
2386 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2387 if (!quotes) return FALSE; // error
2388
2389 for (UINT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
2390 {
2391 if (!NPCConsiderQuote(ubNPC, 0, approach, i, 0, quotes)) continue;
2392 NPCQuoteInfo const& q = quotes[i];
2393 if (q.usFactMustBeTrue == FACT_NPC_HOSTILE_OR_PISSED_OFF) continue;
2394 return TRUE;
2395 }
2396 return FALSE;
2397 }
2398
2399
NPCWillingToAcceptItem(UINT8 const ubNPC,UINT8 const ubMerc,OBJECTTYPE * const o)2400 BOOLEAN NPCWillingToAcceptItem(UINT8 const ubNPC, UINT8 const ubMerc, OBJECTTYPE* const o)
2401 {
2402 // Check if we have a quote that could be used, that applies to this item
2403
2404 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2405 if (!quotes) return FALSE; // error
2406
2407 NPCQuoteInfo* q;
2408 UINT8 ubQuoteNum;
2409 NPCConsiderReceivingItemFromMerc(ubNPC, ubMerc, o, quotes, &q, &ubQuoteNum);
2410 return q != 0;
2411 }
2412
2413
GetInfoForAbandoningEPC(UINT8 const ubNPC,UINT16 * const pusQuoteNum,Fact * const fact_to_set_true)2414 BOOLEAN GetInfoForAbandoningEPC(UINT8 const ubNPC, UINT16* const pusQuoteNum, Fact* const fact_to_set_true)
2415 {
2416 // Check if we have a quote that could be used
2417 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2418 if (!quotes) return FALSE; // error
2419
2420 for (UINT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
2421 {
2422 if (!NPCConsiderQuote(ubNPC, 0, APPROACH_EPC_IN_WRONG_SECTOR, i, 0, quotes)) continue;
2423 NPCQuoteInfo const& q = quotes[i];
2424 *pusQuoteNum = q.ubQuoteNum;
2425 *fact_to_set_true = (Fact)q.usSetFactTrue;
2426 return TRUE;
2427 }
2428 return FALSE;
2429 }
2430
2431
TriggerNPCWithGivenApproach(UINT8 const ubTriggerNPC,Approach const approach)2432 BOOLEAN TriggerNPCWithGivenApproach(UINT8 const ubTriggerNPC, Approach const approach)
2433 {
2434 // Check if we have a quote to trigger...
2435
2436 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubTriggerNPC);
2437 if (!quotes) return FALSE; // error
2438
2439 for (UINT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
2440 {
2441 if (!NPCConsiderQuote(ubTriggerNPC, 0, approach, i, 0, quotes)) continue;
2442
2443 bool const show_panel = quotes[i].ubQuoteNum != IRRELEVANT;
2444 NPCTriggerNPC(ubTriggerNPC, i, approach, show_panel);
2445 return TRUE;
2446 }
2447 return FALSE;
2448 }
2449
2450
SaveNPCInfoToSaveGameFile(HWFILE const f)2451 void SaveNPCInfoToSaveGameFile(HWFILE const f)
2452 {
2453 FOR_EACH(NPCQuoteInfo* const, i, gpNPCQuoteInfoArray)
2454 {
2455 ConditionalInjectNPCQuoteInfoArrayIntoFile(f, *i);
2456 }
2457
2458 FOR_EACH(NPCQuoteInfo* const, i, gpCivQuoteInfoArray)
2459 {
2460 ConditionalInjectNPCQuoteInfoArrayIntoFile(f, *i);
2461 }
2462 }
2463
2464
LoadNPCInfoFromSavedGameFile(HWFILE const f,UINT32 const uiSaveGameVersion)2465 void LoadNPCInfoFromSavedGameFile(HWFILE const f, UINT32 const uiSaveGameVersion)
2466 {
2467 UINT32 cnt;
2468 UINT32 uiNumberToLoad=0;
2469
2470 // If we are trying to restore a saved game prior to version 44, use the
2471 // MAX_NUM_SOLDIERS, else use NUM_PROFILES. Dave used the wrong define!
2472 if( uiSaveGameVersion >= 44 )
2473 uiNumberToLoad = NUM_PROFILES;
2474 else
2475 uiNumberToLoad = MAX_NUM_SOLDIERS;
2476
2477 //Loop through all the NPC quotes
2478 for( cnt=0; cnt<uiNumberToLoad; cnt++ )
2479 {
2480 ConditionalExtractNPCQuoteInfoArrayFromFile(f, gpNPCQuoteInfoArray[cnt]);
2481 }
2482
2483 if ( uiSaveGameVersion >= 56 )
2484 {
2485 for( cnt = 0; cnt < NUM_CIVQUOTE_SECTORS; cnt++)
2486 {
2487 ConditionalExtractNPCQuoteInfoArrayFromFile(f, gpCivQuoteInfoArray[cnt]);
2488 }
2489 }
2490
2491 if ( uiSaveGameVersion < 88 )
2492 {
2493 RefreshNPCScriptRecord( NO_PROFILE, 5 ); // special pass-in value for "replace PC scripts"
2494 RefreshNPCScriptRecord( DARYL, 11 );
2495 RefreshNPCScriptRecord( DARYL, 14 );
2496 RefreshNPCScriptRecord( DARYL, 15 );
2497 }
2498 if ( uiSaveGameVersion < 89 )
2499 {
2500 RefreshNPCScriptRecord( KINGPIN, 23 );
2501 RefreshNPCScriptRecord( KINGPIN, 27 );
2502 }
2503 if ( uiSaveGameVersion < 90 )
2504 {
2505 RefreshNPCScriptRecord( KINGPIN, 25 );
2506 RefreshNPCScriptRecord( KINGPIN, 26 );
2507 }
2508 if ( uiSaveGameVersion < 92 )
2509 {
2510 RefreshNPCScriptRecord( MATT, 14 );
2511 RefreshNPCScriptRecord( AUNTIE, 8 );
2512 }
2513 if ( uiSaveGameVersion < 93 )
2514 {
2515 RefreshNPCScriptRecord( JENNY, 7 );
2516 RefreshNPCScriptRecord( JENNY, 8 );
2517 RefreshNPCScriptRecord( FRANK, 7 );
2518 RefreshNPCScriptRecord( FRANK, 8 );
2519 RefreshNPCScriptRecord( FATHER, 12 );
2520 RefreshNPCScriptRecord( FATHER, 13 );
2521 }
2522 if ( uiSaveGameVersion < 94 )
2523 {
2524 RefreshNPCScriptRecord( CONRAD, 0 );
2525 RefreshNPCScriptRecord( CONRAD, 2 );
2526 RefreshNPCScriptRecord( CONRAD, 9 );
2527 }
2528 if ( uiSaveGameVersion < 95 )
2529 {
2530 RefreshNPCScriptRecord( WALDO, 6 );
2531 RefreshNPCScriptRecord( WALDO, 7 );
2532 RefreshNPCScriptRecord( WALDO, 10 );
2533 RefreshNPCScriptRecord( WALDO, 11 );
2534 RefreshNPCScriptRecord( WALDO, 12 );
2535 }
2536 if ( uiSaveGameVersion < 96 )
2537 {
2538 RefreshNPCScriptRecord( HANS, 18 );
2539 RefreshNPCScriptRecord( ARMAND, 13 );
2540 RefreshNPCScriptRecord( DARREN, 4 );
2541 RefreshNPCScriptRecord( DARREN, 5 );
2542 }
2543 if ( uiSaveGameVersion < 97 )
2544 {
2545 RefreshNPCScriptRecord( JOHN, 22 );
2546 RefreshNPCScriptRecord( JOHN, 23 );
2547 RefreshNPCScriptRecord( SKYRIDER, 19 );
2548 RefreshNPCScriptRecord( SKYRIDER, 21 );
2549 RefreshNPCScriptRecord( SKYRIDER, 22 );
2550 }
2551
2552 if ( uiSaveGameVersion < 98 )
2553 {
2554 RefreshNPCScriptRecord( SKYRIDER, 19 );
2555 RefreshNPCScriptRecord( SKYRIDER, 21 );
2556 RefreshNPCScriptRecord( SKYRIDER, 22 );
2557 }
2558 }
2559
2560
SaveBackupNPCInfoToSaveGameFile(HWFILE const f)2561 void SaveBackupNPCInfoToSaveGameFile(HWFILE const f)
2562 {
2563 FOR_EACH(NPCQuoteInfo* const, i, gpBackupNPCQuoteInfoArray)
2564 {
2565 ConditionalInjectNPCQuoteInfoArrayIntoFile(f, *i);
2566 }
2567 }
2568
2569
LoadBackupNPCInfoFromSavedGameFile(HWFILE const f)2570 void LoadBackupNPCInfoFromSavedGameFile(HWFILE const f)
2571 {
2572 FOR_EACH(NPCQuoteInfo*, i, gpBackupNPCQuoteInfoArray)
2573 {
2574 ConditionalExtractNPCQuoteInfoArrayFromFile(f, *i);
2575 }
2576 }
2577
2578
TriggerFriendWithHostileQuote(UINT8 ubNPC)2579 void TriggerFriendWithHostileQuote( UINT8 ubNPC )
2580 {
2581 UINT8 ubMercsAvailable[ 40 ] = { 0 };
2582 UINT8 ubNumMercsAvailable = 0, ubChosenMerc;
2583
2584 SOLDIERTYPE* const pSoldier = FindSoldierByProfileID(ubNPC);
2585 if (!pSoldier)
2586 {
2587 return;
2588 }
2589 const INT8 bTeam = pSoldier->bTeam;
2590
2591 // Loop through all our guys and find one to yell
2592 CFOR_EACH_IN_TEAM(s, bTeam)
2593 {
2594 // Add guy if he's a candidate...
2595 if (pSoldier->bInSector && s->bLife >= OKLIFE && s->bBreath >= OKBREATH && s->bOppCnt > 0 && s->ubProfile != NO_PROFILE)
2596 {
2597 if (bTeam == CIV_TEAM && pSoldier->ubCivilianGroup != NON_CIV_GROUP && s->ubCivilianGroup != pSoldier->ubCivilianGroup)
2598 {
2599 continue;
2600 }
2601
2602 if (!(gMercProfiles[s->ubProfile].ubMiscFlags & PROFILE_MISC_FLAG_SAID_HOSTILE_QUOTE))
2603 {
2604 ubMercsAvailable[ubNumMercsAvailable] = s->ubProfile;
2605 ubNumMercsAvailable++;
2606 }
2607 }
2608 }
2609
2610 if ( bTeam == CIV_TEAM && pSoldier->ubCivilianGroup != NON_CIV_GROUP && gTacticalStatus.fCivGroupHostile[ pSoldier->ubCivilianGroup ] == CIV_GROUP_NEUTRAL )
2611 {
2612 CivilianGroupMemberChangesSides( pSoldier );
2613 }
2614
2615 if (ubNumMercsAvailable > 0)
2616 {
2617 PauseAITemporarily();
2618 ubChosenMerc = (UINT8) Random( ubNumMercsAvailable );
2619 TriggerNPCWithIHateYouQuote( ubMercsAvailable[ ubChosenMerc ] );
2620 }
2621 else
2622 {
2623 // done... we should enter combat mode with this soldier's team starting,
2624 // after all the dialogue is completed
2625 NPCDoAction( ubNPC, NPC_ACTION_ENTER_COMBAT, 0 );
2626 }
2627 }
2628
2629
ActionIDForMovementRecord(UINT8 const ubNPC,UINT8 const record)2630 UINT8 ActionIDForMovementRecord(UINT8 const ubNPC, UINT8 const record)
2631 {
2632 // Check if we have a quote to trigger...
2633 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2634 if (!quotes) return FALSE; // error
2635
2636 switch (quotes[record].sActionData)
2637 {
2638 case NPC_ACTION_TRAVERSE_MAP_EAST: return QUOTE_ACTION_ID_TRAVERSE_EAST;
2639 case NPC_ACTION_TRAVERSE_MAP_SOUTH: return QUOTE_ACTION_ID_TRAVERSE_SOUTH;
2640 case NPC_ACTION_TRAVERSE_MAP_WEST: return QUOTE_ACTION_ID_TRAVERSE_WEST;
2641 case NPC_ACTION_TRAVERSE_MAP_NORTH: return QUOTE_ACTION_ID_TRAVERSE_NORTH;
2642 default: return QUOTE_ACTION_ID_CHECKFORDEST;
2643 }
2644 }
2645
2646
HandleNPCChangesForTacticalTraversal(const SOLDIERTYPE * s)2647 void HandleNPCChangesForTacticalTraversal(const SOLDIERTYPE* s)
2648 {
2649 if (!s || s->ubProfile == NO_PROFILE || s->fAIFlags & AI_CHECK_SCHEDULE)
2650 {
2651 return;
2652 }
2653
2654 MERCPROFILESTRUCT& p = GetProfile(s->ubProfile);
2655 switch (s->ubQuoteActionID)
2656 {
2657 case QUOTE_ACTION_ID_TRAVERSE_EAST: p.sSectorX++; break;
2658 case QUOTE_ACTION_ID_TRAVERSE_SOUTH: p.sSectorY++; break;
2659 case QUOTE_ACTION_ID_TRAVERSE_WEST: p.sSectorX--; break;
2660 case QUOTE_ACTION_ID_TRAVERSE_NORTH: p.sSectorY--; break;
2661 default: return;
2662 }
2663
2664 // Call to change the NPC's Sector Location
2665 ChangeNpcToDifferentSector(p, p.sSectorX, p.sSectorY, p.bSectorZ);
2666 }
2667
2668
HandleVictoryInNPCSector(INT16 const x,INT16 const y,INT16 const z)2669 void HandleVictoryInNPCSector(INT16 const x, INT16 const y, INT16 const z)
2670 {
2671 // handle special cases of victory in certain sector
2672
2673 // not the surface?..leave
2674 if (z != 0) return;
2675
2676 switch (SECTOR(x, y))
2677 {
2678 case SEC_F10:
2679 FOR_EACH_IN_TEAM(s, CIV_TEAM)
2680 {
2681 // hillbilies are still alive?..leave
2682 if (s->ubCivilianGroup == HICKS_CIV_GROUP && s->bLife > 0) return;
2683 }
2684
2685 // we won over the hillbillies
2686 // set fact they are dead
2687 if (!CheckFact(FACT_HILLBILLIES_KILLED, KEITH))
2688 {
2689 SetFactTrue(FACT_HILLBILLIES_KILLED);
2690 }
2691
2692 // check if keith is out of business
2693 if (CheckFact(FACT_KEITH_OUT_OF_BUSINESS, KEITH) == TRUE)
2694 {
2695 SetFactFalse(FACT_KEITH_OUT_OF_BUSINESS);
2696 }
2697 break;
2698 }
2699 }
2700
2701
HandleShopKeepHasBeenShutDown(UINT8 ubCharNum)2702 BOOLEAN HandleShopKeepHasBeenShutDown( UINT8 ubCharNum )
2703 {
2704 // check if shopkeep has been shutdown, if so handle
2705 switch( ubCharNum )
2706 {
2707 case( KEITH ):
2708 {
2709 // if keith out of business, do action and leave
2710 if( CheckFact( FACT_KEITH_OUT_OF_BUSINESS, KEITH ) == TRUE )
2711 {
2712 TriggerNPCRecord( KEITH, 11 );
2713
2714 return( TRUE );
2715 }
2716 else if( CheckFact( FACT_LOYALTY_LOW, KEITH ) == TRUE )
2717 {
2718 // loyalty is too low
2719 TriggerNPCRecord( KEITH, 7 );
2720
2721 return( TRUE );
2722 }
2723 }
2724 }
2725
2726 return( FALSE );
2727 }
2728
UpdateDarrelScriptToGoTo(SOLDIERTYPE * pSoldier)2729 void UpdateDarrelScriptToGoTo( SOLDIERTYPE * pSoldier )
2730 {
2731 // change destination in Darrel record 10 to go to a gridno adjacent to the
2732 // soldier's gridno, and destination in record 11
2733 const SOLDIERTYPE* const pDarrel = FindSoldierByProfileID(DARREL);
2734 if ( !pDarrel )
2735 {
2736 return;
2737 }
2738
2739 // find a spot to an alternate location nearby
2740 INT16 sAdjustedGridNo = FindGridNoFromSweetSpotExcludingSweetSpot(pDarrel, pSoldier->sGridNo, 5);
2741 if (sAdjustedGridNo == NOWHERE)
2742 {
2743 // yikes! try again with a bigger radius!
2744 sAdjustedGridNo = FindGridNoFromSweetSpotExcludingSweetSpot(pDarrel, pSoldier->sGridNo, 10);
2745 if (sAdjustedGridNo == NOWHERE)
2746 {
2747 // ok, now we're completely foobar
2748 return;
2749 }
2750 }
2751
2752 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(DARREL);
2753 quotes[10].usGoToGridno = sAdjustedGridNo;
2754 quotes[11].sRequiredGridno = -sAdjustedGridNo;
2755 quotes[11].ubTriggerNPC = pSoldier->ubProfile;
2756 }
2757
2758
RecordHasDialogue(UINT8 const ubNPC,UINT8 const ubRecord)2759 BOOLEAN RecordHasDialogue(UINT8 const ubNPC, UINT8 const ubRecord)
2760 {
2761 NPCQuoteInfo* const quotes = EnsureQuoteFileLoaded(ubNPC);
2762 if (!quotes) return FALSE; // error
2763 NPCQuoteInfo const& q = quotes[ubRecord];
2764 return q.ubQuoteNum != NO_QUOTE && q.ubQuoteNum != 0;
2765 }
2766
2767
FindCivQuoteFileIndex(INT16 const x,INT16 const y,INT16 const z)2768 static INT8 FindCivQuoteFileIndex(INT16 const x, INT16 const y, INT16 const z)
2769 {
2770 if (z > 0) return MINERS_CIV_QUOTE_INDEX;
2771
2772 for (UINT8 i = 0; i != NUM_CIVQUOTE_SECTORS; ++i)
2773 {
2774 if (gsCivQuoteSector[i][0] != x) continue;
2775 if (gsCivQuoteSector[i][1] != y) continue;
2776 return i;
2777 }
2778 return -1;
2779 }
2780
2781
ConsiderCivilianQuotes(INT16 const x,INT16 const y,INT16 const z,BOOLEAN const set_as_used)2782 INT8 ConsiderCivilianQuotes(INT16 const x, INT16 const y, INT16 const z, BOOLEAN const set_as_used)
2783 {
2784 INT8 const quote_file_idx = FindCivQuoteFileIndex(x, y, z);
2785 if (quote_file_idx == -1) return -1; // no hints for this sector
2786
2787 NPCQuoteInfo* const quotes = EnsureCivQuoteFileLoaded(quote_file_idx);
2788 if (!quotes) return -1; // error
2789
2790 for (INT8 i = 0; i != NUM_NPC_QUOTE_RECORDS; ++i)
2791 {
2792 if (!NPCConsiderQuote(NO_PROFILE, NO_PROFILE, APPROACH_NONE, i, 0, quotes)) continue;
2793 NPCQuoteInfo& q = quotes[i];
2794
2795 if (set_as_used)
2796 {
2797 TURN_FLAG_ON(q.fFlags, QUOTE_FLAG_SAID);
2798 }
2799
2800 if (q.ubStartQuest != NO_QUEST)
2801 {
2802 StartQuest(q.ubStartQuest, gWorldSectorX, gWorldSectorY);
2803 }
2804
2805 return q.ubQuoteNum;
2806 }
2807
2808 return -1;
2809 }
2810