// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System; namespace PixelCrushers.DialogueSystem { /// /// Mediates between a ConversationModel (data) and ConversationView (user interface) to run a /// conversation. /// public class ConversationController { /// /// The data model of the conversation. /// private ConversationModel m_model = null; /// /// The view (user interface) of the current state of the conversation. /// private ConversationView m_view = null; /// /// The current state of the conversation. /// private ConversationState m_state = null; /// /// Indicates whether the ConversationController is currently running a conversation. /// /// /// true if a conversation is active; false if the conversation is done. /// public bool isActive { get; private set; } /// /// Gets the actor info for this conversation. /// /// /// The actor info. /// public CharacterInfo actorInfo { get { return (m_model != null) ? m_model.actorInfo : null; } } public ConversationModel conversationModel { get { return m_model; } } public ConversationView conversationView { get { return m_view; } } public ConversationState currentState { get { return m_state; } } public ActiveConversationRecord activeConversationRecord { get; set; } public bool reevaluateLinksAfterSubtitle { get; set; } /// /// Gets or sets the IsDialogueEntryValid delegate. /// public IsDialogueEntryValidDelegate isDialogueEntryValid { get { return m_model.isDialogueEntryValid; } set { m_model.isDialogueEntryValid = value; } } /// /// Set true to choice randomly from the next list of valid NPC subtitles instead of /// using the first one in the list. /// public bool randomizeNextEntry { get; set; } /// /// If randomizeNextEntry is set, it checks this property. If it's /// also set, tries to avoid choosing the same entry it did last time. /// public bool randomizeNextEntryNoDuplicate { get; set; } /// /// Gets the conversant info for this conversation. /// /// /// The conversant info. /// public CharacterInfo conversantInfo { get { return (m_model != null) ? m_model.conversantInfo : null; } } /// @cond FOR_V1_COMPATIBILITY public bool IsActive { get { return isActive; } private set { isActive = value; } } public CharacterInfo ActorInfo { get { return actorInfo; } } public ConversationModel ConversationModel { get { return conversationModel; } } public ConversationView ConversationView { get { return conversationView; } } public IsDialogueEntryValidDelegate IsDialogueEntryValid { get { return isDialogueEntryValid; } set { isDialogueEntryValid = value; } } public CharacterInfo ConversantInfo { get { return conversantInfo; } } /// @endcond public delegate void EndConversationDelegate(ConversationController ConversationController); private EndConversationDelegate m_endConversationHandler = null; private int m_currentConversationID; private Response m_currentResponse = null; // Records time when last conversation ended in case a new conversation starts on the same // frame and needs to know. private static int _frameLastConversationEnded = -1; public static int frameLastConversationEnded { get { return _frameLastConversationEnded; } set { _frameLastConversationEnded = value; } } public ConversationController() { } /// /// Initializes a new ConversationController and starts the conversation in the model. /// Also sends OnConversationStart messages to the participants. /// /// /// Data model of the conversation. /// /// /// View to use to provide a user interface for the conversation. /// /// Reevaluate links after subtitle in case sequence or OnConversationLine changed link conditions. /// Always force response menu if only one PC node. /// /// Handler to call to inform when the conversation is done. /// public ConversationController(ConversationModel model, ConversationView view, bool reevaluateLinksAfterSubtitle, bool alwaysForceResponseMenu, EndConversationDelegate endConversationHandler) { isActive = true; this.m_model = model; this.m_view = view; this.m_endConversationHandler = endConversationHandler; this.randomizeNextEntry = false; this.reevaluateLinksAfterSubtitle = reevaluateLinksAfterSubtitle; DialogueManager.instance.currentConversationState = model.firstState; model.InformParticipants(DialogueSystemMessages.OnConversationStart); view.FinishedSubtitleHandler += OnFinishedSubtitle; view.SelectedResponseHandler += OnSelectedResponse; m_currentConversationID = model.GetConversationID(model.firstState); SetConversationOverride(model.firstState); GotoState(model.firstState); } /// /// Initializes a ConversationController and starts the conversation in the model. /// Also sends OnConversationStart messages to the participants. /// /// /// Data model of the conversation. /// /// /// View to use to provide a user interface for the conversation. /// /// /// Handler to call to inform when the conversation is done. /// public void Initialize(ConversationModel model, ConversationView view, bool reevaluateLinksAfterSubtitle, bool alwaysForceResponseMenu, EndConversationDelegate endConversationHandler) { isActive = true; this.m_model = model; this.m_view = view; this.m_endConversationHandler = endConversationHandler; this.randomizeNextEntry = false; this.reevaluateLinksAfterSubtitle = reevaluateLinksAfterSubtitle; DialogueManager.instance.currentConversationState = model.firstState; model.InformParticipants(DialogueSystemMessages.OnConversationStart); view.FinishedSubtitleHandler += OnFinishedSubtitle; view.SelectedResponseHandler += OnSelectedResponse; m_currentConversationID = model.GetConversationID(model.firstState); SetConversationOverride(model.firstState); GotoState(model.firstState); } public void Initialize(ConversationModel model, ConversationView view, bool alwaysForceResponseMenu, EndConversationDelegate endConversationHandler) { isActive = true; this.m_model = model; this.m_view = view; this.m_endConversationHandler = endConversationHandler; this.randomizeNextEntry = false; this.reevaluateLinksAfterSubtitle = false; DialogueManager.instance.currentConversationState = model.firstState; model.InformParticipants(DialogueSystemMessages.OnConversationStart); view.FinishedSubtitleHandler += OnFinishedSubtitle; view.SelectedResponseHandler += OnSelectedResponse; m_currentConversationID = model.GetConversationID(model.firstState); SetConversationOverride(model.firstState); GotoState(model.firstState); } /// /// Closes the currently-running conversation, which also sends OnConversationEnd messages /// to the participants. /// public void Close() { if (isActive) { if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Ending conversation.", new System.Object[] { DialogueDebug.Prefix })); isActive = false; _frameLastConversationEnded = Time.frameCount; m_view.displaySettings.conversationOverrideSettings = null; m_view.FinishedSubtitleHandler -= OnFinishedSubtitle; m_view.SelectedResponseHandler -= OnSelectedResponse; m_view.Close(); DialogueManager.instance.lastConversationEnded = m_model.conversationTitle; m_model.InformParticipants(DialogueSystemMessages.OnConversationEnd, true); if (m_endConversationHandler != null) m_endConversationHandler(this); DialogueManager.instance.currentConversationState = null; } } /// /// Goes to a conversation state. If the state is null, the conversation ends. /// /// /// State. /// public void GotoState(ConversationState state) { this.m_state = state; DialogueManager.instance.currentConversationState = state; if (state != null) { if (state.subtitle != null) state.subtitle.activeConversationRecord = activeConversationRecord; // Check for change of conversation: var newConversationID = m_model.GetConversationID(state); if (newConversationID != m_currentConversationID) { m_currentConversationID = newConversationID; m_model.InformParticipants(DialogueSystemMessages.OnLinkedConversationStart, true); m_model.UpdateParticipantsOnLinkedConversation(newConversationID); m_view.SetPCPortrait(m_model.GetPCSprite(), m_model.GetPCName()); SetConversationOverride(state); } // Use view to show current state: if (state.isGroup) { m_view.ShowLastNPCSubtitle(); } else { bool isPCResponseMenuNext, isPCAutoResponseNext; AnalyzePCResponses(state, out isPCResponseMenuNext, out isPCAutoResponseNext); m_view.StartSubtitle(state.subtitle, isPCResponseMenuNext, isPCAutoResponseNext); } } else { Close(); } } private void AnalyzePCResponses(ConversationState state, out bool isPCResponseMenuNext, out bool isPCAutoResponseNext) { var alwaysForceMenu = m_view.displaySettings.GetAlwaysForceResponseMenu(); var hasForceMenu = false; var hasForceAuto = false; var numPCResponses = (state.pcResponses != null) ? state.pcResponses.Length : 0; for (int i = 0; i < numPCResponses; i++) { if (state.pcResponses[i].formattedText.forceMenu) { hasForceMenu = true; } if (state.pcResponses[i].formattedText.forceAuto) { hasForceAuto = true; break; // [auto] takes precedence over [f]. } } isPCResponseMenuNext = !state.hasNPCResponse && !hasForceAuto && (numPCResponses > 1 || hasForceMenu || (numPCResponses == 1 && alwaysForceMenu && !string.IsNullOrEmpty(state.pcResponses[0].formattedText.text))); isPCAutoResponseNext = !state.hasNPCResponse && hasForceAuto || (numPCResponses == 1 && string.IsNullOrEmpty(state.pcResponses[0].formattedText.text)) || (numPCResponses == 1 && !hasForceMenu && (!alwaysForceMenu || state.pcResponses[0].destinationEntry.isGroup)); } private void SetConversationOverride(ConversationState state) { m_view.displaySettings.conversationOverrideSettings = m_model.GetConversationOverrideSettings(state); DialogueManager.displaySettings.conversationOverrideSettings = m_view.displaySettings.conversationOverrideSettings; } /// /// Handles the finished subtitle event. If the current conversation state has an NPC /// response, the conversation proceeds to that response. Otherwise, if the current /// state has PC responses, then the response menu is shown (or if it has a single /// auto-response, the conversation proceeds directly to that response). If there are no /// responses, the conversation ends. /// /// /// Sender. /// /// /// Event args. /// public void OnFinishedSubtitle(object sender, EventArgs e) { if (reevaluateLinksAfterSubtitle && !DialogueManager.useLinearGroupMode) { m_model.UpdateResponses(m_state); } DialogueManager.instance.activeConversation = activeConversationRecord; var randomize = randomizeNextEntry; randomizeNextEntry = false; if (DialogueManager.useLinearGroupMode) // In linear group mode, check responses once subtitle & its sequence are finished. { m_model.UpdateResponses(m_state); } if (m_state.HasValidNPCResponse()) { GotoState(m_model.GetState(randomize ? m_state.GetRandomNPCEntry(randomizeNextEntryNoDuplicate) : m_state.firstNPCResponse.destinationEntry)); } else if (m_state.HasValidPCResponses()) { bool isPCResponseMenuNext, isPCAutoResponseNext; AnalyzePCResponses(m_state, out isPCResponseMenuNext, out isPCAutoResponseNext); if (isPCAutoResponseNext) { GotoState(m_model.GetState(m_state.pcAutoResponse.destinationEntry)); } else { m_view.StartResponses(m_state.subtitle, m_state.pcResponses); } } else { Close(); } } /// /// Handles the selected response event by proceeding to the state associated with the /// selected response. /// /// /// Sender. /// /// /// Selected response event args. /// public void OnSelectedResponse(object sender, SelectedResponseEventArgs e) { DialogueManager.instance.activeConversation = activeConversationRecord; GotoState(m_model.GetState(e.DestinationEntry)); } /// /// Follows the first PC response in the current state. /// public void GotoFirstResponse() { if (m_state != null) { if (m_state.pcResponses.Length > 0) { m_view.SelectResponse(new SelectedResponseEventArgs(m_state.pcResponses[0])); } } } /// /// Follows the last PC response in the current state. /// public void GotoLastResponse() { if (m_state != null) { if (m_state.pcResponses.Length > 0) { m_view.SelectResponse(new SelectedResponseEventArgs(m_state.pcResponses[m_state.pcResponses.Length - 1])); } } } /// /// Follows a random PC response in the current state. /// public void GotoRandomResponse() { if (m_state != null) { if (m_state.pcResponses.Length > 0) { m_view.SelectResponse(new SelectedResponseEventArgs(m_state.pcResponses[UnityEngine.Random.Range(0, m_state.pcResponses.Length)])); } } } /// /// Follows a response that has been set as the current response by SetCurrentResponse. /// public void GotoCurrentResponse() { if (m_currentResponse != null) { m_view.SelectResponse(new SelectedResponseEventArgs(m_currentResponse)); } else { GotoFirstResponse(); } } /// /// Sets the current response, which can be used by GotoCurrentResponse. Typically only /// used if the dialogue UI's timeout action specifies to select the current response. /// /// public void SetCurrentResponse(Response response) { m_currentResponse = response; } public void UpdateResponses() { if (m_state != null) { m_model.UpdateResponses(m_state); OnFinishedSubtitle(this, EventArgs.Empty); } } /// /// Sets the portrait sprite to use in the UI for an actor. /// This is used when the SetPortrait() sequencer command changes an actor's image. /// /// Actor name. /// Portrait sprite. public void SetActorPortraitSprite(string actorName, Sprite sprite) { m_model.SetActorPortraitSprite(actorName, sprite); m_view.SetActorPortraitSprite(actorName, sprite); } } }