private void OnDialogueResume(DialogueItem dialogue, Subject <DialogueEvent> events) { var story = dialogue.story; // If there was a previous event, we should re-run it with null continue. This puts the choices back in the // right place but doesn't mess with the state of the conversation if (dialogue.lastEvent != null) { // Mark the event as silent, even if it's not, to prevent it from rendering another box var wasSilent = dialogue.lastEvent.silent; dialogue.lastEvent.silent = true; OnDialogueEvent(dialogue, dialogue.lastEvent, events, null); dialogue.lastEvent.silent = wasSilent; } // Advance the story at least once. This way, if the other character talks first, the message will // be there. Otherwise, if there are choices after continuing at least once, the chat box will // default to open, making conversations where the player starts less confusing. if (story.canContinue) { OnNextStory(dialogue, events); } if (story.currentChoices?.Count > 0) { // We open with choices, so pop open the chat box controller for us m_ChatBoxController.ShowOptions(); } else { m_ChatBoxController.HideOptions(); m_ConversationController.StartTyping(false); } }
private void OnDialogueEnded(DialogueItem dialogue, CompositeDisposable subscriptions) { subscriptions.Dispose(); dialogue.ended = true; m_ChatBoxController.HideOptions(); m_ConversationController.StopTyping(); m_DialogueEnded.OnNext(new DialogEndedEvent { item = selectedDialogue.Value, profile = profile.Value, succeeded = dialogue.succeeded }); m_ExitButton.SetActive(true); m_ChatBoxController.gameObject.SetActive(false); AnalyticsEvent.LevelComplete(profile.Value.name, new Dictionary <string, object>() { { "succeeded", dialogue.succeeded } }); var eventHitBuilder1 = new EventHitBuilder() .SetEventCategory("conversation") .SetEventAction("ended") .SetEventLabel(profile.Value.name) .SetEventValue(dialogue.succeeded ? 1L : 0L); GoogleAnalyticsV4.getInstance().LogEvent(eventHitBuilder1); }
private void OnNextStory(DialogueItem dialogue, Subject <DialogueEvent> events, Choice choice = null, string overrideMessage = null, bool silent = false) { var story = dialogue.story; var self = false; var noDelay = false; var noBubble = false; var success = false; if (choice != null) { var substring = choice.text?.Substring(0, Mathf.Min(choice.text?.Length ?? 0, 8)); AnalyticsEvent.Custom("choice", new Dictionary <string, object>() { { "path", story.state?.currentPathString }, { "profile", profile.Value?.name }, { "text", substring }, { "index", choice.index } }); var eventHitBuilder1 = new EventHitBuilder() .SetEventCategory("conversation") .SetEventAction("choice") .SetEventLabel(choice.text) .SetCustomDimension(0, story.state?.currentPathString) .SetCustomDimension(1, profile.Value?.name); GoogleAnalyticsV4.getInstance().LogEvent(eventHitBuilder1); story.ChooseChoiceIndex(choice.index); self = true; noDelay = true; } var eventHitBuilder2 = new EventHitBuilder() .SetEventCategory("conversation") .SetEventAction("position") .SetEventLabel(story.currentText) .SetCustomDimension(0, story.state?.currentPathString) .SetCustomDimension(1, profile.Value?.name); GoogleAnalyticsV4.getInstance().LogEvent(eventHitBuilder2); if (story.canContinue) { var message = story.Continue(); success = story.currentTags.Any(s => s.ToLower() == DialogueConversationController.success); noBubble = story.currentTags.Any(s => s.ToLower() == DialogueConversationController.noBubble); self |= story.currentTags.Any(s => s.ToLower() == DialogueConversationController.self); silent |= story.currentTags.Any(s => s.ToLower() == DialogueConversationController.silent); if (success) { dialogue.succeeded = true; } // Remove starting and ending quotation marks, extraneous line returns message = message.Trim('\n', '"'); if (message.StartsWith(m_StartOfSelfConversation)) { self |= true; message = message.Substring(1); } // If this is the last message of the dialogue, no delay if (!story.canContinue && (story.currentChoices?.Count ?? 0) == 0) { noDelay = true; } var dialogueEvent = new DialogueEvent { text = overrideMessage ?? message, noBubble = noBubble, choices = story.currentChoices, self = self, noDelay = noDelay, success = success, silent = silent }; dialogue.lastEvent = dialogueEvent; events.OnNext(dialogueEvent); } else { var dialogueEvent = new DialogueEvent { finished = true, success = dialogue.succeeded }; dialogue.lastEvent = dialogueEvent; events.OnNext(dialogueEvent); } }
private void OnDialogueEvent(DialogueItem dialogue, DialogueEvent data, Subject <DialogueEvent> events, Subject <DialogueEvent> dialogueContinue) { // Handle the function call for riddles here. This flag prevents also fake choices used to implement // the "Future<int>" that we're doing here from appearing inside the chat box. var story = dialogue.story; var dataRiddle = data.riddle; if (dataRiddle != null) { dialogue.suppressChoices = true; m_ChatBoxController.RequestInput(res => { #if UNITY_WEBGL // ReSharper disable once SpecifyStringComparison var answerWasCorrect = res.Trim().ToLower() == dataRiddle.Trim().ToLower(); #else var answerWasCorrect = string.Equals(res, riddle, StringComparison.CurrentCultureIgnoreCase); #endif AnalyticsEvent.Custom("riddle_input", new Dictionary <string, object>() { { "answer", res } }); var eventHitBuilder1 = new EventHitBuilder() .SetEventCategory("conversation") .SetEventAction("riddle_input") .SetEventLabel(res); GoogleAnalyticsV4.getInstance().LogEvent(eventHitBuilder1); OnNextStory(dialogue, events, story.currentChoices[answerWasCorrect ? 1 : 0], overrideMessage: res); }); } else if (data.text != null && !data.silent) { var self = data.self; var text = data.text; // Render images if one was specified var spriteName = ParseSpriteName(text); // Retrieves all the sprites that have been referenced SOMEWHERE in the scene. See m_Sprites on this // instance and add the sprites to make sure the image can render in the chat. var sprite = spriteName == null ? null : Drawing.sprites[spriteName]; m_ConversationController.Add(new[] { new ChatMessage { noBubble = data.noBubble, message = spriteName != null ? "" : data.text, image = sprite, self = self } }); } // This is definitely no longer the first message. dialogue.first = false; // Show the choices if there are any to show AND we're not awaiting an input if (data.choices?.Count == 0 && dataRiddle == null) { dialogueContinue?.OnNext(data); } else if (data.choices?.Count > 0 && dataRiddle == null) { m_ChatBoxController.responses.Clear(); if (dialogue.suppressChoices) { dialogue.suppressChoices = false; } else { foreach (var choice in data.choices) { m_ChatBoxController.responses.Add(choice.text); } } } }
/// <summary> /// Rigs the views in this screen to start showing the specified dialogue /// </summary> /// <param name="dialogue"></param> /// <returns></returns> private CompositeDisposable ResumeStory(DialogueItem dialogue, DialogueItem oldDialogue = null) { var subscriptions = new CompositeDisposable(); if (oldDialogue != null) { m_ConversationController.SaveState(oldDialogue); } m_ConversationController.RestoreState(dialogue); // Disable the exit button m_ExitButton.SetActive(false); // Make sure the chat box is enabled m_ChatBoxController.gameObject.SetActive(true); // Tracks all the dialogue events in the story var events = new Subject <DialogueEvent>(); // A subject that's used to delay incoming chats var dialogueContinue = new Subject <DialogueEvent>(); // Get the actual story var story = dialogue.story; // Gives the player's name and gender a variable if (story.variablesState.ContainsDefaultGlobalVariable(playerName)) { story.variablesState[playerName] = PlayerProfileController.instance.playerName; } if (story.variablesState.ContainsDefaultGlobalVariable(gender)) { story.variablesState[gender] = PlayerProfileController.instance.playerGender; } if (!story.ContainsExternalBinding(riddle)) { story.BindExternalFunction(riddle, (string riddle) => { // This prevents the function from causing side effects when one of the story threads is // unparking. For example, during a reset state or going to a specific knot. if (dialogue.unparking) { return; } events.OnNext(new DialogueEvent() { riddle = riddle, noDelay = true }); }); } // Start the story when this screen comes into view IObservable <int> transition; if (m_UiScreenView.currentScreen == m_Screen.screenIndex && m_UiScreenView.screensTransitioning == 0) { transition = Observable.Return(m_Screen.screenIndex); } else { transition = m_UiScreenView.onScreenBeginTransition.AsObservable() .Where(index => index == m_Screen.screenIndex); } // Triggers the transition to the scene based on the current activity. transition.Take(1) .Subscribe(ignored => { OnDialogueResume(dialogue, events); }) .AddTo(subscriptions); // When the dialogue ends, fire a dialogue ended event for the current dialogue events .Where(d => d.finished) .Subscribe(ignored => { OnDialogueEnded(dialogue, subscriptions); }) .AddTo(subscriptions); // Show messages and choices from the dialogue system events .Do(d2 => { if (!d2.noBubble) { m_ConversationController.StartTyping(d2.self); } }) .Where(data => data != null) // Include a typing delay .SelectMany(data => Observable.Return(data) // Show the typing box right away, before the delay .Delay(TimeSpan.FromSeconds(data.noBubble && dialogue.first || data.noDelay ? 0 : (data.text ?? "").StartsWith("!") ? m_SecondsPerImageWriting : (m_SecondsPerCharacterWriting * data.text?.Length ?? 0)))) .Subscribe(data => { OnDialogueEvent(dialogue, data, events, dialogueContinue); }) .AddTo(subscriptions); // Continue with a choice whenever one is given m_ChatBoxController.choices .Subscribe(choice => { OnNextStory(dialogue, events, story.currentChoices[choice.index]); }) .AddTo(subscriptions); // Continue after a short delay when a continue is requested without choices dialogueContinue .Where(ignored => !dialogue.ended) .SelectMany(data => Observable.Return(data) .Delay(TimeSpan.FromSeconds(data.text == null ? 0 : data.text.StartsWith("!") ? m_SecondsPerImageReading : (m_SecondsPerCharacterReading * data.text.Length)))) .Subscribe(ignored => { OnNextStory(dialogue, events); }) .AddTo(subscriptions); return(subscriptions); }