From dde1d18bc5f83e566a08b4ec0deee05d004c37a7 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Tue, 28 May 2024 14:28:38 -0700 Subject: [PATCH] chore: Update dialogue manager version --- Assets/Dialogue/books.dialogue.import | 2 +- Assets/Dialogue/clone-machine.dialogue.import | 2 +- Assets/Dialogue/doc.dialogue.import | 2 +- Dialogue/snus-dealer.dialogue.import | 2 +- addons/dialogue_manager/DialogueManager.cs | 7 + .../dialogue_manager/components/code_edit.gd | 28 +- .../code_edit_syntax_highlighter.gd | 8 +- addons/dialogue_manager/components/parser.gd | 23 +- addons/dialogue_manager/dialogue_manager.gd | 1904 +++++++++-------- .../dialogue_reponses_menu.gd | 40 +- .../example_balloon/ExampleBalloon.cs | 15 + .../example_balloon/example_balloon.gd | 14 +- addons/dialogue_manager/import_plugin.gd | 2 +- addons/dialogue_manager/l10n/zh.po | 49 +- addons/dialogue_manager/l10n/zh_TW.po | 45 +- addons/dialogue_manager/plugin.cfg | 2 +- addons/dialogue_manager/plugin.gd | 74 + addons/dialogue_manager/settings.gd | 6 +- addons/dialogue_manager/utilities/builtins.gd | 3 +- addons/dialogue_manager/views/main_view.gd | 23 +- 20 files changed, 1233 insertions(+), 1018 deletions(-) diff --git a/Assets/Dialogue/books.dialogue.import b/Assets/Dialogue/books.dialogue.import index a2928b8..fd22018 100644 --- a/Assets/Dialogue/books.dialogue.import +++ b/Assets/Dialogue/books.dialogue.import @@ -1,6 +1,6 @@ [remap] -importer="dialogue_manager_compiler_11" +importer="dialogue_manager_compiler_12" type="Resource" uid="uid://dilmuoilweoeh" path="res://.godot/imported/books.dialogue-cc272ebae322ae3ca46820dca11a3437.tres" diff --git a/Assets/Dialogue/clone-machine.dialogue.import b/Assets/Dialogue/clone-machine.dialogue.import index 70159e2..629926e 100644 --- a/Assets/Dialogue/clone-machine.dialogue.import +++ b/Assets/Dialogue/clone-machine.dialogue.import @@ -1,6 +1,6 @@ [remap] -importer="dialogue_manager_compiler_11" +importer="dialogue_manager_compiler_12" type="Resource" uid="uid://c2om4y0fm81yr" path="res://.godot/imported/clone-machine.dialogue-8810934a67eacdad52469e9ef5f970fb.tres" diff --git a/Assets/Dialogue/doc.dialogue.import b/Assets/Dialogue/doc.dialogue.import index 68f302f..491bd4e 100644 --- a/Assets/Dialogue/doc.dialogue.import +++ b/Assets/Dialogue/doc.dialogue.import @@ -1,6 +1,6 @@ [remap] -importer="dialogue_manager_compiler_11" +importer="dialogue_manager_compiler_12" type="Resource" uid="uid://dntkvjjr8mrgf" path="res://.godot/imported/doc.dialogue-8f95f6a09d3ac685b71d7e07c49df1c6.tres" diff --git a/Dialogue/snus-dealer.dialogue.import b/Dialogue/snus-dealer.dialogue.import index df1ddd2..bc5cd9b 100644 --- a/Dialogue/snus-dealer.dialogue.import +++ b/Dialogue/snus-dealer.dialogue.import @@ -1,6 +1,6 @@ [remap] -importer="dialogue_manager_compiler_11" +importer="dialogue_manager_compiler_12" type="Resource" uid="uid://c4n7vhoxybu70" path="res://.godot/imported/snus-dealer.dialogue-69dbddee28632f18888364bae03f393d.tres" diff --git a/addons/dialogue_manager/DialogueManager.cs b/addons/dialogue_manager/DialogueManager.cs index ae4f931..74bcb24 100644 --- a/addons/dialogue_manager/DialogueManager.cs +++ b/addons/dialogue_manager/DialogueManager.cs @@ -206,6 +206,13 @@ namespace DialogueManagerRuntime public partial class DialogueLine : RefCounted { + private string id = ""; + public string Id + { + get => id; + set => id = value; + } + private string type = "dialogue"; public string Type { diff --git a/addons/dialogue_manager/components/code_edit.gd b/addons/dialogue_manager/components/code_edit.gd index e57c1af..b921f9a 100644 --- a/addons/dialogue_manager/components/code_edit.gd +++ b/addons/dialogue_manager/components/code_edit.gd @@ -70,26 +70,28 @@ func _ready() -> void: func _gui_input(event: InputEvent) -> void: + # Handle shortcuts that come from the editor if event is InputEventKey and event.is_pressed(): - match event.as_text(): - "Ctrl+Equal", "Command+Equal": - self.font_size += 1 - get_viewport().set_input_as_handled() - "Ctrl+Minus", "Command+Minus": - self.font_size -= 1 - get_viewport().set_input_as_handled() - "Ctrl+0", "Command+0": - self.font_size = theme_overrides.font_size - get_viewport().set_input_as_handled() - "Ctrl+K", "Command+K": + var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event) + match shortcut: + "toggle_comment": toggle_comment() get_viewport().set_input_as_handled() - "Alt+Up": + "move_up": move_line(-1) get_viewport().set_input_as_handled() - "Alt+Down": + "move_down": move_line(1) get_viewport().set_input_as_handled() + "text_size_increase": + self.font_size += 1 + get_viewport().set_input_as_handled() + "text_size_decrease": + self.font_size -= 1 + get_viewport().set_input_as_handled() + "text_size_reset": + self.font_size = theme_overrides.font_size + get_viewport().set_input_as_handled() elif event is InputEventMouse: match event.as_text(): diff --git a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd index fe1b8bd..83dccec 100644 --- a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd +++ b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd @@ -26,11 +26,11 @@ var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$ var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?.*?)\\s*(?:|=)\\s*(?[^\\/]+)$") var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?") var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?[a-zA-Z_][a-zA-Z_0-9]*)(?:(?(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?[^\\]]+)\\]))?\\s*(?(?:\\/|\\*|-|\\+)?=)\\s*(?.*)$") -var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|not|in|null)(?[a-zA-Z_][a-zA-Z_0-9]*)(?:(?(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?[^\\]]+)\\]))?\\s*$") +var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|&&|\\|\\|not|in|null)(?[a-zA-Z_][a-zA-Z_0-9]*)(?:(?(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?[^\\]]+)\\]))?\\s*$") var regex_keyword: RegEx = RegEx.create_from_string("^\\s*(true|false|null)\\s*$") var regex_function: RegEx = RegEx.create_from_string("^\\s*([a-zA-Z_][a-zA-Z_0-9]*\\s*)\\(") var regex_comparison: RegEx = RegEx.create_from_string("^(?.*?)\\s*(?==|>=|<=|<|>|!=)\\s*(?.*)$") -var regex_blogical: RegEx = RegEx.create_from_string("^(?.*?)\\s+(?and|or|in)\\s+(?.*)$") +var regex_blogical: RegEx = RegEx.create_from_string("^(?.*?)\\s+(?and|or|in|&&|\\|\\|)\\s+(?.*)$") var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?not)\\s+(?.*)$") var regex_paren: RegEx = RegEx.create_from_string("\\((?((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)") @@ -186,9 +186,9 @@ func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictio for goto_match in goto_matches: colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color} if "file" in goto_match.names: - colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.members_color} + colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.jumps_color} colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color} + colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.jumps_color} colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color} colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} diff --git a/addons/dialogue_manager/components/parser.gd b/addons/dialogue_manager/components/parser.gd index ad6e3ac..f39a5b0 100644 --- a/addons/dialogue_manager/components/parser.gd +++ b/addons/dialogue_manager/components/parser.gd @@ -43,7 +43,7 @@ var TOKEN_DEFINITIONS: Dictionary = { DialogueConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), DialogueConstants.TOKEN_STRING: RegEx.create_from_string("^(\".*?\"|\'.*?\')"), DialogueConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), - DialogueConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or)( |$)"), + DialogueConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"), DialogueConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*"), DialogueConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"), DialogueConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), @@ -230,6 +230,7 @@ func parse(text: String, path: String) -> Error: line["character"] = first_child.character line["character_replacements"] = first_child.character_replacements line["text"] = first_child.text + line["text_replacements"] = extract_dialogue_replacements(line.text, indent_size + 2) line["translation_key"] = first_child.translation_key parsed_lines[str(id) + ".2"] = first_child line["next_id"] = str(id) + ".2" @@ -691,12 +692,7 @@ func get_line_after_line(id: int, indent_size: int, line: Dictionary) -> String: var next_nonempty_line_id = get_next_nonempty_line_id(id) if next_nonempty_line_id != DialogueConstants.ID_NULL \ and indent_size <= get_indent(raw_lines[next_nonempty_line_id.to_int()]): - # The next line is a title so we need the next nonempty line after that - if is_title_line(raw_lines[next_nonempty_line_id.to_int()]): - return get_next_nonempty_line_id(next_nonempty_line_id.to_int()) - # Otherwise it's a normal line - else: - return next_nonempty_line_id + return next_nonempty_line_id # Otherwise, we grab the ID from the parents next ID after children elif line.has("parent_id") and parsed_lines.has(line.parent_id): return parsed_lines[line.parent_id].next_id_after @@ -927,8 +923,8 @@ func find_next_line_after_responses(line_number: int) -> String: if get_indent(line) <= expected_indent: return str(line_number) - # EOF so must be end of conversation - return DialogueConstants.ID_END_CONVERSATION + # EOF so it's also the end of a block + return DialogueConstants.ID_END ## Get the names of any autoloads in the project @@ -1529,10 +1525,15 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo DialogueConstants.TOKEN_ASSIGNMENT, \ DialogueConstants.TOKEN_OPERATOR, \ DialogueConstants.TOKEN_AND_OR, \ - DialogueConstants.TOKEN_VARIABLE: \ + DialogueConstants.TOKEN_VARIABLE: + var value = token.value.strip_edges() + if value == "&&": + value = "and" + elif value == "||": + value = "or" tree.append({ type = token.type, - value = token.value.strip_edges() + value = value }) DialogueConstants.TOKEN_STRING: diff --git a/addons/dialogue_manager/dialogue_manager.gd b/addons/dialogue_manager/dialogue_manager.gd index 57d7f86..fcdc3c2 100644 --- a/addons/dialogue_manager/dialogue_manager.gd +++ b/addons/dialogue_manager/dialogue_manager.gd @@ -33,16 +33,16 @@ signal bridge_mutated() enum MutationBehaviour { - Wait, - DoNotWait, - Skip + Wait, + DoNotWait, + Skip } enum TranslationSource { - None, - Guess, - CSV, - PO + None, + Guess, + CSV, + PO } @@ -60,10 +60,10 @@ var translation_source: TranslationSource = TranslationSource.Guess ## Used to resolve the current scene. Override if your game manages the current scene itself. var get_current_scene: Callable = func(): - var current_scene: Node = get_tree().current_scene - if current_scene == null: - current_scene = get_tree().root.get_child(get_tree().root.get_child_count() - 1) - return current_scene + var current_scene: Node = get_tree().current_scene + if current_scene == null: + current_scene = get_tree().root.get_child(get_tree().root.get_child_count() - 1) + return current_scene var _has_loaded_autoloads: bool = false var _autoloads: Dictionary = {} @@ -73,251 +73,253 @@ var _node_properties: Array = [] func _ready() -> void: - # Cache the known Node2D properties - _node_properties = ["Script Variables"] - var temp_node: Node2D = Node2D.new() - for property in temp_node.get_property_list(): - _node_properties.append(property.name) - temp_node.free() + # Cache the known Node2D properties + _node_properties = ["Script Variables"] + var temp_node: Node2D = Node2D.new() + for property in temp_node.get_property_list(): + _node_properties.append(property.name) + temp_node.free() - # Make the dialogue manager available as a singleton - if Engine.has_singleton("DialogueManager"): - Engine.unregister_singleton("DialogueManager") - Engine.register_singleton("DialogueManager", self) + # Make the dialogue manager available as a singleton + if Engine.has_singleton("DialogueManager"): + Engine.unregister_singleton("DialogueManager") + Engine.register_singleton("DialogueManager", self) - # Connect up the C# signals if need be - if DialogueSettings.has_dotnet_solution(): - _get_dotnet_dialogue_manager().Prepare() + # Connect up the C# signals if need be + if DialogueSettings.has_dotnet_solution(): + _get_dotnet_dialogue_manager().Prepare() ## Step through lines and run any mutations until we either hit some dialogue or the end of the conversation func get_next_dialogue_line(resource: DialogueResource, key: String = "", extra_game_states: Array = [], mutation_behaviour: MutationBehaviour = MutationBehaviour.Wait) -> DialogueLine: - # You have to provide a valid dialogue resource - if resource == null: - assert(false, DialogueConstants.translate(&"runtime.no_resource")) - if resource.lines.size() == 0: - assert(false, DialogueConstants.translate(&"runtime.no_content").format({ file_path = resource.resource_path })) + # You have to provide a valid dialogue resource + if resource == null: + assert(false, DialogueConstants.translate(&"runtime.no_resource")) + if resource.lines.size() == 0: + assert(false, DialogueConstants.translate(&"runtime.no_content").format({ file_path = resource.resource_path })) - # Inject any "using" states into the game_states - for state_name in resource.using_states: - var autoload = get_tree().root.get_node_or_null(state_name) - if autoload == null: - printerr(DialogueConstants.translate(&"runtime.unknown_autoload").format({ autoload = state_name })) - else: - extra_game_states = [autoload] + extra_game_states + # Inject any "using" states into the game_states + for state_name in resource.using_states: + var autoload = get_tree().root.get_node_or_null(state_name) + if autoload == null: + printerr(DialogueConstants.translate(&"runtime.unknown_autoload").format({ autoload = state_name })) + else: + extra_game_states = [autoload] + extra_game_states - # Get the line data - var dialogue: DialogueLine = await get_line(resource, key, extra_game_states) + # Get the line data + var dialogue: DialogueLine = await get_line(resource, key, extra_game_states) - # If our dialogue is nothing then we hit the end - if not is_valid(dialogue): - (func(): dialogue_ended.emit(resource)).call_deferred() - return null + # If our dialogue is nothing then we hit the end + if not is_valid(dialogue): + (func(): dialogue_ended.emit(resource)).call_deferred() + return null - # Run the mutation if it is one - if dialogue.type == DialogueConstants.TYPE_MUTATION: - var actual_next_id: String = dialogue.next_id.split(",")[0] - match mutation_behaviour: - MutationBehaviour.Wait: - await mutate(dialogue.mutation, extra_game_states) - MutationBehaviour.DoNotWait: - mutate(dialogue.mutation, extra_game_states) - MutationBehaviour.Skip: - pass - if actual_next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]: - # End the conversation - (func(): dialogue_ended.emit(resource)).call_deferred() - return null - else: - return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour) - else: - got_dialogue.emit(dialogue) - return dialogue + # Run the mutation if it is one + if dialogue.type == DialogueConstants.TYPE_MUTATION: + var actual_next_id: String = dialogue.next_id.split(",")[0] + match mutation_behaviour: + MutationBehaviour.Wait: + await mutate(dialogue.mutation, extra_game_states) + MutationBehaviour.DoNotWait: + mutate(dialogue.mutation, extra_game_states) + MutationBehaviour.Skip: + pass + if actual_next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]: + # End the conversation + (func(): dialogue_ended.emit(resource)).call_deferred() + return null + else: + return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour) + else: + got_dialogue.emit(dialogue) + return dialogue func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> ResolvedLineData: - var text: String = translate(data) + var text: String = translate(data) - # Resolve variables - for replacement in data.text_replacements: - var value = await resolve(replacement.expression.duplicate(true), extra_game_states) - var index: int = text.find(replacement.value_in_text) - text = text.substr(0, index) + str(value) + text.substr(index + replacement.value_in_text.length()) + # Resolve variables + for replacement in data.text_replacements: + var value = await resolve(replacement.expression.duplicate(true), extra_game_states) + var index: int = text.find(replacement.value_in_text) + if index > -1: + text = text.substr(0, index) + str(value) + text.substr(index + replacement.value_in_text.length()) - var parser: DialogueManagerParser = DialogueManagerParser.new() + var parser: DialogueManagerParser = DialogueManagerParser.new() - # Resolve random groups - for found in parser.INLINE_RANDOM_REGEX.search_all(text): - var options = found.get_string(&"options").split(&"|") - text = text.replace(&"[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) + # Resolve random groups + for found in parser.INLINE_RANDOM_REGEX.search_all(text): + var options = found.get_string(&"options").split(&"|") + text = text.replace(&"[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) - # Do a pass on the markers to find any conditionals - var markers: ResolvedLineData = parser.extract_markers(text) + # Do a pass on the markers to find any conditionals + var markers: ResolvedLineData = parser.extract_markers(text) - # Resolve any conditionals and update marker positions as needed - if data.type == DialogueConstants.TYPE_DIALOGUE: - var resolved_text: String = markers.text - var conditionals: Array[RegExMatch] = parser.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) - var replacements: Array = [] - for conditional in conditionals: - var condition_raw: String = conditional.strings[conditional.names.condition] - var body: String = conditional.strings[conditional.names.body] - var body_else: String = "" - if &"[else]" in body: - var bits = body.split(&"[else]") - body = bits[0] - body_else = bits[1] - var condition: Dictionary = parser.extract_condition("if " + condition_raw, false, 0) - # If the condition fails then use the else of "" - if not await check_condition({ condition = condition }, extra_game_states): - body = body_else - replacements.append({ - start = conditional.get_start(), - end = conditional.get_end(), - string = conditional.get_string(), - body = body - }) + # Resolve any conditionals and update marker positions as needed + if data.type == DialogueConstants.TYPE_DIALOGUE: + var resolved_text: String = markers.text + var conditionals: Array[RegExMatch] = parser.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) + var replacements: Array = [] + for conditional in conditionals: + var condition_raw: String = conditional.strings[conditional.names.condition] + var body: String = conditional.strings[conditional.names.body] + var body_else: String = "" + if &"[else]" in body: + var bits = body.split(&"[else]") + body = bits[0] + body_else = bits[1] + var condition: Dictionary = parser.extract_condition("if " + condition_raw, false, 0) + # If the condition fails then use the else of "" + if not await check_condition({ condition = condition }, extra_game_states): + body = body_else + replacements.append({ + start = conditional.get_start(), + end = conditional.get_end(), + string = conditional.get_string(), + body = body + }) - for i in range(replacements.size() -1, -1, -1): - var r: Dictionary = replacements[i] - resolved_text = resolved_text.substr(0, r.start) + r.body + resolved_text.substr(r.end, 9999) - # Move any other markers now that the text has changed - var offset: int = r.end - r.start - r.body.length() - for key in [&"pauses", &"speeds", &"time"]: - if markers.get(key) == null: continue - var marker = markers.get(key) - var next_marker: Dictionary = {} - for index in marker: - if index < r.start: - next_marker[index] = marker[index] - elif index > r.start: - next_marker[index - offset] = marker[index] - markers.set(key, next_marker) - var mutations: Array[Array] = markers.mutations - var next_mutations: Array[Array] = [] - for mutation in mutations: - var index = mutation[0] - if index < r.start: - next_mutations.append(mutation) - elif index > r.start: - next_mutations.append([index - offset, mutation[1]]) - markers.mutations = next_mutations + for i in range(replacements.size() -1, -1, -1): + var r: Dictionary = replacements[i] + resolved_text = resolved_text.substr(0, r.start) + r.body + resolved_text.substr(r.end, 9999) + # Move any other markers now that the text has changed + var offset: int = r.end - r.start - r.body.length() + for key in [&"pauses", &"speeds", &"time"]: + if markers.get(key) == null: continue + var marker = markers.get(key) + var next_marker: Dictionary = {} + for index in marker: + if index < r.start: + next_marker[index] = marker[index] + elif index > r.start: + next_marker[index - offset] = marker[index] + markers.set(key, next_marker) + var mutations: Array[Array] = markers.mutations + var next_mutations: Array[Array] = [] + for mutation in mutations: + var index = mutation[0] + if index < r.start: + next_mutations.append(mutation) + elif index > r.start: + next_mutations.append([index - offset, mutation[1]]) + markers.mutations = next_mutations - markers.text = resolved_text + markers.text = resolved_text - parser.free() + parser.free() - return markers + return markers ## Replace any variables, etc in the character name func get_resolved_character(data: Dictionary, extra_game_states: Array = []) -> String: - var character: String = data.get(&"character", "") + var character: String = data.get(&"character", "") - # Resolve variables - for replacement in data.get(&"character_replacements", []): - var value = await resolve(replacement.expression.duplicate(true), extra_game_states) - var index: int = character.find(replacement.value_in_text) - character = character.substr(0, index) + str(value) + character.substr(index + replacement.value_in_text.length()) + # Resolve variables + for replacement in data.get(&"character_replacements", []): + var value = await resolve(replacement.expression.duplicate(true), extra_game_states) + var index: int = character.find(replacement.value_in_text) + if index > -1: + character = character.substr(0, index) + str(value) + character.substr(index + replacement.value_in_text.length()) - # Resolve random groups - var random_regex: RegEx = RegEx.new() - random_regex.compile("\\[\\[(?.*?)\\]\\]") - for found in random_regex.search_all(character): - var options = found.get_string(&"options").split("|") - character = character.replace("[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) + # Resolve random groups + var random_regex: RegEx = RegEx.new() + random_regex.compile("\\[\\[(?.*?)\\]\\]") + for found in random_regex.search_all(character): + var options = found.get_string(&"options").split("|") + character = character.replace("[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) - return character + return character ## Generate a dialogue resource on the fly from some text func create_resource_from_text(text: String) -> Resource: - var parser: DialogueManagerParser = DialogueManagerParser.new() - parser.parse(text, "") - var results: DialogueManagerParseResult = parser.get_data() - var errors: Array[Dictionary] = parser.get_errors() - parser.free() + var parser: DialogueManagerParser = DialogueManagerParser.new() + parser.parse(text, "") + var results: DialogueManagerParseResult = parser.get_data() + var errors: Array[Dictionary] = parser.get_errors() + parser.free() - if errors.size() > 0: - printerr(DialogueConstants.translate(&"runtime.errors").format({ count = errors.size() })) - for error in errors: - printerr(DialogueConstants.translate(&"runtime.error_detail").format({ - line = error.line_number + 1, - message = DialogueConstants.get_error_message(error.error) - })) - assert(false, DialogueConstants.translate(&"runtime.errors_see_details").format({ count = errors.size() })) + if errors.size() > 0: + printerr(DialogueConstants.translate(&"runtime.errors").format({ count = errors.size() })) + for error in errors: + printerr(DialogueConstants.translate(&"runtime.error_detail").format({ + line = error.line_number + 1, + message = DialogueConstants.get_error_message(error.error) + })) + assert(false, DialogueConstants.translate(&"runtime.errors_see_details").format({ count = errors.size() })) - var resource: DialogueResource = DialogueResource.new() - resource.using_states = results.using_states - resource.titles = results.titles - resource.first_title = results.first_title - resource.character_names = results.character_names - resource.lines = results.lines - resource.raw_text = text + var resource: DialogueResource = DialogueResource.new() + resource.using_states = results.using_states + resource.titles = results.titles + resource.first_title = results.first_title + resource.character_names = results.character_names + resource.lines = results.lines + resource.raw_text = text - return resource + return resource ## Show the example balloon func show_example_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> CanvasLayer: - var balloon: Node = load(_get_example_balloon_path()).instantiate() - get_current_scene.call().add_child(balloon) - balloon.start(resource, title, extra_game_states) + var balloon: Node = load(_get_example_balloon_path()).instantiate() + get_current_scene.call().add_child(balloon) + balloon.start(resource, title, extra_game_states) - return balloon + return balloon ## Show the configured dialogue balloon func show_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> Node: - var balloon_path: String = DialogueSettings.get_setting(&"balloon_path", _get_example_balloon_path()) - if not ResourceLoader.exists(balloon_path): - balloon_path = _get_example_balloon_path() - return show_dialogue_balloon_scene(balloon_path, resource, title, extra_game_states) + var balloon_path: String = DialogueSettings.get_setting(&"balloon_path", _get_example_balloon_path()) + if not ResourceLoader.exists(balloon_path): + balloon_path = _get_example_balloon_path() + return show_dialogue_balloon_scene(balloon_path, resource, title, extra_game_states) ## Show a given balloon scene func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> Node: - if balloon_scene is String: - balloon_scene = load(balloon_scene) - if balloon_scene is PackedScene: - balloon_scene = balloon_scene.instantiate() + if balloon_scene is String: + balloon_scene = load(balloon_scene) + if balloon_scene is PackedScene: + balloon_scene = balloon_scene.instantiate() - var balloon: Node = balloon_scene - get_current_scene.call().add_child(balloon) - if balloon.has_method(&"start"): - balloon.start(resource, title, extra_game_states) - elif balloon.has_method(&"Start"): - balloon.Start(resource, title, extra_game_states) - else: - assert(false, DialogueConstants.translate(&"runtime.dialogue_balloon_missing_start_method")) - return balloon + var balloon: Node = balloon_scene + get_current_scene.call().add_child(balloon) + if balloon.has_method(&"start"): + balloon.start(resource, title, extra_game_states) + elif balloon.has_method(&"Start"): + balloon.Start(resource, title, extra_game_states) + else: + assert(false, DialogueConstants.translate(&"runtime.dialogue_balloon_missing_start_method")) + return balloon # Get the path to the example balloon func _get_example_balloon_path() -> String: - var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 - var balloon_path: String = "/example_balloon/small_example_balloon.tscn" if is_small_window else "/example_balloon/example_balloon.tscn" - return get_script().resource_path.get_base_dir() + balloon_path + var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 + var balloon_path: String = "/example_balloon/small_example_balloon.tscn" if is_small_window else "/example_balloon/example_balloon.tscn" + return get_script().resource_path.get_base_dir() + balloon_path ### Dotnet bridge func _get_dotnet_dialogue_manager() -> Node: - return load(get_script().resource_path.get_base_dir() + "/DialogueManager.cs").new() + return load(get_script().resource_path.get_base_dir() + "/DialogueManager.cs").new() func _bridge_get_next_dialogue_line(resource: DialogueResource, key: String, extra_game_states: Array = []) -> void: - # dotnet needs at least one await tick of the signal gets called too quickly - await get_tree().process_frame + # dotnet needs at least one await tick of the signal gets called too quickly + await get_tree().process_frame - var line = await get_next_dialogue_line(resource, key, extra_game_states) - bridge_get_next_dialogue_line_completed.emit(line) + var line = await get_next_dialogue_line(resource, key, extra_game_states) + bridge_get_next_dialogue_line_completed.emit(line) func _bridge_mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: - await mutate(mutation, extra_game_states, is_inline_mutation) - bridge_mutated.emit() + await mutate(mutation, extra_game_states, is_inline_mutation) + bridge_mutated.emit() ### Helpers @@ -325,911 +327,925 @@ func _bridge_mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mu # Get a line by its ID func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine: - key = key.strip_edges() + key = key.strip_edges() - # See if we were given a stack instead of just the one key - var stack: Array = key.split("|") - key = stack.pop_front() - var id_trail: String = "" if stack.size() == 0 else "|" + "|".join(stack) + # See if we were given a stack instead of just the one key + var stack: Array = key.split("|") + key = stack.pop_front() + var id_trail: String = "" if stack.size() == 0 else "|" + "|".join(stack) - # Key is blank so just use the first title - if key == null or key == "": - key = resource.first_title + # Key is blank so just use the first title + if key == null or key == "": + key = resource.first_title - # See if we just ended the conversation - if key in [DialogueConstants.ID_END, DialogueConstants.ID_NULL, null]: - if stack.size() > 0: - return await get_line(resource, "|".join(stack), extra_game_states) - else: - return null - elif key == DialogueConstants.ID_END_CONVERSATION: - return null + # See if we just ended the conversation + if key in [DialogueConstants.ID_END, DialogueConstants.ID_NULL, null]: + if stack.size() > 0: + return await get_line(resource, "|".join(stack), extra_game_states) + else: + return null + elif key == DialogueConstants.ID_END_CONVERSATION: + return null - # See if it is a title - if key.begins_with("~ "): - key = key.substr(2) - if resource.titles.has(key): - key = resource.titles.get(key) + # See if it is a title + if key.begins_with("~ "): + key = key.substr(2) + if resource.titles.has(key): + key = resource.titles.get(key) - if key in resource.titles.values(): - passed_title.emit(resource.titles.find_key(key)) + if key in resource.titles.values(): + passed_title.emit(resource.titles.find_key(key)) - if not resource.lines.has(key): - assert(false, DialogueConstants.translate(&"errors.key_not_found").format({ key = key })) + if not resource.lines.has(key): + assert(false, DialogueConstants.translate(&"errors.key_not_found").format({ key = key })) - var data: Dictionary = resource.lines.get(key) + var data: Dictionary = resource.lines.get(key) - # Check for weighted random lines - if data.has(&"siblings"): - var target_weight: float = randf_range(0, data.siblings.reduce(func(total, sibling): return total + sibling.weight, 0)) - var cummulative_weight: float = 0 - for sibling in data.siblings: - if target_weight < cummulative_weight + sibling.weight: - data = resource.lines.get(sibling.id) - break - else: - cummulative_weight += sibling.weight + # This title key points to another title key so we should jump there instead + if data.type == DialogueConstants.TYPE_TITLE and data.next_id in resource.titles.values(): + return await get_line(resource, data.next_id + id_trail, extra_game_states) - # Check condtiions - if data.type == DialogueConstants.TYPE_CONDITION: - # "else" will have no actual condition - if await check_condition(data, extra_game_states): - return await get_line(resource, data.next_id + id_trail, extra_game_states) - else: - return await get_line(resource, data.next_conditional_id + id_trail, extra_game_states) + # Check for weighted random lines + if data.has(&"siblings"): + var target_weight: float = randf_range(0, data.siblings.reduce(func(total, sibling): return total + sibling.weight, 0)) + var cummulative_weight: float = 0 + for sibling in data.siblings: + if target_weight < cummulative_weight + sibling.weight: + data = resource.lines.get(sibling.id) + break + else: + cummulative_weight += sibling.weight - # Evaluate jumps - elif data.type == DialogueConstants.TYPE_GOTO: - if data.is_snippet: - id_trail = "|" + data.next_id_after + id_trail - return await get_line(resource, data.next_id + id_trail, extra_game_states) + # Check condtiions + if data.type == DialogueConstants.TYPE_CONDITION: + # "else" will have no actual condition + if await check_condition(data, extra_game_states): + return await get_line(resource, data.next_id + id_trail, extra_game_states) + else: + return await get_line(resource, data.next_conditional_id + id_trail, extra_game_states) - elif data.type == DialogueConstants.TYPE_DIALOGUE: - if not data.has(&"id"): - data.id = key + # Evaluate jumps + elif data.type == DialogueConstants.TYPE_GOTO: + if data.is_snippet: + id_trail = "|" + data.next_id_after + id_trail + return await get_line(resource, data.next_id + id_trail, extra_game_states) - # Set up a line object - var line: DialogueLine = await create_dialogue_line(data, extra_game_states) + elif data.type == DialogueConstants.TYPE_DIALOGUE: + if not data.has(&"id"): + data.id = key - # If the jump point somehow has no content then just end - if not line: return null + # Set up a line object + var line: DialogueLine = await create_dialogue_line(data, extra_game_states) - # If we are the first of a list of responses then get the other ones - if data.type == DialogueConstants.TYPE_RESPONSE: - # Note: For some reason C# has occasional issues with using the responses property directly - # so instead we use set and get here. - line.set(&"responses", await get_responses(data.get(&"responses", []), resource, id_trail, extra_game_states)) - return line + # If the jump point somehow has no content then just end + if not line: return null - # Inject the next node's responses if they have any - if resource.lines.has(line.next_id): - var next_line: Dictionary = resource.lines.get(line.next_id) + # If we are the first of a list of responses then get the other ones + if data.type == DialogueConstants.TYPE_RESPONSE: + # Note: For some reason C# has occasional issues with using the responses property directly + # so instead we use set and get here. + line.set(&"responses", await get_responses(data.get(&"responses", []), resource, id_trail, extra_game_states)) + return line - # If the response line is marked as a title then make sure to emit the passed_title signal. - if line.next_id in resource.titles.values(): - passed_title.emit(resource.titles.find_key(line.next_id)) + # Inject the next node's responses if they have any + if resource.lines.has(line.next_id): + var next_line: Dictionary = resource.lines.get(line.next_id) - # If the next line is a title then check where it points to see if that is a set of responses. - if next_line.type == DialogueConstants.TYPE_GOTO and resource.lines.has(next_line.next_id): - next_line = resource.lines.get(next_line.next_id) + # If the response line is marked as a title then make sure to emit the passed_title signal. + if line.next_id in resource.titles.values(): + passed_title.emit(resource.titles.find_key(line.next_id)) - if next_line != null and next_line.type == DialogueConstants.TYPE_RESPONSE: - # Note: For some reason C# has occasional issues with using the responses property directly - # so instead we use set and get here. - line.set(&"responses", await get_responses(next_line.get(&"responses", []), resource, id_trail, extra_game_states)) + # If the next line is a title then check where it points to see if that is a set of responses. + if next_line.type == DialogueConstants.TYPE_GOTO and resource.lines.has(next_line.next_id): + next_line = resource.lines.get(next_line.next_id) - line.next_id = "|".join(stack) if line.next_id == DialogueConstants.ID_NULL else line.next_id + id_trail - return line + if next_line != null and next_line.type == DialogueConstants.TYPE_RESPONSE: + # Note: For some reason C# has occasional issues with using the responses property directly + # so instead we use set and get here. + line.set(&"responses", await get_responses(next_line.get(&"responses", []), resource, id_trail, extra_game_states)) + + line.next_id = "|".join(stack) if line.next_id == DialogueConstants.ID_NULL else line.next_id + id_trail + return line # Show a message or crash with error func show_error_for_missing_state_value(message: String, will_show: bool = true) -> void: - if not will_show: return + if not will_show: return - if DialogueSettings.get_setting(&"ignore_missing_state_values", false): - push_error(message) - elif will_show: - # If you're here then you're missing a method or property in your game state. The error - # message down in the debugger will give you some more information. - assert(false, message) + if DialogueSettings.get_setting(&"ignore_missing_state_values", false): + push_error(message) + elif will_show: + # If you're here then you're missing a method or property in your game state. The error + # message down in the debugger will give you some more information. + assert(false, message) # Translate a string func translate(data: Dictionary) -> String: - if translation_source == TranslationSource.None: - return data.text + if translation_source == TranslationSource.None: + return data.text - if data.translation_key == "" or data.translation_key == data.text: - return tr(data.text) - else: - # Line IDs work slightly differently depending on whether the translation came from a - # CSV or a PO file. CSVs use the line ID (or the line itself) as the translatable string - # whereas POs use the ID as context and the line itself as the translatable string. - match translation_source: - TranslationSource.PO: - return tr(data.text, StringName(data.translation_key)) + if data.translation_key == "" or data.translation_key == data.text: + return tr(data.text) + else: + # Line IDs work slightly differently depending on whether the translation came from a + # CSV or a PO file. CSVs use the line ID (or the line itself) as the translatable string + # whereas POs use the ID as context and the line itself as the translatable string. + match translation_source: + TranslationSource.PO: + return tr(data.text, StringName(data.translation_key)) - TranslationSource.CSV: - return tr(data.translation_key) + TranslationSource.CSV: + return tr(data.translation_key) - TranslationSource.Guess: - var translation_files: Array = ProjectSettings.get_setting(&"internationalization/locale/translations") - if translation_files.filter(func(f: String): return f.get_extension() == &"po").size() > 0: - # Assume PO - return tr(data.text, StringName(data.translation_key)) - else: - # Assume CSV - return tr(data.translation_key) + TranslationSource.Guess: + var translation_files: Array = ProjectSettings.get_setting(&"internationalization/locale/translations") + if translation_files.filter(func(f: String): return f.get_extension() in [&"po", &"mo"]).size() > 0: + # Assume PO + return tr(data.text, StringName(data.translation_key)) + else: + # Assume CSV + return tr(data.translation_key) - return tr(data.translation_key) + return tr(data.translation_key) # Create a line of dialogue func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> DialogueLine: - match data.type: - DialogueConstants.TYPE_DIALOGUE: - var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) - return DialogueLine.new({ - id = data.get(&"id", ""), - type = DialogueConstants.TYPE_DIALOGUE, - next_id = data.next_id, - character = await get_resolved_character(data, extra_game_states), - character_replacements = data.character_replacements, - text = resolved_data.text, - text_replacements = data.text_replacements, - translation_key = data.translation_key, - pauses = resolved_data.pauses, - speeds = resolved_data.speeds, - inline_mutations = resolved_data.mutations, - time = resolved_data.time, - tags = data.get(&"tags", []), - extra_game_states = extra_game_states - }) + match data.type: + DialogueConstants.TYPE_DIALOGUE: + var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DialogueConstants.TYPE_DIALOGUE, + next_id = data.next_id, + character = await get_resolved_character(data, extra_game_states), + character_replacements = data.character_replacements, + text = resolved_data.text, + text_replacements = data.text_replacements, + translation_key = data.translation_key, + pauses = resolved_data.pauses, + speeds = resolved_data.speeds, + inline_mutations = resolved_data.mutations, + time = resolved_data.time, + tags = data.get(&"tags", []), + extra_game_states = extra_game_states + }) - DialogueConstants.TYPE_RESPONSE: - return DialogueLine.new({ - id = data.get(&"id", ""), - type = DialogueConstants.TYPE_RESPONSE, - next_id = data.next_id, - tags = data.get(&"tags", []), - extra_game_states = extra_game_states - }) + DialogueConstants.TYPE_RESPONSE: + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DialogueConstants.TYPE_RESPONSE, + next_id = data.next_id, + tags = data.get(&"tags", []), + extra_game_states = extra_game_states + }) - DialogueConstants.TYPE_MUTATION: - return DialogueLine.new({ - id = data.get(&"id", ""), - type = DialogueConstants.TYPE_MUTATION, - next_id = data.next_id, - mutation = data.mutation, - extra_game_states = extra_game_states - }) + DialogueConstants.TYPE_MUTATION: + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DialogueConstants.TYPE_MUTATION, + next_id = data.next_id, + mutation = data.mutation, + extra_game_states = extra_game_states + }) - return null + return null # Create a response func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResponse: - var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) - return DialogueResponse.new({ - id = data.get(&"id", ""), - type = DialogueConstants.TYPE_RESPONSE, - next_id = data.next_id, - is_allowed = data.is_allowed, - character = await get_resolved_character(data, extra_game_states), - character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), - text = resolved_data.text, - text_replacements = data.text_replacements, - tags = data.get(&"tags", []), - translation_key = data.translation_key - }) + var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) + return DialogueResponse.new({ + id = data.get(&"id", ""), + type = DialogueConstants.TYPE_RESPONSE, + next_id = data.next_id, + is_allowed = data.is_allowed, + character = await get_resolved_character(data, extra_game_states), + character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), + text = resolved_data.text, + text_replacements = data.text_replacements, + tags = data.get(&"tags", []), + translation_key = data.translation_key + }) # Get the current game states func get_game_states(extra_game_states: Array) -> Array: - if not _has_loaded_autoloads: - _has_loaded_autoloads = true - # Add any autoloads to a generic state so we can refer to them by name - for child in get_tree().root.get_children(): - # Ignore the dialogue manager - if child.name == &"DialogueManager": continue - # Ignore the current main scene - if get_tree().current_scene and child.name == get_tree().current_scene.name: continue - # Add the node to our known autoloads - _autoloads[child.name] = child - game_states = [_autoloads] - # Add any other state shortcuts from settings - for node_name in DialogueSettings.get_setting(&"states", []): - var state: Node = get_node_or_null("/root/" + node_name) - if state: - game_states.append(state) + if not _has_loaded_autoloads: + _has_loaded_autoloads = true + # Add any autoloads to a generic state so we can refer to them by name + for child in get_tree().root.get_children(): + # Ignore the dialogue manager + if child.name == &"DialogueManager": continue + # Ignore the current main scene + if get_tree().current_scene and child.name == get_tree().current_scene.name: continue + # Add the node to our known autoloads + _autoloads[child.name] = child + game_states = [_autoloads] + # Add any other state shortcuts from settings + for node_name in DialogueSettings.get_setting(&"states", []): + var state: Node = get_node_or_null("/root/" + node_name) + if state: + game_states.append(state) - var current_scene: Node = get_current_scene.call() - var unique_states: Array = [] - for state in extra_game_states + [current_scene] + game_states: - if state != null and not unique_states.has(state): - unique_states.append(state) - return unique_states + var current_scene: Node = get_current_scene.call() + var unique_states: Array = [] + for state in extra_game_states + [current_scene] + game_states: + if state != null and not unique_states.has(state): + unique_states.append(state) + return unique_states # Check if a condition is met func check_condition(data: Dictionary, extra_game_states: Array) -> bool: - if data.get(&"condition", null) == null: return true - if data.condition.size() == 0: return true + if data.get(&"condition", null) == null: return true + if data.condition.size() == 0: return true - return await resolve(data.condition.expression.duplicate(true), extra_game_states) + return await resolve(data.condition.expression.duplicate(true), extra_game_states) # Make a change to game state or run a method func mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: - var expression: Array[Dictionary] = mutation.expression + var expression: Array[Dictionary] = mutation.expression - # Handle built in mutations - if expression[0].type == DialogueConstants.TOKEN_FUNCTION and expression[0].function in [&"wait", &"debug"]: - var args: Array = await resolve_each(expression[0].value, extra_game_states) - match expression[0].function: - &"wait": - mutated.emit(mutation) - await get_tree().create_timer(float(args[0])).timeout - return + # Handle built in mutations + if expression[0].type == DialogueConstants.TOKEN_FUNCTION and expression[0].function in [&"wait", &"debug"]: + var args: Array = await resolve_each(expression[0].value, extra_game_states) + match expression[0].function: + &"wait": + mutated.emit(mutation) + await get_tree().create_timer(float(args[0])).timeout + return - &"debug": - prints("Debug:", args) - await get_tree().process_frame + &"debug": + prints("Debug:", args) + await get_tree().process_frame - # Or pass through to the resolver - else: - if not mutation_contains_assignment(mutation.expression) and not is_inline_mutation: - mutated.emit(mutation) + # Or pass through to the resolver + else: + if not mutation_contains_assignment(mutation.expression) and not is_inline_mutation: + mutated.emit(mutation) - if mutation.get("is_blocking", true): - await resolve(mutation.expression.duplicate(true), extra_game_states) - return - else: - resolve(mutation.expression.duplicate(true), extra_game_states) + if mutation.get("is_blocking", true): + await resolve(mutation.expression.duplicate(true), extra_game_states) + return + else: + resolve(mutation.expression.duplicate(true), extra_game_states) - # Wait one frame to give the dialogue handler a chance to yield - await get_tree().process_frame + # Wait one frame to give the dialogue handler a chance to yield + await get_tree().process_frame func mutation_contains_assignment(mutation: Array) -> bool: - for token in mutation: - if token.type == DialogueConstants.TOKEN_ASSIGNMENT: - return true - return false + for token in mutation: + if token.type == DialogueConstants.TOKEN_ASSIGNMENT: + return true + return false func resolve_each(array: Array, extra_game_states: Array) -> Array: - var results: Array = [] - for item in array: - results.append(await resolve(item.duplicate(true), extra_game_states)) - return results + var results: Array = [] + for item in array: + results.append(await resolve(item.duplicate(true), extra_game_states)) + return results # Replace an array of line IDs with their response prompts func get_responses(ids: Array, resource: DialogueResource, id_trail: String, extra_game_states: Array) -> Array[DialogueResponse]: - var responses: Array[DialogueResponse] = [] - for id in ids: - var data: Dictionary = resource.lines.get(id).duplicate(true) - data.is_allowed = await check_condition(data, extra_game_states) - if DialogueSettings.get_setting(&"include_all_responses", false) or data.is_allowed: - var response: DialogueResponse = await create_response(data, extra_game_states) - response.next_id += id_trail - responses.append(response) + var responses: Array[DialogueResponse] = [] + for id in ids: + var data: Dictionary = resource.lines.get(id).duplicate(true) + data.is_allowed = await check_condition(data, extra_game_states) + if DialogueSettings.get_setting(&"include_all_responses", false) or data.is_allowed: + var response: DialogueResponse = await create_response(data, extra_game_states) + response.next_id += id_trail + responses.append(response) - return responses + return responses # Get a value on the current scene or game state func get_state_value(property: String, extra_game_states: Array): - # Special case for static primitive calls - if property == "Color": - return Color() - elif property == "Vector2": - return Vector2.ZERO - elif property == "Vector3": - return Vector3.ZERO - elif property == "Vector4": - return Vector4.ZERO - elif property == "Quaternian": - return Quaternion() + # Special case for static primitive calls + if property == "Color": + return Color() + elif property == "Vector2": + return Vector2.ZERO + elif property == "Vector3": + return Vector3.ZERO + elif property == "Vector4": + return Vector4.ZERO + elif property == "Quaternian": + return Quaternion() - var expression = Expression.new() - if expression.parse(property) != OK: - assert(false, DialogueConstants.translate(&"runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() })) + var expression = Expression.new() + if expression.parse(property) != OK: + assert(false, DialogueConstants.translate(&"runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() })) - for state in get_game_states(extra_game_states): - if typeof(state) == TYPE_DICTIONARY: - if state.has(property): - return state.get(property) - else: - var result = expression.execute([], state, false) - if not expression.has_execute_failed(): - return result + for state in get_game_states(extra_game_states): + if typeof(state) == TYPE_DICTIONARY: + if state.has(property): + return state.get(property) + else: + var result = expression.execute([], state, false) + if not expression.has_execute_failed(): + return result - if include_singletons and Engine.has_singleton(property): - return Engine.get_singleton(property) + if include_singletons and Engine.has_singleton(property): + return Engine.get_singleton(property) - if include_classes: - for class_data in ProjectSettings.get_global_class_list(): - if class_data.get(&"class") == property: - return load(class_data.path).new() + if include_classes: + for class_data in ProjectSettings.get_global_class_list(): + if class_data.get(&"class") == property: + return load(class_data.path).new() - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) # Set a value on the current scene or game state func set_state_value(property: String, value, extra_game_states: Array) -> void: - for state in get_game_states(extra_game_states): - if typeof(state) == TYPE_DICTIONARY: - if state.has(property): - state[property] = value - return - elif thing_has_property(state, property): - state.set(property, value) - return + for state in get_game_states(extra_game_states): + if typeof(state) == TYPE_DICTIONARY: + if state.has(property): + state[property] = value + return + elif thing_has_property(state, property): + state.set(property, value) + return - if property.to_snake_case() != property: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found_missing_export").format({ property = property, states = str(get_game_states(extra_game_states)) })) - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) + if property.to_snake_case() != property: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found_missing_export").format({ property = property, states = str(get_game_states(extra_game_states)) })) + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) # Collapse any expressions func resolve(tokens: Array, extra_game_states: Array): - # Handle groups first - for token in tokens: - if token.type == DialogueConstants.TOKEN_GROUP: - token["type"] = "value" - token["value"] = await resolve(token.value, extra_game_states) + # Handle groups first + for token in tokens: + if token.type == DialogueConstants.TOKEN_GROUP: + token["type"] = "value" + token["value"] = await resolve(token.value, extra_game_states) - # Then variables/methods - var i: int = 0 - var limit: int = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] + # Then variables/methods + var i: int = 0 + var limit: int = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_FUNCTION: - var function_name: String = token.function - var args = await resolve_each(token.value, extra_game_states) - if tokens[i - 1].type == DialogueConstants.TOKEN_DOT: - # If we are calling a deeper function then we need to collapse the - # value into the thing we are calling the function on - var caller: Dictionary = tokens[i - 2] - if Builtins.is_supported(caller.value): - caller["type"] = "value" - caller["value"] = Builtins.resolve_method(caller.value, function_name, args) - tokens.remove_at(i) - tokens.remove_at(i-1) - i -= 2 - elif thing_has_method(caller.value, function_name, args): - caller["type"] = "value" - caller["value"] = await resolve_thing_method(caller.value, function_name, args) - tokens.remove_at(i) - tokens.remove_at(i-1) - i -= 2 - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.method_not_callable").format({ method = function_name, object = str(caller.value) })) - else: - var found: bool = false - match function_name: - &"str": - token["type"] = "value" - token["value"] = str(args[0]) - found = true - &"Vector2": - token["type"] = "value" - token["value"] = Vector2(args[0], args[1]) - found = true - &"Vector2i": - token["type"] = "value" - token["value"] = Vector2i(args[0], args[1]) - found = true - &"Vector3": - token["type"] = "value" - token["value"] = Vector3(args[0], args[1], args[2]) - found = true - &"Vector3i": - token["type"] = "value" - token["value"] = Vector3i(args[0], args[1], args[2]) - found = true - &"Vector4": - token["type"] = "value" - token["value"] = Vector4(args[0], args[1], args[2], args[3]) - found = true - &"Vector4i": - token["type"] = "value" - token["value"] = Vector4i(args[0], args[1], args[2], args[3]) - found = true - &"Quaternion": - token["type"] = "value" - token["value"] = Quaternion(args[0], args[1], args[2], args[3]) - found = true - &"Color": - token["type"] = "value" - match args.size(): - 0: - token["value"] = Color() - 1: - token["value"] = Color(args[0]) - 2: - token["value"] = Color(args[0], args[1]) - 3: - token["value"] = Color(args[0], args[1], args[2]) - 4: - token["value"] = Color(args[0], args[1], args[2], args[3]) - found = true - &"load": - token["type"] = "value" - token["value"] = load(args[0]) - found = true - &"emit": - token["type"] = "value" - token["value"] = resolve_signal(args, extra_game_states) - found = true - _: - for state in get_game_states(extra_game_states): - if thing_has_method(state, function_name, args): - token["type"] = "value" - token["value"] = await resolve_thing_method(state, function_name, args) - found = true - break + if token.type == DialogueConstants.TOKEN_FUNCTION: + var function_name: String = token.function + var args = await resolve_each(token.value, extra_game_states) + if tokens[i - 1].type == DialogueConstants.TOKEN_DOT: + # If we are calling a deeper function then we need to collapse the + # value into the thing we are calling the function on + var caller: Dictionary = tokens[i - 2] + if Builtins.is_supported(caller.value): + caller["type"] = "value" + caller["value"] = Builtins.resolve_method(caller.value, function_name, args) + tokens.remove_at(i) + tokens.remove_at(i-1) + i -= 2 + elif thing_has_method(caller.value, function_name, args): + caller["type"] = "value" + caller["value"] = await resolve_thing_method(caller.value, function_name, args) + tokens.remove_at(i) + tokens.remove_at(i-1) + i -= 2 + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.method_not_callable").format({ method = function_name, object = str(caller.value) })) + else: + var found: bool = false + match function_name: + &"str": + token["type"] = "value" + token["value"] = str(args[0]) + found = true + &"Vector2": + token["type"] = "value" + token["value"] = Vector2(args[0], args[1]) + found = true + &"Vector2i": + token["type"] = "value" + token["value"] = Vector2i(args[0], args[1]) + found = true + &"Vector3": + token["type"] = "value" + token["value"] = Vector3(args[0], args[1], args[2]) + found = true + &"Vector3i": + token["type"] = "value" + token["value"] = Vector3i(args[0], args[1], args[2]) + found = true + &"Vector4": + token["type"] = "value" + token["value"] = Vector4(args[0], args[1], args[2], args[3]) + found = true + &"Vector4i": + token["type"] = "value" + token["value"] = Vector4i(args[0], args[1], args[2], args[3]) + found = true + &"Quaternion": + token["type"] = "value" + token["value"] = Quaternion(args[0], args[1], args[2], args[3]) + found = true + &"Callable": + token["type"] = "value" + match args.size(): + 0: + token["value"] = Callable() + 1: + token["value"] = Callable(args[0]) + 2: + token["value"] = Callable(args[0], args[1]) + found = true + &"Color": + token["type"] = "value" + match args.size(): + 0: + token["value"] = Color() + 1: + token["value"] = Color(args[0]) + 2: + token["value"] = Color(args[0], args[1]) + 3: + token["value"] = Color(args[0], args[1], args[2]) + 4: + token["value"] = Color(args[0], args[1], args[2], args[3]) + found = true + &"load": + token["type"] = "value" + token["value"] = load(args[0]) + found = true + &"emit": + token["type"] = "value" + token["value"] = resolve_signal(args, extra_game_states) + found = true + _: + for state in get_game_states(extra_game_states): + if thing_has_method(state, function_name, args): + token["type"] = "value" + token["value"] = await resolve_thing_method(state, function_name, args) + found = true + break - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.method_not_found").format({ - method = args[0] if function_name in ["call", "call_deferred"] else function_name, - states = str(get_game_states(extra_game_states)) - }), not found) + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.method_not_found").format({ + method = args[0] if function_name in ["call", "call_deferred"] else function_name, + states = str(get_game_states(extra_game_states)) + }), not found) - elif token.type == DialogueConstants.TOKEN_DICTIONARY_REFERENCE: - var value - if i > 0 and tokens[i - 1].type == DialogueConstants.TOKEN_DOT: - # If we are deep referencing then we need to get the parent object. - # `parent.value` is the actual object and `token.variable` is the name of - # the property within it. - value = tokens[i - 2].value[token.variable] - # Clean up the previous tokens - token.erase("variable") - tokens.remove_at(i - 1) - tokens.remove_at(i - 2) - i -= 2 - else: - # Otherwise we can just get this variable as a normal state reference - value = get_state_value(token.variable, extra_game_states) + elif token.type == DialogueConstants.TOKEN_DICTIONARY_REFERENCE: + var value + if i > 0 and tokens[i - 1].type == DialogueConstants.TOKEN_DOT: + # If we are deep referencing then we need to get the parent object. + # `parent.value` is the actual object and `token.variable` is the name of + # the property within it. + value = tokens[i - 2].value[token.variable] + # Clean up the previous tokens + token.erase("variable") + tokens.remove_at(i - 1) + tokens.remove_at(i - 2) + i -= 2 + else: + # Otherwise we can just get this variable as a normal state reference + value = get_state_value(token.variable, extra_game_states) - var index = await resolve(token.value, extra_game_states) - if typeof(value) == TYPE_DICTIONARY: - if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # If the next token is an assignment then we need to leave this as a reference - # so that it can be resolved once everything ahead of it has been resolved - token["type"] = "dictionary" - token["value"] = value - token["key"] = index - else: - if value.has(index): - token["type"] = "value" - token["value"] = value[index] - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = token.variable })) - elif typeof(value) == TYPE_ARRAY: - if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # If the next token is an assignment then we need to leave this as a reference - # so that it can be resolved once everything ahead of it has been resolved - token["type"] = "array" - token["value"] = value - token["key"] = index - else: - if index >= 0 and index < value.size(): - token["type"] = "value" - token["value"] = value[index] - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = token.variable })) + var index = await resolve(token.value, extra_game_states) + if typeof(value) == TYPE_DICTIONARY: + if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + token["type"] = "dictionary" + token["value"] = value + token["key"] = index + else: + if value.has(index): + token["type"] = "value" + token["value"] = value[index] + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = token.variable })) + elif typeof(value) == TYPE_ARRAY: + if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + token["type"] = "array" + token["value"] = value + token["key"] = index + else: + if index >= 0 and index < value.size(): + token["type"] = "value" + token["value"] = value[index] + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = token.variable })) - elif token.type == DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: - var dictionary: Dictionary = tokens[i - 1] - var index = await resolve(token.value, extra_game_states) - var value = dictionary.value - if typeof(value) == TYPE_DICTIONARY: - if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # If the next token is an assignment then we need to leave this as a reference - # so that it can be resolved once everything ahead of it has been resolved - dictionary["type"] = "dictionary" - dictionary["key"] = index - dictionary["value"] = value - tokens.remove_at(i) - i -= 1 - else: - if dictionary.value.has(index): - dictionary["value"] = value.get(index) - tokens.remove_at(i) - i -= 1 - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = value })) - elif typeof(value) == TYPE_ARRAY: - if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # If the next token is an assignment then we need to leave this as a reference - # so that it can be resolved once everything ahead of it has been resolved - dictionary["type"] = "array" - dictionary["value"] = value - dictionary["key"] = index - tokens.remove_at(i) - i -= 1 - else: - if index >= 0 and index < value.size(): - dictionary["value"] = value[index] - tokens.remove_at(i) - i -= 1 - else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = value })) + elif token.type == DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: + var dictionary: Dictionary = tokens[i - 1] + var index = await resolve(token.value, extra_game_states) + var value = dictionary.value + if typeof(value) == TYPE_DICTIONARY: + if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + dictionary["type"] = "dictionary" + dictionary["key"] = index + dictionary["value"] = value + tokens.remove_at(i) + i -= 1 + else: + if dictionary.value.has(index): + dictionary["value"] = value.get(index) + tokens.remove_at(i) + i -= 1 + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = value })) + elif typeof(value) == TYPE_ARRAY: + if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + dictionary["type"] = "array" + dictionary["value"] = value + dictionary["key"] = index + tokens.remove_at(i) + i -= 1 + else: + if index >= 0 and index < value.size(): + dictionary["value"] = value[index] + tokens.remove_at(i) + i -= 1 + else: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = value })) - elif token.type == DialogueConstants.TOKEN_ARRAY: - token["type"] = "value" - token["value"] = await resolve_each(token.value, extra_game_states) + elif token.type == DialogueConstants.TOKEN_ARRAY: + token["type"] = "value" + token["value"] = await resolve_each(token.value, extra_game_states) - elif token.type == DialogueConstants.TOKEN_DICTIONARY: - token["type"] = "value" - var dictionary = {} - for key in token.value.keys(): - var resolved_key = await resolve([key], extra_game_states) - var preresolved_value = token.value.get(key) - if typeof(preresolved_value) != TYPE_ARRAY: - preresolved_value = [preresolved_value] - var resolved_value = await resolve(preresolved_value, extra_game_states) - dictionary[resolved_key] = resolved_value - token["value"] = dictionary + elif token.type == DialogueConstants.TOKEN_DICTIONARY: + token["type"] = "value" + var dictionary = {} + for key in token.value.keys(): + var resolved_key = await resolve([key], extra_game_states) + var preresolved_value = token.value.get(key) + if typeof(preresolved_value) != TYPE_ARRAY: + preresolved_value = [preresolved_value] + var resolved_value = await resolve(preresolved_value, extra_game_states) + dictionary[resolved_key] = resolved_value + token["value"] = dictionary - elif token.type == DialogueConstants.TOKEN_VARIABLE or token.type == DialogueConstants.TOKEN_NUMBER: - if str(token.value) == "null": - token["type"] = "value" - token["value"] = null - elif tokens[i - 1].type == DialogueConstants.TOKEN_DOT: - var caller: Dictionary = tokens[i - 2] - var property = token.value - if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # If the next token is an assignment then we need to leave this as a reference - # so that it can be resolved once everything ahead of it has been resolved - caller["type"] = "property" - caller["property"] = property - else: - # If we are requesting a deeper property then we need to collapse the - # value into the thing we are referencing from - caller["type"] = "value" - if Builtins.is_supported(caller.value): - caller["value"] = Builtins.resolve_property(caller.value, property) - else: - caller["value"] = caller.value.get(property) - tokens.remove_at(i) - tokens.remove_at(i-1) - i -= 2 - elif tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: - # It's a normal variable but we will be assigning to it so don't resolve - # it until everything after it has been resolved - token["type"] = "variable" - else: - token["type"] = "value" - token["value"] = get_state_value(str(token.value), extra_game_states) + elif token.type == DialogueConstants.TOKEN_VARIABLE or token.type == DialogueConstants.TOKEN_NUMBER: + if str(token.value) == "null": + token["type"] = "value" + token["value"] = null + elif tokens[i - 1].type == DialogueConstants.TOKEN_DOT: + var caller: Dictionary = tokens[i - 2] + var property = token.value + if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + caller["type"] = "property" + caller["property"] = property + else: + # If we are requesting a deeper property then we need to collapse the + # value into the thing we are referencing from + caller["type"] = "value" + if Builtins.is_supported(caller.value): + caller["value"] = Builtins.resolve_property(caller.value, property) + else: + caller["value"] = caller.value.get(property) + tokens.remove_at(i) + tokens.remove_at(i-1) + i -= 2 + elif tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + # It's a normal variable but we will be assigning to it so don't resolve + # it until everything after it has been resolved + token["type"] = "variable" + else: + token["type"] = "value" + token["value"] = get_state_value(str(token.value), extra_game_states) - i += 1 + i += 1 - # Then multiply and divide - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]: - token["type"] = "value" - token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) - tokens.remove_at(i+1) - tokens.remove_at(i-1) - i -= 1 - i += 1 + # Then multiply and divide + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]: + token["type"] = "value" + token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) + tokens.remove_at(i+1) + tokens.remove_at(i-1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - # Then addition and subtraction - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["+", "-"]: - token["type"] = "value" - token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) - tokens.remove_at(i+1) - tokens.remove_at(i-1) - i -= 1 - i += 1 + # Then addition and subtraction + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["+", "-"]: + token["type"] = "value" + token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) + tokens.remove_at(i+1) + tokens.remove_at(i-1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - # Then negations - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_NOT: - token["type"] = "value" - token["value"] = not tokens[i+1].value - tokens.remove_at(i+1) - i -= 1 - i += 1 + # Then negations + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_NOT: + token["type"] = "value" + token["value"] = not tokens[i+1].value + tokens.remove_at(i+1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - # Then comparisons - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_COMPARISON: - token["type"] = "value" - token["value"] = compare(token.value, tokens[i-1].value, tokens[i+1].value) - tokens.remove_at(i+1) - tokens.remove_at(i-1) - i -= 1 - i += 1 + # Then comparisons + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_COMPARISON: + token["type"] = "value" + token["value"] = compare(token.value, tokens[i-1].value, tokens[i+1].value) + tokens.remove_at(i+1) + tokens.remove_at(i-1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - # Then and/or - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_AND_OR: - token["type"] = "value" - token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) - tokens.remove_at(i+1) - tokens.remove_at(i-1) - i -= 1 - i += 1 + # Then and/or + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_AND_OR: + token["type"] = "value" + token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) + tokens.remove_at(i+1) + tokens.remove_at(i-1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - # Lastly, resolve any assignments - i = 0 - limit = 0 - while i < tokens.size() and limit < 1000: - limit += 1 - var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_ASSIGNMENT: - var lhs: Dictionary = tokens[i - 1] - var value + # Lastly, resolve any assignments + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DialogueConstants.TOKEN_ASSIGNMENT: + var lhs: Dictionary = tokens[i - 1] + var value - match lhs.type: - &"variable": - value = apply_operation(token.value, get_state_value(lhs.value, extra_game_states), tokens[i+1].value) - set_state_value(lhs.value, value, extra_game_states) - &"property": - value = apply_operation(token.value, lhs.value.get(lhs.property), tokens[i+1].value) - if typeof(lhs.value) == TYPE_DICTIONARY: - lhs.value[lhs.property] = value - else: - lhs.value.set(lhs.property, value) - &"dictionary": - value = apply_operation(token.value, lhs.value.get(lhs.key, null), tokens[i+1].value) - lhs.value[lhs.key] = value - &"array": - show_error_for_missing_state_value( - DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = lhs.key, array = lhs.value }), - lhs.key >= lhs.value.size() - ) - value = apply_operation(token.value, lhs.value[lhs.key], tokens[i+1].value) - lhs.value[lhs.key] = value - _: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.left_hand_size_cannot_be_assigned_to")) + match lhs.type: + &"variable": + value = apply_operation(token.value, get_state_value(lhs.value, extra_game_states), tokens[i+1].value) + set_state_value(lhs.value, value, extra_game_states) + &"property": + value = apply_operation(token.value, lhs.value.get(lhs.property), tokens[i+1].value) + if typeof(lhs.value) == TYPE_DICTIONARY: + lhs.value[lhs.property] = value + else: + lhs.value.set(lhs.property, value) + &"dictionary": + value = apply_operation(token.value, lhs.value.get(lhs.key, null), tokens[i+1].value) + lhs.value[lhs.key] = value + &"array": + show_error_for_missing_state_value( + DialogueConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = lhs.key, array = lhs.value }), + lhs.key >= lhs.value.size() + ) + value = apply_operation(token.value, lhs.value[lhs.key], tokens[i+1].value) + lhs.value[lhs.key] = value + _: + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.left_hand_size_cannot_be_assigned_to")) - token["type"] = "value" - token["value"] = value - tokens.remove_at(i+1) - tokens.remove_at(i-1) - i -= 1 - i += 1 + token["type"] = "value" + token["value"] = value + tokens.remove_at(i+1) + tokens.remove_at(i-1) + i -= 1 + i += 1 - if limit >= 1000: - assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) + if limit >= 1000: + assert(false, DialogueConstants.translate(&"runtime.something_went_wrong")) - return tokens[0].value + return tokens[0].value func compare(operator: String, first_value, second_value) -> bool: - match operator: - &"in": - if first_value == null or second_value == null: - return false - else: - return first_value in second_value - &"<": - if first_value == null: - return true - elif second_value == null: - return false - else: - return first_value < second_value - &">": - if first_value == null: - return false - elif second_value == null: - return true - else: - return first_value > second_value - &"<=": - if first_value == null: - return true - elif second_value == null: - return false - else: - return first_value <= second_value - &">=": - if first_value == null: - return false - elif second_value == null: - return true - else: - return first_value >= second_value - &"==": - if first_value == null: - if typeof(second_value) == TYPE_BOOL: - return second_value == false - else: - return second_value == null - else: - return first_value == second_value - &"!=": - if first_value == null: - if typeof(second_value) == TYPE_BOOL: - return second_value == true - else: - return second_value != null - else: - return first_value != second_value + match operator: + &"in": + if first_value == null or second_value == null: + return false + else: + return first_value in second_value + &"<": + if first_value == null: + return true + elif second_value == null: + return false + else: + return first_value < second_value + &">": + if first_value == null: + return false + elif second_value == null: + return true + else: + return first_value > second_value + &"<=": + if first_value == null: + return true + elif second_value == null: + return false + else: + return first_value <= second_value + &">=": + if first_value == null: + return false + elif second_value == null: + return true + else: + return first_value >= second_value + &"==": + if first_value == null: + if typeof(second_value) == TYPE_BOOL: + return second_value == false + else: + return second_value == null + else: + return first_value == second_value + &"!=": + if first_value == null: + if typeof(second_value) == TYPE_BOOL: + return second_value == true + else: + return second_value != null + else: + return first_value != second_value - return false + return false func apply_operation(operator: String, first_value, second_value): - match operator: - &"=": - return second_value - &"+", &"+=": - return first_value + second_value - &"-", &"-=": - return first_value - second_value - &"/", &"/=": - return first_value / second_value - &"*", &"*=": - return first_value * second_value - &"%": - return first_value % second_value - &"and": - return first_value and second_value - &"or": - return first_value or second_value + match operator: + &"=": + return second_value + &"+", &"+=": + return first_value + second_value + &"-", &"-=": + return first_value - second_value + &"/", &"/=": + return first_value / second_value + &"*", &"*=": + return first_value * second_value + &"%": + return first_value % second_value + &"and": + return first_value and second_value + &"or": + return first_value or second_value - assert(false, DialogueConstants.translate(&"runtime.unknown_operator")) + assert(false, DialogueConstants.translate(&"runtime.unknown_operator")) # Check if a dialogue line contains meaningful information func is_valid(line: DialogueLine) -> bool: - if line == null: - return false - if line.type == DialogueConstants.TYPE_MUTATION and line.mutation == null: - return false - if line.type == DialogueConstants.TYPE_RESPONSE and line.get(&"responses").size() == 0: - return false - return true + if line == null: + return false + if line.type == DialogueConstants.TYPE_MUTATION and line.mutation == null: + return false + if line.type == DialogueConstants.TYPE_RESPONSE and line.get(&"responses").size() == 0: + return false + return true func thing_has_method(thing, method: String, args: Array) -> bool: - if Builtins.is_supported(thing): - return thing != _autoloads + if Builtins.is_supported(thing): + return thing != _autoloads - if method in [&"call", &"call_deferred"]: - return thing.has_method(args[0]) + if method in [&"call", &"call_deferred"]: + return thing.has_method(args[0]) - if method == &"emit_signal": - return thing.has_signal(args[0]) + if method == &"emit_signal": + return thing.has_signal(args[0]) - if thing.has_method(method): - return true + if thing.has_method(method): + return true - if method.to_snake_case() != method and DialogueSettings.has_dotnet_solution(): - # If we get this far then the method might be a C# method with a Task return type - return _get_dotnet_dialogue_manager().ThingHasMethod(thing, method) + if method.to_snake_case() != method and DialogueSettings.has_dotnet_solution(): + # If we get this far then the method might be a C# method with a Task return type + return _get_dotnet_dialogue_manager().ThingHasMethod(thing, method) - return false + return false # Check if a given property exists func thing_has_property(thing: Object, property: String) -> bool: - if thing == null: - return false + if thing == null: + return false - for p in thing.get_property_list(): - if _node_properties.has(p.name): - # Ignore any properties on the base Node - continue - if p.name == property: - return true + for p in thing.get_property_list(): + if _node_properties.has(p.name): + # Ignore any properties on the base Node + continue + if p.name == property: + return true - return false + return false func resolve_signal(args: Array, extra_game_states: Array): - if args[0] is Signal: - args[0] = args[0].get_name() + if args[0] is Signal: + args[0] = args[0].get_name() - for state in get_game_states(extra_game_states): - if typeof(state) == TYPE_DICTIONARY: - continue - elif state.has_signal(args[0]): - match args.size(): - 1: - state.emit_signal(args[0]) - 2: - state.emit_signal(args[0], args[1]) - 3: - state.emit_signal(args[0], args[1], args[2]) - 4: - state.emit_signal(args[0], args[1], args[2], args[3]) - 5: - state.emit_signal(args[0], args[1], args[2], args[3], args[4]) - 6: - state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5]) - 7: - state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) - 8: - state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) - return + for state in get_game_states(extra_game_states): + if typeof(state) == TYPE_DICTIONARY: + continue + elif state.has_signal(args[0]): + match args.size(): + 1: + state.emit_signal(args[0]) + 2: + state.emit_signal(args[0], args[1]) + 3: + state.emit_signal(args[0], args[1], args[2]) + 4: + state.emit_signal(args[0], args[1], args[2], args[3]) + 5: + state.emit_signal(args[0], args[1], args[2], args[3], args[4]) + 6: + state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5]) + 7: + state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) + 8: + state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) + return - # The signal hasn't been found anywhere - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.signal_not_found").format({ signal_name = args[0], states = str(get_game_states(extra_game_states)) })) + # The signal hasn't been found anywhere + show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.signal_not_found").format({ signal_name = args[0], states = str(get_game_states(extra_game_states)) })) func resolve_thing_method(thing, method: String, args: Array): - if Builtins.is_supported(thing): - var result = Builtins.resolve_method(thing, method, args) - if not Builtins.has_resolve_method_failed(): - return result + if Builtins.is_supported(thing): + var result = Builtins.resolve_method(thing, method, args) + if not Builtins.has_resolve_method_failed(): + return result - if thing.has_method(method): - # Try to convert any literals to the right type - var method_info: Dictionary = thing.get_method_list().filter(func(m): return method == m.name)[0] - var method_args: Array = method_info.args - if method_info.flags & METHOD_FLAG_VARARG == 0 and method_args.size() < args.size(): - assert(false, DialogueConstants.translate(&"runtime.expected_n_got_n_args").format({ expected = method_args.size(), method = method, received = args.size()})) - for i in range(0, args.size()): - var m: Dictionary = method_args[i] - var to_type:int = typeof(args[i]) - if m.type == TYPE_ARRAY: - match m.hint_string: - &"String": - to_type = TYPE_PACKED_STRING_ARRAY - &"int": - to_type = TYPE_PACKED_INT64_ARRAY - &"float": - to_type = TYPE_PACKED_FLOAT64_ARRAY - &"Vector2": - to_type = TYPE_PACKED_VECTOR2_ARRAY - &"Vector3": - to_type = TYPE_PACKED_VECTOR3_ARRAY - _: - if m.hint_string != "": - assert(false, DialogueConstants.translate(&"runtime.unsupported_array_type").format({ type = m.hint_string})) - if typeof(args[i]) != to_type: - args[i] = convert(args[i], to_type) + if thing.has_method(method): + # Try to convert any literals to the right type + var method_info: Dictionary = thing.get_method_list().filter(func(m): return method == m.name)[0] + var method_args: Array = method_info.args + if method_info.flags & METHOD_FLAG_VARARG == 0 and method_args.size() < args.size(): + assert(false, DialogueConstants.translate(&"runtime.expected_n_got_n_args").format({ expected = method_args.size(), method = method, received = args.size()})) + for i in range(0, args.size()): + var m: Dictionary = method_args[i] + var to_type:int = typeof(args[i]) + if m.type == TYPE_ARRAY: + match m.hint_string: + &"String": + to_type = TYPE_PACKED_STRING_ARRAY + &"int": + to_type = TYPE_PACKED_INT64_ARRAY + &"float": + to_type = TYPE_PACKED_FLOAT64_ARRAY + &"Vector2": + to_type = TYPE_PACKED_VECTOR2_ARRAY + &"Vector3": + to_type = TYPE_PACKED_VECTOR3_ARRAY + _: + if m.hint_string != "": + assert(false, DialogueConstants.translate(&"runtime.unsupported_array_type").format({ type = m.hint_string})) + if typeof(args[i]) != to_type: + args[i] = convert(args[i], to_type) - return await thing.callv(method, args) + return await thing.callv(method, args) - # If we get here then it's probably a C# method with a Task return type - var dotnet_dialogue_manager = _get_dotnet_dialogue_manager() - dotnet_dialogue_manager.ResolveThingMethod(thing, method, args) - return await dotnet_dialogue_manager.Resolved + # If we get here then it's probably a C# method with a Task return type + var dotnet_dialogue_manager = _get_dotnet_dialogue_manager() + dotnet_dialogue_manager.ResolveThingMethod(thing, method, args) + return await dotnet_dialogue_manager.Resolved diff --git a/addons/dialogue_manager/dialogue_reponses_menu.gd b/addons/dialogue_manager/dialogue_reponses_menu.gd index 6da0e5c..69a56db 100644 --- a/addons/dialogue_manager/dialogue_reponses_menu.gd +++ b/addons/dialogue_manager/dialogue_reponses_menu.gd @@ -1,7 +1,7 @@ @icon("./assets/responses_menu.svg") -## A VBoxContainer for dialogue responses provided by [b]Dialogue Manager[/b]. -class_name DialogueResponsesMenu extends VBoxContainer +## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b]. +class_name DialogueResponsesMenu extends Container ## Emitted when a response is selected. @@ -14,8 +14,10 @@ signal response_selected(response) ## The action for accepting a response (is possibly overridden by parent dialogue balloon). @export var next_action: StringName = &"" -# The list of dialogue responses. +## The list of dialogue responses. var responses: Array = []: + get: + return responses set(value): responses = value @@ -64,11 +66,25 @@ func _ready() -> void: response_template.hide() -# This is deprecated. +## Get the selectable items in the menu. +func get_menu_items() -> Array: + var items: Array = [] + for child in get_children(): + if not child.visible: continue + if "Disallowed" in child.name: continue + items.append(child) + + return items + + +## [b]DEPRECATED[/b]. Do not use. func set_responses(next_responses: Array) -> void: self.responses = next_responses +#region Internal + + # Prepare the menu for keyboard and mouse navigation. func _configure_focus() -> void: var items = get_menu_items() @@ -100,18 +116,9 @@ func _configure_focus() -> void: items[0].grab_focus() -## Get the selectable items in the menu. -func get_menu_items() -> Array: - var items: Array = [] - for child in get_children(): - if not child.visible: continue - if "Disallowed" in child.name: continue - items.append(child) +#endregion - return items - - -### Signals +#region Signals func _on_response_mouse_entered(item: Control) -> void: @@ -129,3 +136,6 @@ func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: response_selected.emit(response) elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items(): response_selected.emit(response) + + +#endregion diff --git a/addons/dialogue_manager/example_balloon/ExampleBalloon.cs b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs index a6574e0..a21dc63 100644 --- a/addons/dialogue_manager/example_balloon/ExampleBalloon.cs +++ b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs @@ -105,6 +105,21 @@ namespace DialogueManagerRuntime } + public override async void _Notification(int what) + { + // Detect a change of locale and update the current dialogue line to show the new language + if (what == NotificationTranslationChanged) + { + float visibleRatio = dialogueLabel.VisibleRatio; + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates); + if (visibleRatio < 1.0f) + { + dialogueLabel.Call("skip_typing"); + } + } + } + + public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) { temporaryGameStates = extraGameStates ?? new Array(); diff --git a/addons/dialogue_manager/example_balloon/example_balloon.gd b/addons/dialogue_manager/example_balloon/example_balloon.gd index 875e52f..36f8d59 100644 --- a/addons/dialogue_manager/example_balloon/example_balloon.gd +++ b/addons/dialogue_manager/example_balloon/example_balloon.gd @@ -89,6 +89,15 @@ func _unhandled_input(_event: InputEvent) -> void: get_viewport().set_input_as_handled() +func _notification(what: int) -> void: + # Detect a change of locale and update the current dialogue line to show the new language + if what == NOTIFICATION_TRANSLATION_CHANGED: + var visible_ratio = dialogue_label.visible_ratio + self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id) + if visible_ratio < 1: + dialogue_label.skip_typing() + + ## Start some dialogue func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: temporary_game_states = [self] + extra_game_states @@ -102,7 +111,7 @@ func next(next_id: String) -> void: self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) -### Signals +#region Signals func _on_mutated(_mutation: Dictionary) -> void: @@ -139,3 +148,6 @@ func _on_balloon_gui_input(event: InputEvent) -> void: func _on_responses_menu_response_selected(response: DialogueResponse) -> void: next(response.next_id) + + +#endregion diff --git a/addons/dialogue_manager/import_plugin.gd b/addons/dialogue_manager/import_plugin.gd index 3f0af15..7cf61bc 100644 --- a/addons/dialogue_manager/import_plugin.gd +++ b/addons/dialogue_manager/import_plugin.gd @@ -8,7 +8,7 @@ signal compiled_resource(resource: Resource) const DialogueResource = preload("./dialogue_resource.gd") const DialogueManagerParseResult = preload("./components/parse_result.gd") -const compiler_version = 11 +const compiler_version = 12 func _get_importer_name() -> String: diff --git a/addons/dialogue_manager/l10n/zh.po b/addons/dialogue_manager/l10n/zh.po index b7f032f..887fb1e 100644 --- a/addons/dialogue_manager/l10n/zh.po +++ b/addons/dialogue_manager/l10n/zh.po @@ -4,7 +4,7 @@ msgstr "" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: \n" -"Language-Team: penghao123456、憨憨羊の宇航鸽鸽\n" +"Language-Team: penghao123456、憨憨羊の宇航鸽鸽、ABShinri\n" "Language: zh\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -29,6 +29,9 @@ msgstr "清空历史记录" msgid "save_all_files" msgstr "保存所有文件" +msgid "find_in_files" +msgstr "在文件中查找" + msgid "test_dialogue" msgstr "测试对话" @@ -51,10 +54,10 @@ msgid "docs" msgstr "文档" msgid "insert.wave_bbcode" -msgstr "BBCode [lb]wave[rb]" +msgstr "波浪效果" msgid "insert.shake_bbcode" -msgstr "BBCode [lb]wave[rb]" +msgstr "抖动效果" msgid "insert.typing_pause" msgstr "输入间隔" @@ -95,6 +98,9 @@ msgstr "结束对话" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "save_characters_to_csv" +msgstr "保存角色到 CSV" + msgid "save_to_csv" msgstr "生成 CSV" @@ -134,6 +140,9 @@ msgstr "在 Godot 侧边栏中显示" msgid "settings.revert_to_default_test_scene" msgstr "重置测试场景设定" +msgid "settings.default_balloon_hint" +msgstr "设置调用 \"DialogueManager.show_balloon()\" 时使用的对话框" + msgid "settings.autoload" msgstr "Autoload" @@ -150,10 +159,10 @@ msgid "settings.missing_keys_hint" msgstr "如果你使用静态键,这将会帮助你寻找未添加至翻译文件的键。" msgid "settings.characters_translations" -msgstr "在翻译文件中导出角色名。" +msgstr "在翻译文件中导出角色名" msgid "settings.wrap_long_lines" -msgstr "自动折行" +msgstr "文本编辑器自动换行" msgid "settings.include_failed_responses" msgstr "在判断条件失败时仍显示回复选项" @@ -176,9 +185,39 @@ msgstr "当一个 Autoload 在这里被勾选,他的所有成员会被映射 msgid "settings.states_hint" msgstr "比如,当你开启对于“Foo”的映射时,你可以将“Foo.bar”简写成“bar”。" +msgid "settings.recompile_warning" +msgstr "更改这些选项会强制重新编译所有的对话框,当你清楚在做什么的时候更改。" + +msgid "settings.create_lines_for_responses_with_characters" +msgstr "回复项带角色名时(- char: response),会自动生成为选择后的下一句对话" + +msgid "settings.include_characters_in_translations" +msgstr "导出 CSV 时包括角色名" + +msgid "settings.include_notes_in_translations" +msgstr "导出 CSV 时包括注释(## comments)" + +msgid "settings.check_for_updates" +msgstr "检查升级" + msgid "n_of_n" msgstr "第{index}个,共{total}个" +msgid "search.find" +msgstr "查找:" + +msgid "search.find_all" +msgstr "查找全部..." + +msgid "search.placeholder" +msgstr "请输入查找的内容" + +msgid "search.replace_placeholder" +msgstr "请输入替换的内容" + +msgid "search.replace_selected" +msgstr "替换勾选" + msgid "search.previous" msgstr "查找上一个" diff --git a/addons/dialogue_manager/l10n/zh_TW.po b/addons/dialogue_manager/l10n/zh_TW.po index 3bcf153..bee270d 100644 --- a/addons/dialogue_manager/l10n/zh_TW.po +++ b/addons/dialogue_manager/l10n/zh_TW.po @@ -4,7 +4,7 @@ msgstr "" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: \n" -"Language-Team: 憨憨羊の宇航鴿鴿\n" +"Language-Team: 憨憨羊の宇航鴿鴿、ABShinri\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -29,6 +29,9 @@ msgstr "清空歷史記錄" msgid "save_all_files" msgstr "儲存所有檔案" +msgid "find_in_files" +msgstr "在檔案中查找" + msgid "test_dialogue" msgstr "測試對話" @@ -51,10 +54,10 @@ msgid "docs" msgstr "文檔" msgid "insert.wave_bbcode" -msgstr "BBCode [lb]wave[rb]" +msgstr "波浪特效" msgid "insert.shake_bbcode" -msgstr "BBCode [lb]wave[rb]" +msgstr "震動特效" msgid "insert.typing_pause" msgstr "輸入間隔" @@ -95,6 +98,9 @@ msgstr "結束對話" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "save_characters_to_csv" +msgstr "保存角色到 CSV" + msgid "save_to_csv" msgstr "生成 CSV" @@ -134,6 +140,9 @@ msgstr "在 Godot 側邊欄中顯示" msgid "settings.revert_to_default_test_scene" msgstr "重置測試場景設定" +msgid "settings.default_balloon_hint" +msgstr "設置使用 \"DialogueManager.show_balloon()\" 时的对话框" + msgid "settings.autoload" msgstr "Autoload" @@ -176,9 +185,39 @@ msgstr "當一個 Autoload 在這裏被勾選,他的所有成員會被映射 msgid "settings.states_hint" msgstr "比如,當你開啓對於“Foo”的映射時,你可以將“Foo.bar”簡寫成“bar”。" +msgid "settings.recompile_warning" +msgstr "更改這些選項會強制重新編譯所有的對話框,當你清楚在做什麼的時候更改。" + +msgid "settings.create_lines_for_responses_with_characters" +msgstr "回覆項目帶角色名稱時(- char: response),會自動產生為選擇後的下一句對話" + +msgid "settings.include_characters_in_translations" +msgstr "匯出 CSV 時包含角色名" + +msgid "settings.include_notes_in_translations" +msgstr "匯出 CSV 時包括註解(## comments)" + +msgid "settings.check_for_updates" +msgstr "檢查升級" + msgid "n_of_n" msgstr "第{index}個,共{total}個" +msgid "search.find" +msgstr "搜尋:" + +msgid "search.find_all" +msgstr "搜尋全部..." + +msgid "search.placeholder" +msgstr "請輸入搜尋的內容" + +msgid "search.replace_placeholder" +msgstr "請輸入替換的內容" + +msgid "search.replace_selected" +msgstr "替換勾選" + msgid "search.previous" msgstr "搜尋上一個" diff --git a/addons/dialogue_manager/plugin.cfg b/addons/dialogue_manager/plugin.cfg index 572ad9f..4b10f54 100644 --- a/addons/dialogue_manager/plugin.cfg +++ b/addons/dialogue_manager/plugin.cfg @@ -3,5 +3,5 @@ name="Dialogue Manager" description="A simple but powerful branching dialogue system" author="Nathan Hoad" -version="2.38.0" +version="2.39.1" script="plugin.gd" diff --git a/addons/dialogue_manager/plugin.gd b/addons/dialogue_manager/plugin.gd index 70836f3..8cd7ef7 100644 --- a/addons/dialogue_manager/plugin.gd +++ b/addons/dialogue_manager/plugin.gd @@ -145,6 +145,80 @@ func _build() -> bool: return true +## Get the shortcuts used by the plugin +func get_editor_shortcuts() -> Dictionary: + var shortcuts: Dictionary = { + toggle_comment = [ + _create_event("Ctrl+K"), + _create_event("Ctrl+Slash") + ], + move_up = [ + _create_event("Alt+Up") + ], + move_down = [ + _create_event("Alt+Down") + ], + save = [ + _create_event("Ctrl+Alt+S") + ], + close_file = [ + _create_event("Ctrl+W") + ], + find_in_files = [ + _create_event("Ctrl+Shift+F") + ], + + run_test_scene = [ + _create_event("Ctrl+F5") + ], + text_size_increase = [ + _create_event("Ctrl+Equal") + ], + text_size_decrease = [ + _create_event("Ctrl+Minus") + ], + text_size_reset = [ + _create_event("Ctrl+0") + ] + } + + var paths = get_editor_interface().get_editor_paths() + var settings = load(paths.get_config_dir() + "/editor_settings-4.tres") + + if not settings: return shortcuts + + for s in settings.get("shortcuts"): + for key in shortcuts: + if s.name == "script_text_editor/%s" % key or s.name == "script_editor/%s" % key: + shortcuts[key] = [] + for event in s.shortcuts: + if event is InputEventKey: + shortcuts[key].append(event) + + return shortcuts + + +func _create_event(string: String) -> InputEventKey: + var event: InputEventKey = InputEventKey.new() + var bits = string.split("+") + event.keycode = OS.find_keycode_from_string(bits[bits.size() - 1]) + event.shift_pressed = bits.has("Shift") + event.alt_pressed = bits.has("Alt") + if bits.has("Ctrl") or bits.has("Command"): + event.command_or_control_autoremap = true + return event + + +## Get the editor shortcut that matches an event +func get_editor_shortcut(event: InputEventKey) -> String: + var shortcuts: Dictionary = get_editor_shortcuts() + for key in shortcuts: + for shortcut in shortcuts.get(key, []): + if event.is_match(shortcut): + return key + return "" + + ## Get the current version func get_version() -> String: var config: ConfigFile = ConfigFile.new() diff --git a/addons/dialogue_manager/settings.gd b/addons/dialogue_manager/settings.gd index b968a84..c986cf5 100644 --- a/addons/dialogue_manager/settings.gd +++ b/addons/dialogue_manager/settings.gd @@ -176,9 +176,5 @@ static func has_dotnet_solution() -> bool: var has_dotnet_solution: bool = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) set_user_value("has_dotnet_solution", has_dotnet_solution) return has_dotnet_solution - else: - var plugin_path: String = new().get_script().resource_path.get_base_dir() - if not ResourceLoader.exists(plugin_path + "/DialogueManager.cs"): return false - if load(plugin_path + "/DialogueManager.cs") == null: return false - return true + return false diff --git a/addons/dialogue_manager/utilities/builtins.gd b/addons/dialogue_manager/utilities/builtins.gd index f8442d4..dd7ecef 100644 --- a/addons/dialogue_manager/utilities/builtins.gd +++ b/addons/dialogue_manager/utilities/builtins.gd @@ -11,7 +11,8 @@ const SUPPORTED_BUILTIN_TYPES = [ TYPE_DICTIONARY, TYPE_QUATERNION, TYPE_COLOR, - TYPE_SIGNAL + TYPE_SIGNAL, + TYPE_CALLABLE ] diff --git a/addons/dialogue_manager/views/main_view.gd b/addons/dialogue_manager/views/main_view.gd index 445d53d..f0d46cb 100644 --- a/addons/dialogue_manager/views/main_view.gd +++ b/addons/dialogue_manager/views/main_view.gd @@ -181,19 +181,20 @@ func _unhandled_input(event: InputEvent) -> void: if not visible: return if event is InputEventKey and event.is_pressed(): - match event.as_text(): - "Ctrl+Alt+S", "Command+Alt+S": - get_viewport().set_input_as_handled() - save_file(current_file_path) - "Ctrl+W", "Command+W": + var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event) + match shortcut: + "close_file": get_viewport().set_input_as_handled() close_file(current_file_path) - "Ctrl+F5", "Command+F5": + "save": get_viewport().set_input_as_handled() - _on_test_button_pressed() - "Ctrl+Shift+F", "Command+Shift+F": + save_file(current_file_path) + "find_in_files": get_viewport().set_input_as_handled() _on_find_in_files_button_pressed() + "run_test_scene": + get_viewport().set_input_as_handled() + _on_test_button_pressed() func apply_changes() -> void: @@ -1044,9 +1045,11 @@ func _on_files_list_file_middle_clicked(path: String): func _on_files_popup_menu_about_to_popup() -> void: files_popup_menu.clear() - files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save"), ITEM_SAVE, KEY_MASK_CTRL | KEY_MASK_ALT | KEY_S) + var shortcuts: Dictionary = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcuts() + + files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save"), ITEM_SAVE, OS.find_keycode_from_string(shortcuts.get("save")[0].as_text_keycode())) files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS) - files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close"), ITEM_CLOSE, KEY_MASK_CTRL | KEY_W) + files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close"), ITEM_CLOSE, OS.find_keycode_from_string(shortcuts.get("close_file")[0].as_text_keycode())) files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL) files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS) files_popup_menu.add_separator()