/// <summary> /// Begins the conversation with the NPC. /// Method will block for input if required. /// </summary> public async Task BeginConversation() { if (this.EnqueuedScriptItemCallback == null) { throw new Ultima5ReduxException("Called BeginConversation without declaring a EnqueuedScriptItemCallback."); } System.Console.WriteLine(@"---- PRE-CONVERSATION SCRIPT -----"); _script.PrintComprehensiveScript(); System.Console.WriteLine(@"---- STARTING CONVERSATION -----"); // when a new section comes up, we can add it and then proceed to the next line by adding it // this is perpetual as long as there is more to see _conversationOrder.Add((int)TalkScript.TalkConstants.Description); _conversationOrder.Add((int)TalkScript.TalkConstants.Greeting); _conversationOrderScriptLines.Add(_script.GetScriptLine(TalkScript.TalkConstants.Description)); _conversationOrderScriptLines.Add(_script.GetScriptLine(TalkScript.TalkConstants.Greeting)); // some of these operations can be expensive, so let's call them once and store instead bool npcKnowsAvatar = Npc.KnowTheAvatar; // the index into conversationOrder int nConversationIndex = 0; // while there are more conversation lines to process while (!ConversationEnded) { // if we do not have any conversation left, then we will prompt for questions ///// NO DIALOG LEFT - USER RESPONDS ///// This will result in processable conversation, so it will just fall through while (nConversationIndex >= _conversationOrder.Count) { EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PromptUserForInput_UserInterest)); // we wait patiently for the user to respond await AwaitResponse(); string userResponse = GetResponse(); if (userResponse == string.Empty) { userResponse = TalkScript.TalkConstants.Bye.ToString().ToLower(); } // if someone has asked their name then we need to add "My name is " to beginning else if (userResponse.ToLower() == "name") { TalkScript.ScriptLine scriptLine = new TalkScript.ScriptLine(); scriptLine.AddScriptItem(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.MY_NAME_IS) + " ")); await ProcessLine(scriptLine); } if (Npc.Script.QuestionAnswers.AnswerIsAvailable(userResponse)) { // the user asked a question that we recognize, so let's process the answer await ProcessMultipleLines(Npc.Script.QuestionAnswers.GetQuestionAnswer(userResponse).Answer.SplitIntoSections(), -1); } else { // we didn't recognize the user input - we will tell them so TalkScript.ScriptLine unrecognizedLine = new TalkScript.ScriptLine(); unrecognizedLine.AddScriptItem(new TalkScript.ScriptItem(TalkScript.TalkCommand.UserInputNotRecognized)); await ProcessLine(unrecognizedLine); } // one of the ProcessLine method calls told us that we are done talking // this is okay to quit anytime because we have already populated the queues with the final commands if (ConversationEnded) { return; } } // the current talk line int nTalkLineIndex = _conversationOrder[nConversationIndex]; // the current ScriptLine TalkScript.ScriptLine currentLine = _script.GetScriptLine(_conversationOrder[nConversationIndex]); // Split the line into sections. This greatly simplifies the proceeding loops. List <TalkScript.SplitScriptLine> splitLines = currentLine.SplitIntoSections(); // currentLine = unsplit line with all content // splitLines = a list of all the split up lines // curentSplitLine = current section of the currentLine Debug.Assert(splitLines.Count > 0); // If an AvatarsName is used in conversation, then we may need to process additional logic or ignore the line altogether // if it's a greeting AND her greeting includes my name AND they have NOT yet met the avatar // OR if the Name line contains an IfElseKnowsName (#Eb) if ((!npcKnowsAvatar && _conversationOrder[nConversationIndex] == (int)TalkScript.TalkConstants.Greeting) && (currentLine.ContainsCommand(TalkScript.TalkCommand.AvatarsName) || _script.GetScriptLine(TalkScript.TalkConstants.Name).ContainsCommand(TalkScript.TalkCommand.IfElseKnowsName))) { // randomly add an introduction of the Avatar since they haven't met him if (_gameStateRef.OneInXOdds(2))// || true) { // okay, tell them who you are EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, "\nI am called " + Npc.Name)); } } // if in label && next line include <AvatarName>, then skip label const int StartingIndexForLabel = 0; ///// IT'S A LABEL // if we have just begun a label section, then let's handle it slightly difference then the normal conversation if (splitLines[StartingIndexForLabel].IsLabelDefinition()) { Debug.Assert(splitLines[StartingIndexForLabel].NumberOfScriptItems == 2, "If it is a label definition, then it must have only 2 items defined in it"); int nLabel = splitLines[StartingIndexForLabel].GetScriptItem(1).LabelNum; Debug.Assert(nLabel >= 0 && nLabel <= TalkScript.TOTAL_LABELS - 1, "Label number must be between 0 and 9"); // get the label object TalkScript.ScriptTalkLabel scriptLabel = _script.TalkLabels.GetScriptLabel(nLabel); // Sept 23, 2019 - if we are in a label, but don't know the Avatar's name, then we just move on // if we don't then (#Eb) can expect an answer to a question that we never show the user because // they don't know the Avatar if (scriptLabel.InitialLine.ContainsCommand(TalkScript.TalkCommand.AvatarsName) && !npcKnowsAvatar) { nConversationIndex++; continue; } // we ar going through each of the line sections, but are skipping the first one since we know it is just a label // definition await ProcessMultipleLines(scriptLabel.InitialLine.SplitIntoSections(), nTalkLineIndex); string userResponse; if (scriptLabel.ContainsQuestions()) { int nTimes = 0; do { // need to figure out if we are going to ask a question... if (nTimes++ == 0) { EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PromptUserForInput_NPCQuestion)); } else { EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.WHAT_YOU_SAY)));//"What didst thou say?")); EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PromptUserForInput_NPCQuestion)); } // we wait patiently for the user to respond await AwaitResponse(); userResponse = GetResponse(); } while (userResponse == String.Empty); } else { // is there an actual answer to the question? // if not, then we have already processed our dialog line, let's move onto the next dialog item nConversationIndex++; continue; } // There is an answer available from the NPC if (scriptLabel.QuestionAnswers.AnswerIsAvailable(userResponse)) { // let's get the answer details including the ScriptLine that will follow TalkScript.ScriptQuestionAnswer qa = scriptLabel.QuestionAnswers.GetQuestionAnswer(userResponse); TalkScript.ScriptLine npcResponseLine = qa.Answer; await ProcessMultipleLines(qa.Answer.SplitIntoSections(), nTalkLineIndex); } else // you have entered an answer that isn't in their dialog - so default answer { // Process default response foreach (TalkScript.ScriptLine defaultLine in scriptLabel.DefaultAnswers) { await ProcessMultipleLines(defaultLine.SplitIntoSections(), nTalkLineIndex); } } } else // it's not a label NOR is a question and answer section // it's just a simple text section (probably from the description or greeting) { await ProcessMultipleLines(splitLines, nTalkLineIndex); } // we have gone through all instructions, so lets move onto the next conversation line nConversationIndex++; } }
/// <summary> /// Process an individual line. /// The method evaluates each scriptitem within the line, adding items and text to the output buffer as required. /// Mega function /// </summary> /// <remarks>If you do not SPLIT the line ahead of time then it is going to act wonky...</remarks> /// <param name="scriptLine"></param> /// <param name="nTalkLineIndex"></param> /// <param name="nSplitLine"></param> /// <returns></returns> private async Task ProcessLine(TalkScript.ScriptLine scriptLine, int nTalkLineIndex, int nSplitLine) { // if they already know the avatar then they aren't going to ask again if (scriptLine.ContainsCommand(TalkScript.TalkCommand.AskName) && Npc.KnowTheAvatar) { _currentSkipInstruction = SkipInstruction.DontSkip; return; } int nItem = 0; int nItems = scriptLine.NumberOfScriptItems; do { TalkScript.ScriptItem item = scriptLine.GetScriptItem(nItem); // if this is the very first position of conversation // we must describe what we "see" if (nTalkLineIndex == (int)TalkScript.TalkConstants.Description && nSplitLine == 0 && nItem == 0) { EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.YOU_SEE) + " ")); } switch (item.Command) { case TalkScript.TalkCommand.IfElseKnowsName: Debug.Assert(nItems == 1); if (Npc.KnowTheAvatar) { // we continue to the next block, but skip the one after _currentSkipInstruction = SkipInstruction.SkipAfterNext; return; } else { // we skip the next block because it is the line used when we actually know the Avatar _currentSkipInstruction = SkipInstruction.SkipNext; return; } case TalkScript.TalkCommand.AvatarsName: // we should already know if they know the avatars name.... Debug.Assert(Npc.KnowTheAvatar); EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, TextProcessItem(item))); break; case TalkScript.TalkCommand.AskName: EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.WHATS_YOUR_NAME))); EnqueueToOutputBuffer(item); // we actually wait in the function for the user to respond await AwaitResponse(); string avatarNameResponse = GetResponse(); // did they actually provide the Avatars name? if (avatarNameResponse.ToLower() == _gameStateRef.AvatarsName.ToLower()) { // i met them _gameStateRef.SetMetNPC(Npc); EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.PLEASURE))); break; } else { EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.IF_SAY_SO))); } break; case TalkScript.TalkCommand.CallGuards: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.Change: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.DefineLabel: // if I find a goto label, then i expect I have no further conversation lines left //Debug.Assert(nConversationIndex == conversationOrderScriptLines.Count - 1); // we are going to add the GotoLabel to the script _conversationOrder.Add((int)_script.GetScriptLineLabelIndex(item.LabelNum)); _conversationOrderScriptLines.Add(_script.GetScriptLine(_script.GetScriptLineLabelIndex(item.LabelNum))); _currentSkipInstruction = SkipInstruction.SkipToLabel; return; //return SkipInstruction.SkipToLabel; case TalkScript.TalkCommand.EndCoversation: EnqueueToOutputBuffer(item); ConversationEnded = true; break; case TalkScript.TalkCommand.Gold: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.JoinParty: if (_gameStateRef.IsFullParty()) { string noJoinResponse = GetConversationStr(DataOvlReference.ChunkPhrasesConversation.CANT_JOIN_1) + GetConversationStr(DataOvlReference.ChunkPhrasesConversation.CANT_JOIN_2); EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, noJoinResponse)); //"Thou hast no room for me in thy party! Seek me again if one of thy members doth leave thee.\n")); } else { EnqueueToOutputBuffer(item); ConversationEnded = true; } break; case TalkScript.TalkCommand.KarmaMinusOne: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.KarmaPlusOne: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.KeyWait: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.NewLine: EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, TextProcessItem(item))); break; case TalkScript.TalkCommand.Pause: EnqueueToOutputBuffer(item); break; case TalkScript.TalkCommand.PlainString: // we put it through the processor to change the text around if we are wrapped in a rune tag EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, TextProcessItem(item))); break; case TalkScript.TalkCommand.Rune: EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, TextProcessItem(item))); break; case TalkScript.TalkCommand.UserInputNotRecognized: EnqueueToOutputBuffer(new TalkScript.ScriptItem(TalkScript.TalkCommand.PlainString, GetConversationStr(DataOvlReference.ChunkPhrasesConversation.CANNOT_HELP) + "\n")); break; case TalkScript.TalkCommand.Unknown_Enter: break; case TalkScript.TalkCommand.DoNothingSection: // appears to signify an empty section break; case TalkScript.TalkCommand.StartLabelDefinition: // dirty - advance past the label that it will sink in... nItem++; break; case TalkScript.TalkCommand.Or: case TalkScript.TalkCommand.StartNewSection: throw new Ultima5ReduxException("We should never see the <OR> or <A2> code in conversation"); case TalkScript.TalkCommand.GotoLabel: break; case TalkScript.TalkCommand.PromptUserForInput_NPCQuestion: break; case TalkScript.TalkCommand.PromptUserForInput_UserInterest: break; default: throw new Ultima5ReduxException("Received TalkCommand I wasn't expecting during conversation"); } nItem++; // while we still have more items in the current split line } while (nItem < nItems); _currentSkipInstruction = SkipInstruction.DontSkip; return; //return SkipInstruction.DontSkip; }