diff --git a/Autoloads/ConsoleManager.cs b/Autoloads/ConsoleManager.cs new file mode 100644 index 0000000..6cafdf9 --- /dev/null +++ b/Autoloads/ConsoleManager.cs @@ -0,0 +1,158 @@ +using Godot; +using Limbo.Console.Sharp; +using Mr.BrickAdventures.scripts; +using Mr.BrickAdventures.scripts.components; + +namespace Mr.BrickAdventures.Autoloads; + +public partial class ConsoleManager : Node +{ + private GameManager _gameManager; + private SkillManager _skillManager; + private SkillUnlockerComponent _skillUnlockerComponent; + + public override void _Ready() + { + _gameManager = GetNode("/root/GameManager"); + + RegisterConsoleCommands(); + } + + public override void _ExitTree() + { + UnregisterConsoleCommands(); + } + + [ConsoleCommand("add_coins", "Adds a specified amount of coins to the player's total.")] + private void AddCoinsCommand(int amount) + { + _gameManager.AddCoins(amount); + LimboConsole.Info($"Increased coins by {amount}. Total coins: {_gameManager.GetCoins()}"); + } + + [ConsoleCommand("set_coins", "Sets the player's total coins to a specified amount.")] + private void SetCoinsCommand(int amount) + { + _gameManager.SetCoins(amount); + LimboConsole.Info($"Set coins to {amount}. Total coins: {_gameManager.GetCoins()}"); + } + + [ConsoleCommand("set_lives", "Sets the player's total lives to a specified amount.")] + private void SetLivesCommand(int amount) + { + _gameManager.SetLives(amount); + LimboConsole.Info($"Set lives to {amount}."); + } + + [ConsoleCommand("add_lives", "Adds a specified amount of lives to the player's total.")] + private void AddLivesCommand(int amount) + { + _gameManager.AddLives(amount); + LimboConsole.Info($"Increased lives by {amount}. Total lives: {_gameManager.GetLives()}"); + } + + [ConsoleCommand("set_health", "Sets the player's health to a specified amount.")] + private void SetHealthCommand(float amount) + { + var playerHealthComponent = _gameManager.Player.GetNode("HealthComponent"); + if (playerHealthComponent != null) + { + playerHealthComponent.Health = amount; + LimboConsole.Info($"Set player health to {amount}."); + } + else + { + LimboConsole.Warn("Player HealthComponent not found."); + } + } + + [ConsoleCommand("reset_session", "Resets the current session state.")] + private void ResetSessionCommand() + { + _gameManager.ResetCurrentSessionState(); + LimboConsole.Info("Current session state has been reset."); + } + + [ConsoleCommand("unlock_skill", "Unlocks and activates a skill by its name.")] + private void UnlockSkillCommand(string skillName) + { + if (!GetSkillManagement()) return; + + var skill = _skillManager.GetSkillByName(skillName); + if (skill == null) + { + LimboConsole.Warn($"Skill '{skillName}' not found."); + return; + } + + _gameManager.UnlockSkill(skill); + _skillManager.ActivateSkill(skill); + _skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill); + LimboConsole.Info($"Skill '{skillName}' has been unlocked and activated."); + } + + private bool GetSkillManagement() + { + var player = _gameManager.Player; + if (player == null) + { + LimboConsole.Warn("Player node not found."); + return false; + } + + _skillManager ??= player.GetNode("SkillManager"); + _skillUnlockerComponent ??= player.GetNode("SkillUnlockerComponent"); + + if (_skillManager != null && _skillUnlockerComponent != null) return true; + + LimboConsole.Warn("SkillManager or SkillUnlockerComponent not found on the player."); + return false; + + } + + [ConsoleCommand("unlock_all_skills", "Unlocks and activates all available skills.")] + private void UnlockAllSkillsCommand() + { + if (!GetSkillManagement()) return; + + _skillUnlockerComponent.UnlockAllSkills(); + LimboConsole.Info("All skills have been unlocked and activated."); + } + + [ConsoleCommand("remove_skill", "Deactivates and removes a skill by its name.")] + private void RemoveSkillCommand(string skillName) + { + if (!GetSkillManagement()) return; + + var skill = _skillManager.GetSkillByName(skillName); + if (skill == null) + { + LimboConsole.Warn($"Skill '{skillName}' not found."); + return; + } + + _gameManager.RemoveSkill(skill.Name); + _skillManager.DeactivateSkill(skill); + LimboConsole.Info($"Skill '{skillName}' has been deactivated."); + } + + [ConsoleCommand("remove_all_skills", "Deactivates and removes all skills.")] + private void RemoveAllSkillsCommand() + { + if (!GetSkillManagement()) return; + + foreach (var skill in _skillManager.AvailableSkills) + { + _gameManager.RemoveSkill(skill.Name); + _skillManager.DeactivateSkill(skill); + } + LimboConsole.Info("All skills have been deactivated."); + } + + [ConsoleCommand("next_level", "Advances the game to the next level.")] + private void GoToNextLevelCommand() + { + _gameManager.OnLevelComplete(); + } + +} \ No newline at end of file diff --git a/Autoloads/ConsoleManager.cs.uid b/Autoloads/ConsoleManager.cs.uid new file mode 100644 index 0000000..f2fd635 --- /dev/null +++ b/Autoloads/ConsoleManager.cs.uid @@ -0,0 +1 @@ +uid://bv2teruv4s6vg diff --git a/Autoloads/GameManager.cs b/Autoloads/GameManager.cs index ae81968..7f355d5 100644 --- a/Autoloads/GameManager.cs +++ b/Autoloads/GameManager.cs @@ -9,12 +9,17 @@ namespace Mr.BrickAdventures.Autoloads; public partial class GameManager : Node { - [Export] public Array LevelScenes { get; set; } = new(); + [Export] public Array LevelScenes { get; set; } = []; - public PlayerController Player { get; set; } + public PlayerController Player { + get => GetPlayer(); + private set => _player = value; + } - private List _sceneNodes = new(); - + private List _sceneNodes = []; + private PlayerController _player; + + [Export] public Dictionary PlayerState { get; set; } = new() { { "coins", 0 }, @@ -24,7 +29,8 @@ public partial class GameManager : Node { "unlocked_levels", new Array() {0}}, { "unlocked_skills", new Array() } }; - + + [Export] public Dictionary CurrentSessionState { get; private set; } = new() { { "coins_collected", 0 }, @@ -225,14 +231,14 @@ public partial class GameManager : Node public PlayerController GetPlayer() { - if (Player != null) return Player; + if (_player != null) return _player; foreach (var node in _sceneNodes) { if (node is not PlayerController player) continue; - Player = player; - return Player; + _player = player; + return _player; } GD.PrintErr("PlayerController not found in the scene tree."); diff --git a/Mr. Brick Adventures.csproj b/Mr. Brick Adventures.csproj index 91104ef..a4e2ad9 100644 --- a/Mr. Brick Adventures.csproj +++ b/Mr. Brick Adventures.csproj @@ -5,139 +5,6 @@ Mr.BrickAdventures - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/Mr. Brick Adventures.sln.DotSettings.user b/Mr. Brick Adventures.sln.DotSettings.user index 38db013..122ad95 100644 --- a/Mr. Brick Adventures.sln.DotSettings.user +++ b/Mr. Brick Adventures.sln.DotSettings.user @@ -1,6 +1,10 @@  ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded \ No newline at end of file diff --git a/addons/console/console.gd b/addons/console/console.gd deleted file mode 100644 index c9632b7..0000000 --- a/addons/console/console.gd +++ /dev/null @@ -1,528 +0,0 @@ -extends Node - -var enabled := true -var enable_on_release_build := false: set = set_enable_on_release_build -var pause_enabled := false -signal console_opened -signal console_closed -signal console_unknown_command - - -class ConsoleCommand: - var function: Callable - var arguments: PackedStringArray - var required: int - var description: String - var hidden: bool - - - func _init(in_function: Callable, in_arguments: PackedStringArray, in_required: int = 0, in_description: String = ""): - function = in_function - arguments = in_arguments - required = in_required - description = in_description - - -var control := Control.new() -# If you want to customize the way the console looks, you can direcly modify -# the properties of the rich text and line edit here: -var rich_label := RichTextLabel.new() -var line_edit := LineEdit.new() -var console_commands := {} -var command_parameters := {} -var console_history := [] -var console_history_index := 0 -var was_paused_already := false - - -## Usage: Console.add_command("command_name", , , , "Help description") -func add_command(command_name: String, function: Callable, arguments = [], required: int = 0, description: String = "") -> void: - if (arguments is int): - # Legacy call using an argument number - var param_array: PackedStringArray - for i in range(arguments): - param_array.append("arg_" + str(i + 1)) - console_commands[command_name] = ConsoleCommand.new(function, param_array, required, description) - elif (arguments is Array): - # New array argument system - var str_args: PackedStringArray - for argument in arguments: - str_args.append(str(argument)) - console_commands[command_name] = ConsoleCommand.new(function, str_args, required, description) - - -## Adds a secret command that will not show up in the help or auto-complete. -func add_hidden_command(command_name: String, function: Callable, arguments = [], required: int = 0) -> void: - add_command(command_name, function, arguments, required) - console_commands[command_name].hidden = true - - -## Removes a command from the console. This should be called on a script's _exit_tree() -## if you have console commands for things that are unloaded before the project closes. -func remove_command(command_name: String) -> void: - console_commands.erase(command_name) - command_parameters.erase(command_name) - - -## Useful if you have a list of possible parameters (ex: level names). -func add_command_autocomplete_list(command_name: String, param_list: PackedStringArray): - command_parameters[command_name] = param_list - - -func _enter_tree() -> void: - var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.READ) - if (console_history_file): - while (!console_history_file.eof_reached()): - var line := console_history_file.get_line() - if (line.length()): - add_input_history(line) - - var canvas_layer := CanvasLayer.new() - canvas_layer.layer = 3 - add_child(canvas_layer) - control.anchor_bottom = 1.0 - control.anchor_right = 1.0 - canvas_layer.add_child(control) - var style := StyleBoxFlat.new() - style.bg_color = Color("000000d7") - rich_label.selection_enabled = true - rich_label.context_menu_enabled = true - rich_label.bbcode_enabled = false - rich_label.scroll_following = true - rich_label.anchor_right = 1.0 - rich_label.anchor_bottom = 0.5 - rich_label.add_theme_stylebox_override("normal", style) - control.add_child(rich_label) - rich_label.append_text("Development console.\n") - line_edit.anchor_top = 0.5 - line_edit.anchor_right = 1.0 - line_edit.anchor_bottom = 0.5 - line_edit.placeholder_text = "Enter \"help\" for instructions" - control.add_child(line_edit) - line_edit.text_submitted.connect(on_text_entered) - line_edit.text_changed.connect(on_line_edit_text_changed) - control.visible = false - process_mode = PROCESS_MODE_ALWAYS - - -func _exit_tree() -> void: - var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.WRITE) - if (console_history_file): - var write_index := 0 - var start_write_index := console_history.size() - 100 # Max lines to write - for line in console_history: - if (write_index >= start_write_index): - console_history_file.store_line(line) - write_index += 1 - - -func _ready() -> void: - add_command("quit", quit, 0, 0, "Quits the game.") - add_command("exit", quit, 0, 0, "Quits the game.") - add_command("clear", clear, 0, 0, "Clears the text on the console.") - add_command("delete_history", delete_history, 0, 0, "Deletes the history of previously entered commands.") - add_command("help", help, 0, 0, "Displays instructions on how to use the console.") - add_command("commands_list", commands_list, 0, 0, "Lists all commands and their descriptions.") - add_command("commands", commands, 0, 0, "Lists commands with no descriptions.") - add_command("calc", calculate, ["mathematical expression to evaluate"], 0, "Evaluates the math passed in for quick arithmetic.") - add_command("echo", print_line, ["string"], 1, "Prints given string to the console.") - add_command("echo_warning", print_warning, ["string"], 1, "Prints given string as warning to the console.") - add_command("echo_info", print_info, ["string"], 1, "Prints given string as info to the console.") - add_command("echo_error", print_error, ["string"], 1, "Prints given string as an error to the console.") - add_command("pause", pause, 0, 0, "Pauses node processing.") - add_command("unpause", unpause, 0, 0, "Unpauses node processing.") - add_command("exec", exec, 1, 1, "Execute a script.") - - -func _input(event: InputEvent) -> void: - if (event is InputEventKey): - if (event.get_physical_keycode_with_modifiers() == KEY_QUOTELEFT): # ~ key. - if (event.pressed): - toggle_console() - get_tree().get_root().set_input_as_handled() - elif (event.physical_keycode == KEY_QUOTELEFT and event.is_command_or_control_pressed()): # Toggles console size or opens big console. - if (event.pressed): - if (control.visible): - toggle_size() - else: - toggle_console() - toggle_size() - get_tree().get_root().set_input_as_handled() - elif (event.get_physical_keycode_with_modifiers() == KEY_ESCAPE && control.visible): # Disable console on ESC - if (event.pressed): - toggle_console() - get_tree().get_root().set_input_as_handled() - if (control.visible and event.pressed): - if (event.get_physical_keycode_with_modifiers() == KEY_UP): - get_tree().get_root().set_input_as_handled() - if (console_history_index > 0): - console_history_index -= 1 - if (console_history_index >= 0): - line_edit.text = console_history[console_history_index] - line_edit.caret_column = line_edit.text.length() - reset_autocomplete() - if (event.get_physical_keycode_with_modifiers() == KEY_DOWN): - get_tree().get_root().set_input_as_handled() - if (console_history_index < console_history.size()): - console_history_index += 1 - if (console_history_index < console_history.size()): - line_edit.text = console_history[console_history_index] - line_edit.caret_column = line_edit.text.length() - reset_autocomplete() - else: - line_edit.text = "" - reset_autocomplete() - if (event.get_physical_keycode_with_modifiers() == KEY_PAGEUP): - var scroll := rich_label.get_v_scroll_bar() - var tween := create_tween() - tween.tween_property(scroll, "value", scroll.value - (scroll.page - scroll.page * 0.1), 0.1) - get_tree().get_root().set_input_as_handled() - if (event.get_physical_keycode_with_modifiers() == KEY_PAGEDOWN): - var scroll := rich_label.get_v_scroll_bar() - var tween := create_tween() - tween.tween_property(scroll, "value", scroll.value + (scroll.page - scroll.page * 0.1), 0.1) - get_tree().get_root().set_input_as_handled() - if (event.get_physical_keycode_with_modifiers() == KEY_TAB): - autocomplete() - get_tree().get_root().set_input_as_handled() - - -var suggestions := [] -var current_suggest := 0 -var suggesting := false - - -func autocomplete() -> void: - if (suggesting): - for i in range(suggestions.size()): - if (current_suggest == i): - line_edit.text = str(suggestions[i]) - line_edit.caret_column = line_edit.text.length() - if (current_suggest == suggestions.size() - 1): - current_suggest = 0 - else: - current_suggest += 1 - return - else: - suggesting = true - - if (" " in line_edit.text): # We're searching for a parameter to autocomplete - var split_text := parse_line_input(line_edit.text) - if (split_text.size() > 1): - var command := split_text[0] - var param_input := split_text[1] - if (command_parameters.has(command)): - for param in command_parameters[command]: - if (param_input in param): - suggestions.append(str(command, " ", param)) - else: - var sorted_commands := [] - for command in console_commands: - if (!console_commands[command].hidden): - sorted_commands.append(str(command)) - sorted_commands.sort() - sorted_commands.reverse() - - var prev_index := 0 - for command in sorted_commands: - if (!line_edit.text || command.contains(line_edit.text)): - var index: int = command.find(line_edit.text) - if (index <= prev_index): - suggestions.push_front(command) - else: - suggestions.push_back(command) - prev_index = index - autocomplete() - - -func reset_autocomplete() -> void: - suggestions.clear() - current_suggest = 0 - suggesting = false - - -func toggle_size() -> void: - if (control.anchor_bottom == 1.0): - control.anchor_bottom = 1.9 - else: - control.anchor_bottom = 1.0 - - -func disable(): - enabled = false - toggle_console() # Ensure hidden if opened - - -func enable(): - enabled = true - - -func toggle_console() -> void: - if (enabled): - control.visible = !control.visible - else: - control.visible = false - - if (control.visible): - was_paused_already = get_tree().paused - get_tree().paused = was_paused_already || pause_enabled - line_edit.grab_focus() - console_opened.emit() - else: - control.anchor_bottom = 1.0 - scroll_to_bottom() - reset_autocomplete() - if (pause_enabled && !was_paused_already): - get_tree().paused = false - console_closed.emit() - - -func is_visible(): - return control.visible - - -func scroll_to_bottom() -> void: - var scroll: ScrollBar = rich_label.get_v_scroll_bar() - scroll.value = scroll.max_value - scroll.page - - -func print_error(text: Variant, print_godot := false) -> void: - if not text is String: - text = str(text) - print_line("[color=light_coral] ERROR:[/color] %s" % text, print_godot) - - -func print_info(text: Variant, print_godot := false) -> void: - if not text is String: - text = str(text) - print_line("[color=light_blue] INFO:[/color] %s" % text, print_godot) - - -func print_warning(text: Variant, print_godot := false) -> void: - if not text is String: - text = str(text) - print_line("[color=gold] WARNING:[/color] %s" % text, print_godot) - - -func print_line(text: Variant, print_godot := false) -> void: - if not text is String: - text = str(text) - if (!rich_label): # Tried to print something before the console was loaded. - call_deferred("print_line", text) - else: - rich_label.append_text(text) - rich_label.append_text("\n") - if (print_godot): - print(text) - - -func parse_line_input(text: String) -> PackedStringArray: - var out_array: PackedStringArray - var first_char := true - var in_quotes := false - var escaped := false - var token: String - for c in text: - if (c == '\\'): - escaped = true - continue - elif (escaped): - if (c == 'n'): - c = '\n' - elif (c == 't'): - c = '\t' - elif (c == 'r'): - c = '\r' - elif (c == 'a'): - c = '\a' - elif (c == 'b'): - c = '\b' - elif (c == 'f'): - c = '\f' - escaped = false - elif (c == '\"'): - in_quotes = !in_quotes - continue - elif (c == ' ' || c == '\t'): - if (!in_quotes): - out_array.push_back(token) - token = "" - continue - token += c - out_array.push_back(token) - return out_array - - -func on_text_entered(new_text: String) -> void: - scroll_to_bottom() - reset_autocomplete() - line_edit.clear() - if (line_edit.has_method(&"edit")): - line_edit.call_deferred(&"edit") - - if not new_text.strip_edges().is_empty(): - add_input_history(new_text) - print_line("[i]> " + new_text + "[/i]") - var text_split := parse_line_input(new_text) - var text_command := text_split[0] - - if console_commands.has(text_command): - var arguments := text_split.slice(1) - var console_command: ConsoleCommand = console_commands[text_command] - - # calc is a especial command that needs special treatment - if (text_command.match("calc")): - var expression := "" - for word in arguments: - expression += word - console_command.function.callv([expression]) - return - - if (arguments.size() < console_command.required): - print_error("Too few arguments! Required < %d >" % console_command.required) - return - elif (arguments.size() > console_command.arguments.size()): - arguments.resize(console_command.arguments.size()) - - # Functions fail to call if passed the incorrect number of arguments, so fill out with blank strings. - while (arguments.size() < console_command.arguments.size()): - arguments.append("") - - console_command.function.callv(arguments) - else: - console_unknown_command.emit(text_command) - print_error("Command not found.") - - -func on_line_edit_text_changed(new_text: String) -> void: - reset_autocomplete() - - -func quit() -> void: - get_tree().quit() - - -func clear() -> void: - rich_label.clear() - - -func delete_history() -> void: - console_history.clear() - console_history_index = 0 - DirAccess.remove_absolute("user://console_history.txt") - - -func help() -> void: - rich_label.append_text(" Built in commands: - [color=light_green]calc[/color]: Calculates a given expresion -[color=light_green]clear[/color]: Clears the registry view -[color=light_green]commands[/color]: Shows a reduced list of all the currently registered commands -[color=light_green]commands_list[/color]: Shows a detailed list of all the currently registered commands -[color=light_green]delete_history[/color]: Deletes the commands history -[color=light_green]echo[/color]: Prints a given string to the console -[color=light_green]echo_error[/color]: Prints a given string as an error to the console -[color=light_green]echo_info[/color]: Prints a given string as info to the console -[color=light_green]echo_warning[/color]: Prints a given string as warning to the console -[color=light_green]pause[/color]: Pauses node processing -[color=light_green]unpause[/color]: Unpauses node processing -[color=light_green]quit[/color]: Quits the game -Controls: -[color=light_blue]Up[/color] and [color=light_blue]Down[/color] arrow keys to navigate commands history -[color=light_blue]PageUp[/color] and [color=light_blue]PageDown[/color] to scroll registry -[[color=light_blue]Ctrl[/color] + [color=light_blue]~[/color]] to change console size between half screen and full screen -[color=light_blue]~[/color] or [color=light_blue]Esc[/color] key to close the console -[color=light_blue]Tab[/color] key to autocomplete, [color=light_blue]Tab[/color] again to cycle between matching suggestions\n\n") - - - - - - - - - - - - - - - - - - - - - - - -func calculate(command: String) -> void: - var expression := Expression.new() - var error = expression.parse(command) - if error: - print_error("%s" % expression.get_error_text()) - return - var result = expression.execute() - if not expression.has_execute_failed(): - print_line(str(result)) - else: - print_error("%s" % expression.get_error_text()) - - -func commands() -> void: - var commands := [] - for command in console_commands: - if (!console_commands[command].hidden): - commands.append(str(command)) - commands.sort() - rich_label.append_text(" ") - rich_label.append_text(str(commands) + "\n\n") - - -func commands_list() -> void: - var commands := [] - for command in console_commands: - if (!console_commands[command].hidden): - commands.append(str(command)) - commands.sort() - - for command in commands: - var arguments_string := "" - var description: String = console_commands[command].description - for i in range(console_commands[command].arguments.size()): - if i < console_commands[command].required: - arguments_string += " [color=cornflower_blue]<" + console_commands[command].arguments[i] + ">[/color]" - else: - arguments_string += " <" + console_commands[command].arguments[i] + ">" - rich_label.append_text(" [color=light_green]%s[/color][color=gray]%s[/color]: %s\n" % [command, arguments_string, description]) - rich_label.append_text("\n") - - -func add_input_history(text: String) -> void: - if (!console_history.size() || text != console_history.back()): # Don't add consecutive duplicates - console_history.append(text) - console_history_index = console_history.size() - - -func set_enable_on_release_build(enable: bool): - enable_on_release_build = enable - if (!enable_on_release_build): - if (!OS.is_debug_build()): - disable() - - -func pause() -> void: - get_tree().paused = true - - -func unpause() -> void: - get_tree().paused = false - - -func exec(filename: String) -> void: - var path := "user://%s.txt" % [filename] - var script := FileAccess.open(path, FileAccess.READ) - if (script): - while (!script.eof_reached()): - on_text_entered(script.get_line()) - else: - print_error("File %s not found." % [path]) diff --git a/addons/console/console.gd.uid b/addons/console/console.gd.uid deleted file mode 100644 index 2530002..0000000 --- a/addons/console/console.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ouiu5xh1cs8n diff --git a/addons/console/console_plugin.gd b/addons/console/console_plugin.gd deleted file mode 100644 index 7c99968..0000000 --- a/addons/console/console_plugin.gd +++ /dev/null @@ -1,11 +0,0 @@ -@tool -extends EditorPlugin - - -func _enter_tree(): - print("Console plugin activated.") - add_autoload_singleton("Console", "res://addons/console/console.gd") - - -func _exit_tree(): - remove_autoload_singleton("Console") diff --git a/addons/console/console_plugin.gd.uid b/addons/console/console_plugin.gd.uid deleted file mode 100644 index a8d0e78..0000000 --- a/addons/console/console_plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cv2joe2dgkub1 diff --git a/addons/console/plugin.cfg b/addons/console/plugin.cfg deleted file mode 100644 index fe8c43a..0000000 --- a/addons/console/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Developer Console" -description="Developer console. Press ~ to activate it in game and execute commands." -author="jitspoe" -version="1.3.1" -script="console_plugin.gd" diff --git a/addons/dialogue_manager/DialogueManager.cs b/addons/dialogue_manager/DialogueManager.cs new file mode 100644 index 0000000..04e2944 --- /dev/null +++ b/addons/dialogue_manager/DialogueManager.cs @@ -0,0 +1,576 @@ +using Godot; +using Godot.Collections; +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +#nullable enable + +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 + { + get + { + if (instance == null) + { + instance = Engine.GetSingleton("DialogueManager"); + instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + } + 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 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; + } + } + + // Add a single frame wait in case the method returns before signals can listen + await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); + + // invoke method and handle the result based on return type + object result = info.Invoke(thing, _args); + + if (result is Task taskResult) + { + await taskResult; + try + { + Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); + EmitSignal(SignalName.Resolved, value); + } + catch (Exception) + { + 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) + { + 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 ""; + } + } + } + + + 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 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 $" + + + + + + + + + diff --git a/addons/dialogue_manager/assets/icon.svg.import b/addons/dialogue_manager/assets/icon.svg.import new file mode 100644 index 0000000..3b6fd5e --- /dev/null +++ b/addons/dialogue_manager/assets/icon.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3lr2uas6ax8v" +path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/dialogue_manager/assets/icon.svg" +dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/dialogue_manager/assets/responses_menu.svg b/addons/dialogue_manager/assets/responses_menu.svg new file mode 100644 index 0000000..4e4089d --- /dev/null +++ b/addons/dialogue_manager/assets/responses_menu.svg @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/addons/dialogue_manager/assets/responses_menu.svg.import b/addons/dialogue_manager/assets/responses_menu.svg.import new file mode 100644 index 0000000..83355fc --- /dev/null +++ b/addons/dialogue_manager/assets/responses_menu.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://drjfciwitjm83" +path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/dialogue_manager/assets/responses_menu.svg" +dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/dialogue_manager/assets/update.svg b/addons/dialogue_manager/assets/update.svg new file mode 100644 index 0000000..a5b80ee --- /dev/null +++ b/addons/dialogue_manager/assets/update.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + diff --git a/addons/dialogue_manager/assets/update.svg.import b/addons/dialogue_manager/assets/update.svg.import new file mode 100644 index 0000000..2d8171a --- /dev/null +++ b/addons/dialogue_manager/assets/update.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3baj6rygkb3f" +path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/dialogue_manager/assets/update.svg" +dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/dialogue_manager/compiler/compilation.gd b/addons/dialogue_manager/compiler/compilation.gd new file mode 100644 index 0000000..020dd27 --- /dev/null +++ b/addons/dialogue_manager/compiler/compilation.gd @@ -0,0 +1,1111 @@ +## A single compilation instance of some dialogue. +class_name DMCompilation extends RefCounted + + +#region Compilation locals + + +## A list of file paths that were imported by this file. +var imported_paths: PackedStringArray = [] +## A list of state names from "using" clauses. +var using_states: PackedStringArray = [] +## A map of titles in this file. +var titles: Dictionary = {} +## The first encountered title in this file. +var first_title: String = "" +## A list of character names in this file. +var character_names: PackedStringArray = [] +## A list of any compilation errors. +var errors: Array[Dictionary] = [] +## A map of all compiled lines. +var lines: Dictionary = {} +## A flattened and simplified map of compiled lines for storage in a resource. +var data: Dictionary = {} + + +#endregion + +#region Internal variables + + +# A list of all [RegEx] references +var regex: DMCompilerRegEx = DMCompilerRegEx.new() +# For parsing condition/mutation expressions +var expression_parser: DMExpressionParser = DMExpressionParser.new() + +# A map of titles that came from imported files. +var _imported_titles: Dictionary = {} +# Used to keep track of circular imports. +var _imported_line_map: Dictionary = {} +# The number of imported lines. +var _imported_line_count: int = 0 +# A list of already encountered static line IDs. +var _known_translation_keys: Dictionary = {} +# A noop for retrieving the next line without conditions. +var _first: Callable = func(_s): return true + +# Title jumps are adjusted as they are parsed so any goto lines might need to be adjusted after they are first seen. +var _goto_lines: Dictionary = {} + + +#endregion + +#region Main + + +## Compile some text. +func compile(text: String, path: String = ".") -> Error: + titles = {} + character_names = [] + + parse_line_tree(build_line_tree(inject_imported_files(text + "\n=> END", path))) + + # Convert the compiles lines to a Dictionary so they can be stored. + for id in lines: + var line: DMCompiledLine = lines[id] + data[id] = line.to_data() + + if errors.size() > 0: + return ERR_PARSE_ERROR + + return OK + + +## Inject any imported files +func inject_imported_files(text: String, path: String) -> PackedStringArray: + # Work out imports + var known_imports: Dictionary = {} + + # Include the base file path so that we can get around circular dependencies + known_imports[path.hash()] = "." + + var raw_lines: PackedStringArray = text.split("\n") + + for id in range(0, raw_lines.size()): + var line = raw_lines[id] + if is_import_line(line): + var import_data: Dictionary = extract_import_path_and_name(line) + + if not import_data.has("path"): continue + + var import_hash: int = import_data.path.hash() + if import_data.size() > 0: + # Keep track of titles so we can add imported ones later + if str(import_hash) in _imported_titles.keys(): + add_error(id, 0, DMConstants.ERR_FILE_ALREADY_IMPORTED) + if import_data.prefix in _imported_titles.values(): + add_error(id, 0, DMConstants.ERR_DUPLICATE_IMPORT_NAME) + _imported_titles[str(import_hash)] = import_data.prefix + + # Import the file content + if not known_imports.has(import_hash): + var error: Error = import_content(import_data.path, import_data.prefix, _imported_line_map, known_imports) + if error != OK: + add_error(id, 0, error) + + # Make a map so we can refer compiled lines to where they were imported from + if not _imported_line_map.has(import_hash): + _imported_line_map[import_hash] = { + hash = import_hash, + imported_on_line_number = id, + from_line = 0, + to_line = 0 + } + + var imported_content: String = "" + var cummulative_line_number: int = 0 + for item in _imported_line_map.values(): + item["from_line"] = cummulative_line_number + if known_imports.has(item.hash): + cummulative_line_number += known_imports[item.hash].split("\n").size() + item["to_line"] = cummulative_line_number + if known_imports.has(item.hash): + imported_content += known_imports[item.hash] + "\n" + + if imported_content == "": + _imported_line_count = 0 + return text.split("\n") + else: + _imported_line_count = cummulative_line_number + 1 + # Combine imported lines with the original lines + return (imported_content + "\n" + text).split("\n") + + +## Import content from another dialogue file or return an ERR +func import_content(path: String, prefix: String, imported_line_map: Dictionary, known_imports: Dictionary) -> Error: + if FileAccess.file_exists(path): + var file = FileAccess.open(path, FileAccess.READ) + var content: PackedStringArray = file.get_as_text().strip_edges().split("\n") + + for index in range(0, content.size()): + var line = content[index] + if is_import_line(line): + var import = extract_import_path_and_name(line) + if import.size() > 0: + if not known_imports.has(import.path.hash()): + # Add an empty record into the keys just so we don't end up with cyclic dependencies + known_imports[import.path.hash()] = "" + if import_content(import.path, import.prefix, imported_line_map, known_imports) != OK: + return ERR_LINK_FAILED + + if not imported_line_map.has(import.path.hash()): + # Make a map so we can refer compiled lines to where they were imported from + imported_line_map[import.path.hash()] = { + hash = import.path.hash(), + imported_on_line_number = index, + from_line = 0, + to_line = 0 + } + + _imported_titles[import.prefix] = import.path.hash() + + var origin_hash: int = -1 + for hash_value in known_imports.keys(): + if known_imports[hash_value] == ".": + origin_hash = hash_value + + # Replace any titles or jump points with references to the files they point to (event if they point to their own file) + for i in range(0, content.size()): + var line = content[i] + if line.strip_edges().begins_with("~ "): + var indent: String = "\t".repeat(get_indent(line)) + var title = line.strip_edges().substr(2) + if "/" in line: + var bits = title.split("/") + content[i] = "%s~ %s/%s" % [indent, _imported_titles[bits[0]], bits[1]] + else: + content[i] = "%s~ %s/%s" % [indent, str(path.hash()), title] + + elif "=>< " in line: + var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() + if "/" in jump: + var bits: PackedStringArray = jump.split("/") + var title_hash: int = _imported_titles[bits[0]] + if title_hash == origin_hash: + content[i] = "%s=>< %s" % [line.split("=>< ")[0], bits[1]] + else: + content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], title_hash, bits[1]] + + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): + content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], str(path.hash()), jump] + + elif "=> " in line: + var jump: String = line.substr(line.find("=> ") + "=> ".length()).strip_edges() + if "/" in jump: + var bits: PackedStringArray = jump.split("/") + var title_hash: int = _imported_titles[bits[0]] + if title_hash == origin_hash: + content[i] = "%s=> %s" % [line.split("=> ")[0], bits[1]] + else: + content[i] = "%s=> %s/%s" % [line.split("=> ")[0], title_hash, bits[1]] + + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): + content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] + + imported_paths.append(path) + known_imports[path.hash()] = "\n".join(content) + "\n=> END\n" + return OK + else: + return ERR_FILE_NOT_FOUND + + +## Build a tree of parent/child relationships +func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: + var root: DMTreeLine = DMTreeLine.new("") + var parent_chain: Array[DMTreeLine] = [root] + var previous_line: DMTreeLine + var doc_comments: PackedStringArray = [] + + # Get list of known autoloads + var autoload_names: PackedStringArray = get_autoload_names() + + for i in range(0, raw_lines.size()): + var raw_line: String = raw_lines[i] + var tree_line: DMTreeLine = DMTreeLine.new(str(i - _imported_line_count)) + + tree_line.line_number = i + 1 + tree_line.type = get_line_type(raw_line) + tree_line.text = raw_line.strip_edges() + + # Handle any "using" directives. + if tree_line.type == DMConstants.TYPE_USING: + var using_match: RegExMatch = regex.USING_REGEX.search(raw_line) + if "state" in using_match.names: + var using_state: String = using_match.strings[using_match.names.state].strip_edges() + if not using_state in autoload_names: + add_error(tree_line.line_number, 0, DMConstants.ERR_UNKNOWN_USING) + elif not using_state in using_states: + using_states.append(using_state) + continue + # Ignore import lines because they've already been processed. + elif is_import_line(raw_line): + continue + + tree_line.indent = get_indent(raw_line) + + # Attach doc comments + if raw_line.strip_edges().begins_with("##"): + doc_comments.append(raw_line.replace("##", "").strip_edges()) + elif tree_line.type == DMConstants.TYPE_DIALOGUE: + tree_line.notes = "\n".join(doc_comments) + doc_comments.clear() + + # Empty lines are only kept so that we can work out groupings of things (eg. 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 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) + elif tree_line.indent < parent_chain.size() - 1: + parent_chain.resize(tree_line.indent + 1) + + # Add any titles to the list of known titles + if tree_line.type == DMConstants.TYPE_TITLE: + var title: String = tree_line.text.substr(2) + if title == "": + add_error(i, 2, DMConstants.ERR_EMPTY_TITLE) + elif titles.has(title): + add_error(i, 2, DMConstants.ERR_DUPLICATE_TITLE) + else: + titles[title] = tree_line.id + if "/" in title: + # Replace the hash title with something human readable. + var bits: PackedStringArray = title.split("/") + if _imported_titles.has(bits[0]): + title = _imported_titles[bits[0]] + "/" + bits[1] + titles[title] = tree_line.id + elif first_title == "" and i >= _imported_line_count: + first_title = tree_line.id + + # Append the current line to the current parent (note: the root is the most basic parent). + var parent: DMTreeLine = parent_chain[parent_chain.size() - 1] + tree_line.parent = weakref(parent) + parent.children.append(tree_line) + + previous_line = tree_line + + return root + + +#endregion + +#region Parsing + + +func parse_line_tree(root: DMTreeLine, parent: DMCompiledLine = null) -> Array[DMCompiledLine]: + var compiled_lines: Array[DMCompiledLine] = [] + + for i in range(0, root.children.size()): + var tree_line: DMTreeLine = root.children[i] + var line: DMCompiledLine = DMCompiledLine.new(tree_line.id, tree_line.type) + + match line.type: + DMConstants.TYPE_UNKNOWN: + line.next_id = get_next_matching_sibling_id(root.children, i, parent, _first) + + DMConstants.TYPE_TITLE: + parse_title_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_CONDITION: + parse_condition_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_WHILE: + parse_while_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_MATCH: + parse_match_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_WHEN: + parse_when_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_MUTATION: + parse_mutation_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_GOTO: + # Extract any weighted random calls before parsing dialogue + if tree_line.text.begins_with("%"): + parse_random_line(tree_line, line, root.children, i, parent) + parse_goto_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_RESPONSE: + parse_response_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_RANDOM: + parse_random_line(tree_line, line, root.children, i, parent) + + DMConstants.TYPE_DIALOGUE: + # Extract any weighted random calls before parsing dialogue + if tree_line.text.begins_with("%"): + parse_random_line(tree_line, line, root.children, i, parent) + parse_dialogue_line(tree_line, line, root.children, i, parent) + + # Main line map is keyed by ID + lines[line.id] = line + + # Returned lines order is preserved so that it can be used for compiling children + compiled_lines.append(line) + + return compiled_lines + + +## Parse a title and apply it to the given line +func parse_title_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + line.text = tree_line.text.substr(tree_line.text.find("~ ") + 2).strip_edges() + + # Titles can't have numbers as the first letter (unless they are external titles which get replaced with hashes) + if tree_line.line_number >= _imported_line_count and regex.BEGINS_WITH_NUMBER_REGEX.search(line.text): + result = add_error(tree_line.line_number, 2, DMConstants.ERR_TITLE_BEGINS_WITH_NUMBER) + + # Only import titles are allowed to have "/" in them + var valid_title = regex.VALID_TITLE_REGEX.search(line.text.replace("/", "")) + if not valid_title: + result = add_error(tree_line.line_number, 2, DMConstants.ERR_TITLE_INVALID_CHARACTERS) + + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + ## Update the titles reference to point to the actual first line + titles[line.text] = line.next_id + + ## Update any lines that point to this title + if _goto_lines.has(line.text): + for goto_line in _goto_lines[line.text]: + goto_line.next_id = line.next_id + + return result + + +## Parse a goto and apply it to the given line. +func parse_goto_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Work out where this line is jumping to. + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(tree_line.text, titles) + if goto_data.error: + return add_error(tree_line.line_number, tree_line.indent + 2, goto_data.error) + if goto_data.next_id or goto_data.expression: + line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + add_reference_to_title(goto_data.title, line) + + if goto_data.is_snippet: + line.is_snippet = true + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + return OK + + +## Parse a condition line and apply to the given line +func parse_condition_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Work out the next IDs before parsing the condition line itself so that the last + # child can inherit from the chain. + + # Find the next conditional sibling that is part of this grouping (if there is one). + for next_sibling: DMTreeLine in siblings.slice(sibling_index + 1): + if not next_sibling.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_CONDITION]: + break + elif next_sibling.type == DMConstants.TYPE_CONDITION: + if next_sibling.text.begins_with("el"): + line.next_sibling_id = next_sibling.id + break + else: + break + + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # The next line that isn't a conditional or is a new "if" + return s.type != DMConstants.TYPE_CONDITION or s.text.begins_with("if ") + ) + # Any empty IDs should end the conversation. + if line.next_id_after == DMConstants.ID_NULL: + line.next_id_after = parent.next_id_after if parent != null and parent.next_id_after else DMConstants.ID_END + + # Having no nested body is an immediate failure. + if tree_line.children.size() == 0: + return add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # Try to parse the conditional expression ("else" has no expression). + if "if " in tree_line.text: + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + return add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Parse any nested body lines + parse_children(tree_line, line) + + return OK + + +## Parse a while loop and apply it to the given line. +func parse_while_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + # Parse the while condition + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + return add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Parse the nested body (it should take care of looping back to this line when it finishes) + parse_children(tree_line, line) + + return OK + + +func parse_match_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # The next line after is the next sibling + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + # Extract the condition to match to + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + # Match statements should have children + if tree_line.children.size() == 0: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # Check that all children are when or else. + for child in tree_line.children: + if child.type == DMConstants.TYPE_WHEN: continue + if child.type == DMConstants.TYPE_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) + + # Each child should be a "when" or "else". We don't need those lines themselves, just their + # condition and the line they point to if the conditions passes. + var children: Array[DMCompiledLine] = parse_children(tree_line, line) + for child: DMCompiledLine in children: + # "when" cases + if child.type == DMConstants.TYPE_WHEN: + line.siblings.append({ + condition = child.expression, + next_id = child.next_id + }) + # "else" case + elif child.type == DMConstants.TYPE_CONDITION: + if line.siblings.any(func(s): return s.has("is_else")): + result = add_error(child.line_number, child.indent, DMConstants.ERR_ONLY_ONE_ELSE_ALLOWED) + else: + line.siblings.append({ + next_id = child.next_id, + is_else = true + }) + # Remove the line from the list of all lines because we don't need it any more. + lines.erase(child.id) + + return result + + +func parse_when_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # This when line should be found inside a match line + if parent.type != DMConstants.TYPE_MATCH: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_WHEN_MUST_BELONG_TO_MATCH) + + # When lines should have children + if tree_line.children.size() == 0: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_CONDITION_INDENTATION) + + # The next line after a when is the same as its parent match line + line.next_id_after = parent.next_id_after + + # Extract the condition to match to + var condition: Dictionary = extract_condition(tree_line.text, false, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + + parse_children(tree_line, line) + + return result + + +## Parse a mutation line and apply it to the given line +func parse_mutation_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var mutation: Dictionary = extract_mutation(tree_line.text) + if mutation.has("error"): + return add_error(tree_line.line_number, mutation.index, mutation.error) + else: + line.expression = mutation + + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + + return OK + + +## Parse a response and apply it to the given line. +func parse_response_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # Remove the "- " + tree_line.text = tree_line.text.substr(2) + + # Extract the static line ID + var static_line_id: String = extract_static_line_id(tree_line.text) + if static_line_id: + tree_line.text = tree_line.text.replace("[ID:%s]" % [static_line_id], "") + line.translation_key = static_line_id + + # Handle conditional responses and remove them from the prompt text. + if " [if " in tree_line.text: + var condition = extract_condition(tree_line.text, true, tree_line.indent) + if condition.has("error"): + result = add_error(tree_line.line_number, condition.index, condition.error) + else: + line.expression = condition + # 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, -1, -1): + if siblings[i].type == DMConstants.TYPE_RESPONSE: + original_response = siblings[i] + elif siblings[i].type != DMConstants.TYPE_UNKNOWN: + break + + # If it's the original response then set up an original line. + if original_response == tree_line: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, (func(s: DMTreeLine): + # The next line that isn't a response. + return not s.type in [DMConstants.TYPE_RESPONSE, DMConstants.TYPE_UNKNOWN] + ), true) + line.responses = [line.id] + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the same ID for after the random group. + else: + line.next_id = line.next_id_after + # Otherwise let the original line know about it. + else: + var original_line: DMCompiledLine = lines[original_response.id] + line.next_id_after = original_line.next_id_after + line.siblings = original_line.siblings + original_line.responses.append(line.id) + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the original line's next ID after. + else: + line.next_id = original_line.next_id_after + + parse_character_and_dialogue(tree_line, line, siblings, sibling_index, parent) + + return OK + + +## Parse a randomised line +func parse_random_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + # Find the weight + var weight: float = 1 + var found = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.search(tree_line.text + " ") + var condition: Dictionary = {} + if found: + if found.names.has("weight"): + weight = found.strings[found.names.weight].to_float() + if found.names.has("condition"): + condition = extract_condition(tree_line.text, true, tree_line.indent) + + # Find the original random sibling. It will be the jump off point. + var original_sibling: DMTreeLine = tree_line + for i in range(sibling_index - 1, -1, -1): + if siblings[i] and siblings[i].is_random: + original_sibling = siblings[i] + else: + break + + var weighted_sibling: Dictionary = { weight = weight, id = line.id, condition = condition } + + # If it's the original sibling then set up an original line. + if original_sibling == tree_line: + line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, (func(s: DMTreeLine): + # The next line that isn't a randomised line. + # NOTE: DMTreeLine.is_random won't be set at this point so we need to check for the "%" prefix. + return not s.text.begins_with("%") + ), true) + line.siblings = [weighted_sibling] + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the same ID for after the random group. + else: + line.next_id = line.next_id_after + + # Otherwise let the original line know about it. + else: + var original_line: DMCompiledLine = lines[original_sibling.id] + line.next_id_after = original_line.next_id_after + line.siblings = original_line.siblings + original_line.siblings.append(weighted_sibling) + # If this line has children then the next ID is the first child. + if tree_line.children.size() > 0: + parse_children(tree_line, line) + # Otherwise use the original line's next ID after. + else: + line.next_id = original_line.next_id_after + + # Remove the randomise syntax from the line. + tree_line.text = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(tree_line.text, "") + tree_line.is_random = true + + return OK + + +## Parse some dialogue and apply it to the given line. +func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + # Remove escape character + if tree_line.text.begins_with("\\using"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\if"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\elif"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\else"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\while"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\match"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\when"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\do"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\set"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\-"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\~"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\=>"): tree_line.text = tree_line.text.substr(1) + if tree_line.text.begins_with("\\%"): tree_line.text = tree_line.text.substr(1) + + # Append any further dialogue + for i in range(0, tree_line.children.size()): + var child: DMTreeLine = tree_line.children[i] + if child.type == DMConstants.TYPE_DIALOGUE: + # 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:", "[ID:").replace("[ID:%s]" % [static_line_id], "") + line.translation_key = static_line_id + + # Check for simultaneous lines + if tree_line.text.begins_with("| "): + # Jumps are only allowed on the origin line. + if " =>" in tree_line.text: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES) + # Check for a valid previous line. + tree_line.text = tree_line.text.substr(2) + var previous_sibling: DMTreeLine = siblings[sibling_index - 1] + if previous_sibling.type != DMConstants.TYPE_DIALOGUE: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_CONCURRENT_LINE_WITHOUT_ORIGIN) + else: + # Because the previous line's concurrent_lines array is the same as + # any line before that this doesn't need to check any higher up. + var previous_line: DMCompiledLine = lines[previous_sibling.id] + previous_line.concurrent_lines.append(line.id) + line.concurrent_lines = previous_line.concurrent_lines + + parse_character_and_dialogue(tree_line, line, siblings, sibling_index, parent) + + # Check for any inline expression errors + var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("") + var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(tree_line.text, true, true) + for bbcode: Dictionary in bbcodes: + var tag: String = bbcode.code + var code: String = bbcode.raw_args + if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"): + var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length()) + if expression.size() == 0: + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION) + elif expression[0].type == DMConstants.TYPE_ERROR: + add_error(tree_line.line_number, tree_line.indent + expression[0].i, expression[0].value) + + # If the line isn't part of a weighted random group then make it point to the next + # available sibling. + if line.next_id == DMConstants.ID_NULL and line.siblings.size() == 0: + line.next_id = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # Ignore concurrent lines. + return not s.text.begins_with("| ") + ) + + return result + + +## Parse the character name and dialogue and apply it to a given line. +func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Array[DMTreeLine], sibling_index: int, parent: DMCompiledLine) -> Error: + var result: Error = OK + + var text: String = tree_line.text + + # Attach any doc comments. + line.notes = tree_line.notes + + # Extract tags. + var tag_data: DMResolvedTagData = DMResolvedTagData.new(text) + line.tags = tag_data.tags + text = tag_data.text_without_tags + + # Handle inline gotos and remove them from the prompt text. + if " =><" in text: + # Because of when the return point needs to be known at runtime we need to split + # this line into two (otherwise the return point would be dependent on the balloon). + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, titles) + if goto_data.error: + result = add_error(tree_line.line_number, tree_line.indent + 3, goto_data.error) + if goto_data.next_id or goto_data.expression: + text = goto_data.text_without_goto + var goto_line: DMCompiledLine = DMCompiledLine.new(line.id + ".1", DMConstants.TYPE_GOTO) + goto_line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + if line.type == DMConstants.TYPE_RESPONSE: + goto_line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, func(s: DMTreeLine): + # If this is coming from a response then we want the next non-response line. + return s.type != DMConstants.TYPE_RESPONSE + ) + else: + goto_line.next_id_after = get_next_matching_sibling_id(siblings, sibling_index, parent, _first) + goto_line.is_snippet = true + lines[goto_line.id] = goto_line + line.next_id = goto_line.id + add_reference_to_title(goto_data.title, goto_line) + elif " =>" in text: + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, titles) + if goto_data.error: + result = add_error(tree_line.line_number, tree_line.indent + 2, goto_data.error) + if goto_data.next_id or goto_data.expression: + text = goto_data.text_without_goto + line.next_id = goto_data.next_id + line.next_id_expression = goto_data.expression + add_reference_to_title(goto_data.title, line) + + # Handle the dialogue. + text = text.replace("\\:", "!ESCAPED_COLON!") + if ": " in text: + # If a character was given then split it out. + var bits = Array(text.strip_edges().split(": ")) + line.character = bits.pop_front().strip_edges() + if not line.character in character_names: + character_names.append(line["character"]) + # Character names can have expressions in them. + line.character_replacements = expression_parser.extract_replacements(line.character, tree_line.indent) + for replacement in line.character_replacements: + if replacement.has("error"): + result = add_error(tree_line.line_number, replacement.index, replacement.error) + text = ": ".join(bits).replace("!ESCAPED_COLON!", ":") + else: + line.character = "" + text = text.replace("!ESCAPED_COLON!", ":") + + # Extract any expressions in the dialogue. + line.text_replacements = expression_parser.extract_replacements(text, line.character.length() + 2 + tree_line.indent) + for replacement in line.text_replacements: + if replacement.has("error"): + result = add_error(tree_line.line_number, replacement.index, replacement.error) + + # Replace any newlines. + text = text.replace("\\n", "\n").strip_edges() + + # If there was no manual translation key then just use the text itself (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 + + # IDs can't be duplicated for text that doesn't match. + if line.translation_key != "": + if _known_translation_keys.has(line.translation_key) and _known_translation_keys.get(line.translation_key) != line.text: + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_DUPLICATE_ID) + else: + _known_translation_keys[line.translation_key] = line.text + + return result + + +#endregion + +#region Errors + + +## Add a compilation error to the list. Returns the given error code. +func add_error(line_number: int, column_number: int, error: int) -> Error: + # See if the error was in an imported file + for item in _imported_line_map.values(): + if line_number < item.to_line: + errors.append({ + line_number = item.imported_on_line_number, + column_number = 0, + error = DMConstants.ERR_ERRORS_IN_IMPORTED_FILE, + external_error = error, + external_line_number = line_number + }) + return error + + # Otherwise, it's in this file + errors.append({ + line_number = line_number - _imported_line_count, + column_number = column_number, + error = error + }) + + return error + + +#endregion + +#region Helpers + + +## Get the names of any autoloads in the project. +func get_autoload_names() -> PackedStringArray: + var autoloads: PackedStringArray = [] + + var project = ConfigFile.new() + project.load("res://project.godot") + if project.has_section("autoload"): + return Array(project.get_section_keys("autoload")).filter(func(key): return key != "DialogueManager") + + return autoloads + + +## Check if a line is importing another file. +func is_import_line(text: String) -> bool: + return text.begins_with("import ") and " as " in text + + +## Extract the import information from an import line +func extract_import_path_and_name(line: String) -> Dictionary: + var found: RegExMatch = regex.IMPORT_REGEX.search(line) + if found: + return { + path = found.strings[found.names.path], + prefix = found.strings[found.names.prefix] + } + else: + return {} + + +## Get the indent of a raw line +func get_indent(raw_line: String) -> int: + var tabs: RegExMatch = regex.INDENT_REGEX.search(raw_line) + if tabs: + return tabs.get_string().length() + else: + return 0 + + +## Get the type of a raw line +func get_line_type(raw_line: String) -> String: + raw_line = raw_line.strip_edges() + var text: String = regex.WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line + " ", "").strip_edges() + + if text.begins_with("import "): + return DMConstants.TYPE_IMPORT + + if text.begins_with("using "): + return DMConstants.TYPE_USING + + if text.begins_with("#"): + return DMConstants.TYPE_COMMENT + + if text.begins_with("~ "): + return DMConstants.TYPE_TITLE + + if text.begins_with("if ") or text.begins_with("elif") or text.begins_with("else"): + return DMConstants.TYPE_CONDITION + + if text.begins_with("while "): + return DMConstants.TYPE_WHILE + + if text.begins_with("match "): + return DMConstants.TYPE_MATCH + + if text.begins_with("when "): + return DMConstants.TYPE_WHEN + + if text.begins_with("do ") or text.begins_with("do! ") or text.begins_with("set "): + return DMConstants.TYPE_MUTATION + + if text.begins_with("=> ") or text.begins_with("=>< "): + return DMConstants.TYPE_GOTO + + if text.begins_with("- "): + return DMConstants.TYPE_RESPONSE + + if raw_line.begins_with("%") and text.is_empty(): + return DMConstants.TYPE_RANDOM + + if not text.is_empty(): + return DMConstants.TYPE_DIALOGUE + + return DMConstants.TYPE_UNKNOWN + + +## Get the next sibling that passes a [Callable] matcher. +func get_next_matching_sibling_id(siblings: Array[DMTreeLine], from_index: int, parent: DMCompiledLine, matcher: Callable, with_empty_lines: bool = false) -> String: + for i in range(from_index + 1, siblings.size()): + var next_sibling: DMTreeLine = siblings[i] + + if not with_empty_lines: + # Ignore empty lines + if not next_sibling or next_sibling.type == DMConstants.TYPE_UNKNOWN: + continue + + if matcher.call(next_sibling): + return next_sibling.id + + # If no next ID can be found then check the parent for where to go next. + if parent != null: + return parent.id if parent.type == DMConstants.TYPE_WHILE else parent.next_id_after + + return DMConstants.ID_NULL + + +## Extract a static line ID from some text. +func extract_static_line_id(text: String) -> String: + # Find a static translation key, eg. [ID:something] + var found: RegExMatch = regex.STATIC_LINE_ID_REGEX.search(text) + if found: + return found.strings[found.names.id] + else: + return "" + + +## Extract a condition (or inline condition) from some text. +func extract_condition(text: String, is_wrapped: bool, index: int) -> Dictionary: + var regex: RegEx = regex.WRAPPED_CONDITION_REGEX if is_wrapped else regex.CONDITION_REGEX + var found: RegExMatch = regex.search(text) + + if found == null: + return { + index = 0, + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + var raw_condition: String = found.strings[found.names.expression] + if raw_condition.ends_with(":"): + raw_condition = raw_condition.substr(0, raw_condition.length() - 1) + + var expression: Array = expression_parser.tokenise(raw_condition, DMConstants.TYPE_CONDITION, index + found.get_start("expression")) + + if expression.size() == 0: + return { + index = index + found.get_start("expression"), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + return { + index = expression[0].i, + error = expression[0].value + } + else: + return { + expression = expression + } + + +## Extract a mutation from some text. +func extract_mutation(text: String) -> Dictionary: + var found: RegExMatch = regex.MUTATION_REGEX.search(text) + + if not found: + return { + index = 0, + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + if found.names.has("expression"): + var expression: Array = expression_parser.tokenise(found.strings[found.names.expression], DMConstants.TYPE_MUTATION, found.get_start("expression")) + if expression.size() == 0: + return { + index = found.get_start("expression"), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + return { + index = expression[0].i, + error = expression[0].value + } + else: + return { + expression = expression, + is_blocking = not "!" in found.strings[found.names.keyword] + } + + else: + return { + index = found.get_start(), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + + +## Keep track of lines referencing titles because their own next_id might not have been resolved yet. +func add_reference_to_title(title: String, line: DMCompiledLine) -> void: + if title in [DMConstants.ID_END, DMConstants.ID_END_CONVERSATION, DMConstants.ID_NULL]: return + + if not _goto_lines.has(title): + _goto_lines[title] = [] + _goto_lines[title].append(line) + + +## Parse a nested block of child lines +func parse_children(tree_line: DMTreeLine, line: DMCompiledLine) -> Array[DMCompiledLine]: + var children = parse_line_tree(tree_line, line) + if children.size() > 0: + line.next_id = children.front().id + # The last child should jump to the next line after its parent condition group + var last_child: DMCompiledLine = children.back() + if last_child.next_id == DMConstants.ID_NULL: + last_child.next_id = line.next_id_after + if last_child.siblings.size() > 0: + for sibling in last_child.siblings: + lines.get(sibling.id).next_id = last_child.next_id + + return children + + +#endregion diff --git a/addons/dialogue_manager/compiler/compilation.gd.uid b/addons/dialogue_manager/compiler/compilation.gd.uid new file mode 100644 index 0000000..24a13ee --- /dev/null +++ b/addons/dialogue_manager/compiler/compilation.gd.uid @@ -0,0 +1 @@ +uid://dsgpnyqg6cprg diff --git a/addons/dialogue_manager/compiler/compiled_line.gd b/addons/dialogue_manager/compiler/compiled_line.gd new file mode 100644 index 0000000..9e34f09 --- /dev/null +++ b/addons/dialogue_manager/compiler/compiled_line.gd @@ -0,0 +1,161 @@ +## A compiled line of dialogue. +class_name DMCompiledLine extends RefCounted + + +## The ID of the line +var id: String +## The translation key (or static line ID). +var translation_key: String = "" +## The type of line. +var type: String = "" +## The character name. +var character: String = "" +## Any interpolation expressions for the character name. +var character_replacements: Array[Dictionary] = [] +## The text of the line. +var text: String = "" +## Any interpolation expressions for the text. +var text_replacements: Array[Dictionary] = [] +## Any response siblings associated with this line. +var responses: PackedStringArray = [] +## Any randomise or case siblings for this line. +var siblings: Array[Dictionary] = [] +## Any lines said simultaneously. +var concurrent_lines: PackedStringArray = [] +## Any tags on this line. +var tags: PackedStringArray = [] +## The condition or mutation expression for this line. +var expression: Dictionary = {} +## The 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. +var next_id_expression: Array[Dictionary] = [] +## Whether this jump line should return after the jump target sequence has ended. +var is_snippet: bool = false +## The ID of the next sibling line. +var next_sibling_id: String = "" +## The ID after this line if it belongs to a block (eg. conditions). +var next_id_after: String = "" +## Any doc comments attached to this line. +var notes: String = "" + + +#region Hooks + + +func _init(initial_id: String, initial_type: String) -> void: + id = initial_id + type = initial_type + + +func _to_string() -> String: + var s: Array = [ + "[%s]" % [type], + "%s:" % [character] if character != "" else null, + text if text != "" else null, + expression if expression.size() > 0 else null, + "[%s]" % [",".join(tags)] if tags.size() > 0 else null, + str(siblings) if siblings.size() > 0 else null, + str(responses) if responses.size() > 0 else null, + "=> END" if "end" in next_id else "=> %s" % [next_id], + "(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null, + "(==> %s)" % [next_id_after] if next_id_after != "" else null, + ].filter(func(item): return item != null) + + return " ".join(s) + + +#endregion + +#region Helpers + + +## Express this line as a [Dictionary] that can be stored in a resource. +func to_data() -> Dictionary: + var d: Dictionary = { + id = id, + type = type, + next_id = next_id + } + + if next_id_expression.size() > 0: + d.next_id_expression = next_id_expression + + match type: + DMConstants.TYPE_CONDITION: + d.condition = expression + if not next_sibling_id.is_empty(): + d.next_sibling_id = next_sibling_id + d.next_id_after = next_id_after + + DMConstants.TYPE_WHILE: + d.condition = expression + d.next_id_after = next_id_after + + DMConstants.TYPE_MATCH: + d.condition = expression + d.next_id_after = next_id_after + d.cases = siblings + + DMConstants.TYPE_MUTATION: + d.mutation = expression + + DMConstants.TYPE_GOTO: + d.is_snippet = is_snippet + d.next_id_after = next_id_after + if not siblings.is_empty(): + d.siblings = siblings + + DMConstants.TYPE_RANDOM: + d.siblings = siblings + + DMConstants.TYPE_RESPONSE: + d.text = text + + if not responses.is_empty(): + d.responses = responses + + if translation_key != text: + d.translation_key = translation_key + if not expression.is_empty(): + d.condition = expression + if not character.is_empty(): + d.character = character + if not character_replacements.is_empty(): + d.character_replacements = character_replacements + if not text_replacements.is_empty(): + d.text_replacements = text_replacements + if not tags.is_empty(): + d.tags = tags + if not notes.is_empty(): + d.notes = notes + if not expression_text.is_empty(): + d.condition_as_text = expression_text + + DMConstants.TYPE_DIALOGUE: + d.text = text + + if translation_key != text: + d.translation_key = translation_key + + if not character.is_empty(): + d.character = character + if not character_replacements.is_empty(): + d.character_replacements = character_replacements + if not text_replacements.is_empty(): + d.text_replacements = text_replacements + if not tags.is_empty(): + d.tags = tags + if not notes.is_empty(): + d.notes = notes + if not siblings.is_empty(): + d.siblings = siblings + if not concurrent_lines.is_empty(): + d.concurrent_lines = concurrent_lines + + return d + + +#endregion diff --git a/addons/dialogue_manager/compiler/compiled_line.gd.uid b/addons/dialogue_manager/compiler/compiled_line.gd.uid new file mode 100644 index 0000000..17ec55e --- /dev/null +++ b/addons/dialogue_manager/compiler/compiled_line.gd.uid @@ -0,0 +1 @@ +uid://dg8j5hudp4210 diff --git a/addons/dialogue_manager/compiler/compiler.gd b/addons/dialogue_manager/compiler/compiler.gd new file mode 100644 index 0000000..a370ef6 --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler.gd @@ -0,0 +1,51 @@ +## A compiler of Dialogue Manager dialogue. +class_name DMCompiler extends RefCounted + + +## Compile a dialogue script. +static func compile_string(text: String, path: String) -> DMCompilerResult: + var compilation: DMCompilation = DMCompilation.new() + compilation.compile(text, path) + + var result: DMCompilerResult = DMCompilerResult.new() + result.imported_paths = compilation.imported_paths + result.using_states = compilation.using_states + result.character_names = compilation.character_names + result.titles = compilation.titles + result.first_title = compilation.first_title + result.errors = compilation.errors + result.lines = compilation.data + result.raw_text = text + + return result + + +## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants]. +static func get_line_type(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + return compilation.get_line_type(text) + + +## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text. +static func get_static_line_id(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + return compilation.extract_static_line_id(text) + + +## Get the translatable part of a line. +static func extract_translatable_string(text: String) -> String: + var compilation: DMCompilation = DMCompilation.new() + + var tree_line = DMTreeLine.new("") + tree_line.text = text + var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text)) + compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null) + + return line.text + + +## Get the known titles in a dialogue script. +static func get_titles_in_text(text: String, path: String) -> Dictionary: + var compilation: DMCompilation = DMCompilation.new() + compilation.build_line_tree(compilation.inject_imported_files(text, path)) + return compilation.titles diff --git a/addons/dialogue_manager/compiler/compiler.gd.uid b/addons/dialogue_manager/compiler/compiler.gd.uid new file mode 100644 index 0000000..e041f10 --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler.gd.uid @@ -0,0 +1 @@ +uid://chtfdmr0cqtp4 diff --git a/addons/dialogue_manager/compiler/compiler_regex.gd b/addons/dialogue_manager/compiler/compiler_regex.gd new file mode 100644 index 0000000..ca10413 --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler_regex.gd @@ -0,0 +1,50 @@ +## A collection of [RegEx] for use by the [DMCompiler]. +class_name DMCompilerRegEx extends RefCounted + + +var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?[^\"]+)\" as (?[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)") +var USING_REGEX: RegEx = RegEx.create_from_string("^using (?.*)$") +var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+") +var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$") +var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d") +var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?.*)\\:?") +var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?.*)\\]") +var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?do|do!|set) (?.*)") +var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?.*?)\\]") +var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?[\\d.]+)?( \\[if (?.+?)\\])? ") +var GOTO_REGEX: RegEx = RegEx.create_from_string("=>.*)") + +var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?.*?)\\]\\]") +var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?.+?)\\](?.*?)\\[\\/if\\]") + +var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?.*?)\\]") + +var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}") + +var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+") + +var TOKEN_DEFINITIONS: Dictionary = { + DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("), + DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["), + DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("), + DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"), + DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["), + DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"), + DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"), + DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"), + DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"), + DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"), + DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"), + DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), + DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), + DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), + DMConstants.TOKEN_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( |$)|!)"), + DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"), + DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"), + DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"), + DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), + DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)") +} diff --git a/addons/dialogue_manager/compiler/compiler_regex.gd.uid b/addons/dialogue_manager/compiler/compiler_regex.gd.uid new file mode 100644 index 0000000..bd969df --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler_regex.gd.uid @@ -0,0 +1 @@ +uid://d3tvcrnicjibp diff --git a/addons/dialogue_manager/compiler/compiler_result.gd b/addons/dialogue_manager/compiler/compiler_result.gd new file mode 100644 index 0000000..acbf60f --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler_result.gd @@ -0,0 +1,27 @@ +## The result of using the [DMCompiler] to compile some dialogue. +class_name DMCompilerResult extends RefCounted + + +## Any paths that were imported into the compiled dialogue file. +var imported_paths: PackedStringArray = [] + +## Any "using" directives. +var using_states: PackedStringArray = [] + +## All titles in the file and the line they point to. +var titles: Dictionary = {} + +## The first title in the file. +var first_title: String = "" + +## All character names. +var character_names: PackedStringArray = [] + +## Any compilation errors. +var errors: Array[Dictionary] = [] + +## A map of all compiled lines. +var lines: Dictionary = {} + +## The raw dialogue text. +var raw_text: String = "" diff --git a/addons/dialogue_manager/compiler/compiler_result.gd.uid b/addons/dialogue_manager/compiler/compiler_result.gd.uid new file mode 100644 index 0000000..f1f76fd --- /dev/null +++ b/addons/dialogue_manager/compiler/compiler_result.gd.uid @@ -0,0 +1 @@ +uid://dmk74tknimqvg diff --git a/addons/dialogue_manager/compiler/expression_parser.gd b/addons/dialogue_manager/compiler/expression_parser.gd new file mode 100644 index 0000000..7dfb2af --- /dev/null +++ b/addons/dialogue_manager/compiler/expression_parser.gd @@ -0,0 +1,529 @@ +## A class for parsing a condition/mutation expression for use with the [DMCompiler]. +class_name DMExpressionParser extends RefCounted + + +var include_comments: bool = false + + +# Reference to the common [RegEx] that the parser needs. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +## Break a string down into an expression. +func tokenise(text: String, line_type: String, index: int) -> Array: + var tokens: Array[Dictionary] = [] + var limit: int = 0 + while text.strip_edges() != "" and limit < 1000: + limit += 1 + var found = _find_match(text) + if found.size() > 0: + tokens.append({ + index = index, + type = found.type, + value = found.value + }) + index += found.value.length() + text = found.remaining_text + elif text.begins_with(" "): + index += 1 + text = text.substr(1) + else: + return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index) + + return _build_token_tree(tokens, line_type, "")[0] + + +## Extract any expressions from some text +func extract_replacements(text: String, index: int) -> Array[Dictionary]: + var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text) + + if founds == null or founds.size() == 0: + return [] + + var replacements: Array[Dictionary] = [] + for found in founds: + var replacement: Dictionary = {} + var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2) + + # If there are closing curlie hard-up against the end of a {{...}} block then check for further + # curlies just outside of the block. + var text_suffix: String = text.substr(found.get_end(0)) + var expression_suffix: String = "" + while text_suffix.begins_with("}"): + expression_suffix += "}" + text_suffix = text_suffix.substr(1) + value_in_text += expression_suffix + + var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1)) + if expression.size() == 0: + replacement = { + index = index + found.get_start(1), + error = DMConstants.ERR_INCOMPLETE_EXPRESSION + } + elif expression[0].type == DMConstants.TYPE_ERROR: + replacement = { + index = expression[0].i, + error = expression[0].value + } + else: + replacement = { + value_in_text = "{{%s}}" % value_in_text, + expression = expression + } + replacements.append(replacement) + + return replacements + + +#region Helpers + + +# Create a token that represents an error. +func _build_token_tree_error(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. +func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array: + var tree: Array[Dictionary] = [] + var limit = 0 + while tokens.size() > 0 and limit < 1000: + limit += 1 + var token = tokens.pop_front() + + var error = _check_next_token(token, tokens, line_type, expected_close_token) + if error != OK: + var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token + return [_build_token_tree_error(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(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] + + tree.append({ + type = DMConstants.TOKEN_FUNCTION, + # Consume the trailing "(" + function = token.value.substr(0, token.value.length() - 1), + value = _tokens_to_list(sub_tree[0]), + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_DICTIONARY_REFERENCE: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(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(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens] + + tree.append({ + type = DMConstants.TOKEN_DICTIONARY_REFERENCE, + # Consume the trailing "[" + variable = token.value.substr(0, token.value.length() - 1), + value = args[0], + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_BRACE_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(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): + # Convert Lua style dictionaries to string keys + if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT: + t[i].type = DMConstants.TOKEN_STRING + t[i+1].type = DMConstants.TOKEN_COLON + t[i+1].erase("value") + + tree.append({ + type = DMConstants.TOKEN_DICTIONARY, + value = _tokens_to_dictionary(sub_tree[0]), + i = token.index + }) + + tokens = sub_tree[1] + + DMConstants.TOKEN_BRACKET_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(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]) + + # See if this is referencing a nested dictionary value + if tree.size() > 0: + var previous_token = tree[tree.size() - 1] + if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]: + type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE + value = value[0] + + tree.append({ + type = type, + value = value, + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_PARENS_OPEN: + var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) + + if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] + + tree.append({ + type = DMConstants.TOKEN_GROUP, + value = sub_tree[0], + i = token.index + }) + tokens = sub_tree[1] + + DMConstants.TOKEN_PARENS_CLOSE, \ + DMConstants.TOKEN_BRACE_CLOSE, \ + DMConstants.TOKEN_BRACKET_CLOSE: + if token.type != expected_close_token: + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] + + tree.append({ + type = token.type, + i = token.index + }) + + return [tree, tokens] + + DMConstants.TOKEN_NOT: + # Double nots negate each other + if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT: + tokens.pop_front() + else: + tree.append({ + type = token.type, + i = token.index + }) + + DMConstants.TOKEN_COMMA, \ + DMConstants.TOKEN_COLON, \ + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE: + tree.append({ + type = token.type, + i = token.index + }) + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_ASSIGNMENT, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_AND_OR, \ + DMConstants.TOKEN_VARIABLE: + var value = token.value.strip_edges() + if value == "&&": + value = "and" + elif value == "||": + value = "or" + tree.append({ + type = token.type, + value = value, + i = token.index + }) + + DMConstants.TOKEN_STRING: + if token.value.begins_with("&"): + tree.append({ + type = token.type, + value = StringName(token.value.substr(2, token.value.length() - 3)), + i = token.index + }) + else: + tree.append({ + type = token.type, + value = token.value.substr(1, token.value.length() - 2), + i = token.index + }) + + DMConstants.TOKEN_CONDITION: + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] + + DMConstants.TOKEN_BOOL: + tree.append({ + type = token.type, + value = token.value.to_lower() == "true", + i = token.index + }) + + DMConstants.TOKEN_NUMBER: + var value = token.value.to_float() if "." in token.value else token.value.to_int() + # If previous token is a number and this one is a negative number then + # inject a minus operator token in between them. + if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER: + tree.append(({ + type = DMConstants.TOKEN_OPERATOR, + value = "-", + i = token.index + })) + tree.append({ + type = token.type, + value = -1 * value, + i = token.index + }) + else: + tree.append({ + type = token.type, + value = value, + i = token.index + }) + + if expected_close_token != "": + var index: int = tokens[0].i if tokens.size() > 0 else 0 + return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] + + return [tree, tokens] + + +# Check the next token to see if it is valid to follow this one. +func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error: + var next_token: Dictionary = { type = null } + if next_tokens.size() > 0: + next_token = next_tokens.front() + + # Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary + # then it's an unexpected assignment in a condition line. + if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token): + return DMConstants.ERR_UNEXPECTED_ASSIGNMENT + + # Special case for a negative number after this one + if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"): + return OK + + var expected_token_types = [] + var unexpected_token_types = [] + match token.type: + DMConstants.TOKEN_FUNCTION, \ + DMConstants.TOKEN_PARENS_OPEN: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_BRACKET_CLOSE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE + ] + + DMConstants.TOKEN_BRACE_OPEN: + expected_token_types = [ + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_BRACE_CLOSE + ] + + DMConstants.TOKEN_PARENS_CLOSE, \ + DMConstants.TOKEN_BRACE_CLOSE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE + ] + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE, \ + DMConstants.TOKEN_NOT, \ + DMConstants.TOKEN_AND_OR, \ + DMConstants.TOKEN_DICTIONARY_REFERENCE: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_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, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_COMPARISON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + + DMConstants.TOKEN_BOOL, \ + DMConstants.TOKEN_STRING, \ + DMConstants.TOKEN_NUMBER: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_FUNCTION, + DMConstants.TOKEN_PARENS_OPEN, + DMConstants.TOKEN_BRACE_OPEN, + DMConstants.TOKEN_BRACKET_OPEN + ] + + DMConstants.TOKEN_VARIABLE: + unexpected_token_types = [ + DMConstants.TOKEN_NOT, + DMConstants.TOKEN_BOOL, + DMConstants.TOKEN_STRING, + DMConstants.TOKEN_NUMBER, + DMConstants.TOKEN_VARIABLE, + DMConstants.TOKEN_FUNCTION, + DMConstants.TOKEN_PARENS_OPEN, + DMConstants.TOKEN_BRACE_OPEN, + DMConstants.TOKEN_BRACKET_OPEN + ] + + if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \ + or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): + match next_token.type: + null: + return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION + + DMConstants.TOKEN_FUNCTION: + return DMConstants.ERR_UNEXPECTED_FUNCTION + + DMConstants.TOKEN_PARENS_OPEN, \ + DMConstants.TOKEN_PARENS_CLOSE: + return DMConstants.ERR_UNEXPECTED_BRACKET + + DMConstants.TOKEN_COMPARISON, \ + DMConstants.TOKEN_ASSIGNMENT, \ + DMConstants.TOKEN_OPERATOR, \ + DMConstants.TOKEN_NOT, \ + DMConstants.TOKEN_AND_OR: + return DMConstants.ERR_UNEXPECTED_OPERATOR + + DMConstants.TOKEN_COMMA: + return DMConstants.ERR_UNEXPECTED_COMMA + DMConstants.TOKEN_COLON: + return DMConstants.ERR_UNEXPECTED_COLON + DMConstants.TOKEN_DOT: + return DMConstants.ERR_UNEXPECTED_DOT + + DMConstants.TOKEN_BOOL: + return DMConstants.ERR_UNEXPECTED_BOOLEAN + DMConstants.TOKEN_STRING: + return DMConstants.ERR_UNEXPECTED_STRING + DMConstants.TOKEN_NUMBER: + return DMConstants.ERR_UNEXPECTED_NUMBER + DMConstants.TOKEN_VARIABLE: + return DMConstants.ERR_UNEXPECTED_VARIABLE + + return DMConstants.ERR_INVALID_EXPRESSION + + return OK + + +# Convert a series of comma separated tokens to an [Array]. +func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]: + var list: Array[Array] = [] + var current_item: Array[Dictionary] = [] + for token in tokens: + if token.type == DMConstants.TOKEN_COMMA: + list.append(current_item) + current_item = [] + else: + current_item.append(token) + + if current_item.size() > 0: + list.append(current_item) + + return list + + +# Convert a series of key/value tokens into a [Dictionary] +func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary: + var dictionary = {} + for i in range(0, tokens.size()): + if tokens[i].type == DMConstants.TOKEN_COLON: + if tokens.size() == i + 2: + dictionary[tokens[i - 1]] = tokens[i + 1] + else: + dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i } + + return dictionary + + +# Work out what the next token is from a string. +func _find_match(input: String) -> Dictionary: + for key in regex.TOKEN_DEFINITIONS.keys(): + var regex = regex.TOKEN_DEFINITIONS.get(key) + var found = regex.search(input) + if found: + return { + type = key, + remaining_text = input.substr(found.strings[0].length()), + value = found.strings[0] + } + + return {} + + +#endregion diff --git a/addons/dialogue_manager/compiler/expression_parser.gd.uid b/addons/dialogue_manager/compiler/expression_parser.gd.uid new file mode 100644 index 0000000..0793701 --- /dev/null +++ b/addons/dialogue_manager/compiler/expression_parser.gd.uid @@ -0,0 +1 @@ +uid://dbi4hbar8ubwu diff --git a/addons/dialogue_manager/compiler/resolved_goto_data.gd b/addons/dialogue_manager/compiler/resolved_goto_data.gd new file mode 100644 index 0000000..16bca6f --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_goto_data.gd @@ -0,0 +1,68 @@ +## Data associated with a dialogue jump/goto line. +class_name DMResolvedGotoData extends RefCounted + + +## The title that was specified +var title: String = "" +## The target line's ID +var next_id: String = "" +## An expression to determine the target line at runtime. +var expression: Array[Dictionary] = [] +## The given line text with the jump syntax removed. +var text_without_goto: String = "" +## Whether this is a jump-and-return style jump. +var is_snippet: bool = false +## A parse error if there was one. +var error: int +## The index in the string where +var index: int = 0 + +# An instance of the compiler [RegEx] list. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +func _init(text: String, titles: Dictionary) -> void: + if not "=> " in text and not "=>< " in text: return + + if "=> " in text: + text_without_goto = text.substr(0, text.find("=> ")).strip_edges() + elif "=>< " in text: + is_snippet = true + text_without_goto = text.substr(0, text.find("=>< ")).strip_edges() + + var found: RegExMatch = regex.GOTO_REGEX.search(text) + if found == null: + return + + title = found.strings[found.names.goto].strip_edges() + index = found.get_start(0) + + if title == "": + error = DMConstants.ERR_UNKNOWN_TITLE + return + + # "=> END!" means end the conversation, ignoring any "=><" chains. + if title == "END!": + next_id = DMConstants.ID_END_CONVERSATION + + # "=> END" means end the current title (and go back to the previous one if there is one + # in the stack) + elif title == "END": + next_id = DMConstants.ID_END + + elif titles.has(title): + next_id = titles.get(title) + elif title.begins_with("{{"): + var expression_parser: DMExpressionParser = DMExpressionParser.new() + var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0) + if title_expression[0].has("error"): + error = title_expression[0].error + else: + expression = title_expression[0].expression + else: + next_id = title + error = DMConstants.ERR_UNKNOWN_TITLE + + +func _to_string() -> String: + return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id] diff --git a/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid b/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid new file mode 100644 index 0000000..cb05e08 --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_goto_data.gd.uid @@ -0,0 +1 @@ +uid://llhl5pt47eoq diff --git a/addons/dialogue_manager/compiler/resolved_line_data.gd b/addons/dialogue_manager/compiler/resolved_line_data.gd new file mode 100644 index 0000000..1d1a716 --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_line_data.gd @@ -0,0 +1,167 @@ +## Any data associated with inline dialogue BBCodes. +class_name DMResolvedLineData extends RefCounted + +## The line's text +var text: String = "" +## A map of pauses against where they are found in the text. +var pauses: Dictionary = {} +## A map of speed changes against where they are found in the text. +var speeds: Dictionary = {} +## A list of any mutations to run and where they are found in the text. +var mutations: Array[Array] = [] +## A duration reference for the line. Represented as "auto" or a stringified number. +var time: String = "" + + +func _init(line: String) -> void: + text = line + pauses = {} + speeds = {} + mutations = [] + time = "" + + var bbcodes: Array = [] + + # Remove any escaped brackets (ie. "\[") + var escaped_open_brackets: PackedInt32Array = [] + var escaped_close_brackets: PackedInt32Array = [] + for i in range(0, text.length() - 1): + if text.substr(i, 2) == "\\[": + text = text.substr(0, i) + "!" + text.substr(i + 2) + escaped_open_brackets.append(i) + elif text.substr(i, 2) == "\\]": + text = text.substr(0, i) + "!" + text.substr(i + 2) + escaped_close_brackets.append(i) + + # Extract all of the BB codes so that we know the actual text (we could do this easier with + # a RichTextLabel but then we'd need to await idle_frame which is annoying) + var bbcode_positions = find_bbcode_positions_in_string(text) + var accumulaive_length_offset = 0 + for position in bbcode_positions: + # Ignore our own markers + if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]: + continue + + bbcodes.append({ + bbcode = position.bbcode, + start = position.start, + offset_start = position.start - accumulaive_length_offset + }) + accumulaive_length_offset += position.bbcode.length() + + for bb in bbcodes: + text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length()) + + # Now find any dialogue markers + var next_bbcode_position = find_bbcode_positions_in_string(text, false) + var limit = 0 + while next_bbcode_position.size() > 0 and limit < 1000: + limit += 1 + + var bbcode = next_bbcode_position[0] + + var index = bbcode.start + var code = bbcode.code + var raw_args = bbcode.raw_args + var args = {} + if code in ["do", "do!", "set"]: + var compilation: DMCompilation = DMCompilation.new() + args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args]) + else: + # Could be something like: + # "=1.0" + # " rate=20 level=10" + if raw_args and raw_args[0] == "=": + raw_args = "value" + raw_args + for pair in raw_args.strip_edges().split(" "): + if "=" in pair: + var bits = pair.split("=") + args[bits[0]] = bits[1] + + match code: + "wait": + if pauses.has(index): + pauses[index] += args.get("value").to_float() + else: + pauses[index] = args.get("value").to_float() + "speed": + speeds[index] = args.get("value").to_float() + "/speed": + speeds[index] = 1.0 + "do", "do!", "set": + mutations.append([index, args.get("value")]) + "next": + time = args.get("value") if args.has("value") else "0" + + # Find any BB codes that are after this index and remove the length from their start + var length = bbcode.bbcode.length() + for bb in bbcodes: + if bb.offset_start > bbcode.start: + bb.offset_start -= length + bb.start -= length + + # Find any escaped brackets after this that need moving + for i in range(0, escaped_open_brackets.size()): + if escaped_open_brackets[i] > bbcode.start: + escaped_open_brackets[i] -= length + for i in range(0, escaped_close_brackets.size()): + if escaped_close_brackets[i] > bbcode.start: + escaped_close_brackets[i] -= length + + text = text.substr(0, index) + text.substr(index + length) + next_bbcode_position = find_bbcode_positions_in_string(text, false) + + # Put the BB Codes back in + for bb in bbcodes: + text = text.insert(bb.start, bb.bbcode) + + # Put the escaped brackets back in + for index in escaped_open_brackets: + text = text.left(index) + "[" + text.right(text.length() - index - 1) + for index in escaped_close_brackets: + text = text.left(index) + "]" + text.right(text.length() - index - 1) + + +func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]: + if not "[" in string: return [] + + var positions: Array[Dictionary] = [] + + var open_brace_count: int = 0 + var start: int = 0 + var bbcode: String = "" + var code: String = "" + var is_finished_code: bool = false + for i in range(0, string.length()): + if string[i] == "[": + if open_brace_count == 0: + start = i + bbcode = "" + code = "" + is_finished_code = false + open_brace_count += 1 + + else: + if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"): + code += string[i] + else: + is_finished_code = true + + if open_brace_count > 0: + bbcode += string[i] + + if string[i] == "]": + open_brace_count -= 1 + if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]): + positions.append({ + bbcode = bbcode, + code = code, + start = start, + end = i, + raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges() + }) + + if not find_all: + return positions + + return positions diff --git a/addons/dialogue_manager/compiler/resolved_line_data.gd.uid b/addons/dialogue_manager/compiler/resolved_line_data.gd.uid new file mode 100644 index 0000000..bbea7d2 --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_line_data.gd.uid @@ -0,0 +1 @@ +uid://0k6q8kukq0qa diff --git a/addons/dialogue_manager/compiler/resolved_tag_data.gd b/addons/dialogue_manager/compiler/resolved_tag_data.gd new file mode 100644 index 0000000..e926ada --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_tag_data.gd @@ -0,0 +1,26 @@ +## Tag data associated with a line of dialogue. +class_name DMResolvedTagData extends RefCounted + + +## The list of tags. +var tags: PackedStringArray = [] +## The line with any tag syntax removed. +var text_without_tags: String = "" + +# An instance of the compiler [RegEx]. +var regex: DMCompilerRegEx = DMCompilerRegEx.new() + + +func _init(text: String) -> void: + var resolved_tags: PackedStringArray = [] + var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text) + for tag_match in tag_matches: + text = text.replace(tag_match.get_string(), "") + var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",") + for tag in tags: + tag = tag.replace("#", "") + if not tag in resolved_tags: + resolved_tags.append(tag) + + tags = resolved_tags + text_without_tags = text diff --git a/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid b/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid new file mode 100644 index 0000000..98c6f51 --- /dev/null +++ b/addons/dialogue_manager/compiler/resolved_tag_data.gd.uid @@ -0,0 +1 @@ +uid://cqai3ikuilqfq diff --git a/addons/dialogue_manager/compiler/tree_line.gd b/addons/dialogue_manager/compiler/tree_line.gd new file mode 100644 index 0000000..667daad --- /dev/null +++ b/addons/dialogue_manager/compiler/tree_line.gd @@ -0,0 +1,46 @@ +## An intermediate representation of a dialogue line before it gets compiled. +class_name DMTreeLine extends RefCounted + + +## The line number where this dialogue was found (after imported files have had their content imported). +var line_number: int = 0 +## The parent [DMTreeLine] of this line. +## This is stored as a Weak Reference so that this RefCounted can elegantly free itself. +## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive. +var parent: WeakRef +## The ID of this line. +var id: String +## The type of this line (as a [String] defined in [DMConstants]. +var type: String = "" +## Is this line part of a randomised group? +var is_random: bool = false +## The indent count for this line. +var indent: int = 0 +## The text of this line. +var text: String = "" +## The child [DMTreeLine]s of this line. +var children: Array[DMTreeLine] = [] +## Any doc comments attached to this line. +var notes: String = "" +## 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: + id = initial_id + + +func _to_string() -> String: + var tabs = [] + tabs.resize(indent) + tabs.fill("\t") + tabs = "".join(tabs) + + return tabs.join([tabs + "{\n", + "\tid: %s\n" % [id], + "\ttype: %s\n" % [type], + "\tis_random: %s\n" % ["true" if is_random else "false"], + "\ttext: %s\n" % [text], + "\tnotes: %s\n" % [notes], + "\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n", + "}"]) diff --git a/addons/dialogue_manager/compiler/tree_line.gd.uid b/addons/dialogue_manager/compiler/tree_line.gd.uid new file mode 100644 index 0000000..fe1db3a --- /dev/null +++ b/addons/dialogue_manager/compiler/tree_line.gd.uid @@ -0,0 +1 @@ +uid://dsu4i84dpif14 diff --git a/addons/dialogue_manager/components/code_edit.gd b/addons/dialogue_manager/components/code_edit.gd new file mode 100644 index 0000000..df9a373 --- /dev/null +++ b/addons/dialogue_manager/components/code_edit.gd @@ -0,0 +1,610 @@ +@tool +class_name DMCodeEdit extends CodeEdit + + +signal active_title_change(title: String) +signal error_clicked(line_number: int) +signal external_file_requested(path: String, title: String) + + +# A link back to the owner `MainView` +var main_view + +# Theme overrides for syntax highlighting, etc +var theme_overrides: Dictionary: + set(value): + theme_overrides = value + + syntax_highlighter = DMSyntaxHighlighter.new() + + # General UI + add_theme_color_override("font_color", theme_overrides.text_color) + add_theme_color_override("background_color", theme_overrides.background_color) + add_theme_color_override("current_line_color", theme_overrides.current_line_color) + add_theme_font_override("font", get_theme_font("source", "EditorFonts")) + add_theme_font_size_override("font_size", theme_overrides.font_size * theme_overrides.scale) + font_size = round(theme_overrides.font_size) + get: + return theme_overrides + +# Any parse errors +var errors: Array: + set(next_errors): + errors = next_errors + for i in range(0, get_line_count()): + var is_error: bool = false + for error in errors: + if error.line_number == i: + is_error = true + mark_line_as_error(i, is_error) + _on_code_edit_caret_changed() + get: + return errors + +# The last selection (if there was one) so we can remember it for refocusing +var last_selected_text: String + +var font_size: int: + set(value): + font_size = value + add_theme_font_size_override("font_size", font_size * theme_overrides.scale) + get: + return font_size + +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 + add_gutter(0) + set_gutter_type(0, TextEdit.GUTTER_TYPE_ICON) + + # Add comment delimiter + if not has_comment_delimiter("#"): + add_comment_delimiter("#", "", true) + + 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 + if event is InputEventKey and event.is_pressed(): + var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event) + match shortcut: + "toggle_comment": + toggle_comment() + get_viewport().set_input_as_handled() + "delete_line": + delete_current_line() + get_viewport().set_input_as_handled() + "move_up": + move_line(-1) + get_viewport().set_input_as_handled() + "move_down": + move_line(1) + get_viewport().set_input_as_handled() + "text_size_increase": + self.font_size += 1 + get_viewport().set_input_as_handled() + "text_size_decrease": + self.font_size -= 1 + get_viewport().set_input_as_handled() + "text_size_reset": + self.font_size = theme_overrides.font_size + get_viewport().set_input_as_handled() + + elif event is InputEventMouse: + match event.as_text(): + "Ctrl+Mouse Wheel Up", "Command+Mouse Wheel Up": + self.font_size += 1 + get_viewport().set_input_as_handled() + "Ctrl+Mouse Wheel Down", "Command+Mouse Wheel Down": + self.font_size -= 1 + get_viewport().set_input_as_handled() + + +func _can_drop_data(at_position: Vector2, data) -> bool: + if typeof(data) != TYPE_DICTIONARY: return false + if data.type != "files": return false + + var files: PackedStringArray = Array(data.files) + return files.size() > 0 + + +func _drop_data(at_position: Vector2, data) -> void: + var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+") + + var files: PackedStringArray = Array(data.files) + for file in files: + # Don't import the file into itself + if file == main_view.current_file_path: continue + + if file.get_extension() == "dialogue": + var path = file.replace("res://", "").replace(".dialogue", "") + # Find the first non-import line in the file to add our import + var lines = text.split("\n") + for i in range(0, lines.size()): + if not lines[i].begins_with("import "): + insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)]) + set_caret_line(i) + break + else: + var cursor: Vector2 = get_line_column_at_pos(at_position) + if cursor.x > -1 and cursor.y > -1: + set_cursor(cursor) + remove_secondary_carets() + insert_text("\"%s\"" % file, cursor.y, cursor.x) + grab_focus() + + +func _request_code_completion(force: bool) -> void: + 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("< "): + prompt = prompt.substr(2) + else: + prompt = prompt.substr(1) + + if "=> " in current_line: + if matches_prompt(prompt, "end"): + add_code_completion_option(CodeEdit.KIND_CLASS, "END", "END".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons")) + if matches_prompt(prompt, "end!"): + add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons")) + + # Get all titles, including those in imports + for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path): + # Ignore any imported titles that aren't resolved to human readable. + if title.to_int() > 0: + continue + + elif "/" in title: + var bits = title.split("/") + if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]): + add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) + elif matches_prompt(prompt, title): + add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons")) + + # 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 + var names: PackedStringArray = get_character_names(name_so_far) + 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")) + + # 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: + # Not sure why but if this method isn't overridden then all completions are wrapped in quotes. + return candidates + + +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 + 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") + + +#region Helpers + + +# Get the current caret as a Vector2 +func get_cursor() -> Vector2: + return Vector2(get_caret_column(), get_caret_line()) + + +# Set the caret from a Vector2 +func set_cursor(from_cursor: Vector2) -> void: + set_caret_line(from_cursor.y, false) + set_caret_column(from_cursor.x, false) + + +# Check if a prompt is the start of a string without actually being that string +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([]) + var lines = text.split("\n") + for line in lines: + if line.strip_edges().begins_with("~ "): + titles.append(line.strip_edges().substr(2)) + + return titles + + +## Work out what the next title above the current line is +func check_active_title() -> void: + var line_number = get_caret_line() + var lines = text.split("\n") + # Look at each line above this one to find the next title line + for i in range(line_number, -1, -1): + if lines[i].begins_with("~ "): + active_title_change.emit(lines[i].replace("~ ", "")) + return + + active_title_change.emit("") + + +# Move the caret line to match a given title +func go_to_title(title: String) -> void: + var lines = text.split("\n") + for i in range(0, lines.size()): + if lines[i].strip_edges() == "~ " + title: + set_caret_line(i) + center_viewport_to_caret() + + +func get_character_names(beginning_with: String) -> PackedStringArray: + var names: PackedStringArray = [] + var lines = text.split("\n") + for line in lines: + if ": " in line: + var name: String = WEIGHTED_RANDOM_PREFIX.sub(line.split(": ")[0].strip_edges(), "") + if not name in names and matches_prompt(beginning_with, name): + names.append(name) + return names + + +# Mark a line as an error or not +func mark_line_as_error(line_number: int, is_error: bool) -> void: + # Lines display counting from 1 but are actually indexed from 0 + line_number -= 1 + + if line_number < 0: return + + if is_error: + set_line_background_color(line_number, theme_overrides.error_line_color) + set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons")) + else: + set_line_background_color(line_number, theme_overrides.background_color) + set_line_gutter_icon(line_number, 0, null) + + +# Insert or wrap some bbcode at the caret/selection +func insert_bbcode(open_tag: String, close_tag: String = "") -> void: + if close_tag == "": + insert_text_at_caret(open_tag) + grab_focus() + else: + var selected_text = get_selected_text() + insert_text_at_caret("%s%s%s" % [open_tag, selected_text, close_tag]) + grab_focus() + set_caret_column(get_caret_column() - close_tag.length()) + +# Insert text at current caret position +# Move Caret down 1 line if not => END +func insert_text_at_cursor(text: String) -> void: + if text != "=> END": + insert_text_at_caret(text+"\n") + set_caret_line(get_caret_line()+1) + else: + insert_text_at_caret(text) + grab_focus() + + +# Toggle the selected lines as comments +func toggle_comment() -> void: + begin_complex_operation() + + var comment_delimiter: String = delimiter_comments[0] + var is_first_line: bool = true + var will_comment: bool = true + var selections: Array = [] + var line_offsets: Dictionary = {} + + for caret_index in range(0, get_caret_count()): + var from_line: int = get_caret_line(caret_index) + var from_column: int = get_caret_column(caret_index) + var to_line: int = get_caret_line(caret_index) + var to_column: int = get_caret_column(caret_index) + + if has_selection(caret_index): + from_line = get_selection_from_line(caret_index) + to_line = get_selection_to_line(caret_index) + from_column = get_selection_from_column(caret_index) + to_column = get_selection_to_column(caret_index) + + selections.append({ + from_line = from_line, + from_column = from_column, + to_line = to_line, + to_column = to_column + }) + + for line_number in range(from_line, to_line + 1): + if line_offsets.has(line_number): continue + + var line_text: String = get_line(line_number) + + # The first line determines if we are commenting or uncommentingg + if is_first_line: + is_first_line = false + will_comment = not line_text.strip_edges().begins_with(comment_delimiter) + + # Only comment/uncomment if the current line needs to + if will_comment: + set_line(line_number, comment_delimiter + line_text) + line_offsets[line_number] = 1 + elif line_text.begins_with(comment_delimiter): + set_line(line_number, line_text.substr(comment_delimiter.length())) + line_offsets[line_number] = -1 + else: + line_offsets[line_number] = 0 + + for caret_index in range(0, get_caret_count()): + var selection: Dictionary = selections[caret_index] + select( + selection.from_line, + selection.from_column + line_offsets[selection.from_line], + selection.to_line, + selection.to_column + line_offsets[selection.to_line], + caret_index + ) + set_caret_column(selection.from_column + line_offsets[selection.from_line], false, caret_index) + + end_complex_operation() + + text_set.emit() + text_changed.emit() + + +# Remove the current line +func delete_current_line() -> void: + var cursor = get_cursor() + if get_line_count() == 1: + select_all() + elif cursor.y == 0: + select(0, 0, 1, 0) + else: + select(cursor.y - 1, get_line_width(cursor.y - 1), cursor.y, get_line_width(cursor.y)) + delete_selection() + text_changed.emit() + + +# Move the selected lines up or down +func move_line(offset: int) -> void: + offset = clamp(offset, -1, 1) + + var starting_scroll := scroll_vertical + var cursor = get_cursor() + var reselect: bool = false + var from: int = cursor.y + var to: int = cursor.y + if has_selection(): + reselect = true + from = get_selection_from_line() + to = get_selection_to_line() + + var lines := text.split("\n") + + # Prevent the lines from being out of bounds + if from + offset < 0 or to + offset >= lines.size(): return + + var target_from_index = from - 1 if offset == -1 else to + 1 + var target_to_index = to if offset == -1 else from + var line_to_move = lines[target_from_index] + lines.remove_at(target_from_index) + lines.insert(target_to_index, line_to_move) + + text = "\n".join(lines) + + cursor.y += offset + set_cursor(cursor) + from += offset + to += offset + if reselect: + select(from, 0, to, get_line_width(to)) + + text_changed.emit() + scroll_vertical = starting_scroll + offset + + +#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: + if symbol.begins_with("res://") and symbol.ends_with(".dialogue"): + set_symbol_lookup_word_as_valid(true) + return + + for title in get_titles(): + if symbol == title: + set_symbol_lookup_word_as_valid(true) + return + set_symbol_lookup_word_as_valid(false) + + +func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void: + if symbol.begins_with("res://") and symbol.ends_with(".dialogue"): + external_file_requested.emit(symbol, "") + else: + go_to_title(symbol) + + +func _on_code_edit_text_changed() -> void: + request_code_completion(true) + + +func _on_code_edit_text_set() -> void: + queue_redraw() + + +func _on_code_edit_caret_changed() -> void: + check_active_title() + last_selected_text = get_selected_text() + + +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/addons/dialogue_manager/components/code_edit.gd.uid b/addons/dialogue_manager/components/code_edit.gd.uid new file mode 100644 index 0000000..ab2b9e5 --- /dev/null +++ b/addons/dialogue_manager/components/code_edit.gd.uid @@ -0,0 +1 @@ +uid://djeybvlb332mp diff --git a/addons/dialogue_manager/components/code_edit.tscn b/addons/dialogue_manager/components/code_edit.tscn new file mode 100644 index 0000000..0c25707 --- /dev/null +++ b/addons/dialogue_manager/components/code_edit.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"] + +[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"] +[ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"] + +[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"] +script = ExtResource("1_58cfo") + +[node name="CodeEdit" type="CodeEdit"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "~ title_thing + +if this = \"that\" or 'this' +Nathan: Something +- Then [if test.thing() == 2.0] => somewhere +- Other => END! + +~ somewhere + +set has_something = true +=> END" +highlight_all_occurrences = true +highlight_current_line = true +draw_tabs = true +syntax_highlighter = SubResource("SyntaxHighlighter_cobxx") +scroll_past_end_of_file = true +minimap_draw = true +symbol_lookup_on_click = true +line_folding = true +gutters_draw_line_numbers = true +gutters_draw_fold_gutter = true +delimiter_strings = Array[String](["\" \""]) +delimiter_comments = Array[String](["#"]) +code_completion_enabled = true +code_completion_prefixes = Array[String]([">", "<"]) +indent_automatic = true +auto_brace_completion_enabled = true +auto_brace_completion_highlight_matching = true +auto_brace_completion_pairs = { +"\"": "\"", +"(": ")", +"[": "]", +"{": "}" +} +script = ExtResource("1_g324i") + +[connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"] +[connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"] +[connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"] +[connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"] +[connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"] +[connection signal="text_set" from="." to="." method="_on_code_edit_text_set"] diff --git a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd new file mode 100644 index 0000000..3f4e0d9 --- /dev/null +++ b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd @@ -0,0 +1,231 @@ +@tool +class_name DMSyntaxHighlighter extends SyntaxHighlighter + + +var regex: DMCompilerRegEx = DMCompilerRegEx.new() +var compilation: DMCompilation = DMCompilation.new() +var expression_parser = DMExpressionParser.new() + +var cache: Dictionary = {} + + +func _clear_highlighting_cache() -> void: + cache.clear() + + +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) + + # Prevent an error from popping up while developing + if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty(): + return colors + + # Disable this, as well as the line at the bottom of this function to remove the cache. + if text in cache: + return cache[text] + + var theme: Dictionary = text_edit.theme_overrides + + var index: int = 0 + + match DMCompiler.get_line_type(text): + DMConstants.TYPE_USING: + colors[index] = { color = theme.conditions_color } + colors[index + "using ".length()] = { color = theme.text_color } + + DMConstants.TYPE_IMPORT: + colors[index] = { color = theme.conditions_color } + var import: RegExMatch = regex.IMPORT_REGEX.search(text) + 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 } + + DMConstants.TYPE_TITLE: + colors[index] = { color = theme.titles_color } + + DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN: + colors[0] = { color = theme.conditions_color } + index = text.find(" ") + if index > -1: + var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) + if expression.size() == 0: + colors[index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index) + + DMConstants.TYPE_MUTATION: + colors[0] = { color = theme.mutations_color } + index = text.find(" ") + var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) + if expression.size() == 0: + colors[index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index) + + DMConstants.TYPE_GOTO: + if text.strip_edges().begins_with("%"): + colors[index] = { color = theme.symbols_color } + index = text.find(" ") + _highlight_goto(text, colors, index) + + DMConstants.TYPE_RANDOM: + colors[index] = { color = theme.symbols_color } + + DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE: + if text.strip_edges().begins_with("%"): + colors[index] = { color = theme.symbols_color } + index = text.find(" ", text.find("%")) + colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) } + + var dialogue_text: String = text.substr(index, text.find("=>")) + + # Highlight character name (but ignore ":" within line ID reference) + var split_index: int = dialogue_text.replace("\\:", "??").find(":") + 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) + for replacement: RegExMatch in replacements: + var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2) + var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start()) + var expression_index: int = index + replacement.get_start() + colors[expression_index] = { color = theme.symbols_color } + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[expression_index] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index + 2) + colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color } + colors[expression_index + expression_text.length() + 4] = { color = theme.text_color } + # Tags (and inline mutations) + var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("") + var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true) + for bbcode: Dictionary in bbcodes: + var tag: String = bbcode.code + var code: String = bbcode.raw_args + if code.begins_with("["): + colors[index + bbcode.start] = { color = theme.symbols_color } + colors[index + bbcode.start + 2] = { color = theme.text_color } + var pipe_cursor: int = code.find("|") + while pipe_cursor > -1: + colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color } + colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color } + pipe_cursor = code.find("|", pipe_cursor + 1) + colors[index + bbcode.end - 1] = { color = theme.symbols_color } + colors[index + bbcode.end + 1] = { color = theme.text_color } + else: + colors[index + bbcode.start] = { color = theme.symbols_color } + if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"): + if tag.begins_with("if"): + colors[index + bbcode.start + 1] = { color = theme.conditions_color } + else: + colors[index + bbcode.start + 1] = { color = theme.mutations_color } + var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length()) + if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color } + else: + _highlight_expression(expression, colors, index + 2) + # else and closing if have no expression + elif tag.begins_with("else") or tag.begins_with("/if"): + colors[index + bbcode.start + 1] = { color = theme.conditions_color } + colors[index + bbcode.end] = { color = theme.symbols_color } + colors[index + bbcode.end + 1] = { color = theme.text_color } + # Jumps + if "=> " in text or "=>< " in text: + _highlight_goto(text, colors, index) + + # Order the dictionary keys to prevent CodeEdit from having issues + var ordered_colors: Dictionary = {} + var ordered_keys: Array = colors.keys() + ordered_keys.sort() + for key_index: int in ordered_keys: + ordered_colors[key_index] = colors[key_index] + + cache[text] = ordered_colors + return ordered_colors + + +func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int: + var theme: Dictionary = get_text_edit().theme_overrides + var last_index: int = index + for token: Dictionary in tokens: + last_index = token.i + match token.type: + DMConstants.TOKEN_COMMENT: + colors[index + token.i] = { color = theme.comments_color } + + DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: + colors[index + token.i] = { color = theme.conditions_color } + + DMConstants.TOKEN_VARIABLE: + if token.value in ["true", "false"]: + colors[index + token.i] = { color = theme.conditions_color } + else: + colors[index + token.i] = { color = theme.members_color } + + 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: + colors[index + token.i] = { color = theme.strings_color } + + DMConstants.TOKEN_FUNCTION: + colors[index + token.i] = { color = theme.mutations_color } + colors[index + token.i + token.function.length()] = { color = theme.symbols_color } + for parameter: Array in token.value: + last_index = _highlight_expression(parameter, colors, index) + DMConstants.TOKEN_PARENS_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } + + DMConstants.TOKEN_DICTIONARY_REFERENCE: + colors[index + token.i] = { color = theme.members_color } + colors[index + token.i + token.variable.length()] = { color = theme.symbols_color } + last_index = _highlight_expression(token.value, colors, index) + DMConstants.TOKEN_ARRAY: + colors[index + token.i] = { color = theme.symbols_color } + for item: Array in token.value: + last_index = _highlight_expression(item, colors, index) + DMConstants.TOKEN_BRACKET_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } + + DMConstants.TOKEN_DICTIONARY: + colors[index + token.i] = { color = theme.symbols_color } + last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index) + DMConstants.TOKEN_BRACE_CLOSE: + colors[index + token.i] = { color = theme.symbols_color } + last_index += 1 + + DMConstants.TOKEN_GROUP: + last_index = _highlight_expression(token.value, colors, index) + + return last_index + + +func _highlight_goto(text: String, colors: Dictionary, index: int) -> int: + var theme: Dictionary = get_text_edit().theme_overrides + var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {}) + colors[goto_data.index] = { color = theme.jumps_color } + if "{{" in text: + index = text.find("{{", goto_data.index) + var last_index: int = 0 + if goto_data.error: + colors[index + 2] = { color = theme.critical_color } + else: + last_index = _highlight_expression(goto_data.expression, colors, index) + index = text.find("}}", index + last_index) + colors[index] = { color = theme.jumps_color } + + return index diff --git a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid new file mode 100644 index 0000000..9bad8cc --- /dev/null +++ b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid @@ -0,0 +1 @@ +uid://klpiq4tk3t7a diff --git a/addons/dialogue_manager/components/download_update_panel.gd b/addons/dialogue_manager/components/download_update_panel.gd new file mode 100644 index 0000000..e67a93f --- /dev/null +++ b/addons/dialogue_manager/components/download_update_panel.gd @@ -0,0 +1,84 @@ +@tool +extends Control + + +signal failed() +signal updated(updated_to_version: String) + + +const DialogueConstants = preload("../constants.gd") + +const TEMP_FILE_NAME = "user://temp.zip" + + +@onready var logo: TextureRect = %Logo +@onready var label: Label = $VBox/Label +@onready var http_request: HTTPRequest = $HTTPRequest +@onready var download_button: Button = %DownloadButton + +var next_version_release: Dictionary: + set(value): + next_version_release = value + label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1) + get: + return next_version_release + + +func _ready() -> void: + $VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update") + $VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes") + + +### Signals + + +func _on_download_button_pressed() -> void: + # Safeguard the actual dialogue manager repo from accidentally updating itself + if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"): + prints("You can't update the addon from within itself.") + failed.emit() + return + + http_request.request(next_version_release.zipball_url) + download_button.disabled = true + download_button.text = DialogueConstants.translate(&"update.downloading") + + +func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + failed.emit() + return + + # Save the downloaded zip + var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE) + zip_file.store_buffer(body) + zip_file.close() + + OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager")) + + var zip_reader: ZIPReader = ZIPReader.new() + zip_reader.open(TEMP_FILE_NAME) + var files: PackedStringArray = zip_reader.get_files() + + var base_path = files[1] + # Remove archive folder + files.remove_at(0) + # Remove assets folder + files.remove_at(0) + + for path in files: + var new_file_path: String = path.replace(base_path, "") + if path.ends_with("/"): + DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path) + else: + var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE) + file.store_buffer(zip_reader.read_file(path)) + + zip_reader.close() + DirAccess.remove_absolute(TEMP_FILE_NAME) + + updated.emit(next_version_release.tag_name.substr(1)) + + +func _on_notes_button_pressed() -> void: + OS.shell_open(next_version_release.html_url) diff --git a/addons/dialogue_manager/components/download_update_panel.gd.uid b/addons/dialogue_manager/components/download_update_panel.gd.uid new file mode 100644 index 0000000..7910ab4 --- /dev/null +++ b/addons/dialogue_manager/components/download_update_panel.gd.uid @@ -0,0 +1 @@ +uid://kpwo418lb2t2 diff --git a/addons/dialogue_manager/components/download_update_panel.tscn b/addons/dialogue_manager/components/download_update_panel.tscn new file mode 100644 index 0000000..540abd3 --- /dev/null +++ b/addons/dialogue_manager/components/download_update_panel.tscn @@ -0,0 +1,60 @@ +[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"] + +[ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"] +[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"] + +[node name="DownloadUpdatePanel" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_4tm1k") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -1.0 +offset_top = 9.0 +offset_right = -1.0 +offset_bottom = 9.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 10 + +[node name="Logo" type="TextureRect" parent="VBox"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(300, 80) +layout_mode = 2 +texture = ExtResource("2_4o2m6") +stretch_mode = 5 + +[node name="Label" type="Label" parent="VBox"] +layout_mode = 2 +text = "v1.2.3 is available for download." +horizontal_alignment = 1 + +[node name="Center" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="DownloadButton" type="Button" parent="VBox/Center"] +unique_name_in_owner = true +layout_mode = 2 +text = "Download update" + +[node name="Center2" type="CenterContainer" parent="VBox"] +layout_mode = 2 + +[node name="NotesButton" type="LinkButton" parent="VBox/Center2"] +layout_mode = 2 +text = "Read release notes" + +[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] +[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"] +[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"] diff --git a/addons/dialogue_manager/components/editor_property/editor_property.gd b/addons/dialogue_manager/components/editor_property/editor_property.gd new file mode 100644 index 0000000..5deef65 --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/editor_property.gd @@ -0,0 +1,48 @@ +@tool +extends EditorProperty + + +const DialoguePropertyEditorControl = preload("./editor_property_control.tscn") + + +var editor_plugin: EditorPlugin + +var control = DialoguePropertyEditorControl.instantiate() +var current_value: Resource +var is_updating: bool = false + + +func _init() -> void: + add_child(control) + + control.resource = current_value + + control.pressed.connect(_on_button_pressed) + control.resource_changed.connect(_on_resource_changed) + + +func _update_property() -> void: + var next_value = get_edited_object()[get_edited_property()] + + # The resource might have been deleted elsewhere so check that it's not in a weird state + if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"): + emit_changed(get_edited_property(), null) + return + + if next_value == current_value: return + + is_updating = true + current_value = next_value + control.resource = current_value + is_updating = false + + +### Signals + + +func _on_button_pressed() -> void: + editor_plugin.edit(current_value) + + +func _on_resource_changed(next_resource: Resource) -> void: + emit_changed(get_edited_property(), next_resource) diff --git a/addons/dialogue_manager/components/editor_property/editor_property.gd.uid b/addons/dialogue_manager/components/editor_property/editor_property.gd.uid new file mode 100644 index 0000000..283cc43 --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/editor_property.gd.uid @@ -0,0 +1 @@ +uid://nyypeje1a036 diff --git a/addons/dialogue_manager/components/editor_property/editor_property_control.gd b/addons/dialogue_manager/components/editor_property/editor_property_control.gd new file mode 100644 index 0000000..d063d0e --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/editor_property_control.gd @@ -0,0 +1,147 @@ +@tool +extends HBoxContainer + + +signal pressed() +signal resource_changed(next_resource: Resource) + + +const ITEM_NEW = 100 +const ITEM_QUICK_LOAD = 200 +const ITEM_LOAD = 201 +const ITEM_EDIT = 300 +const ITEM_CLEAR = 301 +const ITEM_FILESYSTEM = 400 + + +@onready var button: Button = $ResourceButton +@onready var menu_button: Button = $MenuButton +@onready var menu: PopupMenu = $Menu +@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog +@onready var files_list = $QuickOpenDialog/FilesList +@onready var new_dialog: FileDialog = $NewDialog +@onready var open_dialog: FileDialog = $OpenDialog + +var editor_plugin: EditorPlugin + +var resource: Resource: + set(next_resource): + resource = next_resource + if button: + button.resource = resource + get: + return resource + +var is_waiting_for_file: bool = false +var quick_selected_file: String = "" + + +func _ready() -> void: + menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons") + editor_plugin = Engine.get_meta("DialogueManagerPlugin") + + +func build_menu() -> void: + menu.clear() + + menu.add_icon_item(editor_plugin._get_plugin_icon(), "New Dialogue", ITEM_NEW) + menu.add_separator() + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD) + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD) + if resource: + menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT) + menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR) + menu.add_separator() + menu.add_item("Show in FileSystem", ITEM_FILESYSTEM) + + menu.size = Vector2.ZERO + + +### Signals + + +func _on_new_dialog_file_selected(path: String) -> void: + editor_plugin.main_view.new_file(path) + is_waiting_for_file = false + if Engine.get_meta("DMCache").has_file(path): + resource_changed.emit(load(path)) + else: + var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource + next_resource.resource_path = path + resource_changed.emit(next_resource) + + +func _on_open_dialog_file_selected(file: String) -> void: + resource_changed.emit(load(file)) + + +func _on_file_dialog_canceled() -> void: + is_waiting_for_file = false + + +func _on_resource_button_pressed() -> void: + if is_instance_valid(resource): + EditorInterface.call_deferred("edit_resource", resource) + else: + build_menu() + menu.position = get_viewport().position + Vector2i( + button.global_position.x + button.size.x - menu.size.x, + 2 + menu_button.global_position.y + button.size.y + ) + menu.popup() + + +func _on_resource_button_resource_dropped(next_resource: Resource) -> void: + resource_changed.emit(next_resource) + + +func _on_menu_button_pressed() -> void: + build_menu() + menu.position = get_viewport().position + Vector2i( + menu_button.global_position.x + menu_button.size.x - menu.size.x, + 2 + menu_button.global_position.y + menu_button.size.y + ) + menu.popup() + + +func _on_menu_id_pressed(id: int) -> void: + match id: + ITEM_NEW: + is_waiting_for_file = true + new_dialog.popup_centered() + + ITEM_QUICK_LOAD: + quick_selected_file = "" + files_list.files = Engine.get_meta("DMCache").get_files() + if resource: + files_list.select_file(resource.resource_path) + quick_open_dialog.popup_centered() + files_list.focus_filter() + + ITEM_LOAD: + is_waiting_for_file = true + open_dialog.popup_centered() + + ITEM_EDIT: + EditorInterface.call_deferred("edit_resource", resource) + + ITEM_CLEAR: + resource_changed.emit(null) + + ITEM_FILESYSTEM: + var file_system = EditorInterface.get_file_system_dock() + file_system.navigate_to_path(resource.resource_path) + + +func _on_files_list_file_double_clicked(file_path: String) -> void: + resource_changed.emit(load(file_path)) + quick_open_dialog.hide() + + +func _on_files_list_file_selected(file_path: String) -> void: + quick_selected_file = file_path + + +func _on_quick_open_dialog_confirmed() -> void: + if quick_selected_file != "": + resource_changed.emit(load(quick_selected_file)) diff --git a/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid b/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid new file mode 100644 index 0000000..aab7d8d --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid @@ -0,0 +1 @@ +uid://dooe2pflnqtve diff --git a/addons/dialogue_manager/components/editor_property/editor_property_control.tscn b/addons/dialogue_manager/components/editor_property/editor_property_control.tscn new file mode 100644 index 0000000..7cb02e8 --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/editor_property_control.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"] + +[ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"] +[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"] +[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"] + +[node name="PropertyEditorButton" type="HBoxContainer"] +offset_right = 40.0 +offset_bottom = 40.0 +size_flags_horizontal = 3 +theme_override_constants/separation = 0 +script = ExtResource("1_het12") + +[node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")] +layout_mode = 2 +text = "" +text_overrun_behavior = 3 +clip_text = true + +[node name="MenuButton" type="Button" parent="."] +layout_mode = 2 + +[node name="Menu" type="PopupMenu" parent="."] + +[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."] +title = "Find Dialogue Resource" +size = Vector2i(400, 600) +min_size = Vector2i(400, 600) +ok_button_text = "Open" + +[node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")] + +[node name="NewDialog" type="FileDialog" parent="."] +size = Vector2i(900, 750) +min_size = Vector2i(900, 750) +dialog_hide_on_ok = true +filters = PackedStringArray("*.dialogue ; Dialogue") + +[node name="OpenDialog" type="FileDialog" parent="."] +title = "Open a File" +size = Vector2i(900, 750) +min_size = Vector2i(900, 750) +ok_button_text = "Open" +dialog_hide_on_ok = true +file_mode = 0 +filters = PackedStringArray("*.dialogue ; Dialogue") + +[connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"] +[connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"] +[connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"] +[connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"] +[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"] +[connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"] +[connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"] +[connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"] +[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"] +[connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"] +[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"] diff --git a/addons/dialogue_manager/components/editor_property/resource_button.gd b/addons/dialogue_manager/components/editor_property/resource_button.gd new file mode 100644 index 0000000..5ba33dc --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/resource_button.gd @@ -0,0 +1,48 @@ +@tool +extends Button + + +signal resource_dropped(next_resource: Resource) + + +var resource: Resource: + set(next_resource): + resource = next_resource + if resource: + icon = Engine.get_meta("DialogueManagerPlugin")._get_plugin_icon() + text = resource.resource_path.get_file().replace(".dialogue", "") + else: + icon = null + text = "" + get: + return resource + + +func _notification(what: int) -> void: + match what: + NOTIFICATION_DRAG_BEGIN: + var data = get_viewport().gui_get_drag_data() + if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"): + add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit")) + add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit")) + + NOTIFICATION_DRAG_END: + self.resource = resource + remove_theme_stylebox_override("normal") + remove_theme_stylebox_override("hover") + + +func _can_drop_data(at_position: Vector2, data) -> bool: + if typeof(data) != TYPE_DICTIONARY: return false + if data.type != "files": return false + + var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") + return files.size() > 0 + + +func _drop_data(at_position: Vector2, data) -> void: + var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") + + if files.size() == 0: return + + resource_dropped.emit(load(files[0])) diff --git a/addons/dialogue_manager/components/editor_property/resource_button.gd.uid b/addons/dialogue_manager/components/editor_property/resource_button.gd.uid new file mode 100644 index 0000000..b1b9d26 --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/resource_button.gd.uid @@ -0,0 +1 @@ +uid://damhqta55t67c diff --git a/addons/dialogue_manager/components/editor_property/resource_button.tscn b/addons/dialogue_manager/components/editor_property/resource_button.tscn new file mode 100644 index 0000000..691e527 --- /dev/null +++ b/addons/dialogue_manager/components/editor_property/resource_button.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"] + +[ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"] + +[node name="ResourceButton" type="Button"] +offset_right = 8.0 +offset_bottom = 8.0 +size_flags_horizontal = 3 +script = ExtResource("1_7u2i7") diff --git a/addons/dialogue_manager/components/errors_panel.gd b/addons/dialogue_manager/components/errors_panel.gd new file mode 100644 index 0000000..0b72d37 --- /dev/null +++ b/addons/dialogue_manager/components/errors_panel.gd @@ -0,0 +1,85 @@ +@tool +extends HBoxContainer + + +signal error_pressed(line_number) + + +const DialogueConstants = preload("../constants.gd") + + +@onready var error_button: Button = $ErrorButton +@onready var next_button: Button = $NextButton +@onready var count_label: Label = $CountLabel +@onready var previous_button: Button = $PreviousButton + +## The index of the current error being shown +var error_index: int = 0: + set(next_error_index): + error_index = wrap(next_error_index, 0, errors.size()) + show_error() + get: + return error_index + +## The list of all errors +var errors: Array = []: + set(next_errors): + errors = next_errors + self.error_index = 0 + get: + return errors + + +func _ready() -> void: + apply_theme() + hide() + + +## Set up colors and icons +func apply_theme() -> void: + error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor")) + error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor")) + error_button.icon = get_theme_icon("StatusError", "EditorIcons") + previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons") + next_button.icon = get_theme_icon("ArrowRight", "EditorIcons") + + +## Move the error index to match a given line +func show_error_for_line_number(line_number: int) -> void: + for i in range(0, errors.size()): + if errors[i].line_number == line_number: + self.error_index = i + + +## Show the current error +func show_error() -> void: + if errors.size() == 0: + hide() + else: + show() + count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() }) + var error = errors[error_index] + error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) + if error.has("external_error"): + error_button.text += " " + DialogueConstants.get_error_message(error.external_error) + + +### Signals + + +func _on_errors_panel_theme_changed() -> void: + apply_theme() + + +func _on_error_button_pressed() -> void: + error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number) + + +func _on_previous_button_pressed() -> void: + self.error_index -= 1 + _on_error_button_pressed() + + +func _on_next_button_pressed() -> void: + self.error_index += 1 + _on_error_button_pressed() diff --git a/addons/dialogue_manager/components/errors_panel.gd.uid b/addons/dialogue_manager/components/errors_panel.gd.uid new file mode 100644 index 0000000..c305a80 --- /dev/null +++ b/addons/dialogue_manager/components/errors_panel.gd.uid @@ -0,0 +1 @@ +uid://d2l8nlb6hhrfp diff --git a/addons/dialogue_manager/components/errors_panel.tscn b/addons/dialogue_manager/components/errors_panel.tscn new file mode 100644 index 0000000..0b653cc --- /dev/null +++ b/addons/dialogue_manager/components/errors_panel.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"] + +[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"] + +[sub_resource type="Image" id="Image_w0gko"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"] +image = SubResource("Image_w0gko") + +[node name="ErrorsPanel" type="HBoxContainer"] +visible = false +offset_right = 1024.0 +offset_bottom = 600.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_nfm3c") +metadata/_edit_layout_mode = 1 + +[node name="ErrorButton" type="Button" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_hover_color = Color(0, 0, 0, 1) +theme_override_constants/h_separation = 3 +icon = SubResource("ImageTexture_s6fxl") +flat = true +alignment = 0 +text_overrun_behavior = 4 + +[node name="Spacer" type="Control" parent="."] +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="."] +layout_mode = 2 +icon = SubResource("ImageTexture_s6fxl") +flat = true + +[node name="CountLabel" type="Label" parent="."] +layout_mode = 2 + +[node name="NextButton" type="Button" parent="."] +layout_mode = 2 +icon = SubResource("ImageTexture_s6fxl") +flat = true + +[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"] +[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"] +[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"] diff --git a/addons/dialogue_manager/components/files_list.gd b/addons/dialogue_manager/components/files_list.gd new file mode 100644 index 0000000..31f6158 --- /dev/null +++ b/addons/dialogue_manager/components/files_list.gd @@ -0,0 +1,150 @@ +@tool +extends VBoxContainer + + +signal file_selected(file_path: String) +signal file_popup_menu_requested(at_position: Vector2) +signal file_double_clicked(file_path: String) +signal file_middle_clicked(file_path: String) + + +const DialogueConstants = preload("../constants.gd") + +const MODIFIED_SUFFIX = "(*)" + + +@export var icon: Texture2D + +@onready var filter_edit: LineEdit = $FilterEdit +@onready var list: ItemList = $List + +var file_map: Dictionary = {} + +var current_file_path: String = "" +var last_selected_file_path: String = "" + +var files: PackedStringArray = []: + set(next_files): + files = next_files + files.sort() + update_file_map() + apply_filter() + get: + return files + +var unsaved_files: Array[String] = [] + +var filter: String = "": + set(next_filter): + filter = next_filter + apply_filter() + get: + return filter + + +func _ready() -> void: + apply_theme() + + filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter") + + +func focus_filter() -> void: + filter_edit.grab_focus() + + +func select_file(file: String) -> void: + list.deselect_all() + for i in range(0, list.get_item_count()): + var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "") + if item_text == get_nice_file(file, item_text.count("/") + 1): + list.select(i) + last_selected_file_path = file + + +func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void: + if not file in unsaved_files and is_unsaved: + unsaved_files.append(file) + elif file in unsaved_files and not is_unsaved: + unsaved_files.erase(file) + apply_filter() + + +func update_file_map() -> void: + file_map = {} + for file in files: + var nice_file: String = get_nice_file(file) + + # See if a value with just the file name is already in the map + for key in file_map.keys(): + if file_map[key] == nice_file: + var bit_count = nice_file.count("/") + 2 + + var existing_nice_file = get_nice_file(key, bit_count) + nice_file = get_nice_file(file, bit_count) + + while nice_file == existing_nice_file: + bit_count += 1 + existing_nice_file = get_nice_file(key, bit_count) + nice_file = get_nice_file(file, bit_count) + + file_map[key] = existing_nice_file + + file_map[file] = nice_file + + +func get_nice_file(file_path: String, path_bit_count: int = 1) -> String: + var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/") + bits = bits.slice(-path_bit_count) + return "/".join(bits) + + +func apply_filter() -> void: + list.clear() + for file in file_map.keys(): + if filter == "" or filter.to_lower() in file.to_lower(): + var nice_file = file_map[file] + if file in unsaved_files: + nice_file += MODIFIED_SUFFIX + var new_id := list.add_item(nice_file) + list.set_item_icon(new_id, icon) + + select_file(current_file_path) + + +func apply_theme() -> void: + if is_instance_valid(filter_edit): + filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) + + +### Signals + + +func _on_theme_changed() -> void: + apply_theme() + + +func _on_filter_edit_text_changed(new_text: String) -> void: + self.filter = new_text + + +func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void: + var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") + var file = file_map.find_key(item_text) + + if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT: + select_file(file) + file_selected.emit(file) + if mouse_button_index == MOUSE_BUTTON_RIGHT: + file_popup_menu_requested.emit(at_position) + + if mouse_button_index == MOUSE_BUTTON_MIDDLE: + file_middle_clicked.emit(file) + + +func _on_list_item_activated(index: int) -> void: + var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") + var file = file_map.find_key(item_text) + select_file(file) + file_double_clicked.emit(file) diff --git a/addons/dialogue_manager/components/files_list.gd.uid b/addons/dialogue_manager/components/files_list.gd.uid new file mode 100644 index 0000000..2a1089a --- /dev/null +++ b/addons/dialogue_manager/components/files_list.gd.uid @@ -0,0 +1 @@ +uid://dqa4a4wwoo0aa diff --git a/addons/dialogue_manager/components/files_list.tscn b/addons/dialogue_manager/components/files_list.tscn new file mode 100644 index 0000000..c9e862b --- /dev/null +++ b/addons/dialogue_manager/components/files_list.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"] + +[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] +[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] + +[node name="FilesList" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +script = ExtResource("1_cytii") +icon = ExtResource("2_3ijx1") + +[node name="FilterEdit" type="LineEdit" parent="."] +layout_mode = 2 +placeholder_text = "Filter files" +clear_button_enabled = true + +[node name="List" type="ItemList" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +allow_rmb_select = true + +[connection signal="theme_changed" from="." to="." method="_on_theme_changed"] +[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"] +[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"] +[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"] diff --git a/addons/dialogue_manager/components/find_in_files.gd b/addons/dialogue_manager/components/find_in_files.gd new file mode 100644 index 0000000..de64b71 --- /dev/null +++ b/addons/dialogue_manager/components/find_in_files.gd @@ -0,0 +1,229 @@ +@tool +extends Control + +signal result_selected(path: String, cursor: Vector2, length: int) + + +const DialogueConstants = preload("../constants.gd") + + +@export var main_view: Control +@export var code_edit: CodeEdit + +@onready var input: LineEdit = %Input +@onready var search_button: Button = %SearchButton +@onready var match_case_button: CheckBox = %MatchCaseButton +@onready var replace_toggle: CheckButton = %ReplaceToggle +@onready var replace_container: VBoxContainer = %ReplaceContainer +@onready var replace_input: LineEdit = %ReplaceInput +@onready var replace_selected_button: Button = %ReplaceSelectedButton +@onready var replace_all_button: Button = %ReplaceAllButton +@onready var results_container: VBoxContainer = %ResultsContainer +@onready var result_template: HBoxContainer = %ResultTemplate + +var current_results: Dictionary = {}: + set(value): + current_results = value + update_results_view() + if current_results.size() == 0: + replace_selected_button.disabled = true + replace_all_button.disabled = true + else: + replace_selected_button.disabled = false + replace_all_button.disabled = false + get: + return current_results + +var selections: PackedStringArray = [] + + +func prepare() -> void: + input.grab_focus() + + var template_label = result_template.get_node("Label") + template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color + template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font")) + + replace_toggle.set_pressed_no_signal(false) + replace_container.hide() + + $VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find") + input.placeholder_text = DialogueConstants.translate(&"search.placeholder") + input.text = "" + search_button.text = DialogueConstants.translate(&"search.find_all") + match_case_button.text = DialogueConstants.translate(&"search.match_case") + replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace") + $VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") + replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder") + replace_input.text = "" + replace_all_button.text = DialogueConstants.translate(&"search.replace_all") + replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected") + + selections.clear() + self.current_results = {} + +#region helpers + + +func update_results_view() -> void: + for child in results_container.get_children(): + child.queue_free() + + for path in current_results.keys(): + var path_label: Label = Label.new() + path_label.text = path + # Show open files + if main_view.open_buffers.has(path): + path_label.text += "(*)" + results_container.add_child(path_label) + for path_result in current_results.get(path): + var result_item: HBoxContainer = result_template.duplicate() + + var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox + var key: String = get_selection_key(path, path_result) + checkbox.toggled.connect(func(is_pressed): + if is_pressed: + if not selections.has(key): + selections.append(key) + else: + if selections.has(key): + selections.remove_at(selections.find(key)) + ) + checkbox.set_pressed_no_signal(selections.has(key)) + checkbox.visible = replace_toggle.button_pressed + + var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel + var colors: Dictionary = code_edit.theme_overrides + var highlight: String = "" + if replace_toggle.button_pressed: + 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.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): + if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click: + result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length()) + ) + + results_container.add_child(result_item) + + +func find_in_files() -> Dictionary: + var results: Dictionary = {} + + var q: String = input.text + var cache = Engine.get_meta("DMCache") + var file: FileAccess + for path in cache.get_files(): + var path_results: Array = [] + var lines: PackedStringArray = [] + + if main_view.open_buffers.has(path): + lines = main_view.open_buffers.get(path).text.split("\n") + else: + file = FileAccess.open(path, FileAccess.READ) + lines = file.get_as_text().split("\n") + + for i in range(0, lines.size()): + var index: int = find_in_line(lines[i], q) + while index > -1: + path_results.append({ + line = i, + index = index, + text = lines[i], + matched_text = lines[i].substr(index, q.length()), + query = q + }) + index = find_in_line(lines[i], q, index + q.length()) + + if file != null and file.is_open(): + file.close() + + if path_results.size() > 0: + results[path] = path_results + + return results + + +func get_selection_key(path: String, path_result: Dictionary) -> String: + return "%s-%d-%d" % [path, path_result.line, path_result.index] + + +func find_in_line(line: String, query: String, from_index: int = 0) -> int: + if match_case_button.button_pressed: + return line.find(query, from_index) + else: + return line.findn(query, from_index) + + +func replace_results(only_selected: bool) -> void: + var file: FileAccess + var lines: PackedStringArray = [] + for path in current_results: + if main_view.open_buffers.has(path): + lines = main_view.open_buffers.get(path).text.split("\n") + else: + file = FileAccess.open(path, FileAccess.READ_WRITE) + lines = file.get_as_text().split("\n") + + # Read the results in reverse because we're going to be modifying them as we go + var path_results: Array = current_results.get(path).duplicate() + path_results.reverse() + for path_result in path_results: + var key: String = get_selection_key(path, path_result) + if not only_selected or (only_selected and selections.has(key)): + lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length()) + + var replaced_text: String = "\n".join(lines) + if file != null and file.is_open(): + file.seek(0) + file.store_string(replaced_text) + file.close() + else: + main_view.open_buffers.get(path).text = replaced_text + if main_view.current_file_path == path: + code_edit.text = replaced_text + + current_results = find_in_files() + + +#endregion + +#region signals + + +func _on_search_button_pressed() -> void: + selections.clear() + self.current_results = find_in_files() + + +func _on_input_text_submitted(new_text: String) -> void: + _on_search_button_pressed() + + +func _on_replace_toggle_toggled(toggled_on: bool) -> void: + replace_container.visible = toggled_on + if toggled_on: + replace_input.grab_focus() + update_results_view() + + +func _on_replace_input_text_changed(new_text: String) -> void: + update_results_view() + + +func _on_replace_selected_button_pressed() -> void: + replace_results(true) + + +func _on_replace_all_button_pressed() -> void: + replace_results(false) + + +func _on_match_case_button_toggled(toggled_on: bool) -> void: + _on_search_button_pressed() + + +#endregion diff --git a/addons/dialogue_manager/components/find_in_files.gd.uid b/addons/dialogue_manager/components/find_in_files.gd.uid new file mode 100644 index 0000000..380a491 --- /dev/null +++ b/addons/dialogue_manager/components/find_in_files.gd.uid @@ -0,0 +1 @@ +uid://q368fmxxa8sd diff --git a/addons/dialogue_manager/components/find_in_files.tscn b/addons/dialogue_manager/components/find_in_files.tscn new file mode 100644 index 0000000..97fca24 --- /dev/null +++ b/addons/dialogue_manager/components/find_in_files.tscn @@ -0,0 +1,139 @@ +[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"] + +[ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"] +bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137) +corner_detail = 1 + +[node name="FindInFiles" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_3xicy") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"] +layout_mode = 2 +text = "Find:" + +[node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"] +unique_name_in_owner = true +layout_mode = 2 +clear_button_enabled = true + +[node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"] +layout_mode = 2 + +[node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Find all..." + +[node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Match case" + +[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Replace" + +[node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] +layout_mode = 2 +text = "Replace with:" + +[node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +clear_button_enabled = true + +[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] +layout_mode = 2 + +[node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Replace selected" + +[node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Replace all" + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +follow_focus = true + +[node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 0 + +[node name="ResultTemplate" type="HBoxContainer" parent="."] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 155.0 +offset_top = -74.0 +offset_right = 838.0 +offset_bottom = -51.0 + +[node name="CheckBox" type="CheckBox" parent="ResultTemplate"] +layout_mode = 2 + +[node name="Label" type="RichTextLabel" parent="ResultTemplate"] +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxFlat_owohg") +bbcode_enabled = true +text = "Result" +fit_content = true +scroll_active = false + +[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"] +[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"] +[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"] +[connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"] diff --git a/addons/dialogue_manager/components/search_and_replace.gd b/addons/dialogue_manager/components/search_and_replace.gd new file mode 100644 index 0000000..ceb4028 --- /dev/null +++ b/addons/dialogue_manager/components/search_and_replace.gd @@ -0,0 +1,218 @@ +@tool +extends VBoxContainer + + +signal open_requested() +signal close_requested() + + +const DialogueConstants = preload("../constants.gd") + + +@onready var input: LineEdit = $Search/Input +@onready var result_label: Label = $Search/ResultLabel +@onready var previous_button: Button = $Search/PreviousButton +@onready var next_button: Button = $Search/NextButton +@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox +@onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton +@onready var replace_panel: HBoxContainer = $Replace +@onready var replace_input: LineEdit = $Replace/Input +@onready var replace_button: Button = $Replace/ReplaceButton +@onready var replace_all_button: Button = $Replace/ReplaceAllButton + +# The code edit we will be affecting (for some reason exporting this didn't work) +var code_edit: CodeEdit: + set(next_code_edit): + code_edit = next_code_edit + code_edit.gui_input.connect(_on_text_edit_gui_input) + code_edit.text_changed.connect(_on_text_edit_text_changed) + get: + return code_edit + +var results: Array = [] +var result_index: int = -1: + set(next_result_index): + result_index = next_result_index + if results.size() > 0: + var r = results[result_index] + code_edit.set_caret_line(r[0]) + code_edit.select(r[0], r[1], r[0], r[1] + r[2]) + else: + result_index = -1 + if is_instance_valid(code_edit): + code_edit.deselect() + + result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() }) + get: + return result_index + + +func _ready() -> void: + apply_theme() + + input.placeholder_text = DialogueConstants.translate(&"search.placeholder") + previous_button.tooltip_text = DialogueConstants.translate(&"search.previous") + next_button.tooltip_text = DialogueConstants.translate(&"search.next") + match_case_button.text = DialogueConstants.translate(&"search.match_case") + $Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace") + replace_button.text = DialogueConstants.translate(&"search.replace") + replace_all_button.text = DialogueConstants.translate(&"search.replace_all") + $Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") + + self.result_index = -1 + + replace_panel.hide() + replace_button.disabled = true + replace_all_button.disabled = true + + hide() + + +func focus_line_edit() -> void: + input.grab_focus() + input.select_all() + + +func apply_theme() -> void: + if is_instance_valid(previous_button): + previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons") + if is_instance_valid(next_button): + next_button.icon = get_theme_icon("ArrowRight", "EditorIcons") + + +# Find text in the code +func search(text: String = "", default_result_index: int = 0) -> void: + results.clear() + + if text == "": + text = input.text + + var lines = code_edit.text.split("\n") + for line_number in range(0, lines.size()): + var line = lines[line_number] + + var column = find_in_line(line, text, 0) + while column > -1: + results.append([line_number, column, text.length()]) + column = find_in_line(line, text, column + 1) + + if results.size() > 0: + replace_button.disabled = false + replace_all_button.disabled = false + else: + replace_button.disabled = true + replace_all_button.disabled = true + + self.result_index = clamp(default_result_index, 0, results.size() - 1) + + +# Find text in a string and match case if requested +func find_in_line(line: String, text: String, from_index: int = 0) -> int: + if match_case_button.button_pressed: + return line.find(text, from_index) + else: + return line.findn(text, from_index) + + +#region Signals + + +func _on_text_edit_gui_input(event: InputEvent) -> void: + if event is InputEventKey and event.is_pressed(): + match event.as_text(): + "Ctrl+F", "Command+F": + open_requested.emit() + get_viewport().set_input_as_handled() + "Ctrl+Shift+R", "Command+Shift+R": + replace_check_button.set_pressed(true) + open_requested.emit() + get_viewport().set_input_as_handled() + + +func _on_text_edit_text_changed() -> void: + results.clear() + + +func _on_search_and_replace_theme_changed() -> void: + apply_theme() + + +func _on_input_text_changed(new_text: String) -> void: + search(new_text) + + +func _on_previous_button_pressed() -> void: + self.result_index = wrapi(result_index - 1, 0, results.size()) + + +func _on_next_button_pressed() -> void: + self.result_index = wrapi(result_index + 1, 0, results.size()) + + +func _on_search_and_replace_visibility_changed() -> void: + if is_instance_valid(input): + if visible: + input.grab_focus() + var selection = code_edit.get_selected_text() + if input.text == "" and selection != "": + input.text = selection + search(selection) + else: + search() + else: + input.text = "" + + +func _on_input_gui_input(event: InputEvent) -> void: + if event is InputEventKey and event.is_pressed(): + match event.as_text(): + "Enter": + search(input.text) + "Escape": + emit_signal("close_requested") + + +func _on_replace_button_pressed() -> void: + if result_index == -1: return + + # 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) + 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: + code_edit.text = code_edit.text.replace(input.text, replace_input.text) + else: + code_edit.text = code_edit.text.replacen(input.text, replace_input.text) + search() + code_edit.text_changed.emit() + + +func _on_replace_check_button_toggled(button_pressed: bool) -> void: + replace_panel.visible = button_pressed + if button_pressed: + replace_input.grab_focus() + + +func _on_input_focus_entered() -> void: + if results.size() == 0: + search() + else: + self.result_index = result_index + + +func _on_match_case_check_box_toggled(button_pressed: bool) -> void: + search() + + +#endregion diff --git a/addons/dialogue_manager/components/search_and_replace.gd.uid b/addons/dialogue_manager/components/search_and_replace.gd.uid new file mode 100644 index 0000000..66ec826 --- /dev/null +++ b/addons/dialogue_manager/components/search_and_replace.gd.uid @@ -0,0 +1 @@ +uid://cijsmjkq21cdq diff --git a/addons/dialogue_manager/components/search_and_replace.tscn b/addons/dialogue_manager/components/search_and_replace.tscn new file mode 100644 index 0000000..52721c4 --- /dev/null +++ b/addons/dialogue_manager/components/search_and_replace.tscn @@ -0,0 +1,87 @@ +[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"] + +[ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] + +[node name="SearchAndReplace" type="VBoxContainer"] +visible = false +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 31.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +script = ExtResource("1_8oj1f") + +[node name="Search" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="Input" type="LineEdit" parent="Search"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Text to search for" +metadata/_edit_use_custom_anchors = true + +[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"] +layout_mode = 2 +text = "Match case" + +[node name="VSeparator" type="VSeparator" parent="Search"] +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="Search"] +layout_mode = 2 +tooltip_text = "Previous" +flat = true + +[node name="ResultLabel" type="Label" parent="Search"] +layout_mode = 2 +text = "0 of 0" + +[node name="NextButton" type="Button" parent="Search"] +layout_mode = 2 +tooltip_text = "Next" +flat = true + +[node name="VSeparator2" type="VSeparator" parent="Search"] +layout_mode = 2 + +[node name="ReplaceCheckButton" type="CheckButton" parent="Search"] +layout_mode = 2 +text = "Replace" + +[node name="Replace" type="HBoxContainer" parent="."] +visible = false +layout_mode = 2 + +[node name="ReplaceLabel" type="Label" parent="Replace"] +layout_mode = 2 +text = "Replace with:" + +[node name="Input" type="LineEdit" parent="Replace"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ReplaceButton" type="Button" parent="Replace"] +layout_mode = 2 +disabled = true +text = "Replace" +flat = true + +[node name="ReplaceAllButton" type="Button" parent="Replace"] +layout_mode = 2 +disabled = true +text = "Replace all" +flat = true + +[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"] +[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"] +[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"] +[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"] +[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"] +[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"] +[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"] +[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"] +[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"] +[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"] +[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"] +[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"] +[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"] diff --git a/addons/dialogue_manager/components/title_list.gd b/addons/dialogue_manager/components/title_list.gd new file mode 100644 index 0000000..67cdcf0 --- /dev/null +++ b/addons/dialogue_manager/components/title_list.gd @@ -0,0 +1,69 @@ +@tool +extends VBoxContainer + +signal title_selected(title: String) + + +const DialogueConstants = preload("../constants.gd") + + +@onready var filter_edit: LineEdit = $FilterEdit +@onready var list: ItemList = $List + +var titles: PackedStringArray: + set(next_titles): + titles = next_titles + apply_filter() + get: + return titles + +var filter: String: + set(next_filter): + filter = next_filter + apply_filter() + get: + return filter + + +func _ready() -> void: + apply_theme() + + filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter") + + +func select_title(title: String) -> void: + list.deselect_all() + for i in range(0, list.get_item_count()): + if list.get_item_text(i) == title.strip_edges(): + list.select(i) + + +func apply_filter() -> void: + list.clear() + for title in titles: + if filter == "" or filter.to_lower() in title.to_lower(): + list.add_item(title.strip_edges()) + + +func apply_theme() -> void: + if is_instance_valid(filter_edit): + filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) + + +### Signals + + +func _on_theme_changed() -> void: + apply_theme() + + +func _on_filter_edit_text_changed(new_text: String) -> void: + self.filter = new_text + + +func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void: + if mouse_button_index == MOUSE_BUTTON_LEFT: + var title = list.get_item_text(index) + title_selected.emit(title) diff --git a/addons/dialogue_manager/components/title_list.gd.uid b/addons/dialogue_manager/components/title_list.gd.uid new file mode 100644 index 0000000..325ca61 --- /dev/null +++ b/addons/dialogue_manager/components/title_list.gd.uid @@ -0,0 +1 @@ +uid://d0k2wndjj0ifm diff --git a/addons/dialogue_manager/components/title_list.tscn b/addons/dialogue_manager/components/title_list.tscn new file mode 100644 index 0000000..ac2b983 --- /dev/null +++ b/addons/dialogue_manager/components/title_list.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"] + +[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"] + +[node name="TitleList" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_5qqmd") + +[node name="FilterEdit" type="LineEdit" parent="."] +layout_mode = 2 +placeholder_text = "Filter titles" +clear_button_enabled = true + +[node name="List" type="ItemList" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +allow_reselect = true + +[connection signal="theme_changed" from="." to="." method="_on_theme_changed"] +[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"] +[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"] diff --git a/addons/dialogue_manager/components/update_button.gd b/addons/dialogue_manager/components/update_button.gd new file mode 100644 index 0000000..cf3f1b9 --- /dev/null +++ b/addons/dialogue_manager/components/update_button.gd @@ -0,0 +1,125 @@ +@tool +extends Button + +const DialogueConstants = preload("../constants.gd") +const DialogueSettings = preload("../settings.gd") + +const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases" + + +@onready var http_request: HTTPRequest = $HTTPRequest +@onready var download_dialog: AcceptDialog = $DownloadDialog +@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel +@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog +@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog +@onready var timer: Timer = $Timer + +var needs_reload: bool = false + +# A lambda that gets called just before refreshing the plugin. Return false to stop the reload. +var on_before_refresh: Callable = func(): return true + + +func _ready() -> void: + hide() + apply_theme() + + # Check for updates on GitHub + check_for_update() + + # Check again every few hours + timer.start(60 * 60 * 12) + + +# Convert a version number to an actually comparable number +func version_to_number(version: String) -> int: + var bits = version.split(".") + return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int() + + +func apply_theme() -> void: + var color: Color = get_theme_color("success_color", "Editor") + + if needs_reload: + color = get_theme_color("error_color", "Editor") + icon = get_theme_icon("Reload", "EditorIcons") + add_theme_color_override("icon_normal_color", color) + add_theme_color_override("icon_focus_color", color) + add_theme_color_override("icon_hover_color", color) + + add_theme_color_override("font_color", color) + add_theme_color_override("font_focus_color", color) + add_theme_color_override("font_hover_color", color) + + +func check_for_update() -> void: + if DialogueSettings.get_user_value("check_for_updates", true): + http_request.request(REMOTE_RELEASES_URL) + + +### Signals + + +func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: return + + var current_version: String = Engine.get_meta("DialogueManagerPlugin").get_version() + + # Work out the next version from the releases information on GitHub + var response = JSON.parse_string(body.get_string_from_utf8()) + if typeof(response) != TYPE_ARRAY: return + + # GitHub releases are in order of creation, not order of version + var versions = (response as Array).filter(func(release): + var version: String = release.tag_name.substr(1) + var major_version: int = version.split(".")[0].to_int() + var current_major_version: int = current_version.split(".")[0].to_int() + return major_version == current_major_version and version_to_number(version) > version_to_number(current_version) + ) + if versions.size() > 0: + download_update_panel.next_version_release = versions[0] + text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) }) + show() + + +func _on_update_button_pressed() -> void: + if needs_reload: + var will_refresh = on_before_refresh.call() + if will_refresh: + EditorInterface.restart_editor(true) + else: + var scale: float = EditorInterface.get_editor_scale() + download_dialog.min_size = Vector2(300, 250) * scale + download_dialog.popup_centered() + + +func _on_download_dialog_close_requested() -> void: + download_dialog.hide() + + +func _on_download_update_panel_updated(updated_to_version: String) -> void: + download_dialog.hide() + + needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload") + needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button") + needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button") + needs_reload_dialog.popup_centered() + + needs_reload = true + text = DialogueConstants.translate(&"update.reload_project") + apply_theme() + + +func _on_download_update_panel_failed() -> void: + download_dialog.hide() + update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed") + update_failed_dialog.popup_centered() + + +func _on_needs_reload_dialog_confirmed() -> void: + EditorInterface.restart_editor(true) + + +func _on_timer_timeout() -> void: + if not needs_reload: + check_for_update() diff --git a/addons/dialogue_manager/components/update_button.gd.uid b/addons/dialogue_manager/components/update_button.gd.uid new file mode 100644 index 0000000..9981132 --- /dev/null +++ b/addons/dialogue_manager/components/update_button.gd.uid @@ -0,0 +1 @@ +uid://cr1tt12dh5ecr diff --git a/addons/dialogue_manager/components/update_button.tscn b/addons/dialogue_manager/components/update_button.tscn new file mode 100644 index 0000000..6cff347 --- /dev/null +++ b/addons/dialogue_manager/components/update_button.tscn @@ -0,0 +1,42 @@ +[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"] + +[ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"] +[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"] + +[node name="UpdateButton" type="Button"] +visible = false +offset_right = 8.0 +offset_bottom = 8.0 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_hover_color = Color(0, 0, 0, 1) +theme_override_colors/font_focus_color = Color(0, 0, 0, 1) +text = "v2.9.0 available" +flat = true +script = ExtResource("1_d2tpb") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="DownloadDialog" type="AcceptDialog" parent="."] +title = "Download update" +size = Vector2i(400, 300) +unresizable = true +min_size = Vector2i(300, 250) +ok_button_text = "Close" + +[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")] + +[node name="UpdateFailedDialog" type="AcceptDialog" parent="."] +dialog_text = "You have been updated to version 2.4.3" + +[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."] + +[node name="Timer" type="Timer" parent="."] +wait_time = 14400.0 + +[connection signal="pressed" from="." to="." method="_on_update_button_pressed"] +[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] +[connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"] +[connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"] +[connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"] +[connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"] +[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"] diff --git a/addons/dialogue_manager/constants.gd b/addons/dialogue_manager/constants.gd new file mode 100644 index 0000000..56b5f02 --- /dev/null +++ b/addons/dialogue_manager/constants.gd @@ -0,0 +1,231 @@ +class_name DMConstants extends RefCounted + + +const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json" +const CACHE_PATH = "user://dialogue_manager_cache.json" + + +enum MutationBehaviour { + Wait, + DoNotWait, + Skip +} + +enum TranslationSource { + None, + Guess, + CSV, + PO +} + +# Token types + +const TOKEN_FUNCTION = &"function" +const TOKEN_DICTIONARY_REFERENCE = &"dictionary_reference" +const TOKEN_DICTIONARY_NESTED_REFERENCE = &"dictionary_nested_reference" +const TOKEN_GROUP = &"group" +const TOKEN_ARRAY = &"array" +const TOKEN_DICTIONARY = &"dictionary" +const TOKEN_PARENS_OPEN = &"parens_open" +const TOKEN_PARENS_CLOSE = &"parens_close" +const TOKEN_BRACKET_OPEN = &"bracket_open" +const TOKEN_BRACKET_CLOSE = &"bracket_close" +const TOKEN_BRACE_OPEN = &"brace_open" +const TOKEN_BRACE_CLOSE = &"brace_close" +const TOKEN_COLON = &"colon" +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" +const TOKEN_NOT = &"not" +const TOKEN_AND_OR = &"and_or" +const TOKEN_STRING = &"string" +const TOKEN_NUMBER = &"number" +const TOKEN_VARIABLE = &"variable" +const TOKEN_COMMENT = &"comment" + +const TOKEN_VALUE = &"value" +const TOKEN_ERROR = &"error" + +# Line types + +const TYPE_UNKNOWN = &"" +const TYPE_IMPORT = &"import" +const TYPE_USING = &"using" +const TYPE_COMMENT = &"comment" +const TYPE_RESPONSE = &"response" +const TYPE_TITLE = &"title" +const TYPE_CONDITION = &"condition" +const TYPE_WHILE = &"while" +const TYPE_MATCH = &"match" +const TYPE_WHEN = &"when" +const TYPE_MUTATION = &"mutation" +const TYPE_GOTO = &"goto" +const TYPE_DIALOGUE = &"dialogue" +const TYPE_RANDOM = &"random" +const TYPE_ERROR = &"error" + +# Line IDs + +const ID_NULL = &"" +const ID_ERROR = &"error" +const ID_ERROR_INVALID_TITLE = &"invalid title" +const ID_ERROR_TITLE_HAS_NO_BODY = &"title has no body" +const ID_END = &"end" +const ID_END_CONVERSATION = &"end!" + +# Errors + +const ERR_ERRORS_IN_IMPORTED_FILE = 100 +const ERR_FILE_ALREADY_IMPORTED = 101 +const ERR_DUPLICATE_IMPORT_NAME = 102 +const ERR_EMPTY_TITLE = 103 +const ERR_DUPLICATE_TITLE = 104 +const ERR_TITLE_INVALID_CHARACTERS = 106 +const ERR_UNKNOWN_TITLE = 107 +const ERR_INVALID_TITLE_REFERENCE = 108 +const ERR_TITLE_REFERENCE_HAS_NO_CONTENT = 109 +const ERR_INVALID_EXPRESSION = 110 +const ERR_UNEXPECTED_CONDITION = 111 +const ERR_DUPLICATE_ID = 112 +const ERR_MISSING_ID = 113 +const ERR_INVALID_INDENTATION = 114 +const ERR_INVALID_CONDITION_INDENTATION = 115 +const ERR_INCOMPLETE_EXPRESSION = 116 +const ERR_INVALID_EXPRESSION_FOR_VALUE = 117 +const ERR_UNKNOWN_LINE_SYNTAX = 118 +const ERR_TITLE_BEGINS_WITH_NUMBER = 119 +const ERR_UNEXPECTED_END_OF_EXPRESSION = 120 +const ERR_UNEXPECTED_FUNCTION = 121 +const ERR_UNEXPECTED_BRACKET = 122 +const ERR_UNEXPECTED_CLOSING_BRACKET = 123 +const ERR_MISSING_CLOSING_BRACKET = 124 +const ERR_UNEXPECTED_OPERATOR = 125 +const ERR_UNEXPECTED_COMMA = 126 +const ERR_UNEXPECTED_COLON = 127 +const ERR_UNEXPECTED_DOT = 128 +const ERR_UNEXPECTED_BOOLEAN = 129 +const ERR_UNEXPECTED_STRING = 130 +const ERR_UNEXPECTED_NUMBER = 131 +const ERR_UNEXPECTED_VARIABLE = 132 +const ERR_INVALID_INDEX = 133 +const ERR_UNEXPECTED_ASSIGNMENT = 134 +const ERR_UNKNOWN_USING = 135 +const ERR_EXPECTED_WHEN_OR_ELSE = 136 +const ERR_ONLY_ONE_ELSE_ALLOWED = 137 +const ERR_WHEN_MUST_BELONG_TO_MATCH = 138 +const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 +const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 +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 +static func get_error_message(error: int) -> String: + match error: + ERR_ERRORS_IN_IMPORTED_FILE: + return translate(&"errors.import_errors") + ERR_FILE_ALREADY_IMPORTED: + return translate(&"errors.already_imported") + ERR_DUPLICATE_IMPORT_NAME: + return translate(&"errors.duplicate_import") + ERR_EMPTY_TITLE: + return translate(&"errors.empty_title") + ERR_DUPLICATE_TITLE: + return translate(&"errors.duplicate_title") + ERR_TITLE_INVALID_CHARACTERS: + return translate(&"errors.invalid_title_string") + ERR_TITLE_BEGINS_WITH_NUMBER: + return translate(&"errors.invalid_title_number") + ERR_UNKNOWN_TITLE: + return translate(&"errors.unknown_title") + ERR_INVALID_TITLE_REFERENCE: + return translate(&"errors.jump_to_invalid_title") + ERR_TITLE_REFERENCE_HAS_NO_CONTENT: + return translate(&"errors.title_has_no_content") + ERR_INVALID_EXPRESSION: + return translate(&"errors.invalid_expression") + ERR_UNEXPECTED_CONDITION: + return translate(&"errors.unexpected_condition") + ERR_DUPLICATE_ID: + return translate(&"errors.duplicate_id") + ERR_MISSING_ID: + return translate(&"errors.missing_id") + ERR_INVALID_INDENTATION: + return translate(&"errors.invalid_indentation") + ERR_INVALID_CONDITION_INDENTATION: + return translate(&"errors.condition_has_no_content") + ERR_INCOMPLETE_EXPRESSION: + return translate(&"errors.incomplete_expression") + ERR_INVALID_EXPRESSION_FOR_VALUE: + return translate(&"errors.invalid_expression_for_value") + ERR_FILE_NOT_FOUND: + return translate(&"errors.file_not_found") + ERR_UNEXPECTED_END_OF_EXPRESSION: + return translate(&"errors.unexpected_end_of_expression") + ERR_UNEXPECTED_FUNCTION: + return translate(&"errors.unexpected_function") + ERR_UNEXPECTED_BRACKET: + return translate(&"errors.unexpected_bracket") + ERR_UNEXPECTED_CLOSING_BRACKET: + return translate(&"errors.unexpected_closing_bracket") + ERR_MISSING_CLOSING_BRACKET: + return translate(&"errors.missing_closing_bracket") + ERR_UNEXPECTED_OPERATOR: + return translate(&"errors.unexpected_operator") + ERR_UNEXPECTED_COMMA: + return translate(&"errors.unexpected_comma") + ERR_UNEXPECTED_COLON: + return translate(&"errors.unexpected_colon") + ERR_UNEXPECTED_DOT: + return translate(&"errors.unexpected_dot") + ERR_UNEXPECTED_BOOLEAN: + return translate(&"errors.unexpected_boolean") + ERR_UNEXPECTED_STRING: + return translate(&"errors.unexpected_string") + ERR_UNEXPECTED_NUMBER: + return translate(&"errors.unexpected_number") + ERR_UNEXPECTED_VARIABLE: + return translate(&"errors.unexpected_variable") + ERR_INVALID_INDEX: + return translate(&"errors.invalid_index") + ERR_UNEXPECTED_ASSIGNMENT: + return translate(&"errors.unexpected_assignment") + ERR_UNKNOWN_USING: + return translate(&"errors.unknown_using") + ERR_EXPECTED_WHEN_OR_ELSE: + return translate(&"errors.expected_when_or_else") + ERR_ONLY_ONE_ELSE_ALLOWED: + return translate(&"errors.only_one_else_allowed") + ERR_WHEN_MUST_BELONG_TO_MATCH: + return translate(&"errors.when_must_belong_to_match") + ERR_CONCURRENT_LINE_WITHOUT_ORIGIN: + return translate(&"errors.concurrent_line_without_origin") + ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: + return translate(&"errors.goto_not_allowed_on_concurrect_lines") + 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 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/addons/dialogue_manager/constants.gd.uid b/addons/dialogue_manager/constants.gd.uid new file mode 100644 index 0000000..f431917 --- /dev/null +++ b/addons/dialogue_manager/constants.gd.uid @@ -0,0 +1 @@ +uid://b1oarbmjtyesf diff --git a/addons/dialogue_manager/dialogue_label.gd b/addons/dialogue_manager/dialogue_label.gd new file mode 100644 index 0000000..da07b45 --- /dev/null +++ b/addons/dialogue_manager/dialogue_label.gd @@ -0,0 +1,232 @@ +@icon("./assets/icon.svg") + +@tool + +## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue. +class_name DialogueLabel extends RichTextLabel + + +## Emitted for each letter typed out. +signal spoke(letter: String, letter_index: int, speed: float) + +## Emitted when typing paused for a `[wait]` +signal paused_typing(duration: float) + +## Emitted when the player skips the typing of dialogue. +signal skipped_typing() + +## Emitted when typing finishes. +signal finished_typing() + + +# The action to press to skip typing. +@export var skip_action: StringName = &"ui_cancel" + +## The speed with which the text types out. +@export var seconds_per_step: float = 0.02 + +## Automatically have a brief pause when these characters are encountered. +@export var pause_at_characters: String = ".?!" + +## Don't auto pause if the character after the pause is one of these. +@export var skip_pause_at_character_if_followed_by: String = ")\"" + +## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br] +## Abbreviations are limitted to 5 characters in length [br] +## Does not support multi-period abbreviations (ex. "p.m.") +@export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"] + +## The amount of time to pause when exposing a character present in `pause_at_characters`. +@export var seconds_per_pause_step: float = 0.3 + +var _already_mutated_indices: PackedInt32Array = [] + + +## The current line of dialogue. +var dialogue_line: + set(next_dialogue_line): + dialogue_line = next_dialogue_line + custom_minimum_size = Vector2.ZERO + text = "" + text = dialogue_line.text + get: + return dialogue_line + +## Whether the label is currently typing itself out. +var is_typing: bool = false: + set(value): + var is_finished: bool = is_typing != value and value == false + is_typing = value + if is_finished: + finished_typing.emit() + get: + return is_typing + +var _last_wait_index: int = -1 +var _last_mutation_index: int = -1 +var _waiting_seconds: float = 0 +var _is_awaiting_mutation: bool = false + + +func _process(delta: float) -> void: + if self.is_typing: + # Type out text + if visible_ratio < 1: + # See if we are waiting + if _waiting_seconds > 0: + _waiting_seconds = _waiting_seconds - delta + # If we are no longer waiting then keep typing + if _waiting_seconds <= 0: + _type_next(delta, _waiting_seconds) + else: + # Make sure any mutations at the end of the line get run + _mutate_inline_mutations(get_total_character_count()) + self.is_typing = false + + +func _unhandled_input(event: InputEvent) -> void: + # Note: this will no longer be reached if using Dialogue Manager > 2.32.2. To make skip handling + # simpler (so all of mouse/keyboard/joypad are together) it is now the responsibility of the + # dialogue balloon. + if self.is_typing and visible_ratio < 1 and InputMap.has_action(skip_action) and event.is_action_pressed(skip_action): + get_viewport().set_input_as_handled() + skip_typing() + + +## Start typing out the text +func type_out() -> void: + text = dialogue_line.text + visible_characters = 0 + visible_ratio = 0 + _waiting_seconds = 0 + _last_wait_index = -1 + _last_mutation_index = -1 + _already_mutated_indices.clear() + + self.is_typing = true + + # Allow typing listeners a chance to connect + await get_tree().process_frame + + if get_total_character_count() == 0: + self.is_typing = false + elif seconds_per_step == 0: + _mutate_remaining_mutations() + visible_characters = get_total_character_count() + self.is_typing = false + + +## Stop typing out the text and jump right to the end +func skip_typing() -> void: + _mutate_remaining_mutations() + visible_characters = get_total_character_count() + self.is_typing = false + skipped_typing.emit() + + +# Type out the next character(s) +func _type_next(delta: float, seconds_needed: float) -> void: + if _is_awaiting_mutation: return + + if visible_characters == get_total_character_count(): + return + + if _last_mutation_index != visible_characters: + _last_mutation_index = visible_characters + _mutate_inline_mutations(visible_characters) + if _is_awaiting_mutation: return + + var additional_waiting_seconds: float = _get_pause(visible_characters) + + # Pause on characters like "." + if _should_auto_pause(): + additional_waiting_seconds += seconds_per_pause_step + + # Pause at literal [wait] directives + if _last_wait_index != visible_characters and additional_waiting_seconds > 0: + _last_wait_index = visible_characters + _waiting_seconds += additional_waiting_seconds + paused_typing.emit(_get_pause(visible_characters)) + else: + visible_characters += 1 + if visible_characters <= get_total_character_count(): + spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters)) + # See if there's time to type out some more in this frame + seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters)) + if seconds_needed > delta: + _waiting_seconds += seconds_needed + else: + _type_next(delta, seconds_needed) + + +# Get the pause for the current typing position if there is one +func _get_pause(at_index: int) -> float: + return dialogue_line.pauses.get(at_index, 0) + + +# Get the speed for the current typing position +func _get_speed(at_index: int) -> float: + var speed: float = 1 + for index in dialogue_line.speeds: + if index > at_index: + return speed + speed = dialogue_line.speeds[index] + return speed + + +# Run any inline mutations that haven't been run yet +func _mutate_remaining_mutations() -> void: + for i in range(visible_characters, get_total_character_count() + 1): + _mutate_inline_mutations(i) + + +# Run any mutations at the current typing position +func _mutate_inline_mutations(index: int) -> void: + for inline_mutation in dialogue_line.inline_mutations: + # inline mutations are an array of arrays in the form of [character index, resolvable function] + if inline_mutation[0] > index: + return + if inline_mutation[0] == index and not _already_mutated_indices.has(index): + _is_awaiting_mutation = true + # The DialogueManager can't be referenced directly here so we need to get it by its path + await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true) + _is_awaiting_mutation = false + + _already_mutated_indices.append(index) + + +# Determine if the current autopause character at the cursor should qualify to pause typing. +func _should_auto_pause() -> bool: + if visible_characters == 0: return false + + var parsed_text: String = get_parsed_text() + + # Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out + # Note: visible characters can be larger than parsed_text after a translation event + if visible_characters >= parsed_text.length(): return false + + # Ignore pause characters if they are next to a non-pause character + if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split(): + return false + + # Ignore "." if it's between two numbers + if visible_characters > 3 and parsed_text[visible_characters - 1] == ".": + var possible_number: String = parsed_text.substr(visible_characters - 2, 3) + if str(float(possible_number)).pad_decimals(1) == possible_number: + return false + + # Ignore "." if it's used in an abbreviation + # Note: does NOT support multi-period abbreviations (ex. p.m.) + if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".": + for abbreviation in skip_pause_at_abbreviations: + if visible_characters >= abbreviation.length(): + var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length()) + if previous_characters == abbreviation: + return false + + # Ignore two non-"." characters next to each other + var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split() + if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters: + return false + + return parsed_text[visible_characters - 1] in pause_at_characters.split() diff --git a/addons/dialogue_manager/dialogue_label.gd.uid b/addons/dialogue_manager/dialogue_label.gd.uid new file mode 100644 index 0000000..6bf86b1 --- /dev/null +++ b/addons/dialogue_manager/dialogue_label.gd.uid @@ -0,0 +1 @@ +uid://g32um0mltv5d diff --git a/addons/dialogue_manager/dialogue_label.tscn b/addons/dialogue_manager/dialogue_label.tscn new file mode 100644 index 0000000..0095933 --- /dev/null +++ b/addons/dialogue_manager/dialogue_label.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"] + +[ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"] + +[node name="DialogueLabel" type="RichTextLabel"] +anchors_preset = 10 +anchor_right = 1.0 +grow_horizontal = 2 +mouse_filter = 1 +bbcode_enabled = true +fit_content = true +scroll_active = false +shortcut_keys_enabled = false +meta_underlined = false +hint_underlined = false +deselect_on_focus_loss_enabled = false +visible_characters_behavior = 1 +script = ExtResource("1_cital") +skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex") diff --git a/addons/dialogue_manager/dialogue_line.gd b/addons/dialogue_manager/dialogue_line.gd new file mode 100644 index 0000000..7213854 --- /dev/null +++ b/addons/dialogue_manager/dialogue_line.gd @@ -0,0 +1,99 @@ +## A line of dialogue returned from [code]DialogueManager[/code]. +class_name DialogueLine extends RefCounted + + +## The ID of this line +var id: String + +## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code] +var type: String = DMConstants.TYPE_DIALOGUE + +## The next line ID after this line. +var next_id: String = "" + +## The character name that is saying this line. +var character: String = "" + +## A dictionary of variable replacements fo the character name. Generally for internal use only. +var character_replacements: Array[Dictionary] = [] + +## The dialogue being spoken. +var text: String = "" + +## A dictionary of replacements for the text. Generally for internal use only. +var text_replacements: Array[Dictionary] = [] + +## The key to use for translating this line. +var translation_key: String = "" + +## A map for when and for how long to pause while typing out the dialogue text. +var pauses: Dictionary = {} + +## A map for speed changes when typing out the dialogue text. +var speeds: Dictionary = {} + +## A map of any mutations to run while typing out the dialogue text. +var inline_mutations: Array[Array] = [] + +## A list of responses attached to this line of dialogue. +var responses: Array = [] + +## A list of lines that are spoken simultaneously with this one. +var concurrent_lines: Array[DialogueLine] = [] + +## A list of any extra game states to check when resolving variables and mutations. +var extra_game_states: Array = [] + +## How long to show this line before advancing to the next. Either a float (of seconds), [code]"auto"[/code], or [code]null[/code]. +var time: String = "" + +## Any #tags that were included in the line +var tags: PackedStringArray = [] + +## The mutation details if this is a mutation line (where [code]type == TYPE_MUTATION[/code]). +var mutation: Dictionary = {} + +## The conditions to check before including this line in the flow of dialogue. If failed the line will be skipped over. +var conditions: Dictionary = {} + + +func _init(data: Dictionary = {}) -> void: + if data.size() > 0: + id = data.id + next_id = data.next_id + type = data.type + extra_game_states = data.get("extra_game_states", []) + + match type: + DMConstants.TYPE_DIALOGUE: + character = data.character + character_replacements = data.get("character_replacements", [] as Array[Dictionary]) + text = data.text + text_replacements = data.get("text_replacements", [] as Array[Dictionary]) + translation_key = data.get("translation_key", data.text) + pauses = data.get("pauses", {}) + speeds = data.get("speeds", {}) + inline_mutations = data.get("inline_mutations", [] as Array[Array]) + time = data.get("time", "") + tags = data.get("tags", []) + concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine]) + + DMConstants.TYPE_MUTATION: + mutation = data.mutation + + +func _to_string() -> String: + match type: + DMConstants.TYPE_DIALOGUE: + return "" % [character, text] + DMConstants.TYPE_MUTATION: + return "" + return "" + + +func get_tag_value(tag_name: String) -> String: + var wrapped := "%s=" % tag_name + for t in tags: + if t.begins_with(wrapped): + return t.replace(wrapped, "").strip_edges() + return "" diff --git a/addons/dialogue_manager/dialogue_line.gd.uid b/addons/dialogue_manager/dialogue_line.gd.uid new file mode 100644 index 0000000..7ec7029 --- /dev/null +++ b/addons/dialogue_manager/dialogue_line.gd.uid @@ -0,0 +1 @@ +uid://rhuq0eyf8ar2 diff --git a/addons/dialogue_manager/dialogue_manager.gd b/addons/dialogue_manager/dialogue_manager.gd new file mode 100644 index 0000000..e073107 --- /dev/null +++ b/addons/dialogue_manager/dialogue_manager.gd @@ -0,0 +1,1483 @@ +extends Node + +const DialogueResource = preload("./dialogue_resource.gd") +const DialogueLine = preload("./dialogue_line.gd") +const DialogueResponse = preload("./dialogue_response.gd") + +const DMConstants = preload("./constants.gd") +const Builtins = preload("./utilities/builtins.gd") +const DMSettings = preload("./settings.gd") +const DMCompiler = preload("./compiler/compiler.gd") +const DMCompilerResult = preload("./compiler/compiler_result.gd") +const DMResolvedLineData = preload("./compiler/resolved_line_data.gd") + + +## Emitted when a dialogue balloon is created and dialogue starts +signal dialogue_started(resource: DialogueResource) + +## Emitted when a title is encountered while traversing dialogue, usually when jumping from a +## goto line +signal passed_title(title: String) + +## Emitted when a line of dialogue is encountered. +signal got_dialogue(line: DialogueLine) + +## Emitted when a mutation is encountered. +signal mutated(mutation: Dictionary) + +## Emitted when some dialogue has reached the end. +signal dialogue_ended(resource: DialogueResource) + +## Used internally. +signal bridge_get_next_dialogue_line_completed(line: DialogueLine) + +## Used internally +signal bridge_dialogue_started(resource: DialogueResource) + +## Used inernally +signal bridge_mutated() + + +## The list of globals that dialogue can query +var game_states: Array = [] + +## Allow dialogue to call singletons +var include_singletons: bool = true + +## Allow dialogue to call static methods/properties on classes +var include_classes: bool = true + +## Manage translation behaviour +var translation_source: DMConstants.TranslationSource = DMConstants.TranslationSource.Guess + +## Used to resolve the current scene. Override if your game manages the current scene itself. +var get_current_scene: Callable = func(): + var current_scene: Node = Engine.get_main_loop().current_scene + if current_scene == null: + current_scene = Engine.get_main_loop().root.get_child(Engine.get_main_loop().root.get_child_count() - 1) + return current_scene + +var _has_loaded_autoloads: bool = false +var _autoloads: Dictionary = {} + +var _node_properties: Array = [] +var _method_info_cache: Dictionary = {} + +var _dotnet_dialogue_manager: RefCounted + +var _expression_parser: DMExpressionParser = DMExpressionParser.new() + + +func _ready() -> void: + # Cache the known Node2D properties + _node_properties = ["Script Variables"] + var temp_node: Node2D = Node2D.new() + for property in temp_node.get_property_list(): + _node_properties.append(property.name) + temp_node.free() + + # Make the dialogue manager available as a singleton + if not Engine.has_singleton("DialogueManager"): + Engine.register_singleton("DialogueManager", self) + + +## Step through lines and run any mutations until we either hit some dialogue or the end of the conversation +func get_next_dialogue_line(resource: DialogueResource, key: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine: + # You have to provide a valid dialogue resource + if resource == null: + assert(false, DMConstants.translate(&"runtime.no_resource")) + if resource.lines.size() == 0: + assert(false, DMConstants.translate(&"runtime.no_content").format({ file_path = resource.resource_path })) + + # Inject any "using" states into the game_states + for state_name in resource.using_states: + var autoload = Engine.get_main_loop().root.get_node_or_null(state_name) + if autoload == null: + printerr(DMConstants.translate(&"runtime.unknown_autoload").format({ autoload = state_name })) + else: + extra_game_states = [autoload] + extra_game_states + + # Inject "self" into the extra game states. + extra_game_states = [{ "self": resource }] + extra_game_states + + # Get the line data + var dialogue: DialogueLine = await get_line(resource, key, extra_game_states) + + # If our dialogue is nothing then we hit the end + if not _is_valid(dialogue): + dialogue_ended.emit.call_deferred(resource) + return null + + # Run the mutation if it is one + if dialogue.type == DMConstants.TYPE_MUTATION: + var actual_next_id: String = dialogue.next_id.split("|")[0] + match mutation_behaviour: + DMConstants.MutationBehaviour.Wait: + await _mutate(dialogue.mutation, extra_game_states) + DMConstants.MutationBehaviour.DoNotWait: + _mutate(dialogue.mutation, extra_game_states) + DMConstants.MutationBehaviour.Skip: + pass + if actual_next_id in [DMConstants.ID_END_CONVERSATION, DMConstants.ID_NULL, null]: + # End the conversation + dialogue_ended.emit.call_deferred(resource) + return null + else: + return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour) + else: + got_dialogue.emit(dialogue) + return dialogue + + +## Get a line by its ID +func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine: + key = key.strip_edges() + + # See if we were given a stack instead of just the one key + var stack: Array = key.split("|") + key = stack.pop_front() + var id_trail: String = "" if stack.size() == 0 else "|" + "|".join(stack) + + # Key is blank so just use the first title (or start of file) + if key == null or key == "": + if resource.first_title.is_empty(): + key = resource.lines.keys()[0] + else: + key = resource.first_title + + # See if we just ended the conversation + if key in [DMConstants.ID_END, DMConstants.ID_NULL, null]: + if stack.size() > 0: + return await get_line(resource, "|".join(stack), extra_game_states) + else: + return null + elif key == DMConstants.ID_END_CONVERSATION: + return null + + # See if it is a title + if key.begins_with("~ "): + key = key.substr(2) + if resource.titles.has(key): + key = resource.titles.get(key) + + if key in resource.titles.values(): + passed_title.emit(resource.titles.find_key(key)) + + if not resource.lines.has(key): + assert(false, DMConstants.translate(&"errors.key_not_found").format({ key = key })) + + var data: Dictionary = resource.lines.get(key) + + # If next_id is an expression we need to resolve it. + if data.has(&"next_id_expression"): + data.next_id = await _resolve(data.next_id_expression, extra_game_states) + + # This title key points to another title key so we should jump there instead + if data.type == DMConstants.TYPE_TITLE and data.next_id in resource.titles.values(): + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + # Handle match statements + if data.type == DMConstants.TYPE_MATCH: + var value = await _resolve_condition_value(data, extra_game_states) + var else_cases: Array[Dictionary] = data.cases.filter(func(s): return s.has("is_else")) + var else_case: Dictionary = {} if else_cases.size() == 0 else else_cases.front() + var next_id: String = "" + for case in data.cases: + if case == else_case: + continue + elif await _check_case_value(value, case, extra_game_states): + next_id = case.next_id + break + # Nothing matched so check for else case + if next_id == "": + if not else_case.is_empty(): + next_id = else_case.next_id + else: + next_id = data.next_id_after + return await get_line(resource, next_id + id_trail, extra_game_states) + + # Check for weighted random lines. + if data.has(&"siblings"): + # Only count siblings that pass their condition (if they have one). + var successful_siblings: Array = data.siblings.filter(func(sibling): return not sibling.has("condition") or await _check_condition(sibling, extra_game_states)) + var target_weight: float = randf_range(0, successful_siblings.reduce(func(total, sibling): return total + sibling.weight, 0)) + var cummulative_weight: float = 0 + for sibling in successful_siblings: + if target_weight < cummulative_weight + sibling.weight: + data = resource.lines.get(sibling.id) + break + else: + cummulative_weight += sibling.weight + + # If this line is blank and it's the last line then check for returning snippets. + if data.type in [DMConstants.TYPE_COMMENT, DMConstants.TYPE_UNKNOWN]: + if data.next_id in [DMConstants.ID_END, DMConstants.ID_NULL, null]: + if stack.size() > 0: + return await get_line(resource, "|".join(stack), extra_game_states) + else: + return null + else: + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + # If the line is a random block then go to the start of the block. + elif data.type == DMConstants.TYPE_RANDOM: + data = resource.lines.get(data.next_id) + + # Check conditions. + elif data.type in [DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE]: + # "else" will have no actual condition. + if await _check_condition(data, extra_game_states): + return await get_line(resource, data.next_id + id_trail, extra_game_states) + elif data.has("next_sibling_id") and not data.next_sibling_id.is_empty(): + return await get_line(resource, data.next_sibling_id + id_trail, extra_game_states) + else: + return await get_line(resource, data.next_id_after + id_trail, extra_game_states) + + # Evaluate jumps. + elif data.type == DMConstants.TYPE_GOTO: + if data.is_snippet and not id_trail.begins_with("|" + data.next_id_after): + id_trail = "|" + data.next_id_after + id_trail + return await get_line(resource, data.next_id + id_trail, extra_game_states) + + elif data.type == DMConstants.TYPE_DIALOGUE: + if not data.has(&"id"): + data.id = key + + # Set up a line object. + var line: DialogueLine = await create_dialogue_line(data, extra_game_states) + + # 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 + # so instead we use set and get here. + line.set(&"responses", await _get_responses(data.get(&"responses", []), resource, id_trail, extra_game_states)) + return line + + # Inject the next node's responses if they have any. + if resource.lines.has(line.next_id): + var next_line: Dictionary = resource.lines.get(line.next_id) + + # If the response line is marked as a title then make sure to emit the passed_title signal. + if line.next_id in resource.titles.values(): + passed_title.emit(resource.titles.find_key(line.next_id)) + + # If the responses come from a snippet then we need to come back here afterwards. + if next_line.type == DMConstants.TYPE_GOTO and next_line.is_snippet and not id_trail.begins_with("|" + next_line.next_id_after): + id_trail = "|" + next_line.next_id_after + id_trail + + # If the next line is a title then check where it points to see if that is a set of responses. + while [DMConstants.TYPE_TITLE, DMConstants.TYPE_GOTO].has(next_line.type) and resource.lines.has(next_line.next_id): + next_line = resource.lines.get(next_line.next_id) + + if next_line != null and next_line.type == DMConstants.TYPE_RESPONSE: + # Note: For some reason C# has occasional issues with using the responses property directly + # so instead we use set and get here. + line.set(&"responses", await _get_responses(next_line.get(&"responses", []), resource, id_trail, extra_game_states)) + + line.next_id = "|".join(stack) if line.next_id == DMConstants.ID_NULL else line.next_id + id_trail + return line + +## Replace any variables, etc in the text. +func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> DMResolvedLineData: + var text: String = translate(data) + + # Resolve variables + 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: + # The replacement wasn't found but maybe the regular quotes have been replaced + # by special quotes while translating. + index = text.replace("“", "\"").replace("”", "\"").find(replacement.value_in_text) + if index > -1: + text = text.substr(0, index) + str(value) + text.substr(index + replacement.value_in_text.length()) + + var compilation: DMCompilation = DMCompilation.new() + + # Resolve random groups + for found in compilation.regex.INLINE_RANDOM_REGEX.search_all(text): + var options = found.get_string(&"options").split(&"|") + text = text.replace(&"[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) + + # Do a pass on the markers to find any conditionals + var markers: DMResolvedLineData = DMResolvedLineData.new(text) + + # Resolve any conditionals and update marker positions as needed + if data.type == DMConstants.TYPE_DIALOGUE: + var resolved_text: String = markers.text + var conditionals: Array[RegExMatch] = compilation.regex.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) + var replacements: Array = [] + for conditional in conditionals: + var condition_raw: String = conditional.strings[conditional.names.condition] + var body: String = conditional.strings[conditional.names.body] + var body_else: String = "" + if &"[else]" in body: + var bits = body.split(&"[else]") + body = bits[0] + body_else = bits[1] + var condition: Dictionary = compilation.extract_condition("if " + condition_raw, false, 0) + # If the condition fails then use the else of "" + if not await _check_condition({ condition = condition }, extra_game_states): + body = body_else + replacements.append({ + start = conditional.get_start(), + end = conditional.get_end(), + string = conditional.get_string(), + body = body + }) + + for i in range(replacements.size() - 1, -1, -1): + var r: Dictionary = replacements[i] + resolved_text = resolved_text.substr(0, r.start) + r.body + resolved_text.substr(r.end, 9999) + # Move any other markers now that the text has changed + var offset: int = r.end - r.start - r.body.length() + for key in [&"pauses", &"speeds", &"time"]: + if markers.get(key) == null: continue + var marker = markers.get(key) + var next_marker: Dictionary = {} + for index in marker: + if index < r.start: + next_marker[index] = marker[index] + elif index > r.start: + next_marker[index - offset] = marker[index] + markers.set(key, next_marker) + var mutations: Array[Array] = markers.mutations + var next_mutations: Array[Array] = [] + for mutation in mutations: + var index = mutation[0] + if index < r.start: + next_mutations.append(mutation) + elif index > r.start: + next_mutations.append([index - offset, mutation[1]]) + markers.mutations = next_mutations + + markers.text = resolved_text + + return markers + + +## Replace any variables, etc in the character name +func get_resolved_character(data: Dictionary, extra_game_states: Array = []) -> String: + var character: String = data.get(&"character", "") + + # Resolve variables + for replacement in data.get(&"character_replacements", []): + var value = await _resolve(replacement.expression.duplicate(true), extra_game_states) + var index: int = character.find(replacement.value_in_text) + if index > -1: + character = character.substr(0, index) + str(value) + character.substr(index + replacement.value_in_text.length()) + + # Resolve random groups + var random_regex: RegEx = RegEx.new() + random_regex.compile("\\[\\[(?.*?)\\]\\]") + for found in random_regex.search_all(character): + var options = found.get_string(&"options").split("|") + character = character.replace("[[%s]]" % found.get_string(&"options"), options[randi_range(0, options.size() - 1)]) + + return character + + +## Generate a dialogue resource on the fly from some text +func create_resource_from_text(text: String) -> Resource: + var result: DMCompilerResult = DMCompiler.compile_string(text, "") + + if result.errors.size() > 0: + printerr(DMConstants.translate(&"runtime.errors").format({ count = result.errors.size() })) + for error in result.errors: + printerr(DMConstants.translate(&"runtime.error_detail").format({ + line = error.line_number + 1, + message = DMConstants.get_error_message(error.error) + })) + assert(false, DMConstants.translate(&"runtime.errors_see_details").format({ count = result.errors.size() })) + + var resource: DialogueResource = DialogueResource.new() + resource.using_states = result.using_states + resource.titles = result.titles + resource.first_title = result.first_title + resource.character_names = result.character_names + resource.lines = result.lines + resource.raw_text = text + + return resource + + +#region Balloon helpers + + +## Show the example balloon +func show_example_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> CanvasLayer: + var balloon: Node = load(_get_example_balloon_path()).instantiate() + _start_balloon.call_deferred(balloon, resource, title, extra_game_states) + return balloon + + +## Show the configured dialogue balloon +func show_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> Node: + var balloon_path: String = DMSettings.get_setting(DMSettings.BALLOON_PATH, _get_example_balloon_path()) + if not ResourceLoader.exists(balloon_path): + balloon_path = _get_example_balloon_path() + return show_dialogue_balloon_scene(balloon_path, resource, title, extra_game_states) + + +## Show a given balloon scene +func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> Node: + if balloon_scene is String: + balloon_scene = load(balloon_scene) + if balloon_scene is PackedScene: + balloon_scene = balloon_scene.instantiate() + + var balloon: Node = balloon_scene + _start_balloon.call_deferred(balloon, resource, title, extra_game_states) + 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) + + if balloon.has_method(&"start"): + balloon.start(resource, title, extra_game_states) + elif balloon.has_method(&"Start"): + balloon.Start(resource, title, extra_game_states) + else: + assert(false, DMConstants.translate(&"runtime.dialogue_balloon_missing_start_method")) + + dialogue_started.emit(resource) + bridge_dialogue_started.emit(resource) + + +# Get the path to the example balloon +func _get_example_balloon_path() -> String: + var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 + var balloon_path: String = "/example_balloon/small_example_balloon.tscn" if is_small_window else "/example_balloon/example_balloon.tscn" + return get_script().resource_path.get_base_dir() + balloon_path + + +#endregion + +#region dotnet bridge + + +func _get_dotnet_dialogue_manager() -> RefCounted: + if not is_instance_valid(_dotnet_dialogue_manager): + _dotnet_dialogue_manager = load(get_script().resource_path.get_base_dir() + "/DialogueManager.cs").new() + return _dotnet_dialogue_manager + + +func _bridge_get_new_instance() -> Node: + # For some reason duplicating the node with its signals doesn't work so we have to copy them over manually + var instance = new() + for s: Dictionary in dialogue_started.get_connections(): + instance.dialogue_started.connect(s.callable) + for s: Dictionary in passed_title.get_connections(): + instance.passed_title.connect(s.callable) + for s: Dictionary in got_dialogue.get_connections(): + instance.got_dialogue.connect(s.callable) + for s: Dictionary in mutated.get_connections(): + instance.mutated.connect(s.callable) + for s: Dictionary in dialogue_ended.get_connections(): + instance.dialogue_ended.connect(s.callable) + instance.get_current_scene = get_current_scene + return instance + + +func _bridge_get_next_dialogue_line(resource: DialogueResource, key: String, extra_game_states: Array = []) -> void: + # dotnet needs at least one await tick of the signal gets called too quickly + await Engine.get_main_loop().process_frame + + var line = await get_next_dialogue_line(resource, key, extra_game_states) + bridge_get_next_dialogue_line_completed.emit(line) + + +func _bridge_mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: + await _mutate(mutation, extra_game_states, is_inline_mutation) + bridge_mutated.emit() + + +#endregion + +#region Internal helpers + + +# Show a message or crash with error +func show_error_for_missing_state_value(message: String, will_show: bool = true) -> void: + if not will_show: return + + if DMSettings.get_setting(DMSettings.IGNORE_MISSING_STATE_VALUES, false): + push_error(message) + elif will_show: + # If you're here then you're missing a method or property in your game state. The error + # message down in the debugger will give you some more information. + assert(false, message) + + +# Translate a string +func translate(data: Dictionary) -> String: + 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) + + if translation_key == "" or translation_key == data.text: + return tr(data.text) + else: + # Line IDs work slightly differently depending on whether the translation came from a + # CSV or a PO file. CSVs use the line ID (or the line itself) as the translatable string + # whereas POs use the ID as context and the line itself as the translatable string. + match translation_source: + DMConstants.TranslationSource.PO: + return tr(data.text, StringName(translation_key)) + + DMConstants.TranslationSource.CSV: + return tr(translation_key) + + DMConstants.TranslationSource.Guess: + var translation_files: Array = ProjectSettings.get_setting(&"internationalization/locale/translations") + if translation_files.filter(func(f: String): return f.get_extension() in [&"po", &"mo"]).size() > 0: + # Assume PO + return tr(data.text, StringName(translation_key)) + else: + # Assume CSV + return tr(translation_key) + + return tr(translation_key) + + +# Create a line of dialogue +func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> DialogueLine: + match data.type: + DMConstants.TYPE_DIALOGUE: + var resolved_data: DMResolvedLineData = await get_resolved_line_data(data, extra_game_states) + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DMConstants.TYPE_DIALOGUE, + next_id = data.next_id, + 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]), + translation_key = data.get(&"translation_key", data.text), + pauses = resolved_data.pauses, + speeds = resolved_data.speeds, + inline_mutations = resolved_data.mutations, + time = resolved_data.time, + tags = data.get(&"tags", []), + extra_game_states = extra_game_states + }) + + DMConstants.TYPE_RESPONSE: + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DMConstants.TYPE_RESPONSE, + next_id = data.next_id, + tags = data.get(&"tags", []), + extra_game_states = extra_game_states + }) + + DMConstants.TYPE_MUTATION: + return DialogueLine.new({ + id = data.get(&"id", ""), + type = DMConstants.TYPE_MUTATION, + next_id = data.next_id, + mutation = data.mutation, + extra_game_states = extra_game_states + }) + + return null + + +# Create a response +func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResponse: + var resolved_data: DMResolvedLineData = await get_resolved_line_data(data, extra_game_states) + return DialogueResponse.new({ + id = data.get(&"id", ""), + 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), + }) + + +# Get the current game states +func _get_game_states(extra_game_states: Array) -> Array: + if not _has_loaded_autoloads: + _has_loaded_autoloads = true + # Add any autoloads to a generic state so we can refer to them by name + for child in Engine.get_main_loop().root.get_children(): + # Ignore the dialogue manager + if child.name == &"DialogueManager": continue + # Ignore the current main scene + if Engine.get_main_loop().current_scene and child.name == Engine.get_main_loop().current_scene.name: continue + # Add the node to our known autoloads + _autoloads[child.name] = child + game_states = [_autoloads] + # Add any other state shortcuts from settings + for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) + if state: + game_states.append(state) + + var current_scene: Node = get_current_scene.call() + var unique_states: Array = [] + for state in extra_game_states + [current_scene] + game_states: + if state != null and not unique_states.has(state): + unique_states.append(state) + return unique_states + + +# Check if a condition is met +func _check_condition(data: Dictionary, extra_game_states: Array) -> bool: + return bool(await _resolve_condition_value(data, extra_game_states)) + + +# Resolve a condition's expression value +func _resolve_condition_value(data: Dictionary, extra_game_states: Array) -> Variant: + if data.get(&"condition", null) == null: return true + if data.condition.is_empty(): return true + + return await _resolve(data.condition.expression.duplicate(true), extra_game_states) + + +# Check if a match value matches a case value +func _check_case_value(match_value: Variant, data: Dictionary, extra_game_states: Array) -> bool: + if data.get(&"condition", null) == null: return true + if data.condition.is_empty(): return true + + var expression: Array[Dictionary] = data.condition.expression.duplicate(true) + + # 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)) + + 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 + + + +# Make a change to game state or run a method +func _mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: + var expression: Array[Dictionary] = mutation.expression + + # Handle built in mutations + if expression[0].type == DMConstants.TOKEN_FUNCTION and expression[0].function in [&"wait", &"Wait", &"debug", &"Debug"]: + var args: Array = await _resolve_each(expression[0].value, extra_game_states) + match expression[0].function: + &"wait", &"Wait": + mutated.emit(mutation.merged({ is_inline = is_inline_mutation })) + await Engine.get_main_loop().create_timer(float(args[0])).timeout + return + + &"debug", &"Debug": + prints("Debug:", args) + await Engine.get_main_loop().process_frame + + # Or pass through to the resolver + else: + if not _mutation_contains_assignment(mutation.expression) and not is_inline_mutation: + mutated.emit(mutation.merged({ is_inline = is_inline_mutation })) + + if mutation.get("is_blocking", true): + await _resolve(mutation.expression.duplicate(true), extra_game_states) + return + else: + _resolve(mutation.expression.duplicate(true), extra_game_states) + + # Wait one frame to give the dialogue handler a chance to yield + await Engine.get_main_loop().process_frame + + +# Check if a mutation contains an assignment token. +func _mutation_contains_assignment(mutation: Array) -> bool: + for token in mutation: + if token.type == DMConstants.TOKEN_ASSIGNMENT: + return true + return false + + +# Replace an array of line IDs with their response prompts +func _get_responses(ids: Array, resource: DialogueResource, id_trail: String, extra_game_states: Array) -> Array[DialogueResponse]: + var responses: Array[DialogueResponse] = [] + for id in ids: + var data: Dictionary = resource.lines.get(id).duplicate(true) + data.is_allowed = await _check_condition(data, extra_game_states) + var response: DialogueResponse = await create_response(data, extra_game_states) + response.next_id += id_trail + responses.append(response) + + return responses + + +# Get a value on the current scene or game state +func _get_state_value(property: String, extra_game_states: Array): + # Special case for static primitive calls + if property == "Color": + return Color() + elif property == "Vector2": + return Vector2.ZERO + elif property == "Vector3": + return Vector3.ZERO + elif property == "Vector4": + return Vector4.ZERO + elif property == "Quaternion": + return Quaternion() + + var expression = Expression.new() + if expression.parse(property) != OK: + assert(false, DMConstants.translate(&"runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() })) + + # Warn about possible name collisions + _warn_about_state_name_collisions(property, extra_game_states) + + for state in _get_game_states(extra_game_states): + if typeof(state) == TYPE_DICTIONARY: + if state.has(property): + return state.get(property) + 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 + + if include_singletons and Engine.has_singleton(property): + return Engine.get_singleton(property) + + if include_classes: + for class_data in ProjectSettings.get_global_class_list(): + if class_data.get(&"class") == property: + return load(class_data.path).new() + + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) + + +# Print warnings for top-level state name collisions. +func _warn_about_state_name_collisions(target_key: String, extra_game_states: Array) -> void: + # Don't run the check if this is a release build + if not OS.is_debug_build(): return + # Also don't run if the setting is off + if not DMSettings.get_setting(DMSettings.WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS, false): return + + # Get the list of state shortcuts. + var state_shortcuts: Array = [] + for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) + if state: + state_shortcuts.append(state) + + # Check any top level names for a collision + var states_with_key: Array = [] + for state in extra_game_states + [get_current_scene.call()] + state_shortcuts: + if state is Dictionary: + if state.keys().has(target_key): + states_with_key.append("Dictionary") + else: + var script: Script = (state as Object).get_script() + if script == null: + continue + + for method in script.get_script_method_list(): + if method.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + for property in script.get_script_property_list(): + if property.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + for signal_info in script.get_script_signal_list(): + if signal_info.name == target_key and not states_with_key.has(state.name): + states_with_key.append(state.name) + break + + if states_with_key.size() > 1: + push_warning(DMConstants.translate(&"runtime.top_level_states_share_name").format({ states = ", ".join(states_with_key), key = target_key })) + + +# Set a value on the current scene or game state +func _set_state_value(property: String, value, extra_game_states: Array) -> void: + for state in _get_game_states(extra_game_states): + if typeof(state) == TYPE_DICTIONARY: + if state.has(property): + state[property] = value + return + elif _thing_has_property(state, property): + state.set(property, value) + return + + if property.to_snake_case() != property: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found_missing_export").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.property_not_found").format({ property = property, states = _get_state_shortcut_names(extra_game_states) })) + + +# Get the list of state shortcut names +func _get_state_shortcut_names(extra_game_states: Array) -> String: + var states = _get_game_states(extra_game_states) + states.erase(_autoloads) + return ", ".join(states.map(func(s): return "\"%s\"" % (s.name if "name" in s else s))) + + +# Resolve an array of expressions. +func _resolve_each(array: Array, extra_game_states: Array) -> Array: + var results: Array = [] + for item in array: + if not item[0].type in [DMConstants.TOKEN_BRACE_CLOSE, DMConstants.TOKEN_BRACKET_CLOSE, DMConstants.TOKEN_PARENS_CLOSE]: + results.append(await _resolve(item.duplicate(true), extra_game_states)) + return results + + +# Collapse any expressions +func _resolve(tokens: Array, extra_game_states: Array): + var i: int = 0 + var limit: int = 0 + + # Handle groups first + for token in tokens: + if token.type == DMConstants.TOKEN_GROUP: + token.type = DMConstants.TOKEN_VALUE + token.value = await _resolve(token.value, extra_game_states) + + # Then variables/methods + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + + if token.type == 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: + # If we are calling a deeper function then we need to collapse the + # value into the thing we are calling the function on + var caller: Dictionary = tokens[i - 2] + if Builtins.is_supported(caller.value): + caller.type = DMConstants.TOKEN_VALUE + caller.value = Builtins.resolve_method(caller.value, function_name, args) + tokens.remove_at(i) + tokens.remove_at(i - 1) + i -= 2 + elif _thing_has_method(caller.value, function_name, args): + caller.type = DMConstants.TOKEN_VALUE + caller.value = await _resolve_thing_method(caller.value, function_name, args) + tokens.remove_at(i) + tokens.remove_at(i - 1) + i -= 2 + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.method_not_callable").format({ method = function_name, object = str(caller.value) })) + else: + var found: bool = false + match function_name: + &"str": + token.type = DMConstants.TOKEN_VALUE + token.value = str(args[0]) + found = true + &"Vector2": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector2(args[0], args[1]) + found = true + &"Vector2i": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector2i(args[0], args[1]) + found = true + &"Vector3": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector3(args[0], args[1], args[2]) + found = true + &"Vector3i": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector3i(args[0], args[1], args[2]) + found = true + &"Vector4": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector4(args[0], args[1], args[2], args[3]) + found = true + &"Vector4i": + token.type = DMConstants.TOKEN_VALUE + token.value = Vector4i(args[0], args[1], args[2], args[3]) + found = true + &"Quaternion": + token.type = DMConstants.TOKEN_VALUE + token.value = Quaternion(args[0], args[1], args[2], args[3]) + found = true + &"Callable": + token.type = DMConstants.TOKEN_VALUE + match args.size(): + 0: + token.value = Callable() + 1: + token.value = Callable(args[0]) + 2: + token.value = Callable(args[0], args[1]) + found = true + &"Color": + token.type = DMConstants.TOKEN_VALUE + match args.size(): + 0: + token.value = Color() + 1: + token.value = Color(args[0]) + 2: + token.value = Color(args[0], args[1]) + 3: + token.value = Color(args[0], args[1], args[2]) + 4: + token.value = Color(args[0], args[1], args[2], args[3]) + found = true + &"load", &"Load": + token.type = DMConstants.TOKEN_VALUE + token.value = load(args[0]) + found = true + &"roll_dice", &"RollDice": + token.type = DMConstants.TOKEN_VALUE + token.value = randi_range(1, args[0]) + found = true + _: + # Check for top level name conflicts + _warn_about_state_name_collisions(function_name, extra_game_states) + + for state in _get_game_states(extra_game_states): + if _thing_has_method(state, function_name, args): + token.type = DMConstants.TOKEN_VALUE + token.value = await _resolve_thing_method(state, function_name, args) + found = true + break + + show_error_for_missing_state_value(DMConstants.translate(&"runtime.method_not_found").format({ + method = args[0] if function_name in ["call", "call_deferred"] else function_name, + states = _get_state_shortcut_names(extra_game_states) + }), not found) + + elif token.type == DMConstants.TOKEN_DICTIONARY_REFERENCE: + var value + if i > 0 and tokens[i - 1].type == DMConstants.TOKEN_DOT: + # If we are deep referencing then we need to get the parent object. + # `parent.value` is the actual object and `token.variable` is the name of + # the property within it. + value = tokens[i - 2].value[token.variable] + # Clean up the previous tokens + token.erase("variable") + tokens.remove_at(i - 1) + tokens.remove_at(i - 2) + i -= 2 + else: + # Otherwise we can just get this variable as a normal state reference + value = _get_state_value(token.variable, extra_game_states) + + var index = await _resolve(token.value, extra_game_states) + if typeof(value) == TYPE_DICTIONARY: + if tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + token.type = "dictionary" + token.value = value + token.key = index + else: + if value.has(index): + token.type = DMConstants.TOKEN_VALUE + token.value = value[index] + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = token.variable })) + elif typeof(value) == TYPE_ARRAY: + if tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + token.type = "array" + token.value = value + token.key = index + else: + if index >= 0 and index < value.size(): + token.type = DMConstants.TOKEN_VALUE + token.value = value[index] + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = token.variable })) + + elif token.type == DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: + var dictionary: Dictionary = tokens[i - 1] + var index = await _resolve(token.value, extra_game_states) + var value = dictionary.value + if typeof(value) == TYPE_DICTIONARY: + if tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + dictionary.type = "dictionary" + dictionary.key = index + dictionary.value = value + tokens.remove_at(i) + i -= 1 + else: + if dictionary.value.has(index): + dictionary.value = value.get(index) + tokens.remove_at(i) + i -= 1 + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.key_not_found").format({ key = str(index), dictionary = value })) + elif typeof(value) == TYPE_ARRAY: + if tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + dictionary.type = "array" + dictionary.value = value + dictionary.key = index + tokens.remove_at(i) + i -= 1 + else: + if index >= 0 and index < value.size(): + dictionary.value = value[index] + tokens.remove_at(i) + i -= 1 + else: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = index, array = value })) + + elif token.type == DMConstants.TOKEN_ARRAY: + token.type = DMConstants.TOKEN_VALUE + token.value = await _resolve_each(token.value, extra_game_states) + + elif token.type == DMConstants.TOKEN_DICTIONARY: + token.type = DMConstants.TOKEN_VALUE + var dictionary = {} + for key in token.value.keys(): + var resolved_key = await _resolve([key], extra_game_states) + var preresolved_value = token.value.get(key) + if typeof(preresolved_value) != TYPE_ARRAY: + preresolved_value = [preresolved_value] + var resolved_value = await _resolve(preresolved_value, extra_game_states) + dictionary[resolved_key] = resolved_value + token.value = dictionary + + elif token.type == DMConstants.TOKEN_VARIABLE or token.type == DMConstants.TOKEN_NUMBER: + if str(token.value) == "null": + token.type = DMConstants.TOKEN_VALUE + token.value = null + elif str(token.value) == "self": + token.type = DMConstants.TOKEN_VALUE + token.value = extra_game_states[0].self + elif tokens[i - 1].type == DMConstants.TOKEN_DOT: + var caller: Dictionary = tokens[i - 2] + var property = token.value + if tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # If the next token is an assignment then we need to leave this as a reference + # so that it can be resolved once everything ahead of it has been resolved + caller.type = "property" + caller.property = property + else: + # If we are requesting a deeper property then we need to collapse the + # value into the thing we are referencing from + caller.type = DMConstants.TOKEN_VALUE + if Builtins.is_supported(caller.value): + caller.value = Builtins.resolve_property(caller.value, property) + else: + caller.value = caller.value.get(property) + tokens.remove_at(i) + tokens.remove_at(i - 1) + i -= 2 + elif tokens.size() > i + 1 and tokens[i + 1].type == DMConstants.TOKEN_ASSIGNMENT: + # It's a normal variable but we will be assigning to it so don't resolve + # it until everything after it has been resolved + token.type = "variable" + else: + if token.type == DMConstants.TOKEN_NUMBER: + token.type = DMConstants.TOKEN_VALUE + token.value = token.value + else: + token.type = DMConstants.TOKEN_VALUE + token.value = _get_state_value(str(token.value), extra_game_states) + + i += 1 + + # Then multiply and divide + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]: + token.type = DMConstants.TOKEN_VALUE + token.value = _apply_operation(token.value, tokens[i - 1].value, tokens[i + 1].value) + tokens.remove_at(i + 1) + tokens.remove_at(i - 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + # Then addition and subtraction + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_OPERATOR and token.value in ["+", "-"]: + token.type = DMConstants.TOKEN_VALUE + token.value = _apply_operation(token.value, tokens[i - 1].value, tokens[i + 1].value) + tokens.remove_at(i + 1) + tokens.remove_at(i - 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + # Then negations + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_NOT: + token.type = DMConstants.TOKEN_VALUE + token.value = not tokens[i + 1].value + tokens.remove_at(i + 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + # Then comparisons + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_COMPARISON: + token.type = DMConstants.TOKEN_VALUE + token.value = _compare(token.value, tokens[i - 1].value, tokens[i + 1].value) + tokens.remove_at(i + 1) + tokens.remove_at(i - 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + # Then and/or + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_AND_OR: + token.type = DMConstants.TOKEN_VALUE + token.value = _apply_operation(token.value, tokens[i - 1].value, tokens[i + 1].value) + tokens.remove_at(i + 1) + tokens.remove_at(i - 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + # Lastly, resolve any assignments + i = 0 + limit = 0 + while i < tokens.size() and limit < 1000: + limit += 1 + var token: Dictionary = tokens[i] + if token.type == DMConstants.TOKEN_ASSIGNMENT: + var lhs: Dictionary = tokens[i - 1] + var value + + match lhs.type: + &"variable": + value = _apply_operation(token.value, _get_state_value(lhs.value, extra_game_states), tokens[i + 1].value) + _set_state_value(lhs.value, value, extra_game_states) + &"property": + value = _apply_operation(token.value, lhs.value.get(lhs.property), tokens[i + 1].value) + if typeof(lhs.value) == TYPE_DICTIONARY: + lhs.value[lhs.property] = value + else: + lhs.value.set(lhs.property, value) + &"dictionary": + value = _apply_operation(token.value, lhs.value.get(lhs.key, null), tokens[i + 1].value) + lhs.value[lhs.key] = value + &"array": + show_error_for_missing_state_value( + DMConstants.translate(&"runtime.array_index_out_of_bounds").format({ index = lhs.key, array = lhs.value }), + lhs.key >= lhs.value.size() + ) + value = _apply_operation(token.value, lhs.value[lhs.key], tokens[i + 1].value) + lhs.value[lhs.key] = value + _: + show_error_for_missing_state_value(DMConstants.translate(&"runtime.left_hand_size_cannot_be_assigned_to")) + + token.type = DMConstants.TOKEN_VALUE + token.value = value + tokens.remove_at(i + 1) + tokens.remove_at(i - 1) + i -= 1 + i += 1 + + if limit >= 1000: + assert(false, DMConstants.translate(&"runtime.something_went_wrong")) + + return tokens[0].value + + +# Compare two values. +func _compare(operator: String, first_value, second_value) -> bool: + match operator: + &"in": + if first_value == null or second_value == null: + return false + else: + return first_value in second_value + &"<": + if first_value == null: + return true + elif second_value == null: + return false + else: + return first_value < second_value + &">": + if first_value == null: + return false + elif second_value == null: + return true + else: + return first_value > second_value + &"<=": + if first_value == null: + return true + elif second_value == null: + return false + else: + return first_value <= second_value + &">=": + if first_value == null: + return false + elif second_value == null: + return true + else: + return first_value >= second_value + &"==": + if first_value == null: + if typeof(second_value) == TYPE_BOOL: + return second_value == false + else: + return second_value == null + else: + return first_value == second_value + &"!=": + if first_value == null: + if typeof(second_value) == TYPE_BOOL: + return second_value == true + else: + return second_value != null + else: + return first_value != second_value + + return false + + +# Apply an operation from one value to another. +func _apply_operation(operator: String, first_value, second_value): + match operator: + &"=": + return second_value + &"+", &"+=": + return first_value + second_value + &"-", &"-=": + return first_value - second_value + &"/", &"/=": + return first_value / second_value + &"*", &"*=": + return first_value * second_value + &"%": + return first_value % second_value + &"and": + return first_value and second_value + &"or": + return first_value or second_value + + assert(false, DMConstants.translate(&"runtime.unknown_operator")) + + +# Check if a dialogue line contains meaningful information. +func _is_valid(line: DialogueLine) -> bool: + if line == null: + return false + if line.type == DMConstants.TYPE_MUTATION and line.mutation == null: + return false + if line.type == DMConstants.TYPE_RESPONSE and line.get(&"responses").size() == 0: + return false + return true + + +# 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: + return false + + if method in [&"call", &"call_deferred"]: + return thing.has_method(args[0]) + + if method == &"emit_signal": + return thing.has_signal(args[0]) + + if thing.has_method(method): + return true + + 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) + + return false + + +# Check if a given property exists +func _thing_has_property(thing: Object, property: String) -> bool: + if thing == null: + return false + + for p in thing.get_property_list(): + if _node_properties.has(p.name): + # Ignore any properties on the base Node + continue + if p.name == property: + return true + + 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 + + +func _get_method_info_for(thing: Variant, method: String, args: Array) -> Dictionary: + # Use the thing instance id as a key for the caching dictionary. + var thing_instance_id: int = thing.get_instance_id() + if not _method_info_cache.has(thing_instance_id): + var methods: Dictionary = {} + for m in thing.get_method_list(): + methods["%s:%d" % [m.name, m.args.size()]] = m + if not methods.has(m.name): + methods[m.name] = m + _method_info_cache[thing_instance_id] = methods + + var methods: Dictionary = _method_info_cache.get(thing_instance_id, {}) + var method_key: String = "%s:%d" % [method, args.size()] + if methods.has(method_key): + return methods.get(method_key) + else: + return methods.get(method) + + +func _resolve_thing_method(thing, method: String, args: Array): + if Builtins.is_supported(thing): + var result = Builtins.resolve_method(thing, method, args) + if not Builtins.has_resolve_method_failed(): + return result + + if thing.has_method(method): + # Try to convert any literals to the right type + var method_info: Dictionary = _get_method_info_for(thing, method, args) + var method_args: Array = method_info.args + if method_info.flags & METHOD_FLAG_VARARG == 0 and method_args.size() < args.size(): + assert(false, DMConstants.translate(&"runtime.expected_n_got_n_args").format({ expected = method_args.size(), method = method, received = args.size()})) + for i in range(0, min(method_args.size(), args.size())): + var m: Dictionary = method_args[i] + var to_type: int = typeof(args[i]) + if m.type == TYPE_ARRAY: + match m.hint_string: + &"String": + to_type = TYPE_PACKED_STRING_ARRAY + &"int": + to_type = TYPE_PACKED_INT64_ARRAY + &"float": + to_type = TYPE_PACKED_FLOAT64_ARRAY + &"Vector2": + to_type = TYPE_PACKED_VECTOR2_ARRAY + &"Vector3": + to_type = TYPE_PACKED_VECTOR3_ARRAY + _: + if m.hint_string != "": + assert(false, DMConstants.translate(&"runtime.unsupported_array_type").format({ type = m.hint_string})) + if typeof(args[i]) != to_type: + args[i] = convert(args[i], to_type) + + return await thing.callv(method, args) + + # If we get here then it's probably a C# method with a Task return type + var dotnet_dialogue_manager = _get_dotnet_dialogue_manager() + dotnet_dialogue_manager.ResolveThingMethod(thing, method, args) + return await dotnet_dialogue_manager.Resolved diff --git a/addons/dialogue_manager/dialogue_manager.gd.uid b/addons/dialogue_manager/dialogue_manager.gd.uid new file mode 100644 index 0000000..d10762e --- /dev/null +++ b/addons/dialogue_manager/dialogue_manager.gd.uid @@ -0,0 +1 @@ +uid://c3rodes2l3gxb diff --git a/addons/dialogue_manager/dialogue_resource.gd b/addons/dialogue_manager/dialogue_resource.gd new file mode 100644 index 0000000..29ade7b --- /dev/null +++ b/addons/dialogue_manager/dialogue_resource.gd @@ -0,0 +1,42 @@ +@tool +@icon("./assets/icon.svg") + +## A collection of dialogue lines for use with [code]DialogueManager[/code]. +class_name DialogueResource extends Resource + + +const DialogueLine = preload("./dialogue_line.gd") + +## A list of state shortcuts +@export var using_states: PackedStringArray = [] + +## A map of titles and the lines they point to. +@export var titles: Dictionary = {} + +## A list of character names. +@export var character_names: PackedStringArray = [] + +## The first title in the file. +@export var first_title: String = "" + +## A map of the encoded lines of dialogue. +@export var lines: Dictionary = {} + +## raw version of the text +@export var raw_text: String + + +## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can +## be a title string or a stringified line number). Runs any mutations along the way and then returns +## the first dialogue line encountered. +func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine: + return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) + + +## Get the list of any titles found in the file. +func get_titles() -> PackedStringArray: + return titles.keys() + + +func _to_string() -> String: + return "" % [",".join(titles.keys())] diff --git a/addons/dialogue_manager/dialogue_resource.gd.uid b/addons/dialogue_manager/dialogue_resource.gd.uid new file mode 100644 index 0000000..27b95d0 --- /dev/null +++ b/addons/dialogue_manager/dialogue_resource.gd.uid @@ -0,0 +1 @@ +uid://dbs4435dsf3ry diff --git a/addons/dialogue_manager/dialogue_response.gd b/addons/dialogue_manager/dialogue_response.gd new file mode 100644 index 0000000..479b81c --- /dev/null +++ b/addons/dialogue_manager/dialogue_response.gd @@ -0,0 +1,63 @@ +## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code]. +class_name DialogueResponse extends RefCounted + + +## The ID of this response +var id: String + +## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code]. +var type: String = DMConstants.TYPE_RESPONSE + +## The next line ID to use if this response is selected by the player. +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 = "" + +## A dictionary of varialbe replaces for the character name. Generally for internal use only. +var character_replacements: Array[Dictionary] = [] + +## The prompt for this response. +var text: String = "" + +## A dictionary of variable replaces for the text. Generally for internal use only. +var text_replacements: Array[Dictionary] = [] + +## Any #tags +var tags: PackedStringArray = [] + +## The key to use for translating the text. +var translation_key: String = "" + + +func _init(data: Dictionary = {}) -> void: + if data.size() > 0: + id = data.id + type = data.type + next_id = data.next_id + is_allowed = data.is_allowed + character = data.character + character_replacements = data.character_replacements + text = data.text + text_replacements = data.text_replacements + tags = data.tags + translation_key = data.translation_key + condition_as_text = data.condition_as_text + + +func _to_string() -> String: + return "" % text + + +func get_tag_value(tag_name: String) -> String: + var wrapped := "%s=" % tag_name + for t in tags: + if t.begins_with(wrapped): + return t.replace(wrapped, "").strip_edges() + return "" diff --git a/addons/dialogue_manager/dialogue_response.gd.uid b/addons/dialogue_manager/dialogue_response.gd.uid new file mode 100644 index 0000000..9b4532a --- /dev/null +++ b/addons/dialogue_manager/dialogue_response.gd.uid @@ -0,0 +1 @@ +uid://cm0xpfeywpqid diff --git a/addons/dialogue_manager/dialogue_responses_menu.gd b/addons/dialogue_manager/dialogue_responses_menu.gd new file mode 100644 index 0000000..0ae2c50 --- /dev/null +++ b/addons/dialogue_manager/dialogue_responses_menu.gd @@ -0,0 +1,147 @@ +@icon("./assets/responses_menu.svg") + +## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b]. +class_name DialogueResponsesMenu extends Container + + +## Emitted when a response is selected. +signal response_selected(response) + + +## Optionally specify a control to duplicate for each response +@export var response_template: Control + +## The action for accepting a response (is possibly overridden by parent dialogue balloon). +@export var next_action: StringName = &"" + +## Hide any responses where [code]is_allowed[/code] is false +@export var hide_failed_responses: bool = false + +## The list of dialogue responses. +var responses: Array = []: + get: + return responses + set(value): + responses = value + + # Remove any current items + for item in get_children(): + if item == response_template: continue + + remove_child(item) + item.queue_free() + + # Add new items + if responses.size() > 0: + for response in responses: + if hide_failed_responses and not response.is_allowed: continue + + var item: Control + if is_instance_valid(response_template): + item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) + item.show() + else: + item = Button.new() + item.name = "Response%d" % get_child_count() + if not response.is_allowed: + item.name = item.name + &"Disallowed" + item.disabled = true + + # If the item has a response property then use that + if "response" in item: + item.response = response + # Otherwise assume we can just set the text + else: + item.text = response.text + + item.set_meta("response", response) + + add_child(item) + + _configure_focus() + + +func _ready() -> void: + visibility_changed.connect(func(): + if visible and get_menu_items().size() > 0: + var first_item: Control = get_menu_items()[0] + if first_item.is_inside_tree(): + first_item.grab_focus() + ) + + if is_instance_valid(response_template): + response_template.hide() + + +## Get the selectable items in the menu. +func get_menu_items() -> Array: + var items: Array = [] + for child in get_children(): + if not child.visible: continue + if "Disallowed" in child.name: continue + items.append(child) + + return items + + +#region Internal + + +# Prepare the menu for keyboard and mouse navigation. +func _configure_focus() -> void: + var items = get_menu_items() + for i in items.size(): + var item: Control = items[i] + + item.focus_mode = Control.FOCUS_ALL + + item.focus_neighbor_left = item.get_path() + item.focus_neighbor_right = item.get_path() + + 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)) + item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response"))) + + items[0].grab_focus() + + +#endregion + +#region Signals + + +func _on_response_mouse_entered(item: Control) -> void: + if "Disallowed" in item.name: return + + item.grab_focus() + + +func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: + if "Disallowed" in item.name: return + + if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: + get_viewport().set_input_as_handled() + response_selected.emit(response) + elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items(): + get_viewport().set_input_as_handled() + response_selected.emit(response) + + +#endregion diff --git a/addons/dialogue_manager/dialogue_responses_menu.gd.uid b/addons/dialogue_manager/dialogue_responses_menu.gd.uid new file mode 100644 index 0000000..0ae73d9 --- /dev/null +++ b/addons/dialogue_manager/dialogue_responses_menu.gd.uid @@ -0,0 +1 @@ +uid://bb52rsfwhkxbn diff --git a/addons/dialogue_manager/editor_translation_parser_plugin.gd b/addons/dialogue_manager/editor_translation_parser_plugin.gd new file mode 100644 index 0000000..137ab70 --- /dev/null +++ b/addons/dialogue_manager/editor_translation_parser_plugin.gd @@ -0,0 +1,53 @@ +class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin + + +## Cached result of parsing a dialogue file. +var data: DMCompilerResult +## List of characters that were added. +var translated_character_names: PackedStringArray = [] +var translated_lines: Array[Dictionary] = [] + + +func _parse_file(path: String) -> Array[PackedStringArray]: + var msgs: Array[PackedStringArray] = [] + var file: FileAccess = FileAccess.open(path, FileAccess.READ) + var text: String = file.get_as_text() + + data = DMCompiler.compile_string(text, path) + + var known_keys: PackedStringArray = PackedStringArray([]) + + # Add all character names if settings ask for it + if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true): + translated_character_names = [] as Array[DialogueLine] + for character_name: String in data.character_names: + if character_name in known_keys: continue + + known_keys.append(character_name) + + translated_character_names.append(character_name) + msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")])) + + # Add all dialogue lines and responses + var dialogue: Dictionary = data.lines + for key: String in dialogue.keys(): + var line: Dictionary = dialogue.get(key) + + if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue + + var translation_key: String = line.get(&"translation_key", line.text) + + if translation_key in known_keys: continue + + known_keys.append(translation_key) + translated_lines.append(line) + if translation_key == line.text: + msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")])) + else: + msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")])) + + return msgs + + +func _get_recognized_extensions() -> PackedStringArray: + return ["dialogue"] diff --git a/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid b/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid new file mode 100644 index 0000000..22ddbe9 --- /dev/null +++ b/addons/dialogue_manager/editor_translation_parser_plugin.gd.uid @@ -0,0 +1 @@ +uid://c6bya881h1egb diff --git a/addons/dialogue_manager/example_balloon/ExampleBalloon.cs b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs new file mode 100644 index 0000000..980f067 --- /dev/null +++ b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs @@ -0,0 +1,223 @@ +using Godot; +using Godot.Collections; + +namespace DialogueManagerRuntime +{ + public partial class ExampleBalloon : CanvasLayer + { + [Export] public string NextAction = "ui_accept"; + [Export] public string SkipAction = "ui_cancel"; + + + Control balloon; + RichTextLabel characterLabel; + RichTextLabel dialogueLabel; + VBoxContainer responsesMenu; + + 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 = value; + ApplyDialogueLine(); + } + } + + Timer MutationCooldown = new Timer(); + + + public override void _Ready() + { + balloon = GetNode("%Balloon"); + characterLabel = GetNode("%CharacterLabel"); + dialogueLabel = GetNode("%DialogueLabel"); + responsesMenu = GetNode("%ResponsesMenu"); + + balloon.Hide(); + + balloon.GuiInput += (@event) => + { + if ((bool)dialogueLabel.Get("is_typing")) + { + bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed(); + bool skipButtonWasPressed = @event.IsActionPressed(SkipAction); + if (mouseWasClicked || skipButtonWasPressed) + { + GetViewport().SetInputAsHandled(); + dialogueLabel.Call("skip_typing"); + return; + } + } + + if (!isWaitingForInput) return; + if (dialogueLine.Responses.Count > 0) return; + + GetViewport().SetInputAsHandled(); + + if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left) + { + Next(dialogueLine.NextId); + } + else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon) + { + Next(dialogueLine.NextId); + } + }; + + if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) + { + responsesMenu.Set("next_action", NextAction); + } + responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => + { + Next(response.NextId); + })); + + + // Hide the balloon when a mutation is running + MutationCooldown.Timeout += () => + { + if (willHideBalloon) + { + willHideBalloon = false; + balloon.Hide(); + } + }; + AddChild(MutationCooldown); + + DialogueManager.Mutated += OnMutated; + } + + + public override void _ExitTree() + { + DialogueManager.Mutated -= OnMutated; + } + + + public override void _UnhandledInput(InputEvent @event) + { + // Only the balloon is allowed to handle input while it's showing + GetViewport().SetInputAsHandled(); + } + + + public override async void _Notification(int what) + { + // Detect a change of locale and update the current dialogue line to show the new language + if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel)) + { + float visibleRatio = dialogueLabel.VisibleRatio; + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates); + if (visibleRatio < 1.0f) + { + dialogueLabel.Call("skip_typing"); + } + } + } + + + public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) + { + temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); + isWaitingForInput = false; + resource = dialogueResource; + + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); + } + + + public async void Next(string nextId) + { + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); + } + + + #region Helpers + + + private async void ApplyDialogueLine() + { + MutationCooldown.Stop(); + + isWaitingForInput = false; + balloon.FocusMode = Control.FocusModeEnum.All; + balloon.GrabFocus(); + + // Set up the character name + characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); + characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); + + // Set up the dialogue + dialogueLabel.Hide(); + dialogueLabel.Set("dialogue_line", dialogueLine); + + // Set up the responses + responsesMenu.Hide(); + responsesMenu.Set("responses", dialogueLine.Responses); + + // Type out the text + balloon.Show(); + willHideBalloon = false; + dialogueLabel.Show(); + if (!string.IsNullOrEmpty(dialogueLine.Text)) + { + dialogueLabel.Call("type_out"); + await ToSignal(dialogueLabel, "finished_typing"); + } + + // Wait for input + if (dialogueLine.Responses.Count > 0) + { + balloon.FocusMode = Control.FocusModeEnum.None; + responsesMenu.Show(); + } + else if (!string.IsNullOrEmpty(dialogueLine.Time)) + { + float time = 0f; + if (!float.TryParse(dialogueLine.Time, out time)) + { + time = dialogueLine.Text.Length * 0.02f; + } + await ToSignal(GetTree().CreateTimer(time), "timeout"); + Next(dialogueLine.NextId); + } + else + { + isWaitingForInput = true; + balloon.FocusMode = Control.FocusModeEnum.All; + balloon.GrabFocus(); + } + } + + + #endregion + + + #region signals + + + private void OnMutated(Dictionary _mutation) + { + isWaitingForInput = false; + willHideBalloon = true; + MutationCooldown.Start(0.1f); + } + + + #endregion + } +} diff --git a/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid new file mode 100644 index 0000000..4b3783a --- /dev/null +++ b/addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid @@ -0,0 +1 @@ +uid://5b3w40kwakl3 diff --git a/addons/dialogue_manager/example_balloon/example_balloon.gd b/addons/dialogue_manager/example_balloon/example_balloon.gd new file mode 100644 index 0000000..c7e6d9a --- /dev/null +++ b/addons/dialogue_manager/example_balloon/example_balloon.gd @@ -0,0 +1,176 @@ +class_name DialogueManagerExampleBalloon extends CanvasLayer +## A basic dialogue balloon for use with Dialogue Manager. + +## The action to use for advancing the dialogue +@export var next_action: StringName = &"ui_accept" + +## The action to use to skip typing the dialogue +@export var skip_action: StringName = &"ui_cancel" + +## The dialogue resource +var resource: DialogueResource + +## Temporary game states +var temporary_game_states: Array = [] + +## See if we are waiting for the player +var is_waiting_for_input: bool = false + +## See if we are running a long mutation and should hide the balloon +var will_hide_balloon: bool = false + +## A dictionary to store any ephemeral variables +var locals: Dictionary = {} + +var _locale: String = TranslationServer.get_locale() + +## The current line +var dialogue_line: DialogueLine: + set(value): + if value: + dialogue_line = value + apply_dialogue_line() + else: + # The dialogue has finished so close the balloon + queue_free() + get: + return dialogue_line + +## A cooldown timer for delaying the balloon hide when encountering a mutation. +var mutation_cooldown: Timer = Timer.new() + +## The base balloon anchor +@onready var balloon: Control = %Balloon + +## The label showing the name of the currently speaking character +@onready var character_label: RichTextLabel = %CharacterLabel + +## The label showing the currently spoken dialogue +@onready var dialogue_label: DialogueLabel = %DialogueLabel + +## The menu of responses +@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu + + +func _ready() -> void: + balloon.hide() + Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated) + + # If the responses menu doesn't have a next action set, use this one + if responses_menu.next_action.is_empty(): + responses_menu.next_action = next_action + + mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout) + add_child(mutation_cooldown) + + +func _unhandled_input(_event: InputEvent) -> void: + # Only the balloon is allowed to handle input while it's showing + get_viewport().set_input_as_handled() + + +func _notification(what: int) -> void: + ## Detect a change of locale and update the current dialogue line to show the new language + if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label): + _locale = TranslationServer.get_locale() + var visible_ratio = dialogue_label.visible_ratio + self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id) + if visible_ratio < 1: + dialogue_label.skip_typing() + + +## Start some dialogue +func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: + temporary_game_states = [self] + extra_game_states + is_waiting_for_input = false + resource = dialogue_resource + self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) + + +## Apply any changes to the balloon given a new [DialogueLine]. +func apply_dialogue_line() -> void: + mutation_cooldown.stop() + + is_waiting_for_input = false + balloon.focus_mode = Control.FOCUS_ALL + balloon.grab_focus() + + character_label.visible = not dialogue_line.character.is_empty() + character_label.text = tr(dialogue_line.character, "dialogue") + + dialogue_label.hide() + dialogue_label.dialogue_line = dialogue_line + + responses_menu.hide() + responses_menu.responses = dialogue_line.responses + + # Show our balloon + balloon.show() + will_hide_balloon = false + + dialogue_label.show() + if not dialogue_line.text.is_empty(): + dialogue_label.type_out() + await dialogue_label.finished_typing + + # Wait for input + if dialogue_line.responses.size() > 0: + balloon.focus_mode = Control.FOCUS_NONE + responses_menu.show() + elif dialogue_line.time != "": + var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() + await get_tree().create_timer(time).timeout + next(dialogue_line.next_id) + else: + is_waiting_for_input = true + balloon.focus_mode = Control.FOCUS_ALL + balloon.grab_focus() + + +## Go to the next line +func next(next_id: String) -> void: + self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) + + +#region Signals + + +func _on_mutation_cooldown_timeout() -> void: + if will_hide_balloon: + will_hide_balloon = false + balloon.hide() + + +func _on_mutated(_mutation: Dictionary) -> void: + is_waiting_for_input = false + will_hide_balloon = true + mutation_cooldown.start(0.1) + + +func _on_balloon_gui_input(event: InputEvent) -> void: + # See if we need to skip typing of the dialogue + if dialogue_label.is_typing: + var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() + var skip_button_was_pressed: bool = event.is_action_pressed(skip_action) + if mouse_was_clicked or skip_button_was_pressed: + get_viewport().set_input_as_handled() + dialogue_label.skip_typing() + return + + if not is_waiting_for_input: return + if dialogue_line.responses.size() > 0: return + + # When there are no response options the balloon itself is the clickable thing + get_viewport().set_input_as_handled() + + if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: + next(dialogue_line.next_id) + elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon: + next(dialogue_line.next_id) + + +func _on_responses_menu_response_selected(response: DialogueResponse) -> void: + next(response.next_id) + + +#endregion diff --git a/addons/dialogue_manager/example_balloon/example_balloon.gd.uid b/addons/dialogue_manager/example_balloon/example_balloon.gd.uid new file mode 100644 index 0000000..6327f9b --- /dev/null +++ b/addons/dialogue_manager/example_balloon/example_balloon.gd.uid @@ -0,0 +1 @@ +uid://d1wt4ma6055l8 diff --git a/addons/dialogue_manager/example_balloon/example_balloon.tscn b/addons/dialogue_manager/example_balloon/example_balloon.tscn new file mode 100644 index 0000000..64b3365 --- /dev/null +++ b/addons/dialogue_manager/example_balloon/example_balloon.tscn @@ -0,0 +1,142 @@ +[gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] + +[ext_resource type="Script" uid="uid://5b3w40kwakl3" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_36de5"] +[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] +[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] +bg_color = Color(0, 0, 0, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.329412, 0.329412, 0.329412, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"] +bg_color = Color(0.121569, 0.121569, 0.121569, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(1, 1, 1, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"] +bg_color = Color(0, 0, 0, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.6, 0.6, 0.6, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"] +bg_color = Color(0, 0, 0, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="Theme" id="Theme_qq3yp"] +default_font_size = 20 +Button/styles/disabled = SubResource("StyleBoxFlat_spyqn") +Button/styles/focus = SubResource("StyleBoxFlat_ri4m3") +Button/styles/hover = SubResource("StyleBoxFlat_e0njw") +Button/styles/normal = SubResource("StyleBoxFlat_e0njw") +MarginContainer/constants/margin_bottom = 15 +MarginContainer/constants/margin_left = 30 +MarginContainer/constants/margin_right = 30 +MarginContainer/constants/margin_top = 15 +PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt") + +[node name="ExampleBalloon" type="CanvasLayer"] +layer = 100 +script = ExtResource("1_36de5") + +[node name="Balloon" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = SubResource("Theme_qq3yp") + +[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_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="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] +layout_mode = 2 + +[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 +mouse_filter = 1 +bbcode_enabled = true +text = "Character" +fit_content = true +scroll_active = false + +[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="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")] +unique_name_in_owner = true +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/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/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/addons/dialogue_manager/example_balloon/small_example_balloon.tscn b/addons/dialogue_manager/example_balloon/small_example_balloon.tscn new file mode 100644 index 0000000..57f8e4e --- /dev/null +++ b/addons/dialogue_manager/example_balloon/small_example_balloon.tscn @@ -0,0 +1,166 @@ +[gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"] + +[ext_resource type="Script" uid="uid://5b3w40kwakl3" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_s2gbs"] +[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"] +[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"] +content_margin_left = 6.0 +content_margin_top = 3.0 +content_margin_right = 6.0 +content_margin_bottom = 3.0 +bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.345098, 0.345098, 0.345098, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ufjut"] +content_margin_left = 6.0 +content_margin_top = 3.0 +content_margin_right = 6.0 +content_margin_bottom = 3.0 +bg_color = Color(0.227451, 0.227451, 0.227451, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(1, 1, 1, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fcbqo"] +content_margin_left = 6.0 +content_margin_top = 3.0 +content_margin_right = 6.0 +content_margin_bottom = 3.0 +bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t6i7a"] +content_margin_left = 6.0 +content_margin_top = 3.0 +content_margin_right = 6.0 +content_margin_bottom = 3.0 +bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"] +bg_color = Color(0, 0, 0, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="Theme" id="Theme_qq3yp"] +default_font_size = 8 +Button/styles/disabled = SubResource("StyleBoxFlat_235ry") +Button/styles/focus = SubResource("StyleBoxFlat_ufjut") +Button/styles/hover = SubResource("StyleBoxFlat_fcbqo") +Button/styles/normal = SubResource("StyleBoxFlat_t6i7a") +MarginContainer/constants/margin_bottom = 4 +MarginContainer/constants/margin_left = 8 +MarginContainer/constants/margin_right = 8 +MarginContainer/constants/margin_top = 4 +PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm") + +[node name="ExampleBalloon" type="CanvasLayer"] +layer = 100 +script = ExtResource("1_s2gbs") + +[node name="Balloon" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = SubResource("Theme_qq3yp") + +[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_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="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] +layout_mode = 2 + +[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 +mouse_filter = 1 +bbcode_enabled = true +text = "Character" +fit_content = true +scroll_active = false + +[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="ResponsesMenu" type="VBoxContainer" parent="Balloon"] +unique_name_in_owner = true +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/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/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/addons/dialogue_manager/export_plugin.gd b/addons/dialogue_manager/export_plugin.gd new file mode 100644 index 0000000..ef65500 --- /dev/null +++ b/addons/dialogue_manager/export_plugin.gd @@ -0,0 +1,26 @@ +class_name DMExportPlugin extends EditorExportPlugin + +const IGNORED_PATHS = [ + "/assets", + "/components", + "/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() + + # Ignore any editor stuff + for ignored_path: String in IGNORED_PATHS: + if path.begins_with(plugin_path + ignored_path): + skip() + + # Ignore C# stuff it not using dotnet + if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"): + skip() diff --git a/addons/dialogue_manager/export_plugin.gd.uid b/addons/dialogue_manager/export_plugin.gd.uid new file mode 100644 index 0000000..efaa0c6 --- /dev/null +++ b/addons/dialogue_manager/export_plugin.gd.uid @@ -0,0 +1 @@ +uid://sa55ra11ji2q diff --git a/addons/dialogue_manager/import_plugin.gd b/addons/dialogue_manager/import_plugin.gd new file mode 100644 index 0000000..9c4ad78 --- /dev/null +++ b/addons/dialogue_manager/import_plugin.gd @@ -0,0 +1,110 @@ +@tool +class_name DMImportPlugin extends EditorImportPlugin + + +signal compiled_resource(resource: Resource) + + +const COMPILER_VERSION = 15 + + +func _get_importer_name() -> String: + return "dialogue_manager" + + +func _get_format_version() -> int: + return COMPILER_VERSION + + +func _get_visible_name() -> String: + return "Dialogue" + + +func _get_import_order() -> int: + return -1000 + + +func _get_priority() -> float: + return 1000.0 + + +func _get_resource_type(): + return "Resource" + + +func _get_recognized_extensions() -> PackedStringArray: + return PackedStringArray(["dialogue"]) + + +func _get_save_extension(): + return "tres" + + +func _get_preset_count() -> int: + return 0 + + +func _get_preset_name(preset_index: int) -> String: + return "Unknown" + + +func _get_import_options(path: String, preset_index: int) -> Array: + # When the options array is empty there is a misleading error on export + # that actually means nothing so let's just have an invisible option. + return [{ + name = "defaults", + default_value = true + }] + + +func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool: + return false + + +func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error: + var cache = Engine.get_meta("DMCache") + + # Get the raw file contents + if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND + + var file: FileAccess = FileAccess.open(source_file, FileAccess.READ) + var raw_text: String = file.get_as_text() + + cache.file_content_changed.emit(source_file, raw_text) + + # Compile the text + var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file) + if result.errors.size() > 0: + printerr("%d errors found in %s" % [result.errors.size(), source_file]) + cache.add_errors_to_file(source_file, result.errors) + return OK + + # Get the current addon version + var config: ConfigFile = ConfigFile.new() + config.load("res://addons/dialogue_manager/plugin.cfg") + var version: String = config.get_value("plugin", "version") + + # Save the results to a resource + var resource: DialogueResource = DialogueResource.new() + resource.set_meta("dialogue_manager_version", version) + + resource.using_states = result.using_states + resource.titles = result.titles + resource.first_title = result.first_title + resource.character_names = result.character_names + resource.lines = result.lines + resource.raw_text = result.raw_text + + # Clear errors and possibly trigger any cascade recompiles + cache.add_file(source_file, result) + + var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) + + compiled_resource.emit(resource) + + # Recompile any dependencies + var dependent_paths: PackedStringArray = cache.get_dependent_paths_for_reimport(source_file) + for path in dependent_paths: + append_import_external_resource(path) + + return err diff --git a/addons/dialogue_manager/import_plugin.gd.uid b/addons/dialogue_manager/import_plugin.gd.uid new file mode 100644 index 0000000..e98bfab --- /dev/null +++ b/addons/dialogue_manager/import_plugin.gd.uid @@ -0,0 +1 @@ +uid://dhwpj6ed8soyq diff --git a/addons/dialogue_manager/inspector_plugin.gd b/addons/dialogue_manager/inspector_plugin.gd new file mode 100644 index 0000000..366c1f3 --- /dev/null +++ b/addons/dialogue_manager/inspector_plugin.gd @@ -0,0 +1,21 @@ +@tool +class_name DMInspectorPlugin extends EditorInspectorPlugin + + +const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd") + + +func _can_handle(object) -> bool: + if object is GDScript: return false + if not object is Node: return false + if "name" in object and object.name == "Dialogue Manager": return false + return true + + +func _parse_property(object: Object, type, name: String, hint_type, hint_string: String, usage_flags: int, wide: bool) -> bool: + if hint_string == "DialogueResource" or ("dialogue" in name.to_lower() and hint_string == "Resource"): + var property_editor = DialogueEditorProperty.new() + add_property_editor(name, property_editor) + return true + + return false diff --git a/addons/dialogue_manager/inspector_plugin.gd.uid b/addons/dialogue_manager/inspector_plugin.gd.uid new file mode 100644 index 0000000..00c8db8 --- /dev/null +++ b/addons/dialogue_manager/inspector_plugin.gd.uid @@ -0,0 +1 @@ +uid://0x31sbqbikov diff --git a/addons/dialogue_manager/l10n/en.mo b/addons/dialogue_manager/l10n/en.mo new file mode 100644 index 0000000..2ab4fdf Binary files /dev/null and b/addons/dialogue_manager/l10n/en.mo differ diff --git a/addons/dialogue_manager/l10n/en.po b/addons/dialogue_manager/l10n/en.po new file mode 100644 index 0000000..132bff3 --- /dev/null +++ b/addons/dialogue_manager/l10n/en.po @@ -0,0 +1,442 @@ +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" + +msgid "start_a_new_file" +msgstr "Start a new file" + +msgid "open_a_file" +msgstr "Open a file" + +msgid "open.open" +msgstr "Open..." + +msgid "open.quick_open" +msgstr "Quick open..." + +msgid "open.no_recent_files" +msgstr "No recent files" + +msgid "open.clear_recent_files" +msgstr "Clear recent files" + +msgid "save_all_files" +msgstr "Save all files" + +msgid "all" +msgstr "All" + +msgid "find_in_files" +msgstr "Find in files..." + +msgid "test_dialogue" +msgstr "Test dialogue from start of file" + +msgid "test_dialogue_from_line" +msgstr "Test dialogue from current line" + +msgid "search_for_text" +msgstr "Search for text" + +msgid "insert" +msgstr "Insert" + +msgid "translations" +msgstr "Translations" + +msgid "sponsor" +msgstr "Sponsor" + +msgid "show_support" +msgstr "Support Dialogue Manager" + +msgid "docs" +msgstr "Docs" + +msgid "insert.wave_bbcode" +msgstr "Wave BBCode" + +msgid "insert.shake_bbcode" +msgstr "Shake BBCode" + +msgid "insert.typing_pause" +msgstr "Typing pause" + +msgid "insert.typing_speed_change" +msgstr "Typing speed change" + +msgid "insert.auto_advance" +msgstr "Auto advance" + +msgid "insert.templates" +msgstr "Templates" + +msgid "insert.title" +msgstr "Title" + +msgid "insert.dialogue" +msgstr "Dialogue" + +msgid "insert.response" +msgstr "Response" + +msgid "insert.random_lines" +msgstr "Random lines" + +msgid "insert.random_text" +msgstr "Random text" + +msgid "insert.actions" +msgstr "Actions" + +msgid "insert.jump" +msgstr "Jump to title" + +msgid "insert.end_dialogue" +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..." + +msgid "save_to_csv" +msgstr "Save lines to CSV..." + +msgid "import_from_csv" +msgstr "Import line changes from CSV..." + +msgid "confirm_close" +msgstr "Save changes to '{path}'?" + +msgid "confirm_close.save" +msgstr "Save changes" + +msgid "confirm_close.discard" +msgstr "Discard" + +msgid "buffer.save" +msgstr "Save" + +msgid "buffer.save_as" +msgstr "Save as..." + +msgid "buffer.close" +msgstr "Close" + +msgid "buffer.close_all" +msgstr "Close all" + +msgid "buffer.close_other_files" +msgstr "Close other files" + +msgid "buffer.copy_file_path" +msgstr "Copy file path" + +msgid "buffer.show_in_filesystem" +msgstr "Show in FileSystem" + +msgid "n_of_n" +msgstr "{index} of {total}" + +msgid "search.find" +msgstr "Find:" + +msgid "search.find_all" +msgstr "Find all..." + +msgid "search.placeholder" +msgstr "Text to search for" + +msgid "search.replace_placeholder" +msgstr "Text to replace it with" + +msgid "search.replace_selected" +msgstr "Replace selected" + +msgid "search.previous" +msgstr "Previous" + +msgid "search.next" +msgstr "Next" + +msgid "search.match_case" +msgstr "Match case" + +msgid "search.toggle_replace" +msgstr "Replace" + +msgid "search.replace_with" +msgstr "Replace with:" + +msgid "search.replace" +msgstr "Replace" + +msgid "search.replace_all" +msgstr "Replace all" + +msgid "files_list.filter" +msgstr "Filter files" + +msgid "titles_list.filter" +msgstr "Filter titles" + +msgid "errors.key_not_found" +msgstr "Key \"{key}\" not found." + +msgid "errors.line_and_message" +msgstr "Error at {line}, {column}: {message}" + +msgid "errors_in_script" +msgstr "You have errors in your script. Fix them and then try again." + +msgid "errors_with_build" +msgstr "You need to fix dialogue errors before you can run your game." + +msgid "errors.import_errors" +msgstr "There are errors in this imported file." + +msgid "errors.already_imported" +msgstr "File already imported." + +msgid "errors.duplicate_import" +msgstr "Duplicate import name." + +msgid "errors.unknown_using" +msgstr "Unknown autoload in using statement." + +msgid "errors.empty_title" +msgstr "Titles cannot be empty." + +msgid "errors.duplicate_title" +msgstr "There is already a title with that name." + +msgid "errors.invalid_title_string" +msgstr "Titles can only contain alphanumeric characters and numbers." + +msgid "errors.invalid_title_number" +msgstr "Titles cannot begin with a number." + +msgid "errors.unknown_title" +msgstr "Unknown title." + +msgid "errors.jump_to_invalid_title" +msgstr "This jump is pointing to an invalid title." + +msgid "errors.title_has_no_content" +msgstr "That title has no content. Maybe change this to a \"=> END\"." + +msgid "errors.invalid_expression" +msgstr "Expression is invalid." + +msgid "errors.unexpected_condition" +msgstr "Unexpected condition." + +msgid "errors.duplicate_id" +msgstr "This ID is already on another line." + +msgid "errors.missing_id" +msgstr "This line is missing an ID." + +msgid "errors.invalid_indentation" +msgstr "Invalid indentation." + +msgid "errors.condition_has_no_content" +msgstr "A condition line needs an indented line below it." + +msgid "errors.incomplete_expression" +msgstr "Incomplete expression." + +msgid "errors.invalid_expression_for_value" +msgstr "Invalid expression for value." + +msgid "errors.file_not_found" +msgstr "File not found." + +msgid "errors.unexpected_end_of_expression" +msgstr "Unexpected end of expression." + +msgid "errors.unexpected_function" +msgstr "Unexpected function." + +msgid "errors.unexpected_bracket" +msgstr "Unexpected bracket." + +msgid "errors.unexpected_closing_bracket" +msgstr "Unexpected closing bracket." + +msgid "errors.missing_closing_bracket" +msgstr "Missing closing bracket." + +msgid "errors.unexpected_operator" +msgstr "Unexpected operator." + +msgid "errors.unexpected_comma" +msgstr "Unexpected comma." + +msgid "errors.unexpected_colon" +msgstr "Unexpected colon." + +msgid "errors.unexpected_dot" +msgstr "Unexpected dot." + +msgid "errors.unexpected_boolean" +msgstr "Unexpected boolean." + +msgid "errors.unexpected_string" +msgstr "Unexpected string." + +msgid "errors.unexpected_number" +msgstr "Unexpected number." + +msgid "errors.unexpected_variable" +msgstr "Unexpected variable." + +msgid "errors.invalid_index" +msgstr "Invalid index." + +msgid "errors.unexpected_assignment" +msgstr "Unexpected assignment." + +msgid "errors.expected_when_or_else" +msgstr "Expecting a when or an else case." + +msgid "errors.only_one_else_allowed" +msgstr "Only one else case is allowed per match." + +msgid "errors.when_must_belong_to_match" +msgstr "When statements can only appear as children of match statements." + +msgid "errors.concurrent_line_without_origin" +msgstr "Concurrent lines need an origin line that doesn't start with \"| \"." + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "Goto references are not allowed on concurrent dialogue lines." + +msgid "errors.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." + +msgid "update.available" +msgstr "v{version} available" + +msgid "update.is_available_for_download" +msgstr "Version %s is available for download!" + +msgid "update.downloading" +msgstr "Downloading..." + +msgid "update.download_update" +msgstr "Download update" + +msgid "update.needs_reload" +msgstr "The project needs to be reloaded to install the update." + +msgid "update.reload_ok_button" +msgstr "Reload project" + +msgid "update.reload_cancel_button" +msgstr "Do it later" + +msgid "update.reload_project" +msgstr "Reload project" + +msgid "update.release_notes" +msgstr "Read release notes" + +msgid "update.success" +msgstr "Dialogue Manager is now v{version}." + +msgid "update.failed" +msgstr "There was a problem downloading the update." + +msgid "runtime.no_resource" +msgstr "No dialogue resource provided." + +msgid "runtime.no_content" +msgstr "\"{file_path}\" has no content." + +msgid "runtime.errors" +msgstr "You have {count} errors in your dialogue text." + +msgid "runtime.error_detail" +msgstr "Line {line}: {message}" + +msgid "runtime.errors_see_details" +msgstr "You have {count} errors in your dialogue text. See Output for details." + +msgid "runtime.invalid_expression" +msgstr "\"{expression}\" is not a valid expression: {error}" + +msgid "runtime.array_index_out_of_bounds" +msgstr "Index {index} out of bounds of array \"{array}\"." + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "Left hand side of expression cannot be assigned to." + +msgid "runtime.key_not_found" +msgstr "Key \"{key}\" not found in dictionary \"{dictionary}\"" + +msgid "runtime.property_not_found" +msgstr "\"{property}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties." + +msgid "runtime.property_not_found_missing_export" +msgstr "\"{property}\" not found. You might need to add an [Export] decorator. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties." + +msgid "runtime.method_not_found" +msgstr "Method \"{method}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties." + +msgid "runtime.signal_not_found" +msgstr "Signal \"{signal_name}\" not found. States with directly referenceable properties/methods/signals include {states}. Autoloads need to be referenced by their name to use their properties." + +msgid "runtime.method_not_callable" +msgstr "\"{method}\" is not a callable method on \"{object}\"" + +msgid "runtime.unknown_operator" +msgstr "Unknown operator." + +msgid "runtime.unknown_autoload" +msgstr "\"{autoload}\" doesn't appear to be a valid autoload." + +msgid "runtime.something_went_wrong" +msgstr "Something went wrong." + +msgid "runtime.expected_n_got_n_args" +msgstr "\"{method}\" was called with {received} arguments but it only has {expected}." + +msgid "runtime.unsupported_array_type" +msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead." + +msgid "runtime.dialogue_balloon_missing_start_method" +msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method." + +msgid "runtime.top_level_states_share_name" +msgstr "Multiple top-level states ({states}) share method/property/signal name \"{key}\". Only the first occurance is accessible to dialogue." + +msgid "translation_plugin.character_name" +msgstr "Character name" \ No newline at end of file diff --git a/addons/dialogue_manager/l10n/es.po b/addons/dialogue_manager/l10n/es.po new file mode 100644 index 0000000..6c6bffb --- /dev/null +++ b/addons/dialogue_manager/l10n/es.po @@ -0,0 +1,387 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"POT-Creation-Date: 2024-02-25 20:58\n" +"PO-Revision-Date: 2024-02-25 20:58\n" +"Last-Translator: you \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "start_a_new_file" +msgstr "Crear un nuevo archivo" + +msgid "open_a_file" +msgstr "Abrir un archivo" + +msgid "open.open" +msgstr "Abrir..." + +msgid "open.no_recent_files" +msgstr "No hay archivos recientes" + +msgid "open.clear_recent_files" +msgstr "Limpiar archivos recientes" + +msgid "save_all_files" +msgstr "Guardar todos los archivos" + +msgid "test_dialogue" +msgstr "Diálogo de prueba" + +msgid "search_for_text" +msgstr "Buscar texto" + +msgid "insert" +msgstr "Insertar" + +msgid "translations" +msgstr "Traducciones" + +msgid "show_support" +msgstr "Contribuye con Dialogue Manager" + +msgid "docs" +msgstr "Docs" + +msgid "insert.wave_bbcode" +msgstr "BBCode ondulado" + +msgid "insert.shake_bbcode" +msgstr "BBCode agitado" + +msgid "insert.typing_pause" +msgstr "Pausa de escritura" + +msgid "insert.typing_speed_change" +msgstr "Cambiar la velocidad de escritura" + +msgid "insert.auto_advance" +msgstr "Avance automático" + +msgid "insert.templates" +msgstr "Plantillas" + +msgid "insert.title" +msgstr "Título" + +msgid "insert.dialogue" +msgstr "Diálogo" + +msgid "insert.response" +msgstr "Respuesta" + +msgid "insert.random_lines" +msgstr "Líneas aleatorias" + +msgid "insert.random_text" +msgstr "Texto aleatorio" + +msgid "insert.actions" +msgstr "Acciones" + +msgid "insert.jump" +msgstr "Ir al título" + +msgid "insert.end_dialogue" +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..." + +msgid "save_to_csv" +msgstr "Guardar líneas en CSV..." + +msgid "import_from_csv" +msgstr "Importar cambios de línea desde CSV..." + +msgid "confirm_close" +msgstr "¿Guardar los cambios en '{path}'?" + +msgid "confirm_close.save" +msgstr "Guardar cambios" + +msgid "confirm_close.discard" +msgstr "Descartar" + +msgid "buffer.save" +msgstr "Guardar" + +msgid "buffer.save_as" +msgstr "Guardar como..." + +msgid "buffer.close" +msgstr "Cerrar" + +msgid "buffer.close_all" +msgstr "Cerrar todo" + +msgid "buffer.close_other_files" +msgstr "Cerrar otros archivos" + +msgid "buffer.copy_file_path" +msgstr "Copiar la ruta del archivo" + +msgid "buffer.show_in_filesystem" +msgstr "Mostrar en el sistema de archivos" + +msgid "n_of_n" +msgstr "{index} de {total}" + +msgid "search.previous" +msgstr "Anterior" + +msgid "search.next" +msgstr "Siguiente" + +msgid "search.match_case" +msgstr "Coincidir mayúsculas/minúsculas" + +msgid "search.toggle_replace" +msgstr "Reemplazar" + +msgid "search.replace_with" +msgstr "Reemplazar con:" + +msgid "search.replace" +msgstr "Reemplazar" + +msgid "search.replace_all" +msgstr "Reemplazar todo" + +msgid "files_list.filter" +msgstr "Filtrar archivos" + +msgid "titles_list.filter" +msgstr "Filtrar títulos" + +msgid "errors.key_not_found" +msgstr "La tecla \"{key}\" no se encuentra." + +msgid "errors.line_and_message" +msgstr "Error en {line}, {column}: {message}" + +msgid "errors_in_script" +msgstr "Tienes errores en tu guion. Corrígelos y luego inténtalo de nuevo." + +msgid "errors_with_build" +msgstr "Debes corregir los errores de diálogo antes de poder ejecutar tu juego." + +msgid "errors.import_errors" +msgstr "Hay errores en este archivo importado." + +msgid "errors.already_imported" +msgstr "Archivo ya importado." + +msgid "errors.duplicate_import" +msgstr "Nombre de importación duplicado." + +msgid "errors.unknown_using" +msgstr "Autoload desconocida en la declaración de uso." + +msgid "errors.empty_title" +msgstr "Los títulos no pueden estar vacíos." + +msgid "errors.duplicate_title" +msgstr "Ya hay un título con ese nombre." + +msgid "errors.nested_title" +msgstr "Los títulos no pueden tener sangría." + +msgid "errors.invalid_title_string" +msgstr "Los títulos solo pueden contener caracteres alfanuméricos y números." + +msgid "errors.invalid_title_number" +msgstr "Los títulos no pueden empezar con un número." + +msgid "errors.unknown_title" +msgstr "Título desconocido." + +msgid "errors.jump_to_invalid_title" +msgstr "Este salto está apuntando a un título inválido." + +msgid "errors.title_has_no_content" +msgstr "Ese título no tiene contenido. Quizá cambiarlo a \"=> FIN\"." + +msgid "errors.invalid_expression" +msgstr "La expresión es inválida." + +msgid "errors.unexpected_condition" +msgstr "Condición inesperada." + +msgid "errors.duplicate_id" +msgstr "Este ID ya está en otra línea." + +msgid "errors.missing_id" +msgstr "Esta línea está sin ID." + +msgid "errors.invalid_indentation" +msgstr "Sangría no válida." + +msgid "errors.condition_has_no_content" +msgstr "Una línea de condición necesita una línea sangrada debajo de ella." + +msgid "errors.incomplete_expression" +msgstr "Expresión incompleta." + +msgid "errors.invalid_expression_for_value" +msgstr "Expresión no válida para valor." + +msgid "errors.file_not_found" +msgstr "Archivo no encontrado." + +msgid "errors.unexpected_end_of_expression" +msgstr "Fin de expresión inesperado." + +msgid "errors.unexpected_function" +msgstr "Función inesperada." + +msgid "errors.unexpected_bracket" +msgstr "Corchete inesperado." + +msgid "errors.unexpected_closing_bracket" +msgstr "Bracket de cierre inesperado." + +msgid "errors.missing_closing_bracket" +msgstr "Falta cerrar corchete." + +msgid "errors.unexpected_operator" +msgstr "Operador inesperado." + +msgid "errors.unexpected_comma" +msgstr "Coma inesperada." + +msgid "errors.unexpected_colon" +msgstr "Dos puntos inesperados" + +msgid "errors.unexpected_dot" +msgstr "Punto inesperado." + +msgid "errors.unexpected_boolean" +msgstr "Booleano inesperado." + +msgid "errors.unexpected_string" +msgstr "String inesperado." + +msgid "errors.unexpected_number" +msgstr "Número inesperado." + +msgid "errors.unexpected_variable" +msgstr "Variable inesperada." + +msgid "errors.invalid_index" +msgstr "Índice no válido." + +msgid "errors.unexpected_assignment" +msgstr "Asignación inesperada." + +msgid "errors.unknown" +msgstr "Sintaxis desconocida." + +msgid "update.available" +msgstr "v{version} disponible" + +msgid "update.is_available_for_download" +msgstr "¡La versión %s ya está disponible para su descarga!" + +msgid "update.downloading" +msgstr "Descargando..." + +msgid "update.download_update" +msgstr "Descargar actualización" + +msgid "update.needs_reload" +msgstr "El proyecto debe ser recargado para instalar la actualización." + +msgid "update.reload_ok_button" +msgstr "Recargar proyecto" + +msgid "update.reload_cancel_button" +msgstr "Hazlo más tarde" + +msgid "update.reload_project" +msgstr "Recargar proyecto" + +msgid "update.release_notes" +msgstr "Leer las notas de la versión" + +msgid "update.success" +msgstr "El Gestor de Diálogo ahora es v{versión}." + +msgid "update.failed" +msgstr "Hubo un problema al descargar la actualización." + +msgid "runtime.no_resource" +msgstr "Recurso de diálogo no proporcionado." + +msgid "runtime.no_content" +msgstr "\"{file_path}\" no tiene contenido." + +msgid "runtime.errors" +msgstr "Tienes {count} errores en tu diálogo de texto." + +msgid "runtime.error_detail" +msgstr "Línea {line}: {message}" + +msgid "runtime.errors_see_details" +msgstr "Tienes {count} errores en tu texto de diálogo. Consulta la salida para más detalles." + +msgid "runtime.invalid_expression" +msgstr "\"{expression}\" no es una expresión válida: {error}" + +msgid "runtime.array_index_out_of_bounds" +msgstr "Índice {index} fuera de los límites del array \"{array}\"." + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "El lado izquierdo de la expresión no se puede asignar." + +msgid "runtime.key_not_found" +msgstr "Clave \"{key}\" no encontrada en el diccionario \"{dictionary}\"" + +msgid "runtime.property_not_found" +msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states})." + +msgid "runtime.property_not_found_missing_export" +msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states}). Es posible que necesites añadir un decorador [Export]." + +msgid "runtime.method_not_found" +msgstr "\"{method}\" no es un método en ningún estado del juego ({states})" + +msgid "runtime.signal_not_found" +msgstr "\"{signal_name}\" no es una señal en ningún estado del juego ({states})" + +msgid "runtime.method_not_callable" +msgstr "\"{method}\" no es un método llamable en \"{object}\"" + +msgid "runtime.unknown_operator" +msgstr "Operador desconocido." + +msgid "runtime.unknown_autoload" +msgstr "\"{autoload}\" parece no ser un autoload válido." + +msgid "runtime.something_went_wrong" +msgstr "Algo salió mal." + +msgid "runtime.expected_n_got_n_args" +msgstr "El método \"{method}\" se llamó con {received} argumentos, pero solo tiene {expected}." + +msgid "runtime.unsupported_array_type" +msgstr "Array[{type}] no está soportado en mutaciones. Utiliza Array como tipo en su lugar." + +msgid "runtime.dialogue_balloon_missing_start_method" +msgstr "Tu globo de diálogo no tiene un método \"start\" o \"Start\"." diff --git a/addons/dialogue_manager/l10n/translations.pot b/addons/dialogue_manager/l10n/translations.pot new file mode 100644 index 0000000..ad06ac7 --- /dev/null +++ b/addons/dialogue_manager/l10n/translations.pot @@ -0,0 +1,432 @@ +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8-bit\n" + +msgid "start_a_new_file" +msgstr "" + +msgid "open_a_file" +msgstr "" + +msgid "open.open" +msgstr "" + +msgid "open.quick_open" +msgstr "" + +msgid "open.no_recent_files" +msgstr "" + +msgid "open.clear_recent_files" +msgstr "" + +msgid "save_all_files" +msgstr "" + +msgid "all" +msgstr "" + +msgid "find_in_files" +msgstr "" + +msgid "test_dialogue" +msgstr "" + +msgid "test_dialogue_from_line" +msgstr "" + +msgid "search_for_text" +msgstr "" + +msgid "insert" +msgstr "" + +msgid "translations" +msgstr "" + +msgid "sponsor" +msgstr "" + +msgid "show_support" +msgstr "" + +msgid "docs" +msgstr "" + +msgid "insert.wave_bbcode" +msgstr "" + +msgid "insert.shake_bbcode" +msgstr "" + +msgid "insert.typing_pause" +msgstr "" + +msgid "insert.typing_speed_change" +msgstr "" + +msgid "insert.auto_advance" +msgstr "" + +msgid "insert.templates" +msgstr "" + +msgid "insert.title" +msgstr "" + +msgid "insert.dialogue" +msgstr "" + +msgid "insert.response" +msgstr "" + +msgid "insert.random_lines" +msgstr "" + +msgid "insert.random_text" +msgstr "" + +msgid "insert.actions" +msgstr "" + +msgid "insert.jump" +msgstr "" + +msgid "insert.end_dialogue" +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 "" + +msgid "import_from_csv" +msgstr "" + +msgid "confirm_close" +msgstr "" + +msgid "confirm_close.save" +msgstr "" + +msgid "confirm_close.discard" +msgstr "" + +msgid "buffer.save" +msgstr "" + +msgid "buffer.save_as" +msgstr "" + +msgid "buffer.close" +msgstr "" + +msgid "buffer.close_all" +msgstr "" + +msgid "buffer.close_other_files" +msgstr "" + +msgid "buffer.copy_file_path" +msgstr "" + +msgid "buffer.show_in_filesystem" +msgstr "" + +msgid "n_of_n" +msgstr "" + +msgid "search.find" +msgstr "" + +msgid "search.find_all" +msgstr "" + +msgid "search.placeholder" +msgstr "" + +msgid "search.replace_placeholder" +msgstr "" + +msgid "search.replace_selected" +msgstr "" + +msgid "search.previous" +msgstr "" + +msgid "search.next" +msgstr "" + +msgid "search.match_case" +msgstr "" + +msgid "search.toggle_replace" +msgstr "" + +msgid "search.replace_with" +msgstr "" + +msgid "search.replace" +msgstr "" + +msgid "search.replace_all" +msgstr "" + +msgid "files_list.filter" +msgstr "" + +msgid "titles_list.filter" +msgstr "" + +msgid "errors.key_not_found" +msgstr "" + +msgid "errors.line_and_message" +msgstr "" + +msgid "errors_in_script" +msgstr "" + +msgid "errors_with_build" +msgstr "" + +msgid "errors.import_errors" +msgstr "" + +msgid "errors.already_imported" +msgstr "" + +msgid "errors.duplicate_import" +msgstr "" + +msgid "errors.unknown_using" +msgstr "" + +msgid "errors.empty_title" +msgstr "" + +msgid "errors.duplicate_title" +msgstr "" + +msgid "errors.invalid_title_string" +msgstr "" + +msgid "errors.invalid_title_number" +msgstr "" + +msgid "errors.unknown_title" +msgstr "" + +msgid "errors.jump_to_invalid_title" +msgstr "" + +msgid "errors.title_has_no_content" +msgstr "" + +msgid "errors.invalid_expression" +msgstr "" + +msgid "errors.unexpected_condition" +msgstr "" + +msgid "errors.duplicate_id" +msgstr "" + +msgid "errors.missing_id" +msgstr "" + +msgid "errors.invalid_indentation" +msgstr "" + +msgid "errors.condition_has_no_content" +msgstr "" + +msgid "errors.incomplete_expression" +msgstr "" + +msgid "errors.invalid_expression_for_value" +msgstr "" + +msgid "errors.file_not_found" +msgstr "" + +msgid "errors.unexpected_end_of_expression" +msgstr "" + +msgid "errors.unexpected_function" +msgstr "" + +msgid "errors.unexpected_bracket" +msgstr "" + +msgid "errors.unexpected_closing_bracket" +msgstr "" + +msgid "errors.missing_closing_bracket" +msgstr "" + +msgid "errors.unexpected_operator" +msgstr "" + +msgid "errors.unexpected_comma" +msgstr "" + +msgid "errors.unexpected_colon" +msgstr "" + +msgid "errors.unexpected_dot" +msgstr "" + +msgid "errors.unexpected_boolean" +msgstr "" + +msgid "errors.unexpected_string" +msgstr "" + +msgid "errors.unexpected_number" +msgstr "" + +msgid "errors.unexpected_variable" +msgstr "" + +msgid "errors.invalid_index" +msgstr "" + +msgid "errors.unexpected_assignment" +msgstr "" + +msgid "errors.expected_when_or_else" +msgstr "" + +msgid "errors.only_one_else_allowed" +msgstr "" + +msgid "errors.when_must_belong_to_match" +msgstr "" + +msgid "errors.concurrent_line_without_origin" +msgstr "" + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "" + +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "" + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "" + +msgid "errors.unknown" +msgstr "" + +msgid "update.available" +msgstr "" + +msgid "update.is_available_for_download" +msgstr "" + +msgid "update.downloading" +msgstr "" + +msgid "update.download_update" +msgstr "" + +msgid "update.needs_reload" +msgstr "" + +msgid "update.reload_ok_button" +msgstr "" + +msgid "update.reload_cancel_button" +msgstr "" + +msgid "update.reload_project" +msgstr "" + +msgid "update.release_notes" +msgstr "" + +msgid "update.success" +msgstr "" + +msgid "update.failed" +msgstr "" + +msgid "runtime.no_resource" +msgstr "" + +msgid "runtime.no_content" +msgstr "" + +msgid "runtime.errors" +msgstr "" + +msgid "runtime.error_detail" +msgstr "" + +msgid "runtime.errors_see_details" +msgstr "" + +msgid "runtime.invalid_expression" +msgstr "" + +msgid "runtime.array_index_out_of_bounds" +msgstr "" + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "" + +msgid "runtime.key_not_found" +msgstr "" + +msgid "runtime.property_not_found" +msgstr "" + +msgid "runtime.property_not_found_missing_export" +msgstr "" + +msgid "runtime.method_not_found" +msgstr "" + +msgid "runtime.signal_not_found" +msgstr "" + +msgid "runtime.method_not_callable" +msgstr "" + +msgid "runtime.unknown_operator" +msgstr "" + +msgid "runtime.unknown_autoload" +msgstr "" + +msgid "runtime.something_went_wrong" +msgstr "" + +msgid "runtime.expected_n_got_n_args" +msgstr "" + +msgid "runtime.unsupported_array_type" +msgstr "" + +msgid "runtime.dialogue_balloon_missing_start_method" +msgstr "" + +msgid "runtime.top_level_states_share_name" +msgstr "" + +msgid "translation_plugin.character_name" +msgstr "" \ No newline at end of file diff --git a/addons/dialogue_manager/l10n/uk.po b/addons/dialogue_manager/l10n/uk.po new file mode 100644 index 0000000..43dd51b --- /dev/null +++ b/addons/dialogue_manager/l10n/uk.po @@ -0,0 +1,438 @@ +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.2.2\n" + +msgid "start_a_new_file" +msgstr "Створити новий файл" + +msgid "open_a_file" +msgstr "Відкрити файл" + +msgid "open.open" +msgstr "Відкрити..." + +msgid "open.quick_open" +msgstr "Швидко відкрити..." + +msgid "open.no_recent_files" +msgstr "Жодних недавніх файлів" + +msgid "open.clear_recent_files" +msgstr "Очистити недавні файли" + +msgid "save_all_files" +msgstr "Зберегти всі файли" + +msgid "find_in_files" +msgstr "Знайти у файлах..." + +msgid "test_dialogue" +msgstr "Протестувати діалог з початку файлу" + +msgid "test_dialogue_from_line" +msgstr "Протестувати діалог з поточного рядка" + +msgid "search_for_text" +msgstr "Пошук тексту" + +msgid "insert" +msgstr "Вставити" + +msgid "translations" +msgstr "Переклади" + +msgid "sponsor" +msgstr "Спонсор" + +msgid "show_support" +msgstr "Підтримка Dialogue Manager" + +msgid "docs" +msgstr "Документація" + +msgid "insert.wave_bbcode" +msgstr "Хвиля BBCode" + +msgid "insert.shake_bbcode" +msgstr "Тряска BBCode" + +msgid "insert.typing_pause" +msgstr "Пауза друку" + +msgid "insert.typing_speed_change" +msgstr "Зміна швидкості друку" + +msgid "insert.auto_advance" +msgstr "Автоматичне просування" + +msgid "insert.templates" +msgstr "Шаблони" + +msgid "insert.title" +msgstr "Заголовок" + +msgid "insert.dialogue" +msgstr "Діалог" + +msgid "insert.response" +msgstr "Відповідь" + +msgid "insert.random_lines" +msgstr "Випадкові рядки" + +msgid "insert.random_text" +msgstr "Випадковий текст" + +msgid "insert.actions" +msgstr "Дії" + +msgid "insert.jump" +msgstr "Перейти до заголовку" + +msgid "insert.end_dialogue" +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..." + +msgid "save_to_csv" +msgstr "Зберегти рядки в CSV..." + +msgid "import_from_csv" +msgstr "Імпортувати зміни рядків з CSV..." + +msgid "confirm_close" +msgstr "Зберегти зміни до «{path}»?" + +msgid "confirm_close.save" +msgstr "Зберегти зміни" + +msgid "confirm_close.discard" +msgstr "Скасувати" + +msgid "buffer.save" +msgstr "Зберегти" + +msgid "buffer.save_as" +msgstr "Зберегти як..." + +msgid "buffer.close" +msgstr "Закрити" + +msgid "buffer.close_all" +msgstr "Закрити все" + +msgid "buffer.close_other_files" +msgstr "Закрити інші файли" + +msgid "buffer.copy_file_path" +msgstr "Копіювати шлях файлу" + +msgid "buffer.show_in_filesystem" +msgstr "Показати у файловій системі" + +msgid "n_of_n" +msgstr "{index} з {total}" + +msgid "search.find" +msgstr "Знайти:" + +msgid "search.find_all" +msgstr "Знайти всі..." + +msgid "search.placeholder" +msgstr "Текст для пошуку" + +msgid "search.replace_placeholder" +msgstr "Текст для заміни" + +msgid "search.replace_selected" +msgstr "Замінити вибране" + +msgid "search.previous" +msgstr "Назад" + +msgid "search.next" +msgstr "Далі" + +msgid "search.match_case" +msgstr "Збіг регістру" + +msgid "search.toggle_replace" +msgstr "Замінити" + +msgid "search.replace_with" +msgstr "Замінити на:" + +msgid "search.replace" +msgstr "Замінити" + +msgid "search.replace_all" +msgstr "Замінити все" + +msgid "files_list.filter" +msgstr "Фільтр файлів" + +msgid "titles_list.filter" +msgstr "Фільтр заголовків" + +msgid "errors.key_not_found" +msgstr "Ключ «{key}» не знайдено." + +msgid "errors.line_and_message" +msgstr "Помилка в {line}, {column}: {message}" + +msgid "errors_in_script" +msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз." + +msgid "errors_with_build" +msgstr "Вам потрібно виправити помилки в діалогах, перш ніж ви зможете запустити гру." + +msgid "errors.import_errors" +msgstr "В імпортованому файлі є помилки." + +msgid "errors.already_imported" +msgstr "Файл уже імпортовано." + +msgid "errors.duplicate_import" +msgstr "Дублювання назви імпорту." + +msgid "errors.unknown_using" +msgstr "Невідоме автозавантаження в операторі «using»." + +msgid "errors.empty_title" +msgstr "Заголовки не можуть бути порожніми." + +msgid "errors.duplicate_title" +msgstr "Заголовок з такою назвою уже є." + +msgid "errors.invalid_title_string" +msgstr "Заголовки можуть містити лише алфавітно-цифрові символи та цифри." + +msgid "errors.invalid_title_number" +msgstr "Заголовки не можуть починатися з цифри." + +msgid "errors.unknown_title" +msgstr "Невідомий заголовок." + +msgid "errors.jump_to_invalid_title" +msgstr "Цей перехід вказує на недійсний заголовок." + +msgid "errors.title_has_no_content" +msgstr "Цей заголовок не має змісту. Можливо, варто змінити його на «=> END»." + +msgid "errors.invalid_expression" +msgstr "Вираз є недійсним." + +msgid "errors.unexpected_condition" +msgstr "Несподівана умова." + +msgid "errors.duplicate_id" +msgstr "Цей ідентифікатор уже є на іншому рядку." + +msgid "errors.missing_id" +msgstr "У цьому рядку відсутній ідентифікатор." + +msgid "errors.invalid_indentation" +msgstr "Неправильний відступ." + +msgid "errors.condition_has_no_content" +msgstr "Рядок умови потребує відступу під ним." + +msgid "errors.incomplete_expression" +msgstr "Незавершений вираз." + +msgid "errors.invalid_expression_for_value" +msgstr "Недійсний вираз для значення." + +msgid "errors.file_not_found" +msgstr "Файл не знайдено." + +msgid "errors.unexpected_end_of_expression" +msgstr "Несподіваний кінець виразу." + +msgid "errors.unexpected_function" +msgstr "Несподівана функція." + +msgid "errors.unexpected_bracket" +msgstr "Несподівана дужка." + +msgid "errors.unexpected_closing_bracket" +msgstr "Несподівана закриваюча дужка." + +msgid "errors.missing_closing_bracket" +msgstr "Відсутня закриваюча дужка." + +msgid "errors.unexpected_operator" +msgstr "Несподіваний оператор." + +msgid "errors.unexpected_comma" +msgstr "Несподівана кома." + +msgid "errors.unexpected_colon" +msgstr "Несподівана двокрапка." + +msgid "errors.unexpected_dot" +msgstr "Несподівана крапка." + +msgid "errors.unexpected_boolean" +msgstr "Несподіваний логічний вираз." + +msgid "errors.unexpected_string" +msgstr "Несподіваний рядок." + +msgid "errors.unexpected_number" +msgstr "Несподіване число." + +msgid "errors.unexpected_variable" +msgstr "Несподівана змінна." + +msgid "errors.invalid_index" +msgstr "Недійсний індекс." + +msgid "errors.unexpected_assignment" +msgstr "Несподіване призначення." + +msgid "errors.expected_when_or_else" +msgstr "Очікувався випадок «when» або «else»." + +msgid "errors.only_one_else_allowed" +msgstr "Для кожного «match» допускається лише один випадок «else»." + +msgid "errors.when_must_belong_to_match" +msgstr "Оператори «when» можуть з’являтися лише як дочірні операторів «match»." + +msgid "errors.concurrent_line_without_origin" +msgstr "Паралельні рядки потребують початкового рядка, який не починається з «|»." + +msgid "errors.goto_not_allowed_on_concurrect_lines" +msgstr "У паралельних діалогових рядках не допускаються Goto посилання." + +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Вкладені рядки діалогу можуть містити лише діалог." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Лише останній рядок вкладеного діалогу може містити перехід." + +msgid "errors.unknown" +msgstr "Невідомий синтаксис." + +msgid "update.available" +msgstr "Доступна версія {version}" + +msgid "update.is_available_for_download" +msgstr "Версія %s доступна для завантаження!" + +msgid "update.downloading" +msgstr "Завантаження..." + +msgid "update.download_update" +msgstr "Завантажити оновлення" + +msgid "update.needs_reload" +msgstr "Щоб установити оновлення, проєкт потрібно перезавантажити." + +msgid "update.reload_ok_button" +msgstr "Перезавантажити проєкт" + +msgid "update.reload_cancel_button" +msgstr "Пізніше" + +msgid "update.reload_project" +msgstr "Перезавантажити проєкт" + +msgid "update.release_notes" +msgstr "Читати зміни оновлення" + +msgid "update.success" +msgstr "Dialogue Manager тепер з версією {version}." + +msgid "update.failed" +msgstr "Виникла проблема із завантаженням оновлення." + +msgid "runtime.no_resource" +msgstr "Ресурс для діалогу не надано." + +msgid "runtime.no_content" +msgstr "«{file_path}» не має вмісту." + +msgid "runtime.errors" +msgstr "У тексті діалогу було виявлено помилки ({count})." + +msgid "runtime.error_detail" +msgstr "Рядок {line}: {message}" + +msgid "runtime.errors_see_details" +msgstr "У тексті діалогу було виявлено помилки ({count}). Див. детальніше у розділі «Вивід»." + +msgid "runtime.invalid_expression" +msgstr "«{expression}» не є допустимим виразом: {error}" + +msgid "runtime.array_index_out_of_bounds" +msgstr "Індекс {index} виходить за межі масиву «{array}»." + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "Ліва частина виразу не може бути призначена." + +msgid "runtime.key_not_found" +msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»" + +msgid "runtime.property_not_found" +msgstr "«{property}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." + +msgid "runtime.property_not_found_missing_export" +msgstr "«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." + +msgid "runtime.method_not_found" +msgstr "Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." + +msgid "runtime.signal_not_found" +msgstr "Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей." + +msgid "runtime.method_not_callable" +msgstr "«{method}» не є методом, який можна викликати в «{object}»" + +msgid "runtime.unknown_operator" +msgstr "Невідомий оператор." + +msgid "runtime.unknown_autoload" +msgstr "Схоже, «{autoload}» не є дійсним автозавантаженням." + +msgid "runtime.something_went_wrong" +msgstr "Щось пішло не так." + +msgid "runtime.expected_n_got_n_args" +msgstr "«{method}» було викликано з аргументами «{received}», але воно має лише «{expected}»." + +msgid "runtime.unsupported_array_type" +msgstr "Array[{type}] не підтримується у модифікаціях. Натомість використовуйте Array як тип." + +msgid "runtime.dialogue_balloon_missing_start_method" +msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»." + +msgid "runtime.top_level_states_share_name" +msgstr "Кілька станів верхнього рівня ({states}) мають спільну назву методу/властивості/сигналу «{key}». Для діалогу доступний лише перший випадок." + +msgid "translation_plugin.character_name" +msgstr "Ім’я персонажа" diff --git a/addons/dialogue_manager/l10n/zh.po b/addons/dialogue_manager/l10n/zh.po new file mode 100644 index 0000000..2a48a51 --- /dev/null +++ b/addons/dialogue_manager/l10n/zh.po @@ -0,0 +1,387 @@ +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: penghao123456、憨憨羊の宇航鸽鸽、ABShinri\n" +"Language: zh\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4\n" + +msgid "start_a_new_file" +msgstr "创建新文件" + +msgid "open_a_file" +msgstr "打开已有文件" + +msgid "open.open" +msgstr "打开……" + +msgid "open.no_recent_files" +msgstr "无历史记录" + +msgid "open.clear_recent_files" +msgstr "清空历史记录" + +msgid "save_all_files" +msgstr "保存所有文件" + +msgid "find_in_files" +msgstr "在文件中查找" + +msgid "test_dialogue" +msgstr "测试对话" + +msgid "search_for_text" +msgstr "查找……" + +msgid "insert" +msgstr "插入" + +msgid "translations" +msgstr "翻译" + +msgid "show_support" +msgstr "支持 Dialogue Manager" + +msgid "docs" +msgstr "文档" + +msgid "insert.wave_bbcode" +msgstr "波浪效果" + +msgid "insert.shake_bbcode" +msgstr "抖动效果" + +msgid "insert.typing_pause" +msgstr "输入间隔" + +msgid "insert.typing_speed_change" +msgstr "输入速度变更" + +msgid "insert.auto_advance" +msgstr "自动切行" + +msgid "insert.templates" +msgstr "模板" + +msgid "insert.title" +msgstr "标题" + +msgid "insert.dialogue" +msgstr "对话" + +msgid "insert.response" +msgstr "回复选项" + +msgid "insert.random_lines" +msgstr "随机行" + +msgid "insert.random_text" +msgstr "随机文本" + +msgid "insert.actions" +msgstr "操作" + +msgid "insert.jump" +msgstr "标题间跳转" + +msgid "insert.end_dialogue" +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" + +msgid "save_to_csv" +msgstr "生成 CSV" + +msgid "import_from_csv" +msgstr "从 CSV 导入" + +msgid "confirm_close" +msgstr "是否要保存到“{path}”?" + +msgid "confirm_close.save" +msgstr "保存" + +msgid "confirm_close.discard" +msgstr "不保存" + +msgid "buffer.save" +msgstr "保存" + +msgid "buffer.save_as" +msgstr "另存为……" + +msgid "buffer.close" +msgstr "关闭" + +msgid "buffer.close_all" +msgstr "全部关闭" + +msgid "buffer.close_other_files" +msgstr "关闭其他文件" + +msgid "buffer.copy_file_path" +msgstr "复制文件路径" + +msgid "buffer.show_in_filesystem" +msgstr "在 Godot 侧边栏中显示" + +msgid "n_of_n" +msgstr "第{index}个,共{total}个" + +msgid "search.find" +msgstr "查找:" + +msgid "search.find_all" +msgstr "查找全部..." + +msgid "search.placeholder" +msgstr "请输入查找的内容" + +msgid "search.replace_placeholder" +msgstr "请输入替换的内容" + +msgid "search.replace_selected" +msgstr "替换勾选" + +msgid "search.previous" +msgstr "查找上一个" + +msgid "search.next" +msgstr "查找下一个" + +msgid "search.match_case" +msgstr "大小写敏感" + +msgid "search.toggle_replace" +msgstr "替换" + +msgid "search.replace_with" +msgstr "替换为" + +msgid "search.replace" +msgstr "替换" + +msgid "search.replace_all" +msgstr "全部替换" + +msgid "files_list.filter" +msgstr "查找文件" + +msgid "titles_list.filter" +msgstr "查找标题" + +msgid "errors.key_not_found" +msgstr "键“{key}”未找到" + +msgid "errors.line_and_message" +msgstr "第{line}行第{colume}列发生错误:{message}" + +msgid "errors_in_script" +msgstr "你的脚本中存在错误。请修复错误,然后重试。" + +msgid "errors_with_build" +msgstr "请先解决 Dialogue 中的错误。" + +msgid "errors.import_errors" +msgstr "被导入的文件存在问题。" + +msgid "errors.already_imported" +msgstr "文件已被导入。" + +msgid "errors.duplicate_import" +msgstr "导入名不能重复。" + +msgid "errors.empty_title" +msgstr "标题名不能为空。" + +msgid "errors.duplicate_title" +msgstr "标题名不能重复。" + +msgid "errors.invalid_title_string" +msgstr "标题名无效。" + +msgid "errors.invalid_title_number" +msgstr "标题不能以数字开始。" + +msgid "errors.unknown_title" +msgstr "标题未定义。" + +msgid "errors.jump_to_invalid_title" +msgstr "标题名无效。" + +msgid "errors.title_has_no_content" +msgstr "目标标题为空。请替换为“=> END”。" + +msgid "errors.invalid_expression" +msgstr "表达式无效。" + +msgid "errors.unexpected_condition" +msgstr "未知条件。" + +msgid "errors.duplicate_id" +msgstr "ID 重复。" + +msgid "errors.missing_id" +msgstr "ID 不存在。" + +msgid "errors.invalid_indentation" +msgstr "缩进无效。" + +msgid "errors.condition_has_no_content" +msgstr "条件下方不能为空。" + +msgid "errors.incomplete_expression" +msgstr "不完整的表达式。" + +msgid "errors.invalid_expression_for_value" +msgstr "无效的赋值表达式。" + +msgid "errors.file_not_found" +msgstr "文件不存在。" + +msgid "errors.unexpected_end_of_expression" +msgstr "表达式 end 不应存在。" + +msgid "errors.unexpected_function" +msgstr "函数不应存在。" + +msgid "errors.unexpected_bracket" +msgstr "方括号不应存在。" + +msgid "errors.unexpected_closing_bracket" +msgstr "方括号不应存在。" + +msgid "errors.missing_closing_bracket" +msgstr "闭方括号不存在。" + +msgid "errors.unexpected_operator" +msgstr "操作符不应存在。" + +msgid "errors.unexpected_comma" +msgstr "逗号不应存在。" + +msgid "errors.unexpected_colon" +msgstr "冒号不应存在。" + +msgid "errors.unexpected_dot" +msgstr "句号不应存在。" + +msgid "errors.unexpected_boolean" +msgstr "布尔值不应存在。" + +msgid "errors.unexpected_string" +msgstr "字符串不应存在。" + +msgid "errors.unexpected_number" +msgstr "数字不应存在。" + +msgid "errors.unexpected_variable" +msgstr "标识符不应存在。" + +msgid "errors.invalid_index" +msgstr "索引无效。" + +msgid "errors.unexpected_assignment" +msgstr "不应在条件判断中使用 = ,应使用 == 。" + +msgid "errors.unknown" +msgstr "语法错误。" + +msgid "update.available" +msgstr "v{version} 更新可用。" + +msgid "update.is_available_for_download" +msgstr "v%s 已经可以下载。" + +msgid "update.downloading" +msgstr "正在下载更新……" + +msgid "update.download_update" +msgstr "下载" + +msgid "update.needs_reload" +msgstr "需要重新加载项目以应用更新。" + +msgid "update.reload_ok_button" +msgstr "重新加载" + +msgid "update.reload_cancel_button" +msgstr "暂不重新加载" + +msgid "update.reload_project" +msgstr "重新加载" + +msgid "update.release_notes" +msgstr "查看发行注记" + +msgid "update.success" +msgstr "v{version} 已成功安装并应用。" + +msgid "update.failed" +msgstr "更新失败。" + +msgid "runtime.no_resource" +msgstr "找不到资源。" + +msgid "runtime.no_content" +msgstr "资源“{file_path}”为空。" + +msgid "runtime.errors" +msgstr "文件中存在{errrors}个错误。" + +msgid "runtime.error_detail" +msgstr "第{index}行:{message}" + +msgid "runtime.errors_see_details" +msgstr "文件中存在{errrors}个错误。请查看调试输出。" + +msgid "runtime.invalid_expression" +msgstr "表达式“{expression}”无效:{error}" + +msgid "runtime.array_index_out_of_bounds" +msgstr "数组索引“{index}”越界。(数组名:“{array}”)" + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "表达式左侧的变量无法被赋值。" + +msgid "runtime.key_not_found" +msgstr "键“{key}”在字典“{dictionary}”中不存在。" + +msgid "runtime.property_not_found" +msgstr "“{property}”不存在。(全局变量:{states})" + +msgid "runtime.property_not_found_missing_export" +msgstr "“{property}”不存在。(全局变量:{states})你可能需要添加一个修饰词 [Export]。" + +msgid "runtime.method_not_found" +msgstr "“{method}”不存在。(全局变量:{states})" + +msgid "runtime.signal_not_found" +msgstr "“{sighal_name}”不存在。(全局变量:{states})" + +msgid "runtime.method_not_callable" +msgstr "{method}不是对象“{object}”上的函数。" + +msgid "runtime.unknown_operator" +msgstr "未知操作符。" + +msgid "runtime.something_went_wrong" +msgstr "有什么出错了。" diff --git a/addons/dialogue_manager/l10n/zh_TW.po b/addons/dialogue_manager/l10n/zh_TW.po new file mode 100644 index 0000000..1be9813 --- /dev/null +++ b/addons/dialogue_manager/l10n/zh_TW.po @@ -0,0 +1,387 @@ +msgid "" +msgstr "" +"Project-Id-Version: Dialogue Manager\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: 憨憨羊の宇航鴿鴿、ABShinri\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4\n" + +msgid "start_a_new_file" +msgstr "創建新檔案" + +msgid "open_a_file" +msgstr "開啟已有檔案" + +msgid "open.open" +msgstr "開啟……" + +msgid "open.no_recent_files" +msgstr "無歷史記錄" + +msgid "open.clear_recent_files" +msgstr "清空歷史記錄" + +msgid "save_all_files" +msgstr "儲存所有檔案" + +msgid "find_in_files" +msgstr "在檔案中查找" + +msgid "test_dialogue" +msgstr "測試對話" + +msgid "search_for_text" +msgstr "搜尋……" + +msgid "insert" +msgstr "插入" + +msgid "translations" +msgstr "翻譯" + +msgid "show_support" +msgstr "支援 Dialogue Manager" + +msgid "docs" +msgstr "文檔" + +msgid "insert.wave_bbcode" +msgstr "波浪特效" + +msgid "insert.shake_bbcode" +msgstr "震動特效" + +msgid "insert.typing_pause" +msgstr "輸入間隔" + +msgid "insert.typing_speed_change" +msgstr "輸入速度變更" + +msgid "insert.auto_advance" +msgstr "自動切行" + +msgid "insert.templates" +msgstr "模板" + +msgid "insert.title" +msgstr "標題" + +msgid "insert.dialogue" +msgstr "對話" + +msgid "insert.response" +msgstr "回覆選項" + +msgid "insert.random_lines" +msgstr "隨機行" + +msgid "insert.random_text" +msgstr "隨機文本" + +msgid "insert.actions" +msgstr "操作" + +msgid "insert.jump" +msgstr "標題間跳轉" + +msgid "insert.end_dialogue" +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" + +msgid "save_to_csv" +msgstr "生成 CSV" + +msgid "import_from_csv" +msgstr "從 CSV 匯入" + +msgid "confirm_close" +msgstr "是否要儲存到“{path}”?" + +msgid "confirm_close.save" +msgstr "儲存" + +msgid "confirm_close.discard" +msgstr "不儲存" + +msgid "buffer.save" +msgstr "儲存" + +msgid "buffer.save_as" +msgstr "儲存爲……" + +msgid "buffer.close" +msgstr "關閉" + +msgid "buffer.close_all" +msgstr "全部關閉" + +msgid "buffer.close_other_files" +msgstr "關閉其他檔案" + +msgid "buffer.copy_file_path" +msgstr "複製檔案位置" + +msgid "buffer.show_in_filesystem" +msgstr "在 Godot 側邊欄中顯示" + +msgid "n_of_n" +msgstr "第{index}個,共{total}個" + +msgid "search.find" +msgstr "搜尋:" + +msgid "search.find_all" +msgstr "搜尋全部..." + +msgid "search.placeholder" +msgstr "請輸入搜尋的內容" + +msgid "search.replace_placeholder" +msgstr "請輸入替換的內容" + +msgid "search.replace_selected" +msgstr "替換勾選" + +msgid "search.previous" +msgstr "搜尋上一個" + +msgid "search.next" +msgstr "搜尋下一個" + +msgid "search.match_case" +msgstr "大小寫敏感" + +msgid "search.toggle_replace" +msgstr "替換" + +msgid "search.replace_with" +msgstr "替換爲" + +msgid "search.replace" +msgstr "替換" + +msgid "search.replace_all" +msgstr "全部替換" + +msgid "files_list.filter" +msgstr "搜尋檔案" + +msgid "titles_list.filter" +msgstr "搜尋標題" + +msgid "errors.key_not_found" +msgstr "鍵“{key}”未找到" + +msgid "errors.line_and_message" +msgstr "第{line}行第{colume}列發生錯誤:{message}" + +msgid "errors_in_script" +msgstr "你的腳本中存在錯誤。請修復錯誤,然後重試。" + +msgid "errors_with_build" +msgstr "請先解決 Dialogue 中的錯誤。" + +msgid "errors.import_errors" +msgstr "被匯入的檔案存在問題。" + +msgid "errors.already_imported" +msgstr "檔案已被匯入。" + +msgid "errors.duplicate_import" +msgstr "匯入名不能重複。" + +msgid "errors.empty_title" +msgstr "標題名不能爲空。" + +msgid "errors.duplicate_title" +msgstr "標題名不能重複。" + +msgid "errors.invalid_title_string" +msgstr "標題名無效。" + +msgid "errors.invalid_title_number" +msgstr "標題不能以數字開始。" + +msgid "errors.unknown_title" +msgstr "標題未定義。" + +msgid "errors.jump_to_invalid_title" +msgstr "標題名無效。" + +msgid "errors.title_has_no_content" +msgstr "目標標題爲空。請替換爲“=> END”。" + +msgid "errors.invalid_expression" +msgstr "表達式無效。" + +msgid "errors.unexpected_condition" +msgstr "未知條件。" + +msgid "errors.duplicate_id" +msgstr "ID 重複。" + +msgid "errors.missing_id" +msgstr "ID 不存在。" + +msgid "errors.invalid_indentation" +msgstr "縮進無效。" + +msgid "errors.condition_has_no_content" +msgstr "條件下方不能爲空。" + +msgid "errors.incomplete_expression" +msgstr "不完整的表達式。" + +msgid "errors.invalid_expression_for_value" +msgstr "無效的賦值表達式。" + +msgid "errors.file_not_found" +msgstr "檔案不存在。" + +msgid "errors.unexpected_end_of_expression" +msgstr "表達式 end 不應存在。" + +msgid "errors.unexpected_function" +msgstr "函數不應存在。" + +msgid "errors.unexpected_bracket" +msgstr "方括號不應存在。" + +msgid "errors.unexpected_closing_bracket" +msgstr "方括號不應存在。" + +msgid "errors.missing_closing_bracket" +msgstr "閉方括號不存在。" + +msgid "errors.unexpected_operator" +msgstr "操作符不應存在。" + +msgid "errors.unexpected_comma" +msgstr "逗號不應存在。" + +msgid "errors.unexpected_colon" +msgstr "冒號不應存在。" + +msgid "errors.unexpected_dot" +msgstr "句號不應存在。" + +msgid "errors.unexpected_boolean" +msgstr "布爾值不應存在。" + +msgid "errors.unexpected_string" +msgstr "字符串不應存在。" + +msgid "errors.unexpected_number" +msgstr "數字不應存在。" + +msgid "errors.unexpected_variable" +msgstr "標識符不應存在。" + +msgid "errors.invalid_index" +msgstr "索引無效。" + +msgid "errors.unexpected_assignment" +msgstr "不應在條件判斷中使用 = ,應使用 == 。" + +msgid "errors.unknown" +msgstr "語法錯誤。" + +msgid "update.available" +msgstr "v{version} 更新可用。" + +msgid "update.is_available_for_download" +msgstr "v%s 已經可以下載。" + +msgid "update.downloading" +msgstr "正在下載更新……" + +msgid "update.download_update" +msgstr "下載" + +msgid "update.needs_reload" +msgstr "需要重新加載項目以套用更新。" + +msgid "update.reload_ok_button" +msgstr "重新加載" + +msgid "update.reload_cancel_button" +msgstr "暫不重新加載" + +msgid "update.reload_project" +msgstr "重新加載" + +msgid "update.release_notes" +msgstr "查看發行註記" + +msgid "update.success" +msgstr "v{version} 已成功安裝並套用。" + +msgid "update.failed" +msgstr "更新失敗。" + +msgid "runtime.no_resource" +msgstr "找不到資源。" + +msgid "runtime.no_content" +msgstr "資源“{file_path}”爲空。" + +msgid "runtime.errors" +msgstr "檔案中存在{errrors}個錯誤。" + +msgid "runtime.error_detail" +msgstr "第{index}行:{message}" + +msgid "runtime.errors_see_details" +msgstr "檔案中存在{errrors}個錯誤。請查看調試輸出。" + +msgid "runtime.invalid_expression" +msgstr "表達式“{expression}”無效:{error}" + +msgid "runtime.array_index_out_of_bounds" +msgstr "數組索引“{index}”越界。(數組名:“{array}”)" + +msgid "runtime.left_hand_size_cannot_be_assigned_to" +msgstr "表達式左側的變量無法被賦值。" + +msgid "runtime.key_not_found" +msgstr "鍵“{key}”在字典“{dictionary}”中不存在。" + +msgid "runtime.property_not_found" +msgstr "“{property}”不存在。(全局變量:{states})" + +msgid "runtime.method_not_found" +msgstr "“{method}”不存在。(全局變量:{states})" + +msgid "runtime.signal_not_found" +msgstr "“{sighal_name}”不存在。(全局變量:{states})" + +msgid "runtime.property_not_found_missing_export" +msgstr "“{property}”不存在。(全局變量:{states})你可能需要添加一個修飾詞 [Export]。" + +msgid "runtime.method_not_callable" +msgstr "{method}不是對象“{object}”上的函數。" + +msgid "runtime.unknown_operator" +msgstr "未知操作符。" + +msgid "runtime.something_went_wrong" +msgstr "有什麼出錯了。" diff --git a/addons/dialogue_manager/plugin.cfg b/addons/dialogue_manager/plugin.cfg new file mode 100644 index 0000000..c8fa08f --- /dev/null +++ b/addons/dialogue_manager/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Dialogue Manager" +description="A powerful nonlinear dialogue system" +author="Nathan Hoad" +version="3.7.1" +script="plugin.gd" diff --git a/addons/dialogue_manager/plugin.cfg.uid b/addons/dialogue_manager/plugin.cfg.uid new file mode 100644 index 0000000..312d0cf --- /dev/null +++ b/addons/dialogue_manager/plugin.cfg.uid @@ -0,0 +1 @@ +uid://hrny2utekhei diff --git a/addons/dialogue_manager/plugin.gd b/addons/dialogue_manager/plugin.gd new file mode 100644 index 0000000..a5d607f --- /dev/null +++ b/addons/dialogue_manager/plugin.gd @@ -0,0 +1,433 @@ +@tool +extends EditorPlugin + + +const MainView = preload("./views/main_view.tscn") + + +var import_plugin: DMImportPlugin +var export_plugin: DMExportPlugin +var inspector_plugin: DMInspectorPlugin +var translation_parser_plugin: DMTranslationParserPlugin +var main_view +var dialogue_cache: DMCache + + +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) + + DMSettings.prepare() + + dialogue_cache = DMCache.new() + Engine.set_meta("DMCache", dialogue_cache) + + import_plugin = DMImportPlugin.new() + add_import_plugin(import_plugin) + + export_plugin = DMExportPlugin.new() + add_export_plugin(export_plugin) + + inspector_plugin = DMInspectorPlugin.new() + add_inspector_plugin(inspector_plugin) + + translation_parser_plugin = DMTranslationParserPlugin.new() + add_translation_parser_plugin(translation_parser_plugin) + + main_view = MainView.instantiate() + EditorInterface.get_editor_main_screen().add_child(main_view) + _make_visible(false) + main_view.add_child(dialogue_cache) + + _update_localization() + + EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) + EditorInterface.get_file_system_dock().file_removed.connect(_on_file_removed) + + add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon) + + # Automatically swap the script on the example balloon depending on if dotnet is being used. + if not FileAccess.file_exists("res://tests/test_basic_dialogue.gd"): + var plugin_path: String = get_plugin_path() + var balloon_file_names: PackedStringArray = ["example_balloon.tscn", "small_example_balloon.tscn"] + for balloon_file_name: String in balloon_file_names: + var balloon_path: String = plugin_path + "/example_balloon/" + balloon_file_name + var balloon_content: String = FileAccess.get_file_as_string(balloon_path) + if "example_balloon.gd" in balloon_content and DMSettings.check_for_dotnet_solution(): + balloon_content = balloon_content \ + # Replace script path with the C# one + .replace("example_balloon.gd", "ExampleBalloon.cs") \ + # Replace script UID with the C# one + .replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs"))) + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(balloon_content) + balloon_file.close() + elif "ExampleBalloon.cs" in balloon_content and not DMSettings.check_for_dotnet_solution(): + balloon_content = balloon_content \ + # Replace script path with the GDScript one + .replace("ExampleBalloon.cs", "example_balloon.gd") \ + # Replace script UID with the GDScript one + .replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd"))) + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(balloon_content) + balloon_file.close() + + # Automatically make any changes to the known custom balloon if there is one. + var balloon_path: String = DMSettings.get_setting(DMSettings.BALLOON_PATH, "") + if balloon_path != "" and FileAccess.file_exists(balloon_path): + var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 + var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" + var example_balloon_path: String = get_plugin_path() + "/example_balloon/" + example_balloon_file_name + + var contents: String = FileAccess.get_file_as_string(balloon_path) + var has_changed: bool = false + + # Make sure the current balloon has a UID unique from the example balloon's + var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) + var balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(balloon_path)) + if example_balloon_uid == balloon_uid: + var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id()) + contents = contents.replace(example_balloon_uid, new_balloon_uid) + has_changed = true + + # Make sure the example balloon copy has the correct renaming of the responses menu + if "reponses" in contents: + contents = contents.replace("reponses", "responses") + has_changed = true + + # Save any changes + if has_changed: + var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) + balloon_file.store_string(contents) + balloon_file.close() + + +func _exit_tree() -> void: + remove_import_plugin(import_plugin) + import_plugin = null + + remove_export_plugin(export_plugin) + export_plugin = null + + remove_inspector_plugin(inspector_plugin) + inspector_plugin = null + + remove_translation_parser_plugin(translation_parser_plugin) + translation_parser_plugin = null + + if is_instance_valid(main_view): + main_view.queue_free() + + Engine.remove_meta("DialogueManagerPlugin") + Engine.remove_meta("DMCache") + + EditorInterface.get_file_system_dock().files_moved.disconnect(_on_files_moved) + EditorInterface.get_file_system_dock().file_removed.disconnect(_on_file_removed) + + remove_tool_menu_item("Create copy of dialogue example balloon...") + + +func _has_main_screen() -> bool: + return true + + +func _make_visible(next_visible: bool) -> void: + if is_instance_valid(main_view): + main_view.visible = next_visible + + +func _get_plugin_name() -> String: + return "Dialogue" + + +func _get_plugin_icon() -> Texture2D: + return load(get_plugin_path() + "/assets/icon.svg") + + +func _handles(object) -> bool: + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() + var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") + var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" + if object is DialogueResource and use_external_editor and DMSettings.get_user_value("open_in_external_editor", false): + var project_path: String = ProjectSettings.globalize_path("res://") + var file_path: String = ProjectSettings.globalize_path(object.resource_path) + OS.create_process(external_editor, [project_path, file_path]) + return false + + return object is DialogueResource + + +func _edit(object) -> void: + if is_instance_valid(main_view) and is_instance_valid(object): + main_view.open_resource(object) + + +func _apply_changes() -> void: + if is_instance_valid(main_view): + main_view.apply_changes() + _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() + + # Ignore errors in other files if we are just running the test scene + if DMSettings.get_user_value("is_running_test_scene", true): return true + + if dialogue_cache != null: + dialogue_cache.reimport_files() + + var files_with_errors = dialogue_cache.get_files_with_errors() + if files_with_errors.size() > 0: + for dialogue_file in files_with_errors: + push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) + EditorInterface.edit_resource(load(files_with_errors[0].path)) + main_view.show_build_error_dialog() + return false + + return true + + +## Get the shortcuts used by the plugin +func get_editor_shortcuts() -> Dictionary: + var shortcuts: Dictionary = { + toggle_comment = [ + _create_event("Ctrl+K"), + _create_event("Ctrl+Slash") + ], + delete_line = [ + _create_event("Ctrl+Shift+K") + ], + move_up = [ + _create_event("Alt+Up") + ], + move_down = [ + _create_event("Alt+Down") + ], + save = [ + _create_event("Ctrl+Alt+S") + ], + close_file = [ + _create_event("Ctrl+W") + ], + find_in_files = [ + _create_event("Ctrl+Shift+F") + ], + + run_test_scene = [ + _create_event("Ctrl+F5") + ], + text_size_increase = [ + _create_event("Ctrl+Equal") + ], + text_size_decrease = [ + _create_event("Ctrl+Minus") + ], + text_size_reset = [ + _create_event("Ctrl+0") + ] + } + + var paths = EditorInterface.get_editor_paths() + var settings + if FileAccess.file_exists(paths.get_config_dir() + "/editor_settings-4.3.tres"): + settings = load(paths.get_config_dir() + "/editor_settings-4.3.tres") + elif FileAccess.file_exists(paths.get_config_dir() + "/editor_settings-4.tres"): + settings = load(paths.get_config_dir() + "/editor_settings-4.tres") + else: + return shortcuts + + for s in settings.get("shortcuts"): + for key in shortcuts: + if s.name == "script_text_editor/%s" % key or s.name == "script_editor/%s" % key: + shortcuts[key] = [] + for event in s.shortcuts: + if event is InputEventKey: + shortcuts[key].append(event) + + return shortcuts + + +func _create_event(string: String) -> InputEventKey: + var event: InputEventKey = InputEventKey.new() + var bits = string.split("+") + event.keycode = OS.find_keycode_from_string(bits[bits.size() - 1]) + event.shift_pressed = bits.has("Shift") + event.alt_pressed = bits.has("Alt") + if bits.has("Ctrl") or bits.has("Command"): + event.command_or_control_autoremap = true + return event + + +## Get the editor shortcut that matches an event +func get_editor_shortcut(event: InputEventKey) -> String: + var shortcuts: Dictionary = get_editor_shortcuts() + for key in shortcuts: + for shortcut in shortcuts.get(key, []): + if event.as_text().split(" ")[0] == shortcut.as_text().split(" ")[0]: + return key + return "" + + +## Get the current version +func get_version() -> String: + var config: ConfigFile = ConfigFile.new() + config.load(get_plugin_path() + "/plugin.cfg") + return config.get_value("plugin", "version") + + +## Get the current path of the plugin +func get_plugin_path() -> String: + return get_script().resource_path.get_base_dir() + + +## Update references to a moved file +func update_import_paths(from_path: String, to_path: String) -> void: + dialogue_cache.move_file_path(from_path, to_path) + + # Reopen the file if it's already open + if main_view.current_file_path == from_path: + if to_path == "": + main_view.close_file(from_path) + else: + main_view.current_file_path = "" + main_view.open_file(to_path) + + # Update any other files that import the moved file + var dependents = dialogue_cache.get_files_with_dependency(from_path) + for dependent in dependents: + dependent.dependencies.remove_at(dependent.dependencies.find(from_path)) + dependent.dependencies.append(to_path) + + # Update the live buffer + if main_view.current_file_path == dependent.path: + main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path) + main_view.open_buffers[main_view.current_file_path].pristine_text = main_view.code_edit.text + + # Open the file and update the path + var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ) + var text = file.get_as_text().replace(from_path, to_path) + file.close() + + file = FileAccess.open(dependent.path, FileAccess.WRITE) + file.store_string(text) + file.close() + + +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 + var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", []) + var files_for_pot_changed: bool = false + for path in dialogue_files: + if not files_for_pot.has(path): + files_for_pot.append(path) + files_for_pot_changed = true + + # Remove any POT references that don't exist any more + for i in range(files_for_pot.size() - 1, -1, -1): + var file_for_pot: String = files_for_pot[i] + if file_for_pot.get_extension() == "dialogue" and not dialogue_files.has(file_for_pot): + files_for_pot.remove_at(i) + files_for_pot_changed = true + + # Update project settings if POT changed + if files_for_pot_changed: + ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot) + ProjectSettings.save() + + +### Callbacks + + +func _copy_dialogue_balloon() -> void: + var scale: float = EditorInterface.get_editor_scale() + var directory_dialog: FileDialog = FileDialog.new() + var label: Label = Label.new() + label.text = "Dialogue balloon files will be copied into chosen directory." + directory_dialog.get_vbox().add_child(label) + directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR + directory_dialog.min_size = Vector2(600, 500) * scale + directory_dialog.dir_selected.connect(func(path): + var plugin_path: String = get_plugin_path() + var is_dotnet: bool = DMSettings.check_for_dotnet_solution() + + var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn") + var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd") + + # Copy the balloon scene file and change the script reference + var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 + var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" + var example_balloon_path: String = plugin_path + "/example_balloon/" + example_balloon_file_name + var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd" + var example_balloon_script_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")) + var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) + + # Copy the script file + var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ) + var file_contents: String = file.get_as_text() + if is_dotnet: + file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon") + else: + file_contents = file_contents.replace("class_name DialogueManagerExampleBalloon ", "") + file = FileAccess.open(balloon_script_path, FileAccess.WRITE) + file.store_string(file_contents) + file.close() + var new_balloon_script_uid_raw: int = ResourceUID.create_id() + ResourceUID.add_id(new_balloon_script_uid_raw, balloon_script_path) + var new_balloon_script_uid: String = ResourceUID.id_to_text(new_balloon_script_uid_raw) + + # Save the new balloon + file_contents = FileAccess.get_file_as_string(example_balloon_path) + if "example_balloon.gd" in file_contents: + file_contents = file_contents.replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path) + else: + file_contents = file_contents.replace(plugin_path + "/example_balloon/ExampleBalloon.cs", balloon_script_path) + var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id()) + file_contents = file_contents.replace(example_balloon_uid, new_balloon_uid).replace(example_balloon_script_uid, new_balloon_script_uid) + file = FileAccess.open(balloon_path, FileAccess.WRITE) + file.store_string(file_contents) + file.close() + + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", balloon_path) + + DMSettings.set_setting(DMSettings.BALLOON_PATH, balloon_path) + + directory_dialog.queue_free() + ) + EditorInterface.get_base_control().add_child(directory_dialog) + directory_dialog.popup_centered() + + +### Signals + + +func _on_files_moved(old_file: String, new_file: String) -> void: + update_import_paths(old_file, new_file) + DMSettings.move_recent_file(old_file, new_file) + + +func _on_file_removed(file: String) -> void: + update_import_paths(file, "") + if is_instance_valid(main_view): + main_view.close_file(file) + _update_localization() diff --git a/addons/dialogue_manager/plugin.gd.uid b/addons/dialogue_manager/plugin.gd.uid new file mode 100644 index 0000000..40573b0 --- /dev/null +++ b/addons/dialogue_manager/plugin.gd.uid @@ -0,0 +1 @@ +uid://bpv426rpvrafa diff --git a/addons/dialogue_manager/settings.gd b/addons/dialogue_manager/settings.gd new file mode 100644 index 0000000..01ed408 --- /dev/null +++ b/addons/dialogue_manager/settings.gd @@ -0,0 +1,352 @@ +@tool +class_name DMSettings extends Node + + +#region Editor + + + +## Wrap lines in the dialogue editor. +const WRAP_LONG_LINES = "editor/wrap_long_lines" +## The template to start new dialogue files with. +const NEW_FILE_TEMPLATE = "editor/new_file_template" + +## Show lines without statis IDs as errors. +const MISSING_TRANSLATIONS_ARE_ERRORS = "editor/translations/missing_translations_are_errors" +## Include character names in the list of translatable strings. +const INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST = "editor/translations/include_characters_in_translatable_strings_list" +## The default locale to use when exporting CSVs +const DEFAULT_CSV_LOCALE = "editor/translations/default_csv_locale" +## Any extra CSV locales to append to the exported translation CSV +const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales" +## Includes a "_character" column in CSV exports. +const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports" +## Includes a "_notes" column in CSV exports +const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_exports" +## 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" +## The names of any autoloads to shortcut into all dialogue files (so you don't have to write `using SomeGlobal` in each file). +const STATE_AUTOLOAD_SHORTCUTS = "runtime/state_autoload_shortcuts" +## Check for possible naming conflicts in state shortcuts. +const WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS = "runtime/warn_about_method_property_or_signal_name_conflicts" + +## Bypass any missing state when running dialogue. +const IGNORE_MISSING_STATE_VALUES = "runtime/advanced/ignore_missing_state_values" +## Whether or not the project is utilising dotnet. +const USES_DOTNET = "runtime/advanced/uses_dotnet" +## 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" + +static var SETTINGS_CONFIGURATION = { + WRAP_LONG_LINES: { + value = false, + type = TYPE_BOOL, + }, + NEW_FILE_TEMPLATE: { + value = "~ start\nNathan: [[Hi|Hello|Howdy]], this is some dialogue.\nNathan: Here are some choices.\n- First one\n\tNathan: You picked the first one.\n- Second one\n\tNathan: You picked the second one.\n- Start again => start\n- End the conversation => END\nNathan: For more information see the online documentation.\n=> END", + type = TYPE_STRING, + hint = PROPERTY_HINT_MULTILINE_TEXT, + }, + + MISSING_TRANSLATIONS_ARE_ERRORS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST: { + value = true, + type = TYPE_BOOL, + }, + DEFAULT_CSV_LOCALE: { + value = "en", + type = TYPE_STRING, + hint = PROPERTY_HINT_LOCALE_ID, + }, + EXTRA_CSV_LOCALES: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], + is_advanced = true + }, + INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + INCLUDE_NOTES_IN_TRANSLATION_EXPORTS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + UPDATE_POT_FILES_AUTOMATICALLY: { + value = true, + type = TYPE_BOOL, + is_advanced = true + }, + + CUSTOM_TEST_SCENE_PATH: { + value = preload("./test_scene.tscn").resource_path, + type = TYPE_STRING, + hint = PROPERTY_HINT_FILE, + is_advanced = true + }, + 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 = "", + type = TYPE_STRING, + hint = PROPERTY_HINT_FILE, + }, + 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, + type = TYPE_BOOL, + is_advanced = true + }, + + IGNORE_MISSING_STATE_VALUES: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, + USES_DOTNET: { + value = false, + type = TYPE_BOOL, + is_hidden = true + }, + 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 + }, +} + + +static func prepare() -> void: + var should_save_settings: bool = false + + # Remap any old settings into their new keys + var legacy_map: Dictionary = { + states = STATE_AUTOLOAD_SHORTCUTS, + missing_translations_are_errors = MISSING_TRANSLATIONS_ARE_ERRORS, + export_characters_in_translation = INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, + wrap_lines = WRAP_LONG_LINES, + new_with_template = null, + new_template = NEW_FILE_TEMPLATE, + include_all_responses = null, + ignore_missing_state_values = IGNORE_MISSING_STATE_VALUES, + custom_test_scene_path = CUSTOM_TEST_SCENE_PATH, + default_csv_locale = DEFAULT_CSV_LOCALE, + balloon_path = BALLOON_PATH, + create_lines_for_responses_with_characters = null, + include_character_in_translation_exports = INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, + include_notes_in_translation_exports = INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, + uses_dotnet = USES_DOTNET, + try_suppressing_startup_unsaved_indicator = null + } + + for legacy_key: String in legacy_map: + if ProjectSettings.has_setting("dialogue_manager/general/%s" % legacy_key): + should_save_settings = true + # Remove the old setting + var value = ProjectSettings.get_setting("dialogue_manager/general/%s" % legacy_key) + ProjectSettings.set_setting("dialogue_manager/general/%s" % legacy_key, null) + if legacy_map.get(legacy_key) != null: + prints("Migrating Dialogue Manager setting %s to %s with value %s" % [legacy_key, legacy_map.get(legacy_key), str(value)]) + ProjectSettings.set_setting("dialogue_manager/%s" % [legacy_map.get(legacy_key)], value) + + # Set up initial settings + for key: String in SETTINGS_CONFIGURATION: + var setting_config: Dictionary = SETTINGS_CONFIGURATION[key] + var setting_name: String = "dialogue_manager/%s" % key + if not ProjectSettings.has_setting(setting_name): + ProjectSettings.set_setting(setting_name, setting_config.value) + ProjectSettings.set_initial_value(setting_name, setting_config.value) + ProjectSettings.add_property_info({ + "name" = setting_name, + "type" = setting_config.type, + "hint" = setting_config.get("hint", PROPERTY_HINT_NONE), + "hint_string" = setting_config.get("hint_string", "") + }) + ProjectSettings.set_as_basic(setting_name, not setting_config.has("is_advanced")) + ProjectSettings.set_as_internal(setting_name, setting_config.has("is_hidden")) + + if should_save_settings: + ProjectSettings.save() + + +static func set_setting(key: String, value) -> void: + if get_setting(key, value) != value: + ProjectSettings.set_setting("dialogue_manager/%s" % key, value) + ProjectSettings.set_initial_value("dialogue_manager/%s" % key, SETTINGS_CONFIGURATION[key].value) + ProjectSettings.save() + + +static func get_setting(key: String, default): + if ProjectSettings.has_setting("dialogue_manager/%s" % key): + return ProjectSettings.get_setting("dialogue_manager/%s" % key) + else: + return default + + +static func get_settings(only_keys: PackedStringArray = []) -> Dictionary: + var settings: Dictionary = {} + for key in SETTINGS_CONFIGURATION.keys(): + if only_keys.is_empty() or key in only_keys: + settings[key] = get_setting(key, SETTINGS_CONFIGURATION[key].value) + return settings + + +#endregion + +#region User + + +static func get_user_config() -> Dictionary: + var user_config: Dictionary = { + check_for_updates = true, + just_refreshed = null, + recent_files = [], + reopen_files = [], + most_recent_reopen_file = "", + file_meta = {}, + run_title = "", + run_resource_path = "", + is_running_test_scene = false, + has_dotnet_solution = false, + open_in_external_editor = false + } + + if FileAccess.file_exists(DMConstants.USER_CONFIG_PATH): + var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.READ) + user_config.merge(JSON.parse_string(file.get_as_text()), true) + + return user_config + + +static func save_user_config(user_config: Dictionary) -> void: + var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.WRITE) + file.store_string(JSON.stringify(user_config)) + + +static func set_user_value(key: String, value) -> void: + var user_config: Dictionary = get_user_config() + user_config[key] = value + save_user_config(user_config) + + +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: + recent_files.erase(path) + recent_files.insert(0, path) + set_user_value("recent_files", recent_files) + + +static func move_recent_file(from_path: String, to_path: String) -> void: + var recent_files: Array = get_user_value("recent_files", []) + for i in range(0, recent_files.size()): + if recent_files[i] == from_path: + recent_files[i] = to_path + set_user_value("recent_files", recent_files) + + +static func remove_recent_file(path: String) -> void: + var recent_files: Array = get_user_value("recent_files", []) + if path in recent_files: + recent_files.erase(path) + set_user_value("recent_files", recent_files) + + +static func get_recent_files() -> Array: + return get_user_value("recent_files", []) + + +static func clear_recent_files() -> void: + set_user_value("recent_files", []) + set_user_value("carets", {}) + + +static func set_caret(path: String, cursor: Vector2) -> void: + 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 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 + if ProjectSettings.has_setting("dotnet/project/solution_directory"): + var directory: String = ProjectSettings.get("dotnet/project/solution_directory") + var file_name: String = ProjectSettings.get("dotnet/project/assembly_name") + has_dotnet_solution = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) + set_setting(DMSettings.USES_DOTNET, has_dotnet_solution) + return has_dotnet_solution + + return get_setting(DMSettings.USES_DOTNET, false) + + +#endregion diff --git a/addons/dialogue_manager/settings.gd.uid b/addons/dialogue_manager/settings.gd.uid new file mode 100644 index 0000000..c93da98 --- /dev/null +++ b/addons/dialogue_manager/settings.gd.uid @@ -0,0 +1 @@ +uid://ce1nk88365m52 diff --git a/addons/dialogue_manager/test_scene.gd b/addons/dialogue_manager/test_scene.gd new file mode 100644 index 0000000..e621667 --- /dev/null +++ b/addons/dialogue_manager/test_scene.gd @@ -0,0 +1,37 @@ +class_name BaseDialogueTestScene extends Node2D + + +const DialogueSettings = preload("./settings.gd") +const DialogueResource = preload("./dialogue_resource.gd") + + +@onready var title: String = DialogueSettings.get_user_value("run_title") +@onready var resource: DialogueResource = load(DialogueSettings.get_user_value("run_resource_path")) + + +func _ready(): + if not Engine.is_embedded_in_editor: + var window: Window = get_viewport() + var screen_index: int = DisplayServer.get_primary_screen() + window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5 + window.mode = Window.MODE_WINDOWED + + # Normally you can just call DialogueManager directly but doing so before the plugin has been + # enabled in settings will throw a compiler error here so I'm using `get_singleton` instead. + var dialogue_manager = Engine.get_singleton("DialogueManager") + dialogue_manager.dialogue_ended.connect(_on_dialogue_ended) + dialogue_manager.show_dialogue_balloon(resource, title if not title.is_empty() else resource.first_title) + + +func _enter_tree() -> void: + DialogueSettings.set_user_value("is_running_test_scene", false) + + +#region Signals + + +func _on_dialogue_ended(_resource: DialogueResource): + get_tree().quit() + + +#endregion diff --git a/addons/dialogue_manager/test_scene.gd.uid b/addons/dialogue_manager/test_scene.gd.uid new file mode 100644 index 0000000..1bee7a1 --- /dev/null +++ b/addons/dialogue_manager/test_scene.gd.uid @@ -0,0 +1 @@ +uid://c8e16qdgu40wo diff --git a/addons/dialogue_manager/test_scene.tscn b/addons/dialogue_manager/test_scene.tscn new file mode 100644 index 0000000..f0786ba --- /dev/null +++ b/addons/dialogue_manager/test_scene.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ugd552efvil0"] + +[ext_resource type="Script" uid="uid://c8e16qdgu40wo" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"] + +[node name="TestScene" type="Node2D"] +script = ExtResource("1_yupoh") diff --git a/addons/dialogue_manager/utilities/builtins.gd b/addons/dialogue_manager/utilities/builtins.gd new file mode 100644 index 0000000..1f8f8ba --- /dev/null +++ b/addons/dialogue_manager/utilities/builtins.gd @@ -0,0 +1,505 @@ +extends Object + + +const DialogueConstants = preload("../constants.gd") + +const SUPPORTED_BUILTIN_TYPES = [ + TYPE_STRING, + TYPE_STRING_NAME, + TYPE_ARRAY, + TYPE_PACKED_STRING_ARRAY, + TYPE_VECTOR2, + TYPE_VECTOR3, + TYPE_VECTOR4, + TYPE_DICTIONARY, + TYPE_QUATERNION, + TYPE_COLOR, + TYPE_SIGNAL, + TYPE_CALLABLE +] + + +static var resolve_method_error: Error = OK + + +static func is_supported(thing, with_method: String = "") -> bool: + if not typeof(thing) in SUPPORTED_BUILTIN_TYPES: return false + + # If given a Dictionary and a method then make sure it's a known Dictionary method. + if typeof(thing) == TYPE_DICTIONARY and with_method != "": + return with_method in [ + &"clear", + &"duplicate", + &"erase", + &"find_key", + &"get", + &"get_or_add", + &"has", + &"has_all", + &"hash", + &"is_empty", + &"is_read_only", + &"keys", + &"make_read_only", + &"merge", + &"merged", + &"recursive_equal", + &"size", + &"values"] + + return true + + +static func resolve_property(builtin, property: String): + match typeof(builtin): + TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY, TYPE_DICTIONARY, TYPE_QUATERNION, TYPE_STRING, TYPE_STRING_NAME: + return builtin[property] + + # Some types have constants that we need to manually resolve + + TYPE_VECTOR2: + return resolve_vector2_property(builtin, property) + TYPE_VECTOR3: + return resolve_vector3_property(builtin, property) + TYPE_VECTOR4: + return resolve_vector4_property(builtin, property) + TYPE_COLOR: + return resolve_color_property(builtin, property) + + +static func resolve_method(thing, method_name: String, args: Array): + resolve_method_error = OK + + # Resolve static methods manually + match typeof(thing): + TYPE_VECTOR2: + match method_name: + "from_angle": + return Vector2.from_angle(args[0]) + + TYPE_COLOR: + match method_name: + "from_hsv": + return Color.from_hsv(args[0], args[1], args[2]) if args.size() == 3 else Color.from_hsv(args[0], args[1], args[2], args[3]) + "from_ok_hsl": + return Color.from_ok_hsl(args[0], args[1], args[2]) if args.size() == 3 else Color.from_ok_hsl(args[0], args[1], args[2], args[3]) + "from_rgbe9995": + return Color.from_rgbe9995(args[0]) + "from_string": + return Color.from_string(args[0], args[1]) + + TYPE_QUATERNION: + match method_name: + "from_euler": + return Quaternion.from_euler(args[0]) + + # Anything else can be evaulatated automatically + var references: Array = ["thing"] + for i in range(0, args.size()): + references.append("arg%d" % i) + var expression = Expression.new() + if expression.parse("thing.%s(%s)" % [method_name, ",".join(references.slice(1))], references) != OK: + assert(false, expression.get_error_text()) + var result = expression.execute([thing] + args, null, false) + if expression.has_execute_failed(): + resolve_method_error = ERR_CANT_RESOLVE + return null + + return result + + +static func has_resolve_method_failed() -> bool: + return resolve_method_error != OK + + +static func resolve_color_property(color: Color, property: String): + match property: + "ALICE_BLUE": + return Color.ALICE_BLUE + "ANTIQUE_WHITE": + return Color.ANTIQUE_WHITE + "AQUA": + return Color.AQUA + "AQUAMARINE": + return Color.AQUAMARINE + "AZURE": + return Color.AZURE + "BEIGE": + return Color.BEIGE + "BISQUE": + return Color.BISQUE + "BLACK": + return Color.BLACK + "BLANCHED_ALMOND": + return Color.BLANCHED_ALMOND + "BLUE": + return Color.BLUE + "BLUE_VIOLET": + return Color.BLUE_VIOLET + "BROWN": + return Color.BROWN + "BURLYWOOD": + return Color.BURLYWOOD + "CADET_BLUE": + return Color.CADET_BLUE + "CHARTREUSE": + return Color.CHARTREUSE + "CHOCOLATE": + return Color.CHOCOLATE + "CORAL": + return Color.CORAL + "CORNFLOWER_BLUE": + return Color.CORNFLOWER_BLUE + "CORNSILK": + return Color.CORNSILK + "CRIMSON": + return Color.CRIMSON + "CYAN": + return Color.CYAN + "DARK_BLUE": + return Color.DARK_BLUE + "DARK_CYAN": + return Color.DARK_CYAN + "DARK_GOLDENROD": + return Color.DARK_GOLDENROD + "DARK_GRAY": + return Color.DARK_GRAY + "DARK_GREEN": + return Color.DARK_GREEN + "DARK_KHAKI": + return Color.DARK_KHAKI + "DARK_MAGENTA": + return Color.DARK_MAGENTA + "DARK_OLIVE_GREEN": + return Color.DARK_OLIVE_GREEN + "DARK_ORANGE": + return Color.DARK_ORANGE + "DARK_ORCHID": + return Color.DARK_ORCHID + "DARK_RED": + return Color.DARK_RED + "DARK_SALMON": + return Color.DARK_SALMON + "DARK_SEA_GREEN": + return Color.DARK_SEA_GREEN + "DARK_SLATE_BLUE": + return Color.DARK_SLATE_BLUE + "DARK_SLATE_GRAY": + return Color.DARK_SLATE_GRAY + "DARK_TURQUOISE": + return Color.DARK_TURQUOISE + "DARK_VIOLET": + return Color.DARK_VIOLET + "DEEP_PINK": + return Color.DEEP_PINK + "DEEP_SKY_BLUE": + return Color.DEEP_SKY_BLUE + "DIM_GRAY": + return Color.DIM_GRAY + "DODGER_BLUE": + return Color.DODGER_BLUE + "FIREBRICK": + return Color.FIREBRICK + "FLORAL_WHITE": + return Color.FLORAL_WHITE + "FOREST_GREEN": + return Color.FOREST_GREEN + "FUCHSIA": + return Color.FUCHSIA + "GAINSBORO": + return Color.GAINSBORO + "GHOST_WHITE": + return Color.GHOST_WHITE + "GOLD": + return Color.GOLD + "GOLDENROD": + return Color.GOLDENROD + "GRAY": + return Color.GRAY + "GREEN": + return Color.GREEN + "GREEN_YELLOW": + return Color.GREEN_YELLOW + "HONEYDEW": + return Color.HONEYDEW + "HOT_PINK": + return Color.HOT_PINK + "INDIAN_RED": + return Color.INDIAN_RED + "INDIGO": + return Color.INDIGO + "IVORY": + return Color.IVORY + "KHAKI": + return Color.KHAKI + "LAVENDER": + return Color.LAVENDER + "LAVENDER_BLUSH": + return Color.LAVENDER_BLUSH + "LAWN_GREEN": + return Color.LAWN_GREEN + "LEMON_CHIFFON": + return Color.LEMON_CHIFFON + "LIGHT_BLUE": + return Color.LIGHT_BLUE + "LIGHT_CORAL": + return Color.LIGHT_CORAL + "LIGHT_CYAN": + return Color.LIGHT_CYAN + "LIGHT_GOLDENROD": + return Color.LIGHT_GOLDENROD + "LIGHT_GRAY": + return Color.LIGHT_GRAY + "LIGHT_GREEN": + return Color.LIGHT_GREEN + "LIGHT_PINK": + return Color.LIGHT_PINK + "LIGHT_SALMON": + return Color.LIGHT_SALMON + "LIGHT_SEA_GREEN": + return Color.LIGHT_SEA_GREEN + "LIGHT_SKY_BLUE": + return Color.LIGHT_SKY_BLUE + "LIGHT_SLATE_GRAY": + return Color.LIGHT_SLATE_GRAY + "LIGHT_STEEL_BLUE": + return Color.LIGHT_STEEL_BLUE + "LIGHT_YELLOW": + return Color.LIGHT_YELLOW + "LIME": + return Color.LIME + "LIME_GREEN": + return Color.LIME_GREEN + "LINEN": + return Color.LINEN + "MAGENTA": + return Color.MAGENTA + "MAROON": + return Color.MAROON + "MEDIUM_AQUAMARINE": + return Color.MEDIUM_AQUAMARINE + "MEDIUM_BLUE": + return Color.MEDIUM_BLUE + "MEDIUM_ORCHID": + return Color.MEDIUM_ORCHID + "MEDIUM_PURPLE": + return Color.MEDIUM_PURPLE + "MEDIUM_SEA_GREEN": + return Color.MEDIUM_SEA_GREEN + "MEDIUM_SLATE_BLUE": + return Color.MEDIUM_SLATE_BLUE + "MEDIUM_SPRING_GREEN": + return Color.MEDIUM_SPRING_GREEN + "MEDIUM_TURQUOISE": + return Color.MEDIUM_TURQUOISE + "MEDIUM_VIOLET_RED": + return Color.MEDIUM_VIOLET_RED + "MIDNIGHT_BLUE": + return Color.MIDNIGHT_BLUE + "MINT_CREAM": + return Color.MINT_CREAM + "MISTY_ROSE": + return Color.MISTY_ROSE + "MOCCASIN": + return Color.MOCCASIN + "NAVAJO_WHITE": + return Color.NAVAJO_WHITE + "NAVY_BLUE": + return Color.NAVY_BLUE + "OLD_LACE": + return Color.OLD_LACE + "OLIVE": + return Color.OLIVE + "OLIVE_DRAB": + return Color.OLIVE_DRAB + "ORANGE": + return Color.ORANGE + "ORANGE_RED": + return Color.ORANGE_RED + "ORCHID": + return Color.ORCHID + "PALE_GOLDENROD": + return Color.PALE_GOLDENROD + "PALE_GREEN": + return Color.PALE_GREEN + "PALE_TURQUOISE": + return Color.PALE_TURQUOISE + "PALE_VIOLET_RED": + return Color.PALE_VIOLET_RED + "PAPAYA_WHIP": + return Color.PAPAYA_WHIP + "PEACH_PUFF": + return Color.PEACH_PUFF + "PERU": + return Color.PERU + "PINK": + return Color.PINK + "PLUM": + return Color.PLUM + "POWDER_BLUE": + return Color.POWDER_BLUE + "PURPLE": + return Color.PURPLE + "REBECCA_PURPLE": + return Color.REBECCA_PURPLE + "RED": + return Color.RED + "ROSY_BROWN": + return Color.ROSY_BROWN + "ROYAL_BLUE": + return Color.ROYAL_BLUE + "SADDLE_BROWN": + return Color.SADDLE_BROWN + "SALMON": + return Color.SALMON + "SANDY_BROWN": + return Color.SANDY_BROWN + "SEA_GREEN": + return Color.SEA_GREEN + "SEASHELL": + return Color.SEASHELL + "SIENNA": + return Color.SIENNA + "SILVER": + return Color.SILVER + "SKY_BLUE": + return Color.SKY_BLUE + "SLATE_BLUE": + return Color.SLATE_BLUE + "SLATE_GRAY": + return Color.SLATE_GRAY + "SNOW": + return Color.SNOW + "SPRING_GREEN": + return Color.SPRING_GREEN + "STEEL_BLUE": + return Color.STEEL_BLUE + "TAN": + return Color.TAN + "TEAL": + return Color.TEAL + "THISTLE": + return Color.THISTLE + "TOMATO": + return Color.TOMATO + "TRANSPARENT": + return Color.TRANSPARENT + "TURQUOISE": + return Color.TURQUOISE + "VIOLET": + return Color.VIOLET + "WEB_GRAY": + return Color.WEB_GRAY + "WEB_GREEN": + return Color.WEB_GREEN + "WEB_MAROON": + return Color.WEB_MAROON + "WEB_PURPLE": + return Color.WEB_PURPLE + "WHEAT": + return Color.WHEAT + "WHITE": + return Color.WHITE + "WHITE_SMOKE": + return Color.WHITE_SMOKE + "YELLOW": + return Color.YELLOW + "YELLOW_GREEN": + return Color.YELLOW_GREEN + + return color[property] + + +static func resolve_vector2_property(vector: Vector2, property: String): + match property: + "AXIS_X": + return Vector2.AXIS_X + "AXIS_Y": + return Vector2.AXIS_Y + "ZERO": + return Vector2.ZERO + "ONE": + return Vector2.ONE + "INF": + return Vector2.INF + "LEFT": + return Vector2.LEFT + "RIGHT": + return Vector2.RIGHT + "UP": + return Vector2.UP + "DOWN": + return Vector2.DOWN + + "DOWN_LEFT": + return Vector2(-1, 1) + "DOWN_RIGHT": + return Vector2(1, 1) + "UP_LEFT": + return Vector2(-1, -1) + "UP_RIGHT": + return Vector2(1, -1) + + return vector[property] + + +static func resolve_vector3_property(vector: Vector3, property: String): + match property: + "AXIS_X": + return Vector3.AXIS_X + "AXIS_Y": + return Vector3.AXIS_Y + "AXIS_Z": + return Vector3.AXIS_Z + "ZERO": + return Vector3.ZERO + "ONE": + return Vector3.ONE + "INF": + return Vector3.INF + "LEFT": + return Vector3.LEFT + "RIGHT": + return Vector3.RIGHT + "UP": + return Vector3.UP + "DOWN": + return Vector3.DOWN + "FORWARD": + return Vector3.FORWARD + "BACK": + return Vector3.BACK + "MODEL_LEFT": + return Vector3(1, 0, 0) + "MODEL_RIGHT": + return Vector3(-1, 0, 0) + "MODEL_TOP": + return Vector3(0, 1, 0) + "MODEL_BOTTOM": + return Vector3(0, -1, 0) + "MODEL_FRONT": + return Vector3(0, 0, 1) + "MODEL_REAR": + return Vector3(0, 0, -1) + + return vector[property] + + +static func resolve_vector4_property(vector: Vector4, property: String): + match property: + "AXIS_X": + return Vector4.AXIS_X + "AXIS_Y": + return Vector4.AXIS_Y + "AXIS_Z": + return Vector4.AXIS_Z + "AXIS_W": + return Vector4.AXIS_W + "ZERO": + return Vector4.ZERO + "ONE": + return Vector4.ONE + "INF": + return Vector4.INF + + return vector[property] diff --git a/addons/dialogue_manager/utilities/builtins.gd.uid b/addons/dialogue_manager/utilities/builtins.gd.uid new file mode 100644 index 0000000..af8698c --- /dev/null +++ b/addons/dialogue_manager/utilities/builtins.gd.uid @@ -0,0 +1 @@ +uid://bnfhuubdv5k20 diff --git a/addons/dialogue_manager/utilities/dialogue_cache.gd b/addons/dialogue_manager/utilities/dialogue_cache.gd new file mode 100644 index 0000000..33fc5ac --- /dev/null +++ b/addons/dialogue_manager/utilities/dialogue_cache.gd @@ -0,0 +1,171 @@ +class_name DMCache extends Node + + +signal file_content_changed(path: String, new_content: String) + + +# Keep track of errors and dependencies +# { +# = { +# path = , +# dependencies = [, ], +# errors = [, ] +# } +# } +var _cache: Dictionary = {} + +var _update_dependency_timer: Timer = Timer.new() +var _update_dependency_paths: PackedStringArray = [] + +var _files_marked_for_reimport: PackedStringArray = [] + + +func _ready() -> void: + add_child(_update_dependency_timer) + _update_dependency_timer.timeout.connect(_on_update_dependency_timeout) + + _build_cache() + + +func mark_files_for_reimport(files: PackedStringArray) -> void: + for file in files: + if not _files_marked_for_reimport.has(file): + _files_marked_for_reimport.append(file) + + +func reimport_files(and_files: PackedStringArray = []) -> void: + for file in and_files: + if not _files_marked_for_reimport.has(file): + _files_marked_for_reimport.append(file) + + if _files_marked_for_reimport.is_empty(): return + + EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) + _files_marked_for_reimport.clear() + + +## Add a dialogue file to the cache. +func add_file(path: String, compile_result: DMCompilerResult = null) -> void: + _cache[path] = { + path = path, + dependencies = [], + errors = [] + } + + if compile_result != null: + _cache[path].dependencies = Array(compile_result.imported_paths).filter(func(d): return d != path) + _cache[path].compiled_at = Time.get_ticks_msec() + + # If this is a fresh cache entry, check for dependencies + if compile_result == null and not _update_dependency_paths.has(path): + queue_updating_dependencies(path) + + +## Get the file paths in the cache +func get_files() -> PackedStringArray: + return _cache.keys() + + +## Check if a file is known to the cache +func has_file(path: String) -> bool: + return _cache.has(path) + + +## Remember any errors in a dialogue file +func add_errors_to_file(path: String, errors: Array[Dictionary]) -> void: + if _cache.has(path): + _cache[path].errors = errors + else: + _cache[path] = { + path = path, + resource_path = "", + dependencies = [], + errors = errors + } + + +## Get a list of files that have errors +func get_files_with_errors() -> Array[Dictionary]: + var files_with_errors: Array[Dictionary] = [] + for dialogue_file in _cache.values(): + if dialogue_file and dialogue_file.errors.size() > 0: + files_with_errors.append(dialogue_file) + return files_with_errors + + +## Queue a file to have its dependencies checked +func queue_updating_dependencies(of_path: String) -> void: + _update_dependency_timer.stop() + if not _update_dependency_paths.has(of_path): + _update_dependency_paths.append(of_path) + _update_dependency_timer.start(0.5) + + +## Update any references to a file path that has moved +func move_file_path(from_path: String, to_path: String) -> void: + if not _cache.has(from_path): return + + if to_path != "": + _cache[to_path] = _cache[from_path].duplicate() + _cache.erase(from_path) + + +## Get every dialogue file that imports on a file of a given path +func get_files_with_dependency(imported_path: String) -> Array: + return _cache.values().filter(func(d): return d.dependencies.has(imported_path)) + + +## Get any paths that are dependent on a given path +func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray: + return get_files_with_dependency(on_path) \ + .filter(func(d): return Time.get_ticks_msec() - d.get("compiled_at", 0) > 3000) \ + .map(func(d): return d.path) + + +# Build the initial cache for dialogue files +func _build_cache() -> void: + var current_files: PackedStringArray = _get_dialogue_files_in_filesystem() + for file in current_files: + add_file(file) + + +# Recursively find any dialogue files in a directory +func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray: + var files: PackedStringArray = [] + + if DirAccess.dir_exists_absolute(path): + var dir = DirAccess.open(path) + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + var file_path: String = (path + "/" + file_name).simplify_path() + if dir.current_is_dir(): + if not file_name in [".godot", ".tmp"]: + files.append_array(_get_dialogue_files_in_filesystem(file_path)) + elif file_name.get_extension() == "dialogue": + files.append(file_path) + file_name = dir.get_next() + + return files + + +#region Signals + + +func _on_update_dependency_timeout() -> void: + _update_dependency_timer.stop() + var import_regex: RegEx = RegEx.create_from_string("import \"(?.*?)\"") + var file: FileAccess + var found_imports: Array[RegExMatch] + for path in _update_dependency_paths: + # Open the file and check for any "import" lines + file = FileAccess.open(path, FileAccess.READ) + found_imports = import_regex.search_all(file.get_as_text()) + var dependencies: PackedStringArray = [] + for found in found_imports: + dependencies.append(found.strings[found.names.path]) + _cache[path].dependencies = dependencies + _update_dependency_paths.clear() + + +#endregion diff --git a/addons/dialogue_manager/utilities/dialogue_cache.gd.uid b/addons/dialogue_manager/utilities/dialogue_cache.gd.uid new file mode 100644 index 0000000..e572006 --- /dev/null +++ b/addons/dialogue_manager/utilities/dialogue_cache.gd.uid @@ -0,0 +1 @@ +uid://d3c83yd6bjp43 diff --git a/addons/dialogue_manager/views/main_view.gd b/addons/dialogue_manager/views/main_view.gd new file mode 100644 index 0000000..5ef4694 --- /dev/null +++ b/addons/dialogue_manager/views/main_view.gd @@ -0,0 +1,1202 @@ +@tool +extends Control + + +const OPEN_OPEN = 100 +const OPEN_QUICK = 101 +const OPEN_CLEAR = 102 + +const TRANSLATIONS_GENERATE_LINE_IDS = 100 +const TRANSLATIONS_SAVE_CHARACTERS_TO_CSV = 201 +const TRANSLATIONS_SAVE_TO_CSV = 202 +const TRANSLATIONS_IMPORT_FROM_CSV = 203 + +const ITEM_SAVE = 100 +const ITEM_SAVE_AS = 101 +const ITEM_CLOSE = 102 +const ITEM_CLOSE_ALL = 103 +const ITEM_CLOSE_OTHERS = 104 +const ITEM_COPY_PATH = 200 +const ITEM_SHOW_IN_FILESYSTEM = 201 + +enum TranslationSource { + CharacterNames, + Lines +} + + +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 +@onready var open_dialog: FileDialog = $OpenDialog +@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog +@onready var quick_open_files_list: VBoxContainer = $QuickOpenDialog/QuickOpenFilesList +@onready var export_dialog: FileDialog = $ExportDialog +@onready var import_dialog: FileDialog = $ImportDialog +@onready var errors_dialog: AcceptDialog = $ErrorsDialog +@onready var build_error_dialog: AcceptDialog = $BuildErrorDialog +@onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog +@onready var updated_dialog: AcceptDialog = $UpdatedDialog +@onready var find_in_files_dialog: AcceptDialog = $FindInFilesDialog +@onready var find_in_files: Control = $FindInFilesDialog/FindInFiles + +# Toolbar +@onready var new_button: Button = %NewButton +@onready var open_button: MenuButton = %OpenButton +@onready var save_all_button: Button = %SaveAllButton +@onready var find_in_files_button: Button = %FindInFilesButton +@onready var test_button: Button = %TestButton +@onready var test_line_button: Button = %TestLineButton +@onready var search_button: Button = %SearchButton +@onready var insert_button: MenuButton = %InsertButton +@onready var translations_button: MenuButton = %TranslationsButton +@onready var support_button: Button = %SupportButton +@onready var docs_button: Button = %DocsButton +@onready var version_label: Label = %VersionLabel +@onready var update_button: Button = %UpdateButton + +@onready var search_and_replace := %SearchAndReplace + +# Code editor +@onready var content: HSplitContainer = %Content +@onready var files_list := %FilesList +@onready var files_popup_menu: PopupMenu = %FilesPopupMenu +@onready var title_list := %TitleList +@onready var code_edit: DMCodeEdit = %CodeEdit +@onready var errors_panel := %ErrorsPanel + +# The currently open file +var current_file_path: String = "": + set(next_current_file_path): + current_file_path = next_current_file_path + files_list.current_file_path = current_file_path + if current_file_path == "" or not open_buffers.has(current_file_path): + save_all_button.disabled = true + test_button.disabled = true + test_line_button.disabled = true + search_button.disabled = true + insert_button.disabled = true + translations_button.disabled = true + content.dragger_visibility = SplitContainer.DRAGGER_HIDDEN + files_list.hide() + title_list.hide() + code_edit.hide() + errors_panel.hide() + banner.show() + else: + test_button.disabled = false + test_line_button.disabled = false + search_button.disabled = false + insert_button.disabled = false + translations_button.disabled = false + content.dragger_visibility = SplitContainer.DRAGGER_VISIBLE + 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(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 + +# A reference to the currently open files and their last saved text +var open_buffers: Dictionary = {} + +# Which thing are we exporting translations for? +var translation_source: TranslationSource = TranslationSource.Lines + +var plugin: EditorPlugin + + +func _ready() -> void: + plugin = Engine.get_meta("DialogueManagerPlugin") + + apply_theme() + + # Start with nothing open + self.current_file_path = "" + + # Set up the update checker + version_label.text = "v%s" % plugin.get_version() + update_button.on_before_refresh = func on_before_refresh(): + # Save everything + DMSettings.set_user_value("just_refreshed", { + current_file_path = current_file_path, + open_buffers = open_buffers + }) + return true + + # Did we just load from an addon version refresh? + var just_refreshed = DMSettings.get_user_value("just_refreshed", null) + if just_refreshed != null: + DMSettings.set_user_value("just_refreshed", null) + call_deferred("load_from_version_refresh", just_refreshed) + + # Hook up the search toolbar + search_and_replace.code_edit = code_edit + + # Connect menu buttons + insert_button.get_popup().id_pressed.connect(_on_insert_button_menu_id_pressed) + translations_button.get_popup().id_pressed.connect(_on_translations_button_menu_id_pressed) + + code_edit.main_view = self + code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DMSettings.get_setting(DMSettings.WRAP_LONG_LINES, false) else TextEdit.LINE_WRAPPING_NONE + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() + editor_settings.settings_changed.connect(_on_editor_settings_changed) + _on_editor_settings_changed() + + # Reopen any files that were open when Godot was closed + if editor_settings.get_setting("text_editor/behavior/files/restore_scripts_on_load"): + var reopen_files: Array = DMSettings.get_user_value("reopen_files", []) + for reopen_file in reopen_files: + open_file(reopen_file) + + self.current_file_path = DMSettings.get_user_value("most_recent_reopen_file", "") + + save_all_button.disabled = true + + close_confirmation_dialog.ok_button_text = DMConstants.translate(&"confirm_close.save") + close_confirmation_dialog.add_button(DMConstants.translate(&"confirm_close.discard"), true, "discard") + + errors_dialog.dialog_text = DMConstants.translate(&"errors_in_script") + + # Update the buffer if a file was modified externally (retains undo step) + Engine.get_meta("DMCache").file_content_changed.connect(_on_cache_file_content_changed) + + 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()) + DMSettings.set_user_value("most_recent_reopen_file", self.current_file_path) + + +func _unhandled_input(event: InputEvent) -> void: + if not visible: return + + if event is InputEventKey and event.is_pressed(): + var shortcut: String = plugin.get_editor_shortcut(event) + match shortcut: + "close_file": + get_viewport().set_input_as_handled() + close_file(current_file_path) + "save": + get_viewport().set_input_as_handled() + save_file(current_file_path) + "find_in_files": + get_viewport().set_input_as_handled() + _on_find_in_files_button_pressed() + "run_test_scene": + get_viewport().set_input_as_handled() + _on_test_button_pressed() + + +func apply_changes() -> void: + save_files() + + +# Load back to the previous buffer regardless of if it was actually saved +func load_from_version_refresh(just_refreshed: Dictionary) -> void: + if just_refreshed.has("current_file_content"): + # We just loaded from a version before multiple buffers + var file: FileAccess = FileAccess.open(just_refreshed.current_file_path, FileAccess.READ) + var file_text: String = file.get_as_text() + open_buffers[just_refreshed.current_file_path] = { + pristine_text = file_text, + text = just_refreshed.current_file_content + } + else: + open_buffers = just_refreshed.open_buffers + + if just_refreshed.current_file_path != "": + EditorInterface.edit_resource(load(just_refreshed.current_file_path)) + else: + EditorInterface.set_main_screen_editor("Dialogue") + + updated_dialog.dialog_text = DMConstants.translate(&"update.success").format({ version = update_button.get_version() }) + updated_dialog.popup_centered() + + +func new_file(path: String, content: String = "") -> void: + if open_buffers.has(path): + remove_file_from_open_buffers(path) + + var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) + if content == "": + file.store_string(DMSettings.get_setting(DMSettings.NEW_FILE_TEMPLATE, "")) + else: + file.store_string(content) + + EditorInterface.get_resource_filesystem().scan() + + +# Open a dialogue resource for editing +func open_resource(resource: DialogueResource) -> void: + open_file(resource.resource_path) + + +func open_file(path: String) -> void: + if not FileAccess.file_exists(path): return + + if not open_buffers.has(path): + var file: FileAccess = FileAccess.open(path, FileAccess.READ) + var text = file.get_as_text() + + open_buffers[path] = { + cursor = Vector2.ZERO, + text = text, + pristine_text = text + } + + DMSettings.add_recent_file(path) + build_open_menu() + + files_list.files = open_buffers.keys() + files_list.select_file(path) + + 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) + + +# Save any open files +func save_files() -> void: + save_all_button.disabled = true + + var saved_files: PackedStringArray = [] + for path in open_buffers: + if open_buffers[path].text != open_buffers[path].pristine_text: + saved_files.append(path) + save_file(path, false) + + if saved_files.size() > 0: + Engine.get_meta("DMCache").mark_files_for_reimport(saved_files) + + +# Save a file +func save_file(path: String, rescan_file_system: bool = true) -> void: + var buffer = open_buffers[path] + + files_list.mark_file_as_unsaved(path, false) + save_all_button.disabled = files_list.unsaved_files.size() == 0 + + # Don't bother saving if there is nothing to save + if buffer.text == buffer.pristine_text: + return + + buffer.pristine_text = buffer.text + + # Save the current text + var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) + file.store_string(buffer.text) + file.close() + + if rescan_file_system: + EditorInterface.get_resource_filesystem().scan() + + +func close_file(path: String) -> void: + if not path in open_buffers.keys(): return + + var buffer = open_buffers[path] + + if buffer.text == buffer.pristine_text: + remove_file_from_open_buffers(path) + await get_tree().process_frame + else: + close_confirmation_dialog.dialog_text = DMConstants.translate(&"confirm_close").format({ path = path.get_file() }) + close_confirmation_dialog.popup_centered() + await confirmation_closed + + +func remove_file_from_open_buffers(path: String) -> void: + if not path in open_buffers.keys(): return + + var current_index = open_buffers.keys().find(current_file_path) + + open_buffers.erase(path) + if open_buffers.size() == 0: + self.current_file_path = "" + else: + current_index = clamp(current_index, 0, open_buffers.size() - 1) + self.current_file_path = open_buffers.keys()[current_index] + + files_list.files = open_buffers.keys() + + +# Apply theme colors and icons to the UI +func apply_theme() -> void: + if is_instance_valid(plugin) and is_instance_valid(code_edit): + var scale: float = EditorInterface.get_editor_scale() + var editor_settings = EditorInterface.get_editor_settings() + code_edit.theme_overrides = { + scale = scale, + + background_color = editor_settings.get_setting("text_editor/theme/highlighting/background_color"), + current_line_color = editor_settings.get_setting("text_editor/theme/highlighting/current_line_color"), + error_line_color = editor_settings.get_setting("text_editor/theme/highlighting/mark_color"), + + critical_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/critical_color"), + notice_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/notice_color"), + + titles_color = editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"), + text_color = editor_settings.get_setting("text_editor/theme/highlighting/text_color"), + conditions_color = editor_settings.get_setting("text_editor/theme/highlighting/keyword_color"), + mutations_color = editor_settings.get_setting("text_editor/theme/highlighting/function_color"), + members_color = editor_settings.get_setting("text_editor/theme/highlighting/member_variable_color"), + strings_color = editor_settings.get_setting("text_editor/theme/highlighting/string_color"), + numbers_color = editor_settings.get_setting("text_editor/theme/highlighting/number_color"), + symbols_color = editor_settings.get_setting("text_editor/theme/highlighting/symbol_color"), + comments_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_color"), + jumps_color = Color(editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"), 0.7), + + 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") + + open_button.icon = get_theme_icon("Load", "EditorIcons") + open_button.tooltip_text = DMConstants.translate(&"open_a_file") + + save_all_button.icon = get_theme_icon("Save", "EditorIcons") + save_all_button.text = DMConstants.translate(&"all") + save_all_button.tooltip_text = DMConstants.translate(&"start_all_files") + + find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons") + find_in_files_button.tooltip_text = DMConstants.translate(&"find_in_files") + + test_button.icon = get_theme_icon("DebugNext", "EditorIcons") + test_button.tooltip_text = DMConstants.translate(&"test_dialogue") + + test_line_button.icon = get_theme_icon("DebugStep", "EditorIcons") + test_line_button.tooltip_text = DMConstants.translate(&"test_dialogue_from_line") + + search_button.icon = get_theme_icon("Search", "EditorIcons") + search_button.tooltip_text = DMConstants.translate(&"search_for_text") + + insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons") + insert_button.text = DMConstants.translate(&"insert") + + translations_button.icon = get_theme_icon("Translation", "EditorIcons") + translations_button.text = DMConstants.translate(&"translations") + + support_button.icon = get_theme_icon("Heart", "EditorIcons") + support_button.text = DMConstants.translate(&"sponsor") + support_button.tooltip_text = DMConstants.translate(&"show_support") + + docs_button.icon = get_theme_icon("Help", "EditorIcons") + docs_button.text = DMConstants.translate(&"docs") + + update_button.apply_theme() + + # Set up the effect menu + var popup: PopupMenu = insert_button.get_popup() + popup.clear() + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.wave_bbcode"), 0) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.shake_bbcode"), 1) + popup.add_separator() + popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DMConstants.translate(&"insert.typing_pause"), 3) + popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DMConstants.translate(&"insert.typing_speed_change"), 4) + popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DMConstants.translate(&"insert.auto_advance"), 5) + popup.add_separator(DMConstants.translate(&"insert.templates")) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.title"), 6) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.dialogue"), 7) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.response"), 8) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.random_lines"), 9) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.random_text"), 10) + popup.add_separator(DMConstants.translate(&"insert.actions")) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.jump"), 11) + popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DMConstants.translate(&"insert.end_dialogue"), 12) + + # Set up the translations menu + popup = translations_button.get_popup() + popup.clear() + popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DMConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS) + popup.add_separator() + popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DMConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV) + popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DMConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV) + popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DMConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV) + + # Dialog sizes + new_dialog.min_size = Vector2(600, 500) * scale + save_dialog.min_size = Vector2(600, 500) * scale + open_dialog.min_size = Vector2(600, 500) * scale + quick_open_dialog.min_size = Vector2(400, 600) * scale + export_dialog.min_size = Vector2(600, 500) * scale + import_dialog.min_size = Vector2(600, 500) * scale + find_in_files_dialog.min_size = Vector2(800, 600) * scale + + +### Helpers + + +# Refresh the open menu with the latest files +func build_open_menu() -> void: + var menu = open_button.get_popup() + menu.clear() + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DMConstants.translate(&"open.open"), OPEN_OPEN) + menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DMConstants.translate(&"open.quick_open"), OPEN_QUICK) + menu.add_separator() + + var recent_files = DMSettings.get_recent_files() + if recent_files.size() == 0: + menu.add_item(DMConstants.translate(&"open.no_recent_files")) + menu.set_item_disabled(2, true) + else: + for path in recent_files: + if FileAccess.file_exists(path): + menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path) + + menu.add_separator() + menu.add_item(DMConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR) + if menu.id_pressed.is_connected(_on_open_menu_id_pressed): + menu.id_pressed.disconnect(_on_open_menu_id_pressed) + menu.id_pressed.connect(_on_open_menu_id_pressed) + + +# Get the last place a CSV, etc was exported +func get_last_export_path(extension: String) -> String: + var filename = current_file_path.get_file().replace(".dialogue", "." + extension) + return DMSettings.get_user_value("last_export_path", current_file_path.get_base_dir()) + "/" + filename + + +# Check the current text for errors +func compile() -> void: + # Skip if nothing to parse + if current_file_path == "": return + + var result: DMCompilerResult = DMCompiler.compile_string(code_edit.text, current_file_path) + code_edit.errors = result.errors + errors_panel.errors = result.errors + title_list.titles = code_edit.get_titles() + + +func show_build_error_dialog() -> void: + build_error_dialog.dialog_text = DMConstants.translate(&"errors_with_build") + build_error_dialog.popup_centered() + + +# Generate translation line IDs for any line that doesn't already have one +func generate_translations_keys() -> void: + var rng: RandomNumberGenerator = RandomNumberGenerator.new() + rng.randomize() + + var cursor: Vector2 = code_edit.get_cursor() + var lines: PackedStringArray = code_edit.text.split("\n") + + 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()): + var line = lines[i] + var found = key_regex.search(line) + if found: + var text = "" + var l = line.replace(found.strings[0], "").strip_edges().strip_edges() + if l.begins_with("- "): + text = DMCompiler.extract_translatable_string(l) + elif ":" in l: + text = l.split(":")[1] + else: + text = l + known_keys[found.strings[found.names.get("key")]] = text + + # Add in any that are missing + for i in lines.size(): + var line = lines[i] + 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 + + var text = "" + if l.begins_with("- "): + text = DMCompiler.extract_translatable_string(l) + else: + text = l.substr(l.find(":") + 1) + + var key: String = "" + if known_keys.values().has(text): + key = known_keys.find_key(text) + else: + var regex: DMCompilerRegEx = DMCompilerRegEx.new() + 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() + + line = line.replace("\\n", "!NEWLINE!") + text = text.replace("\n", "!NEWLINE!") + lines[i] = line.replace(text, text + " [ID:%s]" % [key]).replace("!NEWLINE!", "\\n") + + known_keys[key] = text + + code_edit.text = "\n".join(lines) + code_edit.set_cursor(cursor) + _on_code_edit_text_changed() + + +# Add a translation file to the project settings +func add_path_to_project_translations(path: String) -> void: + var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations") + if not path in translations: + translations.append(path) + ProjectSettings.save() + + +# Export dialogue and responses to CSV +func export_translations_to_csv(path: String) -> void: + var default_locale: String = DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en") + + var file: FileAccess + + # If the file exists, open it first and work out which keys are already in it + var existing_csv: Dictionary = {} + var column_count: int = 2 + var default_locale_column: int = 1 + var character_column: int = -1 + var notes_column: int = -1 + if FileAccess.file_exists(path): + file = FileAccess.open(path, FileAccess.READ) + var is_first_line = true + var line: Array + while !file.eof_reached(): + line = file.get_csv_line() + if is_first_line: + is_first_line = false + column_count = line.size() + for i in range(1, line.size()): + if line[i] == default_locale: + default_locale_column = i + elif line[i] == "_character": + character_column = i + elif line[i] == "_notes": + notes_column = i + + # Make sure the line isn't empty before adding it + if line.size() > 0 and line[0].strip_edges() != "": + existing_csv[line[0]] = line + + # The character column wasn't found in the existing file but the setting is turned on + if character_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): + character_column = column_count + column_count += 1 + existing_csv["keys"].append("_character") + + # The notes column wasn't found in the existing file but the setting is turned on + if notes_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): + notes_column = column_count + column_count += 1 + existing_csv["keys"].append("_notes") + + # Start a new file + file = FileAccess.open(path, FileAccess.WRITE) + + if not FileAccess.file_exists(path): + var headings: PackedStringArray = ["keys", default_locale] + DMSettings.get_setting(DMSettings.EXTRA_CSV_LOCALES, []) + if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): + character_column = headings.size() + headings.append("_character") + if DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): + notes_column = headings.size() + headings.append("_notes") + file.store_csv_line(headings) + column_count = headings.size() + + # Write our translations to file + var known_keys: PackedStringArray = [] + + var dialogue = DMCompiler.compile_string(code_edit.text, current_file_path).lines + + # Make a list of stuff that needs to go into the file + var lines_to_save = [] + for key in dialogue.keys(): + var line: Dictionary = dialogue.get(key) + + if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue + + var translation_key: String = line.get(&"translation_key", line.text) + + if translation_key in known_keys: continue + + known_keys.append(translation_key) + + var line_to_save: PackedStringArray = [] + if existing_csv.has(translation_key): + line_to_save = existing_csv.get(translation_key) + line_to_save.resize(column_count) + existing_csv.erase(translation_key) + else: + line_to_save.resize(column_count) + line_to_save[0] = translation_key + + line_to_save[default_locale_column] = line.text + if character_column > -1: + line_to_save[character_column] = "(response)" if line.type == DMConstants.TYPE_RESPONSE else line.character + if notes_column > -1: + line_to_save[notes_column] = line.notes + + lines_to_save.append(line_to_save) + + # Store lines in the file, starting with anything that already exists that hasn't been touched + for line in existing_csv.values(): + file.store_csv_line(line) + for line in lines_to_save: + file.store_csv_line(line) + + file.close() + + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", path) + + # Add it to the project l10n settings if it's not already there + var language_code: RegExMatch = RegEx.create_from_string("^[a-z]{2,3}").search(default_locale) + var translation_path: String = path.replace(".csv", ".%s.translation" % language_code.get_string()) + call_deferred("add_path_to_project_translations", translation_path) + + +func export_character_names_to_csv(path: String) -> void: + var file: FileAccess + + # If the file exists, open it first and work out which keys are already in it + var existing_csv = {} + var commas = [] + if FileAccess.file_exists(path): + file = FileAccess.open(path, FileAccess.READ) + var is_first_line = true + var line: Array + while !file.eof_reached(): + line = file.get_csv_line() + if is_first_line: + is_first_line = false + for i in range(2, line.size()): + commas.append("") + # Make sure the line isn't empty before adding it + if line.size() > 0 and line[0].strip_edges() != "": + existing_csv[line[0]] = line + + # Start a new file + file = FileAccess.open(path, FileAccess.WRITE) + + if not file.file_exists(path): + file.store_csv_line(["keys", DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en")]) + + # Write our translations to file + var known_keys: PackedStringArray = [] + + var character_names: PackedStringArray = DMCompiler.compile_string(code_edit.text, current_file_path).character_names + + # Make a list of stuff that needs to go into the file + var lines_to_save = [] + for character_name in character_names: + if character_name in known_keys: continue + + known_keys.append(character_name) + + if existing_csv.has(character_name): + var existing_line = existing_csv.get(character_name) + existing_line[1] = character_name + lines_to_save.append(existing_line) + existing_csv.erase(character_name) + else: + lines_to_save.append(PackedStringArray([character_name, character_name] + commas)) + + # Store lines in the file, starting with anything that already exists that hasn't been touched + for line in existing_csv.values(): + file.store_csv_line(line) + for line in lines_to_save: + file.store_csv_line(line) + + file.close() + + EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", path) + + # Add it to the project l10n settings if it's not already there + var translation_path: String = path.replace(".csv", ".en.translation") + call_deferred("add_path_to_project_translations", translation_path) + + +# Import changes back from an exported CSV by matching translation keys +func import_translations_from_csv(path: String) -> void: + var cursor: Vector2 = code_edit.get_cursor() + + if not FileAccess.file_exists(path): return + + # Open the CSV file and build a dictionary of the known keys + var keys: Dictionary = {} + var file: FileAccess = FileAccess.open(path, FileAccess.READ) + var csv_line: Array + while !file.eof_reached(): + csv_line = file.get_csv_line() + if csv_line.size() > 1: + keys[csv_line[0]] = csv_line[1] + + # Now look over each line in the dialogue and replace the content for matched keys + var lines: PackedStringArray = code_edit.text.split("\n") + var start_index: int = 0 + var end_index: int = 0 + for i in range(0, lines.size()): + var line: String = lines[i] + var translation_key: String = DMCompiler.get_static_line_id(line) + if keys.has(translation_key): + if DMCompiler.get_line_type(line) == DMConstants.TYPE_DIALOGUE: + start_index = 0 + # See if we need to skip over a character name + line = line.replace("\\:", "!ESCAPED_COLON!") + if ": " in line: + start_index = line.find(": ") + 2 + lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]").replace("!ESCAPED_COLON!", ":") + + elif DMCompiler.get_line_type(line) == DMConstants.TYPE_RESPONSE: + start_index = line.find("- ") + 2 + # See if we need to skip over a character name + line = line.replace("\\:", "!ESCAPED_COLON!") + if ": " in line: + start_index = line.find(": ") + 2 + end_index = line.length() + if " =>" in line: + end_index = line.find(" =>") + if " [if " in line: + end_index = line.find(" [if ") + lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]" + line.substr(end_index)).replace("!ESCAPED_COLON!", ":") + + code_edit.text = "\n".join(lines) + code_edit.set_cursor(cursor) + + +func show_search_form(is_enabled: bool) -> void: + if code_edit.last_selected_text: + search_and_replace.input.text = code_edit.last_selected_text + + search_and_replace.visible = is_enabled + search_button.set_pressed_no_signal(is_enabled) + 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 + + +func _on_files_moved(old_file: String, new_file: String) -> void: + if open_buffers.has(old_file): + open_buffers[new_file] = open_buffers[old_file] + open_buffers.erase(old_file) + open_buffers[new_file] + + +func _on_cache_file_content_changed(path: String, new_content: String) -> void: + if open_buffers.has(path): + var buffer = open_buffers[path] + if buffer.text == buffer.pristine_text and buffer.text != new_content: + buffer.text = new_content + code_edit.text = new_content + title_list.titles = code_edit.get_titles() + buffer.pristine_text = new_content + + +func _on_editor_settings_changed() -> void: + var editor_settings: EditorSettings = EditorInterface.get_editor_settings() + code_edit.minimap_draw = editor_settings.get_setting("text_editor/appearance/minimap/show_minimap") + code_edit.minimap_width = editor_settings.get_setting("text_editor/appearance/minimap/minimap_width") + code_edit.scroll_smooth = editor_settings.get_setting("text_editor/behavior/navigation/smooth_scrolling") + + +func _on_open_menu_id_pressed(id: int) -> void: + match id: + OPEN_OPEN: + open_dialog.popup_centered() + OPEN_QUICK: + quick_open() + OPEN_CLEAR: + DMSettings.clear_recent_files() + build_open_menu() + _: + var menu = open_button.get_popup() + var item = menu.get_item_text(menu.get_item_index(id)) + open_file(item) + + +func _on_files_list_file_selected(file_path: String) -> void: + self.current_file_path = file_path + + +func _on_insert_button_menu_id_pressed(id: int) -> void: + match id: + 0: + code_edit.insert_bbcode("[wave amp=25 freq=5]", "[/wave]") + 1: + code_edit.insert_bbcode("[shake rate=20 level=10]", "[/shake]") + 3: + code_edit.insert_bbcode("[wait=1]") + 4: + code_edit.insert_bbcode("[speed=0.2]") + 5: + code_edit.insert_bbcode("[next=auto]") + 6: + code_edit.insert_text_at_cursor("~ title") + 7: + code_edit.insert_text_at_cursor("Nathan: This is Some Dialogue") + 8: + code_edit.insert_text_at_cursor("Nathan: Choose a Response...\n- Option 1\n\tNathan: You chose option 1\n- Option 2\n\tNathan: You chose option 2") + 9: + code_edit.insert_text_at_cursor("% Nathan: This is random line 1.\n% Nathan: This is random line 2.\n%1 Nathan: This is weighted random line 3.") + 10: + code_edit.insert_text_at_cursor("Nathan: [[Hi|Hello|Howdy]]") + 11: + code_edit.insert_text_at_cursor("=> title") + 12: + code_edit.insert_text_at_cursor("=> END") + + +func _on_translations_button_menu_id_pressed(id: int) -> void: + match id: + TRANSLATIONS_GENERATE_LINE_IDS: + generate_translations_keys() + + TRANSLATIONS_SAVE_CHARACTERS_TO_CSV: + translation_source = TranslationSource.CharacterNames + export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"]) + export_dialog.current_path = get_last_export_path("csv") + export_dialog.popup_centered() + + TRANSLATIONS_SAVE_TO_CSV: + translation_source = TranslationSource.Lines + export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"]) + export_dialog.current_path = get_last_export_path("csv") + export_dialog.popup_centered() + + TRANSLATIONS_IMPORT_FROM_CSV: + import_dialog.current_path = get_last_export_path("csv") + import_dialog.popup_centered() + + +func _on_export_dialog_file_selected(path: String) -> void: + DMSettings.set_user_value("last_export_path", path.get_base_dir()) + match path.get_extension(): + "csv": + match translation_source: + TranslationSource.CharacterNames: + export_character_names_to_csv(path) + TranslationSource.Lines: + export_translations_to_csv(path) + + +func _on_import_dialog_file_selected(path: String) -> void: + DMSettings.set_user_value("last_export_path", path.get_base_dir()) + import_translations_from_csv(path) + + +func _on_main_view_theme_changed(): + apply_theme() + + +func _on_main_view_visibility_changed() -> void: + if visible and is_instance_valid(code_edit): + code_edit.grab_focus() + + +func _on_new_button_pressed() -> void: + new_dialog.current_file = "untitled" + new_dialog.popup_centered() + + +func _on_new_dialog_confirmed() -> void: + 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: + new_file(path) + open_file(path) + + +func _on_save_dialog_file_selected(path: String) -> void: + if path == "": path = "res://untitled.dialogue" + + new_file(path, code_edit.text) + open_file(path) + + +func _on_open_button_about_to_popup() -> void: + build_open_menu() + + +func _on_open_dialog_file_selected(path: String) -> void: + open_file(path) + + +func _on_quick_open_files_list_file_double_clicked(file_path: String) -> void: + quick_open_dialog.hide() + open_file(file_path) + + +func _on_quick_open_dialog_confirmed() -> void: + if quick_open_files_list.last_selected_file_path: + open_file(quick_open_files_list.last_selected_file_path) + + +func _on_save_all_button_pressed() -> void: + save_files() + + +func _on_find_in_files_button_pressed() -> void: + find_in_files_dialog.popup_centered() + find_in_files.prepare() + + +func _on_code_edit_text_changed() -> void: + var buffer = open_buffers[current_file_path] + buffer.text = code_edit.text + + files_list.mark_file_as_unsaved(current_file_path, buffer.text != buffer.pristine_text) + save_all_button.disabled = open_buffers.values().filter(func(d): return d.text != d.pristine_text).size() == 0 + + 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) + + +func _on_code_edit_caret_changed() -> void: + DMSettings.set_caret(current_file_path, code_edit.get_cursor()) + + +func _on_code_edit_error_clicked(line_number: int) -> void: + errors_panel.show_error_for_line_number(line_number) + + +func _on_title_list_title_selected(title: String) -> void: + code_edit.go_to_title(title) + code_edit.grab_focus() + + +func _on_parse_timer_timeout() -> void: + parse_timer.stop() + compile() + + +func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> void: + code_edit.set_caret_line(line_number - 1) + code_edit.set_caret_column(column_number) + code_edit.grab_focus() + + +func _on_search_button_toggled(button_pressed: bool) -> void: + show_search_form(button_pressed) + + +func _on_search_and_replace_open_requested() -> void: + show_search_form(true) + + +func _on_search_and_replace_close_requested() -> void: + search_button.set_pressed_no_signal(false) + search_and_replace.visible = false + code_edit.grab_focus() + + +func _on_test_button_pressed() -> void: + save_file(current_file_path, false) + Engine.get_meta("DMCache").reimport_files([current_file_path]) + + if errors_panel.errors.size() > 0: + errors_dialog.popup_centered() + return + + run_test_scene("") + + +func _on_test_line_button_pressed() -> void: + save_file(current_file_path) + + if errors_panel.errors.size() > 0: + errors_dialog.popup_centered() + return + + # Find next non-empty line + var line_to_run: int = 0 + for i in range(code_edit.get_cursor().y, code_edit.get_line_count()): + if not code_edit.get_line(i).is_empty(): + line_to_run = i + break + + run_test_scene(str(line_to_run)) + + +func _on_support_button_pressed() -> void: + OS.shell_open("https://patreon.com/nathanhoad") + + +func _on_docs_button_pressed() -> void: + OS.shell_open("https://github.com/nathanhoad/godot_dialogue_manager") + + +func _on_files_list_file_popup_menu_requested(at_position: Vector2) -> void: + files_popup_menu.position = Vector2(get_viewport().position) + files_list.global_position + at_position + files_popup_menu.popup() + + +func _on_files_list_file_middle_clicked(path: String): + close_file(path) + + +func _on_files_popup_menu_about_to_popup() -> void: + files_popup_menu.clear() + + var shortcuts: Dictionary = plugin.get_editor_shortcuts() + + files_popup_menu.add_item(DMConstants.translate(&"buffer.save"), ITEM_SAVE, OS.find_keycode_from_string(shortcuts.get("save")[0].as_text_keycode())) + files_popup_menu.add_item(DMConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close"), ITEM_CLOSE, OS.find_keycode_from_string(shortcuts.get("close_file")[0].as_text_keycode())) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL) + files_popup_menu.add_item(DMConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS) + files_popup_menu.add_separator() + files_popup_menu.add_item(DMConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH) + files_popup_menu.add_item(DMConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM) + + +func _on_files_popup_menu_id_pressed(id: int) -> void: + match id: + ITEM_SAVE: + save_file(current_file_path) + ITEM_SAVE_AS: + save_dialog.popup_centered() + ITEM_CLOSE: + close_file(current_file_path) + ITEM_CLOSE_ALL: + for path in open_buffers.keys(): + close_file(path) + ITEM_CLOSE_OTHERS: + var current_current_file_path: String = current_file_path + for path in open_buffers.keys(): + if path != current_current_file_path: + await close_file(path) + + ITEM_COPY_PATH: + DisplayServer.clipboard_set(current_file_path) + ITEM_SHOW_IN_FILESYSTEM: + show_file_in_filesystem(current_file_path) + + +func _on_code_edit_external_file_requested(path: String, title: String) -> void: + open_file(path) + if title != "": + code_edit.go_to_title(title) + else: + code_edit.set_caret_line(0) + + +func _on_close_confirmation_dialog_confirmed() -> void: + save_file(current_file_path) + remove_file_from_open_buffers(current_file_path) + confirmation_closed.emit() + + +func _on_close_confirmation_dialog_custom_action(action: StringName) -> void: + if action == "discard": + remove_file_from_open_buffers(current_file_path) + close_confirmation_dialog.hide() + confirmation_closed.emit() + + +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/addons/dialogue_manager/views/main_view.gd.uid b/addons/dialogue_manager/views/main_view.gd.uid new file mode 100644 index 0000000..10e66f4 --- /dev/null +++ b/addons/dialogue_manager/views/main_view.gd.uid @@ -0,0 +1 @@ +uid://cipjcc7bkh1pc diff --git a/addons/dialogue_manager/views/main_view.tscn b/addons/dialogue_manager/views/main_view.tscn new file mode 100644 index 0000000..b21f708 --- /dev/null +++ b/addons/dialogue_manager/views/main_view.tscn @@ -0,0 +1,480 @@ +[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"] +[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="2_npj2k"] +[ext_resource type="PackedScene" uid="uid://ctns6ouwwd68i" path="res://addons/dialogue_manager/components/title_list.tscn" id="2_onb4i"] +[ext_resource type="PackedScene" uid="uid://co8yl23idiwbi" path="res://addons/dialogue_manager/components/update_button.tscn" id="2_ph3vs"] +[ext_resource type="PackedScene" uid="uid://gr8nakpbrhby" path="res://addons/dialogue_manager/components/search_and_replace.tscn" id="6_ylh0t"] +[ext_resource type="PackedScene" uid="uid://cs8pwrxr5vxix" path="res://addons/dialogue_manager/components/errors_panel.tscn" id="7_5cvl4"] +[ext_resource type="Script" 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_y6rqu"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ka3gk"] +image = SubResource("Image_y6rqu") + +[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"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_h6qfq") + +[node name="ParseTimer" type="Timer" parent="."] + +[node name="Margin" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 +metadata/_edit_layout_mode = 1 + +[node name="Content" type="HSplitContainer" parent="Margin"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +dragger_visibility = 1 + +[node name="SidePanel" type="VBoxContainer" parent="Margin/Content"] +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Toolbar" type="HBoxContainer" parent="Margin/Content/SidePanel"] +layout_mode = 2 + +[node name="NewButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Start a new file" +flat = true + +[node name="OpenButton" type="MenuButton" parent="Margin/Content/SidePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Open a file" +item_count = 9 +popup/item_0/text = "Open..." +popup/item_0/icon = SubResource("ImageTexture_ka3gk") +popup/item_0/id = 100 +popup/item_1/icon = SubResource("ImageTexture_ka3gk") +popup/item_1/id = 101 +popup/item_2/id = -1 +popup/item_2/separator = true +popup/item_3/text = "res://examples/dialogue.dialogue" +popup/item_3/icon = SubResource("ImageTexture_ka3gk") +popup/item_3/id = 3 +popup/item_4/text = "res://examples/dialogue_with_input.dialogue" +popup/item_4/icon = SubResource("ImageTexture_ka3gk") +popup/item_4/id = 4 +popup/item_5/text = "res://examples/dialogue_for_point_n_click.dialogue" +popup/item_5/icon = SubResource("ImageTexture_ka3gk") +popup/item_5/id = 5 +popup/item_6/text = "res://examples/dialogue_for_visual_novel.dialogue" +popup/item_6/icon = SubResource("ImageTexture_ka3gk") +popup/item_6/id = 6 +popup/item_7/id = -1 +popup/item_7/separator = true +popup/item_8/text = "Clear recent files" +popup/item_8/id = 102 + +[node name="SaveAllButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +disabled = true +flat = true + +[node name="FindInFilesButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Find in files..." +flat = true + +[node name="Bookmarks" type="VSplitContainer" parent="Margin/Content/SidePanel"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="FilesList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_npj2k")] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="FilesPopupMenu" type="PopupMenu" parent="Margin/Content/SidePanel/Bookmarks/FilesList"] +unique_name_in_owner = true + +[node name="TitleList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_onb4i")] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="CodePanel" type="VBoxContainer" parent="Margin/Content"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 4.0 + +[node name="Toolbar" type="HBoxContainer" parent="Margin/Content/CodePanel"] +layout_mode = 2 + +[node name="InsertButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +disabled = true +text = "Insert" +item_count = 15 +popup/item_0/text = "Wave BBCode" +popup/item_0/icon = SubResource("ImageTexture_57eek") +popup/item_0/id = 0 +popup/item_1/text = "Shake BBCode" +popup/item_1/icon = SubResource("ImageTexture_57eek") +popup/item_1/id = 1 +popup/item_2/id = -1 +popup/item_2/separator = true +popup/item_3/text = "Typing pause" +popup/item_3/icon = SubResource("ImageTexture_57eek") +popup/item_3/id = 3 +popup/item_4/text = "Typing speed change" +popup/item_4/icon = SubResource("ImageTexture_57eek") +popup/item_4/id = 4 +popup/item_5/text = "Auto advance" +popup/item_5/icon = SubResource("ImageTexture_57eek") +popup/item_5/id = 5 +popup/item_6/text = "Templates" +popup/item_6/id = -1 +popup/item_6/separator = true +popup/item_7/text = "Title" +popup/item_7/icon = SubResource("ImageTexture_57eek") +popup/item_7/id = 6 +popup/item_8/text = "Dialogue" +popup/item_8/icon = SubResource("ImageTexture_57eek") +popup/item_8/id = 7 +popup/item_9/text = "Response" +popup/item_9/icon = SubResource("ImageTexture_57eek") +popup/item_9/id = 8 +popup/item_10/text = "Random lines" +popup/item_10/icon = SubResource("ImageTexture_57eek") +popup/item_10/id = 9 +popup/item_11/text = "Random text" +popup/item_11/icon = SubResource("ImageTexture_57eek") +popup/item_11/id = 10 +popup/item_12/text = "Actions" +popup/item_12/id = -1 +popup/item_12/separator = true +popup/item_13/text = "Jump to title" +popup/item_13/icon = SubResource("ImageTexture_57eek") +popup/item_13/id = 11 +popup/item_14/text = "End dialogue" +popup/item_14/icon = SubResource("ImageTexture_57eek") +popup/item_14/id = 12 + +[node name="TranslationsButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +disabled = true +text = "Translations" +item_count = 5 +popup/item_0/text = "Generate line IDs" +popup/item_0/icon = SubResource("ImageTexture_57eek") +popup/item_0/id = 100 +popup/item_1/id = -1 +popup/item_1/separator = true +popup/item_2/text = "Save character names to CSV..." +popup/item_2/icon = SubResource("ImageTexture_57eek") +popup/item_2/id = 201 +popup/item_3/text = "Save lines to CSV..." +popup/item_3/icon = SubResource("ImageTexture_57eek") +popup/item_3/id = 202 +popup/item_4/text = "Import line changes from CSV..." +popup/item_4/icon = SubResource("ImageTexture_57eek") +popup/item_4/id = 203 + +[node name="Separator" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] +layout_mode = 2 + +[node name="SearchButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Search for text" +disabled = true +toggle_mode = true +flat = true + +[node name="Separator2" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] +layout_mode = 2 + +[node name="TestButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Test dialogue" +disabled = true +flat = true + +[node name="TestLineButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Test dialogue" +disabled = true +flat = true + +[node name="Spacer2" type="Control" parent="Margin/Content/CodePanel/Toolbar"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SupportButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Support Dialogue Manager" +text = "Sponsor" +flat = true + +[node name="Separator4" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] +layout_mode = 2 + +[node name="DocsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +layout_mode = 2 +text = "Docs" +flat = true + +[node name="VersionLabel" type="Label" parent="Margin/Content/CodePanel/Toolbar"] +unique_name_in_owner = true +modulate = Color(1, 1, 1, 0.490196) +layout_mode = 2 +text = "v2.42.2" +vertical_alignment = 1 + +[node name="UpdateButton" parent="Margin/Content/CodePanel/Toolbar" instance=ExtResource("2_ph3vs")] +unique_name_in_owner = true +layout_mode = 2 +text = "v2.44.1 available" + +[node name="SearchAndReplace" parent="Margin/Content/CodePanel" instance=ExtResource("6_ylh0t")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="CodeEdit" parent="Margin/Content/CodePanel" instance=ExtResource("2_f73fm")] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/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 + +Nathan: Hi, I'm Nathan and this is Coco. +Coco: Meow. +Nathan: Here are some response options. +- First one + Nathan: You picked the first one. +- Second one + Nathan: You picked the second one. +- Start again => start +- End the conversation => END +Nathan: I hope this example is helpful. +Coco: Meow. + +=> END" +scroll_smooth = true +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) +dialog_hide_on_ok = true +filters = PackedStringArray("*.dialogue ; Dialogue") + +[node name="SaveDialog" type="FileDialog" parent="."] +size = Vector2i(900, 750) +min_size = Vector2i(600, 500) +dialog_hide_on_ok = true +filters = PackedStringArray("*.dialogue ; Dialogue") + +[node name="OpenDialog" type="FileDialog" parent="."] +title = "Open a File" +size = Vector2i(900, 750) +min_size = Vector2i(600, 500) +ok_button_text = "Open" +dialog_hide_on_ok = true +file_mode = 0 +filters = PackedStringArray("*.dialogue ; Dialogue") + +[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."] +title = "Quick open" +size = Vector2i(600, 900) +min_size = Vector2i(400, 600) +ok_button_text = "Open" + +[node name="QuickOpenFilesList" parent="QuickOpenDialog" instance=ExtResource("2_npj2k")] + +[node name="ExportDialog" type="FileDialog" parent="."] +size = Vector2i(900, 750) +min_size = Vector2i(600, 500) + +[node name="ImportDialog" type="FileDialog" parent="."] +title = "Open a File" +size = Vector2i(900, 750) +min_size = Vector2i(600, 500) +ok_button_text = "Open" +file_mode = 0 +filters = PackedStringArray("*.csv ; Translation CSV") + +[node name="ErrorsDialog" type="AcceptDialog" parent="."] +title = "Error" +dialog_text = "You have errors in your script. Fix them and then try again." + +[node name="BuildErrorDialog" type="AcceptDialog" parent="."] +title = "Errors" +dialog_text = "You need to fix dialogue errors before you can run your game." + +[node name="CloseConfirmationDialog" type="ConfirmationDialog" parent="."] +title = "Unsaved changes" +ok_button_text = "Save changes" + +[node name="UpdatedDialog" type="AcceptDialog" parent="."] +title = "Updated" +size = Vector2i(191, 100) +dialog_text = "You're now up to date!" + +[node name="FindInFilesDialog" type="AcceptDialog" parent="."] +title = "Find in files" +size = Vector2i(1200, 900) +min_size = Vector2i(800, 600) +ok_button_text = "Done" + +[node name="FindInFiles" parent="FindInFilesDialog" node_paths=PackedStringArray("main_view", "code_edit") instance=ExtResource("10_yold3")] +custom_minimum_size = Vector2(400, 400) +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 +main_view = NodePath("../..") +code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit") + +[connection signal="theme_changed" from="." to="." method="_on_main_view_theme_changed"] +[connection signal="visibility_changed" from="." to="." method="_on_main_view_visibility_changed"] +[connection signal="timeout" from="ParseTimer" to="." method="_on_parse_timer_timeout"] +[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/NewButton" to="." method="_on_new_button_pressed"] +[connection signal="about_to_popup" from="Margin/Content/SidePanel/Toolbar/OpenButton" to="." method="_on_open_button_about_to_popup"] +[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/SaveAllButton" to="." method="_on_save_all_button_pressed"] +[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/FindInFilesButton" to="." method="_on_find_in_files_button_pressed"] +[connection signal="file_middle_clicked" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_middle_clicked"] +[connection signal="file_popup_menu_requested" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_popup_menu_requested"] +[connection signal="file_selected" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_selected"] +[connection signal="about_to_popup" from="Margin/Content/SidePanel/Bookmarks/FilesList/FilesPopupMenu" to="." method="_on_files_popup_menu_about_to_popup"] +[connection signal="id_pressed" from="Margin/Content/SidePanel/Bookmarks/FilesList/FilesPopupMenu" to="." method="_on_files_popup_menu_id_pressed"] +[connection signal="title_selected" from="Margin/Content/SidePanel/Bookmarks/TitleList" to="." method="_on_title_list_title_selected"] +[connection signal="toggled" from="Margin/Content/CodePanel/Toolbar/SearchButton" to="." method="_on_search_button_toggled"] +[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestButton" to="." method="_on_test_button_pressed"] +[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestLineButton" to="." method="_on_test_line_button_pressed"] +[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SupportButton" to="." method="_on_support_button_pressed"] +[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/DocsButton" to="." method="_on_docs_button_pressed"] +[connection signal="close_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_close_requested"] +[connection signal="open_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_open_requested"] +[connection signal="active_title_change" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_active_title_change"] +[connection signal="caret_changed" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_caret_changed"] +[connection signal="error_clicked" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_error_clicked"] +[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"] +[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"] +[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"] +[connection signal="file_double_clicked" from="QuickOpenDialog/QuickOpenFilesList" to="." method="_on_quick_open_files_list_file_double_clicked"] +[connection signal="file_selected" from="ExportDialog" to="." method="_on_export_dialog_file_selected"] +[connection signal="file_selected" from="ImportDialog" to="." method="_on_import_dialog_file_selected"] +[connection signal="confirmed" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_confirmed"] +[connection signal="custom_action" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_custom_action"] +[connection signal="result_selected" from="FindInFilesDialog/FindInFiles" to="." method="_on_find_in_files_result_selected"] diff --git a/addons/limbo_console.cfg b/addons/limbo_console.cfg new file mode 100644 index 0000000..1c53933 --- /dev/null +++ b/addons/limbo_console.cfg @@ -0,0 +1,39 @@ +[main] + +aliases={ +"exit": "quit", +"source": "exec", +"usage": "help" +} +disable_in_release_build=false +print_to_stdout=false +pause_when_open=true +commands_disabled_in_release=["eval"] + +[appearance] + +custom_theme="res://addons/limbo_console_theme.tres" +height_ratio=0.5 +open_speed=5.0 +opacity=1.0 +sparse_mode=false + +[greet] + +greet_user=true +greeting_message="{project_name}" +greet_using_ascii_art=true + +[history] + +persist_history=true +history_lines=1000 + +[autocomplete] + +autocomplete_use_history_with_matches=true + +[autoexec] + +autoexec_script="user://autoexec.lcs" +autoexec_auto_create=true diff --git a/addons/limbo_console/CONTRIBUTING.md b/addons/limbo_console/CONTRIBUTING.md new file mode 100644 index 0000000..feb500c --- /dev/null +++ b/addons/limbo_console/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing + +Thanks for your interest in contributing. Please follow these guidelines to keep the project consistent and maintainable in the long-term. + +## Vision + +- This is a simple text-based interface. Follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle) – keep the code simple and easy to maintain, abstractions minimal, and the API easy to use without reading a manual. +- Don't run any logic if the console is closed or hidden. + +## Code Style & Recommendations + +- Follow the official [GDScript Style Guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html). +- Use [static typing](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html) wherever possible. +- Apply the 80/20 rule: if a feature only benefits a small number of users, make it optional or don’t include it. +- To avoid unnecessary whitespace changes, please enable this setting in Godot: `Editor Settings > Text Editor > Behavior > Files > Trim Trailing Whitespace on Save` diff --git a/addons/limbo_console/LICENSE.md b/addons/limbo_console/LICENSE.md new file mode 100644 index 0000000..764fe19 --- /dev/null +++ b/addons/limbo_console/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Serhii Snitsaruk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/limbo_console/README.md b/addons/limbo_console/README.md new file mode 100644 index 0000000..8dd865a --- /dev/null +++ b/addons/limbo_console/README.md @@ -0,0 +1,128 @@ +

+ LimboConsole logo +

+ +--- +![Limbo Console](.github/demonstration.gif) + +![Static Badge](https://img.shields.io/badge/Godot-4.3-blue?style=flat) +[![GitHub License](https://img.shields.io/github/license/limbonaut/limbo_console)](https://github.com/limbonaut/limbo_console/blob/master/LICENSE.md) + +A simple and easy-to-use in-game dev console with a command interpreter for Godot Engine 4. + +It supports auto-completion with `TAB` for commands and history, auto-correction, inline hints and highlighting, command help text generation, argument parsing for basic types, aliases, custom theming, and more. + +This plugin is currently in development, so expect breaking changes. + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y2TCNH0) + + +## How to use + +> ℹ LimboConsole can be added as a Git submodule + +Place the source code in the `res://addons/limbo_console/` directory, and enable this plugin in the project settings, then reload the project. Toggle the console with the `GRAVE ACCENT` key (aka backtick - the key to the left of the `1` key). This can be changed in the Input Map tab in the project settings. + +Adding a new command is quite simple: + +```gdscript +func _ready() -> void: + LimboConsole.register_command(multiply) + +func multiply(a: float, b: float) -> void: + LimboConsole.info("a * b: " + str(a * b)) +``` + +> ℹ For C# support, see the next section. + +The example above adds a command that multiplies two numbers and prints the result (type `multiply 2 4`). Additionally, you can specify a name and a description: + +```gdscript +LimboConsole.register_command(multiply, "multiply", "multiply two numbers") +``` + +You can add a command as a subcommand of another command: + +```gdscript +# Register `multiply` as a subcommand under a new `math` command. +LimboConsole.register_command(multiply, "math multiply", "Multiply two numbers") +``` + +Now, you can enter `math multiply 2 4` in the console. By the way, the parent command doesn't have to exist. + +Several basic types are supported for command arguments, such as `bool`, `int`, `float`, `String` and `Vector{2,3,4}` types. To enter a `Vector2` argument, enclose its components in parentheses, like this: `(1 2)`. String arguments can also be enclosed in double quotation marks `"`. + +Autocompletion works for both command names and history. It can also be implemented for specific command arguments, as shown in the following example: +```gdscript +LimboConsole.register_command(teleport, "teleport", "teleport to site on this level") +LimboConsole.add_argument_autocomplete_source("teleport", 1, + func(): return ["entrance", "caves", "boss"] +) +``` +For a dynamically generated list of autocomplete values, the code could look like this: +```gdscript +LimboConsole.add_argument_autocomplete_source("teleport", 1, + func(): return get_tree().get_nodes_in_group("teleportation_site").map( + func(node): return node.name) +) +``` + +### Using in C# + +A community-maintained C# wrapper for this project is available as a NuGet package: https://github.com/ryan-linehan/limbo_console_sharp + +Thanks to @ryan-linehan for maintaining it! + +### Methods and properties + +Some notable methods and properties: + +- LimboConsole.enabled +- LimboConsole.register_command(callable, command_name, description) +- LimboConsole.unregister_command(callable_or_command_name) +- LimboConsole.add_alias(alias_name, command_name) +- LimboConsole.info(text_line) +- LimboConsole.error(text_line) +- LimboConsole.warning(text_line) +- LimboConsole.toggle_console() +- LimboConsole.add_argument_autocomplete_source(command_name, argument, callable) +- LimboConsole.execute_script(path, silent) + +This is not a complete list. For the rest, check out `limbo_console.gd`. + +### Keyboard Shortcuts + +- `Grave Accent` *(aka backtick - the key to the left of the `1` key)* — Toggle the console. +- `Enter` — Run entered command. +- `Tab` — Autocomplete command entry or cycle through autocomplete suggestions. +- `Shift+Tab` — Cycle through autocomplete suggestions in reverse. +- `Right` *(when cursor is at the end of command entry)* — Autocomplete according to inline hint (doesn't cycle like `Tab`). +- `Up/Down` — Cycle through command history, replacing contents of command entry. +- `Ctrl+R` — Toggle the history search interface (similar to [fzf](https://github.com/junegunn/fzf)). +- `Ctrl+C` *(when no text selected)* — Clear the command entry. + +### Configuration + +Options can be modified in the project-specific configuration file located at `res://addons/limbo_console.cfg`. This file is stored outside the plugin's directory to support adding the plugin as a Git submodule. + +LimboConsole also supports UI theming. Simply duplicate the `default_theme.tres` file and rename it to `limbo_console_theme.tres`. The file path is important - it should be located at `res://addons/limbo_console_theme.tres`. You can change this location in the config file. +Open the theme resource in Godot to customize it for your game. Console text colors can be adjusted in the `ConsoleColors` category. + +### Scripting + +You can execute simple scripts containing a sequence of commands: +```shell +exec lcs/my_script.lcs +``` + +Simple rules: +- Commands must be provided in the same syntax as in the prompt, with each command on a separate line. +- The script must exist at the specified path, either in the `res://` or `user://` directory. +- The script must have the `.lcs` extension, but when running the `exec` command, you can omit the extension in the command line. +- A line that starts with a '#' is treated as a comment and is not executed as part of the script. + +You can have a script execute automatically every time the game starts. There is a special script called `user://autoexec.lcs` that runs each time the game starts. This can be customized in the configuration file. + +### Contributing + +Check out [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/addons/limbo_console/ascii_art.gd b/addons/limbo_console/ascii_art.gd new file mode 100644 index 0000000..cc30e33 --- /dev/null +++ b/addons/limbo_console/ascii_art.gd @@ -0,0 +1,208 @@ +extends RefCounted + + +const boxed_map: Dictionary = { +'a': """ +▒▄▀█ +░█▀█ +""", +'b': """ +░█▄▄ +▒█▄█ +""", +'c': """ +░▄▀▀ +░▀▄▄ +""", +'d': """ +▒█▀▄ +░█▄▀ +""", +'e': """ +░██▀ +▒█▄▄ +""", +'f': """ +░█▀▀ +░█▀░ +""", +'g': """ +▒▄▀▀ +░▀▄█ +""", +'h': """ +░█▄█ +▒█▒█ +""", +'i': """ +░█ +░█ +""", +'j': """ +░░▒█ +░▀▄█ +""", +'k': """ +░█▄▀ +░█▒█ +""", +'l': """ +░█▒░ +▒█▄▄ +""", +'m': """ +▒█▀▄▀█ +░█▒▀▒█ +""", +'n': """ +░█▄░█ +░█▒▀█ +""", +'o': """ +░█▀█ +▒█▄█ +""", +'p': """ +▒█▀█ +░█▀▀ +""", +'q': """ +░▄▀▄ +░▀▄█ +""", +'r': """ +▒█▀█ +░█▀▄ +""", +'s': """ +░▄▀ +▒▄█ +""", +'t': """ +░▀█▀ +░▒█▒ +""", +'u': """ +░█░█ +▒█▄█ +""", +'v': """ +░█░█ +▒▀▄▀ +""", +'w': """ +▒█░█░█ +░▀▄▀▄▀ +""", +'x': """ +░▀▄▀ +░█▒█ +""", +'y': """ +░▀▄▀ +░▒█▒ +""", +'z': """ +░▀█ +▒█▄ +""", +' ': """ +░ +░ +""", +'_': """ +░░░ +▒▄▄ +""", +',': """ +░▒ +░█ +""", +'.': """ +░░ +░▄ +""", +'!': """ +░█ +░▄ +""", +'-': """ +░▒░ +░▀▀ +""", +'?': """ +░▀▀▄ +░▒█▀ +""", +'\'': """ +░▀ +░░ +""", +':': """ +░▄░ +▒▄▒ +""", +'0': """ +░▄▀▄ +░▀▄▀ +""", +'1': """ +░▄█ +░░█ +""", +'2': """ +░▀█ +░█▄ +""", +'3': """ +░▀██ +░▄▄█ +""", +'4': """ +░█▄ +░░█ +""", +'5': """ +░█▀ +░▄█ +""", +'6': """ +░█▀ +░██ +""", +'7': """ +░▀█ +░█░ +""", +'8': """ +░█▄█ +░█▄█ +""", +'9': """ +░██ +░▄█ +""", +} + +const unsupported_char := """ +░▒░ +▒░▒ +""" + + +static func str_to_boxed_art(p_text: String) -> PackedStringArray: + var lines: PackedStringArray = [] + lines.resize(2) + for c in p_text: + var ascii: String = boxed_map.get(c.to_lower(), unsupported_char) + var parts: PackedStringArray = ascii.split('\n') + lines[0] += parts[1] + lines[1] += parts[2] + return lines + + +static func is_boxed_art_supported(p_text: String) -> bool: + for c in p_text: + if not boxed_map.has(c.to_lower()): + return false + return true diff --git a/addons/limbo_console/ascii_art.gd.uid b/addons/limbo_console/ascii_art.gd.uid new file mode 100644 index 0000000..17004f0 --- /dev/null +++ b/addons/limbo_console/ascii_art.gd.uid @@ -0,0 +1 @@ +uid://coot1au4b7np diff --git a/addons/limbo_console/builtin_commands.gd b/addons/limbo_console/builtin_commands.gd new file mode 100644 index 0000000..6245727 --- /dev/null +++ b/addons/limbo_console/builtin_commands.gd @@ -0,0 +1,169 @@ +extends RefCounted +## BuiltinCommands + + +const Util := preload("res://addons/limbo_console/util.gd") + + +static func register_commands() -> void: + LimboConsole.register_command(cmd_alias, "alias", "add command alias") + LimboConsole.register_command(cmd_aliases, "aliases", "list all aliases") + LimboConsole.register_command(LimboConsole.clear_console, "clear", "clear console screen") + LimboConsole.register_command(cmd_commands, "commands", "list all commands") + LimboConsole.register_command(LimboConsole.info, "echo", "display a line of text") + LimboConsole.register_command(cmd_eval, "eval", "evaluate an expression") + LimboConsole.register_command(cmd_exec, "exec", "execute commands from file") + LimboConsole.register_command(cmd_fps_max, "fps_max", "limit framerate") + LimboConsole.register_command(cmd_fullscreen, "fullscreen", "toggle fullscreen mode") + LimboConsole.register_command(cmd_help, "help", "show command info") + LimboConsole.register_command(cmd_log, "log", "show recent log entries") + LimboConsole.register_command(cmd_quit, "quit", "exit the application") + LimboConsole.register_command(cmd_unalias, "unalias", "remove command alias") + LimboConsole.register_command(cmd_vsync, "vsync", "adjust V-Sync") + LimboConsole.register_command(LimboConsole.erase_history, "erase_history", "erases current history and persisted history") + LimboConsole.add_argument_autocomplete_source("help", 1, LimboConsole.get_command_names.bind(true)) + + +static func cmd_alias(p_alias: String, p_command: String) -> void: + LimboConsole.info("Adding %s => %s" % [LimboConsole.format_name(p_alias), p_command]) + LimboConsole.add_alias(p_alias, p_command) + + +static func cmd_aliases() -> void: + var aliases: Array = LimboConsole.get_aliases() + aliases.sort() + for alias in aliases: + var alias_argv: PackedStringArray = LimboConsole.get_alias_argv(alias) + var cmd_name: String = alias_argv[0] + var desc: String = LimboConsole.get_command_description(cmd_name) + alias_argv[0] = LimboConsole.format_name(cmd_name) + if desc.is_empty(): + LimboConsole.info(LimboConsole.format_name(alias)) + else: + LimboConsole.info("%s is alias of: %s %s" % [ + LimboConsole.format_name(alias), + ' '.join(alias_argv), + LimboConsole.format_tip(" // " + desc) + ]) + + +static func cmd_commands() -> void: + LimboConsole.info("Available commands:") + for name in LimboConsole.get_command_names(false): + var desc: String = LimboConsole.get_command_description(name) + name = LimboConsole.format_name(name) + LimboConsole.info(name if desc.is_empty() else "%s -- %s" % [name, desc]) + + +static func cmd_eval(p_expression: String) -> Error: + var exp := Expression.new() + var err: int = exp.parse(p_expression, LimboConsole.get_eval_input_names()) + if err != OK: + LimboConsole.error(exp.get_error_text()) + return err + var result = exp.execute(LimboConsole.get_eval_inputs(), + LimboConsole.get_eval_base_instance()) + if not exp.has_execute_failed(): + if result != null: + LimboConsole.info(str(result)) + return OK + else: + LimboConsole.error(exp.get_error_text()) + return ERR_SCRIPT_FAILED + + +static func cmd_exec(p_file: String, p_silent: bool = true) -> void: + if not p_file.ends_with(".lcs"): + # Prevent users from reading other game assets. + p_file += ".lcs" + if not FileAccess.file_exists(p_file): + p_file = "user://" + p_file + LimboConsole.execute_script(p_file, p_silent) + + +static func cmd_fps_max(p_limit: int = -1) -> void: + if p_limit < 0: + if Engine.max_fps == 0: + LimboConsole.info("Framerate is unlimited.") + else: + LimboConsole.info("Framerate is limited to %d FPS." % [Engine.max_fps]) + return + + Engine.max_fps = p_limit + if p_limit > 0: + LimboConsole.info("Limiting framerate to %d FPS." % [p_limit]) + elif p_limit == 0: + LimboConsole.info("Removing framerate limits.") + + +static func cmd_fullscreen() -> void: + if LimboConsole.get_viewport().mode == Window.MODE_WINDOWED: + # get_viewport().mode = Window.MODE_EXCLUSIVE_FULLSCREEN + LimboConsole.get_viewport().mode = Window.MODE_FULLSCREEN + LimboConsole.info("Window switched to fullscreen mode.") + else: + LimboConsole.get_viewport().mode = Window.MODE_WINDOWED + LimboConsole.info("Window switched to windowed mode.") + + +static func cmd_help(p_command_name: String = "") -> Error: + if p_command_name.is_empty(): + LimboConsole.print_line(LimboConsole.format_tip("Type %s to list all available commands." % + [LimboConsole.format_name("commands")])) + LimboConsole.print_line(LimboConsole.format_tip("Type %s to get more info about the command." % + [LimboConsole.format_name("help command")])) + return OK + else: + return LimboConsole.usage(p_command_name) + + +static func cmd_log(p_num_lines: int = 10) -> Error: + var fn: String = ProjectSettings.get_setting("debug/file_logging/log_path") + var file = FileAccess.open(fn, FileAccess.READ) + if not file: + LimboConsole.error("Can't open file: " + fn) + return ERR_CANT_OPEN + var contents := file.get_as_text() + var lines := contents.split('\n') + if lines.size() and lines[lines.size() - 1].strip_edges() == "": + lines.remove_at(lines.size() - 1) + lines = lines.slice(maxi(lines.size() - p_num_lines, 0)) + for line in lines: + LimboConsole.print_line(Util.bbcode_escape(line), false) + return OK + + +static func cmd_quit() -> void: + LimboConsole.get_tree().quit() + + +static func cmd_unalias(p_alias: String) -> void: + if LimboConsole.has_alias(p_alias): + LimboConsole.remove_alias(p_alias) + LimboConsole.info("Alias removed.") + else: + LimboConsole.warn("Alias not found.") + + +static func cmd_vsync(p_mode: int = -1) -> void: + if p_mode < 0: + var current: int = DisplayServer.window_get_vsync_mode() + if current == 0: + LimboConsole.info("V-Sync: disabled.") + elif current == 1: + LimboConsole.info('V-Sync: enabled.') + elif current == 2: + LimboConsole.info('Current V-Sync mode: adaptive.') + LimboConsole.info("Adjust V-Sync mode with an argument: 0 - disabled, 1 - enabled, 2 - adaptive.") + elif p_mode == DisplayServer.VSYNC_DISABLED: + LimboConsole.info("Changing to disabled.") + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + elif p_mode == DisplayServer.VSYNC_ENABLED: + LimboConsole.info("Changing to default V-Sync.") + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED) + elif p_mode == DisplayServer.VSYNC_ADAPTIVE: + LimboConsole.info("Changing to adaptive V-Sync.") + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE) + else: + LimboConsole.error("Invalid mode.") + LimboConsole.info("Acceptable modes: 0 - disabled, 1 - enabled, 2 - adaptive.") diff --git a/addons/limbo_console/builtin_commands.gd.uid b/addons/limbo_console/builtin_commands.gd.uid new file mode 100644 index 0000000..04832d2 --- /dev/null +++ b/addons/limbo_console/builtin_commands.gd.uid @@ -0,0 +1 @@ +uid://npao6ckwv7cm diff --git a/addons/limbo_console/command_entry.gd b/addons/limbo_console/command_entry.gd new file mode 100644 index 0000000..aa01428 --- /dev/null +++ b/addons/limbo_console/command_entry.gd @@ -0,0 +1,130 @@ +extends TextEdit +## CommandEntry + + +signal text_submitted(command_line: String) +signal autocomplete_requested() + +var autocomplete_hint: String: + set(value): + if autocomplete_hint != value: + autocomplete_hint = value + queue_redraw() + +var _font: Font +var _font_size: int +var _hint_color: Color +var _sb_normal: StyleBox + +func _init() -> void: + syntax_highlighter = CommandEntryHighlighter.new() + + +func _ready() -> void: + caret_multiple = false + autowrap_mode = TextServer.AUTOWRAP_OFF + scroll_fit_content_height = true + # placeholder_text = "" + + get_v_scroll_bar().visibility_changed.connect(_hide_scrollbars) + get_h_scroll_bar().visibility_changed.connect(_hide_scrollbars) + _hide_scrollbars() + + _font = get_theme_font("font") + _font_size = get_theme_font_size("font_size") + _hint_color = get_theme_color("hint_color") + _sb_normal = get_theme_stylebox("normal") + + +func _notification(what: int) -> void: + match what: + NOTIFICATION_FOCUS_ENTER: + set_process_input(true) + NOTIFICATION_FOCUS_EXIT: + set_process_input(false) + + +func _input(event: InputEvent) -> void: + if not has_focus(): + return + if event is InputEventKey: + if event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER: + if event.is_pressed(): + submit_text() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_C and event.get_modifiers_mask() == KEY_MASK_CTRL and get_selected_text().is_empty(): + # Clear input on Ctrl+C if no text selected. + if event.is_pressed(): + text = "" + text_changed.emit() + get_viewport().set_input_as_handled() + elif event.keycode in [KEY_RIGHT, KEY_END] and get_caret_column() == text.length(): + # Request autocomplete on RIGHT & END. + if event.is_pressed() and not autocomplete_hint.is_empty(): + autocomplete_requested.emit() + get_viewport().set_input_as_handled() + + +func _draw() -> void: + var offset_x: int = 0 + offset_x += _sb_normal.get_offset().x * 0.5 + offset_x += get_line_width(0) + + var offset_y: int = 0 + offset_y += _sb_normal.get_offset().y * 0.5 + offset_y += get_line_height() + 0.5 # + line_spacing + offset_y -= _font.get_descent(_font_size) + + draw_string(_font, Vector2(offset_x, offset_y), autocomplete_hint, 0, -1, _font_size, _hint_color) + + +func submit_text() -> void: + text_submitted.emit(text) + + +func _hide_scrollbars() -> void: + get_v_scroll_bar().hide() + get_h_scroll_bar().hide() + + +class CommandEntryHighlighter extends SyntaxHighlighter: + var command_found_color: Color + var subcommand_color: Color + var command_not_found_color: Color + var text_color: Color + + func _get_line_syntax_highlighting(line: int) -> Dictionary: + var text: String = get_text_edit().text + var command_end_idx: int = -1 # index where last recognized command ends (with subcommands) + + var argv: PackedStringArray = [] # argument vector (aka tokens) + var argi: PackedInt32Array = [] # argument starting indices in text + var start: int = 0 + var cur: int = 0 + for char in text + ' ': + if char == ' ': + if cur > start: + argv.append(text.substr(start, cur - start)) + argi.append(start) + var maybe_command: String = ' '.join(argv) + if LimboConsole.has_command(maybe_command) or LimboConsole.has_alias(maybe_command): + command_end_idx = cur + start = cur + 1 + cur += 1 + + var command_color: Color + var arg_start_idx: int = 0 # index where arguments start + + if command_end_idx > -1: + command_color = command_found_color + arg_start_idx = command_end_idx + 1 + else: + command_color = command_not_found_color + arg_start_idx = argi[1] if argi.size() > 1 else text.length() + + var result: Dictionary + result[0] = { "color": command_color } + if command_end_idx > -1 and argi.size() > 1: + result[argi[1]] = { "color": subcommand_color } + result[arg_start_idx] = { "color": text_color } + return result diff --git a/addons/limbo_console/command_entry.gd.uid b/addons/limbo_console/command_entry.gd.uid new file mode 100644 index 0000000..cfc9284 --- /dev/null +++ b/addons/limbo_console/command_entry.gd.uid @@ -0,0 +1 @@ +uid://ddtreeatktyov diff --git a/addons/limbo_console/command_history.gd b/addons/limbo_console/command_history.gd new file mode 100644 index 0000000..76687ad --- /dev/null +++ b/addons/limbo_console/command_history.gd @@ -0,0 +1,165 @@ +extends RefCounted +## Manages command history. + + +const HISTORY_FILE := "user://limbo_console_history.log" + + +var _entries: PackedStringArray +var _hist_idx = -1 +var _iterators: Array[WrappingIterator] +var _is_dirty: bool = false + + +func push_entry(p_entry: String) -> void: + _push_entry(p_entry) + _reset_iterators() + + +func _push_entry(p_entry: String) -> void: + var idx: int = _entries.find(p_entry) + if idx != -1: + # Duplicate commands not allowed in history. + _entries.remove_at(idx) + _entries.append(p_entry) + _is_dirty = true + + +func get_entry(p_index: int) -> String: + return _entries[clampi(p_index, 0, _entries.size())] + + +func create_iterator() -> WrappingIterator: + var it := WrappingIterator.new(_entries) + _iterators.append(it) + return it + + +func release_iterator(p_iter: WrappingIterator) -> void: + _iterators.erase(p_iter) + + +func size() -> int: + return _entries.size() + + +func trim(p_max_size: int) -> void: + if _entries.size() > p_max_size: + _entries.slice(p_max_size - _entries.size()) + _reset_iterators() + + +func clear() -> void: + _entries.clear() + + +func load(p_path: String = HISTORY_FILE) -> void: + var file := FileAccess.open(p_path, FileAccess.READ) + if not file: + return + while not file.eof_reached(): + var line: String = file.get_line().strip_edges() + if not line.is_empty(): + _push_entry(line) + file.close() + _reset_iterators() + _is_dirty = false + + +func save(p_path: String = HISTORY_FILE) -> void: + if not _is_dirty: + return + var file := FileAccess.open(p_path, FileAccess.WRITE) + if not file: + push_error("LimboConsole: Failed to save console history to file: ", p_path) + return + for line in _entries: + file.store_line(line) + file.close() + _is_dirty = false + + +## Searches history and returns an array starting with most relevant entries. +func fuzzy_match(p_query: String) -> PackedStringArray: + if len(p_query) == 0: + var copy := _entries.duplicate() + copy.reverse() + return copy + + var results: Array = [] + for entry: String in _entries: + var score: int = _compute_match_score(p_query.to_lower(), entry.to_lower()) + if score > 0: + results.append({"entry": entry, "score": score}) + + results.sort_custom(func(a, b): return a.score > b.score) + return results.map(func(rec): return rec.entry) + + +func _reset_iterators() -> void: + for it in _iterators: + it._reassign(_entries) + + +## Scoring function for fuzzy matching. +static func _compute_match_score(query: String, target: String) -> int: + var score: int = 0 + var query_index: int = 0 + + # Exact match. give unbeatable score + if query == target: + score = 99999 + return score + + for i in range(target.length()): + if query_index < query.length() and target[i] == query[query_index]: + score += 10 # Base score for a match + if i == 0 or target[i - 1] == " ": # Bonus for word start + score += 5 + query_index += 1 + if query_index == query.length(): + break + + # Ensure full query matches + return score if query_index == query.length() else 0 + + +## Iterator that wraps around and resets on history change. +class WrappingIterator: + extends RefCounted + + var _idx: int = -1 + var _entries: PackedStringArray + + + func _init(p_entries: PackedStringArray) -> void: + _entries = p_entries + + + func prev() -> String: + _idx = wrapi(_idx - 1, -1, _entries.size()) + if _idx == -1: + return String() + return _entries[_idx] + + + func next() -> String: + _idx = wrapi(_idx + 1, -1, _entries.size()) + if _idx == -1: + return String() + return _entries[_idx] + + + func current() -> String: + if _idx < 0 or _idx >= _entries.size(): + return String() + return _entries[_idx] + + + func reset() -> void: + _idx = -1 + + + func _reassign(p_entries: PackedStringArray) -> void: + _idx = -1 + _entries = p_entries diff --git a/addons/limbo_console/command_history.gd.uid b/addons/limbo_console/command_history.gd.uid new file mode 100644 index 0000000..7cc1345 --- /dev/null +++ b/addons/limbo_console/command_history.gd.uid @@ -0,0 +1 @@ +uid://dc55ouwu3ylf diff --git a/addons/limbo_console/config_mapper.gd b/addons/limbo_console/config_mapper.gd new file mode 100644 index 0000000..a076947 --- /dev/null +++ b/addons/limbo_console/config_mapper.gd @@ -0,0 +1,77 @@ +@tool +extends RefCounted +## Store object properties in an INI-style configuration file. + + +const CONFIG_PATH_PROPERTY := &"CONFIG_PATH" +const MAIN_SECTION_PROPERTY := &"MAIN_SECTION" +const MAIN_SECTION_DEFAULT := "main" + +static var verbose: bool = false + +static func _get_config_file(p_object: Object) -> String: + var from_object_constant = p_object.get(CONFIG_PATH_PROPERTY) + return from_object_constant if from_object_constant is String else "" + + +static func _get_main_section(p_object: Object) -> String: + var from_object_constant = p_object.get(MAIN_SECTION_PROPERTY) + return from_object_constant if from_object_constant != null else MAIN_SECTION_DEFAULT + + +static func _msg(p_text: String, p_arg1 = "") -> void: + if verbose: + print(p_text, p_arg1) + + +## Load object properties from config file and return status. [br] +## If p_config_path is empty, configuration path is taken from object's CONFIG_PATH property. +static func load_from_config(p_object: Object, p_config_path: String = "") -> int: + var config_path: String = p_config_path + if config_path.is_empty(): + config_path = _get_config_file(p_object) + var section: String = _get_main_section(p_object) + var config := ConfigFile.new() + var err: int = config.load(config_path) + if err != OK: + _msg("ConfigMapper: Failed to load config: %s err_code: %d" % [config_path, err]) + return err + _msg("ConfigMapper: Loading config: ", config_path) + + for prop_info in p_object.get_property_list(): + if prop_info.usage & PROPERTY_USAGE_CATEGORY and prop_info.hint_string.is_empty(): + _msg("ConfigMapper: Processing category: ", prop_info.name) + section = prop_info.name + elif prop_info.usage & PROPERTY_USAGE_SCRIPT_VARIABLE and prop_info.usage & PROPERTY_USAGE_STORAGE: + var value = null + if config.has_section_key(section, prop_info.name): + value = config.get_value(section, prop_info.name) + if value != null and typeof(value) == prop_info.type: + _msg("ConfigMapper: Loaded setting: %s section: %s value: %s" % [prop_info.name, section, value]) + p_object.set(prop_info.name, value) + _msg("ConfigMapper: Finished with code: ", OK) + return OK + + +## Save object properties to config file and return status. [br] +## If p_config_path is empty, configuration path is taken from object's CONFIG_PATH property. [br] +## WARNING: replaces file contents! +static func save_to_config(p_object: Object, p_config_path: String = "") -> int: + var config_path: String = p_config_path + if config_path.is_empty(): + config_path = _get_config_file(p_object) + var section: String = _get_main_section(p_object) + var config := ConfigFile.new() + _msg("ConfigMapper: Saving config: ", config_path) + + for prop_info in p_object.get_property_list(): + if prop_info.usage & PROPERTY_USAGE_CATEGORY and prop_info.hint_string.is_empty(): + _msg("ConfigMapper: Processing category: ", prop_info.name) + section = prop_info.name + elif prop_info.usage & PROPERTY_USAGE_SCRIPT_VARIABLE and prop_info.usage & PROPERTY_USAGE_STORAGE: + _msg("ConfigMapper: Saving setting: %s section: %s value: %s" % [prop_info.name, section, p_object.get(prop_info.name)]) + config.set_value(section, prop_info.name, p_object.get(prop_info.name)) + + var err: int = config.save(config_path) + _msg("ConfigMapper: Finished with code: ", err) + return err diff --git a/addons/limbo_console/config_mapper.gd.uid b/addons/limbo_console/config_mapper.gd.uid new file mode 100644 index 0000000..693014b --- /dev/null +++ b/addons/limbo_console/config_mapper.gd.uid @@ -0,0 +1 @@ +uid://dvokj0q23nb50 diff --git a/addons/limbo_console/console_options.gd b/addons/limbo_console/console_options.gd new file mode 100644 index 0000000..4b5ed0b --- /dev/null +++ b/addons/limbo_console/console_options.gd @@ -0,0 +1,40 @@ +extends RefCounted + +# Configuration is outside of limbo_console directory for compatibility with GIT submodules +const CONFIG_PATH := "res://addons/limbo_console.cfg" + +@export var aliases := { + "exit": "quit", + "source": "exec", + "usage": "help", +} +@export var disable_in_release_build: bool = false +@export var print_to_stdout: bool = false +@export var pause_when_open: bool = true + +@export var commands_disabled_in_release: Array = [ + "eval" # enables arbitrary code execution and asset extraction in the running game. +] + +@export_category("appearance") +@export var custom_theme: String = "res://addons/limbo_console_theme.tres" +@export var height_ratio: float = 0.5 +@export var open_speed: float = 5.0 # For instant, set to a really high float like 99999.0 +@export var opacity: float = 1.0 +@export var sparse_mode: bool = false # Print empty line after each command execution. + +@export_category("greet") +@export var greet_user: bool = true +@export var greeting_message: String = "{project_name}" +@export var greet_using_ascii_art: bool = true + +@export_category("history") +@export var persist_history: bool = true +@export var history_lines: int = 1000 + +@export_category("autocomplete") +@export var autocomplete_use_history_with_matches: bool = true + +@export_category("autoexec") +@export var autoexec_script: String = "user://autoexec.lcs" +@export var autoexec_auto_create: bool = true diff --git a/addons/limbo_console/console_options.gd.uid b/addons/limbo_console/console_options.gd.uid new file mode 100644 index 0000000..4918541 --- /dev/null +++ b/addons/limbo_console/console_options.gd.uid @@ -0,0 +1 @@ +uid://c7a12a1pe5esr diff --git a/addons/limbo_console/history_gui.gd b/addons/limbo_console/history_gui.gd new file mode 100644 index 0000000..5517ebb --- /dev/null +++ b/addons/limbo_console/history_gui.gd @@ -0,0 +1,256 @@ +extends Panel + +const CommandHistory := preload("res://addons/limbo_console/command_history.gd") + +# Visual Elements +var _last_highlighted_label: Label +var _history_labels: Array[Label] +var _scroll_bar: VScrollBar +var _scroll_bar_width: int = 12 + +# Indexing Results +var _command: String = "" # Needs default value so first search always processes +var _history: CommandHistory # Command history to search through +var _filter_results: PackedStringArray # Most recent results of performing a search for the _command in _history + +var _display_count: int = 0 # Number of history items to display in search +var _offset: int = 0 # The offset _filter_results +var _sub_index: int = 0 # The highlight index + +# Theme Cache +var _highlight_color: Color + + +# *** GODOT / VIRTUAL + + +func _init(p_history: CommandHistory) -> void: + _history = p_history + + set_anchors_preset(Control.PRESET_FULL_RECT) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + size_flags_vertical = Control.SIZE_EXPAND_FILL + + # Create first label, and set placeholder text to determine the display size + # once this node is _ready(). There should always be one label at minimum + # anyways since this search is usless without a way to show results. + var new_item := Label.new() + new_item.size_flags_vertical = Control.SIZE_SHRINK_END + new_item.size_flags_horizontal = Control.SIZE_EXPAND_FILL + new_item.text = "" + add_child(new_item) + _history_labels.append(new_item) + + _scroll_bar = VScrollBar.new() + add_child(_scroll_bar) + + +func _ready() -> void: + # The sizing of the labels is dependant on visiblity. + visibility_changed.connect(_calculate_display_count) + _scroll_bar.scrolling.connect(_scroll_bar_scrolled) + + _highlight_color = get_theme_color(&"history_highlight_color", &"ConsoleColors") + + +func _input(event: InputEvent) -> void: + if not is_visible_in_tree(): + return + + # Scroll up/down on mouse wheel up/down + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_WHEEL_UP: + _increment_index() + elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + _decrement_index() + + # Remaining inputs are key press handles + if event is not InputEventKey: + return + + # Increment/Decrement index + if event.keycode == KEY_UP and event.is_pressed(): + _increment_index() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_DOWN and event.is_pressed(): + _decrement_index() + get_viewport().set_input_as_handled() + + +# *** PUBLIC + + +## Set visibility of history search +func set_visibility(p_visible: bool) -> void: + if not visible and p_visible: + # It's possible the _history has updated while not visible + # make sure the filtered list is up-to-date + _search_and_filter() + visible = p_visible + + +## Move cursor downwards +func _decrement_index() -> void: + var current_index: int = _get_current_index() + if current_index - 1 < 0: + return + + if _sub_index == 0: + _offset -= 1 + _update_scroll_list() + else: + _sub_index -= 1 + _update_highlight() + + +## Move cursor upwards +func _increment_index() -> void: + var current_index: int = _get_current_index() + if current_index + 1 >= _filter_results.size(): + return + + if _sub_index >= _display_count - 1: + _offset += 1 + _update_scroll_list() + else: + _sub_index += 1 + _update_highlight() + + +## Get the current selected text +func get_current_text() -> String: + var current_text: String = _command + if _history_labels.size() != 0 and _filter_results.size() != 0: + current_text = _filter_results[_get_current_index()] + return current_text + + +## Search for the command in the history +func search(command: String) -> void: + # Don't process if we used the same command before + if command == _command: + return + _command = command + + _search_and_filter() + + +# *** PRIVATE + + +## Update the text in the scroll list to match current offset and filtered results +func _update_scroll_list() -> void: + # Iterate through the number of displayed history items + for i in range(0, _display_count): + var filter_index: int = _offset + i + + # Default empty + _history_labels[i].text = "" + + # Set non empty if in range + var index_in_range: bool = filter_index < _filter_results.size() + if index_in_range: + _history_labels[i].text += _filter_results[filter_index] + + _update_scroll_bar() + + +## Highlight the subindex +func _update_highlight() -> void: + if _sub_index < 0 or _history.size() == 0: + return + + var style := StyleBoxFlat.new() + style.bg_color = _highlight_color + + # Always clear out the highlight of the last label + if is_instance_valid(_last_highlighted_label): + _last_highlighted_label.remove_theme_stylebox_override("normal") + + if _filter_results.size() <= 0: + return + + _history_labels[_sub_index].add_theme_stylebox_override("normal", style) + _last_highlighted_label = _history_labels[_sub_index] + + +## Get the current index of the selected item +func _get_current_index() -> int: + return _offset + _sub_index + + +## Reset offset and sub_indexes to scroll list back to bottom +func _reset_indexes() -> void: + _offset = 0 + _sub_index = 0 + + +## When the scrollbar has been scrolled (by mouse), scroll the list +func _scroll_bar_scrolled() -> void: + _offset = _scroll_bar.max_value - _display_count - _scroll_bar.value + _update_highlight() + _update_scroll_list() + + +func _calculate_display_count(): + if not visible: + return + # The display count is finnicky to get right due to the label needing to be + # rendered so the fize can be determined. This gets the job done, it ain't + # pretty, but it works + var max_y: float = size.y + + var label_size_y: float = (_history_labels[0] as Control).size.y + var label_size_x: float = size.x - _scroll_bar_width + + var display_count: int = int(max_y) / int(label_size_y) + if _display_count != display_count and display_count != 0 and display_count > _display_count: + _display_count = (display_count as int) + + # Since the labels are going from the bottom to the top, the label + # coordinates are offset from the bottom by label size. + # The first label already exists, so it's handlded by itself + _history_labels[0].position.y = size.y - label_size_y + _history_labels[0].set_size(Vector2(label_size_x, label_size_y)) + # The remaining labels may or may not exist already, create them + for i in range(0, _display_count - _history_labels.size()): + var new_item := Label.new() + new_item.size_flags_vertical = Control.SIZE_SHRINK_END + new_item.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + # The +1 is due to the labels going upwards from the bottom, otherwise + # their position will be 1 row lower than they should be + var position_offset: int = _history_labels.size() + 1 + new_item.position.y = size.y - (position_offset * label_size_y) + new_item.set_size(Vector2(label_size_x, label_size_y)) + _history_labels.append(new_item) + add_child(new_item) + + # Update the scroll bar to be positioned correctly + _scroll_bar.size.x = _scroll_bar_width + _scroll_bar.size.y = size.y + _scroll_bar.position.x = label_size_x + + _reset_history_to_beginning() + + +func _update_scroll_bar() -> void: + if _display_count > 0: + var max_size: int = _filter_results.size() + _scroll_bar.max_value = max_size + _scroll_bar.page = _display_count + _scroll_bar.set_value_no_signal((max_size - _display_count) - _offset) + + +## Reset indexes to 0, scroll to the bottom of the history list, and update visuals +func _reset_history_to_beginning() -> void: + _reset_indexes() + _update_highlight() + _update_scroll_list() + + +## Search for the current command and filter the results +func _search_and_filter() -> void: + _filter_results = _history.fuzzy_match(_command) + + _reset_history_to_beginning() diff --git a/addons/limbo_console/history_gui.gd.uid b/addons/limbo_console/history_gui.gd.uid new file mode 100644 index 0000000..5c17ca4 --- /dev/null +++ b/addons/limbo_console/history_gui.gd.uid @@ -0,0 +1 @@ +uid://cpl5jb0mwxanh diff --git a/addons/limbo_console/limbo_console.gd b/addons/limbo_console/limbo_console.gd new file mode 100644 index 0000000..8a84d03 --- /dev/null +++ b/addons/limbo_console/limbo_console.gd @@ -0,0 +1,1083 @@ +extends CanvasLayer +## LimboConsole + +signal toggled(is_shown) + +const THEME_DEFAULT := "res://addons/limbo_console/res/default_theme.tres" + +const AsciiArt := preload("res://addons/limbo_console/ascii_art.gd") +const BuiltinCommands := preload("res://addons/limbo_console/builtin_commands.gd") +const CommandEntry := preload("res://addons/limbo_console/command_entry.gd") +const ConfigMapper := preload("res://addons/limbo_console/config_mapper.gd") +const ConsoleOptions := preload("res://addons/limbo_console/console_options.gd") +const Util := preload("res://addons/limbo_console/util.gd") +const CommandHistory := preload("res://addons/limbo_console/command_history.gd") +const HistoryGui := preload("res://addons/limbo_console/history_gui.gd") + +const MAX_SUBCOMMANDS: int = 4 + +## If false, prevents console from being shown. Commands can still be executed from code. +var enabled: bool = true: + set(value): + enabled = value + set_process_input(enabled) + if not enabled and _control.visible: + _is_open = false + set_process(false) + _hide_console() + +var _control: Control +var _history_gui: HistoryGui +var _control_block: Control +var _output: RichTextLabel +var _entry: CommandEntry +var _previous_gui_focus: Control + +# Theme colors +var _output_command_color: Color +var _output_command_mention_color: Color +var _output_error_color: Color +var _output_warning_color: Color +var _output_text_color: Color +var _output_debug_color: Color +var _entry_text_color: Color +var _entry_hint_color: Color +var _entry_command_found_color: Color +var _entry_subcommand_color: Color +var _entry_command_not_found_color: Color + +var _options: ConsoleOptions +var _commands: Dictionary # "command" => Callable, or "command sub1 sub2" => Callable +var _aliases: Dictionary # "alias" => command_to_run: PackedStringArray (alias may contain subcommands) +var _command_descriptions: Dictionary # command_name => description_text +var _argument_autocomplete_sources: Dictionary # [command_name, arg_idx] => Callable +var _history: CommandHistory +var _history_iter: CommandHistory.WrappingIterator +var _autocomplete_matches: PackedStringArray +var _eval_inputs: Dictionary +var _silent: bool = false +var _was_already_paused: bool = false + +var _open_t: float = 0.0 +var _open_speed: float = 5.0 +var _is_open: bool = false + + +func _init() -> void: + layer = 9999 + process_mode = ProcessMode.PROCESS_MODE_ALWAYS + + _options = ConsoleOptions.new() + ConfigMapper.load_from_config(_options) + + _history = CommandHistory.new() + if _options.persist_history: + _history.load() + _history_iter = _history.create_iterator() + + _build_gui() + _init_theme() + _control.hide() + _control_block.hide() + + _open_speed = _options.open_speed + + if _options.disable_in_release_build: + enabled = OS.is_debug_build() + + +func _ready() -> void: + set_process(false) # Note, if you do it in _init(), it won't actually stop it for some reason. + BuiltinCommands.register_commands() + if _options.greet_user: + _greet() + _add_aliases_from_config.call_deferred() + _run_autoexec_script.call_deferred() + + _entry.autocomplete_requested.connect(_autocomplete) + _entry.text_submitted.connect(_on_entry_text_submitted) + _entry.text_changed.connect(_on_entry_text_changed) + + +func _exit_tree() -> void: + if _options.persist_history: + _history.trim(_options.history_lines) + _history.save() + + +func _handle_command_input(p_event: InputEvent) -> void: + var handled := true + if not _is_open: + pass # Don't accept input while closing console. + elif p_event.keycode == KEY_UP: + _fill_entry(_history_iter.prev()) + _clear_autocomplete() + _update_autocomplete() + elif p_event.keycode == KEY_DOWN: + _fill_entry(_history_iter.next()) + _clear_autocomplete() + _update_autocomplete() + elif p_event.is_action_pressed("limbo_auto_complete_reverse"): + _reverse_autocomplete() + elif p_event.keycode == KEY_TAB: + _autocomplete() + elif p_event.keycode == KEY_PAGEUP: + var scroll_bar: VScrollBar = _output.get_v_scroll_bar() + scroll_bar.value -= scroll_bar.page + elif p_event.keycode == KEY_PAGEDOWN: + var scroll_bar: VScrollBar = _output.get_v_scroll_bar() + scroll_bar.value += scroll_bar.page + else: + handled = false + if handled: + get_viewport().set_input_as_handled() + + +func _handle_history_input(p_event: InputEvent): + # Allow tab complete (reverse) + if p_event.is_action_pressed("limbo_auto_complete_reverse"): + _reverse_autocomplete() + get_viewport().set_input_as_handled() + # Allow tab complete (forward) + elif p_event.keycode == KEY_TAB and p_event.is_pressed(): + _autocomplete() + get_viewport().set_input_as_handled() + # Perform search + elif p_event is InputEventKey: + _history_gui.search(_entry.text) + _entry.grab_focus() + + # Make sure entry is always focused + _entry.grab_focus() + + +func _input(p_event: InputEvent) -> void: + if p_event.is_action_pressed("limbo_console_toggle"): + toggle_console() + get_viewport().set_input_as_handled() + # Check to see if the history gui should open + elif _control.visible and p_event.is_action_pressed("limbo_console_search_history"): + toggle_history() + get_viewport().set_input_as_handled() + elif _history_gui.visible and p_event is InputEventKey: + _handle_history_input(p_event) + elif _control.visible and p_event is InputEventKey and p_event.is_pressed(): + _handle_command_input(p_event) + + +func _process(delta: float) -> void: + var done_sliding := false + if _is_open: + _open_t = move_toward(_open_t, 1.0, _open_speed * delta * 1.0/Engine.time_scale) + if _open_t == 1.0: + done_sliding = true + else: # We close faster than opening. + _open_t = move_toward(_open_t, 0.0, _open_speed * delta * 1.5 * 1.0/Engine.time_scale) + if is_zero_approx(_open_t): + done_sliding = true + + var eased := ease(_open_t, -1.75) + var new_y := remap(eased, 0, 1, -_control.size.y, 0) + _control.position.y = new_y + + if done_sliding: + set_process(false) + if not _is_open: + _hide_console() + + +# *** PUBLIC INTERFACE + + +func open_console() -> void: + if enabled: + _is_open = true + set_process(true) + _show_console() + + +func close_console() -> void: + if enabled: + _is_open = false + set_process(true) + _history_gui.visible = false + if _options.persist_history: + _history.save() + # _hide_console() is called in _process() + + +func is_open() -> bool: + return _is_open + + +func toggle_console() -> void: + if _is_open: + close_console() + else: + open_console() + + +func toggle_history() -> void: + _history_gui.set_visibility(not _history_gui.visible) + # Whenever the history gui becomes visible, make sure it has the latest + # history and do an initial search + if _history_gui.visible: + _history_gui.search(_entry.text) + + +## Clears all messages in the console. +func clear_console() -> void: + _output.text = "" + + +## Erases the history that is persisted to the disk +func erase_history() -> void: + _history.clear() + var file := FileAccess.open(CommandHistory.HISTORY_FILE, FileAccess.WRITE) + if file: + file.store_string("") + file.close() + + +## Prints an info message to the console and the output. +func info(p_line: String) -> void: + print_line(p_line) + + +## Prints an error message to the console and the output. +func error(p_line: String) -> void: + print_line("[color=%s]ERROR:[/color] %s" % [_output_error_color.to_html(), p_line]) + + +## Prints a warning message to the console and the output. +func warn(p_line: String) -> void: + print_line("[color=%s]WARNING:[/color] %s" % [_output_warning_color.to_html(), p_line]) + + +## Prints a debug message to the console and the output. +func debug(p_line: String) -> void: + print_line("[color=%s]DEBUG: %s[/color]" % [_output_debug_color.to_html(), p_line]) + + +## Prints a line using boxed ASCII art style. +func print_boxed(p_line: String) -> void: + for line in AsciiArt.str_to_boxed_art(p_line): + print_line(line) + + +## Prints a line to the console, and optionally to standard output. +func print_line(p_line: String, p_stdout: bool = _options.print_to_stdout) -> void: + if _silent: + return + _output.text += p_line + "\n" + if p_stdout: + print(Util.bbcode_strip(p_line)) + + +## Registers a callable as a command, with optional name and description. +## Name can have up to 4 space-separated identifiers (e.g., "command sub1 sub2 sub3"), +## using letters, digits, or underscores, starting with a non-digit. +func register_command(p_func: Callable, p_name: String = "", p_desc: String = "") -> void: + if p_name and not Util.is_valid_command_sequence(p_name): + push_error("LimboConsole: Failed to register command: %s. Name can have up to 4 space-separated identifiers, using letters, digits, or underscores, starting with non-digit." % [p_name]) + return + + if not _validate_callable(p_func): + push_error("LimboConsole: Failed to register command: %s" % [p_func if p_name.is_empty() else p_name]) + return + var name: String = p_name + if name.is_empty(): + if p_func.is_custom(): + push_error("LimboConsole: Failed to register command: Callable is not method and no name was provided") + return + name = p_func.get_method().trim_prefix("_").trim_prefix("cmd_") + if not OS.is_debug_build() and _options.commands_disabled_in_release.has(name): + return + if _commands.has(name): + push_error("LimboConsole: Command already registered: " + p_name) + return + # Note: It should be possible to have an alias with the same name. + _commands[name] = p_func + _command_descriptions[name] = p_desc + + +## Unregisters the command specified by its name or a callable. +func unregister_command(p_func_or_name) -> void: + var cmd_name: String + if p_func_or_name is Callable: + var key = _commands.find_key(p_func_or_name) + if key != null: + cmd_name = key + elif p_func_or_name is String: + cmd_name = p_func_or_name + if cmd_name.is_empty() or not _commands.has(cmd_name): + push_error("LimboConsole: Unregister failed - command not found: " % [p_func_or_name]) + return + + _commands.erase(cmd_name) + _command_descriptions.erase(cmd_name) + + for i in range(1, 5): + _argument_autocomplete_sources.erase([cmd_name, i]) + + +## Is a command or an alias registered by the given name. +func has_command(p_name: String) -> bool: + return _commands.has(p_name) + + +func get_command_names(p_include_aliases: bool = false) -> PackedStringArray: + var names: PackedStringArray = _commands.keys() + if p_include_aliases: + names.append_array(_aliases.keys()) + names.sort() + return names + + +func get_command_description(p_name: String) -> String: + return _command_descriptions.get(p_name, "") + + +## Registers an alias for command line. [br] +## Alias may contain space-separated parts, e.g. "command sub1" which must match +## against two subsequent arguments on the command line. +func add_alias(p_alias: String, p_command_to_run: String) -> void: + # It should be possible to override commands and existing aliases. + # It should be possible to create aliases for commands that are not yet registered, + # because some commands may be registered by local-to-scene scripts. + _aliases[p_alias] = _parse_command_line(p_command_to_run) + + +## Removes an alias by name. +func remove_alias(p_name: String) -> void: + _aliases.erase(p_name) + + +## Is an alias registered by the given name. +func has_alias(p_name: String) -> bool: + return _aliases.has(p_name) + + +## Lists all registered aliases. +func get_aliases() -> PackedStringArray: + return PackedStringArray(_aliases.keys()) + + +## Returns the alias's actual command as an argument vector. +func get_alias_argv(p_alias: String) -> PackedStringArray: + # TODO: I believe _aliases values are stored as an array so this iis unneccessary? + return _aliases.get(p_alias, [p_alias]).duplicate() + + +## Registers a callable that should return an array of possible values for the given argument and command. +## It will be used for autocompletion. +func add_argument_autocomplete_source(p_command: String, p_argument: int, p_source: Callable) -> void: + if not p_source.is_valid(): + push_error("LimboConsole: Can't add autocomplete source: source callable is not valid") + return + if not has_command(p_command): + push_error("LimboConsole: Can't add autocomplete source: command doesn't exist: ", p_command) + return + if p_argument < 0 or p_argument > 4: + push_error("LimboConsole: Can't add autocomplete source: argument index out of bounds: ", p_argument) + return + var argument_values = p_source.call() + if not _validate_autocomplete_result(argument_values, p_command): + push_error("LimboConsole: Failed to add argument autocomplete source: Callable must return an array.") + return + var key := [p_command, p_argument] + _argument_autocomplete_sources[key] = p_source + + +## Parses the command line and executes the command if it's valid. +func execute_command(p_command_line: String, p_silent: bool = false) -> void: + p_command_line = p_command_line.strip_edges() + if p_command_line.is_empty() or p_command_line.strip_edges().begins_with('#'): + return + + var argv: PackedStringArray = _parse_command_line(p_command_line) + var expanded_argv: PackedStringArray = _join_subcommands(_expand_alias(argv)) + var command_name: String = expanded_argv[0] + var command_args: Array = [] + + _silent = p_silent + if not p_silent: + var history_line: String = " ".join(argv) + _history.push_entry(history_line) + info("[color=%s][b]>[/b] %s[/color] %s" % + [_output_command_color.to_html(), argv[0], " ".join(argv.slice(1))]) + + if not has_command(command_name): + error("Unknown command: " + command_name) + _suggest_similar_command(expanded_argv) + _silent = false + return + + var cmd: Callable = _commands.get(command_name) + var valid: bool = _parse_argv(expanded_argv, cmd, command_args) + if valid: + var err = cmd.callv(command_args) + var failed: bool = typeof(err) == TYPE_INT and err > 0 + if failed: + _suggest_argument_corrections(expanded_argv) + else: + usage(argv[0]) + if _options.sparse_mode: + print_line("") + _silent = false + + +## Execute commands from file. +func execute_script(p_file: String, p_silent: bool = true) -> void: + if FileAccess.file_exists(p_file): + if not p_silent: + LimboConsole.info("Executing " + p_file); + var fa := FileAccess.open(p_file, FileAccess.READ) + while not fa.eof_reached(): + var line: String = fa.get_line() + execute_command(line, p_silent) + else: + error("File not found: " + p_file.trim_prefix("user://")) + + +## Formats the tip text (hopefully useful ;). +func format_tip(p_text: String) -> String: + return "[i][color=" + _output_debug_color.to_html() + "]" + p_text + "[/color][/i]" + + +## Formats the command name for display. +func format_name(p_name: String) -> String: + return "[color=" + _output_command_mention_color.to_html() + "]" + p_name + "[/color]" + + +## Prints the help text for the given command. +func usage(p_command: String) -> Error: + if _aliases.has(p_command): + var alias_argv: PackedStringArray = get_alias_argv(p_command) + var formatted_cmd := "%s %s" % [format_name(alias_argv[0]), ' '.join(alias_argv.slice(1))] + print_line("Alias of: " + formatted_cmd) + p_command = alias_argv[0] + + if not has_command(p_command): + error("Command not found: " + p_command) + return ERR_INVALID_PARAMETER + + var callable: Callable = _commands[p_command] + var method_info: Dictionary = Util.get_method_info(callable) + if method_info.is_empty(): + error("Couldn't find method info for: " + callable.get_method()) + print_line("Usage: ???") + + var usage_line: String = "Usage: %s" % [p_command] + var arg_lines: String = "" + var values_lines: String = "" + var required_args: int = method_info.args.size() - method_info.default_args.size() + + for i in range(method_info.args.size() - callable.get_bound_arguments_count()): + var arg_name: String = method_info.args[i].name.trim_prefix("p_") + var arg_type: int = method_info.args[i].type + if i < required_args: + usage_line += " " + arg_name + else: + usage_line += " [lb]" + arg_name + "[rb]" + var def_spec: String = "" + var num_required_args: int = method_info.args.size() - method_info.default_args.size() + if i >= num_required_args: + var def_value = method_info.default_args[i - num_required_args] + if typeof(def_value) == TYPE_STRING: + def_value = "\"" + def_value + "\"" + def_spec = " = %s" % [def_value] + arg_lines += " %s: %s%s\n" % [arg_name, type_string(arg_type) if arg_type != TYPE_NIL else "Variant", def_spec] + if _argument_autocomplete_sources.has([p_command, i]): + var auto_complete_callable: Callable = _argument_autocomplete_sources[[p_command, i]] + var arg_autocompletes = auto_complete_callable.call() + if len(arg_autocompletes) > 0: + var values: String = str(arg_autocompletes).replace("[", "").replace("]", "") + values_lines += " %s: %s\n" % [arg_name, values] + arg_lines = arg_lines.trim_suffix('\n') + + print_line(usage_line) + + var desc_line: String = "" + desc_line = _command_descriptions.get(p_command, "") + if not desc_line.is_empty(): + desc_line[0] = desc_line[0].capitalize() + if desc_line.right(1) != ".": + desc_line += "." + print_line(desc_line) + + if not arg_lines.is_empty(): + print_line("Arguments:") + print_line(arg_lines) + if not values_lines.is_empty(): + print_line("Values:") + print_line(values_lines) + return OK + + +## Define an input variable for "eval" command. +func add_eval_input(p_name: String, p_value) -> void: + _eval_inputs[p_name] = p_value + + +## Remove specified input variable from "eval" command. +func remove_eval_input(p_name) -> void: + _eval_inputs.erase(p_name) + + +## List the defined input variables used in "eval" command. +func get_eval_input_names() -> PackedStringArray: + return _eval_inputs.keys() + + +## Get input variable values used in "eval" command, listed in the same order as names. +func get_eval_inputs() -> Array: + return _eval_inputs.values() + + +## Define the object that will be used as the base instance for "eval" command. +## When defined, this object will be the "self" for expressions. +## Can be null (the default) to not use any base instance. +func set_eval_base_instance(object): + _eval_inputs["_base_instance"] = object + + +## Get the object that will be used as the base instance for "eval" command. +## Null by default. +func get_eval_base_instance(): + return _eval_inputs.get("_base_instance") + + +# *** PRIVATE + +# *** INITIALIZATION + + +func _build_gui() -> void: + var con := Control.new() # To block mouse input. + _control_block = con + con.set_anchors_preset(Control.PRESET_FULL_RECT) + add_child(con) + + var panel := PanelContainer.new() + _control = panel + panel.anchor_bottom = _options.height_ratio + panel.anchor_right = 1.0 + add_child(panel) + + var vbox := VBoxContainer.new() + vbox.set_anchors_preset(Control.PRESET_FULL_RECT) + panel.add_child(vbox) + + _output = RichTextLabel.new() + _output.size_flags_vertical = Control.SIZE_EXPAND_FILL + _output.scroll_active = true + _output.scroll_following = true + _output.bbcode_enabled = true + _output.focus_mode = Control.FOCUS_CLICK + vbox.add_child(_output) + + _entry = CommandEntry.new() + vbox.add_child(_entry) + + _control.modulate = Color(1.0, 1.0, 1.0, _options.opacity) + + _history_gui = HistoryGui.new(_history) + _output.add_child(_history_gui) + _history_gui.visible = false + + +func _init_theme() -> void: + var theme: Theme + if ResourceLoader.exists(_options.custom_theme, "Theme"): + theme = load(_options.custom_theme) + else: + theme = load(THEME_DEFAULT) + _control.theme = theme + + const CONSOLE_COLORS_THEME_TYPE := &"ConsoleColors" + _output_command_color = theme.get_color(&"output_command_color", CONSOLE_COLORS_THEME_TYPE) + _output_command_mention_color = theme.get_color(&"output_command_mention_color", CONSOLE_COLORS_THEME_TYPE) + _output_text_color = theme.get_color(&"output_text_color", CONSOLE_COLORS_THEME_TYPE) + _output_error_color = theme.get_color(&"output_error_color", CONSOLE_COLORS_THEME_TYPE) + _output_warning_color = theme.get_color(&"output_warning_color", CONSOLE_COLORS_THEME_TYPE) + _output_debug_color = theme.get_color(&"output_debug_color", CONSOLE_COLORS_THEME_TYPE) + _entry_text_color = theme.get_color(&"entry_text_color", CONSOLE_COLORS_THEME_TYPE) + _entry_hint_color = theme.get_color(&"entry_hint_color", CONSOLE_COLORS_THEME_TYPE) + _entry_command_found_color = theme.get_color(&"entry_command_found_color", CONSOLE_COLORS_THEME_TYPE) + _entry_subcommand_color = theme.get_color(&"entry_subcommand_color", CONSOLE_COLORS_THEME_TYPE) + _entry_command_not_found_color = theme.get_color(&"entry_command_not_found_color", CONSOLE_COLORS_THEME_TYPE) + + _output.add_theme_color_override(&"default_color", _output_text_color) + _entry.add_theme_color_override(&"font_color", _entry_text_color) + _entry.add_theme_color_override(&"hint_color", _entry_hint_color) + _entry.syntax_highlighter.command_found_color = _entry_command_found_color + _entry.syntax_highlighter.command_not_found_color = _entry_command_not_found_color + _entry.syntax_highlighter.subcommand_color = _entry_subcommand_color + _entry.syntax_highlighter.text_color = _entry_text_color + + +func _greet() -> void: + var message: String = _options.greeting_message + message = message.format({ + "project_name": ProjectSettings.get_setting("application/config/name"), + "project_version": ProjectSettings.get_setting("application/config/version"), + }) + if not message.is_empty(): + if _options.greet_using_ascii_art and AsciiArt.is_boxed_art_supported(message): + print_boxed(message) + info("") + else: + info("[b]" + message + "[/b]") + BuiltinCommands.cmd_help() + info(format_tip("-----")) + + +func _add_aliases_from_config() -> void: + for alias in _options.aliases: + var target = _options.aliases[alias] + if not alias is String: + push_error("LimboConsole: Config error: Alias name should be String") + elif not target is String: + push_error("LimboConsole: Config error: Alias target should be String") + elif has_command(alias): + push_error("LimboConsole: Config error: Alias or command already registered: ", alias) + elif not has_command(target): + push_error("LimboConsole: Config error: Alias target not found: ", target) + else: + add_alias(alias, target) + + +func _run_autoexec_script() -> void: + if _options.autoexec_script.is_empty(): + return + if _options.autoexec_auto_create and not FileAccess.file_exists(_options.autoexec_script): + FileAccess.open(_options.autoexec_script, FileAccess.WRITE) + if FileAccess.file_exists(_options.autoexec_script): + execute_script(_options.autoexec_script) + + +# *** PARSING + + +## Splits the command line string into an array of arguments (aka argv). +func _parse_command_line(p_line: String) -> PackedStringArray: + var argv: PackedStringArray = [] + var arg: String = "" + var in_quotes: bool = false + var in_brackets: bool = false + var line: String = p_line.strip_edges() + var start: int = 0 + var cur: int = 0 + for char in line: + if char == '"': + in_quotes = not in_quotes + elif char == '(': + in_brackets = true + elif char == ')': + in_brackets = false + elif char == ' ' and not in_quotes and not in_brackets: + if cur > start: + argv.append(line.substr(start, cur - start)) + start = cur + 1 + cur += 1 + if cur > start: + argv.append(line.substr(start, cur)) + return argv + + +## Joins recognized subcommands in the argument vector into a single +## space-separated command sequence at index zero. +func _join_subcommands(p_argv: PackedStringArray) -> PackedStringArray: + for num_parts in range(MAX_SUBCOMMANDS, 1, -1): + if p_argv.size() >= num_parts: + var cmd: String = ' '.join(p_argv.slice(0, num_parts)) + if has_command(cmd) or has_alias(cmd): + var argv: PackedStringArray = [cmd] + return argv + p_argv.slice(num_parts) + return p_argv + + +## Substitutes an array of strings with its real command in argv. +## Will recursively expand aliases until no aliases are left. +func _expand_alias(p_argv: PackedStringArray) -> PackedStringArray: + var argv: PackedStringArray = p_argv.duplicate() + var result := PackedStringArray() + const max_depth: int = 1000 + var current_depth: int = 0 + while not argv.is_empty() and current_depth != max_depth: + argv = _join_subcommands(argv) + var current: String = argv[0] + argv.remove_at(0) + var alias_argv: PackedStringArray = _aliases.get(current, []) + current_depth += 1 + if not alias_argv.is_empty(): + argv = alias_argv + argv + else: + result.append(current) + if current_depth >= max_depth: + push_error("LimboConsole: Max depth for alias reached. Is there a loop in your aliasing?") + return p_argv + return result + + +## Converts arguments from String to types expected by the callable, and returns true if successful. +## The converted values are placed into a separate r_args array. +func _parse_argv(p_argv: PackedStringArray, p_callable: Callable, r_args: Array) -> bool: + var passed := true + + var method_info: Dictionary = Util.get_method_info(p_callable) + if method_info.is_empty(): + error("Couldn't find method info for: " + p_callable.get_method()) + return false + var num_bound_args: int = p_callable.get_bound_arguments_count() + var num_args: int = p_argv.size() + num_bound_args - 1 + var max_args: int = method_info.args.size() + var num_with_defaults: int = method_info.default_args.size() + var required_args: int = max_args - num_with_defaults + + # Join all arguments into a single string if the callable accepts a single string argument. + if max_args - num_bound_args == 1 and method_info.args[0].type == TYPE_STRING: + var a: String = " ".join(p_argv.slice(1)) + if a.left(1) == '"' and a.right(1) == '"': + a = a.trim_prefix('"').trim_suffix('"') + r_args.append(a) + return true + if num_args < required_args: + error("Missing arguments.") + return false + if num_args > max_args: + error("Too many arguments.") + return false + + r_args.resize(p_argv.size() - 1) + for i in range(1, p_argv.size()): + var a: String = p_argv[i] + var incorrect_type := false + var expected_type: int = method_info.args[i - 1].type + + if expected_type == TYPE_STRING: + if a.left(1) == '"' and a.right(1) == '"': + a = a.trim_prefix('"').trim_suffix('"') + r_args[i - 1] = a + elif a.begins_with('(') and a.ends_with(')'): + var vec = _parse_vector_arg(a) + if vec != null: + r_args[i - 1] = vec + else: + r_args[i - 1] = a + passed = false + elif a.is_valid_float(): + r_args[i - 1] = a.to_float() + elif a.is_valid_int(): + r_args[i - 1] = a.to_int() + elif a == "true" or a == "1" or a == "yes": + r_args[i - 1] = true + elif a == "false" or a == "0" or a == "no": + r_args[i - 1] = false + else: + r_args[i - 1] = a.trim_prefix('"').trim_suffix('"') + + var parsed_type: int = typeof(r_args[i - 1]) + + if not _are_compatible_types(expected_type, parsed_type): + error("Argument %d expects %s, but %s provided." % [i, type_string(expected_type), type_string(parsed_type)]) + passed = false + + return passed + + +## Returns true if the parsed type is compatible with the expected type. +func _are_compatible_types(p_expected_type: int, p_parsed_type: int) -> bool: + return p_expected_type == p_parsed_type or \ + p_expected_type == TYPE_NIL or \ + p_expected_type == TYPE_STRING or \ + (p_expected_type in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT] and p_parsed_type in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]) or \ + (p_expected_type in [TYPE_VECTOR2, TYPE_VECTOR2I] and p_parsed_type in [TYPE_VECTOR2, TYPE_VECTOR2I]) or \ + (p_expected_type in [TYPE_VECTOR3, TYPE_VECTOR3I] and p_parsed_type in [TYPE_VECTOR3, TYPE_VECTOR3I]) or \ + (p_expected_type in [TYPE_VECTOR4, TYPE_VECTOR4I] and p_parsed_type in [TYPE_VECTOR4, TYPE_VECTOR4I]) + + +func _parse_vector_arg(p_text): + assert(p_text.begins_with('(') and p_text.ends_with(')'), "Vector string presentation must begin and end with round brackets") + var comp: Array + var token: String + for i in range(1, p_text.length()): + var c: String = p_text[i] + if c.is_valid_int() or c == '.' or c == '-': + token += c + elif c == ',' or c == ' ' or c == ')': + if token.is_empty() and c == ',' and p_text[i - 1] in [',', '(']: + # Support shorthand notation: (,,1) => (0,0,1) + token = '0' + if token.is_valid_float(): + comp.append(token.to_float()) + token = "" + elif not token.is_empty(): + error("Failed to parse vector argument: Not a number: \"" + token + "\"") + info(format_tip("Tip: Supported formats are (1, 2, 3) and (1 2 3) with 2, 3 and 4 elements.")) + return null + else: + error("Failed to parse vector argument: Bad formatting: \"" + p_text + "\"") + info(format_tip("Tip: Supported formats are (1, 2, 3) and (1 2 3) with 2, 3 and 4 elements.")) + return null + if comp.size() == 2: + return Vector2(comp[0], comp[1]) + elif comp.size() == 3: + return Vector3(comp[0], comp[1], comp[2]) + elif comp.size() == 4: + return Vector4(comp[0], comp[1], comp[2], comp[3]) + else: + error("LimboConsole supports 2,3,4-element vectors, but %d-element vector given." % [comp.size()]) + return null + + +# *** AUTOCOMPLETE + +## Auto-completes a command or auto-correction on TAB. +func _autocomplete() -> void: + if not _autocomplete_matches.is_empty(): + var match_str: String = _autocomplete_matches[0] + _fill_entry(match_str) + _autocomplete_matches.remove_at(0) + _autocomplete_matches.push_back(match_str) + _update_autocomplete() + + +## Goes in the opposite direction for the autocomplete suggestion +func _reverse_autocomplete(): + if not _autocomplete_matches.is_empty(): + var match_str = _autocomplete_matches[_autocomplete_matches.size() - 1] + _autocomplete_matches.remove_at(_autocomplete_matches.size() - 1) + _autocomplete_matches.insert(0, match_str) + match_str = _autocomplete_matches[_autocomplete_matches.size() - 1] + _fill_entry(match_str) + _update_autocomplete() + + +## Updates autocomplete suggestions and hint based on user input. +func _update_autocomplete() -> void: + var argv: PackedStringArray = _expand_alias(_parse_command_line(_entry.text)) + if _entry.text.right(1) == ' ' or argv.size() == 0: + argv.append("") + var command_name: String = argv[0] + var last_arg: int = argv.size() - 1 + if _autocomplete_matches.is_empty() and not _entry.text.is_empty(): + if last_arg == 0 and not argv[0].is_empty() \ + and len(argv[0].split(" ")) <= 1: + _add_first_input_autocompletes(command_name) + elif last_arg != 0: + _add_argument_autocompletes(argv) + _add_subcommand_autocompletes(_entry.text) + _add_history_autocompletes() + + if _autocomplete_matches.size() > 0 \ + and _autocomplete_matches[0].length() > _entry.text.length() \ + and _autocomplete_matches[0].begins_with(_entry.text): + _entry.autocomplete_hint = _autocomplete_matches[0].substr(_entry.text.length()) + else: + _entry.autocomplete_hint = "" + + +## Adds auto completes for the first index of a registered +## commands when the command is split on " " +func _add_first_input_autocompletes(command_name: String) -> void: + for cmd_name in get_command_names(true): + var first_input: String = cmd_name.split(" ")[0] + if first_input.begins_with(command_name) and \ + first_input not in _autocomplete_matches: + _autocomplete_matches.append(first_input) + _autocomplete_matches.sort() + + +## Adds auto-completes based on user added arguments for a command. [br] +## p_argv is expected to contain full command as the first element (including subcommands). +func _add_argument_autocompletes(p_argv: PackedStringArray) -> void: + if p_argv.is_empty(): + return + var command: String = p_argv[0] + var last_arg: int = p_argv.size() - 1 + var key := [command, last_arg - 1] # Argument indices are 0-based. + if _argument_autocomplete_sources.has(key): + var argument_values = _argument_autocomplete_sources[key].call() + if not _validate_autocomplete_result(argument_values, command): + argument_values = [] + var matches: PackedStringArray = [] + for value in argument_values: + if str(value).begins_with(p_argv[last_arg]): + matches.append(_entry.text.substr(0, _entry.text.length() - p_argv[last_arg].length()) + str(value)) + matches.sort() + _autocomplete_matches.append_array(matches) + + +## Adds auto-completes based on the history +func _add_history_autocompletes() -> void: + if _options.autocomplete_use_history_with_matches or \ + len(_autocomplete_matches) == 0: + for i in range(_history.size() - 1, -1, -1): + if _history.get_entry(i).begins_with(_entry.text): + _autocomplete_matches.append(_history.get_entry(i)) + + +## Adds subcommand auto-complete suggestions based on registered commands +## and the current user input +func _add_subcommand_autocompletes(typed_val: String) -> void: + var command_names: PackedStringArray = get_command_names(true) + var typed_val_tokens: PackedStringArray = typed_val.split(" ") + var result: Dictionary = {} # Hashset. "autocomplete" => N/A + for cmd in command_names: + var cmd_split = cmd.split(" ") + if len(cmd_split) < len(typed_val_tokens): + continue + + var last_match: int = 0 + for i in len(typed_val_tokens): + if cmd_split[i] != typed_val_tokens[i]: + break + last_match += 1 + + if last_match < len(typed_val_tokens) - 1: + continue + + if len(cmd_split) >= len(typed_val_tokens) \ + and cmd_split[last_match].begins_with(typed_val_tokens[-1]): + var partial_cmd_arr: PackedStringArray = cmd_split.slice(0, last_match + 1) + result.get_or_add(" ".join(partial_cmd_arr)) + + var matches = result.keys() + matches.sort() + _autocomplete_matches.append_array(matches) + + +func _clear_autocomplete() -> void: + _autocomplete_matches.clear() + _entry.autocomplete_hint = "" + + +## Suggests corrections to user input based on similar command names. +func _suggest_similar_command(p_argv: PackedStringArray) -> void: + if _silent: + return + var fuzzy_hit: String = Util.fuzzy_match_string(p_argv[0], 2, get_command_names(true)) + if fuzzy_hit: + info(format_tip("Did you mean %s? ([b]TAB[/b] to fill)" % [format_name(fuzzy_hit)])) + var argv := p_argv.duplicate() + argv[0] = fuzzy_hit + var suggest_command: String = " ".join(argv) + suggest_command = suggest_command.strip_edges() + _autocomplete_matches.append(suggest_command) + + +## Suggests corrections to user input based on similar autocomplete argument values. +func _suggest_argument_corrections(p_argv: PackedStringArray) -> void: + if _silent: + return + var argv: PackedStringArray + var command_name: String = p_argv[0] + command_name = get_alias_argv(command_name)[0] + var corrected := false + + argv.resize(p_argv.size()) + argv[0] = command_name + for i in range(1, p_argv.size()): + var accepted_values = [] + var key := [command_name, i] + var source: Callable = _argument_autocomplete_sources.get(key, Callable()) + if source.is_valid(): + accepted_values = source.call() + if accepted_values == null or not _validate_autocomplete_result(accepted_values, command_name): + continue + var fuzzy_hit: String = Util.fuzzy_match_string(p_argv[i], 2, accepted_values) + if not fuzzy_hit.is_empty(): + argv[i] = fuzzy_hit + corrected = true + else: + argv[i] = p_argv[i] + if corrected: + info(format_tip("Did you mean \"%s %s\"? ([b]TAB[/b] to fill)" % [format_name(command_name), " ".join(argv.slice(1))])) + var suggest_command: String = " ".join(argv) + suggest_command = suggest_command.strip_edges() + _autocomplete_matches.append(suggest_command) + + +# *** MISC + + +func _show_console() -> void: + if not _control.visible and enabled: + _control.show() + _control_block.show() + if _options.pause_when_open: + _was_already_paused = get_tree().paused + if not _was_already_paused: + get_tree().paused = true + _previous_gui_focus = get_viewport().gui_get_focus_owner() + _entry.grab_focus() + toggled.emit(true) + + +func _hide_console() -> void: + if _control.visible: + _control.hide() + _control_block.hide() + + if _options.pause_when_open: + if not _was_already_paused: + get_tree().paused = false + if is_instance_valid(_previous_gui_focus): + _previous_gui_focus.grab_focus() + toggled.emit(false) + + +## Returns true if the callable can be registered as a command. +func _validate_callable(p_callable: Callable) -> bool: + var method_info: Dictionary = Util.get_method_info(p_callable) + if p_callable.is_standard() and method_info.is_empty(): + push_error("LimboConsole: Couldn't find method info for: " + p_callable.get_method()) + return false + if p_callable.is_custom() and not method_info.is_empty() \ + and method_info.get("name") == "" \ + and p_callable.get_bound_arguments_count() > 0: + push_error("LimboConsole: bound anonymous functions are unsupported") + return false + + var ret := true + for arg in method_info.args: + if not arg.type in [TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I]: + push_error("LimboConsole: Unsupported argument type: %s is %s" % [arg.name, type_string(arg.type)]) + ret = false + return ret + + +func _validate_autocomplete_result(p_result: Variant, p_command: String) -> bool: + if typeof(p_result) < TYPE_ARRAY: + push_error("LimboConsole: Argument autocomplete source failed: Expecting array but got: ", + type_string(typeof(p_result)), " command: ", p_command) + return false + return true + + +func _fill_entry(p_line: String) -> void: + _entry.text = p_line + _entry.set_caret_column(p_line.length()) + + +func _on_entry_text_submitted(p_command: String) -> void: + if _history_gui.visible: + _history_gui.visible = false + _clear_autocomplete() + _fill_entry(_history_gui.get_current_text()) + _update_autocomplete() + else: + _clear_autocomplete() + _fill_entry("") + execute_command(p_command) + _update_autocomplete() + + +func _on_entry_text_changed() -> void: + _clear_autocomplete() + if not _entry.text.is_empty(): + _update_autocomplete() + else: + _history_iter.reset() diff --git a/addons/limbo_console/limbo_console.gd.uid b/addons/limbo_console/limbo_console.gd.uid new file mode 100644 index 0000000..63a4ea4 --- /dev/null +++ b/addons/limbo_console/limbo_console.gd.uid @@ -0,0 +1 @@ +uid://dyxornv8vwibg diff --git a/addons/limbo_console/plugin.cfg b/addons/limbo_console/plugin.cfg new file mode 100644 index 0000000..7d7ad8d --- /dev/null +++ b/addons/limbo_console/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="LimboConsole" +description="Yet another in-game console with a simple command interpreter." +author="Serhii Snitsaruk" +version="0.4.1" +script="plugin.gd" diff --git a/addons/limbo_console/plugin.gd b/addons/limbo_console/plugin.gd new file mode 100644 index 0000000..a677aad --- /dev/null +++ b/addons/limbo_console/plugin.gd @@ -0,0 +1,57 @@ +@tool +extends EditorPlugin + +const ConsoleOptions := preload("res://addons/limbo_console/console_options.gd") +const ConfigMapper := preload("res://addons/limbo_console/config_mapper.gd") + +func _enter_tree() -> void: + add_autoload_singleton("LimboConsole", "res://addons/limbo_console/limbo_console.gd") + + # Sync config file (create if not exists) + var console_options := ConsoleOptions.new() + var do_project_setting_save: bool = false + ConfigMapper.load_from_config(console_options) + ConfigMapper.save_to_config(console_options) + + if not ProjectSettings.has_setting("input/limbo_console_toggle"): + print("LimboConsole: Adding \"limbo_console_toggle\" input action to project settings...") + + var key_event := InputEventKey.new() + key_event.keycode = KEY_QUOTELEFT + + ProjectSettings.set_setting("input/limbo_console_toggle", { + "deadzone": 0.5, + "events": [key_event], + }) + do_project_setting_save = true + + if not ProjectSettings.has_setting("input/limbo_auto_complete_reverse"): + print("LimboConsole: Adding \"limbo_auto_complete_reverse\" input action to project settings...") + var key_event = InputEventKey.new() + key_event.keycode = KEY_TAB + key_event.shift_pressed = true + + ProjectSettings.set_setting("input/limbo_auto_complete_reverse", { + "deadzone": 0.5, + "events": [key_event], + }) + do_project_setting_save = true + + if not ProjectSettings.has_setting("input/limbo_console_search_history"): + print("LimboConsole: Adding \"limbo_console_search_history\" input action to project settings...") + var key_event = InputEventKey.new() + key_event.keycode = KEY_R + key_event.ctrl_pressed = true + + ProjectSettings.set_setting("input/limbo_console_search_history", { + "deadzone": 0.5, + "events": [key_event], + }) + do_project_setting_save = true + + if do_project_setting_save: + ProjectSettings.save() + + +func _exit_tree() -> void: + remove_autoload_singleton("LimboConsole") diff --git a/addons/limbo_console/plugin.gd.uid b/addons/limbo_console/plugin.gd.uid new file mode 100644 index 0000000..3ae60d3 --- /dev/null +++ b/addons/limbo_console/plugin.gd.uid @@ -0,0 +1 @@ +uid://b4t0bfyjdn8i0 diff --git a/addons/limbo_console/res/default_theme.tres b/addons/limbo_console/res/default_theme.tres new file mode 100644 index 0000000..6b43cf8 --- /dev/null +++ b/addons/limbo_console/res/default_theme.tres @@ -0,0 +1,176 @@ +[gd_resource type="Theme" load_steps=23 format=3 uid="uid://dq4nntds66bix"] + +[ext_resource type="FontFile" uid="uid://dbbw833hg2v7o" path="res://addons/limbo_console/res/fonts/monaspace_argon_bold.otf" id="1_cry2i"] +[ext_resource type="FontFile" uid="uid://dmeyp84repfbw" path="res://addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf" id="2_h3f73"] +[ext_resource type="FontFile" uid="uid://dhm45nttm5i3s" path="res://addons/limbo_console/res/fonts/monaspace_argon_italic.otf" id="3_2qq11"] +[ext_resource type="FontFile" uid="uid://ds7dvyquauqub" path="res://addons/limbo_console/res/fonts/monaspace_argon_medium.otf" id="4_06w3f"] +[ext_resource type="FontFile" uid="uid://d4js20k8kslqt" path="res://addons/limbo_console/res/fonts/monaspace_argon_regular.otf" id="5_p4ppy"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yia2g"] +content_margin_left = 6.0 +content_margin_top = 4.0 +content_margin_right = 6.0 +content_margin_bottom = 4.0 +bg_color = Color(0.147, 0.168, 0.203, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +corner_detail = 3 +anti_aliasing = false + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_bt363"] +content_margin_left = 2.0 +content_margin_top = 4.0 +content_margin_right = 2.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_txn37"] +content_margin_left = 2.0 +content_margin_top = 4.0 +content_margin_right = 2.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ypc4n"] +content_margin_left = 2.0 +content_margin_top = 4.0 +content_margin_right = 2.0 +content_margin_bottom = 4.0 +bg_color = Color(0, 0, 0, 0) +border_width_top = 1 +border_color = Color(0.211765, 0.239216, 0.290196, 1) + +[sub_resource type="Image" id="Image_jqg6r"] +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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 255, 255, 255, 41, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 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, 41, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 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, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 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, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 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, 40, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 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, 3, 255, 255, 255, 40, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_utg8u"] +image = SubResource("Image_jqg6r") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_xismo"] +content_margin_left = 7.0 +content_margin_top = 7.0 +content_margin_right = 7.0 +content_margin_bottom = 7.0 +texture = SubResource("ImageTexture_utg8u") +texture_margin_left = 6.0 +texture_margin_top = 6.0 +texture_margin_right = 6.0 +texture_margin_bottom = 6.0 + +[sub_resource type="Image" id="Image_rt1dl"] +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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 248, 248, 248, 102, 249, 249, 249, 168, 249, 249, 249, 168, 248, 248, 248, 101, 213, 213, 213, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 102, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 101, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 250, 250, 250, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 248, 248, 248, 101, 249, 249, 249, 168, 248, 248, 248, 168, 250, 250, 250, 99, 213, 213, 213, 6, 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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_foc76"] +image = SubResource("Image_rt1dl") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_oajxf"] +content_margin_left = 6.0 +content_margin_top = 6.0 +content_margin_right = 6.0 +content_margin_bottom = 6.0 +texture = SubResource("ImageTexture_foc76") +texture_margin_left = 5.0 +texture_margin_top = 5.0 +texture_margin_right = 5.0 +texture_margin_bottom = 5.0 + +[sub_resource type="Image" id="Image_2c5qw"] +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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 180, 180, 180, 102, 181, 181, 181, 168, 181, 181, 181, 168, 179, 179, 179, 101, 170, 170, 170, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 180, 180, 180, 102, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 179, 179, 179, 101, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 170, 170, 170, 6, 179, 179, 179, 101, 181, 181, 181, 168, 179, 179, 179, 168, 181, 181, 181, 99, 170, 170, 170, 6, 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, 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, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_alomt"] +image = SubResource("Image_2c5qw") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_ev3il"] +content_margin_left = 7.0 +content_margin_top = 7.0 +content_margin_right = 7.0 +content_margin_bottom = 7.0 +texture = SubResource("ImageTexture_alomt") +texture_margin_left = 6.0 +texture_margin_top = 6.0 +texture_margin_right = 6.0 +texture_margin_bottom = 6.0 + +[sub_resource type="Image" id="Image_c2fg1"] +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, 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, 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, 7, 255, 255, 255, 19, 255, 255, 255, 19, 255, 255, 255, 7, 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, 19, 255, 255, 255, 21, 255, 255, 255, 21, 255, 255, 255, 19, 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, 19, 255, 255, 255, 21, 255, 255, 255, 21, 255, 255, 255, 19, 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, 7, 255, 255, 255, 19, 255, 255, 255, 19, 255, 255, 255, 6, 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, 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, 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": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_lun2q"] +image = SubResource("Image_c2fg1") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_lk88d"] +content_margin_left = 6.0 +content_margin_top = 0.0 +content_margin_right = 6.0 +content_margin_bottom = 0.0 +texture = SubResource("ImageTexture_lun2q") +texture_margin_left = 5.0 +texture_margin_top = 5.0 +texture_margin_right = 5.0 +texture_margin_bottom = 5.0 + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_4ui4a"] +content_margin_left = 6.0 +content_margin_top = 6.0 +content_margin_right = 6.0 +content_margin_bottom = 6.0 +texture = SubResource("ImageTexture_lun2q") +texture_margin_left = 5.0 +texture_margin_top = 5.0 +texture_margin_right = 5.0 +texture_margin_bottom = 5.0 + +[resource] +default_font = ExtResource("5_p4ppy") +default_font_size = 18 +ConsoleColors/colors/entry_command_found_color = Color(0.729412, 0.901961, 0.494118, 1) +ConsoleColors/colors/entry_command_not_found_color = Color(1, 0.2, 0.2, 1) +ConsoleColors/colors/entry_hint_color = Color(0.439216, 0.478431, 0.54902, 1) +ConsoleColors/colors/entry_subcommand_color = Color(0.584314, 0.901961, 0.796078, 1) +ConsoleColors/colors/entry_text_color = Color(0.796078, 0.8, 0.776471, 1) +ConsoleColors/colors/history_highlight_color = Color(0.317647, 0.364706, 0.439216, 1) +ConsoleColors/colors/output_command_color = Color(0.729412, 0.901961, 0.494118, 1) +ConsoleColors/colors/output_command_mention_color = Color(0.584314, 0.901961, 0.796078, 1) +ConsoleColors/colors/output_debug_color = Color(0.439216, 0.478431, 0.54902, 1) +ConsoleColors/colors/output_error_color = Color(1, 0.2, 0.2, 1) +ConsoleColors/colors/output_text_color = Color(0.796078, 0.8, 0.776471, 1) +ConsoleColors/colors/output_warning_color = Color(1, 0.654902, 0.34902, 1) +Panel/styles/panel = SubResource("StyleBoxFlat_yia2g") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_yia2g") +RichTextLabel/fonts/bold_font = ExtResource("1_cry2i") +RichTextLabel/fonts/bold_italics_font = ExtResource("2_h3f73") +RichTextLabel/fonts/italics_font = ExtResource("3_2qq11") +RichTextLabel/fonts/mono_font = ExtResource("4_06w3f") +RichTextLabel/fonts/normal_font = ExtResource("5_p4ppy") +RichTextLabel/styles/focus = SubResource("StyleBoxEmpty_bt363") +RichTextLabel/styles/normal = SubResource("StyleBoxEmpty_txn37") +TextEdit/styles/focus = SubResource("StyleBoxFlat_ypc4n") +TextEdit/styles/normal = SubResource("StyleBoxFlat_ypc4n") +VScrollBar/styles/grabber = SubResource("StyleBoxTexture_xismo") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxTexture_oajxf") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxTexture_ev3il") +VScrollBar/styles/scroll = SubResource("StyleBoxTexture_lk88d") +VScrollBar/styles/scroll_focus = SubResource("StyleBoxTexture_4ui4a") diff --git a/addons/limbo_console/res/fonts/LICENSE.md b/addons/limbo_console/res/fonts/LICENSE.md new file mode 100644 index 0000000..8f61f03 --- /dev/null +++ b/addons/limbo_console/res/fonts/LICENSE.md @@ -0,0 +1,93 @@ +Copyright (c) 2023, GitHub https://github.com/githubnext/monaspace +with Reserved Font Name "Monaspace", including subfamilies: "Argon", "Neon", "Xenon", "Radon", and "Krypton" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/addons/limbo_console/res/fonts/monaspace_argon_bold.otf b/addons/limbo_console/res/fonts/monaspace_argon_bold.otf new file mode 100644 index 0000000..7878b88 Binary files /dev/null and b/addons/limbo_console/res/fonts/monaspace_argon_bold.otf differ diff --git a/addons/limbo_console/res/fonts/monaspace_argon_bold.otf.import b/addons/limbo_console/res/fonts/monaspace_argon_bold.otf.import new file mode 100644 index 0000000..59eea51 --- /dev/null +++ b/addons/limbo_console/res/fonts/monaspace_argon_bold.otf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dbbw833hg2v7o" +path="res://.godot/imported/monaspace_argon_bold.otf-5d916059e7810f56d30161557f70d71b.fontdata" + +[deps] + +source_file="res://addons/limbo_console/res/fonts/monaspace_argon_bold.otf" +dest_files=["res://.godot/imported/monaspace_argon_bold.otf-5d916059e7810f56d30161557f70d71b.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf b/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf new file mode 100644 index 0000000..90423e1 Binary files /dev/null and b/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf differ diff --git a/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf.import b/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf.import new file mode 100644 index 0000000..f3d336a --- /dev/null +++ b/addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dmeyp84repfbw" +path="res://.godot/imported/monaspace_argon_bold_italic.otf-cd05eebec36875096d59dc2e6dfb87db.fontdata" + +[deps] + +source_file="res://addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf" +dest_files=["res://.godot/imported/monaspace_argon_bold_italic.otf-cd05eebec36875096d59dc2e6dfb87db.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/limbo_console/res/fonts/monaspace_argon_italic.otf b/addons/limbo_console/res/fonts/monaspace_argon_italic.otf new file mode 100644 index 0000000..8420390 Binary files /dev/null and b/addons/limbo_console/res/fonts/monaspace_argon_italic.otf differ diff --git a/addons/limbo_console/res/fonts/monaspace_argon_italic.otf.import b/addons/limbo_console/res/fonts/monaspace_argon_italic.otf.import new file mode 100644 index 0000000..032fcb9 --- /dev/null +++ b/addons/limbo_console/res/fonts/monaspace_argon_italic.otf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dhm45nttm5i3s" +path="res://.godot/imported/monaspace_argon_italic.otf-69d64783adde526699a99a191cd14ed6.fontdata" + +[deps] + +source_file="res://addons/limbo_console/res/fonts/monaspace_argon_italic.otf" +dest_files=["res://.godot/imported/monaspace_argon_italic.otf-69d64783adde526699a99a191cd14ed6.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/limbo_console/res/fonts/monaspace_argon_medium.otf b/addons/limbo_console/res/fonts/monaspace_argon_medium.otf new file mode 100644 index 0000000..deeb96d Binary files /dev/null and b/addons/limbo_console/res/fonts/monaspace_argon_medium.otf differ diff --git a/addons/limbo_console/res/fonts/monaspace_argon_medium.otf.import b/addons/limbo_console/res/fonts/monaspace_argon_medium.otf.import new file mode 100644 index 0000000..a18d031 --- /dev/null +++ b/addons/limbo_console/res/fonts/monaspace_argon_medium.otf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://ds7dvyquauqub" +path="res://.godot/imported/monaspace_argon_medium.otf-2c090420a59a2bfcd1b6b28a7c3469ad.fontdata" + +[deps] + +source_file="res://addons/limbo_console/res/fonts/monaspace_argon_medium.otf" +dest_files=["res://.godot/imported/monaspace_argon_medium.otf-2c090420a59a2bfcd1b6b28a7c3469ad.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/limbo_console/res/fonts/monaspace_argon_regular.otf b/addons/limbo_console/res/fonts/monaspace_argon_regular.otf new file mode 100644 index 0000000..b94372a Binary files /dev/null and b/addons/limbo_console/res/fonts/monaspace_argon_regular.otf differ diff --git a/addons/limbo_console/res/fonts/monaspace_argon_regular.otf.import b/addons/limbo_console/res/fonts/monaspace_argon_regular.otf.import new file mode 100644 index 0000000..1a4c684 --- /dev/null +++ b/addons/limbo_console/res/fonts/monaspace_argon_regular.otf.import @@ -0,0 +1,35 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://d4js20k8kslqt" +path="res://.godot/imported/monaspace_argon_regular.otf-7622e0becc7f9143f21c9951dd015f30.fontdata" + +[deps] + +source_file="res://addons/limbo_console/res/fonts/monaspace_argon_regular.otf" +dest_files=["res://.godot/imported/monaspace_argon_regular.otf-7622e0becc7f9143f21c9951dd015f30.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/limbo_console/util.gd b/addons/limbo_console/util.gd new file mode 100644 index 0000000..72b1506 --- /dev/null +++ b/addons/limbo_console/util.gd @@ -0,0 +1,119 @@ +extends Object +## Utility functions + + +static func bbcode_escape(p_text: String) -> String: + return p_text \ + .replace("[", "~LB~") \ + .replace("]", "~RB~") \ + .replace("~LB~", "[lb]") \ + .replace("~RB~", "[rb]") + + +static func bbcode_strip(p_text: String) -> String: + var stripped := "" + var in_brackets: bool = false + for c: String in p_text: + if c == '[': + in_brackets = true + elif c == ']': + in_brackets = false + elif not in_brackets: + stripped += c + return stripped + + +static func get_method_info(p_callable: Callable) -> Dictionary: + var method_info: Dictionary + var method_list: Array[Dictionary] + if p_callable.get_object() is GDScript: + method_list = p_callable.get_object().get_script_method_list() + else: + method_list = p_callable.get_object().get_method_list() + for m in method_list: + if m.name == p_callable.get_method(): + method_info = m + break + if !method_info and p_callable.is_custom(): + var args: Array + var default_args: Array + for i in p_callable.get_argument_count(): + var argument: Dictionary + argument["name"] = "arg%d" % i + argument["type"] = TYPE_NIL + args.push_back(argument) + method_info["name"] = "" + method_info["args"] = args + method_info["default_args"] = default_args + return method_info + + +## Finds the most similar string in an array. +static func fuzzy_match_string(p_string: String, p_max_edit_distance: int, p_array) -> String: + if typeof(p_array) < TYPE_ARRAY: + push_error("LimboConsole: Internal error: p_array is not an array") + return "" + if p_array.size() == 0: + return "" + var best_distance: int = 9223372036854775807 + var best_match: String = "" + for i in p_array.size(): + var elem := str(p_array[i]) + var dist: float = _calculate_osa_distance(p_string, elem) + if dist < best_distance: + best_distance = dist + best_match = elem + return best_match if best_distance <= p_max_edit_distance else "" + + +## Calculates optimal string alignment distance [br] +## See: https://en.wikipedia.org/wiki/Levenshtein_distance +static func _calculate_osa_distance(s1: String, s2: String) -> int: + var s1_len: int = s1.length() + var s2_len: int = s2.length() + + # Iterative approach with 3 matrix rows. + # Most of the work is done on row1 and row2 - row0 is only needed to calculate transposition cost. + var row0: PackedInt32Array # previous-previous + var row1: PackedInt32Array # previous + var row2: PackedInt32Array # current aka the one we need to calculate + row0.resize(s2_len + 1) + row1.resize(s2_len + 1) + row2.resize(s2_len + 1) + + # edit distance is the number of characters to insert to get from empty string to s2 + for i in range(s2_len + 1): + row1[i] = i + + for i in range(s1_len): + # edit distance is the number of characters to delete from s1 to match empty s2 + row2[0] = i + 1 + + for j in range(s2_len): + var deletion_cost: int = row1[j + 1] + 1 + var insertion_cost: int = row2[j] + 1 + var substitution_cost: int = row1[j] if s1[i] == s2[j] else row1[j] + 1 + + row2[j + 1] = min(deletion_cost, insertion_cost, substitution_cost) + + if i > 1 and j > 1 and s1[i - 1] == s2[j] and s1[i - 1] == s2[j]: + var transposition_cost: int = row0[j - 1] + 1 + row2[j + 1] = mini(transposition_cost, row2[j + 1]) + + # Swap rows. + var tmp: PackedInt32Array = row0 + row0 = row1 + row1 = row2 + row2 = tmp + return row1[s2_len] + + +## Returns true, if a string is constructed of one or more space-separated valid +## command identifiers ("command" or "command sub1 sub2"). +## A valid command identifier may contain only letters, digits, and underscores (_), +## and the first character may not be a digit. +static func is_valid_command_sequence(p_string: String) -> bool: + for part in p_string.split(' '): + if not part.is_valid_ascii_identifier(): + return false + return true diff --git a/addons/limbo_console/util.gd.uid b/addons/limbo_console/util.gd.uid new file mode 100644 index 0000000..00e8f18 --- /dev/null +++ b/addons/limbo_console/util.gd.uid @@ -0,0 +1 @@ +uid://cw6s1es6yjip5 diff --git a/addons/phantom_camera/icons/viewfinder/Select.svg b/addons/phantom_camera/icons/viewfinder/Select.svg new file mode 100644 index 0000000..34b109b --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Select.svg @@ -0,0 +1,3 @@ + + + diff --git a/addons/phantom_camera/icons/viewfinder/Select.svg.import b/addons/phantom_camera/icons/viewfinder/Select.svg.import new file mode 100644 index 0000000..81b41c9 --- /dev/null +++ b/addons/phantom_camera/icons/viewfinder/Select.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rghrkoqrm2ig" +path="res://.godot/imported/Select.svg-cdf90b8b400d3b91a023b70c6a823894.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/viewfinder/Select.svg" +dest_files=["res://.godot/imported/Select.svg-cdf90b8b400d3b91a023b70c6a823894.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/icons/warning.svg b/addons/phantom_camera/icons/warning.svg new file mode 100644 index 0000000..63dbedf --- /dev/null +++ b/addons/phantom_camera/icons/warning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/addons/phantom_camera/icons/warning.svg.import b/addons/phantom_camera/icons/warning.svg.import new file mode 100644 index 0000000..2895b37 --- /dev/null +++ b/addons/phantom_camera/icons/warning.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjlv0bg7byjx0" +path="res://.godot/imported/warning.svg-c1b21c265e0842bbdc9ed10735995366.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/phantom_camera/icons/warning.svg" +dest_files=["res://.godot/imported/warning.svg-c1b21c265e0842bbdc9ed10735995366.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn b/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn new file mode 100644 index 0000000..332415d --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list.tscn @@ -0,0 +1,87 @@ +[gd_scene load_steps=8 format=3 uid="uid://mbjdav5oqves"] + +[ext_resource type="Script" uid="uid://c84cxry3t35ny" path="res://addons/phantom_camera/scripts/panel/viewfinder/host_list.gd" id="1_h6ayt"] +[ext_resource type="Texture2D" uid="uid://5fatldiu7dd5" path="res://addons/phantom_camera/icons/phantom_camera_host.svg" id="1_xlgqb"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_w002y"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kq7gm"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.960784, 0.960784, 0.960784, 1) +corner_radius_top_right = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynag5"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_right = 6 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q2svd"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0jvt"] +content_margin_left = 0.0 +content_margin_top = 8.0 +content_margin_right = 0.0 +content_margin_bottom = 8.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 10 + +[node name="PCamHostList" type="VBoxContainer"] +anchors_preset = 9 +anchor_bottom = 1.0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +theme_override_constants/separation = -2 +alignment = 2 +script = ExtResource("1_h6ayt") + +[node name="HostListButton" type="Button" parent="."] +unique_name_in_owner = true +custom_minimum_size = Vector2(40, 40) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_colors/icon_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_colors/icon_hover_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +theme_override_styles/focus = SubResource("StyleBoxEmpty_w002y") +theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/hover = SubResource("StyleBoxFlat_ynag5") +theme_override_styles/pressed = SubResource("StyleBoxFlat_kq7gm") +theme_override_styles/normal = SubResource("StyleBoxFlat_q2svd") +icon = ExtResource("1_xlgqb") +expand_icon = true + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_e0jvt") +horizontal_scroll_mode = 0 + +[node name="HostListContainer" type="VBoxContainer" parent="ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 8 diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn new file mode 100644 index 0000000..9ce67e5 --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn @@ -0,0 +1,68 @@ +[gd_scene load_steps=10 format=3 uid="uid://btn6jgv0vix7"] + +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_anjxo"] +[ext_resource type="Theme" uid="uid://bhppejri5dbsf" path="res://addons/phantom_camera/themes/theme.tres" id="1_wql5t"] +[ext_resource type="Texture2D" uid="uid://rghrkoqrm2ig" path="res://addons/phantom_camera/icons/viewfinder/Select.svg" id="2_71b6g"] +[ext_resource type="ButtonGroup" uid="uid://dfu0b8jbtyr1n" path="res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres" id="3_06a0y"] +[ext_resource type="Script" uid="uid://bv24ubx8mutw7" path="res://addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd" id="3_a5o8b"] +[ext_resource type="Texture2D" uid="uid://cjlv0bg7byjx0" path="res://addons/phantom_camera/icons/warning.svg" id="3_qgpy7"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0rxfi"] +content_margin_right = 6.0 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_llqnh"] + +[sub_resource type="Theme" id="Theme_7h15c"] +Button/colors/icon_hover_color = Color(0.960784, 0.960784, 0.960784, 1) +Button/colors/icon_hover_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_normal_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/constants/icon_max_width = 20 +Button/styles/focus = SubResource("StyleBoxEmpty_llqnh") + +[node name="HostListItem" type="PanelContainer"] +offset_right = 229.0 +offset_bottom = 34.0 +theme_override_styles/panel = SubResource("StyleBoxEmpty_0rxfi") +script = ExtResource("3_a5o8b") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="SelectPCamHost" type="Button" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 4 +tooltip_text = "Select the Phantom Camera Host node from the scene hierarchy" +focus_mode = 0 +theme = SubResource("Theme_7h15c") +icon = ExtResource("2_71b6g") +flat = true + +[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ErrorPCamHost" type="TextureRect" parent="HBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(18, 18) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +tooltip_text = "This Phantom Camera Host node will not affect a Camera node. +See the warning in the Scene Tree for more information." +texture = ExtResource("3_qgpy7") +expand_mode = 1 + +[node name="SwitchPCamHost" type="Button" parent="HBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 +tooltip_text = "Change the viewfinder's camera to the camera of this Phantom Camera Host" +theme = ExtResource("1_wql5t") +theme_override_fonts/font = ExtResource("1_anjxo") +theme_override_font_sizes/font_size = 18 +toggle_mode = true +button_group = ExtResource("3_06a0y") +text = "{ PCamHostName }" diff --git a/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres new file mode 100644 index 0000000..64c4600 --- /dev/null +++ b/addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres @@ -0,0 +1,3 @@ +[gd_resource type="ButtonGroup" format=3 uid="uid://dfu0b8jbtyr1n"] + +[resource] diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd new file mode 100644 index 0000000..27e7eed --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd @@ -0,0 +1,84 @@ +@tool +extends EditorNode3DGizmo + +#var pcam_3d: PhantomCamera3D + +func _redraw() -> void: + clear() + + var icon: Material = get_plugin().get_material(get_plugin().get_name(), self) + add_unscaled_billboard(icon, 0.035) + + var pcam_3d: PhantomCamera3D = get_node_3d() + +# if pcam_3d.is_following(): +# _draw_target(pcam_3d, pcam_3d.get_follow_target_position(), "follow_target") +# if pcam_3d.is_looking_at(): +# _draw_target(pcam_3d, pcam_3d.get_look_at_target_position(), "look_at_target") + + if pcam_3d.is_active(): return + + var frustum_lines: PackedVector3Array = PackedVector3Array() + var height: float = 0.25 + var width: float = height * 1.25 + var forward: float = height * -1.5 + + # Trapezoid + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(-width, height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(width, height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(-width, -height, forward)) + + frustum_lines.push_back(Vector3.ZERO) + frustum_lines.push_back(Vector3(width, -height, forward)) + + ####### + # Frame + ####### + ## Left + frustum_lines.push_back(Vector3(-width, height, forward)) + frustum_lines.push_back(Vector3(-width, -height, forward)) + + ## Bottom + frustum_lines.push_back(Vector3(-width, -height, forward)) + frustum_lines.push_back(Vector3(width, -height, forward)) + + ## Right + frustum_lines.push_back(Vector3(width, -height, forward)) + frustum_lines.push_back(Vector3(width, height, forward)) + + ## Top + frustum_lines.push_back(Vector3(width, height, forward)) + frustum_lines.push_back(Vector3(-width, height, forward)) + + ############## + # Up Direction + ############## + var up_height: float = height + 0.15 + var up_width: float = width / 3 + + ## Left + frustum_lines.push_back(Vector3(0, up_height, forward)) + frustum_lines.push_back(Vector3(-up_width, height, forward)) + + ## Right + frustum_lines.push_back(Vector3(0, up_height, forward)) + frustum_lines.push_back(Vector3(up_width, height, forward)) + + var frustum_material: StandardMaterial3D = get_plugin().get_material("frustum", self) + add_lines(frustum_lines, frustum_material, false) + + +func _draw_target(pcam_3d: Node3D, target_position: Vector3, type: String) -> void: + var target_lines: PackedVector3Array = PackedVector3Array() + var direction: Vector3 = target_position - pcam_3d.global_position + var end_position: Vector3 = pcam_3d.global_basis.z * direction + + target_lines.push_back(Vector3.ZERO) + target_lines.push_back(end_position) + var target_material: StandardMaterial3D = get_plugin().get_material(type, self) + add_lines(target_lines, target_material, false) diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid new file mode 100644 index 0000000..564e0cf --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd.uid @@ -0,0 +1 @@ +uid://cyr6fgximfw6q diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd new file mode 100644 index 0000000..2caf713 --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd @@ -0,0 +1,37 @@ +@tool +extends EditorNode3DGizmoPlugin + +const PhantomCamera3DNode: Script = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd") +const PhantomCamera3DGizmo: Script = preload("res://addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo.gd") +const _icon_texture: Texture2D = preload("res://addons/phantom_camera/icons/phantom_camera_gizmo.svg") +var _gizmo_name: String = "PhantomCamera3D" + +var gizmo_name: String: set = set_gizmo_name +var _gizmo_icon: Texture2D +var _gizmo_spatial_script: Script = PhantomCamera3DNode + + +func set_gizmo_name(name: String) -> void: + _gizmo_name = name + + +func _get_gizmo_name() -> String: + return _gizmo_name + + +func _has_gizmo(spatial: Node3D) -> bool: + return spatial is PhantomCamera3D + + +func _init() -> void: + create_icon_material(gizmo_name, _icon_texture, false, Color.WHITE) + create_material("frustum", Color8(252, 127, 127, 255)) + create_material("follow_target", Color8(185, 58, 89)) + create_material("look_at_target", Color8(61, 207, 225)) + + +func _create_gizmo(for_node_3d: Node3D) -> EditorNode3DGizmo: + if for_node_3d is PhantomCamera3DNode: + return PhantomCamera3DGizmo.new() + else: + return null diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid new file mode 100644 index 0000000..ce528d7 --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_3d_gizmo_plugin.gd.uid @@ -0,0 +1 @@ +uid://bkevga3bx4rfj diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd new file mode 100644 index 0000000..3dd4d3e --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd @@ -0,0 +1,29 @@ +extends EditorNode3DGizmoPlugin + +var _spatial_script: Script = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_noise_emitter_3d.gd") +var _gizmo_icon: Texture2D = preload("res://addons/phantom_camera/icons/phantom_camera_noise_emitter_gizmo.svg") + +var _gizmo_name: StringName = "PhantomCameraNoiseEmitter" + +func _init() -> void: + create_material("main", Color8(252, 127, 127, 255)) + create_handle_material("handles") + create_icon_material(_gizmo_name, _gizmo_icon, false, Color.WHITE) + + +func _has_gizmo(node: Node3D): + return node.get_script() == _spatial_script + + +func _get_gizmo_name() -> String: + return _gizmo_name + + +func _redraw(gizmo: EditorNode3DGizmo): + gizmo.clear() + + var icon: Material = get_material(_gizmo_name, gizmo) + gizmo.add_unscaled_billboard(icon, 0.035) + + #var material = get_material("main", gizmo) + #gizmo.add_lines(_draw_frustum(), material) diff --git a/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid new file mode 100644 index 0000000..2b93b6c --- /dev/null +++ b/addons/phantom_camera/scripts/gizmos/phantom_camera_noise_emitter_gizmo_plugin_3d.gd.uid @@ -0,0 +1 @@ +uid://dddokcd2ug05i diff --git a/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs new file mode 100644 index 0000000..5005230 --- /dev/null +++ b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs @@ -0,0 +1,36 @@ +using System.Linq; +using Godot; + +#nullable enable + +namespace PhantomCamera.Manager; + +public static class PhantomCameraManager +{ + private static GodotObject? _instance; + + public static GodotObject Instance => _instance ??= Engine.GetSingleton("PhantomCameraManager"); + + public static PhantomCamera2D[] PhantomCamera2Ds => + Instance.Call(MethodName.GetPhantomCamera2Ds).AsGodotArray() + .Select(node => new PhantomCamera2D(node)).ToArray(); + + public static PhantomCamera3D[] PhantomCamera3Ds => + Instance.Call(MethodName.GetPhantomCamera3Ds).AsGodotArray() + .Select(node => new PhantomCamera3D(node)).ToArray(); + + public static PhantomCameraHost[] PhantomCameraHosts => + Instance.Call(MethodName.GetPhantomCameraHosts).AsGodotArray() + .Select(node => new PhantomCameraHost(node)).ToArray(); + + public static PhantomCamera2D[] GetPhantomCamera2Ds() => PhantomCamera2Ds; + public static PhantomCamera3D[] GetPhantomCamera3Ds() => PhantomCamera3Ds; + public static PhantomCameraHost[] GetPhantomCameraHosts() => PhantomCameraHosts; + + public static class MethodName + { + public const string GetPhantomCamera2Ds = "get_phantom_camera_2ds"; + public const string GetPhantomCamera3Ds = "get_phantom_camera_3ds"; + public const string GetPhantomCameraHosts = "get_phantom_camera_hosts"; + } +} diff --git a/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid new file mode 100644 index 0000000..cdf7f4e --- /dev/null +++ b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs.uid @@ -0,0 +1 @@ +uid://vtj8iqx4bp43 diff --git a/addons/phantom_camera/scripts/panel/editor.gd b/addons/phantom_camera/scripts/panel/editor.gd new file mode 100644 index 0000000..a10018c --- /dev/null +++ b/addons/phantom_camera/scripts/panel/editor.gd @@ -0,0 +1,23 @@ +@tool +extends VBoxContainer + +#region Onready + +@onready var updater: Control = %UpdateButton +@onready var viewfinder: Control = %ViewfinderPanel + +#endregion + +#region Public Variables + +var editor_plugin: EditorPlugin + +#endregion + + +#region Private Functions + +func _ready(): + updater.editor_plugin = editor_plugin + +#endregion diff --git a/addons/phantom_camera/scripts/panel/editor.gd.uid b/addons/phantom_camera/scripts/panel/editor.gd.uid new file mode 100644 index 0000000..6be368c --- /dev/null +++ b/addons/phantom_camera/scripts/panel/editor.gd.uid @@ -0,0 +1 @@ +uid://cgfwg3paxkj2x diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd new file mode 100644 index 0000000..662e598 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd @@ -0,0 +1,112 @@ +@tool +extends VBoxContainer + +#region Constants + +const _constants := preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") +const _host_list_item: PackedScene = preload("res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item.tscn") + +#endregion + +signal pcam_host_removed(pcam_host: PhantomCameraHost) + +@onready var _host_list_button: Button = %HostListButton +@onready var _host_list_scroll_container: ScrollContainer = %ScrollContainer +@onready var _host_list_item_container: VBoxContainer = %HostListContainer + +var _host_list_open: bool = false + +var _bottom_offset_value: float + +var _pcam_host_list: Array[PhantomCameraHost] +var _pcam_manager: Node + +var _viewfinder_panel: Control + +#region Private Functions + +func _ready() -> void: + _host_list_button.pressed.connect(_host_list_button_pressed) + if Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): + _pcam_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + _pcam_manager.pcam_host_removed_from_scene.connect(_remove_pcam_host) + + if not get_parent() is Control: return # To prevent errors when opening the scene on its own + _viewfinder_panel = get_parent() + _viewfinder_panel.resized.connect(_set_offset_top) + + _host_list_item_container.resized.connect(_set_offset_top) + + +func _set_offset_top() -> void: + offset_top = _set_host_list_size() + + +func _host_list_button_pressed() -> void: + _host_list_open = !_host_list_open + + var tween: Tween = create_tween() + var max_duration: float = 0.6 + + # 300 being the minimum size of the viewfinder's height + var duration: float = clampf( + max_duration / (300 / _host_list_item_container.size.y), + 0.3, + max_duration) + + tween.tween_property(self, "offset_top", _set_host_list_size(), duration)\ + .set_ease(Tween.EASE_OUT)\ + .set_trans(Tween.TRANS_QUINT) + + +func _set_host_list_size() -> float: + if not _host_list_open: + return clampf( + _viewfinder_panel.size.y - \ + _host_list_item_container.size.y - \ + _host_list_button.size.y - 20, + 0, + INF + ) + else: + return (_viewfinder_panel.size.y - _host_list_button.size.y / 2) + + +func _remove_pcam_host(pcam_host: PhantomCameraHost) -> void: + if _pcam_host_list.has(pcam_host): + _pcam_host_list.erase(pcam_host) + + var freed_pcam_host: Control + for host_list_item_instance in _host_list_item_container.get_children(): + if not host_list_item_instance.pcam_host == pcam_host: continue + freed_pcam_host = host_list_item_instance + host_list_item_instance.queue_free() + +#endregion + +#region Public Functions + +func add_pcam_host(pcam_host: PhantomCameraHost, is_default: bool) -> void: + if _pcam_host_list.has(pcam_host): return + + _pcam_host_list.append(pcam_host) + + var host_list_item_instance: PanelContainer = _host_list_item.instantiate() + var switch_pcam_host_button: Button = host_list_item_instance.get_node("%SwitchPCamHost") + if is_default: switch_pcam_host_button.button_pressed = true + + if not pcam_host.tree_exiting.is_connected(_remove_pcam_host): + pcam_host.tree_exiting.connect(_remove_pcam_host.bind(pcam_host)) + + host_list_item_instance.pcam_host = pcam_host + + _host_list_item_container.add_child(host_list_item_instance) + + +func clear_pcam_host_list() -> void: + _pcam_host_list.clear() + + for host_list_item_instance in _host_list_item_container.get_children(): + host_list_item_instance.queue_free() + +#endregion diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid new file mode 100644 index 0000000..6923d3e --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list.gd.uid @@ -0,0 +1 @@ +uid://c84cxry3t35ny diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd new file mode 100644 index 0000000..5707974 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd @@ -0,0 +1,58 @@ +@tool +extends Control + +const button_group_resource: ButtonGroup = preload("res://addons/phantom_camera/panel/viewfinder/host_list/host_list_item_group.tres") +const _constants = preload("res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_constants.gd") + +@onready var select_pcam_host: Button = %SelectPCamHost +@onready var switch_pcam_host: Button = %SwitchPCamHost + +var pcam_host: PhantomCameraHost: + set(value): + pcam_host = value + if not is_instance_valid(value): return + if not pcam_host.renamed.is_connected(_rename_pcam_host): + pcam_host.renamed.connect(_rename_pcam_host) + pcam_host.has_error.connect(_pcam_host_has_error) + get: + return pcam_host + +var _pcam_manager: Node + +#region Private fucntions + +func _ready() -> void: + switch_pcam_host.button_group = button_group_resource + select_pcam_host.pressed.connect(_select_pcam) + switch_pcam_host.pressed.connect(_switch_pcam_host) + + if not is_instance_valid(pcam_host): return + switch_pcam_host.text = pcam_host.name + + _pcam_host_has_error() + + +func _pcam_host_has_error() -> void: + if pcam_host.show_warning: + %ErrorPCamHost.visible = true + else: + %ErrorPCamHost.visible = false + + +func _rename_pcam_host() -> void: + switch_pcam_host.text = pcam_host.name + + +func _select_pcam() -> void: + EditorInterface.get_selection().clear() + EditorInterface.get_selection().add_node(pcam_host) + + +func _switch_pcam_host() -> void: + if not Engine.has_singleton(_constants.PCAM_MANAGER_NODE_NAME): return + if not is_instance_valid(_pcam_manager): + _pcam_manager = Engine.get_singleton(_constants.PCAM_MANAGER_NODE_NAME) + + _pcam_manager.viewfinder_pcam_host_switch.emit(pcam_host) + +#endregion diff --git a/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid new file mode 100644 index 0000000..9df2919 --- /dev/null +++ b/addons/phantom_camera/scripts/panel/viewfinder/host_list_item.gd.uid @@ -0,0 +1 @@ +uid://bv24ubx8mutw7 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs new file mode 100644 index 0000000..267adf0 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs @@ -0,0 +1,253 @@ +using Godot; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum InactiveUpdateMode +{ + Always, + Never +} + +public abstract class PhantomCamera +{ + protected readonly GodotObject Node; + + public delegate void BecameActiveEventHandler(); + public delegate void BecameInactiveEventHandler(); + public delegate void FollowTargetChangedEventHandler(); + public delegate void DeadZoneChangedEventHandler(); + public delegate void TweenStartedEventHandler(); + public delegate void IsTweeningEventHandler(); + public delegate void TweenCompletedEventHandler(); + + public event BecameActiveEventHandler? BecameActive; + public event BecameInactiveEventHandler? BecameInactive; + public event FollowTargetChangedEventHandler? FollowTargetChanged; + public event DeadZoneChangedEventHandler? DeadZoneChanged; + public event TweenStartedEventHandler? TweenStarted; + public event IsTweeningEventHandler? IsTweening; + public event TweenCompletedEventHandler? TweenCompleted; + + private readonly Callable _callableBecameActive; + private readonly Callable _callableBecameInactive; + private readonly Callable _callableFollowTargetChanged; + private readonly Callable _callableDeadZoneChanged; + private readonly Callable _callableTweenStarted; + private readonly Callable _callableIsTweening; + private readonly Callable _callableTweenCompleted; + + public int Priority + { + get => (int)Node.Call(MethodName.GetPriority); + set => Node.Call(MethodName.SetPriority, value); + } + + public bool IsActive => (bool)Node.Call(MethodName.IsActive); + + public bool FollowDamping + { + get => (bool)Node.Call(MethodName.GetFollowDamping); + set => Node.Call(MethodName.SetFollowDamping, value); + } + + public bool IsFollowing => (bool)Node.Call(PhantomCamera.MethodName.IsFollowing); + + public float DeadZoneWidth + { + get => (float)Node.Get(PropertyName.DeadZoneWidth); + set => Node.Set(PropertyName.DeadZoneWidth, value); + } + + public float DeadZoneHeight + { + get => (float)Node.Get(PropertyName.DeadZoneHeight); + set => Node.Set(PropertyName.DeadZoneHeight, value); + } + + public PhantomCameraTween TweenResource + { + get => new((Resource)Node.Call(MethodName.GetTweenResource)); + set => Node.Call(MethodName.SetTweenResource, (GodotObject)value.Resource); + } + + public bool TweenSkip + { + get => (bool)Node.Call(MethodName.GetTweenSkip); + set => Node.Call(MethodName.SetTweenSkip, value); + } + + public float TweenDuration + { + get => (float)Node.Call(MethodName.GetTweenDuration); + set => Node.Call(MethodName.SetTweenDuration, value); + } + + public TransitionType TweenTransition + { + get => (TransitionType)(int)Node.Call(MethodName.GetTweenTransition); + set => Node.Call(MethodName.SetTweenTransition, (int)value); + } + + public EaseType TweenEase + { + get => (EaseType)(int)Node.Call(MethodName.GetTweenEase); + set => Node.Call(MethodName.SetTweenEase, (int)value); + } + + public bool TweenOnLoad + { + get => (bool)Node.Call(MethodName.GetTweenOnLoad); + set => Node.Call(MethodName.SetTweenOnLoad, value); + } + + public InactiveUpdateMode InactiveUpdateMode + { + get => (InactiveUpdateMode)(int)Node.Call(MethodName.GetInactiveUpdateMode); + set => Node.Call(MethodName.SetInactiveUpdateMode, (int)value); + } + + public int HostLayers + { + get => (int)Node.Call(MethodName.GetHostLayers); + set => Node.Call(MethodName.SetHostLayers, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node.Call(MethodName.GetNoiseEmitterLayer); + set => Node.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void TeleportPosition() + { + Node.Call(MethodName.TeleportPosition); + } + + public void SetHostLayersValue(int layer, bool enabled) + { + Node.Call(MethodName.SetHostLayersValue, layer, enabled); + } + + protected PhantomCamera(GodotObject phantomCameraNode) + { + Node = phantomCameraNode; + + _callableBecameActive = Callable.From(() => BecameActive?.Invoke()); + _callableBecameInactive = Callable.From(() => BecameInactive?.Invoke()); + _callableFollowTargetChanged = Callable.From(() => FollowTargetChanged?.Invoke()); + _callableDeadZoneChanged = Callable.From(() => DeadZoneChanged?.Invoke()); + _callableTweenStarted = Callable.From(() => TweenStarted?.Invoke()); + _callableIsTweening = Callable.From(() => IsTweening?.Invoke()); + _callableTweenCompleted = Callable.From(() => TweenCompleted?.Invoke()); + + Node.Connect(SignalName.BecameActive, _callableBecameActive); + Node.Connect(SignalName.BecameInactive, _callableBecameInactive); + Node.Connect(SignalName.FollowTargetChanged, _callableFollowTargetChanged); + Node.Connect(SignalName.DeadZoneChanged, _callableDeadZoneChanged); + Node.Connect(SignalName.TweenStarted, _callableTweenStarted); + Node.Connect(SignalName.IsTweening, _callableIsTweening); + Node.Connect(SignalName.TweenCompleted, _callableTweenCompleted); + } + + ~PhantomCamera() + { + Node.Disconnect(SignalName.BecameActive, _callableBecameActive); + Node.Disconnect(SignalName.BecameInactive, _callableBecameInactive); + Node.Disconnect(SignalName.FollowTargetChanged, _callableFollowTargetChanged); + Node.Disconnect(SignalName.DeadZoneChanged, _callableDeadZoneChanged); + Node.Disconnect(SignalName.TweenStarted, _callableTweenStarted); + Node.Disconnect(SignalName.IsTweening, _callableIsTweening); + Node.Disconnect(SignalName.TweenCompleted, _callableTweenCompleted); + } + + public static class MethodName + { + public const string GetFollowMode = "get_follow_mode"; + public const string IsActive = "is_active"; + + public const string GetPriority = "get_priority"; + public const string SetPriority = "set_priority"; + + public const string IsFollowing = "is_following"; + + public const string GetFollowTarget = "get_follow_target"; + public const string SetFollowTarget = "set_follow_target"; + + public const string GetFollowTargets = "get_follow_targets"; + public const string SetFollowTargets = "set_follow_targets"; + + public const string TeleportPosition = "teleport_position"; + + public const string AppendFollowTargets = "append_follow_targets"; + public const string AppendFollowTargetsArray = "append_follow_targets_array"; + public const string EraseFollowTargets = "erase_follow_targets"; + + public const string GetFollowPath = "get_follow_path"; + public const string SetFollowPath = "set_follow_path"; + + public const string GetFollowOffset = "get_follow_offset"; + public const string SetFollowOffset = "set_follow_offset"; + + public const string GetFollowDamping = "get_follow_damping"; + public const string SetFollowDamping = "set_follow_damping"; + + public const string GetFollowDampingValue = "get_follow_damping_value"; + public const string SetFollowDampingValue = "set_follow_damping_value"; + + public const string GetFollowAxisLock = "get_follow_axis_lock"; + public const string SetFollowAxisLock = "set_follow_axis_lock"; + + public const string GetTweenResource = "get_tween_resource"; + public const string SetTweenResource = "set_tween_resource"; + + public const string GetTweenSkip = "get_tween_skip"; + public const string SetTweenSkip = "set_tween_skip"; + + public const string GetTweenDuration = "get_tween_duration"; + public const string SetTweenDuration = "set_tween_duration"; + + public const string GetTweenTransition = "get_tween_transition"; + public const string SetTweenTransition = "set_tween_transition"; + + public const string GetTweenEase = "get_tween_ease"; + public const string SetTweenEase = "set_tween_ease"; + + public const string GetTweenOnLoad = "get_tween_on_load"; + public const string SetTweenOnLoad = "set_tween_on_load"; + + public const string GetInactiveUpdateMode = "get_inactive_update_mode"; + public const string SetInactiveUpdateMode = "set_inactive_update_mode"; + + public const string GetHostLayers = "get_host_layers"; + public const string SetHostLayers = "set_host_layers"; + public const string SetHostLayersValue = "set_host_layers_value"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string EmitNoise = "emit_noise"; + } + + public static class PropertyName + { + public const string DeadZoneWidth = "dead_zone_width"; + public const string DeadZoneHeight = "dead_zone_height"; + } + + public static class SignalName + { + public const string BecameActive = "became_active"; + public const string BecameInactive = "became_inactive"; + public const string FollowTargetChanged = "follow_target_changed"; + public const string DeadZoneChanged = "dead_zone_changed"; + public const string DeadZoneReached = "dead_zone_reached"; + public const string TweenStarted = "tween_started"; + public const string IsTweening = "is_tweening"; + public const string TweenCompleted = "tween_completed"; + public const string TweenInterrupted = "tween_interrupted"; + public const string NoiseEmitted = "noise_emitted"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid new file mode 100644 index 0000000..856fbdb --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs.uid @@ -0,0 +1 @@ +uid://d3wh0457i0i3 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs new file mode 100644 index 0000000..20b3ba5 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs @@ -0,0 +1,351 @@ +using System.Linq; +using Godot; +using Godot.Collections; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum FollowMode2D +{ + None, + Glued, + Simple, + Group, + Path, + Framed +} + +public enum FollowLockAxis2D +{ + None, + X, + Y, + XY +} + +public static class PhantomCamera2DExtensions +{ + public static PhantomCamera2D AsPhantomCamera2D(this Node2D node2D) + { + return new PhantomCamera2D(node2D); + } + + public static PhantomCameraNoiseEmitter2D AsPhantomCameraNoiseEmitter2D(this Node2D node2D) + { + return new PhantomCameraNoiseEmitter2D(node2D); + } + + public static PhantomCameraNoise2D AsPhantomCameraNoise2D(this Resource resource) + { + return new PhantomCameraNoise2D(resource); + } +} + +public class PhantomCamera2D : PhantomCamera +{ + public Node2D Node2D => (Node2D)Node; + + public delegate void TweenInterruptedEventHandler(Node2D pCam); + public delegate void DeadZoneReachedEventHandler(Vector2 side); + public delegate void NoiseEmittedEventHandler(Transform2D output); + + public event TweenInterruptedEventHandler? TweenInterrupted; + public event DeadZoneReachedEventHandler? DeadZoneReached; + public event NoiseEmittedEventHandler? NoiseEmitted; + + private readonly Callable _callableTweenInterrupted; + private readonly Callable _callableDeadZoneReached; + private readonly Callable _callableNoiseEmitted; + + public Node2D FollowTarget + { + get => (Node2D)Node2D.Call(PhantomCamera.MethodName.GetFollowTarget); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowTarget, value); + } + + public Node2D[] FollowTargets + { + get => Node2D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray(); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowTargets, new Array(value)); + } + + public void AppendFollowTargets(Node2D target) => Node2D.Call(PhantomCamera.MethodName.AppendFollowTargets, target); + public void AppendFollowTargetsArray(Node2D[] targets) => Node2D.Call(PhantomCamera.MethodName.AppendFollowTargetsArray, targets); + public void EraseFollowTargets(Node2D target) => Node2D.Call(PhantomCamera.MethodName.EraseFollowTargets, target); + + public FollowMode2D FollowMode => (FollowMode2D)(int)Node.Call(PhantomCamera.MethodName.GetFollowMode); + + public Path2D FollowPath + { + get => (Path2D)Node2D.Call(PhantomCamera.MethodName.GetFollowPath); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowPath, value); + } + + public Vector2 FollowOffset + { + get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowOffset); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowOffset, value); + } + + public Vector2 FollowDampingValue + { + get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowDampingValue); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value); + } + + public FollowLockAxis2D FollowAxisLock + { + get => (FollowLockAxis2D)(int)Node2D.Call(PhantomCamera.MethodName.GetFollowAxisLock); + set => Node2D.Call(PhantomCamera.MethodName.SetFollowAxisLock, (int)value); + } + + public Vector2 Zoom + { + get => (Vector2)Node2D.Call(MethodName.GetZoom); + set => Node2D.Call(MethodName.SetZoom, value); + } + + public bool SnapToPixel + { + get => (bool)Node2D.Call(MethodName.GetSnapToPixel); + set => Node2D.Call(MethodName.SetSnapToPixel, value); + } + + public bool RotateWithTarget + { + get => (bool)Node2D.Call(MethodName.GetRotateWithTarget); + set => Node2D.Call(MethodName.SetRotateWithTarget, value); + } + + public float RotationOffset + { + get => (float)Node2D.Call(MethodName.GetRotationOffset); + set => Node2D.Call(MethodName.SetRotationOffset, value); + } + + public bool RotationDamping + { + get => (bool)Node2D.Call(MethodName.GetRotationDamping); + set => Node2D.Call(MethodName.SetRotationDamping, value); + } + + public float RotationDampingValue + { + get => (float)Node2D.Call(MethodName.GetRotationDampingValue); + set => Node2D.Call(MethodName.SetRotationDampingValue, value); + } + + public int LimitLeft + { + get => (int)Node2D.Call(MethodName.GetLimitLeft); + set => Node2D.Call(MethodName.SetLimitLeft, value); + } + + public int LimitTop + { + get => (int)Node2D.Call(MethodName.GetLimitTop); + set => Node2D.Call(MethodName.SetLimitTop, value); + } + + public int LimitRight + { + get => (int)Node2D.Call(MethodName.GetLimitRight); + set => Node2D.Call(MethodName.SetLimitRight, value); + } + + public int LimitBottom + { + get => (int)Node2D.Call(MethodName.GetLimitBottom); + set => Node2D.Call(MethodName.SetLimitBottom, value); + } + + public Vector4I LimitMargin + { + get => (Vector4I)Node2D.Call(MethodName.GetLimitMargin); + set => Node2D.Call(MethodName.SetLimitMargin, value); + } + + public bool AutoZoom + { + get => (bool)Node2D.Call(MethodName.GetAutoZoom); + set => Node2D.Call(MethodName.SetAutoZoom, value); + } + + public float AutoZoomMin + { + get => (float)Node2D.Call(MethodName.GetAutoZoomMin); + set => Node2D.Call(MethodName.SetAutoZoomMin, value); + } + + public float AutoZoomMax + { + get => (float)Node2D.Call(MethodName.GetAutoZoomMax); + set => Node2D.Call(MethodName.SetAutoZoomMax, value); + } + + public Vector4 AutoZoomMargin + { + get => (Vector4)Node2D.Call(MethodName.GetAutoZoomMargin); + set => Node2D.Call(MethodName.SetAutoZoomMargin, value); + } + + public bool DrawLimits + { + get => (bool)Node2D.Get(PropertyName.DrawLimits); + set => Node2D.Set(PropertyName.DrawLimits, value); + } + + public PhantomCameraNoise2D Noise + { + get => new((Resource)Node2D.Call(MethodName.GetNoise)); + set => Node2D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public void EmitNoise(Transform2D transform) => Node2D.Call(PhantomCamera.MethodName.EmitNoise, transform); + + public NodePath LimitTarget + { + get => (NodePath)Node2D.Call(MethodName.GetLimitTarget); + set => Node2D.Call(MethodName.SetLimitTarget, value); + } + + public static PhantomCamera2D FromScript(string path) => new(GD.Load(path).New().AsGodotObject()); + public static PhantomCamera2D FromScript(GDScript script) => new(script.New().AsGodotObject()); + + public PhantomCamera2D(GodotObject phantomCameraNode) : base(phantomCameraNode) + { + _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam)); + _callableDeadZoneReached = Callable.From((Vector2 side) => DeadZoneReached?.Invoke(side)); + _callableNoiseEmitted = Callable.From((Transform2D output) => NoiseEmitted?.Invoke(output)); + + Node2D.Connect(SignalName.TweenInterrupted, _callableTweenInterrupted); + Node2D.Connect(SignalName.DeadZoneReached, _callableDeadZoneReached); + Node2D.Connect(SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + ~PhantomCamera2D() + { + Node2D.Disconnect(SignalName.TweenInterrupted, _callableTweenInterrupted); + Node2D.Disconnect(SignalName.DeadZoneReached, _callableDeadZoneReached); + Node2D.Disconnect(SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + public void SetLimitTarget(TileMap tileMap) + { + Node2D.Call(MethodName.SetLimitTarget, tileMap.GetPath()); + } + + public void SetLimitTarget(TileMapLayer tileMapLayer) + { + Node2D.Call(MethodName.SetLimitTarget, tileMapLayer.GetPath()); + } + + public void SetLimitTarget(CollisionShape2D shape2D) + { + Node2D.Call(MethodName.SetLimitTarget, shape2D.GetPath()); + } + + public LimitTargetQueryResult? GetLimitTarget() + { + var result = (NodePath)Node2D.Call(MethodName.GetLimitTarget); + return result.IsEmpty ? null : new LimitTargetQueryResult(Node2D.GetNode(result)); + } + + public void SetLimit(Side side, int value) + { + Node2D.Call(MethodName.SetLimit, (int)side, value); + } + + public int GetLimit(Side side) + { + return (int)Node2D.Call(MethodName.GetLimit, (int)side); + } + + public new static class MethodName + { + public const string GetZoom = "get_zoom"; + public const string SetZoom = "set_zoom"; + + public const string GetSnapToPixel = "get_snap_to_pixel"; + public const string SetSnapToPixel = "set_snap_to_pixel"; + + public const string GetRotateWithTarget = "get_rotate_with_target"; + public const string SetRotateWithTarget = "set_rotate_with_target"; + + public const string GetRotationOffset = "get_rotation_offset"; + public const string SetRotationOffset = "set_rotation_offset"; + + public const string GetRotationDamping = "get_rotation_damping"; + public const string SetRotationDamping = "set_rotation_damping"; + + public const string GetRotationDampingValue = "get_rotation_damping_value"; + public const string SetRotationDampingValue = "set_rotation_damping_value"; + + public const string GetLimit = "get_limit"; + public const string SetLimit = "set_limit"; + + public const string GetLimitLeft = "get_limit_left"; + public const string SetLimitLeft = "set_limit_left"; + + public const string GetLimitTop = "get_limit_top"; + public const string SetLimitTop = "set_limit_top"; + + public const string GetLimitRight = "get_limit_right"; + public const string SetLimitRight = "set_limit_right"; + + public const string GetLimitBottom = "get_limit_bottom"; + public const string SetLimitBottom = "set_limit_bottom"; + + public const string GetLimitTarget = "get_limit_target"; + public const string SetLimitTarget = "set_limit_target"; + + public const string GetLimitMargin = "get_limit_margin"; + public const string SetLimitMargin = "set_limit_margin"; + + public const string GetAutoZoom = "get_auto_zoom"; + public const string SetAutoZoom = "set_auto_zoom"; + + public const string GetAutoZoomMin = "get_auto_zoom_min"; + public const string SetAutoZoomMin = "set_auto_zoom_min"; + + public const string GetAutoZoomMax = "get_auto_zoom_max"; + public const string SetAutoZoomMax = "set_auto_zoom_max"; + + public const string GetAutoZoomMargin = "get_auto_zoom_margin"; + public const string SetAutoZoomMargin = "set_auto_zoom_margin"; + + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + } + + public new static class PropertyName + { + public const string DrawLimits = "draw_limits"; + } +} + +public class LimitTargetQueryResult(GodotObject godotObject) +{ + public bool IsTileMap => godotObject.IsClass("TileMap"); + + public bool IsTileMapLayer => godotObject.IsClass("TileMapLayer"); + + public bool IsCollisionShape2D => godotObject.IsClass("CollisionShape2D"); + + public TileMap? AsTileMap() + { + return IsTileMap ? (TileMap)godotObject : null; + } + + public TileMapLayer? AsTileMapLayer() + { + return IsTileMapLayer ? (TileMapLayer)godotObject : null; + } + + public CollisionShape2D? AsCollisionShape2D() + { + return IsCollisionShape2D ? (CollisionShape2D)godotObject : null; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid new file mode 100644 index 0000000..8cc93f0 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs.uid @@ -0,0 +1 @@ +uid://c38e5qhsf3fk3 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs new file mode 100644 index 0000000..a3081d9 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs @@ -0,0 +1,493 @@ +using System.Linq; +using Godot; +using Godot.Collections; +using PhantomCamera.Noise; + +#nullable enable + +namespace PhantomCamera; + +public enum LookAtMode +{ + None, + Mimic, + Simple, + Group +} + +public enum FollowMode3D +{ + None, + Glued, + Simple, + Group, + Path, + Framed, + ThirdPerson +} + +public enum FollowLockAxis3D +{ + None, + X, + Y, + Z, + XY, + XZ, + YZ, + XYZ +} + +public static class PhantomCamera3DExtensions +{ + public static PhantomCamera3D AsPhantomCamera3D(this Node3D node3D) + { + return new PhantomCamera3D(node3D); + } + + public static PhantomCameraNoiseEmitter3D AsPhantomCameraNoiseEmitter3D(this Node3D node3D) + { + return new PhantomCameraNoiseEmitter3D(node3D); + } + + public static PhantomCameraNoise3D AsPhantomCameraNoise3D(this Resource resource) + { + return new PhantomCameraNoise3D(resource); + } + + public static Camera3DResource AsCamera3DResource(this Resource resource) + { + return new Camera3DResource(resource); + } + + public static Vector3 GetThirdPersonRotation(this PhantomCamera3D pCam3D) => + (Vector3)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonRotation); + + public static void SetThirdPersonRotation(this PhantomCamera3D pCam3D, Vector3 rotation) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonRotation, rotation); + + public static Vector3 GetThirdPersonRotationDegrees(this PhantomCamera3D pCam3D) => + (Vector3)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonRotationDegrees); + + public static void SetThirdPersonDegrees(this PhantomCamera3D pCam3D, Vector3 rotation) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonRotationDegrees, rotation); + + public static Quaternion GetThirdPersonQuaternion(this PhantomCamera3D pCam3D) => + (Quaternion)pCam3D.Node3D.Call(PhantomCamera3D.MethodName.GetThirdPersonQuaternion); + + public static void SetThirdPersonQuaternion(this PhantomCamera3D pCam3D, Quaternion quaternion) => + pCam3D.Node3D.Call(PhantomCamera3D.MethodName.SetThirdPersonQuaternion, quaternion); + +} + +public class PhantomCamera3D : PhantomCamera +{ + public Node3D Node3D => (Node3D)Node; + + public delegate void LookAtTargetChangedEventHandler(); + public delegate void DeadZoneReachedEventHandler(); + public delegate void Camera3DResourceChangedEventHandler(); + public delegate void Camera3DResourcePropertyChangedEventHandler(StringName property, Variant value); + public delegate void TweenInterruptedEventHandler(Node3D pCam); + public delegate void NoiseEmittedEventHandler(Transform3D output); + + public event LookAtTargetChangedEventHandler? LookAtTargetChanged; + public event DeadZoneReachedEventHandler? DeadZoneReached; + public event Camera3DResourceChangedEventHandler? Camera3DResourceChanged; + public event Camera3DResourcePropertyChangedEventHandler? Camera3DResourcePropertyChanged; + public event TweenInterruptedEventHandler? TweenInterrupted; + public event NoiseEmittedEventHandler? NoiseEmitted; + + private readonly Callable _callableLookAtTargetChanged; + private readonly Callable _callableDeadZoneReached; + private readonly Callable _callableCamera3DResourceChanged; + private readonly Callable _callableCamera3DResourcePropertyChanged; + private readonly Callable _callableTweenInterrupted; + private readonly Callable _callableNoiseEmitted; + + public Node3D FollowTarget + { + get => (Node3D)Node3D.Call(PhantomCamera.MethodName.GetFollowTarget); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowTarget, value); + } + + public Node3D[] FollowTargets + { + get => Node3D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray(); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowTargets, new Array(value)); + } + + public void AppendFollowTarget(Node3D target) => Node3D.Call(PhantomCamera.MethodName.AppendFollowTargets, target); + public void AppendFollowTargetArray(Node3D[] targets) => Node3D.Call(PhantomCamera.MethodName.AppendFollowTargetsArray, targets); + public void EraseFollowTarget(Node3D target) => Node3D.Call(PhantomCamera.MethodName.EraseFollowTargets, target); + + public FollowMode3D FollowMode => (FollowMode3D)(int)Node.Call(PhantomCamera.MethodName.GetFollowMode); + + public Path3D FollowPath + { + get => (Path3D)Node3D.Call(PhantomCamera.MethodName.GetFollowPath); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowPath, value); + } + + public Vector3 FollowOffset + { + get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowOffset); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowOffset, value); + } + + public Vector3 FollowDampingValue + { + get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowDampingValue); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value); + } + + public FollowLockAxis3D FollowAxisLock + { + get => (FollowLockAxis3D)(int)Node3D.Call(PhantomCamera.MethodName.GetFollowAxisLock); + set => Node3D.Call(PhantomCamera.MethodName.SetFollowAxisLock, (int)value); + } + + public LookAtMode LookAtMode => (LookAtMode)(int)Node3D.Call(MethodName.GetLookAtMode); + + public Camera3DResource Camera3DResource + { + get => new((Resource)Node3D.Call(MethodName.GetCamera3DResource)); + set => Node3D.Call(MethodName.SetCamera3DResource, value.Resource); + } + + public float SpringLength + { + get => (float)Node3D.Call(MethodName.GetSpringLength); + set => Node3D.Call(MethodName.SetSpringLength, value); + } + + public float VerticalRotationOffset + { + get => (float)Node3D.Call(MethodName.GetVerticalRotationOffset); + set => Node3D.Call(MethodName.SetVerticalRotationOffset, value); + } + + public float HorizontalRotationOffset + { + get => (float)Node3D.Call(MethodName.GetHorizontalRotationOffset); + set => Node3D.Call(MethodName.SetHorizontalRotationOffset, value); + } + + public float FollowDistance + { + get => (float)Node3D.Call(MethodName.GetFollowDistance); + set => Node3D.Call(MethodName.SetFollowDistance, value); + } + + public bool AutoFollowDistance + { + get => (bool)Node3D.Call(MethodName.GetAutoFollowDistance); + set => Node3D.Call(MethodName.SetAutoFollowDistance, value); + } + + public float AutoFollowDistanceMin + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMin); + set => Node3D.Call(MethodName.SetAutoFollowDistanceMin, value); + } + + public float AutoFollowDistanceMax + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMax); + set => Node3D.Call(MethodName.SetAutoFollowDistanceMax, value); + } + + public float AutoFollowDistanceDivisor + { + get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceDivisor); + set => Node3D.Call(MethodName.SetAutoFollowDistanceDivisor, value); + } + + public Node3D LookAtTarget + { + get => (Node3D)Node3D.Call(MethodName.GetLookAtTarget); + set => Node3D.Call(MethodName.SetLookAtTarget, value); + } + + public Node3D[] LookAtTargets + { + get => Node3D.Call(MethodName.GetLookAtTargets).AsGodotArray().ToArray(); + set => Node3D.Call(MethodName.SetLookAtTargets, new Array(value)); + } + + public bool IsLooking => (bool)Node3D.Call(MethodName.IsLooking); + + public int CollisionMask + { + get => (int)Node3D.Call(MethodName.GetCollisionMask); + set => Node3D.Call(MethodName.SetCollisionMask, value); + } + + public void SetCollisionMaskValue(int maskLayer, bool enable) => + Node3D.Call(MethodName.SetCollisionMaskValue, maskLayer, enable); + + public Shape3D Shape + { + get => (Shape3D)Node3D.Call(MethodName.GetShape); + set => Node3D.Call(MethodName.SetShape, value); + } + + public float Margin + { + get => (float)Node3D.Call(MethodName.GetMargin); + set => Node3D.Call(MethodName.SetMargin, value); + } + + public Vector3 LookAtOffset + { + get => (Vector3)Node3D.Call(MethodName.GetLookAtOffset); + set => Node3D.Call(MethodName.SetLookAtOffset, value); + } + + public bool LookAtDamping + { + get => (bool)Node3D.Call(MethodName.GetLookAtDamping); + set => Node3D.Call(MethodName.SetLookAtDamping, value); + } + + public float LookAtDampingValue + { + get => (float)Node3D.Call(MethodName.GetLookAtDampingValue); + set => Node3D.Call(MethodName.SetLookAtDampingValue, value); + } + + public Node3D Up + { + get => (Node3D)Node3D.Call(MethodName.GetUp); + set => Node3D.Call(MethodName.SetUp, value); + } + + public Vector3 UpTarget + { + get => (Vector3)Node3D.Call(MethodName.GetUpTarget); + set => Node3D.Call(MethodName.SetUpTarget, value); + } + + public int CullMask + { + get => (int)Node3D.Call(MethodName.GetCullMask); + set => Node3D.Call(MethodName.SetCullMask, value); + } + + public float HOffset + { + get => (float)Node3D.Call(MethodName.GetHOffset); + set => Node3D.Call(MethodName.SetHOffset, value); + } + + public float VOffset + { + get => (float)Node3D.Call(MethodName.GetVOffset); + set => Node3D.Call(MethodName.SetVOffset, value); + } + + public ProjectionType Projection + { + get => (ProjectionType)(int)Node3D.Call(MethodName.GetProjection); + set => Node3D.Call(MethodName.SetProjection, (int)value); + } + + public float Fov + { + get => (float)Node3D.Call(MethodName.GetFov); + set => Node3D.Call(MethodName.SetFov, value); + } + + public float Size + { + get => (float)Node3D.Call(MethodName.GetSize); + set => Node3D.Call(MethodName.SetSize, value); + } + + public Vector2 FrustumOffset + { + get => (Vector2)Node3D.Call(MethodName.GetFrustumOffset); + set => Node3D.Call(MethodName.SetFrustumOffset, value); + } + + public float Far + { + get => (float)Node3D.Call(MethodName.GetFar); + set => Node3D.Call(MethodName.SetFar, value); + } + + public float Near + { + get => (float)Node3D.Call(MethodName.GetNear); + set => Node3D.Call(MethodName.SetNear, value); + } + + public Environment Environment + { + get => (Environment)Node3D.Call(MethodName.GetEnvironment); + set => Node3D.Call(MethodName.SetEnvironment, value); + } + + public CameraAttributes Attributes + { + get => (CameraAttributes)Node3D.Call(MethodName.GetAttributes); + set => Node3D.Call(MethodName.SetAttributes, value); + } + + public PhantomCameraNoise3D Noise + { + get => new((Resource)Node3D.Call(MethodName.GetNoise)); + set => Node3D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public void EmitNoise(Transform3D transform) => Node3D.Call(PhantomCamera.MethodName.EmitNoise, transform); + + public static PhantomCamera3D FromScript(string path) => new(GD.Load(path).New().AsGodotObject()); + public static PhantomCamera3D FromScript(GDScript script) => new(script.New().AsGodotObject()); + + public PhantomCamera3D(GodotObject phantomCamera3DNode) : base(phantomCamera3DNode) + { + _callableLookAtTargetChanged = Callable.From(() => LookAtTargetChanged?.Invoke()); + _callableDeadZoneReached = Callable.From(() => DeadZoneReached?.Invoke()); + _callableCamera3DResourceChanged = Callable.From(() => Camera3DResourceChanged?.Invoke()); + _callableCamera3DResourcePropertyChanged = Callable.From((StringName property, Variant value) => + Camera3DResourcePropertyChanged?.Invoke(property, value)); + _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam)); + _callableNoiseEmitted = Callable.From((Transform3D output) => NoiseEmitted?.Invoke(output)); + + Node3D.Connect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged); + Node3D.Connect(PhantomCamera.SignalName.DeadZoneReached, _callableDeadZoneReached); + Node3D.Connect(SignalName.Camera3DResourceChanged, _callableCamera3DResourceChanged); + Node3D.Connect(SignalName.Camera3DResourcePropertyChanged, _callableCamera3DResourcePropertyChanged); + Node3D.Connect(PhantomCamera.SignalName.TweenInterrupted, _callableTweenInterrupted); + Node3D.Connect(PhantomCamera.SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + ~PhantomCamera3D() + { + Node3D.Disconnect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged); + Node3D.Disconnect(PhantomCamera.SignalName.DeadZoneReached, _callableDeadZoneReached); + Node3D.Disconnect(SignalName.Camera3DResourceChanged, _callableCamera3DResourceChanged); + Node3D.Disconnect(SignalName.Camera3DResourcePropertyChanged, _callableCamera3DResourcePropertyChanged); + Node3D.Disconnect(PhantomCamera.SignalName.TweenInterrupted, _callableTweenInterrupted); + Node3D.Disconnect(PhantomCamera.SignalName.NoiseEmitted, _callableNoiseEmitted); + } + + public new static class MethodName + { + public const string GetLookAtMode = "get_look_at_mode"; + + public const string GetCamera3DResource = "get_camera_3d_resource"; + public const string SetCamera3DResource = "set_camera_3d_resource"; + + public const string GetThirdPersonRotation = "get_third_person_rotation"; + public const string SetThirdPersonRotation = "set_third_person_rotation"; + + public const string GetThirdPersonRotationDegrees = "get_third_person_rotation_degrees"; + public const string SetThirdPersonRotationDegrees = "set_third_person_rotation_degrees"; + + public const string GetThirdPersonQuaternion = "get_third_person_quaternion"; + public const string SetThirdPersonQuaternion = "set_third_person_quaternion"; + + public const string GetVerticalRotationOffset = "get_vertical_rotation_offset"; + public const string SetVerticalRotationOffset = "set_vertical_rotation_offset"; + + public const string GetHorizontalRotationOffset = "get_horizontal_rotation_offset"; + public const string SetHorizontalRotationOffset = "set_horizontal_rotation_offset"; + + public const string GetSpringLength = "get_spring_length"; + public const string SetSpringLength = "set_spring_length"; + + public const string GetFollowDistance = "get_follow_distance"; + public const string SetFollowDistance = "set_follow_distance"; + + public const string GetAutoFollowDistance = "get_auto_follow_distance"; + public const string SetAutoFollowDistance = "set_auto_follow_distance"; + + public const string GetAutoFollowDistanceMin = "get_auto_follow_distance_min"; + public const string SetAutoFollowDistanceMin = "set_auto_follow_distance_min"; + + public const string GetAutoFollowDistanceMax = "get_auto_follow_distance_max"; + public const string SetAutoFollowDistanceMax = "set_auto_follow_distance_max"; + + public const string GetAutoFollowDistanceDivisor = "get_auto_follow_distance_divisor"; + public const string SetAutoFollowDistanceDivisor = "set_auto_follow_distance_divisor"; + + public const string GetLookAtTarget = "get_look_at_target"; + public const string SetLookAtTarget = "set_look_at_target"; + + public const string GetLookAtTargets = "get_look_at_targets"; + public const string SetLookAtTargets = "set_look_at_targets"; + + public const string IsLooking = "is_looking"; + + public const string GetUp = "get_up"; + public const string SetUp = "set_up"; + + public const string GetUpTarget = "get_up_target"; + public const string SetUpTarget = "set_up_target"; + + public const string GetCollisionMask = "get_collision_mask"; + public const string SetCollisionMask = "set_collision_mask"; + + public const string SetCollisionMaskValue = "set_collision_mask_value"; + + public const string GetShape = "get_shape"; + public const string SetShape = "set_shape"; + + public const string GetMargin = "get_margin"; + public const string SetMargin = "set_margin"; + + public const string GetLookAtOffset = "get_look_at_offset"; + public const string SetLookAtOffset = "set_look_at_offset"; + + public const string GetLookAtDamping = "get_look_at_damping"; + public const string SetLookAtDamping = "set_look_at_damping"; + + public const string GetLookAtDampingValue = "get_look_at_damping_value"; + public const string SetLookAtDampingValue = "set_look_at_damping_value"; + + public const string GetCullMask = "get_cull_mask"; + public const string SetCullMask = "set_cull_mask"; + + public const string GetHOffset = "get_h_offset"; + public const string SetHOffset = "set_h_offset"; + + public const string GetVOffset = "get_v_offset"; + public const string SetVOffset = "set_v_offset"; + + public const string GetProjection = "get_projection"; + public const string SetProjection = "set_projection"; + + public const string GetFov = "get_fov"; + public const string SetFov = "set_fov"; + + public const string GetSize = "get_size"; + public const string SetSize = "set_size"; + + public const string GetFrustumOffset = "get_frustum_offset"; + public const string SetFrustumOffset = "set_frustum_offset"; + + public const string GetFar = "get_far"; + public const string SetFar = "set_far"; + + public const string GetNear = "get_near"; + public const string SetNear = "set_near"; + + public const string GetEnvironment = "get_environment"; + public const string SetEnvironment = "set_environment"; + + public const string GetAttributes = "get_attributes"; + public const string SetAttributes = "set_attributes"; + + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + } + + public new static class SignalName + { + public const string LookAtTargetChanged = "look_at_target_changed"; + public const string Camera3DResourceChanged = "camera_3d_resource_changed"; + public const string Camera3DResourcePropertyChanged = "camera_3d_resource_property_changed"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid new file mode 100644 index 0000000..c1f0801 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs.uid @@ -0,0 +1 @@ +uid://bx3g7jxtwhi04 diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs new file mode 100644 index 0000000..1e73b57 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs @@ -0,0 +1,83 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoiseEmitter2D(GodotObject node) +{ + public Node2D Node2D = (Node2D)node; + + public PhantomCameraNoise2D Noise + { + get => new((Resource)Node2D.Call(MethodName.GetNoise)); + set => Node2D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public bool Continuous + { + get => (bool)Node2D.Call(MethodName.GetContinuous); + set => Node2D.Call(MethodName.SetContinuous, value); + } + + public float GrowthTime + { + get => (float)Node2D.Call(MethodName.GetGrowthTime); + set => Node2D.Call(MethodName.SetGrowthTime, value); + } + + public float Duration + { + get => (float)Node2D.Call(MethodName.GetDuration); + set => Node2D.Call(MethodName.SetDuration, value); + } + + public float DecayTime + { + get => (float)Node2D.Call(MethodName.GetDecayTime); + set => Node2D.Call(MethodName.SetDecayTime, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node2D.Call(MethodName.GetNoiseEmitterLayer); + set => Node2D.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void SetNoiseEmitterLayerValue(int layer, bool value) => + Node2D.Call(MethodName.SetNoiseEmitterLayerValue, layer, value); + + public void Emit() => Node2D.Call(MethodName.Emit); + + public bool IsEmitting() => (bool)Node2D.Call(MethodName.IsEmitting); + + public void Stop() => Node2D.Call(MethodName.Stop); + + public void Toggle() => Node2D.Call(MethodName.Toggle); + + public static class MethodName + { + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + + public const string GetContinuous = "get_continuous"; + public const string SetContinuous = "set_continuous"; + + public const string GetGrowthTime = "get_growth_time"; + public const string SetGrowthTime = "set_growth_time"; + + public const string GetDuration = "get_duration"; + public const string SetDuration = "set_duration"; + + public const string GetDecayTime = "get_decay_time"; + public const string SetDecayTime = "set_decay_time"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string SetNoiseEmitterLayerValue = "set_noise_emitter_layer_value"; + + public const string Emit = "emit"; + public const string IsEmitting = "is_emitting"; + public const string Stop = "stop"; + public const string Toggle = "toggle"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid new file mode 100644 index 0000000..4f35145 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter2D.cs.uid @@ -0,0 +1 @@ +uid://btom8l3wlkn2j diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs new file mode 100644 index 0000000..18620b2 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs @@ -0,0 +1,83 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoiseEmitter3D(GodotObject node) +{ + public Node3D Node3D = (Node3D)node; + + public PhantomCameraNoise3D Noise + { + get => new((Resource)Node3D.Call(MethodName.GetNoise)); + set => Node3D.Call(MethodName.SetNoise, (GodotObject)value.Resource); + } + + public bool Continuous + { + get => (bool)Node3D.Call(MethodName.GetContinuous); + set => Node3D.Call(MethodName.SetContinuous, value); + } + + public float GrowthTime + { + get => (float)Node3D.Call(MethodName.GetGrowthTime); + set => Node3D.Call(MethodName.SetGrowthTime, value); + } + + public float Duration + { + get => (float)Node3D.Call(MethodName.GetDuration); + set => Node3D.Call(MethodName.SetDuration, value); + } + + public float DecayTime + { + get => (float)Node3D.Call(MethodName.GetDecayTime); + set => Node3D.Call(MethodName.SetDecayTime, value); + } + + public int NoiseEmitterLayer + { + get => (int)Node3D.Call(MethodName.GetNoiseEmitterLayer); + set => Node3D.Call(MethodName.SetNoiseEmitterLayer, value); + } + + public void SetNoiseEmitterLayerValue(int layer, bool value) => + Node3D.Call(MethodName.SetNoiseEmitterLayerValue, layer, value); + + public void Emit() => Node3D.Call(MethodName.Emit); + + public bool IsEmitting() => (bool)Node3D.Call(MethodName.IsEmitting); + + public void Stop() => Node3D.Call(MethodName.Stop); + + public void Toggle() => Node3D.Call(MethodName.Toggle); + + public static class MethodName + { + public const string GetNoise = "get_noise"; + public const string SetNoise = "set_noise"; + + public const string GetContinuous = "get_continuous"; + public const string SetContinuous = "set_continuous"; + + public const string GetGrowthTime = "get_growth_time"; + public const string SetGrowthTime = "set_growth_time"; + + public const string GetDuration = "get_duration"; + public const string SetDuration = "set_duration"; + + public const string GetDecayTime = "get_decay_time"; + public const string SetDecayTime = "set_decay_time"; + + public const string GetNoiseEmitterLayer = "get_noise_emitter_layer"; + public const string SetNoiseEmitterLayer = "set_noise_emitter_layer"; + + public const string SetNoiseEmitterLayerValue = "set_noise_emitter_layer_value"; + + public const string Emit = "emit"; + public const string IsEmitting = "is_emitting"; + public const string Stop = "stop"; + public const string Toggle = "toggle"; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid new file mode 100644 index 0000000..bf32a5b --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCameraNoiseEmitter3D.cs.uid @@ -0,0 +1 @@ +uid://buvda14filkjx diff --git a/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs new file mode 100644 index 0000000..be211bd --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs @@ -0,0 +1,128 @@ +using Godot; + +#nullable enable + +namespace PhantomCamera; + +public enum InterpolationMode +{ + Auto, + Idle, + Physics +} + +public static class PhantomCameraHostExtensions +{ + public static PhantomCameraHost AsPhantomCameraHost(this Node node) + { + return new PhantomCameraHost(node); + } +} + +public class PhantomCameraHost() +{ + public Node Node { get; } = null!; + + public PhantomCameraHost(Node node) : this() + { + Node = node; + + _callablePCamBecameActive = Callable.From(pCam => PCamBecameActive?.Invoke(pCam)); + _callablePCamBecameInactive = Callable.From(pCam => PCamBecameInactive?.Invoke(pCam)); + + Node.Connect(SignalName.PCamBecameActive, _callablePCamBecameActive); + Node.Connect(SignalName.PCamBecameInactive, _callablePCamBecameInactive); + } + + ~PhantomCameraHost() + { + Node.Disconnect(SignalName.PCamBecameActive, _callablePCamBecameActive); + Node.Disconnect(SignalName.PCamBecameInactive, _callablePCamBecameInactive); + } + + public delegate void PCamBecameActiveEventHandler(Node pCam); + public delegate void PCamBecameInactiveEventHandler(Node pCam); + + public event PCamBecameActiveEventHandler? PCamBecameActive; + public event PCamBecameInactiveEventHandler? PCamBecameInactive; + + + private readonly Callable _callablePCamBecameActive; + private readonly Callable _callablePCamBecameInactive; + // For when Godot becomes the minimum version + // public InterpolationMode InterpolationMode + // { + // get => (InterpolationMode)(int)Node.Call(MethodName.GetInterpolationMode); + // set => Node.Call(MethodName.SetInterpolationMode, (int)value); + // } + + public int HostLayers + { + get => (int)Node.Call(PhantomCamera.MethodName.GetHostLayers); + set => Node.Call(PhantomCamera.MethodName.SetHostLayers, value); + } + + public void SetHostLayersValue(int layer, bool value) => Node.Call(MethodName.SetHostLayersValue, layer, value); + + public Camera2D? Camera2D => (Camera2D?)Node.Get(PropertyName.Camera2D); + + public Camera3D? Camera3D => (Camera3D?)Node.Get(PropertyName.Camera3D); + + public InterpolationMode InterpolationMode + { + get => (InterpolationMode)(int)Node.Call(MethodName.GetInterpolationMode); + set => Node.Call(MethodName.SetInterpolationMode, (int)value); + } + + public bool TriggerPhantomCameraTween => (bool)Node.Call(MethodName.GetTriggerPhantomCameraTween); + + public ActivePhantomCameraQueryResult? GetActivePhantomCamera() + { + var result = Node.Call(MethodName.GetActivePhantomCamera); + return result.VariantType == Variant.Type.Nil ? null : new ActivePhantomCameraQueryResult(result.AsGodotObject()); + } + + public static class PropertyName + { + public const string Camera2D = "camera_2d"; + public const string Camera3D = "camera_3d"; + } + + public static class MethodName + { + public const string GetActivePhantomCamera = "get_active_pcam"; + public const string GetTriggerPhantomCameraTween = "get_trigger_pcam_tween"; + + public const string GetInterpolationMode = "get_interpolation_mode"; + public const string SetInterpolationMode = "set_interpolation_mode"; + + public const string SetHostLayersValue = "set_host_layers_value"; + } + + public static class SignalName + { + public const string PCamBecameActive = "pcam_became_active"; + public const string PCamBecameInactive = "pcam_became_inactive"; + } +} + +public class ActivePhantomCameraQueryResult(GodotObject godotObject) +{ + public bool Is2D => godotObject.IsClass("Node2D") || ((Node)godotObject).Name.ToString().Contains("PhantomCamera2D") + || ((Node)godotObject).Name.ToString().Contains("PCam2D") + || ((Node)godotObject).Name.ToString().Contains("2D"); + + public bool Is3D => godotObject.IsClass("Node3D") || ((Node)godotObject).Name.ToString().Contains("PhantomCamera3D") + || ((Node)godotObject).Name.ToString().Contains("PCam3D") + || ((Node)godotObject).Name.ToString().Contains("3D"); + + public PhantomCamera2D? AsPhantomCamera2D() + { + return Is2D ? new PhantomCamera2D(godotObject) : null; + } + + public PhantomCamera3D? AsPhantomCamera3D() + { + return Is3D ? new PhantomCamera3D(godotObject) : null; + } +} diff --git a/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid new file mode 100644 index 0000000..ad4b4f5 --- /dev/null +++ b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs.uid @@ -0,0 +1 @@ +uid://cr8brwrls2nn3 diff --git a/addons/phantom_camera/scripts/resources/Camera3DResource.cs b/addons/phantom_camera/scripts/resources/Camera3DResource.cs new file mode 100644 index 0000000..7bd00b5 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/Camera3DResource.cs @@ -0,0 +1,117 @@ +using Godot; + +namespace PhantomCamera; + +public enum KeepAspect +{ + KeepWidth, + KeepHeight +} + +public enum ProjectionType +{ + Perspective, + Orthogonal, + Frustum +} + +public class Camera3DResource(Resource resource) +{ + public readonly Resource Resource = resource; + + public KeepAspect KeepAspect + { + get => (KeepAspect)(int)Resource.Call(MethodName.GetKeepAspect); + set => Resource.Call(MethodName.SetKeepAspect, (int)value); + } + + public int CullMask + { + get => (int)Resource.Call(MethodName.GetCullMask); + set => Resource.Call(MethodName.SetCullMask, value); + } + + public void SetCullMaskValue(int layer, bool value) => Resource.Call(MethodName.SetCullMaskValue, layer, value); + + public float HOffset + { + get => (float)Resource.Call(MethodName.GetHOffset); + set => Resource.Call(MethodName.SetHOffset, value); + } + + public float VOffset + { + get => (float)Resource.Call(MethodName.GetVOffset); + set => Resource.Call(MethodName.SetVOffset, value); + } + + public ProjectionType Projection + { + get => (ProjectionType)(int)Resource.Call(MethodName.GetProjection); + set => Resource.Call(MethodName.SetProjection, (int)value); + } + + public float Fov + { + get => (float)Resource.Call(MethodName.GetFov); + set => Resource.Call(MethodName.SetFov, Mathf.Clamp(value, 1, 179)); + } + + public float Size + { + get => (float)Resource.Call(MethodName.GetSize); + set => Resource.Call(MethodName.SetSize, Mathf.Clamp(value, 0.001f, float.PositiveInfinity)); + } + + public Vector2 FrustumOffset + { + get => (Vector2)Resource.Call(MethodName.GetFrustumOffset); + set => Resource.Call(MethodName.SetFrustumOffset, value); + } + + public float Near + { + get => (float)Resource.Call(MethodName.GetNear); + set => Resource.Call(MethodName.SetNear, Mathf.Clamp(value, 0.001f, float.PositiveInfinity)); + } + + public float Far + { + get => (float)Resource.Call(MethodName.GetFar); + set => Resource.Call(MethodName.SetFar, Mathf.Clamp(value, 0.01f, float.PositiveInfinity)); + } + + public static class MethodName + { + public const string GetKeepAspect = "get_keep_aspect"; + public const string SetKeepAspect = "set_keep_aspect"; + + public const string GetCullMask = "get_cull_mask"; + public const string SetCullMask = "set_cull_mask"; + public const string SetCullMaskValue = "set_cull_mask_value"; + + public const string GetHOffset = "get_h_offset"; + public const string SetHOffset = "set_h_offset"; + + public const string GetVOffset = "get_v_offset"; + public const string SetVOffset = "set_v_offset"; + + public const string GetProjection = "get_projection"; + public const string SetProjection = "set_projection"; + + public const string GetFov = "get_fov"; + public const string SetFov = "set_fov"; + + public const string GetSize = "get_size"; + public const string SetSize = "set_size"; + + public const string GetFrustumOffset = "get_frustum_offset"; + public const string SetFrustumOffset = "set_frustum_offset"; + + public const string GetNear = "get_near"; + public const string SetNear = "set_near"; + + public const string GetFar = "get_far"; + public const string SetFar = "set_far"; + } +} diff --git a/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid b/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid new file mode 100644 index 0000000..d66b43e --- /dev/null +++ b/addons/phantom_camera/scripts/resources/Camera3DResource.cs.uid @@ -0,0 +1 @@ +uid://jedyxlihuwbj diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs new file mode 100644 index 0000000..16b7273 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs @@ -0,0 +1,92 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoise2D(Resource resource) +{ + public readonly Resource Resource = resource; + + public float Amplitude + { + get => (float)Resource.Call(MethodName.GetAmplitude); + set => Resource.Call(MethodName.SetAmplitude, value); + } + + public float Frequency + { + get => (float)Resource.Call(MethodName.GetFrequency); + set => Resource.Call(MethodName.SetFrequency, value); + } + + public bool RandomizeNoiseSeed + { + get => (bool)Resource.Call(MethodName.GetRandomizeNoiseSeed); + set => Resource.Call(MethodName.SetRandomizeNoiseSeed, value); + } + + public int NoiseSeed + { + get => (int)Resource.Call(MethodName.GetNoiseSeed); + set => Resource.Call(MethodName.SetNoiseSeed, value); + } + + public bool RotationalNoise + { + get => (bool)Resource.Call(MethodName.GetRotationalNoise); + set => Resource.Call(MethodName.SetRotationalNoise, value); + } + + public bool PositionalNoise + { + get => (bool)Resource.Call(MethodName.GetPositionalNoise); + set => Resource.Call(MethodName.SetPositionalNoise, value); + } + + public float RotationalMultiplier + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplier); + set => Resource.Call(MethodName.SetRotationalMultiplier, value); + } + + public float PositionalMultiplierX + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierX); + set => Resource.Call(MethodName.SetPositionalMultiplierX, value); + } + + public float PositionalMultiplierY + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierY); + set => Resource.Call(MethodName.SetPositionalMultiplierY, value); + } + + public static class MethodName + { + public const string GetAmplitude = "get_amplitude"; + public const string SetAmplitude = "set_amplitude"; + + public const string GetFrequency = "get_frequency"; + public const string SetFrequency = "set_frequency"; + + public const string GetRandomizeNoiseSeed = "get_randomize_noise_seed"; + public const string SetRandomizeNoiseSeed = "set_randomize_noise_seed"; + + public const string GetNoiseSeed = "get_noise_seed"; + public const string SetNoiseSeed = "set_noise_seed"; + + public const string GetRotationalNoise = "get_rotational_noise"; + public const string SetRotationalNoise = "set_rotational_noise"; + + public const string GetPositionalNoise = "get_positional_noise"; + public const string SetPositionalNoise = "set_positional_noise"; + + public const string GetRotationalMultiplier = "get_rotational_multiplier"; + public const string SetRotationalMultiplier = "set_rotational_multiplier"; + + public const string GetPositionalMultiplierX = "get_positional_multiplier_x"; + public const string SetPositionalMultiplierX = "set_positional_multiplier_x"; + + public const string GetPositionalMultiplierY = "get_positional_multiplier_y"; + public const string SetPositionalMultiplierY = "set_positional_multiplier_y"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid new file mode 100644 index 0000000..16ddb84 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise2D.cs.uid @@ -0,0 +1 @@ +uid://capjdoxs6gs6r diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs new file mode 100644 index 0000000..175a427 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs @@ -0,0 +1,119 @@ +using Godot; + +namespace PhantomCamera.Noise; + +public class PhantomCameraNoise3D(Resource resource) +{ + public readonly Resource Resource = resource; + + public float Amplitude + { + get => (float)Resource.Call(MethodName.GetAmplitude); + set => Resource.Call(MethodName.SetAmplitude, value); + } + + public float Frequency + { + get => (float)Resource.Call(MethodName.GetFrequency); + set => Resource.Call(MethodName.SetFrequency, value); + } + + public bool RandomizeNoiseSeed + { + get => (bool)Resource.Call(MethodName.GetRandomizeNoiseSeed); + set => Resource.Call(MethodName.SetRandomizeNoiseSeed, value); + } + + public int NoiseSeed + { + get => (int)Resource.Call(MethodName.GetNoiseSeed); + set => Resource.Call(MethodName.SetNoiseSeed, value); + } + + public bool RotationalNoise + { + get => (bool)Resource.Call(MethodName.GetRotationalNoise); + set => Resource.Call(MethodName.SetRotationalNoise, value); + } + + public bool PositionalNoise + { + get => (bool)Resource.Call(MethodName.GetPositionalNoise); + set => Resource.Call(MethodName.SetPositionalNoise, value); + } + + public float RotationalMultiplierX + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierX); + set => Resource.Call(MethodName.SetRotationalMultiplierX, value); + } + + public float RotationalMultiplierY + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierY); + set => Resource.Call(MethodName.SetRotationalMultiplierY, value); + } + + public float RotationalMultiplierZ + { + get => (float)Resource.Call(MethodName.GetRotationalMultiplierZ); + set => Resource.Call(MethodName.SetRotationalMultiplierZ, value); + } + + public float PositionalMultiplierX + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierX); + set => Resource.Call(MethodName.SetPositionalMultiplierX, value); + } + + public float PositionalMultiplierY + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierY); + set => Resource.Call(MethodName.SetPositionalMultiplierY, value); + } + + public float PositionalMultiplierZ + { + get => (float)Resource.Call(MethodName.GetPositionalMultiplierZ); + set => Resource.Call(MethodName.SetPositionalMultiplierZ, value); + } + + public static class MethodName + { + public const string GetAmplitude = "get_amplitude"; + public const string SetAmplitude = "set_amplitude"; + + public const string GetFrequency = "get_frequency"; + public const string SetFrequency = "set_frequency"; + + public const string GetRandomizeNoiseSeed = "get_randomize_noise_seed"; + public const string SetRandomizeNoiseSeed = "set_randomize_noise_seed"; + + public const string GetNoiseSeed = "get_noise_seed"; + public const string SetNoiseSeed = "set_noise_seed"; + + public const string GetRotationalNoise = "get_rotational_noise"; + public const string SetRotationalNoise = "set_rotational_noise"; + + public const string GetPositionalNoise = "get_positional_noise"; + public const string SetPositionalNoise = "set_positional_noise"; + + public const string GetRotationalMultiplierX = "get_rotational_multiplier_x"; + public const string SetRotationalMultiplierX = "set_rotational_multiplier_x"; + + public const string GetRotationalMultiplierY = "get_rotational_multiplier_y"; + public const string SetRotationalMultiplierY = "set_rotational_multiplier_y"; + + public const string GetRotationalMultiplierZ = "get_rotational_multiplier_z"; + public const string SetRotationalMultiplierZ = "set_rotational_multiplier_z"; + + public const string GetPositionalMultiplierX = "get_positional_multiplier_x"; + public const string SetPositionalMultiplierX = "set_positional_multiplier_x"; + + public const string GetPositionalMultiplierY = "get_positional_multiplier_y"; + public const string SetPositionalMultiplierY = "set_positional_multiplier_y"; + + public const string GetPositionalMultiplierZ = "get_positional_multiplier_z"; + public const string SetPositionalMultiplierZ = "set_positional_multiplier_z"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid new file mode 100644 index 0000000..53d184f --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraNoise3D.cs.uid @@ -0,0 +1 @@ +uid://chk7643ynhe4f diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs new file mode 100644 index 0000000..1c332b7 --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs @@ -0,0 +1,64 @@ +using Godot; + +namespace PhantomCamera; + +public enum TransitionType +{ + Linear, + Sine, + Quint, + Quart, + Quad, + Expo, + Elastic, + Cubic, + Circ, + Bounce, + Back +} + +public enum EaseType +{ + EaseIn, + EaseOut, + EaseInOut, + EaseOutIn +} + +public static class PhantomCameraTweenExtensions +{ + public static PhantomCameraTween AsPhantomCameraTween(this Resource resource) + { + return new PhantomCameraTween(resource); + } +} + +public class PhantomCameraTween(Resource tweenResource) +{ + public Resource Resource { get; } = tweenResource; + + public float Duration + { + get => (float)Resource.Get(PropertyName.Duration); + set => Resource.Set(PropertyName.Duration, value); + } + + public TransitionType Transition + { + get => (TransitionType)(int)Resource.Get(PropertyName.Transition); + set => Resource.Set(PropertyName.Transition, (int)value); + } + + public EaseType Ease + { + get => (EaseType)(int)Resource.Get(PropertyName.Ease); + set => Resource.Set(PropertyName.Ease, (int)value); + } + + public static class PropertyName + { + public const string Duration = "duration"; + public const string Transition = "transition"; + public const string Ease = "ease"; + } +} diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid new file mode 100644 index 0000000..dd50ead --- /dev/null +++ b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs.uid @@ -0,0 +1 @@ +uid://ybr5c2s0tfvx diff --git a/addons/phantom_camera/themes/button_focus.tres b/addons/phantom_camera/themes/button_focus.tres new file mode 100644 index 0000000..e6fcc45 --- /dev/null +++ b/addons/phantom_camera/themes/button_focus.tres @@ -0,0 +1,17 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://p058hmj3uut0"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/button_hover.tres b/addons/phantom_camera/themes/button_hover.tres new file mode 100644 index 0000000..9d37a86 --- /dev/null +++ b/addons/phantom_camera/themes/button_hover.tres @@ -0,0 +1,13 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://5weqvkjsfso3"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/button_normal.tres b/addons/phantom_camera/themes/button_normal.tres new file mode 100644 index 0000000..4eae33d --- /dev/null +++ b/addons/phantom_camera/themes/button_normal.tres @@ -0,0 +1,17 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://bclbwo3xrdat0"] + +[resource] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/addons/phantom_camera/themes/theme.tres b/addons/phantom_camera/themes/theme.tres new file mode 100644 index 0000000..7ce53d8 --- /dev/null +++ b/addons/phantom_camera/themes/theme.tres @@ -0,0 +1,102 @@ +[gd_resource type="Theme" load_steps=12 format=3 uid="uid://bhppejri5dbsf"] + +[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_5rtjh"] +[ext_resource type="StyleBox" uid="uid://5weqvkjsfso3" path="res://addons/phantom_camera/themes/button_hover.tres" id="2_du6h5"] +[ext_resource type="StyleBox" uid="uid://bclbwo3xrdat0" path="res://addons/phantom_camera/themes/button_normal.tres" id="3_a8j1f"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ek0y3"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rjkuq"] +content_margin_left = 8.0 +content_margin_top = 4.0 +content_margin_right = 8.0 +content_margin_bottom = 4.0 +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x7u0w"] +content_margin_top = 2.0 +content_margin_right = 8.0 +bg_color = Color(0.0784314, 0.109804, 0.129412, 1) +border_width_top = 2 +border_width_right = 2 +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dln2q"] +content_margin_top = 8.0 +content_margin_bottom = 8.0 +draw_center = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wk7ot"] +bg_color = Color(0.227451, 0.72549, 0.603922, 1) +border_color = Color(0.227451, 0.72549, 0.603922, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jidrt"] +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o2xwc"] +bg_color = Color(0.960784, 0.960784, 0.960784, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ul127"] +draw_center = false +border_width_left = 4 +border_width_right = 4 +border_color = Color(0.8, 0.8, 0.8, 0) + +[resource] +default_font = ExtResource("1_5rtjh") +Button/colors/font_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/font_focus_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_hover_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/font_pressed_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_focus_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_hover_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_hover_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/colors/icon_normal_color = Color(0.0784314, 0.109804, 0.129412, 1) +Button/colors/icon_pressed_color = Color(0.227451, 0.72549, 0.603922, 1) +Button/styles/focus = SubResource("StyleBoxFlat_ek0y3") +Button/styles/hover = ExtResource("2_du6h5") +Button/styles/hover_pressed = null +Button/styles/normal = ExtResource("3_a8j1f") +Button/styles/pressed = SubResource("StyleBoxFlat_rjkuq") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_x7u0w") +ScrollContainer/styles/panel = SubResource("StyleBoxFlat_dln2q") +VBoxContainer/constants/separation = 8 +VScrollBar/styles/grabber = SubResource("StyleBoxFlat_wk7ot") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_jidrt") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_o2xwc") +VScrollBar/styles/scroll = SubResource("StyleBoxFlat_ul127") diff --git a/autoloads/config_file_handler.gd b/autoloads/config_file_handler.gd deleted file mode 100644 index 079a48a..0000000 --- a/autoloads/config_file_handler.gd +++ /dev/null @@ -1,11 +0,0 @@ -extends Node - -var settings_config := ConfigFile.new() -const SETTINGS_PATH := "user://settings.ini" - - -func _ready() -> void: - if !FileAccess.file_exists(SETTINGS_PATH): - settings_config.save(SETTINGS_PATH) - else: - settings_config.load(SETTINGS_PATH) \ No newline at end of file diff --git a/autoloads/config_file_handler.gd.uid b/autoloads/config_file_handler.gd.uid deleted file mode 100644 index 10aaa99..0000000 --- a/autoloads/config_file_handler.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dueoywjgtwv7m diff --git a/autoloads/save_system.gd b/autoloads/save_system.gd deleted file mode 100644 index 155b977..0000000 --- a/autoloads/save_system.gd +++ /dev/null @@ -1,42 +0,0 @@ -extends Node - -@export var save_path: String = "user://savegame.save" -@export var version: int = 1 - -@onready var gm: GM = $"/root/GameManager" - - -func save_game(): - var save_data := { - "player_state": gm.player_state, - "version": version, - } - var file := FileAccess.open(save_path, FileAccess.WRITE) - file.store_var(save_data) - file.close() - print("Game saved to: ", save_path) - - -func load_game() -> bool: - if not FileAccess.file_exists(save_path): - return false - var file := FileAccess.open(save_path, FileAccess.READ) - var save_data: Dictionary = file.get_var() - file.close() - - if save_data.has("version") and save_data["version"] != version: - print("Save file version mismatch. Expected: ", version, ", Found: ", save_data["version"]) - return false - print("Game state loaded from: ", save_path) - print("Player state: ", save_data["player_state"]) - gm.player_state = save_data["player_state"] - var skills: Array[SkillData] = [] - for skill in gm.player_state["unlocked_skills"]: - skills.append(skill) - - gm.unlock_skills(skills) - return true - - -func check_save_exists() -> bool: - return FileAccess.file_exists(save_path) diff --git a/autoloads/save_system.gd.uid b/autoloads/save_system.gd.uid deleted file mode 100644 index 394c1f9..0000000 --- a/autoloads/save_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d3tgf7fyr6xek diff --git a/autoloads/steam_controller_input.gd b/autoloads/steam_controller_input.gd deleted file mode 100644 index a3b5fdf..0000000 --- a/autoloads/steam_controller_input.gd +++ /dev/null @@ -1,169 +0,0 @@ -extends Node - -## A globally accessible manager for device-specific actions using SteamInput for any controller -## and standard Godot Input for the keyboard. -## -## All methods in this class that have a "device" parameter can accept -1 -## which means the keyboard device. - -# The actions defined in the Steam .vdf file are listed here -# with true or false indicating if input is analog or digital. -# False is digital (buttons), true is analog (joysticks, triggers, etc). - -var action_names := { - "move": true, - "jump": false, - "left": false, - "right": false, - "down": false, - "up": false, - "interact": false, - "pause": false, - } - -var got_handles := false -var game_action_set -var current_action_set -var actions := {} -# Store the state of each action and the frame it entered that state. -var action_states := {} - - -func init() -> void: - Steam.input_device_connected.connect(_on_steam_input_device_connected) - Steam.input_device_disconnected.connect(_on_steam_input_device_disconnected) - - -func _on_steam_input_device_connected(input_handle: int) -> void: - if not got_handles: - get_handles() - - Steam.activateActionSet(input_handle, current_action_set) - print("Device connected %s" % str(input_handle)) - - -func _on_steam_input_device_disconnected(input_handle: int) -> void: - print("Device disconnected %s" % str(input_handle)) - - -func get_handles() -> void: - got_handles = true - game_action_set = Steam.getActionSetHandle("GameControls") - current_action_set = game_action_set - get_action_handles(action_names) - - -func get_action_handles(action_names: Dictionary) -> void: - for action in action_names.keys(): - if action_names[action]: - actions[action] = Steam.getAnalogActionHandle(action) - else: - actions[action] = Steam.getDigitalActionHandle(action) - - -func get_controllers() -> Array[int]: - var controllers: Array[int] = [-1] - var steam_controllers = Steam.getConnectedControllers() - if steam_controllers: - controllers.append_array(steam_controllers) - - return controllers - - -func get_action_strength(device: int, action: StringName, exact_match: bool = false) -> float: - if device >= 0: - if not got_handles: return 0 - var action_data = Steam.getAnalogActionData(device, actions[action]) - return action_data.x - return Input.get_action_strength(action, exact_match) - - -func get_axis(device: int, negative_action: StringName, positive_action: StringName) -> float: - if device >= 0: - if not got_handles: return 0 - var negative = Steam.getAnalogActionData(device, actions[negative_action]) - var positive = Steam.getAnalogActionData(device, actions[positive_action]) - return positive.x - negative.x - return Input.get_axis(negative_action, positive_action) - - -func get_vector(device: int, negative_x: StringName, positive_x: StringName, negative_y: StringName, positive_y: StringName, deadzone: float = -1.0) -> Vector2: - if device >= 0: - if not got_handles: return Vector2.ZERO - var negative_x_val = Steam.getAnalogActionData(device, actions[negative_x]) - var positive_x_val = Steam.getAnalogActionData(device, actions[positive_x]) - var negative_y_val = Steam.getAnalogActionData(device, actions[negative_y]) - var positive_y_val = Steam.getAnalogActionData(device, actions[positive_y]) - # Steam's y axis is inverted compared to Godot - return Vector2(positive_x_val - negative_x_val, -(positive_y_val - negative_y_val)).normalized() - return Input.get_vector(negative_x, positive_x, negative_y, positive_y, deadzone) - - -func get_move_input(device: int) -> Vector2: - if device >= 0: - if not got_handles: return Vector2.ZERO - # Get the analog stick movement - var action_data = Steam.getAnalogActionData(device, actions["Move"]) - return Vector2(action_data.x, -action_data.y).normalized() - return Vector2(Input.get_axis("left", "right"), Input.get_axis("up", "down")).normalized() - - -func get_action_state(device: int, action: String) -> Dictionary: - # Get the current action, but create the defaults along the way if they don't exist. - if not action_states.get(device): - action_states[device] = {} - if not action_states[device].get(action): - action_states[device][action] = { "held": false, "press_frame": -1, "release_frame": -1 } - return action_states[device][action] - - -func set_action_state(device: int, action: StringName, currently_held: bool, current_frame: int) -> Dictionary: - # Get the state of the action last frame - var previous_action_state := get_action_state(device, action) - - # If we're pressing the action now and we weren't pressing it last frame, - # track that we pressed the action this frame. - if currently_held and not previous_action_state.held: - action_states[device][action].held = true - action_states[device][action].press_frame = current_frame - # If we're not pressing it this frame but we were pressing it last frame, - # track that we released the action this frame. - elif not currently_held and previous_action_state.held: - action_states[device][action].held = false - action_states[device][action].release_frame = current_frame - - # Return the current state of the action - return action_states[device][action] - - -func is_action_pressed(device: int, action: StringName, exact_match: bool = false) -> bool: - if device >= 0: - if not got_handles: return false - var current_frame := Engine.get_process_frames() - var currently_held = Steam.getDigitalActionData(device, actions[action]).state - set_action_state(device, action, currently_held, current_frame) - return currently_held - # If keyboard, use normal Godot input system. - return Input.is_action_pressed(action, exact_match) - - -func is_action_just_pressed(device: int, action: StringName, exact_match: bool = false) -> bool: - if device >= 0: - if not got_handles: return false - var current_frame := Engine.get_process_frames() - var currently_held = Steam.getDigitalActionData(device, actions[action]).state - var action_state := set_action_state(device, action, currently_held, current_frame) - return currently_held and action_state.press_frame == current_frame - # If keyboard, use normal Godot input system. - return Input.is_action_just_pressed(action, exact_match) - - -func is_action_just_released(device: int, action: StringName, exact_match: bool = false) -> bool: - if device >= 0: - if not got_handles: return false - var current_frame := Engine.get_process_frames() - var currently_held = Steam.getDigitalActionData(device, actions[action]).state - var action_state := set_action_state(device, action, currently_held, current_frame) - return not currently_held and action_state.release_frame == current_frame - # If keyboard, use normal Godot input system. - return Input.is_action_just_released(action, exact_match) \ No newline at end of file diff --git a/autoloads/steam_controller_input.gd.uid b/autoloads/steam_controller_input.gd.uid deleted file mode 100644 index b89d7ec..0000000 --- a/autoloads/steam_controller_input.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ctb7ehok6dme3 diff --git a/autoloads/ui_manager.gd b/autoloads/ui_manager.gd deleted file mode 100644 index a5c8914..0000000 --- a/autoloads/ui_manager.gd +++ /dev/null @@ -1,54 +0,0 @@ -extends Node - -@export var ui_stack: Array[Control] = [] -signal screen_pushed(screen: Control) -signal screen_popped(screen: Control) - - -func push_screen(screen: Control) -> void: - if not screen: - push_error("Cannot push a null screen.") - return - - ui_stack.append(screen) - screen.show() - screen.set_process_input(true) - screen.set_focus_mode(Control.FOCUS_ALL) - screen.grab_focus() - screen_pushed.emit(screen) - - -func pop_screen() -> void: - if ui_stack.is_empty(): - return - - var top: Control = ui_stack.pop_back() - top.hide() - top.set_process_input(false) - screen_popped.emit(top) - top.accept_event() - - if not ui_stack.is_empty(): - ui_stack.back().grab_focus() - - -func top_screen() -> Control: - return ui_stack.back() if not ui_stack.is_empty() else null - - -func is_screen_on_top(screen: Control) -> bool: - return not ui_stack.is_empty() and ui_stack.back() == screen - - -func is_visible_on_stack(screen: Control) -> bool: - return ui_stack.has(screen) and screen.visible - - -func close_all() -> void: - while not ui_stack.is_empty(): - pop_screen() - - -func hide_and_disable(screen: Control) -> void: - screen.hide() - screen.set_process_input(false) diff --git a/autoloads/ui_manager.gd.uid b/autoloads/ui_manager.gd.uid deleted file mode 100644 index ba8dbd0..0000000 --- a/autoloads/ui_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vmhlsc3fu3bn diff --git a/objects/camera_manager.tscn b/objects/camera_manager.tscn deleted file mode 100644 index cf4019d..0000000 --- a/objects/camera_manager.tscn +++ /dev/null @@ -1,5 +0,0 @@ -[gd_scene format=3 uid="uid://qq5g75cinhap"] - -[node name="Camera Manager" type="Node"] - -[node name="Camera Nodes" type="StaticBody2D" parent="."] diff --git a/objects/entities/big_coin.tscn b/objects/entities/big_coin.tscn index 10c1b29..0fd6bec 100644 --- a/objects/entities/big_coin.tscn +++ b/objects/entities/big_coin.tscn @@ -1,15 +1,15 @@ [gd_scene load_steps=7 format=3 uid="uid://bargnp4twtmxg"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_aya2w"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="2_7cph7"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_htmrw"] [ext_resource type="Resource" uid="uid://bsnr5v2b2mfsl" path="res://resources/collectables/big_coin.tres" id="3_lk3av"] -[ext_resource type="Script" uid="uid://bg75hnr3q6grk" path="res://scripts/components/fade_away.gd" id="4_wkrj0"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="4_62p7g"] [ext_resource type="AudioStream" uid="uid://bceic1csr8rq3" path="res://sfx/pickup_coin_2.wav" id="5_dbffd"] [sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"] radius = 9.0 -[node name="Big Coin" type="Area2D"] +[node name="Big Coin" type="Area2D" groups=["coins"]] scale = Vector2(2, 2) collision_layer = 2 collision_mask = 4 @@ -23,19 +23,17 @@ hframes = 12 vframes = 12 frame = 51 -[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape", "sfx") groups=["coins"]] -script = ExtResource("2_7cph7") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("3_lk3av") -sfx = NodePath("../sfx") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")] +script = ExtResource("2_htmrw") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("3_lk3av") +Sfx = NodePath("../sfx") -[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")] -script = ExtResource("4_wkrj0") -sprite2d = NodePath("../Sprite2D") -fade_duration = 0.4 -root = NodePath("..") -area2d = NodePath("..") +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("4_62p7g") +Sprite = NodePath("../Sprite2D") +Area = NodePath("..") [node name="sfx" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("5_dbffd") diff --git a/objects/entities/big_treasure.tscn b/objects/entities/big_treasure.tscn index baf907e..504cc13 100644 --- a/objects/entities/big_treasure.tscn +++ b/objects/entities/big_treasure.tscn @@ -1,15 +1,15 @@ [gd_scene load_steps=7 format=3 uid="uid://d08dfqmirnd66"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_1co1x"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="2_cujcq"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_lthbn"] [ext_resource type="Resource" uid="uid://b6xqotmke54x" path="res://resources/collectables/big_treasure.tres" id="3_k64cr"] -[ext_resource type="Script" uid="uid://bg75hnr3q6grk" path="res://scripts/components/fade_away.gd" id="4_nw7tw"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="4_qwwsj"] [ext_resource type="AudioStream" uid="uid://wr7n5ivv06ux" path="res://sfx/pickup_coin_4.wav" id="5_fxf8v"] [sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"] radius = 9.0 -[node name="Big Treasure" type="Area2D"] +[node name="Big Treasure" type="Area2D" groups=["coins"]] collision_layer = 2 collision_mask = 4 @@ -22,19 +22,18 @@ hframes = 12 vframes = 12 frame = 64 -[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape", "sfx") groups=["coins"]] -script = ExtResource("2_cujcq") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("3_k64cr") -sfx = NodePath("../sfx") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")] +script = ExtResource("2_lthbn") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("3_k64cr") +Sfx = NodePath("../sfx") -[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")] -script = ExtResource("4_nw7tw") -sprite2d = NodePath("../Sprite2D") -fade_duration = 0.4 -root = NodePath("..") -area2d = NodePath("..") +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("4_qwwsj") +Sprite = NodePath("../Sprite2D") +FadeDuration = 0.4 +Area = NodePath("..") [node name="sfx" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("5_fxf8v") diff --git a/objects/entities/brick.tscn b/objects/entities/brick.tscn index a0e45f0..240a8da 100644 --- a/objects/entities/brick.tscn +++ b/objects/entities/brick.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=10 format=3 uid="uid://bymro4t7angv5"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_1c3jb"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="2_i6t5k"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="3_8xipx"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="4_rdtjq"] -[ext_resource type="Script" uid="uid://bgty7040ams6s" path="res://scripts/components/projectile_init_component.gd" id="5_2vqt8"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="6_d0tcd"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="7_r41xl"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="2_r41xl"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="3_h7wgh"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="4_ihgvw"] +[ext_resource type="Script" uid="uid://c7n6ecsobohjn" path="res://scripts/components/ProjectileInitComponent.cs" id="5_aiue8"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="6_t4c40"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="7_y10b5"] [ext_resource type="PackedScene" uid="uid://c1iorglk708g0" path="res://objects/fxs/terrain_hit_fx.tscn" id="8_d0tcd"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ar0xf"] @@ -25,12 +25,11 @@ hframes = 12 vframes = 12 frame = 80 -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d", "hit_terrain_fx", "bullet_sprite")] -script = ExtResource("2_i6t5k") -root = NodePath("..") -area2d = NodePath("..") -hit_terrain_fx = NodePath("../TerrainHitFX") -bullet_sprite = NodePath("../Sprite2D") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "TerrainHitFx", "BulletSprite")] +script = ExtResource("2_r41xl") +Area = NodePath("..") +TerrainHitFx = NodePath("../TerrainHitFX") +BulletSprite = NodePath("../Sprite2D") [node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."] position = Vector2(0, 2.38419e-07) @@ -38,30 +37,25 @@ scale = Vector2(0.8, 0.5) [node name="Timer" type="Timer" parent="."] -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("3_8xipx") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "DamageTimer")] +script = ExtResource("3_h7wgh") +Area = NodePath("..") +DamageTimer = NodePath("../Timer") -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] -script = ExtResource("4_rdtjq") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] +script = ExtResource("4_ihgvw") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") -[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("launch_component")] -script = ExtResource("5_2vqt8") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://bgty7040ams6s" +[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("5_aiue8") +LaunchComponent = NodePath("../LaunchComponent") -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("6_d0tcd") -root = NodePath(".") -speed = 180.0 -metadata/_custom_type_script = "uid://873un8agkyja" +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("6_t4c40") +Speed = 64.0 -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] -script = ExtResource("7_r41xl") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://cvcnfrr1udco5" +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("7_y10b5") +LaunchComponent = NodePath("../LaunchComponent") [node name="TerrainHitFX" parent="." instance=ExtResource("8_d0tcd")] diff --git a/objects/entities/brick_player.tscn b/objects/entities/brick_player.tscn index 706e1a8..7330bcf 100644 --- a/objects/entities/brick_player.tscn +++ b/objects/entities/brick_player.tscn @@ -1,43 +1,43 @@ [gd_scene load_steps=48 format=3 uid="uid://bqi5s710xb1ju"] -[ext_resource type="Script" uid="uid://ccuddyoakg04u" path="res://scripts/player.gd" id="1_8j4h4"] +[ext_resource type="Script" uid="uid://csel4s0e4g5uf" path="res://scripts/components/PlayerController.cs" id="1_yysbb"] [ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="2_lgb3u"] +[ext_resource type="Script" uid="uid://btlm1f3l70il" path="res://scripts/components/PlatformMovementComponent.cs" id="2_o1ihh"] [ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="3_2srrh"] -[ext_resource type="Script" uid="uid://b3mrdvre1y567" path="res://scripts/components/ship_movement.gd" id="3_p4n66"] +[ext_resource type="Script" uid="uid://cty54itmnudfm" path="res://scripts/components/ShipMovementComponent.cs" id="3_ur2y5"] [ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="4_ccn81"] -[ext_resource type="Script" uid="uid://oxeqvxkgj87j" path="res://scripts/components/flip_player.gd" id="5_geu10"] [ext_resource type="Texture2D" uid="uid://0l454rfplmqg" path="res://sprites/MrBrick_base-sheet.png" id="5_yysbb"] -[ext_resource type="Script" uid="uid://qeu80jy4vmuf" path="res://scripts/components/score.gd" id="6_fowa2"] -[ext_resource type="Script" uid="uid://btfsq0bvtrx3t" path="res://scripts/components/health.gd" id="7_tqjk8"] -[ext_resource type="Script" uid="uid://dkpu3121y88oo" path="res://scripts/components/player_death.gd" id="8_1v23d"] [ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="8_6lsog"] -[ext_resource type="PackedScene" uid="uid://dyp4i4ru2j2jh" path="res://objects/fxs/explosion_fx.tscn" id="9_hwg11"] -[ext_resource type="Script" uid="uid://nogmyshjrv57" path="res://scripts/components/knockback.gd" id="9_rjyu4"] -[ext_resource type="Script" uid="uid://ulhswh4jjlc6" path="res://scripts/components/stomp_damage_component.gd" id="12_payr4"] -[ext_resource type="Script" uid="uid://dqmbvuutd5c3c" path="res://scripts/components/flashing_component.gd" id="13_hrtyn"] -[ext_resource type="Script" uid="uid://ijrli0x8ij8v" path="res://scripts/components/invulnerability_component.gd" id="14_jopig"] -[ext_resource type="Script" uid="uid://6ffxsx3gknhr" path="res://scripts/components/can_be_launched_component.gd" id="16_kemlv"] -[ext_resource type="Resource" uid="uid://dw5ee2lpeypnb" path="res://resources/skills/brick_throw.tres" id="16_smbir"] -[ext_resource type="Resource" uid="uid://2glvryih82t1" path="res://resources/skills/fire_brick.tres" id="17_6y5qu"] -[ext_resource type="Script" uid="uid://bpy6xtfm8l3hy" path="res://scripts/components/trigger_lever_component.gd" id="17_hglfj"] +[ext_resource type="Script" uid="uid://dy78ak8eykw6e" path="res://scripts/components/FlipComponent.cs" id="9_yysbb"] +[ext_resource type="Script" uid="uid://mnjg3p0aw1ow" path="res://scripts/components/CanPickUpComponent.cs" id="10_yysbb"] +[ext_resource type="Script" uid="uid://ccqb8kd5m0eh7" path="res://scripts/components/ScoreComponent.cs" id="11_o1ihh"] +[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="12_ur2y5"] +[ext_resource type="Script" uid="uid://byw1legrv1ep2" path="res://scripts/components/PlayerDeathComponent.cs" id="13_7til7"] +[ext_resource type="Script" uid="uid://cgfynrn68lp12" path="res://scripts/components/KnockbackComponent.cs" id="14_e5pae"] +[ext_resource type="Script" uid="uid://cecelixl41t3j" path="res://scripts/components/InvulnerabilityComponent.cs" id="15_xuhvf"] +[ext_resource type="Script" uid="uid://dvyd26ricriql" path="res://scripts/components/FlashingComponent.cs" id="16_uno3u"] +[ext_resource type="Script" uid="uid://dtg6115je7b5s" path="res://scripts/components/StompDamageComponent.cs" id="17_bl1gx"] +[ext_resource type="Script" uid="uid://di572axt0c3s8" path="res://scripts/SkillManager.cs" id="18_6lsog"] [ext_resource type="AudioStream" uid="uid://duj2q0rqytaxg" path="res://sfx/jump.wav" id="18_pysae"] -[ext_resource type="Resource" uid="uid://cx5fsbexblp60" path="res://resources/skills/ice_brick.tres" id="18_umfbf"] -[ext_resource type="Resource" uid="uid://cdp8sex36vdq2" path="res://resources/skills/explosive_brick.tres" id="19_5wjb7"] [ext_resource type="AudioStream" uid="uid://bmfn6p88gy575" path="res://sfx/player_hurt.wav" id="19_7anly"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="19_yysbb"] [ext_resource type="AudioStream" uid="uid://ycgtf6wj7mto" path="res://sfx/heal.wav" id="20_bptj5"] -[ext_resource type="Script" uid="uid://cjqe428jwip6b" path="res://scripts/skill_manager.gd" id="20_ppfy7"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="21_d0oiv"] -[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="22_vnsgd"] -[ext_resource type="Script" uid="uid://bjsyeo1n7bsri" path="res://scripts/components/skill_unlocker_component.gd" id="23_qsv2c"] +[ext_resource type="Resource" uid="uid://dw5ee2lpeypnb" path="res://resources/skills/brick_throw.tres" id="20_o1ihh"] +[ext_resource type="Resource" uid="uid://cdp8sex36vdq2" path="res://resources/skills/explosive_brick.tres" id="21_ur2y5"] +[ext_resource type="Resource" uid="uid://cr5lo4h8wm0jc" path="res://resources/skills/fire_brick.tres" id="22_7til7"] +[ext_resource type="Resource" uid="uid://ceakv6oqob6m7" path="res://resources/skills/ice_brick.tres" id="23_e5pae"] +[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="24_xuhvf"] +[ext_resource type="Script" uid="uid://dlh5xcv2sy82s" path="res://scripts/components/SkillUnlockerComponent.cs" id="25_yysbb"] +[ext_resource type="Script" uid="uid://bo506l4x0808e" path="res://scripts/components/HitComponent.cs" id="26_6n1ss"] +[ext_resource type="Script" uid="uid://cjcc7fia15wu3" path="res://scripts/components/CanBeLaunchedComponent.cs" id="27_oefns"] [ext_resource type="PackedScene" uid="uid://bg76mtpcmfm2j" path="res://objects/ui/charging_bar_layer.tscn" id="28_3f5nm"] +[ext_resource type="Script" uid="uid://cqau0810tjk4d" path="res://scripts/components/TriggerLeverComponent.cs" id="28_bnap0"] [ext_resource type="PackedScene" uid="uid://b12tppjkkqpt4" path="res://objects/fxs/hit_particles.tscn" id="28_jh5m0"] -[ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="29_jh5m0"] -[ext_resource type="Script" uid="uid://c1wtrgw0x77xo" path="res://scripts/components/platform_movement.gd" id="31_xoue7"] +[ext_resource type="Script" uid="uid://dr3uv0j7n75s" path="res://scripts/components/ShipShooterComponent.cs" id="30_usc1p"] [ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="32_x2b7c"] -[ext_resource type="Script" uid="uid://d1ctdx52gskv1" path="res://scripts/components/ship_shooter.gd" id="34_gwc8i"] [ext_resource type="PackedScene" uid="uid://dtem8jgcyoqar" path="res://objects/entities/green_laser.tscn" id="36_oxudy"] -[ext_resource type="Script" uid="uid://cfeoalic0mu2j" path="res://scripts/components/kill_player_out_of_screen.gd" id="37_oxudy"] -[ext_resource type="Script" uid="uid://d32kd83lf86iy" path="res://scripts/components/progressive_damage_component.gd" id="38_o1ihh"] +[ext_resource type="Script" uid="uid://diw6opv6yutgi" path="res://scripts/components/KillPlayerOutOfScreenComponent.cs" id="37_qec3q"] +[ext_resource type="Script" uid="uid://3qy7rm28q66a" path="res://scripts/components/ProgressiveDamageComponent.cs" id="38_dhjci"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_xoue7"] shader = ExtResource("2_lgb3u") @@ -83,33 +83,30 @@ scale_curve = SubResource("CurveTexture_xoue7") color = Color(0.764706, 0.443137, 0, 1) color_ramp = SubResource("GradientTexture1D_lgb3u") -[node name="Brick Player" type="CharacterBody2D" node_paths=PackedStringArray("ship_sprite") groups=["player"]] +[node name="Brick Player" type="CharacterBody2D" node_paths=PackedStringArray("ShipSprite") groups=["player"]] collision_layer = 4 collision_mask = 43 -script = ExtResource("1_8j4h4") -movement_types = { +script = ExtResource("1_yysbb") +MovementTypes = Dictionary[String, NodePath]({ "platform": NodePath("Movements/PlatformMovement"), "ship": NodePath("Movements/ShipMovement") -} -ship_sprite = NodePath("Graphics/Ship") +}) +ShipSprite = NodePath("Graphics/Ship") [node name="Movements" type="Node" parent="."] -[node name="PlatformMovement" type="Node" parent="Movements" node_paths=PackedStringArray("jump_sfx", "rotation_target", "body")] -script = ExtResource("31_xoue7") -jump_sfx = NodePath("../../sfx_jump") -rotation_target = NodePath("../../Graphics/Root/Base") -body = NodePath("../..") -type = "platform" +[node name="PlatformMovement" type="Node2D" parent="Movements" node_paths=PackedStringArray("JumpSfx", "RotationTarget", "Body")] +script = ExtResource("2_o1ihh") +JumpSfx = NodePath("../../sfx_jump") +RotationTarget = NodePath("../../Graphics/Root/Base") +Body = NodePath("../..") -[node name="ShipMovement" type="Node" parent="Movements" node_paths=PackedStringArray("body")] -script = ExtResource("3_p4n66") -max_speed = 360.0 -acceleration = 1200.0 -friction = 800.0 -body = NodePath("../..") -type = "ship" -metadata/_custom_type_script = "uid://b3mrdvre1y567" +[node name="ShipMovement" type="Node2D" parent="Movements" node_paths=PackedStringArray("Body")] +script = ExtResource("3_ur2y5") +MaxSpeed = 360.0 +Acceleration = 1200.0 +Friction = 800.0 +Body = NodePath("../..") [node name="Graphics" type="Node2D" parent="."] @@ -140,11 +137,10 @@ visible = false position = Vector2(0, 0.5) shape = SubResource("RectangleShape2D_hdsg1") -[node name="FlipPlayerComponent" type="Node2D" parent="." node_paths=PackedStringArray("eye_left", "eye_right", "platform_movement")] -script = ExtResource("5_geu10") -eye_left = NodePath("../Graphics/Root/Left Eye") -eye_right = NodePath("../Graphics/Root/Right Eye") -platform_movement = NodePath("../Movements/PlatformMovement") +[node name="FlipPlayerComponent" type="Node2D" parent="." node_paths=PackedStringArray("LeftEye", "RightEye")] +script = ExtResource("9_yysbb") +LeftEye = NodePath("../Graphics/Root/Left Eye") +RightEye = NodePath("../Graphics/Root/Right Eye") [node name="StompDamageArea" type="Area2D" parent="."] collision_layer = 0 @@ -156,69 +152,69 @@ position = Vector2(0, 1) shape = SubResource("RectangleShape2D_vad0t") [node name="CanPickUpComponent" type="Node" parent="."] +script = ExtResource("10_yysbb") [node name="ScoreComponent" type="Node" parent="."] -script = ExtResource("6_fowa2") +script = ExtResource("11_o1ihh") -[node name="HealthComponent" type="Node" parent="." node_paths=PackedStringArray("hurt_fx", "heal_fx")] -script = ExtResource("7_tqjk8") -hurt_fx = NodePath("../sfx_hurt") -heal_fx = NodePath("../sfx_heal") +[node name="HealthComponent" type="Node2D" parent="." node_paths=PackedStringArray("HurtSfx", "HealSfx")] +script = ExtResource("12_ur2y5") +HurtSfx = NodePath("../sfx_hurt") +HealSfx = NodePath("../sfx_heal") -[node name="PlayerDeathComponent" type="Node2D" parent="."] +[node name="PlayerDeathComponent" type="Node2D" parent="." node_paths=PackedStringArray("DeathSfx", "HealthComponent")] process_mode = 3 -script = ExtResource("8_1v23d") -death_effect = ExtResource("9_hwg11") +script = ExtResource("13_7til7") +DeathSfx = NodePath("../sfx_hurt") +HealthComponent = NodePath("../HealthComponent") -[node name="KnockbackComponent" type="Node" parent="." node_paths=PackedStringArray("character_body")] -script = ExtResource("9_rjyu4") -character_body = NodePath("..") -knockback_force = 1500.0 +[node name="KnockbackComponent" type="Node" parent="." node_paths=PackedStringArray("Body", "HealthComponent")] +script = ExtResource("14_e5pae") +Body = NodePath("..") +KnockbackForce = 1250.0 +HealthComponent = NodePath("../HealthComponent") -[node name="ThrowTimer" type="Timer" parent="."] +[node name="InvulnerabilityComponent" type="Node" parent="." node_paths=PackedStringArray("FlashingComponent")] +script = ExtResource("15_xuhvf") +FlashingComponent = NodePath("../FlashingComponent Base") -[node name="InvulnerabilityComponent" type="Node" parent="." node_paths=PackedStringArray("flashing_component")] -script = ExtResource("14_jopig") -flashing_component = NodePath("../FlashingComponent Base") +[node name="FlashingComponent Base" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")] +script = ExtResource("16_uno3u") +Sprite = NodePath("../Graphics/Root/Base") +FlashDuration = 1.0 +HealthComponent = NodePath("../HealthComponent") -[node name="FlashingComponent Base" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] -script = ExtResource("13_hrtyn") -sprite = NodePath("../Graphics/Root/Base") -flash_duration = 1.0 -health_component = NodePath("../HealthComponent") +[node name="FlashingComponent LEye" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")] +script = ExtResource("16_uno3u") +Sprite = NodePath("../Graphics/Root/Left Eye") +FlashDuration = 1.0 +HealthComponent = NodePath("../HealthComponent") -[node name="FlashingComponent LEye" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] -script = ExtResource("13_hrtyn") -sprite = NodePath("../Graphics/Root/Left Eye") -flash_duration = 1.0 -health_component = NodePath("../HealthComponent") +[node name="FlashingComponent REye" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")] +script = ExtResource("16_uno3u") +Sprite = NodePath("../Graphics/Root/Right Eye") +FlashDuration = 1.0 +HealthComponent = NodePath("../HealthComponent") -[node name="FlashingComponent REye" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] -script = ExtResource("13_hrtyn") -sprite = NodePath("../Graphics/Root/Right Eye") -flash_duration = 1.0 -health_component = NodePath("../HealthComponent") - -[node name="StompDamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "root")] -script = ExtResource("12_payr4") -damage = 4.0 -area2d = NodePath("../StompDamageArea") -root = NodePath("..") +[node name="StompDamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "Root")] +script = ExtResource("17_bl1gx") +Damage = 4.0 +Area = NodePath("../StompDamageArea") +Root = NodePath("..") [node name="SkillManager" type="Node" parent="."] -script = ExtResource("20_ppfy7") -available_skills = Array[ExtResource("21_d0oiv")]([ExtResource("22_vnsgd"), ExtResource("17_6y5qu"), ExtResource("16_smbir"), ExtResource("18_umfbf"), ExtResource("19_5wjb7")]) +script = ExtResource("18_6lsog") +AvailableSkills = Array[ExtResource("19_yysbb")]([ExtResource("20_o1ihh"), ExtResource("21_ur2y5"), ExtResource("22_7til7"), ExtResource("23_e5pae"), ExtResource("24_xuhvf")]) -[node name="SkillUnlockerComponent" type="Node" parent="." node_paths=PackedStringArray("skill_manager")] -script = ExtResource("23_qsv2c") -skill_manager = NodePath("../SkillManager") +[node name="SkillUnlockerComponent" type="Node" parent="." node_paths=PackedStringArray("SkillManager")] +script = ExtResource("25_yysbb") +SkillManager = NodePath("../SkillManager") -[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component", "hit_fx")] -script = ExtResource("29_jh5m0") -sprite = NodePath("../Graphics/Root/Base") -health_component = NodePath("../HealthComponent") -hit_fx = NodePath("../HitParticles") -metadata/_custom_type_script = "uid://ceq8n7yw7qxpi" +[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Health", "HitFx")] +script = ExtResource("26_6n1ss") +Sprite = NodePath("../Graphics/Root/Base") +Health = NodePath("../HealthComponent") +HitFx = NodePath("../HitParticles") [node name="MagneticArea" type="Area2D" parent="."] collision_layer = 0 @@ -229,10 +225,10 @@ visible = false shape = SubResource("CircleShape2D_ps31c") [node name="CanBeLaunchedComponent" type="Node" parent="."] -script = ExtResource("16_kemlv") +script = ExtResource("27_oefns") [node name="TriggerLeverComponent" type="Node" parent="."] -script = ExtResource("17_hglfj") +script = ExtResource("28_bnap0") [node name="sfx_jump" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("18_pysae") @@ -250,21 +246,21 @@ bus = &"sfx" stream = ExtResource("32_x2b7c") bus = &"sfx" -[node name="ChargingBarLayer" parent="." instance=ExtResource("28_3f5nm")] +[node name="ChargingBarLayer" parent="." node_paths=PackedStringArray("_skillManager") instance=ExtResource("28_3f5nm")] offset_left = -17.0 offset_top = -30.0 offset_right = 23.0 offset_bottom = -20.0 +_skillManager = NodePath("../SkillManager") [node name="HitParticles" parent="." instance=ExtResource("28_jh5m0")] process_material = SubResource("ParticleProcessMaterial_lgb3u") -[node name="ShipShooter" type="Node" parent="." node_paths=PackedStringArray("bullet_spawn", "shoot_sfx")] -script = ExtResource("34_gwc8i") -bullet_scene = ExtResource("36_oxudy") -bullet_spawn = NodePath("../Ship shoot spawn") -shoot_sfx = NodePath("../sfx_shoot") -metadata/_custom_type_script = "uid://d1ctdx52gskv1" +[node name="ShipShooter" type="Node" parent="." node_paths=PackedStringArray("BulletSpawn", "ShootSfx")] +script = ExtResource("30_usc1p") +BulletScene = ExtResource("36_oxudy") +BulletSpawn = NodePath("../Ship shoot spawn") +ShootSfx = NodePath("../sfx_shoot") [node name="Ship shoot spawn" type="Marker2D" parent="."] position = Vector2(17, 5) @@ -274,18 +270,14 @@ gizmo_extents = 1.0 position = Vector2(0, 3) scale = Vector2(0.8, 1.9) -[node name="KillPlayerOutOfScreen" type="Node" parent="." node_paths=PackedStringArray("screen_notifier", "health_component")] -script = ExtResource("37_oxudy") -screen_notifier = NodePath("../VisibleOnScreenNotifier2D") -health_component = NodePath("../HealthComponent") +[node name="KillPlayerOutOfScreen" type="Node" parent="." node_paths=PackedStringArray("ScreenNotifier", "HealthComponent")] +script = ExtResource("37_qec3q") +ScreenNotifier = NodePath("../VisibleOnScreenNotifier2D") +HealthComponent = NodePath("../HealthComponent") -[node name="ProgressiveDamageComponent" type="Node" parent="." node_paths=PackedStringArray("health_component", "sprite", "platform_movement")] -script = ExtResource("38_o1ihh") -health_component = NodePath("../HealthComponent") -sprite = NodePath("../Graphics/Root/Base") -platform_movement = NodePath("../Movements/PlatformMovement") -min_jump_height = 100.0 -jump_reduction_percentage = 0.15 - -[connection signal="on_death" from="HealthComponent" to="PlayerDeathComponent" method="_on_health_component_on_death"] -[connection signal="on_health_change" from="HealthComponent" to="KnockbackComponent" method="_on_health_component_on_health_change"] +[node name="ProgressiveDamageComponent" type="Node" parent="." node_paths=PackedStringArray("HealthComponent", "Sprite", "PlatformMovement")] +process_mode = 4 +script = ExtResource("38_dhjci") +HealthComponent = NodePath("../HealthComponent") +Sprite = NodePath("../Graphics/Root/Base") +PlatformMovement = NodePath("../Movements/PlatformMovement") diff --git a/objects/entities/bullet.tscn b/objects/entities/bullet.tscn index d786c6f..e5f87ce 100644 --- a/objects/entities/bullet.tscn +++ b/objects/entities/bullet.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=10 format=3 uid="uid://bhc7y4xugu4q7"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_w543f"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="2_ll0xw"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="3_keogl"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="4_lqg4f"] -[ext_resource type="Script" uid="uid://bvsgg8lu0a8m6" path="res://scripts/components/lifetime_component.gd" id="5_qsc5m"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="6_6th6w"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="7_e0mqp"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_6th6w"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="3_e0mqp"] +[ext_resource type="Script" uid="uid://oyf25mpc5etr" path="res://scripts/components/LifetimeComponent.cs" id="4_r4wv6"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="5_7bijt"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="6_lycw2"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="7_2aweg"] [ext_resource type="PackedScene" uid="uid://c1iorglk708g0" path="res://objects/fxs/terrain_hit_fx.tscn" id="8_6th6w"] [sub_resource type="CircleShape2D" id="CircleShape2D_txsw8"] @@ -30,42 +30,37 @@ frame = 79 position = Vector2(2.38419e-07, 2.38419e-07) scale = Vector2(0.4, 0.4) -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("2_ll0xw") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "DamageTimer")] +script = ExtResource("2_6th6w") +Area = NodePath("..") +DamageTimer = NodePath("../Timer") [node name="Timer" type="Timer" parent="."] wait_time = 5.0 autostart = true -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] -script = ExtResource("4_lqg4f") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] +script = ExtResource("3_e0mqp") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") -[node name="LifetimeComponent" type="Node" parent="." node_paths=PackedStringArray("root", "timer")] -script = ExtResource("5_qsc5m") -root = NodePath("..") -life_time = 10.0 -timer = NodePath("../Timer") +[node name="LifetimeComponent" type="Node" parent="."] +script = ExtResource("4_r4wv6") +LifeTime = 10.0 -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("6_6th6w") -root = NodePath("..") -speed = 120.0 +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("5_7bijt") +Speed = 120.0 metadata/_custom_type_script = "uid://873un8agkyja" -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] -script = ExtResource("7_e0mqp") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("6_lycw2") +LaunchComponent = NodePath("../LaunchComponent") -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d", "hit_terrain_fx", "bullet_sprite")] -script = ExtResource("3_keogl") -root = NodePath("..") -area2d = NodePath("..") -hit_terrain_fx = NodePath("../TerrainHitFX") -bullet_sprite = NodePath("../Sprite2D") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "TerrainHitFx", "BulletSprite")] +script = ExtResource("7_2aweg") +Area = NodePath("..") +TerrainHitFx = NodePath("../TerrainHitFX") +BulletSprite = NodePath("../Sprite2D") metadata/_custom_type_script = "uid://cdnwrn8v05qhi" [node name="TerrainHitFX" parent="." instance=ExtResource("8_6th6w")] diff --git a/objects/entities/cage.tscn b/objects/entities/cage.tscn index df08133..ff6dbab 100644 --- a/objects/entities/cage.tscn +++ b/objects/entities/cage.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=5 format=3 uid="uid://to2xnqev0pu1"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_5poh3"] -[ext_resource type="Script" uid="uid://ddplvyjqguxtl" path="res://scripts/components/cage_component.gd" id="2_unomj"] +[ext_resource type="Script" uid="uid://dojn0gw8hsv02" path="res://scripts/components/CageComponent.cs" id="2_in1f7"] [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_aivtb"] texture = ExtResource("1_5poh3") @@ -23,7 +23,6 @@ position = Vector2(20, 4) tile_map_data = PackedByteArray(0, 0, 255, 255, 255, 255, 0, 0, 2, 0, 9, 0, 0, 0) tile_set = SubResource("TileSet_67qt2") -[node name="CageComponent" type="Node" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("2_unomj") -root = NodePath("..") -should_free = false +[node name="CageComponent" type="Node" parent="."] +script = ExtResource("2_in1f7") +ShouldFree = false diff --git a/objects/entities/cannon.tscn b/objects/entities/cannon.tscn index 1dbda6f..80129e6 100644 --- a/objects/entities/cannon.tscn +++ b/objects/entities/cannon.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://dstko446qydsc"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_6gptm"] -[ext_resource type="Script" uid="uid://d01dmoafptl2p" path="res://scripts/components/destroyable_component.gd" id="2_2tib2"] -[ext_resource type="Script" uid="uid://btfsq0bvtrx3t" path="res://scripts/components/health.gd" id="3_vevhj"] +[ext_resource type="Script" uid="uid://ctfrbj52ejay4" path="res://scripts/components/DestroyableComponent.cs" id="2_q37h7"] +[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="3_bhwy3"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_j5sus"] size = Vector2(16, 16) @@ -15,13 +15,12 @@ hframes = 12 vframes = 12 frame = 42 -[node name="DestroyableComponent" type="Node" parent="." node_paths=PackedStringArray("root", "health_component")] -script = ExtResource("2_2tib2") -root = NodePath("..") -health_component = NodePath("../HealthComponent") +[node name="DestroyableComponent" type="Node" parent="." node_paths=PackedStringArray("Health")] +script = ExtResource("2_q37h7") +Health = NodePath("../HealthComponent") -[node name="HealthComponent" type="Node" parent="."] -script = ExtResource("3_vevhj") +[node name="HealthComponent" type="Node2D" parent="."] +script = ExtResource("3_bhwy3") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_j5sus") diff --git a/objects/entities/cannon_ray_down.tscn b/objects/entities/cannon_ray_down.tscn index 0055434..11e11cb 100644 --- a/objects/entities/cannon_ray_down.tscn +++ b/objects/entities/cannon_ray_down.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://dfwpha0d18dmn"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_rwgpm"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="2_cprue"] -[ext_resource type="Script" uid="uid://bejv75mi8npj0" path="res://scripts/components/beam_component.gd" id="3_jlh0s"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_hrj61"] +[ext_resource type="Script" uid="uid://df1llrbm80e02" path="res://scripts/components/BeamComponent.cs" id="3_hrj61"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ptfn7"] size = Vector2(8, 16) @@ -20,13 +20,14 @@ region_rect = Rect2(176, 64, 16, 16) [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_ptfn7") -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("2_cprue") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("2_hrj61") +Area = NodePath("..") -[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("root", "sprite2d", "collision_shape")] +[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("Root", "Sprite", "CollisionShape")] position = Vector2(0, -8) -script = ExtResource("3_jlh0s") -root = NodePath("..") -sprite2d = NodePath("../Sprite2D") -collision_shape = NodePath("../CollisionShape2D") +script = ExtResource("3_hrj61") +ExpansionSpeed = 16.0 +Root = NodePath(".") +Sprite = NodePath("../Sprite2D") +CollisionShape = NodePath("../CollisionShape2D") diff --git a/objects/entities/cannon_ray_left.tscn b/objects/entities/cannon_ray_left.tscn index 3ab4acb..bd059cd 100644 --- a/objects/entities/cannon_ray_left.tscn +++ b/objects/entities/cannon_ray_left.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://d3lt4rhxduv44"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_l5x2w"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="2_mxy0q"] -[ext_resource type="Script" uid="uid://bejv75mi8npj0" path="res://scripts/components/beam_component.gd" id="3_mcbof"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_0kbpg"] +[ext_resource type="Script" uid="uid://df1llrbm80e02" path="res://scripts/components/BeamComponent.cs" id="3_0kbpg"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ptfn7"] size = Vector2(16, 8) @@ -22,14 +22,14 @@ region_filter_clip_enabled = true [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_ptfn7") -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("2_mxy0q") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("2_0kbpg") +Area = NodePath("..") -[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("root", "sprite2d", "collision_shape")] +[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("Root", "Sprite", "CollisionShape")] position = Vector2(8, 0) -script = ExtResource("3_mcbof") -direction = Vector2(-1, 0) -root = NodePath("..") -sprite2d = NodePath("../Sprite2D") -collision_shape = NodePath("../CollisionShape2D") +script = ExtResource("3_0kbpg") +ExpansionSpeed = 16.0 +Root = NodePath("..") +Sprite = NodePath("../Sprite2D") +CollisionShape = NodePath("../CollisionShape2D") diff --git a/objects/entities/chaser.tscn b/objects/entities/chaser.tscn index 05f7b90..ec21bf7 100644 --- a/objects/entities/chaser.tscn +++ b/objects/entities/chaser.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://bon6raeddf3tu"] -[ext_resource type="Script" uid="uid://cf4li7whw5old" path="res://scripts/components/chase_level_component.gd" id="1_ico16"] +[ext_resource type="Script" uid="uid://dnpj72mfi1ywl" path="res://scripts/components/ChaseLevelComponent.cs" id="1_ico16"] [node name="Chaser" type="Node2D"] diff --git a/objects/entities/child.tscn b/objects/entities/child.tscn index f1c81ef..5e87e47 100644 --- a/objects/entities/child.tscn +++ b/objects/entities/child.tscn @@ -5,14 +5,14 @@ [ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="3_k41y7"] [ext_resource type="Texture2D" uid="uid://8h05rd26t66q" path="res://sprites/lollipop.png" id="4_vq1oq"] [ext_resource type="Texture2D" uid="uid://dpbpjffbdbovp" path="res://sprites/cap.png" id="5_m7x6t"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="5_wc3ym"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="6_rgif4"] [ext_resource type="Resource" uid="uid://b6apusc0jmi3x" path="res://resources/collectables/child.tres" id="6_vmvuo"] [ext_resource type="AudioStream" uid="uid://drfr0wlgkhkdq" path="res://sfx/child_pickup.wav" id="7_j8eyh"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_dj5g5"] size = Vector2(10, 20) -[node name="Child" type="Area2D"] +[node name="Child" type="Area2D" groups=["Collectables"]] collision_layer = 0 collision_mask = 4 @@ -45,12 +45,12 @@ texture = ExtResource("4_vq1oq") position = Vector2(4, -18) texture = ExtResource("5_m7x6t") -[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape", "sfx")] -script = ExtResource("5_wc3ym") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("6_vmvuo") -sfx = NodePath("../sfx_pickup") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")] +script = ExtResource("6_rgif4") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("6_vmvuo") +Sfx = NodePath("../sfx_pickup") [node name="sfx_pickup" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("7_j8eyh") diff --git a/objects/entities/coin.tscn b/objects/entities/coin.tscn index 1dc80b1..3cc7a98 100644 --- a/objects/entities/coin.tscn +++ b/objects/entities/coin.tscn @@ -1,15 +1,15 @@ [gd_scene load_steps=7 format=3 uid="uid://54w4wisfj8v8"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_epuj5"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="2_y0k47"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_gxix7"] [ext_resource type="Resource" uid="uid://vql535ckoeqm" path="res://resources/collectables/coin.tres" id="3_fm2fq"] -[ext_resource type="Script" uid="uid://bg75hnr3q6grk" path="res://scripts/components/fade_away.gd" id="4_fx1h2"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="4_gxix7"] [ext_resource type="AudioStream" uid="uid://dpyr80hk4kebc" path="res://sfx/pickup_coin_1.wav" id="5_4jc2c"] [sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"] radius = 9.0 -[node name="Coin" type="Area2D"] +[node name="Coin" type="Area2D" groups=["coins"]] collision_layer = 2 collision_mask = 4 @@ -22,19 +22,18 @@ hframes = 12 vframes = 12 frame = 51 -[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape", "sfx") groups=["coins"]] -script = ExtResource("2_y0k47") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("3_fm2fq") -sfx = NodePath("../sfx") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")] +script = ExtResource("2_gxix7") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("3_fm2fq") +Sfx = NodePath("../sfx") -[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")] -script = ExtResource("4_fx1h2") -sprite2d = NodePath("../Sprite2D") -fade_duration = 0.4 -root = NodePath("..") -area2d = NodePath("..") +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("4_gxix7") +Sprite = NodePath("../Sprite2D") +FadeDuration = 0.4 +Area = NodePath("..") [node name="sfx" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("5_4jc2c") diff --git a/objects/entities/collapsing_block.tscn b/objects/entities/collapsing_block.tscn index 6e8b5d6..fe7b665 100644 --- a/objects/entities/collapsing_block.tscn +++ b/objects/entities/collapsing_block.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=5 format=3 uid="uid://c40r76qqacqie"] [ext_resource type="Texture2D" uid="uid://cw42lvnqxubq2" path="res://sprites/PS_Tileset_10_nes.png" id="1_jmsgb"] -[ext_resource type="Script" uid="uid://r0a6xjicrh74" path="res://scripts/components/collapsable.gd" id="2_cs5vh"] +[ext_resource type="Script" uid="uid://xqhrb1c7f6y4" path="res://scripts/components/CollapsableComponent.cs" id="2_jmsgb"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_l44jt"] size = Vector2(16, 16) @@ -32,16 +32,15 @@ wait_time = 0.5 [node name="ResetTimer" type="Timer" parent="."] -[node name="CollapsableComponent" type="Node" parent="." node_paths=PackedStringArray("to_collapse_timer", "reset_timer", "sprite2d", "collision_shape")] -script = ExtResource("2_cs5vh") -to_collapse_timer = NodePath("../ToCollapseTimer") -reset_timer = NodePath("../ResetTimer") -sprite2d = NodePath("../Sprite2D") -collision_shape = NodePath("../StaticCollisionShape2D") -collapse_time = 0.75 -reset_time = 3.0 +[node name="CollapsableComponent" type="Node" parent="." node_paths=PackedStringArray("ToCollapseTimer", "ResetTimer", "Sprite2D", "CollisionShape")] +script = ExtResource("2_jmsgb") +ToCollapseTimer = NodePath("../ToCollapseTimer") +ResetTimer = NodePath("../ResetTimer") +Sprite2D = NodePath("../Sprite2D") +CollisionShape = NodePath("../StaticCollisionShape2D") +ResetTime = 1.0 -[connection signal="body_entered" from="Collapsable detector" to="CollapsableComponent" method="_on_collapsable_detector_body_entered"] -[connection signal="body_exited" from="Collapsable detector" to="CollapsableComponent" method="_on_collapsable_detector_body_exited"] +[connection signal="body_entered" from="Collapsable detector" to="CollapsableComponent" method="OnCollapsableDetectorBodyEntered"] +[connection signal="body_exited" from="Collapsable detector" to="CollapsableComponent" method="OnCollapsableDetectorBodyExited"] [connection signal="timeout" from="ToCollapseTimer" to="CollapsableComponent" method="_on_to_collapse_timer_timeout"] [connection signal="timeout" from="ResetTimer" to="CollapsableComponent" method="_on_reset_timer_timeout"] diff --git a/objects/entities/collapsing_bridge.tscn b/objects/entities/collapsing_bridge.tscn index 28e99b8..3f44887 100644 --- a/objects/entities/collapsing_bridge.tscn +++ b/objects/entities/collapsing_bridge.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=5 format=3 uid="uid://ct8fim6mduyl3"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_rka1x"] -[ext_resource type="Script" uid="uid://r0a6xjicrh74" path="res://scripts/components/collapsable.gd" id="2_3vvum"] +[ext_resource type="Script" uid="uid://xqhrb1c7f6y4" path="res://scripts/components/CollapsableComponent.cs" id="2_w33sr"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_l44jt"] size = Vector2(16, 4) @@ -34,16 +34,16 @@ wait_time = 0.5 [node name="ResetTimer" type="Timer" parent="."] -[node name="CollapsableComponent" type="Node" parent="." node_paths=PackedStringArray("to_collapse_timer", "reset_timer", "sprite2d", "collision_shape")] -script = ExtResource("2_3vvum") -to_collapse_timer = NodePath("../ToCollapseTimer") -reset_timer = NodePath("../ResetTimer") -sprite2d = NodePath("../Sprite2D") -collision_shape = NodePath("../StaticCollisionShape2D") -collapse_time = 0.75 -reset_time = 3.0 +[node name="CollapsableComponent" type="Node" parent="." node_paths=PackedStringArray("ToCollapseTimer", "ResetTimer", "Sprite2D", "CollisionShape")] +script = ExtResource("2_w33sr") +ToCollapseTimer = NodePath("../ToCollapseTimer") +ResetTimer = NodePath("../ResetTimer") +Sprite2D = NodePath("../Sprite2D") +CollisionShape = NodePath("../StaticCollisionShape2D") +CollapseTime = 0.75 +ResetTime = 3.0 -[connection signal="body_entered" from="Collapsable detector" to="CollapsableComponent" method="_on_collapsable_detector_body_entered"] -[connection signal="body_exited" from="Collapsable detector" to="CollapsableComponent" method="_on_collapsable_detector_body_exited"] +[connection signal="body_entered" from="Collapsable detector" to="CollapsableComponent" method="OnCollapsableDetectorBodyEntered"] +[connection signal="body_exited" from="Collapsable detector" to="CollapsableComponent" method="OnCollapsableDetectorBodyExited"] [connection signal="timeout" from="ToCollapseTimer" to="CollapsableComponent" method="_on_to_collapse_timer_timeout"] [connection signal="timeout" from="ResetTimer" to="CollapsableComponent" method="_on_reset_timer_timeout"] diff --git a/objects/entities/enemy.tscn b/objects/entities/enemy.tscn index d679692..8be4216 100644 --- a/objects/entities/enemy.tscn +++ b/objects/entities/enemy.tscn @@ -1,18 +1,19 @@ -[gd_scene load_steps=26 format=3 uid="uid://bwdlmualj6xbw"] +[gd_scene load_steps=27 format=3 uid="uid://bwdlmualj6xbw"] [ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="1_ep4yr"] [ext_resource type="Texture2D" uid="uid://cu72810eyk4dx" path="res://sprites/enemy-robot.png" id="2_hjtwe"] -[ext_resource type="Script" uid="uid://btfsq0bvtrx3t" path="res://scripts/components/health.gd" id="2_o170m"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="4_4eajk"] -[ext_resource type="Script" uid="uid://dmqpif5qhvri3" path="res://scripts/components/side_to_side_movement.gd" id="4_gbsq8"] -[ext_resource type="Script" uid="uid://b37gqnycj6rtk" path="res://scripts/components/periodic_shooting.gd" id="5_m03v0"] -[ext_resource type="Script" uid="uid://dqm371fysuk7i" path="res://scripts/components/enemy_death.gd" id="6_6p3gr"] -[ext_resource type="Script" uid="uid://dqmbvuutd5c3c" path="res://scripts/components/flashing_component.gd" id="7_xsaiy"] -[ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="9_0qjr4"] -[ext_resource type="Script" uid="uid://c07d50s20rl8s" path="res://scripts/components/status_effect_component.gd" id="10_5i27q"] -[ext_resource type="Script" uid="uid://qi2irprbqru2" path="res://scripts/components/fire_effect_component.gd" id="10_ej8vg"] -[ext_resource type="Script" uid="uid://dhj4qtwcqmqkj" path="res://scripts/components/ice_effect_component.gd" id="11_pq0k7"] +[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="3_pxaaa"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="4_xku20"] +[ext_resource type="Script" uid="uid://d2hrr8fruho1d" path="res://scripts/components/SideToSideMovementComponent.cs" id="5_5lji2"] +[ext_resource type="Script" uid="uid://bnaxy8cw3wrko" path="res://scripts/components/PeriodicShootingComponent.cs" id="6_lgbyy"] +[ext_resource type="PackedScene" uid="uid://bhc7y4xugu4q7" path="res://objects/entities/bullet.tscn" id="7_r48kf"] +[ext_resource type="Script" uid="uid://cfdugoeduudar" path="res://scripts/components/EnemyDeathComponent.cs" id="8_pxaaa"] +[ext_resource type="Script" uid="uid://dvyd26ricriql" path="res://scripts/components/FlashingComponent.cs" id="9_xku20"] +[ext_resource type="Script" uid="uid://bo506l4x0808e" path="res://scripts/components/HitComponent.cs" id="10_5lji2"] +[ext_resource type="Script" uid="uid://t8rsvwdwt8ea" path="res://scripts/components/StatusEffectComponent.cs" id="11_lgbyy"] +[ext_resource type="Script" uid="uid://cxuig4xh8nfov" path="res://scripts/components/FireEffectComponent.cs" id="12_pxaaa"] [ext_resource type="AudioStream" uid="uid://b3tsqhr06pbrs" path="res://sfx/enemy_hurt.wav" id="13_u4k3d"] +[ext_resource type="Script" uid="uid://d1388lhp2gpgr" path="res://scripts/components/IceEffectComponent.cs" id="13_xku20"] [ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="14_tdjks"] [ext_resource type="PackedScene" uid="uid://dx80ivlvuuew4" path="res://objects/fxs/fire_fx.tscn" id="15_mc6rj"] [ext_resource type="PackedScene" uid="uid://ck6nml06tm6ue" path="res://objects/fxs/ice_fx.tscn" id="16_68hnm"] @@ -77,38 +78,37 @@ position = Vector2(16, 7) [node name="left bullet spawn" type="Node2D" parent="Sprite2D"] position = Vector2(-17, 7) -[node name="HealthComponent" type="Node" parent="." node_paths=PackedStringArray("hurt_fx")] -script = ExtResource("2_o170m") -hurt_fx = NodePath("../sfx_hurt") +[node name="HealthComponent" type="Node2D" parent="." node_paths=PackedStringArray("HurtSfx")] +script = ExtResource("3_pxaaa") +HurtSfx = NodePath("../sfx_hurt") -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("4_4eajk") -area2d = NodePath("../Hitbox") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("4_xku20") +Area = NodePath("../Hitbox") -[node name="SideToSideMovement" type="Node" parent="." node_paths=PackedStringArray("root", "sprite2d", "left_ray", "right_ray", "left_wall_ray", "right_wall_ray")] -script = ExtResource("4_gbsq8") -root = NodePath("..") -sprite2d = NodePath("../Sprite2D") -speed = 60.0 -wait_time = 0.5 -left_ray = NodePath("../Left Ray") -right_ray = NodePath("../Right Ray") -left_wall_ray = NodePath("../Left Wall Ray") -right_wall_ray = NodePath("../Right Wall Ray") +[node name="SideToSideMovement" type="Node" parent="." node_paths=PackedStringArray("Sprite", "LeftRay", "RightRay", "LeftWallRay", "RightWallRay")] +script = ExtResource("5_5lji2") +Sprite = NodePath("../Sprite2D") +Speed = 60.0 +WaitTime = 0.5 +LeftRay = NodePath("../Left Ray") +RightRay = NodePath("../Right Ray") +LeftWallRay = NodePath("../Left Wall Ray") +RightWallRay = NodePath("../Right Wall Ray") -[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("side_to_side_movement", "root", "bullet_spawn_right", "bullet_spawn_left")] -script = ExtResource("5_m03v0") -side_to_side_movement = NodePath("../SideToSideMovement") -root = NodePath("..") -bullet_spawn_right = NodePath("../Sprite2D/right bullet spawn") -bullet_spawn_left = NodePath("../Sprite2D/left bullet spawn") +[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("SideToSideMovement", "BulletSpawnRight", "BulletSpawnLeft")] +script = ExtResource("6_lgbyy") +BulletScene = ExtResource("7_r48kf") +SideToSideMovement = NodePath("../SideToSideMovement") +BulletSpawnRight = NodePath("../Sprite2D/right bullet spawn") +BulletSpawnLeft = NodePath("../Sprite2D/left bullet spawn") +ShootingIntervalVariation = 0.1 -[node name="EnemyDeathComponent" type="Node" parent="." node_paths=PackedStringArray("root", "collision_shape_2d", "health_component")] -script = ExtResource("6_6p3gr") -root = NodePath("..") -tween_duration = 0.1 -collision_shape_2d = NodePath("../Hitbox/CollisionShape2D") -health_component = NodePath("../HealthComponent") +[node name="EnemyDeathComponent" type="Node" parent="." node_paths=PackedStringArray("CollisionShape", "Health")] +script = ExtResource("8_pxaaa") +TweenDuration = 0.1 +CollisionShape = NodePath("../Hitbox/CollisionShape2D") +Health = NodePath("../HealthComponent") [node name="Hitbox" type="Area2D" parent="."] collision_layer = 8 @@ -136,33 +136,32 @@ target_position = Vector2(0, 8) position = Vector2(16, 0) target_position = Vector2(8, 0) -[node name="FlashingComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] +[node name="FlashingComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")] process_mode = 3 -script = ExtResource("7_xsaiy") -sprite = NodePath("../Sprite2D") -health_component = NodePath("../HealthComponent") +script = ExtResource("9_xku20") +Sprite = NodePath("../Sprite2D") +HealthComponent = NodePath("../HealthComponent") -[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component", "hit_fx")] -script = ExtResource("9_0qjr4") -sprite = NodePath("../Sprite2D") -health_component = NodePath("../HealthComponent") -hit_fx = NodePath("../HitParticles") +[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Health", "HitFx")] +script = ExtResource("10_5lji2") +Sprite = NodePath("../Sprite2D") +Health = NodePath("../HealthComponent") +HitFx = NodePath("../HitParticles") [node name="StatusEffectComponent" type="Node" parent="."] -script = ExtResource("10_5i27q") +script = ExtResource("11_lgbyy") -[node name="FireEffectComponent" type="Node" parent="." node_paths=PackedStringArray("health_component", "status_effect_component", "root", "fire_fx")] -script = ExtResource("10_ej8vg") -health_component = NodePath("../HealthComponent") -status_effect_component = NodePath("../StatusEffectComponent") -root = NodePath("..") -fire_fx = NodePath("../FireFX") +[node name="FireEffectComponent" type="Node" parent="." node_paths=PackedStringArray("Health", "StatusEffectComponent", "FireFX")] +script = ExtResource("12_pxaaa") +Health = NodePath("../HealthComponent") +StatusEffectComponent = NodePath("../StatusEffectComponent") +FireFX = NodePath("../FireFX") -[node name="IceEffectComponent" type="Node" parent="." node_paths=PackedStringArray("status_effect_component", "ice_fx")] -script = ExtResource("11_pq0k7") -components_to_disable = [NodePath("../SideToSideMovement"), NodePath("../PeriodicShootingComponent"), NodePath("../DamageComponent")] -status_effect_component = NodePath("../StatusEffectComponent") -ice_fx = NodePath("../Ice FX") +[node name="IceEffectComponent" type="Node" parent="." node_paths=PackedStringArray("ComponentsToDisable", "StatusEffectComponent", "IceFx")] +script = ExtResource("13_xku20") +ComponentsToDisable = [NodePath("../SideToSideMovement"), NodePath("../PeriodicShootingComponent"), NodePath("../DamageComponent")] +StatusEffectComponent = NodePath("../StatusEffectComponent") +IceFx = NodePath("../Ice FX") [node name="sfx_hurt" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("13_u4k3d") diff --git a/objects/entities/exit_level.tscn b/objects/entities/exit_level.tscn index 08448b6..f0e8963 100644 --- a/objects/entities/exit_level.tscn +++ b/objects/entities/exit_level.tscn @@ -1,9 +1,9 @@ [gd_scene load_steps=6 format=3 uid="uid://12jnkdygpxwc"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_agxwm"] -[ext_resource type="Script" uid="uid://cmh8k0rdsyh7j" path="res://scripts/components/requirement_component.gd" id="2_iysc1"] -[ext_resource type="Script" uid="uid://c8xhgkg8gcqu6" path="res://scripts/components/unlock_on_requirement_component.gd" id="3_5ktpq"] -[ext_resource type="Script" uid="uid://bwamqffvpa452" path="res://scripts/components/exit_door_component.gd" id="4_4jgt0"] +[ext_resource type="Script" uid="uid://dnh0mekg2vqxi" path="res://scripts/components/RequirementComponent.cs" id="2_ed7mh"] +[ext_resource type="Script" uid="uid://gwr4eajx8j50" path="res://scripts/components/UnlockOnRequirementComponent.cs" id="3_ed7mh"] +[ext_resource type="Script" uid="uid://c5mhwlyvfuaip" path="res://scripts/components/ExitDoorComponent.cs" id="4_06sog"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_yfu6m"] size = Vector2(28, 32) @@ -25,19 +25,19 @@ position = Vector2(0, -8) shape = SubResource("RectangleShape2D_yfu6m") [node name="RequirementComponent" type="Node" parent="."] -script = ExtResource("2_iysc1") -requirement_type = 1 +script = ExtResource("2_ed7mh") +RequirementType = 1 metadata/_custom_type_script = "uid://cmh8k0rdsyh7j" -[node name="UnlockOnRequirementComponent" type="Node" parent="." node_paths=PackedStringArray("requirement_component", "unlock_target")] -script = ExtResource("3_5ktpq") -requirement_component = NodePath("../RequirementComponent") -unlock_target = NodePath("../ExitDoorComponent") +[node name="UnlockOnRequirementComponent" type="Node" parent="." node_paths=PackedStringArray("RequirementComponent", "UnlockTarget")] +script = ExtResource("3_ed7mh") +RequirementComponent = NodePath("../RequirementComponent") +UnlockTarget = NodePath("../ExitDoorComponent") metadata/_custom_type_script = "uid://c8xhgkg8gcqu6" -[node name="ExitDoorComponent" type="Node" parent="." node_paths=PackedStringArray("exit_area", "door_sprite")] -script = ExtResource("4_4jgt0") -exit_area = NodePath("..") -door_sprite = NodePath("../Sprite2D") -opened_door_frame = 88 +[node name="ExitDoorComponent" type="Node" parent="." node_paths=PackedStringArray("ExitArea", "DoorSprite")] +script = ExtResource("4_06sog") +ExitArea = NodePath("..") +DoorSprite = NodePath("../Sprite2D") +OpenedDoorFrame = 88 metadata/_custom_type_script = "uid://bwamqffvpa452" diff --git a/objects/entities/exploding_brick.tscn b/objects/entities/exploding_brick.tscn index 3e7ee06..733d2cf 100644 --- a/objects/entities/exploding_brick.tscn +++ b/objects/entities/exploding_brick.tscn @@ -1,13 +1,13 @@ [gd_scene load_steps=12 format=3 uid="uid://5surx230gfw3"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_rcgxf"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="3_y0uai"] -[ext_resource type="Script" uid="uid://beg4dk7d5pvhp" path="res://scripts/components/explosive_component.gd" id="4_8lw0n"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_nqeob"] +[ext_resource type="Script" uid="uid://7uttgdr6cr5y" path="res://scripts/components/ExplosiveComponent.cs" id="3_a2b5x"] [ext_resource type="PackedScene" uid="uid://dyp4i4ru2j2jh" path="res://objects/fxs/explosion_fx.tscn" id="5_6oopj"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="7_a2b5x"] -[ext_resource type="Script" uid="uid://bvsgg8lu0a8m6" path="res://scripts/components/lifetime_component.gd" id="7_nqeob"] -[ext_resource type="Script" uid="uid://c2gbumw4x4t1v" path="res://scripts/components/gravity_motion_component.gd" id="8_4ly8b"] -[ext_resource type="Script" uid="uid://bgty7040ams6s" path="res://scripts/components/projectile_init_component.gd" id="8_nqeob"] +[ext_resource type="Script" uid="uid://oyf25mpc5etr" path="res://scripts/components/LifetimeComponent.cs" id="5_nqeob"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="6_a2b5x"] +[ext_resource type="Script" uid="uid://cwi5qashdag1g" path="res://scripts/components/GravityMotionComponent.cs" id="7_4ly8b"] +[ext_resource type="Script" uid="uid://c7n6ecsobohjn" path="res://scripts/components/ProjectileInitComponent.cs" id="8_nqeob"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ar0xf"] size = Vector2(16, 10) @@ -37,19 +37,17 @@ scale = Vector2(0.8, 0.5) [node name="Timer" type="Timer" parent="."] -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "damage_timer")] -script = ExtResource("3_y0uai") -damage = 5.0 -area2d = NodePath("../Explosion area") -damage_timer = NodePath("../Timer") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("DamageTimer")] +script = ExtResource("2_nqeob") +Damage = 5.0 +DamageTimer = NodePath("../Timer") -[node name="ExplosiveComponent" type="Node" parent="." node_paths=PackedStringArray("root", "damage", "area2d", "explosion_area2d")] -script = ExtResource("4_8lw0n") -root = NodePath("..") -damage = NodePath("../DamageComponent") -area2d = NodePath("../BulletArea") -explosion_area2d = NodePath("../Explosion area") -explosion_effect = ExtResource("5_6oopj") +[node name="ExplosiveComponent" type="Node2D" parent="." node_paths=PackedStringArray("Damage", "Area", "ExplodeArea")] +script = ExtResource("3_a2b5x") +Damage = NodePath("../DamageComponent") +Area = NodePath("../BulletArea") +ExplodeArea = NodePath("../Explosion area") +ExplosionEffect = ExtResource("5_6oopj") [node name="Explosion area" type="Area2D" parent="."] collision_layer = 0 @@ -62,22 +60,19 @@ debug_color = Color(0.919034, 6.36289e-05, 0.69998, 0.42) [node name="ExplosionFX" parent="." instance=ExtResource("5_6oopj")] randomness = 0.24 -[node name="LifetimeComponent" type="Node" parent="." node_paths=PackedStringArray("root", "timer")] -script = ExtResource("7_nqeob") -root = NodePath("..") -life_time = 30.0 -timer = NodePath("../Timer") +[node name="LifetimeComponent" type="Node" parent="."] +script = ExtResource("5_nqeob") +LifeTime = 30.0 -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("7_a2b5x") -root = NodePath("..") -speed = 240.0 -metadata/_custom_type_script = "uid://873un8agkyja" +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("6_a2b5x") +Speed = 240.0 -[node name="GravityMotionComponent" type="Node2D" parent="." node_paths=PackedStringArray("character_body", "launch_component")] -script = ExtResource("8_4ly8b") -character_body = NodePath("..") -launch_component = NodePath("../LaunchComponent") +[node name="GravityMotionComponent" type="Node2D" parent="." node_paths=PackedStringArray("Body", "LaunchComponent")] +script = ExtResource("7_4ly8b") +Body = NodePath("..") +LaunchComponent = NodePath("../LaunchComponent") +Gravity = Vector2(0, 980) [node name="BulletArea" type="Area2D" parent="."] collision_layer = 16 @@ -85,7 +80,6 @@ collision_layer = 16 [node name="CollisionShape2D" type="CollisionShape2D" parent="BulletArea"] shape = SubResource("RectangleShape2D_nqeob") -[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("launch_component")] +[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] script = ExtResource("8_nqeob") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://bgty7040ams6s" +LaunchComponent = NodePath("../LaunchComponent") diff --git a/objects/entities/fire_brick.tscn b/objects/entities/fire_brick.tscn index 74fb710..fde2012 100644 --- a/objects/entities/fire_brick.tscn +++ b/objects/entities/fire_brick.tscn @@ -1,14 +1,14 @@ [gd_scene load_steps=11 format=3 uid="uid://daau4j5hbklk0"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_52l28"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="2_hjcob"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="3_sog2h"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="2_5ybdf"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="3_b1kpf"] [ext_resource type="Resource" uid="uid://obodgnkhxuhe" path="res://resources/status_effect/fire.tres" id="4_xx5l4"] -[ext_resource type="Script" uid="uid://d3brcje121krs" path="res://scripts/components/effect_inflictor_component.gd" id="5_ofrqk"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="6_64yur"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="7_hulun"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="8_kift8"] -[ext_resource type="Script" uid="uid://bgty7040ams6s" path="res://scripts/components/projectile_init_component.gd" id="9_5ybdf"] +[ext_resource type="Script" uid="uid://xjq33vj0rol0" path="res://scripts/components/EffectInflictorComponent.cs" id="5_wfje1"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="6_666k7"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="7_67bn4"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="8_5ybdf"] +[ext_resource type="Script" uid="uid://c7n6ecsobohjn" path="res://scripts/components/ProjectileInitComponent.cs" id="9_b1kpf"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ar0xf"] size = Vector2(16, 10) @@ -26,10 +26,10 @@ hframes = 12 vframes = 12 frame = 80 -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d")] -script = ExtResource("2_hjcob") -root = NodePath("..") -area2d = NodePath("..") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "BulletSprite")] +script = ExtResource("2_5ybdf") +Area = NodePath("..") +BulletSprite = NodePath("../Sprite2D") [node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."] position = Vector2(0, 2.38419e-07) @@ -37,34 +37,28 @@ scale = Vector2(0.8, 0.5) [node name="Timer" type="Timer" parent="."] -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("3_sog2h") -damage = 0.1 -area2d = NodePath("..") -status_effect_data = ExtResource("4_xx5l4") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "DamageTimer")] +script = ExtResource("3_b1kpf") +Area = NodePath("..") +StatusEffectData = ExtResource("4_xx5l4") +DamageTimer = NodePath("../Timer") -[node name="EffectInflictorComponent" type="Node" parent="." node_paths=PackedStringArray("damage")] -script = ExtResource("5_ofrqk") -damage = NodePath("../DamageComponent") +[node name="EffectInflictorComponent" type="Node" parent="." node_paths=PackedStringArray("Damage")] +script = ExtResource("5_wfje1") +Damage = NodePath("../DamageComponent") -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] -script = ExtResource("6_64yur") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] +script = ExtResource("6_666k7") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("7_hulun") -root = NodePath("..") -speed = 200.0 -metadata/_custom_type_script = "uid://873un8agkyja" +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("7_67bn4") +Speed = 200.0 -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] -script = ExtResource("8_kift8") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://cvcnfrr1udco5" +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("8_5ybdf") +LaunchComponent = NodePath("../LaunchComponent") -[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("launch_component")] -script = ExtResource("9_5ybdf") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://bgty7040ams6s" +[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("9_b1kpf") +LaunchComponent = NodePath("../LaunchComponent") diff --git a/objects/entities/flying_enemy.tscn b/objects/entities/flying_enemy.tscn index 318014b..773d1d8 100644 --- a/objects/entities/flying_enemy.tscn +++ b/objects/entities/flying_enemy.tscn @@ -2,20 +2,20 @@ [ext_resource type="Texture2D" uid="uid://22k1u37j6k8y" path="res://sprites/flying_enemy.png" id="1_30hhw"] [ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="1_uyhuj"] -[ext_resource type="Script" uid="uid://btfsq0bvtrx3t" path="res://scripts/components/health.gd" id="2_ivniq"] [ext_resource type="AudioStream" uid="uid://b3tsqhr06pbrs" path="res://sfx/enemy_hurt.wav" id="3_fd2du"] +[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="3_uyhuj"] [ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="4_rhq76"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="5_cmp1h"] -[ext_resource type="Script" uid="uid://b37gqnycj6rtk" path="res://scripts/components/periodic_shooting.gd" id="6_4ajjm"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="6_q78ru"] [ext_resource type="PackedScene" uid="uid://t422jkov2t1b" path="res://objects/entities/red_laser.tscn" id="7_4ajjm"] -[ext_resource type="Script" uid="uid://dqm371fysuk7i" path="res://scripts/components/enemy_death.gd" id="7_uyhuj"] -[ext_resource type="Script" uid="uid://dqmbvuutd5c3c" path="res://scripts/components/flashing_component.gd" id="8_q78ru"] -[ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="9_weo6b"] -[ext_resource type="Script" uid="uid://c07d50s20rl8s" path="res://scripts/components/status_effect_component.gd" id="10_6p4k7"] -[ext_resource type="Script" uid="uid://qi2irprbqru2" path="res://scripts/components/fire_effect_component.gd" id="11_jmybk"] -[ext_resource type="Script" uid="uid://dhj4qtwcqmqkj" path="res://scripts/components/ice_effect_component.gd" id="12_2yvae"] +[ext_resource type="Script" uid="uid://bnaxy8cw3wrko" path="res://scripts/components/PeriodicShootingComponent.cs" id="7_weo6b"] +[ext_resource type="Script" uid="uid://cfdugoeduudar" path="res://scripts/components/EnemyDeathComponent.cs" id="9_6p4k7"] +[ext_resource type="Script" uid="uid://dvyd26ricriql" path="res://scripts/components/FlashingComponent.cs" id="10_jmybk"] +[ext_resource type="Script" uid="uid://bo506l4x0808e" path="res://scripts/components/HitComponent.cs" id="11_2yvae"] +[ext_resource type="Script" uid="uid://t8rsvwdwt8ea" path="res://scripts/components/StatusEffectComponent.cs" id="12_xlup2"] +[ext_resource type="Script" uid="uid://cxuig4xh8nfov" path="res://scripts/components/FireEffectComponent.cs" id="13_mrjm6"] [ext_resource type="PackedScene" uid="uid://b12tppjkkqpt4" path="res://objects/fxs/hit_particles.tscn" id="13_xlup2"] [ext_resource type="PackedScene" uid="uid://dx80ivlvuuew4" path="res://objects/fxs/fire_fx.tscn" id="14_mrjm6"] +[ext_resource type="Script" uid="uid://d1388lhp2gpgr" path="res://scripts/components/IceEffectComponent.cs" id="14_pkino"] [ext_resource type="PackedScene" uid="uid://ck6nml06tm6ue" path="res://objects/fxs/ice_fx.tscn" id="15_pkino"] [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_hil2i"] @@ -106,10 +106,9 @@ explosiveness = 0.5 fixed_fps = 24 process_material = SubResource("ParticleProcessMaterial_fd2du") -[node name="HealthComponent" type="Node" parent="." node_paths=PackedStringArray("hurt_fx")] -script = ExtResource("2_ivniq") -hurt_fx = NodePath("../sfx_hurt") -metadata/_custom_type_script = "uid://btfsq0bvtrx3t" +[node name="HealthComponent" type="Node2D" parent="." node_paths=PackedStringArray("HurtSfx")] +script = ExtResource("3_uyhuj") +HurtSfx = NodePath("../sfx_hurt") [node name="sfx_hurt" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("3_fd2du") @@ -119,53 +118,50 @@ bus = &"sfx" stream = ExtResource("4_rhq76") bus = &"sfx" -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("5_cmp1h") -area2d = NodePath("../Hitbox") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("6_q78ru") +Area = NodePath("../Hitbox") -[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("root", "bullet_spawn_right", "bullet_spawn_left")] -script = ExtResource("6_4ajjm") -bullet_scene = ExtResource("7_4ajjm") -shoot_interval = 2.0 -root = NodePath("..") -bullet_spawn_right = NodePath("../laser spawn point right") -bullet_spawn_left = NodePath("../laser spawn point left") -shooting_interval_variation = 0.5 +[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("BulletSpawnRight", "BulletSpawnLeft")] +script = ExtResource("7_weo6b") +BulletScene = ExtResource("7_4ajjm") +ShootInterval = 2.0 +BulletSpawnRight = NodePath("../laser spawn point right") +BulletSpawnLeft = NodePath("../laser spawn point left") +ShootingIntervalVariation = 0.5 -[node name="EnemyDeathComponent" type="Node" parent="." node_paths=PackedStringArray("root", "collision_shape_2d", "health_component")] -script = ExtResource("7_uyhuj") -root = NodePath("..") -tween_duration = 0.1 -collision_shape_2d = NodePath("../CollisionShape2D") -health_component = NodePath("../HealthComponent") +[node name="EnemyDeathComponent" type="Node" parent="." node_paths=PackedStringArray("CollisionShape", "Health")] +script = ExtResource("9_6p4k7") +TweenDuration = 0.1 +CollisionShape = NodePath("../CollisionShape2D") +Health = NodePath("../HealthComponent") -[node name="FlashingComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] +[node name="FlashingComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")] process_mode = 3 -script = ExtResource("8_q78ru") -sprite = NodePath("../Sprite2D") -health_component = NodePath("../HealthComponent") +script = ExtResource("10_jmybk") +Sprite = NodePath("../Sprite2D") +HealthComponent = NodePath("../HealthComponent") -[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component", "hit_fx")] -script = ExtResource("9_weo6b") -sprite = NodePath("../Sprite2D") -health_component = NodePath("../HealthComponent") -hit_fx = NodePath("../HitParticles") +[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Health", "HitFx")] +script = ExtResource("11_2yvae") +Sprite = NodePath("../Sprite2D") +Health = NodePath("../HealthComponent") +HitFx = NodePath("../HitParticles") [node name="StatusEffectComponent" type="Node" parent="."] -script = ExtResource("10_6p4k7") +script = ExtResource("12_xlup2") -[node name="FireEffectComponent" type="Node" parent="." node_paths=PackedStringArray("health_component", "status_effect_component", "root", "fire_fx")] -script = ExtResource("11_jmybk") -health_component = NodePath("../HealthComponent") -status_effect_component = NodePath("../StatusEffectComponent") -root = NodePath("..") -fire_fx = NodePath("../FireFX") +[node name="FireEffectComponent" type="Node" parent="." node_paths=PackedStringArray("Health", "StatusEffectComponent", "FireFX")] +script = ExtResource("13_mrjm6") +Health = NodePath("../HealthComponent") +StatusEffectComponent = NodePath("../StatusEffectComponent") +FireFX = NodePath("../FireFX") -[node name="IceEffectComponent" type="Node" parent="." node_paths=PackedStringArray("status_effect_component", "ice_fx")] -script = ExtResource("12_2yvae") -components_to_disable = [NodePath("../PeriodicShootingComponent"), NodePath("../DamageComponent")] -status_effect_component = NodePath("../StatusEffectComponent") -ice_fx = NodePath("../Ice FX") +[node name="IceEffectComponent" type="Node" parent="." node_paths=PackedStringArray("ComponentsToDisable", "StatusEffectComponent", "IceFx")] +script = ExtResource("14_pkino") +ComponentsToDisable = [NodePath("../PeriodicShootingComponent"), NodePath("../DamageComponent")] +StatusEffectComponent = NodePath("../StatusEffectComponent") +IceFx = NodePath("../Ice FX") [node name="HitParticles" parent="." instance=ExtResource("13_xlup2")] position = Vector2(0, 1) diff --git a/objects/entities/flying_ship.tscn b/objects/entities/flying_ship.tscn deleted file mode 100644 index 376ac83..0000000 --- a/objects/entities/flying_ship.tscn +++ /dev/null @@ -1,183 +0,0 @@ -[gd_scene load_steps=29 format=3 uid="uid://8wy1jxy1lihn"] - -[ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="1_umrfk"] -[ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="2_vlo17"] -[ext_resource type="Texture2D" uid="uid://b7gp0gqvkv8j4" path="res://sprites/MrBrick_base.png" id="3_e1par"] -[ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="4_d5f6y"] -[ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="5_k3peb"] -[ext_resource type="Script" uid="uid://btfsq0bvtrx3t" path="res://scripts/components/health.gd" id="6_e1par"] -[ext_resource type="AudioStream" uid="uid://bmfn6p88gy575" path="res://sfx/player_hurt.wav" id="7_q8x53"] -[ext_resource type="AudioStream" uid="uid://ycgtf6wj7mto" path="res://sfx/heal.wav" id="8_5aboc"] -[ext_resource type="PackedScene" uid="uid://b12tppjkkqpt4" path="res://objects/fxs/hit_particles.tscn" id="9_buvrj"] -[ext_resource type="Script" uid="uid://ijrli0x8ij8v" path="res://scripts/components/invulnerability_component.gd" id="10_y4xwp"] -[ext_resource type="Script" uid="uid://dqmbvuutd5c3c" path="res://scripts/components/flashing_component.gd" id="11_m7okj"] -[ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="12_dwqug"] -[ext_resource type="Script" uid="uid://b3mrdvre1y567" path="res://scripts/components/ship_movement.gd" id="13_m7okj"] -[ext_resource type="Script" uid="uid://d1ctdx52gskv1" path="res://scripts/components/ship_shooter.gd" id="14_e7gat"] -[ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="15_4fjnq"] -[ext_resource type="PackedScene" uid="uid://dtem8jgcyoqar" path="res://objects/entities/green_laser.tscn" id="15_67g38"] - -[sub_resource type="RectangleShape2D" id="RectangleShape2D_y4xwp"] -size = Vector2(16, 32) - -[sub_resource type="ShaderMaterial" id="ShaderMaterial_q8x53"] -shader = ExtResource("2_vlo17") -shader_parameter/enabled = false -shader_parameter/tint = Color(1, 1, 1, 1) - -[sub_resource type="Gradient" id="Gradient_umrfk"] -offsets = PackedFloat32Array(0, 0.173759, 0.521277, 0.687943, 0.886525, 1) -colors = PackedColorArray(1.635, 0.188, 0, 1, 1.89, 0.318, 0, 1, 1, 0.470588, 0.188235, 1, 1, 0.635294, 0, 1, 1, 0.858824, 0.635294, 1, 1, 1, 1, 0.501961) - -[sub_resource type="GradientTexture1D" id="GradientTexture1D_g31n1"] -gradient = SubResource("Gradient_umrfk") -use_hdr = true - -[sub_resource type="Curve" id="Curve_wcyu2"] -_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.606498, 0.621642), -1.25408, -1.25408, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] -point_count = 3 - -[sub_resource type="CurveTexture" id="CurveTexture_epnjh"] -curve = SubResource("Curve_wcyu2") - -[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_vlo17"] -lifetime_randomness = 0.85 -particle_flag_disable_z = true -emission_shape = 1 -emission_sphere_radius = 3.0 -angle_min = -35.0 -angle_max = 35.0 -direction = Vector3(0, 1, 0) -initial_velocity_min = 30.0 -initial_velocity_max = 60.0 -gravity = Vector3(0, 98, 0) -scale_min = 2.0 -scale_max = 4.0 -scale_curve = SubResource("CurveTexture_epnjh") -color_ramp = SubResource("GradientTexture1D_g31n1") - -[sub_resource type="Gradient" id="Gradient_m7okj"] -colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0) - -[sub_resource type="GradientTexture1D" id="GradientTexture1D_f1fvy"] -gradient = SubResource("Gradient_m7okj") - -[sub_resource type="Curve" id="Curve_dwqug"] -_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] -point_count = 2 - -[sub_resource type="CurveTexture" id="CurveTexture_7b7mt"] -curve = SubResource("Curve_dwqug") - -[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_e7gat"] -resource_local_to_scene = true -lifetime_randomness = 1.0 -particle_flag_disable_z = true -emission_shape = 1 -emission_sphere_radius = 8.0 -direction = Vector3(0.1, -0.5, 0) -initial_velocity_min = 200.0 -initial_velocity_max = 400.0 -gravity = Vector3(0, 80, 0) -damping_min = 400.0 -damping_max = 800.0 -scale_max = 3.0 -scale_curve = SubResource("CurveTexture_7b7mt") -color = Color(0.764706, 0.443137, 0, 1) -color_ramp = SubResource("GradientTexture1D_f1fvy") - -[node name="Flying ship" type="CharacterBody2D"] -collision_layer = 4 -collision_mask = 9 -motion_mode = 1 - -[node name="CollisionShape2D" type="CollisionPolygon2D" parent="."] -position = Vector2(-0.5, 7) -polygon = PackedVector2Array(-13.5, -8, -13.5, -10, -12.5, -11, -10.5, -11, -8.5, -8, -7.5, -7, 10.5, -7, 15.5, -3, 15.5, -1, 10.5, 3, 6.5, 4, 4.5, 5, 2.5, 7, -1.5, 7, -5.5, 4, -9.5, 3, -11.5, 2, -14.5, -1, -14.5, -3, -12.5, -5, -11.5, -5, -11.5, -7, -12.5, -7) - -[node name="Player hitbox" type="CollisionShape2D" parent="."] -position = Vector2(-2, -7) -shape = SubResource("RectangleShape2D_y4xwp") - -[node name="Sprite2D" type="Sprite2D" parent="."] -position = Vector2(0, -1) -texture = ExtResource("1_umrfk") - -[node name="Brick Position" type="Node2D" parent="."] - -[node name="Root" type="Node2D" parent="Brick Position"] -z_index = -1 -position = Vector2(-2, -7) - -[node name="Base" type="Sprite2D" parent="Brick Position/Root"] -material = SubResource("ShaderMaterial_q8x53") -texture = ExtResource("3_e1par") - -[node name="Left Eye" type="Sprite2D" parent="Brick Position/Root"] -position = Vector2(-7, -6) -texture = ExtResource("4_d5f6y") -hframes = 2 - -[node name="Right Eye" type="Sprite2D" parent="Brick Position/Root"] -position = Vector2(6, -5) -texture = ExtResource("5_k3peb") -hframes = 2 - -[node name="Engine particles" type="GPUParticles2D" parent="."] -z_index = -1 -position = Vector2(0, 12) -amount = 64 -fixed_fps = 24 -process_material = SubResource("ParticleProcessMaterial_vlo17") - -[node name="HealthComponent" type="Node" parent="." node_paths=PackedStringArray("hurt_fx", "heal_fx")] -script = ExtResource("6_e1par") -hurt_fx = NodePath("../sfx_hurt") -heal_fx = NodePath("../sfx_heal") -metadata/_custom_type_script = "uid://btfsq0bvtrx3t" - -[node name="sfx_hurt" type="AudioStreamPlayer2D" parent="."] -stream = ExtResource("7_q8x53") - -[node name="sfx_heal" type="AudioStreamPlayer2D" parent="."] -stream = ExtResource("8_5aboc") - -[node name="HitParticles" parent="." instance=ExtResource("9_buvrj")] -position = Vector2(-2, -5) -process_material = SubResource("ParticleProcessMaterial_e7gat") - -[node name="InvulnerabilityComponent" type="Node" parent="." node_paths=PackedStringArray("flashing_component")] -script = ExtResource("10_y4xwp") -flashing_component = NodePath("../FlashingComponent Base") - -[node name="FlashingComponent Base" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")] -script = ExtResource("11_m7okj") -sprite = NodePath("../Brick Position/Root/Base") -flash_duration = 1.0 -health_component = NodePath("../HealthComponent") - -[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component", "hit_fx")] -script = ExtResource("12_dwqug") -sprite = NodePath("../Brick Position/Root/Base") -health_component = NodePath("../HealthComponent") -hit_fx = NodePath("../HitParticles") -metadata/_custom_type_script = "uid://ceq8n7yw7qxpi" - -[node name="ShipMovement" type="Node" parent="." node_paths=PackedStringArray("body")] -script = ExtResource("13_m7okj") -acceleration = 800.0 -friction = 600.0 -body = NodePath("..") - -[node name="Shoot spawn" type="Marker2D" parent="."] -position = Vector2(16, -2) -gizmo_extents = 1.0 - -[node name="ShipShooter" type="Node" parent="." node_paths=PackedStringArray("bullet_spawn", "shoot_sfx")] -script = ExtResource("14_e7gat") -bullet_scene = ExtResource("15_67g38") -bullet_spawn = NodePath("../Shoot spawn") -shoot_sfx = NodePath("../sfx_shoot") - -[node name="sfx_shoot" type="AudioStreamPlayer2D" parent="."] -stream = ExtResource("15_4fjnq") diff --git a/objects/entities/green_laser.tscn b/objects/entities/green_laser.tscn index d6eeeb1..6536713 100644 --- a/objects/entities/green_laser.tscn +++ b/objects/entities/green_laser.tscn @@ -1,13 +1,13 @@ [gd_scene load_steps=10 format=3 uid="uid://dtem8jgcyoqar"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="1_nuljg"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="2_1gu8o"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="3_2vcrp"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="4_2bmqm"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="5_772bd"] -[ext_resource type="Script" uid="uid://bvsgg8lu0a8m6" path="res://scripts/components/lifetime_component.gd" id="6_vd16u"] -[ext_resource type="Script" uid="uid://bgty7040ams6s" path="res://scripts/components/projectile_init_component.gd" id="7_1gu8o"] -[ext_resource type="Script" uid="uid://bpy6xtfm8l3hy" path="res://scripts/components/trigger_lever_component.gd" id="8_2vcrp"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="1_2vcrp"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="2_2bmqm"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="3_772bd"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="4_vd16u"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="5_8s1u3"] +[ext_resource type="Script" uid="uid://oyf25mpc5etr" path="res://scripts/components/LifetimeComponent.cs" id="6_vuegc"] +[ext_resource type="Script" uid="uid://c7n6ecsobohjn" path="res://scripts/components/ProjectileInitComponent.cs" id="7_kkqle"] +[ext_resource type="Script" uid="uid://cqau0810tjk4d" path="res://scripts/components/TriggerLeverComponent.cs" id="8_g8he0"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_n4uav"] size = Vector2(16, 2) @@ -16,65 +16,59 @@ size = Vector2(16, 2) collision_layer = 16 collision_mask = 9 -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("1_nuljg") -damage = 1.0 -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("1_2vcrp") +Area = NodePath("..") metadata/_custom_type_script = "uid://dkmxhjtmu5xlb" [node name="CollisionShape2D" type="CollisionShape2D" parent="."] +visible = false position = Vector2(8, -0.5) shape = SubResource("RectangleShape2D_n4uav") [node name="Line2D" type="Line2D" parent="."] points = PackedVector2Array(0, 0, 16, 0) width = 2.0 -default_color = Color(0, 94.012, 1, 1) +default_color = Color(0, 90, 0, 1) -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d")] -script = ExtResource("2_1gu8o") -root = NodePath("..") -area2d = NodePath("..") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("2_2bmqm") +Area = NodePath("..") metadata/_custom_type_script = "uid://cdnwrn8v05qhi" -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] -script = ExtResource("3_2vcrp") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] +script = ExtResource("3_772bd") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") metadata/_custom_type_script = "uid://1tnr46o1ib4u" [node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."] position = Vector2(8, 0) scale = Vector2(0.8, 0.1) -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] -script = ExtResource("4_2bmqm") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("4_vd16u") +LaunchComponent = NodePath("../LaunchComponent") metadata/_custom_type_script = "uid://cvcnfrr1udco5" -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("5_772bd") -root = NodePath("..") -speed = 320.0 +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("5_8s1u3") +Speed = 320.0 metadata/_custom_type_script = "uid://873un8agkyja" -[node name="LifetimeComponent" type="Node" parent="." node_paths=PackedStringArray("root", "timer")] -script = ExtResource("6_vd16u") -root = NodePath("..") -life_time = 10.0 -timer = NodePath("../Timer") +[node name="LifetimeComponent" type="Node" parent="."] +script = ExtResource("6_vuegc") +LifeTime = 10.0 metadata/_custom_type_script = "uid://bvsgg8lu0a8m6" [node name="Timer" type="Timer" parent="."] wait_time = 5.0 autostart = true -[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("launch_component")] -script = ExtResource("7_1gu8o") -launch_component = NodePath("../LaunchComponent") +[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("7_kkqle") +LaunchComponent = NodePath("../LaunchComponent") metadata/_custom_type_script = "uid://bgty7040ams6s" [node name="TriggerLeverComponent" type="Node" parent="."] -script = ExtResource("8_2vcrp") +script = ExtResource("8_g8he0") metadata/_custom_type_script = "uid://bpy6xtfm8l3hy" diff --git a/objects/entities/homing_bullet.tscn b/objects/entities/homing_bullet.tscn index cbc1491..3e7feb7 100644 --- a/objects/entities/homing_bullet.tscn +++ b/objects/entities/homing_bullet.tscn @@ -1,13 +1,13 @@ [gd_scene load_steps=13 format=3 uid="uid://jiv4e82ycwmq"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_bmfqy"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="2_id4r8"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="3_2uduj"] -[ext_resource type="Script" uid="uid://bvsgg8lu0a8m6" path="res://scripts/components/lifetime_component.gd" id="4_x32ji"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="5_r3id6"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="7_8gqaa"] -[ext_resource type="Script" uid="uid://be8jhvb8t3kif" path="res://scripts/components/homing_missile_motion.gd" id="7_bmfqy"] -[ext_resource type="Script" uid="uid://c0bvan0swashx" path="res://scripts/components/trail_component.gd" id="8_id4r8"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_id4r8"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="3_2uduj"] +[ext_resource type="Script" uid="uid://oyf25mpc5etr" path="res://scripts/components/LifetimeComponent.cs" id="4_x32ji"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="5_id4r8"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="6_2uduj"] +[ext_resource type="Script" uid="uid://c2hplha6af74q" path="res://scripts/components/HomingMissileMotionComponent.cs" id="7_x32ji"] +[ext_resource type="Script" uid="uid://cqkh5e36p5fj0" path="res://scripts/components/TrailComponent.cs" id="8_id4r8"] [sub_resource type="CircleShape2D" id="CircleShape2D_txsw8"] radius = 4.0 @@ -41,47 +41,42 @@ frame = 79 position = Vector2(2.38419e-07, 2.38419e-07) scale = Vector2(0.4, 0.4) -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] script = ExtResource("2_id4r8") -area2d = NodePath("..") +Area = NodePath("..") [node name="Timer" type="Timer" parent="."] wait_time = 5.0 autostart = true -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] script = ExtResource("3_2uduj") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") -[node name="LifetimeComponent" type="Node" parent="." node_paths=PackedStringArray("root", "timer")] +[node name="LifetimeComponent" type="Node" parent="."] script = ExtResource("4_x32ji") -root = NodePath("..") -life_time = 10.0 -timer = NodePath("../Timer") +LifeTime = 10.0 -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("5_r3id6") -root = NodePath("..") -speed = 120.0 +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("5_id4r8") +Speed = 120.0 metadata/_custom_type_script = "uid://873un8agkyja" -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d")] -script = ExtResource("7_8gqaa") -root = NodePath("..") -area2d = NodePath("..") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "BulletSprite")] +script = ExtResource("6_2uduj") +Area = NodePath("..") +BulletSprite = NodePath("../Sprite2D") metadata/_custom_type_script = "uid://cdnwrn8v05qhi" -[node name="HomingMissileMotion" type="Node" parent="." node_paths=PackedStringArray("launch_component", "root", "detection_area")] -script = ExtResource("7_bmfqy") -launch_component = NodePath("../LaunchComponent") -root = NodePath("..") -max_speed = 122.0 -acceleration = 4.0 -detection_area = NodePath("../DetectionArea") -wobble_strength = 10.0 -drag = 1.0 -steering_lerp = 0.01 +[node name="HomingMissileMotion" type="Node" parent="." node_paths=PackedStringArray("Launch", "DetectionArea")] +script = ExtResource("7_x32ji") +Launch = NodePath("../LaunchComponent") +MaxSpeed = 122.0 +Acceleration = 4.0 +WobbleStrength = 10.0 +Drag = 1.0 +SteeringLerp = 0.01 +DetectionArea = NodePath("../DetectionArea") [node name="DetectionArea" type="Area2D" parent="."] collision_layer = 0 diff --git a/objects/entities/ice_brick.tscn b/objects/entities/ice_brick.tscn index a4695ec..c5a4041 100644 --- a/objects/entities/ice_brick.tscn +++ b/objects/entities/ice_brick.tscn @@ -1,14 +1,14 @@ [gd_scene load_steps=11 format=3 uid="uid://bcmx07k12gcsc"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_xusxl"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="2_ilnf5"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="3_3yf4g"] -[ext_resource type="Resource" uid="uid://02l4nbnf2aft" path="res://resources/status_effect/ice.tres" id="4_da7hn"] -[ext_resource type="Script" uid="uid://d3brcje121krs" path="res://scripts/components/effect_inflictor_component.gd" id="5_x7vqb"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="6_7tdxt"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="7_22hnt"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="8_xvjl0"] -[ext_resource type="Script" uid="uid://bgty7040ams6s" path="res://scripts/components/projectile_init_component.gd" id="9_xvjl0"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="2_xvjl0"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="3_22hnt"] +[ext_resource type="Script" uid="uid://xjq33vj0rol0" path="res://scripts/components/EffectInflictorComponent.cs" id="4_22hnt"] +[ext_resource type="Resource" uid="uid://02l4nbnf2aft" path="res://resources/status_effect/ice.tres" id="4_xvjl0"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="5_xvjl0"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="6_6nrp8"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="7_oml3k"] +[ext_resource type="Script" uid="uid://c7n6ecsobohjn" path="res://scripts/components/ProjectileInitComponent.cs" id="8_22hnt"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ar0xf"] size = Vector2(16, 10) @@ -26,10 +26,10 @@ hframes = 12 vframes = 12 frame = 80 -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d")] -script = ExtResource("2_ilnf5") -root = NodePath("..") -area2d = NodePath("..") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "BulletSprite")] +script = ExtResource("2_xvjl0") +Area = NodePath("..") +BulletSprite = NodePath("../Sprite2D") [node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."] position = Vector2(0, 2.38419e-07) @@ -37,34 +37,28 @@ scale = Vector2(0.8, 0.5) [node name="Timer" type="Timer" parent="."] -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("3_3yf4g") -damage = 0.05 -area2d = NodePath("..") -status_effect_data = ExtResource("4_da7hn") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "DamageTimer")] +script = ExtResource("3_22hnt") +Area = NodePath("..") +StatusEffectData = ExtResource("4_xvjl0") +DamageTimer = NodePath("../Timer") -[node name="EffectInflictorComponent" type="Node" parent="." node_paths=PackedStringArray("damage")] -script = ExtResource("5_x7vqb") -damage = NodePath("../DamageComponent") +[node name="EffectInflictorComponent" type="Node" parent="." node_paths=PackedStringArray("Damage")] +script = ExtResource("4_22hnt") +Damage = NodePath("../DamageComponent") -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] -script = ExtResource("6_7tdxt") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] +script = ExtResource("5_xvjl0") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] -script = ExtResource("7_22hnt") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://cvcnfrr1udco5" +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("6_6nrp8") +LaunchComponent = NodePath("../LaunchComponent") -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("8_xvjl0") -root = NodePath("..") -speed = 170.0 -metadata/_custom_type_script = "uid://873un8agkyja" +[node name="LaunchComponent" type="Node2D" parent="."] +script = ExtResource("7_oml3k") +Speed = 170.0 -[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("launch_component")] -script = ExtResource("9_xvjl0") -launch_component = NodePath("../LaunchComponent") -metadata/_custom_type_script = "uid://bgty7040ams6s" +[node name="ProjectileInitComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] +script = ExtResource("8_22hnt") +LaunchComponent = NodePath("../LaunchComponent") diff --git a/objects/entities/jump_pad.tscn b/objects/entities/jump_pad.tscn index c458bad..a346395 100644 --- a/objects/entities/jump_pad.tscn +++ b/objects/entities/jump_pad.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://cm3rixnnev1pg"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_ctugi"] -[ext_resource type="Script" uid="uid://dwok2qx4wpkey" path="res://scripts/components/jump_pad_component.gd" id="2_2ypfs"] +[ext_resource type="Script" uid="uid://bgbnof7aeydmq" path="res://scripts/components/JumpPadComponent.cs" id="2_huktk"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ci3ij"] size = Vector2(16, 6) @@ -20,9 +20,9 @@ hframes = 12 vframes = 12 frame = 120 -[node name="JumpPadComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "sprite2d")] -script = ExtResource("2_2ypfs") -jump_force = 1110.0 -area2d = NodePath("..") -sprite2d = NodePath("../Sprite2D") -start_animation_index = 120 +[node name="JumpPadComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "Sprite")] +script = ExtResource("2_huktk") +JumpForce = 1110.0 +Area = NodePath("..") +Sprite = NodePath("../Sprite2D") +StartAnimationIndex = 120 diff --git a/objects/entities/killzone.tscn b/objects/entities/killzone.tscn index 1f7b37c..58f2cc2 100644 --- a/objects/entities/killzone.tscn +++ b/objects/entities/killzone.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://bqom4cm7r18db"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="1_un3fl"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="1_ggub0"] [sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_cr00c"] @@ -11,7 +11,7 @@ collision_mask = 4 [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("WorldBoundaryShape2D_cr00c") -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("1_un3fl") -damage = 999999.0 -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("1_ggub0") +Damage = 400000.0 +Area = NodePath("..") diff --git a/objects/entities/laser_beam.tscn b/objects/entities/laser_beam.tscn index 7287291..4a636b5 100644 --- a/objects/entities/laser_beam.tscn +++ b/objects/entities/laser_beam.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://c7ku0k8n55hjv"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_brnkm"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="2_x12f0"] -[ext_resource type="Script" uid="uid://bejv75mi8npj0" path="res://scripts/components/beam_component.gd" id="3_6pj0h"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="2_l5xec"] +[ext_resource type="Script" uid="uid://df1llrbm80e02" path="res://scripts/components/BeamComponent.cs" id="3_l5xec"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ptfn7"] size = Vector2(8, 16) @@ -20,13 +20,14 @@ region_rect = Rect2(176, 64, 16, 16) [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_ptfn7") -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("2_x12f0") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("2_l5xec") +Area = NodePath("..") -[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("root", "sprite2d", "collision_shape")] +[node name="BeamComponent" type="Node2D" parent="." node_paths=PackedStringArray("Root", "Sprite", "CollisionShape")] position = Vector2(0, -8) -script = ExtResource("3_6pj0h") -root = NodePath("..") -sprite2d = NodePath("../Sprite2D") -collision_shape = NodePath("../CollisionShape2D") +script = ExtResource("3_l5xec") +ExpansionSpeed = 16.0 +Root = NodePath("..") +Sprite = NodePath("../Sprite2D") +CollisionShape = NodePath("../CollisionShape2D") diff --git a/objects/entities/lever.tscn b/objects/entities/lever.tscn index 6a5b499..36990b2 100644 --- a/objects/entities/lever.tscn +++ b/objects/entities/lever.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=5 format=3 uid="uid://bd51frym6mm7v"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_psg62"] -[ext_resource type="Script" uid="uid://hyuwsp1b336a" path="res://scripts/components/lever_component.gd" id="2_0p0wb"] +[ext_resource type="Script" uid="uid://1oo22ieply7n" path="res://scripts/components/LeverComponent.cs" id="2_a6tw0"] [ext_resource type="AudioStream" uid="uid://beq14we7v3iw4" path="res://sfx/activate_lever.wav" id="3_gipby"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_ke5tv"] @@ -21,12 +21,12 @@ hframes = 12 vframes = 12 frame = 75 -[node name="LeverComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "sprite2d", "sfx")] -script = ExtResource("2_0p0wb") -area2d = NodePath("..") -sprite2d = NodePath("../Sprite2D") -start_animation_index = 75 -sfx = NodePath("../sfx") +[node name="LeverComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "Sprite", "Sfx")] +script = ExtResource("2_a6tw0") +Area = NodePath("..") +Sprite = NodePath("../Sprite2D") +StartAnimationIndex = 75 +Sfx = NodePath("../sfx") [node name="sfx" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("3_gipby") diff --git a/objects/entities/red_laser.tscn b/objects/entities/red_laser.tscn index f413beb..699c657 100644 --- a/objects/entities/red_laser.tscn +++ b/objects/entities/red_laser.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=8 format=3 uid="uid://t422jkov2t1b"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="1_koyoa"] -[ext_resource type="Script" uid="uid://cdnwrn8v05qhi" path="res://scripts/components/bullet_component.gd" id="2_eru3x"] -[ext_resource type="Script" uid="uid://1tnr46o1ib4u" path="res://scripts/components/out_of_screen_component.gd" id="3_m14o3"] -[ext_resource type="Script" uid="uid://cvcnfrr1udco5" path="res://scripts/components/straight_motion_component.gd" id="4_dolct"] -[ext_resource type="Script" uid="uid://873un8agkyja" path="res://scripts/components/launch_component.gd" id="5_eafpt"] -[ext_resource type="Script" uid="uid://bvsgg8lu0a8m6" path="res://scripts/components/lifetime_component.gd" id="6_m14o3"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="1_m14o3"] +[ext_resource type="Script" uid="uid://cfw8nbrarex0i" path="res://scripts/components/BulletComponent.cs" id="2_m14o3"] +[ext_resource type="Script" uid="uid://cs6u3sh68f43j" path="res://scripts/components/OutOfScreenComponent.cs" id="3_m14o3"] +[ext_resource type="Script" uid="uid://c7p06t0eax8am" path="res://scripts/components/StraightMotionComponent.cs" id="4_dolct"] +[ext_resource type="Script" uid="uid://cbexrnnj47f87" path="res://scripts/components/LaunchComponent.cs" id="5_eafpt"] +[ext_resource type="Script" uid="uid://oyf25mpc5etr" path="res://scripts/components/LifetimeComponent.cs" id="6_0c45k"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_n4uav"] size = Vector2(16, 2) @@ -14,9 +14,9 @@ size = Vector2(16, 2) collision_layer = 8 collision_mask = 21 -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("1_koyoa") -area2d = NodePath("..") +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("1_m14o3") +Area = NodePath("..") metadata/_custom_type_script = "uid://dkmxhjtmu5xlb" [node name="CollisionShape2D" type="CollisionShape2D" parent="."] @@ -28,41 +28,31 @@ points = PackedVector2Array(0, 0, 16, 0) width = 2.0 default_color = Color(90, 1, 1, 1) -[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("root", "area2d")] -script = ExtResource("2_eru3x") -root = NodePath("..") -area2d = NodePath("..") +[node name="BulletComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] +script = ExtResource("2_m14o3") +Area = NodePath("..") metadata/_custom_type_script = "uid://cdnwrn8v05qhi" -[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("visibility_notifier", "root")] +[node name="OutOfScreenComponent" type="Node" parent="." node_paths=PackedStringArray("VisibilityNotifier")] script = ExtResource("3_m14o3") -visibility_notifier = NodePath("../VisibleOnScreenNotifier2D") -root = NodePath("..") +VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D") metadata/_custom_type_script = "uid://1tnr46o1ib4u" [node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."] position = Vector2(8, 0) scale = Vector2(0.8, 0.1) -[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("root", "launch_component")] +[node name="StraightMotionComponent" type="Node" parent="." node_paths=PackedStringArray("LaunchComponent")] script = ExtResource("4_dolct") -root = NodePath("..") -launch_component = NodePath("../LaunchComponent") +LaunchComponent = NodePath("../LaunchComponent") metadata/_custom_type_script = "uid://cvcnfrr1udco5" -[node name="LaunchComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] +[node name="LaunchComponent" type="Node2D" parent="."] script = ExtResource("5_eafpt") -root = NodePath("..") -speed = 64.0 +Speed = 64.0 metadata/_custom_type_script = "uid://873un8agkyja" -[node name="LifetimeComponent" type="Node" parent="." node_paths=PackedStringArray("root", "timer")] -script = ExtResource("6_m14o3") -root = NodePath("..") -life_time = 10.0 -timer = NodePath("../Timer") +[node name="LifetimeComponent" type="Node" parent="."] +script = ExtResource("6_0c45k") +LifeTime = 10.0 metadata/_custom_type_script = "uid://bvsgg8lu0a8m6" - -[node name="Timer" type="Timer" parent="."] -wait_time = 5.0 -autostart = true diff --git a/objects/entities/small_heal_potion.tscn b/objects/entities/small_heal_potion.tscn index 6ebd4fb..46ca40d 100644 --- a/objects/entities/small_heal_potion.tscn +++ b/objects/entities/small_heal_potion.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=6 format=3 uid="uid://t6h2ra7kjyq"] [ext_resource type="Texture2D" uid="uid://1x4iq56rhc18" path="res://sprites/health_potions.png" id="1_p0sdo"] -[ext_resource type="Script" uid="uid://cegdd1sravi5m" path="res://scripts/components/heal_component.gd" id="2_piwlp"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="3_k2tv6"] +[ext_resource type="Script" uid="uid://bmx6rk281yim2" path="res://scripts/components/HealComponent.cs" id="2_p0sdo"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="3_p0sdo"] [ext_resource type="Resource" uid="uid://2tl3yoh202no" path="res://resources/collectables/small_health_potion.tres" id="4_p0sdo"] [sub_resource type="CircleShape2D" id="CircleShape2D_uj2v5"] @@ -20,13 +20,13 @@ texture = ExtResource("1_p0sdo") hframes = 4 frame = 1 -[node name="HealComponent" type="Node" parent="." node_paths=PackedStringArray("collectable")] -script = ExtResource("2_piwlp") -collectable = NodePath("../CollectableComponent") +[node name="HealComponent" type="Node" parent="." node_paths=PackedStringArray("Collectable")] +script = ExtResource("2_p0sdo") +Collectable = NodePath("../CollectableComponent") -[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape")] -script = ExtResource("3_k2tv6") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("4_p0sdo") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape")] +script = ExtResource("3_p0sdo") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("4_p0sdo") metadata/_custom_type_script = "uid://pa1bwc4no08q" diff --git a/objects/entities/spaceship_enter.tscn b/objects/entities/spaceship_enter.tscn index bd4e417..ac6cd83 100644 --- a/objects/entities/spaceship_enter.tscn +++ b/objects/entities/spaceship_enter.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://lpovacvt3yyj"] [ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="1_r82pf"] -[ext_resource type="Script" uid="uid://ghb614g22ph7" path="res://scripts/components/spaceship_enter_component.gd" id="2_wanmd"] +[ext_resource type="Script" uid="uid://dtv2r7q4elgre" path="res://scripts/components/SpaceshipEnterComponent.cs" id="2_wanmd"] [sub_resource type="CircleShape2D" id="CircleShape2D_wanmd"] radius = 16.1245 @@ -9,13 +9,10 @@ radius = 16.1245 [node name="Spaceship Enter" type="Area2D"] collision_layer = 0 collision_mask = 4 +script = ExtResource("2_wanmd") [node name="Sprite2D" type="Sprite2D" parent="."] texture = ExtResource("1_r82pf") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("CircleShape2D_wanmd") - -[node name="SpaceshipEnterComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] -script = ExtResource("2_wanmd") -area2d = NodePath("..") diff --git a/objects/entities/spaceship_exit.tscn b/objects/entities/spaceship_exit.tscn index d6426e5..2dff521 100644 --- a/objects/entities/spaceship_exit.tscn +++ b/objects/entities/spaceship_exit.tscn @@ -1,14 +1,14 @@ [gd_scene load_steps=3 format=3 uid="uid://dkqa3q6j2gof4"] -[ext_resource type="Script" uid="uid://ybmwls7hv8j3" path="res://scripts/components/spaceship_exit_component.gd" id="1_1cmfv"] +[ext_resource type="Script" uid="uid://d3gfg05ll8uw3" path="res://scripts/components/SpaceshipExitComponent.cs" id="1_1cmfv"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_njt46"] -[node name="Spaceship exit" type="Area2D" node_paths=PackedStringArray("area2d")] +[node name="Spaceship exit" type="Area2D" node_paths=PackedStringArray("Area")] collision_layer = 0 collision_mask = 4 script = ExtResource("1_1cmfv") -area2d = NodePath(".") +Area = NodePath(".") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_njt46") diff --git a/objects/entities/spike_wheel.tscn b/objects/entities/spike_wheel.tscn index 5b4ca9a..4b2fa98 100644 --- a/objects/entities/spike_wheel.tscn +++ b/objects/entities/spike_wheel.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://buff711j0f2ph"] [ext_resource type="Texture2D" uid="uid://ddlwjg81ro1kl" path="res://sprites/spike_trap.png" id="1_xg6e1"] -[ext_resource type="Script" uid="uid://cq26pfxn17s7s" path="res://scripts/components/spin_component.gd" id="2_3d4lc"] -[ext_resource type="Script" uid="uid://dkmxhjtmu5xlb" path="res://scripts/components/damage_component.gd" id="3_ytlu4"] +[ext_resource type="Script" uid="uid://cqw134ewht3hc" path="res://scripts/components/SpinComponent.cs" id="2_ytlu4"] +[ext_resource type="Script" uid="uid://2i7p7v135u7c" path="res://scripts/components/DamageComponent.cs" id="3_ytlu4"] [sub_resource type="CircleShape2D" id="CircleShape2D_xg6e1"] radius = 8.0 @@ -17,14 +17,13 @@ texture = ExtResource("1_xg6e1") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("CircleShape2D_xg6e1") -[node name="SpinComponent" type="Node2D" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("2_3d4lc") -root = NodePath("../Sprite2D") -speed = 1.0 +[node name="SpinComponent" type="Node2D" parent="."] +script = ExtResource("2_ytlu4") +SpinSpeed = 1.0 metadata/_custom_type_script = "uid://cq26pfxn17s7s" -[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("area2d")] +[node name="DamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area")] script = ExtResource("3_ytlu4") -damage = 1.0 -area2d = NodePath("..") +Damage = 1.0 +Area = NodePath("..") metadata/_custom_type_script = "uid://dkmxhjtmu5xlb" diff --git a/objects/entities/treasure.tscn b/objects/entities/treasure.tscn index db3528d..86c4f68 100644 --- a/objects/entities/treasure.tscn +++ b/objects/entities/treasure.tscn @@ -1,15 +1,15 @@ [gd_scene load_steps=7 format=3 uid="uid://073ts5cxtwbl"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_uh3ex"] -[ext_resource type="Script" uid="uid://pa1bwc4no08q" path="res://scripts/components/collectable.gd" id="2_i1ssp"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_n5rum"] [ext_resource type="Resource" uid="uid://bws2xldndlre1" path="res://resources/collectables/treasure.tres" id="3_uknsr"] -[ext_resource type="Script" uid="uid://bg75hnr3q6grk" path="res://scripts/components/fade_away.gd" id="4_ccbcr"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="4_o1r16"] [ext_resource type="AudioStream" uid="uid://d2rj7uy5f0kwm" path="res://sfx/pickup_coin_3.wav" id="5_xt2rk"] [sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"] radius = 9.0 -[node name="Treasure" type="Area2D"] +[node name="Treasure" type="Area2D" groups=["coins"]] collision_layer = 2 collision_mask = 4 @@ -22,19 +22,18 @@ hframes = 12 vframes = 12 frame = 66 -[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d", "collision_shape", "sfx") groups=["coins"]] -script = ExtResource("2_i1ssp") -area2d = NodePath("..") -collision_shape = NodePath("../CollisionShape2D") -collectable_data = ExtResource("3_uknsr") -sfx = NodePath("../sfx") +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")] +script = ExtResource("2_n5rum") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("3_uknsr") +Sfx = NodePath("../sfx") -[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")] -script = ExtResource("4_ccbcr") -sprite2d = NodePath("../Sprite2D") -fade_duration = 0.4 -root = NodePath("..") -area2d = NodePath("..") +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("4_o1r16") +Sprite = NodePath("../Sprite2D") +FadeDuration = 0.4 +Area = NodePath("..") [node name="sfx" type="AudioStreamPlayer2D" parent="."] stream = ExtResource("5_xt2rk") diff --git a/objects/fxs/explosion_fx.tscn b/objects/fxs/explosion_fx.tscn index 3b045fa..fe77987 100644 --- a/objects/fxs/explosion_fx.tscn +++ b/objects/fxs/explosion_fx.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=9 format=3 uid="uid://dyp4i4ru2j2jh"] -[ext_resource type="Script" uid="uid://f74xpfg7624d" path="res://scripts/components/cleanup_component.gd" id="1_8uvxg"] +[ext_resource type="Script" uid="uid://v7tt4w6bejux" path="res://scripts/components/CleanupComponent.cs" id="1_5wuia"] [sub_resource type="Curve" id="Curve_8umf8"] _data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0.4), 0.0, 0.0, 0, 0] @@ -45,8 +45,7 @@ explosiveness = 1.0 fixed_fps = 24 process_material = SubResource("ParticleProcessMaterial_1ujje") -[node name="CleanUpComponent" type="Node" parent="." node_paths=PackedStringArray("root")] -script = ExtResource("1_8uvxg") -root = NodePath("..") +[node name="CleanUpComponent" type="Node" parent="."] +script = ExtResource("1_5wuia") [connection signal="finished" from="." to="CleanUpComponent" method="clean_up"] diff --git a/objects/fxs/terrain_hit_fx.tscn b/objects/fxs/terrain_hit_fx.tscn index 2a46b77..d6cb70f 100644 --- a/objects/fxs/terrain_hit_fx.tscn +++ b/objects/fxs/terrain_hit_fx.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=8 format=3 uid="uid://c1iorglk708g0"] -[ext_resource type="Script" uid="uid://djfejwp6e402k" path="res://scripts/components/terrain_hit_fx.gd" id="1_22p6x"] +[ext_resource type="Script" uid="uid://cypxrqoeiihbf" path="res://scripts/components/TerrainHitFx.cs" id="1_22p6x"] [sub_resource type="GradientTexture1D" id="GradientTexture1D_6dbny"] diff --git a/objects/game_manager.tscn b/objects/game_manager.tscn index a37909c..6003a22 100644 --- a/objects/game_manager.tscn +++ b/objects/game_manager.tscn @@ -1,22 +1,15 @@ [gd_scene load_steps=7 format=3 uid="uid://b4eifkc31jsun"] -[ext_resource type="Script" uid="uid://dd30bgqiagi25" path="res://scripts/game_manager.gd" id="1_58t7u"] +[ext_resource type="Script" uid="uid://c6eoi3ymefc0x" path="res://Autoloads/GameManager.cs" id="1_t2tr6"] [ext_resource type="PackedScene" uid="uid://bol7g83v2accs" path="res://scenes/level_village_1.tscn" id="2_bentb"] [ext_resource type="PackedScene" uid="uid://chqb11pfoqmeb" path="res://scenes/level_village_2.tscn" id="3_ajlkg"] [ext_resource type="PackedScene" uid="uid://h60obxmju6mo" path="res://scenes/level_village_3.tscn" id="4_se5tb"] [ext_resource type="PackedScene" uid="uid://bhad760x3vvco" path="res://scenes/level_village_4.tscn" id="5_mnosh"] -[ext_resource type="Script" uid="uid://bydv4g1n5s3nf" path="res://scripts/screenshot.gd" id="6_t2tr6"] +[ext_resource type="Script" uid="uid://chrhjch4ymfvr" path="res://scripts/Screenshot.cs" id="6_bbtu1"] [node name="GameManager" type="Node"] -script = ExtResource("1_58t7u") -level_scenes = Array[PackedScene]([ExtResource("2_bentb"), ExtResource("3_ajlkg"), ExtResource("4_se5tb"), ExtResource("5_mnosh")]) -player_state = { -"coins": 0, -"current_level": 0, -"lives": 3, -"unlocked_levels": [ExtResource("4_se5tb")], -"unlocked_skills": [] -} +script = ExtResource("1_t2tr6") +LevelScenes = Array[PackedScene]([ExtResource("2_bentb"), ExtResource("3_ajlkg"), ExtResource("4_se5tb"), ExtResource("5_mnosh")]) -[node name="Node" type="Node" parent="."] -script = ExtResource("6_t2tr6") +[node name="Screenshot" type="Node" parent="."] +script = ExtResource("6_bbtu1") diff --git a/objects/level/base_level.tscn b/objects/level/base_level.tscn index 9dfeca6..8de9580 100644 --- a/objects/level/base_level.tscn +++ b/objects/level/base_level.tscn @@ -1,10 +1,9 @@ -[gd_scene load_steps=21 format=4 uid="uid://fw5fbdy5gcmx"] +[gd_scene load_steps=19 format=4 uid="uid://fw5fbdy5gcmx"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="1_hb5r3"] [ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/entities/brick_player.tscn" id="1_lbnsn"] [ext_resource type="PackedScene" uid="uid://6foggu31cu14" path="res://objects/level/ui_layer.tscn" id="2_lbnsn"] [ext_resource type="PackedScene" uid="uid://cywsu7yrtjdog" path="res://objects/level/global_light.tscn" id="3_3732a"] -[ext_resource type="Resource" uid="uid://cqtalsov2bkpo" path="res://resources/levels/village/village_1.tres" id="4_c2yv5"] [ext_resource type="PackedScene" uid="uid://cb0mnye1ki5a6" path="res://objects/level/camera_2d.tscn" id="5_517ha"] [ext_resource type="Script" uid="uid://d23haq52m7ulv" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="6_6imqp"] [ext_resource type="Script" uid="uid://ccfft4b8rwgbo" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="7_kl81p"] @@ -12,7 +11,6 @@ [ext_resource type="TileSet" uid="uid://cu2sx7qigrqnv" path="res://resources/tilesets/village/terain.tres" id="9_etwin"] [ext_resource type="TileSet" uid="uid://bc5a20s6kuy8e" path="res://resources/tilesets/village/entities.tres" id="10_8xrv5"] [ext_resource type="TileSet" uid="uid://bbppo0irxdmqy" path="res://resources/tilesets/village/foreground.tres" id="11_0vx27"] -[ext_resource type="Script" uid="uid://8r1y8elyw7kt" path="res://scripts/console_management.gd" id="12_8xrv5"] [ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="13_0vx27"] [sub_resource type="Gradient" id="Gradient_qb72p"] @@ -55,23 +53,15 @@ ease = 2 [node name="Brick Player" parent="." instance=ExtResource("1_lbnsn")] -[node name="HitParticles" parent="Brick Player" index="27"] +[node name="HitParticles" parent="Brick Player" index="26"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="WorldEnvironment" parent="." instance=ExtResource("1_hb5r3")] [node name="UI Layer" parent="." instance=ExtResource("2_lbnsn")] -[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")] -player_health = NodePath("../../Brick Player/HealthComponent") - -[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] -current_level = ExtResource("4_c2yv5") -nodes_to_disable = [NodePath("../../Brick Player")] - -[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("skill_unlocker", "components_to_disable")] -skill_unlocker = NodePath("../../Brick Player/SkillUnlockerComponent") -components_to_disable = [NodePath("../../Brick Player")] +[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("SkillUnlockerComponent")] +SkillUnlockerComponent = NodePath("../../Brick Player/SkillUnlockerComponent") [node name="Global Light" parent="." instance=ExtResource("3_3732a")] @@ -83,7 +73,8 @@ limit_top = -10000000 limit_right = 10000000 limit_bottom = 10000000 -[node name="PhantomCamera2D" type="Node2D" parent="." node_paths=PackedStringArray("follow_target")] +[node name="PhantomCamera" type="Node2D" parent="." node_paths=PackedStringArray("follow_target")] +unique_name_in_owner = true top_level = true script = ExtResource("6_6imqp") follow_mode = 2 @@ -106,18 +97,11 @@ tile_set = ExtResource("10_8xrv5") [node name="Foreground layer" type="TileMapLayer" parent="."] tile_set = ExtResource("11_0vx27") -[node name="ConsoleManagement" type="Node" parent="." node_paths=PackedStringArray("player_health", "skill_unlocker", "skill_manager")] -script = ExtResource("12_8xrv5") -player_health = NodePath("../Brick Player/HealthComponent") -skill_unlocker = NodePath("../Brick Player/SkillUnlockerComponent") -skill_manager = NodePath("../Brick Player/SkillManager") -metadata/_custom_type_script = "uid://8r1y8elyw7kt" - [node name="Killzone" parent="." instance=ExtResource("13_0vx27")] position = Vector2(215, 324) -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="on_player_death"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="on_player_death"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] [editable path="Brick Player"] [editable path="UI Layer"] diff --git a/objects/level/ui_layer.tscn b/objects/level/ui_layer.tscn index 64549a3..38e0d84 100644 --- a/objects/level/ui_layer.tscn +++ b/objects/level/ui_layer.tscn @@ -1,8 +1,7 @@ -[gd_scene load_steps=11 format=3 uid="uid://6foggu31cu14"] +[gd_scene load_steps=9 format=3 uid="uid://6foggu31cu14"] [ext_resource type="PackedScene" uid="uid://byxf45ukq82pe" path="res://objects/ui/hud.tscn" id="1_tgtfe"] [ext_resource type="PackedScene" uid="uid://dulkm3ah4tm0u" path="res://objects/ui/death_screen.tscn" id="2_ln68j"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="3_5kt5k"] [ext_resource type="PackedScene" uid="uid://wmw6gaisyrvx" path="res://objects/ui/game_over_screen.tscn" id="4_11xmk"] [ext_resource type="PackedScene" uid="uid://dlm2ri562fynd" path="res://objects/ui/marketplace.tscn" id="5_8e0cs"] [ext_resource type="PackedScene" uid="uid://i6mnjbjcoqe5" path="res://objects/ui/pause_menu.tscn" id="6_1q4vn"] @@ -10,38 +9,26 @@ [ext_resource type="PackedScene" uid="uid://cvfsbiy5ggrpg" path="res://objects/ui/input_settings.tscn" id="8_6pnu3"] [ext_resource type="PackedScene" uid="uid://b5fx1vdfky307" path="res://objects/ui/audio_settings.tscn" id="9_ln68j"] -[sub_resource type="Resource" id="Resource_75pfo"] -script = ExtResource("3_5kt5k") -level_name = "Village - 1" -scene_path = "" -metadata/_custom_type_script = "uid://cp68km8bykymb" - [node name="UI Layer" type="CanvasLayer"] [node name="HUD" parent="." instance=ExtResource("1_tgtfe")] -[node name="DeathScreen" parent="." node_paths=PackedStringArray("nodes_to_disable") instance=ExtResource("2_ln68j")] +[node name="DeathScreen" parent="." instance=ExtResource("2_ln68j")] visible = false offset_top = 32.0 -current_level = SubResource("Resource_75pfo") -nodes_to_disable = [null] [node name="GameOverScreen" parent="." instance=ExtResource("4_11xmk")] visible = false -[node name="Marketplace" parent="." node_paths=PackedStringArray("components_to_disable") instance=ExtResource("5_8e0cs")] +[node name="Marketplace" parent="." instance=ExtResource("5_8e0cs")] visible = false offset_top = 32.0 -components_to_disable = [null] -[node name="Pause menu" parent="." node_paths=PackedStringArray("settings_menu") instance=ExtResource("6_1q4vn")] +[node name="Pause menu" parent="." instance=ExtResource("6_1q4vn")] visible = false -settings_menu = NodePath("../Settings menu") -[node name="Settings menu" parent="." node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("7_hkjav")] +[node name="Settings menu" parent="." instance=ExtResource("7_hkjav")] visible = false -input_settings = NodePath("../Input Settings") -audio_settings = NodePath("../Audio settings") [node name="Input Settings" parent="." instance=ExtResource("8_6pnu3")] visible = false diff --git a/objects/player_skills/brick_throw_skill.tscn b/objects/player_skills/brick_throw_skill.tscn index e8ad897..79e6063 100644 --- a/objects/player_skills/brick_throw_skill.tscn +++ b/objects/player_skills/brick_throw_skill.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=4 format=3 uid="uid://coayig4dxelo2"] -[ext_resource type="Script" uid="uid://cm06xg1l3xtw5" path="res://scripts/components/brick_throw.gd" id="1_hniwk"] +[ext_resource type="Script" uid="uid://b0bv8kw5w5037" path="res://scripts/components/BrickThrowComponent.cs" id="1_ogpnm"] [ext_resource type="PackedScene" uid="uid://bymro4t7angv5" path="res://objects/entities/brick.tscn" id="2_4txoq"] [ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="3_gxahf"] [node name="BrickThrowComponent" type="Node"] -script = ExtResource("1_hniwk") -brick_scene = ExtResource("2_4txoq") -fire_rate = 0.3 -throw_input_behavior = ExtResource("3_gxahf") +script = ExtResource("1_ogpnm") +BrickScene = ExtResource("2_4txoq") +FireRate = 0.6 +ThrowInputBehavior = ExtResource("3_gxahf") diff --git a/objects/player_skills/exploding_brick_throw_skill.tscn b/objects/player_skills/exploding_brick_throw_skill.tscn new file mode 100644 index 0000000..d2c556d --- /dev/null +++ b/objects/player_skills/exploding_brick_throw_skill.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=4 format=3 uid="uid://cfses3kn3y8qw"] + +[ext_resource type="Script" uid="uid://b0bv8kw5w5037" path="res://scripts/components/BrickThrowComponent.cs" id="1_xj8hl"] +[ext_resource type="PackedScene" uid="uid://5surx230gfw3" path="res://objects/entities/exploding_brick.tscn" id="2_xj8hl"] +[ext_resource type="Resource" uid="uid://diuv6pr6a0dup" path="res://resources/throw_behaviors/charge_throw_input.tres" id="3_xj8hl"] + +[node name="BrickThrowComponent" type="Node"] +script = ExtResource("1_xj8hl") +BrickScene = ExtResource("2_xj8hl") +FireRate = 0.6 +ThrowInputBehavior = ExtResource("3_xj8hl") diff --git a/objects/player_skills/fire_brick_throw_skill.tscn b/objects/player_skills/fire_brick_throw_skill.tscn new file mode 100644 index 0000000..e6428e3 --- /dev/null +++ b/objects/player_skills/fire_brick_throw_skill.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=4 format=3 uid="uid://dfm2you3v5ap4"] + +[ext_resource type="Script" uid="uid://b0bv8kw5w5037" path="res://scripts/components/BrickThrowComponent.cs" id="1_rvtv7"] +[ext_resource type="PackedScene" uid="uid://daau4j5hbklk0" path="res://objects/entities/fire_brick.tscn" id="2_rvtv7"] +[ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="3_rr0fu"] + +[node name="BrickThrowComponent" type="Node"] +script = ExtResource("1_rvtv7") +BrickScene = ExtResource("2_rvtv7") +FireRate = 0.3 +ThrowInputBehavior = ExtResource("3_rr0fu") diff --git a/objects/player_skills/ice_brick_throw_skill.tscn b/objects/player_skills/ice_brick_throw_skill.tscn new file mode 100644 index 0000000..ab4134d --- /dev/null +++ b/objects/player_skills/ice_brick_throw_skill.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=4 format=3 uid="uid://dpmmacva7qf8j"] + +[ext_resource type="Script" uid="uid://b0bv8kw5w5037" path="res://scripts/components/BrickThrowComponent.cs" id="1_wjj0s"] +[ext_resource type="PackedScene" uid="uid://bcmx07k12gcsc" path="res://objects/entities/ice_brick.tscn" id="2_wjj0s"] +[ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="3_44pyn"] + +[node name="BrickThrowComponent" type="Node"] +script = ExtResource("1_wjj0s") +BrickScene = ExtResource("2_wjj0s") +FireRate = 0.3 +ThrowInputBehavior = ExtResource("3_44pyn") diff --git a/objects/player_skills/magnetic_skill.tscn b/objects/player_skills/magnetic_skill.tscn index 296a9a0..ab4fdc4 100644 --- a/objects/player_skills/magnetic_skill.tscn +++ b/objects/player_skills/magnetic_skill.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://cunyndudjh2he"] -[ext_resource type="Script" uid="uid://ce8w71vgv37pt" path="res://scripts/components/magnetic_skill.gd" id="1_lwbas"] +[ext_resource type="Script" uid="uid://bi5nx8s1gisbd" path="res://scripts/components/MagneticSkillComponent.cs" id="1_xua8f"] [node name="MagneticSkill" type="Node"] -script = ExtResource("1_lwbas") +script = ExtResource("1_xua8f") diff --git a/objects/tooltip.tscn b/objects/tooltip.tscn index dc2020b..ea52ca6 100644 --- a/objects/tooltip.tscn +++ b/objects/tooltip.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=5 format=3 uid="uid://b4pdt1gv2ymyi"] -[ext_resource type="Script" uid="uid://bsq5pplxqbssh" path="res://scripts/components/tooltip_component.gd" id="1_kkpqq"] +[ext_resource type="Script" uid="uid://cvaa6aqyijcp1" path="res://scripts/components/TooltipComponent.cs" id="1_aw8h8"] [ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_yy7uq"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_hkd8b"] @@ -9,13 +9,13 @@ size = Vector2(38, 22) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lqw6h"] bg_color = Color(0, 0, 0, 1) -[node name="Tooltip" type="Area2D" node_paths=PackedStringArray("area2d", "ui_root", "tooltip_label")] +[node name="Tooltip" type="Area2D" node_paths=PackedStringArray("Area", "UiRoot", "TooltipLabel")] collision_layer = 0 collision_mask = 4 -script = ExtResource("1_kkpqq") -area2d = NodePath(".") -ui_root = NodePath("Panel") -tooltip_label = NodePath("Panel/PanelContainer/Label") +script = ExtResource("1_aw8h8") +Area = NodePath(".") +UiRoot = NodePath("Panel") +TooltipLabel = NodePath("Panel/PanelContainer/Label") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] position = Vector2(1, -3) diff --git a/objects/ui/audio_settings.tscn b/objects/ui/audio_settings.tscn index 219ae27..b84e76d 100644 --- a/objects/ui/audio_settings.tscn +++ b/objects/ui/audio_settings.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=3 format=3 uid="uid://b5fx1vdfky307"] -[ext_resource type="Script" uid="uid://dujk6pnftm7ra" path="res://scripts/ui/audio_settings.gd" id="1_g522b"] +[ext_resource type="Script" uid="uid://g61qqsymqfxd" path="res://scripts/UI/AudioSettings.cs" id="1_g522b"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ivv"] bg_color = Color(0, 0, 0, 1) -[node name="Audio settings" type="Control" node_paths=PackedStringArray("master_volume_slider", "music_volume_slider", "sfx_volume_slider", "audio_settings_control")] +[node name="Audio settings" type="Control" node_paths=PackedStringArray("MasterVolumeSlider", "MusicVolumeSlider", "SfxVolumeSlider", "AudioSettingsControl")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -15,10 +15,10 @@ grow_vertical = 2 size_flags_horizontal = 6 size_flags_vertical = 6 script = ExtResource("1_g522b") -master_volume_slider = NodePath("PanelContainer/MarginContainer/VBoxContainer/Master volume/HSlider") -music_volume_slider = NodePath("PanelContainer/MarginContainer/VBoxContainer/Music volume/HSlider") -sfx_volume_slider = NodePath("PanelContainer/MarginContainer/VBoxContainer/SFX volume/HSlider") -audio_settings_control = NodePath(".") +MasterVolumeSlider = NodePath("PanelContainer/MarginContainer/VBoxContainer/Master volume/HSlider") +MusicVolumeSlider = NodePath("PanelContainer/MarginContainer/VBoxContainer/Music volume/HSlider") +SfxVolumeSlider = NodePath("PanelContainer/MarginContainer/VBoxContainer/SFX volume/HSlider") +AudioSettingsControl = NodePath(".") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/charging_bar_layer.tscn b/objects/ui/charging_bar_layer.tscn index 9e2c4f7..cff0974 100644 --- a/objects/ui/charging_bar_layer.tscn +++ b/objects/ui/charging_bar_layer.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=4 format=3 uid="uid://bg76mtpcmfm2j"] -[ext_resource type="Script" uid="uid://q32rtephu1t1" path="res://scripts/ui/charge_progress_bar.gd" id="1_014ut"] +[ext_resource type="Script" uid="uid://dev2q1228otm2" path="res://scripts/UI/ChargeProgressBar.cs" id="1_014ut"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_arbva"] bg_color = Color(0, 0, 0, 1) @@ -8,10 +8,10 @@ bg_color = Color(0, 0, 0, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_014ut"] bg_color = Color(0.921569, 0.827451, 0.12549, 1) -[node name="ProgressBar" type="ProgressBar" node_paths=PackedStringArray("progress_bar")] +[node name="ProgressBar" type="ProgressBar" node_paths=PackedStringArray("ProgressBar")] offset_right = 40.0 offset_bottom = 10.0 theme_override_styles/background = SubResource("StyleBoxFlat_arbva") theme_override_styles/fill = SubResource("StyleBoxFlat_014ut") script = ExtResource("1_014ut") -progress_bar = NodePath(".") +ProgressBar = NodePath(".") diff --git a/objects/ui/credits.tscn b/objects/ui/credits.tscn index cc8a709..a247c01 100644 --- a/objects/ui/credits.tscn +++ b/objects/ui/credits.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://bwgmrcyj4mvu"] -[ext_resource type="Script" uid="uid://dtjgndehihl6d" path="res://scripts/ui/credits.gd" id="1_3aqj3"] +[ext_resource type="Script" uid="uid://daevj4uootmcw" path="res://scripts/UI/Credits.cs" id="1_3aqj3"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ivv"] bg_color = Color(0, 0, 0, 1) diff --git a/objects/ui/death_screen.tscn b/objects/ui/death_screen.tscn index 05a9e23..f4d7408 100644 --- a/objects/ui/death_screen.tscn +++ b/objects/ui/death_screen.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=4 format=3 uid="uid://dulkm3ah4tm0u"] -[ext_resource type="Script" uid="uid://b3d1p21sviww4" path="res://scripts/ui/death_screen.gd" id="1_eaoak"] +[ext_resource type="Script" uid="uid://csprksav8mj1h" path="res://scripts/UI/DeathScreen.cs" id="1_eaoak"] [ext_resource type="Texture2D" uid="uid://jix7wdn0isr3" path="res://sprites/ppc_icon.png" id="1_m30em"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_eaoak"] bg_color = Color(0, 0, 0, 1) -[node name="DeathScreen" type="Control" node_paths=PackedStringArray("death_screen_root", "current_level_label", "lives_left_label")] +[node name="DeathScreen" type="Control" node_paths=PackedStringArray("CurrentLevelLabel", "LivesLeftLabel")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -14,9 +14,8 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_eaoak") -death_screen_root = NodePath(".") -current_level_label = NodePath("PanelContainer/MarginContainer/VBoxContainer/LevelName") -lives_left_label = NodePath("PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/LivesLeft") +CurrentLevelLabel = NodePath("PanelContainer/MarginContainer/VBoxContainer/LevelName") +LivesLeftLabel = NodePath("PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/LivesLeft") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/game_over_screen.tscn b/objects/ui/game_over_screen.tscn index 2c751cc..9d891b3 100644 --- a/objects/ui/game_over_screen.tscn +++ b/objects/ui/game_over_screen.tscn @@ -1,21 +1,23 @@ -[gd_scene load_steps=3 format=3 uid="uid://wmw6gaisyrvx"] +[gd_scene load_steps=4 format=3 uid="uid://wmw6gaisyrvx"] -[ext_resource type="Script" uid="uid://bkd7o2u4psu4p" path="res://scripts/ui/game_over_screen.gd" id="1_v1o0m"] +[ext_resource type="Script" uid="uid://u4qfsx4w72dv" path="res://scripts/UI/GameOverScreen.cs" id="1_w4aho"] +[ext_resource type="PackedScene" uid="uid://cl00e2ocomk3m" path="res://scenes/main_menu.tscn" id="2_brh6h"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mtg1b"] bg_color = Color(0, 0, 0, 1) -[node name="GameOverScreen" type="Control" node_paths=PackedStringArray("game_over_screen", "restart_button", "main_menu_button")] +[node name="GameOverScreen" type="Control" node_paths=PackedStringArray("GameOverPanel", "RestartButton", "MainMenuButton")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -script = ExtResource("1_v1o0m") -game_over_screen = NodePath(".") -restart_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/RestartLevelButton") -main_menu_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/MenuButton") +script = ExtResource("1_w4aho") +GameOverPanel = NodePath(".") +RestartButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/RestartLevelButton") +MainMenuButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/MenuButton") +MainMenuScene = ExtResource("2_brh6h") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/hud.tscn b/objects/ui/hud.tscn index 8fea845..2065188 100644 --- a/objects/ui/hud.tscn +++ b/objects/ui/hud.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=8 format=3 uid="uid://byxf45ukq82pe"] [ext_resource type="LabelSettings" uid="uid://rvn5ivivfvv6" path="res://resources/ui/hud_label_settings.tres" id="1_4dsh5"] -[ext_resource type="Script" uid="uid://c3pde84b3kdco" path="res://scripts/ui/hud.gd" id="1_ueofj"] +[ext_resource type="Script" uid="uid://wfj674u4486f" path="res://scripts/UI/Hud.cs" id="1_lscdw"] [ext_resource type="FontFile" uid="uid://xm0vbusjr7b7" path="res://fonts/PressStart2P-Regular.ttf" id="1_ygmwt"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mmcdi"] @@ -16,7 +16,7 @@ bg_color = Color(0.47451, 0.47451, 0.47451, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_22dp1"] bg_color = Color(0.858824, 0.254902, 0.380392, 1) -[node name="HUD" type="Control" node_paths=PackedStringArray("coins_label", "health_progressbar", "lives_label")] +[node name="HUD" type="Control" node_paths=PackedStringArray("CoinsLabel", "HealthBar", "LivesLabel")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -25,10 +25,10 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 4 size_flags_vertical = 4 -script = ExtResource("1_ueofj") -coins_label = NodePath("PanelContainer/MarginContainer/HBoxContainer/Coins label") -health_progressbar = NodePath("PanelContainer/MarginContainer/HBoxContainer/ProgressBar") -lives_label = NodePath("PanelContainer/MarginContainer/HBoxContainer/Lives") +script = ExtResource("1_lscdw") +CoinsLabel = NodePath("PanelContainer/MarginContainer/HBoxContainer/Coins label") +HealthBar = NodePath("PanelContainer/MarginContainer/HBoxContainer/ProgressBar") +LivesLabel = NodePath("PanelContainer/MarginContainer/HBoxContainer/Lives") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/input_settings.tscn b/objects/ui/input_settings.tscn index 8c761c6..66d4451 100644 --- a/objects/ui/input_settings.tscn +++ b/objects/ui/input_settings.tscn @@ -1,21 +1,17 @@ -[gd_scene load_steps=4 format=3 uid="uid://cvfsbiy5ggrpg"] +[gd_scene load_steps=3 format=3 uid="uid://cvfsbiy5ggrpg"] [ext_resource type="PackedScene" uid="uid://bxpr4m7lq7clh" path="res://objects/ui/input_button.tscn" id="1_h8s4o"] -[ext_resource type="Script" uid="uid://dppwl7xie2mh" path="res://scripts/ui/input_settings.gd" id="1_v2gus"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_se25o"] bg_color = Color(0, 0, 0, 1) -[node name="Input Settings" type="Control" node_paths=PackedStringArray("action_list", "reset_to_default_button")] +[node name="Input Settings" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -script = ExtResource("1_v2gus") -action_list = NodePath("PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions") -reset_to_default_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Reset to default Button") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/main_menu.tscn b/objects/ui/main_menu.tscn index 3bdf935..293bfae 100644 --- a/objects/ui/main_menu.tscn +++ b/objects/ui/main_menu.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=3 format=3 uid="uid://8b6ol5sssbgo"] -[ext_resource type="Script" uid="uid://hyfvthdbgjbc" path="res://scripts/ui/main_menu.gd" id="1_epxpl"] +[ext_resource type="Script" uid="uid://bna3ggr6n7ycr" path="res://scripts/UI/MainMenu.cs" id="1_epxpl"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qv2q0"] bg_color = Color(0, 0, 0, 1) -[node name="MainMenu" type="Control" node_paths=PackedStringArray("main_menu_control", "new_game_button", "continue_button", "settings_button", "credits_button", "exit_button", "version_label")] +[node name="MainMenu" type="Control" node_paths=PackedStringArray("MainMenuControl", "NewGameButton", "ContinueButton", "SettingsButton", "CreditsButton", "ExitButton", "VersionLabel")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -13,13 +13,13 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_epxpl") -main_menu_control = NodePath(".") -new_game_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/NewGameButton") -continue_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/ContinueButton") -settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/SettingsButton") -credits_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/CreditsButton") -exit_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/QuitButton") -version_label = NodePath("PanelContainer/MarginContainer/VBoxContainer/version") +MainMenuControl = NodePath(".") +NewGameButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/NewGameButton") +ContinueButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/ContinueButton") +SettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/SettingsButton") +CreditsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/CreditsButton") +ExitButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/QuitButton") +VersionLabel = NodePath("PanelContainer/MarginContainer/VBoxContainer/VersionLabel") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 @@ -41,7 +41,7 @@ theme_override_constants/margin_bottom = 32 layout_mode = 2 theme_override_constants/separation = 16 -[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +[node name="TitleLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] layout_mode = 2 text = "GAME_TITLE" horizontal_alignment = 1 @@ -77,7 +77,7 @@ layout_mode = 2 text = "QUIT_BUTTON" flat = true -[node name="version" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +[node name="VersionLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] layout_mode = 2 size_flags_horizontal = 0 size_flags_vertical = 8 diff --git a/objects/ui/marketplace.tscn b/objects/ui/marketplace.tscn index 19e7f2d..a85490d 100644 --- a/objects/ui/marketplace.tscn +++ b/objects/ui/marketplace.tscn @@ -1,34 +1,33 @@ [gd_scene load_steps=12 format=3 uid="uid://dlm2ri562fynd"] -[ext_resource type="Script" uid="uid://duifmqjarjpuv" path="res://scripts/ui/marketplace.gd" id="1_ncpyg"] +[ext_resource type="Script" uid="uid://bnc16gndpl87i" path="res://scripts/UI/Marketplace.cs" id="1_li4x3"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="2_c7w2b"] [ext_resource type="FontFile" uid="uid://xm0vbusjr7b7" path="res://fonts/PressStart2P-Regular.ttf" id="2_ho1tm"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="2_pg4mc"] -[ext_resource type="Resource" uid="uid://dw5ee2lpeypnb" path="res://resources/skills/brick_throw.tres" id="3_bk5yi"] -[ext_resource type="Resource" uid="uid://cdp8sex36vdq2" path="res://resources/skills/explosive_brick.tres" id="4_tx0n3"] -[ext_resource type="Resource" uid="uid://2glvryih82t1" path="res://resources/skills/fire_brick.tres" id="5_fqx8e"] -[ext_resource type="Resource" uid="uid://cx5fsbexblp60" path="res://resources/skills/ice_brick.tres" id="6_6665y"] -[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="7_ukny6"] +[ext_resource type="Resource" uid="uid://dw5ee2lpeypnb" path="res://resources/skills/brick_throw.tres" id="3_1dl8j"] +[ext_resource type="Resource" uid="uid://cdp8sex36vdq2" path="res://resources/skills/explosive_brick.tres" id="4_qdv1c"] +[ext_resource type="Resource" uid="uid://cr5lo4h8wm0jc" path="res://resources/skills/fire_brick.tres" id="5_4m43l"] +[ext_resource type="Resource" uid="uid://ceakv6oqob6m7" path="res://resources/skills/ice_brick.tres" id="6_ialmx"] +[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="7_cycc1"] [ext_resource type="PackedScene" uid="uid://dtl03rod7l2t0" path="res://objects/ui/marketplace_button.tscn" id="9_ode10"] [ext_resource type="PackedScene" uid="uid://ceqjwmihj70lt" path="res://objects/ui/skill_button.tscn" id="10_c7w2b"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ode10"] bg_color = Color(0, 0, 0, 1) -[node name="Marketplace" type="Control" node_paths=PackedStringArray("root", "to_unlock_grid", "unlocked_grid")] +[node name="Marketplace" type="Control" node_paths=PackedStringArray("ToUnlockGrid", "UnlockedGrid")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -script = ExtResource("1_ncpyg") -root = NodePath(".") -skill_data = Array[ExtResource("2_pg4mc")]([ExtResource("3_bk5yi"), ExtResource("4_tx0n3"), ExtResource("5_fqx8e"), ExtResource("6_6665y"), ExtResource("7_ukny6")]) -to_unlock_grid = NodePath("PanelContainer/MarginContainer/VBoxContainer/ToUnlockGridContainer") -unlocked_grid = NodePath("PanelContainer/MarginContainer/VBoxContainer/UnlockedGridContainer") -font = ExtResource("2_ho1tm") -marketplace_button = ExtResource("9_ode10") -skill_button = ExtResource("10_c7w2b") +script = ExtResource("1_li4x3") +Skills = Array[ExtResource("2_c7w2b")]([ExtResource("3_1dl8j"), ExtResource("4_qdv1c"), ExtResource("5_4m43l"), ExtResource("6_ialmx"), ExtResource("7_cycc1")]) +ToUnlockGrid = NodePath("PanelContainer/MarginContainer/VBoxContainer/ToUnlockGridContainer") +UnlockedGrid = NodePath("PanelContainer/MarginContainer/VBoxContainer/UnlockedGridContainer") +Font = ExtResource("2_ho1tm") +MarketplaceButtonScene = ExtResource("9_ode10") +SkillButtonScene = ExtResource("10_c7w2b") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/marketplace_button.tscn b/objects/ui/marketplace_button.tscn index c045013..ec805dd 100644 --- a/objects/ui/marketplace_button.tscn +++ b/objects/ui/marketplace_button.tscn @@ -1,11 +1,9 @@ -[gd_scene load_steps=5 format=3 uid="uid://dtl03rod7l2t0"] +[gd_scene load_steps=3 format=3 uid="uid://dtl03rod7l2t0"] [ext_resource type="Texture2D" uid="uid://cvhoq7aubxlmq" path="res://sprites/ui/magnetic_skill_icon.png" id="1_5kqfg"] -[ext_resource type="Script" uid="uid://dx8lex40lotr5" path="res://scripts/ui/marketplace_button.gd" id="2_ulgvb"] -[ext_resource type="Texture2D" uid="uid://52adghxscdgy" path="res://sprites/locked_skill.png" id="2_vb2qn"] -[ext_resource type="Texture2D" uid="uid://rucyqmgrdld3" path="res://sprites/unlocked_skill.png" id="3_guyun"] +[ext_resource type="Script" uid="uid://vokgv56bjpf1" path="res://scripts/UI/MarketplaceButton.cs" id="2_vb2qn"] -[node name="MarketplaceButton" type="Button" node_paths=PackedStringArray("skill_level_container")] +[node name="MarketplaceButton" type="Button" node_paths=PackedStringArray("SkillLevelContainer")] offset_right = 8.0 offset_bottom = 8.0 size_flags_horizontal = 3 @@ -15,10 +13,8 @@ text = "Fire brick 100" icon = ExtResource("1_5kqfg") flat = true autowrap_mode = 2 -script = ExtResource("2_ulgvb") -unlocked_skill_icon = ExtResource("3_guyun") -locked_skill_icon = ExtResource("2_vb2qn") -skill_level_container = NodePath("HBoxContainer") +script = ExtResource("2_vb2qn") +SkillLevelContainer = NodePath("HBoxContainer") [node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/pause_menu.tscn b/objects/ui/pause_menu.tscn index d995ee9..be67a51 100644 --- a/objects/ui/pause_menu.tscn +++ b/objects/ui/pause_menu.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=4 format=3 uid="uid://i6mnjbjcoqe5"] -[ext_resource type="Script" uid="uid://cugifchx6jhuk" path="res://scripts/ui/pause_menu.gd" id="1_aktha"] +[ext_resource type="Script" uid="uid://cakgxndurgfa3" path="res://scripts/UI/PauseMenu.cs" id="1_h4pd5"] [ext_resource type="PackedScene" uid="uid://cl00e2ocomk3m" path="res://scenes/main_menu.tscn" id="2_h4pd5"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ivv"] bg_color = Color(0, 0, 0, 1) -[node name="Pause menu" type="Control" node_paths=PackedStringArray("pause_menu_control", "resume_button", "quit_button", "settings_button", "exit_to_menu_button")] +[node name="Pause menu" type="Control" node_paths=PackedStringArray("PauseMenuControl", "ResumeButton", "MainMenuButton", "QuitButton", "SettingsButton")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -15,13 +15,13 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 6 size_flags_vertical = 6 -script = ExtResource("1_aktha") -pause_menu_control = NodePath(".") -resume_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button") -quit_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Quit game Button") -settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button") -exit_to_menu_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button") -exit_to_menu_scene = ExtResource("2_h4pd5") +script = ExtResource("1_h4pd5") +PauseMenuControl = NodePath(".") +ResumeButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button") +MainMenuButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button") +QuitButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Quit game Button") +SettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button") +MainMenuScene = ExtResource("2_h4pd5") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 diff --git a/objects/ui/settings_menu.tscn b/objects/ui/settings_menu.tscn index 393c240..f5a956a 100644 --- a/objects/ui/settings_menu.tscn +++ b/objects/ui/settings_menu.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=3 format=3 uid="uid://y0ae6e7t70fj"] -[ext_resource type="Script" uid="uid://c506rigcjlm6x" path="res://scripts/ui/settings_menu.gd" id="1_lt6q2"] +[ext_resource type="Script" uid="uid://i8j47qnrytuo" path="res://scripts/UI/SettingsMenu.cs" id="1_lt6q2"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wgbcl"] bg_color = Color(0, 0, 0, 1) -[node name="Settings menu" type="Control" node_paths=PackedStringArray("settings_menu_control", "input_settings_button", "audio_settings_button", "display_settings_button", "gameplay_settings_button")] +[node name="Settings menu" type="Control" node_paths=PackedStringArray("SettingsMenuControl", "InputSettingsButton", "AudioSettingsButton", "DisplaySettingsButton", "GameplaySettingsButton")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -13,11 +13,11 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_lt6q2") -settings_menu_control = NodePath(".") -input_settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Input Settings Button") -audio_settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Audio Settings Button") -display_settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Display Settings Button") -gameplay_settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Gameplay Settings Button") +SettingsMenuControl = NodePath(".") +InputSettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Input Settings Button") +AudioSettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Audio Settings Button") +DisplaySettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Display Settings Button") +GameplaySettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Gameplay Settings Button") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 @@ -39,7 +39,7 @@ theme_override_constants/margin_bottom = 16 layout_mode = 2 theme_override_constants/separation = 16 -[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +[node name="Title" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 16 text = "SETTINGS" diff --git a/objects/ui/skill_button.tscn b/objects/ui/skill_button.tscn index 503f860..f7e892e 100644 --- a/objects/ui/skill_button.tscn +++ b/objects/ui/skill_button.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://ceqjwmihj70lt"] [ext_resource type="Texture2D" uid="uid://cvhoq7aubxlmq" path="res://sprites/ui/magnetic_skill_icon.png" id="1_wnfk3"] -[ext_resource type="Script" uid="uid://0obbehfd8fki" path="res://scripts/ui/skill_button.gd" id="2_m2732"] +[ext_resource type="Script" uid="uid://bw8dlgq86jrtt" path="res://scripts/UI/SkillButton.cs" id="2_m2732"] [node name="SkillButton" type="Button"] offset_right = 8.0 diff --git a/objects/ui_manager.tscn b/objects/ui_manager.tscn index 2898ece..46ffc4e 100644 --- a/objects/ui_manager.tscn +++ b/objects/ui_manager.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://dyjumgjkqrufa"] -[ext_resource type="Script" uid="uid://vmhlsc3fu3bn" path="res://autoloads/ui_manager.gd" id="1_53w4j"] +[ext_resource type="Script" uid="uid://c3ldmnrwperr4" path="res://Autoloads/UIManager.cs" id="1_53w4j"] [node name="UiManager" type="Node"] script = ExtResource("1_53w4j") diff --git a/project.godot b/project.godot index 42f3f74..dd84546 100644 --- a/project.godot +++ b/project.godot @@ -32,18 +32,21 @@ config/icon="uid://jix7wdn0isr3" GameManager="*res://objects/game_manager.tscn" PhantomCameraManager="*res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd" AudioController="*res://objects/audio_controller.tscn" -SteamIntegrationNode="*res://objects/steam_integration.tscn" -SteamControllerInput="*res://autoloads/steam_controller_input.gd" -Console="*res://addons/console/console.gd" -AchievementsManager="*res://objects/achievements.tscn" -ConfigFileHandler="*res://autoloads/config_file_handler.gd" -UiManager="*res://autoloads/ui_manager.gd" -SaveSystem="*res://autoloads/save_system.gd" +UIManager="*res://Autoloads/UIManager.cs" +ConfigFileHandler="*res://Autoloads/ConfigFileHandler.cs" +SaveSystem="*res://Autoloads/SaveSystem.cs" +ConsoleManager="*res://Autoloads/ConsoleManager.cs" +LimboConsole="*res://addons/limbo_console/limbo_console.gd" +DialogueManager="*res://addons/dialogue_manager/dialogue_manager.gd" [debug] file_logging/enable_file_logging=true +[dialogue_manager] + +runtime/advanced/uses_dotnet=true + [display] window/size/viewport_width=480 @@ -67,13 +70,14 @@ movie_writer/fps=24 [editor_plugins] -enabled=PackedStringArray("res://addons/console/plugin.cfg", "res://addons/phantom_camera/plugin.cfg") +enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg", "res://addons/limbo_console/plugin.cfg", "res://addons/phantom_camera/plugin.cfg") [global_group] coins="" levers="" player="" +Collectables="" [gui] @@ -163,6 +167,21 @@ screenshot={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194333,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +limbo_console_toggle={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +limbo_auto_complete_reverse={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +limbo_console_search_history={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} [internationalization] diff --git a/resources/collectables/big_coin.tres b/resources/collectables/big_coin.tres index 27c143f..ac377c8 100644 --- a/resources/collectables/big_coin.tres +++ b/resources/collectables/big_coin.tres @@ -1,8 +1,8 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://bsnr5v2b2mfsl"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://bsnr5v2b2mfsl"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_fudbo"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_wfbgp"] [resource] -script = ExtResource("1_fudbo") -amount = 5.0 -type = 0 +script = ExtResource("1_wfbgp") +Amount = 5.0 +Type = 0 diff --git a/resources/collectables/big_treasure.tres b/resources/collectables/big_treasure.tres index b483d64..d68a775 100644 --- a/resources/collectables/big_treasure.tres +++ b/resources/collectables/big_treasure.tres @@ -1,8 +1,8 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://b6xqotmke54x"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://b6xqotmke54x"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_037vi"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_5mosu"] [resource] -script = ExtResource("1_037vi") -amount = 100.0 -type = 0 +script = ExtResource("1_5mosu") +Amount = 50.0 +Type = 0 diff --git a/resources/collectables/child.tres b/resources/collectables/child.tres index bd845d6..b8bc1c8 100644 --- a/resources/collectables/child.tres +++ b/resources/collectables/child.tres @@ -1,8 +1,8 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://b6apusc0jmi3x"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://b6apusc0jmi3x"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_2d5tb"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_d8txc"] [resource] -script = ExtResource("1_2d5tb") -amount = 1.0 -type = 1 +script = ExtResource("1_d8txc") +Amount = 1.0 +Type = 1 diff --git a/resources/collectables/coin.tres b/resources/collectables/coin.tres index 2e41d77..e0e2646 100644 --- a/resources/collectables/coin.tres +++ b/resources/collectables/coin.tres @@ -1,8 +1,8 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://vql535ckoeqm"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://vql535ckoeqm"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_veemo"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_7pquc"] [resource] -script = ExtResource("1_veemo") -amount = 1.0 -type = 0 +script = ExtResource("1_7pquc") +Amount = 1.0 +Type = 0 diff --git a/resources/collectables/small_health_potion.tres b/resources/collectables/small_health_potion.tres index a36a276..c69e319 100644 --- a/resources/collectables/small_health_potion.tres +++ b/resources/collectables/small_health_potion.tres @@ -1,9 +1,9 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://2tl3yoh202no"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://2tl3yoh202no"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_brkhb"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_brkhb"] [resource] script = ExtResource("1_brkhb") -amount = 0.25 -type = 2 +Amount = 0.25 +Type = 2 metadata/_custom_type_script = "uid://cb5f0mx0hrt3b" diff --git a/resources/collectables/treasure.tres b/resources/collectables/treasure.tres index f71b1e7..3c72b0f 100644 --- a/resources/collectables/treasure.tres +++ b/resources/collectables/treasure.tres @@ -1,8 +1,8 @@ -[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://bws2xldndlre1"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://bws2xldndlre1"] -[ext_resource type="Script" uid="uid://cb5f0mx0hrt3b" path="res://scripts/resources/collectable_resource.gd" id="1_w50p5"] +[ext_resource type="Script" uid="uid://gptsgaw3agkf" path="res://scripts/Resources/CollectableResource.cs" id="1_clntw"] [resource] -script = ExtResource("1_w50p5") -amount = 50.0 -type = 0 +script = ExtResource("1_clntw") +Amount = 50.0 +Type = 0 diff --git a/resources/levels/forest/forest_1.tres b/resources/levels/forest/forest_1.tres index 9e0e055..b32ddd8 100644 --- a/resources/levels/forest/forest_1.tres +++ b/resources/levels/forest/forest_1.tres @@ -1,9 +1,9 @@ [gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://dsq0y2iw7tfmk"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="1_8mmd7"] +[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_8mmd7"] [resource] script = ExtResource("1_8mmd7") -level_name = "LEVEL_5_NAME" -scene_path = "res://scenes/level_forest_5.tscn" +LevelName = "LEVEL_5_NAME" +ScenePath = "res://scenes/level_forest_5.tscn" metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/levels/village/village_1.tres b/resources/levels/village/village_1.tres index 19a5180..2833437 100644 --- a/resources/levels/village/village_1.tres +++ b/resources/levels/village/village_1.tres @@ -1,10 +1,10 @@ [gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://cqtalsov2bkpo"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="2_a2hpx"] +[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_72j7f"] [resource] resource_local_to_scene = true -script = ExtResource("2_a2hpx") -level_name = "LEVEL_1_NAME" -scene_path = "res://scenes/level_village_1.tscn" +script = ExtResource("1_72j7f") +LevelName = "LEVEL_1_NAME" +ScenePath = "res://scenes/level_village_1.tscn" metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/levels/village/village_2.tres b/resources/levels/village/village_2.tres index 4f20274..6ded9e6 100644 --- a/resources/levels/village/village_2.tres +++ b/resources/levels/village/village_2.tres @@ -1,9 +1,9 @@ [gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://dlwkbjw1l33uq"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="1_1tjmh"] +[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_msres"] [resource] -script = ExtResource("1_1tjmh") -level_name = "LEVEL_2_NAME" -scene_path = "res://scenes/level_village_2.tscn" +script = ExtResource("1_msres") +LevelName = "LEVEL_2_NAME" +ScenePath = "res://scenes/level_village_2.tscn" metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/levels/village/village_3.tres b/resources/levels/village/village_3.tres index 2eb5311..0d3176e 100644 --- a/resources/levels/village/village_3.tres +++ b/resources/levels/village/village_3.tres @@ -1,9 +1,9 @@ [gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://b63u5qfp8p7pv"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="1_3v5yj"] +[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_6vcr8"] [resource] -script = ExtResource("1_3v5yj") -level_name = "LEVEL_3_NAME" -scene_path = "res://scenes/level_village_3.tscn" +script = ExtResource("1_6vcr8") +LevelName = "LEVEL_3_NAME" +ScenePath = "res://scenes/level_village_3.tscn" metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/levels/village/village_4.tres b/resources/levels/village/village_4.tres index 0237690..6ed1da3 100644 --- a/resources/levels/village/village_4.tres +++ b/resources/levels/village/village_4.tres @@ -1,9 +1,9 @@ [gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://cxfei28yu3buh"] -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="1_upuk8"] +[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_h4twv"] [resource] -script = ExtResource("1_upuk8") -level_name = "LEVEL_4_NAME" -scene_path = "res://scenes/level_village_4.tscn" +script = ExtResource("1_h4twv") +LevelName = "LEVEL_4_NAME" +ScenePath = "res://scenes/level_village_4.tscn" metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/levels/village/village_5.tres b/resources/levels/village/village_5.tres deleted file mode 100644 index c0d2df3..0000000 --- a/resources/levels/village/village_5.tres +++ /dev/null @@ -1,9 +0,0 @@ -[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://bhs2j7k0ety3a"] - -[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="1_owe0p"] - -[resource] -script = ExtResource("1_owe0p") -level_name = "LEVEL_5_NAME" -scene_path = "" -metadata/_custom_type_script = "uid://cp68km8bykymb" diff --git a/resources/skills/brick_throw.tres b/resources/skills/brick_throw.tres index 661a69e..e88e85d 100644 --- a/resources/skills/brick_throw.tres +++ b/resources/skills/brick_throw.tres @@ -1,24 +1,18 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=5 format=3 uid="uid://dw5ee2lpeypnb"] +[gd_resource type="Resource" load_steps=6 format=3 uid="uid://dw5ee2lpeypnb"] [ext_resource type="PackedScene" uid="uid://coayig4dxelo2" path="res://objects/player_skills/brick_throw_skill.tscn" id="1_5gnea"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="1_etxe2"] -[ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="1_xwijh"] [ext_resource type="Texture2D" uid="uid://dxtdwgg3po0eg" path="res://sprites/brick_power_Skill_icon.png" id="2_yimbq"] +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="3_lt17o"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="3_yimbq"] +[ext_resource type="Resource" uid="uid://d0qk7kh47b4pu" path="res://resources/skills/upgrades/brick_throw/upgrade_1.tres" id="4_nhovl"] [resource] -script = ExtResource("1_etxe2") -name = "BRICK_POWER" -description = "BRICK_POWER_DESCRIPTION" -node = ExtResource("1_5gnea") -config = { -"fire_rate": 0.6, -"player_controller": NodePath("."), -"throw_input_behavior": ExtResource("1_xwijh"), -"timer": NodePath("ThrowTimer") -} -cost = 50 -icon = ExtResource("2_yimbq") -type = 1 -is_active = false -level = 0 -max_level = 3 +script = ExtResource("3_yimbq") +Name = "BRICK_POWER" +Description = "BRICK_POWER_DESCRIPTION" +Icon = ExtResource("2_yimbq") +IsActive = false +Level = 1 +Type = 1 +Node = ExtResource("1_5gnea") +Upgrades = Array[ExtResource("3_lt17o")]([ExtResource("4_nhovl")]) diff --git a/resources/skills/explosive_brick.tres b/resources/skills/explosive_brick.tres index d7d00de..898d7d2 100644 --- a/resources/skills/explosive_brick.tres +++ b/resources/skills/explosive_brick.tres @@ -1,25 +1,18 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=6 format=3 uid="uid://cdp8sex36vdq2"] +[gd_resource type="Resource" load_steps=6 format=3 uid="uid://cdp8sex36vdq2"] -[ext_resource type="PackedScene" uid="uid://5surx230gfw3" path="res://objects/entities/exploding_brick.tscn" id="1_6pfoa"] -[ext_resource type="Resource" uid="uid://diuv6pr6a0dup" path="res://resources/throw_behaviors/charge_throw_input.tres" id="2_87gkt"] -[ext_resource type="PackedScene" uid="uid://coayig4dxelo2" path="res://objects/player_skills/brick_throw_skill.tscn" id="2_e0o8w"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="3_cgsq1"] +[ext_resource type="PackedScene" uid="uid://cfses3kn3y8qw" path="res://objects/player_skills/exploding_brick_throw_skill.tscn" id="2_gt8f6"] +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="3_txev8"] [ext_resource type="Texture2D" uid="uid://c0xtjfpmkfolk" path="res://sprites/explosive_brick_skill_icon.png" id="3_wkqmb"] +[ext_resource type="Resource" uid="uid://c50advf65oi8c" path="res://resources/skills/upgrades/explosive_brick/upgrade_1.tres" id="4_1x338"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="5_wkqmb"] [resource] -script = ExtResource("3_cgsq1") -name = "EXPLOSIVE_BRICK" -description = "EXPLOSIVE_BRICK_DESCRIPTION" -node = ExtResource("2_e0o8w") -config = { -"brick_scene": ExtResource("1_6pfoa"), -"player_controller": NodePath("."), -"throw_input_behavior": ExtResource("2_87gkt"), -"timer": NodePath("ThrowTimer") -} -cost = 180 -icon = ExtResource("3_wkqmb") -type = 1 -is_active = false -level = 0 -max_level = 1 +script = ExtResource("5_wkqmb") +Name = "EXPLOSIVE_BRICK" +Description = "EXPLOSIVE_BRICK_DESCRIPTION" +Icon = ExtResource("3_wkqmb") +IsActive = false +Level = 0 +Type = 1 +Node = ExtResource("2_gt8f6") +Upgrades = Array[ExtResource("3_txev8")]([ExtResource("4_1x338")]) diff --git a/resources/skills/fire_brick.tres b/resources/skills/fire_brick.tres index eef73dd..adb596d 100644 --- a/resources/skills/fire_brick.tres +++ b/resources/skills/fire_brick.tres @@ -1,25 +1,18 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=6 format=3 uid="uid://2glvryih82t1"] +[gd_resource type="Resource" load_steps=6 format=3 uid="uid://cr5lo4h8wm0jc"] -[ext_resource type="PackedScene" uid="uid://daau4j5hbklk0" path="res://objects/entities/fire_brick.tscn" id="1_2g43l"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="1_2j5ko"] -[ext_resource type="PackedScene" uid="uid://coayig4dxelo2" path="res://objects/player_skills/brick_throw_skill.tscn" id="1_g53fp"] -[ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="2_dm5pj"] +[ext_resource type="PackedScene" uid="uid://dfm2you3v5ap4" path="res://objects/player_skills/fire_brick_throw_skill.tscn" id="2_d6y6i"] +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="3_1ooyb"] [ext_resource type="Texture2D" uid="uid://cocbnr38qsikt" path="res://sprites/fire_brick_skill_icon.png" id="3_w87qb"] +[ext_resource type="Resource" uid="uid://dmfiqqjmfa7m3" path="res://resources/skills/upgrades/fire_brick/upgrade_1.tres" id="4_aj7vs"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="5_us7vb"] [resource] -script = ExtResource("1_2j5ko") -name = "FIRE_BRICK" -description = "FIRE_BIRCK_DESCRIPTION" -node = ExtResource("1_g53fp") -config = { -"brick_scene": ExtResource("1_2g43l"), -"player_controller": NodePath("."), -"throw_input_behavior": ExtResource("2_dm5pj"), -"timer": NodePath("ThrowTimer") -} -cost = 150 -icon = ExtResource("3_w87qb") -type = 1 -is_active = false -level = 0 -max_level = 3 +script = ExtResource("5_us7vb") +Name = "FIRE_BRICK" +Description = "FIRE_BIRCK_DESCRIPTION" +Icon = ExtResource("3_w87qb") +IsActive = false +Level = 0 +Type = 1 +Node = ExtResource("2_d6y6i") +Upgrades = Array[ExtResource("3_1ooyb")]([ExtResource("4_aj7vs")]) diff --git a/resources/skills/ice_brick.tres b/resources/skills/ice_brick.tres index 2e37114..40802a0 100644 --- a/resources/skills/ice_brick.tres +++ b/resources/skills/ice_brick.tres @@ -1,25 +1,18 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=6 format=3 uid="uid://cx5fsbexblp60"] +[gd_resource type="Resource" load_steps=6 format=3 uid="uid://ceakv6oqob6m7"] -[ext_resource type="PackedScene" uid="uid://bcmx07k12gcsc" path="res://objects/entities/ice_brick.tscn" id="1_ci3d1"] -[ext_resource type="PackedScene" uid="uid://coayig4dxelo2" path="res://objects/player_skills/brick_throw_skill.tscn" id="1_rflri"] -[ext_resource type="Resource" uid="uid://br84dsfa3ti04" path="res://resources/throw_behaviors/tap_throw_input.tres" id="2_hsgyv"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="2_pspkt"] +[ext_resource type="PackedScene" uid="uid://dpmmacva7qf8j" path="res://objects/player_skills/ice_brick_throw_skill.tscn" id="2_gm1ka"] [ext_resource type="Texture2D" uid="uid://c1qaxspv8aemf" path="res://sprites/ice_brick_skill_icon.png" id="3_6btth"] +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="3_e3cb0"] +[ext_resource type="Resource" uid="uid://myr57ylobqkn" path="res://resources/skills/upgrades/ice_brick/upgrade_1.tres" id="4_3pbub"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="5_57pl3"] [resource] -script = ExtResource("2_pspkt") -name = "ICE_BRICK" -description = "ICE_BRICK_DESCRIPTION" -node = ExtResource("1_rflri") -config = { -"brick_scene": ExtResource("1_ci3d1"), -"player_controller": NodePath("."), -"throw_input_behavior": ExtResource("2_hsgyv"), -"timer": NodePath("ThrowTimer") -} -cost = 150 -icon = ExtResource("3_6btth") -type = 1 -is_active = false -level = 0 -max_level = 3 +script = ExtResource("5_57pl3") +Name = "ICE_BRICK" +Description = "ICE_BRICK_DESCRIPTION" +Icon = ExtResource("3_6btth") +IsActive = false +Level = 0 +Type = 1 +Node = ExtResource("2_gm1ka") +Upgrades = Array[ExtResource("3_e3cb0")]([ExtResource("4_3pbub")]) diff --git a/resources/skills/magnetic.tres b/resources/skills/magnetic.tres index f6cea5f..1f69f9d 100644 --- a/resources/skills/magnetic.tres +++ b/resources/skills/magnetic.tres @@ -1,22 +1,18 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=4 format=3 uid="uid://d3bjre2etov1n"] +[gd_resource type="Resource" load_steps=6 format=3 uid="uid://d3bjre2etov1n"] [ext_resource type="Texture2D" uid="uid://cvhoq7aubxlmq" path="res://sprites/ui/magnetic_skill_icon.png" id="1_16qcg"] [ext_resource type="PackedScene" uid="uid://cunyndudjh2he" path="res://objects/player_skills/magnetic_skill.tscn" id="1_er41s"] -[ext_resource type="Script" uid="uid://bya240e627ti6" path="res://scripts/resources/skill_data.gd" id="1_r01oq"] +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="3_0j4wg"] +[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="3_htb6q"] +[ext_resource type="Resource" uid="uid://dte14sxesjm3y" path="res://resources/skills/upgrades/magnetic/upgrade_1.tres" id="4_0hjrs"] [resource] -script = ExtResource("1_r01oq") -name = "MAGNETIC" -description = "MAGNETIC_DESCRIPTION" -node = ExtResource("1_er41s") -config = { -"magnetic_area": NodePath("MagneticArea"), -"magnetic_move_duration": 1.25, -"root": NodePath(".") -} -cost = 70 -icon = ExtResource("1_16qcg") -type = 2 -is_active = false -level = 1 -max_level = 1 +script = ExtResource("3_htb6q") +Name = "MAGNETIC" +Description = "MAGNETIC_DESCRIPTION" +Icon = ExtResource("1_16qcg") +IsActive = false +Level = 1 +Type = 2 +Node = ExtResource("1_er41s") +Upgrades = Array[ExtResource("3_0j4wg")]([ExtResource("4_0hjrs")]) diff --git a/resources/skills/upgrades/brick_throw/upgrade_1.tres b/resources/skills/upgrades/brick_throw/upgrade_1.tres new file mode 100644 index 0000000..6a69f6b --- /dev/null +++ b/resources/skills/upgrades/brick_throw/upgrade_1.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="SkillUpgrade" load_steps=2 format=3 uid="uid://d0qk7kh47b4pu"] + +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="1_1l84l"] + +[resource] +script = ExtResource("1_1l84l") +Cost = 50 +Description = "Upgrade Description" +Properties = Dictionary[String, Variant]({ +"FireRate": 0.6 +}) +metadata/_custom_type_script = "uid://dwb0e05pewcsn" diff --git a/resources/skills/upgrades/explosive_brick/upgrade_1.tres b/resources/skills/upgrades/explosive_brick/upgrade_1.tres new file mode 100644 index 0000000..cb3cbed --- /dev/null +++ b/resources/skills/upgrades/explosive_brick/upgrade_1.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="SkillUpgrade" load_steps=2 format=3 uid="uid://c50advf65oi8c"] + +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="1_0hsac"] + +[resource] +script = ExtResource("1_0hsac") +Cost = 240 +Description = "Upgrade Description" +Properties = Dictionary[String, Variant]({ +"FireRate": 0.6 +}) +metadata/_custom_type_script = "uid://dwb0e05pewcsn" diff --git a/resources/skills/upgrades/fire_brick/upgrade_1.tres b/resources/skills/upgrades/fire_brick/upgrade_1.tres new file mode 100644 index 0000000..afb38ca --- /dev/null +++ b/resources/skills/upgrades/fire_brick/upgrade_1.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="SkillUpgrade" load_steps=2 format=3 uid="uid://dmfiqqjmfa7m3"] + +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="1_fyb20"] + +[resource] +script = ExtResource("1_fyb20") +Cost = 150 +Description = "Upgrade Description" +Properties = Dictionary[String, Variant]({ +"FireRate": 0.6 +}) +metadata/_custom_type_script = "uid://dwb0e05pewcsn" diff --git a/resources/skills/upgrades/ice_brick/upgrade_1.tres b/resources/skills/upgrades/ice_brick/upgrade_1.tres new file mode 100644 index 0000000..d7cdd23 --- /dev/null +++ b/resources/skills/upgrades/ice_brick/upgrade_1.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="SkillUpgrade" load_steps=2 format=3 uid="uid://myr57ylobqkn"] + +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="1_bfa1e"] + +[resource] +script = ExtResource("1_bfa1e") +Cost = 140 +Description = "Upgrade Description" +Properties = Dictionary[String, Variant]({ +"FireRate": 0.6 +}) +metadata/_custom_type_script = "uid://dwb0e05pewcsn" diff --git a/resources/skills/upgrades/magnetic/upgrade_1.tres b/resources/skills/upgrades/magnetic/upgrade_1.tres new file mode 100644 index 0000000..4574706 --- /dev/null +++ b/resources/skills/upgrades/magnetic/upgrade_1.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="SkillUpgrade" load_steps=2 format=3 uid="uid://dte14sxesjm3y"] + +[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="1_dkk4n"] + +[resource] +script = ExtResource("1_dkk4n") +Cost = 70 +Description = "Upgrade Description" +Properties = Dictionary[String, Variant]({ +"FireRate": 0.6 +}) +metadata/_custom_type_script = "uid://dwb0e05pewcsn" diff --git a/resources/status_effect/fire.tres b/resources/status_effect/fire.tres index ba8ce8a..4b06418 100644 --- a/resources/status_effect/fire.tres +++ b/resources/status_effect/fire.tres @@ -1,9 +1,9 @@ -[gd_resource type="Resource" script_class="StatusEffectDataResource" load_steps=2 format=3 uid="uid://obodgnkhxuhe"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://obodgnkhxuhe"] -[ext_resource type="Script" uid="uid://cqq2stnfs1doo" path="res://scripts/resources/status_effect_data_resource.gd" id="1_r8ycx"] +[ext_resource type="Script" uid="uid://pw0pu6gb21y2" path="res://scripts/Resources/StatusEffectDataResource.cs" id="1_8qqd7"] [resource] -script = ExtResource("1_r8ycx") -effect_type = 1 -duration = 5.0 -damage_per_second = 0.25 +script = ExtResource("1_8qqd7") +Duration = 5.0 +DamagePerSecond = 0.25 +Type = 1 diff --git a/resources/status_effect/ice.tres b/resources/status_effect/ice.tres index 8bb963e..fbe3c0a 100644 --- a/resources/status_effect/ice.tres +++ b/resources/status_effect/ice.tres @@ -1,9 +1,9 @@ -[gd_resource type="Resource" script_class="StatusEffectDataResource" load_steps=2 format=3 uid="uid://02l4nbnf2aft"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://02l4nbnf2aft"] -[ext_resource type="Script" uid="uid://cqq2stnfs1doo" path="res://scripts/resources/status_effect_data_resource.gd" id="1_kqthd"] +[ext_resource type="Script" uid="uid://pw0pu6gb21y2" path="res://scripts/Resources/StatusEffectDataResource.cs" id="1_qjpq2"] [resource] -script = ExtResource("1_kqthd") -effect_type = 2 -duration = 3.0 -damage_per_second = 0.0 +script = ExtResource("1_qjpq2") +Duration = 3.0 +DamagePerSecond = 0.0 +Type = 2 diff --git a/resources/throw_behaviors/charge_throw_input.tres b/resources/throw_behaviors/charge_throw_input.tres index 7ca6d08..84f741e 100644 --- a/resources/throw_behaviors/charge_throw_input.tres +++ b/resources/throw_behaviors/charge_throw_input.tres @@ -1,11 +1,10 @@ -[gd_resource type="Resource" script_class="ChargeThrowInputResource" load_steps=2 format=3 uid="uid://diuv6pr6a0dup"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://diuv6pr6a0dup"] -[ext_resource type="Script" uid="uid://dl1kpll33o6mv" path="res://scripts/resources/charge_throw_input_resource.gd" id="1_6fb6k"] +[ext_resource type="Script" uid="uid://dtpdh4jp51jis" path="res://scripts/Resources/ChargeThrowInputResource.cs" id="1_6fb6k"] [resource] script = ExtResource("1_6fb6k") -min_power = 0.5 -max_power = 2.0 -max_charge_time = 1.5 -min_charge_duration = 0.1 -metadata/_custom_type_script = "uid://dl1kpll33o6mv" +MinPower = 0.5 +MaxPower = 2.0 +MaxChargeTime = 2.0 +MinChargeDuration = 0.1 diff --git a/resources/throw_behaviors/tap_throw_input.tres b/resources/throw_behaviors/tap_throw_input.tres index 7d4d1c7..59c2f49 100644 --- a/resources/throw_behaviors/tap_throw_input.tres +++ b/resources/throw_behaviors/tap_throw_input.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="TapThrowInputResource" load_steps=2 format=3 uid="uid://br84dsfa3ti04"] +[gd_resource type="Resource" load_steps=2 format=3 uid="uid://br84dsfa3ti04"] -[ext_resource type="Script" uid="uid://blvdnsyi287rf" path="res://scripts/resources/tap_throw_input_resource.gd" id="1_c51dp"] +[ext_resource type="Script" uid="uid://cx7ryqxemgs56" path="res://scripts/Resources/TapThrowInputResource.cs" id="1_c51dp"] [resource] script = ExtResource("1_c51dp") diff --git a/scenes/level_village_1.tscn b/scenes/level_village_1.tscn index 73e8824..d6f9f29 100644 --- a/scenes/level_village_1.tscn +++ b/scenes/level_village_1.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=4 uid="uid://bol7g83v2accs"] +[gd_scene load_steps=22 format=4 uid="uid://bol7g83v2accs"] [ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/entities/brick_player.tscn" id="1_dnj2y"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="2_1vw1j"] @@ -12,8 +12,6 @@ [ext_resource type="TileSet" uid="uid://cu2sx7qigrqnv" path="res://resources/tilesets/village/terain.tres" id="9_ttvjm"] [ext_resource type="TileSet" uid="uid://bc5a20s6kuy8e" path="res://resources/tilesets/village/entities.tres" id="10_ei558"] [ext_resource type="TileSet" uid="uid://bbppo0irxdmqy" path="res://resources/tilesets/village/foreground.tres" id="11_w7c5i"] -[ext_resource type="Script" uid="uid://8r1y8elyw7kt" path="res://scripts/console_management.gd" id="12_2tg3u"] -[ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="13_tvdre"] [ext_resource type="PackedScene" uid="uid://d0s2abysa86rq" path="res://objects/entities/child.tscn" id="15_vqevu"] [ext_resource type="PackedScene" uid="uid://12jnkdygpxwc" path="res://objects/entities/exit_level.tscn" id="16_chnw1"] [ext_resource type="PackedScene" uid="uid://b4pdt1gv2ymyi" path="res://objects/tooltip.tscn" id="18_4bhfj"] @@ -59,23 +57,30 @@ ease = 2 [node name="Brick Player" parent="." instance=ExtResource("1_dnj2y")] z_index = 10 -[node name="HitParticles" parent="Brick Player" index="27"] +[node name="HitParticles" parent="Brick Player" index="26"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="WorldEnvironment" parent="." instance=ExtResource("2_1vw1j")] [node name="UI Layer" parent="." instance=ExtResource("3_4fsls")] -[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")] -player_health = NodePath("../../Brick Player/HealthComponent") +[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("Health")] +Health = NodePath("../../Brick Player/HealthComponent") -[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] -current_level = ExtResource("4_onnch") -nodes_to_disable = [NodePath("../../Brick Player")] +[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("NodesToDisable")] +CurrentLevel = ExtResource("4_onnch") +NodesToDisable = [NodePath("../../Brick Player")] -[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("skill_unlocker", "components_to_disable")] -skill_unlocker = NodePath("../../Brick Player/SkillUnlockerComponent") -components_to_disable = [NodePath("../../Brick Player")] +[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("SkillUnlockerComponent", "ComponentsToDisable")] +SkillUnlockerComponent = NodePath("../../Brick Player/SkillUnlockerComponent") +ComponentsToDisable = [NodePath("../../Brick Player")] + +[node name="Pause menu" parent="UI Layer" index="4" node_paths=PackedStringArray("SettingsControl")] +SettingsControl = NodePath("../Settings menu") + +[node name="Settings menu" parent="UI Layer" index="5" node_paths=PackedStringArray("InputSettingsControl", "AudioSettingsControl")] +InputSettingsControl = NodePath("../Input Settings") +AudioSettingsControl = NodePath("../Audio settings") [node name="Global Light" parent="." instance=ExtResource("4_mc58c")] @@ -111,16 +116,6 @@ tile_set = ExtResource("10_ei558") [node name="Foreground layer" type="TileMapLayer" parent="."] tile_set = ExtResource("11_w7c5i") -[node name="ConsoleManagement" type="Node" parent="." node_paths=PackedStringArray("player_health", "skill_unlocker", "skill_manager")] -script = ExtResource("12_2tg3u") -player_health = NodePath("../Brick Player/HealthComponent") -skill_unlocker = NodePath("../Brick Player/SkillUnlockerComponent") -skill_manager = NodePath("../Brick Player/SkillManager") -metadata/_custom_type_script = "uid://8r1y8elyw7kt" - -[node name="Killzone" parent="." instance=ExtResource("13_tvdre")] -position = Vector2(215, 324) - [node name="Child" parent="." instance=ExtResource("15_vqevu")] position = Vector2(-162, -256) @@ -129,18 +124,18 @@ position = Vector2(1485, 25) [node name="Tooltip" parent="." instance=ExtResource("18_4bhfj")] position = Vector2(712, -343) -text = "LEVEL_1_TOOLTIP_1" +Text = "LEVEL_1_TOOLTIP_1" [node name="Tooltip2" parent="." instance=ExtResource("18_4bhfj")] position = Vector2(552, 25) -text = "LEVEL_1_TOOLTIP_2" +Text = "LEVEL_1_TOOLTIP_2" [node name="Tooltip3" parent="." instance=ExtResource("18_4bhfj")] position = Vector2(-59, -247) -text = "LEVEL_1_TOOLTIP_3" +Text = "LEVEL_1_TOOLTIP_3" -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="on_player_death"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="on_player_death"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] [editable path="Brick Player"] [editable path="UI Layer"] diff --git a/scenes/level_village_2.tscn b/scenes/level_village_2.tscn index cccb78a..88384c3 100644 --- a/scenes/level_village_2.tscn +++ b/scenes/level_village_2.tscn @@ -1,9 +1,8 @@ -[gd_scene load_steps=27 format=4 uid="uid://chqb11pfoqmeb"] +[gd_scene load_steps=24 format=4 uid="uid://chqb11pfoqmeb"] [ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/entities/brick_player.tscn" id="1_wcma7"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="2_ot3dy"] [ext_resource type="PackedScene" uid="uid://6foggu31cu14" path="res://objects/level/ui_layer.tscn" id="3_cjqhe"] -[ext_resource type="Resource" uid="uid://dlwkbjw1l33uq" path="res://resources/levels/village/village_2.tres" id="4_h4uvs"] [ext_resource type="PackedScene" uid="uid://cywsu7yrtjdog" path="res://objects/level/global_light.tscn" id="4_wykfl"] [ext_resource type="PackedScene" uid="uid://cb0mnye1ki5a6" path="res://objects/level/camera_2d.tscn" id="5_8nvkd"] [ext_resource type="Script" uid="uid://d23haq52m7ulv" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="6_ono4h"] @@ -12,9 +11,7 @@ [ext_resource type="TileSet" uid="uid://cu2sx7qigrqnv" path="res://resources/tilesets/village/terain.tres" id="9_lqbd7"] [ext_resource type="TileSet" uid="uid://bc5a20s6kuy8e" path="res://resources/tilesets/village/entities.tres" id="10_4r26m"] [ext_resource type="TileSet" uid="uid://bbppo0irxdmqy" path="res://resources/tilesets/village/foreground.tres" id="11_r0ngp"] -[ext_resource type="Script" uid="uid://8r1y8elyw7kt" path="res://scripts/console_management.gd" id="12_78lb3"] [ext_resource type="TileSet" uid="uid://ccffmjebvuoaj" path="res://resources/tilesets/village/small_foreground.tres" id="13_lc0ll"] -[ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="13_sw2hn"] [ext_resource type="PackedScene" uid="uid://12jnkdygpxwc" path="res://objects/entities/exit_level.tscn" id="15_hcsb6"] [ext_resource type="PackedScene" uid="uid://d0s2abysa86rq" path="res://objects/entities/child.tscn" id="16_lc0ll"] [ext_resource type="PackedScene" uid="uid://to2xnqev0pu1" path="res://objects/entities/cage.tscn" id="17_h4uvs"] @@ -62,23 +59,19 @@ ease = 2 [node name="Brick Player" parent="." instance=ExtResource("1_wcma7")] z_index = 1 -[node name="HitParticles" parent="Brick Player" index="27"] +[node name="HitParticles" parent="Brick Player" index="26"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="WorldEnvironment" parent="." instance=ExtResource("2_ot3dy")] [node name="UI Layer" parent="." instance=ExtResource("3_cjqhe")] -[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")] -player_health = NodePath("../../Brick Player/HealthComponent") +[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("Health")] +Health = NodePath("../../Brick Player/HealthComponent") -[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] -current_level = ExtResource("4_h4uvs") -nodes_to_disable = [NodePath("../../Brick Player")] - -[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("skill_unlocker", "components_to_disable")] -skill_unlocker = NodePath("../../Brick Player/SkillUnlockerComponent") -components_to_disable = [NodePath("../../Brick Player")] +[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("SkillUnlockerComponent", "ComponentsToDisable")] +SkillUnlockerComponent = NodePath("../../Brick Player/SkillUnlockerComponent") +ComponentsToDisable = [NodePath("../../Brick Player")] [node name="Global Light" parent="." instance=ExtResource("4_wykfl")] @@ -124,16 +117,6 @@ tile_set = ExtResource("11_r0ngp") tile_map_data = PackedByteArray("AABw/ykAAAABAAAAAABr/ykAAAAAAAAAAABn/ykAAAACAAAAAABl/ykAAAABAAAAAABj/ykAAAACAAAAAABh/ykAAAABAAAAAABa/ykAAAABAAAAAABY/ykAAAACAAAAAABU/ykAAAADAAAAAABP/ykAAAAAAAAAAACD/ykAAAABAAAAAACO/ykAAAACAAAAAACT/ykAAAABAAAAAACV/ykAAAAAAAAAAACa/ykAAAADAAAAAACq/ykAAAACAAAAAACr/ykAAAAAAAAAAADi/wMAAAAAAAAAAADk/wMAAAACAAAAAADp/wMAAAABAAAAAADs/wMAAAAAAAAAAADt/wMAAAABAAAAAADx/wMAAAADAAAAAADy/wMAAAAAAAAAAADz/wMAAAACAAAAAAD0/wMAAAADAAAAAAD5/wMAAAAAAAAAAAD6/wMAAAAAAAAAAAD7/wMAAAABAAAAAAACAAMAAAABAAAAAAAJAAMAAAABAAAAAAANAAMAAAAAAAAAAAAUAAMAAAABAAAAAAAYAAMAAAADAAAAAAAaAAMAAAADAAAAAAAcAAMAAAACAAAAAAAgAAMAAAAAAAAAAAAiAAMAAAACAAAAAAAmAAMAAAADAAAAAAAoAAMAAAAAAAAAAABGAAMAAAADAAAAAABEAAMAAAACAAAAAABCAAMAAAABAAAAAAA8AAMAAAACAAAAAAA3AAMAAAACAAAAAAA0AAMAAAADAAAAAAAxAAMAAAADAAAAAAAwAAMAAAACAAAAAAAfAAMAAAAAAAAAAAATAAMAAAAAAAAAAAASAAMAAAABAAAAAAAOAAMAAAAAAAAAAABWAAUAAAABAAAAAABZAAUAAAADAAAAAABaAAUAAAABAAAAAABkAAUAAAACAAAAAAB1AAUAAAADAAAAAAB2AAUAAAABAAAAAAB/AAUAAAADAAAAAACsAO//AAAAAAAAAACrAO//AAACAAAAAACnAO//AAACAAAAAACgAO//AAABAAAAAACVAO//AAACAAAAAACUAO//AAADAAAAAACTAO//AAACAAAAAACNAO//AAAAAAAAAACxANX/AAABAAAAAACzANX/AAACAAAAAAC7ANX/AAACAAAAAADBANX/AAACAAAAAADDANX/AAABAAAAAADMANX/AAABAAAAAADQANX/AAABAAAAAADXANX/AAABAAAAAADZANX/AAABAAAAAADdANX/AAAAAAAAAADfANX/AAACAAAAAADnANX/AAABAAAAAADoANX/AAACAAAAAACiAJ//AAACAAAAAACcAJ//AAADAAAAAACbAJ//AAAAAAAAAACYAJ//AAADAAAAAACSAJ//AAADAAAAAACJAJ//AAADAAAAAACGAJ//AAABAAAAAACEAJ//AAACAAAAAACAAJ//AAABAAAAAAB/AJ//AAADAAAAAAB9AJ//AAACAAAAAAA=") tile_set = ExtResource("13_lc0ll") -[node name="ConsoleManagement" type="Node" parent="." node_paths=PackedStringArray("player_health", "skill_unlocker", "skill_manager")] -script = ExtResource("12_78lb3") -player_health = NodePath("../Brick Player/HealthComponent") -skill_unlocker = NodePath("../Brick Player/SkillUnlockerComponent") -skill_manager = NodePath("../Brick Player/SkillManager") -metadata/_custom_type_script = "uid://8r1y8elyw7kt" - -[node name="Killzone" parent="." instance=ExtResource("13_sw2hn")] -position = Vector2(170, 582) - [node name="ExitLevel" parent="." instance=ExtResource("15_hcsb6")] position = Vector2(987, -776) @@ -175,8 +158,8 @@ position = Vector2(792, -784) [node name="Lever" parent="." instance=ExtResource("20_h4uvs")] position = Vector2(-231, -776) -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="on_player_death"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="on_player_death"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] [editable path="Brick Player"] [editable path="UI Layer"] diff --git a/scenes/level_village_3.tscn b/scenes/level_village_3.tscn index 879f963..5b1d61c 100644 --- a/scenes/level_village_3.tscn +++ b/scenes/level_village_3.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=33 format=4 uid="uid://h60obxmju6mo"] +[gd_scene load_steps=30 format=4 uid="uid://h60obxmju6mo"] [ext_resource type="PackedScene" uid="uid://dyp4i4ru2j2jh" path="res://objects/fxs/explosion_fx.tscn" id="1_p30ax"] [ext_resource type="PackedScene" uid="uid://dx80ivlvuuew4" path="res://objects/fxs/fire_fx.tscn" id="2_a7yjf"] @@ -16,10 +16,7 @@ [ext_resource type="Script" uid="uid://ccfft4b8rwgbo" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="14_k7w2w"] [ext_resource type="PackedScene" uid="uid://d0s2abysa86rq" path="res://objects/entities/child.tscn" id="15_dv6gh"] [ext_resource type="PackedScene" uid="uid://6foggu31cu14" path="res://objects/level/ui_layer.tscn" id="16_nr2eo"] -[ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="17_3xroh"] -[ext_resource type="Resource" uid="uid://b63u5qfp8p7pv" path="res://resources/levels/village/village_3.tres" id="17_a7yjf"] [ext_resource type="PackedScene" uid="uid://b4pdt1gv2ymyi" path="res://objects/tooltip.tscn" id="18_l3a7y"] -[ext_resource type="Script" uid="uid://8r1y8elyw7kt" path="res://scripts/console_management.gd" id="19_0pba3"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="20_embdf"] [ext_resource type="PackedScene" uid="uid://cywsu7yrtjdog" path="res://objects/level/global_light.tscn" id="21_fytod"] [ext_resource type="PackedScene" uid="uid://t6h2ra7kjyq" path="res://objects/entities/small_heal_potion.tscn" id="23_m6h4x"] @@ -297,7 +294,7 @@ z_index = 5 position = Vector2(903, -118) metadata/_edit_group_ = true -[node name="HitParticles" parent="Brick Player" index="27"] +[node name="HitParticles" parent="Brick Player" index="26"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="Camera2D" parent="." instance=ExtResource("12_qhkyq")] @@ -325,33 +322,18 @@ position = Vector2(880, -578) [node name="UI Layer" parent="." instance=ExtResource("16_nr2eo")] -[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")] -player_health = NodePath("../../Brick Player/HealthComponent") +[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("Health")] +Health = NodePath("../../Brick Player/HealthComponent") -[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] -current_level = ExtResource("17_a7yjf") -nodes_to_disable = [NodePath("../../Brick Player")] - -[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("skill_unlocker", "components_to_disable")] -skill_unlocker = NodePath("../../Brick Player/SkillUnlockerComponent") -components_to_disable = [NodePath("../../Brick Player")] - -[node name="Killzone" parent="." instance=ExtResource("17_3xroh")] -position = Vector2(2600, 802) +[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("SkillUnlockerComponent", "ComponentsToDisable")] +SkillUnlockerComponent = NodePath("../../Brick Player/SkillUnlockerComponent") +ComponentsToDisable = [NodePath("../../Brick Player")] [node name="Tooltip" parent="." instance=ExtResource("18_l3a7y")] position = Vector2(1016, -104) -text = "Press 'down' to fall from platform!" [node name="Tooltip2" parent="." instance=ExtResource("18_l3a7y")] position = Vector2(1576, -40) -text = "Be careful!" - -[node name="ConsoleManagement" type="Node" parent="." node_paths=PackedStringArray("player_health", "skill_unlocker", "skill_manager")] -script = ExtResource("19_0pba3") -player_health = NodePath("../Brick Player/HealthComponent") -skill_unlocker = NodePath("../Brick Player/SkillUnlockerComponent") -skill_manager = NodePath("../Brick Player/SkillManager") [node name="WorldEnvironment" parent="." instance=ExtResource("20_embdf")] @@ -366,9 +348,8 @@ position = Vector2(1244, -41) [node name="SmallHealPotion3" parent="." instance=ExtResource("23_m6h4x")] position = Vector2(1359, -42) -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="on_player_death"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="on_player_death"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] [editable path="Brick Player"] [editable path="UI Layer"] -[editable path="Killzone"] diff --git a/scenes/level_village_4.tscn b/scenes/level_village_4.tscn index 94c7f48..faa2604 100644 --- a/scenes/level_village_4.tscn +++ b/scenes/level_village_4.tscn @@ -1,11 +1,10 @@ -[gd_scene load_steps=37 format=4 uid="uid://bhad760x3vvco"] +[gd_scene load_steps=34 format=4 uid="uid://bhad760x3vvco"] [ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/entities/brick_player.tscn" id="1_k3uyd"] [ext_resource type="PackedScene" uid="uid://bon6raeddf3tu" path="res://objects/entities/chaser.tscn" id="1_xraal"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="2_a7hm7"] [ext_resource type="PackedScene" uid="uid://6foggu31cu14" path="res://objects/level/ui_layer.tscn" id="3_p8wo6"] [ext_resource type="PackedScene" uid="uid://cywsu7yrtjdog" path="res://objects/level/global_light.tscn" id="4_0lkg8"] -[ext_resource type="Resource" uid="uid://cxfei28yu3buh" path="res://resources/levels/village/village_4.tres" id="5_iqtv2"] [ext_resource type="PackedScene" uid="uid://cb0mnye1ki5a6" path="res://objects/level/camera_2d.tscn" id="5_yqj3b"] [ext_resource type="Script" uid="uid://d23haq52m7ulv" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="6_i5rlu"] [ext_resource type="PackedScene" uid="uid://bd51frym6mm7v" path="res://objects/entities/lever.tscn" id="7_1bwe8"] @@ -14,8 +13,6 @@ [ext_resource type="TileSet" uid="uid://cu2sx7qigrqnv" path="res://resources/tilesets/village/terain.tres" id="9_bjo44"] [ext_resource type="TileSet" uid="uid://bc5a20s6kuy8e" path="res://resources/tilesets/village/entities.tres" id="10_gg8rp"] [ext_resource type="TileSet" uid="uid://bbppo0irxdmqy" path="res://resources/tilesets/village/foreground.tres" id="11_23awj"] -[ext_resource type="Script" uid="uid://8r1y8elyw7kt" path="res://scripts/console_management.gd" id="12_4o5b1"] -[ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="13_1heob"] [ext_resource type="PackedScene" uid="uid://lpovacvt3yyj" path="res://objects/entities/spaceship_enter.tscn" id="14_bajwe"] [ext_resource type="PackedScene" uid="uid://d0s2abysa86rq" path="res://objects/entities/child.tscn" id="15_3iocp"] [ext_resource type="PackedScene" uid="uid://xp4njljog0x2" path="res://objects/entities/flying_enemy.tscn" id="18_162yw"] @@ -115,25 +112,19 @@ texture = SubResource("CanvasTexture_o1yb3") [node name="Chaser" parent="." instance=ExtResource("1_xraal")] position = Vector2(578.405, 15.8615) -[node name="ChaseLevelComponent" parent="Chaser" index="0" node_paths=PackedStringArray("chase_target", "phantom_camera")] -chase_speed = 170.0 -chase_target = NodePath("../../Chase Target") -phantom_camera = NodePath("../../PhantomCamera2D") +[node name="ChaseLevelComponent" parent="Chaser" index="0" node_paths=PackedStringArray("ChaseTarget")] +ChaseTarget = NodePath("../../Chase Target") [node name="WorldEnvironment" parent="." instance=ExtResource("2_a7hm7")] [node name="UI Layer" parent="." instance=ExtResource("3_p8wo6")] -[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")] -player_health = NodePath("../../Brick Player/HealthComponent") +[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("Health")] +Health = NodePath("../../Brick Player/HealthComponent") -[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] -current_level = ExtResource("5_iqtv2") -nodes_to_disable = [NodePath("../../Brick Player")] - -[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("skill_unlocker", "components_to_disable")] -skill_unlocker = NodePath("../../Brick Player/SkillUnlockerComponent") -components_to_disable = [NodePath("../../Brick Player")] +[node name="Marketplace" parent="UI Layer" index="3" node_paths=PackedStringArray("SkillUnlockerComponent", "ComponentsToDisable")] +SkillUnlockerComponent = NodePath("../../Brick Player/SkillUnlockerComponent") +ComponentsToDisable = [NodePath("../../Brick Player")] [node name="Global Light" parent="." instance=ExtResource("4_0lkg8")] @@ -148,7 +139,8 @@ limit_top = -10000000 limit_right = 10000000 limit_bottom = 10000000 -[node name="PhantomCamera2D" type="Node2D" parent="." node_paths=PackedStringArray("follow_target")] +[node name="PhantomCamera" type="Node2D" parent="." node_paths=PackedStringArray("follow_target")] +unique_name_in_owner = true top_level = true script = ExtResource("6_i5rlu") follow_mode = 2 @@ -174,25 +166,12 @@ tile_set = ExtResource("10_gg8rp") [node name="Foreground layer" type="TileMapLayer" parent="."] tile_set = ExtResource("11_23awj") -[node name="ConsoleManagement" type="Node" parent="." node_paths=PackedStringArray("player_health", "skill_unlocker", "skill_manager")] -script = ExtResource("12_4o5b1") -player_health = NodePath("../Brick Player/HealthComponent") -skill_unlocker = NodePath("../Brick Player/SkillUnlockerComponent") -skill_manager = NodePath("../Brick Player/SkillManager") -metadata/_custom_type_script = "uid://8r1y8elyw7kt" - -[node name="Killzone" parent="." instance=ExtResource("13_1heob")] -position = Vector2(215, 324) - -[node name="Spaceship Enter" parent="." instance=ExtResource("14_bajwe")] -position = Vector2(559, 15) - [node name="Chase Target" type="Marker2D" parent="."] position = Vector2(7146.51, 21.1388) [node name="Brick Player" parent="." instance=ExtResource("1_k3uyd")] -[node name="HitParticles" parent="Brick Player" index="27"] +[node name="HitParticles" parent="Brick Player" index="26"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="Enemies" type="Node2D" parent="."] @@ -204,9 +183,6 @@ metadata/_edit_group_ = true [node name="Sprite2D" parent="Enemies/Flying Enemy" index="1"] flip_h = true -[node name="PeriodicShootingComponent" parent="Enemies/Flying Enemy" index="8"] -shoot_direction = Vector2(-1, 0) - [node name="HitParticles" parent="Enemies/Flying Enemy" index="15"] process_material = SubResource("ParticleProcessMaterial_qxfb0") @@ -220,9 +196,6 @@ metadata/_edit_group_ = true [node name="Sprite2D" parent="Enemies/Flying Enemy2" index="1"] flip_h = true -[node name="PeriodicShootingComponent" parent="Enemies/Flying Enemy2" index="8"] -shoot_direction = Vector2(-1, 0) - [node name="HitParticles" parent="Enemies/Flying Enemy2" index="15"] process_material = SubResource("ParticleProcessMaterial_qxfb0") @@ -236,9 +209,6 @@ metadata/_edit_group_ = true [node name="Sprite2D" parent="Enemies/Flying Enemy3" index="1"] flip_h = true -[node name="PeriodicShootingComponent" parent="Enemies/Flying Enemy3" index="8"] -shoot_direction = Vector2(-1, 0) - [node name="HitParticles" parent="Enemies/Flying Enemy3" index="15"] process_material = SubResource("ParticleProcessMaterial_qxfb0") @@ -252,9 +222,6 @@ metadata/_edit_group_ = true [node name="Sprite2D" parent="Enemies/Flying Enemy4" index="1"] flip_h = true -[node name="PeriodicShootingComponent" parent="Enemies/Flying Enemy4" index="8"] -shoot_direction = Vector2(-1, 0) - [node name="HitParticles" parent="Enemies/Flying Enemy4" index="15"] process_material = SubResource("ParticleProcessMaterial_qxfb0") @@ -281,38 +248,38 @@ metadata/_edit_group_ = true [node name="Sprite2D" parent="Enemies mid section path/PathFollow2D/Flying Enemy" index="1"] flip_h = true -[node name="PeriodicShootingComponent" parent="Enemies mid section path/PathFollow2D/Flying Enemy" index="8"] -shoot_direction = Vector2(-1, 0) - [node name="HitParticles" parent="Enemies mid section path/PathFollow2D/Flying Enemy" index="15"] process_material = SubResource("ParticleProcessMaterial_qxfb0") [node name="laser spawn point left" parent="Enemies mid section path/PathFollow2D/Flying Enemy" index="19"] position = Vector2(-12, -2) +[node name="Spaceship Enter" parent="." instance=ExtResource("14_bajwe")] +position = Vector2(559, 15) + [node name="Spaceship exit" parent="." instance=ExtResource("21_e6jir")] position = Vector2(7148, 22) [node name="CollisionShape2D" parent="Spaceship exit" index="0"] position = Vector2(16.5, -10) -[connection signal="spaceship_entered" from="Spaceship Enter/SpaceshipEnterComponent" to="Chaser/ChaseLevelComponent" method="on_ship_entered"] -[connection signal="spaceship_entered" from="Spaceship Enter/SpaceshipEnterComponent" to="Brick Player" method="on_spaceship_entered"] -[connection signal="spaceship_entered" from="Spaceship Enter/SpaceshipEnterComponent" to="Brick Player/Movements/PlatformMovement" method="on_ship_entered"] -[connection signal="spaceship_entered" from="Spaceship Enter/SpaceshipEnterComponent" to="Brick Player/ShipShooter" method="on_ship_entered"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="on_player_death"] -[connection signal="on_death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="on_player_death"] -[connection signal="spaceship_exited" from="Spaceship exit" to="Chaser/ChaseLevelComponent" method="on_ship_exited"] -[connection signal="spaceship_exited" from="Spaceship exit" to="Brick Player" method="on_spaceship_exited"] -[connection signal="spaceship_exited" from="Spaceship exit" to="Brick Player/ShipShooter" method="on_ship_exited"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] +[connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] +[connection signal="SpaceshipEntered" from="Spaceship Enter" to="Chaser/ChaseLevelComponent" method="OnShipEntered"] +[connection signal="SpaceshipEntered" from="Spaceship Enter" to="Brick Player" method="OnSpaceshipEntered"] +[connection signal="SpaceshipEntered" from="Spaceship Enter" to="Brick Player/Movements/PlatformMovement" method="OnShipEntered"] +[connection signal="SpaceshipEntered" from="Spaceship Enter" to="Brick Player/ShipShooter" method="OnShipEntered"] +[connection signal="SpaceshipExit" from="Spaceship exit" to="Chaser/ChaseLevelComponent" method="OnShipExited"] +[connection signal="SpaceshipExit" from="Spaceship exit" to="Brick Player" method="OnSpaceshipExited"] +[connection signal="SpaceshipExit" from="Spaceship exit" to="Brick Player/ShipShooter" method="OnShipExited"] [editable path="Chaser"] [editable path="UI Layer"] -[editable path="Spaceship Enter"] [editable path="Brick Player"] [editable path="Enemies/Flying Enemy"] [editable path="Enemies/Flying Enemy2"] [editable path="Enemies/Flying Enemy3"] [editable path="Enemies/Flying Enemy4"] [editable path="Enemies mid section path/PathFollow2D/Flying Enemy"] +[editable path="Spaceship Enter"] [editable path="Spaceship exit"] diff --git a/scenes/main_menu.tscn b/scenes/main_menu.tscn index b1dac87..35245bf 100644 --- a/scenes/main_menu.tscn +++ b/scenes/main_menu.tscn @@ -8,14 +8,16 @@ [node name="Main menu" type="CanvasLayer"] -[node name="MainMenu" parent="." node_paths=PackedStringArray("settings_control", "credits_control") instance=ExtResource("1_ekxnf")] -settings_control = NodePath("../Settings menu") -credits_control = NodePath("../Credits") +[node name="MainMenu" parent="." node_paths=PackedStringArray("SettingsControl", "CreditsControl") instance=ExtResource("1_ekxnf")] +SettingsControl = NodePath("../Settings menu") +CreditsControl = NodePath("../Credits") -[node name="Settings menu" parent="." node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("2_bqqt6")] +[node name="Settings menu" parent="." node_paths=PackedStringArray("InputSettingsControl", "AudioSettingsControl", "DisplaySettingsControl", "GameplaySettingsControl") instance=ExtResource("2_bqqt6")] visible = false -input_settings = NodePath("../Input Settings") -audio_settings = NodePath("../Audio settings") +InputSettingsControl = NodePath("../Input Settings") +AudioSettingsControl = NodePath("../Audio settings") +DisplaySettingsControl = NodePath("../Display Settings") +GameplaySettingsControl = NodePath("../Gameplay Settings") [node name="Credits" parent="." instance=ExtResource("3_bqqt6")] visible = false @@ -25,3 +27,15 @@ visible = false [node name="Input Settings" parent="." instance=ExtResource("5_rtw2f")] visible = false + +[node name="Gameplay Settings" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="Display Settings" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 diff --git a/scripts/Resources/CollectableResource.cs b/scripts/Resources/CollectableResource.cs index 88b338b..96cb03a 100644 --- a/scripts/Resources/CollectableResource.cs +++ b/scripts/Resources/CollectableResource.cs @@ -4,6 +4,6 @@ namespace Mr.BrickAdventures.scripts.Resources; public partial class CollectableResource : Resource { - [Export] public Variant Amount { get; set; } = 0.0; + [Export] public float Amount { get; set; } = 0.0f; [Export] public CollectableType Type { get; set; } } \ No newline at end of file diff --git a/scripts/Resources/LevelResource.cs b/scripts/Resources/LevelResource.cs new file mode 100644 index 0000000..7db4219 --- /dev/null +++ b/scripts/Resources/LevelResource.cs @@ -0,0 +1,13 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.Resources; + +[GlobalClass] +public partial class LevelResource : Resource +{ + [Export] + public string LevelName { get; set; } = string.Empty; + + [Export] + public string ScenePath { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/scripts/Resources/LevelResource.cs.uid b/scripts/Resources/LevelResource.cs.uid new file mode 100644 index 0000000..eb8fefd --- /dev/null +++ b/scripts/Resources/LevelResource.cs.uid @@ -0,0 +1 @@ +uid://c2h0pqhxiqswe diff --git a/scripts/Resources/SkillData.cs b/scripts/Resources/SkillData.cs index a96a8b1..8d14269 100644 --- a/scripts/Resources/SkillData.cs +++ b/scripts/Resources/SkillData.cs @@ -8,12 +8,12 @@ public partial class SkillData : Resource { [Export] public string Name { get; set; } = "New Skill"; [Export] public string Description { get; set; } = "New Skill"; - [Export] public Dictionary Config { get; set; } = new(); - [Export] public int Cost { get; set; } = 0; [Export] public Texture2D Icon { get; set; } [Export] public bool IsActive { get; set; } = false; [Export] public int Level { get; set; } = 1; - [Export] public int MaxLevel { get; set; } = 1; [Export] public SkillType Type { get; set; } = SkillType.Throw; [Export] public PackedScene Node { get; set; } + [Export] public Array Upgrades { get; set; } = []; + + public int MaxLevel => Upgrades.Count; } \ No newline at end of file diff --git a/scripts/Resources/SkillUpgrade.cs b/scripts/Resources/SkillUpgrade.cs new file mode 100644 index 0000000..b3edde9 --- /dev/null +++ b/scripts/Resources/SkillUpgrade.cs @@ -0,0 +1,12 @@ +using Godot; +using Godot.Collections; + +namespace Mr.BrickAdventures.scripts.Resources; + +[GlobalClass] +public partial class SkillUpgrade : Resource +{ + [Export] public int Cost { get; set; } = 50; + [Export(PropertyHint.MultilineText)] public string Description { get; set; } = "Upgrade Description"; + [Export] public Dictionary Properties { get; set; } = new(); +} \ No newline at end of file diff --git a/scripts/Resources/SkillUpgrade.cs.uid b/scripts/Resources/SkillUpgrade.cs.uid new file mode 100644 index 0000000..705bb80 --- /dev/null +++ b/scripts/Resources/SkillUpgrade.cs.uid @@ -0,0 +1 @@ +uid://dwb0e05pewcsn diff --git a/scripts/Resources/TapThrowInputResource.cs b/scripts/Resources/TapThrowInputResource.cs index 97079e8..81c97fc 100644 --- a/scripts/Resources/TapThrowInputResource.cs +++ b/scripts/Resources/TapThrowInputResource.cs @@ -1,13 +1,13 @@ using Godot; -using Mr.BrickAdventures.scripts.interfaces; namespace Mr.BrickAdventures.scripts.Resources; public partial class TapThrowInputResource : ThrowInputResource { - public override void Update(double delta) + + public override void ProcessInput(InputEvent @event) { - if (Input.IsActionPressed("attack")) + if (@event.IsActionPressed("attack")) { EmitSignalThrowRequested(1f); } diff --git a/scripts/Resources/ThrowInputResource.cs b/scripts/Resources/ThrowInputResource.cs index e7285e7..c9c844e 100644 --- a/scripts/Resources/ThrowInputResource.cs +++ b/scripts/Resources/ThrowInputResource.cs @@ -9,16 +9,14 @@ public abstract partial class ThrowInputResource : Resource, IThrowInput public virtual void ProcessInput(InputEvent @event) { - throw new System.NotImplementedException(); } public virtual void Update(double delta) { - throw new System.NotImplementedException(); } public virtual bool SupportsCharging() { - throw new System.NotImplementedException(); + return false; } } \ No newline at end of file diff --git a/scripts/SkillManager.cs b/scripts/SkillManager.cs index ad53727..a108a99 100644 --- a/scripts/SkillManager.cs +++ b/scripts/SkillManager.cs @@ -1,6 +1,8 @@ using Godot; using Godot.Collections; using Mr.BrickAdventures.Autoloads; +using Mr.BrickAdventures.scripts.components; +using Mr.BrickAdventures.scripts.interfaces; using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts; @@ -9,9 +11,12 @@ public partial class SkillManager : Node { private GameManager _gameManager; [Export] public Array AvailableSkills { get; set; } = []; - + public Dictionary ActiveComponents { get; private set; } = new(); + [Signal] + public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent); + public override void _Ready() { _gameManager = GetNode("/root/GameManager"); @@ -26,56 +31,60 @@ public partial class SkillManager : Node if (skillData.Type == SkillType.Throw) { var unlocked = _gameManager.GetUnlockedSkills(); - foreach (var skill in unlocked) + foreach (var sd in unlocked) { SkillData data = null; foreach (var s in AvailableSkills) { - if (s == (SkillData)skill) + if (s == sd) { data = s; break; } } - if (data != null && data.Type == SkillType.Throw) + if (data is { Type: SkillType.Throw }) RemoveSkill(data.Name); } } var instance = skillData.Node.Instantiate(); - foreach (var key in skillData.Config.Keys) + if (instance is ISkill skill) { - if (instance.HasMethod("get")) // rough presence check - { - var value = skillData.Config[key]; - var parent = GetParent(); - - if (value.VariantType == Variant.Type.NodePath) - { - var np = (NodePath)value; - if (parent.HasNode(np)) - value = parent.GetNode(np); - else if (instance.HasNode(np)) - value = instance.GetNode(np); - else - continue; - } - - // Set via property if exists - instance.Set(key, value); - } + skill.Initialize(Owner, skillData); + skill.Activate(); + } + else + { + GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!"); + instance.QueueFree(); + return; } Owner.AddChild(instance); ActiveComponents[skillData.Name] = instance; + + if (instance is BrickThrowComponent btc) + { + EmitSignalActiveThrowSkillChanged(btc); + } } public void RemoveSkill(string skillName) { if (!ActiveComponents.TryGetValue(skillName, out var component)) return; + + if (component.AsGodotObject() is BrickThrowComponent) + { + EmitSignalActiveThrowSkillChanged(null); + } var inst = (Node)component; + if (inst is ISkill skill) + { + skill.Deactivate(); + } + if (IsInstanceValid(inst)) inst.QueueFree(); @@ -97,7 +106,6 @@ public partial class SkillManager : Node { if (_gameManager.IsSkillUnlocked(sd)) { - GD.Print("Applying skill: ", sd.Name); CallDeferred(MethodName.AddSkill, sd); } else diff --git a/scripts/UI/AudioSettings.cs b/scripts/UI/AudioSettings.cs index 0d605b3..b9acc6f 100644 --- a/scripts/UI/AudioSettings.cs +++ b/scripts/UI/AudioSettings.cs @@ -3,7 +3,7 @@ using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; -public partial class AudioSettings : Node +public partial class AudioSettings : Control { [Export] public Slider MasterVolumeSlider { get; set; } [Export] public Slider MusicVolumeSlider { get; set; } @@ -22,8 +22,15 @@ public partial class AudioSettings : Node MasterVolumeSlider.ValueChanged += OnMasterVolumeChanged; MusicVolumeSlider.ValueChanged += OnMusicVolumeChanged; SfxVolumeSlider.ValueChanged += OnSfxVolumeChanged; + + LoadSettings(); } - + + public override void _ExitTree() + { + SaveSettings(); + } + public override void _UnhandledInput(InputEvent @event) { if (!@event.IsActionReleased("ui_cancel")) return; diff --git a/scripts/UI/ChargeProgressBar.cs b/scripts/UI/ChargeProgressBar.cs index 5e74caf..aab9776 100644 --- a/scripts/UI/ChargeProgressBar.cs +++ b/scripts/UI/ChargeProgressBar.cs @@ -1,33 +1,64 @@ using Godot; +using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts.UI; -public partial class ChargeProgressBar : Node +public partial class ChargeProgressBar : ProgressBar { [Export] public ProgressBar ProgressBar { get; set; } - [Export] public BrickThrowComponent ThrowComponent { get; set; } - + [Export] private SkillManager _skillManager; + + private BrickThrowComponent _throwComponent; private ChargeThrowInputResource _throwInput; public override void _Ready() { - Owner.ChildEnteredTree += OnNodeEntered; ProgressBar.Hide(); + + if (_skillManager == null) + { + return; + } + + _skillManager.ActiveThrowSkillChanged += OnActiveThrowSkillChanged; + + SetupDependencies(); + } + + private void OnActiveThrowSkillChanged(BrickThrowComponent throwComponent) + { + OnOwnerExiting(); + + if (throwComponent == null) return; + + _throwComponent = throwComponent; + _throwComponent.TreeExiting += OnOwnerExiting; SetupDependencies(); } - private void OnNodeEntered(Node node) + private void OnOwnerExiting() { - if (node is not BrickThrowComponent throwComponent || ThrowComponent != null) return; - ThrowComponent = throwComponent; - SetupDependencies(); + if (_throwInput != null) + { + _throwInput.ChargeStarted -= OnChargeStarted; + _throwInput.ChargeStopped -= OnChargeStopped; + _throwInput.ChargeUpdated -= OnChargeUpdated; + _throwInput = null; + } + _throwComponent = null; } + private void SetupDependencies() { - if (ThrowComponent.ThrowInputBehavior is ChargeThrowInputResource throwInput) + if (_throwComponent == null || ProgressBar == null) + { + return; + } + + if (_throwComponent.ThrowInputBehavior is ChargeThrowInputResource throwInput) { _throwInput = throwInput; } diff --git a/scripts/UI/DeathScreen.cs b/scripts/UI/DeathScreen.cs new file mode 100644 index 0000000..ce03d99 --- /dev/null +++ b/scripts/UI/DeathScreen.cs @@ -0,0 +1,72 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.UI; + +[GlobalClass] +public partial class DeathScreen : Control +{ + [Export] public LevelResource CurrentLevel { get; set; } + [Export] public Label CurrentLevelLabel { get; set; } + [Export] public Label LivesLeftLabel { get; set; } + [Export] public float TimeoutTime { get; set; } = 2.0f; + [Export] public Godot.Collections.Array NodesToDisable { get; set; } = new(); + + private GameManager _gameManager; + private Timer _timer; + + public override void _Ready() + { + _gameManager = GetNode("/root/GameManager"); + SetLabels(); + } + + private void SetLabels() + { + if (_gameManager == null) return; + + if (CurrentLevel != null) + { + CurrentLevelLabel.Text = CurrentLevel.LevelName; + } + LivesLeftLabel.Text = $" x {_gameManager.GetLives()}"; + } + + private void SetupTimer() + { + _timer = new Timer(); + _timer.WaitTime = TimeoutTime; + _timer.OneShot = true; + _timer.Timeout += OnTimeout; + AddChild(_timer); + _timer.Start(); + } + + private void ToggleNodes() + { + foreach (var node in NodesToDisable) + { + node.ProcessMode = node.ProcessMode == ProcessModeEnum.Disabled + ? ProcessModeEnum.Inherit + : ProcessModeEnum.Disabled; + } + } + + public void OnPlayerDeath() + { + if (_gameManager == null) return; + + ToggleNodes(); + SetLabels(); + Show(); + SetupTimer(); + } + + private void OnTimeout() + { + if (_gameManager == null || _gameManager.GetLives() == 0) return; + + GetTree().ReloadCurrentScene(); + } +} \ No newline at end of file diff --git a/scripts/UI/DeathScreen.cs.uid b/scripts/UI/DeathScreen.cs.uid new file mode 100644 index 0000000..3e10b43 --- /dev/null +++ b/scripts/UI/DeathScreen.cs.uid @@ -0,0 +1 @@ +uid://csprksav8mj1h diff --git a/scripts/UI/GameOverScreen.cs b/scripts/UI/GameOverScreen.cs index 80e55b7..6cb17a1 100644 --- a/scripts/UI/GameOverScreen.cs +++ b/scripts/UI/GameOverScreen.cs @@ -3,7 +3,7 @@ using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; -public partial class GameOverScreen : Node +public partial class GameOverScreen : Control { [Export] public Control GameOverPanel { get; set; } [Export] public Button RestartButton { get; set; } diff --git a/scripts/UI/Hud.cs b/scripts/UI/Hud.cs index 5939a8e..9f584ce 100644 --- a/scripts/UI/Hud.cs +++ b/scripts/UI/Hud.cs @@ -4,7 +4,7 @@ using Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.scripts.UI; -public partial class Hud : Node +public partial class Hud : Control { [Export] public HealthComponent Health { get; set; } [Export] public Label CoinsLabel { get; set; } diff --git a/scripts/UI/MainMenu.cs b/scripts/UI/MainMenu.cs index b4b5de0..b3aa727 100644 --- a/scripts/UI/MainMenu.cs +++ b/scripts/UI/MainMenu.cs @@ -3,7 +3,7 @@ using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; -public partial class MainMenu : Node +public partial class MainMenu : Control { [Export] public Control MainMenuControl { get; set; } [Export] public Button NewGameButton { get; set; } diff --git a/scripts/UI/Marketplace.cs b/scripts/UI/Marketplace.cs index 1fc580c..687f4f5 100644 --- a/scripts/UI/Marketplace.cs +++ b/scripts/UI/Marketplace.cs @@ -7,13 +7,13 @@ using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts.UI; -public partial class Marketplace : Node +public partial class Marketplace : Control { [Export] public Array Skills { get; set; } = []; [Export] public GridContainer ToUnlockGrid { get; set; } [Export] public GridContainer UnlockedGrid { get; set; } [Export] public Font Font { get; set; } - [Export] public SkillUnlockedComponent SkillUnlockedComponent { get; set; } + [Export] public SkillUnlockerComponent SkillUnlockerComponent { get; set; } [Export] public Array ComponentsToDisable { get; set; } = []; [Export] public PackedScene MarketplaceButtonScene { get; set; } [Export] public PackedScene SkillButtonScene { get; set; } @@ -24,6 +24,8 @@ public partial class Marketplace : Node public override void _Ready() { + _gameManager = GetNode("/root/GameManager"); + var skillsToUnlock = new List(); foreach (var skill in Skills) skillsToUnlock.Add(skill); @@ -33,47 +35,48 @@ public partial class Marketplace : Node var unlockedSkills = _gameManager.GetUnlockedSkills(); foreach (var skill in unlockedSkills) CreateSkillButton(skill); - SkillUnlockedComponent.SkillUnlocked += OnSkillUnlocked; + SkillUnlockerComponent.SkillUnlocked += OnSkillUnlocked; } public override void _ExitTree() { - SkillUnlockedComponent.SkillUnlocked -= OnSkillUnlocked; + SkillUnlockerComponent.SkillUnlocked -= OnSkillUnlocked; } public override void _Input(InputEvent @event) { - var root = Owner as Control; - if (!@event.IsActionPressed("show_marketplace")) return; - if (root != null && root.IsVisible()) + if (IsVisible()) { - root.Hide(); + Hide(); foreach (var c in ComponentsToDisable) c.ProcessMode = ProcessModeEnum.Inherit; } else { - root?.Show(); + Show(); foreach (var c in ComponentsToDisable) c.ProcessMode = ProcessModeEnum.Disabled; } } - private string GetButtonText(SkillData skill) - { - return $"{Tr(skill.Name)} {skill.Cost}"; - } - private void OnSkillUnlocked(SkillData skill) { - if (_skillButtons.Count == 0) CreateSkillButton(skill); + var buttonExists = false; + foreach (var existingButton in _skillButtons) + { + if (existingButton.Name == skill.Name) + { + buttonExists = true; + break; + } + } + + if (!buttonExists) CreateSkillButton(skill); foreach (var btn in _skillButtons) { - if (btn.Data.IsActive) - btn.Activate(); - else - btn.Deactivate(); + if (btn.Data.IsActive) btn.Activate(); + else btn.Deactivate(); } } @@ -93,14 +96,13 @@ public partial class Marketplace : Node private void CreateUpgradeButton(SkillData skill) { var button = MarketplaceButtonScene.Instantiate(); - button.Text = GetButtonText(skill); button.Data = skill; button.Icon = skill.Icon; button.Pressed += () => OnUpgradeButtonPressed(skill); _unlockButtons.Add(button); - UnlockedGrid.AddChild(button); - UnlockedGrid.QueueSort(); + ToUnlockGrid.AddChild(button); + ToUnlockGrid.QueueSort(); } private void OnUpgradeButtonPressed(SkillData skill) @@ -109,34 +111,30 @@ public partial class Marketplace : Node { if (skill.Level < skill.MaxLevel) { - SkillUnlockedComponent.TryUpgradeSkill(skill); - if (!skill.IsActive) SkillUnlockedComponent.SkillManager.ToggleSkillActivation(skill); + SkillUnlockerComponent.TryUpgradeSkill(skill); + if (!skill.IsActive) SkillUnlockerComponent.SkillManager.ToggleSkillActivation(skill); } else { - SkillUnlockedComponent.SkillManager.ToggleSkillActivation(skill); + SkillUnlockerComponent.SkillManager.ToggleSkillActivation(skill); } } else { - SkillUnlockedComponent.TryUnlockSkill(skill); - } - } - - private void RemoveButton(SkillData skill) - { - foreach (var node in ToUnlockGrid.GetChildren()) - { - var child = (Button)node; - if (child.Text != GetButtonText(skill)) continue; - - child.QueueFree(); - break; + SkillUnlockerComponent.TryUnlockSkill(skill); } } private void OnSkillButtonPressed(SkillButton button) { - SkillUnlockedComponent.SkillManager.ToggleSkillActivation(button.Data); + SkillUnlockerComponent.SkillManager.ToggleSkillActivation(button.Data); + + foreach (var btn in _skillButtons) + { + if (btn.Data.IsActive) + btn.Activate(); + else + btn.Deactivate(); + } } } \ No newline at end of file diff --git a/scripts/UI/MarketplaceButton.cs b/scripts/UI/MarketplaceButton.cs index cef9cb8..da287ca 100644 --- a/scripts/UI/MarketplaceButton.cs +++ b/scripts/UI/MarketplaceButton.cs @@ -13,50 +13,78 @@ public partial class MarketplaceButton : Button [Export] public Container SkillLevelContainer { get; set; } private GameManager _gameManager; - private SkillUnlockedComponent _skillUnlockedComponent; + private SkillUnlockerComponent _skillUnlockerComponent; public override void _Ready() { _gameManager = GetNode("/root/GameManager"); - - Setup(); var player = _gameManager.Player; - - var skillUnlockerComponent = player?.GetNodeOrNull("SkillUnlockerComponent"); - if (skillUnlockerComponent == null) return; + if (player == null) return; - skillUnlockerComponent.SkillUnlocked += OnSkillUnlock; + _skillUnlockerComponent = player.GetNodeOrNull("SkillUnlockerComponent"); + if (_skillUnlockerComponent != null) + { + _skillUnlockerComponent.SkillUnlocked += OnSkillStateChanged; + } + + UpdateButtonState(); } public override void _ExitTree() { - _skillUnlockedComponent.SkillUnlocked -= OnSkillUnlock; + if (_skillUnlockerComponent != null) + { + _skillUnlockerComponent.SkillUnlocked -= OnSkillStateChanged; + } + } + + private void OnSkillStateChanged(SkillData skill) + { + if (skill.Name == Data.Name) + { + UpdateButtonState(); + } } - private void Setup() + private void UpdateButtonState() { - if (Data == null) return; + if (Data == null || Data.Upgrades.Count == 0) + { + Visible = false; + return; + } + + var isUnlocked = _gameManager.IsSkillUnlocked(Data); + for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++) + { + SkillLevelContainer.GetChild(i).QueueFree(); + } + for (var i = 0; i < Data.MaxLevel; i++) { var icon = new TextureRect() { - Texture = i < Data.Level ? UnlockedSkillIcon : LockedSkillIcon, + Texture = (isUnlocked && i < Data.Level) ? UnlockedSkillIcon : LockedSkillIcon, + ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional }; SkillLevelContainer.AddChild(icon); } - } - private void OnSkillUnlock(SkillData skill) - { - if (skill.Name != Data.Name) return; - - for (var i = 0; i < Data.MaxLevel; i++) + if (!isUnlocked) { - var icon = SkillLevelContainer.GetChildOrNull(i); - if (icon == null) continue; - icon.Texture = i < Data.Level ? UnlockedSkillIcon : LockedSkillIcon; - Disabled = i >= Data.Level; + Text = $"{Tr(Data.Name)} ({Data.Upgrades[0].Cost})"; + Disabled = false; + } + else if (Data.Level < Data.MaxLevel) + { + Text = $"{Tr(Data.Name)} ({Data.Upgrades[Data.Level].Cost})"; + Disabled = false; + } + else + { + Text = $"{Tr(Data.Name)} (MAX)"; + Disabled = true; } } } \ No newline at end of file diff --git a/scripts/UI/PauseMenu.cs b/scripts/UI/PauseMenu.cs index 9cc2281..95e56fd 100644 --- a/scripts/UI/PauseMenu.cs +++ b/scripts/UI/PauseMenu.cs @@ -3,7 +3,7 @@ using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; -public partial class PauseMenu : Node +public partial class PauseMenu : Control { [Export] public Control PauseMenuControl { get; set; } [Export] public Control SettingsControl { get; set; } diff --git a/scripts/UI/SettingsMenu.cs b/scripts/UI/SettingsMenu.cs index 66890d2..554c440 100644 --- a/scripts/UI/SettingsMenu.cs +++ b/scripts/UI/SettingsMenu.cs @@ -3,7 +3,7 @@ using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; -public partial class SettingsMenu : Node +public partial class SettingsMenu : Control { [Export] public Control InputSettingsControl { get; set; } [Export] public Control AudioSettingsControl { get; set; } @@ -26,10 +26,10 @@ public partial class SettingsMenu : Node DisplaySettingsButton.Pressed += OnDisplaySettingsPressed; GameplaySettingsButton.Pressed += OnGameplaySettingsPressed; - InputSettingsControl.Hide(); - AudioSettingsControl.Hide(); - DisplaySettingsControl.Hide(); - GameplaySettingsControl.Hide(); + InputSettingsControl?.Hide(); + AudioSettingsControl?.Hide(); + DisplaySettingsControl?.Hide(); + GameplaySettingsControl?.Hide(); } public override void _UnhandledInput(InputEvent @event) diff --git a/scripts/achievements.gd b/scripts/achievements.gd deleted file mode 100644 index ae4735b..0000000 --- a/scripts/achievements.gd +++ /dev/null @@ -1,17 +0,0 @@ -class_name Achievements -extends Node - -func unlock_achievement(achievement_name: String) -> bool: - if not Steam.setAchievement(achievement_name): - print("Failed to unlock achievement: ", achievement_name) - return false - - return true - - -func reset_achievement(achievement_name: String) -> bool: - if not Steam.clearAchievement(achievement_name): - print("Failed to reset achievement: ", achievement_name) - return false - - return true \ No newline at end of file diff --git a/scripts/achievements.gd.uid b/scripts/achievements.gd.uid deleted file mode 100644 index 107a087..0000000 --- a/scripts/achievements.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://deguukal87gcb diff --git a/scripts/components/BrickThrowComponent.cs b/scripts/components/BrickThrowComponent.cs index e55f3cf..df5189b 100644 --- a/scripts/components/BrickThrowComponent.cs +++ b/scripts/components/BrickThrowComponent.cs @@ -4,7 +4,7 @@ using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts.components; -public partial class BrickThrowComponent : Node +public partial class BrickThrowComponent : Node, ISkill { [Export] public PackedScene BrickScene { get; set; } [Export] public float FireRate { get; set; } = 1.0f; @@ -13,13 +13,22 @@ public partial class BrickThrowComponent : Node private bool _canThrow = true; private Timer _timer; + private SkillData _skillData; public override void _Ready() { SetupTimer(); _canThrow = true; + } - if (ThrowInputBehavior != null) ThrowInputBehavior.ThrowRequested += ThrowBrick; + public override void _ExitTree() + { + if (ThrowInputBehavior != null) ThrowInputBehavior.ThrowRequested -= ThrowBrick; + if (_timer != null) + { + _timer.Timeout -= OnTimerTimeout; + _timer.QueueFree(); + } } public override void _Input(InputEvent @event) @@ -34,10 +43,12 @@ public partial class BrickThrowComponent : Node private void SetupTimer() { + _timer = new Timer(); _timer.WaitTime = FireRate; _timer.OneShot = false; _timer.Autostart = false; _timer.Timeout += OnTimerTimeout; + AddChild(_timer); } private void OnTimerTimeout() @@ -52,19 +63,59 @@ public partial class BrickThrowComponent : Node var instance = BrickScene.Instantiate(); var init = instance.GetNodeOrNull("ProjectileInitComponent"); + if (init != null && PlayerController.CurrentMovement is PlatformMovementComponent) { - init.Initialize(new ProjectileInitParams() + var @params = new ProjectileInitParams() { Position = PlayerController.GlobalPosition, Rotation = PlayerController.Rotation, Direction = PlayerController.CurrentMovement.LastDirection, PowerMultiplier = powerMultiplier, - }); + }; + + init.Initialize(@params); } GetTree().CurrentScene.AddChild(instance); _canThrow = false; _timer.Start(); } + + public void Initialize(Node owner, SkillData data) + { + PlayerController = owner as PlayerController; + _skillData = data; + + ThrowInputBehavior = (ThrowInputResource)ThrowInputBehavior?.Duplicate(); + + if (PlayerController == null) + { + GD.PushError("BrickThrowComponent: Owner is not a PlayerController."); + } + + if (_skillData.Level > 0 && _skillData.Upgrades.Count >= _skillData.Level) + { + ApplyUpgrade(_skillData.Upgrades[_skillData.Level - 1]); + } + } + + public void Activate() + { + if (ThrowInputBehavior != null) ThrowInputBehavior.ThrowRequested += ThrowBrick; + SetProcessInput(true); + } + + public void Deactivate() + { + if (ThrowInputBehavior != null) ThrowInputBehavior.ThrowRequested -= ThrowBrick; + } + + public void ApplyUpgrade(SkillUpgrade upgrade) + { + foreach (var property in upgrade.Properties) + { + Set(property.Key, property.Value); + } + } } \ No newline at end of file diff --git a/scripts/components/ChaseLevelComponent.cs b/scripts/components/ChaseLevelComponent.cs index d22a499..1595155 100644 --- a/scripts/components/ChaseLevelComponent.cs +++ b/scripts/components/ChaseLevelComponent.cs @@ -1,4 +1,5 @@ using Godot; +using PhantomCamera; namespace Mr.BrickAdventures.scripts.components; @@ -6,7 +7,6 @@ public partial class ChaseLevelComponent : Node { [Export] public float ChaseSpeed { get; set; } = 200.0f; [Export] public Marker2D ChaseTarget { get; set; } - [Export] public GodotObject PhantomCamera { get; set; } [Export] public float MinimumDistance { get; set; } = 10f; [Signal] @@ -17,8 +17,17 @@ public partial class ChaseLevelComponent : Node private bool _isChasing = false; private Node2D _previousCameraFollowTarget = null; + private PhantomCamera2D _phantomCamera = null; + private Node2D _root = null; + - public override void _Process(double delta) + public override void _Ready() + { + _phantomCamera = GetNode("../../%PhantomCamera").AsPhantomCamera2D(); + _root = Owner as Node2D; + } + + public override void _PhysicsProcess(double delta) { if (!_isChasing) return; if (ChaseTarget == null) return; @@ -31,21 +40,22 @@ public partial class ChaseLevelComponent : Node var targetPosition = ChaseTarget.GlobalPosition; - if (Owner is not Node2D root) return; + if (_root == null) return; - var direction = (targetPosition - root.GlobalPosition).Normalized(); - root.GlobalPosition += direction * ChaseSpeed * (float)delta; + var direction = (targetPosition - _root.GlobalPosition).Normalized(); + var speed = direction * ChaseSpeed * (float)delta; + _root.GlobalPosition += speed; } public void OnShipEntered() { - if (ChaseTarget == null || PhantomCamera == null) + if (ChaseTarget == null || _phantomCamera == null) return; if (_isChasing) return; - _previousCameraFollowTarget = (Node2D)PhantomCamera.Call("get_follow_target"); - PhantomCamera.Call("set_follow_target", Owner as Node2D); + _previousCameraFollowTarget = _phantomCamera.FollowTarget; + _phantomCamera.FollowTarget = _root; EmitSignalChaseStarted(); _isChasing = true; } @@ -70,9 +80,9 @@ public partial class ChaseLevelComponent : Node private void StopChasing() { - if (PhantomCamera == null) return; + if (_phantomCamera == null) return; - PhantomCamera.Call("set_follow_target", _previousCameraFollowTarget); + _phantomCamera.FollowTarget = _previousCameraFollowTarget; EmitSignalChaseStopped(); _isChasing = false; } diff --git a/scripts/components/CollectableComponent.cs b/scripts/components/CollectableComponent.cs index 0233570..8906d9e 100644 --- a/scripts/components/CollectableComponent.cs +++ b/scripts/components/CollectableComponent.cs @@ -13,7 +13,7 @@ public partial class CollectableComponent : Node [Export] public CollectableResource Data { get; set; } [Export] public AudioStreamPlayer2D Sfx {get; set; } - [Signal] public delegate void CollectedEventHandler(Variant amount, CollectableType type, Node2D body); + [Signal] public delegate void CollectedEventHandler(float amount, CollectableType type, Node2D body); public override void _Ready() { diff --git a/scripts/components/DamageComponent.cs b/scripts/components/DamageComponent.cs index d343ce7..6891036 100644 --- a/scripts/components/DamageComponent.cs +++ b/scripts/components/DamageComponent.cs @@ -16,16 +16,13 @@ public partial class DamageComponent : Node public override void _Ready() { - if (Area == null) + if (Area != null) { - GD.PushError($"DamageComponent: Area2D node is not set."); - return; + Area.BodyEntered += OnAreaBodyEntered; + Area.BodyExited += OnAreaBodyExited; + Area.AreaEntered += OnAreaAreaEntered; } - Area.BodyEntered += OnAreaBodyEntered; - Area.BodyExited += OnAreaBodyExited; - Area.AreaEntered += OnAreaAreaEntered; - if (DamageTimer != null) { DamageTimer.Timeout += OnDamageTimerTimeout; diff --git a/scripts/components/EnemyDeathComponent.cs b/scripts/components/EnemyDeathComponent.cs index 491af69..3a04c22 100644 --- a/scripts/components/EnemyDeathComponent.cs +++ b/scripts/components/EnemyDeathComponent.cs @@ -28,7 +28,7 @@ public partial class EnemyDeathComponent : Node private void OnDeath() { - CallDeferred(nameof(Die)); + _ = Die(); } private async Task Die() diff --git a/scripts/components/ExitDoorComponent.cs b/scripts/components/ExitDoorComponent.cs index d204421..1a03fb9 100644 --- a/scripts/components/ExitDoorComponent.cs +++ b/scripts/components/ExitDoorComponent.cs @@ -18,6 +18,8 @@ public partial class ExitDoorComponent : Node, IUnlockable public override void _Ready() { + _gameManager = GetNode("/root/GameManager"); + if (ExitArea == null) { GD.PushError("ExitDoorComponent: ExitArea is not set."); @@ -26,12 +28,15 @@ public partial class ExitDoorComponent : Node, IUnlockable ExitArea.BodyEntered += OnExitAreaBodyEntered; - _gameManager = GetNode("/root/gameManager"); } private void OnExitAreaBodyEntered(Node2D body) { - throw new System.NotImplementedException(); + if (Locked) return; + + EmitSignalExitTriggered(); + _gameManager.UnlockLevel((int)_gameManager.PlayerState["current_level"] + 1); + CallDeferred(nameof(GoToNextLevel)); } public void Unlock() diff --git a/scripts/components/ExplosiveComponent.cs b/scripts/components/ExplosiveComponent.cs index 907788a..353c660 100644 --- a/scripts/components/ExplosiveComponent.cs +++ b/scripts/components/ExplosiveComponent.cs @@ -16,13 +16,13 @@ public partial class ExplosiveComponent : Node2D public override void _Ready() { - if (Damage != null) + if (Damage == null) { GD.PushError("ExplosiveComponent: DamageComponent is not set."); return; } - if (ExplodeArea != null) + if (ExplodeArea == null) { GD.PushError("ExplosiveComponent: ExplodeArea is not set."); return; @@ -30,6 +30,8 @@ public partial class ExplosiveComponent : Node2D Area.BodyEntered += OnAreaBodyEntered; Area.AreaEntered += OnAreaAreaEntered; + + PrepareTimer(); } private void OnAreaAreaEntered(Area2D area) diff --git a/scripts/components/FlashingComponent.cs b/scripts/components/FlashingComponent.cs index 7a7db70..e808ea6 100644 --- a/scripts/components/FlashingComponent.cs +++ b/scripts/components/FlashingComponent.cs @@ -30,11 +30,10 @@ public partial class FlashingComponent : Node public void StartFlashing() { if (Sprite == null) return; - - _tween?.Kill(); - + if (_tween != null && _tween.IsRunning()) return; + _tween = CreateTween(); - _tween.SetParallel(true); + _tween.SetParallel(false); var flashes = (int)(FlashDuration / FlashTime); for (var i = 0; i < flashes; i++) diff --git a/scripts/components/GravityMotionComponent.cs b/scripts/components/GravityMotionComponent.cs index 17066e6..4fa3582 100644 --- a/scripts/components/GravityMotionComponent.cs +++ b/scripts/components/GravityMotionComponent.cs @@ -15,9 +15,9 @@ public partial class GravityMotionComponent : Node2D { if (LaunchComponent == null) return; - var direction = LaunchComponent.InitialDirection.X > 0f ? TargetDirection : new Vector2(-TargetDirection.X, TargetDirection.Y); - direction = direction.Normalized(); - _velocity = direction * LaunchComponent.Speed; + var horizontalDirection = LaunchComponent.InitialDirection.Normalized(); + var combinedDirection = (horizontalDirection + TargetDirection).Normalized(); + _velocity = combinedDirection * LaunchComponent.Speed; } public override void _PhysicsProcess(double delta) diff --git a/scripts/components/HealComponent.cs b/scripts/components/HealComponent.cs index 897a6a4..a687b7b 100644 --- a/scripts/components/HealComponent.cs +++ b/scripts/components/HealComponent.cs @@ -19,7 +19,7 @@ public partial class HealComponent : Node Collectable.Collected += OnCollected; } - private void OnCollected(Variant amount, CollectableType type, Node2D body) + private void OnCollected(float amount, CollectableType type, Node2D body) { if (type != CollectableType.Health) return; @@ -28,8 +28,7 @@ public partial class HealComponent : Node var healthComponent = body.GetNodeOrNull("HealthComponent"); if (healthComponent == null) return; - var value = amount.AsSingle(); - healthComponent.IncreaseHealth(value); + healthComponent.IncreaseHealth(amount); if (HealFx != null) { PlayHealFx(); diff --git a/scripts/components/MagneticSkillComponent.cs b/scripts/components/MagneticSkillComponent.cs index 3d92090..a6068d0 100644 --- a/scripts/components/MagneticSkillComponent.cs +++ b/scripts/components/MagneticSkillComponent.cs @@ -1,21 +1,19 @@ using System; using Godot; using Godot.Collections; +using Mr.BrickAdventures.scripts.interfaces; +using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts.components; -public partial class MagneticSkillComponent : Node +public partial class MagneticSkillComponent : Node, ISkill { [Export] public Area2D MagneticArea { get; set; } [Export] public float MagneticMoveDuration { get; set; } = 1.25f; private Array _collectablesToPickUp = []; - - public override void _Ready() - { - MagneticArea.AreaEntered += OnAreaEntered; - MagneticArea.BodyEntered += OnBodyEntered; - } + private Node2D _owner; + private SkillData _skillData; public override void _Process(double delta) { @@ -33,7 +31,7 @@ public partial class MagneticSkillComponent : Node private void OnBodyEntered(Node2D body) { - if (!HasComponentInChildren(body, "Collectable")) return; + if (!HasComponentInChildren(body, "CollectableComponent")) return; if (_collectablesToPickUp.Contains(body)) return; _collectablesToPickUp.Add(body); @@ -41,7 +39,7 @@ public partial class MagneticSkillComponent : Node private void OnAreaEntered(Area2D area) { - if (!HasComponentInChildren(area, "Collectable")) return; + if (!HasComponentInChildren(area, "CollectableComponent")) return; if (_collectablesToPickUp.Contains(area)) return; _collectablesToPickUp.Add(area); @@ -66,13 +64,69 @@ public partial class MagneticSkillComponent : Node private void MoveCollectableToOwner(Node2D collectable) { - if (!IsInstanceValid(collectable)) return; - - if (Owner is not Node2D root) return; + if (!IsInstanceValid(collectable) || !IsInstanceValid(_owner)) return; - var direction = (root.GlobalPosition - collectable.GlobalPosition).Normalized(); + var direction = (_owner.GlobalPosition - collectable.GlobalPosition).Normalized(); var speed = direction.Length() / MagneticMoveDuration; collectable.GlobalPosition += direction.Normalized() * speed; } + + public void Initialize(Node owner, SkillData data) + { + _owner = owner as Node2D; + _skillData = data; + + if (_owner == null) + { + GD.PushWarning("MagneticSkillComponent: Owner is not a Node2D."); + } + + if (MagneticArea == null) + { + if (owner is Area2D area2D) MagneticArea = area2D; + else + { + MagneticArea = owner.GetNodeOrNull("MagneticArea"); + if (MagneticArea == null) + { + GD.PushError("MagneticSkillComponent: MagneticArea is not set."); + return; + } + } + } + + if (_skillData.Level > 0 && _skillData.Upgrades.Count >= _skillData.Level) + { + ApplyUpgrade(_skillData.Upgrades[_skillData.Level - 1]); + } + } + + public void Activate() + { + if (MagneticArea == null) + { + GD.PushError("MagneticSkillComponent: MagneticArea is not set."); + return; + } + + MagneticArea.BodyEntered += OnBodyEntered; + MagneticArea.AreaEntered += OnAreaEntered; + } + + public void Deactivate() + { + if (MagneticArea == null) return; + + MagneticArea.BodyEntered -= OnBodyEntered; + MagneticArea.AreaEntered -= OnAreaEntered; + } + + public void ApplyUpgrade(SkillUpgrade upgrade) + { + foreach (var property in upgrade.Properties) + { + Set(property.Key, property.Value); + } + } } \ No newline at end of file diff --git a/scripts/components/PlatformMovementComponent.cs b/scripts/components/PlatformMovementComponent.cs index 8f4f7c6..03028fa 100644 --- a/scripts/components/PlatformMovementComponent.cs +++ b/scripts/components/PlatformMovementComponent.cs @@ -123,7 +123,8 @@ public partial class PlatformMovementComponent : Node2D, IMovement Body.Velocity = new Vector2(direction * Speed, Body.Velocity.Y); else Body.Velocity = new Vector2(Mathf.MoveToward(Body.Velocity.X, 0, Speed), Body.Velocity.Y); - + + PreviousVelocity = Body.Velocity; Body.MoveAndSlide(); } diff --git a/scripts/components/PlayerController.cs b/scripts/components/PlayerController.cs index 3006488..dd5387a 100644 --- a/scripts/components/PlayerController.cs +++ b/scripts/components/PlayerController.cs @@ -4,7 +4,7 @@ using Mr.BrickAdventures.scripts.interfaces; namespace Mr.BrickAdventures.scripts.components; -public partial class PlayerController : Node2D +public partial class PlayerController : CharacterBody2D { [Export] public string DefaultMovementType { get; set; } = "platform"; diff --git a/scripts/components/PlayerDeathComponent.cs b/scripts/components/PlayerDeathComponent.cs index 2c62493..7db4780 100644 --- a/scripts/components/PlayerDeathComponent.cs +++ b/scripts/components/PlayerDeathComponent.cs @@ -14,7 +14,7 @@ public partial class PlayerDeathComponent : Node2D public override void _Ready() { - _gameManager = GetNode("/root/gameManager"); + _gameManager = GetNode("/root/GameManager"); HealthComponent.Death += OnDeath; } diff --git a/scripts/components/ProjectileComponent.cs b/scripts/components/ProjectileComponent.cs index 5741ca2..18f61da 100644 --- a/scripts/components/ProjectileComponent.cs +++ b/scripts/components/ProjectileComponent.cs @@ -2,6 +2,7 @@ using Godot; namespace Mr.BrickAdventures.scripts.components; +[GlobalClass] public partial class ProjectileComponent : Node2D { [Export] public float Speed { get; set; } = 16f; diff --git a/scripts/components/ProjectileInitComponent.cs b/scripts/components/ProjectileInitComponent.cs index 4821ad6..03ee267 100644 --- a/scripts/components/ProjectileInitComponent.cs +++ b/scripts/components/ProjectileInitComponent.cs @@ -2,7 +2,7 @@ using Godot; namespace Mr.BrickAdventures.scripts.components; -public partial class ProjectileInitParams +public class ProjectileInitParams { public Vector2 Position { get; set; } = Vector2.Zero; public Vector2 Direction { get; set; } = Vector2.Right; @@ -20,7 +20,7 @@ public partial class ProjectileInitComponent : Node var direction = p.Direction; var rotation = p.Rotation; var power = p.PowerMultiplier; - + if (Owner is Node2D root) { root.GlobalPosition = position; diff --git a/scripts/components/RequirementComponent.cs b/scripts/components/RequirementComponent.cs index 70c83d7..1752048 100644 --- a/scripts/components/RequirementComponent.cs +++ b/scripts/components/RequirementComponent.cs @@ -28,9 +28,9 @@ public partial class RequirementComponent : Node } } - private void OnCollected(Variant amount, CollectableType type, Node2D body) + private void OnCollected(float amount, CollectableType type, Node2D body) { - AddProgress(amount.As()); + AddProgress((int)amount); } private void AddProgress(int amount = 1) diff --git a/scripts/components/ScoreComponent.cs b/scripts/components/ScoreComponent.cs index c612990..9db2dcc 100644 --- a/scripts/components/ScoreComponent.cs +++ b/scripts/components/ScoreComponent.cs @@ -7,7 +7,7 @@ namespace Mr.BrickAdventures.scripts.components; public partial class ScoreComponent : Node { private GameManager _gameManager; - private const string CoinsGroupName = "Coins"; + private const string CoinsGroupName = "coins"; public override async void _Ready() { @@ -20,7 +20,7 @@ public partial class ScoreComponent : Node return; } - var coins = GetTree().GetNodesInGroup("Coins"); + var coins = GetTree().GetNodesInGroup(CoinsGroupName); foreach (var coin in coins) { var c = coin.GetNodeOrNull("CollectableComponent"); @@ -30,12 +30,12 @@ public partial class ScoreComponent : Node } } } - - private void OnCollected(Variant amount, CollectableType type, Node2D body) + + private void OnCollected(float amount, CollectableType type, Node2D body) { if (type != CollectableType.Coin) return; - var coinAmount = amount.As(); + var coinAmount = (int)amount; var currentCoins = (int)_gameManager.CurrentSessionState["coins_collected"]; _gameManager.CurrentSessionState["coins_collected"] = currentCoins + coinAmount; } diff --git a/scripts/components/ShipShooterComponent.cs b/scripts/components/ShipShooterComponent.cs index cb6d4d9..1ae46f1 100644 --- a/scripts/components/ShipShooterComponent.cs +++ b/scripts/components/ShipShooterComponent.cs @@ -10,7 +10,7 @@ public partial class ShipShooterComponent : Node [Export] public Marker2D BulletSpawn { get; set; } [Export] public AudioStreamPlayer2D ShootSfx { get; set; } - private bool _canShoot = false; + private bool _canShoot = true; public override void _Ready() { diff --git a/scripts/components/SkillUnlockedComponent.cs b/scripts/components/SkillUnlockerComponent.cs similarity index 70% rename from scripts/components/SkillUnlockedComponent.cs rename to scripts/components/SkillUnlockerComponent.cs index 33d3aa9..4bf6a2f 100644 --- a/scripts/components/SkillUnlockedComponent.cs +++ b/scripts/components/SkillUnlockerComponent.cs @@ -1,11 +1,12 @@ using Godot; using Godot.Collections; using Mr.BrickAdventures.Autoloads; +using Mr.BrickAdventures.scripts.interfaces; using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.scripts.components; -public partial class SkillUnlockedComponent : Node +public partial class SkillUnlockerComponent : Node { [Export] public SkillManager SkillManager { get; set; } @@ -28,11 +29,11 @@ public partial class SkillUnlockedComponent : Node { if (_gameManager == null) return false; if (_gameManager.IsSkillUnlocked(skill)) return false; - if (!HasEnoughCoins(skill.Cost)) return false; + if (!HasEnoughCoins(skill.Upgrades[0].Cost)) return false; skill.Level = 1; skill.IsActive = true; - _gameManager.RemoveCoins(skill.Cost); + _gameManager.RemoveCoins(skill.Upgrades[0].Cost); var skillsUnlocked = (Array)_gameManager.CurrentSessionState["skills_unlocked"]; skillsUnlocked.Add(skill); @@ -59,11 +60,19 @@ public partial class SkillUnlockedComponent : Node { if (_gameManager == null) return false; if (!_gameManager.IsSkillUnlocked(skill)) return false; - if (!HasEnoughCoins(skill.Cost)) return false; if (skill.Level >= skill.MaxLevel) return false; + if (!HasEnoughCoins(skill.Upgrades[skill.Level].Cost)) return false; - _gameManager.RemoveCoins(skill.Cost); + _gameManager.RemoveCoins(skill.Upgrades[skill.Level].Cost); skill.Level++; + if (SkillManager.ActiveComponents.TryGetValue(skill.Name, out Variant componentVariant)) + { + var component = componentVariant.AsGodotObject(); + if (component is ISkill skillInstance) + { + skillInstance.ApplyUpgrade(skill.Upgrades[skill.Level - 1]); + } + } EmitSignalSkillUnlocked(skill); return true; } diff --git a/scripts/components/SkillUnlockedComponent.cs.uid b/scripts/components/SkillUnlockerComponent.cs.uid similarity index 100% rename from scripts/components/SkillUnlockedComponent.cs.uid rename to scripts/components/SkillUnlockerComponent.cs.uid diff --git a/scripts/components/SpaceshipEnterComponent.cs b/scripts/components/SpaceshipEnterComponent.cs index 6eb0355..16b246e 100644 --- a/scripts/components/SpaceshipEnterComponent.cs +++ b/scripts/components/SpaceshipEnterComponent.cs @@ -2,20 +2,19 @@ using Godot; namespace Mr.BrickAdventures.scripts.components; -public partial class SpaceshipEnterComponent : Node +public partial class SpaceshipEnterComponent : Area2D { - [Export] public Area2D Area { get; set; } [Signal] public delegate void SpaceshipEnteredEventHandler(); public override void _Ready() { - Area.BodyEntered += OnBodyEntered; + BodyEntered += OnBodyEntered; } private void OnBodyEntered(Node2D body) { if (body is not PlayerController) return; EmitSignalSpaceshipEntered(); - Owner.QueueFree(); + QueueFree(); } } \ No newline at end of file diff --git a/scripts/components/SpaceshipExitComponent.cs b/scripts/components/SpaceshipExitComponent.cs index 4cf413f..6d75270 100644 --- a/scripts/components/SpaceshipExitComponent.cs +++ b/scripts/components/SpaceshipExitComponent.cs @@ -2,14 +2,13 @@ using Godot; namespace Mr.BrickAdventures.scripts.components; -public partial class SpaceshipExitComponent : Node +public partial class SpaceshipExitComponent : Area2D { - [Export] public Area2D Area { get; set; } [Signal] public delegate void SpaceshipExitEventHandler(); public override void _Ready() { - Area.BodyEntered += OnBodyEntered; + BodyEntered += OnBodyEntered; } private void OnBodyEntered(Node2D body) diff --git a/scripts/components/TooltipComponent.cs b/scripts/components/TooltipComponent.cs index f32961b..3500062 100644 --- a/scripts/components/TooltipComponent.cs +++ b/scripts/components/TooltipComponent.cs @@ -2,9 +2,8 @@ using Godot; namespace Mr.BrickAdventures.scripts.components; -public partial class TooltipComponent : Node +public partial class TooltipComponent : Area2D { - [Export] public Area2D Area { get; set; } [Export] public Control UiRoot { get; set; } [Export] public string Text { get; set; } = string.Empty; [Export] public Label TooltipLabel { get; set; } @@ -13,8 +12,8 @@ public partial class TooltipComponent : Node { TooltipLabel.Text = Text; UiRoot.Visible = false; - Area.BodyEntered += OnBodyEntered; - Area.BodyExited += OnBodyExited; + BodyEntered += OnBodyEntered; + BodyExited += OnBodyExited; } private void OnBodyEntered(Node2D body) diff --git a/scripts/components/beam_component.gd b/scripts/components/beam_component.gd deleted file mode 100644 index 69bda9f..0000000 --- a/scripts/components/beam_component.gd +++ /dev/null @@ -1,64 +0,0 @@ -# class_name BeamComponent -extends Node2D - -@export var expansion_speed: float = 16.0 -@export var max_length: float = 512.0 -@export var direction: Vector2 = Vector2.DOWN - -var current_length: float = 16.0 - -@export var root: Node2D -@export var sprite2d: Sprite2D -@export var collision_shape: CollisionShape2D - - -func _ready() -> void: - collision_shape.shape.extents = Vector2(current_length / 2.0, current_length / 2.0) - sprite2d.scale = Vector2(1, 1) - collision_shape.position = Vector2.ZERO - - -func _process(delta: float) -> void: - var new_length = current_length + expansion_speed * delta - if new_length > max_length: - new_length = max_length - - if not check_for_obstacle(new_length): - expand_beam(new_length) - - -func expand_beam(new_length: float) -> void: - current_length = new_length - - if direction == Vector2.UP: - sprite2d.scale.y = current_length / 16.0 - sprite2d.position.y = -current_length / 2.0 - collision_shape.shape.extents = Vector2(8.0, current_length / 2.0) - collision_shape.position.y = -current_length / 2.0 - elif direction == Vector2.DOWN: - sprite2d.scale.y = current_length / 16.0 - sprite2d.position.y = current_length / 2.0 - collision_shape.shape.extents = Vector2(8.0, current_length / 2.0) - collision_shape.position.y = current_length / 2.0 - elif direction == Vector2.LEFT: - sprite2d.scale.y = current_length / 16.0 - sprite2d.position.x = -current_length / 2.0 - collision_shape.shape.extents = Vector2(current_length / 2.0, 8.0) - collision_shape.position.x = -current_length / 2.0 - elif direction == Vector2.RIGHT: - sprite2d.scale.y = current_length / 16.0 - sprite2d.position.x = current_length / 2.0 - collision_shape.shape.extents = Vector2(current_length / 2.0, 8.0) - collision_shape.position.x = current_length / 2.0 - - -func check_for_obstacle(new_length: float) -> bool: - var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state - var query_start: Vector2 = global_position - var query_end: Vector2 = global_position + direction * new_length - var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(query_start, query_end) - query.collide_with_areas = false - query.collide_with_bodies = true - - var result: Dictionary = space_state.intersect_ray(query) - return result.size() > 0 diff --git a/scripts/components/beam_component.gd.uid b/scripts/components/beam_component.gd.uid deleted file mode 100644 index da83346..0000000 --- a/scripts/components/beam_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bejv75mi8npj0 diff --git a/scripts/components/brick_throw.gd b/scripts/components/brick_throw.gd deleted file mode 100644 index deaca8f..0000000 --- a/scripts/components/brick_throw.gd +++ /dev/null @@ -1,59 +0,0 @@ -class_name BrickThrowComponent -extends Node - -@export var brick_scene: PackedScene -@export var fire_rate: float = 1.0 -@export var player_controller: PlayerController -@export var timer: Timer -@export var throw_input_behavior: ThrowInputResource - -var can_throw: bool = true - - -func _ready() -> void: - setup_timer() - can_throw = true - - if throw_input_behavior: - throw_input_behavior.throw_requested.connect(throw_brick) - - -func _input(event: InputEvent) -> void: - if throw_input_behavior: - throw_input_behavior.process_input(event) - - -func _process(delta: float) -> void: - if throw_input_behavior: - throw_input_behavior.update(delta) - - -func setup_timer() -> void: - timer.wait_time = fire_rate - timer.one_shot = false - timer.autostart = false - timer.timeout.connect(on_timer_timeout) - - -func on_timer_timeout() -> void: - can_throw = true - - -func throw_brick(power_multiplier: float = 1.0) -> void: - if not can_throw: - return - - var instance: Node2D = brick_scene.instantiate() - var init := instance.get_node_or_null("ProjectileInitComponent") as ProjectileInitComponent - if init and player_controller.current_movement is PlatformMovement: - init.initialize({ - "position": player_controller.global_position, - "rotation": player_controller.rotation, - "direction": player_controller.current_movement.last_direction, - "power_multiplier": power_multiplier - }) - - get_tree().current_scene.add_child(instance) - - can_throw = false - timer.start() diff --git a/scripts/components/brick_throw.gd.uid b/scripts/components/brick_throw.gd.uid deleted file mode 100644 index 0ea24ad..0000000 --- a/scripts/components/brick_throw.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cm06xg1l3xtw5 diff --git a/scripts/components/bullet_component.gd b/scripts/components/bullet_component.gd deleted file mode 100644 index c3b6126..0000000 --- a/scripts/components/bullet_component.gd +++ /dev/null @@ -1,37 +0,0 @@ -class_name BulletComponent -extends Node - -@export var root: Node2D -@export var area2d: Area2D -@export var hit_terrain_fx: TerrainHitFx -@export var bullet_sprite: Sprite2D - - -func _ready() -> void: - area2d.body_entered.connect(on_area2d_body_entered) - area2d.area_entered.connect(on_area2d_area_entered) - - - -func on_area2d_body_entered(body: Node2D) -> void: - if body is TileMapLayer: - if bullet_sprite: - bullet_sprite.visible = false - play_terrain_hit_fx() - return - - root.queue_free() - - -func on_area2d_area_entered(_area: Area2D) -> void: - root.queue_free() - - - -func play_terrain_hit_fx() -> void: - if not hit_terrain_fx: - return - - await hit_terrain_fx.trigger_fx() - - root.queue_free() diff --git a/scripts/components/bullet_component.gd.uid b/scripts/components/bullet_component.gd.uid deleted file mode 100644 index e5986bb..0000000 --- a/scripts/components/bullet_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cdnwrn8v05qhi diff --git a/scripts/components/cage_component.gd b/scripts/components/cage_component.gd deleted file mode 100644 index b57ef5b..0000000 --- a/scripts/components/cage_component.gd +++ /dev/null @@ -1,37 +0,0 @@ -class_name CageComponent -extends Node - -@export var lever: LeverComponent -@export var root: Node2D -@export var move_value: Vector2 = Vector2(0, -100) -@export var tween_duration: float = 0.5 -@export var should_free: bool = true - - -func _ready() -> void: - await get_tree().process_frame - if not lever: - var levers_nodes := get_tree().get_nodes_in_group("levers") - for lever_node in levers_nodes: - var lever_component: LeverComponent = lever_node.get_node_or_null("LeverComponent") - if lever_component: - lever_component.activated.connect(on_lever_activated) - else: - lever.activated.connect(on_lever_activated) - - if not root: - printerr("CageComponent: root is not set.") - return - - -func on_lever_activated() -> void: - var tween: Tween = create_tween() - var end_position: Vector2 = root.position + move_value - tween.tween_property(root, "position", end_position, tween_duration) - tween.tween_callback(_on_tween_completed) - - -func _on_tween_completed() -> void: - if not should_free: - return - root.queue_free() \ No newline at end of file diff --git a/scripts/components/cage_component.gd.uid b/scripts/components/cage_component.gd.uid deleted file mode 100644 index a82f5d9..0000000 --- a/scripts/components/cage_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ddplvyjqguxtl diff --git a/scripts/components/can_be_launched_component.gd b/scripts/components/can_be_launched_component.gd deleted file mode 100644 index 5675e50..0000000 --- a/scripts/components/can_be_launched_component.gd +++ /dev/null @@ -1,2 +0,0 @@ -class_name CanBeLaunchedComponent -extends Node \ No newline at end of file diff --git a/scripts/components/can_be_launched_component.gd.uid b/scripts/components/can_be_launched_component.gd.uid deleted file mode 100644 index 6db28a6..0000000 --- a/scripts/components/can_be_launched_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6ffxsx3gknhr diff --git a/scripts/components/can_pickup.gd b/scripts/components/can_pickup.gd deleted file mode 100644 index 2ff50d2..0000000 --- a/scripts/components/can_pickup.gd +++ /dev/null @@ -1,2 +0,0 @@ -class_name CanPickUpComponent -extends Node \ No newline at end of file diff --git a/scripts/components/can_pickup.gd.uid b/scripts/components/can_pickup.gd.uid deleted file mode 100644 index 63c7228..0000000 --- a/scripts/components/can_pickup.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dcvjvjy2nhf8s diff --git a/scripts/components/cannot_stomp_component.gd b/scripts/components/cannot_stomp_component.gd deleted file mode 100644 index 7aa7f6c..0000000 --- a/scripts/components/cannot_stomp_component.gd +++ /dev/null @@ -1,2 +0,0 @@ -class_name CannotStompComponent -extends Node \ No newline at end of file diff --git a/scripts/components/cannot_stomp_component.gd.uid b/scripts/components/cannot_stomp_component.gd.uid deleted file mode 100644 index 730f9e4..0000000 --- a/scripts/components/cannot_stomp_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ct7lsuwjvmwxu diff --git a/scripts/components/charge_throw_component.gd b/scripts/components/charge_throw_component.gd deleted file mode 100644 index a6fd614..0000000 --- a/scripts/components/charge_throw_component.gd +++ /dev/null @@ -1,59 +0,0 @@ -class_name ChargeThrowComponent -extends Node - -@export var min_charge_duration: float = 0.1 -@export var min_power: float = 0.5 -@export var max_power: float = 2.0 -@export var max_charge_time: float = 1.5 - -var charge_start_time: float = 0.0 -var is_charging: bool = false -signal charge_started -signal charge_updated(charge_ratio: float) -signal charge_stopped - - -func _process(_delta: float) -> void: - if not is_charging: - return - - var charge_ratio := get_charge_ratio() - charge_updated.emit(charge_ratio) - - -func start_charging() -> void: - is_charging = true - charge_start_time = Time.get_ticks_msec() / 1000.0 - call_deferred("emit_charge_started") - - -func get_charge_ratio() -> float: - if not is_charging: - return 0.0 - - var held_time := (Time.get_ticks_msec() / 1000.0) - charge_start_time - var t = clamp(held_time / max_charge_time, 0.0, 1.0) - - return lerp(min_power, max_power, t) - - -func stop_charging() -> float: - if not is_charging: - return min_power - - var held_time := (Time.get_ticks_msec() / 1000.0) - charge_start_time - is_charging = false - charge_start_time = 0.0 - charge_stopped.emit() - - if held_time < min_charge_duration: - return min_power - - var t = clamp(held_time / max_charge_time, 0.0, 1.0) - return lerp(min_power, max_power, t) - - -func emit_charge_started() -> void: - if not is_charging: - return - charge_started.emit() \ No newline at end of file diff --git a/scripts/components/charge_throw_component.gd.uid b/scripts/components/charge_throw_component.gd.uid deleted file mode 100644 index 07b7876..0000000 --- a/scripts/components/charge_throw_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6fclevp3peuo diff --git a/scripts/components/chase_level_component.gd b/scripts/components/chase_level_component.gd deleted file mode 100644 index 9b2b981..0000000 --- a/scripts/components/chase_level_component.gd +++ /dev/null @@ -1,69 +0,0 @@ -class_name ChaseLevelComponent -extends Node - -@export var chase_speed: float = 200.0 -@export var chase_target: Marker2D -@export var phantom_camera: PhantomCamera2D -@export var minimum_distance: float = 10.0 -signal chase_started -signal chase_stopped -var is_chasing: bool = false -var previous_camera_follow_target: Node2D = null - - -func _process(delta: float) -> void: - if not is_chasing: - return - - if not chase_target: - printerr("ChaseLevelComponent: chase_target is not set.") - return - - if check_if_reached_target(): - stop_chasing() - return - - var target_position: Vector2 = chase_target.global_position - var current_position: Vector2 = owner.global_position - var direction: Vector2 = (target_position - current_position).normalized() - - owner.global_position += direction * chase_speed * delta - - -func on_ship_entered() -> void: - if not chase_target: - printerr("ChaseLevelComponent: chase_target is not set.") - return - - if not phantom_camera: - printerr("ChaseLevelComponent: phantom_camera is not set.") - return - - previous_camera_follow_target = phantom_camera.get_follow_target() - phantom_camera.set_follow_target(owner as Node2D) - chase_started.emit() - is_chasing = true - - -func on_ship_exited() -> void: - stop_chasing() - - -func check_if_reached_target() -> bool: - if not chase_target: - printerr("ChaseLevelComponent: chase_target is not set.") - return false - - var target_position: Vector2 = chase_target.global_position - var current_position: Vector2 = owner.global_position - return current_position.distance_to(target_position) < minimum_distance - - -func stop_chasing() -> void: - if not phantom_camera: - printerr("ChaseLevelComponent: phantom_camera is not set.") - return - - phantom_camera.set_follow_target(previous_camera_follow_target) - chase_stopped.emit() - is_chasing = false \ No newline at end of file diff --git a/scripts/components/chase_level_component.gd.uid b/scripts/components/chase_level_component.gd.uid deleted file mode 100644 index 2e3ea15..0000000 --- a/scripts/components/chase_level_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cf4li7whw5old diff --git a/scripts/components/cleanup_component.gd b/scripts/components/cleanup_component.gd deleted file mode 100644 index 29ce3da..0000000 --- a/scripts/components/cleanup_component.gd +++ /dev/null @@ -1,8 +0,0 @@ -class_name CleanUpComponent -extends Node - -@export var root: Node - - -func clean_up() -> void: - root.queue_free() \ No newline at end of file diff --git a/scripts/components/cleanup_component.gd.uid b/scripts/components/cleanup_component.gd.uid deleted file mode 100644 index dae9724..0000000 --- a/scripts/components/cleanup_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://f74xpfg7624d diff --git a/scripts/components/collapsable.gd b/scripts/components/collapsable.gd deleted file mode 100644 index 1d68d01..0000000 --- a/scripts/components/collapsable.gd +++ /dev/null @@ -1,55 +0,0 @@ -class_name CollapsableComponent -extends Node - -@export var to_collapse_timer: Timer -@export var reset_timer: Timer -@export var sprite2d: Sprite2D -@export var collision_shape: CollisionShape2D - -@export var collapse_time: float = 0.5 -@export var reset_time: float = 1.0 - -@export var anim_time: float = 0.25 - -func _ready() -> void: - reset_timers() - -func _on_to_collapse_timer_timeout() -> void: - collapse_bridge() - -func _on_reset_timer_timeout() -> void: - reactivate_bridge() - -func collapse_bridge(): - to_collapse_timer.stop() - to_collapse_timer.wait_time = collapse_time - - var bridge_tween = create_tween() - bridge_tween.tween_property(sprite2d, "modulate:a", 0, anim_time) - await bridge_tween.finished - - collision_shape.disabled = true - reset_timer.start() - -func reactivate_bridge(): - reset_timer.stop() - reset_timer.wait_time = reset_time - - var bridge_tween = create_tween() - bridge_tween.tween_property(sprite2d, "modulate:a", 1, anim_time) - await bridge_tween.finished - - collision_shape.disabled = false - -func _on_collapsable_detector_body_entered(_body: Node2D) -> void: - to_collapse_timer.start() - -func reset_timers(): - to_collapse_timer.stop() - to_collapse_timer.wait_time = collapse_time - - -func _on_collapsable_detector_body_exited(_body: Node2D) -> void: - var collapse_time_left: float = abs(to_collapse_timer.time_left - collapse_time) - if collapse_time_left < (0.1 * collapse_time): - reset_timers() diff --git a/scripts/components/collapsable.gd.uid b/scripts/components/collapsable.gd.uid deleted file mode 100644 index 1ce1c00..0000000 --- a/scripts/components/collapsable.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://r0a6xjicrh74 diff --git a/scripts/components/collectable.gd b/scripts/components/collectable.gd deleted file mode 100644 index b54c257..0000000 --- a/scripts/components/collectable.gd +++ /dev/null @@ -1,38 +0,0 @@ -class_name CollectableComponent -extends Node - -var root: Node -var has_fade_away: bool = false - -@export var area2d: Area2D -@export var collision_shape: CollisionShape2D -@export var collectable_data: CollectableResource -@export var sfx: AudioStreamPlayer2D -signal collected(amount: Variant, type: CollectableResource.CollectableType, body: Node2D) - - -func _ready() -> void: - if area2d: - area2d.body_entered.connect(_on_area2d_body_entered) - else: - printerr("Collectable node missing Area2D child.") - - root = get_parent() - - if root.has_node("FadeAwayComponent"): - has_fade_away = true - - -func _on_area2d_body_entered(body: Node2D) -> void: - if body.has_node("CanPickUpComponent"): - collected.emit(collectable_data.amount, collectable_data.type, body) - if collision_shape: - collision_shape.call_deferred("set_disabled", true) - if sfx: - sfx.play() - if not has_fade_away and sfx: - await sfx.finished - root.queue_free() - elif not has_fade_away: - root.queue_free() - diff --git a/scripts/components/collectable.gd.uid b/scripts/components/collectable.gd.uid deleted file mode 100644 index 6ce4f73..0000000 --- a/scripts/components/collectable.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://pa1bwc4no08q diff --git a/scripts/components/damage_component.gd b/scripts/components/damage_component.gd deleted file mode 100644 index 7519e9e..0000000 --- a/scripts/components/damage_component.gd +++ /dev/null @@ -1,94 +0,0 @@ -class_name DamageComponent -extends Node - -@export var damage: float = 0.25 -@export var area2d: Area2D -@export var status_effect_data: StatusEffectDataResource -@export var damage_timer: Timer - -var current_target: Node = null -signal effect_inflicted(target: Node2D, effect: StatusEffectDataResource) - - -func _ready() -> void: - if not area2d: - printerr("No area2d assigned!") - return - - area2d.body_entered.connect(on_area2d_body_entered) - area2d.body_exited.connect(on_area2d_body_exited) - area2d.area_entered.connect(on_area2d_area_entered) - - if damage_timer: - damage_timer.timeout.connect(on_damage_timer_timeout) - - -func _process(_delta: float) -> void: - if not current_target: - return - if damage_timer: - return - process_entity_and_apply_damage(current_target) - - -func deal_damage(target: HealthComponent) -> void: - target.decrease_health(damage) - - -func on_damage_timer_timeout() -> void: - if not current_target: - return - - process_entity_and_apply_damage(current_target) - - -func process_entity_and_apply_damage(body: Node2D) -> void: - if body.has_node("HealthComponent"): - var health_component: HealthComponent = body.get_node("HealthComponent") - var invulnerability_component: InvulnerabilityComponent = body.get_node_or_null("InvulnerabilityComponent") - - if invulnerability_component and invulnerability_component.is_invulnerable(): - return - - if status_effect_data and status_effect_data.effect_type != StatusEffectComponent.EffectType.NONE: - effect_inflicted.emit(body, status_effect_data) - - deal_damage(health_component) - - if invulnerability_component: - invulnerability_component.activate() - - -func on_area2d_body_entered(body: Node2D) -> void: - current_target = body - - if not check_if_processing_is_on(): - return - - if damage_timer: - damage_timer.start() - - process_entity_and_apply_damage(body) - - -func on_area2d_body_exited(body: Node2D) -> void: - if body == current_target: - current_target = null - if damage_timer: - damage_timer.stop() - - -func on_area2d_area_entered(area: Area2D) -> void: - if not check_if_processing_is_on(): - return - - if area == area2d: - return - - var parent := area.get_parent() - if parent.has_node("DamageComponent"): - process_entity_and_apply_damage(parent) - - -func check_if_processing_is_on() -> bool: - return self.process_mode == PROCESS_MODE_INHERIT or self.process_mode == PROCESS_MODE_ALWAYS \ No newline at end of file diff --git a/scripts/components/damage_component.gd.uid b/scripts/components/damage_component.gd.uid deleted file mode 100644 index c1a208c..0000000 --- a/scripts/components/damage_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dkmxhjtmu5xlb diff --git a/scripts/components/destroyable_component.gd b/scripts/components/destroyable_component.gd deleted file mode 100644 index 02dfa7d..0000000 --- a/scripts/components/destroyable_component.gd +++ /dev/null @@ -1,23 +0,0 @@ -class_name DestroyableComponent -extends Node - -@export var root: Node -@export var health_component: HealthComponent -@export var destroy_effect: PackedScene - - -func _ready() -> void: - if not health_component: - printerr("No health component assigned!") - return - - health_component.on_death.connect(on_health_component_death) - - -func on_health_component_death() -> void: - if destroy_effect: - var effect: Node2D = destroy_effect.instantiate() - health_component.get_parent().add_child(effect) - effect.global_position = health_component.global_position - - root.queue_free() diff --git a/scripts/components/destroyable_component.gd.uid b/scripts/components/destroyable_component.gd.uid deleted file mode 100644 index 7258442..0000000 --- a/scripts/components/destroyable_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d01dmoafptl2p diff --git a/scripts/components/effect_inflictor_component.gd b/scripts/components/effect_inflictor_component.gd deleted file mode 100644 index b2d1e3b..0000000 --- a/scripts/components/effect_inflictor_component.gd +++ /dev/null @@ -1,20 +0,0 @@ -class_name EffectInflictorComponent -extends Node - -@export var damage: DamageComponent - - -func _ready() -> void: - if not damage: - printerr("No damage component assigned!") - return - - damage.effect_inflicted.connect(on_effect_inflicted) - - -func on_effect_inflicted(target: Node2D, effect: StatusEffectDataResource) -> void: - var status_effect_component: StatusEffectComponent = target.get_node_or_null("StatusEffectComponent") - if not status_effect_component: - return - - status_effect_component.apply_effect(effect) diff --git a/scripts/components/effect_inflictor_component.gd.uid b/scripts/components/effect_inflictor_component.gd.uid deleted file mode 100644 index 463982b..0000000 --- a/scripts/components/effect_inflictor_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d3brcje121krs diff --git a/scripts/components/enemy_death.gd b/scripts/components/enemy_death.gd deleted file mode 100644 index 6a460a7..0000000 --- a/scripts/components/enemy_death.gd +++ /dev/null @@ -1,28 +0,0 @@ -class_name EnemyDeathComponent -extends Node - -@export var root: Node2D -@export var tween_duration: float = 0.5 -@export var collision_shape_2d: CollisionShape2D -@export var health_component: HealthComponent - -func _ready() -> void: - if not collision_shape_2d: - printerr("No CollisionShape2D assigned!") - return - if not health_component: - printerr("No HealthComponent assigned!") - return - - health_component.on_death.connect(_on_health_component_on_death) - -func _on_health_component_on_death() -> void: - call_deferred("die") - - -func die() -> void: - collision_shape_2d.disabled = true - var tween := create_tween() - tween.tween_property(root, "scale", Vector2(0, 0), tween_duration) - await (tween.finished) - root.queue_free() \ No newline at end of file diff --git a/scripts/components/enemy_death.gd.uid b/scripts/components/enemy_death.gd.uid deleted file mode 100644 index ec5a612..0000000 --- a/scripts/components/enemy_death.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqm371fysuk7i diff --git a/scripts/components/enemy_wave_trigger.gd b/scripts/components/enemy_wave_trigger.gd deleted file mode 100644 index 14f8cd6..0000000 --- a/scripts/components/enemy_wave_trigger.gd +++ /dev/null @@ -1,37 +0,0 @@ -class_name EnemyWaveTrigger -extends Node - -@export var area2d: Area2D -@export var path_follow_node: PathFollow2D -@export var speed: float = 100.0 -@export var loop: bool = false -@export var activate_on_enter: bool = true - -var active: bool = false - - -func _ready() -> void: - area2d.body_entered.connect(_on_body_entered) - if path_follow_node: - path_follow_node.progress = 0 - path_follow_node.set_process(false) - - -func _process(delta: float) -> void: - if not active or not path_follow_node: - return - path_follow_node.progress += speed * delta - if path_follow_node.progress_ratio >= 1.0 and not loop: - active = false - path_follow_node.set_process(false) - - -func _on_body_entered(body: Node2D) -> void: - if activate_on_enter: - start_wave() - - -func start_wave() -> void: - if path_follow_node: - path_follow_node.set_process(true) - active = true \ No newline at end of file diff --git a/scripts/components/enemy_wave_trigger.gd.uid b/scripts/components/enemy_wave_trigger.gd.uid deleted file mode 100644 index 264101b..0000000 --- a/scripts/components/enemy_wave_trigger.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://tmahwsvpkrbv diff --git a/scripts/components/exit_door_component.gd b/scripts/components/exit_door_component.gd deleted file mode 100644 index 96f4cfc..0000000 --- a/scripts/components/exit_door_component.gd +++ /dev/null @@ -1,39 +0,0 @@ -class_name ExitDoorComponent -extends Node - -@export var locked: bool = true -@export var exit_area: Area2D -@export var door_sprite: Sprite2D -@export var opened_door_sfx: AudioStreamPlayer2D -@export var opened_door_frame: int = 0 -signal exit_triggered -@onready var gm: GM = $"/root/GameManager" - - -func _ready() -> void: - if not exit_area: - printerr("ExitDoorComponent: exit_area is not set.") - return - - exit_area.body_entered.connect(on_exit_area_body_entered) - - -func unlock() -> void: - locked = false - if door_sprite: - door_sprite.frame = opened_door_frame - if opened_door_sfx: - opened_door_sfx.play() - - -func on_exit_area_body_entered(_body: Node2D) -> void: - if locked: - return - - exit_triggered.emit() - gm.unlock_level(gm.player_state["current_level"] + 1) - call_deferred("go_to_next_level") - - -func go_to_next_level() -> void: - gm.on_level_complete() \ No newline at end of file diff --git a/scripts/components/exit_door_component.gd.uid b/scripts/components/exit_door_component.gd.uid deleted file mode 100644 index 00328c9..0000000 --- a/scripts/components/exit_door_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwamqffvpa452 diff --git a/scripts/components/explosive_component.gd b/scripts/components/explosive_component.gd deleted file mode 100644 index ecc5c60..0000000 --- a/scripts/components/explosive_component.gd +++ /dev/null @@ -1,64 +0,0 @@ -class_name ExplosiveComponent -extends Node - -@export var root: Node2D -@export var damage: DamageComponent -@export var area2d: Area2D -@export var explosion_area2d: Area2D -@export var explosion_effect: PackedScene -@export var time_to_explode: float = 9.0 -signal on_explosion(body: Node2D) -var timer: Timer - - -func _ready() -> void: - if not damage: - printerr("No damage component assigned!") - return - - if not explosion_area2d: - printerr("No area2d assigned!") - return - - area2d.body_entered.connect(on_area2d_body_entered) - area2d.area_entered.connect(on_area2d_area_entered) - - prepare_timer() - - -func prepare_timer() -> void: - timer = Timer.new() - timer.set_wait_time(time_to_explode) - timer.set_one_shot(true) - timer.autostart = true - timer.timeout.connect(explode) - add_child(timer) - - -func explode() -> void: - timer.stop() - - if explosion_effect: - var explosion_instance: GPUParticles2D = explosion_effect.instantiate() - explosion_instance.global_position = root.global_position - get_tree().current_scene.add_child(explosion_instance) - explosion_instance.emitting = true - - var bodies: Array = explosion_area2d.get_overlapping_bodies() - for body in bodies: - var health_component: HealthComponent = body.get_node_or_null("HealthComponent") - - if damage and health_component: - damage.deal_damage(health_component) - - on_explosion.emit(body) - - root.queue_free() - - -func on_area2d_body_entered(_body: Node2D) -> void: - explode() - - -func on_area2d_area_entered(_area: Area2D) -> void: - explode() diff --git a/scripts/components/explosive_component.gd.uid b/scripts/components/explosive_component.gd.uid deleted file mode 100644 index 8b7fd0a..0000000 --- a/scripts/components/explosive_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://beg4dk7d5pvhp diff --git a/scripts/components/fade_away.gd b/scripts/components/fade_away.gd deleted file mode 100644 index a00bd49..0000000 --- a/scripts/components/fade_away.gd +++ /dev/null @@ -1,27 +0,0 @@ -class_name FadeAwayComponent -extends Node - -@export var sprite2d: Sprite2D -@export var fade_duration: float = 1.0 -@export var speed: float = 10.0 -@export var direction: Vector2 = Vector2.UP -@export var root: Node2D -@export var area2d: Area2D - - -func _ready(): - root = get_parent() - if area2d: - area2d.body_entered.connect(on_area2d_body_entered) - - -func fade_away() -> void: - var fade_tween := create_tween().set_parallel(true) - fade_tween.tween_property(sprite2d, "modulate:a", 0, fade_duration) - fade_tween.tween_property(sprite2d, "position", sprite2d.position + (direction * speed), fade_duration) - await (fade_tween.finished) - root.queue_free() - - -func on_area2d_body_entered(_body: Node2D) -> void: - fade_away() diff --git a/scripts/components/fade_away.gd.uid b/scripts/components/fade_away.gd.uid deleted file mode 100644 index 501bf25..0000000 --- a/scripts/components/fade_away.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bg75hnr3q6grk diff --git a/scripts/components/fire_effect_component.gd b/scripts/components/fire_effect_component.gd deleted file mode 100644 index 0877b2c..0000000 --- a/scripts/components/fire_effect_component.gd +++ /dev/null @@ -1,54 +0,0 @@ -class_name FireEffectComponent -extends Node - -@export var health_component: HealthComponent -@export var status_effect_component: StatusEffectComponent -@export var root: Node2D -@export var fire_fx: GPUParticles2D - -var data: StatusEffectDataResource = null -var should_deal_damage: bool = false -var time_elapsed: float = 0.0 - - -func _ready() -> void: - if not health_component: - health_component = root.get_node("HealthComponent") - if not status_effect_component: - status_effect_component = root.get_node("StatusEffectComponent") - - if not health_component: - printerr("No HealthComponent assigned!") - return - if not status_effect_component: - printerr("No StatusEffectComponent assigned!") - return - - status_effect_component.effect_applied.connect(on_effect_applied) - status_effect_component.effect_removed.connect(on_effect_removed) - - -func _process(delta: float) -> void: - if not should_deal_damage or not health_component or not data: - return - - time_elapsed += delta - if time_elapsed >= 1.0: - health_component.decrease_health(data.damage_per_second) - time_elapsed = 0.0 - - -func on_effect_applied(effect_data: StatusEffectDataResource) -> void: - if effect_data.effect_type == StatusEffectComponent.EffectType.FIRE: - data = effect_data - should_deal_damage = true - if fire_fx: - fire_fx.emitting = true - - -func on_effect_removed(effect_type: StatusEffectComponent.EffectType) -> void: - if effect_type == StatusEffectComponent.EffectType.FIRE: - data = null - should_deal_damage = false - if fire_fx: - fire_fx.emitting = false diff --git a/scripts/components/fire_effect_component.gd.uid b/scripts/components/fire_effect_component.gd.uid deleted file mode 100644 index 25e50ec..0000000 --- a/scripts/components/fire_effect_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://qi2irprbqru2 diff --git a/scripts/components/flashing_component.gd b/scripts/components/flashing_component.gd deleted file mode 100644 index a3134a3..0000000 --- a/scripts/components/flashing_component.gd +++ /dev/null @@ -1,55 +0,0 @@ -class_name FlashingComponent -extends Node - -@export var sprite: Node2D -@export var flash_duration: float = 0.5 -@export var flash_time: float = 0.1 -@export var use_modulate: bool = true -@export var health_component: HealthComponent - -var tween: Tween - - -func _ready() -> void: - if health_component: - health_component.on_health_change.connect(on_health_change) - health_component.on_death.connect(on_death) - - if not sprite: - printerr("No sprite assigned!") - return - - -func start_flashing() -> void: - if not sprite: - return - - if tween: - tween.kill() - - tween = create_tween() - tween.set_parallel(false) - - var flashes: int = int(flash_duration / flash_time) - for i in range(flashes): - var opacity: float = 0.3 if i % 2 == 0 else 1.0 - tween.tween_property(sprite, "modulate:a" if use_modulate else "visible", opacity if use_modulate else float(i % 2 == 0), flash_time) - - tween.tween_callback(stop_flashing) - - -func stop_flashing() -> void: - if use_modulate: - sprite.modulate.a = 1.0 - else: - sprite.visible = true - - -func on_health_change(delta: float, _total_health: float) -> void: - if delta < 0: - start_flashing() - - -func on_death() -> void: - stop_flashing() - \ No newline at end of file diff --git a/scripts/components/flashing_component.gd.uid b/scripts/components/flashing_component.gd.uid deleted file mode 100644 index 5875465..0000000 --- a/scripts/components/flashing_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqmbvuutd5c3c diff --git a/scripts/components/flip_player.gd b/scripts/components/flip_player.gd deleted file mode 100644 index 2471365..0000000 --- a/scripts/components/flip_player.gd +++ /dev/null @@ -1,27 +0,0 @@ -class_name FlipPlayerComponent -extends Node2D - -@export var eye_left: Sprite2D -@export var eye_right: Sprite2D -@export var platform_movement: PlatformMovement - - -func _process(_delta: float) -> void: - if not platform_movement: - return - - var velocity := platform_movement.last_direction - if velocity.x < 0: - eye_left.frame = 1 - eye_right.frame = 1 - eye_left.flip_h = true - eye_right.flip_h = true - elif velocity.x > 0: - eye_left.frame = 1 - eye_right.frame = 1 - eye_left.flip_h = false - eye_right.flip_h = false - else: - eye_left.frame = 0 - eye_right.frame = 0 - diff --git a/scripts/components/flip_player.gd.uid b/scripts/components/flip_player.gd.uid deleted file mode 100644 index d81faa3..0000000 --- a/scripts/components/flip_player.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://oxeqvxkgj87j diff --git a/scripts/components/gravity_motion_component.gd b/scripts/components/gravity_motion_component.gd deleted file mode 100644 index d01e1ba..0000000 --- a/scripts/components/gravity_motion_component.gd +++ /dev/null @@ -1,30 +0,0 @@ -class_name GravityMotionComponent -extends Node2D - -@export var character_body: CharacterBody2D -@export var launch_component: LaunchComponent -@export var gravity: Vector2 = Vector2(0, 980.0) -@export var target_direction: Vector2 = Vector2(1.0, -1.0) - -var velocity: Vector2 = Vector2.ZERO - - -func _ready() -> void: - if not launch_component: - return - var direction := target_direction if launch_component.initial_direction.x > 0 else Vector2(-target_direction.x, target_direction.y) - direction = direction.normalized() - velocity = direction * launch_component.speed - - -func _physics_process(delta: float) -> void: - if not character_body: - return - - velocity += gravity * delta - character_body.velocity = velocity - - character_body.move_and_slide() - - if velocity.length_squared() > 0.01: - character_body.rotation = velocity.angle() \ No newline at end of file diff --git a/scripts/components/gravity_motion_component.gd.uid b/scripts/components/gravity_motion_component.gd.uid deleted file mode 100644 index fcf4f44..0000000 --- a/scripts/components/gravity_motion_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c2gbumw4x4t1v diff --git a/scripts/components/heal_component.gd b/scripts/components/heal_component.gd deleted file mode 100644 index 9b8b281..0000000 --- a/scripts/components/heal_component.gd +++ /dev/null @@ -1,40 +0,0 @@ -class_name HealComponent -extends Node - -@export var heal_fx: GPUParticles2D -@export var collectable: CollectableComponent - - -func _ready() -> void: - if not collectable: - printerr("HealComponent: No CollectableComponent assigned.") - return - - collectable.collected.connect(on_collected) - - -func on_collected(amount: float, type: CollectableResource.CollectableType, _body: Node2D) -> void: - if type != CollectableResource.CollectableType.HEALTH: - return - - if not collectable: - printerr("HealComponent: No CollectableComponent assigned.") - return - - var health_component := _body.get_node_or_null("HealthComponent") as HealthComponent - if not health_component or not health_component is HealthComponent: - printerr("HealComponent: No HealthComponent found on collected body.") - return - health_component.increase_health(amount) - if heal_fx: - play_heal_fx() - else: - owner.queue_free() - - -func play_heal_fx() -> void: - if not heal_fx: - return - - heal_fx.restart() - heal_fx.emitting = true \ No newline at end of file diff --git a/scripts/components/heal_component.gd.uid b/scripts/components/heal_component.gd.uid deleted file mode 100644 index 344b7eb..0000000 --- a/scripts/components/heal_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cegdd1sravi5m diff --git a/scripts/components/health.gd b/scripts/components/health.gd deleted file mode 100644 index d636806..0000000 --- a/scripts/components/health.gd +++ /dev/null @@ -1,48 +0,0 @@ -class_name HealthComponent -extends Node - -@export var health: float = 1.0 -@export var max_health: float = 1.0 -@export var hurt_fx: AudioStreamPlayer2D -@export var heal_fx: AudioStreamPlayer2D -signal on_health_change(delta: float, total_health: float) -signal on_death - - - -func _get_delta(new_value: float) -> float: - return new_value - health - - -func set_health(new_value: float): - _apply_health_change(new_value) - - -func decrease_health(value: float): - _apply_health_change(health - value) - - -func increase_health(value: float): - _apply_health_change(health + value) - - -func _apply_health_change(new_health: float, play_fx: bool = true) -> void: - new_health = clamp(new_health, 0.0, max_health) - var delta := new_health - health - - if delta == 0.0: - return # No change - - if play_fx: - if delta > 0 and heal_fx: - heal_fx.play() - elif delta < 0 and hurt_fx: - hurt_fx.play() - await hurt_fx.finished - - health = new_health - - if health <= 0: - on_death.emit() - else: - on_health_change.emit(delta, health) diff --git a/scripts/components/health.gd.uid b/scripts/components/health.gd.uid deleted file mode 100644 index eb93c41..0000000 --- a/scripts/components/health.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://btfsq0bvtrx3t diff --git a/scripts/components/hit_component.gd b/scripts/components/hit_component.gd deleted file mode 100644 index a9000d4..0000000 --- a/scripts/components/hit_component.gd +++ /dev/null @@ -1,57 +0,0 @@ -class_name HitComponent -extends Node - -@export var sprite: Sprite2D -@export var health_component: HealthComponent -@export var hit_duration: float = 0.1 -@export var hit_fx: GPUParticles2D -@export var flash_mode: bool = true - - -func _ready() -> void: - if health_component: - health_component.on_health_change.connect(on_health_change) - health_component.on_death.connect(on_death) - - if not sprite: - printerr("No sprite assigned!") - return - - if sprite.material and flash_mode: - sprite.material = sprite.material.duplicate() - - -func activate() -> void: - if not flash_mode: - return - sprite.material.set_shader_parameter("enabled", true) - - -func deactivate() -> void: - if not flash_mode: - return - sprite.material.set_shader_parameter("enabled", false) - - -func on_health_change(delta: float, total_health: float) -> void: - if delta < 0: - activate() - await get_tree().create_timer(hit_duration).timeout - deactivate() - - if total_health > 0 and delta < 0: - handle_hit_fx() - - -func on_death() -> void: - activate() - await get_tree().create_timer(hit_duration).timeout - deactivate() - - -func handle_hit_fx() -> void: - if not hit_fx: - return - - hit_fx.restart() - hit_fx.emitting = true diff --git a/scripts/components/hit_component.gd.uid b/scripts/components/hit_component.gd.uid deleted file mode 100644 index f78e883..0000000 --- a/scripts/components/hit_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ceq8n7yw7qxpi diff --git a/scripts/components/homing_missile_motion.gd b/scripts/components/homing_missile_motion.gd deleted file mode 100644 index d103912..0000000 --- a/scripts/components/homing_missile_motion.gd +++ /dev/null @@ -1,71 +0,0 @@ -class_name HomingMissileMotion -extends Node - -@export var launch_component: LaunchComponent -@export var root: Node2D -@export var max_speed: float = 16.0 -@export var acceleration: float = 8.0 -@export var detection_area: Area2D -@export var max_turn_rate: float = 180.0 -@export var wobble_strength := 5.0 # degrees -@export var drag := 0.98 -@export var steering_lerp := 0.05 # low = sluggish - -var steering_direction: Vector2 = velocity.normalized() -var target: Node2D = null -var velocity: Vector2 = Vector2.ZERO - - -func _ready() -> void: - if not detection_area: - printerr("No detection area assigned!") - return - - if not launch_component: - printerr("No launch component assigned!") - return - - detection_area.body_entered.connect(on_detection_area_body_entered) - velocity = launch_component.get_initial_velocity() - - -func _physics_process(delta: float) -> void: - if not launch_component or not root: - return - - if not target: - root.position += velocity * delta - return - - var to_target := (target.global_position - root.global_position).normalized() - steering_direction = steering_direction.lerp(to_target, steering_lerp) - - var angle_to_target := velocity.angle_to(steering_direction) - var max_angle := deg_to_rad(max_turn_rate) * delta - var clamped_angle = clamp(angle_to_target, - max_angle, max_angle) - - var wobble := deg_to_rad(randf_range(-wobble_strength, wobble_strength)) - clamped_angle += wobble - - velocity = velocity.rotated(clamped_angle) - - velocity *= drag - - var desired_speed = min(max_speed, velocity.length() + acceleration * delta) - velocity = velocity.normalized() * desired_speed - - root.position += velocity * delta - root.rotation = velocity.angle() - - -func on_detection_area_body_entered(body: Node) -> void: - if target != null: - return - - if body == null: - return - - target = body - - - \ No newline at end of file diff --git a/scripts/components/homing_missile_motion.gd.uid b/scripts/components/homing_missile_motion.gd.uid deleted file mode 100644 index e23147f..0000000 --- a/scripts/components/homing_missile_motion.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://be8jhvb8t3kif diff --git a/scripts/components/ice_effect_component.gd b/scripts/components/ice_effect_component.gd deleted file mode 100644 index efa4678..0000000 --- a/scripts/components/ice_effect_component.gd +++ /dev/null @@ -1,62 +0,0 @@ -class_name IceEffectComponent -extends Node - -@export var components_to_disable: Array = [] -@export var status_effect_component: StatusEffectComponent -@export var ice_fx: Node2D - -var data: StatusEffectDataResource = null -var ice_effects_applied: int = 0 - - -func _ready() -> void: - if not status_effect_component: - status_effect_component = get_node("StatusEffectComponent") - - if not status_effect_component: - printerr("No StatusEffectComponent assigned!") - return - - status_effect_component.effect_applied.connect(on_effect_applied) - status_effect_component.effect_removed.connect(on_effect_removed) - - -func on_effect_applied(effect_data: StatusEffectDataResource) -> void: - if effect_data.effect_type == StatusEffectComponent.EffectType.ICE: - data = effect_data - ice_effects_applied += 1 - apply_freeze() - - -func on_effect_removed(effect_type: StatusEffectComponent.EffectType) -> void: - if effect_type == StatusEffectComponent.EffectType.ICE: - data = null - ice_effects_applied -= 1 - remove_freeze() - - -func apply_freeze() -> void: - if ice_fx: - ice_fx.visible = true - - for component_path in components_to_disable: - var component: Node = get_node_or_null(component_path) - if not component or ice_effects_applied == 0: - continue - - component.process_mode = PROCESS_MODE_DISABLED - - -func remove_freeze() -> void: - if ice_effects_applied > 0: - return - - if ice_fx: - ice_fx.visible = false - - for component_path in components_to_disable: - var component: Node = get_node_or_null(component_path) - if not component: - continue - - component.process_mode = PROCESS_MODE_ALWAYS \ No newline at end of file diff --git a/scripts/components/ice_effect_component.gd.uid b/scripts/components/ice_effect_component.gd.uid deleted file mode 100644 index 38ba092..0000000 --- a/scripts/components/ice_effect_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dhj4qtwcqmqkj diff --git a/scripts/components/invulnerability_component.gd b/scripts/components/invulnerability_component.gd deleted file mode 100644 index 5950ece..0000000 --- a/scripts/components/invulnerability_component.gd +++ /dev/null @@ -1,30 +0,0 @@ -class_name InvulnerabilityComponent -extends Node - -@export var duration: float = 1.0 -@export var flashing_component: FlashingComponent - -var invulnerable: bool = false - - -func activate() -> void: - if invulnerable: - return - - invulnerable = true - if flashing_component: - flashing_component.start_flashing() - - var timer = get_tree().create_timer(duration) - timer.timeout.connect(deactivate) - - -func deactivate() -> void: - invulnerable = false - if flashing_component: - flashing_component.stop_flashing() - - -func is_invulnerable() -> bool: - return invulnerable - \ No newline at end of file diff --git a/scripts/components/invulnerability_component.gd.uid b/scripts/components/invulnerability_component.gd.uid deleted file mode 100644 index 51a9343..0000000 --- a/scripts/components/invulnerability_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ijrli0x8ij8v diff --git a/scripts/components/jump_pad_component.gd b/scripts/components/jump_pad_component.gd deleted file mode 100644 index 8257ffa..0000000 --- a/scripts/components/jump_pad_component.gd +++ /dev/null @@ -1,42 +0,0 @@ -class_name JumpPadComponent -extends Node - -@export var jump_force: float = 10.0 -@export var area2d: Area2D -@export var sprite2d: Sprite2D -@export var start_animation_index: int = 0 -@export var animation_duration: float = 0.5 - - -func _ready() -> void: - if not area2d: - printerr("JumpPadComponent: area2d is not set.") - return - - if not sprite2d: - printerr("JumpPadComponent: sprite2d is not set.") - return - - area2d.body_entered.connect(_on_body_entered) - - -func _on_body_entered(body: Node2D) -> void: - var can_be_launched: CanBeLaunchedComponent = body.get_node_or_null("CanBeLaunchedComponent") - if not can_be_launched: - return - - if body is PlayerController and body.current_movement is PlatformMovement: - handle_launchpad_animation() - body.velocity.y = -jump_force - if body.current_movement.jump_sfx: - body.current_movement.jump_sfx.play() - - -func handle_launchpad_animation() -> void: - if not sprite2d: - return - - var timer := get_tree().create_timer(animation_duration) - sprite2d.frame = start_animation_index + 1 - await timer.timeout - sprite2d.frame = start_animation_index diff --git a/scripts/components/jump_pad_component.gd.uid b/scripts/components/jump_pad_component.gd.uid deleted file mode 100644 index 3a14c81..0000000 --- a/scripts/components/jump_pad_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dwok2qx4wpkey diff --git a/scripts/components/kill_player_out_of_screen.gd b/scripts/components/kill_player_out_of_screen.gd deleted file mode 100644 index 044ac22..0000000 --- a/scripts/components/kill_player_out_of_screen.gd +++ /dev/null @@ -1,24 +0,0 @@ -class_name KillPlayerOutOfScreen -extends Node - -@export var screen_notifier: VisibleOnScreenNotifier2D -@export var health_component: HealthComponent - - -func _ready() -> void: - if not screen_notifier: - printerr("KillPlayerOutOfScreen: screen_notifier is not set.") - return - - if not health_component: - printerr("KillPlayerOutOfScreen: health_component is not set.") - return - - screen_notifier.screen_exited.connect(out_of_screen) - - -func out_of_screen() -> void: - if not health_component: - return - - health_component.decrease_health(6000) diff --git a/scripts/components/kill_player_out_of_screen.gd.uid b/scripts/components/kill_player_out_of_screen.gd.uid deleted file mode 100644 index c207da6..0000000 --- a/scripts/components/kill_player_out_of_screen.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cfeoalic0mu2j diff --git a/scripts/components/knockback.gd b/scripts/components/knockback.gd deleted file mode 100644 index 7b0c658..0000000 --- a/scripts/components/knockback.gd +++ /dev/null @@ -1,32 +0,0 @@ -class_name KnockbackComponent -extends Node - -@export var character_body: CharacterBody2D -@export var knockback_force: float = 25.0 - -var knockback_mode: bool = false -var knockback_frames: int = 0 - -func apply_knockback(force: float, delta: float) -> void: - var velocity = character_body.velocity.normalized() - var knockback_dir = Vector2(sign(velocity.x) * 1.0, 0.4) - var knockback_vector = -knockback_dir * force * delta - character_body.velocity += knockback_vector - -func _on_health_component_on_health_change(delta: float, total_health: float) -> void: - if total_health <= 0.0 and delta < 0.0: - return - knockback_mode = true - -func _process(_delta: float) -> void: - if knockback_mode: - knockback_frames += 1 - if knockback_frames > 1: - knockback_mode = false - knockback_frames = 0 - -func _physics_process(delta: float) -> void: - if knockback_mode: - apply_knockback(knockback_force, delta) - - diff --git a/scripts/components/knockback.gd.uid b/scripts/components/knockback.gd.uid deleted file mode 100644 index 23683ae..0000000 --- a/scripts/components/knockback.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://nogmyshjrv57 diff --git a/scripts/components/launch_component.gd b/scripts/components/launch_component.gd deleted file mode 100644 index 9a936ff..0000000 --- a/scripts/components/launch_component.gd +++ /dev/null @@ -1,17 +0,0 @@ -class_name LaunchComponent -extends Node2D - -@export var root: Node2D -@export var initial_direction: Vector2 = Vector2.RIGHT -@export var speed: float = 16.0 -@export var spawn_position: Vector2 = Vector2.ZERO -@export var spawn_rotation: float = 0.0 - - -func _ready() -> void: - root.global_position = spawn_position - root.global_rotation = spawn_rotation - - -func get_initial_velocity() -> Vector2: - return initial_direction.normalized() * speed diff --git a/scripts/components/launch_component.gd.uid b/scripts/components/launch_component.gd.uid deleted file mode 100644 index e4dd57e..0000000 --- a/scripts/components/launch_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://873un8agkyja diff --git a/scripts/components/lever_component.gd b/scripts/components/lever_component.gd deleted file mode 100644 index 74162e8..0000000 --- a/scripts/components/lever_component.gd +++ /dev/null @@ -1,49 +0,0 @@ -class_name LeverComponent -extends Node - -@export var area2d: Area2D -@export var sprite2d: Sprite2D -@export var start_animation_index: int = 0 -@export var animation_duration: float = 0.5 -@export var sfx: AudioStreamPlayer2D -signal activated - - -func _ready() -> void: - if not area2d: - printerr("LeverComponent: area2d is not set.") - return - - if not sprite2d: - printerr("LeverComponent: sprite2d is not set.") - return - - area2d.body_entered.connect(_on_body_entered) - area2d.area_entered.connect(_on_area_entered) - - -func _on_body_entered(body: Node2D) -> void: - var trigger_lever: TriggerLeverComponent = body.get_node_or_null("TriggerLeverComponent") - if not trigger_lever: - return - - activate() - - - -func _on_area_entered(area: Area2D) -> void: - var trigger_lever: TriggerLeverComponent = area.get_node_or_null("TriggerLeverComponent") - if not trigger_lever: - return - - activate() - - -func activate() -> void: - activated.emit() - if sfx: - sfx.play() - sprite2d.frame = start_animation_index + 1 - var timer := get_tree().create_timer(animation_duration) - await timer.timeout - sprite2d.frame = start_animation_index diff --git a/scripts/components/lever_component.gd.uid b/scripts/components/lever_component.gd.uid deleted file mode 100644 index 0d5a733..0000000 --- a/scripts/components/lever_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hyuwsp1b336a diff --git a/scripts/components/lifetime_component.gd b/scripts/components/lifetime_component.gd deleted file mode 100644 index 212eda8..0000000 --- a/scripts/components/lifetime_component.gd +++ /dev/null @@ -1,22 +0,0 @@ -class_name LifetimeComponent -extends Node - -@export var root: Node -@export var life_time: float = 5.0 -@export var timer: Timer - - -func _ready() -> void: - if not root: - printerr("Root node not set.") - return - - if not timer: - printerr("Timer node not set.") - return - - timer.timeout.connect(on_timer_timeout) - - -func on_timer_timeout() -> void: - root.queue_free() diff --git a/scripts/components/lifetime_component.gd.uid b/scripts/components/lifetime_component.gd.uid deleted file mode 100644 index b19f460..0000000 --- a/scripts/components/lifetime_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvsgg8lu0a8m6 diff --git a/scripts/components/magnetic_skill.gd b/scripts/components/magnetic_skill.gd deleted file mode 100644 index a45974b..0000000 --- a/scripts/components/magnetic_skill.gd +++ /dev/null @@ -1,63 +0,0 @@ -class_name MagneticSkillComponent -extends Node - -@export var magnetic_area: Area2D -@export var root: Node2D -@export var magnetic_move_duration: float = 1.25 - -var collectables_to_pickup: Array = [] - - -func has_component_in_children(node: Node, component_name: String) -> bool: - if not node: - return false - - if node.has_node(component_name): - return true - - for child in node.get_children(): - if has_component_in_children(child, component_name): - return true - - return false - - -func _ready() -> void: - if not magnetic_area: - printerr("No magnetic_area assigned!") - return - - magnetic_area.body_entered.connect(on_magnetic_area_body_entered) - magnetic_area.area_entered.connect(on_magnetic_area_area_entered) - - -func _process(_delta: float) -> void: - for collectable in collectables_to_pickup: - if not is_instance_valid(collectable): - collectables_to_pickup.erase(collectable) - continue - - move_collectable_to_root(collectable) - - -func on_magnetic_area_body_entered(_body: Node2D) -> void: - pass - - -func on_magnetic_area_area_entered(area: Area2D) -> void: - if not has_component_in_children(area, "Collectable"): - return - - if not collectables_to_pickup.has(area): - collectables_to_pickup.append(area) - - -func move_collectable_to_root(collectable: Node2D) -> void: - if not is_instance_valid(collectable): - return - - var direction: Vector2 = (root.global_position - collectable.global_position).normalized() - var speed: float = direction.length() / magnetic_move_duration - - collectable.global_position += direction.normalized() * speed - diff --git a/scripts/components/magnetic_skill.gd.uid b/scripts/components/magnetic_skill.gd.uid deleted file mode 100644 index 2832e58..0000000 --- a/scripts/components/magnetic_skill.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ce8w71vgv37pt diff --git a/scripts/components/out_of_screen_component.gd b/scripts/components/out_of_screen_component.gd deleted file mode 100644 index 6d991a6..0000000 --- a/scripts/components/out_of_screen_component.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name OutOfScreenComponent -extends Node - -@export var visibility_notifier: VisibleOnScreenNotifier2D -@export var root: Node - - -func _ready() -> void: - if not visibility_notifier: - printerr("Visibility notifier not set.") - return - - visibility_notifier.screen_exited.connect(_on_screen_exited) - - -func _on_screen_exited() -> void: - if not root: - printerr("Root node not set.") - return - - root.queue_free() diff --git a/scripts/components/out_of_screen_component.gd.uid b/scripts/components/out_of_screen_component.gd.uid deleted file mode 100644 index 6c1e3f3..0000000 --- a/scripts/components/out_of_screen_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://1tnr46o1ib4u diff --git a/scripts/components/periodic_shooting.gd b/scripts/components/periodic_shooting.gd deleted file mode 100644 index 3937a30..0000000 --- a/scripts/components/periodic_shooting.gd +++ /dev/null @@ -1,60 +0,0 @@ -class_name PeriodicShootingComponent -extends Node - -@export var bullet_scene: PackedScene = preload("res://objects/entities/bullet.tscn") -@export var shoot_interval: float = 1.0 -@export var shoot_direction: Vector2 = Vector2.RIGHT -@export var side_to_side_movement: SideToSideMovement -@export var root: Node2D -@export var bullet_spawn_right: Node2D -@export var bullet_spawn_left: Node2D -@export var shooting_interval_variation: float = 0.0 - -var timer: Timer - - -func _ready() -> void: - root = get_parent() - setup_timer() - - -func _process(_delta: float) -> void: - if side_to_side_movement: - shoot_direction = side_to_side_movement.direction if side_to_side_movement.direction != Vector2.ZERO else Vector2.RIGHT - - -func shoot() -> void: - if shoot_direction == Vector2.ZERO: - return - - var bullet_instance: Node2D = bullet_scene.instantiate() - var launch_component: LaunchComponent = bullet_instance.get_node_or_null("LaunchComponent") - var spawn_position: Vector2 = bullet_spawn_right.global_position if shoot_direction == Vector2.RIGHT else bullet_spawn_left.global_position - if launch_component: - launch_component.initial_direction = shoot_direction - launch_component.spawn_position = spawn_position - launch_component.spawn_rotation = root.rotation - - bullet_instance.position = spawn_position - get_tree().current_scene.add_child(bullet_instance) - - -func on_timer_timeout() -> void: - shoot() - timer.start() - - - -func get_shoot_interval() -> float: - if shooting_interval_variation > 0.0: - return shoot_interval + randf_range(-shooting_interval_variation, shooting_interval_variation) - return shoot_interval - - -func setup_timer() -> void: - timer = Timer.new() - timer.wait_time = get_shoot_interval() - timer.one_shot = false - timer.autostart = true - timer.timeout.connect(on_timer_timeout) - add_child(timer) diff --git a/scripts/components/periodic_shooting.gd.uid b/scripts/components/periodic_shooting.gd.uid deleted file mode 100644 index 9560a8a..0000000 --- a/scripts/components/periodic_shooting.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b37gqnycj6rtk diff --git a/scripts/components/platform_movement.gd b/scripts/components/platform_movement.gd deleted file mode 100644 index 38773da..0000000 --- a/scripts/components/platform_movement.gd +++ /dev/null @@ -1,118 +0,0 @@ -class_name PlatformMovement -extends PlayerMovement - -@export var speed: float = 300.0 -@export var jump_height: float = 100 -@export var jump_time_to_peak: float = 0.5 -@export var jump_time_to_descent: float = 0.4 -@export var coyote_frames: int = 6 -@export var jump_sfx: AudioStreamPlayer2D -@export var rotation_target: Node2D -@export var body: CharacterBody2D - -var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") -var was_last_floor := false -var coyote_mode := false -var coyote_timer: Timer -var last_direction := Vector2.RIGHT - -@onready var jump_velocity: float = ((2.0 * jump_height) / jump_time_to_peak) * -1.0 -@onready var jump_gravity: float = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0 -@onready var fall_gravity: float = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0 - - -func _ready() -> void: - if not body: - return - - coyote_timer = Timer.new() - coyote_timer.one_shot = true - coyote_timer.wait_time = coyote_frames / 60.0 - coyote_timer.timeout.connect(on_coyote_timer_timeout) - add_child(coyote_timer) - - -func _process(_delta: float) -> void: - if not body or not enabled: - return - - if body.velocity.x > 0.0: - rotation_target.rotation = deg_to_rad(-10) - elif body.velocity.x < 0.0: - rotation_target.rotation = deg_to_rad(10) - else: - rotation_target.rotation = 0 - - calculate_jump_vars() - - -func _physics_process(delta) -> void: - if not body or not enabled: - return - - if body.is_on_floor(): - was_last_floor = true - coyote_mode = false # Reset coyote mode when back on the floor - coyote_timer.stop() # Stop timer when grounded - else: - if was_last_floor: # Start coyote timer only once - coyote_mode = true - coyote_timer.start() - was_last_floor = false - - if not body.is_on_floor(): - body.velocity.y += calculate_gravity() * delta - - if Input.is_action_pressed("jump") and (body.is_on_floor() or coyote_mode): - jump() - - if Input.is_action_just_pressed("down"): - body.position.y += 1 - - var direction := Input.get_axis("left", "right") - if direction != 0: - last_direction = handle_direction(direction) - - if direction: - body.velocity.x = direction * speed - else: - body.velocity.x = move_toward(body.velocity.x, 0, speed) - - previous_velocity = body.velocity - body.move_and_slide() - - -func jump() -> void: - if not body: - return - - body.velocity.y = jump_velocity - coyote_mode = false - if jump_sfx: - jump_sfx.play() - - -func calculate_gravity() -> float: - return jump_gravity if body.velocity.y < 0.0 else fall_gravity - - -func on_coyote_timer_timeout() -> void: - coyote_mode = false - - -func handle_direction(input_dir: float) -> Vector2: - if input_dir > 0: - return Vector2.RIGHT - elif input_dir < 0: - return Vector2.LEFT - return last_direction - - -func on_ship_entered() -> void: - rotation_target.rotation = 0 - - -func calculate_jump_vars() -> void: - jump_velocity = ((2.0 * jump_height) / jump_time_to_peak) * -1.0 - jump_gravity = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0 - fall_gravity = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0 \ No newline at end of file diff --git a/scripts/components/platform_movement.gd.uid b/scripts/components/platform_movement.gd.uid deleted file mode 100644 index 8a0e2a0..0000000 --- a/scripts/components/platform_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cflncpa377l8l diff --git a/scripts/components/player_death.gd b/scripts/components/player_death.gd deleted file mode 100644 index 0e37528..0000000 --- a/scripts/components/player_death.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name PlayerDeathComponent -extends Node2D - -@export var death_sfx: AudioStreamPlayer2D -@export var death_effect: PackedScene - -@onready var gm: GM = $"/root/GameManager" - - -func _on_health_component_on_death() -> void: - if death_sfx: - death_sfx.play() - if death_effect: - var effect: Node2D = death_effect.instantiate() - get_parent().add_child(effect) - effect.global_position = global_position - effect.scale = Vector2(1.5, 1.5) - - gm.remove_lives(1) - gm.reset_current_session_state() - diff --git a/scripts/components/player_death.gd.uid b/scripts/components/player_death.gd.uid deleted file mode 100644 index 0425315..0000000 --- a/scripts/components/player_death.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dkpu3121y88oo diff --git a/scripts/components/player_movement.gd b/scripts/components/player_movement.gd deleted file mode 100644 index 2b37f0c..0000000 --- a/scripts/components/player_movement.gd +++ /dev/null @@ -1,17 +0,0 @@ -class_name PlayerMovement -extends Node - -@export var type: String = "" - -var enabled: bool = true -var previous_velocity: Vector2 = Vector2.ZERO - - -func _process(_delta: float) -> void: - if not enabled: - return - - -func _physics_process(_delta: float) -> void: - if not enabled: - return \ No newline at end of file diff --git a/scripts/components/player_movement.gd.uid b/scripts/components/player_movement.gd.uid deleted file mode 100644 index 1e45c17..0000000 --- a/scripts/components/player_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqtc3sxew0sau diff --git a/scripts/components/progressive_damage_component.gd b/scripts/components/progressive_damage_component.gd deleted file mode 100644 index 6eebabf..0000000 --- a/scripts/components/progressive_damage_component.gd +++ /dev/null @@ -1,67 +0,0 @@ -class_name ProgressiveDamageComponent -extends Node - -@export var health_component: HealthComponent -@export var sprite: Sprite2D -@export var platform_movement: PlatformMovement -@export var min_jump_height: float = 60.0 -@export var jump_reduction_percentage: float = 0.1 # this is a percentage of the jump height per hit - -@onready var max_health: float = health_component.max_health - -var og_jump_height: float = 0.0 - - -func _ready() -> void: - if not health_component: - printerr("ProgressiveDamageComponent: health_component is not set.") - return - if not sprite: - printerr("ProgressiveDamageComponent: sprite is not set.") - return - - health_component.on_health_change.connect(on_health_change) - - if platform_movement: - og_jump_height = platform_movement.jump_height - - -func get_damage_frame() -> int: - if not sprite or not health_component: - return 0 - - var frames_count := sprite.get_hframes() - if frames_count == 0: - return 0 - - var current_health := health_component.health - var health_ratio := current_health / max_health - return int(frames_count * (1.0 - health_ratio)) - - -func get_jump_height() -> float: - if not platform_movement: - return 0.0 - - var jump_height := og_jump_height - if jump_height <= 0: - return 0.0 - - var damage_frame := get_damage_frame() - if damage_frame < 0 or damage_frame >= sprite.get_hframes(): - return jump_height - - var reduction := jump_reduction_percentage * jump_height - var calculated_jump_height := jump_height - (damage_frame * reduction) - return max(calculated_jump_height, min_jump_height) - - -func on_health_change(_delta: float, _total_health: float) -> void: - var frame := get_damage_frame() - if frame < 0 or frame >= sprite.get_hframes(): - return - - sprite.frame = frame - if platform_movement: - platform_movement.jump_height = get_jump_height() - \ No newline at end of file diff --git a/scripts/components/progressive_damage_component.gd.uid b/scripts/components/progressive_damage_component.gd.uid deleted file mode 100644 index 0b2e3bb..0000000 --- a/scripts/components/progressive_damage_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d32kd83lf86iy diff --git a/scripts/components/projectile_component.gd b/scripts/components/projectile_component.gd deleted file mode 100644 index 43aeefe..0000000 --- a/scripts/components/projectile_component.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name ProjectileComponent -extends Node2D - -@export var speed: float = 16.0 -@export var dir: float -@export var spawn_position: Vector2 -@export var spawn_rotation: float -@export var character_body: CharacterBody2D - - -func _ready() -> void: - global_position = spawn_position - global_rotation = spawn_rotation - - -func _physics_process(delta: float) -> void: - if not character_body: - return - - character_body.velocity = Vector2(0, -speed).rotated(dir) - character_body.move_and_slide() \ No newline at end of file diff --git a/scripts/components/projectile_component.gd.uid b/scripts/components/projectile_component.gd.uid deleted file mode 100644 index e7c911d..0000000 --- a/scripts/components/projectile_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bk2fvcv0g4x1q diff --git a/scripts/components/projectile_init_component.gd b/scripts/components/projectile_init_component.gd deleted file mode 100644 index 4c89794..0000000 --- a/scripts/components/projectile_init_component.gd +++ /dev/null @@ -1,20 +0,0 @@ -class_name ProjectileInitComponent -extends Node - -@export var launch_component: LaunchComponent - - -func initialize(params: Dictionary) -> void: - var position := params.get("position", Vector2.ZERO) as Vector2 - var direction := params.get("direction", Vector2.RIGHT) as Vector2 - var rotation := params.get("rotation", 0.0) as float - var power := params.get("power_multiplier", 1.0) as float - - owner.global_position = position - owner.global_rotation = rotation - - if launch_component: - launch_component.initial_direction = direction - launch_component.spawn_position = position - launch_component.spawn_rotation = rotation - launch_component.speed *= power diff --git a/scripts/components/projectile_init_component.gd.uid b/scripts/components/projectile_init_component.gd.uid deleted file mode 100644 index 7a31218..0000000 --- a/scripts/components/projectile_init_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bgty7040ams6s diff --git a/scripts/components/requirement_component.gd b/scripts/components/requirement_component.gd deleted file mode 100644 index bf72946..0000000 --- a/scripts/components/requirement_component.gd +++ /dev/null @@ -1,32 +0,0 @@ -class_name RequirementComponent -extends Node - -@export var requirement_type: CollectableResource.CollectableType -@export var required_amount: int = 1 - -var current_amount: int = 0 -signal requirement_met(requirement_type: CollectableResource.CollectableType) -@onready var gm: GM = $"/root/GameManager" - - -func _ready() -> void: - if not gm: - printerr("RequirementComponent: GameManager not found.") - return - - var collectables: Array[CollectableComponent] = gm.get_colllectable_nodes() - for collectable in collectables: - collectable.collected.connect(on_collected) - - - -func on_collected(amount: int, type: CollectableResource.CollectableType, _body: Node2D) -> void: - if type != requirement_type: - return - add_progress(amount) - - -func add_progress(amount: int = 1) -> void: - current_amount += amount - if current_amount >= required_amount: - requirement_met.emit(requirement_type) diff --git a/scripts/components/requirement_component.gd.uid b/scripts/components/requirement_component.gd.uid deleted file mode 100644 index caa47bb..0000000 --- a/scripts/components/requirement_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cmh8k0rdsyh7j diff --git a/scripts/components/score.gd b/scripts/components/score.gd deleted file mode 100644 index c8b7576..0000000 --- a/scripts/components/score.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name ScoreComponent -extends Node - -@onready var game_manager: GM = $"/root/GameManager" - - -func _ready(): - await get_tree().process_frame - var coins := get_tree().get_nodes_in_group("coins") - - for coin in coins: - coin.collected.connect(on_collected) - - - -func on_collected(amount: int, type: CollectableResource.CollectableType, _body: Node2D) -> void: - if not game_manager: - return - if type != CollectableResource.CollectableType.COIN: - return - game_manager.current_session_state["coins_collected"] += amount diff --git a/scripts/components/score.gd.uid b/scripts/components/score.gd.uid deleted file mode 100644 index 423413f..0000000 --- a/scripts/components/score.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://qeu80jy4vmuf diff --git a/scripts/components/ship_movement.gd b/scripts/components/ship_movement.gd deleted file mode 100644 index 965c40f..0000000 --- a/scripts/components/ship_movement.gd +++ /dev/null @@ -1,30 +0,0 @@ -class_name ShipMovement -extends PlayerMovement - -@export var max_speed: float = 200.0 -@export var acceleration: float = 100.0 -@export var friction: float = 50.0 -@export var body: CharacterBody2D - -var velocity: Vector2 = Vector2.ZERO - - -func _physics_process(delta: float) -> void: - if not body or not enabled: - return - - var input_vector := Vector2( - Input.get_action_strength("right") - Input.get_action_strength("left"), - Input.get_action_strength("down") - Input.get_action_strength("up") - ).normalized() - - if input_vector != Vector2.ZERO: - velocity = velocity.move_toward(input_vector * max_speed, acceleration * delta) - else: - velocity = velocity.move_toward(Vector2.ZERO, friction * delta) - - velocity = velocity.limit_length(max_speed) - body.velocity = velocity - previous_velocity = body.velocity - body.move_and_slide() - \ No newline at end of file diff --git a/scripts/components/ship_movement.gd.uid b/scripts/components/ship_movement.gd.uid deleted file mode 100644 index cc54df9..0000000 --- a/scripts/components/ship_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3mrdvre1y567 diff --git a/scripts/components/ship_shooter.gd b/scripts/components/ship_shooter.gd deleted file mode 100644 index f7ceee9..0000000 --- a/scripts/components/ship_shooter.gd +++ /dev/null @@ -1,50 +0,0 @@ -class_name ShipShooter -extends Node - -@export var bullet_scene: PackedScene -@export var fire_rate: float = 0.2 -@export var bullet_spawn: Marker2D -@export var shoot_sfx: AudioStreamPlayer2D - - -var can_shoot: bool = false - - -func _ready() -> void: - set_process(false) - - -func _process(_delta: float) -> void: - if Input.is_action_just_pressed("attack") and can_shoot: - shoot() - - -func shoot() -> void: - if not can_shoot: - return - - var bullet: Node2D = bullet_scene.instantiate() - var init := bullet.get_node_or_null("ProjectileInitComponent") as ProjectileInitComponent - if init: - init.initialize({ - "position": bullet_spawn.global_position, - }) - get_tree().current_scene.add_child(bullet) - if shoot_sfx: - shoot_sfx.play() - - can_shoot = false - await get_tree().create_timer(fire_rate).timeout - can_shoot = true - - -func on_ship_entered(): - can_shoot = true - set_process(true) - - -func on_ship_exited(): - can_shoot = false - set_process(false) - if shoot_sfx: - shoot_sfx.stop() \ No newline at end of file diff --git a/scripts/components/ship_shooter.gd.uid b/scripts/components/ship_shooter.gd.uid deleted file mode 100644 index d244698..0000000 --- a/scripts/components/ship_shooter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d1ctdx52gskv1 diff --git a/scripts/components/side_to_side_movement.gd b/scripts/components/side_to_side_movement.gd deleted file mode 100644 index 8563f01..0000000 --- a/scripts/components/side_to_side_movement.gd +++ /dev/null @@ -1,101 +0,0 @@ -class_name SideToSideMovement -extends Node - -@export var root: Node2D -@export var sprite2d: Sprite2D -@export var speed: float = 10.0 -@export var wait_time: float = 1.0 -@export var left_ray: RayCast2D -@export var right_ray: RayCast2D -@export var left_wall_ray: RayCast2D -@export var right_wall_ray: RayCast2D - -var direction: Vector2 = Vector2.LEFT -var new_direction: Vector2 = Vector2.LEFT -var timer: Timer -var triggered_direction_change: bool = false -signal direction_changed() - - -func _ready() -> void: - root = get_parent() - - if not sprite2d: - printerr("SideToSideMovement node missing Sprite2D child.") - return - - if not left_ray or not right_ray: - printerr("SideToSideMovement node missing RayCast2D children.") - return - - setup_timer() - direction_changed.connect(on_direction_changed) - - -func _physics_process(delta: float) -> void: - handle_direction() - handle_sprite_flip() - handle_movement(delta) - - -func handle_direction() -> void: - # check if we are colliding with the left wall - if left_wall_ray.is_colliding(): - new_direction = Vector2.RIGHT - direction_changed.emit() - return - - # check if we are colliding with the right wall - if right_wall_ray.is_colliding(): - new_direction = Vector2.LEFT - direction_changed.emit() - return - - # we are not colliding with anything, which means we don't have ground to walk on. Stop moving. - if not left_ray.is_colliding() and not right_ray.is_colliding(): - new_direction = Vector2.ZERO - return - - # If the left ray is not colliding and the right ray is colliding, that means we have ground to the right and we should change direction to the right. - if not left_ray.is_colliding() and right_ray.is_colliding(): - new_direction = Vector2.RIGHT - direction_changed.emit() - return - if not right_ray.is_colliding() and left_ray.is_colliding(): - new_direction = Vector2.LEFT - direction_changed.emit() - return - - -func handle_sprite_flip() -> void: - if direction == Vector2.LEFT: - sprite2d.flip_h = true - else: - sprite2d.flip_h = false - - -func handle_movement(delta: float) -> void: - root.position += direction * speed * delta - - -func on_direction_changed() -> void: - if direction == new_direction or triggered_direction_change: - return - triggered_direction_change = true - direction = Vector2.ZERO - timer.start() - - -func on_timer_timeout() -> void: - timer.stop() - direction = new_direction - triggered_direction_change = false - - -func setup_timer() -> void: - timer = Timer.new() - add_child(timer) - timer.wait_time = wait_time - timer.one_shot = true - timer.timeout.connect(on_timer_timeout) - diff --git a/scripts/components/side_to_side_movement.gd.uid b/scripts/components/side_to_side_movement.gd.uid deleted file mode 100644 index a653dab..0000000 --- a/scripts/components/side_to_side_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmqpif5qhvri3 diff --git a/scripts/components/skill_unlocker_component.gd b/scripts/components/skill_unlocker_component.gd deleted file mode 100644 index 8ef3317..0000000 --- a/scripts/components/skill_unlocker_component.gd +++ /dev/null @@ -1,64 +0,0 @@ -class_name SkillUnlockerComponent -extends Node - -@export var skill_manager: SkillManager - -@onready var game_manager: GM = $"/root/GameManager" -signal skill_unlocked(skill_data: SkillData) - - -func has_enough_coins(amount: int) -> bool: - return game_manager and game_manager.get_coins() >= amount - - -func try_unlock_skill(skill_data: SkillData) -> bool: - if not game_manager: - return false - - if game_manager.is_skill_unlocked(skill_data): - return false - - if not has_enough_coins(skill_data.cost): - return false - - var skill: SkillData = skill_data - skill.level = 1 - skill.is_active = true - - game_manager.remove_coins(skill.cost) - game_manager.current_session_state["skills_unlocked"].append(skill) - skill_manager.add_skill(skill) - skill_unlocked.emit(skill) - return true - - -func unlock_all_skills() -> void: - var available_skills: Array[SkillData] = skill_manager.available_skills - var skills: Array[String] = [] - - for skill in available_skills: - skills.append(skill.name) - skill_unlocked.emit(skill) - - game_manager.unlock_skills(available_skills) - skill_manager.apply_unlocked_skills() - - - -func try_upgrade_skill(skill_data: SkillData) -> bool: - if not game_manager: - return false - - if not game_manager.is_skill_unlocked(skill_data): - return false - - if skill_data.level >= skill_data.max_level: - return false - - if not has_enough_coins(skill_data.cost): - return false - - game_manager.remove_coins(skill_data.cost) - skill_data.level += 1 - skill_unlocked.emit(skill_data) - return true diff --git a/scripts/components/skill_unlocker_component.gd.uid b/scripts/components/skill_unlocker_component.gd.uid deleted file mode 100644 index 1aa9e46..0000000 --- a/scripts/components/skill_unlocker_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bjsyeo1n7bsri diff --git a/scripts/components/spaceship_enter_component.gd b/scripts/components/spaceship_enter_component.gd deleted file mode 100644 index 5eb0bdf..0000000 --- a/scripts/components/spaceship_enter_component.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name SpaceshipEnterComponent -extends Node - -@export var area2d: Area2D -signal spaceship_entered - - -func _ready() -> void: - if not area2d: - printerr("SpaceshipEnterComponent: area2d is not set.") - return - - area2d.body_entered.connect(_on_area2d_body_entered) - - -func _on_area2d_body_entered(body: Node2D) -> void: - if not body is PlayerController: - return - - spaceship_entered.emit() - owner.queue_free() \ No newline at end of file diff --git a/scripts/components/spaceship_enter_component.gd.uid b/scripts/components/spaceship_enter_component.gd.uid deleted file mode 100644 index 8e73b21..0000000 --- a/scripts/components/spaceship_enter_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ghb614g22ph7 diff --git a/scripts/components/spaceship_exit_component.gd b/scripts/components/spaceship_exit_component.gd deleted file mode 100644 index 62e65da..0000000 --- a/scripts/components/spaceship_exit_component.gd +++ /dev/null @@ -1,21 +0,0 @@ -class_name SpaceshipExitComponent -extends Node - -@export var area2d: Area2D -signal spaceship_exited - - -func _ready() -> void: - if not area2d: - printerr("SpaceshipExitComponent: area2d is not set.") - return - - area2d.body_entered.connect(_on_area2d_body_entered) - - -func _on_area2d_body_entered(body: Node2D) -> void: - if not body is PlayerController: - printerr("SpaceshipExitComponent: body is not a PlayerController.") - return - print("SpaceshipExitComponent: PlayerController exited spaceship.") - spaceship_exited.emit() \ No newline at end of file diff --git a/scripts/components/spaceship_exit_component.gd.uid b/scripts/components/spaceship_exit_component.gd.uid deleted file mode 100644 index 245189f..0000000 --- a/scripts/components/spaceship_exit_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ybmwls7hv8j3 diff --git a/scripts/components/spin_component.gd b/scripts/components/spin_component.gd deleted file mode 100644 index b32c1e5..0000000 --- a/scripts/components/spin_component.gd +++ /dev/null @@ -1,18 +0,0 @@ -class_name SpinComponent -extends Node2D - -@export var root: Node2D -@export var speed: float = 8.0 -@export var rotation_direction: Vector2 = Vector2.RIGHT - - -func _process(delta: float) -> void: - spin(delta) - - -func spin(delta: float) -> void: - if not root: - return - - var rotation_speed: float = speed * rotation_direction.x - root.rotation += rotation_speed * delta \ No newline at end of file diff --git a/scripts/components/spin_component.gd.uid b/scripts/components/spin_component.gd.uid deleted file mode 100644 index 29ecfdc..0000000 --- a/scripts/components/spin_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cq26pfxn17s7s diff --git a/scripts/components/status_effect_component.gd b/scripts/components/status_effect_component.gd deleted file mode 100644 index 008bb8d..0000000 --- a/scripts/components/status_effect_component.gd +++ /dev/null @@ -1,45 +0,0 @@ -class_name StatusEffectComponent -extends Node - -enum EffectType { - NONE, - FIRE, - ICE -} -var active_effects: Array = [] -signal effect_applied(effect_data: StatusEffectDataResource) -signal effect_removed(effect_type: EffectType) - - -func apply_effect(effect_data: StatusEffectDataResource) -> void: - var data: StatusEffectDataResource = effect_data.duplicate() - - var timer: Timer = create_timer(effect_data.duration, data) - - var new_effect: Dictionary = { - "data": data, - "elapsed_time": 0.0, - "timer": timer - } - - active_effects.append(new_effect) - effect_applied.emit(effect_data) - - -func remove_effect(effect_data: StatusEffectDataResource) -> void: - for i in range(active_effects.size()): - if active_effects[i].data == effect_data: - active_effects[i].timer.queue_free() - active_effects.remove_at(i) - effect_removed.emit(effect_data.effect_type) - return - - -func create_timer(duration: float, effect_data: StatusEffectDataResource) -> Timer: - var timer = Timer.new() - timer.wait_time = duration - timer.one_shot = true - timer.autostart = true - timer.timeout.connect(func(): remove_effect(effect_data)) - add_child(timer) - return timer \ No newline at end of file diff --git a/scripts/components/status_effect_component.gd.uid b/scripts/components/status_effect_component.gd.uid deleted file mode 100644 index efbeec9..0000000 --- a/scripts/components/status_effect_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c07d50s20rl8s diff --git a/scripts/components/stomp_damage_component.gd b/scripts/components/stomp_damage_component.gd deleted file mode 100644 index 08774f8..0000000 --- a/scripts/components/stomp_damage_component.gd +++ /dev/null @@ -1,47 +0,0 @@ -class_name StompDamageComponent -extends Node - -@export var damage: float = 0.25 -@export var area2d: Area2D -@export var root: PlayerController - - -func _ready() -> void: - if not area2d: - printerr("No area2d assigned!") - return - if not root: - printerr("No root assigned!") - return - - area2d.body_entered.connect(on_area2d_body_entered) - - -func deal_damage(target: HealthComponent) -> void: - target.decrease_health(damage) - - -func on_area2d_body_entered(body: Node2D) -> void: - if body.has_node("HealthComponent"): - var health_component: HealthComponent = body.get_node("HealthComponent") - if not health_component: - printerr("No HealthComponent assigned!") - return - - var cannot_stomp_component: CannotStompComponent = body.get_node_or_null("CannotStompComponent") - if cannot_stomp_component: - return - - if root.global_position.y < body.global_position.y: - if root is PlayerController: - var velocity: Vector2 = root.current_movement.previous_velocity - if velocity.y > 0.0: - deal_damage(health_component) - - var damage_component: DamageComponent = body.get_node_or_null("DamageComponent") - if not damage_component: - return - damage_component.set_process(false) - await get_tree().process_frame - damage_component.set_process(true) - diff --git a/scripts/components/stomp_damage_component.gd.uid b/scripts/components/stomp_damage_component.gd.uid deleted file mode 100644 index 8557532..0000000 --- a/scripts/components/stomp_damage_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ulhswh4jjlc6 diff --git a/scripts/components/straight_motion_component.gd b/scripts/components/straight_motion_component.gd deleted file mode 100644 index 5857fb7..0000000 --- a/scripts/components/straight_motion_component.gd +++ /dev/null @@ -1,9 +0,0 @@ -class_name StraightMotionComponent -extends Node - -@export var root: Node2D -@export var launch_component: LaunchComponent - - -func _physics_process(delta: float) -> void: - root.position += launch_component.get_initial_velocity() * delta diff --git a/scripts/components/straight_motion_component.gd.uid b/scripts/components/straight_motion_component.gd.uid deleted file mode 100644 index f925350..0000000 --- a/scripts/components/straight_motion_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cvcnfrr1udco5 diff --git a/scripts/components/terrain_hit_fx.gd b/scripts/components/terrain_hit_fx.gd deleted file mode 100644 index 1582141..0000000 --- a/scripts/components/terrain_hit_fx.gd +++ /dev/null @@ -1,24 +0,0 @@ -class_name TerrainHitFx -extends Node - -var gpu_particles: Array[GPUParticles2D] = [] - - -func _ready() -> void: - if owner is GPUParticles2D: - gpu_particles.append(owner) - - for child in get_children(): - if child is GPUParticles2D: - gpu_particles.append(child) - - -func trigger_fx() -> Signal: - for fx in gpu_particles: - if fx: - fx.restart() - fx.emitting = true - return gpu_particles[0].finished - - - \ No newline at end of file diff --git a/scripts/components/terrain_hit_fx.gd.uid b/scripts/components/terrain_hit_fx.gd.uid deleted file mode 100644 index 299a17c..0000000 --- a/scripts/components/terrain_hit_fx.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://djfejwp6e402k diff --git a/scripts/components/tooltip_component.gd b/scripts/components/tooltip_component.gd deleted file mode 100644 index 77f5756..0000000 --- a/scripts/components/tooltip_component.gd +++ /dev/null @@ -1,42 +0,0 @@ -class_name TooltipComponent -extends Node - -@export var area2d: Area2D -@export var ui_root: Control -@export var text: String = "" -@export var tooltip_label: Label - - -func _ready() -> void: - if not area2d: - printerr("Tooltip node missing Area2D child.") - return - - if not ui_root: - printerr("Tooltip node missing UI root child.") - return - - if not tooltip_label: - printerr("Tooltip node missing tooltip label child.") - return - - tooltip_label.text = text - ui_root.visible = false - area2d.body_entered.connect(_on_area2d_body_entered) - area2d.body_exited.connect(_on_area2d_body_exited) - - -func show_tooltip() -> void: - ui_root.visible = true - - -func hide_tooltip() -> void: - ui_root.visible = false - - -func _on_area2d_body_entered(_body: Node) -> void: - show_tooltip() - - -func _on_area2d_body_exited(_body: Node) -> void: - hide_tooltip() \ No newline at end of file diff --git a/scripts/components/tooltip_component.gd.uid b/scripts/components/tooltip_component.gd.uid deleted file mode 100644 index fece85c..0000000 --- a/scripts/components/tooltip_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bsq5pplxqbssh diff --git a/scripts/components/trail_component.gd b/scripts/components/trail_component.gd deleted file mode 100644 index c7e2cb0..0000000 --- a/scripts/components/trail_component.gd +++ /dev/null @@ -1,18 +0,0 @@ -class_name TrailComponent -extends Line2D - -@export var max_points: int = 100 - -var queue: Array[Vector2] = [] - - -func _process(_delta: float) -> void: - queue.push_front(owner.global_position) - - if queue.size() > max_points: - queue.pop_back() - - clear_points() - - for point in queue: - add_point(point) \ No newline at end of file diff --git a/scripts/components/trail_component.gd.uid b/scripts/components/trail_component.gd.uid deleted file mode 100644 index 55fb38b..0000000 --- a/scripts/components/trail_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c0bvan0swashx diff --git a/scripts/components/trigger_lever_component.gd b/scripts/components/trigger_lever_component.gd deleted file mode 100644 index 1755cce..0000000 --- a/scripts/components/trigger_lever_component.gd +++ /dev/null @@ -1,2 +0,0 @@ -class_name TriggerLeverComponent -extends Node \ No newline at end of file diff --git a/scripts/components/trigger_lever_component.gd.uid b/scripts/components/trigger_lever_component.gd.uid deleted file mode 100644 index 95ad8b8..0000000 --- a/scripts/components/trigger_lever_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bpy6xtfm8l3hy diff --git a/scripts/components/unlock_on_requirement_component.gd b/scripts/components/unlock_on_requirement_component.gd deleted file mode 100644 index 31a04b3..0000000 --- a/scripts/components/unlock_on_requirement_component.gd +++ /dev/null @@ -1,26 +0,0 @@ -class_name UnlockOnRequirementComponent -extends Node - -@export var requirement_component: RequirementComponent -@export var unlock_target: Node - - -func _ready() -> void: - if not requirement_component: - printerr("UnlockOnRequirementComponent: requirement_component is not set.") - return - - if not unlock_target: - printerr("UnlockOnRequirementComponent: unlock_target is not set.") - return - - requirement_component.requirement_met.connect(on_requirement_met) - - -func on_requirement_met(requirement_type: CollectableResource.CollectableType) -> void: - if requirement_type == requirement_component.requirement_type: - if unlock_target.has_method("unlock"): - unlock_target.unlock() - else: - printerr("UnlockOnRequirementComponent: unlock_target does not have an unlock method.") - diff --git a/scripts/components/unlock_on_requirement_component.gd.uid b/scripts/components/unlock_on_requirement_component.gd.uid deleted file mode 100644 index 3cfd7f0..0000000 --- a/scripts/components/unlock_on_requirement_component.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c8xhgkg8gcqu6 diff --git a/scripts/console_management.gd b/scripts/console_management.gd deleted file mode 100644 index 498360e..0000000 --- a/scripts/console_management.gd +++ /dev/null @@ -1,138 +0,0 @@ -class_name ConsoleManagement -extends Node - -@export var player_health: HealthComponent -@export var skill_unlocker: SkillUnlockerComponent -@export var skill_manager: SkillManager - -@onready var game_manager: GM = $"/root/GameManager" -@onready var achievements: Achievements = $"/root/AchievementsManager" - - -func _ready() -> void: - Console.pause_enabled = true - Console.add_command("add_coins", console_add_coins, ["amount"], 1, "Add coins to the player.") - Console.add_command("set_coins", console_set_coins, ["amount"], 1, "Set the player's coins.") - Console.add_command("set_lives", console_set_lives, ["amount"], 1, "Set the player's lives.") - Console.add_command("set_health", console_set_health, ["amount"], 1, "Set the player's health.") - Console.add_command("unlock_skill", console_unlock_skill, ["skill_name"], 1, "Unlock a skill for the player.") - Console.add_command("remove_skill", console_remove_skill, ["skill_name"], 1, "Remove a skill from the player.") - Console.add_command("remove_all_skills", console_remove_all_skills, [], 0, "Remove all skills from the player.") - Console.add_command("unlock_all_skills", console_unlock_all_skills, [], 0, "Unlock all skills for the player.") - Console.add_command("unlock_achievement", console_unlock_achievement, ["achievement_name"], 1, "Unlock an achievement for the player.") - Console.add_command("reset_achievement", console_reset_achievement, ["achievement_name"], 1, "Reset an achievement for the player.") - - -func console_add_coins(amount: Variant) -> void: - if not game_manager: - return - if not amount.is_valid_int(): - Console.print_error("Invalid amount: " + str(amount)) - return - game_manager.add_coins(int(amount)) - Console.print_info("Added " + str(amount) + " coins.") - - -func console_set_coins(amount: Variant) -> void: - if not game_manager: - return - if not amount.is_valid_int(): - Console.print_error("Invalid amount: " + str(amount)) - return - game_manager.set_coins(int(amount)) - Console.print_info("Set coins to " + str(amount)) - - -func console_set_lives(amount: Variant) -> void: - if not game_manager: - return - if not amount.is_valid_int(): - Console.print_error("Invalid amount: " + str(amount)) - return - game_manager.set_lives(int(amount)) - Console.print_info("Set lives to " + str(amount)) - - -func console_set_health(amount: Variant) -> void: - if not player_health: - return - if not amount.is_valid_float(): - Console.print_error("Invalid amount: " + str(amount)) - return - player_health.set_health(float(amount)) - Console.print_info("Set health to " + str(amount)) - - -func console_unlock_skill(skill_name: Variant) -> void: - if not skill_manager or not skill_unlocker or not game_manager: - return - if not skill_name: - Console.print_error("Invalid skill name: " + str(skill_name)) - return - - var skill_data: SkillData = skill_manager.get_skill_by_name(skill_name) - skill_data.level = 1 - - if not skill_data: - Console.print_error("Skill not found: " + str(skill_name)) - return - - game_manager.unlock_skill(skill_data) - skill_manager.activate_skill(skill_data) - skill_unlocker.skill_unlocked.emit(skill_data) - Console.print_info("Unlocked skill: " + str(skill_name)) - - -func console_unlock_all_skills() -> void: - if not skill_manager or not skill_unlocker or not game_manager: - return - - skill_unlocker.unlock_all_skills() - Console.print_info("Unlocked all skills.") - - -func console_remove_skill(skill_name: Variant) -> void: - if not skill_manager or not skill_unlocker or not game_manager: - return - if not skill_name: - Console.print_error("Invalid skill name: " + str(skill_name)) - return - - game_manager.remove_skill(skill_name) - skill_manager.remove_skill(skill_name) - Console.print_info("Removed skill: " + str(skill_name)) - - -func console_remove_all_skills() -> void: - if not skill_manager or not skill_unlocker or not game_manager: - return - - for skill_name in skill_manager.active_components.keys(): - game_manager.remove_skill(skill_name) - skill_manager.remove_skill(skill_name) - - -func console_unlock_achievement(achievement_name: Variant) -> void: - if not achievement_name: - Console.print_error("Invalid achievement name: " + str(achievement_name)) - return - if not achievements: - Console.print_error("Achievements manager not found.") - return - if not achievements.unlock_achievement(achievement_name): - Console.print_error("Failed to unlock achievement: " + str(achievement_name)) - - Console.print_info("Unlocked achievement: " + str(achievement_name)) - - -func console_reset_achievement(achievement_name: Variant) -> void: - if not achievement_name: - Console.print_error("Invalid achievement name: " + str(achievement_name)) - return - if not achievements: - Console.print_error("Achievements manager not found.") - return - if not achievements.reset_achievement(achievement_name): - Console.print_error("Failed to reset achievement: " + str(achievement_name)) - - Console.print_info("Reset achievement: " + str(achievement_name)) diff --git a/scripts/console_management.gd.uid b/scripts/console_management.gd.uid deleted file mode 100644 index 85e0f88..0000000 --- a/scripts/console_management.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8r1y8elyw7kt diff --git a/scripts/game_manager.gd b/scripts/game_manager.gd deleted file mode 100644 index fe986d7..0000000 --- a/scripts/game_manager.gd +++ /dev/null @@ -1,231 +0,0 @@ -class_name GM -extends Node - -@export var level_scenes: Array[PackedScene] - -@export var player_state := { - "coins": 0, - "lives": 3, - "unlocked_skills": [], - "current_level": 0, - "unlocked_levels": [0], - "completed_levels": [], - } - -var nodes_in_scene := [] - -var current_session_state := { - "coins_collected": 0, - "skills_unlocked": [], - } - - -func _enter_tree() -> void: - get_tree().node_added.connect(on_node_added) - get_tree().node_removed.connect(on_node_removed) - - -func on_node_added(node: Node) -> void: - nodes_in_scene.append(node) - - -func on_node_removed(node: Node) -> void: - if node in nodes_in_scene: - nodes_in_scene.erase(node) - - -func get_colllectable_nodes() -> Array[CollectableComponent]: - var collectable_nodes: Array[CollectableComponent] = [] - for node in nodes_in_scene: - var collectable_component: CollectableComponent = node.get_node_or_null("Collectable") - if not collectable_component: - collectable_component = node.get_node_or_null("CollectableComponent") - if not collectable_component: - continue - collectable_nodes.append(collectable_component) - return collectable_nodes - - -func get_coin_nodes() -> Array[CollectableComponent]: - var coin_nodes := [] - for node in nodes_in_scene: - var collectable_component: CollectableComponent = node.get_node_or_null("Collectable") - if not collectable_component: - continue - if collectable_component.collectable_data.type == CollectableResource.CollectableType.COIN: - coin_nodes.append(collectable_component) - return coin_nodes - - -func get_kid_nodes() -> Array[CollectableComponent]: - var kid_nodes := [] - for node in nodes_in_scene: - var collectable_component: CollectableComponent = node.get_node_or_null("Collectable") - if not collectable_component: - continue - if collectable_component.collectable_data.type == CollectableResource.CollectableType.KID: - kid_nodes.append(collectable_component) - return kid_nodes - - -func get_player_node() -> Node: - for node in nodes_in_scene: - if node is PlayerController: - return node - return null - - -func add_coins(amount: int) -> void: - player_state["coins"] += amount - player_state["coins"] = max(0, player_state["coins"]) - - -func set_coins(amount: int) -> void: - player_state["coins"] = amount - - -func get_coins() -> int: - return player_state["coins"] + current_session_state["coins_collected"] - - -func remove_coins(amount: int) -> void: - var session_coins = current_session_state["coins_collected"] - - if amount <= session_coins: - current_session_state["coins_collected"] -= amount - else: - var remaining_amount = amount - session_coins - current_session_state["coins_collected"] = 0 - player_state["coins"] = max(0, player_state["coins"] - remaining_amount) - - player_state["coins"] = max(0, player_state["coins"]) - - -func add_lives(amount: int) -> void: - player_state["lives"] += amount - - -func remove_lives(amount: int) -> void: - player_state["lives"] -= amount - - -func set_lives(amount: int) -> void: - player_state["lives"] = amount - - -func get_lives() -> int: - return player_state["lives"] - - - -func is_skill_unlocked(skill: SkillData) -> bool: - return skill in player_state["unlocked_skills"] or skill in current_session_state["skills_unlocked"] - - -func unlock_skill(skill: SkillData) -> void: - if not is_skill_unlocked(skill): - player_state["unlocked_skills"].append(skill) - - -func remove_skill(skill: SkillData) -> void: - if is_skill_unlocked(skill): - player_state["unlocked_skills"].erase(skill) - - -func unlock_skills(skills: Array[SkillData]) -> void: - for skill in skills: - unlock_skill(skill) - - -func reset_player_state() -> void: - player_state = { - "coins": 0, - "lives": 3, - "unlocked_skills": [], - "current_level": 0, - "unlocked_levels": [0], - "completed_levels": [], - } - - -func unlock_level(level_index: int) -> void: - if level_index not in player_state["unlocked_levels"]: player_state["unlocked_levels"].append(level_index) - - -func try_to_go_to_next_level() -> void: - var next_level = player_state["current_level"] + 1 - if next_level < level_scenes.size() and next_level in player_state["unlocked_levels"]: - player_state["current_level"] += 1 - get_tree().change_scene_to_packed(level_scenes[next_level]) - - -func mark_level_complete(level_index: int) -> void: - unlock_level(level_index + 1) - if level_index not in player_state["completed_levels"]: player_state["completed_levels"].append(level_index) - - -func reset_current_session_state() -> void: - current_session_state = { - "coins_collected": 0, - "skills_unlocked": [], - } - - -func restart_game() -> void: - reset_player_state() - reset_current_session_state() - get_tree().change_scene_to_packed(level_scenes[0]) - SaveSystem.save_game() - - -func quit_game() -> void: - get_tree().quit() - - -func pause_game() -> void: - Engine.time_scale = 0 - - -func resume_game() -> void: - Engine.time_scale = 1 - - -func start_new_game() -> void: - reset_player_state() - reset_current_session_state() - get_tree().change_scene_to_packed(level_scenes[0]) - SaveSystem.save_game() - - -func continue_game() -> void: - if not SaveSystem.load_game(): - printerr("Failed to load game. Starting a new game instead.") - start_new_game() - return - - if player_state["current_level"] < level_scenes.size(): - get_tree().change_scene_to_packed(level_scenes[player_state["current_level"]]) - else: - printerr("No levels unlocked to continue.") - - -func on_level_complete() -> void: - var level_index = player_state["current_level"] - mark_level_complete(level_index) - add_coins(current_session_state["coins_collected"]) - for skill in current_session_state["skills_unlocked"]: - unlock_skill(skill) - - reset_current_session_state() - try_to_go_to_next_level() - SaveSystem.save_game() - - -func get_unlocked_skills() -> Array: - var skills_unlocked = player_state.get("unlocked_skills", []) - var skills_current_session = current_session_state.get("skills_unlocked", []) - if not skills_current_session: - return skills_unlocked - if not skills_unlocked: - return skills_current_session - return skills_unlocked + skills_current_session diff --git a/scripts/game_manager.gd.uid b/scripts/game_manager.gd.uid deleted file mode 100644 index 013ca68..0000000 --- a/scripts/game_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dd30bgqiagi25 diff --git a/scripts/interfaces/ISkill.cs b/scripts/interfaces/ISkill.cs new file mode 100644 index 0000000..e676f3c --- /dev/null +++ b/scripts/interfaces/ISkill.cs @@ -0,0 +1,13 @@ +using Godot; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.interfaces; + +public interface ISkill +{ + void Initialize(Node owner, SkillData data); + + void Activate(); + void Deactivate(); + void ApplyUpgrade(SkillUpgrade upgrade); +} \ No newline at end of file diff --git a/scripts/interfaces/ISkill.cs.uid b/scripts/interfaces/ISkill.cs.uid new file mode 100644 index 0000000..3d327c3 --- /dev/null +++ b/scripts/interfaces/ISkill.cs.uid @@ -0,0 +1 @@ +uid://crf7t3jqshq6t diff --git a/scripts/player.gd b/scripts/player.gd deleted file mode 100644 index 2c3cf69..0000000 --- a/scripts/player.gd +++ /dev/null @@ -1,64 +0,0 @@ -class_name PlayerController -extends CharacterBody2D - -@export var default_movement_type: String = "platform" -@export var movement_types: Dictionary = {} -@export var ship_sprite: Sprite2D - -var current_movement: PlayerMovement = null -signal movement_switched(movement_type: String) - - -func _ready() -> void: - for movement_type in movement_types: - var movement_node: Node = get_node_or_null(movement_types[movement_type]) - if movement_node and movement_node is PlayerMovement: - movement_node.enabled = false - - switch_movement(default_movement_type) - - -func _unhandled_input(event: InputEvent) -> void: - if event is InputEventKey: - if event.is_action_pressed("switch_movement"): - var next_movement_type: String = get_next_movement_type() - switch_movement(next_movement_type) - - -func switch_movement(movement_type: String) -> void: - if current_movement: - current_movement.enabled = false - - if movement_type in movement_types: - current_movement = get_node_or_null(movement_types[movement_type]) - if not current_movement: - push_error("Movement type '%s' not found in movement_types." % movement_type) - return - current_movement.enabled = true - movement_switched.emit(current_movement.type) - else: - push_error("Movement type '%s' not found in movement_types." % movement_type) - - if not current_movement: - push_error("No current movement set after switching.") - - -func get_next_movement_type() -> String: - var keys: Array = movement_types.keys() - var current_index: int = keys.find(current_movement.type) - if current_index == -1: - return default_movement_type - - current_index = (current_index + 1) % keys.size() - return keys[current_index] - - - -func on_spaceship_entered() -> void: - switch_movement("ship") - ship_sprite.visible = true - - -func on_spaceship_exited() -> void: - switch_movement(default_movement_type) - ship_sprite.visible = false diff --git a/scripts/player.gd.uid b/scripts/player.gd.uid deleted file mode 100644 index b0e8a29..0000000 --- a/scripts/player.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ccuddyoakg04u diff --git a/scripts/resources/charge_throw_input_resource.gd b/scripts/resources/charge_throw_input_resource.gd deleted file mode 100644 index 56b8f19..0000000 --- a/scripts/resources/charge_throw_input_resource.gd +++ /dev/null @@ -1,56 +0,0 @@ -class_name ChargeThrowInputResource -extends ThrowInputResource - -@export var min_power := 0.5 -@export var max_power := 2.0 -@export var max_charge_time := 1.5 -@export var min_charge_duration := 0.1 - -var is_charging := false -var charge_start_time := 0.0 -signal charge_started -signal charge_updated(charge_ratio: float) -signal charge_stopped - - -func process_input(event: InputEvent) -> void: - if event.is_action_pressed("attack"): - is_charging = true - charge_start_time = Time.get_ticks_msec() / 1000.0 - emit_signal("charge_started") - - if event.is_action_released("attack") and is_charging: - var power := _calculate_power() - is_charging = false - emit_signal("throw_requested", power) - emit_signal("charge_stopped") - - -func _calculate_power() -> float: - var now := Time.get_ticks_msec() / 1000.0 - var held_time := now - charge_start_time - if held_time < min_charge_duration: - return min_power - var t = clamp(held_time / max_charge_time, 0.0, 1.0) - return lerp(min_power, max_power, t) - - -func get_charge_ratio() -> float: - if not is_charging: - return min_power - var now := Time.get_ticks_msec() / 1000.0 - var held := now - charge_start_time - var t = clamp(held / max_charge_time, 0.0, 1.0) - return lerp(min_power, max_power, t) - - -func update(_delta: float) -> void: - if not is_charging: - return - - var t = clamp(get_charge_ratio(), min_power, max_power) - emit_signal("charge_updated", t) - - -func supports_charging() -> bool: - return true \ No newline at end of file diff --git a/scripts/resources/charge_throw_input_resource.gd.uid b/scripts/resources/charge_throw_input_resource.gd.uid deleted file mode 100644 index 33a3196..0000000 --- a/scripts/resources/charge_throw_input_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dl1kpll33o6mv diff --git a/scripts/resources/collectable_resource.gd b/scripts/resources/collectable_resource.gd deleted file mode 100644 index 9d48fe4..0000000 --- a/scripts/resources/collectable_resource.gd +++ /dev/null @@ -1,10 +0,0 @@ -class_name CollectableResource -extends Resource - -enum CollectableType { - COIN, - KID, - HEALTH, -} -@export var amount: Variant = 0.0 -@export var type: CollectableType = CollectableType.COIN \ No newline at end of file diff --git a/scripts/resources/collectable_resource.gd.uid b/scripts/resources/collectable_resource.gd.uid deleted file mode 100644 index 01a4825..0000000 --- a/scripts/resources/collectable_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cb5f0mx0hrt3b diff --git a/scripts/resources/level_resource.gd b/scripts/resources/level_resource.gd deleted file mode 100644 index 0a891d9..0000000 --- a/scripts/resources/level_resource.gd +++ /dev/null @@ -1,5 +0,0 @@ -class_name LevelResource -extends Resource - -@export var level_name: String -@export var scene_path: String \ No newline at end of file diff --git a/scripts/resources/level_resource.gd.uid b/scripts/resources/level_resource.gd.uid deleted file mode 100644 index 89be7e5..0000000 --- a/scripts/resources/level_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cp68km8bykymb diff --git a/scripts/resources/skill_data.gd b/scripts/resources/skill_data.gd deleted file mode 100644 index bed8dd9..0000000 --- a/scripts/resources/skill_data.gd +++ /dev/null @@ -1,18 +0,0 @@ -class_name SkillData -extends Resource - -enum SkillType { - ATTACK, - THROW, - MISC, -} -@export var name: String = "" -@export var description: String = "" -@export var node: PackedScene -@export var config: Dictionary = {} -@export var cost: int = 0 -@export var icon: Texture2D -@export var type: SkillType = SkillType.ATTACK -@export var is_active: bool = false -@export var level: int = 1 -@export var max_level: int = 1 \ No newline at end of file diff --git a/scripts/resources/skill_data.gd.uid b/scripts/resources/skill_data.gd.uid deleted file mode 100644 index 58a05be..0000000 --- a/scripts/resources/skill_data.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bya240e627ti6 diff --git a/scripts/resources/status_effect_data_resource.gd b/scripts/resources/status_effect_data_resource.gd deleted file mode 100644 index 0d73f64..0000000 --- a/scripts/resources/status_effect_data_resource.gd +++ /dev/null @@ -1,6 +0,0 @@ -class_name StatusEffectDataResource -extends Resource - -@export var effect_type: StatusEffectComponent.EffectType = StatusEffectComponent.EffectType.NONE -@export var duration: float = 1.0 -@export var damage_per_second: float = 0.25 \ No newline at end of file diff --git a/scripts/resources/status_effect_data_resource.gd.uid b/scripts/resources/status_effect_data_resource.gd.uid deleted file mode 100644 index 37fbeb3..0000000 --- a/scripts/resources/status_effect_data_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cqq2stnfs1doo diff --git a/scripts/resources/tap_throw_input_resource.gd b/scripts/resources/tap_throw_input_resource.gd deleted file mode 100644 index 967229f..0000000 --- a/scripts/resources/tap_throw_input_resource.gd +++ /dev/null @@ -1,6 +0,0 @@ -class_name TapThrowInputResource -extends ThrowInputResource - -func update(_delta: float) -> void: - if Input.is_action_pressed("attack"): - throw_requested.emit(1.0) \ No newline at end of file diff --git a/scripts/resources/tap_throw_input_resource.gd.uid b/scripts/resources/tap_throw_input_resource.gd.uid deleted file mode 100644 index 5c761b5..0000000 --- a/scripts/resources/tap_throw_input_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://blvdnsyi287rf diff --git a/scripts/resources/throw_input_resource.gd b/scripts/resources/throw_input_resource.gd deleted file mode 100644 index 4a6d4f8..0000000 --- a/scripts/resources/throw_input_resource.gd +++ /dev/null @@ -1,16 +0,0 @@ -class_name ThrowInputResource -extends Resource - -signal throw_requested(power_multiplier: float) - - -func process_input(event: InputEvent) -> void: - pass - - -func update(delta: float) -> void: - pass - - -func supports_charging() -> bool: - return false \ No newline at end of file diff --git a/scripts/resources/throw_input_resource.gd.uid b/scripts/resources/throw_input_resource.gd.uid deleted file mode 100644 index 9a36b83..0000000 --- a/scripts/resources/throw_input_resource.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cry02xehijy2v diff --git a/scripts/screenshot.gd b/scripts/screenshot.gd deleted file mode 100644 index 4c91260..0000000 --- a/scripts/screenshot.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends Node - -func _process(_delta: float) -> void: - if OS.is_debug_build() and Input.is_action_just_pressed("screenshot"): - var img := get_viewport().get_texture().get_image() - var id := OS.get_unique_id() + "_" + Time.get_datetime_string_from_system() - var path := "user://screenshot_" + str(id) + ".png" - img.save_png(path) diff --git a/scripts/screenshot.gd.uid b/scripts/screenshot.gd.uid deleted file mode 100644 index 5f95a19..0000000 --- a/scripts/screenshot.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bydv4g1n5s3nf diff --git a/scripts/skill_manager.gd b/scripts/skill_manager.gd deleted file mode 100644 index 3b28c3e..0000000 --- a/scripts/skill_manager.gd +++ /dev/null @@ -1,103 +0,0 @@ -class_name SkillManager -extends Node - -@export var available_skills: Array[SkillData] = [] - -@onready var gm: GM = $"/root/GameManager" - -var active_components: Dictionary = {} - - -func _ready() -> void: - apply_unlocked_skills() - - -func add_skill(skill_data: SkillData) -> void: - if active_components.has(skill_data.name): - return - - if skill_data.type == SkillData.SkillType.THROW: - var unlocked_skills: Array = gm.get_unlocked_skills() - for skill in unlocked_skills: - var data = null - for s in available_skills: - if s == skill: - data = s - break - if data and data.type == SkillData.SkillType.THROW: - remove_skill(data.name) - - var skill_instance := skill_data.node.instantiate() - for key in skill_data.config.keys(): - if key in skill_instance: - var value = skill_data.config[key] - var parent := get_parent() - - if value is NodePath: - if parent.has_node(value): - value = parent.get_node(value) - elif skill_instance.has_node(value): - value = skill_instance.get_node(value) - else: - continue - - skill_instance[key] = value - - owner.add_child(skill_instance) - active_components[skill_data.name] = skill_instance - - -func remove_skill(skill_name: String) -> void: - if not active_components.has(skill_name): - return - - var skill_instance = active_components[skill_name] - if is_instance_valid(skill_instance): - skill_instance.queue_free() - - var skills: Array = gm.get_unlocked_skills() - for s in skills: - if s.name == skill_name: - s.is_active = false - break - active_components.erase(skill_name) - - -func apply_unlocked_skills() -> void: - for skill_data in available_skills: - if gm.is_skill_unlocked(skill_data): - print("Applying skill: ", skill_data.name) - call_deferred("add_skill", skill_data) - else: - remove_skill(skill_data.name) - - -func get_skill_by_name(skill_name: String) -> SkillData: - for skill_data in available_skills: - if skill_data.name == skill_name: - return skill_data - return null - - -func activate_skill(skill: SkillData) -> void: - if not active_components.has(skill.name): - if skill: - add_skill(skill) - skill.is_active = true - - -func deactivate_skill(skill: SkillData) -> void: - if active_components.has(skill.name): - remove_skill(skill.name) - if skill: - skill.is_active = false - - -func toggle_skill_activation(skill: SkillData) -> void: - if not skill: - return - - if active_components.has(skill.name): - deactivate_skill(skill) - else: - activate_skill(skill) diff --git a/scripts/skill_manager.gd.uid b/scripts/skill_manager.gd.uid deleted file mode 100644 index 83ee3c4..0000000 --- a/scripts/skill_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cjqe428jwip6b diff --git a/scripts/steam_integration.gd b/scripts/steam_integration.gd deleted file mode 100644 index 39a2096..0000000 --- a/scripts/steam_integration.gd +++ /dev/null @@ -1,33 +0,0 @@ -class_name SteamIntegration -extends Node - -var app_id: String = "3575090" -var is_on_steam_deck: bool = false -var is_online: bool = false -var has_bought_game: bool = false - - -func _init() -> void: - OS.set_environment("SteamAppId", app_id) - OS.set_environment("SteamGameId", app_id) - - -func _ready() -> void: - Steam.steamInit() - Steam.enableDeviceCallbacks() - SteamControllerInput.init() - var is_running := Steam.isSteamRunning() - - if !is_running: - print("Steam is not running.") - return - - print("Steam is running.") - - is_on_steam_deck = Steam.isSteamRunningOnSteamDeck() - is_online = Steam.loggedOn() - has_bought_game = Steam.isSubscribed() - - if not has_bought_game: - print("You have not bought the game.") - \ No newline at end of file diff --git a/scripts/steam_integration.gd.uid b/scripts/steam_integration.gd.uid deleted file mode 100644 index 34543de..0000000 --- a/scripts/steam_integration.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://f4y8evisxgnc diff --git a/scripts/ui/audio_settings.gd b/scripts/ui/audio_settings.gd deleted file mode 100644 index 98c89dd..0000000 --- a/scripts/ui/audio_settings.gd +++ /dev/null @@ -1,104 +0,0 @@ -extends Node - -@export var master_volume_slider: Slider -@export var music_volume_slider: Slider -@export var sfx_volume_slider: Slider -@export var audio_settings_control: Control -@export var mute_treshold: float = -20.0 - - -func _ready() -> void: - initialize() - if master_volume_slider: - master_volume_slider.value_changed.connect(_on_master_volume_slider_value_changed) - if music_volume_slider: - music_volume_slider.value_changed.connect(_on_music_volume_slider_value_changed) - if sfx_volume_slider: - sfx_volume_slider.value_changed.connect(_on_sfx_volume_slider_value_changed) - - load_settings() - - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("ui_cancel"): - if UiManager.is_screen_on_top(audio_settings_control): - save_settings() - UiManager.pop_screen() - - -func initialize() -> void: - if master_volume_slider: - var volume_db: float = AudioServer.get_bus_volume_db(AudioServer.get_bus_index("Master")) - master_volume_slider.value = volume_db - master_volume_slider.min_value = mute_treshold - master_volume_slider.max_value = 0.0 - if music_volume_slider: - var music_volume_db: float = AudioServer.get_bus_volume_db(AudioServer.get_bus_index("music")) - music_volume_slider.value = music_volume_db - music_volume_slider.min_value = mute_treshold - music_volume_slider.max_value = 0.0 - if sfx_volume_slider: - var sfx_volume_db: float = AudioServer.get_bus_volume_db(AudioServer.get_bus_index("sfx")) - sfx_volume_slider.value = sfx_volume_db - sfx_volume_slider.min_value = mute_treshold - sfx_volume_slider.max_value = 0.0 - - -func _handle_mute(bus_index: int, value: float) -> void: - if AudioServer: - if value == mute_treshold: - AudioServer.set_bus_mute(bus_index, true) - else: - AudioServer.set_bus_mute(bus_index, false) - - -func _on_master_volume_slider_value_changed(value: float) -> void: - if AudioServer: - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), value) - _handle_mute(AudioServer.get_bus_index("Master"), value) - - -func _on_music_volume_slider_value_changed(value: float) -> void: - if AudioServer: - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("music"), value) - _handle_mute(AudioServer.get_bus_index("music"), value) - - -func _on_sfx_volume_slider_value_changed(value: float) -> void: - if AudioServer: - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("sfx"), value) - _handle_mute(AudioServer.get_bus_index("sfx"), value) - - -func save_settings() -> void: - if not AudioServer: - return - - var settings_config := ConfigFileHandler.settings_config - settings_config.set_value("audio_settings", "master_volume", master_volume_slider.value) - settings_config.set_value("audio_settings", "music_volume", music_volume_slider.value) - settings_config.set_value("audio_settings", "sfx_volume", sfx_volume_slider.value) - settings_config.set_value("audio_settings", "mute_treshold", mute_treshold) - settings_config.save(ConfigFileHandler.SETTINGS_PATH) - - -func load_settings() -> void: - if not AudioServer: - return - - var settings_config := ConfigFileHandler.settings_config - if not settings_config.has_section("audio_settings"): - print("Audio settings section not found in config file.") - return - - var master_volume: float = settings_config.get_value("audio_settings", "master_volume", 0.0) - var music_volume: float = settings_config.get_value("audio_settings", "music_volume", 0.0) - var sfx_volume: float = settings_config.get_value("audio_settings", "sfx_volume", 0.0) - - if master_volume_slider: - master_volume_slider.value = master_volume - if music_volume_slider: - music_volume_slider.value = music_volume - if sfx_volume_slider: - sfx_volume_slider.value = sfx_volume - mute_treshold = settings_config.get_value("audio_settings", "mute_treshold", -20.0) diff --git a/scripts/ui/audio_settings.gd.uid b/scripts/ui/audio_settings.gd.uid deleted file mode 100644 index 4228d4c..0000000 --- a/scripts/ui/audio_settings.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dujk6pnftm7ra diff --git a/scripts/ui/charge_progress_bar.gd b/scripts/ui/charge_progress_bar.gd deleted file mode 100644 index 20e6da8..0000000 --- a/scripts/ui/charge_progress_bar.gd +++ /dev/null @@ -1,81 +0,0 @@ -extends Node - -@export var progress_bar: ProgressBar -@export var brick_throw_component: BrickThrowComponent - -var throw_input: ChargeThrowInputResource - - -func _ready() -> void: - owner.child_entered_tree.connect(on_nodes_changed) - - if progress_bar: - progress_bar.hide() - - setup_dependencies() - - -func on_charge_updated(charge_ratio: float) -> void: - if not progress_bar: - return - - progress_bar.value = charge_ratio - progress_bar.show() - - -func on_charge_stopped() -> void: - if not progress_bar: - return - - progress_bar.hide() - - -func on_charge_started() -> void: - if not progress_bar: - return - - progress_bar.show() - - -func setup_progress_bar() -> void: - if not progress_bar: - return - - progress_bar.min_value = throw_input.min_power - progress_bar.max_value = throw_input.max_power - progress_bar.value = throw_input.min_power - progress_bar.step = 0.01 - progress_bar.hide() - - -func setup_dependencies() -> void: - if not brick_throw_component: - return - - if brick_throw_component.throw_input_behavior is ChargeThrowInputResource: - throw_input = brick_throw_component.throw_input_behavior as ChargeThrowInputResource - else: - throw_input = null - - if not throw_input: - return - - if not progress_bar: - return - - if not throw_input.supports_charging(): - progress_bar.hide() - return - - setup_progress_bar() - - throw_input.charge_started.connect(on_charge_started) - throw_input.charge_updated.connect(on_charge_updated) - throw_input.charge_stopped.connect(on_charge_stopped) - - -func on_nodes_changed(node: Node) -> void: - if node is BrickThrowComponent and brick_throw_component == null: - brick_throw_component = node as BrickThrowComponent - setup_dependencies() - return \ No newline at end of file diff --git a/scripts/ui/charge_progress_bar.gd.uid b/scripts/ui/charge_progress_bar.gd.uid deleted file mode 100644 index 6236916..0000000 --- a/scripts/ui/charge_progress_bar.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://q32rtephu1t1 diff --git a/scripts/ui/credits.gd b/scripts/ui/credits.gd deleted file mode 100644 index 9916e90..0000000 --- a/scripts/ui/credits.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends Control - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("ui_cancel"): - if UiManager.is_screen_on_top(self): - UiManager.pop_screen() \ No newline at end of file diff --git a/scripts/ui/credits.gd.uid b/scripts/ui/credits.gd.uid deleted file mode 100644 index f799384..0000000 --- a/scripts/ui/credits.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dtjgndehihl6d diff --git a/scripts/ui/death_screen.gd b/scripts/ui/death_screen.gd deleted file mode 100644 index d8c2fc6..0000000 --- a/scripts/ui/death_screen.gd +++ /dev/null @@ -1,62 +0,0 @@ -class_name DeathScreen -extends Node - -@export var death_screen_root: Control -@export var current_level: LevelResource -@export var current_level_label: Label -@export var lives_left_label: Label -@export var timeout_time: float = 2.0 -@export var nodes_to_disable: Array[Node] = [] - -@onready var gm: GM = $"/root/GameManager" - -var timer: Timer - - -func _ready() -> void: - set_lables() - - -func set_lables() -> void: - if not gm: - return - - current_level_label.text = current_level.level_name - lives_left_label.text = " x " + str(gm.get_lives()) - - -func setup_timer() -> void: - timer = Timer.new() - timer.wait_time = timeout_time - timer.one_shot = true - timer.timeout.connect(on_timeout) - add_child(timer) - timer.start() - - -func toggle_nodes() -> void: - for node in nodes_to_disable: - if node.process_mode == PROCESS_MODE_DISABLED: - node.process_mode = PROCESS_MODE_INHERIT - else: - node.process_mode = PROCESS_MODE_DISABLED - - -func on_player_death() -> void: - if not gm: - return - - toggle_nodes() - set_lables() - death_screen_root.show() - setup_timer() - - -func on_timeout() -> void: - if not gm: - return - - if gm.get_lives() == 0: - return - - get_tree().reload_current_scene() diff --git a/scripts/ui/death_screen.gd.uid b/scripts/ui/death_screen.gd.uid deleted file mode 100644 index 5c9f538..0000000 --- a/scripts/ui/death_screen.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3d1p21sviww4 diff --git a/scripts/ui/game_over_screen.gd b/scripts/ui/game_over_screen.gd deleted file mode 100644 index 1086262..0000000 --- a/scripts/ui/game_over_screen.gd +++ /dev/null @@ -1,40 +0,0 @@ -class_name GameOverScreen -extends Node - -@export var game_over_screen: Control -@export var restart_button: Button -@export var main_menu_button: Button -@export var main_menu_scene: PackedScene - -@onready var gm: GM = $"/root/GameManager" - - -func _ready() -> void: - if not gm: - return - - game_over_screen.hide() - restart_button.pressed.connect(on_restart_button_pressed) - main_menu_button.pressed.connect(on_main_menu_button_pressed) - - -func on_restart_button_pressed() -> void: - if not gm: - return - - gm.restart_game() - - -func on_main_menu_button_pressed() -> void: - if not gm or not main_menu_scene: - return - - gm.reset_player_state() - get_tree().change_scene_to(main_menu_scene) - - -func on_player_death() -> void: - if not gm or not gm.get_lives() == 0: - return - - game_over_screen.show() \ No newline at end of file diff --git a/scripts/ui/game_over_screen.gd.uid b/scripts/ui/game_over_screen.gd.uid deleted file mode 100644 index fc6c5d9..0000000 --- a/scripts/ui/game_over_screen.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bkd7o2u4psu4p diff --git a/scripts/ui/hud.gd b/scripts/ui/hud.gd deleted file mode 100644 index 77cd50a..0000000 --- a/scripts/ui/hud.gd +++ /dev/null @@ -1,51 +0,0 @@ -class_name Hud -extends Node - -@export var player_health: HealthComponent -@export var coins_label: Label -@export var health_progressbar: ProgressBar -@export var lives_label: Label - -@onready var game_manager: GM = $"/root/GameManager" - - -func _ready() -> void: - if not player_health: - var nodes := get_tree().get_nodes_in_group("player") - for node in nodes: - player_health = node.get_node_or_null("HealthComponent") - if player_health: - break - return - - -func _process(_delta: float) -> void: - if not game_manager: - return - - set_health_progressbar() - set_lives_label() - set_coins_label() - - -func set_coins_label() -> void: - if not game_manager: - return - - #todo: set internationalized text - coins_label.text = tr("COINS_LABEL") + ":" + str(game_manager.get_coins()) - - -func set_lives_label() -> void: - if not game_manager: - return - - lives_label.text = tr("LIVES_LABEL") + ":" + str(game_manager.get_lives()) - - -func set_health_progressbar() -> void: - if not player_health: - return - - health_progressbar.value = player_health.health - health_progressbar.max_value = player_health.max_health \ No newline at end of file diff --git a/scripts/ui/hud.gd.uid b/scripts/ui/hud.gd.uid deleted file mode 100644 index c710616..0000000 --- a/scripts/ui/hud.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c3pde84b3kdco diff --git a/scripts/ui/input_settings.gd b/scripts/ui/input_settings.gd deleted file mode 100644 index 9555025..0000000 --- a/scripts/ui/input_settings.gd +++ /dev/null @@ -1,115 +0,0 @@ -class_name InputSettings -extends Control - -@export var input_button_scene: PackedScene = preload("res://objects/ui/input_button.tscn") -@export var action_list: Container -@export var reset_to_default_button: Button - -@export var input_actions: Dictionary = { - 'left': 'Move left', - 'right': 'Move right', - 'up': 'Move up', - 'down': 'Move down', - 'jump': 'Jump', - 'attack': 'Attack', - 'show_marketplace': "Toggle marketplace", - } - -var is_remapping: bool = false -var action_to_remap = null -var remapping_button = null -var buttons: Array[Button] = [] - -@onready var config_file_handler = $'/root/ConfigFileHandler' - - -func _ready() -> void: - create_action_list() - - if buttons.size() > 0: - buttons[0].grab_focus() - - if reset_to_default_button: - reset_to_default_button.pressed.connect(on_reset_button_pressed) - - -func _input(event: InputEvent) -> void: - if is_remapping: - if event is InputEventKey or (event is InputEventMouseButton and event.pressed) or event is InputEventJoypadButton: - if event is InputEventMouseButton and event.double_click: - event.double_click = false - - InputMap.action_erase_events(action_to_remap) - InputMap.action_add_event(action_to_remap, event) - update_action_list(remapping_button, event) - - is_remapping = false - action_to_remap = null - remapping_button = null - - accept_event() - - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("ui_cancel"): - if UiManager.is_screen_on_top(self): - UiManager.pop_screen() - save_settings() - load_settings() - - -func create_action_list() -> void: - InputMap.load_from_project_settings() - for item in action_list.get_children(): - item.queue_free() - - for action in input_actions: - var button := input_button_scene.instantiate() as Button - var action_label := button.find_child("LabelAction") as Label - var input_label := button.find_child("LabelInput") as Label - action_label.text = input_actions[action] - - var events := InputMap.action_get_events(action) - if events.size() > 0: - input_label.text = events[0].as_text().trim_suffix(" (Physical)") - else: - input_label.text = "None" - - action_list.add_child(button) - button.pressed.connect(on_input_button_pressed.bind(button, action)) - buttons.append(button) - - -func on_input_button_pressed(button: Button, action) -> void: - if is_remapping: - return - - is_remapping = true - action_to_remap = action - remapping_button = button - button.find_child("LabelInput").text = "Press any key..." - - -func update_action_list(button: Button, event: InputEvent) -> void: - button.find_child("LabelInput").text = event.as_text().trim_suffix(" (Physical)") - - -func on_reset_button_pressed() -> void: - create_action_list() - - -func save_settings() -> void: - config_file_handler.settings_config.set_value("input_settings", "input_actions", input_actions) - config_file_handler.settings_config.save(config_file_handler.SETTINGS_PATH) - - -func load_settings() -> void: - if not config_file_handler.settings_config.has_section("input_settings"): - return - - var actions = config_file_handler.settings_config.get_value("input_settings", "input_actions", {}) - if not actions: - return - - input_actions = actions - create_action_list() diff --git a/scripts/ui/input_settings.gd.uid b/scripts/ui/input_settings.gd.uid deleted file mode 100644 index f8b32f3..0000000 --- a/scripts/ui/input_settings.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dppwl7xie2mh diff --git a/scripts/ui/main_menu.gd b/scripts/ui/main_menu.gd deleted file mode 100644 index cd9975b..0000000 --- a/scripts/ui/main_menu.gd +++ /dev/null @@ -1,64 +0,0 @@ -extends Node - -@export var main_menu_control: Control -@export var new_game_button: Button -@export var continue_button: Button -@export var settings_button: Button -@export var credits_button: Button -@export var exit_button: Button -@export var version_label: Label -@export var settings_control: Control -@export var credits_control: Control - -@onready var gm: GM = $"/root/GameManager" - - -func _ready() -> void: - if new_game_button: - new_game_button.pressed.connect(_on_new_game_button_pressed) - if continue_button: - continue_button.pressed.connect(_on_continue_button_pressed) - if settings_button: - settings_button.pressed.connect(_on_settings_button_pressed) - if credits_button: - credits_button.pressed.connect(_on_credits_button_pressed) - if exit_button: - exit_button.pressed.connect(quit_game) - if version_label: - version_label.text = "v. " + ProjectSettings.get_setting("application/config/version") - - if not SaveSystem.check_save_exists() and continue_button: - continue_button.disabled = true - else: - continue_button.disabled = false - - continue_button.grab_focus() - - -func _on_new_game_button_pressed() -> void: - if gm: - gm.start_new_game() - else: - printerr("GameManager not found. Cannot start new game.") - - -func _on_continue_button_pressed() -> void: - if gm: - gm.continue_game() - else: - printerr("GameManager not found. Cannot continue game.") - - -func quit_game() -> void: - if gm: - gm.quit_game() - - -func _on_settings_button_pressed() -> void: - if settings_control: - UiManager.push_screen(settings_control) - - -func _on_credits_button_pressed() -> void: - if credits_control: - UiManager.push_screen(credits_control) diff --git a/scripts/ui/main_menu.gd.uid b/scripts/ui/main_menu.gd.uid deleted file mode 100644 index e0ff008..0000000 --- a/scripts/ui/main_menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hyfvthdbgjbc diff --git a/scripts/ui/marketplace.gd b/scripts/ui/marketplace.gd deleted file mode 100644 index 41a5309..0000000 --- a/scripts/ui/marketplace.gd +++ /dev/null @@ -1,126 +0,0 @@ -class_name Marketplace -extends Node - -@export var root: Control -@export var skill_data: Array[SkillData] = [] -@export var to_unlock_grid: GridContainer -@export var unlocked_grid: GridContainer -@export var font: Font -@export var skill_unlocker: SkillUnlockerComponent -@export var components_to_disable: Array[Node] = [] -@export var marketplace_button: PackedScene -@export var skill_button: PackedScene - -@onready var game_manager: GM = $"/root/GameManager" - -var unlock_buttons: Array[Button] = [] -var skill_buttons: Array[SkillButton] = [] - - -func _ready() -> void: - if not skill_unlocker: - return - - var skills_to_unlock: Array[SkillData] = [] - - for skill in skill_data: - skills_to_unlock.append(skill) - - for skill in skills_to_unlock: - create_upgrade_button(skill) - - var unlocked_skills := game_manager.get_unlocked_skills() - for skill in unlocked_skills: - create_skill_button(skill) - - skill_unlocker.skill_unlocked.connect(on_skill_unlocked) - -func _process(_delta: float) -> void: - for btn in skill_buttons: - if not btn.skill_data: - continue - if btn.skill_data.is_active: - btn.activate() - else: - btn.deactivate() - -func _input(event: InputEvent) -> void: - if event.is_action_pressed("show_marketplace"): - if root.is_visible(): - root.hide() - for component in components_to_disable: - component.process_mode = PROCESS_MODE_INHERIT - else: - root.show() - for component in components_to_disable: - component.process_mode = PROCESS_MODE_DISABLED - - -func get_button_text(skill: SkillData) -> String: - return tr(skill.name) + " " + str(skill.cost) - - -func create_upgrade_button(skill: SkillData) -> void: - var button := marketplace_button.instantiate() as MarketplaceButton - button.text = get_button_text(skill) - button.icon = skill.icon - button.skill_data = skill - - button.pressed.connect(func () -> void: _on_button_pressed(skill)) - - unlock_buttons.append(button) - to_unlock_grid.add_child(button) - to_unlock_grid.queue_sort() - - -func create_skill_button(skill: SkillData) -> void: - var button := skill_button.instantiate() as SkillButton - button.skill_data = skill - button.setup() - button.pressed.connect(func() -> void: on_skill_button_pressed(button)) - button.activate() - - skill_buttons.append(button) - unlocked_grid.add_child(button) - unlocked_grid.queue_sort() - - -func remove_button(skill: SkillData): - for child in to_unlock_grid.get_children(): - if child.text == get_button_text(skill): - child.queue_free() - break - - -func _on_button_pressed(skill: SkillData) -> void: - if not skill_unlocker: - return - - if game_manager.is_skill_unlocked(skill): - if skill.level < skill.max_level: - skill_unlocker.try_upgrade_skill(skill) - if not skill.is_active: - skill_unlocker.skill_manager.toggle_skill_activation(skill) - else: - skill_unlocker.skill_manager.toggle_skill_activation(skill) - else: - skill_unlocker.try_unlock_skill(skill) - - -func on_skill_unlocked(skill: SkillData) -> void: - if not skill: - return - - for btn in skill_buttons: - if btn.skill_data and btn.skill_data.name == skill.name: - return - - create_skill_button(skill) - - -func on_skill_button_pressed(button: SkillButton) -> void: - if not skill_unlocker or not button.skill_data: - return - - skill_unlocker.skill_manager.toggle_skill_activation(button.skill_data) - diff --git a/scripts/ui/marketplace.gd.uid b/scripts/ui/marketplace.gd.uid deleted file mode 100644 index 594cb5f..0000000 --- a/scripts/ui/marketplace.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://duifmqjarjpuv diff --git a/scripts/ui/marketplace_button.gd b/scripts/ui/marketplace_button.gd deleted file mode 100644 index 17defb5..0000000 --- a/scripts/ui/marketplace_button.gd +++ /dev/null @@ -1,48 +0,0 @@ -class_name MarketplaceButton -extends Button - -@export var skill_data: SkillData -@export var unlocked_skill_icon: Texture2D -@export var locked_skill_icon: Texture2D -@export var skill_level_container: Container - -@onready var gm: GM = $"/root/GameManager" - - -func _ready() -> void: - if not skill_data: - printerr("MarketplaceButton: skill_data is not set.") - if not unlocked_skill_icon or not locked_skill_icon: - printerr("MarketplaceButton: unlocked_skill_icon or locked_skill_icon is not set.") - return - if not skill_level_container: - printerr("MarketplaceButton: skill_level_container is not set.") - return - - setup() - - var player := gm.get_player_node() - var skill_unlocker_component := player.get_node_or_null("SkillUnlockerComponent") as SkillUnlockerComponent - if skill_unlocker_component: - skill_unlocker_component.skill_unlocked.connect(_on_skill_unlock) - - -func setup() -> void: - if not skill_data: - return - - for i in range(skill_data.max_level): - var _icon := TextureRect.new() - _icon.texture = unlocked_skill_icon if i < skill_data.level else locked_skill_icon - skill_level_container.add_child(_icon) - - -func _on_skill_unlock(skill: SkillData) -> void: - if skill.name == skill_data.name: - for i in range(skill_data.max_level): - var icon := skill_level_container.get_child(i) as TextureRect - if i < skill.level: - icon.texture = unlocked_skill_icon - else: - icon.texture = locked_skill_icon - disabled = skill.level >= skill_data.max_level \ No newline at end of file diff --git a/scripts/ui/marketplace_button.gd.uid b/scripts/ui/marketplace_button.gd.uid deleted file mode 100644 index 5616e41..0000000 --- a/scripts/ui/marketplace_button.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dx8lex40lotr5 diff --git a/scripts/ui/pause_menu.gd b/scripts/ui/pause_menu.gd deleted file mode 100644 index 0cbf772..0000000 --- a/scripts/ui/pause_menu.gd +++ /dev/null @@ -1,90 +0,0 @@ -class_name PauseMenu -extends Node - -@export var pause_menu_control: Control -@export var resume_button: Button -@export var quit_button: Button -@export var settings_button: Button -@export var exit_to_menu_button: Button -@export var settings_menu: Control -@export var exit_to_menu_scene: PackedScene - -@onready var gm: GM = $"/root/GameManager" - -var is_console_open: bool = false - - -func _ready() -> void: - if not pause_menu_control: - printerr("PauseMenu: Pause menu control not set.") - return - - if not resume_button: - printerr("PauseMenu: Resume button not set.") - return - - if not quit_button: - printerr("PauseMenu: Quit button not set.") - return - - if not settings_button: - printerr("PauseMenu: Settings button not set.") - return - - if not exit_to_menu_button: - printerr("PauseMenu: Exit to menu button not set.") - return - - pause_menu_control.hide() - - resume_button.pressed.connect(_on_resume_button_pressed) - quit_button.pressed.connect(_on_quit_button_pressed) - settings_button.pressed.connect(_on_settings_button_pressed) - exit_to_menu_button.pressed.connect(_on_exit_to_menu_button_pressed) - Console.console_opened.connect(_on_console_open) - Console.console_closed.connect(_on_console_close) - - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("pause") and not is_console_open: - if UiManager.is_visible_on_stack(pause_menu_control): - _on_resume_button_pressed() - else: - UiManager.push_screen(pause_menu_control) - gm.pause_game() - - -func _on_resume_button_pressed() -> void: - UiManager.pop_screen() - gm.resume_game() - - -func _on_quit_button_pressed() -> void: - gm.quit_game() - - -func _on_settings_button_pressed() -> void: - if not settings_menu: - printerr("PauseMenu: Settings menu scene not set.") - return - - UiManager.push_screen(settings_menu) - gm.pause_game() - - -func _on_exit_to_menu_button_pressed() -> void: - if not exit_to_menu_scene: - printerr("PauseMenu: Exit to menu scene not set.") - return - - gm.resume_game() - gm.reset_current_session_state() - get_tree().change_scene_to_packed(exit_to_menu_scene) - - -func _on_console_open(): - is_console_open = true - - -func _on_console_close(): - is_console_open = false diff --git a/scripts/ui/pause_menu.gd.uid b/scripts/ui/pause_menu.gd.uid deleted file mode 100644 index df10beb..0000000 --- a/scripts/ui/pause_menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cugifchx6jhuk diff --git a/scripts/ui/settings_menu.gd b/scripts/ui/settings_menu.gd deleted file mode 100644 index b8daf49..0000000 --- a/scripts/ui/settings_menu.gd +++ /dev/null @@ -1,70 +0,0 @@ -class_name SettingsMenu -extends Node - -@export var input_settings: Control -@export var audio_settings: Control -@export var display_settings: Control -@export var gameplay_settings: Control -@export var settings_menu_control: Control -@export var input_settings_button: Button -@export var audio_settings_button: Button -@export var display_settings_button: Button -@export var gameplay_settings_button: Button - - -func _ready() -> void: - if not settings_menu_control or not input_settings_button or not audio_settings_button or not display_settings_button or not gameplay_settings_button: - printerr("No settings menu control or buttons found.") - return - - if input_settings: - input_settings_button.pressed.connect(_on_input_settings_button_pressed) - input_settings.hide() - - if audio_settings: - audio_settings_button.pressed.connect(_on_audio_settings_button_pressed) - audio_settings.hide() - - if display_settings: - display_settings_button.pressed.connect(_on_display_settings_button_pressed) - display_settings.hide() - - if gameplay_settings: - gameplay_settings_button.pressed.connect(_on_gameplay_settings_button_pressed) - gameplay_settings.hide() - - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("ui_cancel"): - if UiManager.is_screen_on_top(settings_menu_control): - UiManager.pop_screen() - - -func _on_input_settings_button_pressed() -> void: - if not input_settings: - return - - UiManager.push_screen(input_settings) - - -func _on_audio_settings_button_pressed() -> void: - if not audio_settings: - return - - UiManager.push_screen(audio_settings) - - -func _on_display_settings_button_pressed() -> void: - if not display_settings: - return - - UiManager.push_screen(display_settings) - - -func _on_gameplay_settings_button_pressed() -> void: - if not gameplay_settings: - return - - UiManager.push_screen(gameplay_settings) - - diff --git a/scripts/ui/settings_menu.gd.uid b/scripts/ui/settings_menu.gd.uid deleted file mode 100644 index b3580c5..0000000 --- a/scripts/ui/settings_menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c506rigcjlm6x diff --git a/scripts/ui/skill_button.gd b/scripts/ui/skill_button.gd deleted file mode 100644 index 10a2831..0000000 --- a/scripts/ui/skill_button.gd +++ /dev/null @@ -1,22 +0,0 @@ -class_name SkillButton -extends Button - -@export var skill_data: SkillData - - -func setup() -> void: - if not skill_data: - return - - icon = skill_data.icon - text = tr(skill_data.name) - - -func activate() -> void: - set("theme_override_colors/font_color", Color("#49aa10")) - - -func deactivate() -> void: - set("theme_override_colors/font_color", Color("#ffffff")) - - diff --git a/scripts/ui/skill_button.gd.uid b/scripts/ui/skill_button.gd.uid deleted file mode 100644 index 741a43d..0000000 --- a/scripts/ui/skill_button.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://0obbehfd8fki