diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs index 069c8fad..20351c08 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs @@ -6,420 +6,457 @@ using System.Threading.Tasks; #nullable enable -namespace DialogueManagerRuntime; - -public enum TranslationSource +namespace DialogueManagerRuntime { - None, - Guess, - CSV, - PO -} - -public partial class DialogueManager : Node -{ - public delegate void PassedTitleEventHandler(string title); - public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); - public delegate void MutatedEventHandler(Dictionary mutation); - public delegate void DialogueEndedEventHandler(Resource dialogueResource); - - public static PassedTitleEventHandler? PassedTitle; - public static GotDialogueEventHandler? GotDialogue; - public static MutatedEventHandler? Mutated; - public static DialogueEndedEventHandler? DialogueEnded; - - [Signal] public delegate void ResolvedEventHandler(Variant value); - - private static GodotObject? instance; - public static GodotObject Instance - { - get + public enum TranslationSource { - if (instance == null) - { - instance = Engine.GetSingleton("DialogueManager"); - } - return instance; - } - } - - - public static Godot.Collections.Array GameStates - { - get => (Godot.Collections.Array)Instance.Get("game_states"); - set => Instance.Set("game_states", value); - } - - - public static bool IncludeSingletons - { - get => (bool)Instance.Get("include_singletons"); - set => Instance.Set("include_singletons", value); - } - - - public static bool IncludeClasses - { - get => (bool)Instance.Get("include_classes"); - set => Instance.Set("include_classes", value); - } - - - public static TranslationSource TranslationSource - { - get => (TranslationSource)(int)Instance.Get("translation_source"); - set => Instance.Set("translation_source", (int)value); - } - - - public static Func GetCurrentScene - { - set => Instance.Set("get_current_scene", Callable.From(value)); - } - - - public void Prepare() - { - Instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title))); - Instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line)))); - Instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation))); - Instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource))); - } - - - public static async Task GetSingleton() - { - if (instance != null) - return instance; - - var tree = Engine.GetMainLoop(); - int x = 0; - - // Try and find the singleton for a few seconds - while (!Engine.HasSingleton("DialogueManager") && x < 300) - { - await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); - x++; + None, + Guess, + CSV, + PO } - // If it times out something is wrong - if (x >= 300) + public partial class DialogueManager : RefCounted { - throw new Exception("The DialogueManager singleton is missing."); - } + public delegate void DialogueStartedEventHandler(Resource dialogueResource); + public delegate void PassedTitleEventHandler(string title); + public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); + public delegate void MutatedEventHandler(Dictionary mutation); + public delegate void DialogueEndedEventHandler(Resource dialogueResource); - instance = Engine.GetSingleton("DialogueManager"); - return instance; - } + public static DialogueStartedEventHandler? DialogueStarted; + public static PassedTitleEventHandler? PassedTitle; + public static GotDialogueEventHandler? GotDialogue; + public static MutatedEventHandler? Mutated; + public static DialogueEndedEventHandler? DialogueEnded; - public static async Task GetNextDialogueLine(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - Instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array()); - var result = await Instance.ToSignal(Instance, "bridge_get_next_dialogue_line_completed"); + [Signal] public delegate void ResolvedEventHandler(Variant value); - if ((RefCounted)result[0] == null) - return null; - - return new DialogueLine((RefCounted)result[0]); - } + private static GodotObject? instance; + public static GodotObject Instance + { + get + { + if (instance == null) + { + instance = Engine.GetSingleton("DialogueManager"); + instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + } + return instance; + } + } - public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); - } + public static Godot.Collections.Array GameStates + { + get => (Godot.Collections.Array)Instance.Get("game_states"); + set => Instance.Set("game_states", value); + } - public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } - - public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } - - public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } + public static bool IncludeSingletons + { + get => (bool)Instance.Get("include_singletons"); + set => Instance.Set("include_singletons", value); + } - public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); - } + public static bool IncludeClasses + { + get => (bool)Instance.Get("include_classes"); + set => Instance.Set("include_classes", value); + } - public static async void Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) - { - Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array(), isInlineMutation); - await Instance.ToSignal(Instance, "bridge_mutated"); - } + public static TranslationSource TranslationSource + { + get => (TranslationSource)(int)Instance.Get("translation_source"); + set => Instance.Set("translation_source", (int)value); + } - public bool ThingHasMethod(GodotObject thing, string method) - { - MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); - return info != null; - } + public static Func GetCurrentScene + { + set => Instance.Set("get_current_scene", Callable.From(value)); + } - public async void ResolveThingMethod(GodotObject thing, string method, Array args) - { - MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + public static void Prepare(GodotObject instance) + { + instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title))); + instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line)))); + instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation))); + instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource))); + } - if (info == null) - return; + + public static async Task GetSingleton() + { + if (instance != null) return instance; + + var tree = Engine.GetMainLoop(); + int x = 0; + + // Try and find the singleton for a few seconds + while (!Engine.HasSingleton("DialogueManager") && x < 300) + { + await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); + x++; + } + + // If it times out something is wrong + if (x >= 300) + { + throw new Exception("The DialogueManager singleton is missing."); + } + + instance = Engine.GetSingleton("DialogueManager"); + return instance; + } + + public static Resource CreateResourceFromText(string text) + { + return (Resource)Instance.Call("create_resource_from_text", text); + } + + public static async Task GetNextDialogueLine(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + var instance = (Node)Instance.Call("_bridge_get_new_instance"); + Prepare(instance); + instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array()); + var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed"); + instance.QueueFree(); + + if ((RefCounted)result[0] == null) return null; + + return new DialogueLine((RefCounted)result[0]); + } + + + public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static async void Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) + { + Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array(), isInlineMutation); + await Instance.ToSignal(Instance, "bridge_mutated"); + } + + + public bool ThingHasMethod(GodotObject thing, string method, Array args) + { + var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var methodInfo in methodInfos) + { + if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length) + { + return true; + } + } + + return false; + } + + + public async void ResolveThingMethod(GodotObject thing, string method, Array args) + { + MethodInfo? info = null; + var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var methodInfo in methodInfos) + { + if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length) + { + info = methodInfo; + } + } + + if (info == null) return; #nullable disable - // Convert the method args to something reflection can handle - ParameterInfo[] argTypes = info.GetParameters(); - object[] _args = new object[argTypes.Length]; - for (int i = 0; i < argTypes.Length; i++) - { - // check if args is assignable from derived type - if (i < args.Count && args[i].Obj != null) - { - if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) - { - _args[i] = args[i].Obj; + // Convert the method args to something reflection can handle + ParameterInfo[] argTypes = info.GetParameters(); + object[] _args = new object[argTypes.Length]; + for (int i = 0; i < argTypes.Length; i++) + { + // check if args is assignable from derived type + if (i < args.Count && args[i].Obj != null) + { + if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) + { + _args[i] = args[i].Obj; + } + // fallback to assigning primitive types + else + { + _args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType); + } + } + else if (argTypes[i].DefaultValue != null) + { + _args[i] = argTypes[i].DefaultValue; + } + } + + // Add a single frame wait in case the method returns before signals can listen + await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); + + // invoke method and handle the result based on return type + object result = info.Invoke(thing, _args); + + if (result is Task taskResult) + { + await taskResult; + try + { + Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); + EmitSignal(SignalName.Resolved, value); + } + catch (Exception err) + { + EmitSignal(SignalName.Resolved); + } + } + else + { + EmitSignal(SignalName.Resolved, (Variant)result); + } } - // fallback to assigning primitive types - else - { - _args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType); - } - } - else if (argTypes[i].DefaultValue != null) - { - _args[i] = argTypes[i].DefaultValue; - } - } - - // Add a single frame wait in case the method returns before signals can listen - await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); - - // invoke method and handle the result based on return type - object result = info.Invoke(thing, _args); - - if (result is Task taskResult) - { - // await Tasks and handle result if it is a Task - await taskResult; - var taskType = taskResult.GetType(); - if (taskType.IsGenericType && taskType.GetGenericTypeDefinition() == typeof(Task<>)) - { - var resultProperty = taskType.GetProperty("Result"); - var taskResultValue = resultProperty.GetValue(taskResult); - EmitSignal(SignalName.Resolved, (Variant)taskResultValue); - } - else - { - EmitSignal(SignalName.Resolved, null); - } - } - else - { - EmitSignal(SignalName.Resolved, (Variant)result); - } - } #nullable enable -} - - -public partial class DialogueLine : RefCounted -{ - private string id = ""; - public string Id - { - get => id; - set => id = value; - } - - private string type = "dialogue"; - public string Type - { - get => type; - set => type = value; - } - - private string next_id = ""; - public string NextId - { - get => next_id; - set => next_id = value; - } - - private string character = ""; - public string Character - { - get => character; - set => character = value; - } - - private string text = ""; - public string Text - { - get => text; - set => text = value; - } - - private string translation_key = ""; - public string TranslationKey - { - get => translation_key; - set => translation_key = value; - } - - private Array responses = new Array(); - public Array Responses - { - get => responses; - } - - private string? time = null; - public string? Time - { - get => time; - } - - private Dictionary pauses = new Dictionary(); - public Dictionary Pauses - { - get => pauses; - } - - private Dictionary speeds = new Dictionary(); - public Dictionary Speeds - { - get => speeds; - } - - private Array inline_mutations = new Array(); - public Array InlineMutations - { - get => inline_mutations; - } - - private Array extra_game_states = new Array(); - - private Array tags = new Array(); - public Array Tags - { - get => tags; - } - - public DialogueLine(RefCounted data) - { - type = (string)data.Get("type"); - next_id = (string)data.Get("next_id"); - character = (string)data.Get("character"); - text = (string)data.Get("text"); - translation_key = (string)data.Get("translation_key"); - pauses = (Dictionary)data.Get("pauses"); - speeds = (Dictionary)data.Get("speeds"); - inline_mutations = (Array)data.Get("inline_mutations"); - time = (string)data.Get("time"); - tags = (Array)data.Get("tags"); - - foreach (var response in (Array)data.Get("responses")) - { - responses.Add(new DialogueResponse(response)); - } - } - - - public string GetTagValue(string tagName) - { - string wrapped = $"{tagName}="; - foreach (var tag in tags) - { - if (tag.StartsWith(wrapped)) - { - return tag.Substring(wrapped.Length); - } - } - return ""; - } - - public override string ToString() - { - switch (type) - { - case "dialogue": - return $""; - case "mutation": - return ""; - default: - return ""; - } - } -} - - -public partial class DialogueResponse : RefCounted -{ - private string next_id = ""; - public string NextId - { - get => next_id; - set => next_id = value; - } - - private bool is_allowed = true; - public bool IsAllowed - { - get => is_allowed; - set => is_allowed = value; - } - - private string text = ""; - public string Text - { - get => text; - set => text = value; - } - - private string translation_key = ""; - public string TranslationKey - { - get => translation_key; - set => translation_key = value; - } - - private Array tags = new Array(); - public Array Tags - { - get => tags; - } - - public DialogueResponse(RefCounted data) - { - next_id = (string)data.Get("next_id"); - is_allowed = (bool)data.Get("is_allowed"); - text = (string)data.Get("text"); - translation_key = (string)data.Get("translation_key"); - tags = (Array)data.Get("tags"); - } - - public string GetTagValue(string tagName) - { - string wrapped = $"{tagName}="; - foreach (var tag in tags) - { - if (tag.StartsWith(wrapped)) - { - return tag.Substring(wrapped.Length); - } - } - return ""; - } - - public override string ToString() - { - return $" id; + set => id = value; + } + + private string type = "dialogue"; + public string Type + { + get => type; + set => type = value; + } + + private string next_id = ""; + public string NextId + { + get => next_id; + set => next_id = value; + } + + private string character = ""; + public string Character + { + get => character; + set => character = value; + } + + private string text = ""; + public string Text + { + get => text; + set => text = value; + } + + private string translation_key = ""; + public string TranslationKey + { + get => translation_key; + set => translation_key = value; + } + + private Array responses = new Array(); + public Array Responses + { + get => responses; + } + + private string? time = null; + public string? Time + { + get => time; + } + + private Dictionary pauses = new Dictionary(); + public Dictionary Pauses + { + get => pauses; + } + + private Dictionary speeds = new Dictionary(); + public Dictionary Speeds + { + get => speeds; + } + + private Array inline_mutations = new Array(); + public Array InlineMutations + { + get => inline_mutations; + } + + private Array concurrent_lines = new Array(); + public Array ConcurrentLines + { + get => concurrent_lines; + } + + private Array extra_game_states = new Array(); + public Array ExtraGameStates + { + get => extra_game_states; + } + + private Array tags = new Array(); + public Array Tags + { + get => tags; + } + + public DialogueLine(RefCounted data) + { + type = (string)data.Get("type"); + next_id = (string)data.Get("next_id"); + character = (string)data.Get("character"); + text = (string)data.Get("text"); + translation_key = (string)data.Get("translation_key"); + pauses = (Dictionary)data.Get("pauses"); + speeds = (Dictionary)data.Get("speeds"); + inline_mutations = (Array)data.Get("inline_mutations"); + time = (string)data.Get("time"); + tags = (Array)data.Get("tags"); + + foreach (var concurrent_line_data in (Array)data.Get("concurrent_lines")) + { + concurrent_lines.Add(new DialogueLine(concurrent_line_data)); + } + + foreach (var response in (Array)data.Get("responses")) + { + responses.Add(new DialogueResponse(response)); + } + } + + + public string GetTagValue(string tagName) + { + string wrapped = $"{tagName}="; + foreach (var tag in tags) + { + if (tag.StartsWith(wrapped)) + { + return tag.Substring(wrapped.Length); + } + } + return ""; + } + + public override string ToString() + { + switch (type) + { + case "dialogue": + return $""; + case "mutation": + return ""; + default: + return ""; + } + } + } + + + public partial class DialogueResponse : RefCounted + { + private string next_id = ""; + public string NextId + { + get => next_id; + set => next_id = value; + } + + private bool is_allowed = true; + public bool IsAllowed + { + get => is_allowed; + set => is_allowed = value; + } + + private string text = ""; + public string Text + { + get => text; + set => text = value; + } + + private string translation_key = ""; + public string TranslationKey + { + get => translation_key; + set => translation_key = value; + } + + private Array tags = new Array(); + public Array Tags + { + get => tags; + } + + public DialogueResponse(RefCounted data) + { + next_id = (string)data.Get("next_id"); + is_allowed = (bool)data.Get("is_allowed"); + text = (string)data.Get("text"); + translation_key = (string)data.Get("translation_key"); + tags = (Array)data.Get("tags"); + } + + public string GetTagValue(string tagName) + { + string wrapped = $"{tagName}="; + foreach (var tag in tags) + { + if (tag.StartsWith(wrapped)) + { + return tag.Substring(wrapped.Length); + } + } + return ""; + } + + public override string ToString() + { + return $" Error: + titles = {} + character_names = [] + + parse_line_tree(build_line_tree(inject_imported_files(text + "\n=> END", path))) + + # Convert the compiles lines to a Dictionary so they can be stored. + for id in lines: + var line: DMCompiledLine = lines[id] + data[id] = line.to_data() + + if errors.size() > 0: + return ERR_PARSE_ERROR + + return OK + + +## Inject any imported files +func inject_imported_files(text: String, path: String) -> PackedStringArray: + # Work out imports + var known_imports: Dictionary = {} + + # Include the base file path so that we can get around circular dependencies + known_imports[path.hash()] = "." + + var raw_lines: PackedStringArray = text.split("\n") + + for id in range(0, raw_lines.size()): + var line = raw_lines[id] + if is_import_line(line): + var import_data: Dictionary = extract_import_path_and_name(line) + + if not import_data.has("path"): continue + + var import_hash: int = import_data.path.hash() + if import_data.size() > 0: + # Keep track of titles so we can add imported ones later + if str(import_hash) in _imported_titles.keys(): + add_error(id, 0, DMConstants.ERR_FILE_ALREADY_IMPORTED) + if import_data.prefix in _imported_titles.values(): + add_error(id, 0, DMConstants.ERR_DUPLICATE_IMPORT_NAME) + _imported_titles[str(import_hash)] = import_data.prefix + + # Import the file content + if not known_imports.has(import_hash): + var error: Error = import_content(import_data.path, import_data.prefix, _imported_line_map, known_imports) + if error != OK: + add_error(id, 0, error) + + # Make a map so we can refer compiled lines to where they were imported from + if not _imported_line_map.has(import_hash): + _imported_line_map[import_hash] = { + hash = import_hash, + imported_on_line_number = id, + from_line = 0, + to_line = 0 + } + + var imported_content: String = "" + var cummulative_line_number: int = 0 + for item in _imported_line_map.values(): + item["from_line"] = cummulative_line_number + if known_imports.has(item.hash): + cummulative_line_number += known_imports[item.hash].split("\n").size() + item["to_line"] = cummulative_line_number + if known_imports.has(item.hash): + imported_content += known_imports[item.hash] + "\n" + + if imported_content == "": + _imported_line_count = 0 + return text.split("\n") + else: + _imported_line_count = cummulative_line_number + 1 + # Combine imported lines with the original lines + return (imported_content + "\n" + text).split("\n") + + +## Import content from another dialogue file or return an ERR +func import_content(path: String, prefix: String, imported_line_map: Dictionary, known_imports: Dictionary) -> Error: + if FileAccess.file_exists(path): + var file = FileAccess.open(path, FileAccess.READ) + var content: PackedStringArray = file.get_as_text().strip_edges().split("\n") + + for index in range(0, content.size()): + var line = content[index] + if is_import_line(line): + var import = extract_import_path_and_name(line) + if import.size() > 0: + if not known_imports.has(import.path.hash()): + # Add an empty record into the keys just so we don't end up with cyclic dependencies + known_imports[import.path.hash()] = "" + if import_content(import.path, import.prefix, imported_line_map, known_imports) != OK: + return ERR_LINK_FAILED + + if not imported_line_map.has(import.path.hash()): + # Make a map so we can refer compiled lines to where they were imported from + imported_line_map[import.path.hash()] = { + hash = import.path.hash(), + imported_on_line_number = index, + from_line = 0, + to_line = 0 + } + + _imported_titles[import.prefix] = import.path.hash() + + var origin_hash: int = -1 + for hash_value in known_imports.keys(): + if known_imports[hash_value] == ".": + origin_hash = hash_value + + # Replace any titles or jump points with references to the files they point to (event if they point to their own file) + for i in range(0, content.size()): + var line = content[i] + if line.strip_edges().begins_with("~ "): + var title = line.strip_edges().substr(2) + if "/" in line: + var bits = title.split("/") + content[i] = "~ %s/%s" % [_imported_titles[bits[0]], bits[1]] + else: + content[i] = "~ %s/%s" % [str(path.hash()), title] + + elif "=>< " in line: + var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() + if "/" in jump: + var bits: PackedStringArray = jump.split("/") + var title_hash: int = _imported_titles[bits[0]] + if title_hash == origin_hash: + content[i] = "%s=>< %s" % [line.split("=>< ")[0], bits[1]] + else: + content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], title_hash, bits[1]] + + elif not jump in ["END", "END!"]: + content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], str(path.hash()), jump] + + elif "=> " in line: + var jump: String = line.substr(line.find("=> ") + "=> ".length()).strip_edges() + if "/" in jump: + var bits: PackedStringArray = jump.split("/") + var title_hash: int = _imported_titles[bits[0]] + if title_hash == origin_hash: + content[i] = "%s=> %s" % [line.split("=> ")[0], bits[1]] + else: + content[i] = "%s=> %s/%s" % [line.split("=> ")[0], title_hash, bits[1]] + + elif not jump in ["END", "END!"]: + content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] + + imported_paths.append(path) + known_imports[path.hash()] = "\n".join(content) + "\n=> END\n" + return OK + else: + return ERR_FILE_NOT_FOUND + + +## Build a tree of parent/child relationships +func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: + var root: DMTreeLine = DMTreeLine.new("") + var parent_chain: Array[DMTreeLine] = [root] + var previous_line: DMTreeLine + var doc_comments: PackedStringArray = [] + + # Get list of known autoloads + var autoload_names: PackedStringArray = get_autoload_names() + + for i in range(0, raw_lines.size()): + var raw_line: String = raw_lines[i] + var tree_line: DMTreeLine = DMTreeLine.new(str(i - _imported_line_count)) + + tree_line.line_number = i + 1 + tree_line.type = get_line_type(raw_line) + tree_line.text = raw_line.strip_edges() + + # Handle any "using" directives. + if tree_line.type == DMConstants.TYPE_USING: + var using_match: RegExMatch = regex.USING_REGEX.search(raw_line) + if "state" in using_match.names: + var using_state: String = using_match.strings[using_match.names.state].strip_edges() + if not using_state in autoload_names: + add_error(tree_line.line_number, 0, DMConstants.ERR_UNKNOWN_USING) + elif not using_state in using_states: + using_states.append(using_state) + continue + # Ignore import lines because they've already been processed. + elif is_import_line(raw_line): + continue + + tree_line.indent = get_indent(raw_line) + + # Attach doc comments + if raw_line.strip_edges().begins_with("##"): + doc_comments.append(raw_line.replace("##", "").strip_edges()) + elif tree_line.type == DMConstants.TYPE_DIALOGUE: + tree_line.notes = "\n".join(doc_comments) + doc_comments.clear() + + # Empty lines are only kept so that we can work out groupings of things (eg. responses and + # randomised lines). Therefore we only need to keep one empty line in a row even if there + # are multiple. The indent of an empty line is assumed to be the same as the non-empty line + # following it. That way, grouping calculations should work. + if tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and raw_lines.size() > i + 1: + var next_line = raw_lines[i + 1] + if previous_line and previous_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: + continue + else: + tree_line.type = DMConstants.TYPE_UNKNOWN + tree_line.indent = get_indent(next_line) + + # Check for indentation changes + if tree_line.indent > parent_chain.size() - 1: + parent_chain.append(previous_line) + elif tree_line.indent < parent_chain.size() - 1: + parent_chain.resize(tree_line.indent + 1) + + # Add any titles to the list of known titles + if tree_line.type == DMConstants.TYPE_TITLE: + var title: String = tree_line.text.substr(2) + if title == "": + add_error(i, 2, DMConstants.ERR_EMPTY_TITLE) + elif titles.has(title): + add_error(i, 2, DMConstants.ERR_DUPLICATE_TITLE) + else: + titles[title] = tree_line.id + if "/" in title: + # Replace the hash title with something human readable. + var bits: PackedStringArray = title.split("/") + if _imported_titles.has(bits[0]): + title = _imported_titles[bits[0]] + "/" + bits[1] + titles[title] = tree_line.id + elif first_title == "" and i >= _imported_line_count: + first_title = tree_line.id + + # Append the current line to the current parent (note: the root is the most basic parent). + var parent: DMTreeLine = parent_chain[parent_chain.size() - 1] + tree_line.parent = weakref(parent) + parent.children.append(tree_line) + + previous_line = tree_line + + return root + + +#endregion + +#region Parsing + + +func parse_line_tree(root: DMTreeLine, parent: DMCompiledLine = null) -> Array[DMCompiledLine]: + var compiled_lines: Array[DMCompiledLine] = [] + + for i in range(0, root.children.size()): + var tree_line: DMTreeLine = root.children[i] + var line: DMCompiledLine = DMCompiledLine.new(tree_line.id, tree_line.type) + + match line.type: + DMConstants.TYPE_UNKNOWN: + line.next_id = get_next_matching_sibling_id(root.children, i, parent, _first) + + DMConstants.TYPE_TITLE: + parse_title_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_CONDITION: + parse_condition_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_WHILE: + parse_while_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_MATCH: + parse_match_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_WHEN: + parse_when_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_MUTATION: + parse_mutation_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_GOTO: + # Extract any weighted random calls before parsing dialogue + if tree_line.text.begins_with("%"): + parse_random_line(tree_line, line, root.children, i, parent) + parse_goto_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_RESPONSE: + parse_response_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_RANDOM: + parse_random_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_DIALOGUE: + # Extract any weighted random calls before parsing dialogue + if tree_line.text.begins_with("%"): + parse_random_line(tree_line, line, root.children, i, parent) + parse_dialogue_line(tree_line, line, root.children, i, parent) + + # Main line map is keyed by ID + lines[line.id] = line + + # Returned lines order is preserved so that it can be used for compiling children + compiled_lines.append(line) + + return compiled_lines + + +## Parse a title and apply it to the given line +func parse_title_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + line.text = tree_line.text.substr(tree_line.text.find("~ ") + 2).strip_edges() + + # Titles can't have numbers as the first letter (unless they are external titles which get replaced with hashes) + if tree_line.line_number >= _imported_line_count and regex.BEGINS_WITH_NUMBER_REGEX.search(line.text): + result = add_error(tree_line.line_number, 2, DMConstants.ERR_TITLE_BEGINS_WITH_NUMBER) + + # Only import titles are allowed to have "/" in them + var valid_title = regex.VALID_TITLE_REGEX.search(line.text.replace("/", "")) + if not valid_title: + result = add_error(tree_line.line_number, 2, DMConstants.ERR_TITLE_INVALID_CHARACTERS) + + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + ## Update the titles reference to point to the actual first line + titles[line.text] = line.next_id + + ## Update any lines that point to this title + if _goto_lines.has(line.text): + for goto_line in _goto_lines[line.text]: + goto_line.next_id = line.next_id + + return result + + +## Parse a goto and apply it to the given line. +func parse_goto_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Work out where this line is jumping to. + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(tree_line.text, titles) + if goto_data.error: + return add_error(tree_line.line_number, tree_line.indent + 2, goto_data.error) + if goto_data.next_id or goto_data.expression: + line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + add_reference_to_title(goto_data.title, line) + + if goto_data.is_snippet: + line.is_snippet = true + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + return OK + + +## Parse a condition line and apply to the given line +func parse_condition_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Work out the next IDs before parsing the condition line itself so that the last + # child can inherit from the chain. + + # Find the next conditional sibling that is part of this grouping (if there is one). + for next_sibling: DMTreeLine in siblings.slice(sibling_index + 1): + if not next_sibling.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_CONDITION]: + break + elif next_sibling.type == DMConstants.TYPE_CONDITION: + if next_sibling.text.begins_with("el"): + line.next_sibling_id = next_sibling.id + break + else: + break + + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # The next line that isn't a conditional or is a new "if" + return s.type != DMConstants.TYPE_CONDITION or s.text.begins_with("if ") + ) + # Any empty IDs should end the conversation. + if line.next_id_after == DMConstants.ID_NULL: + line.next_id_after = parent.next_id_after if parent != null and parent.next_id_after else DMConstants.ID_END + + # Having no nested body is an immediate failure. + if tree_line.children.size() == 0: + return add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # Try to parse the conditional expression ("else" has no expression). + if "if " in tree_line.text: + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + return add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Parse any nested body lines + parse_children(tree_line, line) + + return OK + + +## Parse a while loop and apply it to the given line. +func parse_while_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + # Parse the while condition + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + return add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Parse the nested body (it should take care of looping back to this line when it finishes) + parse_children(tree_line, line) + + return OK + + +func parse_match_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # The next line after is the next sibling + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + # Extract the condition to match to + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Match statements should have children + if tree_line.children.size() == 0: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # Check that all children are when or else. + for child in tree_line.children: + if child.type == DMConstants.TYPE_WHEN: continue + if child.type == DMConstants.TYPE_CONDITION and child.text.begins_with("else"): continue + + result = add_error(child.line_number, child.indent, DMConstants.ERR_EXPECTED_WHEN_OR_ELSE) + + # Each child should be a "when" or "else". We don't need those lines themselves, just their + # condition and the line they point to if the conditions passes. + var children: Array[DMCompiledLine] = parse_children(tree_line, line) + for child: DMCompiledLine in children: + # "when" cases + if child.type == DMConstants.TYPE_WHEN: + line.siblings.append({ + condition = child.expression, + next_id = child.next_id + }) + # "else" case + elif child.type == DMConstants.TYPE_CONDITION: + if line.siblings.any(func(s): return s.has("is_else")): + result = add_error(child.line_number, child.indent, DMConstants.ERR_ONLY_ONE_ELSE_ALLOWED) + else: + line.siblings.append({ + next_id = child.next_id, + is_else = true + }) + # Remove the line from the list of all lines because we don't need it any more. + lines.erase(child.id) + + return result + + +func parse_when_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # This when line should be found inside a match line + if parent.type != DMConstants.TYPE_MATCH: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_WHEN_MUST_BELONG_TO_MATCH) + + # When lines should have children + if tree_line.children.size() == 0: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # The next line after a when is the same as its parent match line + line.next_id_after = parent.next_id_after + + # Extract the condition to match to + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + parse_children(tree_line, line) + + return result + + +## Parse a mutation line and apply it to the given line +func parse_mutation_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var mutation: Dictionary = extract_mutation(tree_line.text) + if mutation.has("error"): + return add_error(tree_line.line_number, mutation.index, mutation.error) + else: + line.expression = mutation + + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + return OK + + +## Parse a response and apply it to the given line. +func parse_response_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # Remove the "- " + tree_line.text = tree_line.text.substr(2) + + # Extract the static line ID + var static_line_id: String = extract_static_line_id(tree_line.text) + if static_line_id: + tree_line.text = tree_line.text.replace("[ID:%s]" % [static_line_id], "") + line.translation_key = static_line_id + + # Handle conditional responses and remove them from the prompt text. + if " [if " in tree_line.text: + var condition = extract_condition(tree_line.text, true, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + tree_line.text = regex.WRAPPED_CONDITION_REGEX.sub(tree_line.text, "").strip_edges() + + # Find the original response in this group of responses. + var original_response: DMTreeLine = tree_line + for i in range(sibling_index - 1, 0, -1): + if siblings[i].type == DMConstants.TYPE_RESPONSE: + original_response = siblings[i] + elif siblings[i].type != DMConstants.TYPE_UNKNOWN: + break + + # If it's the original response then set up an original line. + if original_response == tree_line: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, (func(s: DMTreeLine): + # The next line that isn't a response. + return not s.type in [DMConstants.TYPE_RESPONSE, DMConstants.TYPE_UNKNOWN] + ), true) + line.responses = [line.id] + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the same ID for after the random group. + else: + line.next_id = line.next_id_after + # Otherwise let the original line know about it. + else: + var original_line: DMCompiledLine = lines[original_response.id] + line.next_id_after = original_line.next_id_after + line.siblings = original_line.siblings + original_line.responses.append(line.id) + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the original line's next ID after. + else: + line.next_id = original_line.next_id_after + + parse_character_and_dialogue(tree_line, line, siblings, sibling_index, parent) + + return OK + + +## Parse a randomised line +func parse_random_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Find the weight + var weight: float = 1 + var found = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.search(tree_line.text + " ") + var condition: Dictionary = {} + if found: + if found.names.has("weight"): + weight = found.strings[found.names.weight].to_float() + if found.names.has("condition"): + condition = extract_condition(tree_line.text, true, tree_line.indent) + + # Find the original random sibling. It will be the jump off point. + var original_sibling: DMTreeLine = tree_line + for i in range(sibling_index - 1, -1, -1): + if siblings[i] and siblings[i].is_random: + original_sibling = siblings[i] + else: + break + + var weighted_sibling: Dictionary = { weight = weight, id = line.id, condition = condition } + + # If it's the original sibling then set up an original line. + if original_sibling == tree_line: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, (func(s: DMTreeLine): + # The next line that isn't a randomised line. + # NOTE: DMTreeLine.is_random won't be set at this point so we need to check for the "%" prefix. + return not s.text.begins_with("%") + ), true) + line.siblings = [weighted_sibling] + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the same ID for after the random group. + else: + line.next_id = line.next_id_after + + # Otherwise let the original line know about it. + else: + var original_line: DMCompiledLine = lines[original_sibling.id] + line.next_id_after = original_line.next_id_after + line.siblings = original_line.siblings + original_line.siblings.append(weighted_sibling) + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the original line's next ID after. + else: + line.next_id = original_line.next_id_after + + # Remove the randomise syntax from the line. + tree_line.text = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(tree_line.text, "") + tree_line.is_random = true + + return OK + + +## Parse some dialogue and apply it to the given line. +func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # Remove escape character + if tree_line.text.begins_with("\\using"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\if"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\elif"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\else"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\while"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\match"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\when"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\do"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\set"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\-"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\~"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\=>"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\%"): tree_line.text = tree_line.text.substr(1) + + # Append any further dialogue + for i in range(0, tree_line.children.size()): + var child: DMTreeLine = tree_line.children[i] + if child.type == DMConstants.TYPE_DIALOGUE: + tree_line.text += "\n" + child.text + else: + result = add_error(child.line_number, child.indent, DMConstants.ERR_INVALID_INDENTATION) + + # Extract the static line ID + var static_line_id: String = extract_static_line_id(tree_line.text) + if static_line_id: + tree_line.text = tree_line.text.replace("[ID:%s]" % [static_line_id], "") + line.translation_key = static_line_id + + # Check for simultaneous lines + if tree_line.text.begins_with("| "): + # Jumps are only allowed on the origin line. + if " =>" in tree_line.text: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES) + # Check for a valid previous line. + tree_line.text = tree_line.text.substr(2) + var previous_sibling: DMTreeLine = siblings[sibling_index - 1] + if previous_sibling.type != DMConstants.TYPE_DIALOGUE: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_CONCURRENT_LINE_WITHOUT_ORIGIN) + else: + # Because the previous line's concurrent_lines array is the same as + # any line before that this doesn't need to check any higher up. + var previous_line: DMCompiledLine = lines[previous_sibling.id] + previous_line.concurrent_lines.append(line.id) + line.concurrent_lines = previous_line.concurrent_lines + + parse_character_and_dialogue(tree_line, line, siblings, sibling_index, parent) + + # Check for any inline expression errors + var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("") + var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(tree_line.text, true, true) + for bbcode: Dictionary in bbcodes: + var tag: String = bbcode.code + var code: String = bbcode.raw_args + if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"): + var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length()) + if expression.size() == 0: + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION) + elif expression[0].type == DMConstants.TYPE_ERROR: + add_error(tree_line.line_number, tree_line.indent + expression[0].index, expression[0].value) + + # If the line isn't part of a weighted random group then make it point to the next + # available sibling. + if line.next_id == DMConstants.ID_NULL and line.siblings.size() == 0: + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # Ignore concurrent lines. + return not s.text.begins_with("| ") + ) + + return result + + +## Parse the character name and dialogue and apply it to a given line. +func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + var text: String = tree_line.text + + # Attach any doc comments. + line.notes = tree_line.notes + + # Extract tags. + var tag_data: DMResolvedTagData = DMResolvedTagData.new(text) + line.tags = tag_data.tags + text = tag_data.text_without_tags + + # Handle inline gotos and remove them from the prompt text. + if " =><" in text: + # Because of when the return point needs to be known at runtime we need to split + # this line into two (otherwise the return point would be dependent on the balloon). + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, titles) + if goto_data.error: + result = add_error(tree_line.line_number, tree_line.indent + 3, goto_data.error) + if goto_data.next_id or goto_data.expression: + text = goto_data.text_without_goto + var goto_line: DMCompiledLine = DMCompiledLine.new(line.id + ".1", DMConstants.TYPE_GOTO) + goto_line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + if line.type == DMConstants.TYPE_RESPONSE: + goto_line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # If this is coming from a response then we want the next non-response line. + return s.type != DMConstants.TYPE_RESPONSE + ) + else: + goto_line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + goto_line.is_snippet = true + lines[goto_line.id] = goto_line + line.next_id = goto_line.id + add_reference_to_title(goto_data.title, goto_line) + elif " =>" in text: + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, titles) + if goto_data.error: + result = add_error(tree_line.line_number, tree_line.indent + 2, goto_data.error) + if goto_data.next_id or goto_data.expression: + text = goto_data.text_without_goto + line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + add_reference_to_title(goto_data.title, line) + + # Handle the dialogue. + text = text.replace("\\:", "!ESCAPED_COLON!") + if ": " in text: + # If a character was given then split it out. + var bits = Array(text.strip_edges().split(": ")) + line.character = bits.pop_front().strip_edges() + if not line.character in character_names: + character_names.append(line["character"]) + # Character names can have expressions in them. + line.character_replacements = expression_parser.extract_replacements(line.character, tree_line.indent) + for replacement in line.character_replacements: + if replacement.has("error"): + result = add_error(tree_line.line_number, replacement.index, replacement.error) + text = ": ".join(bits).replace("!ESCAPED_COLON!", ":") + else: + line.character = "" + text = text.replace("!ESCAPED_COLON!", ":") + + # Extract any expressions in the dialogue. + line.text_replacements = expression_parser.extract_replacements(text, line.character.length() + 2 + tree_line.indent) + for replacement in line.text_replacements: + if replacement.has("error"): + result = add_error(tree_line.line_number, replacement.index, replacement.error) + + # Replace any newlines. + text = text.replace("\\n", "\n").strip_edges() + + # If there was no manual translation key then just use the text itself + if line.translation_key == "": + line.translation_key = text + + line.text = text + + # IDs can't be duplicated for text that doesn't match. + if line.translation_key != "": + if _known_translation_keys.has(line.translation_key) and _known_translation_keys.get(line.translation_key) != line.text: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_DUPLICATE_ID) + else: + _known_translation_keys[line.translation_key] = line.text + # Show an error if missing translations is enabled + elif DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) + + return result + + +#endregion + +#region Errors + + +## Add a compilation error to the list. Returns the given error code. +func add_error(line_number: int, column_number: int, error: int) -> Error: + # See if the error was in an imported file + for item in _imported_line_map.values(): + if line_number < item.to_line: + errors.append({ + line_number = item.imported_on_line_number, + column_number = 0, + error = DMConstants.ERR_ERRORS_IN_IMPORTED_FILE, + external_error = error, + external_line_number = line_number + }) + return error + + # Otherwise, it's in this file + errors.append({ + line_number = line_number - _imported_line_count, + column_number = column_number, + error = error + }) + + return error + + +#endregion + +#region Helpers + + +## Get the names of any autoloads in the project. +func get_autoload_names() -> PackedStringArray: + var autoloads: PackedStringArray = [] + + var project = ConfigFile.new() + project.load("res://project.godot") + if project.has_section("autoload"): + return Array(project.get_section_keys("autoload")).filter(func(key): return key != "DialogueManager") + + return autoloads + + +## Check if a line is importing another file. +func is_import_line(text: String) -> bool: + return text.begins_with("import ") and " as " in text + + +## Extract the import information from an import line +func extract_import_path_and_name(line: String) -> Dictionary: + var found: RegExMatch = regex.IMPORT_REGEX.search(line) + if found: + return { + path = found.strings[found.names.path], + prefix = found.strings[found.names.prefix] + } + else: + return {} + + +## Get the indent of a raw line +func get_indent(raw_line: String) -> int: + var tabs: RegExMatch = regex.INDENT_REGEX.search(raw_line) + if tabs: + return tabs.get_string().length() + else: + return 0 + + +## Get the type of a raw line +func get_line_type(raw_line: String) -> String: + raw_line = raw_line.strip_edges() + var text: String = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line + " ", "").strip_edges() + + if text.begins_with("import "): + return DMConstants.TYPE_IMPORT + + if text.begins_with("using "): + return DMConstants.TYPE_USING + + if text.begins_with("#"): + return DMConstants.TYPE_COMMENT + + if text.begins_with("~ "): + return DMConstants.TYPE_TITLE + + if text.begins_with("if ") or text.begins_with("elif") or text.begins_with("else"): + return DMConstants.TYPE_CONDITION + + if text.begins_with("while "): + return DMConstants.TYPE_WHILE + + if text.begins_with("match "): + return DMConstants.TYPE_MATCH + + if text.begins_with("when "): + return DMConstants.TYPE_WHEN + + if text.begins_with("do ") or text.begins_with("do! ") or text.begins_with("set "): + return DMConstants.TYPE_MUTATION + + if text.begins_with("=> ") or text.begins_with("=>< "): + return DMConstants.TYPE_GOTO + + if text.begins_with("- "): + return DMConstants.TYPE_RESPONSE + + if raw_line.begins_with("%") and text.is_empty(): + return DMConstants.TYPE_RANDOM + + if not text.is_empty(): + return DMConstants.TYPE_DIALOGUE + + return DMConstants.TYPE_UNKNOWN + + +## Get the next sibling that passes a [Callable] matcher. +func get_next_matching_sibling_id(siblings: Array[DMTreeLine], from_index: int, parent: DMCompiledLine, matcher: Callable, with_empty_lines: bool = false) -> String: + for i in range(from_index + 1, siblings.size()): + var next_sibling: DMTreeLine = siblings[i] + + if not with_empty_lines: + # Ignore empty lines + if not next_sibling or next_sibling.type == DMConstants.TYPE_UNKNOWN: + continue + + if matcher.call(next_sibling): + return next_sibling.id + + # If no next ID can be found then check the parent for where to go next. + if parent != null: + return parent.id if parent.type == DMConstants.TYPE_WHILE else parent.next_id_after + + return DMConstants.ID_NULL + + +## Extract a static line ID from some text. +func extract_static_line_id(text: String) -> String: + # Find a static translation key, eg. [ID:something] + var found: RegExMatch = regex.STATIC_LINE_ID_REGEX.search(text) + if found: + return found.strings[found.names.id] + else: + return "" + + +## Extract a condition (or inline condition) from some text. +func extract_condition(text: String, is_wrapped: bool, index: int) -> Dictionary: + var regex: RegEx = regex.WRAPPED_CONDITION_REGEX if is_wrapped else regex.CONDITION_REGEX + var found: RegExMatch = regex.search(text) + + if found == null: + return { + index = 0, + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + var raw_condition: String = found.strings[found.names.expression] + if raw_condition.ends_with(":"): + raw_condition = raw_condition.substr(0, raw_condition.length() - 1) + + var expression: Array = expression_parser.tokenise(raw_condition, DMConstants.TYPE_CONDITION, index + found.get_start("expression")) + + if expression.size() == 0: + return { + index = index + found.get_start("expression"), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + return { + index = expression[0].index, + error = expression[0].value + } + else: + return { + expression = expression + } + + +## Extract a mutation from some text. +func extract_mutation(text: String) -> Dictionary: + var found: RegExMatch = regex.MUTATION_REGEX.search(text) + + if not found: + return { + index = 0, + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + if found.names.has("expression"): + var expression: Array = expression_parser.tokenise(found.strings[found.names.expression], DMConstants.TYPE_MUTATION, found.get_start("expression")) + if expression.size() == 0: + return { + index = found.get_start("expression"), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + return { + index = expression[0].index, + error = expression[0].value + } + else: + return { + expression = expression, + is_blocking = not "!" in found.strings[found.names.keyword] + } + + else: + return { + index = found.get_start(), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + +## Keep track of lines referencing titles because their own next_id might not have been resolved yet. +func add_reference_to_title(title: String, line: DMCompiledLine) -> void: + if title in [DMConstants.ID_END, DMConstants.ID_END_CONVERSATION, DMConstants.ID_NULL]: return + + if not _goto_lines.has(title): + _goto_lines[title] = [] + _goto_lines[title].append(line) + + +## Parse a nested block of child lines +func parse_children(tree_line: DMTreeLine, line: DMCompiledLine) -> Array[DMCompiledLine]: + var children = parse_line_tree(tree_line, line) + if children.size() > 0: + line.next_id = children.front().id + # The last child should jump to the next line after its parent condition group + var last_child: DMCompiledLine = children.back() + if last_child.next_id == DMConstants.ID_NULL: + last_child.next_id = line.next_id_after + if last_child.siblings.size() > 0: + for sibling in last_child.siblings: + lines.get(sibling.id).next_id = last_child.next_id + + return children + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compilation.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compilation.gd.uid new file mode 100644 index 00000000..24a13eeb --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compilation.gd.uid @@ -0,0 +1 @@ +uid://dsgpnyqg6cprg diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd new file mode 100644 index 00000000..972fd5cb --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd @@ -0,0 +1,157 @@ +## A compiled line of dialogue. +class_name DMCompiledLine extends RefCounted + + +## The ID of the line +var id: String +## The translation key (or static line ID). +var translation_key: String = "" +## The type of line. +var type: String = "" +## The character name. +var character: String = "" +## Any interpolation expressions for the character name. +var character_replacements: Array[Dictionary] = [] +## The text of the line. +var text: String = "" +## Any interpolation expressions for the text. +var text_replacements: Array[Dictionary] = [] +## Any response siblings associated with this line. +var responses: PackedStringArray = [] +## Any randomise or case siblings for this line. +var siblings: Array[Dictionary] = [] +## Any lines said simultaneously. +var concurrent_lines: PackedStringArray = [] +## Any tags on this line. +var tags: PackedStringArray = [] +## The condition or mutation expression for this line. +var expression: Dictionary = {} +## The next sequential line to go to after this line. +var next_id: String = "" +## The next line to go to after this line if it is unknown and compile time. +var next_id_expression: Array[Dictionary] = [] +## Whether this jump line should return after the jump target sequence has ended. +var is_snippet: bool = false +## The ID of the next sibling line. +var next_sibling_id: String = "" +## The ID after this line if it belongs to a block (eg. conditions). +var next_id_after: String = "" +## Any doc comments attached to this line. +var notes: String = "" + + +#region Hooks + + +func _init(initial_id: String, initial_type: String) -> void: + id = initial_id + type = initial_type + + +func _to_string() -> String: + var s: Array = [ + "[%s]" % [type], + "%s:" % [character] if character != "" else null, + text if text != "" else null, + expression if expression.size() > 0 else null, + "[%s]" % [",".join(tags)] if tags.size() > 0 else null, + str(siblings) if siblings.size() > 0 else null, + str(responses) if responses.size() > 0 else null, + "=> END" if "end" in next_id else "=> %s" % [next_id], + "(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null, + "(==> %s)" % [next_id_after] if next_id_after != "" else null, + ].filter(func(item): return item != null) + + return " ".join(s) + + +#endregion + +#region Helpers + + +## Express this line as a [Dictionary] that can be stored in a resource. +func to_data() -> Dictionary: + var d: Dictionary = { + id = id, + type = type, + next_id = next_id + } + + if next_id_expression.size() > 0: + d.next_id_expression = next_id_expression + + match type: + DMConstants.TYPE_CONDITION: + d.condition = expression + if not next_sibling_id.is_empty(): + d.next_sibling_id = next_sibling_id + d.next_id_after = next_id_after + + DMConstants.TYPE_WHILE: + d.condition = expression + d.next_id_after = next_id_after + + DMConstants.TYPE_MATCH: + d.condition = expression + d.next_id_after = next_id_after + d.cases = siblings + + DMConstants.TYPE_MUTATION: + d.mutation = expression + + DMConstants.TYPE_GOTO: + d.is_snippet = is_snippet + d.next_id_after = next_id_after + if not siblings.is_empty(): + d.siblings = siblings + + DMConstants.TYPE_RANDOM: + d.siblings = siblings + + DMConstants.TYPE_RESPONSE: + d.text = text + + if not responses.is_empty(): + d.responses = responses + + if translation_key != text: + d.translation_key = translation_key + if not expression.is_empty(): + d.condition = expression + if not character.is_empty(): + d.character = character + if not character_replacements.is_empty(): + d.character_replacements = character_replacements + if not text_replacements.is_empty(): + d.text_replacements = text_replacements + if not tags.is_empty(): + d.tags = tags + if not notes.is_empty(): + d.notes = notes + + DMConstants.TYPE_DIALOGUE: + d.text = text + + if translation_key != text: + d.translation_key = translation_key + + if not character.is_empty(): + d.character = character + if not character_replacements.is_empty(): + d.character_replacements = character_replacements + if not text_replacements.is_empty(): + d.text_replacements = text_replacements + if not tags.is_empty(): + d.tags = tags + if not notes.is_empty(): + d.notes = notes + if not siblings.is_empty(): + d.siblings = siblings + if not concurrent_lines.is_empty(): + d.concurrent_lines = concurrent_lines + + return d + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd.uid new file mode 100644 index 00000000..17ec55e9 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd.uid @@ -0,0 +1 @@ +uid://dg8j5hudp4210 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd new file mode 100644 index 00000000..a370ef6a --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd @@ -0,0 +1,51 @@ +## A compiler of Dialogue Manager dialogue. +class_name DMCompiler extends RefCounted + + +## Compile a dialogue script. +static func compile_string(text: String, path: String) -> DMCompilerResult: + var compilation: DMCompilation = DMCompilation.new() + compilation.compile(text, path) + + var result: DMCompilerResult = DMCompilerResult.new() + result.imported_paths = compilation.imported_paths + result.using_states = compilation.using_states + result.character_names = compilation.character_names + result.titles = compilation.titles + result.first_title = compilation.first_title + result.errors = compilation.errors + result.lines = compilation.data + result.raw_text = text + + return result + + +## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants]. +static func get_line_type(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + return compilation.get_line_type(text) + + +## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text. +static func get_static_line_id(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + return compilation.extract_static_line_id(text) + + +## Get the translatable part of a line. +static func extract_translatable_string(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + + var tree_line = DMTreeLine.new("") + tree_line.text = text + var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text)) + compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null) + + return line.text + + +## Get the known titles in a dialogue script. +static func get_titles_in_text(text: String, path: String) -> Dictionary: + var compilation: DMCompilation = DMCompilation.new() + compilation.build_line_tree(compilation.inject_imported_files(text, path)) + return compilation.titles diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd.uid new file mode 100644 index 00000000..e041f104 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler.gd.uid @@ -0,0 +1 @@ +uid://chtfdmr0cqtp4 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd new file mode 100644 index 00000000..ead998ba --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd @@ -0,0 +1,49 @@ +## A collection of [RegEx] for use by the [DMCompiler]. +class_name DMCompilerRegEx extends RefCounted + + +var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?[^\"]+)\" as (?[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)") +var USING_REGEX: RegEx = RegEx.create_from_string("^using (?.*)$") +var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+") +var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$") +var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d") +var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?.*)\\:?") +var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?.*)\\]") +var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?do|do!|set) (?.*)") +var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?.*?)\\]") +var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?[\\d.]+)?( \\[if (?.+?)\\])? ") +var GOTO_REGEX: RegEx = RegEx.create_from_string("=>.*)") + +var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?.*?)\\]\\]") +var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?.+?)\\](?.*?)\\[\\/if\\]") + +var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?.*?)\\]") + +var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}") + +var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+") + +var TOKEN_DEFINITIONS: Dictionary = { + DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("), + DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["), + DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("), + DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"), + DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["), + DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"), + DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"), + DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"), + DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"), + DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"), + DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"), + DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), + DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), + DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), + DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), + DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"), + DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), + DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"), + DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"), + DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"), + DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), + DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)") +} diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd.uid new file mode 100644 index 00000000..bd969dfa --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd.uid @@ -0,0 +1 @@ +uid://d3tvcrnicjibp diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd new file mode 100644 index 00000000..acbf60f7 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd @@ -0,0 +1,27 @@ +## The result of using the [DMCompiler] to compile some dialogue. +class_name DMCompilerResult extends RefCounted + + +## Any paths that were imported into the compiled dialogue file. +var imported_paths: PackedStringArray = [] + +## Any "using" directives. +var using_states: PackedStringArray = [] + +## All titles in the file and the line they point to. +var titles: Dictionary = {} + +## The first title in the file. +var first_title: String = "" + +## All character names. +var character_names: PackedStringArray = [] + +## Any compilation errors. +var errors: Array[Dictionary] = [] + +## A map of all compiled lines. +var lines: Dictionary = {} + +## The raw dialogue text. +var raw_text: String = "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd.uid new file mode 100644 index 00000000..f1f76fd0 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_result.gd.uid @@ -0,0 +1 @@ +uid://dmk74tknimqvg diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd new file mode 100644 index 00000000..384340fc --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd @@ -0,0 +1,497 @@ +## A class for parsing a condition/mutation expression for use with the [DMCompiler]. +class_name DMExpressionParser extends RefCounted + + +# Reference to the common [RegEx] that the parser needs. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +## Break a string down into an expression. +func tokenise(text: String, line_type: String, index: int) -> Array: + var tokens: Array[Dictionary] = [] + var limit: int = 0 + while text.strip_edges() != "" and limit < 1000: + limit += 1 + var found = _find_match(text) + if found.size() > 0: + tokens.append({ + index = index, + type = found.type, + value = found.value + }) + index += found.value.length() + text = found.remaining_text + elif text.begins_with(" "): + index += 1 + text = text.substr(1) + else: + return _build_token_tree_error(DMConstants.ERR_INVALID_EXPRESSION, index) + + return _build_token_tree(tokens, line_type, "")[0] + + +## Extract any expressions from some text +func extract_replacements(text: String, index: int) -> Array[Dictionary]: + var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text) + + if founds == null or founds.size() == 0: + return [] + + var replacements: Array[Dictionary] = [] + for found in founds: + var replacement: Dictionary = {} + var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2) + + # If there are closing curlie hard-up against the end of a {{...}} block then check for further + # curlies just outside of the block. + var text_suffix: String = text.substr(found.get_end(0)) + var expression_suffix: String = "" + while text_suffix.begins_with("}"): + expression_suffix += "}" + text_suffix = text_suffix.substr(1) + value_in_text += expression_suffix + + var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1)) + if expression.size() == 0: + replacement = { + index = index + found.get_start(1), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + replacement = { + index = expression[0].index, + error = expression[0].value + } + else: + replacement = { + value_in_text = "{{%s}}" % value_in_text, + expression = expression + } + replacements.append(replacement) + + return replacements + + +#region Helpers + + +# Create a token that represents an error. +func _build_token_tree_error(error: int, index: int) -> Array: + return [{ type = DMConstants.TOKEN_ERROR, value = error, index = index }] + + +# Convert a list of tokens into an abstract syntax tree. +func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array: + var tree: Array[Dictionary] = [] + var limit = 0 + while tokens.size() > 0 and limit < 1000: + limit += 1 + var token = tokens.pop_front() + + var error = _check_next_token(token, tokens, line_type, expected_close_token) + if error != OK: + var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token + return [_build_token_tree_error(error, error_token.index), tokens] + + match token.type: + DMConstants.TOKEN_FUNCTION: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + + tree.append({ + type = DMConstants.TOKEN_FUNCTION, + # Consume the trailing "(" + function = token.value.substr(0, token.value.length() - 1), + value = _tokens_to_list(sub_tree[0]), + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_DICTIONARY_REFERENCE: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + + var args = _tokens_to_list(sub_tree[0]) + if args.size() != 1: + return [_build_token_tree_error(DMConstants.ERR_INVALID_INDEX, token.index), tokens] + + tree.append({ + type = DMConstants.TOKEN_DICTIONARY_REFERENCE, + # Consume the trailing "[" + variable = token.value.substr(0, token.value.length() - 1), + value = args[0], + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_BRACE_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + + var t = sub_tree[0] + for i in range(0, t.size() - 2): + # Convert Lua style dictionaries to string keys + if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT: + t[i].type = DMConstants.TOKEN_STRING + t[i+1].type = DMConstants.TOKEN_COLON + t[i+1].erase("value") + + tree.append({ + type = DMConstants.TOKEN_DICTIONARY, + value = _tokens_to_dictionary(sub_tree[0]), + i = token.index + }) + + tokens = sub_tree[1] + + DMConstants.TOKEN_BRACKET_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + + var type = DMConstants.TOKEN_ARRAY + var value = _tokens_to_list(sub_tree[0]) + + # See if this is referencing a nested dictionary value + if tree.size() > 0: + var previous_token = tree[tree.size() - 1] + if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]: + type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE + value = value[0] + + tree.append({ + type = type, + value = value, + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_PARENS_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + + tree.append({ + type = DMConstants.TOKEN_GROUP, + value = sub_tree[0], + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_PARENS_CLOSE, \ + DMConstants.TOKEN_BRACE_CLOSE, \ + DMConstants.TOKEN_BRACKET_CLOSE: + if token.type != expected_close_token: + return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] + + tree.append({ + type = token.type, + i = token.index + }) + + return [tree, tokens] + + DMConstants.TOKEN_NOT: + # Double nots negate each other + if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT: + tokens.pop_front() + else: + tree.append({ + type = token.type, + i = token.index + }) + + DMConstants.TOKEN_COMMA, \ + DMConstants.TOKEN_COLON, \ + DMConstants.TOKEN_DOT: + tree.append({ + type = token.type, + i = token.index + }) + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_ASSIGNMENT, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_AND_OR, \ + DMConstants.TOKEN_VARIABLE: + var value = token.value.strip_edges() + if value == "&&": + value = "and" + elif value == "||": + value = "or" + tree.append({ + type = token.type, + value = value, + i = token.index + }) + + DMConstants.TOKEN_STRING: + if token.value.begins_with("&"): + tree.append({ + type = token.type, + value = StringName(token.value.substr(2, token.value.length() - 3)), + i = token.index + }) + else: + tree.append({ + type = token.type, + value = token.value.substr(1, token.value.length() - 2), + i = token.index + }) + + DMConstants.TOKEN_CONDITION: + return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] + + DMConstants.TOKEN_BOOL: + tree.append({ + type = token.type, + value = token.value.to_lower() == "true", + i = token.index + }) + + DMConstants.TOKEN_NUMBER: + var value = token.value.to_float() if "." in token.value else token.value.to_int() + # If previous token is a number and this one is a negative number then + # inject a minus operator token in between them. + if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER: + tree.append(({ + type = DMConstants.TOKEN_OPERATOR, + value = "-", + i = token.index + })) + tree.append({ + type = token.type, + value = -1 * value, + i = token.index + }) + else: + tree.append({ + type = token.type, + value = value, + i = token.index + }) + + if expected_close_token != "": + var index: int = tokens[0].index if tokens.size() > 0 else 0 + return [_build_token_tree_error(DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] + + return [tree, tokens] + + +# Check the next token to see if it is valid to follow this one. +func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error: + var next_token: Dictionary = { type = null } + if next_tokens.size() > 0: + next_token = next_tokens.front() + + # Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary + # then it's an unexpected assignment in a condition line. + if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token): + return DMConstants.ERR_UNEXPECTED_ASSIGNMENT + + # Special case for a negative number after this one + if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"): + return OK + + var expected_token_types = [] + var unexpected_token_types = [] + match token.type: + DMConstants.TOKEN_FUNCTION, \ + DMConstants.TOKEN_PARENS_OPEN: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_BRACKET_CLOSE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE + ] + + DMConstants.TOKEN_BRACE_OPEN: + expected_token_types = [ + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_BRACE_CLOSE + ] + + DMConstants.TOKEN_PARENS_CLOSE, \ + DMConstants.TOKEN_BRACE_CLOSE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE + ] + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_COMMA, \ + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NOT, \ + DMConstants.TOKEN_AND_OR, \ + DMConstants.TOKEN_DICTIONARY_REFERENCE: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_COLON: + unexpected_token_types = [ + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_BOOL, \ + DMConstants.TOKEN_STRING, \ + DMConstants.TOKEN_NUMBER: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_FUNCTION, + DMConstants.TOKEN_PARENS_OPEN, + DMConstants.TOKEN_BRACE_OPEN, + DMConstants.TOKEN_BRACKET_OPEN + ] + + DMConstants.TOKEN_VARIABLE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_FUNCTION, + DMConstants.TOKEN_PARENS_OPEN, + DMConstants.TOKEN_BRACE_OPEN, + DMConstants.TOKEN_BRACKET_OPEN + ] + + if (expected_token_types.size() > 0 and not next_token.type in expected_token_types or unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): + match next_token.type: + null: + return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION + + DMConstants.TOKEN_FUNCTION: + return DMConstants.ERR_UNEXPECTED_FUNCTION + + DMConstants.TOKEN_PARENS_OPEN, \ + DMConstants.TOKEN_PARENS_CLOSE: + return DMConstants.ERR_UNEXPECTED_BRACKET + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_ASSIGNMENT, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_NOT, \ + DMConstants.TOKEN_AND_OR: + return DMConstants.ERR_UNEXPECTED_OPERATOR + + DMConstants.TOKEN_COMMA: + return DMConstants.ERR_UNEXPECTED_COMMA + DMConstants.TOKEN_COLON: + return DMConstants.ERR_UNEXPECTED_COLON + DMConstants.TOKEN_DOT: + return DMConstants.ERR_UNEXPECTED_DOT + + DMConstants.TOKEN_BOOL: + return DMConstants.ERR_UNEXPECTED_BOOLEAN + DMConstants.TOKEN_STRING: + return DMConstants.ERR_UNEXPECTED_STRING + DMConstants.TOKEN_NUMBER: + return DMConstants.ERR_UNEXPECTED_NUMBER + DMConstants.TOKEN_VARIABLE: + return DMConstants.ERR_UNEXPECTED_VARIABLE + + return DMConstants.ERR_INVALID_EXPRESSION + + return OK + + +# Convert a series of comma separated tokens to an [Array]. +func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]: + var list: Array[Array] = [] + var current_item: Array[Dictionary] = [] + for token in tokens: + if token.type == DMConstants.TOKEN_COMMA: + list.append(current_item) + current_item = [] + else: + current_item.append(token) + + if current_item.size() > 0: + list.append(current_item) + + return list + + +# Convert a series of key/value tokens into a [Dictionary] +func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary: + var dictionary = {} + for i in range(0, tokens.size()): + if tokens[i].type == DMConstants.TOKEN_COLON: + if tokens.size() == i + 2: + dictionary[tokens[i - 1]] = tokens[i + 1] + else: + dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i } + + return dictionary + + +# Work out what the next token is from a string. +func _find_match(input: String) -> Dictionary: + for key in regex.TOKEN_DEFINITIONS.keys(): + var regex = regex.TOKEN_DEFINITIONS.get(key) + var found = regex.search(input) + if found: + return { + type = key, + remaining_text = input.substr(found.strings[0].length()), + value = found.strings[0] + } + + return {} + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd.uid new file mode 100644 index 00000000..0793701f --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd.uid @@ -0,0 +1 @@ +uid://dbi4hbar8ubwu diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd new file mode 100644 index 00000000..16bca6f5 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd @@ -0,0 +1,68 @@ +## Data associated with a dialogue jump/goto line. +class_name DMResolvedGotoData extends RefCounted + + +## The title that was specified +var title: String = "" +## The target line's ID +var next_id: String = "" +## An expression to determine the target line at runtime. +var expression: Array[Dictionary] = [] +## The given line text with the jump syntax removed. +var text_without_goto: String = "" +## Whether this is a jump-and-return style jump. +var is_snippet: bool = false +## A parse error if there was one. +var error: int +## The index in the string where +var index: int = 0 + +# An instance of the compiler [RegEx] list. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +func _init(text: String, titles: Dictionary) -> void: + if not "=> " in text and not "=>< " in text: return + + if "=> " in text: + text_without_goto = text.substr(0, text.find("=> ")).strip_edges() + elif "=>< " in text: + is_snippet = true + text_without_goto = text.substr(0, text.find("=>< ")).strip_edges() + + var found: RegExMatch = regex.GOTO_REGEX.search(text) + if found == null: + return + + title = found.strings[found.names.goto].strip_edges() + index = found.get_start(0) + + if title == "": + error = DMConstants.ERR_UNKNOWN_TITLE + return + + # "=> END!" means end the conversation, ignoring any "=><" chains. + if title == "END!": + next_id = DMConstants.ID_END_CONVERSATION + + # "=> END" means end the current title (and go back to the previous one if there is one + # in the stack) + elif title == "END": + next_id = DMConstants.ID_END + + elif titles.has(title): + next_id = titles.get(title) + elif title.begins_with("{{"): + var expression_parser: DMExpressionParser = DMExpressionParser.new() + var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0) + if title_expression[0].has("error"): + error = title_expression[0].error + else: + expression = title_expression[0].expression + else: + next_id = title + error = DMConstants.ERR_UNKNOWN_TITLE + + +func _to_string() -> String: + return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid new file mode 100644 index 00000000..cb05e08b --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid @@ -0,0 +1 @@ +uid://llhl5pt47eoq diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd new file mode 100644 index 00000000..1d1a7167 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd @@ -0,0 +1,167 @@ +## Any data associated with inline dialogue BBCodes. +class_name DMResolvedLineData extends RefCounted + +## The line's text +var text: String = "" +## A map of pauses against where they are found in the text. +var pauses: Dictionary = {} +## A map of speed changes against where they are found in the text. +var speeds: Dictionary = {} +## A list of any mutations to run and where they are found in the text. +var mutations: Array[Array] = [] +## A duration reference for the line. Represented as "auto" or a stringified number. +var time: String = "" + + +func _init(line: String) -> void: + text = line + pauses = {} + speeds = {} + mutations = [] + time = "" + + var bbcodes: Array = [] + + # Remove any escaped brackets (ie. "\[") + var escaped_open_brackets: PackedInt32Array = [] + var escaped_close_brackets: PackedInt32Array = [] + for i in range(0, text.length() - 1): + if text.substr(i, 2) == "\\[": + text = text.substr(0, i) + "!" + text.substr(i + 2) + escaped_open_brackets.append(i) + elif text.substr(i, 2) == "\\]": + text = text.substr(0, i) + "!" + text.substr(i + 2) + escaped_close_brackets.append(i) + + # Extract all of the BB codes so that we know the actual text (we could do this easier with + # a RichTextLabel but then we'd need to await idle_frame which is annoying) + var bbcode_positions = find_bbcode_positions_in_string(text) + var accumulaive_length_offset = 0 + for position in bbcode_positions: + # Ignore our own markers + if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]: + continue + + bbcodes.append({ + bbcode = position.bbcode, + start = position.start, + offset_start = position.start - accumulaive_length_offset + }) + accumulaive_length_offset += position.bbcode.length() + + for bb in bbcodes: + text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length()) + + # Now find any dialogue markers + var next_bbcode_position = find_bbcode_positions_in_string(text, false) + var limit = 0 + while next_bbcode_position.size() > 0 and limit < 1000: + limit += 1 + + var bbcode = next_bbcode_position[0] + + var index = bbcode.start + var code = bbcode.code + var raw_args = bbcode.raw_args + var args = {} + if code in ["do", "do!", "set"]: + var compilation: DMCompilation = DMCompilation.new() + args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args]) + else: + # Could be something like: + # "=1.0" + # " rate=20 level=10" + if raw_args and raw_args[0] == "=": + raw_args = "value" + raw_args + for pair in raw_args.strip_edges().split(" "): + if "=" in pair: + var bits = pair.split("=") + args[bits[0]] = bits[1] + + match code: + "wait": + if pauses.has(index): + pauses[index] += args.get("value").to_float() + else: + pauses[index] = args.get("value").to_float() + "speed": + speeds[index] = args.get("value").to_float() + "/speed": + speeds[index] = 1.0 + "do", "do!", "set": + mutations.append([index, args.get("value")]) + "next": + time = args.get("value") if args.has("value") else "0" + + # Find any BB codes that are after this index and remove the length from their start + var length = bbcode.bbcode.length() + for bb in bbcodes: + if bb.offset_start > bbcode.start: + bb.offset_start -= length + bb.start -= length + + # Find any escaped brackets after this that need moving + for i in range(0, escaped_open_brackets.size()): + if escaped_open_brackets[i] > bbcode.start: + escaped_open_brackets[i] -= length + for i in range(0, escaped_close_brackets.size()): + if escaped_close_brackets[i] > bbcode.start: + escaped_close_brackets[i] -= length + + text = text.substr(0, index) + text.substr(index + length) + next_bbcode_position = find_bbcode_positions_in_string(text, false) + + # Put the BB Codes back in + for bb in bbcodes: + text = text.insert(bb.start, bb.bbcode) + + # Put the escaped brackets back in + for index in escaped_open_brackets: + text = text.left(index) + "[" + text.right(text.length() - index - 1) + for index in escaped_close_brackets: + text = text.left(index) + "]" + text.right(text.length() - index - 1) + + +func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]: + if not "[" in string: return [] + + var positions: Array[Dictionary] = [] + + var open_brace_count: int = 0 + var start: int = 0 + var bbcode: String = "" + var code: String = "" + var is_finished_code: bool = false + for i in range(0, string.length()): + if string[i] == "[": + if open_brace_count == 0: + start = i + bbcode = "" + code = "" + is_finished_code = false + open_brace_count += 1 + + else: + if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"): + code += string[i] + else: + is_finished_code = true + + if open_brace_count > 0: + bbcode += string[i] + + if string[i] == "]": + open_brace_count -= 1 + if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]): + positions.append({ + bbcode = bbcode, + code = code, + start = start, + end = i, + raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges() + }) + + if not find_all: + return positions + + return positions diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd.uid new file mode 100644 index 00000000..bbea7d26 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_line_data.gd.uid @@ -0,0 +1 @@ +uid://0k6q8kukq0qa diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd new file mode 100644 index 00000000..e926adae --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd @@ -0,0 +1,26 @@ +## Tag data associated with a line of dialogue. +class_name DMResolvedTagData extends RefCounted + + +## The list of tags. +var tags: PackedStringArray = [] +## The line with any tag syntax removed. +var text_without_tags: String = "" + +# An instance of the compiler [RegEx]. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +func _init(text: String) -> void: + var resolved_tags: PackedStringArray = [] + var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text) + for tag_match in tag_matches: + text = text.replace(tag_match.get_string(), "") + var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",") + for tag in tags: + tag = tag.replace("#", "") + if not tag in resolved_tags: + resolved_tags.append(tag) + + tags = resolved_tags + text_without_tags = text diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid new file mode 100644 index 00000000..98c6f51d --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid @@ -0,0 +1 @@ +uid://cqai3ikuilqfq diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd new file mode 100644 index 00000000..f172a8a4 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd @@ -0,0 +1,44 @@ +## An intermediate representation of a dialogue line before it gets compiled. +class_name DMTreeLine extends RefCounted + + +## The line number where this dialogue was found (after imported files have had their content imported). +var line_number: int = 0 +## The parent [DMTreeLine] of this line. +## This is stored as a Weak Reference so that this RefCounted can elegantly free itself. +## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive. +var parent: WeakRef +## The ID of this line. +var id: String +## The type of this line (as a [String] defined in [DMConstants]. +var type: String = "" +## Is this line part of a randomised group? +var is_random: bool = false +## The indent count for this line. +var indent: int = 0 +## The text of this line. +var text: String = "" +## The child [DMTreeLine]s of this line. +var children: Array[DMTreeLine] = [] +## Any doc comments attached to this line. +var notes: String = "" + + +func _init(initial_id: String) -> void: + id = initial_id + + +func _to_string() -> String: + var tabs = [] + tabs.resize(indent) + tabs.fill("\t") + tabs = "".join(tabs) + + return tabs.join([tabs + "{\n", + "\tid: %s\n" % [id], + "\ttype: %s\n" % [type], + "\tis_random: %s\n" % ["true" if is_random else "false"], + "\ttext: %s\n" % [text], + "\tnotes: %s\n" % [notes], + "\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n", + "}"]) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd.uid new file mode 100644 index 00000000..fe1db3a6 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd.uid @@ -0,0 +1 @@ +uid://dsu4i84dpif14 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd index 05ef00ff..e5a7c8c5 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd @@ -1,5 +1,5 @@ @tool -extends CodeEdit +class_name DMCodeEdit extends CodeEdit signal active_title_change(title: String) @@ -7,10 +7,6 @@ signal error_clicked(line_number: int) signal external_file_requested(path: String, title: String) -const DialogueManagerParser = preload("./parser.gd") -const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd") - - # A link back to the owner `MainView` var main_view @@ -19,7 +15,7 @@ var theme_overrides: Dictionary: set(value): theme_overrides = value - syntax_highlighter = DialogueSyntaxHighlighter.new() + syntax_highlighter = DMSyntaxHighlighter.new() # General UI add_theme_color_override("font_color", theme_overrides.text_color) @@ -67,7 +63,7 @@ func _ready() -> void: if not has_comment_delimiter("#"): add_comment_delimiter("#", "", true) - syntax_highlighter = DialogueSyntaxHighlighter.new() + syntax_highlighter = DMSyntaxHighlighter.new() func _gui_input(event: InputEvent) -> void: @@ -111,26 +107,34 @@ func _can_drop_data(at_position: Vector2, data) -> bool: if typeof(data) != TYPE_DICTIONARY: return false if data.type != "files": return false - var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") + var files: PackedStringArray = Array(data.files) return files.size() > 0 func _drop_data(at_position: Vector2, data) -> void: var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+") - var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") + var files: PackedStringArray = Array(data.files) for file in files: # Don't import the file into itself if file == main_view.current_file_path: continue - var path = file.replace("res://", "").replace(".dialogue", "") - # Find the first non-import line in the file to add our import - var lines = text.split("\n") - for i in range(0, lines.size()): - if not lines[i].begins_with("import "): - insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)]) - set_caret_line(i) - break + if file.get_extension() == "dialogue": + var path = file.replace("res://", "").replace(".dialogue", "") + # Find the first non-import line in the file to add our import + var lines = text.split("\n") + for i in range(0, lines.size()): + if not lines[i].begins_with("import "): + insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)]) + set_caret_line(i) + break + else: + var cursor: Vector2 = get_line_column_at_pos(at_position) + if cursor.x > -1 and cursor.y > -1: + set_cursor(cursor) + remove_secondary_carets() + insert_text("\"%s\"" % file, cursor.y, cursor.x) + grab_focus() func _request_code_completion(force: bool) -> void: @@ -151,17 +155,18 @@ func _request_code_completion(force: bool) -> void: add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons")) # Get all titles, including those in imports - var parser: DialogueManagerParser = DialogueManagerParser.new() - parser.prepare(text, main_view.current_file_path, false) - for title in parser.titles: - if "/" in title: + for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path): + # Ignore any imported titles that aren't resolved to human readable. + if title.to_int() > 0: + continue + + elif "/" in title: var bits = title.split("/") if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]): add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) elif matches_prompt(prompt, title): add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons")) update_code_completion_options(true) - parser.free() return var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") @@ -205,8 +210,8 @@ func get_cursor() -> Vector2: # Set the caret from a Vector2 func set_cursor(from_cursor: Vector2) -> void: - set_caret_line(from_cursor.y) - set_caret_column(from_cursor.x) + set_caret_line(from_cursor.y, false) + set_caret_column(from_cursor.x, false) # Check if a prompt is the start of a string without actually being that string @@ -219,8 +224,9 @@ func get_titles() -> PackedStringArray: var titles = PackedStringArray([]) var lines = text.split("\n") for line in lines: - if line.begins_with("~ "): - titles.append(line.substr(2).strip_edges()) + if line.strip_edges().begins_with("~ "): + titles.append(line.strip_edges().substr(2)) + return titles @@ -259,6 +265,11 @@ func get_character_names(beginning_with: String) -> PackedStringArray: # Mark a line as an error or not func mark_line_as_error(line_number: int, is_error: bool) -> void: + # Lines display counting from 1 but are actually indexed from 0 + line_number -= 1 + + if line_number < 0: return + if is_error: set_line_background_color(line_number, theme_overrides.error_line_color) set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons")) @@ -372,6 +383,7 @@ func delete_current_line() -> void: func move_line(offset: int) -> void: offset = clamp(offset, -1, 1) + var starting_scroll := scroll_vertical var cursor = get_cursor() var reselect: bool = false var from: int = cursor.y @@ -395,12 +407,14 @@ func move_line(offset: int) -> void: text = "\n".join(lines) cursor.y += offset + set_cursor(cursor) from += offset to += offset if reselect: select(from, 0, to, get_line_width(to)) - set_cursor(cursor) + text_changed.emit() + scroll_vertical = starting_scroll + offset ### Signals diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd.uid index 6d44ac73..ab2b9e57 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd.uid @@ -1 +1 @@ -uid://1ymv6jff0eay +uid://djeybvlb332mp diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.tscn index a974ea34..0c25707b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"] +[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"] +[ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"] [sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"] script = ExtResource("1_58cfo") diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd index ce5535f5..1553d1f1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd @@ -1,55 +1,18 @@ @tool -extends SyntaxHighlighter +class_name DMSyntaxHighlighter extends SyntaxHighlighter -const DialogueManagerParser = preload("./parser.gd") - - -enum ExpressionType {DO, SET, IF} - - -var dialogue_manager_parser: DialogueManagerParser = DialogueManagerParser.new() - -var regex_titles: RegEx = RegEx.create_from_string("^\\s*(?~\\s+[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)") -var regex_comments: RegEx = RegEx.create_from_string("(?:(?>\"(?:\\\\\"|[^\"\\n])*\")[^\"\\n]*?\\s*(?<comment>#[^\\n]*)$|^[^\"#\\n]*?\\s*(?<comment2>#[^\\n]*))") -var regex_mutation: RegEx = RegEx.create_from_string("^\\s*(do|do!|set) (?<mutation>.*)") -var regex_condition: RegEx = RegEx.create_from_string("^\\s*(if|elif|while|else if) (?<condition>.*)") -var regex_wcondition: RegEx = RegEx.create_from_string("\\[if (?<condition>((?:[^\\[\\]]*)|(?:\\[(?1)\\]))*?)\\]") -var regex_wendif: RegEx = RegEx.create_from_string("\\[(\\/if|else)\\]") -var regex_rgroup: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]") -var regex_endconditions: RegEx = RegEx.create_from_string("^\\s*(endif|else):?\\s*$") -var regex_tags: RegEx = RegEx.create_from_string("\\[(?<tag>(?!(?:ID:.*)|if)[a-zA-Z_][a-zA-Z0-9_]*!?)(?:[= ](?<val>[^\\[\\]]+))?\\](?:(?<text>(?!\\[\\/\\k<tag>\\]).*?)?(?<end>\\[\\/\\k<tag>\\]))?") -var regex_dialogue: RegEx = RegEx.create_from_string("^\\s*(?:(?<random>\\%[\\d.]* )|(?<response>- ))?(?:(?<character>[^#:]*): )?(?<dialogue>.*)$") -var regex_goto: RegEx = RegEx.create_from_string("=><? (?:(?<file>[^\\/]+)\\/)?(?<title>[^\\/]*)") -var regex_string: RegEx = RegEx.create_from_string("^&?(?<delimiter>[\"'])(?<content>(?:\\\\{2})*|(?:.*?[^\\\\](?:\\\\{2})*))\\1$") -var regex_escape: RegEx = RegEx.create_from_string("\\\\.") -var regex_number: RegEx = RegEx.create_from_string("^-?(?:(?:0x(?:[0-9A-Fa-f]{2})+)|(?:0b[01]+)|(?:\\d+(?:(?:[\\.]\\d*)?(?:e\\d+)?)|(?:_\\d+)+)?)$") -var regex_array: RegEx = RegEx.create_from_string("\\[((?>[^\\[\\]]+|(?R))*)\\]") -var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$") -var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?<left>.*?)\\s*(?<colon>:|=)\\s*(?<right>[^\\/]+)$") -var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?") -var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*(?<op>(?:\\/|\\*|-|\\+)?=)\\s*(?<val>.*)$") -var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|&&|\\|\\|not|in|null)(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\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("^(?<left>.*?)\\s*(?<op>==|>=|<=|<|>|!=)\\s*(?<right>.*)$") -var regex_blogical: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s+(?<op>and|or|in|&&|\\|\\|)\\s+(?<right>.*)$") -var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?<op>not)\\s+(?<right>.*)$") -var regex_paren: RegEx = RegEx.create_from_string("\\((?<paren>((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)") +var regex: DMCompilerRegEx = DMCompilerRegEx.new() +var compilation: DMCompilation = DMCompilation.new() +var expression_parser = DMExpressionParser.new() var cache: Dictionary = {} -func _notification(what: int) -> void: - if what == NOTIFICATION_PREDELETE: - dialogue_manager_parser.free() - - func _clear_highlighting_cache() -> void: - cache = {} + cache.clear() -## Returns the syntax coloring for a dialogue file line func _get_line_syntax_highlighting(line: int) -> Dictionary: var colors: Dictionary = {} var text_edit: TextEdit = get_text_edit() @@ -63,323 +26,194 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: if text in cache: return cache[text] - # Comments have to be removed to make the remaining processing easier. - # Count both end-of-line and single-line comments - # Comments are not allowed within dialogue lines or response lines, so we ask the parser what it thinks the current line is - if not (dialogue_manager_parser.is_dialogue_line(text) or dialogue_manager_parser.is_response_line(text)) or dialogue_manager_parser.is_line_empty(text) or dialogue_manager_parser.is_import_line(text): - var comment_matches: Array[RegExMatch] = regex_comments.search_all(text) - for comment_match in comment_matches: - for i in ["comment", "comment2"]: - if i in comment_match.names: - colors[comment_match.get_start(i)] = {"color": text_edit.theme_overrides.comments_color} - text = text.substr(0, comment_match.get_start(i)) + var theme: Dictionary = text_edit.theme_overrides - # Dialogues - var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text) - for dialogue_match in dialogue_matches: - if "random" in dialogue_match.names: - colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color} - colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color} - if "response" in dialogue_match.names: - colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color} - colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color} - if "character" in dialogue_match.names: - colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color} - colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true) + var index: int = 0 - # Title lines - if dialogue_manager_parser.is_title_line(text): - var title_matches: Array[RegExMatch] = regex_titles.search_all(text) - for title_match in title_matches: - colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color} + match DMCompiler.get_line_type(text): + DMConstants.TYPE_USING: + colors[index] = { color = theme.conditions_color } + colors[index + "using ".length()] = { color = theme.text_color } - # Import lines - var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text) - for import_match in import_matches: - colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} - colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color} - colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color} - colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color} - colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color} + DMConstants.TYPE_IMPORT: + colors[index] = { color = theme.conditions_color } + var import: RegExMatch = regex.IMPORT_REGEX.search(text) + colors[index + import.get_start("path") - 1] = { color = theme.strings_color } + colors[index + import.get_end("path") + 1] = { color = theme.conditions_color } + colors[index + import.get_start("prefix")] = { color = theme.text_color } - # Using clauses - var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text) - for using_match in using_matches: - colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} - colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TYPE_COMMENT: + colors[index] = { color = theme.comments_color } - # Condition keywords and expressions - var condition_matches: Array[RegExMatch] = regex_condition.search_all(text) - for condition_match in condition_matches: - colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} - colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true) - # endif/else - var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text) - for endcondition_match in endcondition_matches: - colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color} - colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} + DMConstants.TYPE_TITLE: + colors[index] = { color = theme.titles_color } - # Mutations - var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text) - for mutation_match in mutation_matches: - colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color} - colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1] == "do" else ExpressionType.SET, mutation_match.get_string("mutation")), true) + DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN: + colors[0] = { color = theme.conditions_color } + index = text.find(" ") + if index > -1: + var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index) + + DMConstants.TYPE_MUTATION: + colors[0] = { color = theme.mutations_color } + index = text.find(" ") + var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index) + + DMConstants.TYPE_GOTO: + if text.strip_edges().begins_with("%"): + colors[index] = { color = theme.symbols_color } + index = text.find(" ") + _highlight_goto(text, colors, index) + + DMConstants.TYPE_RANDOM: + colors[index] = { color = theme.symbols_color } + + DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE: + if text.strip_edges().begins_with("%"): + colors[index] = { color = theme.symbols_color } + index = text.find(" ", text.find("%")) + colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) } + + var dialogue_text: String = text.substr(index, text.find("=>")) + + # Highlight character name + var split_index: int = dialogue_text.replace("\\:", "??").find(":") + colors[index + split_index + 1] = { color = theme.text_color } + + # Interpolation + var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) + for replacement: RegExMatch in replacements: + var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2) + var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start()) + var expression_index: int = index + replacement.get_start() + colors[expression_index] = { color = theme.symbols_color } + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[expression_index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index + 2) + colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color } + colors[expression_index + expression_text.length() + 4] = { color = theme.text_color } + # Tags (and inline mutations) + var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("") + var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true) + for bbcode: Dictionary in bbcodes: + var tag: String = bbcode.code + var code: String = bbcode.raw_args + if code.begins_with("["): + colors[index + bbcode.start] = { color = theme.symbols_color } + colors[index + bbcode.start + 2] = { color = theme.text_color } + var pipe_cursor: int = code.find("|") + while pipe_cursor > -1: + colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color } + colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color } + pipe_cursor = code.find("|", pipe_cursor + 1) + colors[index + bbcode.end - 1] = { color = theme.symbols_color } + colors[index + bbcode.end + 1] = { color = theme.text_color } + else: + colors[index + bbcode.start] = { color = theme.symbols_color } + if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"): + if tag.begins_with("if"): + colors[index + bbcode.start + 1] = { color = theme.conditions_color } + else: + colors[index + bbcode.start + 1] = { color = theme.mutations_color } + var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length()) + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index + 2) + # else and closing if have no expression + elif tag.begins_with("else") or tag.begins_with("/if"): + colors[index + bbcode.start + 1] = { color = theme.conditions_color } + colors[index + bbcode.end] = { color = theme.symbols_color } + colors[index + bbcode.end + 1] = { color = theme.text_color } + # Jumps + if "=> " in text or "=>< " in text: + _highlight_goto(text, colors, index) # Order the dictionary keys to prevent CodeEdit from having issues - var new_colors: Dictionary = {} + var ordered_colors: Dictionary = {} var ordered_keys: Array = colors.keys() ordered_keys.sort() - for index in ordered_keys: - new_colors[index] = colors[index] + for key_index: int in ordered_keys: + ordered_colors[key_index] = colors[key_index] - cache[text] = new_colors - return new_colors + cache[text] = ordered_colors + return ordered_colors -## Return the syntax highlighting for a dialogue line -func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary: - var text_edit: TextEdit = get_text_edit() - var colors: Dictionary = {} +func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int: + var theme: Dictionary = get_text_edit().theme_overrides + var last_index: int = index + for token: Dictionary in tokens: + last_index = token.i + match token.type: + DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: + colors[index + token.i] = { color = theme.conditions_color } - # #tag style tags - var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text) - for hashtag_match in hashtag_matches: - colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color } - colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color } + DMConstants.TOKEN_VARIABLE: + if token.value in ["true", "false"]: + colors[index + token.i] = { color = theme.conditions_color } + else: + colors[index + token.i] = { color = theme.members_color } - # bbcode-like global tags - var tag_matches: Array[RegExMatch] = regex_tags.search_all(text) - for tag_match in tag_matches: - colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - if "val" in tag_match.names: - colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true) - colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color} - # Show the text color straight in the editor for better ease-of-use - if tag_match.get_string("tag") == "color": - colors[start_index + tag_match.get_start("val")] = {"color": Color.from_string(tag_match.get_string("val"), text_edit.theme_overrides.text_color)} - if "text" in tag_match.names: - colors[start_index + tag_match.get_start("text")] = {"color": text_edit.theme_overrides.text_color} - # Text can still contain tags if several effects are applied ([center][b]Something[/b][/center], so recursing - colors.merge(_get_dialogue_syntax_highlighting(start_index + tag_match.get_start("text"), tag_match.get_string("text")), true) - colors[start_index + tag_match.get_end("text")] = {"color": text_edit.theme_overrides.symbols_color} - if "end" in tag_match.names: - colors[start_index + tag_match.get_start("end")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + tag_match.get_end("end")] = {"color": text_edit.theme_overrides.text_color} - colors[start_index + tag_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, DMConstants.TOKEN_COMMA, DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: + colors[index + token.i] = { color = theme.symbols_color } - # ID tag - var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text) - for translation_match in translation_matches: - colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color} - colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TOKEN_STRING: + colors[index + token.i] = { color = theme.strings_color } - # Replacements - var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text) - for replacement_match in replacement_matches: - colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true) - colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TOKEN_FUNCTION: + colors[index + token.i] = { color = theme.mutations_color } + colors[index + token.i + token.function.length()] = { color = theme.symbols_color } + for parameter: Array in token.value: + last_index = _highlight_expression(parameter, colors, index) + DMConstants.TOKEN_PARENS_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } - # Jump at the end of a response - var goto_matches: Array[RegExMatch] = regex_goto.search_all(text) - 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.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.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} + DMConstants.TOKEN_DICTIONARY_REFERENCE: + colors[index + token.i] = { color = theme.members_color } + colors[index + token.i + token.variable.length()] = { color = theme.symbols_color } + last_index = _highlight_expression(token.value, colors, index) + DMConstants.TOKEN_ARRAY: + colors[index + token.i] = { color = theme.symbols_color } + for item: Array in token.value: + last_index = _highlight_expression(item, colors, index) + DMConstants.TOKEN_BRACKET_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } - # Wrapped condition - var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text) - for wcondition_match in wcondition_matches: - colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color} - colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true) - colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} - # [/if] tag for color matching with the opening tag - var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text) - for wendif_match in wendif_matches: - colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color} - colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TOKEN_DICTIONARY: + colors[index + token.i] = { color = theme.symbols_color } + last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index) + DMConstants.TOKEN_BRACE_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } + last_index += 1 - # Random groups - var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text) - for rgroup_match in rgroup_matches: - colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color} - var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options")) - for separator_match in separator_matches: - colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} - colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} + DMConstants.TOKEN_GROUP: + last_index = _highlight_expression(token.value, colors, index) - return colors + return last_index -## Returns the syntax highlighting for an expression (mutation set/do, or condition) -func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary: - var text_edit: TextEdit = get_text_edit() - var colors: Dictionary = {} +func _highlight_goto(text: String, colors: Dictionary, index: int) -> int: + var theme: Dictionary = get_text_edit().theme_overrides + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {}) + colors[goto_data.index] = { color = theme.jumps_color } + if "{{" in text: + index = text.find("{{", goto_data.index) + var last_index: int = 0 + if goto_data.error: + colors[index + 2] = { color = theme.critical_color } + else: + last_index = _highlight_expression(goto_data.expression, colors, index) + index = text.find("}}", index + last_index) + colors[index] = { color = theme.jumps_color } - if type == ExpressionType.SET: - var assignment_matches: Array[RegExMatch] = regex_assignment.search_all(text) - for assignment_match in assignment_matches: - colors[start_index + assignment_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color} - if "attr" in assignment_match.names: - colors[start_index + assignment_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color} - colors[start_index + assignment_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color} - if "key" in assignment_match.names: - # Braces are outside of the key, so coloring them symbols_color - colors[start_index + assignment_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color} - colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("key"), assignment_match.get_string("key")), true) - colors[start_index + assignment_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + assignment_match.get_end("key") + 1] = {"color": text_edit.theme_overrides.text_color} - - colors[start_index + assignment_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + assignment_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("val"), assignment_match.get_string("val")), true) - else: - colors.merge(_get_literal_syntax_highlighting(start_index, text), true) - - return colors - - -## Return the syntax highlighting for a literal -## For this purpose, "literal" refers to a regular code line that could be used to get a value out of: -## - function calls -## - real literals (bool, string, int, float, etc.) -## - logical operators (>, <, >=, or, and, not, etc.) -func _get_literal_syntax_highlighting(start_index: int, text: String) -> Dictionary: - var text_edit: TextEdit = get_text_edit() - var colors: Dictionary = {} - - # Remove spaces at start/end of the literal - var text_length: int = text.length() - text = text.lstrip(" ") - start_index += text_length - text.length() - text = text.rstrip(" ") - - # Parenthesis expression. - var paren_matches: Array[RegExMatch] = regex_paren.search_all(text) - for paren_match in paren_matches: - colors[start_index + paren_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + paren_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + paren_match.get_start("paren"), paren_match.get_string("paren")), true) - colors[start_index + paren_match.get_end(0) - 1] = {"color": text_edit.theme_overrides.symbols_color} - - # Strings - var string_matches: Array[RegExMatch] = regex_string.search_all(text) - for string_match in string_matches: - colors[start_index + string_match.get_start(0)] = {"color": text_edit.theme_overrides.strings_color} - if "content" in string_match.names: - var escape_matches: Array[RegExMatch] = regex_escape.search_all(string_match.get_string("content")) - for escape_match in escape_matches: - colors[start_index + string_match.get_start("content") + escape_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + string_match.get_start("content") + escape_match.get_end(0)] = {"color": text_edit.theme_overrides.strings_color} - - # Numbers - var number_matches: Array[RegExMatch] = regex_number.search_all(text) - for number_match in number_matches: - colors[start_index + number_match.get_start(0)] = {"color": text_edit.theme_overrides.numbers_color} - - # Arrays - var array_matches: Array[RegExMatch] = regex_array.search_all(text) - for array_match in array_matches: - colors[start_index + array_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors.merge(_get_list_syntax_highlighting(start_index + array_match.get_start(1), array_match.strings[1]), true) - colors[start_index + array_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} - - # Dictionaries - var dict_matches: Array[RegExMatch] = regex_dict.search_all(text) - for dict_match in dict_matches: - colors[start_index + dict_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} - colors.merge(_get_list_syntax_highlighting(start_index + dict_match.get_start(1), dict_match.strings[1]), true) - colors[start_index + dict_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} - - # Dictionary key: value pairs - var kvdict_matches: Array[RegExMatch] = regex_kvdict.search_all(text) - for kvdict_match in kvdict_matches: - colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("left"), kvdict_match.get_string("left")), true) - colors[start_index + kvdict_match.get_start("colon")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + kvdict_match.get_end("colon")] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("right"), kvdict_match.get_string("right")), true) - - # Booleans - var bool_matches: Array[RegExMatch] = regex_keyword.search_all(text) - for bool_match in bool_matches: - colors[start_index + bool_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} - - # Functions - var function_matches: Array[RegExMatch] = regex_function.search_all(text) - for function_match in function_matches: - var last_brace_index: int = text.rfind(")") - colors[start_index + function_match.get_start(1)] = {"color": text_edit.theme_overrides.mutations_color} - colors[start_index + function_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} - colors.merge(_get_list_syntax_highlighting(start_index + function_match.get_end(0), text.substr(function_match.get_end(0), last_brace_index - function_match.get_end(0))), true) - colors[start_index + last_brace_index] = {"color": text_edit.theme_overrides.symbols_color} - - # Variables - var varname_matches: Array[RegExMatch] = regex_varname.search_all(text) - for varname_match in varname_matches: - colors[start_index + varname_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color} - if "attr" in varname_match.names: - colors[start_index + varname_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color} - colors[start_index + varname_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color} - if "key" in varname_match.names: - # Braces are outside of the key, so coloring them symbols_color - colors[start_index + varname_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color} - colors.merge(_get_literal_syntax_highlighting(start_index + varname_match.get_start("key"), varname_match.get_string("key")), true) - colors[start_index + varname_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color} - - # Comparison operators - var comparison_matches: Array[RegExMatch] = regex_comparison.search_all(text) - for comparison_match in comparison_matches: - colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("left"), comparison_match.get_string("left")), true) - colors[start_index + comparison_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color} - colors[start_index + comparison_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} - var right = comparison_match.get_string("right") - if right.ends_with(":"): - right = right.substr(0, right.length() - 1) - colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("right"), right), true) - colors[start_index + comparison_match.get_start("right") + right.length()] = { "color": text_edit.theme_overrides.symbols_color } - - # Logical binary operators - var blogical_matches: Array[RegExMatch] = regex_blogical.search_all(text) - for blogical_match in blogical_matches: - colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("left"), blogical_match.get_string("left")), true) - colors[start_index + blogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color} - colors[start_index + blogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("right"), blogical_match.get_string("right")), true) - - # Logical unary operators - var ulogical_matches: Array[RegExMatch] = regex_ulogical.search_all(text) - for ulogical_match in ulogical_matches: - colors[start_index + ulogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color} - colors[start_index + ulogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} - colors.merge(_get_literal_syntax_highlighting(start_index + ulogical_match.get_start("right"), ulogical_match.get_string("right")), true) - - return colors - - -## Returns the syntax coloring for a list of literals separated by commas -func _get_list_syntax_highlighting(start_index: int, text: String) -> Dictionary: - var text_edit: TextEdit = get_text_edit() - var colors: Dictionary = {} - - # Comma-separated list of literals (for arrays and function arguments) - var element_matches: Array[RegExMatch] = regex_commas.search_all(text) - for element_match in element_matches: - colors.merge(_get_literal_syntax_highlighting(start_index + element_match.get_start(1), element_match.strings[1]), true) - - return colors + return index diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid index 4001229c..9bad8cc9 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid @@ -1 +1 @@ -uid://c2pb8gpka0t0u +uid://klpiq4tk3t7a diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd.uid deleted file mode 100644 index 6e116dea..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cvqm0f453kjn1 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd index 617d3082..e67a93ff 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd @@ -34,7 +34,7 @@ func _ready() -> void: func _on_download_button_pressed() -> void: # Safeguard the actual dialogue manager repo from accidentally updating itself - if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"): + if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"): prints("You can't update the addon from within itself.") failed.emit() return diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd.uid index a3e7d6f9..7910ab4a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.gd.uid @@ -1 +1 @@ -uid://dtgq7prk0yh50 +uid://kpwo418lb2t2 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.tscn index f3f45236..540abd33 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/download_update_panel.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"] +[ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"] [ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"] [node name="DownloadUpdatePanel" type="Control"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property.gd.uid index b3f9ba88..283cc439 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property.gd.uid @@ -1 +1 @@ -uid://du1qxltygjocp +uid://nyypeje1a036 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd index ca52c79e..d063d0ea 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd @@ -63,7 +63,7 @@ func build_menu() -> void: func _on_new_dialog_file_selected(path: String) -> void: editor_plugin.main_view.new_file(path) is_waiting_for_file = false - if Engine.get_meta("DialogueCache").has_file(path): + if Engine.get_meta("DMCache").has_file(path): resource_changed.emit(load(path)) else: var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource @@ -81,7 +81,7 @@ func _on_file_dialog_canceled() -> void: func _on_resource_button_pressed() -> void: if is_instance_valid(resource): - editor_plugin.get_editor_interface().call_deferred("edit_resource", resource) + EditorInterface.call_deferred("edit_resource", resource) else: build_menu() menu.position = get_viewport().position + Vector2i( @@ -112,7 +112,7 @@ func _on_menu_id_pressed(id: int) -> void: ITEM_QUICK_LOAD: quick_selected_file = "" - files_list.files = Engine.get_meta("DialogueCache").get_files() + files_list.files = Engine.get_meta("DMCache").get_files() if resource: files_list.select_file(resource.resource_path) quick_open_dialog.popup_centered() @@ -123,13 +123,13 @@ func _on_menu_id_pressed(id: int) -> void: open_dialog.popup_centered() ITEM_EDIT: - editor_plugin.get_editor_interface().call_deferred("edit_resource", resource) + EditorInterface.call_deferred("edit_resource", resource) ITEM_CLEAR: resource_changed.emit(null) ITEM_FILESYSTEM: - var file_system = editor_plugin.get_editor_interface().get_file_system_dock() + var file_system = EditorInterface.get_file_system_dock() file_system.navigate_to_path(resource.resource_path) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid index e4e3956e..aab7d8d5 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid @@ -1 +1 @@ -uid://d1wvrhfmr8ry6 +uid://dooe2pflnqtve diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.tscn index 8adb6a1c..7cb02e89 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/editor_property_control.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"] +[ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"] [ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"] [ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.gd.uid index 211c69af..b1b9d269 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.gd.uid @@ -1 +1 @@ -uid://b8gxhsrredou6 +uid://damhqta55t67c diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.tscn index d68d8513..691e527b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/editor_property/resource_button.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"] +[ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"] [node name="ResourceButton" type="Button"] offset_right = 8.0 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd index f3e294d9..0b72d376 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd @@ -59,7 +59,7 @@ func show_error() -> void: show() count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() }) var error = errors[error_index] - error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) + error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) if error.has("external_error"): error_button.text += " " + DialogueConstants.get_error_message(error.external_error) @@ -72,7 +72,7 @@ func _on_errors_panel_theme_changed() -> void: func _on_error_button_pressed() -> void: - emit_signal("error_pressed", errors[error_index].line_number, errors[error_index].column_number) + error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number) func _on_previous_button_pressed() -> void: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd.uid index 05f6b9c4..c305a807 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.gd.uid @@ -1 +1 @@ -uid://bodru4vhssqjm +uid://d2l8nlb6hhrfp diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.tscn index 11fecab7..0b653cca 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/errors_panel.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"] +[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"] -[sub_resource type="Image" id="Image_d2tnf"] +[sub_resource type="Image" id="Image_w0gko"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -12,7 +12,7 @@ data = { } [sub_resource type="ImageTexture" id="ImageTexture_s6fxl"] -image = SubResource("Image_d2tnf") +image = SubResource("Image_w0gko") [node name="ErrorsPanel" type="HBoxContainer"] visible = false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd index d778c733..31f61584 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd @@ -21,6 +21,7 @@ const MODIFIED_SUFFIX = "(*)" var file_map: Dictionary = {} var current_file_path: String = "" +var last_selected_file_path: String = "" var files: PackedStringArray = []: set(next_files): @@ -33,7 +34,7 @@ var files: PackedStringArray = []: var unsaved_files: Array[String] = [] -var filter: String: +var filter: String = "": set(next_filter): filter = next_filter apply_filter() @@ -57,6 +58,7 @@ func select_file(file: String) -> void: var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "") if item_text == get_nice_file(file, item_text.count("/") + 1): list.select(i) + last_selected_file_path = file func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void: @@ -112,6 +114,8 @@ func apply_filter() -> void: func apply_theme() -> void: if is_instance_valid(filter_edit): filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) ### Signals diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd.uid index c58b9f1f..2a1089a5 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.gd.uid @@ -1 +1 @@ -uid://cxxkkarhbbf07 +uid://dqa4a4wwoo0aa diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.tscn index 9d6f8547..c9e862b1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/files_list.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] +[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] [ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] [node name="FilesList" type="VBoxContainer"] @@ -9,6 +9,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +size_flags_vertical = 3 script = ExtResource("1_cytii") icon = ExtResource("2_3ijx1") diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd index 916b744a..2614ecaa 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd @@ -114,7 +114,7 @@ func find_in_files() -> Dictionary: var results: Dictionary = {} var q: String = input.text - var cache = Engine.get_meta("DialogueCache") + var cache = Engine.get_meta("DMCache") var file: FileAccess for path in cache.get_files(): var path_results: Array = [] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd.uid index ffa0a01e..380a4918 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd.uid @@ -1 +1 @@ -uid://xe50vmll2xq4 +uid://q368fmxxa8sd diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.tscn index 8aaef4b9..97fca248 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] +[ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"] bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd deleted file mode 100644 index d467cb9a..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd +++ /dev/null @@ -1,10 +0,0 @@ -class_name DialogueManagerParseResult extends RefCounted - -var imported_paths: PackedStringArray = [] -var using_states: PackedStringArray = [] -var titles: Dictionary = {} -var character_names: PackedStringArray = [] -var first_title: String = "" -var lines: Dictionary = {} -var errors: Array[Dictionary] = [] -var raw_text: String = "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd.uid deleted file mode 100644 index 2d25bb0e..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parse_result.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://nkbwbj4jt5h5 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd deleted file mode 100644 index 295ccf84..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd +++ /dev/null @@ -1,1798 +0,0 @@ -@tool - -class_name DialogueManagerParser extends Object - - -const DialogueConstants = preload("../constants.gd") -const DialogueSettings = preload("../settings.gd") -const ResolvedLineData = preload("./resolved_line_data.gd") -const ResolvedTagData = preload("./resolved_tag_data.gd") -const DialogueManagerParseResult = preload("./parse_result.gd") - - -var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)") -var USING_REGEX: RegEx = RegEx.create_from_string("^using (?<state>.*)$") -var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+$") -var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d") -var TRANSLATION_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<tr>.*?)\\]") -var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]") -var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<mutation>.*)") -var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if) (?<condition>.*)") -var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.*)\\]") -var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}") -var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<jump_to_title>.*)") -var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+") -var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]") -var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]") - -var TOKEN_DEFINITIONS: Dictionary = { - DialogueConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*\\("), - DialogueConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*\\["), - DialogueConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("), - DialogueConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"), - DialogueConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["), - DialogueConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"), - DialogueConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"), - DialogueConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"), - DialogueConstants.TOKEN_COLON: RegEx.create_from_string("^:"), - DialogueConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"), - DialogueConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"), - DialogueConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), - DialogueConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), - DialogueConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), - 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_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)"), - DialogueConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)") -} - -var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)? ") - -var raw_lines: PackedStringArray = [] -var parent_stack: Array[String] = [] - -var parsed_lines: Dictionary = {} -var imported_paths: PackedStringArray = [] -var using_states: PackedStringArray = [] -var titles: Dictionary = {} -var character_names: PackedStringArray = [] -var first_title: String = "" -var errors: Array[Dictionary] = [] -var raw_text: String = "" - -var _imported_line_map: Dictionary = {} -var _imported_line_count: int = 0 - -var while_loopbacks: Array[String] = [] - - -## Parse some raw dialogue text and return a dictionary containing parse results -static func parse_string(string: String, path: String) -> DialogueManagerParseResult: - var parser = new() - var error: Error = parser.parse(string, path) - var data: DialogueManagerParseResult = parser.get_data() - parser.free() - - if error == OK: - return data - else: - return null - - -## Extract bbcode and other markers from a string -static func extract_markers_from_string(string: String) -> ResolvedLineData: - var parser = new() - var markers: ResolvedLineData = parser.extract_markers(string) - parser.free() - - return markers - - -## Parse some raw dialogue text and return a dictionary containing parse results -func parse(text: String, path: String) -> Error: - prepare(text, path) - raw_text = text - - # Parse all of the content - var known_translations = {} - - # Get list of known autoloads - var autoload_names: PackedStringArray = get_autoload_names() - - # Keep track of the last doc comment - var doc_comments: Array[String] = [] - - # Then parse all lines - for id in range(0, raw_lines.size()): - var raw_line: String = raw_lines[id] - - var line: Dictionary = { - id = str(id), - next_id = DialogueConstants.ID_NULL - } - - # Work out if we are inside a conditional or option or if we just - # indented back out of one - var indent_size: int = get_indent(raw_line) - if indent_size < parent_stack.size() and not is_line_empty(raw_line): - for _tab in range(0, parent_stack.size() - indent_size): - parent_stack.pop_back() - - # If we are indented then this line should know about its parent - if parent_stack.size() > 0: - line["parent_id"] = parent_stack.back() - - # Trim any indentation (now that we've calculated it) so we can check - # the begining of each line for its type - raw_line = raw_line.strip_edges(true, false) - - # Grab translations - var translation_key: String = extract_translation(raw_line) - if translation_key != "": - line["translation_key"] = translation_key - raw_line = raw_line.replace("[ID:%s]" % translation_key, "") - - # Check for each kind of line - - # Start shortcuts - if raw_line.begins_with("using "): - var using_match: RegExMatch = USING_REGEX.search(raw_line) - if "state" in using_match.names: - var using_state: String = using_match.strings[using_match.names.state].strip_edges() - if not using_state in autoload_names: - add_error(id, 0, DialogueConstants.ERR_UNKNOWN_USING) - elif not using_state in using_states: - using_states.append(using_state) - continue - - # Response - elif is_response_line(raw_line): - # Add any doc notes - line["notes"] = "\n".join(doc_comments) - doc_comments = [] - - parent_stack.append(str(id)) - line["type"] = DialogueConstants.TYPE_RESPONSE - - # Extract any #tags - var tag_data: ResolvedTagData = extract_tags(raw_line) - line["tags"] = tag_data.tags - raw_line = tag_data.line_without_tags - - if " [if " in raw_line: - line["condition"] = extract_condition(raw_line, true, indent_size) - if " =>" in raw_line: - line["next_id"] = extract_goto(raw_line) - if " =><" in raw_line: - # Because of when the return point needs to be known at runtime we need to split - # this line into two (otherwise the return point would be dependent on the balloon) - var goto_line: Dictionary = { - type = DialogueConstants.TYPE_GOTO, - next_id = extract_goto(raw_line), - next_id_after = find_next_line_after_responses(id), - is_snippet = true - } - parsed_lines[str(id) + ".1"] = goto_line - line["next_id"] = str(id) + ".1" - - # Make sure the added goto line can actually go to somewhere - if goto_line.next_id in [DialogueConstants.ID_ERROR, DialogueConstants.ID_ERROR_INVALID_TITLE, DialogueConstants.ID_ERROR_TITLE_HAS_NO_BODY]: - line["next_id"] = goto_line.next_id - - line["character"] = "" - line["character_replacements"] = [] as Array[Dictionary] - line["text"] = extract_response_prompt(raw_line) - - var previous_response_id = find_previous_response_id(id) - if parsed_lines.has(previous_response_id): - var previous_response = parsed_lines[previous_response_id] - # Add this response to the list on the first response so that it is the - # authority on what is in the list of responses - previous_response["responses"] = previous_response["responses"] + PackedStringArray([str(id)]) - else: - # No previous response so this is the first in the list - line["responses"] = PackedStringArray([str(id)]) - - line["next_id_after"] = find_next_line_after_responses(id) - - # If this response has no body then the next id is the next id after - if not line.has("next_id") or line.next_id == DialogueConstants.ID_NULL: - var next_nonempty_line_id = get_next_nonempty_line_id(id) - if next_nonempty_line_id != DialogueConstants.ID_NULL: - if get_indent(raw_lines[next_nonempty_line_id.to_int()]) <= indent_size: - line["next_id"] = line.next_id_after - else: - line["next_id"] = next_nonempty_line_id - - line["text_replacements"] = extract_dialogue_replacements(line.get("text"), indent_size + 2) - for replacement in line.text_replacements: - if replacement.has("error"): - add_error(id, replacement.index, replacement.error) - - # If this response has a character name in it then it will automatically be - # injected as a line of dialogue if the player selects it - var response_text: String = line.text.replace("\\:", "!ESCAPED_COLON!") - if ": " in response_text: - if DialogueSettings.get_setting("create_lines_for_responses_with_characters", true): - var first_child: Dictionary = { - type = DialogueConstants.TYPE_DIALOGUE, - next_id = line.next_id, - next_id_after = line.next_id_after, - text_replacements = line.text_replacements, - tags = line.tags, - translation_key = line.get("translation_key") - } - parse_response_character_and_text(id, response_text, first_child, indent_size, parsed_lines) - 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" - else: - parse_response_character_and_text(id, response_text, line, indent_size, parsed_lines) - else: - line["text"] = response_text.replace("!ESCAPED_COLON!", ":") - - # Title - elif is_title_line(raw_line): - line["type"] = DialogueConstants.TYPE_TITLE - if not raw_lines[id].begins_with("~"): - add_error(id, indent_size + 2, DialogueConstants.ERR_NESTED_TITLE) - else: - line["text"] = extract_title(raw_line) - # Titles can't have numbers as the first letter (unless they are external titles which get replaced with hashes) - if id >= _imported_line_count and BEGINS_WITH_NUMBER_REGEX.search(line.text): - add_error(id, 2, DialogueConstants.ERR_TITLE_BEGINS_WITH_NUMBER) - # Only import titles are allowed to have "/" in them - var valid_title = VALID_TITLE_REGEX.search(raw_line.replace("/", "").substr(2).strip_edges()) - if not valid_title: - add_error(id, 2, DialogueConstants.ERR_TITLE_INVALID_CHARACTERS) - - # Condition - elif is_condition_line(raw_line, false): - parent_stack.append(str(id)) - line["type"] = DialogueConstants.TYPE_CONDITION - line["condition"] = extract_condition(raw_line, false, indent_size) - line["next_id_after"] = find_next_line_after_conditions(id) - var next_sibling_id = find_next_condition_sibling(id) - line["next_conditional_id"] = next_sibling_id if is_valid_id(next_sibling_id) else line.next_id_after - - elif is_condition_line(raw_line, true): - parent_stack.append(str(id)) - line["type"] = DialogueConstants.TYPE_CONDITION - line["next_id_after"] = find_next_line_after_conditions(id) - line["next_conditional_id"] = line["next_id_after"] - - elif is_while_condition_line(raw_line): - parent_stack.append(str(id)) - line["type"] = DialogueConstants.TYPE_CONDITION - line["condition"] = extract_condition(raw_line, false, indent_size) - line["next_id_after"] = find_next_line_after_conditions(id) - while_loopbacks.append(find_last_line_within_conditions(id)) - line["next_conditional_id"] = line["next_id_after"] - - # Mutation - elif is_mutation_line(raw_line): - line["type"] = DialogueConstants.TYPE_MUTATION - line["mutation"] = extract_mutation(raw_line) - - # Goto - elif is_goto_line(raw_line): - line["type"] = DialogueConstants.TYPE_GOTO - - if raw_line.begins_with("%"): - apply_weighted_random(id, raw_line, indent_size, line) - - line["next_id"] = extract_goto(raw_line) - if is_goto_snippet_line(raw_line): - line["is_snippet"] = true - line["next_id_after"] = get_line_after_line(id, indent_size, line) - else: - line["is_snippet"] = false - - # Nested dialogue - elif is_nested_dialogue_line(raw_line, parsed_lines, raw_lines, indent_size): - var parent_line: Dictionary = parsed_lines.values().back() - var parent_indent_size: int = get_indent(raw_lines[parent_line.id.to_int()]) - var should_update_translation_key: bool = parent_line.translation_key == parent_line.text - var suffix: String = raw_line.strip_edges(true, false) - if suffix == "": - suffix = " " - parent_line["text"] += "\n" + suffix - parent_line["text_replacements"] = extract_dialogue_replacements(parent_line.text, parent_line.character.length() + 2 + parent_indent_size) - for replacement in parent_line.text_replacements: - if replacement.has("error"): - add_error(id, replacement.index, replacement.error) - - if should_update_translation_key: - parent_line["translation_key"] = parent_line.text - - parent_line["next_id"] = get_line_after_line(id, parent_indent_size, parent_line) - - # Ignore this line when checking for indent errors - remove_error(parent_line.id.to_int(), DialogueConstants.ERR_INVALID_INDENTATION) - - var next_line = raw_lines[parent_line.next_id.to_int()] - if not is_dialogue_line(next_line) and get_indent(next_line) >= indent_size: - add_error(parent_line.next_id.to_int(), indent_size, DialogueConstants.ERR_INVALID_INDENTATION) - - continue - - elif raw_line.strip_edges().begins_with("##"): - doc_comments.append(raw_line.replace("##", "").strip_edges()) - continue - - elif is_line_empty(raw_line) or is_import_line(raw_line): - continue - - # Regular dialogue - else: - # Remove escape character - if raw_line.begins_with("\\using"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\if"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\elif"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\else"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\while"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\-"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\~"): raw_line = raw_line.substr(1) - if raw_line.begins_with("\\=>"): raw_line = raw_line.substr(1) - - # Check for jumps - if " => " in raw_line: - line["next_id"] = extract_goto(raw_line) - raw_line = raw_line.split(" => ")[0] - - # Add any doc notes - line["notes"] = "\n".join(doc_comments) - doc_comments = [] - - # Work out any weighted random siblings - if raw_line.begins_with("%"): - apply_weighted_random(id, raw_line, indent_size, line) - raw_line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line, "") - - line["type"] = DialogueConstants.TYPE_DIALOGUE - - # Extract any tags before we process the line - var tag_data: ResolvedTagData = extract_tags(raw_line) - line["tags"] = tag_data.tags - raw_line = tag_data.line_without_tags - - var l = raw_line.replace("\\:", "!ESCAPED_COLON!") - if ": " in l: - var bits = Array(l.strip_edges().split(": ")) - line["character"] = bits.pop_front().strip_edges() - if not line["character"] in character_names: - character_names.append(line["character"]) - # You can use variables in the character's name - line["character_replacements"] = extract_dialogue_replacements(line.character, indent_size) - for replacement in line.character_replacements: - if replacement.has("error"): - add_error(id, replacement.index, replacement.error) - line["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":") - else: - line["character"] = "" - line["character_replacements"] = [] as Array[Dictionary] - line["text"] = l.replace("!ESCAPED_COLON!", ":") - - line["text_replacements"] = extract_dialogue_replacements(line.text, line.character.length() + 2 + indent_size) - for replacement in line.text_replacements: - if replacement.has("error"): - add_error(id, replacement.index, replacement.error) - - # Unescape any newlines - line["text"] = line.text.replace("\\n", "\n").strip_edges() - - # Work out where to go after this line - if line.next_id == DialogueConstants.ID_NULL: - line["next_id"] = get_line_after_line(id, indent_size, line) - - # Check for duplicate translation keys - if line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: - if line.has("translation_key"): - if known_translations.has(line.translation_key) and known_translations.get(line.translation_key) != line.text: - add_error(id, indent_size, DialogueConstants.ERR_DUPLICATE_ID) - else: - known_translations[line.translation_key] = line.text - else: - # Default translations key - if DialogueSettings.get_setting("missing_translations_are_errors", false): - add_error(id, indent_size, DialogueConstants.ERR_MISSING_ID) - else: - line["translation_key"] = line.text - - ## Error checking - - # Can't find goto - var jump_index: int = raw_line.find("=>") - match line.next_id: - DialogueConstants.ID_ERROR: - add_error(id, jump_index, DialogueConstants.ERR_UNKNOWN_TITLE) - DialogueConstants.ID_ERROR_INVALID_TITLE: - add_error(id, jump_index, DialogueConstants.ERR_INVALID_TITLE_REFERENCE) - DialogueConstants.ID_ERROR_TITLE_HAS_NO_BODY: - add_error(id, jump_index, DialogueConstants.ERR_TITLE_REFERENCE_HAS_NO_CONTENT) - - # Line after condition isn't indented once to the right - if line.type == DialogueConstants.TYPE_CONDITION: - if is_valid_id(line.next_id): - var next_line: String = raw_lines[line.next_id.to_int()] - var next_indent: int = get_indent(next_line) - if next_indent != indent_size + 1: - add_error(line.next_id.to_int(), next_indent, DialogueConstants.ERR_INVALID_INDENTATION) - else: - add_error(id, indent_size, DialogueConstants.ERR_INVALID_CONDITION_INDENTATION) - - # Line after normal line is indented to the right - elif line.type in [ - DialogueConstants.TYPE_TITLE, - DialogueConstants.TYPE_DIALOGUE, - DialogueConstants.TYPE_MUTATION, - DialogueConstants.TYPE_GOTO - ] and is_valid_id(line.next_id): - var next_line = raw_lines[line.next_id.to_int()] - if next_line != null and get_indent(next_line) > indent_size: - add_error(id, indent_size, DialogueConstants.ERR_INVALID_INDENTATION) - - # Parsing condition failed - if line.has("condition") and line.condition.has("error"): - add_error(id, line.condition.index, line.condition.error) - - # Parsing mutation failed - elif line.has("mutation") and line.mutation.has("error"): - add_error(id, line.mutation.index, line.mutation.error) - - # Line failed to parse at all - if line.get("type") == DialogueConstants.TYPE_UNKNOWN: - add_error(id, 0, DialogueConstants.ERR_UNKNOWN_LINE_SYNTAX) - - # If there are no titles then use the first actual line - if first_title == "" and not is_import_line(raw_line): - first_title = str(id) - - # If this line is the last line of a while loop, edit the id of its next line - if str(id) in while_loopbacks: - if is_goto_snippet_line(raw_line): - line["next_id_after"] = line["parent_id"] - elif is_condition_line(raw_line, true) or is_while_condition_line(raw_line): - line["next_conditional_id"] = line["parent_id"] - line["next_id_after"] = line["parent_id"] - elif is_goto_line(raw_line) or is_title_line(raw_line): - pass - else: - line["next_id"] = line["parent_id"] - - # Done! - parsed_lines[str(id)] = line - - # Assume the last line ends the dialogue - var last_line: Dictionary = parsed_lines.values()[parsed_lines.values().size() - 1] - if last_line.next_id == "": - last_line.next_id = DialogueConstants.ID_END - - if errors.size() > 0: - return ERR_PARSE_ERROR - - return OK - - -func get_data() -> DialogueManagerParseResult: - var data: DialogueManagerParseResult = DialogueManagerParseResult.new() - data.imported_paths = imported_paths - data.using_states = using_states - data.titles = titles - data.character_names = character_names - data.first_title = first_title - data.lines = parsed_lines - data.errors = errors - data.raw_text = raw_text - return data - - -## Get the last parse errors -func get_errors() -> Array[Dictionary]: - return errors - - -## Prepare the parser by collecting all lines and titles -func prepare(text: String, path: String, include_imported_titles_hashes: bool = true) -> void: - using_states = [] - errors = [] - imported_paths = [] - _imported_line_map = {} - while_loopbacks = [] - titles = {} - character_names = [] - first_title = "" - raw_lines = text.split("\n") - - # Work out imports - var known_imports: Dictionary = {} - - # Include the base file path so that we can get around circular dependencies - known_imports[path.hash()] = "." - - var imported_titles: Dictionary = {} - for id in range(0, raw_lines.size()): - var line = raw_lines[id] - if is_import_line(line): - var import_data = extract_import_path_and_name(line) - var import_hash: int = import_data.path.hash() - if import_data.size() > 0: - # Keep track of titles so we can add imported ones later - if str(import_hash) in imported_titles.keys(): - add_error(id, 0, DialogueConstants.ERR_FILE_ALREADY_IMPORTED) - if import_data.prefix in imported_titles.values(): - add_error(id, 0, DialogueConstants.ERR_DUPLICATE_IMPORT_NAME) - imported_titles[str(import_hash)] = import_data.prefix - - # Import the file content - if not known_imports.has(import_hash): - var error: Error = import_content(import_data.path, import_data.prefix, _imported_line_map, known_imports) - if error != OK: - add_error(id, 0, error) - - # Make a map so we can refer compiled lines to where they were imported from - if not _imported_line_map.has(import_hash): - _imported_line_map[import_hash] = { - hash = import_hash, - imported_on_line_number = id, - from_line = 0, - to_line = 0 - } - - var imported_content: String = "" - var cummulative_line_number: int = 0 - for item in _imported_line_map.values(): - item["from_line"] = cummulative_line_number - if known_imports.has(item.hash): - cummulative_line_number += known_imports[item.hash].split("\n").size() - item["to_line"] = cummulative_line_number - if known_imports.has(item.hash): - imported_content += known_imports[item.hash] + "\n" - - _imported_line_count = cummulative_line_number + 1 - - # Join it with the actual content - raw_lines = (imported_content + "\n" + text).split("\n") - - # Find all titles first - for id in range(0, raw_lines.size()): - if raw_lines[id].begins_with("~ "): - var title: String = extract_title(raw_lines[id]) - if title == "": - add_error(id, 2, DialogueConstants.ERR_EMPTY_TITLE) - elif titles.has(title): - add_error(id, 2, DialogueConstants.ERR_DUPLICATE_TITLE) - else: - var next_nonempty_line_id: String = get_next_nonempty_line_id(id) - if next_nonempty_line_id != DialogueConstants.ID_NULL: - titles[title] = next_nonempty_line_id - if "/" in title: - if include_imported_titles_hashes == false: - titles.erase(title) - var bits: PackedStringArray = title.split("/") - if imported_titles.has(bits[0]): - title = imported_titles[bits[0]] + "/" + bits[1] - titles[title] = next_nonempty_line_id - elif first_title == "": - first_title = next_nonempty_line_id - else: - titles[title] = DialogueConstants.ID_ERROR_TITLE_HAS_NO_BODY - - -func add_error(line_number: int, column_number: int, error: int) -> void: - # See if the error was in an imported file - for item in _imported_line_map.values(): - if line_number < item.to_line: - errors.append({ - line_number = item.imported_on_line_number, - column_number = 0, - error = DialogueConstants.ERR_ERRORS_IN_IMPORTED_FILE, - external_error = error, - external_line_number = line_number - }) - return - - # Otherwise, it's in this file - errors.append({ - line_number = line_number - _imported_line_count, - column_number = column_number, - error = error - }) - - -func remove_error(line_number: int, error: int) -> void: - for i in range(errors.size() - 1, -1, -1): - var err = errors[i] - var is_native_error = err.line_number == line_number - _imported_line_count and err.error == error - var is_external_error = err.get("external_line_number") == line_number and err.get("external_error") == error - if is_native_error or is_external_error: - errors.remove_at(i) - return - - -func is_import_line(line: String) -> bool: - return line.begins_with("import ") and " as " in line - - -func is_title_line(line: String) -> bool: - return line.strip_edges(true, false).begins_with("~ ") - - -func is_condition_line(line: String, include_else: bool = true) -> bool: - line = line.strip_edges(true, false) - if line.begins_with("if ") or line.begins_with("elif ") or line.begins_with("else if"): return true - if include_else and line.begins_with("else"): return true - return false - -func is_while_condition_line(line: String) -> bool: - line = line.strip_edges(true, false) - if line.begins_with("while "): return true - return false - - -func is_mutation_line(line: String) -> bool: - line = line.strip_edges(true, false) - return line.begins_with("do ") or line.begins_with("do! ") or line.begins_with("set ") - - -func is_goto_line(line: String) -> bool: - line = line.strip_edges(true, false) - line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line, "") - return line.begins_with("=> ") or line.begins_with("=>< ") - - -func is_goto_snippet_line(line: String) -> bool: - line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line.strip_edges(), "") - return line.begins_with("=>< ") - - -func is_nested_dialogue_line(raw_line: String, parsed_lines: Dictionary, raw_lines: PackedStringArray, indent_size: int) -> bool: - if parsed_lines.values().is_empty(): return false - if raw_line.strip_edges().begins_with("#"): return false - - var parent_line: Dictionary = parsed_lines.values().back() - if parent_line.type != DialogueConstants.TYPE_DIALOGUE: return false - if get_indent(raw_lines[parent_line.id.to_int()]) >= indent_size: return false - return true - - -func is_dialogue_line(line: String) -> bool: - if line == null: return false - if is_response_line(line): return false - if is_title_line(line): return false - if is_condition_line(line, true): return false - if is_mutation_line(line): return false - if is_goto_line(line): return false - return true - - -func is_response_line(line: String) -> bool: - return line.strip_edges(true, false).begins_with("- ") - - -func is_valid_id(id: String) -> bool: - return false if id in [DialogueConstants.ID_NULL, DialogueConstants.ID_ERROR, DialogueConstants.ID_END_CONVERSATION] else true - - -func is_line_empty(line: String) -> bool: - line = line.strip_edges() - - if line == "": return true - if line == "endif": return true - if line.begins_with("#"): return true - - return false - - -func get_line_after_line(id: int, indent_size: int, line: Dictionary) -> String: - # Unless the next line is an outdent we can assume it comes next - 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()]): - 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 - - else: - return DialogueConstants.ID_NULL - - -func get_indent(line: String) -> int: - var tabs: RegExMatch = INDENT_REGEX.search(line) - if tabs: - return tabs.get_string().length() - else: - return 0 - - -func get_next_nonempty_line_id(line_number: int) -> String: - for i in range(line_number + 1, raw_lines.size()): - if not is_line_empty(raw_lines[i]): - return str(i) - return DialogueConstants.ID_NULL - - -func find_previous_response_id(line_number: int) -> String: - var line = raw_lines[line_number] - var indent_size = get_indent(line) - - # Look back up the list to find the previous response - var last_found_response_id: String = str(line_number) - - for i in range(line_number - 1, -1, -1): - line = raw_lines[i] - - if is_line_empty(line): continue - - # If its a response at the same indent level then its a match - elif get_indent(line) == indent_size: - if line.strip_edges().begins_with("- "): - last_found_response_id = str(i) - else: - return last_found_response_id - - # Return itself if nothing was found - return last_found_response_id - - -func apply_weighted_random(id: int, raw_line: String, indent_size: int, line: Dictionary) -> void: - var weight: float = 1 - var found = WEIGHTED_RANDOM_SIBLINGS_REGEX.search(raw_line) - if found and found.names.has("weight"): - weight = found.strings[found.names.weight].to_float() - - # Look back up the list to find the first weighted random line in this group - var original_random_line: Dictionary = {} - for i in range(id, 0, -1): - # Ignore doc comment lines - if raw_lines[i].strip_edges().begins_with("##"): - continue - # Lines that aren't prefixed with the random token are a dead end - if not raw_lines[i].strip_edges().begins_with("%") or get_indent(raw_lines[i]) != indent_size: - break - # Make sure we group random dialogue and random lines separately - elif WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line.strip_edges(), "").begins_with("=") != WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_lines[i].strip_edges(), "").begins_with("="): - break - # Otherwise we've found the origin - elif parsed_lines.has(str(i)) and parsed_lines[str(i)].has("siblings"): - original_random_line = parsed_lines[str(i)] - break - - # Attach it to the original random line and work out where to go after the line - if original_random_line.size() > 0: - original_random_line["siblings"] += [{ weight = weight, id = str(id) }] - if original_random_line.type != DialogueConstants.TYPE_GOTO: - # Update the next line for all siblings (not goto lines, though, they manage their - # own next ID) - original_random_line["next_id"] = get_line_after_line(id, indent_size, line) - for sibling in original_random_line["siblings"]: - if sibling.id in parsed_lines: - parsed_lines[sibling.id]["next_id"] = original_random_line["next_id"] - elif original_random_line.has("next_id_after"): - original_random_line["next_id_after"] = get_line_after_line(id, indent_size, line) - for sibling in original_random_line["siblings"]: - if sibling.id in parsed_lines: - parsed_lines[sibling.id]["next_id_after"] = original_random_line["next_id_after"] - - line["next_id"] = original_random_line.next_id - # Or set up this line as the original - else: - line["siblings"] = [{ weight = weight, id = str(id) }] - line["next_id"] = get_line_after_line(id, indent_size, line) - - if line.next_id == DialogueConstants.ID_NULL: - line["next_id"] = DialogueConstants.ID_END - - -func find_next_condition_sibling(line_number: int) -> String: - var line = raw_lines[line_number] - var expected_indent = get_indent(line) - - # Look down the list and find an elif or else at the same indent level - for i in range(line_number + 1, raw_lines.size()): - line = raw_lines[i] - if is_line_empty(line): continue - - var l = line.strip_edges() - if l.begins_with("~ "): - return DialogueConstants.ID_END_CONVERSATION - - elif get_indent(line) < expected_indent: - return DialogueConstants.ID_NULL - - elif get_indent(line) == expected_indent: - # Found an if, which begins a different block - if l.begins_with("if"): - return DialogueConstants.ID_NULL - - # Found what we're looking for - elif (l.begins_with("elif ") or l.begins_with("else")): - return str(i) - - return DialogueConstants.ID_NULL - - -func find_next_line_after_conditions(line_number: int) -> String: - var line = raw_lines[line_number] - var expected_indent = get_indent(line) - - # Look down the list for the first non condition line at the same or less indent level - for i in range(line_number + 1, raw_lines.size()): - line = raw_lines[i] - - if is_line_empty(line): continue - - var line_indent = get_indent(line) - line = line.strip_edges() - - if is_title_line(line): - return get_next_nonempty_line_id(i) - - elif line_indent > expected_indent: - continue - - elif line_indent == expected_indent: - if line.begins_with("elif ") or line.begins_with("else"): - continue - else: - return str(i) - - elif line_indent < expected_indent: - # We have to check the parent of this block - for p in range(line_number - 1, -1, -1): - line = raw_lines[p] - - if is_line_empty(line): continue - - line_indent = get_indent(line) - if line_indent < expected_indent: - return parsed_lines[str(p)].get("next_id_after", DialogueConstants.ID_NULL) - - return DialogueConstants.ID_END_CONVERSATION - -func find_last_line_within_conditions(line_number: int) -> String: - var line = raw_lines[line_number] - var expected_indent = get_indent(line) - - var candidate = DialogueConstants.ID_NULL - - # Look down the list for the last line that has an indent level 1 more than this line - # Ending the search when you find a line the same or less indent level - for i in range(line_number + 1, raw_lines.size()): - line = raw_lines[i] - - if is_line_empty(line): continue - - var line_indent = get_indent(line) - line = line.strip_edges() - - if line_indent > expected_indent + 1: - continue - elif line_indent == (expected_indent + 1): - candidate = i - else: - break - - return str(candidate) - -func find_next_line_after_responses(line_number: int) -> String: - var line = raw_lines[line_number] - var expected_indent = get_indent(line) - - # Find the first line after this one that has a smaller indent that isn't another option - # If we hit the eof then we give up - for i in range(line_number + 1, raw_lines.size()): - line = raw_lines[i] - - if is_line_empty(line): continue - - var indent = get_indent(line) - - line = line.strip_edges() - - # We hit a title so the next line is a new start - if is_title_line(line): - return get_next_nonempty_line_id(i) - - # Another option - elif line.begins_with("- "): - if indent == expected_indent: - # ...at the same level so we continue - continue - elif indent < expected_indent: - # ...outdented so check the previous parent - var previous_parent = parent_stack[parent_stack.size() - 2] - if parsed_lines.has(str(previous_parent)): - return parsed_lines[str(previous_parent)].next_id_after - else: - return DialogueConstants.ID_NULL - - # We're at the end of a conditional so jump back up to see what's after it - elif line.begins_with("elif ") or line.begins_with("else"): - for p in range(line_number - 1, -1, -1): - line = raw_lines[p] - - if is_line_empty(line): continue - - var line_indent = get_indent(line) - if line_indent < expected_indent: - return parsed_lines[str(p)].next_id_after - - # Otherwise check the indent for an outdent - else: - line_number = i - line = raw_lines[line_number] - if get_indent(line) <= expected_indent: - return str(line_number) - - # EOF so it's also the end of a block - return DialogueConstants.ID_END - - -## Get the names of any autoloads in the project -func get_autoload_names() -> PackedStringArray: - var autoloads: PackedStringArray = [] - - var project = ConfigFile.new() - project.load("res://project.godot") - if project.has_section("autoload"): - return Array(project.get_section_keys("autoload")).filter(func(key): return key != "DialogueManager") - - return autoloads - - -## Import content from another dialogue file or return an ERR -func import_content(path: String, prefix: String, imported_line_map: Dictionary, known_imports: Dictionary) -> Error: - if FileAccess.file_exists(path): - var file = FileAccess.open(path, FileAccess.READ) - var content: PackedStringArray = file.get_as_text().split("\n") - - var imported_titles: Dictionary = {} - - for index in range(0, content.size()): - var line = content[index] - if is_import_line(line): - var import = extract_import_path_and_name(line) - if import.size() > 0: - if not known_imports.has(import.path.hash()): - # Add an empty record into the keys just so we don't end up with cyclic dependencies - known_imports[import.path.hash()] = "" - if import_content(import.path, import.prefix, imported_line_map, known_imports) != OK: - return ERR_LINK_FAILED - - if not imported_line_map.has(import.path.hash()): - # Make a map so we can refer compiled lines to where they were imported from - imported_line_map[import.path.hash()] = { - hash = import.path.hash(), - imported_on_line_number = index, - from_line = 0, - to_line = 0 - } - - imported_titles[import.prefix] = import.path.hash() - - var origin_hash: int = -1 - for hash_value in known_imports.keys(): - if known_imports[hash_value] == ".": - origin_hash = hash_value - - # Replace any titles or jump points with references to the files they point to (event if they point to their own file) - for i in range(0, content.size()): - var line = content[i] - if is_title_line(line): - var title = extract_title(line) - if "/" in line: - var bits = title.split("/") - content[i] = "~ %s/%s" % [imported_titles[bits[0]], bits[1]] - else: - content[i] = "~ %s/%s" % [str(path.hash()), title] - - elif "=>< " in line: - var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() - if "/" in jump: - var bits: PackedStringArray = jump.split("/") - var title_hash: int = imported_titles[bits[0]] - if title_hash == origin_hash: - content[i] = "%s=>< %s" % [line.split("=>< ")[0], bits[1]] - else: - content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], title_hash, bits[1]] - - elif not jump in ["END", "END!"]: - content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], str(path.hash()), jump] - - elif "=> " in line: - var jump: String = line.substr(line.find("=> ") + "=> ".length()).strip_edges() - if "/" in jump: - var bits: PackedStringArray = jump.split("/") - var title_hash: int = imported_titles[bits[0]] - if title_hash == origin_hash: - content[i] = "%s=> %s" % [line.split("=> ")[0], bits[1]] - else: - content[i] = "%s=> %s/%s" % [line.split("=> ")[0], title_hash, bits[1]] - - elif not jump in ["END", "END!"]: - content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] - - imported_paths.append(path) - known_imports[path.hash()] = "\n".join(content) + "\n=> END\n" - return OK - else: - return ERR_FILE_NOT_FOUND - - -func extract_import_path_and_name(line: String) -> Dictionary: - var found: RegExMatch = IMPORT_REGEX.search(line) - if found: - return { - path = found.strings[found.names.path], - prefix = found.strings[found.names.prefix] - } - else: - return {} - - -func extract_title(line: String) -> String: - return line.substr(2).strip_edges() - - -func extract_translation(line: String) -> String: - # Find a static translation key, eg. [ID:something] - var found: RegExMatch = TRANSLATION_REGEX.search(line) - if found: - return found.strings[found.names.tr] - else: - return "" - - -func extract_response_prompt(line: String) -> String: - # Find just the text prompt from a response, ignoring any conditions or gotos - line = line.substr(2) - if " [if " in line: - line = line.substr(0, line.find(" [if ")) - if " =>" in line: - line = line.substr(0, line.find(" =>")) - - # Without the translation key if there is one - var translation_key: String = extract_translation(line) - if translation_key: - line = line.replace("[ID:%s]" % translation_key, "") - - return line.replace("\\n", "\n").strip_edges() - - -func parse_response_character_and_text(id: int, text: String, line: Dictionary, indent_size: int, parsed_lines: Dictionary) -> void: - var bits = Array(text.strip_edges().split(": ")) - line["character"] = bits.pop_front().strip_edges() - line["character_replacements"] = extract_dialogue_replacements(line.character, line.character.length() + 2 + indent_size) - for replacement in line.character_replacements: - if replacement.has("error"): - add_error(id, replacement.index, replacement.error) - - if not line["character"] in character_names: - character_names.append(line["character"]) - - line["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":").strip_edges() - - if line.get("translation_key", null) == null: - line["translation_key"] = line.text - - -func extract_mutation(line: String) -> Dictionary: - var found: RegExMatch = MUTATION_REGEX.search(line) - - if not found: - return { - index = 0, - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - - if found.names.has("mutation"): - var expression: Array = tokenise(found.strings[found.names.mutation], DialogueConstants.TYPE_MUTATION, found.get_start("mutation")) - if expression.size() == 0: - return { - index = found.get_start("mutation"), - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - elif expression[0].type == DialogueConstants.TYPE_ERROR: - return { - index = expression[0].index, - error = expression[0].value - } - else: - return { - expression = expression, - is_blocking = not "!" in found.strings[found.names.keyword] - } - - else: - return { - index = found.get_start(), - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - - -func extract_condition(raw_line: String, is_wrapped: bool, index: int) -> Dictionary: - var condition: Dictionary = {} - - var regex: RegEx = WRAPPED_CONDITION_REGEX if is_wrapped else CONDITION_REGEX - var found: RegExMatch = regex.search(raw_line) - - if found == null: - return { - index = 0, - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - - var raw_condition: String = found.strings[found.names.condition] - var expression: Array = tokenise(raw_condition, DialogueConstants.TYPE_CONDITION, index + found.get_start("condition")) - - if expression.size() == 0: - return { - index = index + found.get_start("condition"), - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - elif expression[0].type == DialogueConstants.TYPE_ERROR: - return { - index = expression[0].index, - error = expression[0].value - } - else: - return { - expression = expression - } - - -func extract_dialogue_replacements(text: String, index: int) -> Array[Dictionary]: - var founds: Array[RegExMatch] = REPLACEMENTS_REGEX.search_all(text) - - if founds == null or founds.size() == 0: - return [] - - var replacements: Array[Dictionary] = [] - for found in founds: - var replacement: Dictionary = {} - var value_in_text: String = found.strings[1] - var expression: Array = tokenise(value_in_text, DialogueConstants.TYPE_DIALOGUE, index + found.get_start(1)) - if expression.size() == 0: - replacement = { - index = index + found.get_start(1), - error = DialogueConstants.ERR_INCOMPLETE_EXPRESSION - } - elif expression[0].type == DialogueConstants.TYPE_ERROR: - replacement = { - index = expression[0].index, - error = expression[0].value - } - else: - replacement = { - value_in_text = "{{%s}}" % value_in_text, - expression = expression - } - replacements.append(replacement) - - return replacements - - -func extract_goto(line: String) -> String: - var found: RegExMatch = GOTO_REGEX.search(line) - - if found == null: return DialogueConstants.ID_ERROR - - var title: String = found.strings[found.names.jump_to_title].strip_edges() - - if " " in title or title == "": - return DialogueConstants.ID_ERROR_INVALID_TITLE - - # "=> END!" means end the conversation - if title == "END!": - return DialogueConstants.ID_END_CONVERSATION - # "=> END" means end the current title (and go back to the previous one if there is one - # in the stack) - elif title == "END": - return DialogueConstants.ID_END - - elif titles.has(title): - return titles.get(title) - else: - return DialogueConstants.ID_ERROR - - -func extract_tags(line: String) -> ResolvedTagData: - var resolved_tags: PackedStringArray = [] - var tag_matches: Array[RegExMatch] = TAGS_REGEX.search_all(line) - for tag_match in tag_matches: - line = line.replace(tag_match.get_string(), "") - var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",") - for tag in tags: - tag = tag.replace("#", "") - if not tag in resolved_tags: - resolved_tags.append(tag) - - return ResolvedTagData.new({ - tags = resolved_tags, - line_without_tags = line - }) - - -func extract_markers(line: String) -> ResolvedLineData: - var text: String = line - var pauses: Dictionary = {} - var speeds: Dictionary = {} - var mutations: Array[Array] = [] - var bbcodes: Array = [] - var time: String = "" - - # Remove any escaped brackets (ie. "\[") - var escaped_open_brackets: PackedInt32Array = [] - var escaped_close_brackets: PackedInt32Array = [] - for i in range(0, text.length() - 1): - if text.substr(i, 2) == "\\[": - text = text.substr(0, i) + "!" + text.substr(i + 2) - escaped_open_brackets.append(i) - elif text.substr(i, 2) == "\\]": - text = text.substr(0, i) + "!" + text.substr(i + 2) - escaped_close_brackets.append(i) - - # Extract all of the BB codes so that we know the actual text (we could do this easier with - # a RichTextLabel but then we'd need to await idle_frame which is annoying) - var bbcode_positions = find_bbcode_positions_in_string(text) - var accumulaive_length_offset = 0 - for position in bbcode_positions: - # Ignore our own markers - if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]: - continue - - bbcodes.append({ - bbcode = position.bbcode, - start = position.start, - offset_start = position.start - accumulaive_length_offset - }) - accumulaive_length_offset += position.bbcode.length() - - for bb in bbcodes: - text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length()) - - # Now find any dialogue markers - var next_bbcode_position = find_bbcode_positions_in_string(text, false) - var limit = 0 - while next_bbcode_position.size() > 0 and limit < 1000: - limit += 1 - - var bbcode = next_bbcode_position[0] - - var index = bbcode.start - var code = bbcode.code - var raw_args = bbcode.raw_args - var args = {} - if code in ["do", "do!", "set"]: - args["value"] = extract_mutation("%s %s" % [code, raw_args]) - else: - # Could be something like: - # "=1.0" - # " rate=20 level=10" - if raw_args and raw_args[0] == "=": - raw_args = "value" + raw_args - for pair in raw_args.strip_edges().split(" "): - if "=" in pair: - var bits = pair.split("=") - args[bits[0]] = bits[1] - - match code: - "wait": - if pauses.has(index): - pauses[index] += args.get("value").to_float() - else: - pauses[index] = args.get("value").to_float() - "speed": - speeds[index] = args.get("value").to_float() - "/speed": - speeds[index] = 1.0 - "do", "do!", "set": - mutations.append([index, args.get("value")]) - "next": - time = args.get("value") if args.has("value") else "0" - - # Find any BB codes that are after this index and remove the length from their start - var length = bbcode.bbcode.length() - for bb in bbcodes: - if bb.offset_start > bbcode.start: - bb.offset_start -= length - bb.start -= length - - # Find any escaped brackets after this that need moving - for i in range(0, escaped_open_brackets.size()): - if escaped_open_brackets[i] > bbcode.start: - escaped_open_brackets[i] -= length - for i in range(0, escaped_close_brackets.size()): - if escaped_close_brackets[i] > bbcode.start: - escaped_close_brackets[i] -= length - - text = text.substr(0, index) + text.substr(index + length) - next_bbcode_position = find_bbcode_positions_in_string(text, false) - - # Put the BB Codes back in - for bb in bbcodes: - text = text.insert(bb.start, bb.bbcode) - - # Put the escaped brackets back in - for index in escaped_open_brackets: - text = text.left(index) + "[" + text.right(text.length() - index - 1) - for index in escaped_close_brackets: - text = text.left(index) + "]" + text.right(text.length() - index - 1) - - return ResolvedLineData.new({ - text = text, - pauses = pauses, - speeds = speeds, - mutations = mutations, - time = time - }) - - -func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> Array[Dictionary]: - if not "[" in string: return [] - - var positions: Array[Dictionary] = [] - - var open_brace_count: int = 0 - var start: int = 0 - var bbcode: String = "" - var code: String = "" - var is_finished_code: bool = false - for i in range(0, string.length()): - if string[i] == "[": - if open_brace_count == 0: - start = i - bbcode = "" - code = "" - is_finished_code = false - open_brace_count += 1 - - else: - if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"): - code += string[i] - else: - is_finished_code = true - - if open_brace_count > 0: - bbcode += string[i] - - if string[i] == "]": - open_brace_count -= 1 - if open_brace_count == 0 and not code in ["if", "else", "/if"]: - positions.append({ - bbcode = bbcode, - code = code, - start = start, - raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges() - }) - - if not find_all: - return positions - - return positions - - -func tokenise(text: String, line_type: String, index: int) -> Array: - var tokens: Array[Dictionary] = [] - var limit: int = 0 - while text.strip_edges() != "" and limit < 1000: - limit += 1 - var found = find_match(text) - if found.size() > 0: - tokens.append({ - index = index, - type = found.type, - value = found.value - }) - index += found.value.length() - text = found.remaining_text - elif text.begins_with(" "): - index += 1 - text = text.substr(1) - else: - return build_token_tree_error(DialogueConstants.ERR_INVALID_EXPRESSION, index) - - return build_token_tree(tokens, line_type, "")[0] - - -func build_token_tree_error(error: int, index: int) -> Array: - return [{ type = DialogueConstants.TOKEN_ERROR, value = error, index = index }] - - -func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array: - var tree: Array[Dictionary] = [] - var limit = 0 - while tokens.size() > 0 and limit < 1000: - limit += 1 - var token = tokens.pop_front() - - var error = check_next_token(token, tokens, line_type, expected_close_token) - if error != OK: - return [build_token_tree_error(error, token.index), tokens] - - match token.type: - DialogueConstants.TOKEN_FUNCTION: - var sub_tree = build_token_tree(tokens, line_type, DialogueConstants.TOKEN_PARENS_CLOSE) - - if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: - return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] - - tree.append({ - type = DialogueConstants.TOKEN_FUNCTION, - # Consume the trailing "(" - function = token.value.substr(0, token.value.length() - 1), - value = tokens_to_list(sub_tree[0]) - }) - tokens = sub_tree[1] - - DialogueConstants.TOKEN_DICTIONARY_REFERENCE: - var sub_tree = build_token_tree(tokens, line_type, DialogueConstants.TOKEN_BRACKET_CLOSE) - - if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: - return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] - - var args = tokens_to_list(sub_tree[0]) - if args.size() != 1: - return [build_token_tree_error(DialogueConstants.ERR_INVALID_INDEX, token.index), tokens] - - tree.append({ - type = DialogueConstants.TOKEN_DICTIONARY_REFERENCE, - # Consume the trailing "[" - variable = token.value.substr(0, token.value.length() - 1), - value = args[0] - }) - tokens = sub_tree[1] - - DialogueConstants.TOKEN_BRACE_OPEN: - var sub_tree = build_token_tree(tokens, line_type, DialogueConstants.TOKEN_BRACE_CLOSE) - - if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: - return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] - - var t = sub_tree[0] - for i in range(0, t.size() - 2): - # Convert Lua style dictionaries to string keys - if t[i].type == DialogueConstants.TOKEN_VARIABLE and t[i+1].type == DialogueConstants.TOKEN_ASSIGNMENT: - t[i].type = DialogueConstants.TOKEN_STRING - t[i+1].type = DialogueConstants.TOKEN_COLON - t[i+1].erase("value") - - tree.append({ - type = DialogueConstants.TOKEN_DICTIONARY, - value = tokens_to_dictionary(sub_tree[0]) - }) - - tokens = sub_tree[1] - - DialogueConstants.TOKEN_BRACKET_OPEN: - var sub_tree = build_token_tree(tokens, line_type, DialogueConstants.TOKEN_BRACKET_CLOSE) - - if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: - return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] - - var type = DialogueConstants.TOKEN_ARRAY - var value = tokens_to_list(sub_tree[0]) - - # See if this is referencing a nested dictionary value - if tree.size() > 0: - var previous_token = tree[tree.size() - 1] - if previous_token.type in [DialogueConstants.TOKEN_DICTIONARY_REFERENCE, DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]: - type = DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE - value = value[0] - - tree.append({ - type = type, - value = value - }) - tokens = sub_tree[1] - - DialogueConstants.TOKEN_PARENS_OPEN: - var sub_tree = build_token_tree(tokens, line_type, DialogueConstants.TOKEN_PARENS_CLOSE) - - if sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: - return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] - - tree.append({ - type = DialogueConstants.TOKEN_GROUP, - value = sub_tree[0] - }) - tokens = sub_tree[1] - - DialogueConstants.TOKEN_PARENS_CLOSE, \ - DialogueConstants.TOKEN_BRACE_CLOSE, \ - DialogueConstants.TOKEN_BRACKET_CLOSE: - if token.type != expected_close_token: - return [build_token_tree_error(DialogueConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] - - return [tree, tokens] - - DialogueConstants.TOKEN_NOT: - # Double nots negate each other - if tokens.size() > 0 and tokens.front().type == DialogueConstants.TOKEN_NOT: - tokens.pop_front() - else: - tree.append({ - type = token.type - }) - - DialogueConstants.TOKEN_COMMA, \ - DialogueConstants.TOKEN_COLON, \ - DialogueConstants.TOKEN_DOT: - tree.append({ - type = token.type - }) - - DialogueConstants.TOKEN_COMPARISON, \ - DialogueConstants.TOKEN_ASSIGNMENT, \ - DialogueConstants.TOKEN_OPERATOR, \ - DialogueConstants.TOKEN_AND_OR, \ - DialogueConstants.TOKEN_VARIABLE: - var value = token.value.strip_edges() - if value == "&&": - value = "and" - elif value == "||": - value = "or" - tree.append({ - type = token.type, - value = value - }) - - DialogueConstants.TOKEN_STRING: - if token.value.begins_with("&"): - tree.append({ - type = token.type, - value = StringName(token.value.substr(2, token.value.length() - 3)) - }) - else: - tree.append({ - type = token.type, - value = token.value.substr(1, token.value.length() - 2) - }) - - DialogueConstants.TOKEN_CONDITION: - return [build_token_tree_error(DialogueConstants.ERR_UNEXPECTED_CONDITION, token.index), token] - - DialogueConstants.TOKEN_BOOL: - tree.append({ - type = token.type, - value = token.value.to_lower() == "true" - }) - - DialogueConstants.TOKEN_NUMBER: - var value = token.value.to_float() if "." in token.value else token.value.to_int() - # If previous token is a number and this one is a negative number then - # inject a minus operator token in between them. - if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DialogueConstants.TOKEN_NUMBER: - tree.append(({ - type = DialogueConstants.TOKEN_OPERATOR, - value = "-" - })) - tree.append({ - type = token.type, - value = -1 * value - }) - else: - tree.append({ - type = token.type, - value = value - }) - - if expected_close_token != "": - var index: int = tokens[0].index if tokens.size() > 0 else 0 - return [build_token_tree_error(DialogueConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] - - return [tree, tokens] - - -func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error: - var next_token: Dictionary = { type = null } - if next_tokens.size() > 0: - next_token = next_tokens.front() - - # Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary - # then it's an unexpected assignment in a condition line. - if token.type == DialogueConstants.TOKEN_ASSIGNMENT and line_type == DialogueConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token): - return DialogueConstants.ERR_UNEXPECTED_ASSIGNMENT - - # Special case for a negative number after this one - if token.type == DialogueConstants.TOKEN_NUMBER and next_token.type == DialogueConstants.TOKEN_NUMBER and next_token.value.begins_with("-"): - return OK - - var expected_token_types = [] - var unexpected_token_types = [] - match token.type: - DialogueConstants.TOKEN_FUNCTION, \ - DialogueConstants.TOKEN_PARENS_OPEN: - unexpected_token_types = [ - null, - DialogueConstants.TOKEN_COMMA, - DialogueConstants.TOKEN_COLON, - DialogueConstants.TOKEN_COMPARISON, - DialogueConstants.TOKEN_ASSIGNMENT, - DialogueConstants.TOKEN_OPERATOR, - DialogueConstants.TOKEN_AND_OR, - DialogueConstants.TOKEN_DOT - ] - - DialogueConstants.TOKEN_BRACKET_CLOSE: - unexpected_token_types = [ - DialogueConstants.TOKEN_NOT, - DialogueConstants.TOKEN_BOOL, - DialogueConstants.TOKEN_STRING, - DialogueConstants.TOKEN_NUMBER, - DialogueConstants.TOKEN_VARIABLE - ] - - DialogueConstants.TOKEN_BRACE_OPEN: - expected_token_types = [ - DialogueConstants.TOKEN_STRING, - DialogueConstants.TOKEN_VARIABLE, - DialogueConstants.TOKEN_NUMBER, - DialogueConstants.TOKEN_BRACE_CLOSE - ] - - DialogueConstants.TOKEN_PARENS_CLOSE, \ - DialogueConstants.TOKEN_BRACE_CLOSE: - unexpected_token_types = [ - DialogueConstants.TOKEN_NOT, - DialogueConstants.TOKEN_ASSIGNMENT, - DialogueConstants.TOKEN_BOOL, - DialogueConstants.TOKEN_STRING, - DialogueConstants.TOKEN_NUMBER, - DialogueConstants.TOKEN_VARIABLE - ] - - DialogueConstants.TOKEN_COMPARISON, \ - DialogueConstants.TOKEN_OPERATOR, \ - DialogueConstants.TOKEN_COMMA, \ - DialogueConstants.TOKEN_DOT, \ - DialogueConstants.TOKEN_NOT, \ - DialogueConstants.TOKEN_AND_OR, \ - DialogueConstants.TOKEN_DICTIONARY_REFERENCE: - unexpected_token_types = [ - null, - DialogueConstants.TOKEN_COMMA, - DialogueConstants.TOKEN_COLON, - DialogueConstants.TOKEN_COMPARISON, - DialogueConstants.TOKEN_ASSIGNMENT, - DialogueConstants.TOKEN_OPERATOR, - DialogueConstants.TOKEN_AND_OR, - DialogueConstants.TOKEN_PARENS_CLOSE, - DialogueConstants.TOKEN_BRACE_CLOSE, - DialogueConstants.TOKEN_BRACKET_CLOSE, - DialogueConstants.TOKEN_DOT - ] - - DialogueConstants.TOKEN_COLON: - unexpected_token_types = [ - DialogueConstants.TOKEN_COMMA, - DialogueConstants.TOKEN_COLON, - DialogueConstants.TOKEN_COMPARISON, - DialogueConstants.TOKEN_ASSIGNMENT, - DialogueConstants.TOKEN_OPERATOR, - DialogueConstants.TOKEN_AND_OR, - DialogueConstants.TOKEN_PARENS_CLOSE, - DialogueConstants.TOKEN_BRACE_CLOSE, - DialogueConstants.TOKEN_BRACKET_CLOSE, - DialogueConstants.TOKEN_DOT - ] - - DialogueConstants.TOKEN_BOOL, \ - DialogueConstants.TOKEN_STRING, \ - DialogueConstants.TOKEN_NUMBER: - unexpected_token_types = [ - DialogueConstants.TOKEN_NOT, - DialogueConstants.TOKEN_ASSIGNMENT, - DialogueConstants.TOKEN_BOOL, - DialogueConstants.TOKEN_STRING, - DialogueConstants.TOKEN_NUMBER, - DialogueConstants.TOKEN_VARIABLE, - DialogueConstants.TOKEN_FUNCTION, - DialogueConstants.TOKEN_PARENS_OPEN, - DialogueConstants.TOKEN_BRACE_OPEN, - DialogueConstants.TOKEN_BRACKET_OPEN - ] - - DialogueConstants.TOKEN_VARIABLE: - unexpected_token_types = [ - DialogueConstants.TOKEN_NOT, - DialogueConstants.TOKEN_BOOL, - DialogueConstants.TOKEN_STRING, - DialogueConstants.TOKEN_NUMBER, - DialogueConstants.TOKEN_VARIABLE, - DialogueConstants.TOKEN_FUNCTION, - DialogueConstants.TOKEN_PARENS_OPEN, - DialogueConstants.TOKEN_BRACE_OPEN, - DialogueConstants.TOKEN_BRACKET_OPEN - ] - - if (expected_token_types.size() > 0 and not next_token.type in expected_token_types or unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): - match next_token.type: - null: - return DialogueConstants.ERR_UNEXPECTED_END_OF_EXPRESSION - - DialogueConstants.TOKEN_FUNCTION: - return DialogueConstants.ERR_UNEXPECTED_FUNCTION - - DialogueConstants.TOKEN_PARENS_OPEN, \ - DialogueConstants.TOKEN_PARENS_CLOSE: - return DialogueConstants.ERR_UNEXPECTED_BRACKET - - DialogueConstants.TOKEN_COMPARISON, \ - DialogueConstants.TOKEN_ASSIGNMENT, \ - DialogueConstants.TOKEN_OPERATOR, \ - DialogueConstants.TOKEN_NOT, \ - DialogueConstants.TOKEN_AND_OR: - return DialogueConstants.ERR_UNEXPECTED_OPERATOR - - DialogueConstants.TOKEN_COMMA: - return DialogueConstants.ERR_UNEXPECTED_COMMA - DialogueConstants.TOKEN_COLON: - return DialogueConstants.ERR_UNEXPECTED_COLON - DialogueConstants.TOKEN_DOT: - return DialogueConstants.ERR_UNEXPECTED_DOT - - DialogueConstants.TOKEN_BOOL: - return DialogueConstants.ERR_UNEXPECTED_BOOLEAN - DialogueConstants.TOKEN_STRING: - return DialogueConstants.ERR_UNEXPECTED_STRING - DialogueConstants.TOKEN_NUMBER: - return DialogueConstants.ERR_UNEXPECTED_NUMBER - DialogueConstants.TOKEN_VARIABLE: - return DialogueConstants.ERR_UNEXPECTED_VARIABLE - - return DialogueConstants.ERR_INVALID_EXPRESSION - - return OK - - -func tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]: - var list: Array[Array] = [] - var current_item: Array[Dictionary] = [] - for token in tokens: - if token.type == DialogueConstants.TOKEN_COMMA: - list.append(current_item) - current_item = [] - else: - current_item.append(token) - - if current_item.size() > 0: - list.append(current_item) - - return list - - -func tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary: - var dictionary = {} - for i in range(0, tokens.size()): - if tokens[i].type == DialogueConstants.TOKEN_COLON: - if tokens.size() == i + 2: - dictionary[tokens[i-1]] = tokens[i+1] - else: - dictionary[tokens[i-1]] = { type = DialogueConstants.TOKEN_GROUP, value = tokens.slice(i+1) } - - return dictionary - - -func find_match(input: String) -> Dictionary: - for key in TOKEN_DEFINITIONS.keys(): - var regex = TOKEN_DEFINITIONS.get(key) - var found = regex.search(input) - if found: - return { - type = key, - remaining_text = input.substr(found.strings[0].length()), - value = found.strings[0] - } - - return {} diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd.uid deleted file mode 100644 index 22f6601c..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/parser.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://kmwjxv2fbqfk diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd deleted file mode 100644 index 10735862..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd +++ /dev/null @@ -1,15 +0,0 @@ -extends RefCounted - -var text: String = "" -var pauses: Dictionary = {} -var speeds: Dictionary = {} -var mutations: Array[Array] = [] -var time: String = "" - - -func _init(data: Dictionary) -> void: - text = data.text - pauses = data.pauses - speeds = data.speeds - mutations = data.mutations - time = data.time diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd.uid deleted file mode 100644 index a749f2fd..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_line_data.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dubbguqmg0y3y diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd deleted file mode 100644 index 728cc423..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends RefCounted - - -var tags: PackedStringArray = [] -var line_without_tags: String = "" - - -func _init(data: Dictionary) -> void: - tags = data.tags - line_without_tags = data.line_without_tags diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd.uid deleted file mode 100644 index 0214d822..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/resolved_tag_data.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://do3qcb4qvked0 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd.uid index d8f5c01d..66ec8267 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd.uid @@ -1 +1 @@ -uid://bs2b6vbxavpev +uid://cijsmjkq21cdq diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.tscn index 82dd60d7..52721c4a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] +[ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] [node name="SearchAndReplace" type="VBoxContainer"] visible = false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd index ee7cd139..67cdcf0f 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd @@ -48,6 +48,8 @@ func apply_filter() -> void: func apply_theme() -> void: if is_instance_valid(filter_edit): filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) ### Signals diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd.uid index 4e00eaa4..325ca61d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.gd.uid @@ -1 +1 @@ -uid://5t2mxrdh0xm3 +uid://d0k2wndjj0ifm diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.tscn index 6273122e..ac2b9833 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/title_list.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"] +[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"] [node name="TitleList" type="VBoxContainer"] anchors_preset = 15 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd index 2f77c63d..cf3f1b94 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd @@ -86,9 +86,9 @@ func _on_update_button_pressed() -> void: if needs_reload: var will_refresh = on_before_refresh.call() if will_refresh: - Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true) + EditorInterface.restart_editor(true) else: - var scale: float = Engine.get_meta("DialogueManagerPlugin").get_editor_interface().get_editor_scale() + var scale: float = EditorInterface.get_editor_scale() download_dialog.min_size = Vector2(300, 250) * scale download_dialog.popup_centered() @@ -117,7 +117,7 @@ func _on_download_update_panel_failed() -> void: func _on_needs_reload_dialog_confirmed() -> void: - Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true) + EditorInterface.restart_editor(true) func _on_timer_timeout() -> void: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd.uid index f1f658a2..9981132e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.gd.uid @@ -1 +1 @@ -uid://boqd8cx71f1af +uid://cr1tt12dh5ecr diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.tscn index 533a94e1..6cff3471 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/update_button.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"] +[ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"] [ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"] [node name="UpdateButton" type="Button"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd index cb556838..91e6bc7c 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd @@ -1,9 +1,23 @@ -extends Node +class_name DMConstants extends RefCounted const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json" const CACHE_PATH = "user://dialogue_manager_cache.json" + +enum MutationBehaviour { + Wait, + DoNotWait, + Skip +} + +enum TranslationSource { + None, + Guess, + CSV, + PO +} + # Token types const TOKEN_FUNCTION = &"function" @@ -33,21 +47,27 @@ const TOKEN_NUMBER = &"number" const TOKEN_VARIABLE = &"variable" const TOKEN_COMMENT = &"comment" +const TOKEN_VALUE = &"value" const TOKEN_ERROR = &"error" # Line types -const TYPE_UNKNOWN = &"unknown" +const TYPE_UNKNOWN = &"" +const TYPE_IMPORT = &"import" +const TYPE_USING = &"using" +const TYPE_COMMENT = &"comment" const TYPE_RESPONSE = &"response" const TYPE_TITLE = &"title" const TYPE_CONDITION = &"condition" +const TYPE_WHILE = &"while" +const TYPE_MATCH = &"match" +const TYPE_WHEN = &"when" const TYPE_MUTATION = &"mutation" const TYPE_GOTO = &"goto" const TYPE_DIALOGUE = &"dialogue" +const TYPE_RANDOM = &"random" const TYPE_ERROR = &"error" -const TYPE_ELSE = &"else" - # Line IDs const ID_NULL = &"" @@ -64,7 +84,6 @@ const ERR_FILE_ALREADY_IMPORTED = 101 const ERR_DUPLICATE_IMPORT_NAME = 102 const ERR_EMPTY_TITLE = 103 const ERR_DUPLICATE_TITLE = 104 -const ERR_NESTED_TITLE = 105 const ERR_TITLE_INVALID_CHARACTERS = 106 const ERR_UNKNOWN_TITLE = 107 const ERR_INVALID_TITLE_REFERENCE = 108 @@ -95,6 +114,11 @@ const ERR_UNEXPECTED_VARIABLE = 132 const ERR_INVALID_INDEX = 133 const ERR_UNEXPECTED_ASSIGNMENT = 134 const ERR_UNKNOWN_USING = 135 +const ERR_EXPECTED_WHEN_OR_ELSE = 136 +const ERR_ONLY_ONE_ELSE_ALLOWED = 137 +const ERR_WHEN_MUST_BELONG_TO_MATCH = 138 +const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 +const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 ## Get the error message @@ -110,8 +134,6 @@ static func get_error_message(error: int) -> String: return translate(&"errors.empty_title") ERR_DUPLICATE_TITLE: return translate(&"errors.duplicate_title") - ERR_NESTED_TITLE: - return translate(&"errors.nested_title") ERR_TITLE_INVALID_CHARACTERS: return translate(&"errors.invalid_title_string") ERR_TITLE_BEGINS_WITH_NUMBER: @@ -172,14 +194,22 @@ static func get_error_message(error: int) -> String: return translate(&"errors.unexpected_assignment") ERR_UNKNOWN_USING: return translate(&"errors.unknown_using") + ERR_EXPECTED_WHEN_OR_ELSE: + return translate(&"errors.expected_when_or_else") + ERR_ONLY_ONE_ELSE_ALLOWED: + return translate(&"errors.only_one_else_allowed") + ERR_WHEN_MUST_BELONG_TO_MATCH: + return translate(&"errors.when_must_belong_to_match") + ERR_CONCURRENT_LINE_WITHOUT_ORIGIN: + return translate(&"errors.concurrent_line_without_origin") + ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: + return translate(&"errors.goto_not_allowed_on_concurrect_lines") return translate(&"errors.unknown") static func translate(string: String) -> String: - var temp_node = new() - var base_path = temp_node.get_script().resource_path.get_base_dir() - temp_node.free() + var base_path = new().get_script().resource_path.get_base_dir() var language: String = TranslationServer.get_tool_locale() var translations_path: String = "%s/l10n/%s.po" % [base_path, language] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd.uid index 785ae80d..f431917b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd.uid @@ -1 +1 @@ -uid://c4hfjcd11f1l4 +uid://b1oarbmjtyesf diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd index 37928399..da07b45b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd @@ -47,6 +47,7 @@ var dialogue_line: set(next_dialogue_line): dialogue_line = next_dialogue_line custom_minimum_size = Vector2.ZERO + text = "" text = dialogue_line.text get: return dialogue_line @@ -186,12 +187,13 @@ func _mutate_inline_mutations(index: int) -> void: if inline_mutation[0] > index: return if inline_mutation[0] == index and not _already_mutated_indices.has(index): - _already_mutated_indices.append(index) _is_awaiting_mutation = true # The DialogueManager can't be referenced directly here so we need to get it by its path - await Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true) + await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true) _is_awaiting_mutation = false + _already_mutated_indices.append(index) + # Determine if the current autopause character at the cursor should qualify to pause typing. func _should_auto_pause() -> bool: @@ -210,7 +212,7 @@ func _should_auto_pause() -> bool: # Ignore "." if it's between two numbers if visible_characters > 3 and parsed_text[visible_characters - 1] == ".": var possible_number: String = parsed_text.substr(visible_characters - 2, 3) - if str(float(possible_number)) == possible_number: + if str(float(possible_number)).pad_decimals(1) == possible_number: return false # Ignore "." if it's used in an abbreviation diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd.uid index f1667090..6bf86b15 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.gd.uid @@ -1 +1 @@ -uid://c544a0pa1ckmf +uid://g32um0mltv5d diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.tscn index df48b649..00959334 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_label.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"] -[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"] +[ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"] [node name="DialogueLabel" type="RichTextLabel"] anchors_preset = 10 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd index ced12f21..7213854c 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd @@ -2,14 +2,11 @@ class_name DialogueLine extends RefCounted -const _DialogueConstants = preload("./constants.gd") - - ## The ID of this line var id: String ## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code] -var type: String = _DialogueConstants.TYPE_DIALOGUE +var type: String = DMConstants.TYPE_DIALOGUE ## The next line ID after this line. var next_id: String = "" @@ -41,6 +38,9 @@ var inline_mutations: Array[Array] = [] ## A list of responses attached to this line of dialogue. var responses: Array = [] +## A list of lines that are spoken simultaneously with this one. +var concurrent_lines: Array[DialogueLine] = [] + ## A list of any extra game states to check when resolving variables and mutations. var extra_game_states: Array = [] @@ -65,7 +65,7 @@ func _init(data: Dictionary = {}) -> void: extra_game_states = data.get("extra_game_states", []) match type: - _DialogueConstants.TYPE_DIALOGUE: + DMConstants.TYPE_DIALOGUE: character = data.character character_replacements = data.get("character_replacements", [] as Array[Dictionary]) text = data.text @@ -76,16 +76,17 @@ func _init(data: Dictionary = {}) -> void: inline_mutations = data.get("inline_mutations", [] as Array[Array]) time = data.get("time", "") tags = data.get("tags", []) + concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine]) - _DialogueConstants.TYPE_MUTATION: + DMConstants.TYPE_MUTATION: mutation = data.mutation func _to_string() -> String: match type: - _DialogueConstants.TYPE_DIALOGUE: + DMConstants.TYPE_DIALOGUE: return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text] - _DialogueConstants.TYPE_MUTATION: + DMConstants.TYPE_MUTATION: return "<DialogueLine mutation>" return "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd.uid index f3c38433..7ec7029a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_line.gd.uid @@ -1 +1 @@ -uid://cub47u83kehp +uid://rhuq0eyf8ar2 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd index 96e83e88..ab9a17ed 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd @@ -1,16 +1,19 @@ extends Node - -const DialogueConstants = preload("./constants.gd") -const Builtins = preload("./utilities/builtins.gd") -const DialogueSettings = preload("./settings.gd") const DialogueResource = preload("./dialogue_resource.gd") const DialogueLine = preload("./dialogue_line.gd") const DialogueResponse = preload("./dialogue_response.gd") -const DialogueManagerParser = preload("./components/parser.gd") -const DialogueManagerParseResult = preload("./components/parse_result.gd") -const ResolvedLineData = preload("./components/resolved_line_data.gd") +const DMConstants = preload("./constants.gd") +const Builtins = preload("./utilities/builtins.gd") +const DMSettings = preload("./settings.gd") +const DMCompiler = preload("./compiler/compiler.gd") +const DMCompilerResult = preload("./compiler/compiler_result.gd") +const DMResolvedLineData = preload("./compiler/resolved_line_data.gd") + + +## Emitted when a dialogue balloon is created and dialogue starts +signal dialogue_started(resource: DialogueResource) ## Emitted when a title is encountered while traversing dialogue, usually when jumping from a ## goto line @@ -28,24 +31,13 @@ signal dialogue_ended(resource: DialogueResource) ## Used internally. signal bridge_get_next_dialogue_line_completed(line: DialogueLine) +## Used internally +signal bridge_dialogue_started(resource: DialogueResource) + ## Used inernally signal bridge_mutated() -enum MutationBehaviour { - Wait, - DoNotWait, - Skip -} - -enum TranslationSource { - None, - Guess, - CSV, - PO -} - - ## The list of globals that dialogue can query var game_states: Array = [] @@ -56,13 +48,13 @@ var include_singletons: bool = true var include_classes: bool = true ## Manage translation behaviour -var translation_source: TranslationSource = TranslationSource.Guess +var translation_source: DMConstants.TranslationSource = DMConstants.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 + var current_scene: Node = Engine.get_main_loop().current_scene if current_scene == null: - current_scene = get_tree().root.get_child(get_tree().root.get_child_count() - 1) + current_scene = Engine.get_main_loop().root.get_child(Engine.get_main_loop().root.get_child_count() - 1) return current_scene var _has_loaded_autoloads: bool = false @@ -71,6 +63,8 @@ var _autoloads: Dictionary = {} var _node_properties: Array = [] var _method_info_cache: Dictionary = {} +var _dotnet_dialogue_manager: RefCounted + func _ready() -> void: # Cache the known Node2D properties @@ -81,52 +75,50 @@ func _ready() -> void: 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) - - # Connect up the C# signals if need be - if DialogueSettings.check_for_dotnet_solution(): - _get_dotnet_dialogue_manager().Prepare() + if not Engine.has_singleton("DialogueManager"): + Engine.register_singleton("DialogueManager", self) ## 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: +func get_next_dialogue_line(resource: DialogueResource, key: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine: # You have to provide a valid dialogue resource if resource == null: - assert(false, DialogueConstants.translate(&"runtime.no_resource")) + assert(false, DMConstants.translate(&"runtime.no_resource")) if resource.lines.size() == 0: - assert(false, DialogueConstants.translate(&"runtime.no_content").format({ file_path = resource.resource_path })) + assert(false, DMConstants.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) + var autoload = Engine.get_main_loop().root.get_node_or_null(state_name) if autoload == null: - printerr(DialogueConstants.translate(&"runtime.unknown_autoload").format({ autoload = state_name })) + printerr(DMConstants.translate(&"runtime.unknown_autoload").format({ autoload = state_name })) else: extra_game_states = [autoload] + extra_game_states + # Inject "self" into the extra game states. + extra_game_states = [{ "self": resource }] + 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() + if not _is_valid(dialogue): + dialogue_ended.emit.call_deferred(resource) 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] + if dialogue.type == DMConstants.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: + DMConstants.MutationBehaviour.Wait: + await _mutate(dialogue.mutation, extra_game_states) + DMConstants.MutationBehaviour.DoNotWait: + _mutate(dialogue.mutation, extra_game_states) + DMConstants.MutationBehaviour.Skip: pass - if actual_next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]: + if actual_next_id in [DMConstants.ID_END_CONVERSATION, DMConstants.ID_NULL, null]: # End the conversation - (func(): dialogue_ended.emit(resource)).call_deferred() + dialogue_ended.emit.call_deferred(resource) return null else: return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour) @@ -135,30 +127,196 @@ func get_next_dialogue_line(resource: DialogueResource, key: String = "", extra_ return dialogue -func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> ResolvedLineData: +## Get a line by its ID +func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine: + 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) + + # Key is blank so just use the first title (or start of file) + if key == null or key == "": + if resource.first_title.is_empty(): + key = resource.lines.keys()[0] + else: + key = resource.first_title + + # See if we just ended the conversation + if key in [DMConstants.ID_END, DMConstants.ID_NULL, null]: + if stack.size() > 0: + return await get_line(resource, "|".join(stack), extra_game_states) + else: + return null + elif key == DMConstants.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) + + if key in resource.titles.values(): + passed_title.emit(resource.titles.find_key(key)) + + if not resource.lines.has(key): + assert(false, DMConstants.translate(&"errors.key_not_found").format({ key = key })) + + var data: Dictionary = resource.lines.get(key) + + # If next_id is an expression we need to resolve it. + if data.has(&"next_id_expression"): + data.next_id = await _resolve(data.next_id_expression, extra_game_states) + + # This title key points to another title key so we should jump there instead + if data.type == DMConstants.TYPE_TITLE and data.next_id in resource.titles.values(): + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + # Handle match statements + if data.type == DMConstants.TYPE_MATCH: + var value = await _resolve_condition_value(data, extra_game_states) + var else_cases: Array[Dictionary] = data.cases.filter(func(s): return s.has("is_else")) + var else_case: Dictionary = {} if else_cases.size() == 0 else else_cases.front() + var next_id: String = "" + for case in data.cases: + if case == else_case: + continue + elif await _check_case_value(value, case, extra_game_states): + next_id = case.next_id + # Nothing matched so check for else case + if next_id == "": + if not else_case.is_empty(): + next_id = else_case.next_id + else: + next_id = data.next_id_after + return await get_line(resource, next_id + id_trail, extra_game_states) + + # Check for weighted random lines. + if data.has(&"siblings"): + # Only count siblings that pass their condition (if they have one). + var successful_siblings: Array = data.siblings.filter(func(sibling): return not sibling.has("condition") or await _check_condition(sibling, extra_game_states)) + var target_weight: float = randf_range(0, successful_siblings.reduce(func(total, sibling): return total + sibling.weight, 0)) + var cummulative_weight: float = 0 + for sibling in successful_siblings: + if target_weight < cummulative_weight + sibling.weight: + data = resource.lines.get(sibling.id) + break + else: + cummulative_weight += sibling.weight + + # Find any simultaneously said lines. + var concurrent_lines: Array[DialogueLine] = [] + if data.has(&"concurrent_lines"): + # If the list includes this line then it isn't the origin line so ignore it. + if not data.concurrent_lines.has(data.id): + for concurrent_id: String in data.concurrent_lines: + var concurrent_line: DialogueLine = await get_line(resource, concurrent_id, extra_game_states) + if concurrent_line: + concurrent_lines.append(concurrent_line) + + # If this line is blank and it's the last line then check for returning snippets. + if data.type in [DMConstants.TYPE_COMMENT, DMConstants.TYPE_UNKNOWN]: + if data.next_id in [DMConstants.ID_END, DMConstants.ID_NULL, null]: + if stack.size() > 0: + return await get_line(resource, "|".join(stack), extra_game_states) + else: + return null + else: + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + # If the line is a random block then go to the start of the block. + elif data.type == DMConstants.TYPE_RANDOM: + data = resource.lines.get(data.next_id) + + # Check conditions. + elif data.type in [DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE]: + # "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) + elif data.has("next_sibling_id") and not data.next_sibling_id.is_empty(): + return await get_line(resource, data.next_sibling_id + id_trail, extra_game_states) + else: + return await get_line(resource, data.next_id_after + id_trail, extra_game_states) + + # Evaluate jumps. + elif data.type == DMConstants.TYPE_GOTO: + if data.is_snippet and not id_trail.begins_with("|" + data.next_id_after): + id_trail = "|" + data.next_id_after + id_trail + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + elif data.type == DMConstants.TYPE_DIALOGUE: + if not data.has(&"id"): + data.id = key + + # Set up a line object. + var line: DialogueLine = await create_dialogue_line(data, extra_game_states) + line.concurrent_lines = concurrent_lines + + # If the jump point somehow has no content then just end. + if not line: return null + + # If we are the first of a list of responses then get the other ones. + if data.type == DMConstants.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 + + # 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 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 the responses come from a snippet then we need to come back here afterwards. + if next_line.type == DMConstants.TYPE_GOTO and next_line.is_snippet and not id_trail.begins_with("|" + next_line.next_id_after): + id_trail = "|" + next_line.next_id_after + id_trail + + # If the next line is a title then check where it points to see if that is a set of responses. + while [DMConstants.TYPE_TITLE, DMConstants.TYPE_GOTO].has(next_line.type) and resource.lines.has(next_line.next_id): + next_line = resource.lines.get(next_line.next_id) + + if next_line != null and next_line.type == DMConstants.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 == DMConstants.ID_NULL else line.next_id + id_trail + return line + +## Replace any variables, etc in the text. +func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> DMResolvedLineData: var text: String = translate(data) # Resolve variables - for replacement in data.text_replacements: - var value = await resolve(replacement.expression.duplicate(true), extra_game_states) + for replacement in data.get(&"text_replacements", [] as Array[Dictionary]): + var value = await _resolve(replacement.expression.duplicate(true), extra_game_states) var index: int = text.find(replacement.value_in_text) + if index == -1: + # The replacement wasn't found but maybe the regular quotes have been replaced + # by special quotes while translating. + index = text.replace("“", "\"").replace("”", "\"").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 compilation: DMCompilation = DMCompilation.new() # Resolve random groups - for found in parser.INLINE_RANDOM_REGEX.search_all(text): + for found in compilation.regex.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) + var markers: DMResolvedLineData = DMResolvedLineData.new(text) # Resolve any conditionals and update marker positions as needed - if data.type == DialogueConstants.TYPE_DIALOGUE: + if data.type == DMConstants.TYPE_DIALOGUE: var resolved_text: String = markers.text - var conditionals: Array[RegExMatch] = parser.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) + var conditionals: Array[RegExMatch] = compilation.regex.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) var replacements: Array = [] for conditional in conditionals: var condition_raw: String = conditional.strings[conditional.names.condition] @@ -168,9 +326,9 @@ func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> var bits = body.split(&"[else]") body = bits[0] body_else = bits[1] - var condition: Dictionary = parser.extract_condition("if " + condition_raw, false, 0) + var condition: Dictionary = compilation.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): + if not await _check_condition({ condition = condition }, extra_game_states): body = body_else replacements.append({ start = conditional.get_start(), @@ -179,7 +337,7 @@ func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> body = body }) - for i in range(replacements.size() -1, -1, -1): + 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 @@ -206,8 +364,6 @@ func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> markers.text = resolved_text - parser.free() - return markers @@ -217,7 +373,7 @@ func get_resolved_character(data: Dictionary, extra_game_states: Array = []) -> # Resolve variables for replacement in data.get(&"character_replacements", []): - var value = await resolve(replacement.expression.duplicate(true), extra_game_states) + 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()) @@ -234,44 +390,41 @@ func get_resolved_character(data: Dictionary, extra_game_states: Array = []) -> ## 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 result: DMCompilerResult = DMCompiler.compile_string(text, "") - if errors.size() > 0: - printerr(DialogueConstants.translate(&"runtime.errors").format({ count = errors.size() })) - for error in errors: - printerr(DialogueConstants.translate(&"runtime.error_detail").format({ + if result.errors.size() > 0: + printerr(DMConstants.translate(&"runtime.errors").format({ count = result.errors.size() })) + for error in result.errors: + printerr(DMConstants.translate(&"runtime.error_detail").format({ line = error.line_number + 1, - message = DialogueConstants.get_error_message(error.error) + message = DMConstants.get_error_message(error.error) })) - assert(false, DialogueConstants.translate(&"runtime.errors_see_details").format({ count = errors.size() })) + assert(false, DMConstants.translate(&"runtime.errors_see_details").format({ count = result.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.using_states = result.using_states + resource.titles = result.titles + resource.first_title = result.first_title + resource.character_names = result.character_names + resource.lines = result.lines resource.raw_text = text return resource +#region Balloon helpers + + ## 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) - + _start_balloon.call_deferred(balloon, resource, title, extra_game_states) 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()) + var balloon_path: String = DMSettings.get_setting(DMSettings.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) @@ -285,14 +438,23 @@ func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, titl balloon_scene = balloon_scene.instantiate() var balloon: Node = balloon_scene + _start_balloon.call_deferred(balloon, resource, title, extra_game_states) + return balloon + + +# Call "start" on the given balloon. +func _start_balloon(balloon: Node, resource: DialogueResource, title: String, extra_game_states: Array) -> void: 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 + assert(false, DMConstants.translate(&"runtime.dialogue_balloon_missing_start_method")) + + dialogue_started.emit(resource) + bridge_dialogue_started.emit(resource) # Get the path to the example balloon @@ -302,141 +464,57 @@ func _get_example_balloon_path() -> String: return get_script().resource_path.get_base_dir() + balloon_path -### Dotnet bridge +#endregion + +#region dotnet bridge -func _get_dotnet_dialogue_manager() -> Node: - return load(get_script().resource_path.get_base_dir() + "/DialogueManager.cs").new() +func _get_dotnet_dialogue_manager() -> RefCounted: + if not is_instance_valid(_dotnet_dialogue_manager): + _dotnet_dialogue_manager = load(get_script().resource_path.get_base_dir() + "/DialogueManager.cs").new() + return _dotnet_dialogue_manager + + +func _bridge_get_new_instance() -> Node: + # For some reason duplicating the node with its signals doesn't work so we have to copy them over manually + var instance = new() + for s: Dictionary in dialogue_started.get_connections(): + instance.dialogue_started.connect(s.callable) + for s: Dictionary in passed_title.get_connections(): + instance.passed_title.connect(s.callable) + for s: Dictionary in got_dialogue.get_connections(): + instance.got_dialogue.connect(s.callable) + for s: Dictionary in mutated.get_connections(): + instance.mutated.connect(s.callable) + for s: Dictionary in dialogue_ended.get_connections(): + instance.dialogue_ended.connect(s.callable) + instance.get_current_scene = get_current_scene + return instance 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 + await Engine.get_main_loop().process_frame 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) + await _mutate(mutation, extra_game_states, is_inline_mutation) bridge_mutated.emit() -### Helpers +#endregion - -# Get a line by its ID -func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine: - 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) - - # 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 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 not resource.lines.has(key): - assert(false, DialogueConstants.translate(&"errors.key_not_found").format({ key = key })) - - var data: Dictionary = resource.lines.get(key) - - # 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 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 - - # 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) - - # 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) - - elif data.type == DialogueConstants.TYPE_DIALOGUE: - if not data.has(&"id"): - data.id = key - - # Set up a line object - var line: DialogueLine = await create_dialogue_line(data, extra_game_states) - - # If the jump point somehow has no content then just end - if not line: return null - - # 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 - - # 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 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 the responses come from a snippet then we need to come back here afterwards - if next_line.type == DialogueConstants.TYPE_GOTO and next_line.is_snippet: - id_trail = "|" + next_line.next_id_after + id_trail - - # 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 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 +#region Internal helpers # 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 DialogueSettings.get_setting(&"ignore_missing_state_values", false): + if DMSettings.get_setting(DMSettings.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 @@ -446,48 +524,50 @@ func show_error_for_missing_state_value(message: String, will_show: bool = true) # Translate a string func translate(data: Dictionary) -> String: - if translation_source == TranslationSource.None: + if translation_source == DMConstants.TranslationSource.None: return data.text - if data.translation_key == "" or data.translation_key == data.text: + var translation_key: String = data.get(&"translation_key", data.text) + + if translation_key == "" or 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)) + DMConstants.TranslationSource.PO: + return tr(data.text, StringName(translation_key)) - TranslationSource.CSV: - return tr(data.translation_key) + DMConstants.TranslationSource.CSV: + return tr(translation_key) - TranslationSource.Guess: + DMConstants.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)) + return tr(data.text, StringName(translation_key)) else: # Assume CSV - return tr(data.translation_key) + return tr(translation_key) - return tr(data.translation_key) + return tr(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) + DMConstants.TYPE_DIALOGUE: + var resolved_data: DMResolvedLineData = await get_resolved_line_data(data, extra_game_states) return DialogueLine.new({ id = data.get(&"id", ""), - type = DialogueConstants.TYPE_DIALOGUE, + type = DMConstants.TYPE_DIALOGUE, next_id = data.next_id, character = await get_resolved_character(data, extra_game_states), - character_replacements = data.character_replacements, + character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), text = resolved_data.text, - text_replacements = data.text_replacements, - translation_key = data.translation_key, + text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]), + translation_key = data.get(&"translation_key", data.text), pauses = resolved_data.pauses, speeds = resolved_data.speeds, inline_mutations = resolved_data.mutations, @@ -496,19 +576,19 @@ func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> Dialogu extra_game_states = extra_game_states }) - DialogueConstants.TYPE_RESPONSE: + DMConstants.TYPE_RESPONSE: return DialogueLine.new({ id = data.get(&"id", ""), - type = DialogueConstants.TYPE_RESPONSE, + type = DMConstants.TYPE_RESPONSE, next_id = data.next_id, tags = data.get(&"tags", []), extra_game_states = extra_game_states }) - DialogueConstants.TYPE_MUTATION: + DMConstants.TYPE_MUTATION: return DialogueLine.new({ id = data.get(&"id", ""), - type = DialogueConstants.TYPE_MUTATION, + type = DMConstants.TYPE_MUTATION, next_id = data.next_id, mutation = data.mutation, extra_game_states = extra_game_states @@ -519,37 +599,37 @@ func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> Dialogu # 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) + var resolved_data: DMResolvedLineData = await get_resolved_line_data(data, extra_game_states) return DialogueResponse.new({ id = data.get(&"id", ""), - type = DialogueConstants.TYPE_RESPONSE, + type = DMConstants.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, + text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]), tags = data.get(&"tags", []), - translation_key = data.translation_key + translation_key = data.get(&"translation_key", data.text) }) # Get the current game states -func get_game_states(extra_game_states: Array) -> Array: +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(): + for child in Engine.get_main_loop().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 + if Engine.get_main_loop().current_scene and child.name == Engine.get_main_loop().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) + for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): + var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) if state: game_states.append(state) @@ -562,75 +642,97 @@ func get_game_states(extra_game_states: Array) -> Array: # 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 +func _check_condition(data: Dictionary, extra_game_states: Array) -> bool: + return bool(await _resolve_condition_value(data, extra_game_states)) - return await resolve(data.condition.expression.duplicate(true), extra_game_states) + +# Resolve a condition's expression value +func _resolve_condition_value(data: Dictionary, extra_game_states: Array) -> Variant: + if data.get(&"condition", null) == null: return true + if data.condition.is_empty(): return true + + return await _resolve(data.condition.expression.duplicate(true), extra_game_states) + + +# Check if a match value matches a case value +func _check_case_value(match_value: Variant, data: Dictionary, extra_game_states: Array) -> bool: + if data.get(&"condition", null) == null: return true + if data.condition.is_empty(): return true + + var expression: Array[Dictionary] = data.condition.expression.duplicate(true) + + # If the when is a comparison when insert the match value as the first value to compare to + var already_compared: bool = false + if expression[0].type == DMConstants.TOKEN_COMPARISON: + expression.insert(0, { + type = DMConstants.TOKEN_VALUE, + value = match_value + }) + already_compared = true + + var resolved_value = await _resolve(expression, extra_game_states) + + if already_compared: + return resolved_value + else: + return match_value == resolved_value # Make a change to game state or run a method -func mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: +func _mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: 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) + if expression[0].type == DMConstants.TOKEN_FUNCTION and expression[0].function in [&"wait", &"Wait", &"debug", &"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 + &"wait", &"Wait": + mutated.emit(mutation.merged({ is_inline = is_inline_mutation })) + await Engine.get_main_loop().create_timer(float(args[0])).timeout return - &"debug": + &"debug", &"Debug": prints("Debug:", args) - await get_tree().process_frame + await Engine.get_main_loop().process_frame # Or pass through to the resolver else: - if not mutation_contains_assignment(mutation.expression) and not is_inline_mutation: - mutated.emit(mutation) + if not _mutation_contains_assignment(mutation.expression) and not is_inline_mutation: + mutated.emit(mutation.merged({ is_inline = is_inline_mutation })) if mutation.get("is_blocking", true): - await resolve(mutation.expression.duplicate(true), extra_game_states) + await _resolve(mutation.expression.duplicate(true), extra_game_states) return else: - resolve(mutation.expression.duplicate(true), extra_game_states) + _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 + await Engine.get_main_loop().process_frame -func mutation_contains_assignment(mutation: Array) -> bool: +# Check if a mutation contains an assignment token. +func _mutation_contains_assignment(mutation: Array) -> bool: for token in mutation: - if token.type == DialogueConstants.TOKEN_ASSIGNMENT: + if token.type == DMConstants.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 - - # 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]: +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) + data.is_allowed = await _check_condition(data, extra_game_states) + var response: DialogueResponse = await create_response(data, extra_game_states) + response.next_id += id_trail + responses.append(response) return responses # Get a value on the current scene or game state -func get_state_value(property: String, extra_game_states: Array): +func _get_state_value(property: String, extra_game_states: Array): # Special case for static primitive calls if property == "Color": return Color() @@ -640,14 +742,17 @@ func get_state_value(property: String, extra_game_states: Array): return Vector3.ZERO elif property == "Vector4": return Vector4.ZERO - elif property == "Quaternian": + elif property == "Quaternion": 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() })) + assert(false, DMConstants.translate(&"runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() })) - for state in get_game_states(extra_game_states): + # Warn about possible name collisions + _warn_about_state_name_collisions(property, extra_game_states) + + for state in _get_game_states(extra_game_states): if typeof(state) == TYPE_DICTIONARY: if state.has(property): return state.get(property) @@ -664,152 +769,211 @@ func get_state_value(property: String, extra_game_states: Array): 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 = _get_state_shortcut_names(extra_game_states) })) + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) + + +# Print warnings for top-level state name collisions. +func _warn_about_state_name_collisions(target_key: String, extra_game_states: Array) -> void: + # Don't run the check if this is a release build + if not OS.is_debug_build(): return + # Also don't run if the setting is off + if not DMSettings.get_setting(DMSettings.WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS, false): return + + # Get the list of state shortcuts. + var state_shortcuts: Array = [] + for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): + var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) + if state: + state_shortcuts.append(state) + + # Check any top level names for a collision + var states_with_key: Array = [] + for state in extra_game_states + [get_current_scene.call()] + state_shortcuts: + if state is Dictionary: + if state.keys().has(target_key): + states_with_key.append("Dictionary") + else: + var script: Script = (state as Object).get_script() + if script == null: + continue + + for method in script.get_script_method_list(): + if method.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + for property in script.get_script_property_list(): + if property.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + for signal_info in script.get_script_signal_list(): + if signal_info.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + if states_with_key.size() > 1: + push_warning(DMConstants.translate(&"runtime.top_level_states_share_name").format({ states = ", ".join(states_with_key), key = target_key })) # 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): +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): + 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 = _get_state_shortcut_names(extra_game_states) })) + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found_missing_export").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) else: - show_error_for_missing_state_value(DialogueConstants.translate(&"runtime.property_not_found").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) # Get the list of state shortcut names func _get_state_shortcut_names(extra_game_states: Array) -> String: - var states = get_game_states(extra_game_states) + var states = _get_game_states(extra_game_states) states.erase(_autoloads) return ", ".join(states.map(func(s): return "\"%s\"" % (s.name if "name" in s else s))) -# 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) +# Resolve an array of expressions. +func _resolve_each(array: Array, extra_game_states: Array) -> Array: + var results: Array = [] + for item in array: + if not item[0].type in [DMConstants.TOKEN_BRACE_CLOSE, DMConstants.TOKEN_BRACKET_CLOSE, DMConstants.TOKEN_PARENS_CLOSE]: + results.append(await _resolve(item.duplicate(true), extra_game_states)) + return results - # Then variables/methods + +# Collapse any expressions +func _resolve(tokens: Array, extra_game_states: Array): var i: int = 0 var limit: int = 0 + + # Handle groups first + for token in tokens: + if token.type == DMConstants.TOKEN_GROUP: + token.type = DMConstants.TOKEN_VALUE + token.value = await _resolve(token.value, extra_game_states) + + # Then variables/methods + i = 0 + limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_FUNCTION: + if token.type == DMConstants.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: + var args = await _resolve_each(token.value, extra_game_states) + if tokens[i - 1].type == DMConstants.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) + caller.type = DMConstants.TOKEN_VALUE + caller.value = Builtins.resolve_method(caller.value, function_name, args) tokens.remove_at(i) - tokens.remove_at(i-1) + 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) + elif _thing_has_method(caller.value, function_name, args): + caller.type = DMConstants.TOKEN_VALUE + caller.value = await _resolve_thing_method(caller.value, function_name, args) tokens.remove_at(i) - tokens.remove_at(i-1) + 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) })) + show_error_for_missing_state_value(DMConstants.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]) + token.type = DMConstants.TOKEN_VALUE + token.value = str(args[0]) found = true &"Vector2": - token["type"] = "value" - token["value"] = Vector2(args[0], args[1]) + token.type = DMConstants.TOKEN_VALUE + token.value = Vector2(args[0], args[1]) found = true &"Vector2i": - token["type"] = "value" - token["value"] = Vector2i(args[0], args[1]) + token.type = DMConstants.TOKEN_VALUE + token.value = Vector2i(args[0], args[1]) found = true &"Vector3": - token["type"] = "value" - token["value"] = Vector3(args[0], args[1], args[2]) + token.type = DMConstants.TOKEN_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]) + token.type = DMConstants.TOKEN_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]) + token.type = DMConstants.TOKEN_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]) + token.type = DMConstants.TOKEN_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]) + token.type = DMConstants.TOKEN_VALUE + token.value = Quaternion(args[0], args[1], args[2], args[3]) found = true &"Callable": - token["type"] = "value" + token.type = DMConstants.TOKEN_VALUE match args.size(): 0: - token["value"] = Callable() + token.value = Callable() 1: - token["value"] = Callable(args[0]) + token.value = Callable(args[0]) 2: - token["value"] = Callable(args[0], args[1]) + token.value = Callable(args[0], args[1]) found = true &"Color": - token["type"] = "value" + token.type = DMConstants.TOKEN_VALUE match args.size(): 0: - token["value"] = Color() + token.value = Color() 1: - token["value"] = Color(args[0]) + token.value = Color(args[0]) 2: - token["value"] = Color(args[0], args[1]) + token.value = Color(args[0], args[1]) 3: - token["value"] = Color(args[0], args[1], args[2]) + token.value = Color(args[0], args[1], args[2]) 4: - token["value"] = Color(args[0], args[1], args[2], args[3]) + token.value = Color(args[0], args[1], args[2], args[3]) found = true - &"load": - token["type"] = "value" - token["value"] = load(args[0]) + &"load", &"Load": + token.type = DMConstants.TOKEN_VALUE + token.value = load(args[0]) found = true - &"emit": - token["type"] = "value" - token["value"] = resolve_signal(args, extra_game_states) + &"roll_dice", &"RollDice": + token.type = DMConstants.TOKEN_VALUE + token.value = randi_range(1, args[0]) 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) + # Check for top level name conflicts + _warn_about_state_name_collisions(function_name, extra_game_states) + + for state in _get_game_states(extra_game_states): + if _thing_has_method(state, function_name, args): + token.type = DMConstants.TOKEN_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({ + show_error_for_missing_state_value(DMConstants.translate(&"runtime.method_not_found").format({ method = args[0] if function_name in ["call", "call_deferred"] else function_name, states = _get_state_shortcut_names(extra_game_states) }), not found) - elif token.type == DialogueConstants.TOKEN_DICTIONARY_REFERENCE: + elif token.type == DMConstants.TOKEN_DICTIONARY_REFERENCE: var value - if i > 0 and tokens[i - 1].type == DialogueConstants.TOKEN_DOT: + if i > 0 and tokens[i - 1].type == DMConstants.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. @@ -821,119 +985,126 @@ func resolve(tokens: Array, extra_game_states: Array): i -= 2 else: # Otherwise we can just get this variable as a normal state reference - value = get_state_value(token.variable, extra_game_states) + value = _get_state_value(token.variable, extra_game_states) - var index = await resolve(token.value, 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 tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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 + token.type = "dictionary" + token.value = value + token.key = index else: if value.has(index): - token["type"] = "value" - token["value"] = value[index] + token.type = DMConstants.TOKEN_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 })) + show_error_for_missing_state_value(DMConstants.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 tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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 + token.type = "array" + token.value = value + token.key = index else: if index >= 0 and index < value.size(): - token["type"] = "value" - token["value"] = value[index] + token.type = DMConstants.TOKEN_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 })) + show_error_for_missing_state_value(DMConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = token.variable })) - elif token.type == DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: + elif token.type == DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: var dictionary: Dictionary = tokens[i - 1] - var index = await resolve(token.value, extra_game_states) + 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 tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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 + 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) + 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 })) + show_error_for_missing_state_value(DMConstants.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 tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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 + 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] + 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 })) + show_error_for_missing_state_value(DMConstants.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 == DMConstants.TOKEN_ARRAY: + token.type = DMConstants.TOKEN_VALUE + token.value = await _resolve_each(token.value, extra_game_states) - elif token.type == DialogueConstants.TOKEN_DICTIONARY: - token["type"] = "value" + elif token.type == DMConstants.TOKEN_DICTIONARY: + token.type = DMConstants.TOKEN_VALUE var dictionary = {} for key in token.value.keys(): - var resolved_key = await resolve([key], extra_game_states) + 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) + var resolved_value = await _resolve(preresolved_value, extra_game_states) dictionary[resolved_key] = resolved_value - token["value"] = dictionary + token.value = dictionary - elif token.type == DialogueConstants.TOKEN_VARIABLE or token.type == DialogueConstants.TOKEN_NUMBER: + elif token.type == DMConstants.TOKEN_VARIABLE or token.type == DMConstants.TOKEN_NUMBER: if str(token.value) == "null": - token["type"] = "value" - token["value"] = null - elif tokens[i - 1].type == DialogueConstants.TOKEN_DOT: + token.type = DMConstants.TOKEN_VALUE + token.value = null + elif str(token.value) == "self": + token.type = DMConstants.TOKEN_VALUE + token.value = extra_game_states[0].self + elif tokens[i - 1].type == DMConstants.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 tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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 + 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" + caller.type = DMConstants.TOKEN_VALUE if Builtins.is_supported(caller.value): - caller["value"] = Builtins.resolve_property(caller.value, property) + caller.value = Builtins.resolve_property(caller.value, property) else: - caller["value"] = caller.value.get(property) + caller.value = caller.value.get(property) tokens.remove_at(i) - tokens.remove_at(i-1) + tokens.remove_at(i - 1) i -= 2 - elif tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: + elif tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.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" + token.type = "variable" else: - token["type"] = "value" - token["value"] = get_state_value(str(token.value), extra_game_states) + if token.type == DMConstants.TOKEN_NUMBER: + token.type = DMConstants.TOKEN_VALUE + token.value = token.value + else: + token.type = DMConstants.TOKEN_VALUE + token.value = _get_state_value(str(token.value), extra_game_states) i += 1 @@ -943,16 +1114,16 @@ func resolve(tokens: Array, extra_game_states: Array): 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) + if token.type == DMConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]: + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) # Then addition and subtraction i = 0 @@ -960,16 +1131,16 @@ func resolve(tokens: Array, extra_game_states: Array): 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) + if token.type == DMConstants.TOKEN_OPERATOR and token.value in ["+", "-"]: + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) # Then negations i = 0 @@ -977,15 +1148,15 @@ func resolve(tokens: Array, extra_game_states: Array): 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) + if token.type == DMConstants.TOKEN_NOT: + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) # Then comparisons i = 0 @@ -993,16 +1164,16 @@ func resolve(tokens: Array, extra_game_states: Array): 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) + if token.type == DMConstants.TOKEN_COMPARISON: + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) # Then and/or i = 0 @@ -1010,16 +1181,16 @@ func resolve(tokens: Array, extra_game_states: Array): 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) + if token.type == DMConstants.TOKEN_AND_OR: + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) # Lastly, resolve any assignments i = 0 @@ -1027,47 +1198,48 @@ func resolve(tokens: Array, extra_game_states: Array): while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] - if token.type == DialogueConstants.TOKEN_ASSIGNMENT: + if token.type == DMConstants.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) + 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) + 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) + 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 }), + DMConstants.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) + 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")) + show_error_for_missing_state_value(DMConstants.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) + token.type = DMConstants.TOKEN_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")) + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) return tokens[0].value -func compare(operator: String, first_value, second_value) -> bool: +# Compare two values. +func _compare(operator: String, first_value, second_value) -> bool: match operator: &"in": if first_value == null or second_value == null: @@ -1122,7 +1294,8 @@ func compare(operator: String, first_value, second_value) -> bool: return false -func apply_operation(operator: String, first_value, second_value): +# Apply an operation from one value to another. +func _apply_operation(operator: String, first_value, second_value): match operator: &"=": return second_value @@ -1141,23 +1314,26 @@ func apply_operation(operator: String, first_value, second_value): &"or": return first_value or second_value - assert(false, DialogueConstants.translate(&"runtime.unknown_operator")) + assert(false, DMConstants.translate(&"runtime.unknown_operator")) -# Check if a dialogue line contains meaningful information -func is_valid(line: DialogueLine) -> bool: +# 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: + if line.type == DMConstants.TYPE_MUTATION and line.mutation == null: return false - if line.type == DialogueConstants.TYPE_RESPONSE and line.get(&"responses").size() == 0: + if line.type == DMConstants.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): +# Check that a thing has a given method. +func _thing_has_method(thing, method: String, args: Array) -> bool: + if Builtins.is_supported(thing, method): return thing != _autoloads + elif thing is Dictionary: + return false if method in [&"call", &"call_deferred"]: return thing.has_method(args[0]) @@ -1168,15 +1344,15 @@ func thing_has_method(thing, method: String, args: Array) -> bool: if thing.has_method(method): return true - if method.to_snake_case() != method and DialogueSettings.check_for_dotnet_solution(): + if method.to_snake_case() != method and DMSettings.check_for_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 _get_dotnet_dialogue_manager().ThingHasMethod(thing, method, args) return false # Check if a given property exists -func thing_has_property(thing: Object, property: String) -> bool: +func _thing_has_property(thing: Object, property: String) -> bool: if thing == null: return false @@ -1190,50 +1366,26 @@ func thing_has_property(thing: Object, property: String) -> bool: return false -func resolve_signal(args: Array, extra_game_states: Array): - 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 - - # 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 = _get_state_shortcut_names(extra_game_states) })) - - -func get_method_info_for(thing, method: String) -> Dictionary: +func _get_method_info_for(thing: Variant, method: String, args: Array) -> Dictionary: # Use the thing instance id as a key for the caching dictionary. var thing_instance_id: int = thing.get_instance_id() if not _method_info_cache.has(thing_instance_id): var methods: Dictionary = {} for m in thing.get_method_list(): - methods[m.name] = m + methods["%s:%d" % [m.name, m.args.size()]] = m + if not methods.has(m.name): + methods[m.name] = m _method_info_cache[thing_instance_id] = methods - return _method_info_cache.get(thing_instance_id, {}).get(method) + var methods: Dictionary = _method_info_cache.get(thing_instance_id, {}) + var method_key: String = "%s:%d" % [method, args.size()] + if methods.has(method_key): + return methods.get(method_key) + else: + return methods.get(method) -func resolve_thing_method(thing, method: String, args: Array): +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(): @@ -1241,13 +1393,13 @@ func resolve_thing_method(thing, method: String, args: Array): if thing.has_method(method): # Try to convert any literals to the right type - var method_info: Dictionary = get_method_info_for(thing, method) + var method_info: Dictionary = _get_method_info_for(thing, method, args) 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()): + assert(false, DMConstants.translate(&"runtime.expected_n_got_n_args").format({ expected = method_args.size(), method = method, received = args.size()})) + for i in range(0, min(method_args.size(), args.size())): var m: Dictionary = method_args[i] - var to_type:int = typeof(args[i]) + var to_type: int = typeof(args[i]) if m.type == TYPE_ARRAY: match m.hint_string: &"String": @@ -1262,7 +1414,7 @@ func resolve_thing_method(thing, method: String, args: Array): to_type = TYPE_PACKED_VECTOR3_ARRAY _: if m.hint_string != "": - assert(false, DialogueConstants.translate(&"runtime.unsupported_array_type").format({ type = m.hint_string})) + assert(false, DMConstants.translate(&"runtime.unsupported_array_type").format({ type = m.hint_string})) if typeof(args[i]) != to_type: args[i] = convert(args[i], to_type) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd.uid index 5e9562ce..d10762e9 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd.uid @@ -1 +1 @@ -uid://cst7cjlwxlxj3 +uid://c3rodes2l3gxb diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd.uid deleted file mode 100644 index 7378658a..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bq8fntgcwiosq diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd index 68f52a02..29ade7b1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd @@ -5,7 +5,6 @@ class_name DialogueResource extends Resource -const _DialogueManager = preload("./dialogue_manager.gd") const DialogueLine = preload("./dialogue_line.gd") ## A list of state shortcuts @@ -30,7 +29,7 @@ const DialogueLine = preload("./dialogue_line.gd") ## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can ## be a title string or a stringified line number). Runs any mutations along the way and then returns ## the first dialogue line encountered. -func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: _DialogueManager.MutationBehaviour = _DialogueManager.MutationBehaviour.Wait) -> DialogueLine: +func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine: return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd.uid index 80b5e3b8..27b95d09 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_resource.gd.uid @@ -1 +1 @@ -uid://3we8jdve7k1n +uid://dbs4435dsf3ry diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd index 92cec241..701ce926 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd @@ -2,14 +2,11 @@ class_name DialogueResponse extends RefCounted -const _DialogueConstants = preload("./constants.gd") - - ## The ID of this response var id: String ## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code]. -var type: String = _DialogueConstants.TYPE_RESPONSE +var type: String = DMConstants.TYPE_RESPONSE ## The next line ID to use if this response is selected by the player. var next_id: String = "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd.uid index 4a77d74f..9b4532ae 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd.uid @@ -1 +1 @@ -uid://5oenbic4tlvj +uid://cm0xpfeywpqid diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd similarity index 89% rename from Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd rename to Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd index 69a56db9..cd66ae5b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_reponses_menu.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd @@ -14,6 +14,9 @@ signal response_selected(response) ## The action for accepting a response (is possibly overridden by parent dialogue balloon). @export var next_action: StringName = &"" +## Hide any responses where [code]is_allowed[/code] is false +@export var hide_failed_responses: bool = false + ## The list of dialogue responses. var responses: Array = []: get: @@ -31,6 +34,8 @@ var responses: Array = []: # Add new items if responses.size() > 0: for response in responses: + if hide_failed_responses and not response.is_allowed: continue + var item: Control if is_instance_valid(response_template): item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) @@ -39,7 +44,7 @@ var responses: Array = []: item = Button.new() item.name = "Response%d" % get_child_count() if not response.is_allowed: - item.name = String(item.name) + "Disallowed" + item.name = item.name + &"Disallowed" item.disabled = true # If the item has a response property then use that @@ -59,7 +64,9 @@ var responses: Array = []: func _ready() -> void: visibility_changed.connect(func(): if visible and get_menu_items().size() > 0: - get_menu_items()[0].grab_focus() + var first_item: Control = get_menu_items()[0] + if first_item.is_inside_tree(): + first_item.grab_focus() ) if is_instance_valid(response_template): @@ -77,11 +84,6 @@ func get_menu_items() -> Array: return items -## [b]DEPRECATED[/b]. Do not use. -func set_responses(next_responses: Array) -> void: - self.responses = next_responses - - #region Internal @@ -130,11 +132,11 @@ func _on_response_mouse_entered(item: Control) -> void: func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: if "Disallowed" in item.name: return - get_viewport().set_input_as_handled() - if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: + get_viewport().set_input_as_handled() 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(): + get_viewport().set_input_as_handled() response_selected.emit(response) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd.uid new file mode 100644 index 00000000..0ae73d9d --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd.uid @@ -0,0 +1 @@ +uid://bb52rsfwhkxbn diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd index 7c019849..137ab700 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd @@ -1,43 +1,52 @@ -extends EditorTranslationParserPlugin +class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin -const DialogueConstants = preload("./constants.gd") -const DialogueSettings = preload("./settings.gd") -const DialogueManagerParser = preload("./components/parser.gd") -const DialogueManagerParseResult = preload("./components/parse_result.gd") +## Cached result of parsing a dialogue file. +var data: DMCompilerResult +## List of characters that were added. +var translated_character_names: PackedStringArray = [] +var translated_lines: Array[Dictionary] = [] -func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> void: +func _parse_file(path: String) -> Array[PackedStringArray]: + var msgs: Array[PackedStringArray] = [] var file: FileAccess = FileAccess.open(path, FileAccess.READ) var text: String = file.get_as_text() - var data: DialogueManagerParseResult = DialogueManagerParser.parse_string(text, path) + data = DMCompiler.compile_string(text, path) + var known_keys: PackedStringArray = PackedStringArray([]) # Add all character names if settings ask for it - if DialogueSettings.get_setting("export_characters_in_translation", true): - var character_names: PackedStringArray = data.character_names - for character_name in character_names: + if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true): + translated_character_names = [] as Array[DialogueLine] + for character_name: String in data.character_names: if character_name in known_keys: continue known_keys.append(character_name) - msgids_context_plural.append([character_name.replace('"', '\\"'), "dialogue", ""]) + translated_character_names.append(character_name) + msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")])) # Add all dialogue lines and responses var dialogue: Dictionary = data.lines - for key in dialogue.keys(): + for key: String in dialogue.keys(): var line: Dictionary = dialogue.get(key) - if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue - if line.translation_key in known_keys: continue + if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue - known_keys.append(line.translation_key) + var translation_key: String = line.get(&"translation_key", line.text) - if line.translation_key == "" or line.translation_key == line.text: - msgids_context_plural.append([line.text.replace('"', '\\"'), "", ""]) + if translation_key in known_keys: continue + + known_keys.append(translation_key) + translated_lines.append(line) + if translation_key == line.text: + msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")])) else: - msgids_context_plural.append([line.text.replace('"', '\\"'), line.translation_key.replace('"', '\\"'), ""]) + msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")])) + + return msgs func _get_recognized_extensions() -> PackedStringArray: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid index 89a24de9..22ddbe98 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid @@ -1 +1 @@ -uid://cgib8ttuvr1o8 +uid://c6bya881h1egb diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs index 9eff9e16..58527122 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs @@ -1,204 +1,223 @@ using Godot; using Godot.Collections; -namespace DialogueManagerRuntime; - -public partial class ExampleBalloon : CanvasLayer +namespace DialogueManagerRuntime { - [Export] public string NextAction = "ui_accept"; - [Export] public string SkipAction = "ui_cancel"; - - - Control balloon; - RichTextLabel characterLabel; - RichTextLabel dialogueLabel; - VBoxContainer responsesMenu; - - Resource resource; - Array<Variant> temporaryGameStates = new Array<Variant>(); - bool isWaitingForInput = false; - bool willHideBalloon = false; - - DialogueLine dialogueLine; - DialogueLine DialogueLine + public partial class ExampleBalloon : CanvasLayer { - get => dialogueLine; - set - { - isWaitingForInput = false; - balloon.FocusMode = Control.FocusModeEnum.All; - balloon.GrabFocus(); + [Export] public string NextAction = "ui_accept"; + [Export] public string SkipAction = "ui_cancel"; - if (value == null) - { - QueueFree(); - return; - } - dialogueLine = value; - UpdateDialogue(); - } + Control balloon; + RichTextLabel characterLabel; + RichTextLabel dialogueLabel; + VBoxContainer responsesMenu; + + Resource resource; + Array<Variant> temporaryGameStates = new Array<Variant>(); + bool isWaitingForInput = false; + bool willHideBalloon = false; + + DialogueLine dialogueLine; + DialogueLine DialogueLine + { + get => dialogueLine; + set + { + if (value == null) + { + QueueFree(); + return; + } + + dialogueLine = value; + ApplyDialogueLine(); + } + } + + Timer MutationCooldown = new Timer(); + + + public override void _Ready() + { + balloon = GetNode<Control>("%Balloon"); + characterLabel = GetNode<RichTextLabel>("%CharacterLabel"); + dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel"); + responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu"); + + balloon.Hide(); + + balloon.GuiInput += (@event) => + { + if ((bool)dialogueLabel.Get("is_typing")) + { + bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed(); + bool skipButtonWasPressed = @event.IsActionPressed(SkipAction); + if (mouseWasClicked || skipButtonWasPressed) + { + GetViewport().SetInputAsHandled(); + dialogueLabel.Call("skip_typing"); + return; + } + } + + if (!isWaitingForInput) return; + if (dialogueLine.Responses.Count > 0) return; + + GetViewport().SetInputAsHandled(); + + if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left) + { + Next(dialogueLine.NextId); + } + else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon) + { + Next(dialogueLine.NextId); + } + }; + + if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) + { + responsesMenu.Set("next_action", NextAction); + } + responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => + { + Next(response.NextId); + })); + + + // Hide the balloon when a mutation is running + MutationCooldown.Timeout += () => + { + if (willHideBalloon) + { + willHideBalloon = false; + balloon.Hide(); + } + }; + AddChild(MutationCooldown); + + DialogueManager.Mutated += OnMutated; + } + + + public override void _ExitTree() + { + DialogueManager.Mutated -= OnMutated; + } + + + public override void _UnhandledInput(InputEvent @event) + { + // Only the balloon is allowed to handle input while it's showing + GetViewport().SetInputAsHandled(); + } + + + 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 && IsInstanceValid(dialogueLabel)) + { + 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<Variant> extraGameStates = null) + { + temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>()); + isWaitingForInput = false; + resource = dialogueResource; + + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); + } + + + public async void Next(string nextId) + { + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); + } + + + #region Helpers + + + private async void ApplyDialogueLine() + { + MutationCooldown.Stop(); + + isWaitingForInput = false; + balloon.FocusMode = Control.FocusModeEnum.All; + balloon.GrabFocus(); + + // Set up the character name + characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); + characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); + + // Set up the dialogue + dialogueLabel.Hide(); + dialogueLabel.Set("dialogue_line", dialogueLine); + + // Set up the responses + responsesMenu.Hide(); + responsesMenu.Set("responses", dialogueLine.Responses); + + // Type out the text + balloon.Show(); + willHideBalloon = false; + dialogueLabel.Show(); + if (!string.IsNullOrEmpty(dialogueLine.Text)) + { + dialogueLabel.Call("type_out"); + await ToSignal(dialogueLabel, "finished_typing"); + } + + // Wait for input + if (dialogueLine.Responses.Count > 0) + { + balloon.FocusMode = Control.FocusModeEnum.None; + responsesMenu.Show(); + } + else if (!string.IsNullOrEmpty(dialogueLine.Time)) + { + float time = 0f; + if (!float.TryParse(dialogueLine.Time, out time)) + { + time = dialogueLine.Text.Length * 0.02f; + } + await ToSignal(GetTree().CreateTimer(time), "timeout"); + Next(dialogueLine.NextId); + } + else + { + isWaitingForInput = true; + balloon.FocusMode = Control.FocusModeEnum.All; + balloon.GrabFocus(); + } + } + + + #endregion + + + #region signals + + + private void OnMutated(Dictionary _mutation) + { + isWaitingForInput = false; + willHideBalloon = true; + MutationCooldown.Start(0.1f); + } + + + #endregion } - - - public override void _Ready() - { - balloon = GetNode<Control>("%Balloon"); - characterLabel = GetNode<RichTextLabel>("%CharacterLabel"); - dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel"); - responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu"); - - balloon.Hide(); - - balloon.GuiInput += (@event) => - { - if ((bool)dialogueLabel.Get("is_typing")) - { - bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed(); - bool skipButtonWasPressed = @event.IsActionPressed(SkipAction); - if (mouseWasClicked || skipButtonWasPressed) - { - GetViewport().SetInputAsHandled(); - dialogueLabel.Call("skip_typing"); - return; - } - } - - if (!isWaitingForInput) - return; - if (dialogueLine.Responses.Count > 0) - return; - - GetViewport().SetInputAsHandled(); - - if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left) - { - Next(dialogueLine.NextId); - } - else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon) - { - Next(dialogueLine.NextId); - } - }; - - if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) - { - responsesMenu.Set("next_action", NextAction); - } - responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => - { - Next(response.NextId); - })); - - DialogueManager.Mutated += OnMutated; - } - - - public override void _ExitTree() - { - DialogueManager.Mutated -= OnMutated; - } - - - public override void _UnhandledInput(InputEvent @event) - { - // Only the balloon is allowed to handle input while it's showing - GetViewport().SetInputAsHandled(); - } - - public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null) - { - temporaryGameStates = extraGameStates ?? new Array<Variant>(); - isWaitingForInput = false; - resource = dialogueResource; - - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); - } - - - public async void Next(string nextId) - { - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); - } - - - #region Helpers - - - private async void UpdateDialogue() - { - if (!IsNodeReady()) - { - await ToSignal(this, SignalName.Ready); - } - - // Set up the character name - characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); - characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); - - // Set up the dialogue - dialogueLabel.Hide(); - dialogueLabel.Set("dialogue_line", dialogueLine); - - // Set up the responses - responsesMenu.Hide(); - responsesMenu.Set("responses", dialogueLine.Responses); - - // Type out the text - balloon.Show(); - willHideBalloon = false; - dialogueLabel.Show(); - if (!string.IsNullOrEmpty(dialogueLine.Text)) - { - dialogueLabel.Call("type_out"); - await ToSignal(dialogueLabel, "finished_typing"); - } - - // Wait for input - if (dialogueLine.Responses.Count > 0) - { - balloon.FocusMode = Control.FocusModeEnum.None; - responsesMenu.Show(); - } - else if (!string.IsNullOrEmpty(dialogueLine.Time)) - { - float time = 0f; - if (!float.TryParse(dialogueLine.Time, out time)) - { - time = dialogueLine.Text.Length * 0.02f; - } - await ToSignal(GetTree().CreateTimer(time), "timeout"); - Next(dialogueLine.NextId); - } - else - { - isWaitingForInput = true; - balloon.FocusMode = Control.FocusModeEnum.All; - balloon.GrabFocus(); - } - } - - - #endregion - - - #region signals - - - private void OnMutated(Dictionary _mutation) - { - isWaitingForInput = false; - willHideBalloon = true; - GetTree().CreateTimer(0.1f).Timeout += () => - { - if (willHideBalloon) - { - willHideBalloon = false; - balloon.Hide(); - } - }; - } - - - #endregion -} \ No newline at end of file +} diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid index 1058856a..4b3783a5 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid @@ -1 +1 @@ -uid://dac8psvf0vqvj +uid://5b3w40kwakl3 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd index ffeec571..c7e6d9a0 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd @@ -1,4 +1,5 @@ -extends CanvasLayer +class_name DialogueManagerExampleBalloon extends CanvasLayer +## A basic dialogue balloon for use with Dialogue Manager. ## The action to use for advancing the dialogue @export var next_action: StringName = &"ui_accept" @@ -6,11 +7,6 @@ extends CanvasLayer ## The action to use to skip typing the dialogue @export var skip_action: StringName = &"ui_cancel" -@onready var balloon: Control = %Balloon -@onready var character_label: RichTextLabel = %CharacterLabel -@onready var dialogue_label: DialogueLabel = %DialogueLabel -@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu - ## The dialogue resource var resource: DialogueResource @@ -23,59 +19,38 @@ var is_waiting_for_input: bool = false ## See if we are running a long mutation and should hide the balloon var will_hide_balloon: bool = false +## A dictionary to store any ephemeral variables +var locals: Dictionary = {} + var _locale: String = TranslationServer.get_locale() ## The current line var dialogue_line: DialogueLine: - set(next_dialogue_line): - is_waiting_for_input = false - balloon.focus_mode = Control.FOCUS_ALL - balloon.grab_focus() - - # The dialogue has finished so close the balloon - if not next_dialogue_line: - queue_free() - return - - # If the node isn't ready yet then none of the labels will be ready yet either - if not is_node_ready(): - await ready - - dialogue_line = next_dialogue_line - - character_label.visible = not dialogue_line.character.is_empty() - character_label.text = tr(dialogue_line.character, "dialogue") - - dialogue_label.hide() - dialogue_label.dialogue_line = dialogue_line - - responses_menu.hide() - responses_menu.set_responses(dialogue_line.responses) - - # Show our balloon - balloon.show() - will_hide_balloon = false - - dialogue_label.show() - if not dialogue_line.text.is_empty(): - dialogue_label.type_out() - await dialogue_label.finished_typing - - # Wait for input - if dialogue_line.responses.size() > 0: - balloon.focus_mode = Control.FOCUS_NONE - responses_menu.show() - elif dialogue_line.time != "": - var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() - await get_tree().create_timer(time).timeout - next(dialogue_line.next_id) + set(value): + if value: + dialogue_line = value + apply_dialogue_line() else: - is_waiting_for_input = true - balloon.focus_mode = Control.FOCUS_ALL - balloon.grab_focus() + # The dialogue has finished so close the balloon + queue_free() get: return dialogue_line +## A cooldown timer for delaying the balloon hide when encountering a mutation. +var mutation_cooldown: Timer = Timer.new() + +## The base balloon anchor +@onready var balloon: Control = %Balloon + +## The label showing the name of the currently speaking character +@onready var character_label: RichTextLabel = %CharacterLabel + +## The label showing the currently spoken dialogue +@onready var dialogue_label: DialogueLabel = %DialogueLabel + +## The menu of responses +@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu + func _ready() -> void: balloon.hide() @@ -85,6 +60,9 @@ func _ready() -> void: if responses_menu.next_action.is_empty(): responses_menu.next_action = next_action + mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout) + add_child(mutation_cooldown) + func _unhandled_input(_event: InputEvent) -> void: # Only the balloon is allowed to handle input while it's showing @@ -103,12 +81,52 @@ func _notification(what: int) -> void: ## Start some dialogue func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: - temporary_game_states = [self] + extra_game_states + temporary_game_states = [self] + extra_game_states is_waiting_for_input = false resource = dialogue_resource self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) +## Apply any changes to the balloon given a new [DialogueLine]. +func apply_dialogue_line() -> void: + mutation_cooldown.stop() + + is_waiting_for_input = false + balloon.focus_mode = Control.FOCUS_ALL + balloon.grab_focus() + + character_label.visible = not dialogue_line.character.is_empty() + character_label.text = tr(dialogue_line.character, "dialogue") + + dialogue_label.hide() + dialogue_label.dialogue_line = dialogue_line + + responses_menu.hide() + responses_menu.responses = dialogue_line.responses + + # Show our balloon + balloon.show() + will_hide_balloon = false + + dialogue_label.show() + if not dialogue_line.text.is_empty(): + dialogue_label.type_out() + await dialogue_label.finished_typing + + # Wait for input + if dialogue_line.responses.size() > 0: + balloon.focus_mode = Control.FOCUS_NONE + responses_menu.show() + elif dialogue_line.time != "": + var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() + await get_tree().create_timer(time).timeout + next(dialogue_line.next_id) + else: + is_waiting_for_input = true + balloon.focus_mode = Control.FOCUS_ALL + balloon.grab_focus() + + ## Go to the next line func next(next_id: String) -> void: self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) @@ -117,14 +135,16 @@ func next(next_id: String) -> void: #region Signals +func _on_mutation_cooldown_timeout() -> void: + if will_hide_balloon: + will_hide_balloon = false + balloon.hide() + + func _on_mutated(_mutation: Dictionary) -> void: is_waiting_for_input = false will_hide_balloon = true - get_tree().create_timer(0.1).timeout.connect(func(): - if will_hide_balloon: - will_hide_balloon = false - balloon.hide() - ) + mutation_cooldown.start(0.1) func _on_balloon_gui_input(event: InputEvent) -> void: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd.uid index dd4b136e..6327f9b6 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.gd.uid @@ -1 +1 @@ -uid://by1vx32y4g8gs +uid://d1wt4ma6055l8 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn index 6facc260..91d8a7df 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] -[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"] +[ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] -[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"] +[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] bg_color = Color(0, 0, 0, 1) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn index 30118337..c4d2145f 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"] -[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"] +[ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"] -[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_1j1j0"] +[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"] content_margin_left = 6.0 @@ -104,6 +104,7 @@ grow_vertical = 2 theme = SubResource("Theme_qq3yp") [node name="Panel" type="Panel" parent="Balloon"] +clip_children = 2 layout_mode = 1 anchors_preset = 12 anchor_top = 1.0 @@ -115,6 +116,7 @@ offset_right = -4.0 offset_bottom = -4.0 grow_horizontal = 2 grow_vertical = 0 +mouse_filter = 1 [node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] layout_mode = 1 @@ -142,7 +144,6 @@ unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." -skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex") [node name="Responses" type="MarginContainer" parent="Balloon"] layout_mode = 1 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd new file mode 100644 index 00000000..1cdcb7e8 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd @@ -0,0 +1,20 @@ +class_name DMExportPlugin extends EditorExportPlugin + +const IGNORED_PATHS = [ + "/assets", + "/components", + "/l10n", + "/views" +] + +func _export_file(path: String, type: String, features: PackedStringArray) -> void: + var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + + # Ignore any editor stuff + for ignored_path: String in IGNORED_PATHS: + if path.begins_with(plugin_path + ignored_path): + skip() + + # Ignore C# stuff it not using dotnet + if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"): + skip() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd.uid new file mode 100644 index 00000000..efaa0c6e --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd.uid @@ -0,0 +1 @@ +uid://sa55ra11ji2q diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd index 157ea131..345fe844 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd @@ -1,20 +1,16 @@ @tool -extends EditorImportPlugin +class_name DMImportPlugin extends EditorImportPlugin signal compiled_resource(resource: Resource) -const DialogueResource = preload("./dialogue_resource.gd") -const DialogueManagerParser = preload("./components/parser.gd") -const DialogueManagerParseResult = preload("./components/parse_result.gd") - -const compiler_version = 12 +const COMPILER_VERSION = 14 func _get_importer_name() -> String: # NOTE: A change to this forces a re-import of all dialogue - return "dialogue_manager_compiler_%s" % compiler_version + return "dialogue_manager_compiler_%s" % COMPILER_VERSION func _get_visible_name() -> String: @@ -63,7 +59,7 @@ func _get_option_visibility(path: String, option_name: StringName, options: Dict func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error: - var cache = Engine.get_meta("DialogueCache") + var cache = Engine.get_meta("DMCache") # Get the raw file contents if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND @@ -73,17 +69,12 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo cache.file_content_changed.emit(source_file, raw_text) - # Parse the text - var parser: DialogueManagerParser = DialogueManagerParser.new() - var err: Error = parser.parse(raw_text, source_file) - var data: DialogueManagerParseResult = parser.get_data() - var errors: Array[Dictionary] = parser.get_errors() - parser.free() - - if err != OK: - printerr("%d errors found in %s" % [errors.size(), source_file]) - cache.add_errors_to_file(source_file, errors) - return err + # Compile the text + var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file) + if result.errors.size() > 0: + printerr("%d errors found in %s" % [result.errors.size(), source_file]) + cache.add_errors_to_file(source_file, result.errors) + return ERR_PARSE_ERROR # Get the current addon version var config: ConfigFile = ConfigFile.new() @@ -94,17 +85,17 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo var resource: DialogueResource = DialogueResource.new() resource.set_meta("dialogue_manager_version", version) - resource.using_states = data.using_states - resource.titles = data.titles - resource.first_title = data.first_title - resource.character_names = data.character_names - resource.lines = data.lines - resource.raw_text = data.raw_text + resource.using_states = result.using_states + resource.titles = result.titles + resource.first_title = result.first_title + resource.character_names = result.character_names + resource.lines = result.lines + resource.raw_text = result.raw_text # Clear errors and possibly trigger any cascade recompiles - cache.add_file(source_file, data) + cache.add_file(source_file, result) - err = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) + var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) compiled_resource.emit(resource) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd.uid index 5abad422..e98bfab8 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd.uid @@ -1 +1 @@ -uid://db6mool8x8kfk +uid://dhwpj6ed8soyq diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd index e3cb7e51..366c1f3a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd @@ -1,5 +1,5 @@ @tool -extends EditorInspectorPlugin +class_name DMInspectorPlugin extends EditorInspectorPlugin const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd") diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd.uid index 54023494..00c8db87 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/inspector_plugin.gd.uid @@ -1 +1 @@ -uid://dsdontxsvnauk +uid://0x31sbqbikov diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po index c1b6000a..5e241657 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po @@ -21,6 +21,9 @@ msgstr "Open a file" msgid "open.open" msgstr "Open..." +msgid "open.quick_open" +msgstr "Quick open..." + msgid "open.no_recent_files" msgstr "No recent files" @@ -30,11 +33,17 @@ msgstr "Clear recent files" msgid "save_all_files" msgstr "Save all files" +msgid "all" +msgstr "All" + msgid "find_in_files" msgstr "Find in files..." msgid "test_dialogue" -msgstr "Test dialogue" +msgstr "Test dialogue from start of file" + +msgid "test_dialogue_from_line" +msgstr "Test dialogue from current line" msgid "search_for_text" msgstr "Search for text" @@ -45,9 +54,6 @@ msgstr "Insert" msgid "translations" msgstr "Translations" -msgid "settings" -msgstr "Settings" - msgid "sponsor" msgstr "Sponsor" @@ -141,84 +147,6 @@ msgstr "Copy file path" msgid "buffer.show_in_filesystem" msgstr "Show in FileSystem" -msgid "settings.invalid_test_scene" -msgstr "\"{path}\" does not extend BaseDialogueTestScene." - -msgid "settings.revert_to_default_test_scene" -msgstr "Revert to default test scene" - -msgid "settings.default_balloon_hint" -msgstr "Custom balloon to use when calling \"DialogueManager.show_balloon()\"" - -msgid "settings.revert_to_default_balloon" -msgstr "Revert to default balloon" - -msgid "settings.default_balloon_path" -msgstr "<example balloon>" - -msgid "settings.autoload" -msgstr "Autoload" - -msgid "settings.path" -msgstr "Path" - -msgid "settings.new_template" -msgstr "New dialogue files will start with template text" - -msgid "settings.missing_keys" -msgstr "Treat missing translation keys as errors" - -msgid "settings.missing_keys_hint" -msgstr "If you are using static translation keys then having this enabled will help you find any lines that you haven't added a key to yet." - -msgid "settings.characters_translations" -msgstr "Export character names in translation files" - -msgid "settings.wrap_long_lines" -msgstr "Wrap long lines" - -msgid "settings.include_failed_responses" -msgstr "Include responses with failed conditions" - -msgid "settings.ignore_missing_state_values" -msgstr "Skip over missing state value errors (not recommended)" - -msgid "settings.custom_test_scene" -msgstr "Custom test scene (must extend BaseDialogueTestScene)" - -msgid "settings.default_csv_locale" -msgstr "Default CSV Locale" - -msgid "settings.states_shortcuts" -msgstr "State Shortcuts" - -msgid "settings.states_message" -msgstr "If an autoload is enabled here you can refer to its properties, methods, and signals without having to use its name." - -msgid "settings.states_hint" -msgstr "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\"" - -msgid "settings.recompile_warning" -msgstr "Changing these settings will force a recompile of all dialogue. Only change them if you know what you are doing." - -msgid "settings.create_lines_for_responses_with_characters" -msgstr "Create child dialogue line for responses with character names in them" - -msgid "settings.open_in_external_editor" -msgstr "Open dialogue files in external editor" - -msgid "settings.external_editor_warning" -msgstr "Note: Syntax highlighting and detailed error checking are not supported in external editors." - -msgid "settings.include_characters_in_translations" -msgstr "Include character names in translation exports" - -msgid "settings.include_notes_in_translations" -msgstr "Include notes (## comments) in translation exports" - -msgid "settings.check_for_updates" -msgstr "Check for updates" - msgid "n_of_n" msgstr "{index} of {total}" @@ -294,9 +222,6 @@ msgstr "Titles cannot be empty." msgid "errors.duplicate_title" msgstr "There is already a title with that name." -msgid "errors.nested_title" -msgstr "Titles cannot be indented." - msgid "errors.invalid_title_string" msgstr "Titles can only contain alphanumeric characters and numbers." @@ -384,6 +309,21 @@ msgstr "Invalid index." msgid "errors.unexpected_assignment" msgstr "Unexpected assignment." +msgid "errors.expected_when_or_else" +msgstr "Expecting a when or an else case." + +msgid "errors.only_one_else_allowed" +msgstr "Only one else case is allowed per match." + +msgid "errors.when_must_belong_to_match" +msgstr "When statements can only appear as children of match statements." + +msgid "errors.concurrent_line_without_origin" +msgstr "Concurrent lines need an origin line that doesn't start with \"| \"." + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "Goto references are not allowed on concurrent dialogue lines." + msgid "errors.unknown" msgstr "Unknown syntax." @@ -478,4 +418,10 @@ msgid "runtime.unsupported_array_type" msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead." msgid "runtime.dialogue_balloon_missing_start_method" -msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method." \ No newline at end of file +msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method." + +msgid "runtime.top_level_states_share_name" +msgstr "Multiple top-level states ({states}) share method/property/signal name \"{key}\". Only the first occurance is accessible to dialogue." + +msgid "translation_plugin.character_name" +msgstr "Character name" \ No newline at end of file diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po index 3cbaa32e..ef604e1d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po @@ -41,9 +41,6 @@ msgstr "Insertar" msgid "translations" msgstr "Traducciones" -msgid "settings" -msgstr "Ajustes" - msgid "show_support" msgstr "Contribuye con Dialogue Manager" @@ -134,82 +131,6 @@ msgstr "Copiar la ruta del archivo" msgid "buffer.show_in_filesystem" msgstr "Mostrar en el sistema de archivos" -msgid "settings.invalid_test_scene" -msgstr "\"{path}\" no extiende BaseDialogueTestScene." - -msgid "settings.revert_to_default_test_scene" -msgstr "Revertir a la escena de prueba por defecto" - -msgid "settings.default_balloon_hint" -msgstr "" -"Globo personalizado para usar al llamar a \"DialogueManager.show_balloon()\"" - -msgid "settings.revert_to_default_balloon" -msgstr "Volver al globo predeterminado" - -msgid "settings.default_balloon_path" -msgstr "<globo de ejemplo>" - -msgid "settings.autoload" -msgstr "Autocarga" - -msgid "settings.path" -msgstr "Ruta" - -msgid "settings.new_template" -msgstr "Los nuevos archivos de diálogo empezarán con una plantilla" - -msgid "settings.missing_keys" -msgstr "Tratar las claves de traducción faltantes como errores" - -msgid "settings.missing_keys_hint" -msgstr "Si estás utilizando claves de traducción estáticas, tener esta opción habilitada te ayudará a encontrar cualquier línea a la que aún no le hayas añadido una clave." - -msgid "settings.characters_translations" -msgstr "Exportar nombres de personajes en archivos de traducción" - -msgid "settings.wrap_long_lines" -msgstr "Romper líneas largas" - -msgid "settings.include_failed_responses" -msgstr "Incluir respuestas con condiciones fallidas" - -msgid "settings.ignore_missing_state_values" -msgstr "Omitir errores de valores de estado faltantes (no recomendado)" - -msgid "settings.custom_test_scene" -msgstr "Escena de prueba personalizada (debe extender BaseDialogueTestScene)" - -msgid "settings.default_csv_locale" -msgstr "Localización CSV por defecto" - -msgid "settings.states_shortcuts" -msgstr "Atajos de teclado" - -msgid "settings.states_message" -msgstr "Si un autoload está habilitado aquí, puedes referirte a sus propiedades y métodos sin tener que usar su nombre." - -msgid "settings.states_hint" -msgstr "ie. En lugar de \"SomeState.some_property\" podría simplemente usar \"some_property\"" - -msgid "settings.recompile_warning" -msgstr "Cambiar estos ajustes obligará a recompilar todo el diálogo. Hazlo solo si sabes lo que estás haciendo." - -msgid "settings.create_lines_for_responses_with_characters" -msgstr "Crear línea de diálogo para respuestas con nombres de personajes dentro." - -msgid "settings.open_in_external_editor" -msgstr "Abrir archivos de diálogo en el editor externo" - -msgid "settings.external_editor_warning" -msgstr "Nota: El resaltado de sintaxis y la verificación detallada de errores no están soportados en editores externos." - -msgid "settings.include_characters_in_translations" -msgstr "Incluir nombres de personajes en las exportaciones de traducción" - -msgid "settings.include_notes_in_translations" -msgstr "Incluir notas (## comentarios) en las exportaciones de traducción" - msgid "n_of_n" msgstr "{index} de {total}" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot index 4aeec3b2..85cb8ac9 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot @@ -14,6 +14,9 @@ msgstr "" msgid "open.open" msgstr "" +msgid "open.quick_open" +msgstr "" + msgid "open.no_recent_files" msgstr "" @@ -23,12 +26,18 @@ msgstr "" msgid "save_all_files" msgstr "" +msgid "all" +msgstr "" + msgid "find_in_files" msgstr "" msgid "test_dialogue" msgstr "" +msgid "test_dialogue_from_line" +msgstr "" + msgid "search_for_text" msgstr "" @@ -38,9 +47,6 @@ msgstr "" msgid "translations" msgstr "" -msgid "settings" -msgstr "" - msgid "sponsor" msgstr "" @@ -131,84 +137,6 @@ msgstr "" msgid "buffer.show_in_filesystem" msgstr "" -msgid "settings.invalid_test_scene" -msgstr "" - -msgid "settings.revert_to_default_test_scene" -msgstr "" - -msgid "settings.default_balloon_hint" -msgstr "" - -msgid "settings.revert_to_default_balloon" -msgstr "" - -msgid "settings.default_balloon_path" -msgstr "" - -msgid "settings.autoload" -msgstr "" - -msgid "settings.path" -msgstr "" - -msgid "settings.new_template" -msgstr "" - -msgid "settings.missing_keys" -msgstr "" - -msgid "settings.missing_keys_hint" -msgstr "" - -msgid "settings.characters_translations" -msgstr "" - -msgid "settings.wrap_long_lines" -msgstr "" - -msgid "settings.include_failed_responses" -msgstr "" - -msgid "settings.ignore_missing_state_values" -msgstr "" - -msgid "settings.custom_test_scene" -msgstr "" - -msgid "settings.default_csv_locale" -msgstr "" - -msgid "settings.states_shortcuts" -msgstr "" - -msgid "settings.states_message" -msgstr "" - -msgid "settings.states_hint" -msgstr "" - -msgid "settings.recompile_warning" -msgstr "" - -msgid "settings.create_lines_for_responses_with_characters" -msgstr "" - -msgid "settings.open_in_external_editor" -msgstr "" - -msgid "settings.external_editor_warning" -msgstr "" - -msgid "settings.include_characters_in_translations" -msgstr "" - -msgid "settings.include_notes_in_translations" -msgstr "" - -msgid "settings.check_for_updates" -msgstr "" - msgid "n_of_n" msgstr "" @@ -284,9 +212,6 @@ msgstr "" msgid "errors.duplicate_title" msgstr "" -msgid "errors.nested_title" -msgstr "" - msgid "errors.invalid_title_string" msgstr "" @@ -374,6 +299,21 @@ msgstr "" msgid "errors.unexpected_assignment" msgstr "" +msgid "errors.expected_when_or_else" +msgstr "" + +msgid "errors.only_one_else_allowed" +msgstr "" + +msgid "errors.when_must_belong_to_match" +msgstr "" + +msgid "errors.concurrent_line_without_origin" +msgstr "" + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "" + msgid "errors.unknown" msgstr "" @@ -468,4 +408,10 @@ msgid "runtime.unsupported_array_type" msgstr "" msgid "runtime.dialogue_balloon_missing_start_method" +msgstr "" + +msgid "runtime.top_level_states_share_name" +msgstr "" + +msgid "translation_plugin.character_name" msgstr "" \ No newline at end of file diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po index da1c5ab5..8cd41ac4 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po @@ -4,7 +4,7 @@ msgstr "" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: \n" -"Language-Team: Veydzher\n" +"Language-Team: \n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,8 +20,11 @@ msgstr "Відкрити файл" msgid "open.open" msgstr "Відкрити..." +msgid "open.quick_open" +msgstr "Швидко відкрити..." + msgid "open.no_recent_files" -msgstr "Немає недавніх файлів" +msgstr "Жодних недавніх файлів" msgid "open.clear_recent_files" msgstr "Очистити недавні файли" @@ -30,13 +33,16 @@ msgid "save_all_files" msgstr "Зберегти всі файли" msgid "find_in_files" -msgstr "Знайти у файліх..." +msgstr "Знайти у файлах..." msgid "test_dialogue" -msgstr "Тестувати діалог" +msgstr "Протестувати діалог з початку файлу" + +msgid "test_dialogue_from_line" +msgstr "Протестувати діалог з поточного рядка" msgid "search_for_text" -msgstr "Шукати текст" +msgstr "Пошук тексту" msgid "insert" msgstr "Вставити" @@ -44,9 +50,6 @@ msgstr "Вставити" msgid "translations" msgstr "Переклади" -msgid "settings" -msgstr "Налаштування" - msgid "sponsor" msgstr "Спонсор" @@ -84,7 +87,7 @@ msgid "insert.response" msgstr "Відповідь" msgid "insert.random_lines" -msgstr "Випадковий рядок" +msgstr "Випадкові рядки" msgid "insert.random_text" msgstr "Випадковий текст" @@ -93,7 +96,7 @@ msgid "insert.actions" msgstr "Дії" msgid "insert.jump" -msgstr "Перехід до заголовку" +msgstr "Перейти до заголовку" msgid "insert.end_dialogue" msgstr "Кінець діалогу" @@ -108,10 +111,10 @@ msgid "save_to_csv" msgstr "Зберегти рядки в CSV..." msgid "import_from_csv" -msgstr "Імпортувати зміни в рядках з CSV..." +msgstr "Імпортувати зміни рядків з CSV..." msgid "confirm_close" -msgstr "Зберегти зміни до '{path}'?" +msgstr "Зберегти зміни до «{path}»?" msgid "confirm_close.save" msgstr "Зберегти зміни" @@ -138,85 +141,7 @@ msgid "buffer.copy_file_path" msgstr "Копіювати шлях файлу" msgid "buffer.show_in_filesystem" -msgstr "Показати у системі файлів" - -msgid "settings.invalid_test_scene" -msgstr "«{path}» не розширює BaseDialogueTestScene." - -msgid "settings.revert_to_default_test_scene" -msgstr "Повернутися до стандартної тестової сцени" - -msgid "settings.default_balloon_hint" -msgstr "Користувацьке діалогове вікно для використання під час виклику «DialogueManager.show_balloon()»" - -msgid "settings.revert_to_default_balloon" -msgstr "Повернутися до стандартного діалогового вікна" - -msgid "settings.default_balloon_path" -msgstr "<приклад діалогового вікна>" - -msgid "settings.autoload" -msgstr "Авто. завантаження" - -msgid "settings.path" -msgstr "Шлях" - -msgid "settings.new_template" -msgstr "Нові файли діалогів починатимуться з тексту шаблону" - -msgid "settings.missing_keys" -msgstr "Вважати відсутні ключі перекладу як помилками" - -msgid "settings.missing_keys_hint" -msgstr "Якщо ви використовуєте статичні ключі перекладу, увімкнення цього параметра допоможе вам знайти рядки, до яких ви ще не додали ключ." - -msgid "settings.characters_translations" -msgstr "Експорт імен персонажів у файлах перекладу" - -msgid "settings.wrap_long_lines" -msgstr "Переносити довгі рядки" - -msgid "settings.include_failed_responses" -msgstr "Включити відповіді з невдалими умовами" - -msgid "settings.ignore_missing_state_values" -msgstr "Пропускати помилки пропущених значень стану (не рекомендується)" - -msgid "settings.custom_test_scene" -msgstr "Користувацька тестова сцена (повинна розширювати BaseDialogueTestScene)" - -msgid "settings.default_csv_locale" -msgstr "Стандартна мова CSV" - -msgid "settings.states_shortcuts" -msgstr "Скорочення станів" - -msgid "settings.states_message" -msgstr "Якщо автозавантаження увімкнено, ви можете звертатися до його властивостей і методів без необхідності використовувати його назву." - -msgid "settings.states_hint" -msgstr "тобто, замість «ЯкийсьСтан.якась_властивість» ви можете просто використовувати «якусь_властивість»" - -msgid "settings.recompile_warning" -msgstr "Зміна цих параметрів призведе до перекомпіляції усіх діалогів. Змінюйте їх, тільки якщо ви знаєте, що робите." - -msgid "settings.create_lines_for_responses_with_characters" -msgstr "Створити діалогову лінію для відповідей дочірнього елемента з іменами персонажів" - -msgid "settings.open_in_external_editor" -msgstr "Відкрити файли діалогів у зовнішньому редакторі" - -msgid "settings.external_editor_warning" -msgstr "Примітка: Підсвічування синтаксису та детальна перевірка помилок не підтримуються у зовнішніх редакторах." - -msgid "settings.include_characters_in_translations" -msgstr "Включати імена персонажів до експорту перекладу" - -msgid "settings.include_notes_in_translations" -msgstr "Включати примітки (## коментарі) до експорту перекладу" - -msgid "settings.check_for_updates" -msgstr "Перевірити наявність оновлень" +msgstr "Показати у файловій системі" msgid "n_of_n" msgstr "{index} з {total}" @@ -234,7 +159,7 @@ msgid "search.replace_placeholder" msgstr "Текст для заміни" msgid "search.replace_selected" -msgstr "Замінити виділене" +msgstr "Замінити вибране" msgid "search.previous" msgstr "Назад" @@ -267,7 +192,7 @@ msgid "errors.key_not_found" msgstr "Ключ «{key}» не знайдено." msgid "errors.line_and_message" -msgstr "Помилка на {line}, {column}: {message}" +msgstr "Помилка в {line}, {column}: {message}" msgid "errors_in_script" msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз." @@ -285,16 +210,13 @@ msgid "errors.duplicate_import" msgstr "Дублювання назви імпорту." msgid "errors.unknown_using" -msgstr "Невідоме автозавантаження в операторі використання." +msgstr "Невідоме автозавантаження в операторі «using»." msgid "errors.empty_title" msgstr "Заголовки не можуть бути порожніми." msgid "errors.duplicate_title" -msgstr "З такою назвою уже є заголовок." - -msgid "errors.nested_title" -msgstr "Заголовки не повинні мати відступів." +msgstr "Заголовок з такою назвою уже є." msgid "errors.invalid_title_string" msgstr "Заголовки можуть містити лише алфавітно-цифрові символи та цифри." @@ -306,7 +228,7 @@ msgid "errors.unknown_title" msgstr "Невідомий заголовок." msgid "errors.jump_to_invalid_title" -msgstr "Цей перехід вказує на недійсну назву." +msgstr "Цей перехід вказує на недійсний заголовок." msgid "errors.title_has_no_content" msgstr "Цей заголовок не має змісту. Можливо, варто змінити його на «=> END»." @@ -318,7 +240,7 @@ msgid "errors.unexpected_condition" msgstr "Несподівана умова." msgid "errors.duplicate_id" -msgstr "Цей ідентифікатор вже на іншому рядку." +msgstr "Цей ідентифікатор уже є на іншому рядку." msgid "errors.missing_id" msgstr "У цьому рядку відсутній ідентифікатор." @@ -383,6 +305,21 @@ msgstr "Недійсний індекс." msgid "errors.unexpected_assignment" msgstr "Несподіване призначення." +msgid "errors.expected_when_or_else" +msgstr "Очікувався випадок «when» або «else»." + +msgid "errors.only_one_else_allowed" +msgstr "Для кожного «match» допускається лише один випадок «else»." + +msgid "errors.when_must_belong_to_match" +msgstr "Оператори «when» можуть з’являтися лише як дочірні операторів «match»." + +msgid "errors.concurrent_line_without_origin" +msgstr "Паралельні рядки потребують початкового рядка, який не починається з «|»." + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "У паралельних діалогових рядках не допускаються Goto посилання." + msgid "errors.unknown" msgstr "Невідомий синтаксис." @@ -399,7 +336,7 @@ msgid "update.download_update" msgstr "Завантажити оновлення" msgid "update.needs_reload" -msgstr "Щоб встановити оновлення, проєкт потрібно перезавантажити." +msgstr "Щоб установити оновлення, проєкт потрібно перезавантажити." msgid "update.reload_ok_button" msgstr "Перезавантажити проєкт" @@ -411,10 +348,10 @@ msgid "update.reload_project" msgstr "Перезавантажити проєкт" msgid "update.release_notes" -msgstr "Читати примітки оновлення" +msgstr "Читати зміни оновлення" msgid "update.success" -msgstr "Менеджер діалогів тепер має версію {version}." +msgstr "Dialogue Manager тепер з версією {version}." msgid "update.failed" msgstr "Виникла проблема із завантаженням оновлення." @@ -441,22 +378,22 @@ msgid "runtime.array_index_out_of_bounds" msgstr "Індекс {index} виходить за межі масиву «{array}»." msgid "runtime.left_hand_size_cannot_be_assigned_to" -msgstr "Ліва частина виразу не може бути присвоєна." +msgstr "Ліва частина виразу не може бути призначена." msgid "runtime.key_not_found" -msgstr "Ключ «{key}» у словнику «{dictionary}»" +msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»" msgid "runtime.property_not_found" -msgstr "«{property}» не є властивістю для жодного стану гри ({states})." +msgstr "«{property}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." msgid "runtime.property_not_found_missing_export" -msgstr "«{property}» не є властивістю для жодного стану гри ({states}). Можливо, вам слід додати декоратор [Export]." +msgstr "«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." msgid "runtime.method_not_found" -msgstr "«{method}» не є методом на жодному зі станів гри ({states})" +msgstr "Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." msgid "runtime.signal_not_found" -msgstr "«{signal_name}» не є сигналом на жодному зі станів гри ({states})" +msgstr "Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." msgid "runtime.method_not_callable" msgstr "«{method}» не є методом, який можна викликати в «{object}»" @@ -465,16 +402,22 @@ msgid "runtime.unknown_operator" msgstr "Невідомий оператор." msgid "runtime.unknown_autoload" -msgstr "«{autoload}» не є дійсним автозавантаженням." +msgstr "Схоже, «{autoload}» не є дійсним автозавантаженням." msgid "runtime.something_went_wrong" msgstr "Щось пішло не так." msgid "runtime.expected_n_got_n_args" -msgstr "«{method}» було викликано з аргументами «{received}», але він має лише «{expected}»." +msgstr "«{method}» було викликано з аргументами «{received}», але воно має лише «{expected}»." msgid "runtime.unsupported_array_type" -msgstr "Array[{type}] не підтримується в мутаціях. Натомість використовуйте Array як тип." +msgstr "Array[{type}] не підтримується у модифікаціях. Натомість використовуйте Array як тип." msgid "runtime.dialogue_balloon_missing_start_method" -msgstr "У вашому діалоговому вікні відсутній метод «start» або «Start»." \ No newline at end of file +msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»." + +msgid "runtime.top_level_states_share_name" +msgstr "Кілька станів верхнього рівня ({states}) мають спільну назву методу/властивості/сигналу «{key}». Для діалогу доступний лише перший випадок." + +msgid "translation_plugin.character_name" +msgstr "Ім’я персонажа" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po index 887fb1eb..bafd1d58 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po @@ -44,9 +44,6 @@ msgstr "插入" msgid "translations" msgstr "翻译" -msgid "settings" -msgstr "设置" - msgid "show_support" msgstr "支持 Dialogue Manager" @@ -137,69 +134,6 @@ msgstr "复制文件路径" msgid "buffer.show_in_filesystem" msgstr "在 Godot 侧边栏中显示" -msgid "settings.revert_to_default_test_scene" -msgstr "重置测试场景设定" - -msgid "settings.default_balloon_hint" -msgstr "设置调用 \"DialogueManager.show_balloon()\" 时使用的对话框" - -msgid "settings.autoload" -msgstr "Autoload" - -msgid "settings.path" -msgstr "路径" - -msgid "settings.new_template" -msgstr "新建文件时自动插入模板" - -msgid "settings.missing_keys" -msgstr "将翻译键缺失视为错误" - -msgid "settings.missing_keys_hint" -msgstr "如果你使用静态键,这将会帮助你寻找未添加至翻译文件的键。" - -msgid "settings.characters_translations" -msgstr "在翻译文件中导出角色名" - -msgid "settings.wrap_long_lines" -msgstr "文本编辑器自动换行" - -msgid "settings.include_failed_responses" -msgstr "在判断条件失败时仍显示回复选项" - -msgid "settings.ignore_missing_state_values" -msgstr "忽略全局变量缺失错误(不建议)" - -msgid "settings.custom_test_scene" -msgstr "自定义测试场景(必须继承自BaseDialogueTestScene)" - -msgid "settings.default_csv_locale" -msgstr "默认 CSV 区域格式" - -msgid "settings.states_shortcuts" -msgstr "全局变量映射" - -msgid "settings.states_message" -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}个" @@ -272,9 +206,6 @@ msgstr "标题名不能为空。" msgid "errors.duplicate_title" msgstr "标题名不能重复。" -msgid "errors.nested_title" -msgstr "标题不能嵌套。" - msgid "errors.invalid_title_string" msgstr "标题名无效。" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po index bee270d5..e20feee8 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po @@ -44,9 +44,6 @@ msgstr "插入" msgid "translations" msgstr "翻譯" -msgid "settings" -msgstr "設定" - msgid "show_support" msgstr "支援 Dialogue Manager" @@ -137,69 +134,6 @@ msgstr "複製檔案位置" msgid "buffer.show_in_filesystem" msgstr "在 Godot 側邊欄中顯示" -msgid "settings.revert_to_default_test_scene" -msgstr "重置測試場景設定" - -msgid "settings.default_balloon_hint" -msgstr "設置使用 \"DialogueManager.show_balloon()\" 时的对话框" - -msgid "settings.autoload" -msgstr "Autoload" - -msgid "settings.path" -msgstr "路徑" - -msgid "settings.new_template" -msgstr "新建檔案時自動插入模板" - -msgid "settings.missing_keys" -msgstr "將翻譯鍵缺失視爲錯誤" - -msgid "settings.missing_keys_hint" -msgstr "如果你使用靜態鍵,這將會幫助你尋找未添加至翻譯檔案的鍵。" - -msgid "settings.wrap_long_lines" -msgstr "自動折行" - -msgid "settings.characters_translations" -msgstr "在翻譯檔案中匯出角色名。" - -msgid "settings.include_failed_responses" -msgstr "在判斷條件失敗時仍顯示回復選項" - -msgid "settings.ignore_missing_state_values" -msgstr "忽略全局變量缺失錯誤(不建議)" - -msgid "settings.custom_test_scene" -msgstr "自訂測試場景(必須繼承自BaseDialogueTestScene)" - -msgid "settings.default_csv_locale" -msgstr "預設 CSV 區域格式" - -msgid "settings.states_shortcuts" -msgstr "全局變量映射" - -msgid "settings.states_message" -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}個" @@ -272,9 +206,6 @@ msgstr "標題名不能爲空。" msgid "errors.duplicate_title" msgstr "標題名不能重複。" -msgid "errors.nested_title" -msgstr "標題不能嵌套。" - msgid "errors.invalid_title_string" msgstr "標題名無效。" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg index 177fbfb6..9b558dbb 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg @@ -1,7 +1,7 @@ [plugin] name="Dialogue Manager" -description="A simple but powerful branching dialogue system" +description="A powerful nonlinear dialogue system" author="Nathan Hoad" -version="2.41.4" +version="3.4.0" script="plugin.gd" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg.uid new file mode 100644 index 00000000..312d0cf0 --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg.uid @@ -0,0 +1 @@ +uid://hrny2utekhei diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd index b1435c73..4c4edb24 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd @@ -2,21 +2,15 @@ extends EditorPlugin -const DialogueConstants = preload("./constants.gd") -const DialogueImportPlugin = preload("./import_plugin.gd") -const DialogueInspectorPlugin = preload("./inspector_plugin.gd") -const DialogueTranslationParserPlugin = preload("./editor_translation_parser_plugin.gd") -const DialogueSettings = preload("./settings.gd") -const DialogueCache = preload("./components/dialogue_cache.gd") const MainView = preload("./views/main_view.tscn") -const DialogueResource = preload("./dialogue_resource.gd") -var import_plugin: DialogueImportPlugin -var inspector_plugin: DialogueInspectorPlugin -var translation_parser_plugin: DialogueTranslationParserPlugin +var import_plugin: DMImportPlugin +var export_plugin: DMExportPlugin +var inspector_plugin: DMInspectorPlugin +var translation_parser_plugin: DMTranslationParserPlugin var main_view -var dialogue_cache: DialogueCache +var dialogue_cache: DMCache func _enter_tree() -> void: @@ -25,46 +19,89 @@ func _enter_tree() -> void: if Engine.is_editor_hint(): Engine.set_meta("DialogueManagerPlugin", self) - DialogueSettings.prepare() + DMSettings.prepare() - dialogue_cache = DialogueCache.new() - Engine.set_meta("DialogueCache", dialogue_cache) + dialogue_cache = DMCache.new() + Engine.set_meta("DMCache", dialogue_cache) - import_plugin = DialogueImportPlugin.new() + import_plugin = DMImportPlugin.new() add_import_plugin(import_plugin) - inspector_plugin = DialogueInspectorPlugin.new() + export_plugin = DMExportPlugin.new() + add_export_plugin(export_plugin) + + inspector_plugin = DMInspectorPlugin.new() add_inspector_plugin(inspector_plugin) - translation_parser_plugin = DialogueTranslationParserPlugin.new() + translation_parser_plugin = DMTranslationParserPlugin.new() add_translation_parser_plugin(translation_parser_plugin) main_view = MainView.instantiate() - get_editor_interface().get_editor_main_screen().add_child(main_view) + EditorInterface.get_editor_main_screen().add_child(main_view) _make_visible(false) main_view.add_child(dialogue_cache) _update_localization() - get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved) - get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed) + EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) + EditorInterface.get_file_system_dock().file_removed.connect(_on_file_removed) add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon) - # Prevent the project from showing as unsaved even though it was only just opened - if DialogueSettings.get_setting("try_suppressing_startup_unsaved_indicator", false) \ - and Engine.get_physics_frames() == 0 \ - and get_editor_interface().has_method("save_all_scenes"): - var timer: Timer = Timer.new() - var suppress_unsaved_marker: Callable - suppress_unsaved_marker = func(): - if Engine.get_frames_per_second() >= 10: - timer.stop() - get_editor_interface().call("save_all_scenes") - timer.queue_free() - timer.timeout.connect(suppress_unsaved_marker) - add_child(timer) - timer.start(0.1) + # Automatically swap the script on the example balloon depending on if dotnet is being used. + if not FileAccess.file_exists("res://tests/test_basic_dialogue.gd"): + var plugin_path: String = get_plugin_path() + var balloon_file_names: PackedStringArray = ["example_balloon.tscn", "small_example_balloon.tscn"] + for balloon_file_name: String in balloon_file_names: + var balloon_path: String = plugin_path + "/example_balloon/" + balloon_file_name + var balloon_content: String = FileAccess.get_file_as_string(balloon_path) + if "example_balloon.gd" in balloon_content and DMSettings.check_for_dotnet_solution(): + balloon_content = balloon_content \ + # Replace script path with the C# one + .replace("example_balloon.gd", "ExampleBalloon.cs") \ + # Replace script UID with the C# one + .replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs"))) + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(balloon_content) + balloon_file.close() + elif "ExampleBalloon.cs" in balloon_content and not DMSettings.check_for_dotnet_solution(): + balloon_content = balloon_content \ + # Replace script path with the GDScript one + .replace("ExampleBalloon.cs", "example_balloon.gd") \ + # Replace script UID with the GDScript one + .replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd"))) + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(balloon_content) + balloon_file.close() + + # Automatically make any changes to the known custom balloon if there is one. + var balloon_path: String = DMSettings.get_setting(DMSettings.BALLOON_PATH, "") + if balloon_path != "" and FileAccess.file_exists(balloon_path): + var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 + var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" + var example_balloon_path: String = get_plugin_path() + "/example_balloon/" + example_balloon_file_name + + var contents: String = FileAccess.get_file_as_string(balloon_path) + var has_changed: bool = false + + # Make sure the current balloon has a UID unique from the example balloon's + var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) + var balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(balloon_path)) + if example_balloon_uid == balloon_uid: + var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id()) + contents = contents.replace(example_balloon_uid, new_balloon_uid) + has_changed = true + + # Make sure the example balloon copy has the correct renaming of the responses menu + if "reponses" in contents: + contents = contents.replace("reponses", "responses") + has_changed = true + + # Save any changes + if has_changed: + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(contents) + balloon_file.close() func _exit_tree() -> void: @@ -73,6 +110,9 @@ func _exit_tree() -> void: remove_import_plugin(import_plugin) import_plugin = null + remove_export_plugin(export_plugin) + export_plugin = null + remove_inspector_plugin(inspector_plugin) inspector_plugin = null @@ -83,10 +123,10 @@ func _exit_tree() -> void: main_view.queue_free() Engine.remove_meta("DialogueManagerPlugin") - Engine.remove_meta("DialogueCache") + Engine.remove_meta("DMCache") - get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved) - get_editor_interface().get_file_system_dock().file_removed.disconnect(_on_file_removed) + EditorInterface.get_file_system_dock().files_moved.disconnect(_on_files_moved) + EditorInterface.get_file_system_dock().file_removed.disconnect(_on_file_removed) remove_tool_menu_item("Create copy of dialogue example balloon...") @@ -109,10 +149,10 @@ func _get_plugin_icon() -> Texture2D: func _handles(object) -> bool: - var editor_settings: EditorSettings = get_editor_interface().get_editor_settings() + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" - if object is DialogueResource and use_external_editor and DialogueSettings.get_user_value("open_in_external_editor", false): + if object is DialogueResource and use_external_editor and DMSettings.get_user_value("open_in_external_editor", false): var project_path: String = ProjectSettings.globalize_path("res://") var file_path: String = ProjectSettings.globalize_path(object.resource_path) OS.create_process(external_editor, [project_path, file_path]) @@ -134,17 +174,19 @@ func _apply_changes() -> void: func _build() -> bool: # If this is the dotnet Godot then we need to check if the solution file exists - DialogueSettings.check_for_dotnet_solution() + DMSettings.check_for_dotnet_solution() # Ignore errors in other files if we are just running the test scene - if DialogueSettings.get_user_value("is_running_test_scene", true): return true + if DMSettings.get_user_value("is_running_test_scene", true): return true if dialogue_cache != null: + dialogue_cache.reimport_files() + var files_with_errors = dialogue_cache.get_files_with_errors() if files_with_errors.size() > 0: for dialogue_file in files_with_errors: push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) - get_editor_interface().edit_resource(load(files_with_errors[0].path)) + EditorInterface.edit_resource(load(files_with_errors[0].path)) main_view.show_build_error_dialog() return false @@ -191,7 +233,7 @@ func get_editor_shortcuts() -> Dictionary: ] } - var paths = get_editor_interface().get_editor_paths() + var paths = EditorInterface.get_editor_paths() var settings if FileAccess.file_exists(paths.get_config_dir() + "/editor_settings-4.3.tres"): settings = load(paths.get_config_dir() + "/editor_settings-4.3.tres") @@ -265,7 +307,7 @@ func update_import_paths(from_path: String, to_path: String) -> void: # Update the live buffer if main_view.current_file_path == dependent.path: main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path) - main_view.pristine_text = main_view.code_edit.text + main_view.open_buffers[main_view.current_file_path].pristine_text = main_view.code_edit.text # Open the file and update the path var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ) @@ -305,7 +347,7 @@ func _update_localization() -> void: func _copy_dialogue_balloon() -> void: - var scale: float = get_editor_interface().get_editor_scale() + var scale: float = EditorInterface.get_editor_scale() var directory_dialog: FileDialog = FileDialog.new() var label: Label = Label.new() label.text = "Dialogue balloon files will be copied into chosen directory." @@ -314,38 +356,53 @@ func _copy_dialogue_balloon() -> void: directory_dialog.min_size = Vector2(600, 500) * scale directory_dialog.dir_selected.connect(func(path): var plugin_path: String = get_plugin_path() + var is_dotnet: bool = DMSettings.check_for_dotnet_solution() - var is_dotnet: bool = DialogueSettings.check_for_dotnet_solution() var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn") var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd") # Copy the balloon scene file and change the script reference var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" + var example_balloon_path: String = plugin_path + "/example_balloon/" + example_balloon_file_name var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd" - var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_file_name, FileAccess.READ) - var file_contents: String = file.get_as_text().replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path) + var example_balloon_script_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")) + var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) + + # Copy the script file + var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ) + var file_contents: String = file.get_as_text() + if is_dotnet: + file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon") + else: + file_contents = file_contents.replace("class_name DialogueManagerExampleBalloon ", "") + file = FileAccess.open(balloon_script_path, FileAccess.WRITE) + file.store_string(file_contents) + file.close() + var new_balloon_script_uid_raw: int = ResourceUID.create_id() + ResourceUID.add_id(new_balloon_script_uid_raw, balloon_script_path) + var new_balloon_script_uid: String = ResourceUID.id_to_text(new_balloon_script_uid_raw) + + # Save the new balloon + file_contents = FileAccess.get_file_as_string(example_balloon_path) + if "example_balloon.gd" in file_contents: + file_contents = file_contents.replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path) + else: + file_contents = file_contents.replace(plugin_path + "/example_balloon/ExampleBalloon.cs", balloon_script_path) + var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id()) + file_contents = file_contents.replace(example_balloon_uid, new_balloon_uid).replace(example_balloon_script_uid, new_balloon_script_uid) file = FileAccess.open(balloon_path, FileAccess.WRITE) file.store_string(file_contents) file.close() - # Copy the script file - file = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ) - file_contents = file.get_as_text() - if is_dotnet: - file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon") - file = FileAccess.open(balloon_script_path, FileAccess.WRITE) - file.store_string(file_contents) - file.close() + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", balloon_path) - get_editor_interface().get_resource_filesystem().scan() - get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", balloon_path) - - DialogueSettings.set_setting("balloon_path", balloon_path) + DMSettings.set_setting(DMSettings.BALLOON_PATH, balloon_path) directory_dialog.queue_free() ) - get_editor_interface().get_base_control().add_child(directory_dialog) + EditorInterface.get_base_control().add_child(directory_dialog) directory_dialog.popup_centered() @@ -354,10 +411,11 @@ func _copy_dialogue_balloon() -> void: func _on_files_moved(old_file: String, new_file: String) -> void: update_import_paths(old_file, new_file) - DialogueSettings.move_recent_file(old_file, new_file) + DMSettings.move_recent_file(old_file, new_file) func _on_file_removed(file: String) -> void: update_import_paths(file, "") if is_instance_valid(main_view): main_view.close_file(file) + _update_localization() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd.uid index ef1542e8..40573b06 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd.uid @@ -1 +1 @@ -uid://bi3tbngwono0h +uid://bpv426rpvrafa diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd index fb9e6e31..0a0c12f2 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd @@ -1,90 +1,199 @@ @tool -extends Node +class_name DMSettings extends Node -const DialogueConstants = preload("./constants.gd") +#region Editor -### Editor config -const DEFAULT_SETTINGS = { - states = [], - missing_translations_are_errors = false, - export_characters_in_translation = true, - wrap_lines = false, - new_with_template = true, - include_all_responses = false, - ignore_missing_state_values = false, - custom_test_scene_path = preload("./test_scene.tscn").resource_path, - default_csv_locale = "en", - balloon_path = "", - create_lines_for_responses_with_characters = true, - include_character_in_translation_exports = false, - include_notes_in_translation_exports = false, - uses_dotnet = false, - try_suppressing_startup_unsaved_indicator = false +## Wrap lines in the dialogue editor. +const WRAP_LONG_LINES = "editor/wrap_long_lines" +## The template to start new dialogue files with. +const NEW_FILE_TEMPLATE = "editor/new_file_template" + +## Show lines without statis IDs as errors. +const MISSING_TRANSLATIONS_ARE_ERRORS = "editor/translations/missing_translations_are_errors" +## Include character names in the list of translatable strings. +const INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST = "editor/translations/include_characters_in_translatable_strings_list" +## The default locale to use when exporting CSVs +const DEFAULT_CSV_LOCALE = "editor/translations/default_csv_locale" +## Any extra CSV locales to append to the exported translation CSV +const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales" +## Includes a "_character" column in CSV exports. +const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports" +## Includes a "_notes" column in CSV exports +const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_exports" + +## A custom test scene to use when testing dialogue. +const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path" + +## The custom balloon for this game. +const BALLOON_PATH = "runtime/balloon_path" +## The names of any autoloads to shortcut into all dialogue files (so you don't have to write `using SomeGlobal` in each file). +const STATE_AUTOLOAD_SHORTCUTS = "runtime/state_autoload_shortcuts" +## Check for possible naming conflicts in state shortcuts. +const WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS = "runtime/warn_about_method_property_or_signal_name_conflicts" + +## Bypass any missing state when running dialogue. +const IGNORE_MISSING_STATE_VALUES = "runtime/advanced/ignore_missing_state_values" +## Whether or not the project is utilising dotnet. +const USES_DOTNET = "runtime/advanced/uses_dotnet" + + +const SETTINGS_CONFIGURATION = { + WRAP_LONG_LINES: { + value = false, + type = TYPE_BOOL, + }, + NEW_FILE_TEMPLATE: { + value = "~ start\nNathan: [[Hi|Hello|Howdy]], this is some dialogue.\nNathan: Here are some choices.\n- First one\n\tNathan: You picked the first one.\n- Second one\n\tNathan: You picked the second one.\n- Start again => start\n- End the conversation => END\nNathan: For more information see the online documentation.\n=> END", + type = TYPE_STRING, + hint = PROPERTY_HINT_MULTILINE_TEXT, + }, + + MISSING_TRANSLATIONS_ARE_ERRORS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST: { + value = true, + type = TYPE_BOOL, + }, + DEFAULT_CSV_LOCALE: { + value = "en", + type = TYPE_STRING, + hint = PROPERTY_HINT_LOCALE_ID, + }, + EXTRA_CSV_LOCALES: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + is_advanced = true + }, + INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + INCLUDE_NOTES_IN_TRANSLATION_EXPORTS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + + CUSTOM_TEST_SCENE_PATH: { + value = preload("./test_scene.tscn").resource_path, + type = TYPE_STRING, + hint = PROPERTY_HINT_FILE, + is_advanced = true + }, + + BALLOON_PATH: { + value = "", + type = TYPE_STRING, + hint = PROPERTY_HINT_FILE, + }, + STATE_AUTOLOAD_SHORTCUTS: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + }, + WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + + IGNORE_MISSING_STATE_VALUES: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + USES_DOTNET: { + value = false, + type = TYPE_BOOL, + is_hidden = true + } } static func prepare() -> void: - # Migrate previous keys - for key in [ - "states", - "missing_translations_are_errors", - "export_characters_in_translation", - "wrap_lines", - "new_with_template", - "include_all_responses", - "custom_test_scene_path" - ]: - if ProjectSettings.has_setting("dialogue_manager/%s" % key): - var value = ProjectSettings.get_setting("dialogue_manager/%s" % key) - ProjectSettings.set_setting("dialogue_manager/%s" % key, null) - set_setting(key, value) + var should_save_settings: bool = false + + # Remap any old settings into their new keys + var legacy_map: Dictionary = { + states = STATE_AUTOLOAD_SHORTCUTS, + missing_translations_are_errors = MISSING_TRANSLATIONS_ARE_ERRORS, + export_characters_in_translation = INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, + wrap_lines = WRAP_LONG_LINES, + new_with_template = null, + new_template = NEW_FILE_TEMPLATE, + include_all_responses = null, + ignore_missing_state_values = IGNORE_MISSING_STATE_VALUES, + custom_test_scene_path = CUSTOM_TEST_SCENE_PATH, + default_csv_locale = DEFAULT_CSV_LOCALE, + balloon_path = BALLOON_PATH, + create_lines_for_responses_with_characters = null, + include_character_in_translation_exports = INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, + include_notes_in_translation_exports = INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, + uses_dotnet = USES_DOTNET, + try_suppressing_startup_unsaved_indicator = null + } + + for legacy_key: String in legacy_map: + if ProjectSettings.has_setting("dialogue_manager/general/%s" % legacy_key): + should_save_settings = true + # Remove the old setting + var value = ProjectSettings.get_setting("dialogue_manager/general/%s" % legacy_key) + ProjectSettings.set_setting("dialogue_manager/general/%s" % legacy_key, null) + if legacy_map.get(legacy_key) != null: + prints("Migrating Dialogue Manager setting %s to %s with value %s" % [legacy_key, legacy_map.get(legacy_key), str(value)]) + ProjectSettings.set_setting("dialogue_manager/%s" % [legacy_map.get(legacy_key)], value) # Set up initial settings - for setting in DEFAULT_SETTINGS: - var setting_name: String = "dialogue_manager/general/%s" % setting + for key: String in SETTINGS_CONFIGURATION: + var setting_config: Dictionary = SETTINGS_CONFIGURATION[key] + var setting_name: String = "dialogue_manager/%s" % key if not ProjectSettings.has_setting(setting_name): - set_setting(setting, DEFAULT_SETTINGS[setting]) - ProjectSettings.set_initial_value(setting_name, DEFAULT_SETTINGS[setting]) - if setting.ends_with("_path"): - ProjectSettings.add_property_info({ - "name": setting_name, - "type": TYPE_STRING, - "hint": PROPERTY_HINT_FILE, - }) + ProjectSettings.set_setting(setting_name, setting_config.value) + ProjectSettings.set_initial_value(setting_name, setting_config.value) + ProjectSettings.add_property_info({ + "name" = setting_name, + "type" = setting_config.type, + "hint" = setting_config.get("hint", PROPERTY_HINT_NONE), + "hint_string" = setting_config.get("hint_string", "") + }) + ProjectSettings.set_as_basic(setting_name, not setting_config.has("is_advanced")) + ProjectSettings.set_as_internal(setting_name, setting_config.has("is_hidden")) - # Some settings shouldn't be edited directly in the Project Settings window - ProjectSettings.set_as_internal("dialogue_manager/general/states", true) - ProjectSettings.set_as_internal("dialogue_manager/general/custom_test_scene_path", true) - ProjectSettings.set_as_internal("dialogue_manager/general/uses_dotnet", true) - - ProjectSettings.save() + if should_save_settings: + ProjectSettings.save() static func set_setting(key: String, value) -> void: - ProjectSettings.set_setting("dialogue_manager/general/%s" % key, value) - ProjectSettings.set_initial_value("dialogue_manager/general/%s" % key, DEFAULT_SETTINGS[key]) - ProjectSettings.save() + if get_setting(key, value) != value: + ProjectSettings.set_setting("dialogue_manager/%s" % key, value) + ProjectSettings.set_initial_value("dialogue_manager/%s" % key, SETTINGS_CONFIGURATION[key].value) + ProjectSettings.save() static func get_setting(key: String, default): - if ProjectSettings.has_setting("dialogue_manager/general/%s" % key): - return ProjectSettings.get_setting("dialogue_manager/general/%s" % key) + if ProjectSettings.has_setting("dialogue_manager/%s" % key): + return ProjectSettings.get_setting("dialogue_manager/%s" % key) else: return default static func get_settings(only_keys: PackedStringArray = []) -> Dictionary: var settings: Dictionary = {} - for key in DEFAULT_SETTINGS.keys(): + for key in SETTINGS_CONFIGURATION.keys(): if only_keys.is_empty() or key in only_keys: - settings[key] = get_setting(key, DEFAULT_SETTINGS[key]) + settings[key] = get_setting(key, SETTINGS_CONFIGURATION[key].value) return settings -### User config +#endregion + +#region User static func get_user_config() -> Dictionary: @@ -102,15 +211,15 @@ static func get_user_config() -> Dictionary: open_in_external_editor = false } - if FileAccess.file_exists(DialogueConstants.USER_CONFIG_PATH): - var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.READ) + if FileAccess.file_exists(DMConstants.USER_CONFIG_PATH): + var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.READ) user_config.merge(JSON.parse_string(file.get_as_text()), true) return user_config static func save_user_config(user_config: Dictionary) -> void: - var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.WRITE) + var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.WRITE) file.store_string(JSON.stringify(user_config)) @@ -181,7 +290,10 @@ static func check_for_dotnet_solution() -> bool: var directory: String = ProjectSettings.get("dotnet/project/solution_directory") var file_name: String = ProjectSettings.get("dotnet/project/assembly_name") has_dotnet_solution = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) - set_setting("uses_dotnet", has_dotnet_solution) + set_setting(DMSettings.USES_DOTNET, has_dotnet_solution) return has_dotnet_solution - return get_setting("uses_dotnet", false) + return get_setting(DMSettings.USES_DOTNET, false) + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd.uid index 02a39a88..c93da98d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd.uid @@ -1 +1 @@ -uid://cjvhwnu0y1fjw +uid://ce1nk88365m52 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd index a8089114..e6216672 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd @@ -10,23 +10,28 @@ const DialogueResource = preload("./dialogue_resource.gd") func _ready(): - var screen_index: int = DisplayServer.get_primary_screen() - DisplayServer.window_set_position(Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - DisplayServer.window_get_size()) * 0.5) - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + if not Engine.is_embedded_in_editor: + var window: Window = get_viewport() + var screen_index: int = DisplayServer.get_primary_screen() + window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5 + window.mode = Window.MODE_WINDOWED # Normally you can just call DialogueManager directly but doing so before the plugin has been # enabled in settings will throw a compiler error here so I'm using `get_singleton` instead. var dialogue_manager = Engine.get_singleton("DialogueManager") dialogue_manager.dialogue_ended.connect(_on_dialogue_ended) - dialogue_manager.show_dialogue_balloon(resource, title) + dialogue_manager.show_dialogue_balloon(resource, title if not title.is_empty() else resource.first_title) func _enter_tree() -> void: DialogueSettings.set_user_value("is_running_test_scene", false) -### Signals +#region Signals func _on_dialogue_ended(_resource: DialogueResource): get_tree().quit() + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd.uid index 41092b09..1bee7a14 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.gd.uid @@ -1 +1 @@ -uid://bursc6hnxrrwh +uid://c8e16qdgu40wo diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.tscn index ba86f10c..f0786bad 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/test_scene.tscn @@ -1,6 +1,6 @@ -[gd_scene load_steps=2 format=3 uid="uid://v821vseta20u"] +[gd_scene load_steps=2 format=3 uid="uid://ugd552efvil0"] -[ext_resource type="Script" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"] +[ext_resource type="Script" uid="uid://c8e16qdgu40wo" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"] [node name="TestScene" type="Node2D"] script = ExtResource("1_yupoh") diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd index 56640e75..1f8f8ba3 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd @@ -7,6 +7,7 @@ const SUPPORTED_BUILTIN_TYPES = [ TYPE_STRING, TYPE_STRING_NAME, TYPE_ARRAY, + TYPE_PACKED_STRING_ARRAY, TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4, @@ -21,13 +22,37 @@ const SUPPORTED_BUILTIN_TYPES = [ static var resolve_method_error: Error = OK -static func is_supported(thing) -> bool: - return typeof(thing) in SUPPORTED_BUILTIN_TYPES +static func is_supported(thing, with_method: String = "") -> bool: + if not typeof(thing) in SUPPORTED_BUILTIN_TYPES: return false + + # If given a Dictionary and a method then make sure it's a known Dictionary method. + if typeof(thing) == TYPE_DICTIONARY and with_method != "": + return with_method in [ + &"clear", + &"duplicate", + &"erase", + &"find_key", + &"get", + &"get_or_add", + &"has", + &"has_all", + &"hash", + &"is_empty", + &"is_read_only", + &"keys", + &"make_read_only", + &"merge", + &"merged", + &"recursive_equal", + &"size", + &"values"] + + return true static func resolve_property(builtin, property: String): match typeof(builtin): - TYPE_ARRAY, TYPE_DICTIONARY, TYPE_QUATERNION, TYPE_STRING, TYPE_STRING_NAME: + TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY, TYPE_DICTIONARY, TYPE_QUATERNION, TYPE_STRING, TYPE_STRING_NAME: return builtin[property] # Some types have constants that we need to manually resolve @@ -406,6 +431,15 @@ static func resolve_vector2_property(vector: Vector2, property: String): "DOWN": return Vector2.DOWN + "DOWN_LEFT": + return Vector2(-1, 1) + "DOWN_RIGHT": + return Vector2(1, 1) + "UP_LEFT": + return Vector2(-1, -1) + "UP_RIGHT": + return Vector2(1, -1) + return vector[property] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd.uid index d70ccf83..af8698cd 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/builtins.gd.uid @@ -1 +1 @@ -uid://baiufk0ysfoj3 +uid://bnfhuubdv5k20 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd similarity index 79% rename from Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd rename to Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd index 0a309356..dd1da441 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/dialogue_cache.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd @@ -1,9 +1,4 @@ -extends Node - - -const DialogueConstants = preload("../constants.gd") -const DialogueSettings = preload("../settings.gd") -const DialogueManagerParseResult = preload("./parse_result.gd") +class_name DMCache extends Node signal file_content_changed(path: String, new_content: String) @@ -22,6 +17,8 @@ var _cache: Dictionary = {} var _update_dependency_timer: Timer = Timer.new() var _update_dependency_paths: PackedStringArray = [] +var _files_marked_for_reimport: PackedStringArray = [] + func _ready() -> void: add_child(_update_dependency_timer) @@ -30,34 +27,36 @@ func _ready() -> void: _build_cache() -func reimport_files(files: PackedStringArray = []) -> void: - if files.is_empty(): files = get_files() - - var file_system: EditorFileSystem = Engine.get_meta("DialogueManagerPlugin") \ - .get_editor_interface() \ - .get_resource_filesystem() - - # NOTE: Godot 4.2rc1 has an issue with reimporting more than one - # file at a time so we do them one by one +func mark_files_for_reimport(files: PackedStringArray) -> void: for file in files: - file_system.reimport_files([file]) - await get_tree().create_timer(0.2) + if not _files_marked_for_reimport.has(file): + _files_marked_for_reimport.append(file) + + +func reimport_files(and_files: PackedStringArray = []) -> void: + for file in and_files: + if not _files_marked_for_reimport.has(file): + _files_marked_for_reimport.append(file) + + if _files_marked_for_reimport.is_empty(): return + + EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) ## Add a dialogue file to the cache. -func add_file(path: String, parse_results: DialogueManagerParseResult = null) -> void: +func add_file(path: String, compile_result: DMCompilerResult = null) -> void: _cache[path] = { path = path, dependencies = [], errors = [] } - if parse_results != null: - _cache[path].dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path) - _cache[path].parsed_at = Time.get_ticks_msec() + if compile_result != null: + _cache[path].dependencies = Array(compile_result.imported_paths).filter(func(d): return d != path) + _cache[path].compiled_at = Time.get_ticks_msec() # If this is a fresh cache entry, check for dependencies - if parse_results == null and not _update_dependency_paths.has(path): + if compile_result == null and not _update_dependency_paths.has(path): queue_updating_dependencies(path) @@ -118,7 +117,7 @@ func get_files_with_dependency(imported_path: String) -> Array: ## Get any paths that are dependent on a given path func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray: return get_files_with_dependency(on_path) \ - .filter(func(d): return Time.get_ticks_msec() - d.get("parsed_at", 0) > 3000) \ + .filter(func(d): return Time.get_ticks_msec() - d.get("compiled_at", 0) > 3000) \ .map(func(d): return d.path) @@ -149,7 +148,7 @@ func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringA return files -### Signals +#region Signals func _on_update_dependency_timeout() -> void: @@ -166,3 +165,6 @@ func _on_update_dependency_timeout() -> void: dependencies.append(found.strings[found.names.path]) _cache[path].dependencies = dependencies _update_dependency_paths.clear() + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd.uid new file mode 100644 index 00000000..e572006a --- /dev/null +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd.uid @@ -0,0 +1 @@ +uid://d3c83yd6bjp43 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd index 9ef4f44f..b890f6e9 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd @@ -2,13 +2,9 @@ extends Control -const DialogueConstants = preload("../constants.gd") -const DialogueSettings = preload("../settings.gd") -const DialogueResource = preload("../dialogue_resource.gd") -const DialogueManagerParser = preload("../components/parser.gd") - const OPEN_OPEN = 100 -const OPEN_CLEAR = 101 +const OPEN_QUICK = 101 +const OPEN_CLEAR = 102 const TRANSLATIONS_GENERATE_LINE_IDS = 100 const TRANSLATIONS_SAVE_CHARACTERS_TO_CSV = 201 @@ -32,17 +28,17 @@ enum TranslationSource { signal confirmation_closed() -@onready var parse_timer := $ParseTimer +@onready var parse_timer: Timer = $ParseTimer # Dialogs @onready var new_dialog: FileDialog = $NewDialog @onready var save_dialog: FileDialog = $SaveDialog @onready var open_dialog: FileDialog = $OpenDialog +@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog +@onready var quick_open_files_list: VBoxContainer = $QuickOpenDialog/QuickOpenFilesList @onready var export_dialog: FileDialog = $ExportDialog @onready var import_dialog: FileDialog = $ImportDialog @onready var errors_dialog: AcceptDialog = $ErrorsDialog -@onready var settings_dialog: AcceptDialog = $SettingsDialog -@onready var settings_view := $SettingsDialog/SettingsView @onready var build_error_dialog: AcceptDialog = $BuildErrorDialog @onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog @onready var updated_dialog: AcceptDialog = $UpdatedDialog @@ -55,10 +51,10 @@ signal confirmation_closed() @onready var save_all_button: Button = %SaveAllButton @onready var find_in_files_button: Button = %FindInFilesButton @onready var test_button: Button = %TestButton +@onready var test_line_button: Button = %TestLineButton @onready var search_button: Button = %SearchButton @onready var insert_button: MenuButton = %InsertButton @onready var translations_button: MenuButton = %TranslationsButton -@onready var settings_button: Button = %SettingsButton @onready var support_button: Button = %SupportButton @onready var docs_button: Button = %DocsButton @onready var version_label: Label = %VersionLabel @@ -71,7 +67,7 @@ signal confirmation_closed() @onready var files_list := %FilesList @onready var files_popup_menu: PopupMenu = %FilesPopupMenu @onready var title_list := %TitleList -@onready var code_edit := %CodeEdit +@onready var code_edit: DMCodeEdit = %CodeEdit @onready var errors_panel := %ErrorsPanel # The currently open file @@ -79,9 +75,10 @@ var current_file_path: String = "": set(next_current_file_path): current_file_path = next_current_file_path files_list.current_file_path = current_file_path - if current_file_path == "": + if current_file_path == "" or not open_buffers.has(current_file_path): save_all_button.disabled = true test_button.disabled = true + test_line_button.disabled = true search_button.disabled = true insert_button.disabled = true translations_button.disabled = true @@ -92,6 +89,7 @@ var current_file_path: String = "": errors_panel.hide() else: test_button.disabled = false + test_line_button.disabled = false search_button.disabled = false insert_button.disabled = false translations_button.disabled = false @@ -103,7 +101,7 @@ var current_file_path: String = "": code_edit.text = open_buffers[current_file_path].text code_edit.errors = [] code_edit.clear_undo_history() - code_edit.set_cursor(DialogueSettings.get_caret(current_file_path)) + code_edit.set_cursor(DMSettings.get_caret(current_file_path)) code_edit.grab_focus() _on_code_edit_text_changed() @@ -134,16 +132,16 @@ func _ready() -> void: version_label.text = "v%s" % plugin.get_version() update_button.on_before_refresh = func on_before_refresh(): # Save everything - DialogueSettings.set_user_value("just_refreshed", { + DMSettings.set_user_value("just_refreshed", { current_file_path = current_file_path, open_buffers = open_buffers }) return true # Did we just load from an addon version refresh? - var just_refreshed = DialogueSettings.get_user_value("just_refreshed", null) + var just_refreshed = DMSettings.get_user_value("just_refreshed", null) if just_refreshed != null: - DialogueSettings.set_user_value("just_refreshed", null) + DMSettings.set_user_value("just_refreshed", null) call_deferred("load_from_version_refresh", just_refreshed) # Hook up the search toolbar @@ -154,35 +152,35 @@ func _ready() -> void: translations_button.get_popup().id_pressed.connect(_on_translations_button_menu_id_pressed) code_edit.main_view = self - code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE - var editor_settings: EditorSettings = plugin.get_editor_interface().get_editor_settings() + code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DMSettings.get_setting(DMSettings.WRAP_LONG_LINES, false) else TextEdit.LINE_WRAPPING_NONE + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() editor_settings.settings_changed.connect(_on_editor_settings_changed) _on_editor_settings_changed() # Reopen any files that were open when Godot was closed if editor_settings.get_setting("text_editor/behavior/files/restore_scripts_on_load"): - var reopen_files: Array = DialogueSettings.get_user_value("reopen_files", []) + var reopen_files: Array = DMSettings.get_user_value("reopen_files", []) for reopen_file in reopen_files: open_file(reopen_file) - self.current_file_path = DialogueSettings.get_user_value("most_recent_reopen_file", "") + self.current_file_path = DMSettings.get_user_value("most_recent_reopen_file", "") save_all_button.disabled = true - close_confirmation_dialog.ok_button_text = DialogueConstants.translate(&"confirm_close.save") - close_confirmation_dialog.add_button(DialogueConstants.translate(&"confirm_close.discard"), true, "discard") + close_confirmation_dialog.ok_button_text = DMConstants.translate(&"confirm_close.save") + close_confirmation_dialog.add_button(DMConstants.translate(&"confirm_close.discard"), true, "discard") - errors_dialog.dialog_text = DialogueConstants.translate(&"errors_in_script") + errors_dialog.dialog_text = DMConstants.translate(&"errors_in_script") # Update the buffer if a file was modified externally (retains undo step) - Engine.get_meta("DialogueCache").file_content_changed.connect(_on_cache_file_content_changed) + Engine.get_meta("DMCache").file_content_changed.connect(_on_cache_file_content_changed) - plugin.get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved) + EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) func _exit_tree() -> void: - DialogueSettings.set_user_value("reopen_files", open_buffers.keys()) - DialogueSettings.set_user_value("most_recent_reopen_file", self.current_file_path) + DMSettings.set_user_value("reopen_files", open_buffers.keys()) + DMSettings.set_user_value("most_recent_reopen_file", self.current_file_path) func _unhandled_input(event: InputEvent) -> void: @@ -222,13 +220,12 @@ func load_from_version_refresh(just_refreshed: Dictionary) -> void: else: open_buffers = just_refreshed.open_buffers - var interface: EditorInterface = plugin.get_editor_interface() if just_refreshed.current_file_path != "": - interface.edit_resource(load(just_refreshed.current_file_path)) + EditorInterface.edit_resource(load(just_refreshed.current_file_path)) else: - interface.set_main_screen_editor("Dialogue") + EditorInterface.set_main_screen_editor("Dialogue") - updated_dialog.dialog_text = DialogueConstants.translate(&"update.success").format({ version = update_button.get_version() }) + updated_dialog.dialog_text = DMConstants.translate(&"update.success").format({ version = update_button.get_version() }) updated_dialog.popup_centered() @@ -238,26 +235,11 @@ func new_file(path: String, content: String = "") -> void: var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) if content == "": - if DialogueSettings.get_setting("new_with_template", true): - file.store_string("\n".join([ - "~ this_is_a_node_title", - "", - "Nathan: [[Hi|Hello|Howdy]], this is some dialogue.", - "Nathan: Here are some choices.", - "- First one", - "\tNathan: You picked the first one.", - "- Second one", - "\tNathan: You picked the second one.", - "- Start again => this_is_a_node_title", - "- End the conversation => END", - "Nathan: For more information see the online documentation.", - "", - "=> END" - ])) + file.store_string(DMSettings.get_setting(DMSettings.NEW_FILE_TEMPLATE, "")) else: file.store_string(content) - plugin.get_editor_interface().get_resource_filesystem().scan() + EditorInterface.get_resource_filesystem().scan() # Open a dialogue resource for editing @@ -266,6 +248,8 @@ func open_resource(resource: DialogueResource) -> void: func open_file(path: String) -> void: + if not FileAccess.file_exists(path): return + if not open_buffers.has(path): var file: FileAccess = FileAccess.open(path, FileAccess.READ) var text = file.get_as_text() @@ -276,7 +260,7 @@ func open_file(path: String) -> void: pristine_text = text } - DialogueSettings.add_recent_file(path) + DMSettings.add_recent_file(path) build_open_menu() files_list.files = open_buffers.keys() @@ -286,11 +270,7 @@ func open_file(path: String) -> void: func show_file_in_filesystem(path: String) -> void: - var file_system_dock: FileSystemDock = plugin \ - .get_editor_interface() \ - .get_file_system_dock() - - file_system_dock.navigate_to_path(path) + EditorInterface.get_file_system_dock().navigate_to_path(path) # Save any open files @@ -304,7 +284,7 @@ func save_files() -> void: save_file(path, false) if saved_files.size() > 0: - Engine.get_meta("DialogueCache").reimport_files(saved_files) + Engine.get_meta("DMCache").mark_files_for_reimport(saved_files) # Save a file @@ -326,10 +306,7 @@ func save_file(path: String, rescan_file_system: bool = true) -> void: file.close() if rescan_file_system: - plugin \ - .get_editor_interface() \ - .get_resource_filesystem()\ - .scan() + EditorInterface.get_resource_filesystem().scan() func close_file(path: String) -> void: @@ -341,7 +318,7 @@ func close_file(path: String) -> void: remove_file_from_open_buffers(path) await get_tree().process_frame else: - close_confirmation_dialog.dialog_text = DialogueConstants.translate(&"confirm_close").format({ path = path.get_file() }) + close_confirmation_dialog.dialog_text = DMConstants.translate(&"confirm_close").format({ path = path.get_file() }) close_confirmation_dialog.popup_centered() await confirmation_closed @@ -364,8 +341,8 @@ func remove_file_from_open_buffers(path: String) -> void: # Apply theme colors and icons to the UI func apply_theme() -> void: if is_instance_valid(plugin) and is_instance_valid(code_edit): - var scale: float = plugin.get_editor_interface().get_editor_scale() - var editor_settings = plugin.get_editor_interface().get_editor_settings() + var scale: float = EditorInterface.get_editor_scale() + var editor_settings = EditorInterface.get_editor_settings() code_edit.theme_overrides = { scale = scale, @@ -391,77 +368,77 @@ func apply_theme() -> void: } new_button.icon = get_theme_icon("New", "EditorIcons") - new_button.tooltip_text = DialogueConstants.translate(&"start_a_new_file") + new_button.tooltip_text = DMConstants.translate(&"start_a_new_file") open_button.icon = get_theme_icon("Load", "EditorIcons") - open_button.tooltip_text = DialogueConstants.translate(&"open_a_file") + open_button.tooltip_text = DMConstants.translate(&"open_a_file") save_all_button.icon = get_theme_icon("Save", "EditorIcons") - save_all_button.tooltip_text = DialogueConstants.translate(&"start_all_files") + save_all_button.text = DMConstants.translate(&"all") + save_all_button.tooltip_text = DMConstants.translate(&"start_all_files") find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons") - find_in_files_button.tooltip_text = DialogueConstants.translate(&"find_in_files") + find_in_files_button.tooltip_text = DMConstants.translate(&"find_in_files") - test_button.icon = get_theme_icon("PlayScene", "EditorIcons") - test_button.tooltip_text = DialogueConstants.translate(&"test_dialogue") + test_button.icon = get_theme_icon("DebugNext", "EditorIcons") + test_button.tooltip_text = DMConstants.translate(&"test_dialogue") + + test_line_button.icon = get_theme_icon("DebugStep", "EditorIcons") + test_line_button.tooltip_text = DMConstants.translate(&"test_dialogue_from_line") search_button.icon = get_theme_icon("Search", "EditorIcons") - search_button.tooltip_text = DialogueConstants.translate(&"search_for_text") + search_button.tooltip_text = DMConstants.translate(&"search_for_text") insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons") - insert_button.text = DialogueConstants.translate(&"insert") + insert_button.text = DMConstants.translate(&"insert") translations_button.icon = get_theme_icon("Translation", "EditorIcons") - translations_button.text = DialogueConstants.translate(&"translations") - - settings_button.icon = get_theme_icon("Tools", "EditorIcons") - settings_button.tooltip_text = DialogueConstants.translate(&"settings") + translations_button.text = DMConstants.translate(&"translations") support_button.icon = get_theme_icon("Heart", "EditorIcons") - support_button.text = DialogueConstants.translate(&"sponsor") - support_button.tooltip_text = DialogueConstants.translate(&"show_support") + support_button.text = DMConstants.translate(&"sponsor") + support_button.tooltip_text = DMConstants.translate(&"show_support") docs_button.icon = get_theme_icon("Help", "EditorIcons") - docs_button.text = DialogueConstants.translate(&"docs") + docs_button.text = DMConstants.translate(&"docs") update_button.apply_theme() # Set up the effect menu var popup: PopupMenu = insert_button.get_popup() popup.clear() - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.wave_bbcode"), 0) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.shake_bbcode"), 1) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.wave_bbcode"), 0) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.shake_bbcode"), 1) popup.add_separator() - popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate(&"insert.typing_pause"), 3) - popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate(&"insert.typing_speed_change"), 4) - popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate(&"insert.auto_advance"), 5) - popup.add_separator(DialogueConstants.translate(&"insert.templates")) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.title"), 6) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.dialogue"), 7) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.response"), 8) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_lines"), 9) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_text"), 10) - popup.add_separator(DialogueConstants.translate(&"insert.actions")) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.jump"), 11) - popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.end_dialogue"), 12) + popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DMConstants.translate(&"insert.typing_pause"), 3) + popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DMConstants.translate(&"insert.typing_speed_change"), 4) + popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DMConstants.translate(&"insert.auto_advance"), 5) + popup.add_separator(DMConstants.translate(&"insert.templates")) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.title"), 6) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.dialogue"), 7) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.response"), 8) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.random_lines"), 9) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.random_text"), 10) + popup.add_separator(DMConstants.translate(&"insert.actions")) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.jump"), 11) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.end_dialogue"), 12) # Set up the translations menu popup = translations_button.get_popup() popup.clear() - popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS) + popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DMConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS) popup.add_separator() - popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV) - popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV) - popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV) + popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DMConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV) + popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DMConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV) + popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DMConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV) # Dialog sizes new_dialog.min_size = Vector2(600, 500) * scale save_dialog.min_size = Vector2(600, 500) * scale open_dialog.min_size = Vector2(600, 500) * scale + quick_open_dialog.min_size = Vector2(400, 600) * scale export_dialog.min_size = Vector2(600, 500) * scale import_dialog.min_size = Vector2(600, 500) * scale - settings_dialog.min_size = Vector2(1000, 600) * scale - settings_dialog.max_size = Vector2(1000, 600) * scale find_in_files_dialog.min_size = Vector2(800, 600) * scale @@ -472,12 +449,13 @@ func apply_theme() -> void: func build_open_menu() -> void: var menu = open_button.get_popup() menu.clear() - menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate(&"open.open"), OPEN_OPEN) + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DMConstants.translate(&"open.open"), OPEN_OPEN) + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DMConstants.translate(&"open.quick_open"), OPEN_QUICK) menu.add_separator() - var recent_files = DialogueSettings.get_recent_files() + var recent_files = DMSettings.get_recent_files() if recent_files.size() == 0: - menu.add_item(DialogueConstants.translate(&"open.no_recent_files")) + menu.add_item(DMConstants.translate(&"open.no_recent_files")) menu.set_item_disabled(2, true) else: for path in recent_files: @@ -485,7 +463,7 @@ func build_open_menu() -> void: menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path) menu.add_separator() - menu.add_item(DialogueConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR) + menu.add_item(DMConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR) if menu.id_pressed.is_connected(_on_open_menu_id_pressed): menu.id_pressed.disconnect(_on_open_menu_id_pressed) menu.id_pressed.connect(_on_open_menu_id_pressed) @@ -494,25 +472,22 @@ func build_open_menu() -> void: # Get the last place a CSV, etc was exported func get_last_export_path(extension: String) -> String: var filename = current_file_path.get_file().replace(".dialogue", "." + extension) - return DialogueSettings.get_user_value("last_export_path", current_file_path.get_base_dir()) + "/" + filename + return DMSettings.get_user_value("last_export_path", current_file_path.get_base_dir()) + "/" + filename # Check the current text for errors -func parse() -> void: +func compile() -> void: # Skip if nothing to parse if current_file_path == "": return - var parser = DialogueManagerParser.new() - var errors: Array[Dictionary] = [] - if parser.parse(code_edit.text, current_file_path) != OK: - errors = parser.get_errors() - code_edit.errors = errors - errors_panel.errors = errors - parser.free() + var result: DMCompilerResult = DMCompiler.compile_string(code_edit.text, current_file_path) + code_edit.errors = result.errors + errors_panel.errors = result.errors + title_list.titles = code_edit.get_titles() func show_build_error_dialog() -> void: - build_error_dialog.dialog_text = DialogueConstants.translate(&"errors_with_build") + build_error_dialog.dialog_text = DMConstants.translate(&"errors_with_build") build_error_dialog.popup_centered() @@ -521,8 +496,6 @@ func generate_translations_keys() -> void: randomize() seed(Time.get_unix_time_from_system()) - var parser = DialogueManagerParser.new() - var cursor: Vector2 = code_edit.get_cursor() var lines: PackedStringArray = code_edit.text.split("\n") @@ -538,7 +511,7 @@ func generate_translations_keys() -> void: var text = "" var l = line.replace(found.strings[0], "").strip_edges().strip_edges() if l.begins_with("- "): - text = parser.extract_response_prompt(l) + text = DMCompiler.extract_translatable_string(l) elif ":" in l: text = l.split(":")[1] else: @@ -550,34 +523,43 @@ func generate_translations_keys() -> void: var line = lines[i] var l = line.strip_edges() - if parser.is_line_empty(l): continue - if parser.is_condition_line(l, true): continue - if parser.is_title_line(l): continue - if parser.is_mutation_line(l): continue - if parser.is_goto_line(l): continue - if parser.is_import_line(l): continue + if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue if "[ID:" in line: continue - var key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10) - while key in known_keys: - key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10) - var text = "" if l.begins_with("- "): - text = parser.extract_response_prompt(l) + text = DMCompiler.extract_translatable_string(l) else: text = l.substr(l.find(":") + 1) - lines[i] = line.replace(text, text + " [ID:%s]" % key) + var key: String = "" + if known_keys.values().has(text): + key = known_keys.find_key(text) + else: + var regex: DMCompilerRegEx = DMCompilerRegEx.new() + key = regex.ALPHA_NUMERIC.sub(text.strip_edges(), "_", true).substr(0, 30) + if key.begins_with("_"): + key = key.substr(1) + if key.ends_with("_"): + key = key.substr(0, key.length() - 1) + + # Make sure key is unique + var hashed_key: String = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) + while hashed_key in known_keys and text != known_keys.get(hashed_key): + hashed_key = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) + key = hashed_key.to_upper() + + line = line.replace("\\n", "!NEWLINE!") + text = text.replace("\n", "!NEWLINE!") + lines[i] = line.replace(text, text + " [ID:%s]" % [key]).replace("!NEWLINE!", "\\n") + known_keys[key] = text code_edit.text = "\n".join(lines) code_edit.set_cursor(cursor) _on_code_edit_text_changed() - parser.free() - # Add a translation file to the project settings func add_path_to_project_translations(path: String) -> void: @@ -589,7 +571,7 @@ func add_path_to_project_translations(path: String) -> void: # Export dialogue and responses to CSV func export_translations_to_csv(path: String) -> void: - var default_locale: String = DialogueSettings.get_setting("default_csv_locale", "en") + var default_locale: String = DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en") var file: FileAccess @@ -621,13 +603,13 @@ func export_translations_to_csv(path: String) -> void: existing_csv[line[0]] = line # The character column wasn't found in the existing file but the setting is turned on - if character_column == -1 and DialogueSettings.get_setting("include_character_in_translation_exports", false): + if character_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): character_column = column_count column_count += 1 existing_csv["keys"].append("_character") # The notes column wasn't found in the existing file but the setting is turned on - if notes_column == -1 and DialogueSettings.get_setting("include_notes_in_translation_exports", false): + if notes_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): notes_column = column_count column_count += 1 existing_csv["keys"].append("_notes") @@ -636,11 +618,11 @@ func export_translations_to_csv(path: String) -> void: file = FileAccess.open(path, FileAccess.WRITE) if not FileAccess.file_exists(path): - var headings: PackedStringArray = ["keys", default_locale] - if DialogueSettings.get_setting("include_character_in_translation_exports", false): + var headings: PackedStringArray = ["keys", default_locale] + DMSettings.get_setting(DMSettings.EXTRA_CSV_LOCALES, []) + if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): character_column = headings.size() headings.append("_character") - if DialogueSettings.get_setting("include_notes_in_translation_exports", false): + if DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): notes_column = headings.size() headings.append("_notes") file.store_csv_line(headings) @@ -649,30 +631,33 @@ func export_translations_to_csv(path: String) -> void: # Write our translations to file var known_keys: PackedStringArray = [] - var dialogue: Dictionary = DialogueManagerParser.parse_string(code_edit.text, current_file_path).lines + var dialogue = DMCompiler.compile_string(code_edit.text, current_file_path).lines # Make a list of stuff that needs to go into the file var lines_to_save = [] for key in dialogue.keys(): var line: Dictionary = dialogue.get(key) - if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue - if line.translation_key in known_keys: continue + if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue - known_keys.append(line.translation_key) + var translation_key: String = line.get(&"translation_key", line.text) + + if translation_key in known_keys: continue + + known_keys.append(translation_key) var line_to_save: PackedStringArray = [] - if existing_csv.has(line.translation_key): - line_to_save = existing_csv.get(line.translation_key) + if existing_csv.has(translation_key): + line_to_save = existing_csv.get(translation_key) line_to_save.resize(column_count) - existing_csv.erase(line.translation_key) + existing_csv.erase(translation_key) else: line_to_save.resize(column_count) - line_to_save[0] = line.translation_key + line_to_save[0] = translation_key line_to_save[default_locale_column] = line.text if character_column > -1: - line_to_save[character_column] = "(response)" if line.type == DialogueConstants.TYPE_RESPONSE else line.character + line_to_save[character_column] = "(response)" if line.type == DMConstants.TYPE_RESPONSE else line.character if notes_column > -1: line_to_save[notes_column] = line.notes @@ -686,8 +671,8 @@ func export_translations_to_csv(path: String) -> void: file.close() - plugin.get_editor_interface().get_resource_filesystem().scan() - plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path) + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", path) # Add it to the project l10n settings if it's not already there var language_code: RegExMatch = RegEx.create_from_string("^[a-z]{2,3}").search(default_locale) @@ -719,12 +704,12 @@ func export_character_names_to_csv(path: String) -> void: file = FileAccess.open(path, FileAccess.WRITE) if not file.file_exists(path): - file.store_csv_line(["keys", DialogueSettings.get_setting("default_csv_locale", "en")]) + file.store_csv_line(["keys", DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en")]) # Write our translations to file var known_keys: PackedStringArray = [] - var character_names: PackedStringArray = DialogueManagerParser.parse_string(code_edit.text, current_file_path).character_names + var character_names: PackedStringArray = DMCompiler.compile_string(code_edit.text, current_file_path).character_names # Make a list of stuff that needs to go into the file var lines_to_save = [] @@ -749,8 +734,8 @@ func export_character_names_to_csv(path: String) -> void: file.close() - plugin.get_editor_interface().get_resource_filesystem().scan() - plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path) + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", path) # Add it to the project l10n settings if it's not already there var translation_path: String = path.replace(".csv", ".en.translation") @@ -772,17 +757,15 @@ func import_translations_from_csv(path: String) -> void: if csv_line.size() > 1: keys[csv_line[0]] = csv_line[1] - var parser: DialogueManagerParser = DialogueManagerParser.new() - # Now look over each line in the dialogue and replace the content for matched keys var lines: PackedStringArray = code_edit.text.split("\n") var start_index: int = 0 var end_index: int = 0 for i in range(0, lines.size()): var line: String = lines[i] - var translation_key: String = parser.extract_translation(line) + var translation_key: String = DMCompiler.get_static_line_id(line) if keys.has(translation_key): - if parser.is_dialogue_line(line): + if DMCompiler.get_line_type(line) == DMConstants.TYPE_DIALOGUE: start_index = 0 # See if we need to skip over a character name line = line.replace("\\:", "!ESCAPED_COLON!") @@ -790,7 +773,7 @@ func import_translations_from_csv(path: String) -> void: start_index = line.find(": ") + 2 lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]").replace("!ESCAPED_COLON!", ":") - elif parser.is_response_line(line): + elif DMCompiler.get_line_type(line) == DMConstants.TYPE_RESPONSE: start_index = line.find("- ") + 2 # See if we need to skip over a character name line = line.replace("\\:", "!ESCAPED_COLON!") @@ -806,8 +789,6 @@ func import_translations_from_csv(path: String) -> void: code_edit.text = "\n".join(lines) code_edit.set_cursor(cursor) - parser.free() - func show_search_form(is_enabled: bool) -> void: if code_edit.last_selected_text: @@ -831,14 +812,15 @@ func _on_files_moved(old_file: String, new_file: String) -> void: func _on_cache_file_content_changed(path: String, new_content: String) -> void: if open_buffers.has(path): var buffer = open_buffers[path] - if buffer.text != new_content: + if buffer.text == buffer.pristine_text and buffer.text != new_content: buffer.text = new_content - buffer.pristine_text = new_content code_edit.text = new_content + title_list.titles = code_edit.get_titles() + buffer.pristine_text = new_content func _on_editor_settings_changed() -> void: - var editor_settings: EditorSettings = plugin.get_editor_interface().get_editor_settings() + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() code_edit.minimap_draw = editor_settings.get_setting("text_editor/appearance/minimap/show_minimap") code_edit.minimap_width = editor_settings.get_setting("text_editor/appearance/minimap/minimap_width") code_edit.scroll_smooth = editor_settings.get_setting("text_editor/behavior/navigation/smooth_scrolling") @@ -848,8 +830,12 @@ func _on_open_menu_id_pressed(id: int) -> void: match id: OPEN_OPEN: open_dialog.popup_centered() + OPEN_QUICK: + quick_open_files_list.files = Engine.get_meta("DMCache").get_files() + quick_open_dialog.popup_centered() + quick_open_files_list.focus_filter() OPEN_CLEAR: - DialogueSettings.clear_recent_files() + DMSettings.clear_recent_files() build_open_menu() _: var menu = open_button.get_popup() @@ -912,7 +898,7 @@ func _on_translations_button_menu_id_pressed(id: int) -> void: func _on_export_dialog_file_selected(path: String) -> void: - DialogueSettings.set_user_value("last_export_path", path.get_base_dir()) + DMSettings.set_user_value("last_export_path", path.get_base_dir()) match path.get_extension(): "csv": match translation_source: @@ -923,7 +909,7 @@ func _on_export_dialog_file_selected(path: String) -> void: func _on_import_dialog_file_selected(path: String) -> void: - DialogueSettings.set_user_value("last_export_path", path.get_base_dir()) + DMSettings.set_user_value("last_export_path", path.get_base_dir()) import_translations_from_csv(path) @@ -937,16 +923,25 @@ func _on_main_view_visibility_changed() -> void: func _on_new_button_pressed() -> void: - new_dialog.current_file = "" + new_dialog.current_file = "dialogue" new_dialog.popup_centered() +func _on_new_dialog_confirmed() -> void: + if new_dialog.current_file.get_basename() == "": + var path = "res://untitled.dialogue" + new_file(path) + open_file(path) + + func _on_new_dialog_file_selected(path: String) -> void: new_file(path) open_file(path) func _on_save_dialog_file_selected(path: String) -> void: + if path == "": path = "res://untitled.dialogue" + new_file(path, code_edit.text) open_file(path) @@ -959,6 +954,16 @@ func _on_open_dialog_file_selected(path: String) -> void: open_file(path) +func _on_quick_open_files_list_file_double_clicked(file_path: String) -> void: + quick_open_dialog.hide() + open_file(file_path) + + +func _on_quick_open_dialog_confirmed() -> void: + if quick_open_files_list.current_file_path: + open_file(quick_open_files_list.current_file_path) + + func _on_save_all_button_pressed() -> void: save_files() @@ -969,8 +974,6 @@ func _on_find_in_files_button_pressed() -> void: func _on_code_edit_text_changed() -> void: - title_list.titles = code_edit.get_titles() - var buffer = open_buffers[current_file_path] buffer.text = code_edit.text @@ -982,11 +985,10 @@ func _on_code_edit_text_changed() -> void: func _on_code_edit_active_title_change(title: String) -> void: title_list.select_title(title) - DialogueSettings.set_user_value("run_title", title) func _on_code_edit_caret_changed() -> void: - DialogueSettings.set_caret(current_file_path, code_edit.get_cursor()) + DMSettings.set_caret(current_file_path, code_edit.get_cursor()) func _on_code_edit_error_clicked(line_number: int) -> void: @@ -1000,11 +1002,11 @@ func _on_title_list_title_selected(title: String) -> void: func _on_parse_timer_timeout() -> void: parse_timer.stop() - parse() + compile() func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> void: - code_edit.set_caret_line(line_number) + code_edit.set_caret_line(line_number - 1) code_edit.set_caret_column(column_number) code_edit.grab_focus() @@ -1023,34 +1025,39 @@ func _on_search_and_replace_close_requested() -> void: code_edit.grab_focus() -func _on_settings_button_pressed() -> void: - settings_view.prepare() - settings_dialog.popup_centered() - - -func _on_settings_view_script_button_pressed(path: String) -> void: - settings_dialog.hide() - plugin.get_editor_interface().edit_resource(load(path)) - - func _on_test_button_pressed() -> void: + save_file(current_file_path, false) + Engine.get_meta("DMCache").reimport_files([current_file_path]) + + if errors_panel.errors.size() > 0: + errors_dialog.popup_centered() + return + + DMSettings.set_user_value("run_title", "") + DMSettings.set_user_value("is_running_test_scene", true) + DMSettings.set_user_value("run_resource_path", current_file_path) + var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") + EditorInterface.play_custom_scene(test_scene_path) + + +func _on_test_line_button_pressed() -> void: save_file(current_file_path) if errors_panel.errors.size() > 0: errors_dialog.popup_centered() return - DialogueSettings.set_user_value("is_running_test_scene", true) - DialogueSettings.set_user_value("run_resource_path", current_file_path) - var test_scene_path: String = DialogueSettings.get_setting("custom_test_scene_path", "res://addons/dialogue_manager/test_scene.tscn") - plugin.get_editor_interface().play_custom_scene(test_scene_path) - - -func _on_settings_dialog_confirmed() -> void: - settings_view.apply_settings_changes() - parse() - code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE - code_edit.grab_focus() + # Find next non-empty line + var line_to_run: int = 0 + for i in range(code_edit.get_cursor().y, code_edit.get_line_count()): + if not code_edit.get_line(i).is_empty(): + line_to_run = i + break; + DMSettings.set_user_value("run_title", str(line_to_run)) + DMSettings.set_user_value("is_running_test_scene", true) + DMSettings.set_user_value("run_resource_path", current_file_path) + var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") + EditorInterface.play_custom_scene(test_scene_path) func _on_support_button_pressed() -> void: @@ -1075,14 +1082,14 @@ func _on_files_popup_menu_about_to_popup() -> void: var shortcuts: Dictionary = plugin.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, 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_item(DMConstants.translate(&"buffer.save"), ITEM_SAVE, OS.find_keycode_from_string(shortcuts.get("save")[0].as_text_keycode())) + files_popup_menu.add_item(DMConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close"), ITEM_CLOSE, OS.find_keycode_from_string(shortcuts.get("close_file")[0].as_text_keycode())) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS) files_popup_menu.add_separator() - files_popup_menu.add_item(DialogueConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH) - files_popup_menu.add_item(DialogueConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM) + files_popup_menu.add_item(DMConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH) + files_popup_menu.add_item(DMConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM) func _on_files_popup_menu_id_pressed(id: int) -> void: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd.uid index 20ad1451..10e66f49 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd.uid +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd.uid @@ -1 +1 @@ -uid://qkrp3bclecwq +uid://cipjcc7bkh1pc diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn index 2bc4f2d9..4e70b263 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn @@ -1,17 +1,16 @@ -[gd_scene load_steps=16 format=3 uid="uid://cbuf1q3xsse3q"] +[gd_scene load_steps=15 format=3 uid="uid://cbuf1q3xsse3q"] -[ext_resource type="Script" path="res://addons/dialogue_manager/views/main_view.gd" id="1_h6qfq"] +[ext_resource type="Script" uid="uid://cipjcc7bkh1pc" path="res://addons/dialogue_manager/views/main_view.gd" id="1_h6qfq"] [ext_resource type="PackedScene" uid="uid://civ6shmka5e8u" path="res://addons/dialogue_manager/components/code_edit.tscn" id="2_f73fm"] [ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="2_npj2k"] [ext_resource type="PackedScene" uid="uid://ctns6ouwwd68i" path="res://addons/dialogue_manager/components/title_list.tscn" id="2_onb4i"] [ext_resource type="PackedScene" uid="uid://co8yl23idiwbi" path="res://addons/dialogue_manager/components/update_button.tscn" id="2_ph3vs"] [ext_resource type="PackedScene" uid="uid://gr8nakpbrhby" path="res://addons/dialogue_manager/components/search_and_replace.tscn" id="6_ylh0t"] [ext_resource type="PackedScene" uid="uid://cs8pwrxr5vxix" path="res://addons/dialogue_manager/components/errors_panel.tscn" id="7_5cvl4"] -[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"] -[ext_resource type="PackedScene" uid="uid://cpg4lg1r3ff6m" path="res://addons/dialogue_manager/views/settings_view.tscn" id="9_8bf36"] +[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"] [ext_resource type="PackedScene" uid="uid://0n7hwviyyly4" path="res://addons/dialogue_manager/components/find_in_files.tscn" id="10_yold3"] -[sub_resource type="Image" id="Image_3ewak"] +[sub_resource type="Image" id="Image_faxki"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -20,10 +19,10 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_wmrmd"] -image = SubResource("Image_3ewak") +[sub_resource type="ImageTexture" id="ImageTexture_ka3gk"] +image = SubResource("Image_faxki") -[sub_resource type="Image" id="Image_jj6l1"] +[sub_resource type="Image" id="Image_y6rqu"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -32,10 +31,10 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_r0npg"] -image = SubResource("Image_jj6l1") +[sub_resource type="ImageTexture" id="ImageTexture_57eek"] +image = SubResource("Image_y6rqu") -[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_4re8k"] +[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_kb7f8"] script = ExtResource("7_necsa") [node name="MainView" type="Control"] @@ -68,6 +67,7 @@ metadata/_edit_layout_mode = 1 unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 +dragger_visibility = 1 [node name="SidePanel" type="VBoxContainer" parent="Margin/Content"] custom_minimum_size = Vector2(150, 0) @@ -89,31 +89,28 @@ layout_mode = 2 tooltip_text = "Open a file" item_count = 9 popup/item_0/text = "Open..." -popup/item_0/icon = SubResource("ImageTexture_wmrmd") +popup/item_0/icon = SubResource("ImageTexture_ka3gk") popup/item_0/id = 100 -popup/item_1/text = "" -popup/item_1/id = -1 -popup/item_1/separator = true -popup/item_2/text = "res://blah.dialogue" -popup/item_2/icon = SubResource("ImageTexture_wmrmd") -popup/item_2/id = 2 +popup/item_1/icon = SubResource("ImageTexture_ka3gk") +popup/item_1/id = 101 +popup/item_2/id = -1 +popup/item_2/separator = true popup/item_3/text = "res://examples/dialogue.dialogue" -popup/item_3/icon = SubResource("ImageTexture_wmrmd") +popup/item_3/icon = SubResource("ImageTexture_ka3gk") popup/item_3/id = 3 popup/item_4/text = "res://examples/dialogue_with_input.dialogue" -popup/item_4/icon = SubResource("ImageTexture_wmrmd") +popup/item_4/icon = SubResource("ImageTexture_ka3gk") popup/item_4/id = 4 popup/item_5/text = "res://examples/dialogue_for_point_n_click.dialogue" -popup/item_5/icon = SubResource("ImageTexture_wmrmd") +popup/item_5/icon = SubResource("ImageTexture_ka3gk") popup/item_5/id = 5 popup/item_6/text = "res://examples/dialogue_for_visual_novel.dialogue" -popup/item_6/icon = SubResource("ImageTexture_wmrmd") +popup/item_6/icon = SubResource("ImageTexture_ka3gk") popup/item_6/id = 6 -popup/item_7/text = "" popup/item_7/id = -1 popup/item_7/separator = true popup/item_8/text = "Clear recent files" -popup/item_8/id = 101 +popup/item_8/id = 102 [node name="SaveAllButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] unique_name_in_owner = true @@ -133,6 +130,7 @@ size_flags_vertical = 3 [node name="FilesList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_npj2k")] unique_name_in_owner = true +visible = false layout_mode = 2 size_flags_vertical = 3 @@ -141,6 +139,7 @@ unique_name_in_owner = true [node name="TitleList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_onb4i")] unique_name_in_owner = true +visible = false layout_mode = 2 [node name="CodePanel" type="VBoxContainer" parent="Margin/Content"] @@ -154,73 +153,72 @@ layout_mode = 2 [node name="InsertButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"] unique_name_in_owner = true layout_mode = 2 +disabled = true text = "Insert" item_count = 15 popup/item_0/text = "Wave BBCode" -popup/item_0/icon = SubResource("ImageTexture_r0npg") -popup/item_0/id = 0 +popup/item_0/icon = SubResource("ImageTexture_57eek") popup/item_1/text = "Shake BBCode" -popup/item_1/icon = SubResource("ImageTexture_r0npg") +popup/item_1/icon = SubResource("ImageTexture_57eek") popup/item_1/id = 1 -popup/item_2/text = "" popup/item_2/id = -1 popup/item_2/separator = true popup/item_3/text = "Typing pause" -popup/item_3/icon = SubResource("ImageTexture_r0npg") +popup/item_3/icon = SubResource("ImageTexture_57eek") popup/item_3/id = 3 popup/item_4/text = "Typing speed change" -popup/item_4/icon = SubResource("ImageTexture_r0npg") +popup/item_4/icon = SubResource("ImageTexture_57eek") popup/item_4/id = 4 popup/item_5/text = "Auto advance" -popup/item_5/icon = SubResource("ImageTexture_r0npg") +popup/item_5/icon = SubResource("ImageTexture_57eek") popup/item_5/id = 5 popup/item_6/text = "Templates" popup/item_6/id = -1 popup/item_6/separator = true popup/item_7/text = "Title" -popup/item_7/icon = SubResource("ImageTexture_r0npg") +popup/item_7/icon = SubResource("ImageTexture_57eek") popup/item_7/id = 6 popup/item_8/text = "Dialogue" -popup/item_8/icon = SubResource("ImageTexture_r0npg") +popup/item_8/icon = SubResource("ImageTexture_57eek") popup/item_8/id = 7 popup/item_9/text = "Response" -popup/item_9/icon = SubResource("ImageTexture_r0npg") +popup/item_9/icon = SubResource("ImageTexture_57eek") popup/item_9/id = 8 popup/item_10/text = "Random lines" -popup/item_10/icon = SubResource("ImageTexture_r0npg") +popup/item_10/icon = SubResource("ImageTexture_57eek") popup/item_10/id = 9 popup/item_11/text = "Random text" -popup/item_11/icon = SubResource("ImageTexture_r0npg") +popup/item_11/icon = SubResource("ImageTexture_57eek") popup/item_11/id = 10 popup/item_12/text = "Actions" popup/item_12/id = -1 popup/item_12/separator = true popup/item_13/text = "Jump to title" -popup/item_13/icon = SubResource("ImageTexture_r0npg") +popup/item_13/icon = SubResource("ImageTexture_57eek") popup/item_13/id = 11 popup/item_14/text = "End dialogue" -popup/item_14/icon = SubResource("ImageTexture_r0npg") +popup/item_14/icon = SubResource("ImageTexture_57eek") popup/item_14/id = 12 [node name="TranslationsButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"] unique_name_in_owner = true layout_mode = 2 +disabled = true text = "Translations" item_count = 5 popup/item_0/text = "Generate line IDs" -popup/item_0/icon = SubResource("ImageTexture_r0npg") +popup/item_0/icon = SubResource("ImageTexture_57eek") popup/item_0/id = 100 -popup/item_1/text = "" popup/item_1/id = -1 popup/item_1/separator = true popup/item_2/text = "Save character names to CSV..." -popup/item_2/icon = SubResource("ImageTexture_r0npg") +popup/item_2/icon = SubResource("ImageTexture_57eek") popup/item_2/id = 201 popup/item_3/text = "Save lines to CSV..." -popup/item_3/icon = SubResource("ImageTexture_r0npg") +popup/item_3/icon = SubResource("ImageTexture_57eek") popup/item_3/id = 202 popup/item_4/text = "Import line changes from CSV..." -popup/item_4/icon = SubResource("ImageTexture_r0npg") +popup/item_4/icon = SubResource("ImageTexture_57eek") popup/item_4/id = 203 [node name="Separator" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] @@ -230,22 +228,25 @@ layout_mode = 2 unique_name_in_owner = true layout_mode = 2 tooltip_text = "Search for text" +disabled = true toggle_mode = true flat = true +[node name="Separator2" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] +layout_mode = 2 + [node name="TestButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] unique_name_in_owner = true layout_mode = 2 tooltip_text = "Test dialogue" +disabled = true flat = true -[node name="Separator3" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] -layout_mode = 2 - -[node name="SettingsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +[node name="TestLineButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] unique_name_in_owner = true layout_mode = 2 -tooltip_text = "Settings" +tooltip_text = "Test dialogue" +disabled = true flat = true [node name="Spacer2" type="Control" parent="Margin/Content/CodePanel/Toolbar"] @@ -272,12 +273,13 @@ flat = true unique_name_in_owner = true modulate = Color(1, 1, 1, 0.490196) layout_mode = 2 -text = "v2.41.3" +text = "v2.42.2" vertical_alignment = 1 [node name="UpdateButton" parent="Margin/Content/CodePanel/Toolbar" instance=ExtResource("2_ph3vs")] unique_name_in_owner = true layout_mode = 2 +text = "v2.44.1 available" [node name="SearchAndReplace" parent="Margin/Content/CodePanel" instance=ExtResource("6_ylh0t")] unique_name_in_owner = true @@ -285,29 +287,32 @@ layout_mode = 2 [node name="CodeEdit" parent="Margin/Content/CodePanel" instance=ExtResource("2_f73fm")] unique_name_in_owner = true +visible = false layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +theme_override_colors/current_line_color = Color(0.266667, 0.278431, 0.352941, 0.243137) theme_override_colors/background_color = Color(0.156863, 0.164706, 0.211765, 1) theme_override_colors/font_color = Color(0.972549, 0.972549, 0.94902, 1) +theme_override_font_sizes/font_size = 14 theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1) -theme_override_colors/current_line_color = Color(0.266667, 0.278431, 0.352941, 0.243137) -theme_override_font_sizes/font_size = 21 -text = "~ this_is_a_node_title +text = "~ start -Nathan: [[Hi|Hello|Howdy]], this is some dialogue. -Nathan: Here are some choices. +Nathan: Hi, I'm Nathan and this is Coco. +Coco: Meow. +Nathan: Here are some response options. - First one Nathan: You picked the first one. - Second one Nathan: You picked the second one. -- Start again => this_is_a_node_title +- Start again => start - End the conversation => END -Nathan: For more information see the online documentation. +Nathan: I hope this example is helpful. +Coco: Meow. => END" scroll_smooth = true -syntax_highlighter = SubResource("SyntaxHighlighter_4re8k") +syntax_highlighter = SubResource("SyntaxHighlighter_kb7f8") [node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")] unique_name_in_owner = true @@ -315,33 +320,41 @@ layout_mode = 2 [node name="NewDialog" type="FileDialog" parent="."] size = Vector2i(900, 750) -min_size = Vector2i(900, 750) +min_size = Vector2i(600, 500) dialog_hide_on_ok = true filters = PackedStringArray("*.dialogue ; Dialogue") [node name="SaveDialog" type="FileDialog" parent="."] size = Vector2i(900, 750) -min_size = Vector2i(900, 750) +min_size = Vector2i(600, 500) dialog_hide_on_ok = true filters = PackedStringArray("*.dialogue ; Dialogue") [node name="OpenDialog" type="FileDialog" parent="."] title = "Open a File" size = Vector2i(900, 750) -min_size = Vector2i(900, 750) +min_size = Vector2i(600, 500) ok_button_text = "Open" dialog_hide_on_ok = true file_mode = 0 filters = PackedStringArray("*.dialogue ; Dialogue") +[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."] +title = "Quick open" +size = Vector2i(600, 900) +min_size = Vector2i(400, 600) +ok_button_text = "Open" + +[node name="QuickOpenFilesList" parent="QuickOpenDialog" instance=ExtResource("2_npj2k")] + [node name="ExportDialog" type="FileDialog" parent="."] size = Vector2i(900, 750) -min_size = Vector2i(900, 750) +min_size = Vector2i(600, 500) [node name="ImportDialog" type="FileDialog" parent="."] title = "Open a File" size = Vector2i(900, 750) -min_size = Vector2i(900, 750) +min_size = Vector2i(600, 500) ok_button_text = "Open" file_mode = 0 filters = PackedStringArray("*.csv ; Translation CSV") @@ -350,21 +363,6 @@ filters = PackedStringArray("*.csv ; Translation CSV") title = "Error" dialog_text = "You have errors in your script. Fix them and then try again." -[node name="SettingsDialog" type="AcceptDialog" parent="."] -title = "Settings" -size = Vector2i(1500, 900) -unresizable = true -min_size = Vector2i(1500, 900) -max_size = Vector2i(1500, 900) -ok_button_text = "Done" - -[node name="SettingsView" parent="SettingsDialog" instance=ExtResource("9_8bf36")] -offset_left = 8.0 -offset_top = 8.0 -offset_right = -8.0 -offset_bottom = -49.0 -current_tab = 0 - [node name="BuildErrorDialog" type="AcceptDialog" parent="."] title = "Errors" dialog_text = "You need to fix dialogue errors before you can run your game." @@ -381,7 +379,7 @@ dialog_text = "You're now up to date!" [node name="FindInFilesDialog" type="AcceptDialog" parent="."] title = "Find in files" size = Vector2i(1200, 900) -min_size = Vector2i(1200, 900) +min_size = Vector2i(800, 600) ok_button_text = "Done" [node name="FindInFiles" parent="FindInFilesDialog" node_paths=PackedStringArray("main_view", "code_edit") instance=ExtResource("10_yold3")] @@ -408,7 +406,7 @@ code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit") [connection signal="title_selected" from="Margin/Content/SidePanel/Bookmarks/TitleList" to="." method="_on_title_list_title_selected"] [connection signal="toggled" from="Margin/Content/CodePanel/Toolbar/SearchButton" to="." method="_on_search_button_toggled"] [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestButton" to="." method="_on_test_button_pressed"] -[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SettingsButton" to="." method="_on_settings_button_pressed"] +[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestLineButton" to="." method="_on_test_line_button_pressed"] [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SupportButton" to="." method="_on_support_button_pressed"] [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/DocsButton" to="." method="_on_docs_button_pressed"] [connection signal="close_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_close_requested"] @@ -419,13 +417,14 @@ code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit") [connection signal="external_file_requested" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_external_file_requested"] [connection signal="text_changed" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_text_changed"] [connection signal="error_pressed" from="Margin/Content/CodePanel/ErrorsPanel" to="." method="_on_errors_panel_error_pressed"] +[connection signal="confirmed" from="NewDialog" to="." method="_on_new_dialog_confirmed"] [connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"] [connection signal="file_selected" from="SaveDialog" to="." method="_on_save_dialog_file_selected"] [connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"] +[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"] +[connection signal="file_double_clicked" from="QuickOpenDialog/QuickOpenFilesList" to="." method="_on_quick_open_files_list_file_double_clicked"] [connection signal="file_selected" from="ExportDialog" to="." method="_on_export_dialog_file_selected"] [connection signal="file_selected" from="ImportDialog" to="." method="_on_import_dialog_file_selected"] -[connection signal="confirmed" from="SettingsDialog" to="." method="_on_settings_dialog_confirmed"] -[connection signal="script_button_pressed" from="SettingsDialog/SettingsView" to="." method="_on_settings_view_script_button_pressed"] [connection signal="confirmed" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_confirmed"] [connection signal="custom_action" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_custom_action"] [connection signal="result_selected" from="FindInFilesDialog/FindInFiles" to="." method="_on_find_in_files_result_selected"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd deleted file mode 100644 index 1a1e748c..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd +++ /dev/null @@ -1,280 +0,0 @@ -@tool -extends TabContainer - - -signal script_button_pressed(path: String) - - -const DialogueConstants = preload("../constants.gd") -const DialogueSettings = preload("../settings.gd") -const BaseDialogueTestScene = preload("../test_scene.gd") - - -enum PathTarget { - CustomTestScene, - Balloon -} - - -# Editor -@onready var new_template_button: CheckBox = $Editor/NewTemplateButton -@onready var characters_translations_button: CheckBox = $Editor/CharactersTranslationsButton -@onready var wrap_lines_button: Button = $Editor/WrapLinesButton -@onready var default_csv_locale: LineEdit = $Editor/DefaultCSVLocale - -# Runtime -@onready var include_all_responses_button: CheckBox = $Runtime/IncludeAllResponsesButton -@onready var ignore_missing_state_values: CheckBox = $Runtime/IgnoreMissingStateValues -@onready var balloon_path_input: LineEdit = $Runtime/CustomBalloon/BalloonPath -@onready var revert_balloon_button: Button = $Runtime/CustomBalloon/RevertBalloonPath -@onready var load_balloon_button: Button = $Runtime/CustomBalloon/LoadBalloonPath -@onready var states_title: Label = $Runtime/StatesTitle -@onready var globals_list: Tree = $Runtime/GlobalsList - -# Advanced -@onready var check_for_updates: CheckBox = $Advanced/CheckForUpdates -@onready var include_characters_in_translations: CheckBox = $Advanced/IncludeCharactersInTranslations -@onready var include_notes_in_translations: CheckBox = $Advanced/IncludeNotesInTranslations -@onready var open_in_external_editor_button: CheckBox = $Advanced/OpenInExternalEditorButton -@onready var test_scene_path_input: LineEdit = $Advanced/CustomTestScene/TestScenePath -@onready var revert_test_scene_button: Button = $Advanced/CustomTestScene/RevertTestScene -@onready var load_test_scene_button: Button = $Advanced/CustomTestScene/LoadTestScene -@onready var custom_test_scene_file_dialog: FileDialog = $CustomTestSceneFileDialog -@onready var create_lines_for_response_characters: CheckBox = $Advanced/CreateLinesForResponseCharacters -@onready var missing_translations_button: CheckBox = $Advanced/MissingTranslationsButton - -var all_globals: Dictionary = {} -var enabled_globals: Array = [] -var path_target: PathTarget = PathTarget.CustomTestScene - -var _default_test_scene_path: String = preload("../test_scene.tscn").resource_path - -var _recompile_if_changed_settings: Dictionary - - -func _ready() -> void: - new_template_button.text = DialogueConstants.translate(&"settings.new_template") - $Editor/MissingTranslationsHint.text = DialogueConstants.translate(&"settings.missing_keys_hint") - characters_translations_button.text = DialogueConstants.translate(&"settings.characters_translations") - wrap_lines_button.text = DialogueConstants.translate(&"settings.wrap_long_lines") - $Editor/DefaultCSVLocaleLabel.text = DialogueConstants.translate(&"settings.default_csv_locale") - - include_all_responses_button.text = DialogueConstants.translate(&"settings.include_failed_responses") - ignore_missing_state_values.text = DialogueConstants.translate(&"settings.ignore_missing_state_values") - $Runtime/CustomBalloonLabel.text = DialogueConstants.translate(&"settings.default_balloon_hint") - states_title.text = DialogueConstants.translate(&"settings.states_shortcuts") - $Runtime/StatesMessage.text = DialogueConstants.translate(&"settings.states_message") - $Runtime/StatesHint.text = DialogueConstants.translate(&"settings.states_hint") - - check_for_updates.text = DialogueConstants.translate(&"settings.check_for_updates") - include_characters_in_translations.text = DialogueConstants.translate(&"settings.include_characters_in_translations") - include_notes_in_translations.text = DialogueConstants.translate(&"settings.include_notes_in_translations") - open_in_external_editor_button.text = DialogueConstants.translate(&"settings.open_in_external_editor") - $Advanced/ExternalWarning.text = DialogueConstants.translate(&"settings.external_editor_warning") - $Advanced/CustomTestSceneLabel.text = DialogueConstants.translate(&"settings.custom_test_scene") - $Advanced/RecompileWarning.text = DialogueConstants.translate(&"settings.recompile_warning") - missing_translations_button.text = DialogueConstants.translate(&"settings.missing_keys") - create_lines_for_response_characters.text = DialogueConstants.translate(&"settings.create_lines_for_responses_with_characters") - - current_tab = 0 - - -func prepare() -> void: - _recompile_if_changed_settings = _get_settings_that_require_recompilation() - - test_scene_path_input.placeholder_text = DialogueSettings.get_setting("custom_test_scene_path", _default_test_scene_path) - revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path - revert_test_scene_button.icon = get_theme_icon("RotateLeft", "EditorIcons") - revert_test_scene_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_test_scene") - load_test_scene_button.icon = get_theme_icon("Load", "EditorIcons") - - var balloon_path: String = DialogueSettings.get_setting("balloon_path", "") - if not FileAccess.file_exists(balloon_path): - DialogueSettings.set_setting("balloon_path", "") - balloon_path = "" - balloon_path_input.placeholder_text = balloon_path if balloon_path != "" else DialogueConstants.translate(&"settings.default_balloon_path") - revert_balloon_button.visible = balloon_path != "" - revert_balloon_button.icon = get_theme_icon("RotateLeft", "EditorIcons") - revert_balloon_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_balloon") - load_balloon_button.icon = get_theme_icon("Load", "EditorIcons") - - var scale: float = Engine.get_meta("DialogueManagerPlugin").get_editor_interface().get_editor_scale() - custom_test_scene_file_dialog.min_size = Vector2(600, 500) * scale - - states_title.add_theme_font_override("font", get_theme_font("bold", "EditorFonts")) - - check_for_updates.set_pressed_no_signal(DialogueSettings.get_user_value("check_for_updates", true)) - characters_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("export_characters_in_translation", true)) - wrap_lines_button.set_pressed_no_signal(DialogueSettings.get_setting("wrap_lines", false)) - include_all_responses_button.set_pressed_no_signal(DialogueSettings.get_setting("include_all_responses", false)) - ignore_missing_state_values.set_pressed_no_signal(DialogueSettings.get_setting("ignore_missing_state_values", false)) - new_template_button.set_pressed_no_signal(DialogueSettings.get_setting("new_with_template", true)) - default_csv_locale.text = DialogueSettings.get_setting("default_csv_locale", "en") - - missing_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("missing_translations_are_errors", false)) - create_lines_for_response_characters.set_pressed_no_signal(DialogueSettings.get_setting("create_lines_for_responses_with_characters", true)) - - include_characters_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_character_in_translation_exports", false)) - include_notes_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_notes_in_translation_exports", false)) - open_in_external_editor_button.set_pressed_no_signal(DialogueSettings.get_user_value("open_in_external_editor", false)) - - var editor_settings: EditorSettings = Engine.get_meta("DialogueManagerPlugin").get_editor_interface().get_editor_settings() - var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") - var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" - if not use_external_editor: - open_in_external_editor_button.hide() - $Advanced/ExternalWarning.hide() - $Advanced/ExternalSeparator.hide() - - var project = ConfigFile.new() - var err = project.load("res://project.godot") - assert(err == OK, "Could not find the project file") - - all_globals.clear() - if project.has_section("autoload"): - for key in project.get_section_keys("autoload"): - if key != "DialogueManager": - all_globals[key] = project.get_value("autoload", key) - - enabled_globals = DialogueSettings.get_setting("states", []).duplicate() - globals_list.clear() - var root = globals_list.create_item() - for name in all_globals.keys(): - var item: TreeItem = globals_list.create_item(root) - item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK) - item.set_checked(0, name in enabled_globals) - item.set_text(0, name) - item.add_button(1, get_theme_icon("Edit", "EditorIcons")) - item.set_text(2, all_globals.get(name, "").replace("*res://", "res://")) - - globals_list.set_column_expand(0, false) - globals_list.set_column_custom_minimum_width(0, 250) - globals_list.set_column_expand(1, false) - globals_list.set_column_custom_minimum_width(1, 40) - globals_list.set_column_titles_visible(true) - globals_list.set_column_title(0, DialogueConstants.translate(&"settings.autoload")) - globals_list.set_column_title(1, "") - globals_list.set_column_title(2, DialogueConstants.translate(&"settings.path")) - - -func apply_settings_changes() -> void: - if _recompile_if_changed_settings != _get_settings_that_require_recompilation(): - Engine.get_meta("DialogueCache").reimport_files() - - -func _get_settings_that_require_recompilation() -> Dictionary: - return DialogueSettings.get_settings([ - "missing_translations_are_errors", - "create_lines_for_responses_with_characters" - ]) - - -### Signals - - -func _on_missing_translations_button_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("missing_translations_are_errors", toggled_on) - - -func _on_characters_translations_button_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("export_characters_in_translation", toggled_on) - - -func _on_wrap_lines_button_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("wrap_lines", toggled_on) - - -func _on_include_all_responses_button_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("include_all_responses", toggled_on) - - -func _on_globals_list_item_selected() -> void: - var item = globals_list.get_selected() - var is_checked = not item.is_checked(0) - item.set_checked(0, is_checked) - - if is_checked: - enabled_globals.append(item.get_text(0)) - else: - enabled_globals.erase(item.get_text(0)) - - DialogueSettings.set_setting("states", enabled_globals) - - -func _on_globals_list_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void: - emit_signal("script_button_pressed", item.get_text(2)) - - -func _on_sample_template_toggled(toggled_on): - DialogueSettings.set_setting("new_with_template", toggled_on) - - -func _on_revert_test_scene_pressed() -> void: - DialogueSettings.set_setting("custom_test_scene_path", _default_test_scene_path) - test_scene_path_input.placeholder_text = _default_test_scene_path - revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path - - -func _on_load_test_scene_pressed() -> void: - path_target = PathTarget.CustomTestScene - custom_test_scene_file_dialog.popup_centered() - - -func _on_custom_test_scene_file_dialog_file_selected(path: String) -> void: - match path_target: - PathTarget.CustomTestScene: - # Check that the test scene is a subclass of BaseDialogueTestScene - var test_scene: PackedScene = load(path) - if test_scene and test_scene.instantiate() is BaseDialogueTestScene: - DialogueSettings.set_setting("custom_test_scene_path", path) - test_scene_path_input.placeholder_text = path - revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path - else: - var accept: AcceptDialog = AcceptDialog.new() - accept.dialog_text = DialogueConstants.translate(&"settings.invalid_test_scene").format({ path = path }) - add_child(accept) - accept.popup_centered.call_deferred() - - PathTarget.Balloon: - DialogueSettings.set_setting("balloon_path", path) - balloon_path_input.placeholder_text = path - revert_balloon_button.visible = balloon_path_input.placeholder_text != "" - - -func _on_ignore_missing_state_values_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("ignore_missing_state_values", toggled_on) - - -func _on_default_csv_locale_text_changed(new_text: String) -> void: - DialogueSettings.set_setting("default_csv_locale", new_text) - - -func _on_revert_balloon_path_pressed() -> void: - DialogueSettings.set_setting("balloon_path", "") - balloon_path_input.placeholder_text = DialogueConstants.translate(&"settings.default_balloon_path") - revert_balloon_button.visible = DialogueSettings.get_setting("balloon_path", "") != "" - - -func _on_load_balloon_path_pressed() -> void: - path_target = PathTarget.Balloon - custom_test_scene_file_dialog.popup_centered() - - -func _on_create_lines_for_response_characters_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("create_lines_for_responses_with_characters", toggled_on) - - -func _on_open_in_external_editor_button_toggled(toggled_on: bool) -> void: - DialogueSettings.set_user_value("open_in_external_editor", toggled_on) - - -func _on_include_characters_in_translations_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("include_character_in_translation_exports", toggled_on) - - -func _on_include_notes_in_translations_toggled(toggled_on: bool) -> void: - DialogueSettings.set_setting("include_notes_in_translation_exports", toggled_on) - - -func _on_keep_up_to_date_toggled(toggled_on: bool) -> void: - DialogueSettings.set_user_value("check_for_updates", toggled_on) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd.uid b/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd.uid deleted file mode 100644 index 3195616a..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://md7m321a3hur diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.tscn deleted file mode 100644 index 09df0a27..00000000 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/settings_view.tscn +++ /dev/null @@ -1,221 +0,0 @@ -[gd_scene load_steps=3 format=3 uid="uid://cpg4lg1r3ff6m"] - -[ext_resource type="Script" path="res://addons/dialogue_manager/views/settings_view.gd" id="1_06uxa"] - -[sub_resource type="Theme" id="Theme_3a8rc"] -HSeparator/constants/separation = 20 - -[node name="SettingsView" type="TabContainer"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_right = -206.0 -offset_bottom = -345.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme = SubResource("Theme_3a8rc") -current_tab = 2 -script = ExtResource("1_06uxa") - -[node name="Editor" type="VBoxContainer" parent="."] -visible = false -layout_mode = 2 - -[node name="NewTemplateButton" type="CheckBox" parent="Editor"] -layout_mode = 2 -button_pressed = true -text = "New dialogue files will start with template text" - -[node name="MissingTranslationsHint" type="Label" parent="Editor"] -modulate = Color(1, 1, 1, 0.501961) -custom_minimum_size = Vector2(10, 0) -layout_mode = 2 -text = "If you are using static translation keys then having this enabled will help you find any lines that you haven't added a key to yet." -autowrap_mode = 3 - -[node name="CharactersTranslationsButton" type="CheckBox" parent="Editor"] -layout_mode = 2 -button_pressed = true -text = "Export character names in translation files" - -[node name="WrapLinesButton" type="CheckBox" parent="Editor"] -layout_mode = 2 -button_pressed = true -text = "Wrap long lines" - -[node name="HSeparator" type="HSeparator" parent="Editor"] -layout_mode = 2 - -[node name="DefaultCSVLocaleLabel" type="Label" parent="Editor"] -layout_mode = 2 -text = "Default CSV Locale" - -[node name="DefaultCSVLocale" type="LineEdit" parent="Editor"] -layout_mode = 2 - -[node name="Runtime" type="VBoxContainer" parent="."] -visible = false -layout_mode = 2 - -[node name="IncludeAllResponsesButton" type="CheckBox" parent="Runtime"] -layout_mode = 2 -text = "Include responses with failed conditions" - -[node name="IgnoreMissingStateValues" type="CheckBox" parent="Runtime"] -layout_mode = 2 -text = "Skip over missing state value errors (not recommended)" - -[node name="HSeparator" type="HSeparator" parent="Runtime"] -layout_mode = 2 - -[node name="CustomBalloonLabel" type="Label" parent="Runtime"] -layout_mode = 2 -text = "Custom balloon to use when calling \"DialogueManager.show_balloon()\"" - -[node name="CustomBalloon" type="HBoxContainer" parent="Runtime"] -layout_mode = 2 - -[node name="BalloonPath" type="LineEdit" parent="Runtime/CustomBalloon"] -layout_mode = 2 -size_flags_horizontal = 3 -focus_mode = 0 -editable = false -shortcut_keys_enabled = false -middle_mouse_paste_enabled = false - -[node name="RevertBalloonPath" type="Button" parent="Runtime/CustomBalloon"] -visible = false -layout_mode = 2 -tooltip_text = "Revert to default test scene" -flat = true - -[node name="LoadBalloonPath" type="Button" parent="Runtime/CustomBalloon"] -layout_mode = 2 - -[node name="HSeparator2" type="HSeparator" parent="Runtime"] -layout_mode = 2 - -[node name="StatesTitle" type="Label" parent="Runtime"] -layout_mode = 2 -text = "State Shortcuts" - -[node name="StatesMessage" type="Label" parent="Runtime"] -layout_mode = 2 -text = "If an autoload is enabled here you can refer to its properties and methods without having to use its name." - -[node name="StatesHint" type="Label" parent="Runtime"] -modulate = Color(1, 1, 1, 0.501961) -custom_minimum_size = Vector2(10, 0) -layout_mode = 2 -text = "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\"" -autowrap_mode = 3 - -[node name="GlobalsList" type="Tree" parent="Runtime"] -layout_mode = 2 -size_flags_vertical = 3 -columns = 3 -column_titles_visible = true -allow_reselect = true -hide_folding = true -hide_root = true -select_mode = 1 - -[node name="Advanced" type="VBoxContainer" parent="."] -layout_mode = 2 - -[node name="CheckForUpdates" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Check for updates" - -[node name="HSeparator" type="HSeparator" parent="Advanced"] -layout_mode = 2 - -[node name="IncludeCharactersInTranslations" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Include character names in translation exports" - -[node name="IncludeNotesInTranslations" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Include notes (## comments) in translation exports" - -[node name="ExternalSeparator" type="HSeparator" parent="Advanced"] -layout_mode = 2 - -[node name="OpenInExternalEditorButton" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Open dialogue files in external editor" - -[node name="ExternalWarning" type="Label" parent="Advanced"] -layout_mode = 2 -text = "Note: Syntax highlighting and detailed error checking are not supported in external editors." - -[node name="HSeparator3" type="HSeparator" parent="Advanced"] -layout_mode = 2 - -[node name="CustomTestSceneLabel" type="Label" parent="Advanced"] -layout_mode = 2 -text = "Custom test scene (must extend BaseDialogueTestScene)" - -[node name="CustomTestScene" type="HBoxContainer" parent="Advanced"] -layout_mode = 2 - -[node name="TestScenePath" type="LineEdit" parent="Advanced/CustomTestScene"] -layout_mode = 2 -size_flags_horizontal = 3 -focus_mode = 0 -placeholder_text = "res://addons/dialogue_manager/test_scene.tscn" -editable = false -shortcut_keys_enabled = false -middle_mouse_paste_enabled = false - -[node name="RevertTestScene" type="Button" parent="Advanced/CustomTestScene"] -visible = false -layout_mode = 2 -tooltip_text = "Revert to default test scene" -flat = true - -[node name="LoadTestScene" type="Button" parent="Advanced/CustomTestScene"] -layout_mode = 2 - -[node name="HSeparator4" type="HSeparator" parent="Advanced"] -layout_mode = 2 - -[node name="RecompileWarning" type="Label" parent="Advanced"] -layout_mode = 2 -text = "Changing these settings will force a recompile of all dialogue. Only change them if you know what you are doing." - -[node name="MissingTranslationsButton" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Treat missing translation keys as errors" - -[node name="CreateLinesForResponseCharacters" type="CheckBox" parent="Advanced"] -layout_mode = 2 -text = "Create child dialogue line for responses with character names in them" - -[node name="CustomTestSceneFileDialog" type="FileDialog" parent="."] -title = "Open a File" -ok_button_text = "Open" -file_mode = 0 -filters = PackedStringArray("*.tscn ; Scene") - -[connection signal="toggled" from="Editor/NewTemplateButton" to="." method="_on_sample_template_toggled"] -[connection signal="toggled" from="Editor/CharactersTranslationsButton" to="." method="_on_characters_translations_button_toggled"] -[connection signal="toggled" from="Editor/WrapLinesButton" to="." method="_on_wrap_lines_button_toggled"] -[connection signal="text_changed" from="Editor/DefaultCSVLocale" to="." method="_on_default_csv_locale_text_changed"] -[connection signal="toggled" from="Runtime/IncludeAllResponsesButton" to="." method="_on_include_all_responses_button_toggled"] -[connection signal="toggled" from="Runtime/IgnoreMissingStateValues" to="." method="_on_ignore_missing_state_values_toggled"] -[connection signal="pressed" from="Runtime/CustomBalloon/RevertBalloonPath" to="." method="_on_revert_balloon_path_pressed"] -[connection signal="pressed" from="Runtime/CustomBalloon/LoadBalloonPath" to="." method="_on_load_balloon_path_pressed"] -[connection signal="button_clicked" from="Runtime/GlobalsList" to="." method="_on_globals_list_button_clicked"] -[connection signal="item_selected" from="Runtime/GlobalsList" to="." method="_on_globals_list_item_selected"] -[connection signal="toggled" from="Advanced/CheckForUpdates" to="." method="_on_keep_up_to_date_toggled"] -[connection signal="toggled" from="Advanced/IncludeCharactersInTranslations" to="." method="_on_include_characters_in_translations_toggled"] -[connection signal="toggled" from="Advanced/IncludeNotesInTranslations" to="." method="_on_include_notes_in_translations_toggled"] -[connection signal="toggled" from="Advanced/OpenInExternalEditorButton" to="." method="_on_open_in_external_editor_button_toggled"] -[connection signal="pressed" from="Advanced/CustomTestScene/RevertTestScene" to="." method="_on_revert_test_scene_pressed"] -[connection signal="pressed" from="Advanced/CustomTestScene/LoadTestScene" to="." method="_on_load_test_scene_pressed"] -[connection signal="toggled" from="Advanced/MissingTranslationsButton" to="." method="_on_missing_translations_button_toggled"] -[connection signal="toggled" from="Advanced/CreateLinesForResponseCharacters" to="." method="_on_create_lines_for_response_characters_toggled"] -[connection signal="file_selected" from="CustomTestSceneFileDialog" to="." method="_on_custom_test_scene_file_dialog_file_selected"] diff --git a/Zennysoft.Game.Ma/project.godot b/Zennysoft.Game.Ma/project.godot index 38b90f89..c762bf71 100644 --- a/Zennysoft.Game.Ma/project.godot +++ b/Zennysoft.Game.Ma/project.godot @@ -24,8 +24,8 @@ DialogueController="*res://src/game/DialogueController.cs" [dialogue_manager] -general/wrap_lines=true -general/balloon_path="res://src/ui/dialogue/Balloon.tscn" +editor/wrap_long_lines=true +runtime/balloon_path="res://src/ui/dialogue/Balloon.tscn" [display] diff --git a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue index 80e37531..092c4d06 100644 --- a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue +++ b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue @@ -1,3 +1,11 @@ -~ start +~ general Hi... +=> END + +~ introduction +I haven't had a script added yet. +=> END + +~ hit +Yep, hitting me does work though. => END \ No newline at end of file diff --git a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import index 210e7396..bb7761ed 100644 --- a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import +++ b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import @@ -1,6 +1,6 @@ [remap] -importer="dialogue_manager_compiler_12" +importer="dialogue_manager_compiler_14" type="Resource" uid="uid://lao0opxww3ib" path="res://.godot/imported/Dialogue.dialogue-176033575bc12c347010f3a30b2e302a.tres" diff --git a/Zennysoft.Game.Ma/src/enemy/EnemyModelView2D.cs b/Zennysoft.Game.Ma/src/enemy/EnemyModelView2D.cs index 5fe27a3d..6f65454a 100644 --- a/Zennysoft.Game.Ma/src/enemy/EnemyModelView2D.cs +++ b/Zennysoft.Game.Ma/src/enemy/EnemyModelView2D.cs @@ -1,4 +1,4 @@ -using Chickensoft.AutoInject; +using Chickensoft.AutoInject; using Chickensoft.Introspection; using Godot; @@ -31,105 +31,105 @@ public partial class EnemyModelView2D : Node3D, IEnemyModelView public void Setup() { - AnimationTree.AnimationFinished += AnimationTree_AnimationFinished; - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Start(IDLE_FORWARD); + AnimationTree.AnimationFinished += AnimationTree_AnimationFinished; + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Start(IDLE_FORWARD); } public void PlayPrimaryAttackAnimation() { - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(PRIMARY_ATTACK); + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(PRIMARY_ATTACK); } public void PlaySecondaryAttackAnimation() { - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(SECONDARY_ATTACK); + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(SECONDARY_ATTACK); } public void PlayPrimarySkillAnimation() { - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(PRIMARY_SKILL); + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(PRIMARY_SKILL); } public void PlayHitAnimation() { - LoadShader("res://src/vfx/shaders/DamageHit.gdshader"); - var tweener = GetTree().CreateTween(); - tweener.TweenMethod(Callable.From((float x) => SetShaderValue(x)), 0.0f, 1.0f, 1.0f); + LoadShader("res://src/vfx/shaders/DamageHit.gdshader"); + var tweener = GetTree().CreateTween(); + tweener.TweenMethod(Callable.From((float x) => SetShaderValue(x)), 0.0f, 1.0f, 1.0f); } public void PlayDeathAnimation() { - LoadShader("res://src/vfx/shaders/PixelMelt.gdshader"); - var tweener = GetTree().CreateTween(); - tweener.TweenMethod(Callable.From((float x) => SetShaderValue(x)), 0.0f, 1.0f, 0.8f); - tweener.TweenCallback(Callable.From(QueueFree)); + LoadShader("res://src/vfx/shaders/PixelMelt.gdshader"); + var tweener = GetTree().CreateTween(); + tweener.TweenMethod(Callable.From((float x) => SetShaderValue(x)), 0.0f, 1.0f, 0.8f); + tweener.TweenCallback(Callable.From(QueueFree)); } public void RotateModel(Basis enemyBasis, Vector3 cameraDirection, bool isWalking) => RotateModel(enemyBasis, cameraDirection, 0.55f, 0.45f, isWalking); public virtual void RotateModel( - Basis enemyBasis, - Vector3 cameraDirection, - float rotateUpperThreshold, - float rotateLowerThreshold, - bool isWalking) + Basis enemyBasis, + Vector3 cameraDirection, + float rotateUpperThreshold, + float rotateLowerThreshold, + bool isWalking) { - var enemyForwardDirection = enemyBasis.Z; - var enemyLeftDirection = enemyBasis.X; + var enemyForwardDirection = enemyBasis.Z; + var enemyLeftDirection = enemyBasis.X; - var leftDotProduct = enemyLeftDirection.Dot(cameraDirection); - var forwardDotProduct = enemyForwardDirection.Dot(cameraDirection); + var leftDotProduct = enemyLeftDirection.Dot(cameraDirection); + var forwardDotProduct = enemyForwardDirection.Dot(cameraDirection); - // Check if forward facing. If the dot product is -1, the enemy is facing the camera. - if (forwardDotProduct < -rotateUpperThreshold) - { - if (isWalking) - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD_WALK); - else - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD); - } - // Check if backward facing. If the dot product is 1, the enemy is facing the same direction as the camera. - else if (forwardDotProduct > rotateUpperThreshold) - { - if (isWalking) - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_BACK_WALK); - else - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_BACK); - } - else - { - // If the dot product of the perpendicular direction is positive (up to 1), the enemy is facing to the left (since it's mirrored). - AnimatedSprite.FlipH = leftDotProduct > 0; - // Check if side facing. If the dot product is close to zero in the positive or negative direction, its close to the threshold for turning. - if (Mathf.Abs(forwardDotProduct) < rotateLowerThreshold) - { - if (isWalking) - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_LEFT_WALK); - else - AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_LEFT); - } - } + // Check if forward facing. If the dot product is -1, the enemy is facing the camera. + if (forwardDotProduct < -rotateUpperThreshold) + { + if (isWalking) + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD_WALK); + else + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD); + } + // Check if backward facing. If the dot product is 1, the enemy is facing the same direction as the camera. + else if (forwardDotProduct > rotateUpperThreshold) + { + if (isWalking) + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_BACK_WALK); + else + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_BACK); + } + else + { + // If the dot product of the perpendicular direction is positive (up to 1), the enemy is facing to the left (since it's mirrored). + AnimatedSprite.FlipH = leftDotProduct > 0; + // Check if side facing. If the dot product is close to zero in the positive or negative direction, its close to the threshold for turning. + if (Mathf.Abs(forwardDotProduct) < rotateLowerThreshold) + { + if (isWalking) + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_LEFT_WALK); + else + AnimationTree.Get(PARAMETERS_PLAYBACK).As<AnimationNodeStateMachinePlayback>().Travel(IDLE_LEFT); + } + } } private void LoadShader(string shaderPath) { - var shader = GD.Load<Shader>(shaderPath); - AnimatedSprite.Material = new ShaderMaterial(); - var shaderMaterial = (ShaderMaterial)AnimatedSprite.Material; - shaderMaterial.Shader = shader; + var shader = GD.Load<Shader>(shaderPath); + AnimatedSprite.Material = new ShaderMaterial(); + var shaderMaterial = (ShaderMaterial)AnimatedSprite.Material; + shaderMaterial.Shader = shader; } private void AnimationTree_AnimationFinished(StringName animName) { - if (animName == PRIMARY_ATTACK || animName == SECONDARY_ATTACK || animName == PRIMARY_SKILL) - { - AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD); - } + if (animName == PRIMARY_ATTACK || animName == SECONDARY_ATTACK || animName == PRIMARY_SKILL) + { + AnimationTree.Get("parameters/playback").As<AnimationNodeStateMachinePlayback>().Travel(IDLE_FORWARD); + } } private void SetShaderValue(float shaderValue) { - var shaderMaterial = (ShaderMaterial)AnimatedSprite.Material; - shaderMaterial.SetShaderParameter("progress", shaderValue); + var shaderMaterial = (ShaderMaterial)AnimatedSprite.Material; + shaderMaterial.SetShaderParameter("progress", shaderValue); } } diff --git a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.cs b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.cs index db4769b8..c7f00902 100644 --- a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.cs +++ b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.cs @@ -23,46 +23,46 @@ public partial class Ballos : Enemy, IHasPrimaryAttack, IHasSecondaryAttack public void OnReady() { - SetPhysicsProcess(true); - ((EnemyModelView2D)_enemyModelView).Hitbox.AreaEntered += Hitbox_AreaEntered; + SetPhysicsProcess(true); + ((EnemyModelView2D)_enemyModelView).Hitbox.AreaEntered += Hitbox_AreaEntered; } public void OnPhysicsProcess(double delta) { - _enemyLogic.Input(new EnemyLogic.Input.PhysicsTick(delta)); + _enemyLogic.Input(new EnemyLogic.Input.PhysicsTick(delta)); - if (_enemyLogic.Value is EnemyLogic.State.FollowPlayer && GlobalPosition.DistanceTo(_player.CurrentPosition) < 2.5f) - _enemyLogic.Input(new EnemyLogic.Input.StartAttacking()); - if (_enemyLogic.Value is EnemyLogic.State.FollowPlayer && GlobalPosition.DistanceTo(_player.CurrentPosition) > 45f) - _enemyLogic.Input(new EnemyLogic.Input.LostPlayer()); - if (_enemyLogic.Value is EnemyLogic.State.Attacking && GlobalPosition.DistanceTo(_player.CurrentPosition) > 2.5f) - _enemyLogic.Input(new EnemyLogic.Input.Alerted()); + if (_enemyLogic.Value is EnemyLogic.State.FollowPlayer && GlobalPosition.DistanceTo(_player.CurrentPosition) < 2.5f) + _enemyLogic.Input(new EnemyLogic.Input.StartAttacking()); + if (_enemyLogic.Value is EnemyLogic.State.FollowPlayer && GlobalPosition.DistanceTo(_player.CurrentPosition) > 45f) + _enemyLogic.Input(new EnemyLogic.Input.LostPlayer()); + if (_enemyLogic.Value is EnemyLogic.State.Attacking && GlobalPosition.DistanceTo(_player.CurrentPosition) > 2.5f) + _enemyLogic.Input(new EnemyLogic.Input.Alerted()); } public override void TakeAction() { - var rng = new RandomNumberGenerator(); - var options = new List<Action>() { PrimaryAttack, SecondaryAttack }; - var selection = rng.RandWeighted([0.875f, 0.125f]); - options[(int)selection].Invoke(); + var rng = new RandomNumberGenerator(); + var options = new List<Action>() { PrimaryAttack, SecondaryAttack }; + var selection = rng.RandWeighted([0.875f, 0.125f]); + options[(int)selection].Invoke(); } public void PrimaryAttack() { - _enemyModelView.PlayPrimaryAttackAnimation(); + _enemyModelView.PlayPrimaryAttackAnimation(); } public void SecondaryAttack() { - _enemyModelView.PlaySecondaryAttackAnimation(); + _enemyModelView.PlaySecondaryAttackAnimation(); } private void Hitbox_AreaEntered(Area3D area) { - var target = area.GetOwner(); - if (target is IPlayer player) - { - var damage = _enemyStatResource.CurrentAttack * PrimaryAttackElementalDamageBonus; - player.TakeDamage(damage, PrimaryAttackElementalType, BattleExtensions.IsCriticalHit(_enemyStatResource.Luck)); - } + var target = area.GetOwner(); + if (target is IPlayer player) + { + var damage = _enemyStatResource.CurrentAttack * PrimaryAttackElementalDamageBonus; + player.TakeDamage(damage, PrimaryAttackElementalType, BattleExtensions.IsCriticalHit(_enemyStatResource.Luck)); + } } -} \ No newline at end of file +} diff --git a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.tscn b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.tscn index 5dbb83db..193d1f8e 100644 --- a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.tscn +++ b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/Ballos.tscn @@ -1,8 +1,27 @@ -[gd_scene load_steps=6 format=3 uid="uid://feegakykn3fv"] +[gd_scene load_steps=8 format=3 uid="uid://feegakykn3fv"] [ext_resource type="Script" uid="uid://dwfxs5yrf7i3v" path="res://src/enemy/enemy_types/05. ballos/Ballos.cs" id="1_v2urn"] +[ext_resource type="Script" uid="uid://dnkmr0eq1sij0" path="res://src/enemy/EnemyStatResource.cs" id="2_iy2fp"] [ext_resource type="PackedScene" uid="uid://c5xijwxkg4pf6" path="res://src/enemy/enemy_types/05. ballos/BallosModelView.tscn" id="2_v2urn"] +[sub_resource type="Resource" id="Resource_ko6aj"] +script = ExtResource("2_iy2fp") +CurrentHP = 100.0 +MaximumHP = 100 +CurrentAttack = 20 +CurrentDefense = 12 +MaxAttack = 20 +MaxDefense = 12 +ExpFromDefeat = 50 +Luck = 0.05 +TelluricResistance = 0.0 +AeolicResistance = 0.0 +HydricResistance = 0.0 +IgneousResistance = 0.0 +FerrumResistance = 0.0 +DropsSoulGemChance = 0.75 +metadata/_custom_type_script = "uid://dnkmr0eq1sij0" + [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_cwfph"] radius = 0.717471 height = 2.02807 @@ -16,12 +35,12 @@ radius = 1.20703 [node name="Ballos" type="CharacterBody3D"] process_mode = 1 -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0) collision_layer = 10 collision_mask = 11 axis_lock_linear_y = true axis_lock_angular_x = true script = ExtResource("1_v2urn") +_enemyStatResource = SubResource("Resource_ko6aj") [node name="CollisionShape" type="CollisionShape3D" parent="."] unique_name_in_owner = true diff --git a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/BallosModelView.tscn b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/BallosModelView.tscn index 836f5870..91b4d42b 100644 --- a/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/BallosModelView.tscn +++ b/Zennysoft.Game.Ma/src/enemy/enemy_types/05. ballos/BallosModelView.tscn @@ -796,6 +796,7 @@ size = Vector2i(400, 400) render_target_update_mode = 4 [node name="AnimatedSprite" type="AnimatedSprite2D" parent="Sprite3D/SubViewportContainer/SubViewport"] +unique_name_in_owner = true sprite_frames = SubResource("SpriteFrames_mlptn") animation = &"idle_front_walk" offset = Vector2(200, 200) diff --git a/Zennysoft.Game.Ma/src/game/DialogueController.cs b/Zennysoft.Game.Ma/src/game/DialogueController.cs index 7517f5be..aead9b3d 100644 --- a/Zennysoft.Game.Ma/src/game/DialogueController.cs +++ b/Zennysoft.Game.Ma/src/game/DialogueController.cs @@ -13,18 +13,18 @@ public partial class DialogueController : Node public override void _Ready() { - DialogueBalloon = GD.Load<PackedScene>("res://src/ui/dialogue/DialogueBalloon.tscn"); + DialogueBalloon = GD.Load<PackedScene>("res://src/ui/dialogue/DialogueBalloon.tscn"); } public static void ShowDialogue(Resource dialogueResource, string dialogueTitle) { - Interrupt(); - _currentlyActiveDialogue = DialogueManager.ShowDialogueBalloonScene(DialogueBalloon, dialogueResource, dialogueTitle); + Interrupt(); + _currentlyActiveDialogue = DialogueManager.ShowDialogueBalloonScene(DialogueBalloon, dialogueResource, dialogueTitle); } public static void Interrupt() { - if (IsInstanceValid(_currentlyActiveDialogue)) - _currentlyActiveDialogue.QueueFree(); + if (IsInstanceValid(_currentlyActiveDialogue)) + _currentlyActiveDialogue.QueueFree(); } } diff --git a/Zennysoft.Game.Ma/src/game/Game.cs b/Zennysoft.Game.Ma/src/game/Game.cs index 9f67c41c..f51f96ee 100644 --- a/Zennysoft.Game.Ma/src/game/Game.cs +++ b/Zennysoft.Game.Ma/src/game/Game.cs @@ -79,201 +79,201 @@ public partial class Game : Node3D, IGame public void Setup() { - _container = new SimpleInjector.Container(); - Module.Bootstrap(_container); - _container.Verify(); + _container = new SimpleInjector.Container(); + Module.Bootstrap(_container); + _container.Verify(); - GameRepo = _container.GetInstance<IGameRepo>(); - GameLogic = _container.GetInstance<IGameLogic>(); - GameEventDepot = new GameEventDepot(); - GameLogic.Set(GameRepo); - GameLogic.Set(AppRepo); - GameLogic.Set(GameEventDepot); - GameLogic.Set(Player); - GameLogic.Set(Map); - GameLogic.Set(InGameUI); - Instantiator = new Instantiator(GetTree()); - RescuedItems = new RescuedItemDatabase(); + GameRepo = _container.GetInstance<IGameRepo>(); + GameLogic = _container.GetInstance<IGameLogic>(); + GameEventDepot = new GameEventDepot(); + GameLogic.Set(GameRepo); + GameLogic.Set(AppRepo); + GameLogic.Set(GameEventDepot); + GameLogic.Set(Player); + GameLogic.Set(Map); + GameLogic.Set(InGameUI); + Instantiator = new Instantiator(GetTree()); + RescuedItems = new RescuedItemDatabase(); - GameChunk = new SaveChunk<GameData>( - (chunk) => - { - var gameData = new GameData() - { - PlayerData = new PlayerData() - { - PlayerStats = new PlayerStats() - { - CurrentHP = Player.Stats.CurrentHP.Value, - MaximumHP = Player.Stats.MaximumHP.Value, - CurrentVT = Player.Stats.CurrentVT.Value, - MaximumVT = Player.Stats.MaximumVT.Value, - CurrentAttack = Player.Stats.CurrentAttack.Value, - BonusAttack = Player.Stats.BonusAttack.Value, - MaxAttack = Player.Stats.MaxAttack.Value, - CurrentDefense = Player.Stats.CurrentDefense.Value, - BonusDefense = Player.Stats.BonusDefense.Value, - MaxDefense = Player.Stats.MaxDefense.Value, - CurrentExp = Player.Stats.CurrentExp.Value, - CurrentLevel = Player.Stats.CurrentLevel.Value, - ExpToNextLevel = Player.Stats.ExpToNextLevel.Value, - Luck = Player.Stats.Luck.Value - }, - Inventory = Player.Inventory - }, - MapData = new MapData() - { - FloorScenes = Map.FloorScenes - }, - RescuedItems = new RescuedItemDatabase() { Items = RescuedItems.Items } - }; + GameChunk = new SaveChunk<GameData>( + (chunk) => + { + var gameData = new GameData() + { + PlayerData = new PlayerData() + { + PlayerStats = new PlayerStats() + { + CurrentHP = Player.Stats.CurrentHP.Value, + MaximumHP = Player.Stats.MaximumHP.Value, + CurrentVT = Player.Stats.CurrentVT.Value, + MaximumVT = Player.Stats.MaximumVT.Value, + CurrentAttack = Player.Stats.CurrentAttack.Value, + BonusAttack = Player.Stats.BonusAttack.Value, + MaxAttack = Player.Stats.MaxAttack.Value, + CurrentDefense = Player.Stats.CurrentDefense.Value, + BonusDefense = Player.Stats.BonusDefense.Value, + MaxDefense = Player.Stats.MaxDefense.Value, + CurrentExp = Player.Stats.CurrentExp.Value, + CurrentLevel = Player.Stats.CurrentLevel.Value, + ExpToNextLevel = Player.Stats.ExpToNextLevel.Value, + Luck = Player.Stats.Luck.Value + }, + Inventory = Player.Inventory + }, + MapData = new MapData() + { + FloorScenes = Map.FloorScenes + }, + RescuedItems = new RescuedItemDatabase() { Items = RescuedItems.Items } + }; - return gameData; - }, - onLoad: (chunk, data) => - { - RescuedItems = data.RescuedItems; - chunk.LoadChunkSaveData(data.PlayerData); - chunk.LoadChunkSaveData(data.MapData); - } - ); + return gameData; + }, + onLoad: (chunk, data) => + { + RescuedItems = data.RescuedItems; + chunk.LoadChunkSaveData(data.PlayerData); + chunk.LoadChunkSaveData(data.MapData); + } + ); } public void OnResolved() { - var saveFileManager = _container.GetInstance<IMaSaveFileManager<GameData>>(); - SaveFile = new SaveFile<GameData>( - root: GameChunk, - onSave: saveFileManager.Save, - onLoad: async () => - { - try - { - var gameData = await saveFileManager.Load(); - return gameData; - } - catch (FileNotFoundException) - { - GD.Print("No save file found."); - } + var saveFileManager = _container.GetInstance<IMaSaveFileManager<GameData>>(); + SaveFile = new SaveFile<GameData>( + root: GameChunk, + onSave: saveFileManager.Save, + onLoad: async () => + { + try + { + var gameData = await saveFileManager.Load(); + return gameData; + } + catch (FileNotFoundException) + { + GD.Print("No save file found."); + } - return null; - } - ); + return null; + } + ); - GameBinding = GameLogic.Bind(); - GameBinding - .Handle((in GameLogic.Output.StartGame _) => - { - GameRepo.Resume(); - InGameUI.Show(); - }) - .Handle((in GameLogic.Output.GoToOverworld _) => - { - GameEventDepot.OnOverworldEntered(); - }) - .Handle((in GameLogic.Output.SetPauseMode output) => CallDeferred(nameof(SetPauseMode), output.IsPaused)) - .Handle((in GameLogic.Output.ShowPauseMenu _) => - { - PauseMenu.Show(); - PauseMenu.FadeIn(); - Input.MouseMode = Input.MouseModeEnum.Visible; - PauseMenu.SetProcessUnhandledInput(true); - }) - .Handle((in GameLogic.Output.HidePauseMenu _) => { PauseMenu.Hide(); }) - .Handle((in GameLogic.Output.ExitPauseMenu _) => { PauseMenu.FadeOut(); Input.MouseMode = Input.MouseModeEnum.Visible; PauseMenu.SetProcessUnhandledInput(false); }) - .Handle((in GameLogic.Output.LoadNextFloor _) => { Map.SpawnNextFloor(); }) - .Handle((in GameLogic.Output.LoadMap _) => { Map.LoadMap(); }) - .Handle((in GameLogic.Output.ShowFloorClearMenu _) => { FloorClearMenu.Show(); FloorClearMenu.FadeIn(); }) - .Handle((in GameLogic.Output.ExitFloorClearMenu _) => { FloorClearMenu.FadeOut(); }) - .Handle((in GameLogic.Output.ShowAskForTeleport _) => { GameRepo.Pause(); InGameUI.UseTeleportPrompt.FadeIn(); InGameUI.SetProcessInput(true); }) - .Handle((in GameLogic.Output.HideAskForTeleport _) => { GameRepo.Resume(); InGameUI.UseTeleportPrompt.FadeOut(); InGameUI.SetProcessInput(false); }) - .Handle((in GameLogic.Output.ShowLostScreen _) => { DeathMenu.Show(); DeathMenu.FadeIn(); }) - .Handle((in GameLogic.Output.ExitLostScreen _) => { DeathMenu.FadeOut(); }) - .Handle((in GameLogic.Output.DoubleExpTimeStart output) => { DoubleEXPTimer.WaitTime = output.lengthOfTimeInSeconds; DoubleEXPTimer.Start(); }) - .Handle((in GameLogic.Output.SaveGame _) => - { - SaveFile.Save(); - AppRepo.OnExitGame(); - GetTree().Quit(); - // Back to title screen - }); - GameLogic.Start(); + GameBinding = GameLogic.Bind(); + GameBinding + .Handle((in GameLogic.Output.StartGame _) => + { + GameRepo.Resume(); + InGameUI.Show(); + }) + .Handle((in GameLogic.Output.GoToOverworld _) => + { + GameEventDepot.OnOverworldEntered(); + }) + .Handle((in GameLogic.Output.SetPauseMode output) => CallDeferred(nameof(SetPauseMode), output.IsPaused)) + .Handle((in GameLogic.Output.ShowPauseMenu _) => + { + PauseMenu.Show(); + PauseMenu.FadeIn(); + Input.MouseMode = Input.MouseModeEnum.Visible; + PauseMenu.SetProcessUnhandledInput(true); + }) + .Handle((in GameLogic.Output.HidePauseMenu _) => { PauseMenu.Hide(); }) + .Handle((in GameLogic.Output.ExitPauseMenu _) => { PauseMenu.FadeOut(); Input.MouseMode = Input.MouseModeEnum.Visible; PauseMenu.SetProcessUnhandledInput(false); }) + .Handle((in GameLogic.Output.LoadNextFloor _) => { Map.SpawnNextFloor(); }) + .Handle((in GameLogic.Output.LoadMap _) => { Map.LoadMap(); }) + .Handle((in GameLogic.Output.ShowFloorClearMenu _) => { FloorClearMenu.Show(); FloorClearMenu.FadeIn(); }) + .Handle((in GameLogic.Output.ExitFloorClearMenu _) => { FloorClearMenu.FadeOut(); }) + .Handle((in GameLogic.Output.ShowAskForTeleport _) => { GameRepo.Pause(); InGameUI.UseTeleportPrompt.FadeIn(); InGameUI.SetProcessInput(true); }) + .Handle((in GameLogic.Output.HideAskForTeleport _) => { GameRepo.Resume(); InGameUI.UseTeleportPrompt.FadeOut(); InGameUI.SetProcessInput(false); }) + .Handle((in GameLogic.Output.ShowLostScreen _) => { DeathMenu.Show(); DeathMenu.FadeIn(); }) + .Handle((in GameLogic.Output.ExitLostScreen _) => { DeathMenu.FadeOut(); }) + .Handle((in GameLogic.Output.DoubleExpTimeStart output) => { DoubleEXPTimer.WaitTime = output.lengthOfTimeInSeconds; DoubleEXPTimer.Start(); }) + .Handle((in GameLogic.Output.SaveGame _) => + { + SaveFile.Save(); + AppRepo.OnExitGame(); + GetTree().Quit(); + // Back to title screen + }); + GameLogic.Start(); - GameLogic.Input(new GameLogic.Input.Initialize()); + GameLogic.Input(new GameLogic.Input.Initialize()); - this.Provide(); + this.Provide(); - PauseMenu.TransitionCompleted += OnPauseMenuTransitioned; - PauseMenu.UnpauseButtonPressed += PauseMenu_UnpauseButtonPressed; + PauseMenu.TransitionCompleted += OnPauseMenuTransitioned; + PauseMenu.UnpauseButtonPressed += PauseMenu_UnpauseButtonPressed; - InGameUI.UseTeleportPrompt.TeleportToNextFloor += UseTeleportPrompt_TeleportToNextFloor; - InGameUI.UseTeleportPrompt.CloseTeleportPrompt += UseTeleportPrompt_CloseTeleportPrompt; + InGameUI.UseTeleportPrompt.TeleportToNextFloor += UseTeleportPrompt_TeleportToNextFloor; + InGameUI.UseTeleportPrompt.CloseTeleportPrompt += UseTeleportPrompt_CloseTeleportPrompt; - FloorClearMenu.GoToNextFloor += FloorClearMenu_GoToNextFloor; - FloorClearMenu.SaveAndExit += FloorClearMenu_SaveAndExit; - FloorClearMenu.TransitionCompleted += FloorClearMenu_TransitionCompleted; + FloorClearMenu.GoToNextFloor += FloorClearMenu_GoToNextFloor; + FloorClearMenu.SaveAndExit += FloorClearMenu_SaveAndExit; + FloorClearMenu.TransitionCompleted += FloorClearMenu_TransitionCompleted; - GameEventDepot.RestorativePickedUp += GameEventDepot_RestorativePickedUp; + GameEventDepot.RestorativePickedUp += GameEventDepot_RestorativePickedUp; - DoubleEXPTimer.Timeout += DoubleEXPTimer_Timeout; + DoubleEXPTimer.Timeout += DoubleEXPTimer_Timeout; - _effectService = new EffectService(this, Player, Map); + _effectService = new EffectService(this, Player, Map); } public void LoadExistingGame() => SaveFile.Load().ContinueWith((_) => CallDeferred(nameof(FinishedLoadingSaveFile))); public void TogglePause() { - if (GameLogic.Value is GameLogic.State.Paused) - GameLogic.Input(new GameLogic.Input.UnpauseGame()); - else - GameLogic.Input(new GameLogic.Input.PauseGame()); + if (GameLogic.Value is GameLogic.State.Paused) + GameLogic.Input(new GameLogic.Input.UnpauseGame()); + else + GameLogic.Input(new GameLogic.Input.PauseGame()); } public void FloorExitReached() { - GameLogic.Input(new GameLogic.Input.AskForTeleport()); - GameEventDepot.OnTeleportEntered(); + GameLogic.Input(new GameLogic.Input.AskForTeleport()); + GameEventDepot.OnTeleportEntered(); } public async Task UseItem(InventoryItem item) { - switch (item) - { - case ConsumableItem consumableItem: - EnactConsumableItemEffects(consumableItem); - break; - case EffectItem effectItem: - EnactEffectItemEffects(effectItem); - break; - case ThrowableItem throwableItem: - EnactThrowableItemEffects(throwableItem); - break; - } + switch (item) + { + case ConsumableItem consumableItem: + EnactConsumableItemEffects(consumableItem); + break; + case EffectItem effectItem: + EnactEffectItemEffects(effectItem); + break; + case ThrowableItem throwableItem: + EnactThrowableItemEffects(throwableItem); + break; + } - await ToSignal(GetTree().CreateTimer(0.3f), "timeout"); + await ToSignal(GetTree().CreateTimer(0.3f), "timeout"); - RemoveItemOrSubtractFromItemCount(item); + RemoveItemOrSubtractFromItemCount(item); } public void DropItem(InventoryItem item) { - var droppedScene = GD.Load<PackedScene>("res://src/items/dropped/DroppedItem.tscn"); - var dropped = droppedScene.Instantiate<DroppedItem>(); - dropped.Item = item; - AddChild(dropped); - dropped.Drop(); + var droppedScene = GD.Load<PackedScene>("res://src/items/dropped/DroppedItem.tscn"); + var dropped = droppedScene.Instantiate<DroppedItem>(); + dropped.Item = item; + AddChild(dropped); + dropped.Drop(); } public void ThrowItem(InventoryItem item) { - var thrownScene = GD.Load<PackedScene>("res://src/items/thrown/ThrownItem.tscn"); - var thrown = thrownScene.Instantiate<ThrownItem>(); - thrown.ItemThatIsThrown = (InventoryItem)item; - AddChild(thrown); - thrown.Position += new Vector3(0, 1.5f, 0); - thrown.Throw(_effectService); + var thrownScene = GD.Load<PackedScene>("res://src/items/thrown/ThrownItem.tscn"); + var thrown = thrownScene.Instantiate<ThrownItem>(); + thrown.ItemThatIsThrown = (InventoryItem)item; + AddChild(thrown); + thrown.Position += new Vector3(0, 1.5f, 0); + thrown.Throw(_effectService); } public IDungeonFloor CurrentFloor => Map.CurrentFloor; @@ -282,18 +282,18 @@ public partial class Game : Node3D, IGame private void DropRestorative(Vector3 vector) { - var restorativeScene = GD.Load<PackedScene>("res://src/items/restorative/Restorative.tscn"); - var restorative = restorativeScene.Instantiate<Restorative>(); - AddChild(restorative); - restorative.GlobalPosition = vector; + var restorativeScene = GD.Load<PackedScene>("res://src/items/restorative/Restorative.tscn"); + var restorative = restorativeScene.Instantiate<Restorative>(); + AddChild(restorative); + restorative.GlobalPosition = vector; } private void UseTeleportPrompt_CloseTeleportPrompt() => GameLogic.Input(new GameLogic.Input.HideAskForTeleport()); private void UseTeleportPrompt_TeleportToNextFloor() { - GameLogic.Input(new GameLogic.Input.FloorExitReached()); - GameEventDepot.OnDungeonAThemeAreaEntered(); + GameLogic.Input(new GameLogic.Input.FloorExitReached()); + GameEventDepot.OnDungeonAThemeAreaEntered(); } private void PauseMenu_UnpauseButtonPressed() => GameLogic.Input(new GameLogic.Input.UnpauseGame()); @@ -302,13 +302,13 @@ public partial class Game : Node3D, IGame private void FloorClearMenu_TransitionCompleted() { - GameRepo.Resume(); - if (Player.EquippedWeapon.Value.ItemTag == ItemTag.BreaksOnChange) - Player.Unequip(Player.EquippedWeapon.Value); - if (Player.EquippedArmor.Value.ItemTag == ItemTag.BreaksOnChange) - Player.Unequip(Player.EquippedArmor.Value); - if (Player.EquippedAccessory.Value.ItemTag == ItemTag.BreaksOnChange) - Player.Unequip(Player.EquippedAccessory.Value); + GameRepo.Resume(); + if (Player.EquippedWeapon.Value.ItemTag == ItemTag.BreaksOnChange) + Player.Unequip(Player.EquippedWeapon.Value); + if (Player.EquippedArmor.Value.ItemTag == ItemTag.BreaksOnChange) + Player.Unequip(Player.EquippedArmor.Value); + if (Player.EquippedAccessory.Value.ItemTag == ItemTag.BreaksOnChange) + Player.Unequip(Player.EquippedAccessory.Value); } private void FloorClearMenu_GoToNextFloor() => GameLogic.Input(new GameLogic.Input.GoToNextFloor()); @@ -319,8 +319,8 @@ public partial class Game : Node3D, IGame private void SetPauseMode(bool isPaused) { - if (GetTree() != null) - GetTree().Paused = isPaused; + if (GetTree() != null) + GetTree().Paused = isPaused; } private void DoubleEXPTimer_Timeout() => GameRepo.EndDoubleExp(); @@ -335,88 +335,88 @@ public partial class Game : Node3D, IGame private void EnactConsumableItemEffects(ConsumableItem consumableItem) { - if (Player.Stats.CurrentHP == Player.Stats.MaximumHP && consumableItem.RaiseHPAmount > 0) - Player.RaiseHP(consumableItem.RaiseHPAmount); - if (Player.Stats.CurrentVT == Player.Stats.MaximumVT && consumableItem.RaiseVTAmount > 0) - Player.RaiseVT(consumableItem.RaiseVTAmount); + if (Player.Stats.CurrentHP == Player.Stats.MaximumHP && consumableItem.RaiseHPAmount > 0) + Player.RaiseHP(consumableItem.RaiseHPAmount); + if (Player.Stats.CurrentVT == Player.Stats.MaximumVT && consumableItem.RaiseVTAmount > 0) + Player.RaiseVT(consumableItem.RaiseVTAmount); - if (consumableItem.HealHPAmount > 0 && Player.Stats.CurrentHP != Player.Stats.MaximumHP) - Player.HealHP(consumableItem.HealHPAmount); - if (consumableItem.HealVTAmount > 0 && Player.Stats.CurrentVT != Player.Stats.MaximumVT) - Player.HealVT(consumableItem.HealVTAmount); + if (consumableItem.HealHPAmount > 0 && Player.Stats.CurrentHP != Player.Stats.MaximumHP) + Player.HealHP(consumableItem.HealHPAmount); + if (consumableItem.HealVTAmount > 0 && Player.Stats.CurrentVT != Player.Stats.MaximumVT) + Player.HealVT(consumableItem.HealVTAmount); } private void EnactEffectItemEffects(EffectItem effectItem) { - switch (effectItem.UsableItemTag) - { - case UsableItemTag.TeleportAllEnemiesToRoom: - _effectService.TeleportEnemiesToCurrentRoom(); - break; - case UsableItemTag.KillHalfEnemiesInRoom: - _effectService.KillHalfEnemiesInRoom(); - break; - case UsableItemTag.TurnAllEnemiesIntoHealingItem: - _effectService.TurnAllEnemiesInRoomIntoHealingItem(); - break; - case UsableItemTag.HealsAllInRoomToMaxHP: - _effectService.HealAllEnemiesAndPlayerInRoomToFull(); - break; - case UsableItemTag.AbsorbHPFromAllEnemiesInRoom: - _effectService.AbsorbHPFromAllEnemiesInRoom(); - break; - case UsableItemTag.DealElementalDamageToAllEnemiesInRoom: - _effectService.DealElementalDamageToAllEnemiesInRoom(ElementType.Hydric); - break; - case UsableItemTag.SwapHPAndVT: - _effectService.SwapHPandVT(); - break; - case UsableItemTag.RaiseCurrentWeaponAttack: - _effectService.RaiseCurrentWeaponAttack(); - break; - case UsableItemTag.RaiseCurrentDefenseArmor: - _effectService.RaiseCurrentArmorDefense(); - break; - case UsableItemTag.RaiseLevel: - _effectService.RaiseLevel(); - break; - case UsableItemTag.RandomEffect: - _effectService.RandomEffect(effectItem); - break; - } + switch (effectItem.UsableItemTag) + { + case UsableItemTag.TeleportAllEnemiesToRoom: + _effectService.TeleportEnemiesToCurrentRoom(); + break; + case UsableItemTag.KillHalfEnemiesInRoom: + _effectService.KillHalfEnemiesInRoom(); + break; + case UsableItemTag.TurnAllEnemiesIntoHealingItem: + _effectService.TurnAllEnemiesInRoomIntoHealingItem(); + break; + case UsableItemTag.HealsAllInRoomToMaxHP: + _effectService.HealAllEnemiesAndPlayerInRoomToFull(); + break; + case UsableItemTag.AbsorbHPFromAllEnemiesInRoom: + _effectService.AbsorbHPFromAllEnemiesInRoom(); + break; + case UsableItemTag.DealElementalDamageToAllEnemiesInRoom: + _effectService.DealElementalDamageToAllEnemiesInRoom(ElementType.Hydric); + break; + case UsableItemTag.SwapHPAndVT: + _effectService.SwapHPandVT(); + break; + case UsableItemTag.RaiseCurrentWeaponAttack: + _effectService.RaiseCurrentWeaponAttack(); + break; + case UsableItemTag.RaiseCurrentDefenseArmor: + _effectService.RaiseCurrentArmorDefense(); + break; + case UsableItemTag.RaiseLevel: + _effectService.RaiseLevel(); + break; + case UsableItemTag.RandomEffect: + _effectService.RandomEffect(effectItem); + break; + } } private void EnactThrowableItemEffects(ThrowableItem throwableItem) { - switch (throwableItem.ThrowableItemTag) - { - case ThrowableItemTag.DoubleExp: - GameRepo.StartDoubleEXP(TimeSpan.FromSeconds(30)); - break; - case ThrowableItemTag.TeleportToRandomLocation: - _effectService.TeleportToRandomRoom(Player); - InGameUI.CloseInventory(); - break; - case ThrowableItemTag.CanChangeAffinity: - _effectService.ChangeAffinity(throwableItem); - break; - case ThrowableItemTag.WarpToExitIfFound: - _effectService.WarpToExit(Player); - InGameUI.CloseInventory(); - break; - } + switch (throwableItem.ThrowableItemTag) + { + case ThrowableItemTag.DoubleExp: + GameRepo.StartDoubleEXP(TimeSpan.FromSeconds(30)); + break; + case ThrowableItemTag.TeleportToRandomLocation: + _effectService.TeleportToRandomRoom(Player); + InGameUI.CloseInventory(); + break; + case ThrowableItemTag.CanChangeAffinity: + _effectService.ChangeAffinity(throwableItem); + break; + case ThrowableItemTag.WarpToExitIfFound: + _effectService.WarpToExit(Player); + InGameUI.CloseInventory(); + break; + } - if (throwableItem.HealHPAmount > 0) - Player.HealHP(throwableItem.HealHPAmount); - if (throwableItem.HealVTAmount > 0) - Player.HealVT(throwableItem.HealVTAmount); + if (throwableItem.HealHPAmount > 0) + Player.HealHP(throwableItem.HealHPAmount); + if (throwableItem.HealVTAmount > 0) + Player.HealVT(throwableItem.HealVTAmount); } private void RemoveItemOrSubtractFromItemCount(InventoryItem item) { - if (item is IStackable stackableItem && stackableItem.Count > 1) - stackableItem.SetCount(stackableItem.Count - 1); - else - GameRepo.RemoveItemFromInventory(item); + if (item is IStackable stackableItem && stackableItem.Count > 1) + stackableItem.SetCount(stackableItem.Count - 1); + else + GameRepo.RemoveItemFromInventory(item); } } diff --git a/Zennysoft.Game.Ma/src/inventory_menu/InventoryMenu.cs b/Zennysoft.Game.Ma/src/inventory_menu/InventoryMenu.cs index 6a87b351..fb7aa19f 100644 --- a/Zennysoft.Game.Ma/src/inventory_menu/InventoryMenu.cs +++ b/Zennysoft.Game.Ma/src/inventory_menu/InventoryMenu.cs @@ -269,7 +269,6 @@ public partial class InventoryMenu : Control, IInventoryMenu if (currentItem is EquipableItem equipable) { - ; UseButton.Text = equipable.IsEquipped ? "Unequip" : "Equip"; ThrowButton.Disabled = equipable.IsEquipped ? true : false; ThrowButton.FocusMode = equipable.IsEquipped ? FocusModeEnum.None : FocusModeEnum.All; diff --git a/Zennysoft.Game.Ma/src/inventory_menu/ItemSlot.cs b/Zennysoft.Game.Ma/src/inventory_menu/ItemSlot.cs index 16d24680..936ef1b0 100644 --- a/Zennysoft.Game.Ma/src/inventory_menu/ItemSlot.cs +++ b/Zennysoft.Game.Ma/src/inventory_menu/ItemSlot.cs @@ -43,54 +43,54 @@ public partial class ItemSlot : HBoxContainer, IItemSlot public void OnReady() { - ItemName.Text = Item.ItemName; - ItemTexture.Texture = Item.GetTexture(); - Player.EquippedWeapon.Sync += EquipableItem_Sync; - Player.EquippedArmor.Sync += EquipableItem_Sync; - Player.EquippedAccessory.Sync += EquipableItem_Sync; + ItemName.Text = Item.ItemName; + ItemTexture.Texture = Item.GetTexture(); + Player.EquippedWeapon.Sync += EquipableItem_Sync; + Player.EquippedArmor.Sync += EquipableItem_Sync; + Player.EquippedAccessory.Sync += EquipableItem_Sync; - if (Item is IStackable stackableItem) - { - ItemCount.Text = $"{stackableItem.Count:D2}"; - ItemCount.Visible = true; - } + if (Item is IStackable stackableItem) + { + ItemCount.Text = $"{stackableItem.Count:D2}"; + ItemCount.Visible = true; + } } private void EquipableItem_Sync(EquipableItem obj) { - if (Item is EquipableItem equipableItem && equipableItem == obj) - { - SetEquippedSelectedItemStyle(); - } - if (Item is EquipableItem unequippedItem && unequippedItem != obj) - { - SetItemStyle(); - } + if (Item is EquipableItem equipableItem && equipableItem == obj) + { + SetEquippedSelectedItemStyle(); + } + if (Item is EquipableItem unequippedItem && unequippedItem != obj) + { + SetItemStyle(); + } } public void SetItemStyle() { - ItemName.LabelSettings = ItemFont; + ItemName.LabelSettings = ItemFont; } public void SetSelectedItemStyle() { - if (Item is EquipableItem equipableItem && equipableItem.IsEquipped) - { - ItemName.LabelSettings = SelectedEquippedItemFont; - } - else - { - ItemName.LabelSettings = SelectedItemFont; - } + if (Item is EquipableItem equipableItem && equipableItem.IsEquipped) + { + ItemName.LabelSettings = SelectedEquippedItemFont; + } + else + { + ItemName.LabelSettings = SelectedItemFont; + } } public void SetEquippedItemStyle() { - ItemName.LabelSettings = EquippedItemFont; + ItemName.LabelSettings = EquippedItemFont; } public void SetEquippedSelectedItemStyle() { - ItemName.LabelSettings = SelectedEquippedItemFont; + ItemName.LabelSettings = SelectedEquippedItemFont; } public InventoryItem Item { get; set; } = default!; diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/AeolicCaster.tres b/Zennysoft.Game.Ma/src/items/effect/resources/AeolicCaster.tres index f3d3d997..c4ad8120 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/AeolicCaster.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/AeolicCaster.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_yytis") -UsableItemTag = 0 +Name = "Aeolic Caster" +Description = "Deal Aeolic Damage to all enemies in the room." +UsableItemTag = 9 ElementalDamageType = 1 Name = "Aeolic Caster" Description = "Deal Aeolic Damage to all enemies in the room." -Texture = ExtResource("1_u0h2q") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_u0h2q") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/ClothResolution.tres b/Zennysoft.Game.Ma/src/items/effect/resources/ClothResolution.tres index 0a35a09e..b67808f6 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/ClothResolution.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/ClothResolution.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_53wiy") -UsableItemTag = 0 +Name = "Cloth Resolution" +Description = "Raises currently equipped armor's defense by 1." +UsableItemTag = 11 ElementalDamageType = 0 Name = "Cloth Resolution" Description = "Raises currently equipped armor's defense by 1." -Texture = ExtResource("1_1mm10") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_1mm10") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/Cosmos.tres b/Zennysoft.Game.Ma/src/items/effect/resources/Cosmos.tres index 1d443866..1b951713 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/Cosmos.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/Cosmos.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_n1557") -UsableItemTag = 0 +Name = "Cosmos" +Description = "Raises current Level by 1." +UsableItemTag = 12 ElementalDamageType = 0 Name = "Cosmos" Description = "Raises current Level by 1." -Texture = ExtResource("1_dn5pk") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_dn5pk") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/DevicBalance.tres b/Zennysoft.Game.Ma/src/items/effect/resources/DevicBalance.tres index e7e37615..a3f5a6d6 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/DevicBalance.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/DevicBalance.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_50lqu") -UsableItemTag = 0 +Name = "Devic Balance" +Description = "Kill half of all enemies in current room." +UsableItemTag = 6 ElementalDamageType = 0 Name = "Devic Balance" Description = "Kill half of all enemies in current room." -Texture = ExtResource("1_j7j7j") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_j7j7j") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/DevicRecall.tres b/Zennysoft.Game.Ma/src/items/effect/resources/DevicRecall.tres index 806960f4..1a3907ac 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/DevicRecall.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/DevicRecall.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_0uaie") -UsableItemTag = 0 +Name = "Devic Recall" +Description = "Teleports all enemies to current room." +UsableItemTag = 4 ElementalDamageType = 0 Name = "Devic Recall" Description = "Teleports all enemies to current room." -Texture = ExtResource("1_j462p") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_j462p") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/EntropicSeal.tres b/Zennysoft.Game.Ma/src/items/effect/resources/EntropicSeal.tres index a6686e25..8c748f9e 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/EntropicSeal.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/EntropicSeal.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_3l06v") -UsableItemTag = 0 +Name = "Entropic Seal" +Description = "Random effect." +UsableItemTag = 13 ElementalDamageType = 0 Name = "Entropic Seal" Description = "Random effect." -Texture = ExtResource("1_dbua5") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_dbua5") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/FerrousResolution.tres b/Zennysoft.Game.Ma/src/items/effect/resources/FerrousResolution.tres index 812995ee..71f3c365 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/FerrousResolution.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/FerrousResolution.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_uojwo") -UsableItemTag = 0 +Name = "Ferrous Resolution" +Description = "Raises currently equipped weapon's attack by 1." +UsableItemTag = 10 ElementalDamageType = 0 Name = "Ferrous Resolution" Description = "Raises currently equipped weapon's attack by 1." -Texture = ExtResource("1_6wt4w") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_6wt4w") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/FerrumCaster.tres b/Zennysoft.Game.Ma/src/items/effect/resources/FerrumCaster.tres index 2a7bdf05..480d3b71 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/FerrumCaster.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/FerrumCaster.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_i4na1") -UsableItemTag = 0 +Name = "Ferrum Caster" +Description = "Deal Ferrum Damage to all enemies in the room." +UsableItemTag = 9 ElementalDamageType = 5 Name = "Ferrum Caster" Description = "Deal Ferrum Damage to all enemies in the room." -Texture = ExtResource("1_xsfvc") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_xsfvc") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/HeavensRebellion.tres b/Zennysoft.Game.Ma/src/items/effect/resources/HeavensRebellion.tres index 6fcd9083..8a2d055d 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/HeavensRebellion.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/HeavensRebellion.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_drp30") -UsableItemTag = 0 +Name = "Heaven's Rebellion" +Description = "Heals self and all enemies in current room to maximum HP." +UsableItemTag = 8 ElementalDamageType = 0 Name = "Heaven's Rebellion" Description = "Heals self and all enemies in current room to maximum HP." -Texture = ExtResource("1_g0a3x") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_g0a3x") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/HydricCaster.tres b/Zennysoft.Game.Ma/src/items/effect/resources/HydricCaster.tres index 9b563cf4..a0dbfc8d 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/HydricCaster.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/HydricCaster.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_mj844") -UsableItemTag = 0 +Name = "Hydric Caster" +Description = "Deal Hydric Damage to all enemies in the room." +UsableItemTag = 9 ElementalDamageType = 3 Name = "Hydric Caster" Description = "Deal Hydric Damage to all enemies in the room." -Texture = ExtResource("1_8v5wn") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_8v5wn") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/IgneousCaster.tres b/Zennysoft.Game.Ma/src/items/effect/resources/IgneousCaster.tres index e085e761..f3edcb41 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/IgneousCaster.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/IgneousCaster.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_cl8lc") -UsableItemTag = 0 +Name = "Igneous Caster" +Description = "Deal Igneous Damage to all enemies in the room." +UsableItemTag = 9 ElementalDamageType = 4 Name = "Igneous Caster" Description = "Deal Igneous Damage to all enemies in the room." -Texture = ExtResource("1_3iam5") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_3iam5") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/Kyuuketsuki.tres b/Zennysoft.Game.Ma/src/items/effect/resources/Kyuuketsuki.tres index 37ab9f66..e5263b24 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/Kyuuketsuki.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/Kyuuketsuki.tres @@ -5,14 +5,17 @@ [resource] script = ExtResource("1_hxj1b") -UsableItemTags = Array[int]([7]) Name = "Kyuuketsuki" Description = "Absorbs HP from all enemies in the room." -Texture = ExtResource("1_0tcgy") +UsableItemTag = 7 +ElementalDamageType = 0 +Name = "Kyuuketsuki" +Description = "Absorbs HP from all enemies in the room." SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 -ItemTags = Array[int]([]) +ItemTag = 0 +Texture = ExtResource("1_0tcgy") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/Perspective.tres b/Zennysoft.Game.Ma/src/items/effect/resources/Perspective.tres index e027848d..034fc0bf 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/Perspective.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/Perspective.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_lq0bc") -UsableItemTag = 0 +Name = "Perspective" +Description = "Swap current HP and VT." +UsableItemTag = 3 ElementalDamageType = 0 Name = "Perspective" Description = "Swap current HP and VT." -Texture = ExtResource("1_31uf7") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_31uf7") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/SineMorphization.tres b/Zennysoft.Game.Ma/src/items/effect/resources/SineMorphization.tres index 806dbd43..01079d8d 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/SineMorphization.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/SineMorphization.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("1_ksb1c") -UsableItemTag = 0 +Name = "Sine Morphization" +Description = "Turns all enemies in room to healing item." +UsableItemTag = 5 ElementalDamageType = 0 Name = "Sine Morphization" Description = "Turns all enemies in room to healing item." -Texture = ExtResource("1_vqhky") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_vqhky") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/items/effect/resources/TelluricCaster.tres b/Zennysoft.Game.Ma/src/items/effect/resources/TelluricCaster.tres index 14c76744..d5ada803 100644 --- a/Zennysoft.Game.Ma/src/items/effect/resources/TelluricCaster.tres +++ b/Zennysoft.Game.Ma/src/items/effect/resources/TelluricCaster.tres @@ -5,15 +5,17 @@ [resource] script = ExtResource("2_38yjb") -UsableItemTag = 0 +Name = "Telluric Caster" +Description = "Deal Telluric Damage to all enemies in the room." +UsableItemTag = 9 ElementalDamageType = 2 Name = "Telluric Caster" Description = "Deal Telluric Damage to all enemies in the room." -Texture = ExtResource("1_x62ct") SpawnRate = 0.5 ThrowSpeed = 12.0 HealHPAmount = 0 HealVTAmount = 0 ThrowDamage = 5 ItemTag = 0 +Texture = ExtResource("1_x62ct") metadata/_custom_type_script = "uid://b5w4iw4iqmxtn" diff --git a/Zennysoft.Game.Ma/src/map/Map.cs b/Zennysoft.Game.Ma/src/map/Map.cs index 11f30dbe..2e7c3909 100644 --- a/Zennysoft.Game.Ma/src/map/Map.cs +++ b/Zennysoft.Game.Ma/src/map/Map.cs @@ -58,66 +58,66 @@ public partial class Map : Node3D, IMap public void OnResolved() { - FloorScenes = []; + FloorScenes = []; - MapChunk = new SaveChunk<MapData>( - onSave: (chunk) => new MapData() - { - FloorScenes = FloorScenes, - }, - onLoad: (chunk, data) => - { - FloorScenes = data.FloorScenes; - } - ); + MapChunk = new SaveChunk<MapData>( + onSave: (chunk) => new MapData() + { + FloorScenes = FloorScenes, + }, + onLoad: (chunk, data) => + { + FloorScenes = data.FloorScenes; + } + ); - GameChunk.AddChunk(MapChunk); + GameChunk.AddChunk(MapChunk); - this.Provide(); + this.Provide(); } public void LoadMap() { - foreach (var floor in _floors) - FloorScenes.Add(floor.ResourcePath); + foreach (var floor in _floors) + FloorScenes.Add(floor.ResourcePath); - LoadFloor(); - CurrentFloor.InitializeDungeon(); - var transform = GetPlayerSpawnPosition(); - Player.TeleportPlayer(transform); - CurrentFloor.FloorIsLoaded = true; - Game.NextFloorLoaded(); + LoadFloor(); + CurrentFloor.InitializeDungeon(); + var transform = GetPlayerSpawnPosition(); + Player.TeleportPlayer(transform); + CurrentFloor.FloorIsLoaded = true; + Game.NextFloorLoaded(); } public void SpawnNextFloor() { - var oldFloor = CurrentFloor; - oldFloor.CallDeferred(MethodName.QueueFree, []); - LoadFloor(); - CurrentFloor.InitializeDungeon(); - var transform = GetPlayerSpawnPosition(); - Player.TeleportPlayer(transform); - CurrentFloor.FloorIsLoaded = true; - Game.NextFloorLoaded(); - CurrentFloorNumber += 1; + var oldFloor = CurrentFloor; + oldFloor.CallDeferred(MethodName.QueueFree, []); + LoadFloor(); + CurrentFloor.InitializeDungeon(); + var transform = GetPlayerSpawnPosition(); + Player.TeleportPlayer(transform); + CurrentFloor.FloorIsLoaded = true; + Game.NextFloorLoaded(); + CurrentFloorNumber += 1; } public IDungeonRoom GetPlayersCurrentRoom() { - var rooms = CurrentFloor.Rooms; - var playersRoom = rooms.SingleOrDefault(x => x.IsPlayerInRoom); - return playersRoom; + var rooms = CurrentFloor.Rooms; + var playersRoom = rooms.SingleOrDefault(x => x.IsPlayerInRoom); + return playersRoom; } public Transform3D GetPlayerSpawnPosition() => CurrentFloor.GetPlayerSpawnPoint(); private void LoadFloor() { - var currentFloorScene = FloorScenes.First(); - var instantiator = new Instantiator(GetTree()); - var loadedScene = instantiator.LoadAndInstantiate<Node3D>(currentFloorScene); - AddChild(loadedScene); - CurrentFloor = (IDungeonFloor)loadedScene; - FloorScenes.Remove(currentFloorScene); + var currentFloorScene = FloorScenes.First(); + var instantiator = new Instantiator(GetTree()); + var loadedScene = instantiator.LoadAndInstantiate<Node3D>(currentFloorScene); + AddChild(loadedScene); + CurrentFloor = (IDungeonFloor)loadedScene; + FloorScenes.Remove(currentFloorScene); } } diff --git a/Zennysoft.Game.Ma/src/map/Map.tscn b/Zennysoft.Game.Ma/src/map/Map.tscn index 155ac271..62efaf03 100644 --- a/Zennysoft.Game.Ma/src/map/Map.tscn +++ b/Zennysoft.Game.Ma/src/map/Map.tscn @@ -14,6 +14,6 @@ [node name="Map" type="Node3D"] script = ExtResource("1_bw70o") -_floors = Array[PackedScene]([ExtResource("2_0m8h8"), ExtResource("2_merfv"), ExtResource("4_8y0oy"), ExtResource("5_uag72"), ExtResource("6_55rmo"), ExtResource("7_f6kwn"), ExtResource("8_ne2vg"), ExtResource("9_abpbr"), ExtResource("10_caf7v"), ExtResource("11_y74f3")]) +_floors = Array[PackedScene]([ExtResource("2_0m8h8"), ExtResource("11_y74f3"), ExtResource("2_merfv"), ExtResource("4_8y0oy"), ExtResource("5_uag72"), ExtResource("6_55rmo"), ExtResource("7_f6kwn"), ExtResource("8_ne2vg"), ExtResource("9_abpbr"), ExtResource("10_caf7v")]) [node name="WorldEnvironment" type="WorldEnvironment" parent="."] diff --git a/Zennysoft.Game.Ma/src/map/dungeon/code/DungeonFloor.cs b/Zennysoft.Game.Ma/src/map/dungeon/code/DungeonFloor.cs index 0bfbd85f..60fc91c3 100644 --- a/Zennysoft.Game.Ma/src/map/dungeon/code/DungeonFloor.cs +++ b/Zennysoft.Game.Ma/src/map/dungeon/code/DungeonFloor.cs @@ -24,41 +24,41 @@ public partial class DungeonFloor : Node3D, IDungeonFloor public void InitializeDungeon() { - Rooms = []; - Rooms = FindAllDungeonRooms([.. GetChildren()], Rooms); - _playerSpawnPoint = RandomizePlayerSpawnPoint(); - var monsterRooms = Rooms.OfType<MonsterRoom>(); - foreach (var room in monsterRooms) - room.SpawnEnemies(EnemyDatabase); - DungeonGenerator.EmitSignal("done_generating"); + Rooms = []; + Rooms = FindAllDungeonRooms([.. GetChildren()], Rooms); + _playerSpawnPoint = RandomizePlayerSpawnPoint(); + var monsterRooms = Rooms.OfType<MonsterRoom>(); + foreach (var room in monsterRooms) + room.SpawnEnemies(EnemyDatabase); + DungeonGenerator.EmitSignal("done_generating"); } public Transform3D GetPlayerSpawnPoint() => new Transform3D(_playerSpawnPoint.Basis, new Vector3(_playerSpawnPoint.Origin.X, -1.75f, _playerSpawnPoint.Origin.Z)); private Transform3D RandomizePlayerSpawnPoint() { - var randomSpawnLocations = Rooms - .OfType<MonsterRoom>() - .Select(x => x.PlayerSpawn); - var godotCollection = new Godot.Collections.Array<Marker3D>(randomSpawnLocations); - var result = godotCollection.PickRandom(); - return result.GlobalTransform; + var randomSpawnLocations = Rooms + .OfType<MonsterRoom>() + .Select(x => x.PlayerSpawn); + var godotCollection = new Godot.Collections.Array<Marker3D>(randomSpawnLocations); + var result = godotCollection.PickRandom(); + return result.GlobalTransform; } private static ImmutableList<IDungeonRoom> FindAllDungeonRooms(List<Node> nodesToSearch, ImmutableList<IDungeonRoom> roomsFound) { - if (nodesToSearch.Count == 0) - return roomsFound; + if (nodesToSearch.Count == 0) + return roomsFound; - foreach (var node in nodesToSearch) - { - if (node is IDungeonRoom dungeonRoom) - roomsFound = roomsFound.Add(dungeonRoom); + foreach (var node in nodesToSearch) + { + if (node is IDungeonRoom dungeonRoom) + roomsFound = roomsFound.Add(dungeonRoom); - if (node.HasSignal("dungeon_done_generating")) - node.EmitSignal("dungeon_done_generating"); - } + if (node.HasSignal("dungeon_done_generating")) + node.EmitSignal("dungeon_done_generating"); + } - return FindAllDungeonRooms(nodesToSearch.SelectMany(x => x.GetChildren()).ToList(), roomsFound); + return FindAllDungeonRooms(nodesToSearch.SelectMany(x => x.GetChildren()).ToList(), roomsFound); } } diff --git a/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/16. Seshat's Room.tscn b/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/16. Seshat's Room.tscn index 7fba0ce5..152a7bbb 100644 --- a/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/16. Seshat's Room.tscn +++ b/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/16. Seshat's Room.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=99 format=4 uid="uid://crf30tibwsnri"] +[gd_scene load_steps=100 format=4 uid="uid://crf30tibwsnri"] [ext_resource type="Script" uid="uid://ce73fuh74l81l" path="res://addons/SimpleDungeons/DungeonRoom3D.gd" id="1_bglxp"] [ext_resource type="Script" uid="uid://dhollu4j3pynq" path="res://src/map/dungeon/code/MonsterRoom.cs" id="2_5aadh"] @@ -30,6 +30,7 @@ [ext_resource type="Texture2D" uid="uid://da42up4mv46kj" path="res://src/map/dungeon/models/Set A/16. Seshat's Room/16_A1_SESHATS_ROOM_claude-monet.jpg" id="24_t8nkf"] [ext_resource type="Material" uid="uid://bsafm3t4drpl" path="res://src/map/dungeon/textures/MinimapTexture.tres" id="25_25af7"] [ext_resource type="Texture2D" uid="uid://b5hbvxvxl8gn6" path="res://src/map/dungeon/models/Set A/16. Seshat's Room/16_A1_SESHATS_ROOM_Single-Peacock-Feather-Transparent-Free-PNG.png" id="25_77sl0"] +[ext_resource type="Texture2D" uid="uid://bt1n54xtem20m" path="res://src/npc/Seshat/seshat.png" id="31_1cfoy"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_qg4wx"] resource_name = "Material.024" @@ -1073,6 +1074,7 @@ skeleton = NodePath("") [node name="ROOM" type="MeshInstance3D" parent="Model/Seshat\'s Room/16_A1_SESHATS_ROOM"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.18809, 14.7067, 47.3259) +visible = false mesh = SubResource("ArrayMesh_1wv4u") skeleton = NodePath("") @@ -1280,3 +1282,9 @@ mesh = SubResource("PlaneMesh_s0txx") skeleton = NodePath("../..") [node name="NPC" type="Node3D" parent="."] + +[node name="Sprite3D" type="Sprite3D" parent="NPC"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.967271, 0.0981301, -0.047853) +pixel_size = 0.005 +billboard = 2 +texture = ExtResource("31_1cfoy") diff --git a/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/17. Gesthemii's Room.tscn b/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/17. Gesthemii's Room.tscn index 4f2041da..05981bf8 100644 --- a/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/17. Gesthemii's Room.tscn +++ b/Zennysoft.Game.Ma/src/map/dungeon/rooms/Set A/17. Gesthemii's Room.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=78 format=4 uid="uid://cw33vpar237pm"] +[gd_scene load_steps=79 format=4 uid="uid://cw33vpar237pm"] [ext_resource type="Script" uid="uid://ce73fuh74l81l" path="res://addons/SimpleDungeons/DungeonRoom3D.gd" id="1_m4la0"] [ext_resource type="Script" uid="uid://dhollu4j3pynq" path="res://src/map/dungeon/code/MonsterRoom.cs" id="2_56tk6"] @@ -28,6 +28,7 @@ [ext_resource type="Texture2D" uid="uid://dgmxl2uu287k4" path="res://src/map/dungeon/models/Set A/17. Gesthemii's Room/17_A1_GESTHEMIIS_ROOM_outside_desert.png" id="21_6gfr6"] [ext_resource type="Texture2D" uid="uid://d0q55hxiafsye" path="res://src/map/dungeon/models/Set A/17. Gesthemii's Room/17_A1_GESTHEMIIS_ROOM_RUG_GETHSTEMI.png" id="22_uayj1"] [ext_resource type="Texture2D" uid="uid://dix6yhpcu52jv" path="res://src/map/dungeon/models/Set A/17. Gesthemii's Room/17_A1_GESTHEMIIS_ROOM_mottled.png" id="23_rrfdh"] +[ext_resource type="PackedScene" uid="uid://b5tl2jawmvwtd" path="res://src/npc/Gesthesemii/Gesthemii.tscn" id="29_43nhx"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_loa1y"] resource_name = "Material.014" @@ -1036,3 +1037,8 @@ visible = false layers = 2 mesh = SubResource("PlaneMesh_s0txx") skeleton = NodePath("../..") + +[node name="NPC" type="Node3D" parent="."] + +[node name="Gesthemii" parent="NPC" instance=ExtResource("29_43nhx")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0431279, -0.710172, 2.12679) diff --git a/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png b/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png new file mode 100644 index 00000000..449da542 Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png.import b/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png.import new file mode 100644 index 00000000..3ddbdf6f --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Clalo/CLALO_ver.2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://loeqa6twpa3q" +path="res://.godot/imported/CLALO_ver.2.png-bd5f373ada3f179eabb413cfe6fbf703.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Clalo/CLALO_ver.2.png" +dest_files=["res://.godot/imported/CLALO_ver.2.png-bd5f373ada3f179eabb413cfe6fbf703.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Gesthesemii/Gesthemii.tscn b/Zennysoft.Game.Ma/src/npc/Gesthesemii/Gesthemii.tscn new file mode 100644 index 00000000..3d7c8e53 --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Gesthesemii/Gesthemii.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=7 format=3 uid="uid://b5tl2jawmvwtd"] + +[ext_resource type="Script" uid="uid://dcqssoikr3pl7" path="res://src/npc/Npc.cs" id="1_d5tpe"] +[ext_resource type="Resource" uid="uid://lao0opxww3ib" path="res://src/dialog/Dialogue.dialogue" id="2_xassj"] +[ext_resource type="Texture2D" uid="uid://gtylrk5yxgnp" path="res://src/npc/Gesthesemii/gesthesemii2.png" id="3_xassj"] + +[sub_resource type="CylinderShape3D" id="CylinderShape3D_wfhgc"] +radius = 3.0 + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_kg3qv"] +radius = 0.837722 +height = 2.8375 + +[sub_resource type="CylinderShape3D" id="CylinderShape3D_nwuwj"] +height = 2.24425 +radius = 1.941 + +[node name="Gesthemii" type="Node3D"] +script = ExtResource("1_d5tpe") +Dialogue = ExtResource("2_xassj") + +[node name="Sprite" type="Sprite3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) +gi_mode = 0 +pixel_size = 0.008 +billboard = 2 +double_sided = false +alpha_cut = 2 +texture_filter = 0 +texture = ExtResource("3_xassj") + +[node name="DialogueZone" type="Area3D" parent="."] +unique_name_in_owner = true +collision_layer = 2 +collision_mask = 2 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="DialogueZone"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00131226, 0, -0.00723076) +shape = SubResource("CylinderShape3D_wfhgc") + +[node name="Collision" type="RigidBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.105047, -0.0490516, 0) +collision_mask = 0 +gravity_scale = 0.0 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Collision"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.169075, 0, 0) +shape = SubResource("CapsuleShape3D_kg3qv") + +[node name="Hitbox" type="Area3D" parent="."] +unique_name_in_owner = true +collision_layer = 16 +collision_mask = 16 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Hitbox"] +shape = SubResource("CylinderShape3D_nwuwj") diff --git a/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png new file mode 100644 index 00000000..2b6e78f7 Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png.import b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png.import new file mode 100644 index 00000000..be675bb2 --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gesthesemii2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://gtylrk5yxgnp" +path="res://.godot/imported/gesthesemii2.png-3d63416010cd48d84ed561821c69e40e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Gesthesemii/gesthesemii2.png" +dest_files=["res://.godot/imported/gesthesemii2.png-3d63416010cd48d84ed561821c69e40e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png new file mode 100644 index 00000000..4a2ccf97 Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png.import b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png.import new file mode 100644 index 00000000..5a320e88 --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Gesthesemii/gethesemii.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b1mj13j2peypc" +path="res://.godot/imported/gethesemii.png-8d75036e08b2192c219748544bae288b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Gesthesemii/gethesemii.png" +dest_files=["res://.godot/imported/gethesemii.png-8d75036e08b2192c219748544bae288b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png b/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png new file mode 100644 index 00000000..32d45312 Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png.import b/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png.import new file mode 100644 index 00000000..40490d0c --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Goddess of Guidance/goddess-of-guidance.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cojh7jjk70psy" +path="res://.godot/imported/goddess-of-guidance.png-500677be7008aa69424d184c70c7ea1d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Goddess of Guidance/goddess-of-guidance.png" +dest_files=["res://.godot/imported/goddess-of-guidance.png-500677be7008aa69424d184c70c7ea1d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Npc.cs b/Zennysoft.Game.Ma/src/npc/Npc.cs index b6e2d529..58ab454c 100644 --- a/Zennysoft.Game.Ma/src/npc/Npc.cs +++ b/Zennysoft.Game.Ma/src/npc/Npc.cs @@ -20,45 +20,45 @@ public partial class Npc : Node3D public void OnReady() { - SetPhysicsProcess(true); - DialogueZone.BodyEntered += DialogueZone_BodyEntered; - DialogueZone.BodyExited += DialogueZone_BodyExited; - Hitbox.AreaEntered += Hitbox_AreaEntered; + SetPhysicsProcess(true); + DialogueZone.BodyEntered += DialogueZone_BodyEntered; + DialogueZone.BodyExited += DialogueZone_BodyExited; + Hitbox.AreaEntered += Hitbox_AreaEntered; } private void Hitbox_AreaEntered(Area3D area) { - DialogueController.ShowDialogue(Dialogue, "hit"); + DialogueController.ShowDialogue(Dialogue, "hit"); } private void Hitbox_BodyEntered(Node body) { - DialogueController.ShowDialogue(Dialogue, "hit"); + DialogueController.ShowDialogue(Dialogue, "hit"); } private void DialogueZone_BodyExited(Node3D body) { - _isInDialogueZone = false; - DialogueController.Interrupt(); + _isInDialogueZone = false; + DialogueController.Interrupt(); } private void DialogueZone_BodyEntered(Node3D body) { - _isInDialogueZone = true; + _isInDialogueZone = true; } public override void _UnhandledInput(InputEvent @event) { - if (@event.IsActionPressed(GameInputs.Interact) && _isInDialogueZone) - { - if (_isIntroductionComplete) - DialogueController.ShowDialogue(Dialogue, "general"); - else - { - DialogueController.ShowDialogue(Dialogue, "introduction"); - _isIntroductionComplete = true; - } - } + if (@event.IsActionPressed(GameInputs.Interact) && _isInDialogueZone) + { + if (_isIntroductionComplete) + DialogueController.ShowDialogue(Dialogue, "general"); + else + { + DialogueController.ShowDialogue(Dialogue, "introduction"); + _isIntroductionComplete = true; + } + } } } diff --git a/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG b/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG new file mode 100644 index 00000000..df9e50ba Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG differ diff --git a/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG.import b/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG.import new file mode 100644 index 00000000..47de60bd --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Puer/PUER ver.2.PNG.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdn7fs4ehxvf0" +path="res://.godot/imported/PUER ver.2.PNG-d994334c306bcf9f2e5bdd3105f5e6d5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Puer/PUER ver.2.PNG" +dest_files=["res://.godot/imported/PUER ver.2.PNG-d994334c306bcf9f2e5bdd3105f5e6d5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png b/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png new file mode 100644 index 00000000..302a438a Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png.import b/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png.import new file mode 100644 index 00000000..ced30fd2 --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Rat/rat_ver,2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dffyycokg2vcr" +path="res://.godot/imported/rat_ver,2.png-f6e59ef09a403a1c5118efc205d6e0aa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Rat/rat_ver,2.png" +dest_files=["res://.godot/imported/rat_ver,2.png-f6e59ef09a403a1c5118efc205d6e0aa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png b/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png new file mode 100644 index 00000000..b8fee553 Binary files /dev/null and b/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png differ diff --git a/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png.import b/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png.import new file mode 100644 index 00000000..f6511160 --- /dev/null +++ b/Zennysoft.Game.Ma/src/npc/Seshat/seshat.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bt1n54xtem20m" +path="res://.godot/imported/seshat.png-02a4b362f584ce2171e108b824412341.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/npc/Seshat/seshat.png" +dest_files=["res://.godot/imported/seshat.png-02a4b362f584ce2171e108b824412341.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/Zennysoft.Game.Ma/src/player/Player.cs b/Zennysoft.Game.Ma/src/player/Player.cs index ab3fd085..56279a30 100644 --- a/Zennysoft.Game.Ma/src/player/Player.cs +++ b/Zennysoft.Game.Ma/src/player/Player.cs @@ -20,7 +20,7 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<ISaveChunk<Play private PlayerLogic.IBinding PlayerBinding { get; set; } = default!; - [Dependency] private IGameRepo _gameRepo => this.DependOn<IGameRepo>(); + [Dependency] private IGameRepo _gameRepo => this.DependOn<IGameRepo>(() => new GameRepo()); #endregion #region Save @@ -106,508 +106,508 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<ISaveChunk<Play #region Initialization public void Initialize() { - AnimationPlayer.AnimationFinished += OnAnimationFinished; - _expToNextLevel = new Dictionary<int, int> - { - { 2, 12 }, - { 3, 39 }, - { 4, 87 }, - { 5, 162 }, - { 6, 270 }, - { 7, 417 }, - { 8, 609 } - }; + AnimationPlayer.AnimationFinished += OnAnimationFinished; + _expToNextLevel = new Dictionary<int, int> + { + { 2, 12 }, + { 3, 39 }, + { 4, 87 }, + { 5, 162 }, + { 6, 270 }, + { 7, 417 }, + { 8, 609 } + }; } public void Setup() { - var container = new SimpleInjector.Container(); - container.Register<IPlayerLogic, PlayerLogic>(Lifestyle.Singleton); - container.Verify(); + var container = new SimpleInjector.Container(); + container.Register<IPlayerLogic, PlayerLogic>(Lifestyle.Singleton); + container.Verify(); - Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration }; - Stats = new PlayerStatController(); - Stats.Init( - new PlayerStats - { - CurrentHP = PlayerStatResource.CurrentHP, - MaximumHP = PlayerStatResource.MaximumHP, - CurrentVT = PlayerStatResource.CurrentVT, - MaximumVT = PlayerStatResource.MaximumVT, - CurrentAttack = PlayerStatResource.CurrentAttack, - BonusAttack = PlayerStatResource.BonusAttack, - MaxAttack = PlayerStatResource.MaxAttack, - CurrentDefense = PlayerStatResource.CurrentDefense, - BonusDefense = PlayerStatResource.BonusDefense, - MaxDefense = PlayerStatResource.MaxDefense, - CurrentExp = PlayerStatResource.CurrentExp, - CurrentLevel = PlayerStatResource.CurrentLevel, - ExpToNextLevel = PlayerStatResource.ExpToNextLevel, - Luck = PlayerStatResource.Luck - }); + Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration }; + Stats = new PlayerStatController(); + Stats.Init( + new PlayerStats + { + CurrentHP = PlayerStatResource.CurrentHP, + MaximumHP = PlayerStatResource.MaximumHP, + CurrentVT = PlayerStatResource.CurrentVT, + MaximumVT = PlayerStatResource.MaximumVT, + CurrentAttack = PlayerStatResource.CurrentAttack, + BonusAttack = PlayerStatResource.BonusAttack, + MaxAttack = PlayerStatResource.MaxAttack, + CurrentDefense = PlayerStatResource.CurrentDefense, + BonusDefense = PlayerStatResource.BonusDefense, + MaxDefense = PlayerStatResource.MaxDefense, + CurrentExp = PlayerStatResource.CurrentExp, + CurrentLevel = PlayerStatResource.CurrentLevel, + ExpToNextLevel = PlayerStatResource.ExpToNextLevel, + Luck = PlayerStatResource.Luck + }); - Inventory = new Inventory(); + Inventory = new Inventory(); - PlayerLogic = container.GetInstance<IPlayerLogic>(); - PlayerLogic.Set(this as IPlayer); - PlayerLogic.Set(Settings); - PlayerLogic.Set(Stats); - PlayerLogic.Set(_gameRepo); + PlayerLogic = container.GetInstance<IPlayerLogic>(); + PlayerLogic.Set(this as IPlayer); + PlayerLogic.Set(Settings); + PlayerLogic.Set(Stats); + PlayerLogic.Set(_gameRepo); - var defaultWeapon = new Weapon(); - defaultWeapon.Stats = _defaultWeapon; - var defaultArmor = new Armor(); - defaultArmor.Stats = _defaultArmor; - Inventory.TryAdd(defaultWeapon); - Inventory.TryAdd(defaultArmor); + var defaultWeapon = new Weapon(); + defaultWeapon.Stats = _defaultWeapon; + var defaultArmor = new Armor(); + defaultArmor.Stats = _defaultArmor; + Inventory.TryAdd(defaultWeapon); + Inventory.TryAdd(defaultArmor); - EquippedWeapon.Sync += EquippedWeapon_Sync; - EquippedArmor.Sync += EquippedArmor_Sync; - EquippedAccessory.Sync += EquippedAccessory_Sync; - Stats.CurrentHP.Sync += CurrentHP_Sync; - Stats.CurrentExp.Sync += CurrentEXP_Sync; + EquippedWeapon.Sync += EquippedWeapon_Sync; + EquippedArmor.Sync += EquippedArmor_Sync; + EquippedAccessory.Sync += EquippedAccessory_Sync; + Stats.CurrentHP.Sync += CurrentHP_Sync; + Stats.CurrentExp.Sync += CurrentEXP_Sync; - Equip(defaultWeapon); - Equip(defaultArmor); + Equip(defaultWeapon); + Equip(defaultArmor); - HealthTimer.WaitTime = _healthTimerWaitTime; - HealthTimer.Timeout += OnHealthTimerTimeout; - Hitbox.AreaEntered += Hitbox_AreaEntered; - CollisionDetector.AreaEntered += CollisionDetector_AreaEntered; + HealthTimer.WaitTime = _healthTimerWaitTime; + HealthTimer.Timeout += OnHealthTimerTimeout; + Hitbox.AreaEntered += Hitbox_AreaEntered; + CollisionDetector.AreaEntered += CollisionDetector_AreaEntered; } public void OnResolved() { - PlayerChunk = new SaveChunk<PlayerData>( - onSave: (chunk) => new PlayerData() - { - PlayerStats = new PlayerStats() - { - CurrentHP = Stats.CurrentHP.Value, - MaximumHP = Stats.MaximumHP.Value, - CurrentVT = Stats.CurrentVT.Value, - MaximumVT = Stats.MaximumVT.Value, - CurrentAttack = Stats.CurrentAttack.Value, - BonusAttack = Stats.BonusAttack.Value, - MaxAttack = Stats.MaxAttack.Value, - CurrentDefense = Stats.CurrentDefense.Value, - BonusDefense = Stats.BonusDefense.Value, - MaxDefense = Stats.MaxDefense.Value, - CurrentExp = Stats.CurrentExp.Value, - CurrentLevel = Stats.CurrentLevel.Value, - ExpToNextLevel = Stats.ExpToNextLevel.Value, - Luck = Stats.Luck.Value - }, - Inventory = Inventory - }, - onLoad: (chunk, data) => - { - Stats.Init(data.PlayerStats); - Inventory = data.Inventory; - } - ); + PlayerChunk = new SaveChunk<PlayerData>( + onSave: (chunk) => new PlayerData() + { + PlayerStats = new PlayerStats() + { + CurrentHP = Stats.CurrentHP.Value, + MaximumHP = Stats.MaximumHP.Value, + CurrentVT = Stats.CurrentVT.Value, + MaximumVT = Stats.MaximumVT.Value, + CurrentAttack = Stats.CurrentAttack.Value, + BonusAttack = Stats.BonusAttack.Value, + MaxAttack = Stats.MaxAttack.Value, + CurrentDefense = Stats.CurrentDefense.Value, + BonusDefense = Stats.BonusDefense.Value, + MaxDefense = Stats.MaxDefense.Value, + CurrentExp = Stats.CurrentExp.Value, + CurrentLevel = Stats.CurrentLevel.Value, + ExpToNextLevel = Stats.ExpToNextLevel.Value, + Luck = Stats.Luck.Value + }, + Inventory = Inventory + }, + onLoad: (chunk, data) => + { + Stats.Init(data.PlayerStats); + Inventory = data.Inventory; + } + ); - PlayerBinding = PlayerLogic.Bind(); + PlayerBinding = PlayerLogic.Bind(); - PlayerBinding - .Handle((in PlayerLogic.Output.Animations.Attack output) => - { - if (PlayerIsHittingGeometry()) - { - AnimationPlayer.Play("hit_wall"); - _gameRepo.OnPlayerAttackedWall(); - } - else - { - var attackSpeed = ((Weapon)EquippedWeapon.Value).AttackSpeed; - AnimationPlayer.SetSpeedScale((float)attackSpeed); - AnimationPlayer.Play("attack"); - _gameRepo.OnPlayerAttack(); - } - }) - .Handle((in PlayerLogic.Output.ThrowItem output) => - { - }) - .Handle((in PlayerLogic.Output.Move output) => - { - Move(output.delta); - }); + PlayerBinding + .Handle((in PlayerLogic.Output.Animations.Attack output) => + { + if (PlayerIsHittingGeometry()) + { + AnimationPlayer.Play("hit_wall"); + _gameRepo.OnPlayerAttackedWall(); + } + else + { + var attackSpeed = ((Weapon)EquippedWeapon.Value).AttackSpeed; + AnimationPlayer.SetSpeedScale((float)attackSpeed); + AnimationPlayer.Play("attack"); + _gameRepo.OnPlayerAttack(); + } + }) + .Handle((in PlayerLogic.Output.ThrowItem output) => + { + }) + .Handle((in PlayerLogic.Output.Move output) => + { + Move(output.delta); + }); - GameChunk.AddChunk(PlayerChunk); + GameChunk.AddChunk(PlayerChunk); - PlayerLogic.Start(); - this.Provide(); + PlayerLogic.Start(); + this.Provide(); } public void OnReady() { - SetPhysicsProcess(true); - SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2; + SetPhysicsProcess(true); + SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2; } #endregion public void Attack() { - PlayerLogic.Input(new PlayerLogic.Input.Attack()); + PlayerLogic.Input(new PlayerLogic.Input.Attack()); } public void PlayerPause() { - Game.TogglePause(); + Game.TogglePause(); } public void RaiseHP(int amountToRaise) { - Stats.SetMaximumHP(Stats.MaximumHP.Value + amountToRaise); - Stats.SetCurrentHP(Stats.MaximumHP.Value); - _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXHP Up."); + Stats.SetMaximumHP(Stats.MaximumHP.Value + amountToRaise); + Stats.SetCurrentHP(Stats.MaximumHP.Value); + _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXHP Up."); } public void HealHP(int amountToRestore) { - Stats.SetCurrentHP(Stats.CurrentHP.Value + amountToRestore); - var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; - _gameRepo.AnnounceMessageInInventory($"{raiseString}HP Restored."); + Stats.SetCurrentHP(Stats.CurrentHP.Value + amountToRestore); + var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; + _gameRepo.AnnounceMessageInInventory($"{raiseString}HP Restored."); } public void RaiseVT(int amountToRaise) { - if (Stats.CurrentVT == Stats.MaximumVT) - { - Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise); - Stats.SetCurrentVT(Stats.MaximumVT.Value); - _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXVT Up."); - } + if (Stats.CurrentVT == Stats.MaximumVT) + { + Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise); + Stats.SetCurrentVT(Stats.MaximumVT.Value); + _gameRepo.AnnounceMessageInInventory($"{amountToRaise}MAXVT Up."); + } } public void HealVT(int amountToRestore) { - Stats.SetCurrentVT(Stats.CurrentVT.Value + amountToRestore); - var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; - _gameRepo.AnnounceMessageInInventory($"{raiseString}VT Restored."); + Stats.SetCurrentVT(Stats.CurrentVT.Value + amountToRestore); + var raiseString = amountToRestore == 1000 ? "MAX" : $"{amountToRestore}"; + _gameRepo.AnnounceMessageInInventory($"{raiseString}VT Restored."); } public void ModifyBonusAttack(int amount) { - Stats.SetBonusAttack(Stats.BonusAttack.Value + amount); + Stats.SetBonusAttack(Stats.BonusAttack.Value + amount); } public void ModifyBonusDefense(int amount) { - Stats.SetBonusDefense(Stats.BonusDefense.Value + amount); + Stats.SetBonusDefense(Stats.BonusDefense.Value + amount); } public void ModifyMaximumHP(int amount) { - Stats.SetMaximumHP(Stats.MaximumHP.Value + amount); + Stats.SetMaximumHP(Stats.MaximumHP.Value + amount); } public void ModifyMaximumVT(int amount) { - Stats.SetMaximumVT(Stats.MaximumVT.Value + amount); + Stats.SetMaximumVT(Stats.MaximumVT.Value + amount); } public void ModifyBonusLuck(double amount) { - Stats.SetLuck(Stats.Luck.Value + amount); + Stats.SetLuck(Stats.Luck.Value + amount); } public void Move(float delta) { - var rawInput = GlobalInputVector; - var strafeLeftInput = LeftStrafeInputVector; - var strafeRightInput = RightStrafeInputVector; + var rawInput = GlobalInputVector; + var strafeLeftInput = LeftStrafeInputVector; + var strafeRightInput = RightStrafeInputVector; - var transform = Transform; - transform.Basis = new Basis(Vector3.Up, Settings.RotationSpeed * -rawInput.X * delta) * transform.Basis; - var moveDirection = new Vector3(strafeRightInput - strafeLeftInput, 0, rawInput.Z).Normalized(); - var velocity = Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration; - _knockbackStrength = _knockbackStrength * 0.9f; - Transform = Transform with { Basis = transform.Basis }; - Velocity = velocity + (_knockbackDirection * _knockbackStrength); - MoveAndSlide(); + var transform = Transform; + transform.Basis = new Basis(Vector3.Up, Settings.RotationSpeed * -rawInput.X * delta) * transform.Basis; + var moveDirection = new Vector3(strafeRightInput - strafeLeftInput, 0, rawInput.Z).Normalized(); + var velocity = Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration; + _knockbackStrength = _knockbackStrength * 0.9f; + Transform = Transform with { Basis = transform.Basis }; + Velocity = velocity + (_knockbackDirection * _knockbackStrength); + MoveAndSlide(); } public void TeleportPlayer(Transform3D newTransform) { - Transform = newTransform; + Transform = newTransform; } public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false) { - if (Stats.CurrentHP.Value > 0) - { - damage = CalculateDefenseResistance(damage); - if (isCriticalHit) - damage *= 2; - Stats.SetCurrentHP(Stats.CurrentHP.Value - (int)damage); - } + if (Stats.CurrentHP.Value > 0) + { + damage = CalculateDefenseResistance(damage); + if (isCriticalHit) + damage *= 2; + Stats.SetCurrentHP(Stats.CurrentHP.Value - (int)damage); + } } public void Knockback(float impulse) { - _knockbackStrength = impulse; - _knockbackDirection = GlobalBasis.Z.Normalized(); + _knockbackStrength = impulse; + _knockbackDirection = GlobalBasis.Z.Normalized(); } public void GainExp(double expGained) { - Stats.SetCurrentExp(Stats.CurrentExp.Value + expGained); + Stats.SetCurrentExp(Stats.CurrentExp.Value + expGained); } public void LevelUp() { - var nextLevel = Stats.CurrentLevel.Value + 1; - var expToNextLevel = _expToNextLevel[nextLevel]; - var newCurrentExp = Mathf.Max(Stats.CurrentExp.Value - Stats.ExpToNextLevel.Value, 0); - Stats.SetCurrentLevel(nextLevel); - Stats.SetExpToNextLevel(expToNextLevel); - Stats.SetCurrentExp(newCurrentExp); + var nextLevel = Stats.CurrentLevel.Value + 1; + var expToNextLevel = _expToNextLevel[nextLevel]; + var newCurrentExp = Mathf.Max(Stats.CurrentExp.Value - Stats.ExpToNextLevel.Value, 0); + Stats.SetCurrentLevel(nextLevel); + Stats.SetExpToNextLevel(expToNextLevel); + Stats.SetCurrentExp(newCurrentExp); } public void Die() => PlayerLogic.Input(new PlayerLogic.Input.Die()); public override void _UnhandledInput(InputEvent @event) { - if (@event.IsActionPressed(GameInputs.Pause)) - PlayerPause(); + if (@event.IsActionPressed(GameInputs.Pause)) + PlayerPause(); - if (@event.IsActionPressed(GameInputs.Attack)) - Attack(); + if (@event.IsActionPressed(GameInputs.Attack)) + Attack(); } public void OnPhysicsProcess(double delta) { - PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta)); - PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform)); + PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta)); + PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform)); } public void Equip(EquipableItem equipable) { - if (equipable is Weapon weapon) - { - Unequip(_equippedWeapon.Value); - weapon.IsEquipped = true; - _equippedWeapon.OnNext(weapon); - } - else if (equipable is Armor armor) - { - Unequip(_equippedArmor.Value); - armor.IsEquipped = true; - _equippedArmor.OnNext(armor); - } - else if (equipable is Accessory accessory) - { - Unequip(_equippedAccessory.Value); - accessory.IsEquipped = true; - _equippedAccessory.OnNext(accessory); - } - else - throw new NotImplementedException("Item type is not supported."); + if (equipable is Weapon weapon) + { + Unequip(_equippedWeapon.Value); + weapon.IsEquipped = true; + _equippedWeapon.OnNext(weapon); + } + else if (equipable is Armor armor) + { + Unequip(_equippedArmor.Value); + armor.IsEquipped = true; + _equippedArmor.OnNext(armor); + } + else if (equipable is Accessory accessory) + { + Unequip(_equippedAccessory.Value); + accessory.IsEquipped = true; + _equippedAccessory.OnNext(accessory); + } + else + throw new NotImplementedException("Item type is not supported."); } public void Unequip(EquipableItem equipable) { - if (equipable is Weapon weapon) - { - weapon.IsEquipped = false; - ModifyBonusAttack(-weapon.Damage); - _equippedWeapon.OnNext(new Weapon()); - } - else if (equipable is Armor armor) - { - armor.IsEquipped = false; - ModifyBonusDefense(-armor.Defense); - _equippedArmor.OnNext(new Armor()); - } - else if (equipable is Accessory accessory) - { - accessory.IsEquipped = false; - ModifyMaximumHP(-accessory.MaxHPUp); - ModifyMaximumVT(-accessory.MaxVTUp); - ModifyBonusAttack(-accessory.ATKUp); - ModifyBonusDefense(-accessory.DEFUp); - ModifyBonusLuck(-accessory.LuckUp); - _equippedAccessory.OnNext(new Accessory()); - } - else - throw new NotImplementedException("Item type is not supported."); + if (equipable is Weapon weapon) + { + weapon.IsEquipped = false; + ModifyBonusAttack(-weapon.Damage); + _equippedWeapon.OnNext(new Weapon()); + } + else if (equipable is Armor armor) + { + armor.IsEquipped = false; + ModifyBonusDefense(-armor.Defense); + _equippedArmor.OnNext(new Armor()); + } + else if (equipable is Accessory accessory) + { + accessory.IsEquipped = false; + ModifyMaximumHP(-accessory.MaxHPUp); + ModifyMaximumVT(-accessory.MaxVTUp); + ModifyBonusAttack(-accessory.ATKUp); + ModifyBonusDefense(-accessory.DEFUp); + ModifyBonusLuck(-accessory.LuckUp); + _equippedAccessory.OnNext(new Accessory()); + } + else + throw new NotImplementedException("Item type is not supported."); - if (equipable.ItemTag == ItemTag.BreaksOnChange) - Inventory.Remove(equipable); + if (equipable.ItemTag == ItemTag.BreaksOnChange) + Inventory.Remove(equipable); } private static Vector3 GlobalInputVector { - get - { - var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown); - var input = new Vector3 - { - X = rawInput.X, - Z = rawInput.Y - }; - return input with { Y = 0f }; - } + get + { + var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown); + var input = new Vector3 + { + X = rawInput.X, + Z = rawInput.Y + }; + return input with { Y = 0f }; + } } private static float LeftStrafeInputVector { - get - { - return Input.GetActionStrength(GameInputs.StrafeLeft); - } + get + { + return Input.GetActionStrength(GameInputs.StrafeLeft); + } } private static float RightStrafeInputVector { - get - { - return Input.GetActionStrength(GameInputs.StrafeRight); - } + get + { + return Input.GetActionStrength(GameInputs.StrafeRight); + } } private void ThrowItem() { - var itemScene = GD.Load<PackedScene>("res://src/items/throwable/ThrowableItem.tscn"); - var throwItem = itemScene.Instantiate<ThrowableItem>(); - GetTree().Root.AddChildEx(throwItem); - throwItem.GlobalPosition = CurrentPosition + new Vector3(0, 3.5f, 0); - throwItem.GlobalRotation = GlobalRotation; + var itemScene = GD.Load<PackedScene>("res://src/items/throwable/ThrowableItem.tscn"); + var throwItem = itemScene.Instantiate<ThrowableItem>(); + GetTree().Root.AddChildEx(throwItem); + throwItem.GlobalPosition = CurrentPosition + new Vector3(0, 3.5f, 0); + throwItem.GlobalRotation = GlobalRotation; } private void OnAnimationFinished(StringName animation) { - PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); + PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); } private void OnExitTree() { - PlayerLogic.Stop(); - PlayerBinding.Dispose(); - AnimationPlayer.AnimationFinished -= OnAnimationFinished; + PlayerLogic.Stop(); + PlayerBinding.Dispose(); + AnimationPlayer.AnimationFinished -= OnAnimationFinished; } private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition; private void OnHealthTimerTimeout() { - if (Stats.CurrentHP.Value <= 0) - return; + if (Stats.CurrentHP.Value <= 0) + return; - if (Stats.CurrentVT.Value > 0) - { - if (((Accessory)EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption) - { - reduceOnTick = !reduceOnTick; - } - Stats.SetCurrentHP(Stats.CurrentHP.Value + 1); - if (reduceOnTick) - Stats.SetCurrentVT(Stats.CurrentVT.Value - 1); - } - else - Stats.SetCurrentHP(Stats.CurrentHP.Value - 1); + if (Stats.CurrentVT.Value > 0) + { + if (((Accessory)EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption) + { + reduceOnTick = !reduceOnTick; + } + Stats.SetCurrentHP(Stats.CurrentHP.Value + 1); + if (reduceOnTick) + Stats.SetCurrentVT(Stats.CurrentVT.Value - 1); + } + else + Stats.SetCurrentHP(Stats.CurrentHP.Value - 1); } private void EquippedWeapon_Sync(EquipableItem obj) { - ModifyBonusAttack(((Weapon)obj).Damage); + ModifyBonusAttack(((Weapon)obj).Damage); } private void EquippedArmor_Sync(EquipableItem obj) { - ModifyBonusDefense(((Armor)obj).Defense); + ModifyBonusDefense(((Armor)obj).Defense); } private void EquippedAccessory_Sync(EquipableItem accessory) { - ModifyMaximumHP(((Accessory)accessory).MaxHPUp); - ModifyMaximumVT(((Accessory)accessory).MaxVTUp); - ModifyBonusAttack(((Accessory)accessory).ATKUp); - ModifyBonusDefense(((Accessory)accessory).DEFUp); - ModifyBonusLuck(((Accessory)accessory).LuckUp); + ModifyMaximumHP(((Accessory)accessory).MaxHPUp); + ModifyMaximumVT(((Accessory)accessory).MaxVTUp); + ModifyBonusAttack(((Accessory)accessory).ATKUp); + ModifyBonusDefense(((Accessory)accessory).DEFUp); + ModifyBonusLuck(((Accessory)accessory).LuckUp); } private void CurrentHP_Sync(int newHealth) { - if (newHealth <= 0) - Die(); + if (newHealth <= 0) + Die(); } private void CurrentEXP_Sync(double newExp) { - if (Stats.CurrentExp.Value >= Stats.ExpToNextLevel.Value) - LevelUp(); + if (Stats.CurrentExp.Value >= Stats.ExpToNextLevel.Value) + LevelUp(); } private double CalculateDefenseResistance(double incomingDamage) { - return Mathf.Max(incomingDamage - Stats.CurrentDefense.Value - Stats.BonusDefense.Value, 0.0); + return Mathf.Max(incomingDamage - Stats.CurrentDefense.Value - Stats.BonusDefense.Value, 0.0); } private void Hitbox_AreaEntered(Area3D area) { - var target = area.GetOwner(); - if (target is IEnemy enemy) - HitEnemy(enemy); + var target = area.GetOwner(); + if (target is IEnemy enemy) + HitEnemy(enemy); } private void HitEnemy(IEnemy enemy) { - var attackValue = Stats.CurrentAttack.Value + Stats.BonusAttack.Value; - var ignoreElementalResistance = ((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.IgnoreAffinity; - var isCriticalHit = BattleExtensions.IsCriticalHit(Stats.Luck.Value); - var element = ((Weapon)EquippedWeapon.Value).WeaponElement; + var attackValue = Stats.CurrentAttack.Value + Stats.BonusAttack.Value; + var ignoreElementalResistance = ((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.IgnoreAffinity; + var isCriticalHit = BattleExtensions.IsCriticalHit(Stats.Luck.Value); + var element = ((Weapon)EquippedWeapon.Value).WeaponElement; - enemy.TakeDamage( - attackValue * ((Weapon)EquippedWeapon.Value).ElementalDamageBonus, - element, - isCriticalHit, - false, - ignoreElementalResistance); + enemy.TakeDamage( + attackValue * ((Weapon)EquippedWeapon.Value).ElementalDamageBonus, + element, + isCriticalHit, + false, + ignoreElementalResistance); - if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback) - enemy.Knockback(0.3f, -CurrentBasis.Z.Normalized()); + if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback) + enemy.Knockback(0.3f, -CurrentBasis.Z.Normalized()); - _gameRepo.OnPlayerAttackedEnemy(); + _gameRepo.OnPlayerAttackedEnemy(); } private void CollisionDetector_AreaEntered(Area3D area) { - if (area.GetParent() is InventoryItem inventoryItem) - { - var isAdded = Inventory.TryAdd(inventoryItem); - if (isAdded) - { - _gameRepo.AnnounceMessageOnMainScreen($"{inventoryItem.ItemName} picked up."); - inventoryItem.QueueFree(); - } - else - _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {inventoryItem.ItemName}."); - } - if (area.GetParent() is DroppedItem droppedItem) - { - var isAdded = Inventory.TryAdd(droppedItem.Item); - if (isAdded) - { - _gameRepo.AnnounceMessageOnMainScreen($"{droppedItem.Item.ItemName} picked up."); - droppedItem.QueueFree(); - } - else - _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {droppedItem.Item.ItemName}."); - } + if (area.GetParent() is InventoryItem inventoryItem) + { + var isAdded = Inventory.TryAdd(inventoryItem); + if (isAdded) + { + _gameRepo.AnnounceMessageOnMainScreen($"{inventoryItem.ItemName} picked up."); + inventoryItem.QueueFree(); + } + else + _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {inventoryItem.ItemName}."); + } + if (area.GetParent() is DroppedItem droppedItem) + { + var isAdded = Inventory.TryAdd(droppedItem.Item); + if (isAdded) + { + _gameRepo.AnnounceMessageOnMainScreen($"{droppedItem.Item.ItemName} picked up."); + droppedItem.QueueFree(); + } + else + _gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {droppedItem.Item.ItemName}."); + } } private bool PlayerIsHittingGeometry() { - var collisions = WallCheck.GetCollidingBodies(); - return collisions.Count > 0; + var collisions = WallCheck.GetCollidingBodies(); + return collisions.Count > 0; } private void WallCheck_BodyEntered(Node body) { - PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); - GD.Print("Hit wall"); - AnimationPlayer.Stop(); + PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished()); + GD.Print("Hit wall"); + AnimationPlayer.Stop(); } } diff --git a/Zennysoft.Game.Ma/src/ui/dialogue/Balloon.tscn b/Zennysoft.Game.Ma/src/ui/dialogue/Balloon.tscn index 2b1b6696..095f7e64 100644 --- a/Zennysoft.Game.Ma/src/ui/dialogue/Balloon.tscn +++ b/Zennysoft.Game.Ma/src/ui/dialogue/Balloon.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://c3iswacrgya7e" path="res://src/ui/dialogue/DialogueBalloon.cs" id="1_36de5"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] [ext_resource type="FontFile" uid="uid://cb41qqmxqurj8" path="res://src/ui/fonts/FT88-Bold.ttf" id="2_fn8n4"] -[ext_resource type="Script" uid="uid://bq8fntgcwiosq" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"] +[ext_resource type="Script" uid="uid://bq8fntgcwiosq" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"] [ext_resource type="Script" uid="uid://by6wev1st2yuh" path="res://src/ui/dialogue/ResponseExample.cs" id="5_0xrfp"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hrxr4"] diff --git a/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.tscn b/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.tscn index 05f9e132..b79badfd 100644 --- a/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.tscn +++ b/Zennysoft.Game.Ma/src/ui/dialogue/DialogueBalloon.tscn @@ -1,10 +1,10 @@ [gd_scene load_steps=20 format=3 uid="uid://drrynjlwqt8wi"] -[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_okfmu"] +[ext_resource type="Script" uid="uid://5b3w40kwakl3" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_okfmu"] [ext_resource type="FontFile" uid="uid://dit3vylt7hmmx" path="res://src/ui/fonts/FT88-Regular.ttf" id="2_c4c1f"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_jm6sr"] [ext_resource type="FontFile" uid="uid://cb41qqmxqurj8" path="res://src/ui/fonts/FT88-Bold.ttf" id="3_bc8ok"] -[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_yiii3"] +[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_yiii3"] [ext_resource type="FontFile" uid="uid://bohbd123672ea" path="res://src/ui/fonts/FT88-Italic.ttf" id="5_2dxvx"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] @@ -192,8 +192,8 @@ response_template = NodePath("ResponseExample") [node name="ResponseExample" type="Button" parent="Balloon/CenterContainer/Responses/ResponsesMenu"] layout_mode = 2 -theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) theme_override_colors/font_color = Color(0.737255, 0.705882, 0.690196, 1) +theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) button_mask = 0 text = "Response example"