handles enemy respawning properly

This commit is contained in:
Patrick Nueser 2024-06-28 16:12:11 +02:00
parent 1e4be7ba8d
commit 3cf084e69b
17 changed files with 1342 additions and 21 deletions

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Hyper Space
Space Invaders like space shooter
## Dependencies
* Godot 4.2.2

368
addons/copilot/Copilot.gd Normal file
View File

@ -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)

View File

@ -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"]

View File

@ -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())

30
addons/copilot/LLM.gd Normal file
View File

@ -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

View File

@ -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)

View File

@ -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)

21
addons/copilot/Plugin.gd Normal file
View File

@ -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()

117
addons/copilot/UUID.gd Normal file
View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 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.

View File

@ -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={