feat: allows for the skipping of dialogue after the text is fully visible; still need to look at integrating with user options
This commit is contained in:
committed by
Julian Murgia
parent
886f402395
commit
bd2f28214b
@@ -6,6 +6,9 @@ class_name ESCDialogManager
|
|||||||
# Emitted when the say function has completed showing the text
|
# Emitted when the say function has completed showing the text
|
||||||
signal say_finished
|
signal say_finished
|
||||||
|
|
||||||
|
# Emitted when text has just become fully visible
|
||||||
|
signal say_visible
|
||||||
|
|
||||||
# Emitted when the player has chosen an option
|
# Emitted when the player has chosen an option
|
||||||
signal option_chosen(option)
|
signal option_chosen(option)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# Escoria dialog player
|
# Escoria dialog player
|
||||||
extends Node
|
extends StateMachine
|
||||||
class_name ESCDialogPlayer
|
class_name ESCDialogPlayer
|
||||||
|
|
||||||
|
|
||||||
# A regular expression that separates the translation key from the text
|
|
||||||
const KEYTEXT_REGEX = "^((?<key>[^:]+):)?\"(?<text>.+)\""
|
|
||||||
|
|
||||||
|
|
||||||
# Emitted when an answer as chosem
|
# Emitted when an answer as chosem
|
||||||
#
|
#
|
||||||
# ##### Parameters
|
# ##### Parameters
|
||||||
@@ -18,78 +14,51 @@ signal option_chosen(option)
|
|||||||
signal say_finished
|
signal say_finished
|
||||||
|
|
||||||
|
|
||||||
# Whether the player is currently speaking
|
|
||||||
var is_speaking: bool = false
|
|
||||||
|
|
||||||
|
|
||||||
# Reference to the currently playing dialog manager
|
# Reference to the currently playing dialog manager
|
||||||
var _dialog_manager: ESCDialogManager = null
|
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
|
# Register the dialog player and load the dialog resources
|
||||||
func _ready():
|
func _ready():
|
||||||
if !Engine.is_editor_hint():
|
if Engine.is_editor_hint():
|
||||||
escoria.dialog_player = self
|
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
|
# Creates the states for this state machine.
|
||||||
#
|
func _create_states() -> void:
|
||||||
# #### Parameters
|
states_map = {
|
||||||
#
|
"idle": DialogIdle.new(),
|
||||||
# - event: The input event
|
"say": DialogSay.new(),
|
||||||
func _input(event):
|
"say_fast": DialogSayFast.new(),
|
||||||
if event is InputEventMouseButton and event.pressed and is_speaking:
|
"visible": DialogVisible.new(),
|
||||||
speedup()
|
"finish": DialogFinish.new(),
|
||||||
get_tree().set_input_as_handled()
|
"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
|
# Adds any created states into the state machine as children.
|
||||||
#
|
func _add_states_to_machine() -> void:
|
||||||
# #### Parameters
|
for key in states_map:
|
||||||
#
|
add_child(states_map[key])
|
||||||
# - 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 ""
|
|
||||||
|
|
||||||
|
|
||||||
# Make a character say a text
|
# Make a character say some text
|
||||||
#
|
#
|
||||||
# #### Parameters
|
# #### Parameters
|
||||||
#
|
#
|
||||||
@@ -97,81 +66,13 @@ func _get_voice_file(key: String, start: String = "") -> String:
|
|||||||
# - type: UI to use for the dialog
|
# - type: UI to use for the dialog
|
||||||
# - text: Text to say
|
# - text: Text to say
|
||||||
func say(character: String, type: String, text: String) -> void:
|
func say(character: String, type: String, text: String) -> void:
|
||||||
if type == "":
|
states_map["say"].initialize(character, type, text)
|
||||||
type = ESCProjectSettingsManager.get_setting(
|
_change_state("say")
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
_dialog_manager.say(self, character, text, type)
|
# Called when a dialog line is to be sped up.
|
||||||
|
|
||||||
# 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
|
|
||||||
func speedup() -> void:
|
func speedup() -> void:
|
||||||
if is_speaking and escoria.inputs_manager.input_mode != \
|
_change_state("say_fast")
|
||||||
escoria.inputs_manager.INPUT_NONE and \
|
|
||||||
_dialog_manager != null:
|
|
||||||
_dialog_manager.speedup()
|
|
||||||
|
|
||||||
|
|
||||||
# Display a list of choices
|
# Display a list of choices
|
||||||
@@ -180,34 +81,20 @@ func speedup() -> void:
|
|||||||
#
|
#
|
||||||
# - dialog: The dialog to start
|
# - dialog: The dialog to start
|
||||||
func start_dialog_choices(dialog: ESCDialog, type: String = "simple"):
|
func start_dialog_choices(dialog: ESCDialog, type: String = "simple"):
|
||||||
if dialog.options.empty():
|
states_map["choices"].initialize(self, dialog, type)
|
||||||
escoria.logger.error(
|
_change_state("choices")
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Interrupt the currently running dialog
|
# Interrupt the currently running dialog
|
||||||
func interrupt():
|
func interrupt() -> void:
|
||||||
if _dialog_manager != null:
|
_change_state("interrupt")
|
||||||
_dialog_manager.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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends State
|
||||||
|
class_name DialogIdle
|
||||||
|
|
||||||
|
|
||||||
|
func enter():
|
||||||
|
escoria.logger.trace(self, "Dialog State Machine: Entered 'idle'.")
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -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 = "^((?<key>[^:]+):)?\"(?<text>.+)\""
|
||||||
|
|
||||||
|
|
||||||
|
# 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")
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
32
addons/escoria-core/patterns/state_machine/state.gd
Normal file
32
addons/escoria-core/patterns/state_machine/state.gd
Normal file
@@ -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
|
||||||
93
addons/escoria-core/patterns/state_machine/state_machine.gd
Normal file
93
addons/escoria-core/patterns/state_machine/state_machine.gd
Normal file
@@ -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
|
||||||
@@ -41,6 +41,7 @@ func has_chooser_type(type: String) -> bool:
|
|||||||
# - type: Type of dialog box to use
|
# - type: Type of dialog box to use
|
||||||
func say(dialog_player: Node, global_id: String, text: String, type: String):
|
func say(dialog_player: Node, global_id: String, text: String, type: String):
|
||||||
_dialog_player = dialog_player
|
_dialog_player = dialog_player
|
||||||
|
|
||||||
if type == "floating":
|
if type == "floating":
|
||||||
_type_player = preload(\
|
_type_player = preload(\
|
||||||
"res://addons/escoria-dialog-simple/types/floating.tscn"\
|
"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()
|
).instance()
|
||||||
|
|
||||||
_type_player.connect("say_finished", self, "_on_say_finished", [], CONNECT_ONESHOT)
|
_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)
|
_dialog_player.add_child(_type_player)
|
||||||
_type_player.say(global_id, text)
|
_type_player.say(global_id, text)
|
||||||
@@ -66,6 +68,10 @@ func _on_say_finished():
|
|||||||
emit_signal("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
|
# Present an option chooser to the player and sends the signal
|
||||||
# `option_chosen` with the chosen dialog option
|
# `option_chosen` with the chosen dialog option
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ extends Popup
|
|||||||
# Signal emitted when text has been said
|
# Signal emitted when text has been said
|
||||||
signal say_finished
|
signal say_finished
|
||||||
|
|
||||||
|
# Signal emitted when text has just become fully visible
|
||||||
|
signal say_visible
|
||||||
|
|
||||||
|
|
||||||
# The text speed per character for normal display
|
# The text speed per character for normal display
|
||||||
var _text_speed_per_character
|
var _text_speed_per_character
|
||||||
@@ -122,6 +125,7 @@ func _on_dialog_line_typed(object, key):
|
|||||||
text_node.visible_characters = -1
|
text_node.visible_characters = -1
|
||||||
$Timer.start(time_to_disappear)
|
$Timer.start(time_to_disappear)
|
||||||
$Timer.connect("timeout", self, "_on_dialog_finished")
|
$Timer.connect("timeout", self, "_on_dialog_finished")
|
||||||
|
emit_signal("say_visible")
|
||||||
|
|
||||||
|
|
||||||
func _calculate_time_to_disappear() -> float:
|
func _calculate_time_to_disappear() -> float:
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ extends RichTextLabel
|
|||||||
# Signal emitted when text has been said
|
# Signal emitted when text has been said
|
||||||
signal say_finished
|
signal say_finished
|
||||||
|
|
||||||
|
# Signal emitted when text has just become fully visible
|
||||||
|
signal say_visible
|
||||||
|
|
||||||
|
|
||||||
# The text speed per character for normal display
|
# The text speed per character for normal display
|
||||||
var _text_speed_per_character
|
var _text_speed_per_character
|
||||||
@@ -54,6 +57,8 @@ func _ready():
|
|||||||
bbcode_enabled = true
|
bbcode_enabled = true
|
||||||
$Tween.connect("tween_completed", self, "_on_dialog_line_typed")
|
$Tween.connect("tween_completed", self, "_on_dialog_line_typed")
|
||||||
|
|
||||||
|
connect("tree_exiting", self, "_on_tree_exiting")
|
||||||
|
|
||||||
escoria.connect("paused", self, "_on_paused")
|
escoria.connect("paused", self, "_on_paused")
|
||||||
escoria.connect("resumed", self, "_on_resumed")
|
escoria.connect("resumed", self, "_on_resumed")
|
||||||
|
|
||||||
@@ -145,6 +150,7 @@ func _on_dialog_line_typed(object, key):
|
|||||||
text_node.visible_characters = -1
|
text_node.visible_characters = -1
|
||||||
$Timer.start(time_to_disappear)
|
$Timer.start(time_to_disappear)
|
||||||
$Timer.connect("timeout", self, "_on_dialog_finished")
|
$Timer.connect("timeout", self, "_on_dialog_finished")
|
||||||
|
emit_signal("say_visible")
|
||||||
|
|
||||||
|
|
||||||
func _calculate_time_to_disappear() -> float:
|
func _calculate_time_to_disappear() -> float:
|
||||||
@@ -157,9 +163,7 @@ func _get_number_of_words() -> int:
|
|||||||
|
|
||||||
# Ending the dialog
|
# Ending the dialog
|
||||||
func _on_dialog_finished():
|
func _on_dialog_finished():
|
||||||
# Make the speaking item animation stop talking, if it is still alive
|
_stop_character_talking()
|
||||||
if is_instance_valid(_current_character) and _current_character != null:
|
|
||||||
_current_character.stop_talking()
|
|
||||||
emit_signal("say_finished")
|
emit_signal("say_finished")
|
||||||
|
|
||||||
|
|
||||||
@@ -175,3 +179,14 @@ func _on_resumed():
|
|||||||
if not tween.is_active():
|
if not tween.is_active():
|
||||||
is_paused = false
|
is_paused = false
|
||||||
tween.resume_all()
|
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()
|
||||||
|
|||||||
@@ -104,6 +104,41 @@ _global_script_classes=[ {
|
|||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
"path": "res://addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd"
|
"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",
|
"base": "Resource",
|
||||||
"class": "ESCActionManager",
|
"class": "ESCActionManager",
|
||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
@@ -194,7 +229,7 @@ _global_script_classes=[ {
|
|||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
"path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd"
|
"path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd"
|
||||||
}, {
|
}, {
|
||||||
"base": "Node",
|
"base": "StateMachine",
|
||||||
"class": "ESCDialogPlayer",
|
"class": "ESCDialogPlayer",
|
||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
"path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd"
|
"path": "res://addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd"
|
||||||
@@ -574,6 +609,16 @@ _global_script_classes=[ {
|
|||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
"path": "res://addons/escoria-core/game/core-scripts/esc/commands/spawn.gd"
|
"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",
|
"base": "ESCBaseCommand",
|
||||||
"class": "StopCommand",
|
"class": "StopCommand",
|
||||||
"language": "GDScript",
|
"language": "GDScript",
|
||||||
@@ -649,6 +694,13 @@ _global_script_class_icons={
|
|||||||
"ChangeSceneCommand": "",
|
"ChangeSceneCommand": "",
|
||||||
"CustomCommand": "",
|
"CustomCommand": "",
|
||||||
"DecGlobalCommand": "",
|
"DecGlobalCommand": "",
|
||||||
|
"DialogChoices": "",
|
||||||
|
"DialogFinish": "",
|
||||||
|
"DialogIdle": "",
|
||||||
|
"DialogInterrupt": "",
|
||||||
|
"DialogSay": "",
|
||||||
|
"DialogSayFast": "",
|
||||||
|
"DialogVisible": "",
|
||||||
"ESCActionManager": "",
|
"ESCActionManager": "",
|
||||||
"ESCAnimationName": "",
|
"ESCAnimationName": "",
|
||||||
"ESCAnimationPlayer": "",
|
"ESCAnimationPlayer": "",
|
||||||
@@ -743,6 +795,8 @@ _global_script_class_icons={
|
|||||||
"SlideBlockCommand": "",
|
"SlideBlockCommand": "",
|
||||||
"SlideCommand": "",
|
"SlideCommand": "",
|
||||||
"SpawnCommand": "",
|
"SpawnCommand": "",
|
||||||
|
"State": "",
|
||||||
|
"StateMachine": "",
|
||||||
"StopCommand": "",
|
"StopCommand": "",
|
||||||
"StopSndCommand": "",
|
"StopSndCommand": "",
|
||||||
"TeleportCommand": "",
|
"TeleportCommand": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user