Added dialogue manager plugin
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using System;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A special <see cref="Area2D"/> node to hold information about starting dialogue.
|
||||
///
|
||||
/// Assuming <c>DialogueResource</c> and <c>DialogueCue</c> have been configured you can
|
||||
/// call <c>Action()</c> on this node at runtime to start dialogue.
|
||||
/// </summary>
|
||||
[Tool, Icon("uid://ja67jst5iqyy")]
|
||||
public partial class DialogueActionable2D : Area2D
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitted when this <see cref="DialogueActionable2D"/> has <c>Action()</c> called on it.
|
||||
/// </summary>
|
||||
[Signal] public delegate void ActionedEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when the dialogue resource associated with this <see cref="DialogueActionable2D"/> ends. NOTE: The
|
||||
/// signal is also emitted if the same resource is used for multiple <see cref="DialogueActionable2D"/> nodes in the tree.
|
||||
/// </summary>
|
||||
[Signal] public delegate void DialogueEndedEventHandler();
|
||||
|
||||
|
||||
private Resource dialogueResource = null;
|
||||
|
||||
/// <summary>
|
||||
/// The dialogue resource to use when starting dialogue.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Resource DialogueResource
|
||||
{
|
||||
get => dialogueResource;
|
||||
set
|
||||
{
|
||||
dialogueResource = value;
|
||||
if (dialogueResource == null)
|
||||
{
|
||||
DialogueCue = "";
|
||||
}
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target cue to start dialogue from.
|
||||
/// </summary>
|
||||
[Export] public string DialogueCue = "";
|
||||
|
||||
/// <summary>
|
||||
/// The dialogue balloon that was last used by calling <c>Action()</c> (if there was one).
|
||||
/// </summary>
|
||||
public Node DialogueBalloon;
|
||||
|
||||
/// <summary>
|
||||
/// The method used to start dialogue when <c>Action()</c> is called. Override if you need different logic.
|
||||
/// </summary>
|
||||
public static Func<Resource, string, Array<Variant>, Node> StartDialogue = (withDialogueResource, fromCue, extraGameStates) =>
|
||||
DialogueManager.ShowDialogueBalloon(withDialogueResource, fromCue, extraGameStates);
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
AddToGroup("dialogue_actionables");
|
||||
// Touch the instance to make sure the DialogueManager bridge signals are connected
|
||||
_ = DialogueManager.Instance;
|
||||
DialogueManager.DialogueEnded += OnDialogueEnded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
DialogueManager.DialogueEnded -= OnDialogueEnded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region Public
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Action this <see cref="DialogueActionable2D"/>. If a dialogue resource and cue have been set on this node then it will start dialogue.
|
||||
/// </summary>
|
||||
public void Action()
|
||||
{
|
||||
if (IsInstanceValid(DialogueResource) && !string.IsNullOrEmpty(DialogueCue))
|
||||
{
|
||||
DialogueBalloon = StartDialogue(DialogueResource, DialogueCue, new Array<Variant> { new Dictionary { { "actionable", this } }, Owner });
|
||||
}
|
||||
EmitSignal(SignalName.Actioned);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find the nearest <see cref="DialogueActionable2D"/> to a given position.
|
||||
/// </summary>
|
||||
public static DialogueActionable2D GetNearestActionableTo(Vector2 targetPosition)
|
||||
{
|
||||
float nearestDistance = float.PositiveInfinity;
|
||||
DialogueActionable2D nearestActionable = null;
|
||||
foreach (Node node in ((SceneTree)Engine.GetMainLoop()).GetNodesInGroup("dialogue_actionables"))
|
||||
{
|
||||
if (node is not DialogueActionable2D actionable) continue;
|
||||
|
||||
float distance = actionable.GlobalPosition.DistanceSquaredTo(targetPosition);
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearestActionable = actionable;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestActionable;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
private void OnDialogueEnded(Resource endingDialogueResource)
|
||||
{
|
||||
if (endingDialogueResource == DialogueResource)
|
||||
{
|
||||
EmitSignal(SignalName.DialogueEnded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0sxb0wcim1wy
|
||||
@@ -0,0 +1,141 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using System;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A special <see cref="Area3D"/> node to hold information about starting dialogue.
|
||||
///
|
||||
/// Assuming <c>DialogueResource</c> and <c>DialogueCue</c> have been configured you can
|
||||
/// call <c>Action()</c> on this node at runtime to start dialogue.
|
||||
/// </summary>
|
||||
[Tool, Icon("uid://dppsuhlvbcbjc")]
|
||||
public partial class DialogueActionable3D : Area3D
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitted when this <see cref="DialogueActionable3D"/> has <c>Action()</c> called on it.
|
||||
/// </summary>
|
||||
[Signal] public delegate void ActionedEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when the dialogue resource associated with this <see cref="DialogueActionable3D"/> ends. NOTE: The
|
||||
/// signal is also emitted if the same resource is used for multiple <see cref="DialogueActionable3D"/> nodes in the tree.
|
||||
/// </summary>
|
||||
[Signal] public delegate void DialogueEndedEventHandler();
|
||||
|
||||
|
||||
private Resource dialogueResource = null;
|
||||
|
||||
/// <summary>
|
||||
/// The dialogue resource to use when starting dialogue.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Resource DialogueResource
|
||||
{
|
||||
get => dialogueResource;
|
||||
set
|
||||
{
|
||||
dialogueResource = value;
|
||||
if (dialogueResource == null)
|
||||
{
|
||||
DialogueCue = "";
|
||||
}
|
||||
NotifyPropertyListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target cue to start dialogue from.
|
||||
/// </summary>
|
||||
[Export] public string DialogueCue = "";
|
||||
|
||||
/// <summary>
|
||||
/// The dialogue balloon that was last used by calling <c>Action()</c> (if there was one).
|
||||
/// </summary>
|
||||
public Node DialogueBalloon;
|
||||
|
||||
/// <summary>
|
||||
/// The method used to start dialogue when <c>Action()</c> is called. Override if you need different logic.
|
||||
/// </summary>
|
||||
public static Func<Resource, string, Array<Variant>, Node> StartDialogue = (withDialogueResource, fromCue, extraGameStates) =>
|
||||
DialogueManager.ShowDialogueBalloon(withDialogueResource, fromCue, extraGameStates);
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
AddToGroup("dialogue_actionables");
|
||||
// Touch the instance to make sure the DialogueManager bridge signals are connected
|
||||
_ = DialogueManager.Instance;
|
||||
DialogueManager.DialogueEnded += OnDialogueEnded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
DialogueManager.DialogueEnded -= OnDialogueEnded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region Public
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Action this <see cref="DialogueActionable3D"/>. If a dialogue resource and cue have been set on this node then it will start dialogue.
|
||||
/// </summary>
|
||||
public void Action()
|
||||
{
|
||||
if (IsInstanceValid(DialogueResource) && !string.IsNullOrEmpty(DialogueCue))
|
||||
{
|
||||
DialogueBalloon = StartDialogue(DialogueResource, DialogueCue, new Array<Variant> { new Dictionary { { "actionable", this } }, Owner });
|
||||
}
|
||||
EmitSignal(SignalName.Actioned);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find the nearest <see cref="DialogueActionable3D"/> to a given position.
|
||||
/// </summary>
|
||||
public static DialogueActionable3D GetNearestActionableTo(Vector3 targetPosition)
|
||||
{
|
||||
float nearestDistance = float.PositiveInfinity;
|
||||
DialogueActionable3D nearestActionable = null;
|
||||
foreach (Node node in ((SceneTree)Engine.GetMainLoop()).GetNodesInGroup("dialogue_actionables"))
|
||||
{
|
||||
if (node is not DialogueActionable3D actionable) continue;
|
||||
|
||||
float distance = actionable.GlobalPosition.DistanceSquaredTo(targetPosition);
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearestActionable = actionable;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestActionable;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
private void OnDialogueEnded(Resource endingDialogueResource)
|
||||
{
|
||||
if (endingDialogueResource == DialogueResource)
|
||||
{
|
||||
EmitSignal(SignalName.DialogueEnded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://5skdphbgu68t
|
||||
@@ -0,0 +1,82 @@
|
||||
@tool
|
||||
|
||||
@icon("uid://ja67jst5iqyy")
|
||||
|
||||
## A special [Area2D] node to hold information about starting dialogue.
|
||||
##
|
||||
## Assuming [code]dialogue_resource[/code] and [code]cue[/code] have been configured you can
|
||||
## call [code]action()[/code] on this node at runtime to start dialogue.
|
||||
class_name DialogueActionable2D extends Area2D
|
||||
|
||||
|
||||
## Emitted when this [DialogueActionable2D] has [code]action()[/code] called on it.
|
||||
signal actioned()
|
||||
|
||||
## Emitted when the [DialogueResource] associated with this [DialogueActionable2D] ends. [b]NOTE:[/b] The
|
||||
## signal is also emitted if the same resource is used for multiple [DialogueActionable2D] nodes in the tree.
|
||||
signal dialogue_ended()
|
||||
|
||||
|
||||
## The [DialogueResource] to use when starting dialogue.
|
||||
@export var dialogue_resource: DialogueResource = null:
|
||||
set(value):
|
||||
dialogue_resource = value
|
||||
if dialogue_resource == null:
|
||||
dialogue_cue = ""
|
||||
notify_property_list_changed()
|
||||
get:
|
||||
return dialogue_resource
|
||||
|
||||
## The target cue to start dialogue from.
|
||||
@export var dialogue_cue: String = ""
|
||||
|
||||
## The dialogue balloon that was last used by calling [code]action()[/code] (if there was one).
|
||||
var dialogue_balloon: Node
|
||||
|
||||
## The method used to start dialogue action [code]action()[/code] is called. Override if you need different logic.
|
||||
static var start_dialogue: Callable = func(with_dialogue_resource: DialogueResource, from_cue: String, extra_game_states: Array) -> Node:
|
||||
return Engine.get_singleton("DialogueManager").show_dialogue_balloon(with_dialogue_resource, from_cue, extra_game_states)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
add_to_group("dialogue_actionables")
|
||||
Engine.get_singleton("DialogueManager").dialogue_ended.connect(_on_dialogue_ended)
|
||||
|
||||
|
||||
#region Public
|
||||
|
||||
|
||||
## Action this [DialogueActionable2D]. If a [DialogueResource] and cue have been set on this node then
|
||||
## it will start dialogue.
|
||||
func action() -> void:
|
||||
if is_instance_valid(dialogue_resource) and not dialogue_cue.is_empty():
|
||||
dialogue_balloon = start_dialogue.call(dialogue_resource, dialogue_cue, [{ actionable = self }, owner])
|
||||
actioned.emit()
|
||||
|
||||
|
||||
## Find the nearest [DialogueActionable2D] to a given position.
|
||||
static func get_nearest_actionable_to(target_position: Vector2) -> DialogueActionable2D:
|
||||
var nearest_distance: float = INF
|
||||
var nearest_actionable: DialogueActionable2D = null
|
||||
var actionables: Array[Node] = (Engine.get_main_loop() as SceneTree).get_nodes_in_group("dialogue_actionables")
|
||||
for actionable: DialogueActionable2D in actionables:
|
||||
var distance: float = actionable.global_position.distance_squared_to(target_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_actionable = actionable
|
||||
|
||||
return nearest_actionable
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_dialogue_ended(ending_dialogue_resource: DialogueResource) -> void:
|
||||
if ending_dialogue_resource == dialogue_resource:
|
||||
dialogue_ended.emit()
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqcur1rm7433g
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ja67jst5iqyy"
|
||||
path="res://.godot/imported/dialogue_actionable_2d.svg-fab4e58d6b3980b100fb8a5494b4d5cb.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/actionable/dialogue_actionable_2d.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_actionable_2d.svg-fab4e58d6b3980b100fb8a5494b4d5cb.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
|
||||
@@ -0,0 +1,82 @@
|
||||
@tool
|
||||
|
||||
@icon("uid://dppsuhlvbcbjc")
|
||||
|
||||
## A special [Area3D] node to hold information about starting dialogue.
|
||||
##
|
||||
## Assuming [code]dialogue_resource[/code] and [code]cue[/code] have been configured you can
|
||||
## call [code]action()[/code] on this node at runtime to start dialogue.
|
||||
class_name DialogueActionable3D extends Area3D
|
||||
|
||||
|
||||
## Emitted when this [DialogueActionable3D] has [code]action()[/code] called on it.
|
||||
signal actioned()
|
||||
|
||||
## Emitted when the [DialogueResource] associated with this [DialogueActionable2D] ends. [b]NOTE:[/b] The
|
||||
## signal is also emitted if the same resource is used for multiple [DialogueActionable2D] nodes in the tree.
|
||||
signal dialogue_ended()
|
||||
|
||||
|
||||
## The [DialogueResource] to use when starting dialogue.
|
||||
@export var dialogue_resource: DialogueResource = null:
|
||||
set(value):
|
||||
dialogue_resource = value
|
||||
if dialogue_resource == null:
|
||||
dialogue_cue = ""
|
||||
notify_property_list_changed()
|
||||
get:
|
||||
return dialogue_resource
|
||||
|
||||
## The target cue to start dialogue from.
|
||||
@export var dialogue_cue: String = ""
|
||||
|
||||
## The dialogue balloon that was last used by calling [code]action()[/code] (if there was one).
|
||||
var dialogue_balloon: Node
|
||||
|
||||
## The method used to start dialogue action [code]action()[/code] is called. Override if you need different logic.
|
||||
static var start_dialogue: Callable = func(with_dialogue_resource: DialogueResource, from_cue: String, extra_game_states: Array) -> Node:
|
||||
return Engine.get_singleton("DialogueManager").show_dialogue_balloon(with_dialogue_resource, from_cue, extra_game_states)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
add_to_group("dialogue_actionables")
|
||||
Engine.get_singleton("DialogueManager").dialogue_ended.connect(_on_dialogue_ended)
|
||||
|
||||
|
||||
#region Public
|
||||
|
||||
|
||||
## Action this [DialogueActionable3D]. If a [DialogueResource] and cue have been set on this node then
|
||||
## it will start dialogue.
|
||||
func action() -> void:
|
||||
if is_instance_valid(dialogue_resource) and not dialogue_cue.is_empty():
|
||||
dialogue_balloon = start_dialogue.call(dialogue_resource, dialogue_cue, [{ actionable = self }, owner])
|
||||
actioned.emit()
|
||||
|
||||
|
||||
## Find the nearest [DialogueActionable3D] to a given position.
|
||||
static func get_nearest_actionable_to(target_position: Vector3) -> DialogueActionable3D:
|
||||
var nearest_distance: float = INF
|
||||
var nearest_actionable: DialogueActionable3D = null
|
||||
var actionables: Array[Node] = (Engine.get_main_loop() as SceneTree).get_nodes_in_group("dialogue_actionables")
|
||||
for actionable: DialogueActionable3D in actionables:
|
||||
var distance: float = actionable.global_position.distance_squared_to(target_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_actionable = actionable
|
||||
|
||||
return nearest_actionable
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_dialogue_ended(ending_dialogue_resource: DialogueResource) -> void:
|
||||
if ending_dialogue_resource == dialogue_resource:
|
||||
dialogue_ended.emit()
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5mlp6r4mts0r
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dppsuhlvbcbjc"
|
||||
path="res://.godot/imported/dialogue_actionable_3d.svg-66fe4924d1964b29285ae978ab731ec3.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/actionable/dialogue_actionable_3d.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_actionable_3d.svg-66fe4924d1964b29285ae978ab731ec3.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=0
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,79 @@
|
||||
using Godot;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a <see cref="DialogueStateContext"/> to a scene to expose a node (usually the base node) to dialogue.
|
||||
/// </summary>
|
||||
[Tool, Icon("uid://c22t5famre1yo")]
|
||||
public partial class DialogueStateContext : Node
|
||||
{
|
||||
private string alias = "";
|
||||
|
||||
/// <summary>
|
||||
/// The name used in dialogue to refer to the exposed target node.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public string Alias
|
||||
{
|
||||
get => alias;
|
||||
set
|
||||
{
|
||||
alias = value;
|
||||
UpdateConfigurationWarnings();
|
||||
}
|
||||
}
|
||||
|
||||
private Node target = null;
|
||||
|
||||
/// <summary>
|
||||
/// The target whose values are exposed to dialogue.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Node Target
|
||||
{
|
||||
get => target;
|
||||
set
|
||||
{
|
||||
target = value;
|
||||
UpdateConfigurationWarnings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
DialogueManager.Instance.Call("register_state_context", Alias, Target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
{
|
||||
DialogueManager.Instance.Call("unregister_state_context", Alias);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override string[] _GetConfigurationWarnings()
|
||||
{
|
||||
var warnings = new System.Collections.Generic.List<string>();
|
||||
|
||||
if (string.IsNullOrEmpty(Alias))
|
||||
{
|
||||
warnings.Add("Alias cannot be empty.");
|
||||
}
|
||||
|
||||
if (Target == null)
|
||||
{
|
||||
warnings.Add("Target cannot be null.");
|
||||
}
|
||||
|
||||
return warnings.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4hjd43iw4vj8
|
||||
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
|
||||
@icon("uid://c22t5famre1yo")
|
||||
|
||||
## Add a [DialogueStateContext] to a scene to expose a node (usually the base node) to dialogue.
|
||||
class_name DialogueStateContext extends Node
|
||||
|
||||
|
||||
## The name used in dialogue to refer to the exposed target node.
|
||||
@export var alias: String = "":
|
||||
set(value):
|
||||
alias = value
|
||||
update_configuration_warnings()
|
||||
get:
|
||||
return alias
|
||||
|
||||
## The target who's values are exposed to dialogue.
|
||||
@export var target: Node:
|
||||
set(value):
|
||||
target = value
|
||||
update_configuration_warnings()
|
||||
get:
|
||||
return target
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
Engine.get_singleton("DialogueManager").register_state_context(alias, target)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
Engine.get_singleton("DialogueManager").unregister_state_context(alias)
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings: PackedStringArray = []
|
||||
|
||||
if alias.is_empty():
|
||||
warnings.append(DMConstants.translate("Alias cannot be empty."))
|
||||
|
||||
if target == null:
|
||||
warnings.append(DMConstants.translate("Target cannot be null."))
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://cd3222nx3y3k5
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c22t5famre1yo"
|
||||
path="res://.godot/imported/dialogue_state_context.svg-767b17bec3f66ee3adfc460e2805a08f.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/context/dialogue_state_context.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_state_context.svg-767b17bec3f66ee3adfc460e2805a08f.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
|
||||
@@ -0,0 +1,356 @@
|
||||
using Godot;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A RichTextLabel specifically for use with Dialogue Manager dialogue.
|
||||
/// </summary>
|
||||
[Tool, Icon("uid://s0s1348tpm6d")]
|
||||
public partial class DialogueLabel : RichTextLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitted for each letter typed out.
|
||||
/// </summary>
|
||||
[Signal] public delegate void SpokeEventHandler(string letter, int letterIndex, float speed);
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when the player skips the typing of dialogue.
|
||||
/// </summary>
|
||||
[Signal] public delegate void SkippedTypingEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when typing starts.
|
||||
/// </summary>
|
||||
[Signal] public delegate void StartedTypingEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when typing finishes.
|
||||
/// </summary>
|
||||
[Signal] public delegate void FinishedTypingEventHandler();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The action to press to skip typing.
|
||||
/// </summary>
|
||||
[Export] public StringName SkipAction = "ui_cancel";
|
||||
|
||||
/// <summary>
|
||||
/// The speed with which the text types out.
|
||||
/// </summary>
|
||||
[Export] public float SecondsPerStep = 0.02f;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically have a brief pause when these characters are encountered.
|
||||
/// </summary>
|
||||
[Export] public string PauseAtCharacters = ".?!";
|
||||
|
||||
/// <summary>
|
||||
/// Don't auto pause if the character after the pause is one of these.
|
||||
/// </summary>
|
||||
[Export] public string SkipPauseAtCharacterIfFollowedBy = ")\"";
|
||||
|
||||
/// <summary>
|
||||
/// Don't auto pause after these abbreviations (only if "." is in <c>PauseAtCharacters</c>).
|
||||
/// Abbreviations are limited to 5 characters in length.
|
||||
/// Does not support multi-period abbreviations (ex. "p.m.")
|
||||
/// </summary>
|
||||
[Export] public string[] SkipPauseAtAbbreviations = { "Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex" };
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time to pause when exposing a character present in <c>PauseAtCharacters</c>.
|
||||
/// </summary>
|
||||
[Export] public float SecondsPerPauseStep = 0.3f;
|
||||
|
||||
private readonly System.Collections.Generic.List<int> alreadyMutatedIndices = new();
|
||||
|
||||
|
||||
private DialogueLine dialogueLine = null;
|
||||
|
||||
/// <summary>
|
||||
/// The current line of dialogue.
|
||||
/// </summary>
|
||||
public DialogueLine DialogueLine
|
||||
{
|
||||
get => dialogueLine;
|
||||
set
|
||||
{
|
||||
if (value != dialogueLine)
|
||||
{
|
||||
dialogueLine = value;
|
||||
UpdateText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the label is currently typing itself out.
|
||||
/// </summary>
|
||||
public bool IsTyping
|
||||
{
|
||||
get => isTyping && !isAwaitingMutation;
|
||||
set
|
||||
{
|
||||
bool didJustFinish = isTyping != value && value == false && VisibleCharacters == GetTotalCharacterCount();
|
||||
isTyping = value;
|
||||
if (didJustFinish)
|
||||
{
|
||||
EmitSignal(SignalName.FinishedTyping);
|
||||
}
|
||||
}
|
||||
}
|
||||
private bool isTyping = false;
|
||||
|
||||
private int lastWaitIndex = -1;
|
||||
private int lastMutationIndex = -1;
|
||||
private float waitingSeconds = 0;
|
||||
private bool isAwaitingMutation = false;
|
||||
private bool isSkippingMutations = false;
|
||||
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (isTyping)
|
||||
{
|
||||
// Type out text
|
||||
if (VisibleRatio < 1)
|
||||
{
|
||||
// See if we are waiting
|
||||
if (waitingSeconds > 0)
|
||||
{
|
||||
waitingSeconds -= (float)delta;
|
||||
}
|
||||
// If we are no longer waiting then keep typing
|
||||
if (waitingSeconds <= 0)
|
||||
{
|
||||
TypeNext((float)delta, waitingSeconds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure any mutations at the end of the line get run
|
||||
_ = MutateInlineMutations(GetTotalCharacterCount());
|
||||
IsTyping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label's text from the current dialogue line. Override if you want
|
||||
/// to do something more interesting in your subclass.
|
||||
/// </summary>
|
||||
protected virtual void UpdateText()
|
||||
{
|
||||
if (IsInstanceValid(dialogueLine))
|
||||
{
|
||||
Text = dialogueLine.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
Text = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Start typing out the text.
|
||||
/// </summary>
|
||||
public async void TypeOut()
|
||||
{
|
||||
UpdateText();
|
||||
VisibleCharacters = 0;
|
||||
VisibleRatio = 0;
|
||||
waitingSeconds = 0;
|
||||
lastWaitIndex = -1;
|
||||
lastMutationIndex = -1;
|
||||
alreadyMutatedIndices.Clear();
|
||||
|
||||
IsTyping = true;
|
||||
EmitSignal(SignalName.StartedTyping);
|
||||
|
||||
// Allow typing listeners a chance to connect
|
||||
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
|
||||
if (GetTotalCharacterCount() == 0)
|
||||
{
|
||||
IsTyping = false;
|
||||
}
|
||||
else if (SecondsPerStep == 0)
|
||||
{
|
||||
MutateRemainingMutations();
|
||||
VisibleCharacters = GetTotalCharacterCount();
|
||||
IsTyping = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Stop typing out the text and jump right to the end.
|
||||
/// </summary>
|
||||
public void SkipTyping()
|
||||
{
|
||||
MutateRemainingMutations();
|
||||
VisibleCharacters = GetTotalCharacterCount();
|
||||
IsTyping = false;
|
||||
EmitSignal(SignalName.SkippedTyping);
|
||||
}
|
||||
|
||||
|
||||
// Type out the next character(s)
|
||||
private void TypeNext(float delta, float secondsNeeded)
|
||||
{
|
||||
if (isAwaitingMutation) return;
|
||||
|
||||
if (VisibleCharacters == GetTotalCharacterCount())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastMutationIndex != VisibleCharacters)
|
||||
{
|
||||
lastMutationIndex = VisibleCharacters;
|
||||
_ = MutateInlineMutations(VisibleCharacters);
|
||||
if (isAwaitingMutation) return;
|
||||
}
|
||||
|
||||
// Pause on characters like "."
|
||||
float pauseWaitingSeconds = ShouldAutoPause() ? SecondsPerPauseStep : 0f;
|
||||
if (lastWaitIndex != VisibleCharacters && pauseWaitingSeconds > 0)
|
||||
{
|
||||
lastWaitIndex = VisibleCharacters;
|
||||
waitingSeconds += pauseWaitingSeconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
VisibleCharacters += 1;
|
||||
if (VisibleCharacters <= GetTotalCharacterCount())
|
||||
{
|
||||
EmitSignal(SignalName.Spoke, GetParsedText()[VisibleCharacters - 1].ToString(), VisibleCharacters - 1, GetSpeed(VisibleCharacters));
|
||||
}
|
||||
// See if there's time to type out some more in this frame
|
||||
secondsNeeded += SecondsPerStep * (1.0f / GetSpeed(VisibleCharacters));
|
||||
if (secondsNeeded > delta)
|
||||
{
|
||||
waitingSeconds += secondsNeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
TypeNext(delta, secondsNeeded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the speed for the current typing position
|
||||
private float GetSpeed(int atIndex)
|
||||
{
|
||||
float speed = 1;
|
||||
foreach (var speedEntry in dialogueLine.Speeds)
|
||||
{
|
||||
if ((int)speedEntry.Key > atIndex)
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
speed = (float)speedEntry.Value;
|
||||
}
|
||||
return speed;
|
||||
}
|
||||
|
||||
|
||||
// Run any inline mutations that haven't been run yet
|
||||
private void MutateRemainingMutations()
|
||||
{
|
||||
isSkippingMutations = true;
|
||||
for (int i = VisibleCharacters; i < GetTotalCharacterCount() + 1; i++)
|
||||
{
|
||||
_ = MutateInlineMutations(i);
|
||||
}
|
||||
isSkippingMutations = false;
|
||||
}
|
||||
|
||||
|
||||
// Run any mutations at the current typing position
|
||||
private async Task MutateInlineMutations(int index)
|
||||
{
|
||||
if (!IsInstanceValid(dialogueLine)) return;
|
||||
|
||||
foreach (Godot.Collections.Array inlineMutation in dialogueLine.InlineMutations)
|
||||
{
|
||||
// Inline mutations are an array of arrays in the form of [character index, resolvable function]
|
||||
if ((int)inlineMutation[0] > index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((int)inlineMutation[0] == index && !alreadyMutatedIndices.Contains(index))
|
||||
{
|
||||
if (isSkippingMutations)
|
||||
{
|
||||
_ = DialogueManager.Mutate(inlineMutation[1].AsGodotDictionary(), dialogueLine.ExtraGameStates, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
isAwaitingMutation = true;
|
||||
await DialogueManager.Mutate(inlineMutation[1].AsGodotDictionary(), dialogueLine.ExtraGameStates, true);
|
||||
isAwaitingMutation = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alreadyMutatedIndices.Add(index);
|
||||
}
|
||||
|
||||
|
||||
// Determine if the current autopause character at the cursor should qualify to pause typing.
|
||||
private bool ShouldAutoPause()
|
||||
{
|
||||
if (VisibleCharacters == 0) return false;
|
||||
|
||||
string parsedText = GetParsedText();
|
||||
|
||||
// Avoid out-of-bounds when the label auto-translates and the text changes to one shorter while typing out
|
||||
// Note: visible characters can be larger than parsed_text after a translation event
|
||||
if (VisibleCharacters >= parsedText.Length) return false;
|
||||
|
||||
// Ignore pause characters if they are next to a non-pause character
|
||||
if (SkipPauseAtCharacterIfFollowedBy.Contains(parsedText[VisibleCharacters]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore "." if it's between two numbers
|
||||
if (VisibleCharacters > 3 && parsedText[VisibleCharacters - 1] == '.')
|
||||
{
|
||||
string possibleNumber = parsedText.Substring(VisibleCharacters - 2, 3);
|
||||
if (float.TryParse(possibleNumber, NumberStyles.Float, CultureInfo.InvariantCulture, out float number) && number.ToString("0.0", CultureInfo.InvariantCulture) == possibleNumber)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore "." if it's used in an abbreviation
|
||||
// Note: does NOT support multi-period abbreviations (ex. p.m.)
|
||||
if (PauseAtCharacters.Contains('.') && parsedText[VisibleCharacters - 1] == '.')
|
||||
{
|
||||
foreach (string abbreviation in SkipPauseAtAbbreviations)
|
||||
{
|
||||
int start = VisibleCharacters - abbreviation.Length - 1;
|
||||
if (VisibleCharacters >= abbreviation.Length && start >= 0 && parsedText.Substring(start, abbreviation.Length) == abbreviation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore two non-"." characters next to each other
|
||||
string otherPauseCharacters = PauseAtCharacters.Replace(".", "");
|
||||
if (VisibleCharacters > 1 && otherPauseCharacters.Contains(parsedText[VisibleCharacters - 1]) && otherPauseCharacters.Contains(parsedText[VisibleCharacters]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return PauseAtCharacters.Contains(parsedText[VisibleCharacters - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8beyexrpj6n7
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene format=3 uid="uid://c3qmxhslp0dl7"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b8beyexrpj6n7" path="res://addons/dialogue_manager/nodes/dialogue_label/DialogueLabel.cs" id="1_cital"]
|
||||
|
||||
[node name="DialogueLabel" type="RichTextLabel"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
shortcut_keys_enabled = false
|
||||
meta_underlined = false
|
||||
hint_underlined = false
|
||||
deselect_on_focus_loss_enabled = false
|
||||
visible_characters_behavior = 1
|
||||
script = ExtResource("1_cital")
|
||||
SkipPauseAtAbbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
|
||||
@@ -0,0 +1,228 @@
|
||||
@icon("uid://s0s1348tpm6d")
|
||||
|
||||
@tool
|
||||
|
||||
## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue.
|
||||
class_name DialogueLabel extends RichTextLabel
|
||||
|
||||
|
||||
## Emitted for each letter typed out.
|
||||
signal spoke(letter: String, letter_index: int, speed: float)
|
||||
|
||||
## Emitted when the player skips the typing of dialogue.
|
||||
signal skipped_typing()
|
||||
|
||||
## Emitted when typing starts
|
||||
signal started_typing()
|
||||
|
||||
## Emitted when typing finishes.
|
||||
signal finished_typing()
|
||||
|
||||
|
||||
## The action to press to skip typing.
|
||||
@export var skip_action: StringName = &"ui_cancel"
|
||||
|
||||
## The speed with which the text types out.
|
||||
@export var seconds_per_step: float = 0.02
|
||||
|
||||
## Automatically have a brief pause when these characters are encountered.
|
||||
@export var pause_at_characters: String = ".?!"
|
||||
|
||||
## Don't auto pause if the character after the pause is one of these.
|
||||
@export var skip_pause_at_character_if_followed_by: String = ")\""
|
||||
|
||||
## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br]
|
||||
## Abbreviations are limitted to 5 characters in length [br]
|
||||
## Does not support multi-period abbreviations (ex. "p.m.")
|
||||
@export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"]
|
||||
|
||||
## The amount of time to pause when exposing a character present in `pause_at_characters`.
|
||||
@export var seconds_per_pause_step: float = 0.3
|
||||
|
||||
var _already_mutated_indices: PackedInt32Array = []
|
||||
|
||||
|
||||
## The current line of dialogue.
|
||||
var dialogue_line: Variant:
|
||||
set(value):
|
||||
if value != dialogue_line:
|
||||
dialogue_line = value
|
||||
_update_text()
|
||||
get:
|
||||
return dialogue_line
|
||||
|
||||
## Whether the label is currently typing itself out.
|
||||
var is_typing: bool = false:
|
||||
set(value):
|
||||
var did_just_finish: bool = _is_typing != value and value == false and visible_characters == get_total_character_count()
|
||||
_is_typing = value
|
||||
if did_just_finish:
|
||||
finished_typing.emit()
|
||||
get:
|
||||
return _is_typing and not _is_awaiting_mutation
|
||||
var _is_typing: bool = false
|
||||
|
||||
var _last_wait_index: int = -1
|
||||
var _last_mutation_index: int = -1
|
||||
var _waiting_seconds: float = 0
|
||||
var _is_awaiting_mutation: bool = false
|
||||
var _is_skipping_mutations: bool = false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _is_typing:
|
||||
# Type out text
|
||||
if visible_ratio < 1:
|
||||
# See if we are waiting
|
||||
if _waiting_seconds > 0:
|
||||
_waiting_seconds = _waiting_seconds - delta
|
||||
# If we are no longer waiting then keep typing
|
||||
if _waiting_seconds <= 0:
|
||||
_type_next(delta, _waiting_seconds)
|
||||
else:
|
||||
# Make sure any mutations at the end of the line get run
|
||||
_mutate_inline_mutations(get_total_character_count())
|
||||
is_typing = false
|
||||
|
||||
|
||||
## Sets the label's text from the current dialogue line. Override if you want
|
||||
## to do something more interesting in your subclass.
|
||||
func _update_text() -> void:
|
||||
if is_instance_valid(dialogue_line):
|
||||
text = dialogue_line.text
|
||||
else:
|
||||
text = ""
|
||||
|
||||
|
||||
## Start typing out the text
|
||||
func type_out() -> void:
|
||||
_update_text()
|
||||
visible_characters = 0
|
||||
visible_ratio = 0
|
||||
_waiting_seconds = 0
|
||||
_last_wait_index = -1
|
||||
_last_mutation_index = -1
|
||||
_already_mutated_indices.clear()
|
||||
|
||||
is_typing = true
|
||||
started_typing.emit()
|
||||
|
||||
# Allow typing listeners a chance to connect
|
||||
await get_tree().process_frame
|
||||
|
||||
if get_total_character_count() == 0:
|
||||
is_typing = false
|
||||
elif seconds_per_step == 0:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
is_typing = false
|
||||
|
||||
|
||||
## Stop typing out the text and jump right to the end
|
||||
func skip_typing() -> void:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
is_typing = false
|
||||
skipped_typing.emit()
|
||||
|
||||
|
||||
# Type out the next character(s)
|
||||
func _type_next(delta: float, seconds_needed: float) -> void:
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
if visible_characters == get_total_character_count():
|
||||
return
|
||||
|
||||
if _last_mutation_index != visible_characters:
|
||||
_last_mutation_index = visible_characters
|
||||
_mutate_inline_mutations(visible_characters)
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
# Pause on characters like "."
|
||||
var waiting_seconds: float = seconds_per_pause_step if _should_auto_pause() else 0.0
|
||||
if _last_wait_index != visible_characters and waiting_seconds > 0:
|
||||
_last_wait_index = visible_characters
|
||||
_waiting_seconds += waiting_seconds
|
||||
else:
|
||||
visible_characters += 1
|
||||
if visible_characters <= get_total_character_count():
|
||||
spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters))
|
||||
# See if there's time to type out some more in this frame
|
||||
seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters))
|
||||
if seconds_needed > delta:
|
||||
_waiting_seconds += seconds_needed
|
||||
else:
|
||||
_type_next(delta, seconds_needed)
|
||||
|
||||
|
||||
# Get the speed for the current typing position
|
||||
func _get_speed(at_index: int) -> float:
|
||||
var speed: float = 1
|
||||
for index: int in dialogue_line.speeds:
|
||||
if index > at_index:
|
||||
return speed
|
||||
speed = dialogue_line.speeds[index]
|
||||
return speed
|
||||
|
||||
|
||||
# Run any inline mutations that haven't been run yet
|
||||
func _mutate_remaining_mutations() -> void:
|
||||
_is_skipping_mutations = true
|
||||
for i: int in range(visible_characters, get_total_character_count() + 1):
|
||||
_mutate_inline_mutations(i)
|
||||
_is_skipping_mutations = false
|
||||
|
||||
|
||||
# Run any mutations at the current typing position
|
||||
func _mutate_inline_mutations(index: int) -> void:
|
||||
for inline_mutation: Array in dialogue_line.inline_mutations:
|
||||
# inline mutations are an array of arrays in the form of [character index, resolvable function]
|
||||
if inline_mutation[0] > index:
|
||||
return
|
||||
if inline_mutation[0] == index and not _already_mutated_indices.has(index):
|
||||
if _is_skipping_mutations:
|
||||
Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
||||
else:
|
||||
_is_awaiting_mutation = true
|
||||
# The DialogueManager can't be referenced directly here so we need to get it by its path
|
||||
await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
||||
_is_awaiting_mutation = false
|
||||
|
||||
_already_mutated_indices.append(index)
|
||||
|
||||
|
||||
# Determine if the current autopause character at the cursor should qualify to pause typing.
|
||||
func _should_auto_pause() -> bool:
|
||||
if visible_characters == 0: return false
|
||||
|
||||
var parsed_text: String = get_parsed_text()
|
||||
|
||||
# Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out
|
||||
# Note: visible characters can be larger than parsed_text after a translation event
|
||||
if visible_characters >= parsed_text.length(): return false
|
||||
|
||||
# Ignore pause characters if they are next to a non-pause character
|
||||
if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split():
|
||||
return false
|
||||
|
||||
# Ignore "." if it's between two numbers
|
||||
if visible_characters > 3 and parsed_text[visible_characters - 1] == ".":
|
||||
var possible_number: String = parsed_text.substr(visible_characters - 2, 3)
|
||||
if str(float(possible_number)).pad_decimals(1) == possible_number:
|
||||
return false
|
||||
|
||||
# Ignore "." if it's used in an abbreviation
|
||||
# Note: does NOT support multi-period abbreviations (ex. p.m.)
|
||||
if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".":
|
||||
for abbreviation: String in skip_pause_at_abbreviations:
|
||||
if visible_characters >= abbreviation.length():
|
||||
var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length())
|
||||
if previous_characters == abbreviation:
|
||||
return false
|
||||
|
||||
# Ignore two non-"." characters next to each other
|
||||
var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split()
|
||||
if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters:
|
||||
return false
|
||||
|
||||
return parsed_text[visible_characters - 1] in pause_at_characters.split()
|
||||
@@ -0,0 +1 @@
|
||||
uid://g32um0mltv5d
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://s0s1348tpm6d"
|
||||
path="res://.godot/imported/dialogue_label.svg-d4dcce28fb3dfdb414d1a2c4579fb6a5.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/dialogue_label/dialogue_label.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_label.svg-d4dcce28fb3dfdb414d1a2c4579fb6a5.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
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene format=3 uid="uid://ckvgyvclnwggo"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/nodes/dialogue_label/dialogue_label.gd" id="1_cital"]
|
||||
|
||||
[node name="DialogueLabel" type="RichTextLabel" unique_id=1840273907]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
shortcut_keys_enabled = false
|
||||
meta_underlined = false
|
||||
hint_underlined = false
|
||||
deselect_on_focus_loss_enabled = false
|
||||
visible_characters_behavior = 1
|
||||
script = ExtResource("1_cital")
|
||||
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
|
||||
@@ -0,0 +1,79 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A special marker node that helps locate the in-world representation of the speaking character.
|
||||
/// Generally, you would position a <see cref="DialogueMarker2D"/> at the mouth of the character and use
|
||||
/// its location as the origin of a speech balloon.
|
||||
/// </summary>
|
||||
[Icon("uid://cim0y62o6g36i")]
|
||||
public partial class DialogueMarker2D : Marker2D
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the character that owns this marker.
|
||||
/// </summary>
|
||||
[Export] public string CharacterName = "";
|
||||
|
||||
|
||||
#region Static
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all <see cref="DialogueMarker2D"/> nodes.
|
||||
/// </summary>
|
||||
public static Array<DialogueMarker2D> All()
|
||||
{
|
||||
var markers = new Array<DialogueMarker2D>();
|
||||
foreach (Node node in ((SceneTree)Engine.GetMainLoop()).GetNodesInGroup("2d_dialogue_markers"))
|
||||
{
|
||||
if (node is DialogueMarker2D marker)
|
||||
{
|
||||
markers.Add(marker);
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find a marker with a given character name.
|
||||
/// </summary>
|
||||
public static DialogueMarker2D FindForCharacter(string targetCharacterName)
|
||||
{
|
||||
foreach (DialogueMarker2D marker in All())
|
||||
{
|
||||
if (marker.CharacterName == targetCharacterName)
|
||||
{
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
AddToGroup("2d_dialogue_markers");
|
||||
}
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the marker's position relative to the viewport.
|
||||
/// </summary>
|
||||
public Vector2 GetPositionInViewport()
|
||||
{
|
||||
return GetGlobalTransformWithCanvas().Origin;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnftkt8d8dj8i
|
||||
@@ -0,0 +1,87 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// A special marker node that helps locate the in-world representation of the speaking character.
|
||||
/// Generally, you would position a <see cref="DialogueMarker3D"/> at the mouth of the character and use
|
||||
/// its location as the origin of a speech balloon.
|
||||
/// </summary>
|
||||
[Icon("uid://bo1hdyjmnjdql")]
|
||||
public partial class DialogueMarker3D : Marker3D
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the character that owns this marker.
|
||||
/// </summary>
|
||||
[Export] public string CharacterName = "";
|
||||
|
||||
|
||||
#region Static
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all <see cref="DialogueMarker3D"/> nodes.
|
||||
/// </summary>
|
||||
public static Array<DialogueMarker3D> All()
|
||||
{
|
||||
var markers = new Array<DialogueMarker3D>();
|
||||
foreach (Node node in ((SceneTree)Engine.GetMainLoop()).GetNodesInGroup("3d_dialogue_markers"))
|
||||
{
|
||||
if (node is DialogueMarker3D marker)
|
||||
{
|
||||
markers.Add(marker);
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find a marker with a given character name.
|
||||
/// </summary>
|
||||
public static DialogueMarker3D FindForCharacter(string targetCharacterName)
|
||||
{
|
||||
foreach (DialogueMarker3D marker in All())
|
||||
{
|
||||
if (marker.CharacterName == targetCharacterName)
|
||||
{
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
AddToGroup("3d_dialogue_markers");
|
||||
}
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the marker's position relative to the viewport.
|
||||
/// </summary>
|
||||
public Vector2 GetPositionInViewport()
|
||||
{
|
||||
Camera3D camera = GetViewport().GetCamera3D();
|
||||
if (IsInstanceValid(camera))
|
||||
{
|
||||
return camera.UnprojectPosition(GlobalPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3hrgs61lvn5x
|
||||
@@ -0,0 +1,46 @@
|
||||
@icon("uid://cim0y62o6g36i")
|
||||
|
||||
## A special marker node that helps locate the in-world representation of the speaking character.
|
||||
## Generally, you would position a [DialogueMaker2D] at the mouth of the character and use
|
||||
## its location as the origin of a speech balloon.
|
||||
class_name DialogueMarker2D extends Marker2D
|
||||
|
||||
|
||||
## The name of the character that owns this marker.
|
||||
@export var character_name: String = ""
|
||||
|
||||
|
||||
#region Static
|
||||
|
||||
## Get all [DialogueMarker2D] nodes.
|
||||
static func all() -> Array[DialogueMarker2D]:
|
||||
var markers: Array[DialogueMarker2D] = []
|
||||
for node: DialogueMarker2D in (Engine.get_main_loop() as SceneTree).get_nodes_in_group("2d_dialogue_markers"):
|
||||
markers.append(node)
|
||||
return markers
|
||||
|
||||
|
||||
## Find a marker with a given character name.
|
||||
static func find_for_character(target_character_name: String) -> DialogueMarker2D:
|
||||
for marker: DialogueMarker2D in all():
|
||||
if marker.character_name == target_character_name:
|
||||
return marker
|
||||
return null
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("2d_dialogue_markers")
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
## Get the marker's position relative to the viewport.
|
||||
func get_position_in_viewport() -> Vector2:
|
||||
return get_global_transform_with_canvas().origin
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxge1wx70prau
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cim0y62o6g36i"
|
||||
path="res://.godot/imported/dialogue_marker_2d.svg-1eedb01d2a05819be5ce0e675c93eeab.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/marker/dialogue_marker_2d.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_marker_2d.svg-1eedb01d2a05819be5ce0e675c93eeab.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
|
||||
@@ -0,0 +1,50 @@
|
||||
@icon("uid://bo1hdyjmnjdql")
|
||||
|
||||
## A special marker node that helps locate the in-world representation of the speaking character.
|
||||
## Generally, you would position a [DialogueMaker3D] at the mouth of the character and use
|
||||
## its location as the origin of a speech balloon.
|
||||
class_name DialogueMarker3D extends Marker3D
|
||||
|
||||
|
||||
## The name of the character that owns this marker.
|
||||
@export var character_name: String = ""
|
||||
|
||||
|
||||
#region Static
|
||||
|
||||
## Get all [DialogueMarker3D] nodes.
|
||||
static func all() -> Array[DialogueMarker3D]:
|
||||
var markers: Array[DialogueMarker3D] = []
|
||||
for node: DialogueMarker3D in (Engine.get_main_loop() as SceneTree).get_nodes_in_group("3d_dialogue_markers"):
|
||||
markers.append(node)
|
||||
return markers
|
||||
|
||||
|
||||
## Find a marker with a given character name.
|
||||
static func find_for_character(target_character_name: String) -> DialogueMarker3D:
|
||||
for marker: DialogueMarker3D in all():
|
||||
if marker.character_name == target_character_name:
|
||||
return marker
|
||||
return null
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("3d_dialogue_markers")
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
## Get the marker's position relative to the viewport.
|
||||
func get_position_in_viewport() -> Vector2:
|
||||
var camera: Camera3D = get_viewport().get_camera_3d()
|
||||
if is_instance_valid(camera):
|
||||
return camera.unproject_position(global_position)
|
||||
else:
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb636aic4yoo6
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bo1hdyjmnjdql"
|
||||
path="res://.godot/imported/dialogue_marker_3d.svg-f68ab9ede59096854a7c6ee123dd8e4e.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/nodes/marker/dialogue_marker_3d.svg"
|
||||
dest_files=["res://.godot/imported/dialogue_marker_3d.svg-f68ab9ede59096854a7c6ee123dd8e4e.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
|
||||
@@ -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