From 3cf084e69b01621c57c1611925c4bb84b96a9fb3 Mon Sep 17 00:00:00 2001 From: Patrick Nueser Date: Fri, 28 Jun 2024 16:12:11 +0200 Subject: [PATCH] handles enemy respawning properly --- README.md | 7 + addons/copilot/Copilot.gd | 368 +++++++++++++++++++++++++++++ addons/copilot/CopilotUI.tscn | 293 +++++++++++++++++++++++ addons/copilot/GithubCopilot.gd | 189 +++++++++++++++ addons/copilot/LLM.gd | 30 +++ addons/copilot/OpenAIChat.gd | 110 +++++++++ addons/copilot/OpenAICompletion.gd | 76 ++++++ addons/copilot/Plugin.gd | 21 ++ addons/copilot/UUID.gd | 117 +++++++++ addons/copilot/large_icon.tres | 45 ++++ addons/copilot/plugin.cfg | 7 + addons/copilot/small_icon.tres | 45 ++++ assets/LICENSE.txt | 2 +- enemy/enemy.gd | 18 +- main/main.gd | 19 +- player/player.gd | 12 +- project.godot | 4 + 17 files changed, 1342 insertions(+), 21 deletions(-) create mode 100644 README.md create mode 100644 addons/copilot/Copilot.gd create mode 100644 addons/copilot/CopilotUI.tscn create mode 100644 addons/copilot/GithubCopilot.gd create mode 100644 addons/copilot/LLM.gd create mode 100644 addons/copilot/OpenAIChat.gd create mode 100644 addons/copilot/OpenAICompletion.gd create mode 100644 addons/copilot/Plugin.gd create mode 100644 addons/copilot/UUID.gd create mode 100644 addons/copilot/large_icon.tres create mode 100644 addons/copilot/plugin.cfg create mode 100644 addons/copilot/small_icon.tres diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cafbce --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Hyper Space + +Space Invaders like space shooter + +## Dependencies + +* Godot 4.2.2 diff --git a/addons/copilot/Copilot.gd b/addons/copilot/Copilot.gd new file mode 100644 index 0000000..7c2371d --- /dev/null +++ b/addons/copilot/Copilot.gd @@ -0,0 +1,368 @@ +@tool +extends Control + +@onready var llms = $LLMs +@onready var context_label = $VBoxParent/Context +@onready var status_label = $VBoxParent/Status +@onready var model_select = $VBoxParent/ModelSetting/Model +@onready var shortcut_modifier_select = $VBoxParent/ShortcutSetting/HBoxContainer/Modifier +@onready var shortcut_key_select = $VBoxParent/ShortcutSetting/HBoxContainer/Key +@onready var multiline_toggle = $VBoxParent/MultilineSetting/Multiline +@onready var openai_key_title = $VBoxParent/OpenAiSetting/Label +@onready var openai_key_input = $VBoxParent/OpenAiSetting/OpenAiKey +@onready var version_label = $Version +@onready var info = $VBoxParent/Info + +@export var icon_shader : ShaderMaterial +@export var highlight_color : Color + +var editor_interface : EditorInterface +var screen = "Script" + +var request_code_state = null +var cur_highlight = null +var indicator = null + +var models = {} +var openai_api_key +var cur_model +var cur_shortcut_modifier = "Control" if is_mac() else "Alt" +var cur_shortcut_key = "C" +var allow_multiline = true + +const PREFERENCES_STORAGE_NAME = "user://copilot.cfg" +const PREFERENCES_PASS = "F4fv2Jxpasp20VS5VSp2Yp2v9aNVJ21aRK" +const GITHUB_COPILOT_DISCLAIMER = "Use GitHub Copilot keys at your own risk. Retrieve key by following instructions [url=https://gitlab.com/aaamoon/copilot-gpt4-service?tab=readme-ov-file#obtaining-copilot-token]here[/url]." + +func _ready(): + #Initialize dock, load settings + populate_models() + populate_modifiers() + load_config() + +func populate_models(): + #Add all found models to settings + model_select.clear() + for llm in llms.get_children(): + var new_models = llm._get_models() + for model in new_models: + model_select.add_item(model) + models[model] = get_path_to(llm) + model_select.select(0) + set_model(model_select.get_item_text(0)) + +func populate_modifiers(): + #Add available shortcut modifiers based on platform + shortcut_modifier_select.clear() + var modifiers = ["Alt", "Ctrl", "Shift"] + if is_mac(): modifiers = ["Cmd", "Option", "Control", "Shift"] + for modifier in modifiers: + shortcut_modifier_select.add_item(modifier) + apply_by_value(shortcut_modifier_select, cur_shortcut_modifier) + +func _unhandled_key_input(event): + #Handle input + if event is InputEventKey: + if cur_highlight: + #If completion is shown, TAB will accept it + #and the TAB input ignored + if event.keycode == KEY_TAB: + undo_input() + clear_highlights() + #BACKSPACE will remove it + elif event.keycode == KEY_BACKSPACE: + revert_change() + clear_highlights() + #Any other key press will plainly accept it + else: + clear_highlights() + #If shortcut modifier and key are pressed, request completion + if shortcut_key_pressed(event) and shortcut_modifier_pressed(event): + request_completion() + +func is_mac(): + #Platform check + return OS.get_name() == "macOS" + +func shortcut_key_pressed(event): + #Check if selected shortcut key is pressed + var key_string = OS.get_keycode_string(event.keycode) + return key_string == cur_shortcut_key + +func shortcut_modifier_pressed(event): + #Check if selected shortcut modifier is pressed + match cur_shortcut_modifier: + "Control": + return event.ctrl_pressed + "Ctrl": + return event.ctrl_pressed + "Alt": + return event.alt_pressed + "Option": + return event.alt_pressed + "Shift": + return event.shift_pressed + "Cmd": + return event.meta_pressed + _: + return false + +func clear_highlights(): + #Clear all currently highlighted lines + #and reset request status + request_code_state = null + cur_highlight = null + var editor = get_code_editor() + for line in range(editor.get_line_count()): + editor.set_line_background_color(line, Color(0, 0, 0, 0)) + +func undo_input(): + #Undo last input in code editor + var editor = get_code_editor() + editor.undo() + +func update_loading_indicator(create = false): + #Make sure loading indicator is placed at caret position + if screen != "Script": return + var editor = get_code_editor() + if !editor: return + var line_height = editor.get_line_height() + if !is_instance_valid(indicator): + if !create: return + indicator = ColorRect.new() + indicator.material = icon_shader + indicator.custom_minimum_size = Vector2(line_height, line_height) + editor.add_child(indicator) + var pos = editor.get_caret_draw_pos() + var pre_post = get_pre_post() + #Caret position returned from Godot is not reliable + #Needs to be adjusted for empty lines + var is_on_empty_line = pre_post[0].right(1) == "\n" + var offset = line_height/2-1 if is_on_empty_line else line_height-1 + indicator.position = Vector2(pos.x, pos.y - offset) + editor.editable = false + +func remove_loading_indicator(): + #Free loading indicator, and return editor to editable state + if is_instance_valid(indicator): indicator.queue_free() + set_status("") + var editor = get_code_editor() + editor.editable = true + +func set_status(text): + #Update status label in dock + status_label.text = "" + +func insert_completion(content: String, pre, post): + #Overwrite code editor text to insert received completion + var editor = get_code_editor() + var scroll = editor.scroll_vertical + + var caret_text = pre + content + var lines_from = pre.split("\n") + var lines_to = caret_text.split("\n") + + cur_highlight = [lines_from.size(), lines_to.size()] + + editor.set_text(pre + content + post) + editor.set_caret_line(lines_to.size()) + editor.set_caret_column(lines_to[-1].length()) + editor.scroll_vertical = scroll + editor.update_code_completion_options(false) + +func revert_change(): + #Revert inserted completion + var code_edit = get_code_editor() + var scroll = code_edit.scroll_vertical + var old_text = request_code_state[0] + request_code_state[1] + var lines_from = request_code_state[0].strip_edges(false, true).split("\n") + code_edit.set_text(old_text) + code_edit.set_caret_line(lines_from.size()-1) + code_edit.set_caret_column(lines_from[-1].length()) + code_edit.scroll_vertical = scroll + clear_highlights() + +func _process(delta): + #Update visuals and context label + update_highlights() + update_loading_indicator() + update_context() + +func update_highlights(): + #Make sure highlighted lines persist until explicitely removed + #via key input + if cur_highlight: + var editor = get_code_editor() + for line in range(cur_highlight[0]-1, cur_highlight[1]): + editor.set_line_background_color(line, highlight_color) + +func update_context(): + #Show currently edited file in dock + var script = get_current_script() + if script: context_label.text = script.resource_path.get_file() + +func on_main_screen_changed(_screen): + #Track current editor screen (2D, 3D, Script) + screen = _screen + +func get_current_script(): + #Get currently edited script + if !editor_interface: return + var script_editor = editor_interface.get_script_editor() + return script_editor.get_current_script() + +func get_code_editor(): + #Get currently used code editor + #This does not return the shader editor! + if !editor_interface: return + var script_editor = editor_interface.get_script_editor() + var base_editor = script_editor.get_current_editor() + if base_editor: + var code_edit = base_editor.get_base_editor() + return code_edit + return null + +func request_completion(): + #Get current code and request completion from active model + if request_code_state: return + set_status("Asking %s..." % cur_model) + update_loading_indicator(true) + var pre_post = get_pre_post() + var llm = get_llm() + if !llm: return + llm._send_user_prompt(pre_post[0], pre_post[1]) + request_code_state = pre_post + +func get_pre_post(): + #Split current code based on caret position + var editor = get_code_editor() + var text = editor.get_text() + var pos = Vector2(editor.get_caret_line(), editor.get_caret_column()) + var pre = "" + var post = "" + for i in range(pos.x): + pre += editor.get_line(i) + "\n" + pre += editor.get_line(pos.x).substr(0,pos.y) + post += editor.get_line(pos.x).substr(pos.y) + "\n" + for ii in range(pos.x+1, editor.get_line_count()): + post += editor.get_line(ii) + "\n" + return [pre, post] + +func get_llm(): + #Get currently active llm and set active model + var llm = get_node(models[cur_model]) + llm._set_api_key(openai_api_key) + llm._set_model(cur_model) + llm._set_multiline(allow_multiline) + return llm + +func matches_request_state(pre, post): + #Check if code passed for completion request matches current code + return request_code_state[0] == pre and request_code_state[1] == post + +func set_openai_api_key(key): + #Apply API key + openai_api_key = key + +func set_model(model_name): + #Apply selected model + cur_model = model_name + # Handle some special model scenarios + if "github-copilot" in model_name: + openai_key_title.text = "GitHub Copilot API Key" + info.parse_bbcode(GITHUB_COPILOT_DISCLAIMER) + info.show() + else: + openai_key_title.text = "OpenAI API Key" + info.hide() + +func set_shortcut_modifier(modifier): + #Apply selected shortcut modifier + cur_shortcut_modifier = modifier + +func set_shortcut_key(key): + #Apply selected shortcut key + cur_shortcut_key = key + +func set_multiline(active): + #Apply selected multiline setting + allow_multiline = active + +func _on_code_completion_received(completion, pre, post): + #Attempt to insert received code completion + remove_loading_indicator() + if matches_request_state(pre, post): + insert_completion(completion, pre, post) + else: + clear_highlights() + +func _on_code_completion_error(error): + #Display error + remove_loading_indicator() + clear_highlights() + push_error(error) + +func _on_open_ai_key_changed(key): + #Apply setting and store in config file + set_openai_api_key(key) + store_config() + +func _on_model_selected(index): + #Apply setting and store in config file + set_model(model_select.get_item_text(index)) + store_config() + +func _on_shortcut_modifier_selected(index): + #Apply setting and store in config file + set_shortcut_modifier(shortcut_modifier_select.get_item_text(index)) + store_config() + +func _on_shortcut_key_selected(index): + #Apply setting and store in config file + set_shortcut_key(shortcut_key_select.get_item_text(index)) + store_config() + +func _on_multiline_toggled(button_pressed): + #Apply setting and store in config file + set_multiline(button_pressed) + store_config() + +func store_config(): + #Store current setting in config file + var config = ConfigFile.new() + config.set_value("preferences", "model", cur_model) + config.set_value("preferences", "shortcut_modifier", cur_shortcut_modifier) + config.set_value("preferences", "shortcut_key", cur_shortcut_key) + config.set_value("preferences", "allow_multiline", allow_multiline) + config.set_value("keys", "openai", openai_api_key) + config.save_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) + +func load_config(): + #Retrieve current settings from config file + var config = ConfigFile.new() + var err = config.load_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) + if err != OK: return + cur_model = config.get_value("preferences", "model", cur_model) + apply_by_value(model_select, cur_model) + set_model(model_select.get_item_text(model_select.selected)) + cur_shortcut_modifier = config.get_value("preferences", "shortcut_modifier", cur_shortcut_modifier) + apply_by_value(shortcut_modifier_select, cur_shortcut_modifier) + cur_shortcut_key = config.get_value("preferences", "shortcut_key", cur_shortcut_key) + apply_by_value(shortcut_key_select, cur_shortcut_key) + allow_multiline = config.get_value("preferences", "allow_multiline", allow_multiline) + multiline_toggle.set_pressed_no_signal(allow_multiline) + openai_api_key = config.get_value("keys", "openai", "") + openai_key_input.text = openai_api_key + +func apply_by_value(option_button, value): + #Select item for option button based on value instead of index + for i in option_button.item_count: + if option_button.get_item_text(i) == value: + option_button.select(i) + +func set_version(version): + version_label.text = "v%s" % version + + +func on_info_meta_clicked(meta): + OS.shell_open(meta) diff --git a/addons/copilot/CopilotUI.tscn b/addons/copilot/CopilotUI.tscn new file mode 100644 index 0000000..40078eb --- /dev/null +++ b/addons/copilot/CopilotUI.tscn @@ -0,0 +1,293 @@ +[gd_scene load_steps=7 format=3 uid="uid://rv5dl08lcb8e"] + +[ext_resource type="Script" path="res://addons/copilot/Copilot.gd" id="1_pq1gj"] +[ext_resource type="Script" path="res://addons/copilot/OpenAIChat.gd" id="2"] +[ext_resource type="Material" uid="uid://cccmbprav6vgu" path="res://addons/copilot/small_icon.tres" id="2_gdw4j"] +[ext_resource type="Script" path="res://addons/copilot/OpenAICompletion.gd" id="3_loa2x"] +[ext_resource type="Material" uid="uid://bl1rtf743e4l3" path="res://addons/copilot/large_icon.tres" id="3_xn70b"] +[ext_resource type="Script" path="res://addons/copilot/GithubCopilot.gd" id="6_hmh8w"] + +[node name="Copilot" 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_pq1gj") +icon_shader = ExtResource("2_gdw4j") +highlight_color = Color(0.223529, 0.254902, 0.298039, 1) + +[node name="VBoxParent" type="VBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Indicator" type="ColorRect" parent="VBoxParent"] +material = ExtResource("3_xn70b") +custom_minimum_size = Vector2(200, 200) +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="ContextTitle" type="Label" parent="VBoxParent"] +modulate = Color(1, 1, 1, 0.7) +layout_mode = 2 +text = "Current Context" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="Context" type="Label" parent="VBoxParent"] +layout_mode = 2 +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="Status" type="Label" parent="VBoxParent"] +modulate = Color(1, 1, 1, 0.7) +custom_minimum_size = Vector2(2.08165e-12, 100) +layout_mode = 2 +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="HowToTitle" type="Label" parent="VBoxParent"] +layout_mode = 2 +text = "How To Use" + +[node name="Separator1" type="HSeparator" parent="VBoxParent"] +layout_mode = 2 + +[node name="HowTo" type="Label" parent="VBoxParent"] +layout_mode = 2 +text = "Press the selected shortcut in the code editor to request a completion from Copilot at the current caret position" +autowrap_mode = 3 + +[node name="SettingTitle" type="Label" parent="VBoxParent"] +layout_mode = 2 +text = "Settings" + +[node name="Separator2" type="HSeparator" parent="VBoxParent"] +layout_mode = 2 + +[node name="OpenAiSetting" type="HBoxContainer" parent="VBoxParent"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="VBoxParent/OpenAiSetting"] +custom_minimum_size = Vector2(100, 2.08165e-12) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "OpenAI API Key" +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="VSeparator" type="VSeparator" parent="VBoxParent/OpenAiSetting"] +layout_mode = 2 + +[node name="OpenAiKey" type="LineEdit" parent="VBoxParent/OpenAiSetting"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 +size_flags_horizontal = 10 +placeholder_text = "API Key" +secret = true + +[node name="ModelSetting" type="HBoxContainer" parent="VBoxParent"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="VBoxParent/ModelSetting"] +custom_minimum_size = Vector2(100, 2.08165e-12) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "Model" +vertical_alignment = 1 + +[node name="VSeparator" type="VSeparator" parent="VBoxParent/ModelSetting"] +layout_mode = 2 + +[node name="Model" type="OptionButton" parent="VBoxParent/ModelSetting"] +layout_mode = 2 +size_flags_horizontal = 10 +item_count = 3 +selected = 1 +fit_to_longest_item = false +popup/item_0/text = "text-davinci-003" +popup/item_0/id = 0 +popup/item_1/text = "gpt-3.5-turbo" +popup/item_1/id = 1 +popup/item_2/text = "gpt-4" +popup/item_2/id = 2 + +[node name="ShortcutSetting" type="HBoxContainer" parent="VBoxParent"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="VBoxParent/ShortcutSetting"] +custom_minimum_size = Vector2(100, 2.08165e-12) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "Shortcut" +vertical_alignment = 1 + +[node name="VSeparator" type="VSeparator" parent="VBoxParent/ShortcutSetting"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxParent/ShortcutSetting"] +layout_mode = 2 +size_flags_horizontal = 10 + +[node name="Modifier" type="OptionButton" parent="VBoxParent/ShortcutSetting/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 10 +item_count = 4 +selected = 2 +popup/item_0/text = "Cmd" +popup/item_0/id = 0 +popup/item_1/text = "Option" +popup/item_1/id = 1 +popup/item_2/text = "Control" +popup/item_2/id = 2 +popup/item_3/text = "Shift" +popup/item_3/id = 3 + +[node name="Key" type="OptionButton" parent="VBoxParent/ShortcutSetting/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 10 +item_count = 32 +selected = 2 +popup/item_0/text = "A" +popup/item_0/id = 0 +popup/item_1/text = "B" +popup/item_1/id = 1 +popup/item_2/text = "C" +popup/item_2/id = 2 +popup/item_3/text = "D" +popup/item_3/id = 3 +popup/item_4/text = "E" +popup/item_4/id = 4 +popup/item_5/text = "F" +popup/item_5/id = 5 +popup/item_6/text = "G" +popup/item_6/id = 6 +popup/item_7/text = "H" +popup/item_7/id = 7 +popup/item_8/text = "L" +popup/item_8/id = 8 +popup/item_9/text = "M" +popup/item_9/id = 9 +popup/item_10/text = "N" +popup/item_10/id = 10 +popup/item_11/text = "O" +popup/item_11/id = 11 +popup/item_12/text = "P" +popup/item_12/id = 12 +popup/item_13/text = "Q" +popup/item_13/id = 13 +popup/item_14/text = "R" +popup/item_14/id = 14 +popup/item_15/text = "S" +popup/item_15/id = 15 +popup/item_16/text = "T" +popup/item_16/id = 16 +popup/item_17/text = "U" +popup/item_17/id = 17 +popup/item_18/text = "V" +popup/item_18/id = 18 +popup/item_19/text = "X" +popup/item_19/id = 19 +popup/item_20/text = "Y" +popup/item_20/id = 20 +popup/item_21/text = "Z" +popup/item_21/id = 21 +popup/item_22/text = "1" +popup/item_22/id = 22 +popup/item_23/text = "2" +popup/item_23/id = 23 +popup/item_24/text = "3" +popup/item_24/id = 24 +popup/item_25/text = "4" +popup/item_25/id = 25 +popup/item_26/text = "5" +popup/item_26/id = 26 +popup/item_27/text = "6" +popup/item_27/id = 27 +popup/item_28/text = "7" +popup/item_28/id = 28 +popup/item_29/text = "8" +popup/item_29/id = 29 +popup/item_30/text = "9" +popup/item_30/id = 30 +popup/item_31/text = "0" +popup/item_31/id = 31 + +[node name="MultilineSetting" type="HBoxContainer" parent="VBoxParent"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="VBoxParent/MultilineSetting"] +custom_minimum_size = Vector2(100, 2.08165e-12) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "Multiline Completions" +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="VSeparator" type="VSeparator" parent="VBoxParent/MultilineSetting"] +layout_mode = 2 + +[node name="Multiline" type="CheckBox" parent="VBoxParent/MultilineSetting"] +layout_mode = 2 +size_flags_horizontal = 10 +button_pressed = true +text = "Enabled" + +[node name="Info" type="RichTextLabel" parent="VBoxParent"] +layout_mode = 2 +focus_mode = 2 +fit_content = true +selection_enabled = true + +[node name="LLMs" type="Node" parent="."] + +[node name="OpenAICompletion" type="Node" parent="LLMs"] +script = ExtResource("3_loa2x") + +[node name="OpenAIChat" type="Node" parent="LLMs"] +script = ExtResource("2") + +[node name="GithubCopilot" type="Node" parent="LLMs"] +script = ExtResource("6_hmh8w") + +[node name="Version" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -23.0 +grow_horizontal = 2 +grow_vertical = 0 +horizontal_alignment = 2 +vertical_alignment = 2 + +[connection signal="text_changed" from="VBoxParent/OpenAiSetting/OpenAiKey" to="." method="_on_open_ai_key_changed"] +[connection signal="item_selected" from="VBoxParent/ModelSetting/Model" to="." method="_on_model_selected"] +[connection signal="item_selected" from="VBoxParent/ShortcutSetting/HBoxContainer/Modifier" to="." method="_on_shortcut_modifier_selected"] +[connection signal="item_selected" from="VBoxParent/ShortcutSetting/HBoxContainer/Key" to="." method="_on_shortcut_key_selected"] +[connection signal="toggled" from="VBoxParent/MultilineSetting/Multiline" to="." method="_on_multiline_toggled"] +[connection signal="meta_clicked" from="VBoxParent/Info" to="." method="on_info_meta_clicked"] +[connection signal="completion_error" from="LLMs/OpenAICompletion" to="." method="_on_code_completion_error"] +[connection signal="completion_received" from="LLMs/OpenAICompletion" to="." method="_on_code_completion_received"] +[connection signal="completion_error" from="LLMs/OpenAIChat" to="." method="_on_code_completion_error"] +[connection signal="completion_received" from="LLMs/OpenAIChat" to="." method="_on_code_completion_received"] +[connection signal="completion_error" from="LLMs/GithubCopilot" to="." method="_on_code_completion_error"] +[connection signal="completion_received" from="LLMs/GithubCopilot" to="." method="_on_code_completion_received"] diff --git a/addons/copilot/GithubCopilot.gd b/addons/copilot/GithubCopilot.gd new file mode 100644 index 0000000..99b8c02 --- /dev/null +++ b/addons/copilot/GithubCopilot.gd @@ -0,0 +1,189 @@ +@tool +extends "res://addons/copilot/LLM.gd" + +const URL = "https://api.githubcopilot.com/chat/completions" +const AUTH_URL = "https://api.github.com/copilot_internal/v2/token" +const SYSTEM_TEMPLATE = """You are a brilliant coding assistant for the game-engine Godot. The version used is Godot 4.0, and all code must be valid GDScript! +That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: +- Use @export annotation for exports +- Use Node3D instead of Spatial, and position instead of translation +- Use randf_range and randi_range instead of rand_range +- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) +- Same for sort_custom calls, pass a Callable(TARGET_OBJECT, TARGET_FUNC) +- Use rad_to_deg instead of rad2deg +- Use PackedByteArray instead of PoolByteArray +- Use instantiate instead of instance +- You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" + +Remember, this is not Python. It's GDScript for use in Godot. + +You may only answer in code, never add any explanations. In your prompt, there will be an !INSERT_CODE_HERE! tag. Only respond with plausible code that may be inserted at that point. Never repeat the full script, only the parts to be inserted. Treat this as if it was an autocompletion. You may continue whatever word or expression was left unfinished before the tag. Make sure indentation matches the surrounding context.""" +const INSERT_TAG = "!INSERT_CODE_HERE!" +const MAX_LENGTH = 8500 + +const PREFERENCES_STORAGE_NAME = "user://github_copilot_llm.cfg" +const PREFERENCES_PASS = "Jr55ICpdp3M3CuWHX0WHLqg3yh4XBjbXX" + +var machine_id +var session_id +var auth_token + +signal auth_token_retrieved + +class Message: + var role: String + var content: String + + func get_json(): + return { + "role": role, + "content": content + } + +const ROLES = { + "SYSTEM": "system", + "USER": "user", + "ASSISTANT": "assistant" +} + +func _get_models(): + return [ + "gpt-4-github-copilot" + ] + +func _set_model(model_name): + model = model_name.replace("github-copilot", "") + +func _send_user_prompt(user_prompt, user_suffix): + var messages = format_prompt(user_prompt, user_suffix) + get_completion(messages, user_prompt, user_suffix) + +func format_prompt(prompt, suffix): + var messages = [] + var system_prompt = SYSTEM_TEMPLATE + + var combined_prompt = prompt + suffix + var diff = combined_prompt.length() - MAX_LENGTH + if diff > 0: + if suffix.length() > diff: + suffix = suffix.substr(0,diff) + else: + prompt = prompt.substr(diff - suffix.length()) + suffix = "" + var user_prompt = prompt + INSERT_TAG + suffix + + var msg = Message.new() + msg.role = ROLES.SYSTEM + msg.content = system_prompt + messages.append(msg.get_json()) + + msg = Message.new() + msg.role = ROLES.USER + msg.content = user_prompt + messages.append(msg.get_json()) + + return messages + +func gen_hex_str(length: int) -> String: + var rng = RandomNumberGenerator.new() + var result = PackedByteArray() + for i in range(length / 2): + result.push_back(rng.randi_range(0, 255)) + var hex_str = "" + for byte in result: + hex_str += "%02x" % byte + return hex_str + +func create_headers(token: String, stream: bool): + var contentType: String = "application/json; charset=utf-8" + if stream: + contentType = "text/event-stream; charset=utf-8" + + load_config() + var uuidString: String = UUID.v4() + + return [ + "Authorization: %s" % ("Bearer " + token), + "X-Request-Id: %s" % uuidString, + "Vscode-Sessionid: %s" % session_id, + "Vscode-Machineid: %s" % machine_id, + "Editor-Version: vscode/1.83.1", + "Editor-Plugin-Version: copilot-chat/0.8.0", + "Openai-Organization: github-copilot", + "Openai-Intent: conversation-panel", + "Content-Type: %s" % contentType, + "User-Agent: GitHubCopilotChat/0.8.0", + "Accept: */*", + "Accept-Encoding: gzip,deflate,br", + "Connection: close" + ] + +func get_auth(): + var headers = [ + "Accept-Encoding: gzip", + "Authorization: token %s" % api_key + ] + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.connect("request_completed",on_auth_request_completed) + var error = http_request.request(AUTH_URL, headers, HTTPClient.METHOD_GET) + if error != OK: + emit_signal("completion_error", null) + +func get_completion(messages, prompt, suffix): + if not auth_token: + get_auth() + await auth_token_retrieved + + var body = { + "model": model, + "messages": messages, + "temperature": 0.7, + "top_p": 1, + "n": 1, + "stream": false, + } + var headers = create_headers(auth_token, false) + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) + var json_body = JSON.stringify(body) + var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) + if error != OK: + emit_signal("completion_error", null) + +func on_auth_request_completed(result, response_code, headers, body): + var test_json_conv = JSON.new() + test_json_conv.parse(body.get_string_from_utf8()) + var json = test_json_conv.get_data() + auth_token = json.token + auth_token_retrieved.emit() + +func on_request_completed(result, response_code, headers, body, pre, post, http_request): + var test_json_conv = JSON.new() + test_json_conv.parse(body.get_string_from_utf8()) + var json = test_json_conv.get_data() + var response = json + if !response.has("choices") : + emit_signal("completion_error", response) + return + var completion = response.choices[0].message + if is_instance_valid(http_request): + http_request.queue_free() + emit_signal("completion_received", completion.content, pre, post) + +func store_config(): + var config = ConfigFile.new() + config.set_value("auth", "machine_id", machine_id) + config.save_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) + +func load_config(): + var config = ConfigFile.new() + var err = config.load_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) + if not session_id: + session_id = gen_hex_str(8) + "-" + gen_hex_str(4) + "-" + gen_hex_str(4) + "-" + gen_hex_str(4) + "-" + gen_hex_str(25) + if err != OK: + machine_id = UUID.v4() + store_config() + return + machine_id = config.get_value("auth", "machine_id", UUID.v4()) diff --git a/addons/copilot/LLM.gd b/addons/copilot/LLM.gd new file mode 100644 index 0000000..a15fa72 --- /dev/null +++ b/addons/copilot/LLM.gd @@ -0,0 +1,30 @@ +@tool +extends Node + +var model +var api_key +var allow_multiline + +signal completion_received(completion, pre, post) +signal completion_error(error) + +#Expects return value of String Array +func _get_models(): + return [] + +#Sets active model +func _set_model(model_name): + model = model_name + +#Sets API key +func _set_api_key(key): + api_key = key + +#Determines if multiline completions are allowed +func _set_multiline(allowed): + allow_multiline = allowed + +#Sends user prompt +func _send_user_prompt(user_prompt, user_suffix): + pass + diff --git a/addons/copilot/OpenAIChat.gd b/addons/copilot/OpenAIChat.gd new file mode 100644 index 0000000..5dbf857 --- /dev/null +++ b/addons/copilot/OpenAIChat.gd @@ -0,0 +1,110 @@ +@tool +extends "res://addons/copilot/LLM.gd" + +const URL = "https://api.openai.com/v1/chat/completions" +const SYSTEM_TEMPLATE = """You are a brilliant coding assistant for the game-engine Godot. The version used is Godot 4.0, and all code must be valid GDScript! +That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: +- Use @export annotation for exports +- Use Node3D instead of Spatial, and position instead of translation +- Use randf_range and randi_range instead of rand_range +- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) +- Same for sort_custom calls, pass a Callable(TARGET_OBJECT, TARGET_FUNC) +- Use rad_to_deg instead of rad2deg +- Use PackedByteArray instead of PoolByteArray +- Use instantiate instead of instance +- You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" + +Remember, this is not Python. It's GDScript for use in Godot. + +You may only answer in code, never add any explanations. In your prompt, there will be an !INSERT_CODE_HERE! tag. Only respond with plausible code that may be inserted at that point. Never repeat the full script, only the parts to be inserted. Treat this as if it was an autocompletion. You may continue whatever word or expression was left unfinished before the tag. Make sure indentation matches the surrounding context.""" +const INSERT_TAG = "!INSERT_CODE_HERE!" +const MAX_LENGTH = 8500 + +class Message: + var role: String + var content: String + + func get_json(): + return { + "role": role, + "content": content + } + +const ROLES = { + "SYSTEM": "system", + "USER": "user", + "ASSISTANT": "assistant" +} + +func _get_models(): + return [ + "gpt-3.5-turbo", + "gpt-4" + ] + +func _set_model(model_name): + model = model_name + +func _send_user_prompt(user_prompt, user_suffix): + var messages = format_prompt(user_prompt, user_suffix) + get_completion(messages, user_prompt, user_suffix) + +func format_prompt(prompt, suffix): + var messages = [] + var system_prompt = SYSTEM_TEMPLATE + + var combined_prompt = prompt + suffix + var diff = combined_prompt.length() - MAX_LENGTH + if diff > 0: + if suffix.length() > diff: + suffix = suffix.substr(0,diff) + else: + prompt = prompt.substr(diff - suffix.length()) + suffix = "" + var user_prompt = prompt + INSERT_TAG + suffix + + var msg = Message.new() + msg.role = ROLES.SYSTEM + msg.content = system_prompt + messages.append(msg.get_json()) + + msg = Message.new() + msg.role = ROLES.USER + msg.content = user_prompt + messages.append(msg.get_json()) + + return messages + +func get_completion(messages, prompt, suffix): + var body = { + "model": model, + "messages": messages, + "temperature": 0.7, + "max_tokens": 500, + "stop": "\n\n" if allow_multiline else "\n" + } + var headers = [ + "Content-Type: application/json", + "Authorization: Bearer %s" % api_key + ] + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) + var json_body = JSON.stringify(body) + var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) + if error != OK: + emit_signal("completion_error", null) + + +func on_request_completed(result, response_code, headers, body, pre, post, http_request): + var test_json_conv = JSON.new() + test_json_conv.parse(body.get_string_from_utf8()) + var json = test_json_conv.get_data() + var response = json + if !response.has("choices") : + emit_signal("completion_error", response) + return + var completion = response.choices[0].message + if is_instance_valid(http_request): + http_request.queue_free() + emit_signal("completion_received", completion.content, pre, post) diff --git a/addons/copilot/OpenAICompletion.gd b/addons/copilot/OpenAICompletion.gd new file mode 100644 index 0000000..312cb72 --- /dev/null +++ b/addons/copilot/OpenAICompletion.gd @@ -0,0 +1,76 @@ +@tool +extends "res://addons/copilot/LLM.gd" + +const URL = "https://api.openai.com/v1/completions" +const PROMPT_PREFIX = """#This is a GDScript script using Godot 4.0. +#That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: +#- Use @export annotation for exports +#- Use Node3D instead of Spatial, and position instead of translation +#- Use randf_range and randi_range instead of rand_range +#- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) +#- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) +#- Use rad_to_deg instead of rad2deg +#- Use PackedByteArray instead of PoolByteArray +#- Use instantiate instead of instance +#- You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" +# +#Remember, this is not Python. It's GDScript for use in Godot. + + +""" +const MAX_LENGTH = 8500 + +func _get_models(): + return [ + "text-davinci-003" + ] + +func _set_model(model_name): + model = model_name + +func _send_user_prompt(user_prompt, user_suffix): + get_completion(user_prompt, user_suffix) + +func get_completion(_prompt, _suffix): + var prompt = _prompt + var suffix = _suffix + var combined_prompt = prompt + suffix + var diff = combined_prompt.length() - MAX_LENGTH + if diff > 0: + if suffix.length() > diff: + suffix = suffix.substr(0,diff) + else: + prompt = prompt.substr(diff - suffix.length()) + suffix = "" + var body = { + "model": model, + "prompt": PROMPT_PREFIX + prompt, + "suffix": suffix, + "temperature": 0.7, + "max_tokens": 500, + "stop": "\n\n" if allow_multiline else "\n" + } + var headers = [ + "Content-Type: application/json", + "Authorization: Bearer %s" % api_key + ] + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) + var json_body = JSON.stringify(body) + var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) + if error != OK: + emit_signal("completion_error", null) + +func on_request_completed(result, response_code, headers, body, pre, post, http_request): + var test_json_conv = JSON.new() + test_json_conv.parse(body.get_string_from_utf8()) + var json = test_json_conv.get_data() + var response = json + if !response.has("choices"): + emit_signal("completion_error", response) + return + var completion = response.choices[0].text + if is_instance_valid(http_request): + http_request.queue_free() + emit_signal("completion_received", completion, pre, post) diff --git a/addons/copilot/Plugin.gd b/addons/copilot/Plugin.gd new file mode 100644 index 0000000..5ac7fb0 --- /dev/null +++ b/addons/copilot/Plugin.gd @@ -0,0 +1,21 @@ +@tool +extends EditorPlugin + +const version = "1.0.0" +const scene_path = "res://addons/copilot/CopilotUI.tscn" + +var dock +var editor_interface = get_editor_interface() + +func _enter_tree() -> void: + if(!dock): + dock = load(scene_path).instantiate() + add_control_to_dock(EditorPlugin.DOCK_SLOT_RIGHT_UL, dock) + main_screen_changed.connect(Callable(dock, "on_main_screen_changed")) + dock.editor_interface = get_editor_interface() + dock.set_version(version) + +func _exit_tree(): + remove_control_from_docks(dock) + dock.queue_free() + diff --git a/addons/copilot/UUID.gd b/addons/copilot/UUID.gd new file mode 100644 index 0000000..80b5411 --- /dev/null +++ b/addons/copilot/UUID.gd @@ -0,0 +1,117 @@ +# From: https://github.com/binogure-studio/godot-uuid +# Credit: binogure-studio + +# Note: The code might not be as pretty it could be, since it's written +# in a way that maximizes performance. Methods are inlined and loops are avoided. +extends Node +class_name UUID + +const BYTE_MASK: int = 0b11111111 + +static func uuidbin(): + # 16 random bytes with the bytes on index 6 and 8 modified + return [ + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK, + ((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + ] + +static func uuidbinrng(rng: RandomNumberGenerator): + return [ + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK, + ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + ] + +static func v4(): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbin() + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +static func v4_rng(rng: RandomNumberGenerator): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbinrng(rng) + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +var _uuid: Array + +func _init(rng := RandomNumberGenerator.new()) -> void: + _uuid = uuidbinrng(rng) + +func as_array() -> Array: + return _uuid.duplicate() + +func as_dict(big_endian := true) -> Dictionary: + if big_endian: + return { + "low" : (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8 ) + _uuid[3], + "mid" : (_uuid[4] << 8 ) + _uuid[5], + "hi" : (_uuid[6] << 8 ) + _uuid[7], + "clock": (_uuid[8] << 8 ) + _uuid[9], + "node" : (_uuid[10] << 40) + (_uuid[11] << 32) + (_uuid[12] << 24) + (_uuid[13] << 16) + (_uuid[14] << 8 ) + _uuid[15] + } + else: + return { + "low" : _uuid[0] + (_uuid[1] << 8 ) + (_uuid[2] << 16) + (_uuid[3] << 24), + "mid" : _uuid[4] + (_uuid[5] << 8 ), + "hi" : _uuid[6] + (_uuid[7] << 8 ), + "clock": _uuid[8] + (_uuid[9] << 8 ), + "node" : _uuid[10] + (_uuid[11] << 8 ) + (_uuid[12] << 16) + (_uuid[13] << 24) + (_uuid[14] << 32) + (_uuid[15] << 40) + } + +func as_string() -> String: + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + _uuid[0], _uuid[1], _uuid[2], _uuid[3], + + # mid + _uuid[4], _uuid[5], + + # hi + _uuid[6], _uuid[7], + + # clock + _uuid[8], _uuid[9], + + # node + _uuid[10], _uuid[11], _uuid[12], _uuid[13], _uuid[14], _uuid[15] + ] + +func is_equal(other) -> bool: + # Godot Engine compares Array recursively + # There's no need for custom comparison here. + return _uuid == other._uuid diff --git a/addons/copilot/large_icon.tres b/addons/copilot/large_icon.tres new file mode 100644 index 0000000..887fe51 --- /dev/null +++ b/addons/copilot/large_icon.tres @@ -0,0 +1,45 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://bl1rtf743e4l3"] + +[sub_resource type="Shader" id="9"] +code = "shader_type canvas_item; + +uniform vec4 circle_color : source_color = vec4(0.0, 1.0, 1.0, 1.0); +uniform float circle_speed : hint_range(0.0, 10.0) = 1.0; +uniform float circle_width : hint_range(0.0, 1.0) = 0.1; +uniform float circle_count : hint_range(1.0, 20.0) = 6.0; +uniform float circle_size : hint_range(0.1, 2.0) = 0.5; + +// Glow settings +uniform float glow_strength : hint_range(0.0, 1.0) = 0.5; +uniform float glow_radius : hint_range(0.0, 1.0) = 0.2; + +void fragment() { + vec2 uv = UV * 3.0 - vec2(1.5, 1.5); + float len = length(uv); + + float circle = 0.0; + for (float i = 0.0; i < circle_count; i++) { + float t = i / circle_count; + float time_offset = t * 6.28318; // 2 * PI + float radius = (1.0 - t * circle_size) * (1.0 + sin(TIME * circle_speed + time_offset) * 0.1); + float circle_strength = smoothstep(radius - circle_width, radius, len) - smoothstep(radius, radius + circle_width, len); + circle = max(circle, circle_strength); + } + + // Glow effect + float glow = smoothstep(circle_width, circle_width + glow_radius, circle); + circle += glow_strength * glow; + + vec4 col = vec4(circle_color.rgb * circle, circle_color.a * circle); + COLOR = col; +}" + +[resource] +shader = SubResource("9") +shader_parameter/circle_color = Color(0.533, 0.60475, 0.82, 1) +shader_parameter/circle_speed = 2.881 +shader_parameter/circle_width = 0.05 +shader_parameter/circle_count = 4.0 +shader_parameter/circle_size = 0.8 +shader_parameter/glow_strength = 0.4 +shader_parameter/glow_radius = 0.0 diff --git a/addons/copilot/plugin.cfg b/addons/copilot/plugin.cfg new file mode 100644 index 0000000..7708dc7 --- /dev/null +++ b/addons/copilot/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Copilot" +description="Use large language models for AI assisted development in the Godot Engine." +author="Markus Sobkowski" +version="1.0" +script="Plugin.gd" diff --git a/addons/copilot/small_icon.tres b/addons/copilot/small_icon.tres new file mode 100644 index 0000000..7250b9d --- /dev/null +++ b/addons/copilot/small_icon.tres @@ -0,0 +1,45 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cccmbprav6vgu"] + +[sub_resource type="Shader" id="9"] +code = "shader_type canvas_item; + +uniform vec4 circle_color : source_color = vec4(0.0, 1.0, 1.0, 1.0); +uniform float circle_speed : hint_range(0.0, 10.0) = 1.0; +uniform float circle_width : hint_range(0.0, 1.0) = 0.1; +uniform float circle_count : hint_range(1.0, 20.0) = 6.0; +uniform float circle_size : hint_range(0.1, 2.0) = 0.5; + +// Glow settings +uniform float glow_strength : hint_range(0.0, 1.0) = 0.5; +uniform float glow_radius : hint_range(0.0, 1.0) = 0.2; + +void fragment() { + vec2 uv = UV * 3.0 - vec2(1.5, 1.5); + float len = length(uv); + + float circle = 0.0; + for (float i = 0.0; i < circle_count; i++) { + float t = i / circle_count; + float time_offset = t * 6.28318; // 2 * PI + float radius = (1.0 - t * circle_size) * (1.0 + sin(TIME * circle_speed + time_offset) * 0.1); + float circle_strength = smoothstep(radius - circle_width, radius, len) - smoothstep(radius, radius + circle_width, len); + circle = max(circle, circle_strength); + } + + // Glow effect + float glow = smoothstep(circle_width, circle_width + glow_radius, circle); + circle += glow_strength * glow; + + vec4 col = vec4(circle_color.rgb * circle, circle_color.a * circle); + COLOR = col; +}" + +[resource] +shader = SubResource("9") +shader_parameter/circle_color = Color(0.533, 0.60475, 0.82, 1) +shader_parameter/circle_speed = 4.0 +shader_parameter/circle_width = 0.3 +shader_parameter/circle_count = 2.0 +shader_parameter/circle_size = 0.8 +shader_parameter/glow_strength = 1.0 +shader_parameter/glow_radius = 0.05 diff --git a/assets/LICENSE.txt b/assets/LICENSE.txt index 6ac1081..db89517 100644 --- a/assets/LICENSE.txt +++ b/assets/LICENSE.txt @@ -1,3 +1,3 @@ GrafxKid's publicly released pixel art assets fall under the CC0 1.0 Universal Public Domain License. -CC0 1.0 terms - https://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file +CC0 1.0 terms - https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/enemy/enemy.gd b/enemy/enemy.gd index 9aff5aa..8fbb69b 100644 --- a/enemy/enemy.gd +++ b/enemy/enemy.gd @@ -2,17 +2,19 @@ extends Area2D signal died -var start_pos = Vector2.ZERO -var speed = 0 +var start_pos: Vector2 = Vector2.ZERO +var speed: float = 0 +var is_exploded = false -var bullet_scene = preload("res://enemy_bullet/enemy_bullet.tscn") +var bullet_scene: PackedScene = preload("res://enemy_bullet/enemy_bullet.tscn") -@onready var screensize = get_viewport_rect().size +@onready var screensize: Vector2 = get_viewport_rect().size func start(pos): speed = 0 position = Vector2(pos.x, -pos.y) start_pos = pos + is_exploded = false await get_tree().create_timer(randf_range(0.25, 0.55)).timeout var tween = create_tween().set_trans(Tween.TRANS_BACK) tween.tween_property(self, "position:y", start_pos.y, 1.4) @@ -23,6 +25,10 @@ func start(pos): $ShootTimer.start() func explode(): + if is_exploded: + return + + is_exploded = true speed = 0 $AnimationPlayer.play("explode") set_deferred("monitoring", false) @@ -44,3 +50,7 @@ func _on_shoot_timer_timeout(): b.start(position) $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start() + + +func _on_enemy_ready(pos): + start(pos) diff --git a/main/main.gd b/main/main.gd index 5c75887..20ec8cb 100644 --- a/main/main.gd +++ b/main/main.gd @@ -23,12 +23,19 @@ func spawn_enemies(): var e = enemy.instantiate() var pos = Vector2(x * (16 + 8) + 24, 16 * 4 + y * 16) add_child(e) - e.start(pos) - e.died.connect(_on_enemy_died) + e.call_deferred("start", pos) + e.connect("died", Callable(self, "_on_enemy_died")) + e.add_to_group("enemies") + + enemies_count = 27 func _on_enemy_died(value): score += value $CanvasLayer/UI.update_score(score) + enemies_count -= 1 + + if enemies_count == 0: + call_deferred("spawn_enemies") func _on_start_pressed(): start_button.hide() @@ -41,11 +48,3 @@ func _on_player_died(): game_over.hide() start_button.show() - -func _on_child_entered_tree(node): - if node.is_in_group("enemies"): - enemies_count += 1 - -func _on_child_exiting_tree(node): - if node.is_in_group("enemies"): - enemies_count -= 1 diff --git a/player/player.gd b/player/player.gd index 48df37f..1c68aab 100644 --- a/player/player.gd +++ b/player/player.gd @@ -5,11 +5,11 @@ signal shield_changed @onready var screensize = get_viewport_rect().size -@export var speed = 150 -@export var cooldown = 0.25 -@export var bullet_scene: PackedScene +@export var speed: int = 150 +@export var cooldown: float = 0.25 +@export var bullet_scene:PackedScene -@export var max_shield = 10 +@export var max_shield: float = 10.0 var shield = max_shield: set = set_shield @@ -66,11 +66,11 @@ func _on_gun_cooldown_timeout(): func _on_player_area_entered(area): if area.is_in_group("enemies"): area.explode() - shield -= max_shield / 2 # Replace with function body. + shield -= max_shield / 2.0 # Replace with function body. func _on_area_entered(area): if area.is_in_group("enemies"): area.explode() - shield -= max_shield / 2 # Replace with function body. + shield -= max_shield / 2.0 # Replace with function body. diff --git a/project.godot b/project.godot index 5a4f76a..7b7bf92 100644 --- a/project.godot +++ b/project.godot @@ -26,6 +26,10 @@ window/size/window_width_override=480 window/size/window_height_override=640 window/stretch/mode="canvas_items" +[editor_plugins] + +enabled=PackedStringArray("res://addons/copilot/plugin.cfg") + [input] left={