diff --git a/addons/gdLinter/Settings/ignore.gd b/addons/gdLinter/Settings/ignore.gd new file mode 100644 index 0000000..6c763c3 --- /dev/null +++ b/addons/gdLinter/Settings/ignore.gd @@ -0,0 +1,44 @@ +class_name GDLinterIgnore +extends Resource + +@export_group("Name Checks") +@export var _function_name: bool = false +@export var _class_name: bool = false +@export var _sub_class_name: bool = false +@export var _signal_name: bool = false +@export var _class_variable_name: bool = false +@export var _class_load_variable_name: bool = false +@export var _function_variable_name: bool = false +@export var _function_preload_variable_name: bool = false +@export var _function_argument_name: bool = false +@export var _loop_variable_name: bool = false +@export var _enum_name: bool = false +@export var _enum_element_name: bool = false +@export var _constant_name: bool = false +@export var _load_constant_name: bool = false + +@export_group("Basic Checks") +@export var _duplicated_load: bool = false +@export var _expression_not_assigned: bool = false +@export var _unnecessary_pass: bool = false +@export var _unused_argument: bool = false +@export var _comparison_with_itself: bool = false + +@export_group("Class Checks") +@export var _private_method_call: bool = false +@export var _class_definitions_order: bool = false + +@export_group("Design Checks") +@export var _max_public_methods: bool = false +@export var _function_arguments_number: bool = false + +@export_group("Format Checks") +@export var _max_file_lines: bool = false +@export var _trailing_whitespace: bool = false +@export var _max_line_length: bool = false +@export var _mixed_tabs_and_spaces: bool = false + +@export_group("Misc Checks") +@export var _no_elif_return: bool = false +@export var _no_else_return: bool = false + diff --git a/addons/gdLinter/Settings/ignore.tres b/addons/gdLinter/Settings/ignore.tres new file mode 100644 index 0000000..ec32cbf --- /dev/null +++ b/addons/gdLinter/Settings/ignore.tres @@ -0,0 +1,35 @@ +[gd_resource type="Resource" script_class="GDLinterIgnore" load_steps=2 format=3 uid="uid://6ip8eigu30by"] + +[ext_resource type="Script" path="res://addons/gdLinter/Settings/ignore.gd" id="1_8j37n"] + +[resource] +script = ExtResource("1_8j37n") +_function_name = false +_class_name = false +_sub_class_name = false +_signal_name = false +_class_variable_name = false +_class_load_variable_name = false +_function_variable_name = false +_function_preload_variable_name = false +_function_argument_name = false +_loop_variable_name = false +_enum_name = false +_enum_element_name = false +_constant_name = false +_load_constant_name = false +_duplicated_load = false +_expression_not_assigned = false +_unnecessary_pass = false +_unused_argument = false +_comparison_with_itself = false +_private_method_call = false +_class_definitions_order = false +_max_public_methods = false +_function_arguments_number = false +_max_file_lines = false +_trailing_whitespace = false +_max_line_length = false +_mixed_tabs_and_spaces = false +_no_elif_return = false +_no_else_return = false diff --git a/addons/gdLinter/UI/Basic.gd b/addons/gdLinter/UI/Basic.gd new file mode 100644 index 0000000..d3a6cb4 --- /dev/null +++ b/addons/gdLinter/UI/Basic.gd @@ -0,0 +1,39 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var duplicated_load: CheckBox = %DuplicatedLoad +@onready var expression_not_assigned: CheckBox = %ExpressionNotAssigned +@onready var unnecessary_pass: CheckBox = %UnnecessaryPass +@onready var unused_argument: CheckBox = %UnusedArgument +@onready var comparision_with_itself: CheckBox = %ComparisionWithItself + + +func init() -> void: + _owner = owner + duplicated_load.button_pressed = _owner.ignore.get("_duplicated_load") + expression_not_assigned.button_pressed = _owner.ignore.get("_expression_not_assigned") + unnecessary_pass.button_pressed = _owner.ignore.get("_unnecessary_pass") + unused_argument.button_pressed = _owner.ignore.get("_unused_argument") + comparision_with_itself.button_pressed = _owner.ignore.get("_comparison_with_itself") + + +func _on_duplicated_load_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_duplicated_load", toggled_on) + + +func _on_expression_not_assigned_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_expression_not_assigned", toggled_on) + + +func _on_unnecessary_pass_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_unnecessary_pass", toggled_on) + + +func _on_unused_argument_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_unused_argument", toggled_on) + + +func _on_comparision_with_itself_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_comparison_with_itself", toggled_on) diff --git a/addons/gdLinter/UI/Class.gd b/addons/gdLinter/UI/Class.gd new file mode 100644 index 0000000..c415576 --- /dev/null +++ b/addons/gdLinter/UI/Class.gd @@ -0,0 +1,21 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var private_method_call: CheckBox = %PrivateMethodCall +@onready var class_definition_order: CheckBox = %ClassDefinitionOrder + + +func init() -> void: + _owner = owner + private_method_call.button_pressed = _owner.ignore.get("_private_method_call") + class_definition_order.button_pressed = _owner.ignore.get("_class_definitions_order") + + +func _on_private_method_call_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_private_method_call", toggled_on) + + +func _on_class_definition_order_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_class_definitions_order", toggled_on) diff --git a/addons/gdLinter/UI/Design.gd b/addons/gdLinter/UI/Design.gd new file mode 100644 index 0000000..9c81347 --- /dev/null +++ b/addons/gdLinter/UI/Design.gd @@ -0,0 +1,21 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var max_public_methods: CheckBox = %MaxPublicMethods +@onready var function_argument_number: CheckBox = %FunctionArgumentNumber + + +func init() -> void: + _owner = owner + max_public_methods.button_pressed = _owner.ignore.get("_max_public_methods") + function_argument_number.button_pressed = _owner.ignore.get("_function_arguments_number") + + +func _on_max_public_methods_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_max_public_methods", toggled_on) + + +func _on_function_argument_number_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_function_arguments_number", toggled_on) diff --git a/addons/gdLinter/UI/Dock.gd b/addons/gdLinter/UI/Dock.gd new file mode 100644 index 0000000..4f38490 --- /dev/null +++ b/addons/gdLinter/UI/Dock.gd @@ -0,0 +1,101 @@ +@tool +class_name GDLinterDock +extends Control + +var gd_linter: GDLinter +var error_descriptions := preload("res://addons/gdLinter/error_descriptions.gd").new() +var script_text_editor: ScriptEditorBase +var color_error: Color = EditorInterface.get_editor_settings()\ + .get_setting("text_editor/theme/highlighting/comment_markers/critical_color") + +var num_problems: int = 0 +var num_ignored_problems: int = 0 + +var _ignore: GDLinterIgnore = preload("res://addons/gdLinter/Settings/ignore.tres") + +@onready var file: Label = %File +@onready var problems_num: Label = %ProblemsNum +@onready var ignored_problems_num: Label = %IgnoredProblemsNum +@onready var version: Label = %Version +@onready var tree: Tree = %Tree +@onready var gd_linter_ignore_window: GDLinterIgnoreWindow = $GdLinterIgnoreWindow + + +func _ready() -> void: + gd_linter_ignore_window.ignore = _ignore + gd_linter_ignore_window.dock_ui = self + tree.add_theme_color_override("font_color", color_error) + tree.set_column_title(0, "Line") + tree.set_column_title(1, "Error") + tree.set_column_title_alignment(0, HORIZONTAL_ALIGNMENT_LEFT) + tree.set_column_title_alignment(1, HORIZONTAL_ALIGNMENT_LEFT) + tree.set_column_custom_minimum_width(0, 75) + tree.set_column_custom_minimum_width(1, 0) + tree.set_column_expand(0, false) + tree.set_column_expand(1, true) + tree.set_column_clip_content(0, false) + tree.set_column_clip_content(1, true) + tree.set_column_expand_ratio(0, 4) + tree.item_activated.connect(_on_item_activated) + +func reset_problem_num() -> void: + num_problems = 0 + num_ignored_problems = 0 + + +func create_item(line: int, name: String) -> void: + var regex = RegEx.new() + regex.compile("(?<=\\()[^\\)]+") + var result := regex.search_all(name) + var error_type := result[-1].strings[0] + if _ignore.get(str_dash_to_underscore(error_type)): + num_ignored_problems += 1 + return + + var item := tree.create_item() + item.set_text(0, str(line)) + item.set_text(1, name) + item.set_metadata(0, line) + + if error_descriptions.error.has(error_type): + item.set_tooltip_text(1, error_descriptions.error[error_type]) + num_problems += 1 + + +func set_problems_label(number: int) -> void: + problems_num.text = str(number) + + +func set_ignored_problems_label(number: int) -> void: + ignored_problems_num.text = str(number) + +func clear_items() -> void: + reset_problem_num() + tree.clear() + tree.create_item() + + +func _on_item_activated() -> void: + var selected: TreeItem = tree.get_selected() + var line := selected.get_metadata(0) + + EditorInterface.edit_script(load(file.text), line) + + if not EditorInterface.get_editor_settings().get("text_editor/external/use_external_editor"): + EditorInterface.set_main_screen_editor("Script") + + +func str_dash_to_underscore(string: String) -> String: + return "_" + string.replace("-", "_") + + +func is_error_ignored(name: String) -> bool: + var regex = RegEx.new() + regex.compile("(?<=\\()[^\\)]+") + var result := regex.search_all(name) + var error_type := result[-1].strings[0] + return _ignore.get(str_dash_to_underscore(error_type)) + + +func _on_button_pressed() -> void: + gd_linter_ignore_window.popup() diff --git a/addons/gdLinter/UI/Dock.tscn b/addons/gdLinter/UI/Dock.tscn new file mode 100644 index 0000000..9cd6778 --- /dev/null +++ b/addons/gdLinter/UI/Dock.tscn @@ -0,0 +1,111 @@ +[gd_scene load_steps=3 format=3 uid="uid://d1eqlqvotirg1"] + +[ext_resource type="Script" path="res://addons/gdLinter/UI/Dock.gd" id="1_u5r3b"] +[ext_resource type="PackedScene" uid="uid://cgk7hjif0ujw1" path="res://addons/gdLinter/UI/GDLinterIgnoreWindow.tscn" id="3_ib5aw"] + +[node name="Dock" 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_u5r3b") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Header" type="PanelContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="VBoxContainer/Header"] +layout_mode = 2 +bbcode_enabled = true +text = "[center]GDLint Plugin 2.0.2[/center]" +fit_content = true + +[node name="LintedFile" type="PanelContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/LintedFile"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/LintedFile/HBoxContainer"] +layout_mode = 2 +text = "Currently linted file:" + +[node name="File" type="Label" parent="VBoxContainer/LintedFile/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="MiddleContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +size_flags_stretch_ratio = 20.0 + +[node name="Tree" type="Tree" parent="VBoxContainer/MiddleContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 0 +theme_override_colors/font_color = Color(0.77, 0.35, 0.35, 1) +theme_override_constants/v_separation = 0 +columns = 2 +column_titles_visible = true +hide_folding = true +hide_root = true +select_mode = 1 +scroll_horizontal_enabled = false + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MiddleContainer"] +layout_mode = 2 + +[node name="Button" type="Button" parent="VBoxContainer/MiddleContainer/VBoxContainer"] +layout_mode = 2 +text = "Ignore +Settings" + +[node name="Statusbar" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 10 + +[node name="ProblemsContainer" type="HBoxContainer" parent="VBoxContainer/Statusbar"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="ProblemsNum" type="Label" parent="VBoxContainer/Statusbar/ProblemsContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "0" + +[node name="ProblemsLbl" type="Label" parent="VBoxContainer/Statusbar/ProblemsContainer"] +layout_mode = 2 +text = "problems found" + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/Statusbar/ProblemsContainer"] +layout_mode = 2 + +[node name="IgnoredProblemsNum" type="Label" parent="VBoxContainer/Statusbar/ProblemsContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "0" + +[node name="IgnoredProblemsLbl" type="Label" parent="VBoxContainer/Statusbar/ProblemsContainer"] +layout_mode = 2 +text = "problems ignored" + +[node name="Version" type="Label" parent="VBoxContainer/Statusbar"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +horizontal_alignment = 2 + +[node name="GdLinterIgnoreWindow" parent="." instance=ExtResource("3_ib5aw")] +dialog_text = "" + +[connection signal="pressed" from="VBoxContainer/MiddleContainer/VBoxContainer/Button" to="." method="_on_button_pressed"] diff --git a/addons/gdLinter/UI/Format.gd b/addons/gdLinter/UI/Format.gd new file mode 100644 index 0000000..f61661e --- /dev/null +++ b/addons/gdLinter/UI/Format.gd @@ -0,0 +1,33 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var max_file_lines: CheckBox = %MaxFileLines +@onready var trailing_whitespace_check_box: CheckBox = %TrailingWhitespaceCheckBox +@onready var max_line_length: CheckBox = %MaxLineLength +@onready var mixed_tabs_and_spaces: CheckBox = %MixedTabsAndSpaces + + +func init() -> void: + _owner = owner + max_file_lines.button_pressed = _owner.ignore.get("_max_file_lines") + trailing_whitespace_check_box.button_pressed = _owner.ignore.get("_trailing_whitespace") + max_line_length.button_pressed = _owner.ignore.get("_max_line_length") + mixed_tabs_and_spaces.button_pressed = _owner.ignore.get("_mixed_tabs_and_spaces") + + +func _on_max_file_lines_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_max_file_lines", toggled_on) + + +func _on_trailing_whitespace_check_box_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_trailing_whitespace", toggled_on) + + +func _on_max_line_length_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_max_line_length", toggled_on) + + +func _on_mixed_tabs_and_spaces_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_mixed_tabs_and_spaces", toggled_on) diff --git a/addons/gdLinter/UI/GDLinterIgnoreWindow.gd b/addons/gdLinter/UI/GDLinterIgnoreWindow.gd new file mode 100644 index 0000000..7a00b80 --- /dev/null +++ b/addons/gdLinter/UI/GDLinterIgnoreWindow.gd @@ -0,0 +1,31 @@ +@tool +class_name GDLinterIgnoreWindow +extends AcceptDialog + +var ignore: GDLinterIgnore +var dock_ui: GDLinterDock + +@onready var basic: MarginContainer = %Basic +@onready var design: MarginContainer = %Design +@onready var format: MarginContainer = %Format +@onready var misc: MarginContainer = %Misc +@onready var _name: MarginContainer = %Name +@onready var _class: MarginContainer = %Class + + +func reapply_linting() -> void: + var current_script := EditorInterface.get_script_editor().get_current_script() + dock_ui.gd_linter.script_editor.editor_script_changed.emit(current_script) + + +func _on_confirmed() -> void: + reapply_linting() + + +func _on_about_to_popup() -> void: + basic.init() + design.init() + format.init() + misc.init() + _name.init() + _class.init() diff --git a/addons/gdLinter/UI/GDLinterIgnoreWindow.tscn b/addons/gdLinter/UI/GDLinterIgnoreWindow.tscn new file mode 100644 index 0000000..1974c03 --- /dev/null +++ b/addons/gdLinter/UI/GDLinterIgnoreWindow.tscn @@ -0,0 +1,261 @@ +[gd_scene load_steps=8 format=3 uid="uid://cgk7hjif0ujw1"] + +[ext_resource type="Script" path="res://addons/gdLinter/UI/GDLinterIgnoreWindow.gd" id="1_71hhq"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Name.gd" id="2_35t2j"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Basic.gd" id="3_ijjp7"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Class.gd" id="4_s2mg3"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Design.gd" id="5_2ncqt"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Format.gd" id="6_5fvrg"] +[ext_resource type="Script" path="res://addons/gdLinter/UI/Misc.gd" id="7_l8gkb"] + +[node name="GdLinterIgnoreWindow" type="AcceptDialog"] +disable_3d = true +title = "GDLinter Ignore Settings" +initial_position = 1 +size = Vector2i(480, 539) +dialog_text = "fghgfdfggfd" +script = ExtResource("1_71hhq") + +[node name="PanelContainer" type="PanelContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -34.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TabContainer" type="TabContainer" parent="PanelContainer"] +layout_mode = 2 + +[node name="Name" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +layout_mode = 2 +script = ExtResource("2_35t2j") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Name"] +layout_mode = 2 + +[node name="FunctionName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Function Name" + +[node name="ClassName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Class Name" + +[node name="SubClassName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Sub Class Name" + +[node name="SignalName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Signal Name" + +[node name="ClassVariableName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Class Variable Name" + +[node name="ClassLoadVariableName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Class Load Variable Name" + +[node name="FunctionVariableName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Function Variable Name" + +[node name="FunctionPreloadVariableName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Function Preload Variable Name" + +[node name="FunctionArgumentName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Function Argument Name" + +[node name="LoopVariableName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Loop Variable Name" + +[node name="EnumName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Enum Name" + +[node name="EnumElementName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Enum Element Name" + +[node name="ConstantName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Constant Name" + +[node name="LoadConstantName" type="CheckBox" parent="PanelContainer/TabContainer/Name/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Load Constant Name" + +[node name="Basic" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +script = ExtResource("3_ijjp7") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Basic"] +layout_mode = 2 + +[node name="DuplicatedLoad" type="CheckBox" parent="PanelContainer/TabContainer/Basic/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Duplicated Load" + +[node name="ExpressionNotAssigned" type="CheckBox" parent="PanelContainer/TabContainer/Basic/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Expression Not Assigned" + +[node name="UnnecessaryPass" type="CheckBox" parent="PanelContainer/TabContainer/Basic/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Unnecessary Pass" + +[node name="UnusedArgument" type="CheckBox" parent="PanelContainer/TabContainer/Basic/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Unused Argument" + +[node name="ComparisionWithItself" type="CheckBox" parent="PanelContainer/TabContainer/Basic/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Comparision With Itself" + +[node name="Class" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +script = ExtResource("4_s2mg3") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Class"] +layout_mode = 2 + +[node name="PrivateMethodCall" type="CheckBox" parent="PanelContainer/TabContainer/Class/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Private Method Call" + +[node name="ClassDefinitionOrder" type="CheckBox" parent="PanelContainer/TabContainer/Class/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Class Definition Order" + +[node name="Design" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +script = ExtResource("5_2ncqt") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Design"] +layout_mode = 2 + +[node name="MaxPublicMethods" type="CheckBox" parent="PanelContainer/TabContainer/Design/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Max Public Methods" + +[node name="FunctionArgumentNumber" type="CheckBox" parent="PanelContainer/TabContainer/Design/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Function Argument Number" + +[node name="Format" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +script = ExtResource("6_5fvrg") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Format"] +layout_mode = 2 + +[node name="MaxFileLines" type="CheckBox" parent="PanelContainer/TabContainer/Format/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Max File Lines" + +[node name="TrailingWhitespaceCheckBox" type="CheckBox" parent="PanelContainer/TabContainer/Format/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Trailing Whitespace" + +[node name="MaxLineLength" type="CheckBox" parent="PanelContainer/TabContainer/Format/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Max Line Length" + +[node name="MixedTabsAndSpaces" type="CheckBox" parent="PanelContainer/TabContainer/Format/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Mixed Tabs And Spaces" + +[node name="Misc" type="MarginContainer" parent="PanelContainer/TabContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +script = ExtResource("7_l8gkb") + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/TabContainer/Misc"] +layout_mode = 2 + +[node name="NoElifReturn" type="CheckBox" parent="PanelContainer/TabContainer/Misc/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "No Elif Return" + +[node name="NoElseReturn" type="CheckBox" parent="PanelContainer/TabContainer/Misc/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "No Else Return" + +[connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"] +[connection signal="confirmed" from="." to="." method="_on_confirmed"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/FunctionName" to="PanelContainer/TabContainer/Name" method="_on_function_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/ClassName" to="PanelContainer/TabContainer/Name" method="_on_class_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/SubClassName" to="PanelContainer/TabContainer/Name" method="_on_sub_class_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/SignalName" to="PanelContainer/TabContainer/Name" method="_on_signal_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/ClassVariableName" to="PanelContainer/TabContainer/Name" method="_on_class_variable_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/ClassLoadVariableName" to="PanelContainer/TabContainer/Name" method="_on_class_load_variable_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/FunctionVariableName" to="PanelContainer/TabContainer/Name" method="_on_function_variable_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/FunctionPreloadVariableName" to="PanelContainer/TabContainer/Name" method="_on_function_preload_variable_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/FunctionArgumentName" to="PanelContainer/TabContainer/Name" method="_on_function_argument_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/LoopVariableName" to="PanelContainer/TabContainer/Name" method="_on_loop_variable_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/EnumName" to="PanelContainer/TabContainer/Name" method="_on_enum_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/EnumElementName" to="PanelContainer/TabContainer/Name" method="_on_enum_element_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/ConstantName" to="PanelContainer/TabContainer/Name" method="_on_constant_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Name/VBoxContainer/LoadConstantName" to="PanelContainer/TabContainer/Name" method="_on_load_constant_name_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Basic/VBoxContainer/DuplicatedLoad" to="PanelContainer/TabContainer/Basic" method="_on_duplicated_load_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Basic/VBoxContainer/ExpressionNotAssigned" to="PanelContainer/TabContainer/Basic" method="_on_expression_not_assigned_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Basic/VBoxContainer/UnnecessaryPass" to="PanelContainer/TabContainer/Basic" method="_on_unnecessary_pass_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Basic/VBoxContainer/UnusedArgument" to="PanelContainer/TabContainer/Basic" method="_on_unused_argument_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Basic/VBoxContainer/ComparisionWithItself" to="PanelContainer/TabContainer/Basic" method="_on_comparision_with_itself_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Class/VBoxContainer/PrivateMethodCall" to="PanelContainer/TabContainer/Class" method="_on_private_method_call_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Class/VBoxContainer/ClassDefinitionOrder" to="PanelContainer/TabContainer/Class" method="_on_class_definition_order_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Design/VBoxContainer/MaxPublicMethods" to="PanelContainer/TabContainer/Design" method="_on_max_public_methods_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Design/VBoxContainer/FunctionArgumentNumber" to="PanelContainer/TabContainer/Design" method="_on_function_argument_number_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Format/VBoxContainer/MaxFileLines" to="PanelContainer/TabContainer/Format" method="_on_max_file_lines_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Format/VBoxContainer/TrailingWhitespaceCheckBox" to="PanelContainer/TabContainer/Format" method="_on_trailing_whitespace_check_box_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Format/VBoxContainer/MaxLineLength" to="PanelContainer/TabContainer/Format" method="_on_max_line_length_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Format/VBoxContainer/MixedTabsAndSpaces" to="PanelContainer/TabContainer/Format" method="_on_mixed_tabs_and_spaces_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Misc/VBoxContainer/NoElifReturn" to="PanelContainer/TabContainer/Misc" method="_on_no_elif_return_toggled"] +[connection signal="toggled" from="PanelContainer/TabContainer/Misc/VBoxContainer/NoElseReturn" to="PanelContainer/TabContainer/Misc" method="_on_no_else_return_toggled"] diff --git a/addons/gdLinter/UI/Misc.gd b/addons/gdLinter/UI/Misc.gd new file mode 100644 index 0000000..6e6bbe5 --- /dev/null +++ b/addons/gdLinter/UI/Misc.gd @@ -0,0 +1,21 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var no_elif_return: CheckBox = %NoElifReturn +@onready var no_else_return: CheckBox = %NoElseReturn + + +func init() -> void: + _owner = owner + no_elif_return.button_pressed = _owner.ignore.get("_no_elif_return") + no_else_return.button_pressed = _owner.ignore.get("_no_else_return") + + +func _on_no_elif_return_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_no_elif_return", toggled_on) + + +func _on_no_else_return_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_no_else_return", toggled_on) diff --git a/addons/gdLinter/UI/Name.gd b/addons/gdLinter/UI/Name.gd new file mode 100644 index 0000000..6b3920e --- /dev/null +++ b/addons/gdLinter/UI/Name.gd @@ -0,0 +1,93 @@ +@tool +extends MarginContainer + +var _owner: GDLinterIgnoreWindow = owner + +@onready var function_name: CheckBox = %FunctionName +@onready var sub_class_name: CheckBox = %SubClassName +@onready var signal_name: CheckBox = %SignalName +@onready var class_variable_name: CheckBox = %ClassVariableName +@onready var class_load_variable_name: CheckBox = %ClassLoadVariableName +@onready var function_variable_name: CheckBox = %FunctionVariableName +@onready var function_preload_variable_name: CheckBox = %FunctionPreloadVariableName +@onready var function_argument_name: CheckBox = %FunctionArgumentName +@onready var loop_variable_name: CheckBox = %LoopVariableName +@onready var enum_name: CheckBox = %EnumName +@onready var enum_element_name: CheckBox = %EnumElementName +@onready var constant_name: CheckBox = %ConstantName +@onready var load_constant_name: CheckBox = %LoadConstantName +@onready var _class_name: CheckBox = %ClassName + + +func init() -> void: + _owner = owner + function_name.button_pressed = _owner.ignore.get("_function_name") + _class_name.button_pressed = _owner.ignore.get("_class_name") + sub_class_name.button_pressed = _owner.ignore.get("_sub_class_name") + signal_name.button_pressed = _owner.ignore.get("_signal_name") + class_variable_name.button_pressed = _owner.ignore.get("_class_variable_name") + class_load_variable_name.button_pressed = _owner.ignore.get("_class_load_variable_name") + function_variable_name.button_pressed = _owner.ignore.get("_function_variable_name") + function_preload_variable_name.button_pressed = _owner.ignore.get("_function_preload_variable_name") + function_argument_name.button_pressed = _owner.ignore.get("_function_argument_name") + loop_variable_name.button_pressed = _owner.ignore.get("_loop_variable_name") + enum_name.button_pressed = _owner.ignore.get("_enum_name") + enum_element_name.button_pressed = _owner.ignore.get("_enum_element_name") + constant_name.button_pressed = _owner.ignore.get("_constant_name") + load_constant_name.button_pressed = _owner.ignore.get("_load_constant_name") + + +func _on_function_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_function_name", toggled_on) + + +func _on_class_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_class_name", toggled_on) + + +func _on_sub_class_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_sub_class_name", toggled_on) + + +func _on_signal_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_signal_name", toggled_on) + + +func _on_class_variable_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_class_variable_name", toggled_on) + + +func _on_class_load_variable_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_class_load_variable_name", toggled_on) + + +func _on_function_variable_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_function_variable_name", toggled_on) + + +func _on_function_preload_variable_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_function_preload_variable_name", toggled_on) + + +func _on_function_argument_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_function_argument_name", toggled_on) + + +func _on_loop_variable_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_loop_variable_name", toggled_on) + + +func _on_enum_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_enum_name", toggled_on) + + +func _on_enum_element_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_enum_element_name", toggled_on) + + +func _on_constant_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_constant_name", toggled_on) + + +func _on_load_constant_name_toggled(toggled_on: bool) -> void: + _owner.ignore.set("_load_constant_name", toggled_on) diff --git a/addons/gdLinter/error_descriptions.gd b/addons/gdLinter/error_descriptions.gd new file mode 100644 index 0000000..684d888 --- /dev/null +++ b/addons/gdLinter/error_descriptions.gd @@ -0,0 +1,51 @@ +extends Resource + +var error := { + #region Name Checks + "function-name": "Validates if function name conforms to snake_case, _private_snake_case, or _on_PascalCase_snake_case.", + "class-name": "Validates if class name conforms to PascalCase.", + "sub-class-name": "Validates if class name conforms to _PrivatePascalCase.", + "signal-name": "Validates if signal name conforms to PascalCase.", + "class-variable-name": "Validates if class variable name conforms to snake_case or _private_snake_case.", + "class-load-variable-name": "Validates if class load variable (var variable = load(...)) name conforms to PascalCase, snake_case or private_snake_case.", + "function-variable-name": "alidates if function variable name conforms to snake_case.", + "function-preload-variable-name": "Validates if function preload variable (var Variable = preload(...)) name conforms to PascalCase.", + "function-argument-name": "Validates if function argument (formal parameter) name conforms to snake_case or _private_snake_case.", + "loop-variable-name": "Validates if loop variable name conforms to snake_case or _private_snake_case.", + "enum-name": "Validates if enum name conforms to PascalCase.", + "enum-element-name": "Validates if enum element name conforms to UPPER_SNAKE_CASE.", + "constant-name": "Validates if constant name conforms to UPPER_SNAKE_CASE.", + "load-constant-name": "Validates if load constant (const constant = load(...)) name conforms to PascalCase, snake_case or private_snake_case.", + #endregion + + #region Basic Checks + "duplicated-load": "Copy-pasted load(...) for the same path e.g. load('res://asdf.tscn') in multiple places. To fix, simply extract string to constant.", + "expression-not-assigned": "Standalone expression like 1 + 1 which is not used in any way. To fix, simply remove that expression.", + "unnecessary-pass": "Pass which is not the only expression on class or function body. To fix, simple remove that pass statement.", + "unused-argument": "Unused funtion argument. To fix, simply remove it or mark as explicitly unused by prefixing with underscore _ e.g. _unused_arg.", + "comparison-with-itself": "Redundant comparison like e.g. x == x which is always true. To fix, simply remove that expression.", + #endregion + + #region Class Checks + "private-method-call": """private (prefixed with underscore _) function was called. + E.g. player._private_func(). To fix, redesign your approach so that private function is not being called.""", + "class-definitions-order": "Class statements are not in order.", + #endregion + + #region Design Checks + "max-public-methods": "Validates maximum number of public methods (class-level functions).", + "function-arguments-number": "Validates number of function arguments.", + #endregion + + #region Format Checks + "max-file-lines": "Validates maximum number of file lines.", + "trailing-whitespace": "Validates if any trailing whitespaces are present.", + "max-line-length": "Validates maxium line length for each line.", + "mixed-tabs-and-spaces": "Validates if either only tabs or only spaces are used for indentation.", + #endregion + + #region Misc Checks + "no-elif-return": "Validates if unnecessary elif is present in case if body was ended with return.", + "no-else-return": "Validates if unnecessary else is present in case if (and each elif) body was ended with return." + #endregion +} diff --git a/addons/gdLinter/gdLinter.gd b/addons/gdLinter/gdLinter.gd new file mode 100644 index 0000000..ca76314 --- /dev/null +++ b/addons/gdLinter/gdLinter.gd @@ -0,0 +1,188 @@ +@tool +class_name GDLinter +extends EditorPlugin + +const DockScene := preload("res://addons/gdLinter/UI/Dock.tscn") + + +var icon_error := EditorInterface.get_editor_theme().get_icon("Error", "EditorIcons") +var color_error: Color = EditorInterface.get_editor_settings()\ + .get_setting("text_editor/theme/highlighting/comment_markers/critical_color") + +var icon_error_ignore := EditorInterface.get_editor_theme().get_icon("ErrorWarning", "EditorIcons") +var icon_ignore := EditorInterface.get_editor_theme().get_icon("Warning", "EditorIcons") + +var icon_success := EditorInterface.get_editor_theme().get_icon("StatusSuccess", "EditorIcons") +var color_success: Color = EditorInterface.get_editor_settings()\ + .get_setting("text_editor/theme/highlighting/comment_markers/notice_color") + +var bottom_panel_button: Button +var highlight_lines: PackedInt32Array +var item_lists: Array[ItemList] +var script_editor: ScriptEditor + +var _dock_ui: GDLinterDock +var _is_gdlint_installed: bool +var _ignore: Resource +var _gdlint_path: String + + +func _enter_tree() -> void: + # install the GDLint dock + _dock_ui = DockScene.instantiate() + _dock_ui.gd_linter = self + bottom_panel_button = add_control_to_bottom_panel(_dock_ui, "GDLint") + + # connect signal to lint on save + resource_saved.connect(on_resource_saved) + + script_editor = EditorInterface.get_script_editor() + script_editor.editor_script_changed.connect(_on_editor_script_changed) + _gdlint_path = get_gdlint_path() + get_gdlint_version() + prints("Loading GDLint Plugin success") + +# TODO: Reenable again? +# Dunno how highlighting lines in Godot works, since it get removed after a second or so +# So I use this evil workaround straight from hell: +#func _process(_delta: float) -> void: + #if not get_current_editor(): + #return + # + #if not highlight_lines.is_empty(): + #set_line_color(color_error) + + +func _on_editor_script_changed(script: Script) -> void: + _dock_ui.clear_items() + on_resource_saved(script) + + +func get_gdlint_version() -> void: + var output := [] + OS.execute(_gdlint_path, ["--version"], output) + _is_gdlint_installed = true if not output[0].is_empty() else false + if _is_gdlint_installed: + _dock_ui.version.text = "Using %s" % output[0] + else: + _dock_ui.version.text = "gdlint not found!" + + +func _exit_tree() -> void: + if is_instance_valid(_dock_ui): + remove_control_from_bottom_panel(_dock_ui) + _dock_ui.free() + + if Engine.get_version_info().hex >= 0x40201: + prints("Unload GDLint Plugin success") + + +func on_resource_saved(resource: Resource) -> void: + if not resource is GDScript: + return + + _dock_ui.clear_items() + clear_highlights() + + # Show resource path in the GDLint Dock + _dock_ui.file.text = resource.resource_path + + # Execute linting and get its output + var filepath: String = ProjectSettings.globalize_path(resource.resource_path) + var gdlint_output: Array = [] + var output_array: PackedStringArray + var exit_code = OS.execute(_gdlint_path, [filepath], gdlint_output, true) + if not exit_code == -1: + var output_string: String = gdlint_output[0] + output_array = output_string.replace(filepath+":", "Line ").split("\n") + + _dock_ui.set_problems_label(_dock_ui.num_problems) + _dock_ui.set_ignored_problems_label(_dock_ui.num_ignored_problems) + + # Workaround until unique name bug is fixed + # https://github.com/Scony/godot-gdscript-toolkit/issues/284 + # Hope I won't break other stuff with it + if not output_array.size() or output_array[0] == "Line ": + printerr("gdLint Error: ", output_array, "\n File can't be linted!") + return + + # When there is no error + if output_array.size() <= 2: + bottom_panel_button.add_theme_constant_override(&"icon_max_width", 8) + bottom_panel_button.icon = icon_success + return + + # When errors are found create buttons in the dock + for i in output_array.size()-2: + var regex := RegEx.new() + regex.compile("\\d+") + var result := regex.search(output_array[i]) + if result: + var current_line := int(result.strings[0])-1 + var error := output_array[i].rsplit(":", true, 1) + if len(error) > 1: + _dock_ui.create_item(current_line+1, error[1]) + if _dock_ui.is_error_ignored(error[1]): + continue + highlight_lines.append(current_line) + + _dock_ui.set_problems_label(_dock_ui.num_problems) + _dock_ui.set_ignored_problems_label(_dock_ui.num_ignored_problems) + + # Error, no Ignore + if _dock_ui.num_problems > 0 and _dock_ui.num_ignored_problems <= 0: + bottom_panel_button.icon = icon_error + # no Error, Ignore + elif _dock_ui.num_problems <= 0 and _dock_ui.num_ignored_problems > 0: + bottom_panel_button.icon = icon_ignore + # Error, Ignore + elif _dock_ui.num_problems > 0 and _dock_ui.num_ignored_problems > 0: + bottom_panel_button.icon = icon_error_ignore + else: + bottom_panel_button.icon = null + _dock_ui.script_text_editor = EditorInterface.get_script_editor().get_current_editor() + + +func set_line_color(color: Color) -> void: + var current_code_editor := get_current_editor() + if current_code_editor == null: + return + + for line: int in highlight_lines: + # Skip line if this one is from the old code editor + if line > current_code_editor.get_line_count()-1: + continue + current_code_editor.set_line_background_color(line, + color.darkened(0.5)) + + +func clear_highlights() -> void: + set_line_color(Color(0, 0, 0, 0)) + highlight_lines.clear() + + +func get_current_editor() -> CodeEdit: + var current_editor := EditorInterface.get_script_editor().get_current_editor() + if current_editor == null: + return + return current_editor.get_base_editor() as CodeEdit + + +func get_gdlint_path() -> String: + if OS.get_name() == "Windows": + return "gdlint" + + # macOS & Linux + var output := [] + OS.execute("python3", ["-m", "site", "--user-base"], output) + var python_bin_folder := (output[0] as String).strip_edges().path_join("bin") + if FileAccess.file_exists(python_bin_folder.path_join("gdlint")): + return python_bin_folder.path_join("gdlint") + + # Linux dirty hardcoded fallback + if OS.get_name() == "Linux": + if FileAccess.file_exists("/usr/bin/gdlint"): + return "/usr/bin/gdlint" + + # Global fallback + return "gdlint" diff --git a/addons/gdLinter/plugin.cfg b/addons/gdLinter/plugin.cfg new file mode 100644 index 0000000..f9487ba --- /dev/null +++ b/addons/gdLinter/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="gdLinter" +description="Runs `gdlint` on save to automatically lint your GDScript as you code." +author="Falli" +version="2.0.2" +script="gdLinter.gd" diff --git a/data/buildings/basic/researcher/researcher.gd b/data/buildings/basic/researcher/researcher.gd index 7cc6b02..241b94c 100644 --- a/data/buildings/basic/researcher/researcher.gd +++ b/data/buildings/basic/researcher/researcher.gd @@ -1,5 +1,10 @@ extends Node class_name Researcher -func interact() -> bool: +const RESEARCH_MENU = preload("res://scene/research_menu.tscn") + +func interact(on_interaction_finished: Callable) -> bool: + var build_menu = RESEARCH_MENU.instantiate() + build_menu.research.connect(on_interaction_finished) + add_sibling(build_menu) return true diff --git a/data/buildings/basic/researcher/researcher_building.tres b/data/buildings/basic/researcher/researcher_building.tres index 3587a8c..8034270 100644 --- a/data/buildings/basic/researcher/researcher_building.tres +++ b/data/buildings/basic/researcher/researcher_building.tres @@ -1,8 +1,9 @@ -[gd_resource type="Resource" script_class="Building" load_steps=5 format=3 uid="uid://bibep1rd0jml2"] +[gd_resource type="Resource" script_class="Building" load_steps=6 format=3 uid="uid://bibep1rd0jml2"] [ext_resource type="Texture2D" uid="uid://n806c03hgaq1" path="res://assets/scifi_tilesheet@2.png" id="1_kruad"] [ext_resource type="Resource" uid="uid://bpjj0x7jr1k6u" path="res://data/game_resources/carbon/carbon_resource.tres" id="2_hybxl"] [ext_resource type="Script" path="res://data/buildings/building.gd" id="3_pleu3"] +[ext_resource type="PackedScene" uid="uid://bw3j3vxpsfxst" path="res://data/buildings/basic/researcher/researcher.tscn" id="4_sugjm"] [sub_resource type="AtlasTexture" id="AtlasTexture_n7l2d"] atlas = ExtResource("1_kruad") @@ -16,3 +17,4 @@ description = "Research new buildings and technologies" cost = { ExtResource("2_hybxl"): 6000 } +world_scene = ExtResource("4_sugjm") diff --git a/data/buildings/building_base.gd b/data/buildings/building_base.gd index 8121f96..ab3232b 100644 --- a/data/buildings/building_base.gd +++ b/data/buildings/building_base.gd @@ -2,6 +2,7 @@ class_name BuildingBase extends Node2D var _building_data: Building +var building_data_scene @onready var sprite_2d: Sprite2D = $Sprite2D @@ -9,11 +10,9 @@ func initialize(data: Building, grid_location: Vector2i) -> void: _building_data = data position = Grid.grid_to_world_center(grid_location) sprite_2d.texture = _building_data.atlas_texture - var building_data_scene = _building_data.world_scene.instantiate() - add_child(building_data_scene) + if _building_data.world_scene: + building_data_scene = _building_data.world_scene.instantiate() + add_child(building_data_scene) -func _ready() -> void: - _building_data.ready(self) - -func interact() -> bool: - return false +func interact(on_interaction_finished: Callable) -> bool: + return building_data_scene.interact(on_interaction_finished) diff --git a/data/game_resources/gem/gem_resource.tres b/data/game_resources/gem/gem_resource.tres index c7f0b25..6ca06dc 100644 --- a/data/game_resources/gem/gem_resource.tres +++ b/data/game_resources/gem/gem_resource.tres @@ -9,3 +9,5 @@ atlas_location = Vector2i(5, 6) pickup_value = 10 name = "Gem" spawn_patterns = Array[Resource("res://scripts/spawn_pattern.gd")]([ExtResource("2_n3fuo")]) +storage_max = 0 +skills_needed = Array[int]([0]) diff --git a/data/research/gem_research.tres b/data/research/gem_research.tres new file mode 100644 index 0000000..8ad60fb --- /dev/null +++ b/data/research/gem_research.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="Research" load_steps=5 format=3 uid="uid://ddsmk1qmb2ohf"] + +[ext_resource type="Texture2D" uid="uid://n806c03hgaq1" path="res://assets/scifi_tilesheet@2.png" id="1_i07ie"] +[ext_resource type="Script" path="res://data/research/research.gd" id="1_swmp6"] +[ext_resource type="Resource" uid="uid://bpjj0x7jr1k6u" path="res://data/game_resources/carbon/carbon_resource.tres" id="2_gc3ag"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_1i8g4"] +atlas = ExtResource("1_i07ie") +region = Rect2(320, 384, 64, 64) + +[resource] +script = ExtResource("1_swmp6") +atlas_texture = SubResource("AtlasTexture_1i8g4") +name = "Gems" +description = "Allows the gathering of Gems" +cost = { +ExtResource("2_gc3ag"): 10 +} diff --git a/data/research/research.gd b/data/research/research.gd new file mode 100644 index 0000000..3380227 --- /dev/null +++ b/data/research/research.gd @@ -0,0 +1,8 @@ +class_name Research +extends Resource + +@export var atlas_texture: AtlasTexture +@export var name: String +@export var description: String +@export var cost: Dictionary +@export var world_scene: PackedScene diff --git a/project.godot b/project.godot index cb82b17..9a066b3 100644 --- a/project.godot +++ b/project.godot @@ -25,6 +25,10 @@ BuildingManager="*res://scripts/autoloads/building_manager.gd" gdscript/warnings/untyped_declaration=1 +[editor_plugins] + +enabled=PackedStringArray("res://addons/gdLinter/plugin.cfg") + [gui] theme/custom="res://data/world_theme.tres" diff --git a/scene/research_menu.tscn b/scene/research_menu.tscn new file mode 100644 index 0000000..2f6215c --- /dev/null +++ b/scene/research_menu.tscn @@ -0,0 +1,80 @@ +[gd_scene load_steps=3 format=3 uid="uid://be35i6l6srg64"] + +[ext_resource type="Script" path="res://scripts/research_menu.gd" id="1_k3x02"] + +[sub_resource type="Theme" id="Theme_0djom"] + +[node name="ResearchMenu" type="CanvasLayer"] +script = ExtResource("1_k3x02") + +[node name="MarginContainer" type="MarginContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 128 +theme_override_constants/margin_top = 128 +theme_override_constants/margin_right = 128 +theme_override_constants/margin_bottom = 128 + +[node name="PanelContainer" type="PanelContainer" parent="MarginContainer"] +layout_mode = 2 +theme = SubResource("Theme_0djom") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/PanelContainer"] +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/PanelContainer/HBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ResearchItems" type="ItemList" parent="MarginContainer/PanelContainer/HBoxContainer/ScrollContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +allow_search = false +auto_height = true + +[node name="DetailsContainer" type="VBoxContainer" parent="MarginContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 3.0 + +[node name="Title" type="Label" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Select research" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer"] +layout_mode = 2 + +[node name="Description" type="Label" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +text = " " + +[node name="ResearchMaterials" type="GridContainer" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +columns = 2 + +[node name="ResearchButtonsContainer" type="HBoxContainer" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +alignment = 1 + +[node name="StartResearchButton" type="Button" parent="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer/ResearchButtonsContainer"] +layout_mode = 2 +text = "Start Research" + +[connection signal="item_selected" from="MarginContainer/PanelContainer/HBoxContainer/ScrollContainer/VBoxContainer/ResearchItems" to="." method="_on_research_selected"] +[connection signal="pressed" from="MarginContainer/PanelContainer/HBoxContainer/DetailsContainer/ResearchButtonsContainer/StartResearchButton" to="." method="_on_build_button_pressed"] diff --git a/scripts/build_menu.gd b/scripts/build_menu.gd index 7e3eb78..a2b4a76 100644 --- a/scripts/build_menu.gd +++ b/scripts/build_menu.gd @@ -20,7 +20,7 @@ const BASIC_BUILDING_GROUP = preload("res://data/buildings/basic/basic_building_ func _ready() -> void: _add_building_group(BASIC_BUILDING_GROUP) _add_building_group(ADVANCED_BUILDING_GROUP) - #building_groups.set_focus() + building_groups.grab_focus() func _add_building_group(group: BuildingGroup) -> void: building_groups.add_item(group.name, group.atlas_texture) diff --git a/scripts/cell_data.gd b/scripts/cell_data.gd index 19d1b49..e246cea 100644 --- a/scripts/cell_data.gd +++ b/scripts/cell_data.gd @@ -33,6 +33,9 @@ func get_resource() -> GameResource: func has_resource() -> bool: return has_layer(Constants.TilemapLayers.ENVIRONMENT) + +func get_building() -> BuildingBase: + return layer_info[Constants.TilemapLayers.BUILDINGS] as BuildingBase func has_building() -> bool: return has_layer(Constants.TilemapLayers.BUILDINGS) diff --git a/scripts/game_resource.gd b/scripts/game_resource.gd index 1c79ee2..9f26f34 100644 --- a/scripts/game_resource.gd +++ b/scripts/game_resource.gd @@ -8,6 +8,7 @@ signal gained_resource(res: GameResource) @export var name: String @export var spawn_patterns: Array[SpawnPattern] @export var storage_max: int +@export var skills_needed: Array[Skills.ABILITIES] = [] func _to_string() -> String: return name @@ -17,3 +18,6 @@ func get_spawn_locations() -> Array[Vector2i]: for spawn in spawn_patterns: spawns.append_array(spawn.get_spawn_locations()) return spawns + +func can_harvest(skills: Skills) -> bool: + return skills.has_skills(skills_needed) diff --git a/scripts/player.gd b/scripts/player.gd index ccdf334..4e5b7ae 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -9,6 +9,9 @@ var _interaction_options: InteractionWheel var _interacting := false var _attempting_build: Building var _build_placement: Sprite2D +var _in_menu := false + +var _skills := Skills.new() @onready var sprite: AnimatedSprite2D = $Sprite @onready var interaction_timer: Timer = $InteractionTimer @@ -17,7 +20,7 @@ const BUILD_MENU = preload("res://scene/build_menu.tscn") const BUILDING_BASE = preload("res://data/buildings/building_base.tscn") func _physics_process(delta: float) -> void: - var input_direction := Input.get_vector("move_left", "move_right", "move_up", "move_down") + var input_direction := Input.get_vector("move_left", "move_right", "move_up", "move_down") if not _in_menu else Vector2.ZERO velocity = input_direction * SPEED / delta move_and_slide() @@ -39,24 +42,31 @@ func _physics_process(delta: float) -> void: func _input(event: InputEvent) -> void: if event.is_action_pressed("interact"): - if _attempting_build and Grid.get_location_data(interaction_location).is_buildable(): + var interact_data: CellData = Grid.get_location_data(interaction_location) + if _attempting_build and interact_data.is_buildable(): _build() - elif not _interacting and Grid.get_location_data(interaction_location).is_interactable(): - var interaction = INTERACTION_BAR.instantiate() - interaction.interaction_finished.connect(_on_interaction_finished) - interaction.grid_position = interaction_location - _interacting = true - add_sibling(interaction) + elif not _interacting and interact_data.is_interactable(): + if interact_data.has_resource(): + var interaction = INTERACTION_BAR.instantiate() + interaction.interaction_finished.connect(_on_interaction_finished) + interaction.grid_position = interaction_location + _interacting = true + add_sibling(interaction) + elif interact_data.has_building(): + _in_menu = interact_data.get_building().interact(_on_interaction_finished) if event.is_action_pressed("build"): var build_menu = BUILD_MENU.instantiate() build_menu.build.connect(_on_build_menu_build) add_sibling(build_menu) + _in_menu = true func _on_interaction_finished() -> void: _interacting = false + _in_menu = false func _on_build_menu_build(building: Building) -> void: print("Building: %s" % building.name) + _in_menu = false _attempting_build = building _build_placement = Sprite2D.new() _build_placement.texture = _attempting_build.atlas_texture @@ -66,8 +76,8 @@ func _on_build_menu_build(building: Building) -> void: func _build() -> void: ResourceManager.use_resources(_attempting_build.cost) var build: BuildingBase = BUILDING_BASE.instantiate() - build.initialize(_attempting_build, interaction_location) add_sibling(build) + build.initialize(_attempting_build, interaction_location) Grid.change_location_building(interaction_location, build) if not ResourceManager.has_resources(_attempting_build.cost): diff --git a/scripts/research_menu.gd b/scripts/research_menu.gd new file mode 100644 index 0000000..a7d9c6e --- /dev/null +++ b/scripts/research_menu.gd @@ -0,0 +1,53 @@ +extends CanvasLayer + +signal research(research: Research) + +var researches: Array[Research] = [] +var selected_research: Research + +const GEM_RESEARCH = preload("res://data/research/gem_research.tres") + +@onready var research_items: ItemList = %ResearchItems + +@onready var title: Label = %Title +@onready var description: Label = %Description +@onready var research_materials: GridContainer = %ResearchMaterials +@onready var research_buttons_container: HBoxContainer = %ResearchButtonsContainer + +func _ready() -> void: + _add_research(GEM_RESEARCH) + research_items.grab_focus() + +func _add_research(group: Research) -> void: + research_items.add_item(group.name, group.atlas_texture) + researches.append(group) + +func _on_research_selected(index: int) -> void: + selected_research = researches[index] + title.text = selected_research.name + description.text = selected_research.description + + for child in research_materials.get_children(): + child.queue_free() + for res: GameResource in selected_research.cost.keys(): + var image = TextureRect.new() + var texture = AtlasTexture.new() + texture.atlas = preload("res://assets/scifi_tilesheet@2.png") + texture.region = Rect2(64 * res.atlas_location, Vector2(64, 64)) + image.texture = texture + research_materials.add_child(image) + var label = Label.new() + label.text = str(selected_research.cost[res]) + if not ResourceManager.has_amount(res, selected_research.cost[res]): + label.add_theme_color_override("font_color", Color.RED) + research_materials.add_child(label) + + description.show() + research_materials.show() + research_buttons_container.show() + + +func _on_build_button_pressed() -> void: + if selected_research: + research.emit(selected_research) + queue_free() diff --git a/scripts/skills.gd b/scripts/skills.gd new file mode 100644 index 0000000..6e2f26c --- /dev/null +++ b/scripts/skills.gd @@ -0,0 +1,18 @@ +class_name Skills +extends Resource + +enum ABILITIES { HARVEST_GEM, CORRUPT_RESOURCE, CORRUPT_BUILDING } + +var _aquired_skills: Array[ABILITIES] = [] + +func aquire_skill(skill: ABILITIES) -> void: + _aquired_skills.append(skill) + +func has_skill(skill: ABILITIES) -> bool: + return _aquired_skills.has(skill) + +func has_skills(skills: Array[ABILITIES]) -> bool: + var _has_skills = true + for skill: ABILITIES in skills: + _has_skills = _has_skills and has_skill(skill) + return _has_skills