1 /*
2 ** p_converstation.cpp
3 ** Implements Strife style conversation dialogs
4 **
5 **---------------------------------------------------------------------------
6 ** Copyright 2004-2008 Randy Heit
7 ** All rights reserved.
8 **
9 ** Redistribution and use in source and binary forms, with or without
10 ** modification, are permitted provided that the following conditions
11 ** are met:
12 **
13 ** 1. Redistributions of source code must retain the above copyright
14 ** notice, this list of conditions and the following disclaimer.
15 ** 2. Redistributions in binary form must reproduce the above copyright
16 ** notice, this list of conditions and the following disclaimer in the
17 ** documentation and/or other materials provided with the distribution.
18 ** 3. The name of the author may not be used to endorse or promote products
19 ** derived from this software without specific prior written permission.
20 **
21 ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 **---------------------------------------------------------------------------
32 **
33 */
34
35 #include <assert.h>
36
37 #include "actor.h"
38 #include "p_conversation.h"
39 #include "w_wad.h"
40 #include "cmdlib.h"
41 #include "s_sound.h"
42 #include "v_text.h"
43 #include "v_video.h"
44 #include "m_random.h"
45 #include "gi.h"
46 #include "templates.h"
47 #include "a_strifeglobal.h"
48 #include "a_keys.h"
49 #include "p_enemy.h"
50 #include "gstrings.h"
51 #include "sound/i_music.h"
52 #include "p_setup.h"
53 #include "d_net.h"
54 #include "g_level.h"
55 #include "d_event.h"
56 #include "d_gui.h"
57 #include "doomstat.h"
58 #include "c_console.h"
59 #include "sbar.h"
60 #include "farchive.h"
61 #include "p_lnspec.h"
62 #include "r_utility.h"
63 #include "p_local.h"
64 #include "menu/menu.h"
65
66 // The conversations as they exist inside a SCRIPTxx lump.
67 struct Response
68 {
69 SDWORD GiveType;
70 SDWORD Item[3];
71 SDWORD Count[3];
72 char Reply[32];
73 char Yes[80];
74 SDWORD Link;
75 DWORD Log;
76 char No[80];
77 };
78
79 struct Speech
80 {
81 DWORD SpeakerType;
82 SDWORD DropType;
83 SDWORD ItemCheck[3];
84 SDWORD Link;
85 char Name[16];
86 char Sound[8];
87 char Backdrop[8];
88 char Dialogue[320];
89 Response Responses[5];
90 };
91
92 // The Teaser version of the game uses an older version of the structure
93 struct TeaserSpeech
94 {
95 DWORD SpeakerType;
96 SDWORD DropType;
97 DWORD VoiceNumber;
98 char Name[16];
99 char Dialogue[320];
100 Response Responses[5];
101 };
102
103 static FRandom pr_randomspeech("RandomSpeech");
104
105 void GiveSpawner (player_t *player, const PClass *type);
106
107 TArray<FStrifeDialogueNode *> StrifeDialogues;
108
109 typedef TMap<int, int> FDialogueIDMap; // maps dialogue IDs to dialogue array index (for ACS)
110 typedef TMap<FName, int> FDialogueMap; // maps actor class names to dialogue array index
111
112 FClassMap StrifeTypes;
113 static FDialogueIDMap DialogueRoots;
114 static FDialogueMap ClassRoots;
115 static int ConversationMenuY;
116
117 static int ConversationPauseTic;
118 static bool ShowGold;
119
120 static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type);
121 static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, DWORD &prevSpeakerType);
122 static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, DWORD &prevSpeakerType);
123 static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses);
124 static bool DrawConversationMenu ();
125 static void PickConversationReply (int replyindex);
126 static void TerminalResponse (const char *str);
127
128 static FStrifeDialogueNode *PrevNode;
129
130 #define NUM_RANDOM_LINES 10
131 #define NUM_RANDOM_GOODBYES 3
132
133 //============================================================================
134 //
135 // GetStrifeType
136 //
137 // Given an item type number, returns the corresponding PClass.
138 //
139 //============================================================================
140
SetStrifeType(int convid,const PClass * Class)141 void SetStrifeType(int convid, const PClass *Class)
142 {
143 StrifeTypes[convid] = Class;
144 }
145
ClearStrifeTypes()146 void ClearStrifeTypes()
147 {
148 StrifeTypes.Clear();
149 }
150
SetConversation(int convid,const PClass * Class,int dlgindex)151 void SetConversation(int convid, const PClass *Class, int dlgindex)
152 {
153 if (convid != -1)
154 {
155 DialogueRoots[convid] = dlgindex;
156 }
157 if (Class != NULL)
158 {
159 ClassRoots[Class->TypeName] = dlgindex;
160 }
161 }
162
GetStrifeType(int typenum)163 const PClass *GetStrifeType (int typenum)
164 {
165 const PClass **ptype = StrifeTypes.CheckKey(typenum);
166 if (ptype == NULL) return NULL;
167 else return *ptype;
168 }
169
GetConversation(int conv_id)170 int GetConversation(int conv_id)
171 {
172 int *pindex = DialogueRoots.CheckKey(conv_id);
173 if (pindex == NULL) return -1;
174 else return *pindex;
175 }
176
GetConversation(FName classname)177 int GetConversation(FName classname)
178 {
179 int *pindex = ClassRoots.CheckKey(classname);
180 if (pindex == NULL) return -1;
181 else return *pindex;
182 }
183
184 //============================================================================
185 //
186 // P_LoadStrifeConversations
187 //
188 // Loads the SCRIPT00 and SCRIPTxx files for a corresponding map.
189 //
190 //============================================================================
191
P_LoadStrifeConversations(MapData * map,const char * mapname)192 void P_LoadStrifeConversations (MapData *map, const char *mapname)
193 {
194 P_FreeStrifeConversations ();
195 if (map->Size(ML_CONVERSATION) > 0)
196 {
197 map->Seek(ML_CONVERSATION);
198 LoadScriptFile (map->lumpnum, map->file, map->Size(ML_CONVERSATION), false, 0);
199 }
200 else
201 {
202 if (strnicmp (mapname, "MAP", 3) != 0)
203 {
204 return;
205 }
206 char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 };
207 char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 };
208
209 if (!LoadScriptFile(scriptname_t, false, 2))
210 {
211 if (!LoadScriptFile (scriptname_b, false, 1))
212 {
213 LoadScriptFile ("SCRIPT00", false, 1);
214 }
215 }
216 }
217 }
218
219 //============================================================================
220 //
221 // LoadScriptFile
222 //
223 // Loads a SCRIPTxx file and converts it into a more useful internal format.
224 //
225 //============================================================================
226
LoadScriptFile(const char * name,bool include,int type)227 bool LoadScriptFile (const char *name, bool include, int type)
228 {
229 int lumpnum = Wads.CheckNumForName (name);
230 FileReader *lump;
231
232 if (lumpnum < 0)
233 {
234 return false;
235 }
236 lump = Wads.ReopenLumpNum (lumpnum);
237
238 bool res = LoadScriptFile(lumpnum, lump, Wads.LumpLength(lumpnum), include, type);
239 delete lump;
240 return res;
241 }
242
LoadScriptFile(int lumpnum,FileReader * lump,int numnodes,bool include,int type)243 static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type)
244 {
245 int i;
246 DWORD prevSpeakerType;
247 FStrifeDialogueNode *node;
248 char buffer[4];
249
250 lump->Read(buffer, 4);
251 lump->Seek(-4, SEEK_CUR);
252
253 // The binary format is so primitive that this check is enough to detect it.
254 bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0);
255
256 if ((type == 1 && !isbinary) || (type == 2 && isbinary))
257 {
258 DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
259 return false;
260 }
261
262 if (!isbinary)
263 {
264 P_ParseUSDF(lumpnum, lump, numnodes);
265 }
266 else
267 {
268 if (!include)
269 {
270 LoadScriptFile("SCRIPT00", true, 1);
271 }
272 if (!(gameinfo.flags & GI_SHAREWARE))
273 {
274 // Strife scripts are always a multiple of 1516 bytes because each entry
275 // is exactly 1516 bytes long.
276 if (numnodes % 1516 != 0)
277 {
278 DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
279 return false;
280 }
281 numnodes /= 1516;
282 }
283 else
284 {
285 // And the teaser version has 1488-byte entries.
286 if (numnodes % 1488 != 0)
287 {
288 DPrintf("Incorrect data format for %s.", Wads.GetLumpFullName(lumpnum));
289 return false;
290 }
291 numnodes /= 1488;
292 }
293
294 prevSpeakerType = 0;
295
296 for (i = 0; i < numnodes; ++i)
297 {
298 if (!(gameinfo.flags & GI_SHAREWARE))
299 {
300 node = ReadRetailNode (lump, prevSpeakerType);
301 }
302 else
303 {
304 node = ReadTeaserNode (lump, prevSpeakerType);
305 }
306 node->ThisNodeNum = StrifeDialogues.Push(node);
307 }
308 }
309 return true;
310 }
311
312 //============================================================================
313 //
314 // ReadRetailNode
315 //
316 // Converts a single dialogue node from the Retail version of Strife.
317 //
318 //============================================================================
319
ReadRetailNode(FileReader * lump,DWORD & prevSpeakerType)320 static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, DWORD &prevSpeakerType)
321 {
322 FStrifeDialogueNode *node;
323 Speech speech;
324 char fullsound[16];
325 const PClass *type;
326 int j;
327
328 node = new FStrifeDialogueNode;
329
330 lump->Read (&speech, sizeof(speech));
331
332 // Byte swap all the ints in the original data
333 speech.SpeakerType = LittleLong(speech.SpeakerType);
334 speech.DropType = LittleLong(speech.DropType);
335 speech.Link = LittleLong(speech.Link);
336
337 // Assign the first instance of a conversation as the default for its
338 // actor, so newly spawned actors will use this conversation by default.
339 type = GetStrifeType (speech.SpeakerType);
340 node->SpeakerType = type;
341
342 if ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType)
343 {
344 if (type != NULL)
345 {
346 ClassRoots[type->TypeName] = StrifeDialogues.Size();
347 }
348 DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
349 prevSpeakerType = speech.SpeakerType;
350 }
351
352 // Convert the rest of the data to our own internal format.
353 node->Dialogue = ncopystring (speech.Dialogue);
354
355 // The speaker's portrait, if any.
356 speech.Dialogue[0] = 0; //speech.Backdrop[8] = 0;
357 node->Backdrop = TexMan.CheckForTexture (speech.Backdrop, FTexture::TEX_MiscPatch);
358
359 // The speaker's voice for this node, if any.
360 speech.Backdrop[0] = 0; //speech.Sound[8] = 0;
361 mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound);
362 node->SpeakerVoice = fullsound;
363
364 // The speaker's name, if any.
365 speech.Sound[0] = 0; //speech.Name[16] = 0;
366 node->SpeakerName = ncopystring (speech.Name);
367
368 // The item the speaker should drop when killed.
369 node->DropType = GetStrifeType (speech.DropType);
370
371 // Items you need to have to make the speaker use a different node.
372 node->ItemCheck.Resize(3);
373 for (j = 0; j < 3; ++j)
374 {
375 node->ItemCheck[j].Item = GetStrifeType (speech.ItemCheck[j]);
376 node->ItemCheck[j].Amount = -1;
377 }
378 node->ItemCheckNode = speech.Link;
379 node->Children = NULL;
380
381 ParseReplies (&node->Children, &speech.Responses[0]);
382
383 return node;
384 }
385
386 //============================================================================
387 //
388 // ReadTeaserNode
389 //
390 // Converts a single dialogue node from the Teaser version of Strife.
391 //
392 //============================================================================
393
ReadTeaserNode(FileReader * lump,DWORD & prevSpeakerType)394 static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, DWORD &prevSpeakerType)
395 {
396 FStrifeDialogueNode *node;
397 TeaserSpeech speech;
398 char fullsound[16];
399 const PClass *type;
400 int j;
401
402 node = new FStrifeDialogueNode;
403
404 lump->Read (&speech, sizeof(speech));
405
406 // Byte swap all the ints in the original data
407 speech.SpeakerType = LittleLong(speech.SpeakerType);
408 speech.DropType = LittleLong(speech.DropType);
409
410 // Assign the first instance of a conversation as the default for its
411 // actor, so newly spawned actors will use this conversation by default.
412 type = GetStrifeType (speech.SpeakerType);
413 node->SpeakerType = type;
414
415 if ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType)
416 {
417 if (type != NULL)
418 {
419 ClassRoots[type->TypeName] = StrifeDialogues.Size();
420 }
421 DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
422 prevSpeakerType = speech.SpeakerType;
423 }
424
425 // Convert the rest of the data to our own internal format.
426 node->Dialogue = ncopystring (speech.Dialogue);
427
428 // The Teaser version doesn't have portraits.
429 node->Backdrop.SetInvalid();
430
431 // The speaker's voice for this node, if any.
432 if (speech.VoiceNumber != 0)
433 {
434 mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber);
435 node->SpeakerVoice = fullsound;
436 }
437 else
438 {
439 node->SpeakerVoice = 0;
440 }
441
442 // The speaker's name, if any.
443 speech.Dialogue[0] = 0; //speech.Name[16] = 0;
444 node->SpeakerName = ncopystring (speech.Name);
445
446 // The item the speaker should drop when killed.
447 node->DropType = GetStrifeType (speech.DropType);
448
449 // Items you need to have to make the speaker use a different node.
450 node->ItemCheck.Resize(3);
451 for (j = 0; j < 3; ++j)
452 {
453 node->ItemCheck[j].Item = NULL;
454 node->ItemCheck[j].Amount = -1;
455 }
456 node->ItemCheckNode = 0;
457 node->Children = NULL;
458
459 ParseReplies (&node->Children, &speech.Responses[0]);
460
461 return node;
462 }
463
464 //============================================================================
465 //
466 // ParseReplies
467 //
468 // Convert PC responses. Rather than being stored inside the main node, they
469 // hang off it as a singly-linked list, so no space is wasted on replies that
470 // don't even matter.
471 //
472 //============================================================================
473
ParseReplies(FStrifeDialogueReply ** replyptr,Response * responses)474 static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses)
475 {
476 FStrifeDialogueReply *reply;
477 int j, k;
478
479 // Byte swap first.
480 for (j = 0; j < 5; ++j)
481 {
482 responses[j].GiveType = LittleLong(responses[j].GiveType);
483 responses[j].Link = LittleLong(responses[j].Link);
484 responses[j].Log = LittleLong(responses[j].Log);
485 for (k = 0; k < 3; ++k)
486 {
487 responses[j].Item[k] = LittleLong(responses[j].Item[k]);
488 responses[j].Count[k] = LittleLong(responses[j].Count[k]);
489 }
490 }
491
492 for (j = 0; j < 5; ++j)
493 {
494 Response *rsp = &responses[j];
495
496 // If the reply has no text and goes nowhere, then it doesn't
497 // need to be remembered.
498 if (rsp->Reply[0] == 0 && rsp->Link == 0)
499 {
500 continue;
501 }
502 reply = new FStrifeDialogueReply;
503
504 // The next node to use when this reply is chosen.
505 reply->NextNode = rsp->Link;
506
507 // The message to record in the log for this reply.
508 reply->LogNumber = rsp->Log;
509 reply->LogString = NULL;
510
511 // The item to receive when this reply is used.
512 reply->GiveType = GetStrifeType (rsp->GiveType);
513 reply->ActionSpecial = 0;
514
515 // Do you need anything special for this reply to succeed?
516 reply->ItemCheck.Resize(3);
517 for (k = 0; k < 3; ++k)
518 {
519 reply->ItemCheck[k].Item = GetStrifeType (rsp->Item[k]);
520 reply->ItemCheck[k].Amount = rsp->Count[k];
521 }
522
523 // If the first item check has a positive amount required, then
524 // add that to the reply string. Otherwise, use the reply as-is.
525 reply->Reply = copystring (rsp->Reply);
526 reply->NeedsGold = (rsp->Count[0] > 0);
527
528 // QuickYes messages are shown when you meet the item checks.
529 // QuickNo messages are shown when you don't.
530 if (rsp->Yes[0] == '_' && rsp->Yes[1] == 0)
531 {
532 reply->QuickYes = NULL;
533 }
534 else
535 {
536 reply->QuickYes = ncopystring (rsp->Yes);
537 }
538 if (reply->ItemCheck[0].Item != 0)
539 {
540 reply->QuickNo = ncopystring (rsp->No);
541 }
542 else
543 {
544 reply->QuickNo = NULL;
545 }
546 reply->Next = *replyptr;
547 *replyptr = reply;
548 replyptr = &reply->Next;
549 }
550 }
551
552 //============================================================================
553 //
554 // FStrifeDialogueNode :: ~FStrifeDialogueNode
555 //
556 //============================================================================
557
~FStrifeDialogueNode()558 FStrifeDialogueNode::~FStrifeDialogueNode ()
559 {
560 if (SpeakerName != NULL) delete[] SpeakerName;
561 if (Dialogue != NULL) delete[] Dialogue;
562 FStrifeDialogueReply *tokill = Children;
563 while (tokill != NULL)
564 {
565 FStrifeDialogueReply *next = tokill->Next;
566 delete tokill;
567 tokill = next;
568 }
569 }
570
571 //============================================================================
572 //
573 // FStrifeDialogueReply :: ~FStrifeDialogueReply
574 //
575 //============================================================================
576
~FStrifeDialogueReply()577 FStrifeDialogueReply::~FStrifeDialogueReply ()
578 {
579 if (Reply != NULL) delete[] Reply;
580 if (QuickYes != NULL) delete[] QuickYes;
581 if (QuickNo != NULL) delete[] QuickNo;
582 }
583
584 //============================================================================
585 //
586 // FindNode
587 //
588 // Returns the index that matches the given conversation node.
589 //
590 //============================================================================
591
FindNode(const FStrifeDialogueNode * node)592 static int FindNode (const FStrifeDialogueNode *node)
593 {
594 int rootnode = 0;
595
596 while (StrifeDialogues[rootnode] != node)
597 {
598 rootnode++;
599 }
600 return rootnode;
601 }
602
603 //============================================================================
604 //
605 // CheckStrifeItem
606 //
607 // Checks if you have an item. A NULL itemtype is always considered to be
608 // present.
609 //
610 //============================================================================
611
CheckStrifeItem(player_t * player,const PClass * itemtype,int amount=-1)612 static bool CheckStrifeItem (player_t *player, const PClass *itemtype, int amount=-1)
613 {
614 AInventory *item;
615
616 if (itemtype == NULL || amount == 0)
617 return true;
618
619 item = player->ConversationPC->FindInventory (itemtype);
620 if (item == NULL)
621 return false;
622
623 return amount < 0 || item->Amount >= amount;
624 }
625
626 //============================================================================
627 //
628 // TakeStrifeItem
629 //
630 // Takes away some of an item, unless that item is special and should not
631 // be removed.
632 //
633 //============================================================================
634
TakeStrifeItem(player_t * player,const PClass * itemtype,int amount)635 static void TakeStrifeItem (player_t *player, const PClass *itemtype, int amount)
636 {
637 if (itemtype == NULL || amount == 0)
638 return;
639
640 // Don't take quest items.
641 if (itemtype->IsDescendantOf (PClass::FindClass(NAME_QuestItem)))
642 return;
643
644 // Don't take keys.
645 if (itemtype->IsDescendantOf (RUNTIME_CLASS(AKey)))
646 return;
647
648 // Don't take the sigil.
649 if (itemtype == RUNTIME_CLASS(ASigil))
650 return;
651
652 player->mo->TakeInventory(itemtype, amount);
653 }
654
655 CUSTOM_CVAR(Float, dlg_musicvolume, 1.0f, CVAR_ARCHIVE)
656 {
657 if (self < 0.f) self = 0.f;
658 else if (self > 1.f) self = 1.f;
659 }
660
661 //============================================================================
662 //
663 // The conversation menu
664 //
665 //============================================================================
666
667 class DConversationMenu : public DMenu
668 {
669 DECLARE_CLASS(DConversationMenu, DMenu)
670
671 FString mSpeaker;
672 FBrokenLines *mDialogueLines;
673 TArray<FString> mResponseLines;
674 TArray<unsigned int> mResponses;
675 bool mShowGold;
676 FStrifeDialogueNode *mCurNode;
677 int mYpos;
678
679 public:
680 static int mSelection;
681
682 //=============================================================================
683 //
684 //
685 //
686 //=============================================================================
687
DConversationMenu(FStrifeDialogueNode * CurNode)688 DConversationMenu(FStrifeDialogueNode *CurNode)
689 {
690 mCurNode = CurNode;
691 mDialogueLines = NULL;
692 mShowGold = false;
693
694 // Format the speaker's message.
695 const char * toSay = CurNode->Dialogue;
696 if (strncmp (toSay, "RANDOM_", 7) == 0)
697 {
698 FString dlgtext;
699
700 dlgtext.Format("TXT_%s_%02d", toSay, 1+(pr_randomspeech() % NUM_RANDOM_LINES));
701 toSay = GStrings[dlgtext];
702 if (toSay == NULL)
703 {
704 toSay = GStrings["TXT_GOAWAY"]; // Ok, it's lame - but it doesn't look like an error to the player. ;)
705 }
706 }
707 else
708 {
709 // handle string table replacement
710 if (toSay[0] == '$')
711 {
712 toSay = GStrings(toSay + 1);
713 }
714 }
715 if (toSay == NULL)
716 {
717 toSay = ".";
718 }
719 mDialogueLines = V_BreakLines (SmallFont, screen->GetWidth()/CleanXfac - 24*2, toSay);
720
721 FStrifeDialogueReply *reply;
722 int i,j;
723 for (reply = CurNode->Children, i = 1; reply != NULL; reply = reply->Next)
724 {
725 if (reply->Reply == NULL)
726 {
727 continue;
728 }
729 mShowGold |= reply->NeedsGold;
730
731 const char *ReplyText = reply->Reply;
732 if (ReplyText[0] == '$')
733 {
734 ReplyText = GStrings(ReplyText + 1);
735 }
736 FString ReplyString = ReplyText;
737 if (reply->NeedsGold) ReplyString.AppendFormat(" for %u", reply->ItemCheck[0].Amount);
738
739 FBrokenLines *ReplyLines = V_BreakLines (SmallFont, 320-50-10, ReplyString);
740
741 mResponses.Push(mResponseLines.Size());
742 for (j = 0; ReplyLines[j].Width >= 0; ++j)
743 {
744 mResponseLines.Push(ReplyLines[j].Text);
745 }
746 ++i;
747 V_FreeBrokenLines (ReplyLines);
748 }
749 char goodbye[25];
750 mysnprintf(goodbye, countof(goodbye), "TXT_RANDOMGOODBYE_%d", 1+(pr_randomspeech() % NUM_RANDOM_GOODBYES));
751 const char *goodbyestr = GStrings[goodbye];
752 if (goodbyestr == NULL) goodbyestr = "Bye.";
753 mResponses.Push(mResponseLines.Size());
754 mResponseLines.Push(FString(goodbyestr));
755
756 // Determine where the top of the reply list should be positioned.
757 i = OptionSettings.mLinespacing;
758 mYpos = MIN<int> (140, 192 - mResponseLines.Size() * i);
759 for (i = 0; mDialogueLines[i].Width >= 0; ++i)
760 { }
761 i = 44 + i * 10;
762 if (mYpos - 100 < i - screen->GetHeight() / CleanYfac / 2)
763 {
764 mYpos = i - screen->GetHeight() / CleanYfac / 2 + 100;
765 }
766 ConversationMenuY = mYpos;
767 //ConversationMenu.indent = 50;
768 }
769
770 //=============================================================================
771 //
772 //
773 //
774 //=============================================================================
775
Destroy()776 void Destroy()
777 {
778 V_FreeBrokenLines(mDialogueLines);
779 mDialogueLines = NULL;
780 I_SetMusicVolume (1.f);
781 }
782
DimAllowed()783 bool DimAllowed()
784 {
785 return false;
786 }
787
788 //=============================================================================
789 //
790 //
791 //
792 //=============================================================================
793
MenuEvent(int mkey,bool fromcontroller)794 bool MenuEvent(int mkey, bool fromcontroller)
795 {
796 if (demoplayback)
797 { // During demo playback, don't let the user do anything besides close this menu.
798 if (mkey == MKEY_Back)
799 {
800 Close();
801 return true;
802 }
803 return false;
804 }
805 if (mkey == MKEY_Up)
806 {
807 if (--mSelection < 0) mSelection = mResponses.Size() - 1;
808 return true;
809 }
810 else if (mkey == MKEY_Down)
811 {
812 if (++mSelection >= (int)mResponses.Size()) mSelection = 0;
813 return true;
814 }
815 else if (mkey == MKEY_Back)
816 {
817 Net_WriteByte (DEM_CONVNULL);
818 Close();
819 return true;
820 }
821 else if (mkey == MKEY_Enter)
822 {
823 if ((unsigned)mSelection >= mResponses.Size())
824 {
825 Net_WriteByte(DEM_CONVCLOSE);
826 }
827 else
828 {
829 // Send dialogue and reply numbers across the wire.
830 assert((unsigned)mCurNode->ThisNodeNum < StrifeDialogues.Size());
831 assert(StrifeDialogues[mCurNode->ThisNodeNum] == mCurNode);
832 Net_WriteByte(DEM_CONVREPLY);
833 Net_WriteWord(mCurNode->ThisNodeNum);
834 Net_WriteByte(mSelection);
835 }
836 Close();
837 return true;
838 }
839 return false;
840 }
841
842 //=============================================================================
843 //
844 //
845 //
846 //=============================================================================
847
MouseEvent(int type,int x,int y)848 bool MouseEvent(int type, int x, int y)
849 {
850 int sel = -1;
851 int fh = OptionSettings.mLinespacing;
852
853 // convert x/y from screen to virtual coordinates, according to CleanX/Yfac use in DrawTexture
854 x = ((x - (screen->GetWidth() / 2)) / CleanXfac) + 160;
855 y = ((y - (screen->GetHeight() / 2)) / CleanYfac) + 100;
856
857 if (x >= 24 && x <= 320-24 && y >= mYpos && y < mYpos + fh * (int)mResponseLines.Size())
858 {
859 sel = (y - mYpos) / fh;
860 for(unsigned i=0;i<mResponses.Size(); i++)
861 {
862 if ((int)mResponses[i] > sel)
863 {
864 sel = i-1;
865 break;
866 }
867 }
868 }
869 if (sel != -1 && sel != mSelection)
870 {
871 //S_Sound (CHAN_VOICE | CHAN_UI, "menu/cursor", snd_menuvolume, ATTN_NONE);
872 }
873 mSelection = sel;
874 if (type == MOUSE_Release)
875 {
876 return MenuEvent(MKEY_Enter, true);
877 }
878 return true;
879 }
880
881
882 //=============================================================================
883 //
884 //
885 //
886 //=============================================================================
887
Responder(event_t * ev)888 bool Responder(event_t *ev)
889 {
890 if (demoplayback)
891 { // No interaction during demo playback
892 return false;
893 }
894 if (ev->type == EV_GUI_Event && ev->subtype == EV_GUI_Char && ev->data1 >= '0' && ev->data1 <= '9')
895 { // Activate an item of type numberedmore (dialogue only)
896 mSelection = ev->data1 == '0' ? 9 : ev->data1 - '1';
897 return MenuEvent(MKEY_Enter, false);
898 }
899 return Super::Responder(ev);
900 }
901
902 //============================================================================
903 //
904 // DrawConversationMenu
905 //
906 //============================================================================
907
Drawer()908 void Drawer()
909 {
910 const char *speakerName;
911 int x, y, linesize;
912 int width, fontheight;
913
914 player_t *cp = &players[consoleplayer];
915
916 assert (mDialogueLines != NULL);
917 assert (mCurNode != NULL);
918
919 FStrifeDialogueNode *CurNode = mCurNode;
920
921 if (CurNode == NULL)
922 {
923 Close ();
924 return;
925 }
926
927 // [CW] Freeze the game depending on MAPINFO options.
928 if (ConversationPauseTic < gametic && !multiplayer && !(level.flags2 & LEVEL2_CONV_SINGLE_UNFREEZE))
929 {
930 menuactive = MENU_On;
931 }
932
933 if (CurNode->Backdrop.isValid())
934 {
935 screen->DrawTexture (TexMan(CurNode->Backdrop), 0, 0, DTA_320x200, true, TAG_DONE);
936 }
937 x = 16 * screen->GetWidth() / 320;
938 y = 16 * screen->GetHeight() / 200;
939 linesize = 10 * CleanYfac;
940
941 // Who is talking to you?
942 if (CurNode->SpeakerName != NULL)
943 {
944 speakerName = CurNode->SpeakerName;
945 if (speakerName[0] == '$') speakerName = GStrings(speakerName+1);
946 }
947 else
948 {
949 speakerName = cp->ConversationNPC->GetTag("Person");
950 }
951
952 // Dim the screen behind the dialogue (but only if there is no backdrop).
953 if (!CurNode->Backdrop.isValid())
954 {
955 int i;
956 for (i = 0; mDialogueLines[i].Width >= 0; ++i)
957 { }
958 screen->Dim (0, 0.45f, 14 * screen->GetWidth() / 320, 13 * screen->GetHeight() / 200,
959 308 * screen->GetWidth() / 320 - 14 * screen->GetWidth () / 320,
960 speakerName == NULL ? linesize * i + 6 * CleanYfac
961 : linesize * i + 6 * CleanYfac + linesize * 3/2);
962 }
963
964 // Dim the screen behind the PC's choices.
965
966 screen->Dim (0, 0.45f, (24-160) * CleanXfac + screen->GetWidth()/2,
967 (mYpos - 2 - 100) * CleanYfac + screen->GetHeight()/2,
968 272 * CleanXfac,
969 MIN<int>(mResponseLines.Size() * OptionSettings.mLinespacing + 4, 200 - mYpos) * CleanYfac);
970
971 if (speakerName != NULL)
972 {
973 screen->DrawText (SmallFont, CR_WHITE, x, y, speakerName,
974 DTA_CleanNoMove, true, TAG_DONE);
975 y += linesize * 3 / 2;
976 }
977 x = 24 * screen->GetWidth() / 320;
978 for (int i = 0; mDialogueLines[i].Width >= 0; ++i)
979 {
980 screen->DrawText (SmallFont, CR_UNTRANSLATED, x, y, mDialogueLines[i].Text,
981 DTA_CleanNoMove, true, TAG_DONE);
982 y += linesize;
983 }
984
985 if (ShowGold)
986 {
987 AInventory *coin = cp->ConversationPC->FindInventory (RUNTIME_CLASS(ACoin));
988 char goldstr[32];
989
990 mysnprintf (goldstr, countof(goldstr), "%d", coin != NULL ? coin->Amount : 0);
991 screen->DrawText (SmallFont, CR_GRAY, 21, 191, goldstr, DTA_320x200, true,
992 DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
993 screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
994 3, 190, DTA_320x200, true,
995 DTA_FillColor, 0, DTA_Alpha, HR_SHADOW, TAG_DONE);
996 screen->DrawText (SmallFont, CR_GRAY, 20, 190, goldstr, DTA_320x200, true, TAG_DONE);
997 screen->DrawTexture (TexMan(((AInventory *)GetDefaultByType (RUNTIME_CLASS(ACoin)))->Icon),
998 2, 189, DTA_320x200, true, TAG_DONE);
999 }
1000
1001 y = mYpos;
1002 fontheight = OptionSettings.mLinespacing;
1003
1004 int response = 0;
1005 for (unsigned i = 0; i < mResponseLines.Size(); i++, y += fontheight)
1006 {
1007 width = SmallFont->StringWidth(mResponseLines[i]);
1008 x = 64;
1009
1010 screen->DrawText (SmallFont, CR_GREEN, x, y, mResponseLines[i], DTA_Clean, true, TAG_DONE);
1011
1012 if (i == mResponses[response])
1013 {
1014 char tbuf[16];
1015
1016 response++;
1017 mysnprintf (tbuf, countof(tbuf), "%d.", response);
1018 x = 50 - SmallFont->StringWidth (tbuf);
1019 screen->DrawText (SmallFont, CR_GREY, x, y, tbuf, DTA_Clean, true, TAG_DONE);
1020
1021 if (response == mSelection+1)
1022 {
1023 int color = ((DMenu::MenuTime%8) < 4) || DMenu::CurrentMenu != this ? CR_RED:CR_GREY;
1024
1025 x = (50 + 3 - 160) * CleanXfac + screen->GetWidth() / 2;
1026 int yy = (y + fontheight/2 - 5 - 100) * CleanYfac + screen->GetHeight() / 2;
1027 screen->DrawText (ConFont, color, x, yy, "\xd",
1028 DTA_CellX, 8 * CleanXfac,
1029 DTA_CellY, 8 * CleanYfac,
1030 TAG_DONE);
1031 }
1032 }
1033 }
1034 }
1035
1036 };
1037
1038 IMPLEMENT_ABSTRACT_CLASS(DConversationMenu)
1039 int DConversationMenu::mSelection; // needs to be preserved if the same dialogue is restarted
1040
1041
1042 //============================================================================
1043 //
1044 // P_FreeStrifeConversations
1045 //
1046 //============================================================================
1047
P_FreeStrifeConversations()1048 void P_FreeStrifeConversations ()
1049 {
1050 FStrifeDialogueNode *node;
1051
1052 while (StrifeDialogues.Pop (node))
1053 {
1054 delete node;
1055 }
1056
1057 DialogueRoots.Clear();
1058 ClassRoots.Clear();
1059
1060 PrevNode = NULL;
1061 if (DMenu::CurrentMenu != NULL && DMenu::CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
1062 {
1063 DMenu::CurrentMenu->Close();
1064 }
1065 }
1066
1067 //============================================================================
1068 //
1069 // P_StartConversation
1070 //
1071 // Begins a conversation between a PC and NPC.
1072 //
1073 //============================================================================
1074
P_StartConversation(AActor * npc,AActor * pc,bool facetalker,bool saveangle)1075 void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveangle)
1076 {
1077 AActor *oldtarget;
1078 int i;
1079
1080 // Make sure this is actually a player.
1081 if (pc->player == NULL) return;
1082
1083 // [CW] If an NPC is talking to a PC already, then don't let
1084 // anyone else talk to the NPC.
1085 for (i = 0; i < MAXPLAYERS; i++)
1086 {
1087 if (!playeringame[i] || pc->player == &players[i])
1088 continue;
1089
1090 if (npc == players[i].ConversationNPC)
1091 return;
1092 }
1093
1094 pc->velx = pc->vely = 0; // Stop moving
1095 pc->player->velx = pc->player->vely = 0;
1096 static_cast<APlayerPawn*>(pc)->PlayIdle ();
1097
1098 pc->player->ConversationPC = pc;
1099 pc->player->ConversationNPC = npc;
1100 npc->flags5 |= MF5_INCONVERSATION;
1101
1102 FStrifeDialogueNode *CurNode = npc->Conversation;
1103
1104 if (pc->player == &players[consoleplayer])
1105 {
1106 S_Sound (CHAN_VOICE | CHAN_UI, gameinfo.chatSound, 1, ATTN_NONE);
1107 }
1108
1109 npc->reactiontime = 2;
1110 pc->player->ConversationFaceTalker = facetalker;
1111 if (saveangle)
1112 {
1113 pc->player->ConversationNPCAngle = npc->angle;
1114 }
1115 oldtarget = npc->target;
1116 npc->target = pc;
1117 if (facetalker)
1118 {
1119 A_FaceTarget (npc);
1120 pc->angle = pc->AngleTo(npc);
1121 }
1122 if ((npc->flags & MF_FRIENDLY) || (npc->flags4 & MF4_NOHATEPLAYERS))
1123 {
1124 npc->target = oldtarget;
1125 }
1126
1127 // Check if we should jump to another node
1128 while (CurNode->ItemCheck.Size() > 0 && CurNode->ItemCheck[0].Item != NULL)
1129 {
1130 bool jump = true;
1131 for (i = 0; i < (int)CurNode->ItemCheck.Size(); ++i)
1132 {
1133 if(!CheckStrifeItem (pc->player, CurNode->ItemCheck[i].Item, CurNode->ItemCheck[i].Amount))
1134 {
1135 jump = false;
1136 break;
1137 }
1138 }
1139 if (jump && CurNode->ItemCheckNode > 0)
1140 {
1141 int root = pc->player->ConversationNPC->ConversationRoot;
1142 CurNode = StrifeDialogues[root + CurNode->ItemCheckNode - 1];
1143 }
1144 else
1145 {
1146 break;
1147 }
1148 }
1149
1150 // The rest is only done when the conversation is actually displayed.
1151 if (pc->player == &players[consoleplayer])
1152 {
1153 if (CurNode->SpeakerVoice != 0)
1154 {
1155 I_SetMusicVolume (dlg_musicvolume);
1156 S_Sound (npc, CHAN_VOICE|CHAN_NOPAUSE, CurNode->SpeakerVoice, 1, ATTN_NORM);
1157 }
1158
1159 DConversationMenu *cmenu = new DConversationMenu(CurNode);
1160
1161
1162 if (CurNode != PrevNode)
1163 { // Only reset the selection if showing a different menu.
1164 DConversationMenu::mSelection = 0;
1165 PrevNode = CurNode;
1166 }
1167
1168 // And open the menu
1169 M_StartControlPanel (false);
1170 M_ActivateMenu(cmenu);
1171 ConversationPauseTic = gametic + 20;
1172 menuactive = MENU_OnNoPause;
1173 }
1174 }
1175
1176 //============================================================================
1177 //
1178 // P_ResumeConversation
1179 //
1180 // Resumes a conversation that was interrupted by a slideshow.
1181 //
1182 //============================================================================
1183
P_ResumeConversation()1184 void P_ResumeConversation ()
1185 {
1186 for (int i = 0; i < MAXPLAYERS; i++)
1187 {
1188 if (!playeringame[i])
1189 continue;
1190
1191 player_t *p = &players[i];
1192
1193 if (p->ConversationPC != NULL && p->ConversationNPC != NULL)
1194 {
1195 P_StartConversation (p->ConversationNPC, p->ConversationPC, p->ConversationFaceTalker, false);
1196 }
1197 }
1198 }
1199
1200 //============================================================================
1201 //
1202 // HandleReply
1203 //
1204 // Run by the netcode on all machines.
1205 //
1206 //============================================================================
1207
HandleReply(player_t * player,bool isconsole,int nodenum,int replynum)1208 static void HandleReply(player_t *player, bool isconsole, int nodenum, int replynum)
1209 {
1210 const char *replyText = NULL;
1211 FStrifeDialogueReply *reply;
1212 FStrifeDialogueNode *node;
1213 AActor *npc;
1214 bool takestuff;
1215 int i;
1216
1217 if (player->ConversationNPC == NULL || (unsigned)nodenum >= StrifeDialogues.Size())
1218 {
1219 return;
1220 }
1221
1222 // Find the reply.
1223 node = StrifeDialogues[nodenum];
1224 for (i = 0, reply = node->Children; reply != NULL && i != replynum; ++i, reply = reply->Next)
1225 { }
1226 npc = player->ConversationNPC;
1227 if (reply == NULL)
1228 {
1229 // The default reply was selected
1230 npc->angle = player->ConversationNPCAngle;
1231 npc->flags5 &= ~MF5_INCONVERSATION;
1232 return;
1233 }
1234
1235 // Check if you have the requisite items for this choice
1236 for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
1237 {
1238 if (!CheckStrifeItem(player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount))
1239 {
1240 // No, you don't. Say so and let the NPC animate negatively.
1241 if (reply->QuickNo && isconsole)
1242 {
1243 TerminalResponse(reply->QuickNo);
1244 }
1245 npc->ConversationAnimation(2);
1246 npc->angle = player->ConversationNPCAngle;
1247 npc->flags5 &= ~MF5_INCONVERSATION;
1248 return;
1249 }
1250 }
1251
1252 // Yay, you do! Let the NPC animate affirmatively.
1253 npc->ConversationAnimation(1);
1254
1255 // If this reply gives you something, then try to receive it.
1256 takestuff = true;
1257 if (reply->GiveType != NULL)
1258 {
1259 if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AInventory)))
1260 {
1261 if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AWeapon)))
1262 {
1263 if (player->mo->FindInventory(reply->GiveType) != NULL)
1264 {
1265 takestuff = false;
1266 }
1267 }
1268
1269 if (takestuff)
1270 {
1271 AInventory *item = static_cast<AInventory *>(Spawn(reply->GiveType, 0, 0, 0, NO_REPLACE));
1272 // Items given here should not count as items!
1273 item->ClearCounters();
1274 if (item->GetClass()->TypeName == NAME_FlameThrower)
1275 {
1276 // The flame thrower gives less ammo when given in a dialog
1277 static_cast<AWeapon*>(item)->AmmoGive1 = 40;
1278 }
1279 item->flags |= MF_DROPPED;
1280 if (!item->CallTryPickup(player->mo))
1281 {
1282 item->Destroy();
1283 takestuff = false;
1284 }
1285 }
1286
1287 if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(ASlideshowStarter)))
1288 gameaction = ga_slideshow;
1289 }
1290 else
1291 {
1292 // Trying to give a non-inventory item.
1293 takestuff = false;
1294 if (isconsole)
1295 {
1296 Printf("Attempting to give non-inventory item %s\n", reply->GiveType->TypeName.GetChars());
1297 }
1298 }
1299 }
1300
1301 if (reply->ActionSpecial != 0)
1302 {
1303 takestuff |= !!P_ExecuteSpecial(reply->ActionSpecial, NULL, player->mo, false,
1304 reply->Args[0], reply->Args[1], reply->Args[2], reply->Args[3], reply->Args[4]);
1305 }
1306
1307 // Take away required items if the give was successful or none was needed.
1308 if (takestuff)
1309 {
1310 for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
1311 {
1312 TakeStrifeItem (player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount);
1313 }
1314 replyText = reply->QuickYes;
1315 }
1316 else
1317 {
1318 replyText = "$txt_haveenough";
1319 }
1320
1321 // Update the quest log, if needed.
1322 if (reply->LogString != NULL)
1323 {
1324 const char *log = reply->LogString;
1325 if (log[0] == '$')
1326 {
1327 log = GStrings(log + 1);
1328 }
1329
1330 player->SetLogText(log);
1331 }
1332 else if (reply->LogNumber != 0)
1333 {
1334 player->SetLogNumber(reply->LogNumber);
1335 }
1336
1337 if (replyText != NULL && isconsole)
1338 {
1339 TerminalResponse(replyText);
1340 }
1341
1342 // Does this reply alter the speaker's conversation node? If NextNode is positive,
1343 // the next time they talk, they will show the new node. If it is negative, then they
1344 // will show the new node right away without terminating the dialogue.
1345 if (reply->NextNode != 0)
1346 {
1347 int rootnode = npc->ConversationRoot;
1348 const bool isNegative = reply->NextNode < 0;
1349 const unsigned next = (unsigned)(rootnode + (isNegative ? -1 : 1) * reply->NextNode - 1);
1350
1351 if (next < StrifeDialogues.Size())
1352 {
1353 npc->Conversation = StrifeDialogues[next];
1354
1355 if (isNegative)
1356 {
1357 if (gameaction != ga_slideshow)
1358 {
1359 P_StartConversation (npc, player->mo, player->ConversationFaceTalker, false);
1360 return;
1361 }
1362 else
1363 {
1364 S_StopSound (npc, CHAN_VOICE);
1365 }
1366 }
1367 }
1368 else
1369 {
1370 Printf ("Next node %u is invalid, no such dialog page\n", next);
1371 }
1372 }
1373
1374 npc->angle = player->ConversationNPCAngle;
1375
1376 // [CW] Set these to NULL because we're not using to them
1377 // anymore. However, this can interfere with slideshows
1378 // so we don't set them to NULL in that case.
1379 if (gameaction != ga_slideshow)
1380 {
1381 npc->flags5 &= ~MF5_INCONVERSATION;
1382 player->ConversationFaceTalker = false;
1383 player->ConversationNPC = NULL;
1384 player->ConversationPC = NULL;
1385 player->ConversationNPCAngle = 0;
1386 }
1387
1388 if (isconsole)
1389 {
1390 I_SetMusicVolume (1.f);
1391 }
1392 }
1393
1394 //============================================================================
1395 //
1396 // P_ConversationCommand
1397 //
1398 // Complete a conversation command.
1399 //
1400 //============================================================================
1401
P_ConversationCommand(int netcode,int pnum,BYTE ** stream)1402 void P_ConversationCommand (int netcode, int pnum, BYTE **stream)
1403 {
1404 player_t *player = &players[pnum];
1405
1406 // The conversation menus are normally closed by the menu code, but that
1407 // doesn't happen during demo playback, so we need to do it here.
1408 if (demoplayback && DMenu::CurrentMenu != NULL &&
1409 DMenu::CurrentMenu->IsKindOf(RUNTIME_CLASS(DConversationMenu)))
1410 {
1411 DMenu::CurrentMenu->Close();
1412 }
1413 if (netcode == DEM_CONVREPLY)
1414 {
1415 int nodenum = ReadWord(stream);
1416 int replynum = ReadByte(stream);
1417 HandleReply(player, pnum == consoleplayer, nodenum, replynum);
1418 }
1419 else
1420 {
1421 assert(netcode == DEM_CONVNULL || netcode == DEM_CONVCLOSE);
1422 if (player->ConversationNPC != NULL)
1423 {
1424 player->ConversationNPC->angle = player->ConversationNPCAngle;
1425 player->ConversationNPC->flags5 &= ~MF5_INCONVERSATION;
1426 }
1427 if (netcode == DEM_CONVNULL)
1428 {
1429 player->ConversationFaceTalker = false;
1430 player->ConversationNPC = NULL;
1431 player->ConversationPC = NULL;
1432 player->ConversationNPCAngle = 0;
1433 }
1434 }
1435 }
1436
1437 //============================================================================
1438 //
1439 // TerminalResponse
1440 //
1441 // Similar to C_MidPrint, but lower and colored and sized to match the
1442 // rest of the dialogue text.
1443 //
1444 //============================================================================
1445
TerminalResponse(const char * str)1446 static void TerminalResponse (const char *str)
1447 {
1448 if (str != NULL)
1449 {
1450 // handle string table replacement
1451 if (str[0] == '$')
1452 {
1453 str = GStrings(str + 1);
1454 }
1455
1456 if (StatusBar != NULL)
1457 {
1458 AddToConsole(-1, str);
1459 AddToConsole(-1, "\n");
1460 // The message is positioned a bit above the menu choices, because
1461 // merchants can tell you something like this but continue to show
1462 // their dialogue screen. I think most other conversations use this
1463 // only as a response for terminating the dialogue.
1464 StatusBar->AttachMessage(new DHUDMessageFadeOut(SmallFont, str,
1465 float(CleanWidth/2) + 0.4f, float(ConversationMenuY - 110 + CleanHeight/2), CleanWidth, -CleanHeight,
1466 CR_UNTRANSLATED, 3, 1), MAKE_ID('T','A','L','K'));
1467 }
1468 else
1469 {
1470 Printf("%s\n", str);
1471 }
1472 }
1473 }
1474
1475
operator <<(FArchive & arc,FStrifeDialogueNode * & node)1476 template<> FArchive &operator<< (FArchive &arc, FStrifeDialogueNode *&node)
1477 {
1478 DWORD convnum;
1479 if (arc.IsStoring())
1480 {
1481 arc.WriteCount (node == NULL? ~0u : node->ThisNodeNum);
1482 }
1483 else
1484 {
1485 convnum = arc.ReadCount();
1486 if (convnum >= StrifeDialogues.Size())
1487 {
1488 node = NULL;
1489 }
1490 else
1491 {
1492 node = StrifeDialogues[convnum];
1493 }
1494 }
1495 return arc;
1496 }
1497