summaryrefslogtreecommitdiff
path: root/addons/escoria-dialog-simple
diff options
context:
space:
mode:
Diffstat (limited to 'addons/escoria-dialog-simple')
-rw-r--r--addons/escoria-dialog-simple/chooser/simple.gd99
-rw-r--r--addons/escoria-dialog-simple/chooser/simple.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/chooser/simple.tscn52
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple.gd211
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple_settings.gd24
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple_settings.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd34
-rw-r--r--addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/plugin.cfg7
-rw-r--r--addons/escoria-dialog-simple/plugin.gd145
-rw-r--r--addons/escoria-dialog-simple/plugin.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_choices.gd44
-rw-r--r--addons/escoria-dialog-simple/states/dialog_choices.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_finish.gd19
-rw-r--r--addons/escoria-dialog-simple/states/dialog_finish.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_idle.gd5
-rw-r--r--addons/escoria-dialog-simple/states/dialog_idle.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_interrupt.gd24
-rw-r--r--addons/escoria-dialog-simple/states/dialog_interrupt.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say.gd172
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say_fast.gd29
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say_fast.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say_finish.gd29
-rw-r--r--addons/escoria-dialog-simple/states/dialog_say_finish.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/states/dialog_visible.gd34
-rw-r--r--addons/escoria-dialog-simple/states/dialog_visible.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/types/avatar.gd239
-rw-r--r--addons/escoria-dialog-simple/types/avatar.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/types/avatar.tscn53
-rw-r--r--addons/escoria-dialog-simple/types/floating.gd287
-rw-r--r--addons/escoria-dialog-simple/types/floating.gd.uid1
-rw-r--r--addons/escoria-dialog-simple/types/floating.tscn12
34 files changed, 1534 insertions, 0 deletions
diff --git a/addons/escoria-dialog-simple/chooser/simple.gd b/addons/escoria-dialog-simple/chooser/simple.gd
new file mode 100644
index 0000000..d7c0297
--- /dev/null
+++ b/addons/escoria-dialog-simple/chooser/simple.gd
@@ -0,0 +1,99 @@
+# A simple dialog chooser that shows selectable lines of text
+# Supports timeout and avatar display
+extends ESCDialogOptionsChooser
+
+
+@export var color_normal = Color(1.0,1.0,1.0,1.0) # (Color, RGB)
+@export var color_hover = Color(165.0,42.0,42.0, 1.0) # (Color, RGB)
+
+
+var _no_more_options: bool = false
+
+
+# Hide the chooser at the start just to be safe
+func _ready() -> void:
+ hide_chooser()
+ process_mode = PROCESS_MODE_PAUSABLE
+
+
+# Process the timeout display
+func _process(delta: float) -> void:
+ if $MarginContainer.visible and self.dialog and self.dialog.timeout > 0:
+ $TimerProgress.value = (
+ self.dialog.timeout - $Timer.time_left
+ ) / self.dialog.timeout * 100
+
+
+# Show the chooser
+func show_chooser():
+ var _vbox = $MarginContainer/ScrollContainer/VBoxContainer
+ for option_node in _vbox.get_children():
+ _vbox.remove_child(option_node)
+
+ _remove_avatar()
+
+ for option in self.dialog.options:
+ if option.is_valid():
+ var _option_node = Button.new()
+ _option_node.text = (option as ESCDialogOption).option
+ _option_node.flat = true
+ _option_node.add_theme_color_override("font_color", color_normal)
+ _option_node.add_theme_color_override("font_color_hover", color_hover)
+ _vbox.add_child(_option_node)
+
+ _option_node.pressed.connect(_on_answer_selected.bind(option))
+
+ # If we've no options left, signify as much and start the timer with a
+ # very short interval so the appropriate signal can be fired. Note that
+ # we have to fire the signal AFTER this method returns as the caller
+ # is almost certainly yielding after this method returns.
+ if _vbox.get_child_count() == 0:
+ _no_more_options = true
+ $Timer.start(0.05)
+ return
+
+ if self.dialog.avatar != "-":
+ $AvatarContainer.add_child(
+ ResourceLoader.load(self.dialog.avatar).instantiate()
+ )
+
+ $MarginContainer.show()
+
+ if self.dialog.timeout > 0:
+ $Timer.start(self.dialog.timeout)
+
+
+# Hide the chooser
+func hide_chooser():
+ $MarginContainer.hide()
+
+
+# An option was choosen, emit the option
+#
+# #### Parameters
+# - option: Option that was chosen
+func _option_chosen(option: ESCDialogOption):
+ _remove_avatar()
+ $TimerProgress.value = 0
+ option_chosen.emit(option)
+
+
+# An option was chosen directly from the list
+#
+# #### Parameters
+# - option: Option that was chosen
+func _on_answer_selected(option: ESCDialogOption):
+ _option_chosen(option)
+
+
+# The timeout came and a option was selected
+func _on_Timer_timeout() -> void:
+ var option_chosen = null if _no_more_options else self.dialog.options[self.dialog.timeout_option - 1]
+ _no_more_options = false
+ _option_chosen(option_chosen)
+
+
+# Remove the avatar
+func _remove_avatar():
+ if $AvatarContainer.get_child_count() > 0:
+ $AvatarContainer.remove_child($AvatarContainer.get_child(0))
diff --git a/addons/escoria-dialog-simple/chooser/simple.gd.uid b/addons/escoria-dialog-simple/chooser/simple.gd.uid
new file mode 100644
index 0000000..05b63ec
--- /dev/null
+++ b/addons/escoria-dialog-simple/chooser/simple.gd.uid
@@ -0,0 +1 @@
+uid://bfwgvr8mfsqvb
diff --git a/addons/escoria-dialog-simple/chooser/simple.tscn b/addons/escoria-dialog-simple/chooser/simple.tscn
new file mode 100644
index 0000000..0dcab9f
--- /dev/null
+++ b/addons/escoria-dialog-simple/chooser/simple.tscn
@@ -0,0 +1,52 @@
+[gd_scene load_steps=4 format=3 uid="uid://bkygfxo5vhqsy"]
+
+[ext_resource type="Script" uid="uid://bfwgvr8mfsqvb" path="res://addons/escoria-dialog-simple/chooser/simple.gd" id="1"]
+
+[sub_resource type="Gradient" id="1"]
+colors = PackedColorArray(1, 0, 0, 1, 1, 0, 0, 1)
+
+[sub_resource type="GradientTexture2D" id="2"]
+gradient = SubResource("1")
+
+[node name="text_dialog_choice" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 0
+offset_left = 20.0
+offset_top = 20.0
+offset_right = 1280.0
+offset_bottom = 900.0
+mouse_filter = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 20
+
+[node name="Timer" type="Timer" parent="."]
+one_shot = true
+
+[node name="TimerProgress" type="TextureProgressBar" parent="."]
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 0
+anchor_right = 1.0
+nine_patch_stretch = true
+texture_progress = SubResource("2")
+
+[node name="AvatarContainer" type="Node2D" parent="."]
+position = Vector2(94, 68)
+
+[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple.gd b/addons/escoria-dialog-simple/esc_dialog_simple.gd
new file mode 100644
index 0000000..6966159
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple.gd
@@ -0,0 +1,211 @@
+## A simple dialog manager for Escoria
+extends ESCDialogManager
+
+
+## State machine that governs how the dialog manager behaves
+var state_machine = preload("res://addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd").new()
+
+# The currently running player
+var _type_player: Node = null
+
+
+var _preserved_type_player_type: String = ""
+
+# Reference to the dialog player
+var _dialog_player: Node = null
+
+# Basic state tracking
+var _is_saying: bool = false
+
+# Whether to preserve the next dialog box used by `say`, or, if already
+# preserving a dialog box, whether to continue using that dialog box
+var _should_preserve_dialog_box: bool = false
+
+
+func _ready() -> void:
+ add_child(state_machine)
+
+
+# Check whether a specific type is supported by the
+# dialog plugin
+#
+# #### Parameters
+# - type: required type
+# **Returns** Whether the type is supported or not
+func has_type(type: String) -> bool:
+ return true if type in ["floating", "avatar"] else false
+
+
+# Check whether a specific chooser type is supported by the
+# dialog plugin
+#
+# #### Parameters
+# - type: required chooser type
+# **Returns** Whether the type is supported or not
+func has_chooser_type(type: String) -> bool:
+ return true if type == "simple" else false
+
+
+# Instructs the dialog manager to preserve the next dialog box used by a `say`
+# command until a call to `disable_preserve_dialog_box` is made.
+#
+# This method should be idempotent, i.e. if called after the first time and
+# prior to `disable_preserve_dialog_box` being called, the result should be the
+# same.
+func enable_preserve_dialog_box() -> void:
+ _should_preserve_dialog_box = true
+
+
+# Instructs the dialog manager to no longer preserve the currently-preserved
+# dialog box or to not preserve the next dialog box used by a `say` command
+# (this is the default state).
+#
+# This method should be idempotent, i.e. if called after the first time and
+# prior to `enable_preserve_dialog_box` being called, the result should be the
+# same.
+func disable_preserve_dialog_box() -> void:
+ _should_preserve_dialog_box = false
+
+ if is_instance_valid(_dialog_player) and _dialog_player.get_children().has(_type_player):
+ _dialog_player.remove_child(_type_player)
+ _preserved_type_player_type = ""
+
+
+# Output a text said by the item specified by the global id. Emit
+# `say_finished` after finishing displaying the text.
+#
+# #### Parameters
+# - dialog_player: Node of the dialog player in the UI
+# - global_id: Global id of the item that is speaking
+# - text: Text to say, optional prefixed by a translation key separated
+# by a ":"
+# - type: Type of dialog box to use
+# - *key*: Translation key
+func say(dialog_player: Node, global_id: String, text: String, type: String, key: String):
+ _dialog_player = dialog_player
+
+ _initialize_say_states(global_id, text, type, key)
+
+ if _should_preserve_dialog_box:
+ # If the dialog box type doesn't match what's currently being reused (if anything),
+ # we want to remove the old one (if it exists) and then initialize and add the new dialog
+ # box type to the dialog player
+ if type != _preserved_type_player_type:
+ if is_instance_valid(_type_player) and _dialog_player.get_children().has(_type_player):
+ _dialog_player.remove_child(_type_player)
+
+ _init_type_player(type)
+
+ _preserved_type_player_type = type
+ else:
+ _init_type_player(type)
+
+ state_machine._change_state("say")
+
+
+func do_say(global_id: String, text: String) -> void:
+ # Only add_child here in order to prevent _type_player from running its _process method
+ # before we're ready, and only if it's necessary
+ if not _dialog_player.get_children().has(_type_player):
+ _dialog_player.add_child(_type_player)
+
+ _type_player.say(global_id, text)
+
+
+func _init_type_player(type: String) -> void:
+ if type == "floating":
+ _type_player = preload(\
+ "res://addons/escoria-dialog-simple/types/floating.tscn"\
+ ).instantiate()
+ else:
+ _type_player = preload(\
+ "res://addons/escoria-dialog-simple/types/avatar.tscn"\
+ ).instantiate()
+
+ _type_player.say_finished.connect(_on_say_finished)
+ _type_player.say_visible.connect(_on_say_visible)
+
+
+func _initialize_say_states(global_id: String, text: String, type: String, key: String) -> void:
+ state_machine.states_map["say"].initialize(self, global_id, text, type, key)
+ state_machine.states_map["finish"].initialize(_dialog_player)
+ state_machine.states_map["say_fast"].initialize(self)
+ state_machine.states_map["say_finish"].initialize(self)
+ state_machine.states_map["visible"].initialize(self)
+ state_machine.states_map["interrupt"].initialize(self)
+
+
+func _on_say_finished():
+ if not _should_preserve_dialog_box and _dialog_player.get_children().has(_type_player):
+ _dialog_player.remove_child(_type_player)
+
+ _is_saying = false
+
+ say_finished.emit()
+
+
+func _on_say_visible():
+ say_visible.emit()
+
+
+# Present an option chooser to the player and sends the signal
+# `option_chosen` with the chosen dialog option
+#
+# #### Parameters
+# - dialog_player: Node of the dialog player in the UI
+# - dialog: Information about the dialog to display
+# - type: The dialog chooser type to use
+func choose(dialog_player: Node, dialog: ESCDialog, type: String):
+ _dialog_player = dialog_player
+
+ state_machine.states_map["choices"].initialize(dialog_player, self, dialog, type)
+ state_machine._change_state("choices")
+
+
+func do_choose(dialog_player: Node, dialog: ESCDialog, type: String = "simple"):
+ var chooser
+
+ if type == "simple" or type == "":
+ chooser = preload(\
+ "res://addons/escoria-dialog-simple/chooser/simple.tscn"\
+ ).instantiate()
+
+ dialog_player.add_child(chooser)
+ chooser.set_dialog(dialog)
+ chooser.show_chooser()
+
+ var option = await chooser.option_chosen
+ dialog_player.remove_child(chooser)
+ option_chosen.emit(option)
+
+
+# Trigger running the dialogue faster
+func speedup():
+ if is_instance_valid(_type_player):
+ _type_player.speedup()
+
+
+# Trigger an instant finish of the current dialog
+func finish():
+ if is_instance_valid(_type_player):
+ _type_player.finish()
+
+
+# The say command has been interrupted, cancel the dialog display
+func interrupt():
+ if _dialog_player.get_children().has(_type_player):
+ (
+ escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\
+ as ESCSpeechPlayer
+ ).set_state("off")
+
+ if not _should_preserve_dialog_box and _dialog_player.get_children().has(_type_player):
+ _dialog_player.remove_child(_type_player)
+
+ say_finished.emit()
+
+
+# To be called if voice audio has finished.
+func voice_audio_finished():
+ if is_instance_valid(_type_player):
+ _type_player.voice_audio_finished()
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple.gd.uid b/addons/escoria-dialog-simple/esc_dialog_simple.gd.uid
new file mode 100644
index 0000000..57fe82e
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple.gd.uid
@@ -0,0 +1 @@
+uid://dgyquhdnffck5
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd b/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd
new file mode 100644
index 0000000..a750876
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd
@@ -0,0 +1,24 @@
+extends Resource
+class_name SimpleDialogSettings
+
+
+const SETTINGS_ROOT = "escoria/dialog_simple"
+
+const AVATARS_PATH = "%s/avatars_path" % SETTINGS_ROOT
+const TEXT_TIME_PER_LETTER_MS = "%s/text_time_per_letter_ms" % SETTINGS_ROOT
+const TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE = 100
+const TEXT_TIME_PER_LETTER_MS_FAST = "%s/text_time_per_fast_letter_ms" % SETTINGS_ROOT
+const TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE = 25
+const READING_SPEED_IN_WPM = "%s/reading_speed_in_wpm" % SETTINGS_ROOT
+const READING_SPEED_IN_WPM_DEFAULT_VALUE = 200
+const CLEAR_TEXT_BY_CLICK_ONLY = "%s/clear_text_by_click_only" % SETTINGS_ROOT
+const LEFT_CLICK_ACTION = "%s/left_click_action" % SETTINGS_ROOT
+
+const STOP_TALKING_ANIMATION_ON = "%s/stop_talking_animation_on" % SETTINGS_ROOT
+
+const LEFT_CLICK_ACTION_SPEED_UP = "Speed up"
+const LEFT_CLICK_ACTION_INSTANT_FINISH = "Instant finish"
+const LEFT_CLICK_ACTION_NOTHING = "None"
+
+const STOP_TALKING_ANIMATION_ON_END_OF_TEXT = "End of text"
+const STOP_TALKING_ANIMATION_ON_END_OF_AUDIO = "End of audio"
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd.uid b/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd.uid
new file mode 100644
index 0000000..b8e5107
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple_settings.gd.uid
@@ -0,0 +1 @@
+uid://cw5gdwad3515m
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd b/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd
new file mode 100644
index 0000000..4c48f60
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd
@@ -0,0 +1,34 @@
+extends StateMachine
+## Instanciation of this dialogs state machine implementation.
+
+# Constructor
+func _init():
+ _create_states()
+ _add_states_to_machine()
+ current_state_name = "idle"
+ # This line is very important here: it defines the initial state of the
+ # dialogs state machine. Since Escoria can't guess which state is the default one
+ # it has to be assigned here. If it happens to be null on initialize() call,
+ # an error is triggered.
+ START_STATE = states_map[current_state_name]
+ initialize(START_STATE)
+
+
+# Creates the states for this state machine.
+func _create_states() -> void:
+ states_map = {
+ "idle": preload("res://addons/escoria-dialog-simple/states/dialog_idle.gd").new(),
+ "say": preload("res://addons/escoria-dialog-simple/states/dialog_say.gd").new(),
+ "say_fast": preload("res://addons/escoria-dialog-simple/states/dialog_say_fast.gd").new(),
+ "say_finish": preload("res://addons/escoria-dialog-simple/states/dialog_say_finish.gd").new(),
+ "visible": preload("res://addons/escoria-dialog-simple/states/dialog_visible.gd").new(),
+ "finish": preload("res://addons/escoria-dialog-simple/states/dialog_finish.gd").new(),
+ "interrupt": preload("res://addons/escoria-dialog-simple/states/dialog_interrupt.gd").new(),
+ "choices": preload("res://addons/escoria-dialog-simple/states/dialog_choices.gd").new()
+ }
+
+
+# Adds any created states into the state machine as children.
+func _add_states_to_machine() -> void:
+ for key in states_map:
+ add_child(states_map[key])
diff --git a/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd.uid b/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd.uid
new file mode 100644
index 0000000..13db836
--- /dev/null
+++ b/addons/escoria-dialog-simple/esc_dialog_simple_state_machine.gd.uid
@@ -0,0 +1 @@
+uid://ck878sot1akca
diff --git a/addons/escoria-dialog-simple/plugin.cfg b/addons/escoria-dialog-simple/plugin.cfg
new file mode 100644
index 0000000..70cad5a
--- /dev/null
+++ b/addons/escoria-dialog-simple/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Escoria Simple Dialogs"
+description="Very basic dialogs for Escoria based games"
+author="Escoria project"
+version="0.1.0"
+script="plugin.gd"
diff --git a/addons/escoria-dialog-simple/plugin.gd b/addons/escoria-dialog-simple/plugin.gd
new file mode 100644
index 0000000..fd552f8
--- /dev/null
+++ b/addons/escoria-dialog-simple/plugin.gd
@@ -0,0 +1,145 @@
+@tool
+# A simple dialog manager for Escoria
+extends EditorPlugin
+
+const MANAGER_CLASS = "res://addons/escoria-dialog-simple/esc_dialog_simple.gd"
+
+
+var left_click_actions: PackedStringArray = [
+ SimpleDialogSettings.LEFT_CLICK_ACTION_SPEED_UP,
+ SimpleDialogSettings.LEFT_CLICK_ACTION_INSTANT_FINISH,
+ SimpleDialogSettings.LEFT_CLICK_ACTION_NOTHING
+]
+
+var stop_talking_animation_on_options: PackedStringArray = [
+ SimpleDialogSettings.STOP_TALKING_ANIMATION_ON_END_OF_TEXT,
+ SimpleDialogSettings.STOP_TALKING_ANIMATION_ON_END_OF_AUDIO
+]
+
+
+# Override function to return the plugin name.
+func _get_plugin_name():
+ return "escoria-dialog-simple"
+
+
+# Unregister ourselves
+func _disable_plugin():
+ print("Disabling plugin Escoria Dialog Simple")
+ ESCProjectSettingsManager.remove_setting(
+ ESCProjectSettingsManager.DEFAULT_DIALOG_TYPE
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.AVATARS_PATH
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.CLEAR_TEXT_BY_CLICK_ONLY
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.READING_SPEED_IN_WPM
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.LEFT_CLICK_ACTION
+ )
+
+ ESCProjectSettingsManager.remove_setting(
+ SimpleDialogSettings.STOP_TALKING_ANIMATION_ON
+ )
+
+ EscoriaPlugin.deregister_dialog_manager(MANAGER_CLASS)
+
+
+# Add ourselves to the list of dialog managers
+func _enable_plugin():
+ print("Enabling plugin Escoria Dialog Simple")
+
+ if EscoriaPlugin.register_dialog_manager(self, MANAGER_CLASS):
+ ESCProjectSettingsManager.register_setting(
+ ESCProjectSettingsManager.DEFAULT_DIALOG_TYPE,
+ "floating",
+ {
+ "type": TYPE_STRING
+ }
+ )
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.AVATARS_PATH,
+ "res://game/dialog_avatars",
+ {
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_DIR
+ }
+ )
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS,
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE,
+ {
+ "type": TYPE_FLOAT
+ }
+ )
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST,
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE,
+ {
+ "type": TYPE_FLOAT
+ }
+ )
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.CLEAR_TEXT_BY_CLICK_ONLY,
+ false,
+ {
+ "type": TYPE_BOOL
+ }
+ )
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.READING_SPEED_IN_WPM,
+ SimpleDialogSettings.READING_SPEED_IN_WPM_DEFAULT_VALUE,
+ {
+ "type": TYPE_INT
+ }
+ )
+
+ var left_click_actions_string: String = ",".join(left_click_actions)
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.LEFT_CLICK_ACTION,
+ SimpleDialogSettings.LEFT_CLICK_ACTION_SPEED_UP,
+ {
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": left_click_actions_string
+ }
+ )
+
+ var stop_talking_animation_on_options_string: String = ",".join(stop_talking_animation_on_options)
+
+ ESCProjectSettingsManager.register_setting(
+ SimpleDialogSettings.STOP_TALKING_ANIMATION_ON,
+ SimpleDialogSettings.STOP_TALKING_ANIMATION_ON_END_OF_AUDIO,
+ {
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": stop_talking_animation_on_options_string
+ }
+ )
+
+ else:
+ get_editor_interface().set_plugin_enabled(
+ _get_plugin_name(),
+ false
+ )
diff --git a/addons/escoria-dialog-simple/plugin.gd.uid b/addons/escoria-dialog-simple/plugin.gd.uid
new file mode 100644
index 0000000..af1685b
--- /dev/null
+++ b/addons/escoria-dialog-simple/plugin.gd.uid
@@ -0,0 +1 @@
+uid://xixeu3ksbt73
diff --git a/addons/escoria-dialog-simple/states/dialog_choices.gd b/addons/escoria-dialog-simple/states/dialog_choices.gd
new file mode 100644
index 0000000..3779a6d
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_choices.gd
@@ -0,0 +1,44 @@
+extends State
+
+
+# The owning dialog player.
+var _dialog_player
+
+# The dialog to start.
+var _dialog: ESCDialog
+var _type: String = "simple"
+
+var _dialog_chooser_ui: ESCDialogManager = null
+
+var _ready_to_choose: bool
+
+
+func initialize(dialog_player, dialog_chooser_ui: ESCDialogManager, dialog: ESCDialog, type: String) -> void:
+ _dialog_player = dialog_player
+ _dialog_chooser_ui = dialog_chooser_ui
+ _dialog = dialog
+ _type = type
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'choices'.")
+
+ if _dialog.options.is_empty():
+ escoria.logger.error(
+ self,
+ "Received dialog options array was empty."
+ )
+
+ _ready_to_choose = true
+
+
+func update(_delta):
+ if _ready_to_choose:
+ _ready_to_choose = false
+ _dialog_chooser_ui.do_choose(_dialog_player, _dialog, _type)
+ var option = await _dialog_chooser_ui.option_chosen
+
+ escoria.logger.trace(self, "Dialog State Machine: 'choices' -> 'idle'")
+
+ finished.emit("idle")
+ _dialog_player.option_chosen.emit(option)
diff --git a/addons/escoria-dialog-simple/states/dialog_choices.gd.uid b/addons/escoria-dialog-simple/states/dialog_choices.gd.uid
new file mode 100644
index 0000000..2e5d12c
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_choices.gd.uid
@@ -0,0 +1 @@
+uid://vmw64noavihm
diff --git a/addons/escoria-dialog-simple/states/dialog_finish.gd b/addons/escoria-dialog-simple/states/dialog_finish.gd
new file mode 100644
index 0000000..7dac600
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_finish.gd
@@ -0,0 +1,19 @@
+extends State
+
+
+# Owning dialog player
+var _dialog_player
+
+
+func initialize(dialog_player) -> void:
+ _dialog_player = dialog_player
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'finish'.")
+
+
+func update(_delta):
+ escoria.logger.trace(self, "Dialog State Machine: 'finish' -> 'idle'")
+ finished.emit("idle")
+ _dialog_player.say_finished.emit()
diff --git a/addons/escoria-dialog-simple/states/dialog_finish.gd.uid b/addons/escoria-dialog-simple/states/dialog_finish.gd.uid
new file mode 100644
index 0000000..e9cc6da
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_finish.gd.uid
@@ -0,0 +1 @@
+uid://drpcjgaq0gd8j
diff --git a/addons/escoria-dialog-simple/states/dialog_idle.gd b/addons/escoria-dialog-simple/states/dialog_idle.gd
new file mode 100644
index 0000000..d53d89e
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_idle.gd
@@ -0,0 +1,5 @@
+extends State
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'idle'.")
diff --git a/addons/escoria-dialog-simple/states/dialog_idle.gd.uid b/addons/escoria-dialog-simple/states/dialog_idle.gd.uid
new file mode 100644
index 0000000..1a0adf1
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_idle.gd.uid
@@ -0,0 +1 @@
+uid://3huotinfsd51
diff --git a/addons/escoria-dialog-simple/states/dialog_interrupt.gd b/addons/escoria-dialog-simple/states/dialog_interrupt.gd
new file mode 100644
index 0000000..83fb707
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_interrupt.gd
@@ -0,0 +1,24 @@
+extends State
+
+
+# Reference to the currently playing dialog manager
+var _dialog_manager: ESCDialogManager = null
+
+
+func initialize(dialog_manager: ESCDialogManager) -> void:
+ _dialog_manager = dialog_manager
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'interrupt'.")
+
+ if _dialog_manager != null:
+ if not _dialog_manager.say_finished.is_connected(_on_say_finished):
+ _dialog_manager.say_finished.connect(_on_say_finished)
+
+ _dialog_manager.interrupt()
+
+
+func _on_say_finished() -> void:
+ escoria.logger.trace(self, "Dialog State Machine: 'interrupt' -> 'finish'")
+ finished.emit("finish")
diff --git a/addons/escoria-dialog-simple/states/dialog_interrupt.gd.uid b/addons/escoria-dialog-simple/states/dialog_interrupt.gd.uid
new file mode 100644
index 0000000..3cce4b7
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_interrupt.gd.uid
@@ -0,0 +1 @@
+uid://cicognkh6ct0i
diff --git a/addons/escoria-dialog-simple/states/dialog_say.gd b/addons/escoria-dialog-simple/states/dialog_say.gd
new file mode 100644
index 0000000..c46aea0
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say.gd
@@ -0,0 +1,172 @@
+extends State
+
+
+# Reference to the currently playing dialog manager
+var _dialog_manager: ESCDialogManager = null
+
+# Character that is talking
+var _character: String
+
+# UI to use for the dialog
+var _type: String
+
+# Translation key
+var _key: String = ""
+
+# Text to say
+var _text: String
+
+var _ready_to_say: bool
+
+# flag for whether the dialog manager has started to "say" to ensure that it has
+# prior to exiting this state as other states need this to happen to continue
+# (other states rely on the setup that the dialog manager does)
+var _say_started: bool
+
+var _stop_talking_animation_on_option: String
+
+
+func initialize(dialog_manager: ESCDialogManager, character: String, text: String, type: String, key: String) -> void:
+ _dialog_manager = dialog_manager
+ _character = character
+ _text = text
+ _type = type
+ _key = key
+ _stop_talking_animation_on_option = \
+ ESCProjectSettingsManager.get_setting(SimpleDialogSettings.STOP_TALKING_ANIMATION_ON)
+
+
+func handle_input(_event):
+ if _event is InputEventMouseButton and _event.pressed:
+ if escoria.inputs_manager.input_mode != \
+ escoria.inputs_manager.INPUT_NONE and \
+ _dialog_manager != null:
+
+ var left_click_action = ESCProjectSettingsManager.get_setting(SimpleDialogSettings.LEFT_CLICK_ACTION)
+
+ _handle_left_click_action(left_click_action)
+
+
+func _handle_left_click_action(left_click_action: String) -> void:
+ match left_click_action:
+ SimpleDialogSettings.LEFT_CLICK_ACTION_SPEED_UP:
+ if _dialog_manager.say_visible.is_connected(_on_say_visible):
+ _dialog_manager.say_visible.disconnect(_on_say_visible)
+
+ escoria.logger.trace(self, "Dialog State Machine: 'say' -> 'say_fast'")
+ finished.emit("say_fast")
+ SimpleDialogSettings.LEFT_CLICK_ACTION_INSTANT_FINISH:
+ if _dialog_manager.say_visible.is_connected(_on_say_visible):
+ _dialog_manager.say_visible.disconnect(_on_say_visible)
+
+ escoria.logger.trace(self, "Dialog State Machine: 'say' -> 'say_finish'")
+ finished.emit("say_finish")
+
+ get_viewport().set_input_as_handled()
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'say'.")
+
+ _say_started = false
+
+ if not _dialog_manager.say_visible.is_connected(_on_say_visible):
+ _dialog_manager.say_visible.connect(_on_say_visible)
+
+ if _key and not _key.is_empty():
+ var _speech_resource = _get_voice_file(_key)
+
+ if _speech_resource == "":
+ escoria.logger.warn(
+ self,
+ "Unable to find voice file with key '%s'." % _key
+ )
+ else:
+ (
+ escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\
+ as ESCSpeechPlayer
+ ).set_state(_speech_resource)
+
+ if _stop_talking_animation_on_option == SimpleDialogSettings.STOP_TALKING_ANIMATION_ON_END_OF_AUDIO:
+ if not (
+ escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\
+ as ESCSpeechPlayer
+ ).stream.finished.is_connected(_on_audio_finished):
+
+ (
+ escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\
+ as ESCSpeechPlayer
+ ).stream.finished.connect(_on_audio_finished)
+
+ var translated_text: String = tr(_key)
+
+ # Only update the text if the translated text was found; otherwise, raise
+ # a warning and use the original, untranslated text.
+ if translated_text == _key:
+ escoria.logger.warn(
+ self,
+ "Unable to find translation key '%s'. Using untranslated text." % _key
+ )
+ else:
+ _text = translated_text
+
+ _ready_to_say = true
+
+
+func exit() -> void:
+ if not _say_started:
+ _dialog_manager.do_say(_character, _text)
+ _say_started = true
+
+
+func update(_delta):
+ if _ready_to_say:
+ _dialog_manager.do_say(_character, _text)
+ _say_started = true
+ _ready_to_say = false
+
+
+# Find the matching voice output file for the given key
+#
+# #### Parameters
+#
+# - key: Text key provided
+# - start: Starting folder to search for voices
+#
+# **Returns** The path to the matching voice file
+func _get_voice_file(key: String, start: String = "") -> String:
+ if start == "":
+ start = ESCProjectSettingsManager.get_setting(
+ ESCProjectSettingsManager.SPEECH_FOLDER
+ )
+ var _dir = DirAccess.open(start)
+ if _dir != null:
+ _dir.list_dir_begin() # TODOConverter3To4 fill missing arguments https://github.com/godotengine/godot/pull/40547
+ var file_name = _dir.get_next()
+ while file_name != "":
+ if _dir.current_is_dir():
+ var _voice_file = _get_voice_file(
+ key,
+ start.path_join(file_name)
+ )
+ if _voice_file != "":
+ return _voice_file
+ else:
+ if file_name == "%s.%s.import" % [
+ key,
+ ESCProjectSettingsManager.get_setting(
+ ESCProjectSettingsManager.SPEECH_EXTENSION
+ )
+ ]:
+ return start.path_join(file_name.trim_suffix(".import"))
+ file_name = _dir.get_next()
+ return ""
+
+
+func _on_say_visible() -> void:
+ escoria.logger.trace(self, "Dialog State Machine: 'say' -> 'visible'")
+ finished.emit("visible")
+
+
+func _on_audio_finished() -> void:
+ _dialog_manager.voice_audio_finished()
diff --git a/addons/escoria-dialog-simple/states/dialog_say.gd.uid b/addons/escoria-dialog-simple/states/dialog_say.gd.uid
new file mode 100644
index 0000000..c51fafb
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say.gd.uid
@@ -0,0 +1 @@
+uid://u44euoojg2al
diff --git a/addons/escoria-dialog-simple/states/dialog_say_fast.gd b/addons/escoria-dialog-simple/states/dialog_say_fast.gd
new file mode 100644
index 0000000..14d9f05
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say_fast.gd
@@ -0,0 +1,29 @@
+extends State
+
+
+# Reference to the currently playing dialog manager
+var _dialog_manager: ESCDialogManager = null
+
+
+func initialize(dialog_manager: ESCDialogManager) -> void:
+ _dialog_manager = dialog_manager
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'say_fast'.")
+
+ if escoria.inputs_manager.input_mode != \
+ escoria.inputs_manager.INPUT_NONE and \
+ _dialog_manager != null:
+
+ if not _dialog_manager.say_visible.is_connected(_on_say_visible):
+ _dialog_manager.say_visible.connect(_on_say_visible)
+
+ _dialog_manager.speedup()
+ else:
+ escoria.logger.error(self, "Illegal state.")
+
+
+func _on_say_visible() -> void:
+ escoria.logger.trace(self, "Dialog State Machine: 'say_fast' -> 'visible'")
+ finished.emit("visible")
diff --git a/addons/escoria-dialog-simple/states/dialog_say_fast.gd.uid b/addons/escoria-dialog-simple/states/dialog_say_fast.gd.uid
new file mode 100644
index 0000000..5954fcd
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say_fast.gd.uid
@@ -0,0 +1 @@
+uid://cqeelw42dgxp8
diff --git a/addons/escoria-dialog-simple/states/dialog_say_finish.gd b/addons/escoria-dialog-simple/states/dialog_say_finish.gd
new file mode 100644
index 0000000..0e5d8aa
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say_finish.gd
@@ -0,0 +1,29 @@
+extends State
+
+
+# Reference to the currently playing dialog manager
+var _dialog_manager: ESCDialogManager = null
+
+
+func initialize(dialog_manager: ESCDialogManager) -> void:
+ _dialog_manager = dialog_manager
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'say_finish'.")
+
+ if escoria.inputs_manager.input_mode != \
+ escoria.inputs_manager.INPUT_NONE and \
+ _dialog_manager != null:
+
+ if not _dialog_manager.say_visible.is_connected(_on_say_visible):
+ _dialog_manager.say_visible.connect(_on_say_visible)
+
+ _dialog_manager.finish()
+ else:
+ escoria.logger.error(self, "Illegal state.")
+
+
+func _on_say_visible() -> void:
+ escoria.logger.trace(self, "Dialog State Machine: 'say_finish' -> 'visible'")
+ finished.emit("visible")
diff --git a/addons/escoria-dialog-simple/states/dialog_say_finish.gd.uid b/addons/escoria-dialog-simple/states/dialog_say_finish.gd.uid
new file mode 100644
index 0000000..c2bf455
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_say_finish.gd.uid
@@ -0,0 +1 @@
+uid://cckwcr7x1kao
diff --git a/addons/escoria-dialog-simple/states/dialog_visible.gd b/addons/escoria-dialog-simple/states/dialog_visible.gd
new file mode 100644
index 0000000..d885cb3
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_visible.gd
@@ -0,0 +1,34 @@
+extends State
+
+
+# Reference to the currently playing dialog manager
+var _dialog_manager: ESCDialogManager = null
+
+
+func initialize(dialog_manager: ESCDialogManager) -> void:
+ _dialog_manager = dialog_manager
+
+
+func enter():
+ escoria.logger.trace(self, "Dialog State Machine: Entered 'visible'.")
+
+ if not _dialog_manager.say_finished.is_connected(_on_say_finished):
+ _dialog_manager.say_finished.connect(_on_say_finished)
+
+
+func handle_input(_event):
+ if _event is InputEventMouseButton and _event.pressed:
+ if escoria.inputs_manager.input_mode != \
+ escoria.inputs_manager.INPUT_NONE:
+
+ if _dialog_manager.say_finished.is_connected(_on_say_finished):
+ _dialog_manager.say_finished.disconnect(_on_say_finished)
+
+ finished.emit("interrupt")
+ get_viewport().set_input_as_handled()
+
+
+# Handles the end of a say function after it has emitted say_finished.
+func _on_say_finished():
+ escoria.logger.trace(self, "Dialog State Machine: 'visible' -> 'finish'")
+ finished.emit("finish")
diff --git a/addons/escoria-dialog-simple/states/dialog_visible.gd.uid b/addons/escoria-dialog-simple/states/dialog_visible.gd.uid
new file mode 100644
index 0000000..860ace8
--- /dev/null
+++ b/addons/escoria-dialog-simple/states/dialog_visible.gd.uid
@@ -0,0 +1 @@
+uid://b7hcll3bfcp8u
diff --git a/addons/escoria-dialog-simple/types/avatar.gd b/addons/escoria-dialog-simple/types/avatar.gd
new file mode 100644
index 0000000..ce0e254
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/avatar.gd
@@ -0,0 +1,239 @@
+# A dialog GUI showing a dialog box and character portraits
+extends Window
+
+
+# Signal emitted when text has been said
+signal say_finished
+
+# Signal emitted when text has just become fully visible
+signal say_visible
+
+
+# The text speed per character for normal display
+var _text_time_per_character: float
+
+# The text speed per character if the dialog line is skipped
+var _fast_text_time_per_character: float
+
+# The reading speed to be used in determining the length of time text remains
+# on the screen.
+var _reading_speed_in_wpm: int
+
+# Used to extract words from lines of text.
+var _word_regex: RegEx = RegEx.new()
+
+# Whether the current dialog is speeding up
+var _is_speeding_up: bool = false
+
+# The current line of text being displayed.
+var _current_line: String
+
+
+# The node holding the avatar
+@onready var avatar_node = $Panel/MarginContainer/HSplitContainer/VBoxContainer\
+ /avatar
+
+# The node showing the text
+@onready var text_node = $Panel/MarginContainer/HSplitContainer/text
+
+# The tween node for text animations
+@onready var tween: Tween3 = Tween3.new(self)
+
+# Whether the dialog manager is paused
+@onready var is_paused: bool = true
+
+# Build up the UI
+func _ready():
+ _text_time_per_character = ProjectSettings.get_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS
+ )
+
+ if _text_time_per_character < 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a non-negative number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS,
+ escoria.TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE
+ ]
+ )
+
+ _text_time_per_character = escoria.TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE
+
+ _fast_text_time_per_character = ProjectSettings.get_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST
+ )
+
+ if _fast_text_time_per_character < 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a non-negative number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST,
+ escoria.TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE
+ ]
+ )
+
+ _fast_text_time_per_character = escoria.TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE
+
+ _reading_speed_in_wpm = ProjectSettings.get_setting(
+ SimpleDialogSettings.READING_SPEED_IN_WPM
+ )
+
+ if _reading_speed_in_wpm <= 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a positive number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.READING_SPEED_IN_WPM,
+ escoria.READING_SPEED_IN_WPM_DEFAULT_VALUE
+ ]
+ )
+
+ _reading_speed_in_wpm = escoria.READING_SPEED_IN_WPM_DEFAULT_VALUE
+
+ _word_regex.compile("\\S+")
+
+ text_node.bbcode_enabled = true
+ tween.finished.connect(_on_dialog_line_typed.bind("", ""))
+
+ escoria.paused.connect(_on_paused)
+ escoria.resumed.connect(_on_resumed)
+
+ tree_exited.connect(_on_tree_exited)
+
+
+# Switch the current character
+#
+# #### Parameters
+# - name: The name of the current character
+func set_current_character(name: String):
+ if ProjectSettings.get_setting("escoria/dialog_simple/avatars_path").is_empty():
+ escoria.logger.warn(self, "Unable to load avatar '%s': Avatar path not specified" % name)
+ return
+
+ var avatar = "%s/%s.tres" % [
+ ProjectSettings.get_setting("escoria/dialog_simple/avatars_path"),
+ name
+ ]
+ if ResourceLoader.exists(avatar):
+ avatar_node.texture = ResourceLoader.load(avatar)
+
+ if avatar_node.texture is AnimatedTexture:
+ avatar_node.texture.current_frame = 0
+ avatar_node.texture.pause = false
+ else:
+ escoria.logger.warn(self, "Unable to load avatar '%s': Resource not found in path '%s'" %
+ [name, ProjectSettings.get_setting("escoria/dialog_simple/avatars_path")])
+
+
+# Make a character say something
+#
+# #### Parameters
+# - character: The global id of the character speaking
+# - line: Line to say
+func say(character: String, line: String):
+ _current_line = line
+
+ _is_speeding_up = false
+
+ popup_centered()
+ set_current_character(character)
+
+ text_node.text = tr(line)
+
+ text_node.visible_ratio = 0.0
+ var time_show_full_text = _text_time_per_character / 1000 * len(line)
+
+ tween.reset()
+
+ tween.interpolate_property(text_node, "visible_ratio",
+ 0.0, 1.0, time_show_full_text,
+ Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
+ tween.play()
+
+
+# Called by the dialog player when the
+func speedup():
+ if not _is_speeding_up:
+ _is_speeding_up = true
+ var time_show_full_text = _fast_text_time_per_character / 1000 * len(_current_line)
+ tween.reset()
+ tween.interpolate_property(text_node, "visible_ratio",
+ text_node.visible_ratio, 1.0, time_show_full_text,
+ Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
+ tween.play()
+
+
+# Called by the dialog player when user wants to finish dialogue immediately.
+func finish():
+ tween.reset()
+ tween.interpolate_property(text_node, "visible_ratio",
+ text_node.visible_ratio, 1.0, 0.0)
+ tween.play()
+
+
+# To be called if voice audio has finished.
+func voice_audio_finished():
+ if avatar_node and avatar_node.texture:
+ avatar_node.texture.current_frame = 0
+ avatar_node.texture.pause = true
+
+
+# The dialog line was printed, start the waiting time and then finish
+# the dialog
+func _on_dialog_line_typed(object, key):
+ if avatar_node.texture is AnimatedTexture:
+ avatar_node.texture.current_frame = 0
+ avatar_node.texture.pause = true
+
+ text_node.visible_characters = -1
+
+ var time_to_disappear: float = _calculate_time_to_disappear()
+
+ if not $Timer.timeout.is_connected(_on_dialog_finished):
+ $Timer.timeout.connect(_on_dialog_finished)
+
+ $Timer.start(time_to_disappear)
+
+ say_visible.emit()
+
+
+func _calculate_time_to_disappear() -> float:
+ return (_get_number_of_words() / _reading_speed_in_wpm as float) * 60
+
+
+func _get_number_of_words() -> int:
+ return _word_regex.search_all(text_node.get_text()).size()
+
+
+# Ending the dialog
+func _on_dialog_finished():
+ $Timer.stop()
+
+ # Only trigger to clear the text if we aren't limiting the clearing trigger to a click.
+ if not ESCProjectSettingsManager.get_setting(SimpleDialogSettings.CLEAR_TEXT_BY_CLICK_ONLY):
+ say_finished.emit()
+
+
+# Handler managing pause notification from Escoria
+func _on_paused():
+ if tween.is_running():
+ is_paused = true
+ tween.stop()
+
+
+# Handler managing resume notification from Escoria
+func _on_resumed():
+ if not tween.is_running():
+ # We can't rely on "show()" to make an invisible popup reappear, as per the docs for
+ # CanvasItem. Instead, we need to use one of the popup_* methods.
+ if is_inside_tree():
+ popup_centered()
+
+ is_paused = false
+ tween.resume()
+
+
+func _on_tree_exited():
+ queue_free()
diff --git a/addons/escoria-dialog-simple/types/avatar.gd.uid b/addons/escoria-dialog-simple/types/avatar.gd.uid
new file mode 100644
index 0000000..8b66ed8
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/avatar.gd.uid
@@ -0,0 +1 @@
+uid://cfkvypxfuu2mt
diff --git a/addons/escoria-dialog-simple/types/avatar.tscn b/addons/escoria-dialog-simple/types/avatar.tscn
new file mode 100644
index 0000000..6136f5f
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/avatar.tscn
@@ -0,0 +1,53 @@
+[gd_scene load_steps=2 format=3 uid="uid://cp75ofyuetxux"]
+
+[ext_resource type="Script" uid="uid://cfkvypxfuu2mt" path="res://addons/escoria-dialog-simple/types/avatar.gd" id="1"]
+
+[node name="dialog_box" type="Window"]
+position = Vector2i(0, 36)
+size = Vector2i(510, 180)
+visible = false
+unresizable = true
+borderless = true
+popup_window = true
+script = ExtResource("1")
+
+[node name="Timer" type="Timer" parent="."]
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="Panel"]
+layout_mode = 0
+anchor_right = 1.0
+anchor_bottom = 1.0
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+
+[node name="HSplitContainer" type="HSplitContainer" parent="Panel/MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 35
+dragger_visibility = 1
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer/HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.3
+
+[node name="avatar" type="TextureRect" parent="Panel/MarginContainer/HSplitContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+stretch_mode = 4
+
+[node name="text" type="RichTextLabel" parent="Panel/MarginContainer/HSplitContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+bbcode_enabled = true
+text = "Here be some text"
diff --git a/addons/escoria-dialog-simple/types/floating.gd b/addons/escoria-dialog-simple/types/floating.gd
new file mode 100644
index 0000000..1ca78d6
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/floating.gd
@@ -0,0 +1,287 @@
+# A dialog UI using a label above the head of the character
+extends RichTextLabel
+
+
+# Signal emitted when text has been said
+signal say_finished
+
+# Signal emitted when text has just become fully visible
+signal say_visible
+
+
+# The text speed per character for normal display
+var _text_time_per_character: float
+
+# The text speed per character if the dialog line is skipped
+var _fast_text_time_per_character: float
+
+# The reading speed to be used in determining the length of time text remains
+# on the screen.
+var _reading_speed_in_wpm: int
+
+# Used to extract words from lines of text.
+var _word_regex: RegEx = RegEx.new()
+
+
+# Current character speaking, to keep track of reference for animation purposes
+var _current_character
+
+# Whether the current dialog is speeding up
+var _is_speeding_up: bool = false
+
+# The current line of text being displayed.
+var _current_line: String
+
+
+# Tween node for text animation
+@onready var tween: Tween3 = Tween3.new(self)
+
+# The node showing the text
+@onready var text_node: RichTextLabel = self
+
+# Whether the dialog manager is paused
+@onready var is_paused: bool = true
+
+var dialog_location_node = null
+
+# Enable bbcode and catch the signal when a tween completed
+func _ready():
+ _text_time_per_character = ProjectSettings.get_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS
+ )
+
+ if _text_time_per_character < 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a non-negative number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS,
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE
+ ]
+ )
+
+ _text_time_per_character = SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_DEFAULT_VALUE
+
+ _fast_text_time_per_character = ProjectSettings.get_setting(
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST
+ )
+
+ if _fast_text_time_per_character < 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a non-negative number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST,
+ SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE
+ ]
+ )
+
+ _fast_text_time_per_character = SimpleDialogSettings.TEXT_TIME_PER_LETTER_MS_FAST_DEFAULT_VALUE
+
+ _reading_speed_in_wpm = ProjectSettings.get_setting(
+ SimpleDialogSettings.READING_SPEED_IN_WPM
+ )
+
+ if _reading_speed_in_wpm <= 0:
+ escoria.logger.warn(
+ self,
+ "%s setting must be a positive number. Will use default value of %s." %
+ [
+ SimpleDialogSettings.READING_SPEED_IN_WPM,
+ SimpleDialogSettings.READING_SPEED_IN_WPM_DEFAULT_VALUE
+ ]
+ )
+
+ _reading_speed_in_wpm = SimpleDialogSettings.READING_SPEED_IN_WPM_DEFAULT_VALUE
+
+ _word_regex.compile("\\S+")
+
+ bbcode_enabled = true
+
+ tween.finished.connect(_on_dialog_line_typed.bind("", ""))
+
+ tree_exiting.connect(_on_tree_exiting)
+
+ escoria.paused.connect(_on_paused)
+ escoria.resumed.connect(_on_resumed)
+
+ _current_line = ""
+
+
+func _process(delta):
+ if _current_character.is_inside_tree() and \
+ is_instance_valid(dialog_location_node):
+ # Position the RichTextLabel on the character's dialog position, if any.
+ position = dialog_location_node.get_global_transform_with_canvas().origin
+ position.x -= size.x / 2
+
+ _account_for_margin_x()
+
+ _account_for_margin_y()
+
+
+# Make a character say something
+#
+# #### Parameters
+# - character: The global id of the character speaking
+# - line: Line to say
+func say(character: String, line: String) :
+ _current_line = line
+
+ show()
+
+ _is_speeding_up = false
+
+ # Position the RichTextLabel on the character's dialog position, if any.
+ _current_character = escoria.object_manager.get_object(character).node
+
+ var dialog_location_count:int = 0
+
+ for c in escoria.object_manager.get_object(character).node.get_children():
+ if c is Marker2D:
+ # Identify any Postion2D nodes
+ if c is ESCDialogLocation:
+ dialog_location_count += 1
+ dialog_location_node = c
+
+ if dialog_location_count > 1:
+ escoria.logger.warn(
+ self,
+ "Multiple ESCDialogLocation nodes found " +
+ "object %s. Last one will be used." % _current_character)
+
+ # Set text color to color set in the actor
+ var text_color = _current_character.dialog_color
+ var text_color_html = text_color.to_html(false)
+
+ text_node.text = "[center][color=#" + text_color_html + "]" \
+ .format([text_color_html]) + tr(line) + "[/color][center]"
+
+ if _current_character.is_inside_tree() and \
+ is_instance_valid(dialog_location_node):
+ position = dialog_location_node.get_global_transform_with_canvas().origin
+
+ position.x -= size.x / 2
+ else:
+ position.x = 0
+ size.x = ProjectSettings.get_setting("display/window/size/viewport_width")
+
+ _account_for_margin_x()
+
+ _account_for_margin_y()
+
+ _current_character.start_talking()
+
+ text_node.visible_ratio = 0.0
+ var time_show_full_text = _text_time_per_character / 1000 * len(_current_line)
+
+ tween.reset()
+
+ tween.interpolate_property(text_node, "visible_ratio",
+ 0.0, 1.0, time_show_full_text,
+ Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
+ tween.play()
+ set_process(true)
+
+
+# Called by the dialog player when user wants to finish dialogue fast.
+func speedup():
+ if not _is_speeding_up:
+ _is_speeding_up = true
+ var time_show_full_text = _fast_text_time_per_character / 1000 * len(_current_line)
+
+ tween.reset()
+
+ tween.interpolate_property(text_node, "visible_ratio",
+ text_node.visible_ratio, 1.0, time_show_full_text,
+ Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
+ tween.play()
+
+
+# Called by the dialog player when user wants to finish dialogue immediately.
+func finish():
+ tween.reset()
+
+ tween.interpolate_property(text_node, "visible_ratio",
+ text_node.visible_ratio, 1.0, 0.0)
+ tween.play()
+
+
+# To be called if voice audio has finished.
+func voice_audio_finished():
+ _stop_character_talking()
+
+
+# The dialog line was printed, start the waiting time and then finish
+# the dialog
+func _on_dialog_line_typed(object, key):
+ _stop_character_talking()
+ text_node.visible_characters = -1
+
+ var time_to_disappear: float = _calculate_time_to_disappear()
+ $Timer.start(time_to_disappear)
+ $Timer.timeout.connect(_on_dialog_finished)
+
+ say_visible.emit()
+
+
+func _calculate_time_to_disappear() -> float:
+ return (_get_number_of_words() / _reading_speed_in_wpm as float) * 60
+
+
+func _get_number_of_words() -> int:
+ return _word_regex.search_all(text_node.get_text()).size()
+
+
+# Ending the dialog
+func _on_dialog_finished():
+ # Only trigger to clear the text if we aren't limiting the clearing trigger to a click.
+ if not ESCProjectSettingsManager.get_setting(SimpleDialogSettings.CLEAR_TEXT_BY_CLICK_ONLY):
+ say_finished.emit()
+
+
+# Handler managing pause notification from Escoria
+func _on_paused():
+ if tween.is_running():
+ is_paused = true
+ tween.stop()
+
+
+# Handler managing resume notification from Escoria
+func _on_resumed():
+ if not tween.is_running():
+ is_paused = false
+ tween.resume()
+
+
+ # Handler to deal with this node being removed
+func _on_tree_exiting() -> void:
+ _stop_character_talking()
+
+
+func _stop_character_talking():
+ # Make the speaking item animation stop talking, if it is still alive
+ if is_instance_valid(_current_character) and _current_character != null:
+ _current_character.stop_talking()
+
+
+func _account_for_margin_x() -> void:
+ if position.x < 0:
+ position.x = 0
+
+ var screen_margin_x = position.x + size.x - \
+ ProjectSettings.get("display/window/size/viewport_width")
+
+ if screen_margin_x > 0:
+ position.x -= screen_margin_x
+
+
+func _account_for_margin_y() -> void:
+ if position.y < 0:
+ position.y = 0
+
+ var screen_margin_y = position.y + size.y - \
+ ProjectSettings.get("display/window/size/viewport_height")
+
+ if screen_margin_y > 0:
+ position.y -= screen_margin_y
diff --git a/addons/escoria-dialog-simple/types/floating.gd.uid b/addons/escoria-dialog-simple/types/floating.gd.uid
new file mode 100644
index 0000000..62a7e80
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/floating.gd.uid
@@ -0,0 +1 @@
+uid://jhr1gt0fmm0u
diff --git a/addons/escoria-dialog-simple/types/floating.tscn b/addons/escoria-dialog-simple/types/floating.tscn
new file mode 100644
index 0000000..f3ee470
--- /dev/null
+++ b/addons/escoria-dialog-simple/types/floating.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://duatpb2fc36db"]
+
+[ext_resource type="Script" uid="uid://jhr1gt0fmm0u" path="res://addons/escoria-dialog-simple/types/floating.gd" id="1"]
+
+[node name="dialog_label" type="RichTextLabel"]
+offset_right = 643.0
+offset_bottom = 60.0
+bbcode_enabled = true
+text = "Here be some text."
+script = ExtResource("1")
+
+[node name="Timer" type="Timer" parent="."]