|
|
|
// This code is part of the Fungus library (https://github.com/snozbot/fungus)
|
|
|
|
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)
|
|
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
using UnityEngine.UI;
|
|
|
|
using System.Collections;
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
using System.Linq;
|
|
|
|
using MoonSharp.Interpreter;
|
|
|
|
|
|
|
|
namespace Fungus
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// Presents multiple choice buttons to the players.
|
|
|
|
/// </summary>
|
|
|
|
public class MenuDialog : MonoBehaviour
|
|
|
|
{
|
|
|
|
[Tooltip("Automatically select the first interactable button when the menu is shown.")]
|
|
|
|
[SerializeField] protected bool autoSelectFirstButton = false;
|
|
|
|
|
|
|
|
protected Button[] cachedButtons;
|
|
|
|
|
|
|
|
protected Slider cachedSlider;
|
|
|
|
private int nextOptionIndex;
|
|
|
|
|
|
|
|
#region Public members
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Currently active Menu Dialog used to display Menu options
|
|
|
|
/// </summary>
|
|
|
|
public static MenuDialog ActiveMenuDialog { get; set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A cached list of button objects in the menu dialog.
|
|
|
|
/// </summary>
|
|
|
|
/// <value>The cached buttons.</value>
|
|
|
|
public virtual Button[] CachedButtons { get { return cachedButtons; } }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A cached slider object used for the timer in the menu dialog.
|
|
|
|
/// </summary>
|
|
|
|
/// <value>The cached slider.</value>
|
|
|
|
public virtual Slider CachedSlider { get { return cachedSlider; } }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Sets the active state of the Menu Dialog gameobject.
|
|
|
|
/// </summary>
|
|
|
|
public virtual void SetActive(bool state)
|
|
|
|
{
|
|
|
|
gameObject.SetActive(state);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Returns a menu dialog by searching for one in the scene or creating one if none exists.
|
|
|
|
/// </summary>
|
|
|
|
public static MenuDialog GetMenuDialog()
|
|
|
|
{
|
|
|
|
if (ActiveMenuDialog == null)
|
|
|
|
{
|
|
|
|
// Use first Menu Dialog found in the scene (if any)
|
|
|
|
var md = GameObject.FindObjectOfType<MenuDialog>();
|
|
|
|
if (md != null)
|
|
|
|
{
|
|
|
|
ActiveMenuDialog = md;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ActiveMenuDialog == null)
|
|
|
|
{
|
|
|
|
// Auto spawn a menu dialog object from the prefab
|
|
|
|
GameObject prefab = Resources.Load<GameObject>("Prefabs/MenuDialog");
|
|
|
|
if (prefab != null)
|
|
|
|
{
|
|
|
|
GameObject go = Instantiate(prefab) as GameObject;
|
|
|
|
go.SetActive(false);
|
|
|
|
go.name = "MenuDialog";
|
|
|
|
ActiveMenuDialog = go.GetComponent<MenuDialog>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ActiveMenuDialog;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual void Awake()
|
|
|
|
{
|
|
|
|
Button[] optionButtons = GetComponentsInChildren<Button>();
|
|
|
|
cachedButtons = optionButtons;
|
|
|
|
|
|
|
|
Slider timeoutSlider = GetComponentInChildren<Slider>();
|
|
|
|
cachedSlider = timeoutSlider;
|
|
|
|
|
|
|
|
if (Application.isPlaying)
|
|
|
|
{
|
|
|
|
// Don't auto disable buttons in the editor
|
|
|
|
Clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
CheckEventSystem();
|
|
|
|
}
|
|
|
|
|
|
|
|
// There must be an Event System in the scene for Say and Menu input to work.
|
|
|
|
// This method will automatically instantiate one if none exists.
|
|
|
|
protected virtual void CheckEventSystem()
|
|
|
|
{
|
|
|
|
EventSystem eventSystem = GameObject.FindObjectOfType<EventSystem>();
|
|
|
|
if (eventSystem == null)
|
|
|
|
{
|
|
|
|
// Auto spawn an Event System from the prefab
|
|
|
|
GameObject prefab = Resources.Load<GameObject>("Prefabs/EventSystem");
|
|
|
|
if (prefab != null)
|
|
|
|
{
|
|
|
|
GameObject go = Instantiate(prefab) as GameObject;
|
|
|
|
go.name = "EventSystem";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual void OnEnable()
|
|
|
|
{
|
|
|
|
// The canvas may fail to update if the menu dialog is enabled in the first game frame.
|
|
|
|
// To fix this we just need to force a canvas update when the object is enabled.
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual IEnumerator WaitForTimeout(float timeoutDuration, Block targetBlock)
|
|
|
|
{
|
|
|
|
float elapsedTime = 0;
|
|
|
|
|
|
|
|
Slider timeoutSlider = CachedSlider;
|
|
|
|
|
|
|
|
while (elapsedTime < timeoutDuration)
|
|
|
|
{
|
|
|
|
if (timeoutSlider != null)
|
|
|
|
{
|
|
|
|
float t = 1f - elapsedTime / timeoutDuration;
|
|
|
|
timeoutSlider.value = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
elapsedTime += Time.deltaTime;
|
|
|
|
|
|
|
|
yield return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Clear();
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
|
|
|
|
HideSayDialog();
|
|
|
|
|
|
|
|
if (targetBlock != null)
|
|
|
|
{
|
|
|
|
targetBlock.StartExecution();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected IEnumerator CallBlock(Block block)
|
|
|
|
{
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
block.StartExecution();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected IEnumerator CallLuaClosure(LuaEnvironment luaEnv, Closure callback)
|
|
|
|
{
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
if (callback != null)
|
|
|
|
{
|
|
|
|
luaEnv.RunLuaFunction(callback, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Clear all displayed options in the Menu Dialog.
|
|
|
|
/// </summary>
|
|
|
|
public virtual void Clear()
|
|
|
|
{
|
|
|
|
StopAllCoroutines();
|
|
|
|
|
|
|
|
//if something was shown notify that we are ending
|
|
|
|
if(nextOptionIndex != 0)
|
|
|
|
MenuSignals.DoMenuEnd(this);
|
|
|
|
|
|
|
|
nextOptionIndex = 0;
|
|
|
|
|
|
|
|
var optionButtons = CachedButtons;
|
|
|
|
for (int i = 0; i < optionButtons.Length; i++)
|
|
|
|
{
|
|
|
|
var button = optionButtons[i];
|
|
|
|
button.onClick.RemoveAllListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < optionButtons.Length; i++)
|
|
|
|
{
|
|
|
|
var button = optionButtons[i];
|
|
|
|
if (button != null)
|
|
|
|
{
|
|
|
|
button.transform.SetSiblingIndex(i);
|
|
|
|
button.gameObject.SetActive(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Slider timeoutSlider = CachedSlider;
|
|
|
|
if (timeoutSlider != null)
|
|
|
|
{
|
|
|
|
timeoutSlider.gameObject.SetActive(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Hides any currently displayed Say Dialog.
|
|
|
|
/// </summary>
|
|
|
|
public virtual void HideSayDialog()
|
|
|
|
{
|
|
|
|
var sayDialog = SayDialog.GetSayDialog();
|
|
|
|
if (sayDialog != null)
|
|
|
|
{
|
|
|
|
sayDialog.FadeWhenDone = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Adds the option to the list of displayed options. Calls a Block when selected.
|
|
|
|
/// Will cause the Menu dialog to become visible if it is not already visible.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns><c>true</c>, if the option was added successfully.</returns>
|
|
|
|
/// <param name="text">The option text to display on the button.</param>
|
|
|
|
/// <param name="interactable">If false, the option is displayed but is not selectable.</param>
|
|
|
|
/// <param name="hideOption">If true, the option is not displayed but the menu knows that option can or did exist</param>
|
|
|
|
/// <param name="targetBlock">Block to execute when the option is selected.</param>
|
|
|
|
public virtual bool AddOption(string text, bool interactable, bool hideOption, Block targetBlock)
|
|
|
|
{
|
|
|
|
var block = targetBlock;
|
|
|
|
UnityEngine.Events.UnityAction action = delegate
|
|
|
|
{
|
|
|
|
EventSystem.current.SetSelectedGameObject(null);
|
|
|
|
StopAllCoroutines();
|
|
|
|
// Stop timeout
|
|
|
|
Clear();
|
|
|
|
HideSayDialog();
|
|
|
|
if (block != null)
|
|
|
|
{
|
|
|
|
var flowchart = block.GetFlowchart();
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
// Use a coroutine to call the block on the next frame
|
|
|
|
// Have to use the Flowchart gameobject as the MenuDialog is now inactive
|
|
|
|
flowchart.StartCoroutine(CallBlock(block));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return AddOption(text, interactable, hideOption, action);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Adds the option to the list of displayed options, calls a Lua function when selected.
|
|
|
|
/// Will cause the Menu dialog to become visible if it is not already visible.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns><c>true</c>, if the option was added successfully.</returns>
|
|
|
|
public virtual bool AddOption(string text, bool interactable, LuaEnvironment luaEnv, Closure callBack)
|
|
|
|
{
|
|
|
|
if (!gameObject.activeSelf)
|
|
|
|
{
|
|
|
|
gameObject.SetActive(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy to local variables
|
|
|
|
LuaEnvironment env = luaEnv;
|
|
|
|
Closure call = callBack;
|
|
|
|
UnityEngine.Events.UnityAction action = delegate
|
|
|
|
{
|
|
|
|
StopAllCoroutines();
|
|
|
|
// Stop timeout
|
|
|
|
Clear();
|
|
|
|
HideSayDialog();
|
|
|
|
// Use a coroutine to call the callback on the next frame
|
|
|
|
StartCoroutine(CallLuaClosure(env, call));
|
|
|
|
};
|
|
|
|
|
|
|
|
return AddOption(text, interactable, false, action);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Adds the option to the list of displayed options. Calls a Block when selected.
|
|
|
|
/// Will cause the Menu dialog to become visible if it is not already visible.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns><c>true</c>, if the option was added successfully.</returns>
|
|
|
|
/// <param name="text">The option text to display on the button.</param>
|
|
|
|
/// <param name="interactable">If false, the option is displayed but is not selectable.</param>
|
|
|
|
/// <param name="hideOption">If true, the option is not displayed but the menu knows that option can or did exist</param>
|
|
|
|
/// <param name="action">Action attached to the button on the menu item</param>
|
|
|
|
private bool AddOption(string text, bool interactable, bool hideOption, UnityEngine.Events.UnityAction action)
|
|
|
|
{
|
|
|
|
if (nextOptionIndex >= CachedButtons.Length)
|
|
|
|
{
|
|
|
|
Debug.LogWarning("Unable to add menu item, not enough buttons: " + text);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
//if first option notify that a menu has started
|
|
|
|
if(nextOptionIndex == 0)
|
|
|
|
MenuSignals.DoMenuStart(this);
|
|
|
|
|
|
|
|
var button = cachedButtons[nextOptionIndex];
|
|
|
|
|
|
|
|
//move forward for next call
|
|
|
|
nextOptionIndex++;
|
|
|
|
|
|
|
|
//don't need to set anything on it
|
|
|
|
if (hideOption)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
button.gameObject.SetActive(true);
|
|
|
|
button.interactable = interactable;
|
|
|
|
if (interactable && autoSelectFirstButton && !cachedButtons.Select(x => x.gameObject).Contains(EventSystem.current.currentSelectedGameObject))
|
|
|
|
{
|
|
|
|
EventSystem.current.SetSelectedGameObject(button.gameObject);
|
|
|
|
}
|
|
|
|
|
|
|
|
TextAdapter textAdapter = new TextAdapter();
|
|
|
|
textAdapter.InitFromGameObject(button.gameObject, true);
|
|
|
|
if (textAdapter.HasTextObject())
|
|
|
|
{
|
|
|
|
text = TextVariationHandler.SelectVariations(text);
|
|
|
|
|
|
|
|
textAdapter.Text = text;
|
|
|
|
}
|
|
|
|
|
|
|
|
button.onClick.AddListener(action);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Show a timer during which the player can select an option. Calls a Block when the timer expires.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="duration">The duration during which the player can select an option.</param>
|
|
|
|
/// <param name="targetBlock">Block to execute if the player does not select an option in time.</param>
|
|
|
|
public virtual void ShowTimer(float duration, Block targetBlock)
|
|
|
|
{
|
|
|
|
if (cachedSlider != null)
|
|
|
|
{
|
|
|
|
cachedSlider.gameObject.SetActive(true);
|
|
|
|
gameObject.SetActive(true);
|
|
|
|
StopAllCoroutines();
|
|
|
|
StartCoroutine(WaitForTimeout(duration, targetBlock));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Debug.LogWarning("Unable to show timer, no slider set");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Show a timer during which the player can select an option. Calls a Lua function when the timer expires.
|
|
|
|
/// </summary>
|
|
|
|
public virtual IEnumerator ShowTimer(float duration, LuaEnvironment luaEnv, Closure callBack)
|
|
|
|
{
|
|
|
|
if (CachedSlider == null ||
|
|
|
|
duration <= 0f)
|
|
|
|
{
|
|
|
|
yield break;
|
|
|
|
}
|
|
|
|
|
|
|
|
CachedSlider.gameObject.SetActive(true);
|
|
|
|
StopAllCoroutines();
|
|
|
|
|
|
|
|
float elapsedTime = 0;
|
|
|
|
Slider timeoutSlider = CachedSlider;
|
|
|
|
|
|
|
|
while (elapsedTime < duration)
|
|
|
|
{
|
|
|
|
if (timeoutSlider != null)
|
|
|
|
{
|
|
|
|
float t = 1f - elapsedTime / duration;
|
|
|
|
timeoutSlider.value = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
elapsedTime += Time.deltaTime;
|
|
|
|
|
|
|
|
yield return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Clear();
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
HideSayDialog();
|
|
|
|
|
|
|
|
if (callBack != null)
|
|
|
|
{
|
|
|
|
luaEnv.RunLuaFunction(callBack, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Returns true if the Menu Dialog is currently displayed.
|
|
|
|
/// </summary>
|
|
|
|
public virtual bool IsActive()
|
|
|
|
{
|
|
|
|
return gameObject.activeInHierarchy;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Returns the number of currently displayed options.
|
|
|
|
/// </summary>
|
|
|
|
public virtual int DisplayedOptionsCount
|
|
|
|
{
|
|
|
|
get {
|
|
|
|
int count = 0;
|
|
|
|
for (int i = 0; i < cachedButtons.Length; i++)
|
|
|
|
{
|
|
|
|
var button = cachedButtons[i];
|
|
|
|
if (button.gameObject.activeSelf)
|
|
|
|
{
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Shuffle the parent order of the cached buttons, allows for randomising button order, buttons are auto reordered when cleared
|
|
|
|
/// </summary>
|
|
|
|
public void Shuffle(System.Random r)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < CachedButtons.Length; i++)
|
|
|
|
{
|
|
|
|
CachedButtons[i].transform.SetSiblingIndex(r.Next(CachedButtons.Length));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|