diff --git a/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd b/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd index faeac1f1..10c4a2b1 100644 --- a/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd +++ b/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd @@ -6,6 +6,9 @@ class_name ESCDialogManager # Emitted when the say function has completed showing the text signal say_finished +# Emitted when text has just become fully visible +signal say_visible + # Emitted when the player has chosen an option signal option_chosen(option) diff --git a/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd b/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd index 39f0d23b..42dcba6a 100644 --- a/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd +++ b/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd @@ -1,12 +1,8 @@ # Escoria dialog player -extends Node +extends StateMachine class_name ESCDialogPlayer -# A regular expression that separates the translation key from the text -const KEYTEXT_REGEX = "^((?[^:]+):)?\"(?.+)\"" - - # Emitted when an answer as chosem # # ##### Parameters @@ -18,78 +14,51 @@ signal option_chosen(option) signal say_finished -# Whether the player is currently speaking -var is_speaking: bool = false - - # Reference to the currently playing dialog manager var _dialog_manager: ESCDialogManager = null -# Regular expression object for the separation of key and text -var _keytext_regex: RegEx = RegEx.new() - - -# Constructor -func _init() -> void: - _keytext_regex.compile(KEYTEXT_REGEX) - - # Register the dialog player and load the dialog resources func _ready(): - if !Engine.is_editor_hint(): - escoria.dialog_player = self + if Engine.is_editor_hint(): + return + + escoria.dialog_player = self + + _create_states() + _add_states_to_machine() + + states_map["say"].connect("dialog_manager_set", self, "_on_dialog_manager_set") + + current_state_name = "idle" + START_STATE = states_map[current_state_name] + + initialize(START_STATE) -# Trigger the speedup function in the dialog manager -# -# #### Parameters -# -# - event: The input event -func _input(event): - if event is InputEventMouseButton and event.pressed and is_speaking: - speedup() - get_tree().set_input_as_handled() +# Creates the states for this state machine. +func _create_states() -> void: + states_map = { + "idle": DialogIdle.new(), + "say": DialogSay.new(), + "say_fast": DialogSayFast.new(), + "visible": DialogVisible.new(), + "finish": DialogFinish.new(), + "interrupt": DialogInterrupt.new(), + "choices": DialogChoices.new(), + } + + # This state needs a reference to this class. + states_map["finish"].initialize(self) -# 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 = Directory.new() - if _dir.open(start) == OK: - _dir.list_dir_begin(true, true) - var file_name = _dir.get_next() - while file_name != "": - if _dir.current_is_dir(): - var _voice_file = _get_voice_file( - key, - start.plus_file(file_name) - ) - if _voice_file != "": - return _voice_file - else: - if file_name == "%s.%s.import" % [ - key, - ESCProjectSettingsManager.get_setting( - ESCProjectSettingsManager.SPEECH_EXTENSION - ) - ]: - return start.plus_file(file_name.trim_suffix(".import")) - file_name = _dir.get_next() - return "" +# 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]) -# Make a character say a text +# Make a character say some text # # #### Parameters # @@ -97,81 +66,13 @@ func _get_voice_file(key: String, start: String = "") -> String: # - type: UI to use for the dialog # - text: Text to say func say(character: String, type: String, text: String) -> void: - if type == "": - type = ESCProjectSettingsManager.get_setting( - ESCProjectSettingsManager.DEFAULT_DIALOG_TYPE - ) - is_speaking = true - for _manager_class in ESCProjectSettingsManager.get_setting( - ESCProjectSettingsManager.DIALOG_MANAGERS - ): - if ResourceLoader.exists(_manager_class): - var _manager: ESCDialogManager = load(_manager_class).new() - if _manager.has_type(type): - _dialog_manager = _manager - else: - _dialog_manager = null - - if _dialog_manager == null: - escoria.logger.error( - self, - "No dialog manager called %s configured." % type - ) - - _dialog_manager.connect("say_finished", self, "_on_say_finished", [], CONNECT_ONESHOT) - - var matches = _keytext_regex.search(text) - if not matches: - escoria.logger.error( - self, - "Unexpected text encountered : %s." % text - ) - var key = matches.get_string("key") - if matches.get_string("key") != "": - var _speech_resource = _get_voice_file( - matches.get_string("key") - ) - if _speech_resource == "": - escoria.logger.warn( - self, - "Unable to find voice file with key '%s'." % matches.get_string("key") - ) - else: - ( - escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\ - as ESCSpeechPlayer - ).set_state(_speech_resource) - - var translated_text: String = tr(matches.get_string("key")) - - # Only update the text if the translated text was found; otherwise, raise - # a warning and use the original, untranslated text. - if translated_text == matches.get_string("key"): - escoria.logger.warn( - self, - "Unable to find translation key '%s'. Using untranslated text." % matches.get_string("key") - ) - text = matches.get_string("text") - else: - text = translated_text - else: - text = matches.get_string("text") + states_map["say"].initialize(character, type, text) + _change_state("say") - _dialog_manager.say(self, character, text, type) - -# Handles the end of a say function after it has emitted say_finished. -func _on_say_finished(): - is_speaking = false - emit_signal("say_finished") - - -# Called when a dialog line is skipped +# Called when a dialog line is to be sped up. func speedup() -> void: - if is_speaking and escoria.inputs_manager.input_mode != \ - escoria.inputs_manager.INPUT_NONE and \ - _dialog_manager != null: - _dialog_manager.speedup() + _change_state("say_fast") # Display a list of choices @@ -180,34 +81,20 @@ func speedup() -> void: # # - dialog: The dialog to start func start_dialog_choices(dialog: ESCDialog, type: String = "simple"): - if dialog.options.empty(): - escoria.logger.error( - self, - "Received dialog options array was empty." - ) - - var _dialog_chooser_ui: ESCDialogManager = null - - for _manager_class in ESCProjectSettingsManager.get_setting( - ESCProjectSettingsManager.DIALOG_MANAGERS - ): - if ResourceLoader.exists(_manager_class): - var _manager: ESCDialogManager = load(_manager_class).new() - if _manager.has_chooser_type(type): - _dialog_chooser_ui = _manager - - if _dialog_chooser_ui == null: - escoria.logger.error( - self, - "No dialog manager supports the chooser type %s." % type - ) - - _dialog_chooser_ui.choose(self, dialog) - var option = yield(_dialog_chooser_ui, "option_chosen") - emit_signal("option_chosen", option) + states_map["choices"].initialize(self, dialog, type) + _change_state("choices") # Interrupt the currently running dialog -func interrupt(): - if _dialog_manager != null: - _dialog_manager.interrupt() +func interrupt() -> void: + _change_state("interrupt") + + +# Since the dialog manager is determined when a `say` command is performed and +# other states need to know which one was picked, we notify the necessary states +# via this method. +func _on_dialog_manager_set(dialog_manager: ESCDialogManager) -> void: + _dialog_manager = dialog_manager + states_map["say_fast"].initialize(dialog_manager) + states_map["visible"].initialize(dialog_manager) + states_map["interrupt"].initialize(dialog_manager) diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_choices.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_choices.gd new file mode 100644 index 00000000..80fd5ff9 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_choices.gd @@ -0,0 +1,58 @@ +extends State +class_name DialogChoices + + +# 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: ESCDialog, type: String) -> void: + _dialog_player = dialog_player + _dialog = dialog + _type = type + + +func enter(): + escoria.logger.trace(self, "Dialog State Machine: Entered 'choices'.") + + if _dialog.options.empty(): + escoria.logger.error( + self, + "Received dialog options array was empty." + ) + + for _manager_class in ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.DIALOG_MANAGERS + ): + if ResourceLoader.exists(_manager_class): + var _manager: ESCDialogManager = load(_manager_class).new() + if _manager.has_chooser_type(_type): + _dialog_chooser_ui = _manager + + if _dialog_chooser_ui == null: + escoria.logger.error( + self, + "No dialog manager supports the chooser type %s." % _type + ) + + _ready_to_choose = true + + +func update(_delta): + if _ready_to_choose: + _ready_to_choose = false + _dialog_chooser_ui.choose(self, _dialog) + var option = yield(_dialog_chooser_ui, "option_chosen") + + escoria.logger.trace(self, "Dialog State Machine: 'choices' -> 'idle'") + + emit_signal("finished", "idle") + _dialog_player.emit_signal("option_chosen", option) diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_finish.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_finish.gd new file mode 100644 index 00000000..996b6938 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_finish.gd @@ -0,0 +1,20 @@ +extends State +class_name DialogFinish + + +# 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'") + emit_signal("finished", "idle") + _dialog_player.emit_signal("say_finished") diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_idle.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_idle.gd new file mode 100644 index 00000000..caa41d73 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_idle.gd @@ -0,0 +1,6 @@ +extends State +class_name DialogIdle + + +func enter(): + escoria.logger.trace(self, "Dialog State Machine: Entered 'idle'.") diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_interrupt.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_interrupt.gd new file mode 100644 index 00000000..cf0ae2d8 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_interrupt.gd @@ -0,0 +1,26 @@ +extends State +class_name DialogInterrupt + + +# 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 not _dialog_manager.is_connected("say_finished", self, "_on_say_finished"): + _dialog_manager.connect("say_finished", self, "_on_say_finished", [], CONNECT_ONESHOT) + + if _dialog_manager != null: + _dialog_manager.interrupt() + + +func _on_say_finished() -> void: + escoria.logger.trace(self, "Dialog State Machine: 'interrupt' -> 'finish'") + emit_signal("finished", "finish") + diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say.gd new file mode 100644 index 00000000..b0452433 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say.gd @@ -0,0 +1,177 @@ +extends State +class_name DialogSay + + +signal dialog_manager_set(dialog_manager) + + +# A regular expression that separates the translation key from the text +const KEYTEXT_REGEX = "^((?[^:]+):)?\"(?.+)\"" + + +# 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 + +# Text to say +var _text: String + +# Regular expression object for the separation of key and text +var _keytext_regex: RegEx = RegEx.new() + +var _ready_to_say: bool + + +# Constructor +func _init() -> void: + _keytext_regex.compile(KEYTEXT_REGEX) + + +func initialize(character: String, type: String, text: String) -> void: + _character = character + _type = type + _text = text + + +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: + + if _dialog_manager.is_connected("say_visible", self, "_on_say_visible"): + _dialog_manager.disconnect("say_visible", self, "_on_say_visible") + + escoria.logger.trace(self, "Dialog State Machine: 'say' -> 'say_fast'") + + emit_signal("finished", "say_fast") + get_tree().set_input_as_handled() + + +func enter(): + escoria.logger.trace(self, "Dialog State Machine: Entered 'say'.") + + if _type == "": + _type = ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.DEFAULT_DIALOG_TYPE + ) + + var dialog_manager: ESCDialogManager = null + + for _manager_class in ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.DIALOG_MANAGERS + ): + if ResourceLoader.exists(_manager_class): + var _manager: ESCDialogManager = load(_manager_class).new() + if _manager.has_type(_type): + dialog_manager = _manager + else: + dialog_manager = null + + if dialog_manager == null: + escoria.logger.error( + self, + "No dialog manager called '%s' configured." % _type + ) + + _dialog_manager = dialog_manager + emit_signal("dialog_manager_set", dialog_manager) + + if not _dialog_manager.is_connected("say_visible", self, "_on_say_visible"): + _dialog_manager.connect("say_visible", self, "_on_say_visible", [], CONNECT_ONESHOT) + + var matches = _keytext_regex.search(_text) + + if not matches: + escoria.logger.error( + self, + "Unexpected text encountered: %s." % _text + ) + + var key = matches.get_string("key") + + if matches.get_string("key") != "": + var _speech_resource = _get_voice_file( + matches.get_string("key") + ) + + if _speech_resource == "": + escoria.logger.warn( + self, + "Unable to find voice file with key '%s'." % matches.get_string("key") + ) + else: + ( + escoria.object_manager.get_object(escoria.object_manager.SPEECH).node\ + as ESCSpeechPlayer + ).set_state(_speech_resource) + + var translated_text: String = tr(matches.get_string("key")) + + # Only update the text if the translated text was found; otherwise, raise + # a warning and use the original, untranslated text. + if translated_text == matches.get_string("key"): + escoria.logger.warn( + self, + "Unable to find translation key '%s'. Using untranslated text." % matches.get_string("key") + ) + _text = matches.get_string("text") + else: + _text = translated_text + else: + _text = matches.get_string("text") + + _ready_to_say = true + + +func update(_delta): + if _ready_to_say: + _dialog_manager.say(self, _character, _text, _type) + _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 = Directory.new() + if _dir.open(start) == OK: + _dir.list_dir_begin(true, true) + var file_name = _dir.get_next() + while file_name != "": + if _dir.current_is_dir(): + var _voice_file = _get_voice_file( + key, + start.plus_file(file_name) + ) + if _voice_file != "": + return _voice_file + else: + if file_name == "%s.%s.import" % [ + key, + ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.SPEECH_EXTENSION + ) + ]: + return start.plus_file(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'") + emit_signal("finished", "visible") diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say_fast.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say_fast.gd new file mode 100644 index 00000000..82e801f6 --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say_fast.gd @@ -0,0 +1,30 @@ +extends State +class_name DialogSayFast + + +# 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.is_connected("say_visible", self, "_on_say_visible"): + _dialog_manager.connect("say_visible", self, "_on_say_visible", [], CONNECT_ONESHOT) + + _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'") + emit_signal("finished", "visible") diff --git a/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_visible.gd b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_visible.gd new file mode 100644 index 00000000..53897e3d --- /dev/null +++ b/addons/escoria-core/game/scenes/dialogs/state_machine/dialog_visible.gd @@ -0,0 +1,35 @@ +extends State +class_name DialogVisible + + +# 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.is_connected("say_finished", self, "_on_say_finished"): + _dialog_manager.connect("say_finished", self, "_on_say_finished", [], CONNECT_ONESHOT) + + +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.is_connected("say_finished", self, "_on_say_finished"): + _dialog_manager.disconnect("say_finished", self, "_on_say_finished") + + emit_signal("finished", "interrupt") + get_tree().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'") + emit_signal("finished", "finish") diff --git a/addons/escoria-core/patterns/state_machine/state.gd b/addons/escoria-core/patterns/state_machine/state.gd new file mode 100644 index 00000000..7e0c0ba5 --- /dev/null +++ b/addons/escoria-core/patterns/state_machine/state.gd @@ -0,0 +1,32 @@ +""" +Base interface for all states: it doesn't do anything in itself +but forces us to pass the right arguments to the methods below +and makes sure every State object had all of these methods. +""" +extends Node +class_name State + + +signal finished(next_state_name) + + +# Initialize the state. E.g. change the animation +func enter(): + return + + +# Clean up the state. Reinitialize values like a timer +func exit(): + return + + +func handle_input(_event): + return + + +func update(_delta): + return + + +func _on_animation_finished(_anim_name): + return diff --git a/addons/escoria-core/patterns/state_machine/state_machine.gd b/addons/escoria-core/patterns/state_machine/state_machine.gd new file mode 100644 index 00000000..13ff0757 --- /dev/null +++ b/addons/escoria-core/patterns/state_machine/state_machine.gd @@ -0,0 +1,93 @@ +""" +Base interface for a generic state machine +It handles initializing, setting the machine active or not +delegating _physics_process, _input calls to the State nodes, +and changing the current/active state. +""" +extends Node +class_name StateMachine + + +signal state_changed(current_state) + + +""" +You must set a starting node from the inspector or on +the node that inherits from this state machine interface +If you don't the game will crash (on purpose, so you won't +forget to initialize the state machine) +""" +export(NodePath) var START_STATE +var states_map = {} + +var states_stack = [] # can also be used as a pushdown automaton +var current_state = null +var current_state_name = "" +var _active = false setget set_active + + +func initialize(start_state): + for child in get_children(): + child.connect("finished", self, "_change_state") + + set_active(true) + states_stack.push_front(start_state) + current_state = states_stack[0] + current_state.enter() + + +func set_active(value): + _active = value + set_physics_process(value) + set_process_input(value) + if not _active: + states_stack = [] + current_state = null + + +func _input(event): + current_state.handle_input(event) + + +func _physics_process(delta): + current_state.update(delta) + + +func _on_animation_finished(anim_name): + if not _active: + return + current_state._on_animation_finished(anim_name) + + +func _change_state(state_name): + if not _active: + return + + escoria.logger.trace( + self, + "Dialog State Machine: Changing state from '%s' to '%s'." % [current_state_name, state_name] + ) + + current_state.exit() + + if state_name == "previous": + states_stack.pop_front() + else: + states_stack[0] = states_map[state_name] + + current_state = states_stack[0] + + emit_signal("state_changed", current_state) + + #if state_name != "previous": + current_state.enter() + + current_state_name = state_name + + +func get_current_state_name(): + for key in states_map.keys(): + if states_map[key] == current_state: + return key + + return null diff --git a/addons/escoria-dialog-simple/esc_dialog_simple.gd b/addons/escoria-dialog-simple/esc_dialog_simple.gd index 0d75eb81..36db710c 100644 --- a/addons/escoria-dialog-simple/esc_dialog_simple.gd +++ b/addons/escoria-dialog-simple/esc_dialog_simple.gd @@ -41,6 +41,7 @@ func has_chooser_type(type: String) -> bool: # - type: Type of dialog box to use func say(dialog_player: Node, global_id: String, text: String, type: String): _dialog_player = dialog_player + if type == "floating": _type_player = preload(\ "res://addons/escoria-dialog-simple/types/floating.tscn"\ @@ -51,6 +52,7 @@ func say(dialog_player: Node, global_id: String, text: String, type: String): ).instance() _type_player.connect("say_finished", self, "_on_say_finished", [], CONNECT_ONESHOT) + _type_player.connect("say_visible", self, "_on_say_visible", [], CONNECT_ONESHOT) _dialog_player.add_child(_type_player) _type_player.say(global_id, text) @@ -66,6 +68,10 @@ func _on_say_finished(): emit_signal("say_finished") +func _on_say_visible(): + emit_signal("say_visible") + + # Present an option chooser to the player and sends the signal # `option_chosen` with the chosen dialog option # diff --git a/addons/escoria-dialog-simple/types/avatar.gd b/addons/escoria-dialog-simple/types/avatar.gd index ada8581a..5f030734 100644 --- a/addons/escoria-dialog-simple/types/avatar.gd +++ b/addons/escoria-dialog-simple/types/avatar.gd @@ -5,6 +5,9 @@ extends Popup # 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_speed_per_character @@ -122,6 +125,7 @@ func _on_dialog_line_typed(object, key): text_node.visible_characters = -1 $Timer.start(time_to_disappear) $Timer.connect("timeout", self, "_on_dialog_finished") + emit_signal("say_visible") func _calculate_time_to_disappear() -> float: diff --git a/addons/escoria-dialog-simple/types/floating.gd b/addons/escoria-dialog-simple/types/floating.gd index c8ee7c8f..857d74e8 100644 --- a/addons/escoria-dialog-simple/types/floating.gd +++ b/addons/escoria-dialog-simple/types/floating.gd @@ -5,6 +5,9 @@ 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_speed_per_character @@ -54,6 +57,8 @@ func _ready(): bbcode_enabled = true $Tween.connect("tween_completed", self, "_on_dialog_line_typed") + connect("tree_exiting", self, "_on_tree_exiting") + escoria.connect("paused", self, "_on_paused") escoria.connect("resumed", self, "_on_resumed") @@ -145,6 +150,7 @@ func _on_dialog_line_typed(object, key): text_node.visible_characters = -1 $Timer.start(time_to_disappear) $Timer.connect("timeout", self, "_on_dialog_finished") + emit_signal("say_visible") func _calculate_time_to_disappear() -> float: @@ -157,9 +163,7 @@ func _get_number_of_words() -> int: # Ending the dialog func _on_dialog_finished(): - # 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() + _stop_character_talking() emit_signal("say_finished") @@ -175,3 +179,14 @@ func _on_resumed(): if not tween.is_active(): is_paused = false tween.resume_all() + + + # 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() diff --git a/project.godot b/project.godot index 6b48cbd7..e4da912f 100644 --- a/project.godot +++ b/project.godot @@ -104,6 +104,41 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd" }, { +"base": "State", +"class": "DialogChoices", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_choices.gd" +}, { +"base": "State", +"class": "DialogFinish", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_finish.gd" +}, { +"base": "State", +"class": "DialogIdle", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_idle.gd" +}, { +"base": "State", +"class": "DialogInterrupt", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_interrupt.gd" +}, { +"base": "State", +"class": "DialogSay", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say.gd" +}, { +"base": "State", +"class": "DialogSayFast", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_say_fast.gd" +}, { +"base": "State", +"class": "DialogVisible", +"language": "GDScript", +"path": "res://addons/escoria-core/game/scenes/dialogs/state_machine/dialog_visible.gd" +}, { "base": "Resource", "class": "ESCActionManager", "language": "GDScript", @@ -194,7 +229,7 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd" }, { -"base": "Node", +"base": "StateMachine", "class": "ESCDialogPlayer", "language": "GDScript", "path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd" @@ -574,6 +609,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/escoria-core/game/core-scripts/esc/commands/spawn.gd" }, { +"base": "Node", +"class": "State", +"language": "GDScript", +"path": "res://addons/escoria-core/patterns/state_machine/state.gd" +}, { +"base": "Node", +"class": "StateMachine", +"language": "GDScript", +"path": "res://addons/escoria-core/patterns/state_machine/state_machine.gd" +}, { "base": "ESCBaseCommand", "class": "StopCommand", "language": "GDScript", @@ -649,6 +694,13 @@ _global_script_class_icons={ "ChangeSceneCommand": "", "CustomCommand": "", "DecGlobalCommand": "", +"DialogChoices": "", +"DialogFinish": "", +"DialogIdle": "", +"DialogInterrupt": "", +"DialogSay": "", +"DialogSayFast": "", +"DialogVisible": "", "ESCActionManager": "", "ESCAnimationName": "", "ESCAnimationPlayer": "", @@ -743,6 +795,8 @@ _global_script_class_icons={ "SlideBlockCommand": "", "SlideCommand": "", "SpawnCommand": "", +"State": "", +"StateMachine": "", "StopCommand": "", "StopSndCommand": "", "TeleportCommand": "",