diff --git a/Zennysoft.Game.Ma/addons/.gdignore b/Zennysoft.Game.Ma/addons/.gdignore deleted file mode 100644 index e69de29b..00000000 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs index b3189c77..04e29440 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs @@ -1,6 +1,7 @@ using Godot; using Godot.Collections; using System; +using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -8,458 +9,568 @@ using System.Threading.Tasks; namespace DialogueManagerRuntime { - public enum TranslationSource - { - None, - Guess, - CSV, - PO - } - - public partial class DialogueManager : RefCounted - { - 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); - - public static DialogueStartedEventHandler? DialogueStarted; - 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 + public enum TranslationSource { - get - { - if (instance == null) + None, + Guess, + CSV, + PO + } + + public partial class DialogueManager : RefCounted + { + 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); + + public static DialogueStartedEventHandler? DialogueStarted; + 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 { - instance = Engine.GetSingleton("DialogueManager"); - instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + get + { + if (instance == null) + { + instance = Engine.GetSingleton("DialogueManager"); + instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + } + return instance; + } } - 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 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))); - } - - - 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) + public static Godot.Collections.Array GameStates { - return true; + get => (Godot.Collections.Array)Instance.Get("game_states"); + set => Instance.Set("game_states", value); } - } - - 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) + public static bool IncludeSingletons { - info = methodInfo; + get => (bool)Instance.Get("include_singletons"); + set => Instance.Set("include_singletons", value); } - } - if (info == null) - return; + + 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 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))); + } + + + 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 Array StaticIdToLineIds(Resource dialogueResource, string staticId) + { + return (Array)Instance.Call("static_id_to_line_ids", dialogueResource, staticId); + } + + + public static string StaticIdToLineId(Resource dialogueResource, string staticId) + { + return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId); + } + + + 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 Array GetMembersForAutoload(Script script) + { + Array members = new Array(); + + string typeName = script.ResourcePath.GetFile().GetBaseName(); + var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName); + foreach (var matchingType in matchingTypes) + { + var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var memberInfo in memberInfos) + { + string type; + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + FieldInfo fieldInfo = memberInfo as FieldInfo; + + if (fieldInfo.FieldType.ToString().Contains("EventHandler")) + { + type = "signal"; + } + else if (fieldInfo.IsLiteral) + { + type = "constant"; + } + else + { + type = "property"; + } + break; + case MemberTypes.Method: + type = "method"; + break; + + default: + continue; + } + + members.Add(new Dictionary() { + { "name", memberInfo.Name }, + { "type", type } + }); + } + } + + return members; + } + + + public bool ThingHasConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + return true; + } + } + + return false; + } + + + public Variant ResolveThingConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + try + { + Variant value = fieldInfo.GetValue(thing) switch + { + int v => Variant.From((long)v), + float v => Variant.From((double)v), + System.String v => Variant.From((string)v), + _ => Variant.From(fieldInfo.GetValue(thing)) + }; + return value; + } + catch (Exception) + { + throw new Exception($"Constant {property} of type ${fieldInfo.GetValue(thing).GetType()} is not supported by Variant."); + } + } + } + + throw new Exception($"{property} is not a public constant on {thing}"); + } + + + 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().Where(p => !p.HasDefaultValue).Count()) + { + 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().Where(p => !p.HasDefaultValue).Count()) + { + 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; - } - // 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; - } - } + // 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); + // 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); + // 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); + if (result is Task taskResult) + { + await taskResult; + try + { + Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); + EmitSignal(SignalName.Resolved, value); + } + catch (Exception) + { + EmitSignal(SignalName.Resolved); + } + } + else + { + EmitSignal(SignalName.Resolved, (Variant)result); + } } - catch (Exception) - { - EmitSignal(SignalName.Resolved); - } - } - 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 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) + public partial class DialogueLine : RefCounted { - string wrapped = $"{tagName}="; - foreach (var tag in tags) - { - if (tag.StartsWith(wrapped)) + private string id = ""; + public string Id { - return tag.Substring(wrapped.Length); + get => id; + set => id = value; } - } - 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)) + private string type = "dialogue"; + public string Type { - return tag.Substring(wrapped.Length); + 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) + { + id = (string)data.Get("id"); + 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 ""; + } } - } - return ""; } - public override string ToString() + + public partial class DialogueResponse : RefCounted { - return $" next_id; + set => next_id = value; + } + + private bool is_allowed = true; + public bool IsAllowed + { + get => is_allowed; + set => is_allowed = value; + } + + private string condition_as_text = ""; + public string ConditionAsText + { + get => condition_as_text; + set => condition_as_text = 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 $"< " in line: var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() @@ -185,7 +186,7 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary, else: content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], title_hash, bits[1]] - elif not jump in ["END", "END!"]: + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], str(path.hash()), jump] elif "=> " in line: @@ -198,7 +199,7 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary, else: content[i] = "%s=> %s/%s" % [line.split("=> ")[0], title_hash, bits[1]] - elif not jump in ["END", "END!"]: + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] imported_paths.append(path) @@ -249,18 +250,22 @@ func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: 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 + # Empty lines are only kept so that we can work out groupings of things (eg. 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]: + if get_line_type(next_line) in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: continue else: tree_line.type = DMConstants.TYPE_UNKNOWN tree_line.indent = get_indent(next_line) + # Nothing should be more than a single indent past its parent. + if tree_line.indent > parent_chain.size(): + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_INDENTATION) + # Check for indentation changes if tree_line.indent > parent_chain.size() - 1: parent_chain.append(previous_line) @@ -481,6 +486,7 @@ func parse_match_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Arr # 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_UNKNOWN: 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) @@ -569,11 +575,15 @@ func parse_response_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: result = add_error(tree_line.line_number, condition.index, condition.error) else: line.expression = condition + # Extract just the raw condition text + var found: RegExMatch = regex.WRAPPED_CONDITION_REGEX.search(tree_line.text) + line.expression_text = found.strings[found.names.expression] + 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): + for i in range(sibling_index - 1, -1, -1): if siblings[i].type == DMConstants.TYPE_RESPONSE: original_response = siblings[i] elif siblings[i].type != DMConstants.TYPE_UNKNOWN: @@ -690,14 +700,32 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: for i in range(0, tree_line.children.size()): var child: DMTreeLine = tree_line.children[i] if child.type == DMConstants.TYPE_DIALOGUE: + # Nested dialogue lines cannot have further nested dialogue. + if child.children.size() > 0: + add_error(child.children[0].line_number, child.children[0].indent, DMConstants.ERR_INVALID_INDENTATION) + # Mark this as a dialogue child of another dialogue line. + child.is_nested_dialogue = true + var child_line = DMCompiledLine.new("", DMConstants.TYPE_DIALOGUE) + parse_character_and_dialogue(child, child_line, [], 0, parent) + var child_static_line_id: String = extract_static_line_id(child.text) + if child_line.character != "" or child_static_line_id != "": + add_error(child.line_number, child.indent, DMConstants.ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE) + # Check that only the last child (or none) has a jump reference + if i < tree_line.children.size() - 1 and " =>" in child.text: + add_error(child.line_number, child.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + if i == 0 and " =>" in tree_line.text: + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + tree_line.text += "\n" + child.text + elif child.type == DMConstants.TYPE_UNKNOWN: + tree_line.text += "\n" 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], "") + tree_line.text = tree_line.text.replace(" [ID:", "[ID:").replace("[ID:%s]" % [static_line_id], "") line.translation_key = static_line_id # Check for simultaneous lines @@ -730,7 +758,7 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: 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) + add_error(tree_line.line_number, tree_line.indent + expression[0].i, expression[0].value) # If the line isn't part of a weighted random group then make it point to the next # available sibling. @@ -817,9 +845,14 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s # 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 + # If there was no manual translation key then just use the text itself (unless this is a + # child dialogue below another dialogue line). + if not tree_line.is_nested_dialogue and line.translation_key == "": + # Show an error if missing translations is enabled + if DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) + else: + line.translation_key = text line.text = text @@ -829,9 +862,6 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s 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 @@ -1009,7 +1039,7 @@ func extract_condition(text: String, is_wrapped: bool, index: int) -> Dictionary } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -1037,7 +1067,7 @@ func extract_mutation(text: String) -> Dictionary: } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd index 972fd5cb..9e34f098 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd @@ -26,6 +26,8 @@ var concurrent_lines: PackedStringArray = [] var tags: PackedStringArray = [] ## The condition or mutation expression for this line. var expression: Dictionary = {} +## The express as the raw text that was given. +var expression_text: String = "" ## 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. @@ -129,6 +131,8 @@ func to_data() -> Dictionary: d.tags = tags if not notes.is_empty(): d.notes = notes + if not expression_text.is_empty(): + d.condition_as_text = expression_text DMConstants.TYPE_DIALOGUE: d.text = text diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd index ead998ba..ca104135 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd @@ -38,6 +38,7 @@ var TOKEN_DEFINITIONS: Dictionary = { 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_NULL_COALESCE: 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( |$)|!)"), diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd index 384340fc..7dfb2afc 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd @@ -2,6 +2,9 @@ class_name DMExpressionParser extends RefCounted +var include_comments: bool = false + + # Reference to the common [RegEx] that the parser needs. var regex: DMCompilerRegEx = DMCompilerRegEx.new() @@ -25,7 +28,7 @@ func tokenise(text: String, line_type: String, index: int) -> Array: index += 1 text = text.substr(1) else: - return _build_token_tree_error(DMConstants.ERR_INVALID_EXPRESSION, index) + return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index) return _build_token_tree(tokens, line_type, "")[0] @@ -59,7 +62,7 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: } elif expression[0].type == DMConstants.TYPE_ERROR: replacement = { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -76,8 +79,13 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: # 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 }] +func _build_token_tree_error(tree: Array, error: int, index: int) -> Array: + tree.insert(0, { + type = DMConstants.TOKEN_ERROR, + value = error, + i = index + }) + return tree # Convert a list of tokens into an abstract syntax tree. @@ -91,14 +99,22 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, error, error_token.index), tokens] match token.type: + DMConstants.TOKEN_COMMENT: + if include_comments: + tree.append({ + type = DMConstants.TOKEN_COMMENT, + value = token.value, + i = token.index + }) + 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] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_FUNCTION, @@ -113,11 +129,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), 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] + return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens] tree.append({ type = DMConstants.TOKEN_DICTIONARY_REFERENCE, @@ -132,7 +148,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var t = sub_tree[0] for i in range(0, t.size() - 2): @@ -154,7 +170,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var type = DMConstants.TOKEN_ARRAY var value = _tokens_to_list(sub_tree[0]) @@ -177,7 +193,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_GROUP, @@ -190,7 +206,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl 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] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] tree.append({ type = token.type, @@ -211,10 +227,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_COLON, \ - DMConstants.TOKEN_DOT: + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE: tree.append({ type = token.type, - i = token.index + i = token.index }) DMConstants.TOKEN_COMPARISON, \ @@ -230,7 +247,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl tree.append({ type = token.type, value = value, - i = token.index + i = token.index }) DMConstants.TOKEN_STRING: @@ -248,7 +265,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) DMConstants.TOKEN_CONDITION: - return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] DMConstants.TOKEN_BOOL: tree.append({ @@ -280,8 +297,8 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) 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] + var index: int = tokens[0].i if tokens.size() > 0 else 0 + return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] return [tree, tokens] @@ -347,8 +364,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_COMPARISON, \ DMConstants.TOKEN_OPERATOR, \ - DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE, \ DMConstants.TOKEN_NOT, \ DMConstants.TOKEN_AND_OR, \ DMConstants.TOKEN_DICTIONARY_REFERENCE: @@ -366,6 +383,20 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_DOT ] + DMConstants.TOKEN_COMMA: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + 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, @@ -409,7 +440,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t 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): + 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 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd index f172a8a4..667daad1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd @@ -22,6 +22,8 @@ var text: String = "" var children: Array[DMTreeLine] = [] ## Any doc comments attached to this line. var notes: String = "" +## Is this a dialogue line that is the child of another dialogue line? +var is_nested_dialogue: bool = false func _init(initial_id: String) -> void: 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 e5a7c8c5..df9a3738 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd @@ -53,6 +53,10 @@ var font_size: int: var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s") +var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new() +var _autoloads: Dictionary[String, String] = {} +var _autoload_member_cache: Dictionary[String, Dictionary] = {} + func _ready() -> void: # Add error gutter @@ -65,6 +69,10 @@ func _ready() -> void: syntax_highlighter = DMSyntaxHighlighter.new() + # Keep track of any autoloads + ProjectSettings.settings_changed.connect(_on_project_settings_changed) + _on_project_settings_changed() + func _gui_input(event: InputEvent) -> void: # Handle shortcuts that come from the editor @@ -141,6 +149,7 @@ func _request_code_completion(force: bool) -> void: var cursor: Vector2 = get_cursor() var current_line: String = get_line(cursor.y) + # Match jumps if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")): var prompt: String = current_line.split("=>")[1] if prompt.begins_with("< "): @@ -166,9 +175,8 @@ func _request_code_completion(force: bool) -> void: 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) - return + # Match character names var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]: # Only show names starting with that character @@ -176,9 +184,72 @@ func _request_code_completion(force: bool) -> void: if names.size() > 0: for name in names: add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons")) - update_code_completion_options(true) - else: - cancel_code_completion() + + # Match autoloads on mutation lines + for prefix in ["do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]: + if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))): + var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3) + # Find the last couple of tokens + var possible_prompt: String = expression.reverse() + possible_prompt = possible_prompt.substr(0, possible_prompt.find(" ")) + possible_prompt = possible_prompt.substr(0, possible_prompt.find("(")) + possible_prompt = possible_prompt.reverse() + var segments: PackedStringArray = possible_prompt.split(".").slice(-2) + var auto_completes: Array[Dictionary] = [] + + # Autoloads and state shortcuts + if segments.size() == 1: + var prompt: String = segments[0] + for autoload in _autoloads.keys(): + if matches_prompt(prompt, autoload): + auto_completes.append({ + prompt = prompt, + text = autoload, + type = "script" + }) + for autoload in get_state_shortcuts(): + for member: Dictionary in get_members_for_autoload(autoload): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + # Members of an autoload + elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "): + var prompt: String = segments[1] + for member: Dictionary in get_members_for_autoload(segments[0]): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + auto_completes.sort_custom(func(a, b): return a.text < b.text) + + for auto_complete in auto_completes: + var icon: Texture2D + var text: String = auto_complete.text + match auto_complete.type: + "script": + icon = get_theme_icon("Script", "EditorIcons") + "property": + icon = get_theme_icon("MemberProperty", "EditorIcons") + "method": + icon = get_theme_icon("MemberMethod", "EditorIcons") + text += "()" + "signal": + icon = get_theme_icon("MemberSignal", "EditorIcons") + "constant": + icon = get_theme_icon("MemberConstant", "EditorIcons") + var insert: String = text.substr(auto_complete.prompt.length()) + add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon) + + update_code_completion_options(true) + if get_code_completion_options().size() == 0: + cancel_code_completion() func _filter_code_completion_candidates(candidates: Array) -> Array: @@ -190,17 +261,21 @@ func _confirm_code_completion(replace: bool) -> void: var completion = get_code_completion_option(get_code_completion_selected_index()) begin_complex_operation() # Delete any part of the text that we've already typed - for i in range(0, completion.display_text.length() - completion.insert_text.length()): - backspace() + if completion.insert_text.length() > 0: + for i in range(0, completion.display_text.length() - completion.insert_text.length()): + backspace() # Insert the whole match insert_text_at_caret(completion.display_text) end_complex_operation() + if completion.display_text.ends_with("()"): + set_cursor(get_cursor() - Vector2.RIGHT) + # Close the autocomplete menu on the next tick call_deferred("cancel_code_completion") -### Helpers +#region Helpers # Get the current caret as a Vector2 @@ -219,6 +294,69 @@ func matches_prompt(prompt: String, matcher: String) -> bool: return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower()) +func get_state_shortcuts() -> PackedStringArray: + # Get any shortcuts defined in settings + var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, []) + # Check for "using" clauses + for line: String in text.split("\n"): + var found: RegExMatch = compiler_regex.USING_REGEX.search(line) + if found: + shortcuts.append(found.strings[found.names.state]) + # Check for any other script sources + for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []): + shortcuts.append(extra_script_source) + + return shortcuts + + +func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]: + # Debounce method list lookups + if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000: + return _autoload_member_cache.get(autoload_name).get("members") + + if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return [] + + var autoload = load(_autoloads.get(autoload_name, autoload_name)) + var script: Script = autoload if autoload is Script else autoload.get_script() + + if not is_instance_valid(script): return [] + + var members: Array[Dictionary] = [] + if script.resource_path.ends_with(".gd"): + for m: Dictionary in script.get_script_method_list(): + if not m.name.begins_with("@"): + members.append({ + name = m.name, + type = "method" + }) + for m: Dictionary in script.get_script_property_list(): + members.append({ + name = m.name, + type = "property" + }) + for m: Dictionary in script.get_script_signal_list(): + members.append({ + name = m.name, + type = "signal" + }) + for c: String in script.get_script_constant_map(): + members.append({ + name = c, + type = "constant" + }) + elif script.resource_path.ends_with(".cs"): + var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new() + for m: Dictionary in dotnet.GetMembersForAutoload(script): + members.append(m) + + _autoload_member_cache[autoload_name] = { + at = Time.get_ticks_msec(), + members = members + } + + return members + + ## Get a list of titles from the current text func get_titles() -> PackedStringArray: var titles = PackedStringArray([]) @@ -417,7 +555,18 @@ func move_line(offset: int) -> void: scroll_vertical = starting_scroll + offset -### Signals +#endregion + +#region Signals + + +func _on_project_settings_changed() -> void: + _autoloads = {} + var project = ConfigFile.new() + project.load("res://project.godot") + for autoload in project.get_section_keys("autoload"): + if autoload != "DialogueManager": + _autoloads[autoload] = project.get_value("autoload", autoload).substr(1) func _on_code_edit_symbol_validate(symbol: String) -> void: @@ -456,3 +605,6 @@ func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void: var line_errors = errors.filter(func(error): return error.line_number == line) if line_errors.size() > 0: error_clicked.emit(line) + + +#endregion 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 1553d1f1..3f4e0d93 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 @@ -14,6 +14,8 @@ func _clear_highlighting_cache() -> void: func _get_line_syntax_highlighting(line: int) -> Dictionary: + expression_parser.include_comments = true + var colors: Dictionary = {} var text_edit: TextEdit = get_text_edit() var text: String = text_edit.get_line(line) @@ -38,9 +40,10 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: 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 } + if import: + 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 } DMConstants.TYPE_COMMENT: colors[index] = { color = theme.comments_color } @@ -53,7 +56,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: 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: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -62,7 +65,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: 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: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -84,9 +87,13 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: var dialogue_text: String = text.substr(index, text.find("=>")) - # Highlight character name + # Highlight character name (but ignore ":" within line ID reference) var split_index: int = dialogue_text.replace("\\:", "??").find(":") - colors[index + split_index + 1] = { color = theme.text_color } + if text.substr(split_index - 3, 3) != "[ID": + colors[index + split_index + 1] = { color = theme.text_color } + else: + # If there's no character name then just highlight the text as dialogue. + colors[index] = { color = theme.text_color } # Interpolation var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) @@ -155,6 +162,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int for token: Dictionary in tokens: last_index = token.i match token.type: + DMConstants.TOKEN_COMMENT: + colors[index + token.i] = { color = theme.comments_color } + DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: colors[index + token.i] = { color = theme.conditions_color } @@ -164,7 +174,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int else: colors[index + token.i] = { color = theme.members_color } - DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, DMConstants.TOKEN_COMMA, DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: + DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \ + DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \ + DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: colors[index + token.i] = { color = theme.symbols_color } DMConstants.TOKEN_STRING: 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 2614ecaa..de64b717 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 @@ -99,7 +99,7 @@ func update_results_view() -> void: var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]" else: - highlight = "[bgcolor=" + colors.symbols_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" + highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length()) result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text] result_label.gui_input.connect(func(event): diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd index e91574e9..ceb40280 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd @@ -114,7 +114,7 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int: return line.findn(text, from_index) -### Signals +#region Signals func _on_text_edit_gui_input(event: InputEvent) -> void: @@ -177,14 +177,17 @@ func _on_replace_button_pressed() -> void: # Replace the selection at result index var r: Array = results[result_index] + code_edit.begin_complex_operation() var lines: PackedStringArray = code_edit.text.split("\n") var line: String = lines[r[0]] line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2]) lines[r[0]] = line code_edit.text = "\n".join(lines) - search(input.text, result_index) + code_edit.end_complex_operation() code_edit.text_changed.emit() + search(input.text, result_index) + func _on_replace_all_button_pressed() -> void: if match_case_button.button_pressed: @@ -210,3 +213,6 @@ func _on_input_focus_entered() -> void: func _on_match_case_check_box_toggled(button_pressed: bool) -> void: search() + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd index 91e6bc7c..56b5f02e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd @@ -37,6 +37,7 @@ const TOKEN_COMPARISON = &"comparison" const TOKEN_ASSIGNMENT = &"assignment" const TOKEN_OPERATOR = &"operator" const TOKEN_COMMA = &"comma" +const TOKEN_NULL_COALESCE = &"null_coalesce" const TOKEN_DOT = &"dot" const TOKEN_CONDITION = &"condition" const TOKEN_BOOL = &"bool" @@ -119,6 +120,12 @@ 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 +const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141 +const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142 + + +static var _current_locale: String = "" +static var _current_translation: Translation ## Get the error message @@ -204,16 +211,21 @@ static func get_error_message(error: int) -> String: return translate(&"errors.concurrent_line_without_origin") ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: return translate(&"errors.goto_not_allowed_on_concurrect_lines") + ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE: + return translate(&"errors.unexpected_syntax_on_nested_dialogue_line") + ERR_NESTED_DIALOGUE_INVALID_JUMP: + return translate(&"errors.err_nested_dialogue_invalid_jump") return translate(&"errors.unknown") static func translate(string: String) -> String: - 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] - var fallback_translations_path: String = "%s/l10n/%s.po" % [base_path, TranslationServer.get_tool_locale().substr(0, 2)] - var en_translations_path: String = "%s/l10n/en.po" % base_path - var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else (fallback_translations_path if FileAccess.file_exists(fallback_translations_path) else en_translations_path)) - return translations.get_message(string) + var locale: String = TranslationServer.get_tool_locale() + if _current_translation == null or _current_locale != locale: + var base_path: String = new().get_script().resource_path.get_base_dir() + var translation_path: String = "%s/l10n/%s.po" % [base_path, locale] + var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)] + var en_translation_path: String = "%s/l10n/en.po" % base_path + _current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path)) + _current_locale = locale + return _current_translation.get_message(string) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd index ab9a17ed..e073107e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd @@ -65,6 +65,8 @@ var _method_info_cache: Dictionary = {} var _dotnet_dialogue_manager: RefCounted +var _expression_parser: DMExpressionParser = DMExpressionParser.new() + func _ready() -> void: # Cache the known Node2D properties @@ -185,6 +187,7 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) continue elif await _check_case_value(value, case, extra_game_states): next_id = case.next_id + break # Nothing matched so check for else case if next_id == "": if not else_case.is_empty(): @@ -206,16 +209,6 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) 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]: @@ -252,11 +245,18 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) # 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 + # Find any simultaneously said lines. + 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): + # Resolve IDs to their actual lines. + for line_id: String in data.concurrent_lines: + line.concurrent_lines.append(await get_line(resource, line_id, extra_game_states)) + # 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 @@ -293,7 +293,15 @@ func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> var text: String = translate(data) # Resolve variables - for replacement in data.get(&"text_replacements", [] as Array[Dictionary]): + var text_replacements: Array[Dictionary] = data.get(&"text_replacements", [] as Array[Dictionary]) + if text_replacements.size() == 0 and "{{" in text: + # This line is translated but has expressions that didn't exist in the base text. + text_replacements = _expression_parser.extract_replacements(text, 0) + + for replacement in text_replacements: + if replacement.has("error"): + assert(false, "%s \"%s\"" % [DMConstants.get_error_message(replacement.get("error")), text]) + var value = await _resolve(replacement.expression.duplicate(true), extra_game_states) var index: int = text.find(replacement.value_in_text) if index == -1: @@ -442,6 +450,18 @@ func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, titl return balloon +## Resolve a static line ID to an actual line ID +func static_id_to_line_id(resource: DialogueResource, static_id: String) -> String: + var ids = static_id_to_line_ids(resource, static_id) + if ids.size() == 0: return "" + return ids[0] + + +## Resolve a static line ID to any actual line IDs that match +func static_id_to_line_ids(resource: DialogueResource, static_id: String) -> PackedStringArray: + return resource.lines.values().filter(func(l): return l.get(&"translation_key", "") == static_id).map(func(l): return l.id) + + # 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) @@ -524,7 +544,7 @@ func show_error_for_missing_state_value(message: String, will_show: bool = true) # Translate a string func translate(data: Dictionary) -> String: - if translation_source == DMConstants.TranslationSource.None: + if TranslationServer.get_loaded_locales().size() == 0 or translation_source == DMConstants.TranslationSource.None: return data.text var translation_key: String = data.get(&"translation_key", data.text) @@ -605,12 +625,13 @@ func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResp type = DMConstants.TYPE_RESPONSE, next_id = data.next_id, is_allowed = data.is_allowed, + condition_as_text = data.get(&"condition_as_text", ""), 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.get(&"text_replacements", [] as Array[Dictionary]), tags = data.get(&"tags", []), - translation_key = data.get(&"translation_key", data.text) + translation_key = data.get(&"translation_key", data.text), }) @@ -629,7 +650,7 @@ func _get_game_states(extra_game_states: Array) -> Array: game_states = [_autoloads] # Add any other state shortcuts from settings 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) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: game_states.append(state) @@ -661,21 +682,32 @@ func _check_case_value(match_value: Variant, data: Dictionary, extra_game_states 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 + # Check for multiple values + var expressions_to_check: Array = [] + var previous_comma_index: int = 0 + for i in range(0, expression.size()): + if expression[i].type == DMConstants.TOKEN_COMMA: + expressions_to_check.append(expression.slice(previous_comma_index, i)) + previous_comma_index = i + 1 + elif i == expression.size() - 1: + expressions_to_check.append(expression.slice(previous_comma_index)) - var resolved_value = await _resolve(expression, extra_game_states) + for expression_to_check in expressions_to_check: + # 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_to_check[0].type == DMConstants.TOKEN_COMPARISON: + expression_to_check.insert(0, { + type = DMConstants.TOKEN_VALUE, + value = match_value + }) + already_compared = true + + var resolved_value = await _resolve(expression_to_check, extra_game_states) + if (already_compared and resolved_value) or match_value == resolved_value: + return true + + return false - if already_compared: - return resolved_value - else: - return match_value == resolved_value # Make a change to game state or run a method @@ -757,6 +789,13 @@ func _get_state_value(property: String, extra_game_states: Array): if state.has(property): return state.get(property) else: + # Try for a C# constant first + if state.get_script() \ + and state.get_script().resource_path.ends_with(".cs") \ + and _get_dotnet_dialogue_manager().ThingHasConstant(state, property): + return _get_dotnet_dialogue_manager().ResolveThingConstant(state, property) + + # Otherwise just let Godot try and resolve it. var result = expression.execute([], state, false) if not expression.has_execute_failed(): return result @@ -782,7 +821,7 @@ func _warn_about_state_name_collisions(target_key: String, extra_game_states: Ar # 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) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: state_shortcuts.append(state) @@ -867,7 +906,18 @@ func _resolve(tokens: Array, extra_game_states: Array): limit += 1 var token: Dictionary = tokens[i] - if token.type == DMConstants.TOKEN_FUNCTION: + if token.type == DMConstants.TOKEN_NULL_COALESCE: + var caller: Dictionary = tokens[i - 1] + if caller.value == null: + # If the caller is null then the method/property is also null + caller.type = DMConstants.TOKEN_VALUE + caller.value = null + tokens.remove_at(i + 1) + tokens.remove_at(i) + else: + token.type = DMConstants.TOKEN_DOT + + elif 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 == DMConstants.TOKEN_DOT: @@ -1330,6 +1380,9 @@ func _is_valid(line: DialogueLine) -> bool: # Check that a thing has a given method. func _thing_has_method(thing, method: String, args: Array) -> bool: + if not is_instance_valid(thing): + return false + if Builtins.is_supported(thing, method): return thing != _autoloads elif thing is Dictionary: @@ -1344,7 +1397,7 @@ func _thing_has_method(thing, method: String, args: Array) -> bool: if thing.has_method(method): return true - if method.to_snake_case() != method and DMSettings.check_for_dotnet_solution(): + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): # 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, args) @@ -1363,6 +1416,10 @@ func _thing_has_property(thing: Object, property: String) -> bool: if p.name == property: return true + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): + # If we get this far then the property might be a C# constant. + return _get_dotnet_dialogue_manager().ThingHasConstant(thing, property) + return false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd index 701ce926..479b81cf 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd @@ -14,6 +14,9 @@ var next_id: String = "" ## [code]true[/code] if the condition of this line was met. var is_allowed: bool = true +## The original condition text. +var condition_as_text: String = "" + ## A character (depending on the "characters in responses" behaviour setting). var character: String = "" @@ -45,6 +48,7 @@ func _init(data: Dictionary = {}) -> void: text_replacements = data.text_replacements tags = data.tags translation_key = data.translation_key + condition_as_text = data.condition_as_text func _to_string() -> String: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd index cd66ae5b..0ae2c50d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd @@ -100,16 +100,20 @@ func _configure_focus() -> void: if i == 0: item.focus_neighbor_top = item.get_path() + item.focus_neighbor_left = item.get_path() item.focus_previous = item.get_path() else: item.focus_neighbor_top = items[i - 1].get_path() + item.focus_neighbor_left = items[i - 1].get_path() item.focus_previous = items[i - 1].get_path() if i == items.size() - 1: item.focus_neighbor_bottom = item.get_path() + item.focus_neighbor_right = item.get_path() item.focus_next = item.get_path() else: item.focus_neighbor_bottom = items[i + 1].get_path() + item.focus_neighbor_right = items[i + 1].get_path() item.focus_next = items[i + 1].get_path() item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) 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 58527122..980f0679 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs @@ -5,219 +5,219 @@ namespace DialogueManagerRuntime { public partial class ExampleBalloon : CanvasLayer { - [Export] public string NextAction = "ui_accept"; - [Export] public string SkipAction = "ui_cancel"; + [Export] public string NextAction = "ui_accept"; + [Export] public string SkipAction = "ui_cancel"; - Control balloon; - RichTextLabel characterLabel; - RichTextLabel dialogueLabel; - VBoxContainer responsesMenu; + Control balloon; + RichTextLabel characterLabel; + RichTextLabel dialogueLabel; + VBoxContainer responsesMenu; - Resource resource; - Array temporaryGameStates = new Array(); - bool isWaitingForInput = false; - bool willHideBalloon = false; + Resource resource; + Array temporaryGameStates = new Array(); + bool isWaitingForInput = false; + bool willHideBalloon = false; - DialogueLine dialogueLine; - DialogueLine DialogueLine - { - get => dialogueLine; - set - { - if (value == null) - { - QueueFree(); - return; - } + DialogueLine dialogueLine; + DialogueLine DialogueLine + { + get => dialogueLine; + set + { + if (value == null) + { + QueueFree(); + return; + } - dialogueLine = value; - ApplyDialogueLine(); - } - } + dialogueLine = value; + ApplyDialogueLine(); + } + } - Timer MutationCooldown = new Timer(); + Timer MutationCooldown = new Timer(); - public override void _Ready() - { - balloon = GetNode("%Balloon"); - characterLabel = GetNode("%CharacterLabel"); - dialogueLabel = GetNode("%DialogueLabel"); - responsesMenu = GetNode("%ResponsesMenu"); + public override void _Ready() + { + balloon = GetNode("%Balloon"); + characterLabel = GetNode("%CharacterLabel"); + dialogueLabel = GetNode("%DialogueLabel"); + responsesMenu = GetNode("%ResponsesMenu"); - balloon.Hide(); + 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; - } - } + 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; + if (!isWaitingForInput) return; + if (dialogueLine.Responses.Count > 0) return; - GetViewport().SetInputAsHandled(); + 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 (@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); - })); + 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); + // Hide the balloon when a mutation is running + MutationCooldown.Timeout += () => + { + if (willHideBalloon) + { + willHideBalloon = false; + balloon.Hide(); + } + }; + AddChild(MutationCooldown); - DialogueManager.Mutated += OnMutated; - } + DialogueManager.Mutated += OnMutated; + } - public override void _ExitTree() - { - 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 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 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 extraGameStates = null) - { - temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); - isWaitingForInput = false; - resource = dialogueResource; + public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) + { + temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); + isWaitingForInput = false; + resource = dialogueResource; - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); - } + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); + } - public async void Next(string nextId) - { - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); - } + public async void Next(string nextId) + { + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); + } - #region Helpers + #region Helpers - private async void ApplyDialogueLine() - { - MutationCooldown.Stop(); + private async void ApplyDialogueLine() + { + MutationCooldown.Stop(); - isWaitingForInput = false; - balloon.FocusMode = Control.FocusModeEnum.All; - balloon.GrabFocus(); + 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 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 dialogue + dialogueLabel.Hide(); + dialogueLabel.Set("dialogue_line", dialogueLine); - // Set up the responses - responsesMenu.Hide(); - responsesMenu.Set("responses", dialogueLine.Responses); + // 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"); - } + // 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(); - } - } + // 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 + #endregion - #region signals + #region signals - private void OnMutated(Dictionary _mutation) - { - isWaitingForInput = false; - willHideBalloon = true; - MutationCooldown.Start(0.1f); - } + private void OnMutated(Dictionary _mutation) + { + isWaitingForInput = false; + willHideBalloon = true; + MutationCooldown.Start(0.1f); + } - #endregion + #endregion } } 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 85e82348..64b33658 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 @@ -40,7 +40,7 @@ corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 @@ -61,7 +61,7 @@ MarginContainer/constants/margin_bottom = 15 MarginContainer/constants/margin_left = 30 MarginContainer/constants/margin_right = 30 MarginContainer/constants/margin_top = 15 -Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -77,33 +77,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" parent="Balloon"] layout_mode = 1 anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 21.0 -offset_top = -183.0 -offset_right = -19.0 -offset_bottom = -19.0 +offset_top = -219.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 mouse_filter = 1 -[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -113,37 +108,35 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_a8ve6")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." -[node name="Responses" type="MarginContainer" parent="Balloon"] -layout_mode = 1 -anchors_preset = 7 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -147.0 -offset_top = -558.0 -offset_right = 494.0 -offset_bottom = -154.0 -grow_horizontal = 2 -grow_vertical = 0 - -[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses" node_paths=PackedStringArray("response_template")] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -290.5 +offset_top = -35.0 +offset_right = 290.5 +offset_bottom = 35.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 +alignment = 1 script = ExtResource("3_72ixx") response_template = NodePath("ResponseExample") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] 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 6e00358e..57f8e4e1 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 @@ -66,7 +66,7 @@ corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"] bg_color = Color(0, 0, 0, 1) border_width_left = 1 border_width_top = 1 @@ -87,7 +87,7 @@ MarginContainer/constants/margin_bottom = 4 MarginContainer/constants/margin_left = 8 MarginContainer/constants/margin_right = 8 MarginContainer/constants/margin_top = 4 -Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -103,33 +103,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" parent="Balloon"] layout_mode = 1 anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 3.0 -offset_top = -62.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_top = -71.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 mouse_filter = 1 -[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -139,36 +134,33 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_hfvdi")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." -[node name="Responses" type="MarginContainer" parent="Balloon"] -layout_mode = 1 -anchors_preset = 7 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -124.0 -offset_top = -218.0 -offset_right = 125.0 -offset_bottom = -50.0 -grow_horizontal = 2 -grow_vertical = 0 - -[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -116.5 +offset_top = -9.0 +offset_right = 116.5 +offset_bottom = 9.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 script = ExtResource("3_1j1j0") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response Example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd index 1cdcb7e8..ef65500a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd @@ -3,10 +3,16 @@ class_name DMExportPlugin extends EditorExportPlugin const IGNORED_PATHS = [ "/assets", "/components", - "/l10n", - "/views" + "/views", + "inspector_plugin", + "test_scene" ] + +func _get_name() -> String: + return "Dialogue Manager Export Plugin" + + func _export_file(path: String, type: String, features: PackedStringArray) -> void: var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd index 345fe844..9c4ad78b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd @@ -5,12 +5,15 @@ class_name DMImportPlugin extends EditorImportPlugin signal compiled_resource(resource: Resource) -const COMPILER_VERSION = 14 +const COMPILER_VERSION = 15 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" + + +func _get_format_version() -> int: + return COMPILER_VERSION func _get_visible_name() -> String: @@ -74,7 +77,7 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo 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 + return OK # Get the current addon version var config: ConfigFile = ConfigFile.new() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po index 5e241657..132bff33 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po @@ -5,7 +5,7 @@ msgstr "" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" -"Language: de\n" +"Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -108,6 +108,15 @@ msgstr "End dialogue" msgid "generate_line_ids" msgstr "Generate line IDs" +msgid "use_uuid_only_for_ids" +msgstr "Use UUID only for IDs" + +msgid "set_id_prefix_length" +msgstr "Set ID prefix length" + +msgid "id_prefix_length" +msgstr "ID prefix length:" + msgid "save_characters_to_csv" msgstr "Save character names to CSV..." @@ -324,6 +333,12 @@ 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.unexpected_syntax_on_nested_dialogue_line" +msgstr "Nested dialogue lines may only contain dialogue." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Only the last line of nested dialogue is allowed to include a jump." + msgid "errors.unknown" msgstr "Unknown syntax." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po index ef604e1d..6c6bffbb 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po @@ -92,6 +92,15 @@ msgstr "Finalizar diálogo" msgid "generate_line_ids" msgstr "Generar IDs de línea" +msgid "use_uuid_only_for_ids" +msgstr "Usar solo UUID como ID" + +msgid "set_id_prefix_length" +msgstr "Establecer la longitud del prefijo de ID" + +msgid "id_prefix_length" +msgstr "Longitud del prefijo de ID:" + msgid "save_characters_to_csv" msgstr "Guardar los nombres de los personajes en un archivo CSV..." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot index 85cb8ac9..ad06ac7e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot @@ -101,6 +101,15 @@ msgstr "" msgid "generate_line_ids" msgstr "" +msgid "use_uuid_only_for_ids" +msgstr "" + +msgid "set_id_prefix_length" +msgstr "" + +msgid "id_prefix_length" +msgstr "" + msgid "save_to_csv" msgstr "" @@ -314,6 +323,12 @@ msgstr "" msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "" +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "" + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "" + msgid "errors.unknown" msgstr "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po index 8cd41ac4..43dd51b6 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po @@ -104,6 +104,15 @@ msgstr "Кінець діалогу" msgid "generate_line_ids" msgstr "Згенерувати ідентифікатори рядків" +msgid "use_uuid_only_for_ids" +msgstr "Використовувати лише UUID як ID" + +msgid "set_id_prefix_length" +msgstr "Встановити довжину префікса ID" + +msgid "id_prefix_length" +msgstr "Довжина префікса ID:" + msgid "save_characters_to_csv" msgstr "Зберегти імена персонажів в CSV..." @@ -320,6 +329,12 @@ msgstr "Паралельні рядки потребують початково msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "У паралельних діалогових рядках не допускаються Goto посилання." +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Вкладені рядки діалогу можуть містити лише діалог." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Лише останній рядок вкладеного діалогу може містити перехід." + msgid "errors.unknown" msgstr "Невідомий синтаксис." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po index bafd1d58..2a48a51d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po @@ -95,6 +95,15 @@ msgstr "结束对话" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "use_uuid_only_for_ids" +msgstr "仅使用UUID作为ID" + +msgid "set_id_prefix_length" +msgstr "设置ID前缀长度" + +msgid "id_prefix_length" +msgstr "ID前缀长度:" + msgid "save_characters_to_csv" msgstr "保存角色到 CSV" 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 e20feee8..1be98135 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po @@ -95,6 +95,15 @@ msgstr "結束對話" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "use_uuid_only_for_ids" +msgstr "僅使用 UUID 作為 ID" + +msgid "set_id_prefix_length" +msgstr "設定 ID 前綴長度" + +msgid "id_prefix_length" +msgstr "ID 前綴長度:" + msgid "save_characters_to_csv" msgstr "保存角色到 CSV" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg index 9b558dbb..c8fa08f1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg @@ -3,5 +3,5 @@ name="Dialogue Manager" description="A powerful nonlinear dialogue system" author="Nathan Hoad" -version="3.4.0" +version="3.7.1" script="plugin.gd" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd index 4c4edb24..a5d607fa 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd @@ -13,9 +13,15 @@ var main_view var dialogue_cache: DMCache -func _enter_tree() -> void: +func _enable_plugin() -> void: add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd") + +func _disable_plugin() -> void: + remove_autoload_singleton("DialogueManager") + + +func _enter_tree() -> void: if Engine.is_editor_hint(): Engine.set_meta("DialogueManagerPlugin", self) @@ -105,8 +111,6 @@ func _enter_tree() -> void: func _exit_tree() -> void: - remove_autoload_singleton("DialogueManager") - remove_import_plugin(import_plugin) import_plugin = null @@ -172,6 +176,11 @@ func _apply_changes() -> void: _update_localization() +func _save_external_data() -> void: + if dialogue_cache != null: + dialogue_cache.reimport_files() + + func _build() -> bool: # If this is the dotnet Godot then we need to check if the solution file exists DMSettings.check_for_dotnet_solution() @@ -320,6 +329,9 @@ func update_import_paths(from_path: String, to_path: String) -> void: func _update_localization() -> void: + if not DMSettings.get_setting(DMSettings.UPDATE_POT_FILES_AUTOMATICALLY, true): + return + var dialogue_files = dialogue_cache.get_files() # Add any new files to POT generation diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd index 0a0c12f2..01ed4089 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd @@ -23,9 +23,13 @@ const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales" 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" +## Automatically update the project's list of translatable files when dialogue files are added or removed +const UPDATE_POT_FILES_AUTOMATICALLY = "editor/translations/update_pot_files_automatically" ## A custom test scene to use when testing dialogue. const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path" +## Extra script files to include in the auto-complete-able list +const EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES = "editor/advanced/extra_auto_complete_script_sources" ## The custom balloon for this game. const BALLOON_PATH = "runtime/balloon_path" @@ -38,9 +42,12 @@ const WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS = "runtime/warn_about_ 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" +## Maximum length of text prefix in auto-generated IDs +const AUTO_GENERATED_ID_PREFIX_LENGTH = "editor/translations/auto_generated_id_prefix_length" +## Use only UUID for auto-generated IDs without text prefix +const USE_UUID_ONLY_FOR_IDS = "editor/translations/use_uuid_only_for_ids" - -const SETTINGS_CONFIGURATION = { +static var SETTINGS_CONFIGURATION = { WRAP_LONG_LINES: { value = false, type = TYPE_BOOL, @@ -68,6 +75,8 @@ const SETTINGS_CONFIGURATION = { EXTRA_CSV_LOCALES: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], is_advanced = true }, INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { @@ -80,6 +89,11 @@ const SETTINGS_CONFIGURATION = { type = TYPE_BOOL, is_advanced = true }, + UPDATE_POT_FILES_AUTOMATICALLY: { + value = true, + type = TYPE_BOOL, + is_advanced = true + }, CUSTOM_TEST_SCENE_PATH: { value = preload("./test_scene.tscn").resource_path, @@ -87,6 +101,13 @@ const SETTINGS_CONFIGURATION = { hint = PROPERTY_HINT_FILE, is_advanced = true }, + EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d/%d:*.*" % [TYPE_STRING, PROPERTY_HINT_FILE], + is_advanced = true + }, BALLOON_PATH: { value = "", @@ -96,6 +117,8 @@ const SETTINGS_CONFIGURATION = { STATE_AUTOLOAD_SHORTCUTS: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], }, WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: { value = false, @@ -112,7 +135,19 @@ const SETTINGS_CONFIGURATION = { value = false, type = TYPE_BOOL, is_hidden = true - } + }, + AUTO_GENERATED_ID_PREFIX_LENGTH: { + value = 30, + type = TYPE_INT, + hint = PROPERTY_HINT_RANGE, + hint_string = "0,100,1", + is_advanced = true + }, + USE_UUID_ONLY_FOR_IDS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, } @@ -203,7 +238,7 @@ static func get_user_config() -> Dictionary: recent_files = [], reopen_files = [], most_recent_reopen_file = "", - carets = {}, + file_meta = {}, run_title = "", run_resource_path = "", is_running_test_scene = false, @@ -229,10 +264,17 @@ static func set_user_value(key: String, value) -> void: save_user_config(user_config) -static func get_user_value(key: String, default = null): +static func get_user_value(key: String, default = null) -> Variant: return get_user_config().get(key, default) +static func forget_path(path: String) -> void: + remove_recent_file(path) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta.erase(path) + set_user_value("file_meta", file_meta) + + static func add_recent_file(path: String) -> void: var recent_files: Array = get_user_value("recent_files", []) if path in recent_files: @@ -266,23 +308,34 @@ static func clear_recent_files() -> void: static func set_caret(path: String, cursor: Vector2) -> void: - var carets: Dictionary = get_user_value("carets", {}) - carets[path] = { - x = cursor.x, - y = cursor.y - } - set_user_value("carets", carets) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ cursor = "%d,%d" % [cursor.x, cursor.y] }, true) + set_user_value("file_meta", file_meta) static func get_caret(path: String) -> Vector2: - var carets = get_user_value("carets", {}) - if carets.has(path): - var caret = carets.get(path) - return Vector2(caret.x, caret.y) + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + var cursor: PackedStringArray = file_meta.get(path).get("cursor", "0,0").split(",") + return Vector2(cursor[0].to_int(), cursor[1].to_int()) else: return Vector2.ZERO +static func set_scroll(path: String, scroll_vertical: int) -> void: + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ scroll_vertical = scroll_vertical }, true) + set_user_value("file_meta", file_meta) + + +static func get_scroll(path: String) -> int: + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + return file_meta.get(path).get("scroll_vertical", 0) + else: + return 0 + + static func check_for_dotnet_solution() -> bool: if Engine.is_editor_hint(): var has_dotnet_solution: bool = false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd index dd1da441..33fc5ac2 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd @@ -41,6 +41,7 @@ func reimport_files(and_files: PackedStringArray = []) -> void: if _files_marked_for_reimport.is_empty(): return EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) + _files_marked_for_reimport.clear() ## Add a dialogue file to the cache. 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 b890f6e9..5ef46947 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd @@ -30,6 +30,12 @@ signal confirmation_closed() @onready var parse_timer: Timer = $ParseTimer +# Banner +@onready var banner: CenterContainer = %Banner +@onready var banner_new_button: Button = %BannerNewButton +@onready var banner_quick_open: Button = %BannerQuickOpen +@onready var banner_examples: Button = %BannerExamples + # Dialogs @onready var new_dialog: FileDialog = $NewDialog @onready var save_dialog: FileDialog = $SaveDialog @@ -87,6 +93,7 @@ var current_file_path: String = "": title_list.hide() code_edit.hide() errors_panel.hide() + banner.show() else: test_button.disabled = false test_line_button.disabled = false @@ -97,17 +104,25 @@ var current_file_path: String = "": files_list.show() title_list.show() code_edit.show() + banner.hide() + + var cursor: Vector2 = DMSettings.get_caret(current_file_path) + var scroll_vertical: int = DMSettings.get_scroll(current_file_path) code_edit.text = open_buffers[current_file_path].text code_edit.errors = [] code_edit.clear_undo_history() - code_edit.set_cursor(DMSettings.get_caret(current_file_path)) + code_edit.set_cursor(cursor) + code_edit.scroll_vertical = scroll_vertical code_edit.grab_focus() _on_code_edit_text_changed() errors_panel.errors = [] code_edit.errors = [] + + if search_and_replace.visible: + search_and_replace.search() get: return current_file_path @@ -177,6 +192,8 @@ func _ready() -> void: EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) + code_edit.get_v_scroll_bar().value_changed.connect(_on_code_edit_scroll_changed) + func _exit_tree() -> void: DMSettings.set_user_value("reopen_files", open_buffers.keys()) @@ -269,6 +286,12 @@ func open_file(path: String) -> void: self.current_file_path = path +func quick_open() -> void: + quick_open_files_list.files = Engine.get_meta("DMCache").get_files() + quick_open_dialog.popup_centered() + quick_open_files_list.focus_filter() + + func show_file_in_filesystem(path: String) -> void: EditorInterface.get_file_system_dock().navigate_to_path(path) @@ -367,6 +390,9 @@ func apply_theme() -> void: font_size = editor_settings.get_setting("interface/editor/code_font_size") } + banner_new_button.icon = get_theme_icon("New", "EditorIcons") + banner_quick_open.icon = get_theme_icon("Load", "EditorIcons") + new_button.icon = get_theme_icon("New", "EditorIcons") new_button.tooltip_text = DMConstants.translate(&"start_a_new_file") @@ -493,8 +519,8 @@ func show_build_error_dialog() -> void: # Generate translation line IDs for any line that doesn't already have one func generate_translations_keys() -> void: - randomize() - seed(Time.get_unix_time_from_system()) + var rng: RandomNumberGenerator = RandomNumberGenerator.new() + rng.randomize() var cursor: Vector2 = code_edit.get_cursor() var lines: PackedStringArray = code_edit.text.split("\n") @@ -502,6 +528,8 @@ func generate_translations_keys() -> void: var key_regex = RegEx.new() key_regex.compile("\\[ID:(?.*?)\\]") + var compiled_lines: Dictionary = DMCompiler.compile_string(code_edit.text, "").lines + # Make list of known keys var known_keys = {} for i in range(0, lines.size()): @@ -524,6 +552,7 @@ func generate_translations_keys() -> void: var l = line.strip_edges() if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue + if not compiled_lines.has(str(i)): continue if "[ID:" in line: continue @@ -538,17 +567,24 @@ func generate_translations_keys() -> void: 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) + if DMSettings.get_setting(DMSettings.USE_UUID_ONLY_FOR_IDS, false): + # Generate UUID only + var uuid = str(randi() % 1000000).sha1_text().substr(0, 12) + key = uuid.to_upper() + else: + # Generate text prefix + hash + var prefix_length = DMSettings.get_setting(DMSettings.AUTO_GENERATED_ID_PREFIX_LENGTH, 30) + key = regex.ALPHA_NUMERIC.sub(text.strip_edges(), "_", true).substr(0, prefix_length) + 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() + # 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!") @@ -799,6 +835,16 @@ func show_search_form(is_enabled: bool) -> void: search_and_replace.focus_line_edit() +func run_test_scene(from_key: String) -> void: + DMSettings.set_user_value("run_title", from_key) + 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") + if ResourceUID.has_id(ResourceUID.text_to_id(test_scene_path)): + test_scene_path = ResourceUID.get_id_path(ResourceUID.text_to_id(test_scene_path)) + EditorInterface.play_custom_scene(test_scene_path) + + ### Signals @@ -831,9 +877,7 @@ func _on_open_menu_id_pressed(id: int) -> void: 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() + quick_open() OPEN_CLEAR: DMSettings.clear_recent_files() build_open_menu() @@ -923,15 +967,16 @@ func _on_main_view_visibility_changed() -> void: func _on_new_button_pressed() -> void: - new_dialog.current_file = "dialogue" + new_dialog.current_file = "untitled" 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) + var path: String = new_dialog.current_path + if path.get_file() == ".dialogue": + path = "%s/untitled.dialogue" % path.get_basename() + new_file(path) + open_file(path) func _on_new_dialog_file_selected(path: String) -> void: @@ -960,8 +1005,8 @@ func _on_quick_open_files_list_file_double_clicked(file_path: String) -> void: func _on_quick_open_dialog_confirmed() -> void: - if quick_open_files_list.current_file_path: - open_file(quick_open_files_list.current_file_path) + if quick_open_files_list.last_selected_file_path: + open_file(quick_open_files_list.last_selected_file_path) func _on_save_all_button_pressed() -> void: @@ -983,6 +1028,10 @@ func _on_code_edit_text_changed() -> void: parse_timer.start(1) +func _on_code_edit_scroll_changed(value: int) -> void: + DMSettings.set_scroll(current_file_path, code_edit.scroll_vertical) + + func _on_code_edit_active_title_change(title: String) -> void: title_list.select_title(title) @@ -1033,11 +1082,7 @@ func _on_test_button_pressed() -> void: 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) + run_test_scene("") func _on_test_line_button_pressed() -> void: @@ -1052,12 +1097,9 @@ func _on_test_line_button_pressed() -> void: 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) + break + + run_test_scene(str(line_to_run)) func _on_support_button_pressed() -> void: @@ -1139,3 +1181,22 @@ func _on_close_confirmation_dialog_custom_action(action: StringName) -> void: func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void: open_file(path) code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length) + code_edit.set_line_as_center_visible(cursor.y) + + +func _on_banner_image_gui_input(event: InputEvent) -> void: + if event.is_pressed(): + OS.shell_open("https://bravestcoconut.com/wishlist") + + +func _on_banner_new_button_pressed() -> void: + new_dialog.current_file = "untitled" + new_dialog.popup_centered() + + +func _on_banner_quick_open_pressed() -> void: + quick_open() + + +func _on_banner_examples_pressed() -> void: + OS.shell_open("https://itch.io/c/5226650/godot-dialogue-manager-example-projects") 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 4e70b263..b21f7084 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3 uid="uid://cbuf1q3xsse3q"] +[gd_scene load_steps=16 format=3 uid="uid://cbuf1q3xsse3q"] [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"] @@ -8,20 +8,9 @@ [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" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"] +[ext_resource type="Texture2D" uid="uid://cnm67htuohhlo" path="res://addons/dialogue_manager/assets/banner.png" id="9_y6rqu"] [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_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", -"height": 16, -"mipmaps": false, -"width": 16 -} - -[sub_resource type="ImageTexture" id="ImageTexture_ka3gk"] -image = SubResource("Image_faxki") - [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), @@ -31,10 +20,22 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_57eek"] +[sub_resource type="ImageTexture" id="ImageTexture_ka3gk"] image = SubResource("Image_y6rqu") -[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_kb7f8"] +[sub_resource type="Image" id="Image_mpdoc"] +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", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_57eek"] +image = SubResource("Image_mpdoc") + +[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_xv2j4"] script = ExtResource("7_necsa") [node name="MainView" type="Control"] @@ -132,7 +133,6 @@ size_flags_vertical = 3 unique_name_in_owner = true visible = false layout_mode = 2 -size_flags_vertical = 3 [node name="FilesPopupMenu" type="PopupMenu" parent="Margin/Content/SidePanel/Bookmarks/FilesList"] unique_name_in_owner = true @@ -158,6 +158,7 @@ text = "Insert" item_count = 15 popup/item_0/text = "Wave BBCode" popup/item_0/icon = SubResource("ImageTexture_57eek") +popup/item_0/id = 0 popup/item_1/text = "Shake BBCode" popup/item_1/icon = SubResource("ImageTexture_57eek") popup/item_1/id = 1 @@ -291,9 +292,9 @@ 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_colors/background_color = Color(0.156863, 0.164706, 0.211765, 1) +theme_override_colors/current_line_color = Color(0.266667, 0.278431, 0.352941, 0.243137) theme_override_font_sizes/font_size = 14 theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1) text = "~ start @@ -312,12 +313,57 @@ Coco: Meow. => END" scroll_smooth = true -syntax_highlighter = SubResource("SyntaxHighlighter_kb7f8") +syntax_highlighter = SubResource("SyntaxHighlighter_xv2j4") [node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")] unique_name_in_owner = true layout_mode = 2 +[node name="Banner" type="CenterContainer" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 34.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="PanelContainer" type="VBoxContainer" parent="Banner"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="BannerImage" type="TextureRect" parent="Banner/PanelContainer"] +custom_minimum_size = Vector2(600, 200) +layout_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 2 +texture = ExtResource("9_y6rqu") +expand_mode = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="Banner/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="BannerNewButton" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "New Dialogue..." + +[node name="BannerQuickOpen" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Quick open..." + +[node name="BannerExamples" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Example projects..." + [node name="NewDialog" type="FileDialog" parent="."] size = Vector2i(900, 750) min_size = Vector2i(600, 500) @@ -417,6 +463,10 @@ 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="gui_input" from="Banner/PanelContainer/BannerImage" to="." method="_on_banner_image_gui_input"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerNewButton" to="." method="_on_banner_new_button_pressed"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerQuickOpen" to="." method="_on_banner_quick_open_pressed"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerExamples" to="." method="_on_banner_examples_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"] diff --git a/Zennysoft.Game.Ma/project.godot b/Zennysoft.Game.Ma/project.godot index a93a4fdd..b1962f62 100644 --- a/Zennysoft.Game.Ma/project.godot +++ b/Zennysoft.Game.Ma/project.godot @@ -30,8 +30,8 @@ runtime/advanced/uses_dotnet=true [display] -window/size/viewport_width=1920 -window/size/viewport_height=1080 +window/size/viewport_width=1280 +window/size/viewport_height=720 [dotnet] diff --git a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import index bb7761ed..d06b262d 100644 --- a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import +++ b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_14" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://lao0opxww3ib" path="res://.godot/imported/Dialogue.dialogue-176033575bc12c347010f3a30b2e302a.tres" diff --git a/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml b/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml index 112e6c2b..3baf022f 100644 --- a/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml +++ b/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml @@ -1,12 +1,12 @@ @startuml EnemyLogic state "EnemyLogic State" as Zennysoft_Game_Ma_EnemyLogic_State { state "Alive" as Zennysoft_Game_Ma_EnemyLogic_State_Alive { + state "Idle" as Zennysoft_Game_Ma_EnemyLogic_State_Idle state "Activated" as Zennysoft_Game_Ma_EnemyLogic_State_Activated { + state "Patrolling" as Zennysoft_Game_Ma_EnemyLogic_State_Patrolling state "FollowPlayer" as Zennysoft_Game_Ma_EnemyLogic_State_FollowPlayer state "Attacking" as Zennysoft_Game_Ma_EnemyLogic_State_Attacking - state "Patrolling" as Zennysoft_Game_Ma_EnemyLogic_State_Patrolling } - state "Idle" as Zennysoft_Game_Ma_EnemyLogic_State_Idle } state "Defeated" as Zennysoft_Game_Ma_EnemyLogic_State_Defeated } diff --git a/Zennysoft.Game.Ma/src/game/Game.tscn b/Zennysoft.Game.Ma/src/game/Game.tscn index ff47afc5..ab34902c 100644 --- a/Zennysoft.Game.Ma/src/game/Game.tscn +++ b/Zennysoft.Game.Ma/src/game/Game.tscn @@ -83,7 +83,7 @@ script = ExtResource("1_ytcii") [node name="SubViewportContainer" type="SubViewportContainer" parent="."] material = SubResource("ShaderMaterial_e75a2") -custom_minimum_size = Vector2(1280, 960) +custom_minimum_size = Vector2(1280, 720) anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -94,7 +94,7 @@ stretch = true [node name="SubViewport" type="SubViewport" parent="SubViewportContainer"] transparent_bg = true handle_input_locally = false -size = Vector2i(1920, 1080) +size = Vector2i(1280, 720) render_target_update_mode = 4 [node name="PauseContainer" type="Node3D" parent="SubViewportContainer/SubViewport"] @@ -119,6 +119,7 @@ wait_time = 30.0 [node name="InGameUI" parent="." instance=ExtResource("5_lxtnp")] unique_name_in_owner = true visible = false +custom_minimum_size = Vector2(1280, 720) [node name="InGameAudio" parent="." instance=ExtResource("6_qc71l")] unique_name_in_owner = true diff --git a/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import b/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import index 53b52b20..09775e7e 100644 --- a/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import +++ b/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_12" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://b681mabgdkg6j" path="res://.godot/imported/ran.dialogue-f0ecb078c74af14bf6b7b4a9926b74f8.tres" diff --git a/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import b/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import index 3a37d471..b14548cf 100644 --- a/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import +++ b/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_12" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://cf7ycgdiihyh" path="res://.godot/imported/ratdialogue.dialogue-7389c3591865c01b56ddf5a724bb1208.tres" diff --git a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn index 0c70a096..d92ac925 100644 --- a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn +++ b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn @@ -23,7 +23,7 @@ script = ExtResource("1_sc13i") [node name="PlayerInfoUI" parent="." instance=ExtResource("4_46s5l")] unique_name_in_owner = true -custom_minimum_size = Vector2(1024, 768) +custom_minimum_size = Vector2(1280, 720) layout_mode = 1 [node name="MiniMap" parent="." instance=ExtResource("2_6sfje")] diff --git a/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn b/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn index 7cf138b6..979cf88b 100644 --- a/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn +++ b/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn @@ -10,6 +10,7 @@ layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +offset_bottom = 40.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_d8yyu") diff --git a/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn b/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn index dc3f4c0d..3372bf3f 100644 --- a/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn +++ b/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=14 format=3 uid="uid://bea2waybmgd6u"] -[ext_resource type="Script" path="res://src/ui/in_game_ui/UseTeleportPrompt.cs" id="1_x3wkp"] +[ext_resource type="Script" uid="uid://dvn7g207w5jaj" path="res://src/ui/in_game_ui/UseTeleportPrompt.cs" id="1_x3wkp"] [ext_resource type="FontFile" uid="uid://cm8j5vcdop5x0" path="res://src/ui/fonts/Mrs-Eaves-OT-Roman_31443.ttf" id="2_i6kb2"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ahhj2"] @@ -102,9 +102,9 @@ tracks/1/keys = { [sub_resource type="AnimationLibrary" id="AnimationLibrary_7x216"] _data = { -"RESET": SubResource("Animation_xrfau"), -"fade_in": SubResource("Animation_efhb5"), -"fade_out": SubResource("Animation_ibgld") +&"RESET": SubResource("Animation_xrfau"), +&"fade_in": SubResource("Animation_efhb5"), +&"fade_out": SubResource("Animation_ibgld") } [node name="UseTeleportPrompt" type="Control"] @@ -145,8 +145,8 @@ focus_neighbor_left = NodePath(".") focus_neighbor_top = NodePath(".") focus_neighbor_right = NodePath(".") focus_neighbor_bottom = NodePath("../NoButton") -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) theme_override_fonts/font = ExtResource("2_i6kb2") theme_override_font_sizes/font_size = 50 theme_override_styles/focus = SubResource("StyleBoxEmpty_1tca4") @@ -162,8 +162,8 @@ focus_neighbor_left = NodePath(".") focus_neighbor_top = NodePath("../YesButton") focus_neighbor_right = NodePath(".") focus_neighbor_bottom = NodePath(".") -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) theme_override_fonts/font = ExtResource("2_i6kb2") theme_override_font_sizes/font_size = 50 theme_override_styles/focus = SubResource("StyleBoxEmpty_yoep7") @@ -175,5 +175,5 @@ text = "No" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] unique_name_in_owner = true libraries = { -"": SubResource("AnimationLibrary_7x216") +&"": SubResource("AnimationLibrary_7x216") }