diff options
Diffstat (limited to 'addons/escoria-dialog-simple')
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="."] |
