using Godot;
using Godot.Collections;
namespace DialogueManagerRuntime
{
///
/// A for dialogue responses provided by Dialogue Manager.
///
public partial class DialogueResponsesMenu : Container
{
///
/// Emitted when a response is focused.
///
[Signal] public delegate void ResponseFocusedEventHandler(Variant response);
///
/// Emitted when a response is selected.
///
[Signal] public delegate void ResponseSelectedEventHandler(DialogueResponse response);
///
/// Optionally specify a control to duplicate for each response.
///
[Export] public Control ResponseTemplate;
///
/// The action for accepting a response (is possibly overridden by parent dialogue balloon).
///
[Export] public StringName NextAction = "";
///
/// Automatically set up focus neighbours when the responses list changes.
///
[Export] public bool AutoConfigureFocus = true;
///
/// Automatically focus the first item when showing.
///
[Export] public bool AutoFocusFirstItem = true;
///
/// Hide any responses where IsAllowed is false.
///
[Export] public bool HideFailedResponses = false;
private Godot.Collections.Array responses = new();
///
/// The list of dialogue responses.
///
public Godot.Collections.Array Responses
{
get => responses;
set
{
responses = value;
ApplyResponses();
}
}
// The previously focused item in this menu.
private Control previouslyFocusedItem = null;
public override void _Ready()
{
VisibilityChanged += () =>
{
if (AutoFocusFirstItem && Visible && GetMenuItems().Count > 0)
{
Control firstItem = GetMenuItems()[0];
if (firstItem.IsInsideTree())
{
firstItem.GrabFocus();
}
}
};
if (IsInstanceValid(ResponseTemplate))
{
ResponseTemplate.Hide();
}
GetViewport().GuiFocusChanged += OnFocusChanged;
}
public override void _ExitTree()
{
GetViewport().GuiFocusChanged -= OnFocusChanged;
}
///
/// Get the selectable items in the menu.
///
public Array GetMenuItems()
{
var items = new Array();
foreach (Node child in GetChildren())
{
if (child is not Control control) continue;
if (!control.Visible) continue;
if (control.Name.ToString().Contains("Disallowed")) continue;
items.Add(control);
}
return items;
}
///
/// Prepare the menu for keyboard and mouse navigation.
///
public void ConfigureFocus()
{
Array items = GetMenuItems();
for (int i = 0; i < items.Count; i++)
{
Control item = items[i];
item.FocusMode = FocusModeEnum.All;
item.FocusNeighborLeft = item.GetPath();
item.FocusNeighborRight = item.GetPath();
if (i == 0)
{
item.FocusNeighborTop = item.GetPath();
item.FocusNeighborLeft = item.GetPath();
item.FocusPrevious = item.GetPath();
}
else
{
item.FocusNeighborTop = items[i - 1].GetPath();
item.FocusNeighborLeft = items[i - 1].GetPath();
item.FocusPrevious = items[i - 1].GetPath();
}
if (i == items.Count - 1)
{
item.FocusNeighborBottom = item.GetPath();
item.FocusNeighborRight = item.GetPath();
item.FocusNext = item.GetPath();
}
else
{
item.FocusNeighborBottom = items[i + 1].GetPath();
item.FocusNeighborRight = items[i + 1].GetPath();
item.FocusNext = items[i + 1].GetPath();
}
DialogueResponse response = (DialogueResponse)item.GetMeta("response");
item.MouseEntered += () => OnResponseMouseEntered(item);
item.GuiInput += (@event) => OnResponseGuiInput(@event, item, response);
}
if (items.Count == 0) return;
previouslyFocusedItem = items[0];
if (AutoFocusFirstItem)
{
items[0].GrabFocus();
}
}
#region Internal
// Set up the visual side of things.
private void ApplyResponses()
{
// Remove any current items
foreach (Node item in GetChildren())
{
if (item == ResponseTemplate) continue;
RemoveChild(item);
item.QueueFree();
}
// Add new items
if (responses.Count > 0)
{
foreach (DialogueResponse response in responses)
{
// The response might be tagged with "show_if"
if (HideFailedResponses && !response.IsAllowed && response.GetTagValue("show_if") != "true")
{
continue;
}
if (response.GetTagValue("hide_if") == "true")
{
continue;
}
Control item;
if (IsInstanceValid(ResponseTemplate))
{
item = (Control)ResponseTemplate.Duplicate((int)(DuplicateFlags.Groups | DuplicateFlags.Scripts | DuplicateFlags.Signals));
item.Show();
}
else
{
item = new Button();
}
item.Name = $"Response{GetChildCount()}";
if (!response.IsAllowed)
{
item.Name = $"{item.Name}Disallowed";
item.Set("disabled", true);
}
// If the item has a response property then use that
string responsePropertyName = GetResponsePropertyName(item);
if (responsePropertyName != null)
{
item.Set(responsePropertyName, response);
}
// Otherwise assume we can just set the text
else
{
item.Set("text", response.Text);
}
item.SetMeta("response", response);
AddChild(item);
}
if (AutoConfigureFocus)
{
ConfigureFocus();
}
}
}
// Find the name of the item's "response" property (if it has one).
private static string GetResponsePropertyName(Control item)
{
foreach (Dictionary property in item.GetPropertyList())
{
string name = (string)property["name"];
if (name == "response" || name == "Response")
{
return name;
}
}
return null;
}
#endregion
#region Signals
private void OnFocusChanged(Control control)
{
if (control.Name.ToString().Contains("Disallowed")) return;
if (!GetMenuItems().Contains(control)) return;
if (previouslyFocusedItem != control)
{
previouslyFocusedItem = control;
EmitSignal(SignalName.ResponseFocused, control);
}
}
private void OnResponseMouseEntered(Control item)
{
if (item.Name.ToString().Contains("Disallowed")) return;
item.GrabFocus();
}
private void OnResponseGuiInput(InputEvent @event, Control item, DialogueResponse response)
{
if (item.Name.ToString().Contains("Disallowed")) return;
if (@event is InputEventMouseButton mouseButtonEvent && mouseButtonEvent.IsPressed() && mouseButtonEvent.ButtonIndex == MouseButton.Left)
{
GetViewport().SetInputAsHandled();
EmitSignal(SignalName.ResponseSelected, response);
}
else if (@event.IsActionPressed(NextAction.IsEmpty ? "ui_accept" : NextAction) && GetMenuItems().Contains(item))
{
GetViewport().SetInputAsHandled();
EmitSignal(SignalName.ResponseSelected, response);
}
}
#endregion
}
}