mirror of
https://github.com/ZDoom/gzdoom-gles.git
synced 2024-12-16 07:10:53 +00:00
cd180d29c7
This is to ensure that the Class pointer can be set right on creation. ZDoom had always depended on handling this lazily which poses some problems for the VM. So now there is a variadic Create<classtype> function taking care of that, but to ensure that it gets used, direct access to the new operator has been blocked. This also neccessitated making DArgs a regular object because they get created before the type system is up. Since the few uses of DArgs are easily controllable this wasn't a big issue. - did a bit of optimization on the bots' decision making whether to pick up a health item or not.
1208 lines
31 KiB
C++
1208 lines
31 KiB
C++
/*
|
|
** p_converstation.cpp
|
|
** Implements Strife style conversation dialogs
|
|
**
|
|
**---------------------------------------------------------------------------
|
|
** Copyright 2004-2008 Randy Heit
|
|
** All rights reserved.
|
|
**
|
|
** Redistribution and use in source and binary forms, with or without
|
|
** modification, are permitted provided that the following conditions
|
|
** are met:
|
|
**
|
|
** 1. Redistributions of source code must retain the above copyright
|
|
** notice, this list of conditions and the following disclaimer.
|
|
** 2. Redistributions in binary form must reproduce the above copyright
|
|
** notice, this list of conditions and the following disclaimer in the
|
|
** documentation and/or other materials provided with the distribution.
|
|
** 3. The name of the author may not be used to endorse or promote products
|
|
** derived from this software without specific prior written permission.
|
|
**
|
|
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
**---------------------------------------------------------------------------
|
|
**
|
|
*/
|
|
|
|
#include <assert.h>
|
|
|
|
#include "actor.h"
|
|
#include "p_conversation.h"
|
|
#include "w_wad.h"
|
|
#include "cmdlib.h"
|
|
#include "s_sound.h"
|
|
#include "v_text.h"
|
|
#include "v_video.h"
|
|
#include "m_random.h"
|
|
#include "gi.h"
|
|
#include "templates.h"
|
|
#include "a_keys.h"
|
|
#include "p_enemy.h"
|
|
#include "gstrings.h"
|
|
#include "sound/i_music.h"
|
|
#include "p_setup.h"
|
|
#include "d_net.h"
|
|
#include "g_level.h"
|
|
#include "d_event.h"
|
|
#include "d_gui.h"
|
|
#include "doomstat.h"
|
|
#include "c_console.h"
|
|
#include "sbar.h"
|
|
#include "p_lnspec.h"
|
|
#include "r_utility.h"
|
|
#include "p_local.h"
|
|
#include "menu/menu.h"
|
|
#include "g_levellocals.h"
|
|
#include "vm.h"
|
|
#include "actorinlines.h"
|
|
|
|
// The conversations as they exist inside a SCRIPTxx lump.
|
|
struct Response
|
|
{
|
|
int32_t GiveType;
|
|
int32_t Item[3];
|
|
int32_t Count[3];
|
|
char Reply[32];
|
|
char Yes[80];
|
|
int32_t Link;
|
|
uint32_t Log;
|
|
char No[80];
|
|
};
|
|
|
|
struct Speech
|
|
{
|
|
uint32_t SpeakerType;
|
|
int32_t DropType;
|
|
int32_t ItemCheck[3];
|
|
int32_t Link;
|
|
char Name[16];
|
|
char Sound[8];
|
|
char Backdrop[8];
|
|
char Dialogue[320];
|
|
Response Responses[5];
|
|
};
|
|
|
|
// The Teaser version of the game uses an older version of the structure
|
|
struct TeaserSpeech
|
|
{
|
|
uint32_t SpeakerType;
|
|
int32_t DropType;
|
|
uint32_t VoiceNumber;
|
|
char Name[16];
|
|
char Dialogue[320];
|
|
Response Responses[5];
|
|
};
|
|
|
|
static FRandom pr_randomspeech("RandomSpeech");
|
|
|
|
TArray<FStrifeDialogueNode *> StrifeDialogues;
|
|
|
|
typedef TMap<int, int> FDialogueIDMap; // maps dialogue IDs to dialogue array index (for ACS)
|
|
typedef TMap<FName, int> FDialogueMap; // maps actor class names to dialogue array index
|
|
|
|
FClassMap StrifeTypes;
|
|
static FDialogueIDMap DialogueRoots;
|
|
static FDialogueMap ClassRoots;
|
|
static int ConversationMenuY;
|
|
|
|
static int ConversationPauseTic;
|
|
static int StaticLastReply;
|
|
|
|
static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type);
|
|
static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, uint32_t &prevSpeakerType);
|
|
static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, uint32_t &prevSpeakerType);
|
|
static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses);
|
|
static bool DrawConversationMenu ();
|
|
static void PickConversationReply (int replyindex);
|
|
static void TerminalResponse (const char *str);
|
|
|
|
static FStrifeDialogueNode *PrevNode;
|
|
|
|
//============================================================================
|
|
//
|
|
// GetStrifeType
|
|
//
|
|
// Given an item type number, returns the corresponding PClass.
|
|
//
|
|
//============================================================================
|
|
|
|
void SetStrifeType(int convid, PClassActor *Class)
|
|
{
|
|
StrifeTypes[convid] = Class;
|
|
}
|
|
|
|
void ClearStrifeTypes()
|
|
{
|
|
StrifeTypes.Clear();
|
|
}
|
|
|
|
void SetConversation(int convid, PClassActor *Class, int dlgindex)
|
|
{
|
|
if (convid != -1)
|
|
{
|
|
DialogueRoots[convid] = dlgindex;
|
|
}
|
|
if (Class != NULL)
|
|
{
|
|
ClassRoots[Class->TypeName] = dlgindex;
|
|
}
|
|
}
|
|
|
|
PClassActor *GetStrifeType (int typenum)
|
|
{
|
|
PClassActor **ptype = StrifeTypes.CheckKey(typenum);
|
|
if (ptype == NULL) return NULL;
|
|
else return *ptype;
|
|
}
|
|
|
|
int GetConversation(int conv_id)
|
|
{
|
|
int *pindex = DialogueRoots.CheckKey(conv_id);
|
|
if (pindex == NULL) return -1;
|
|
else return *pindex;
|
|
}
|
|
|
|
int GetConversation(FName classname)
|
|
{
|
|
int *pindex = ClassRoots.CheckKey(classname);
|
|
if (pindex == NULL) return -1;
|
|
else return *pindex;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// P_LoadStrifeConversations
|
|
//
|
|
// Loads the SCRIPT00 and SCRIPTxx files for a corresponding map.
|
|
//
|
|
//============================================================================
|
|
|
|
void P_LoadStrifeConversations (MapData *map, const char *mapname)
|
|
{
|
|
P_FreeStrifeConversations ();
|
|
if (map->Size(ML_CONVERSATION) > 0)
|
|
{
|
|
map->Seek(ML_CONVERSATION);
|
|
LoadScriptFile (map->lumpnum, map->file, map->Size(ML_CONVERSATION), false, 0);
|
|
}
|
|
else
|
|
{
|
|
if (strnicmp (mapname, "MAP", 3) != 0)
|
|
{
|
|
return;
|
|
}
|
|
char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 };
|
|
char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 };
|
|
|
|
if (!LoadScriptFile(scriptname_t, false, 2))
|
|
{
|
|
if (!LoadScriptFile (scriptname_b, false, 1))
|
|
{
|
|
if (gameinfo.Dialogue.IsNotEmpty())
|
|
{
|
|
if (LoadScriptFile(gameinfo.Dialogue, false, 0)) return;
|
|
}
|
|
|
|
LoadScriptFile ("SCRIPT00", false, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// LoadScriptFile
|
|
//
|
|
// Loads a SCRIPTxx file and converts it into a more useful internal format.
|
|
//
|
|
//============================================================================
|
|
|
|
bool LoadScriptFile (const char *name, bool include, int type)
|
|
{
|
|
int lumpnum = Wads.CheckNumForName (name);
|
|
FileReader *lump;
|
|
|
|
if (lumpnum < 0)
|
|
{
|
|
return false;
|
|
}
|
|
lump = Wads.ReopenLumpNum (lumpnum);
|
|
|
|
bool res = LoadScriptFile(lumpnum, lump, Wads.LumpLength(lumpnum), include, type);
|
|
delete lump;
|
|
return res;
|
|
}
|
|
|
|
static bool LoadScriptFile(int lumpnum, FileReader *lump, int numnodes, bool include, int type)
|
|
{
|
|
int i;
|
|
uint32_t prevSpeakerType;
|
|
FStrifeDialogueNode *node;
|
|
char buffer[4];
|
|
|
|
lump->Read(buffer, 4);
|
|
lump->Seek(-4, SEEK_CUR);
|
|
|
|
// The binary format is so primitive that this check is enough to detect it.
|
|
bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0);
|
|
|
|
if ((type == 1 && !isbinary) || (type == 2 && isbinary))
|
|
{
|
|
DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum));
|
|
return false;
|
|
}
|
|
|
|
if (!isbinary)
|
|
{
|
|
P_ParseUSDF(lumpnum, lump, numnodes);
|
|
}
|
|
else
|
|
{
|
|
if (!include)
|
|
{
|
|
LoadScriptFile("SCRIPT00", true, 1);
|
|
}
|
|
if (!(gameinfo.flags & GI_SHAREWARE))
|
|
{
|
|
// Strife scripts are always a multiple of 1516 bytes because each entry
|
|
// is exactly 1516 bytes long.
|
|
if (numnodes % 1516 != 0)
|
|
{
|
|
DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum));
|
|
return false;
|
|
}
|
|
numnodes /= 1516;
|
|
}
|
|
else
|
|
{
|
|
// And the teaser version has 1488-byte entries.
|
|
if (numnodes % 1488 != 0)
|
|
{
|
|
DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", Wads.GetLumpFullName(lumpnum));
|
|
return false;
|
|
}
|
|
numnodes /= 1488;
|
|
}
|
|
|
|
prevSpeakerType = 0;
|
|
|
|
for (i = 0; i < numnodes; ++i)
|
|
{
|
|
if (!(gameinfo.flags & GI_SHAREWARE))
|
|
{
|
|
node = ReadRetailNode (lump, prevSpeakerType);
|
|
}
|
|
else
|
|
{
|
|
node = ReadTeaserNode (lump, prevSpeakerType);
|
|
}
|
|
node->ThisNodeNum = StrifeDialogues.Push(node);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// ReadRetailNode
|
|
//
|
|
// Converts a single dialogue node from the Retail version of Strife.
|
|
//
|
|
//============================================================================
|
|
|
|
static FStrifeDialogueNode *ReadRetailNode (FileReader *lump, uint32_t &prevSpeakerType)
|
|
{
|
|
FStrifeDialogueNode *node;
|
|
Speech speech;
|
|
char fullsound[16];
|
|
PClassActor *type;
|
|
int j;
|
|
|
|
node = new FStrifeDialogueNode;
|
|
|
|
lump->Read (&speech, sizeof(speech));
|
|
|
|
// Byte swap all the ints in the original data
|
|
speech.SpeakerType = LittleLong(speech.SpeakerType);
|
|
speech.DropType = LittleLong(speech.DropType);
|
|
speech.Link = LittleLong(speech.Link);
|
|
|
|
// Assign the first instance of a conversation as the default for its
|
|
// actor, so newly spawned actors will use this conversation by default.
|
|
type = GetStrifeType (speech.SpeakerType);
|
|
node->SpeakerType = type;
|
|
|
|
if ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType)
|
|
{
|
|
if (type != NULL)
|
|
{
|
|
ClassRoots[type->TypeName] = StrifeDialogues.Size();
|
|
}
|
|
DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
|
|
prevSpeakerType = speech.SpeakerType;
|
|
}
|
|
|
|
// Convert the rest of the data to our own internal format.
|
|
node->Dialogue = speech.Dialogue;
|
|
|
|
// The speaker's portrait, if any.
|
|
speech.Dialogue[0] = 0; //speech.Backdrop[8] = 0;
|
|
node->Backdrop = speech.Backdrop;
|
|
|
|
// The speaker's voice for this node, if any.
|
|
speech.Backdrop[0] = 0; //speech.Sound[8] = 0;
|
|
mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound);
|
|
node->SpeakerVoice = fullsound;
|
|
|
|
// The speaker's name, if any.
|
|
speech.Sound[0] = 0; //speech.Name[16] = 0;
|
|
node->SpeakerName = speech.Name;
|
|
|
|
// The item the speaker should drop when killed.
|
|
node->DropType = GetStrifeType(speech.DropType);
|
|
|
|
// Items you need to have to make the speaker use a different node.
|
|
node->ItemCheck.Resize(3);
|
|
for (j = 0; j < 3; ++j)
|
|
{
|
|
auto inv = GetStrifeType(speech.ItemCheck[j]);
|
|
if (!inv->IsDescendantOf(RUNTIME_CLASS(AInventory))) inv = nullptr;
|
|
node->ItemCheck[j].Item = inv;
|
|
node->ItemCheck[j].Amount = -1;
|
|
}
|
|
node->ItemCheckNode = speech.Link;
|
|
node->Children = NULL;
|
|
|
|
ParseReplies (&node->Children, &speech.Responses[0]);
|
|
|
|
return node;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// ReadTeaserNode
|
|
//
|
|
// Converts a single dialogue node from the Teaser version of Strife.
|
|
//
|
|
//============================================================================
|
|
|
|
static FStrifeDialogueNode *ReadTeaserNode (FileReader *lump, uint32_t &prevSpeakerType)
|
|
{
|
|
FStrifeDialogueNode *node;
|
|
TeaserSpeech speech;
|
|
char fullsound[16];
|
|
PClassActor *type;
|
|
int j;
|
|
|
|
node = new FStrifeDialogueNode;
|
|
|
|
lump->Read (&speech, sizeof(speech));
|
|
|
|
// Byte swap all the ints in the original data
|
|
speech.SpeakerType = LittleLong(speech.SpeakerType);
|
|
speech.DropType = LittleLong(speech.DropType);
|
|
|
|
// Assign the first instance of a conversation as the default for its
|
|
// actor, so newly spawned actors will use this conversation by default.
|
|
type = GetStrifeType(speech.SpeakerType);
|
|
node->SpeakerType = type;
|
|
|
|
if ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType)
|
|
{
|
|
if (type != NULL)
|
|
{
|
|
ClassRoots[type->TypeName] = StrifeDialogues.Size();
|
|
}
|
|
DialogueRoots[speech.SpeakerType] = StrifeDialogues.Size();
|
|
prevSpeakerType = speech.SpeakerType;
|
|
}
|
|
|
|
// Convert the rest of the data to our own internal format.
|
|
node->Dialogue = speech.Dialogue;
|
|
|
|
// The Teaser version doesn't have portraits.
|
|
node->Backdrop = "";
|
|
|
|
// The speaker's voice for this node, if any.
|
|
if (speech.VoiceNumber != 0)
|
|
{
|
|
mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber);
|
|
node->SpeakerVoice = fullsound;
|
|
}
|
|
else
|
|
{
|
|
node->SpeakerVoice = 0;
|
|
}
|
|
|
|
// The speaker's name, if any.
|
|
speech.Dialogue[0] = 0; //speech.Name[16] = 0;
|
|
node->SpeakerName = speech.Name;
|
|
|
|
// The item the speaker should drop when killed.
|
|
node->DropType = GetStrifeType (speech.DropType);
|
|
|
|
// Items you need to have to make the speaker use a different node.
|
|
node->ItemCheck.Resize(3);
|
|
for (j = 0; j < 3; ++j)
|
|
{
|
|
node->ItemCheck[j].Item = NULL;
|
|
node->ItemCheck[j].Amount = -1;
|
|
}
|
|
node->ItemCheckNode = 0;
|
|
node->Children = NULL;
|
|
|
|
ParseReplies (&node->Children, &speech.Responses[0]);
|
|
|
|
return node;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// ParseReplies
|
|
//
|
|
// Convert PC responses. Rather than being stored inside the main node, they
|
|
// hang off it as a singly-linked list, so no space is wasted on replies that
|
|
// don't even matter.
|
|
//
|
|
//============================================================================
|
|
|
|
static void ParseReplies (FStrifeDialogueReply **replyptr, Response *responses)
|
|
{
|
|
FStrifeDialogueReply *reply;
|
|
int j, k;
|
|
|
|
// Byte swap first.
|
|
for (j = 0; j < 5; ++j)
|
|
{
|
|
responses[j].GiveType = LittleLong(responses[j].GiveType);
|
|
responses[j].Link = LittleLong(responses[j].Link);
|
|
responses[j].Log = LittleLong(responses[j].Log);
|
|
for (k = 0; k < 3; ++k)
|
|
{
|
|
responses[j].Item[k] = LittleLong(responses[j].Item[k]);
|
|
responses[j].Count[k] = LittleLong(responses[j].Count[k]);
|
|
}
|
|
}
|
|
|
|
for (j = 0; j < 5; ++j)
|
|
{
|
|
Response *rsp = &responses[j];
|
|
|
|
// If the reply has no text and goes nowhere, then it doesn't
|
|
// need to be remembered.
|
|
if (rsp->Reply[0] == 0 && rsp->Link == 0)
|
|
{
|
|
continue;
|
|
}
|
|
reply = new FStrifeDialogueReply;
|
|
|
|
// The next node to use when this reply is chosen.
|
|
reply->NextNode = rsp->Link;
|
|
|
|
// The message to record in the log for this reply.
|
|
reply->LogNumber = rsp->Log;
|
|
reply->LogString = "";
|
|
|
|
// The item to receive when this reply is used.
|
|
reply->GiveType = GetStrifeType (rsp->GiveType);
|
|
reply->ActionSpecial = 0;
|
|
|
|
// Do you need anything special for this reply to succeed?
|
|
reply->ItemCheck.Resize(3);
|
|
for (k = 0; k < 3; ++k)
|
|
{
|
|
auto inv = GetStrifeType(rsp->Item[k]);
|
|
if (!inv->IsDescendantOf(RUNTIME_CLASS(AInventory))) inv = nullptr;
|
|
reply->ItemCheck[k].Item = inv;
|
|
reply->ItemCheck[k].Amount = rsp->Count[k];
|
|
}
|
|
reply->PrintAmount = reply->ItemCheck[0].Amount;
|
|
reply->ItemCheckRequire.Clear();
|
|
reply->ItemCheckExclude.Clear();
|
|
|
|
// If the first item check has a positive amount required, then
|
|
// add that to the reply string. Otherwise, use the reply as-is.
|
|
reply->Reply = rsp->Reply;
|
|
reply->NeedsGold = (rsp->Count[0] > 0);
|
|
|
|
// QuickYes messages are shown when you meet the item checks.
|
|
// QuickNo messages are shown when you don't.
|
|
if (rsp->Yes[0] == '_' && rsp->Yes[1] == 0)
|
|
{
|
|
reply->QuickYes = "";
|
|
}
|
|
else
|
|
{
|
|
reply->QuickYes = rsp->Yes;
|
|
}
|
|
if (reply->ItemCheck[0].Item != 0)
|
|
{
|
|
reply->QuickNo = rsp->No;
|
|
}
|
|
else
|
|
{
|
|
reply->QuickNo = "";
|
|
}
|
|
reply->Next = *replyptr;
|
|
*replyptr = reply;
|
|
replyptr = &reply->Next;
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// FStrifeDialogueNode :: ~FStrifeDialogueNode
|
|
//
|
|
//============================================================================
|
|
|
|
FStrifeDialogueNode::~FStrifeDialogueNode ()
|
|
{
|
|
FStrifeDialogueReply *tokill = Children;
|
|
while (tokill != NULL)
|
|
{
|
|
FStrifeDialogueReply *next = tokill->Next;
|
|
delete tokill;
|
|
tokill = next;
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// FindNode
|
|
//
|
|
// Returns the index that matches the given conversation node.
|
|
//
|
|
//============================================================================
|
|
|
|
static int FindNode (const FStrifeDialogueNode *node)
|
|
{
|
|
int rootnode = 0;
|
|
|
|
while (StrifeDialogues[rootnode] != node)
|
|
{
|
|
rootnode++;
|
|
}
|
|
return rootnode;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// CheckStrifeItem
|
|
//
|
|
// Checks if you have an item. A NULL itemtype is always considered to be
|
|
// present.
|
|
//
|
|
//============================================================================
|
|
|
|
static bool CheckStrifeItem (player_t *player, PClassActor *itemtype, int amount=-1)
|
|
{
|
|
AInventory *item;
|
|
|
|
if (itemtype == NULL || amount == 0)
|
|
return true;
|
|
|
|
item = player->ConversationPC->FindInventory (itemtype);
|
|
if (item == NULL)
|
|
return false;
|
|
|
|
return amount < 0 || item->Amount >= amount;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// TakeStrifeItem
|
|
//
|
|
// Takes away some of an item, unless that item is special and should not
|
|
// be removed.
|
|
//
|
|
//============================================================================
|
|
|
|
static void TakeStrifeItem (player_t *player, PClassActor *itemtype, int amount)
|
|
{
|
|
if (itemtype == NULL || amount == 0)
|
|
return;
|
|
|
|
// Don't take quest items.
|
|
if (itemtype->IsDescendantOf (PClass::FindClass(NAME_QuestItem)))
|
|
return;
|
|
|
|
// Don't take keys.
|
|
if (itemtype->IsDescendantOf (PClass::FindActor(NAME_Key)))
|
|
return;
|
|
|
|
// Don't take the sigil.
|
|
if (itemtype->TypeName == NAME_Sigil)
|
|
return;
|
|
|
|
player->mo->TakeInventory(itemtype, amount);
|
|
}
|
|
|
|
CUSTOM_CVAR(Float, dlg_musicvolume, 1.0f, CVAR_ARCHIVE)
|
|
{
|
|
if (self < 0.f) self = 0.f;
|
|
else if (self > 1.f) self = 1.f;
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// ShouldSkipReply
|
|
//
|
|
// Determines whether this reply should be skipped or not.
|
|
//
|
|
//============================================================================
|
|
|
|
static bool ShouldSkipReply(FStrifeDialogueReply *reply, player_t *player)
|
|
{
|
|
if (reply->Reply.IsEmpty())
|
|
return true;
|
|
|
|
int i;
|
|
for (i = 0; i < (int)reply->ItemCheckRequire.Size(); ++i)
|
|
{
|
|
if (!CheckStrifeItem(player, reply->ItemCheckRequire[i].Item, reply->ItemCheckRequire[i].Amount))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < (int)reply->ItemCheckExclude.Size(); ++i)
|
|
{
|
|
if (CheckStrifeItem(player, reply->ItemCheckExclude[i].Item, reply->ItemCheckExclude[i].Amount))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(FStrifeDialogueReply, ShouldSkipReply)
|
|
{
|
|
PARAM_SELF_STRUCT_PROLOGUE(FStrifeDialogueReply);
|
|
PARAM_POINTER(player, player_t);
|
|
ACTION_RETURN_BOOL(ShouldSkipReply(self, player));
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(DConversationMenu, SendConversationReply)
|
|
{
|
|
PARAM_PROLOGUE;
|
|
PARAM_INT(node);
|
|
PARAM_INT(reply);
|
|
switch (node)
|
|
{
|
|
case -1:
|
|
Net_WriteByte(DEM_CONVNULL);
|
|
break;
|
|
|
|
case -2:
|
|
Net_WriteByte(DEM_CONVCLOSE);
|
|
break;
|
|
|
|
default:
|
|
Net_WriteByte(DEM_CONVREPLY);
|
|
Net_WriteWord(node);
|
|
Net_WriteByte(reply);
|
|
break;
|
|
}
|
|
StaticLastReply = reply;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Needed for the conversion process.
|
|
class DBrokenLines : public DObject
|
|
{
|
|
DECLARE_ABSTRACT_CLASS(DBrokenLines, DObject)
|
|
|
|
public:
|
|
FBrokenLines *mBroken;
|
|
unsigned int mCount;
|
|
|
|
DBrokenLines(FBrokenLines *broken, unsigned int count)
|
|
{
|
|
mBroken = broken;
|
|
mCount = count;
|
|
}
|
|
|
|
void OnDestroy() override
|
|
{
|
|
V_FreeBrokenLines(mBroken);
|
|
}
|
|
};
|
|
|
|
//============================================================================
|
|
//
|
|
// P_FreeStrifeConversations
|
|
//
|
|
//============================================================================
|
|
|
|
void P_FreeStrifeConversations ()
|
|
{
|
|
FStrifeDialogueNode *node;
|
|
|
|
while (StrifeDialogues.Pop (node))
|
|
{
|
|
delete node;
|
|
}
|
|
|
|
DialogueRoots.Clear();
|
|
ClassRoots.Clear();
|
|
|
|
PrevNode = NULL;
|
|
if (CurrentMenu != NULL && CurrentMenu->IsKindOf("ConversationMenu"))
|
|
{
|
|
CurrentMenu->Close();
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// P_StartConversation
|
|
//
|
|
// Begins a conversation between a PC and NPC.
|
|
//
|
|
//============================================================================
|
|
|
|
void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveangle)
|
|
{
|
|
AActor *oldtarget;
|
|
int i;
|
|
|
|
// Make sure this is actually a player.
|
|
if (pc->player == NULL) return;
|
|
|
|
// [CW] If an NPC is talking to a PC already, then don't let
|
|
// anyone else talk to the NPC.
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || pc->player == &players[i])
|
|
continue;
|
|
|
|
if (npc == players[i].ConversationNPC)
|
|
return;
|
|
}
|
|
|
|
pc->Vel.Zero();
|
|
pc->player->Vel.Zero();
|
|
static_cast<APlayerPawn*>(pc)->PlayIdle ();
|
|
|
|
pc->player->ConversationPC = pc;
|
|
pc->player->ConversationNPC = npc;
|
|
npc->flags5 |= MF5_INCONVERSATION;
|
|
|
|
FStrifeDialogueNode *CurNode = npc->Conversation;
|
|
|
|
if (pc->player == &players[consoleplayer])
|
|
{
|
|
S_Sound (CHAN_VOICE | CHAN_UI, gameinfo.chatSound, 1, ATTN_NONE);
|
|
}
|
|
|
|
npc->reactiontime = 2;
|
|
pc->player->ConversationFaceTalker = facetalker;
|
|
if (saveangle)
|
|
{
|
|
pc->player->ConversationNPCAngle = npc->Angles.Yaw;
|
|
}
|
|
oldtarget = npc->target;
|
|
npc->target = pc;
|
|
if (facetalker)
|
|
{
|
|
A_FaceTarget (npc);
|
|
pc->Angles.Yaw = pc->AngleTo(npc);
|
|
}
|
|
if ((npc->flags & MF_FRIENDLY) || (npc->flags4 & MF4_NOHATEPLAYERS))
|
|
{
|
|
npc->target = oldtarget;
|
|
}
|
|
|
|
// Check if we should jump to another node
|
|
while (CurNode->ItemCheck.Size() > 0 && CurNode->ItemCheck[0].Item != NULL)
|
|
{
|
|
bool jump = true;
|
|
for (i = 0; i < (int)CurNode->ItemCheck.Size(); ++i)
|
|
{
|
|
if(!CheckStrifeItem (pc->player, CurNode->ItemCheck[i].Item, CurNode->ItemCheck[i].Amount))
|
|
{
|
|
jump = false;
|
|
break;
|
|
}
|
|
}
|
|
if (jump && CurNode->ItemCheckNode > 0)
|
|
{
|
|
int root = pc->player->ConversationNPC->ConversationRoot;
|
|
CurNode = StrifeDialogues[root + CurNode->ItemCheckNode - 1];
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The rest is only done when the conversation is actually displayed.
|
|
if (pc->player == &players[consoleplayer])
|
|
{
|
|
if (CurNode->SpeakerVoice != 0)
|
|
{
|
|
I_SetMusicVolume (dlg_musicvolume);
|
|
S_Sound (npc, CHAN_VOICE|CHAN_NOPAUSE, CurNode->SpeakerVoice, 1, ATTN_NORM);
|
|
}
|
|
|
|
// Create the menu. This may be a user-defined class so check if it is good to use.
|
|
FName cls = CurNode->MenuClassName;
|
|
if (cls == NAME_None) cls = gameinfo.DefaultConversationMenuClass;
|
|
if (cls == NAME_None) cls = "ConversationMenu";
|
|
auto mcls = PClass::FindClass(cls);
|
|
if (mcls == nullptr || !mcls->IsDescendantOf("ConversationMenu")) mcls = PClass::FindClass("ConversationMenu");
|
|
assert(mcls);
|
|
|
|
auto cmenu = mcls->CreateNew();
|
|
IFVIRTUALPTRNAME(cmenu, "ConversationMenu", Init)
|
|
{
|
|
VMValue params[] = { cmenu, CurNode, pc->player, StaticLastReply };
|
|
VMReturn ret(&ConversationMenuY);
|
|
VMCall(func, params, countof(params), &ret, 1);
|
|
}
|
|
|
|
if (CurNode != PrevNode)
|
|
{ // Only reset the selection if showing a different menu.
|
|
StaticLastReply = 0;
|
|
PrevNode = CurNode;
|
|
}
|
|
|
|
// And open the menu
|
|
M_StartControlPanel (false);
|
|
M_ActivateMenu((DMenu*)cmenu);
|
|
menuactive = MENU_OnNoPause;
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// P_ResumeConversation
|
|
//
|
|
// Resumes a conversation that was interrupted by a slideshow.
|
|
//
|
|
//============================================================================
|
|
|
|
void P_ResumeConversation ()
|
|
{
|
|
for (int i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i])
|
|
continue;
|
|
|
|
player_t *p = &players[i];
|
|
|
|
if (p->ConversationPC != NULL && p->ConversationNPC != NULL)
|
|
{
|
|
P_StartConversation (p->ConversationNPC, p->ConversationPC, p->ConversationFaceTalker, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// HandleReply
|
|
//
|
|
// Run by the netcode on all machines.
|
|
//
|
|
//============================================================================
|
|
|
|
static void HandleReply(player_t *player, bool isconsole, int nodenum, int replynum)
|
|
{
|
|
const char *replyText = NULL;
|
|
FStrifeDialogueReply *reply;
|
|
FStrifeDialogueNode *node;
|
|
AActor *npc;
|
|
bool takestuff;
|
|
int i;
|
|
|
|
if (player->ConversationNPC == NULL || (unsigned)nodenum >= StrifeDialogues.Size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Find the reply.
|
|
node = StrifeDialogues[nodenum];
|
|
for (i = 0, reply = node->Children; reply != NULL && i != replynum; ++i, reply = reply->Next)
|
|
{ }
|
|
npc = player->ConversationNPC;
|
|
if (reply == NULL)
|
|
{
|
|
// The default reply was selected
|
|
npc->Angles.Yaw = player->ConversationNPCAngle;
|
|
npc->flags5 &= ~MF5_INCONVERSATION;
|
|
return;
|
|
}
|
|
|
|
// Check if you have the requisite items for this choice
|
|
for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
|
|
{
|
|
if (!CheckStrifeItem(player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount))
|
|
{
|
|
// No, you don't. Say so and let the NPC animate negatively.
|
|
if (reply->QuickNo.IsNotEmpty() && isconsole)
|
|
{
|
|
TerminalResponse(reply->QuickNo);
|
|
}
|
|
npc->ConversationAnimation(2);
|
|
npc->Angles.Yaw = player->ConversationNPCAngle;
|
|
npc->flags5 &= ~MF5_INCONVERSATION;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Yay, you do! Let the NPC animate affirmatively.
|
|
npc->ConversationAnimation(1);
|
|
|
|
// If this reply gives you something, then try to receive it.
|
|
takestuff = true;
|
|
if (reply->GiveType != NULL)
|
|
{
|
|
if (reply->GiveType->IsDescendantOf(RUNTIME_CLASS(AInventory)))
|
|
{
|
|
if (reply->GiveType->IsDescendantOf(NAME_Weapon))
|
|
{
|
|
if (player->mo->FindInventory(reply->GiveType) != NULL)
|
|
{
|
|
takestuff = false;
|
|
}
|
|
}
|
|
|
|
if (takestuff)
|
|
{
|
|
AInventory *item = static_cast<AInventory *>(Spawn(reply->GiveType));
|
|
// Items given here should not count as items!
|
|
item->ClearCounters();
|
|
if (item->GetClass()->TypeName == NAME_FlameThrower)
|
|
{
|
|
// The flame thrower gives less ammo when given in a dialog
|
|
static_cast<AWeapon*>(item)->AmmoGive1 = 40;
|
|
}
|
|
item->flags |= MF_DROPPED;
|
|
if (!item->CallTryPickup(player->mo))
|
|
{
|
|
item->Destroy();
|
|
takestuff = false;
|
|
}
|
|
}
|
|
|
|
if (reply->GiveType->IsDescendantOf("SlideshowStarter"))
|
|
gameaction = ga_slideshow;
|
|
}
|
|
else
|
|
{
|
|
// Trying to give a non-inventory item.
|
|
takestuff = false;
|
|
if (isconsole)
|
|
{
|
|
Printf("Attempting to give non-inventory item %s\n", reply->GiveType->TypeName.GetChars());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (reply->ActionSpecial != 0)
|
|
{
|
|
takestuff |= !!P_ExecuteSpecial(reply->ActionSpecial, NULL, player->mo, false,
|
|
reply->Args[0], reply->Args[1], reply->Args[2], reply->Args[3], reply->Args[4]);
|
|
}
|
|
|
|
// Take away required items if the give was successful or none was needed.
|
|
if (takestuff)
|
|
{
|
|
for (i = 0; i < (int)reply->ItemCheck.Size(); ++i)
|
|
{
|
|
TakeStrifeItem (player, reply->ItemCheck[i].Item, reply->ItemCheck[i].Amount);
|
|
}
|
|
replyText = reply->QuickYes;
|
|
}
|
|
else
|
|
{
|
|
replyText = "$txt_haveenough";
|
|
}
|
|
|
|
// Update the quest log, if needed.
|
|
if (reply->LogString.IsNotEmpty())
|
|
{
|
|
const char *log = reply->LogString;
|
|
if (log[0] == '$')
|
|
{
|
|
log = GStrings(log + 1);
|
|
}
|
|
|
|
player->SetLogText(log);
|
|
}
|
|
else if (reply->LogNumber != 0)
|
|
{
|
|
player->SetLogNumber(reply->LogNumber);
|
|
}
|
|
|
|
if (replyText != NULL && isconsole)
|
|
{
|
|
TerminalResponse(replyText);
|
|
}
|
|
|
|
// Does this reply alter the speaker's conversation node? If NextNode is positive,
|
|
// the next time they talk, they will show the new node. If it is negative, then they
|
|
// will show the new node right away without terminating the dialogue.
|
|
if (reply->NextNode != 0)
|
|
{
|
|
int rootnode = npc->ConversationRoot;
|
|
const bool isNegative = reply->NextNode < 0;
|
|
const unsigned next = (unsigned)(rootnode + (isNegative ? -1 : 1) * reply->NextNode - 1);
|
|
|
|
if (next < StrifeDialogues.Size())
|
|
{
|
|
npc->Conversation = StrifeDialogues[next];
|
|
|
|
if (isNegative)
|
|
{
|
|
if (gameaction != ga_slideshow)
|
|
{
|
|
P_StartConversation (npc, player->mo, player->ConversationFaceTalker, false);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
S_StopSound (npc, CHAN_VOICE);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Printf ("Next node %u is invalid, no such dialog page\n", next);
|
|
}
|
|
}
|
|
|
|
npc->Angles.Yaw = player->ConversationNPCAngle;
|
|
|
|
// [CW] Set these to NULL because we're not using to them
|
|
// anymore. However, this can interfere with slideshows
|
|
// so we don't set them to NULL in that case.
|
|
if (gameaction != ga_slideshow)
|
|
{
|
|
npc->flags5 &= ~MF5_INCONVERSATION;
|
|
player->ConversationFaceTalker = false;
|
|
player->ConversationNPC = NULL;
|
|
player->ConversationPC = NULL;
|
|
player->ConversationNPCAngle = 0.;
|
|
}
|
|
|
|
if (isconsole)
|
|
{
|
|
I_SetMusicVolume (1.f);
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// P_ConversationCommand
|
|
//
|
|
// Complete a conversation command.
|
|
//
|
|
//============================================================================
|
|
|
|
void P_ConversationCommand (int netcode, int pnum, uint8_t **stream)
|
|
{
|
|
player_t *player = &players[pnum];
|
|
|
|
// The conversation menus are normally closed by the menu code, but that
|
|
// doesn't happen during demo playback, so we need to do it here.
|
|
if (demoplayback && CurrentMenu != NULL && CurrentMenu->IsKindOf("ConversationMenu"))
|
|
{
|
|
CurrentMenu->Close();
|
|
}
|
|
if (netcode == DEM_CONVREPLY)
|
|
{
|
|
int nodenum = ReadWord(stream);
|
|
int replynum = ReadByte(stream);
|
|
HandleReply(player, pnum == consoleplayer, nodenum, replynum);
|
|
}
|
|
else
|
|
{
|
|
assert(netcode == DEM_CONVNULL || netcode == DEM_CONVCLOSE);
|
|
if (player->ConversationNPC != NULL)
|
|
{
|
|
player->ConversationNPC->Angles.Yaw = player->ConversationNPCAngle;
|
|
player->ConversationNPC->flags5 &= ~MF5_INCONVERSATION;
|
|
}
|
|
if (netcode == DEM_CONVNULL)
|
|
{
|
|
player->ConversationFaceTalker = false;
|
|
player->ConversationNPC = NULL;
|
|
player->ConversationPC = NULL;
|
|
player->ConversationNPCAngle = 0.;
|
|
}
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// TerminalResponse
|
|
//
|
|
// Similar to C_MidPrint, but lower and colored and sized to match the
|
|
// rest of the dialogue text.
|
|
//
|
|
//============================================================================
|
|
|
|
static void TerminalResponse (const char *str)
|
|
{
|
|
if (str != NULL)
|
|
{
|
|
// handle string table replacement
|
|
if (str[0] == '$')
|
|
{
|
|
str = GStrings(str + 1);
|
|
}
|
|
|
|
if (StatusBar != NULL)
|
|
{
|
|
AddToConsole(-1, str);
|
|
AddToConsole(-1, "\n");
|
|
// The message is positioned a bit above the menu choices, because
|
|
// merchants can tell you something like this but continue to show
|
|
// their dialogue screen. I think most other conversations use this
|
|
// only as a response for terminating the dialogue.
|
|
StatusBar->AttachMessage(Create<DHUDMessageFadeOut>(SmallFont, str,
|
|
float(CleanWidth/2) + 0.4f, float(ConversationMenuY - 110 + CleanHeight/2), CleanWidth, -CleanHeight,
|
|
CR_UNTRANSLATED, 3.f, 1.f), MAKE_ID('T','A','L','K'));
|
|
}
|
|
else
|
|
{
|
|
Printf("%s\n", str);
|
|
}
|
|
}
|
|
}
|
|
|
|
DEFINE_FIELD(FStrifeDialogueNode, DropType);
|
|
DEFINE_FIELD(FStrifeDialogueNode, ThisNodeNum);
|
|
DEFINE_FIELD(FStrifeDialogueNode, ItemCheckNode);
|
|
DEFINE_FIELD(FStrifeDialogueNode, SpeakerType);
|
|
DEFINE_FIELD(FStrifeDialogueNode, SpeakerName);
|
|
DEFINE_FIELD(FStrifeDialogueNode, SpeakerVoice);
|
|
DEFINE_FIELD(FStrifeDialogueNode, Backdrop);
|
|
DEFINE_FIELD(FStrifeDialogueNode, Dialogue);
|
|
DEFINE_FIELD(FStrifeDialogueNode, Goodbye);
|
|
DEFINE_FIELD(FStrifeDialogueNode, Children);
|
|
DEFINE_FIELD(FStrifeDialogueNode, MenuClassName);
|
|
DEFINE_FIELD(FStrifeDialogueNode, UserData);
|
|
|
|
DEFINE_FIELD(FStrifeDialogueReply, Next);
|
|
DEFINE_FIELD(FStrifeDialogueReply, GiveType);
|
|
DEFINE_FIELD(FStrifeDialogueReply, ActionSpecial);
|
|
DEFINE_FIELD(FStrifeDialogueReply, Args);
|
|
DEFINE_FIELD(FStrifeDialogueReply, PrintAmount);
|
|
DEFINE_FIELD(FStrifeDialogueReply, Reply);
|
|
DEFINE_FIELD(FStrifeDialogueReply, QuickYes);
|
|
DEFINE_FIELD(FStrifeDialogueReply, QuickNo);
|
|
DEFINE_FIELD(FStrifeDialogueReply, LogString);
|
|
DEFINE_FIELD(FStrifeDialogueReply, NextNode);
|
|
DEFINE_FIELD(FStrifeDialogueReply, LogNumber);
|
|
DEFINE_FIELD(FStrifeDialogueReply, NeedsGold);
|