409 lines
16 KiB
C#
409 lines
16 KiB
C#
// Copyright (c) Pixel Crushers. All rights reserved.
|
|
|
|
using UnityEngine;
|
|
using System;
|
|
|
|
namespace PixelCrushers.DialogueSystem
|
|
{
|
|
|
|
/// <summary>
|
|
/// Mediates between a ConversationModel (data) and ConversationView (user interface) to run a
|
|
/// conversation.
|
|
/// </summary>
|
|
public class ConversationController
|
|
{
|
|
|
|
/// <summary>
|
|
/// The data model of the conversation.
|
|
/// </summary>
|
|
private ConversationModel m_model = null;
|
|
|
|
/// <summary>
|
|
/// The view (user interface) of the current state of the conversation.
|
|
/// </summary>
|
|
private ConversationView m_view = null;
|
|
|
|
/// <summary>
|
|
/// The current state of the conversation.
|
|
/// </summary>
|
|
private ConversationState m_state = null;
|
|
|
|
/// <summary>
|
|
/// Indicates whether the ConversationController is currently running a conversation.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if a conversation is active; <c>false</c> if the conversation is done.
|
|
/// </value>
|
|
public bool isActive { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the actor info for this conversation.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The actor info.
|
|
/// </value>
|
|
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; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the IsDialogueEntryValid delegate.
|
|
/// </summary>
|
|
public IsDialogueEntryValidDelegate isDialogueEntryValid
|
|
{
|
|
get { return m_model.isDialogueEntryValid; }
|
|
set { m_model.isDialogueEntryValid = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set true to choice randomly from the next list of valid NPC subtitles instead of
|
|
/// using the first one in the list.
|
|
/// </summary>
|
|
public bool randomizeNextEntry { get; set; }
|
|
|
|
/// <summary>
|
|
/// If randomizeNextEntry is set, it checks this property. If it's
|
|
/// also set, tries to avoid choosing the same entry it did last time.
|
|
/// </summary>
|
|
public bool randomizeNextEntryNoDuplicate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the conversant info for this conversation.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The conversant info.
|
|
/// </value>
|
|
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; } }
|
|
|
|
public ConversationController()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new ConversationController and starts the conversation in the model.
|
|
/// Also sends OnConversationStart messages to the participants.
|
|
/// </summary>
|
|
/// <param name='model'>
|
|
/// Data model of the conversation.
|
|
/// </param>
|
|
/// <param name='view'>
|
|
/// View to use to provide a user interface for the conversation.
|
|
/// </param>
|
|
/// <param name='endConversationHandler'>
|
|
/// Handler to call to inform when the conversation is done.
|
|
/// </param>
|
|
public ConversationController(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;
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a ConversationController and starts the conversation in the model.
|
|
/// Also sends OnConversationStart messages to the participants.
|
|
/// </summary>
|
|
/// <param name='model'>
|
|
/// Data model of the conversation.
|
|
/// </param>
|
|
/// <param name='view'>
|
|
/// View to use to provide a user interface for the conversation.
|
|
/// </param>
|
|
/// <param name='endConversationHandler'>
|
|
/// Handler to call to inform when the conversation is done.
|
|
/// </param>
|
|
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;
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the currently-running conversation, which also sends OnConversationEnd messages
|
|
/// to the participants.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Goes to a conversation state. If the state is <c>null</c>, the conversation ends.
|
|
/// </summary>
|
|
/// <param name='state'>
|
|
/// State.
|
|
/// </param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name='sender'>
|
|
/// Sender.
|
|
/// </param>
|
|
/// <param name='e'>
|
|
/// Event args.
|
|
/// </param>
|
|
public void OnFinishedSubtitle(object sender, EventArgs e)
|
|
{
|
|
DialogueManager.instance.activeConversation = activeConversationRecord;
|
|
var randomize = randomizeNextEntry;
|
|
randomizeNextEntry = false;
|
|
if (m_state.hasNPCResponse)
|
|
{
|
|
GotoState(m_model.GetState(randomize ? m_state.GetRandomNPCEntry(randomizeNextEntryNoDuplicate) : m_state.firstNPCResponse.destinationEntry));
|
|
}
|
|
else if (m_state.hasPCResponses)
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the selected response event by proceeding to the state associated with the
|
|
/// selected response.
|
|
/// </summary>
|
|
/// <param name='sender'>
|
|
/// Sender.
|
|
/// </param>
|
|
/// <param name='e'>
|
|
/// Selected response event args.
|
|
/// </param>
|
|
public void OnSelectedResponse(object sender, SelectedResponseEventArgs e)
|
|
{
|
|
DialogueManager.instance.activeConversation = activeConversationRecord;
|
|
GotoState(m_model.GetState(e.DestinationEntry));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Follows the first PC response in the current state.
|
|
/// </summary>
|
|
public void GotoFirstResponse()
|
|
{
|
|
if (m_state != null)
|
|
{
|
|
if (m_state.pcResponses.Length > 0)
|
|
{
|
|
m_view.SelectResponse(new SelectedResponseEventArgs(m_state.pcResponses[0]));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Follows the last PC response in the current state.
|
|
/// </summary>
|
|
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]));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Follows a random PC response in the current state.
|
|
/// </summary>
|
|
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)]));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Follows a response that has been set as the current response by SetCurrentResponse.
|
|
/// </summary>
|
|
public void GotoCurrentResponse()
|
|
{
|
|
if (m_currentResponse != null)
|
|
{
|
|
m_view.SelectResponse(new SelectedResponseEventArgs(m_currentResponse));
|
|
}
|
|
else
|
|
{
|
|
GotoFirstResponse();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
public void SetCurrentResponse(Response response)
|
|
{
|
|
m_currentResponse = response;
|
|
}
|
|
|
|
public void UpdateResponses()
|
|
{
|
|
if (m_state != null)
|
|
{
|
|
m_model.UpdateResponses(m_state);
|
|
OnFinishedSubtitle(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="actorName">Actor name.</param>
|
|
/// <param name="sprite">Portrait sprite.</param>
|
|
public void SetActorPortraitSprite(string actorName, Sprite sprite)
|
|
{
|
|
m_model.SetActorPortraitSprite(actorName, sprite);
|
|
m_view.SetActorPortraitSprite(actorName, sprite);
|
|
}
|
|
|
|
}
|
|
|
|
}
|