Added dialogue manager plugin
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> for dialogue responses provided by Dialogue Manager.
|
||||
/// </summary>
|
||||
public partial class DialogueResponsesMenu : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitted when a response is focused.
|
||||
/// </summary>
|
||||
[Signal] public delegate void ResponseFocusedEventHandler(Variant response);
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when a response is selected.
|
||||
/// </summary>
|
||||
[Signal] public delegate void ResponseSelectedEventHandler(DialogueResponse response);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Optionally specify a control to duplicate for each response.
|
||||
/// </summary>
|
||||
[Export] public Control ResponseTemplate;
|
||||
|
||||
/// <summary>
|
||||
/// The action for accepting a response (is possibly overridden by parent dialogue balloon).
|
||||
/// </summary>
|
||||
[Export] public StringName NextAction = "";
|
||||
|
||||
/// <summary>
|
||||
/// Automatically set up focus neighbours when the responses list changes.
|
||||
/// </summary>
|
||||
[Export] public bool AutoConfigureFocus = true;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically focus the first item when showing.
|
||||
/// </summary>
|
||||
[Export] public bool AutoFocusFirstItem = true;
|
||||
|
||||
/// <summary>
|
||||
/// Hide any responses where <c>IsAllowed</c> is false.
|
||||
/// </summary>
|
||||
[Export] public bool HideFailedResponses = false;
|
||||
|
||||
private Godot.Collections.Array<DialogueResponse> responses = new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of dialogue responses.
|
||||
/// </summary>
|
||||
public Godot.Collections.Array<DialogueResponse> 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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the selectable items in the menu.
|
||||
/// </summary>
|
||||
public Array<Control> GetMenuItems()
|
||||
{
|
||||
var items = new Array<Control>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Prepare the menu for keyboard and mouse navigation.
|
||||
/// </summary>
|
||||
public void ConfigureFocus()
|
||||
{
|
||||
Array<Control> 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bf0gf4upyxj5l
|
||||
@@ -0,0 +1,184 @@
|
||||
@icon("uid://drjfciwitjm83")
|
||||
|
||||
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
|
||||
class_name DialogueResponsesMenu extends Container
|
||||
|
||||
|
||||
## Emitted when a response is focused.
|
||||
signal response_focused(response: Variant)
|
||||
|
||||
## Emitted when a response is selected.
|
||||
signal response_selected(response: Variant)
|
||||
|
||||
|
||||
## Optionally specify a control to duplicate for each response
|
||||
@export var response_template: Control
|
||||
|
||||
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
|
||||
@export var next_action: StringName = &""
|
||||
|
||||
## Automatically set up focus neighbours when the responses list changes.
|
||||
@export var auto_configure_focus: bool = true
|
||||
|
||||
## Automatically focus the first item when showing.
|
||||
@export var auto_focus_first_item: bool = true
|
||||
|
||||
## Hide any responses where [code]is_allowed[/code] is false
|
||||
@export var hide_failed_responses: bool = false
|
||||
|
||||
## The list of dialogue responses.
|
||||
var responses: Array = []:
|
||||
set(value):
|
||||
responses = value
|
||||
_apply_responses()
|
||||
get:
|
||||
return responses
|
||||
|
||||
# The previously focused item in this menu.
|
||||
var _previously_focused_item: Control = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visibility_changed.connect(func() -> void:
|
||||
if auto_focus_first_item and visible and get_menu_items().size() > 0:
|
||||
var first_item: Control = get_menu_items()[0]
|
||||
if first_item.is_inside_tree():
|
||||
first_item.grab_focus()
|
||||
)
|
||||
|
||||
if is_instance_valid(response_template):
|
||||
response_template.hide()
|
||||
|
||||
get_viewport().gui_focus_changed.connect(_on_focus_changed)
|
||||
|
||||
|
||||
## Get the selectable items in the menu.
|
||||
func get_menu_items() -> Array:
|
||||
var items: Array = []
|
||||
for child: Node in get_children():
|
||||
if not child.visible: continue
|
||||
if "Disallowed" in child.name: continue
|
||||
items.append(child)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
## Prepare the menu for keyboard and mouse navigation.
|
||||
func configure_focus() -> void:
|
||||
var items: Array = get_menu_items()
|
||||
for i: int in items.size():
|
||||
var item: Control = items[i]
|
||||
|
||||
item.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
|
||||
if i == 0:
|
||||
item.focus_neighbor_top = item.get_path()
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_previous = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_top = items[i - 1].get_path()
|
||||
item.focus_neighbor_left = items[i - 1].get_path()
|
||||
item.focus_previous = items[i - 1].get_path()
|
||||
|
||||
if i == items.size() - 1:
|
||||
item.focus_neighbor_bottom = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
item.focus_next = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_bottom = items[i + 1].get_path()
|
||||
item.focus_neighbor_right = items[i + 1].get_path()
|
||||
item.focus_next = items[i + 1].get_path()
|
||||
|
||||
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
|
||||
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response")))
|
||||
|
||||
_previously_focused_item = items[0]
|
||||
|
||||
if auto_focus_first_item:
|
||||
items[0].grab_focus()
|
||||
|
||||
|
||||
#region Internal
|
||||
|
||||
|
||||
# Set up the visual side of things.
|
||||
func _apply_responses() -> void:
|
||||
# Remove any current items
|
||||
for item: Node in get_children():
|
||||
if item == response_template: continue
|
||||
|
||||
remove_child(item)
|
||||
item.queue_free()
|
||||
|
||||
# Add new items
|
||||
if responses.size() > 0:
|
||||
for response: Variant in responses:
|
||||
if hide_failed_responses and not response.is_allowed \
|
||||
# The response might be tagged with "show_if"
|
||||
and (not response.has_method("get_tag_value") or response.get_tag_value("show_if") != "true"):
|
||||
continue
|
||||
|
||||
if response.has_method("get_tag_value") and response.get_tag_value("hide_if") == "true":
|
||||
continue
|
||||
|
||||
var item: Control
|
||||
if is_instance_valid(response_template):
|
||||
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
|
||||
item.show()
|
||||
else:
|
||||
item = Button.new()
|
||||
item.name = "Response%d" % get_child_count()
|
||||
if not response.is_allowed:
|
||||
item.name = item.name + &"Disallowed"
|
||||
item.disabled = true
|
||||
|
||||
# If the item has a response property then use that
|
||||
if "response" in item:
|
||||
item.response = response
|
||||
# Otherwise assume we can just set the text
|
||||
else:
|
||||
item.text = response.text
|
||||
|
||||
item.set_meta("response", response)
|
||||
|
||||
add_child(item)
|
||||
|
||||
if auto_configure_focus:
|
||||
configure_focus()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_focus_changed(control: Control) -> void:
|
||||
if "Disallowed" in control.name: return
|
||||
if not control in get_menu_items(): return
|
||||
|
||||
if _previously_focused_item != control:
|
||||
_previously_focused_item = control
|
||||
response_focused.emit(control)
|
||||
|
||||
|
||||
func _on_response_mouse_entered(item: Control) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
item.grab_focus()
|
||||
|
||||
|
||||
func _on_response_gui_input(event: InputEvent, item: Control, response: Variant) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items():
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb52rsfwhkxbn
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://drjfciwitjm83"
|
||||
path="res://.godot/imported/dialogue_responses_menu.svg-c2f1aeac68c19756dc5732e31dba2439.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/responses_menu/dialogue_responses_menu.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_responses_menu.svg-c2f1aeac68c19756dc5732e31dba2439.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
Reference in New Issue
Block a user