diff --git a/addons/block_code/simple_nodes/simple_character/simple_character.gd b/addons/block_code/simple_nodes/simple_character/simple_character.gd index 5b4de358..829186a0 100644 --- a/addons/block_code/simple_nodes/simple_character/simple_character.gd +++ b/addons/block_code/simple_nodes/simple_character/simple_character.gd @@ -13,6 +13,10 @@ func get_custom_class(): return "SimpleCharacter" +static func get_base_class(): + return "CharacterBody2D" + + static func get_exposed_properties() -> Array[String]: return ["position"] diff --git a/addons/block_code/types/types.gd b/addons/block_code/types/types.gd index 75162777..df75bc6b 100644 --- a/addons/block_code/types/types.gd +++ b/addons/block_code/types/types.gd @@ -35,6 +35,7 @@ const cast_relationships = [ [TYPE_FLOAT, TYPE_INT, "int(%s)"], [TYPE_INT, TYPE_STRING, "str(%s)"], [TYPE_FLOAT, TYPE_STRING, "str(%s)"], + [TYPE_COLOR, TYPE_STRING, "str(%s)"], ] # Directed graph, edges are CastGraphEdge @@ -169,3 +170,13 @@ class PriorityQueue: if found_pair: found_pair[1] = priority _sort() + + +# Global classes +class OptionData: + var selected: int + var items: Array + + func _init(p_items: Array = [], p_selected: int = 0): + items = p_items + selected = p_selected diff --git a/addons/block_code/ui/block_canvas/node_block_canvas/node_block_canvas.gd b/addons/block_code/ui/block_canvas/node_block_canvas/node_block_canvas.gd index a9784ada..04ebad21 100644 --- a/addons/block_code/ui/block_canvas/node_block_canvas/node_block_canvas.gd +++ b/addons/block_code/ui/block_canvas/node_block_canvas/node_block_canvas.gd @@ -24,6 +24,8 @@ func generate_script_from_current_window(script_inherits: String = ""): script += "var VAR_DICT := {}\n\n" + var init_func = InstructionTree.TreeNode.new("func _init():") + for entry_block in entry_blocks: script += entry_block.get_entry_statement() + "\n" @@ -39,4 +41,10 @@ func generate_script_from_current_window(script_inherits: String = ""): script += "\n" + if entry_block.signal_name: + init_func.add_child(InstructionTree.TreeNode.new("{0}.connect(_on_{0})".format([entry_block.signal_name]))) + + if init_func.children: + script += InstructionTree.new().generate_text(init_func) + return script diff --git a/addons/block_code/ui/blocks/control_block/control_block.gd b/addons/block_code/ui/blocks/control_block/control_block.gd index 23110ee9..825ecb05 100644 --- a/addons/block_code/ui/blocks/control_block/control_block.gd +++ b/addons/block_code/ui/blocks/control_block/control_block.gd @@ -6,6 +6,7 @@ const Constants = preload("res://addons/block_code/ui/constants.gd") @export var block_formats: Array = [] @export var statements: Array = [] +@export var defaults: Dictionary = {} var snaps: Array var param_name_input_pairs_array: Array @@ -25,7 +26,7 @@ func _ready(): if param_input_strings_array: for i in param_name_input_pairs_array.size(): for pair in param_name_input_pairs_array[i]: - pair[1].set_plain_text(param_input_strings_array[i][pair[0]]) + pair[1].set_raw_input(param_input_strings_array[i][pair[0]]) func _on_drag_drop_area_mouse_down(): @@ -68,14 +69,14 @@ func get_instruction_node() -> InstructionTree.TreeNode: func get_serialized_props() -> Array: var props := super() - props.append_array(serialize_props(["block_formats", "statements"])) + props.append_array(serialize_props(["block_formats", "statements", "defaults"])) var _param_input_strings_array = [] for param_name_input_pairs in param_name_input_pairs_array: var _param_input_strings: Dictionary = {} for pair in param_name_input_pairs: - _param_input_strings[pair[0]] = pair[1].get_plain_text() + _param_input_strings[pair[0]] = pair[1].get_raw_input() _param_input_strings_array.append(_param_input_strings) @@ -134,7 +135,7 @@ func format(): row_hbox.mouse_filter = Control.MOUSE_FILTER_IGNORE row_hbox_container.add_child(row_hbox) - param_name_input_pairs_array.append(StatementBlock.format_string(self, row_hbox, block_formats[i])) + param_name_input_pairs_array.append(StatementBlock.format_string(self, row_hbox, block_formats[i], defaults)) %Rows.add_child(row) diff --git a/addons/block_code/ui/blocks/entry_block/entry_block.gd b/addons/block_code/ui/blocks/entry_block/entry_block.gd index fc8b1d91..3b53937b 100644 --- a/addons/block_code/ui/blocks/entry_block/entry_block.gd +++ b/addons/block_code/ui/blocks/entry_block/entry_block.gd @@ -2,6 +2,9 @@ class_name EntryBlock extends StatementBlock +## if non-empty, this block defines a callback that will be connected to the signal with this name +@export var signal_name: String + func _ready(): block_type = Types.BlockType.ENTRY @@ -22,8 +25,10 @@ func get_entry_statement() -> String: for pair in param_name_input_pairs: formatted_statement = formatted_statement.replace("{%s}" % pair[0], pair[1].get_string()) - # One line, should not have \n - if formatted_statement.find("\n") != -1: - push_error("Entry block has multiline statement.") - return formatted_statement + + +func get_serialized_props() -> Array: + var props := super() + props.append_array(serialize_props(["signal_name"])) + return props diff --git a/addons/block_code/ui/blocks/entry_block/entry_block.tscn b/addons/block_code/ui/blocks/entry_block/entry_block.tscn index e1351721..62df8e41 100644 --- a/addons/block_code/ui/blocks/entry_block/entry_block.tscn +++ b/addons/block_code/ui/blocks/entry_block/entry_block.tscn @@ -5,9 +5,11 @@ [node name="EntryBlock" instance=ExtResource("1_byjbb")] script = ExtResource("2_3ik8h") +defaults = null block_name = "entry_block" label = "EntryBlock" block_type = 1 + [node name="Background" parent="VBoxContainer/TopMarginContainer" index="0"] show_top = false diff --git a/addons/block_code/ui/blocks/parameter_block/parameter_block.gd b/addons/block_code/ui/blocks/parameter_block/parameter_block.gd index 76b2be75..bb2ce232 100644 --- a/addons/block_code/ui/blocks/parameter_block/parameter_block.gd +++ b/addons/block_code/ui/blocks/parameter_block/parameter_block.gd @@ -5,6 +5,7 @@ extends Block @export var block_format: String = "" @export var statement: String = "" @export var variant_type: Variant.Type +@export var defaults: Dictionary = {} @onready var _panel := $Panel @onready var _hbox := %HBoxContainer @@ -26,7 +27,7 @@ func _ready(): if param_input_strings: for pair in param_name_input_pairs: - pair[1].set_plain_text(param_input_strings[pair[0]]) + pair[1].set_raw_input(param_input_strings[pair[0]]) func _on_drag_drop_area_mouse_down(): @@ -35,11 +36,11 @@ func _on_drag_drop_area_mouse_down(): func get_serialized_props() -> Array: var props := super() - props.append_array(serialize_props(["block_format", "statement"])) + props.append_array(serialize_props(["block_format", "statement", "defaults", "variant_type"])) var _param_input_strings: Dictionary = {} for pair in param_name_input_pairs: - _param_input_strings[pair[0]] = pair[1].get_plain_text() + _param_input_strings[pair[0]] = pair[1].get_raw_input() props.append(["param_input_strings", _param_input_strings]) @@ -65,4 +66,4 @@ static func get_scene_path(): func format(): - param_name_input_pairs = StatementBlock.format_string(self, %HBoxContainer, block_format) + param_name_input_pairs = StatementBlock.format_string(self, %HBoxContainer, block_format, defaults) diff --git a/addons/block_code/ui/blocks/parameter_block/parameter_block.tscn b/addons/block_code/ui/blocks/parameter_block/parameter_block.tscn index fa6ddb00..46cc8b65 100644 --- a/addons/block_code/ui/blocks/parameter_block/parameter_block.tscn +++ b/addons/block_code/ui/blocks/parameter_block/parameter_block.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://addons/block_code/ui/blocks/parameter_block/parameter_block.gd" id="1_0hajy"] [ext_resource type="PackedScene" uid="uid://c7puyxpqcq6xo" path="res://addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.tscn" id="2_gy5co"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0afbg"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dbera"] bg_color = Color(1, 1, 1, 1) border_width_left = 3 border_width_top = 3 @@ -19,6 +19,7 @@ offset_right = 16.0 offset_bottom = 8.0 size_flags_horizontal = 0 script = ExtResource("1_0hajy") +defaults = null block_name = "parameter_block" label = "Param" block_type = 3 @@ -26,7 +27,7 @@ block_type = 3 [node name="Panel" type="Panel" parent="."] unique_name_in_owner = true layout_mode = 2 -theme_override_styles/panel = SubResource("StyleBoxFlat_0afbg") +theme_override_styles/panel = SubResource("StyleBoxFlat_dbera") [node name="DragDropArea" parent="." instance=ExtResource("2_gy5co")] layout_mode = 2 diff --git a/addons/block_code/ui/blocks/statement_block/statement_block.gd b/addons/block_code/ui/blocks/statement_block/statement_block.gd index 3f7dff8b..f8d78e6b 100644 --- a/addons/block_code/ui/blocks/statement_block/statement_block.gd +++ b/addons/block_code/ui/blocks/statement_block/statement_block.gd @@ -4,6 +4,7 @@ extends Block @export var block_format: String = "" @export var statement: String = "" +@export var defaults: Dictionary = {} @onready var _background := %Background @onready var _hbox := %HBoxContainer @@ -23,7 +24,7 @@ func _ready(): if param_input_strings: for pair in param_name_input_pairs: - pair[1].set_plain_text(param_input_strings[pair[0]]) + pair[1].set_raw_input(param_input_strings[pair[0]]) func _on_drag_drop_area_mouse_down(): @@ -32,11 +33,11 @@ func _on_drag_drop_area_mouse_down(): func get_serialized_props() -> Array: var props := super() - props.append_array(serialize_props(["block_format", "statement"])) + props.append_array(serialize_props(["block_format", "statement", "defaults"])) var _param_input_strings: Dictionary = {} for pair in param_name_input_pairs: - _param_input_strings[pair[0]] = pair[1].get_plain_text() + _param_input_strings[pair[0]] = pair[1].get_raw_input() props.append(["param_input_strings", _param_input_strings]) return props @@ -75,10 +76,10 @@ func get_instruction_node() -> InstructionTree.TreeNode: func format(): - param_name_input_pairs = format_string(self, %HBoxContainer, block_format) + param_name_input_pairs = format_string(self, %HBoxContainer, block_format, defaults) -static func format_string(parent_block: Block, attach_to: Node, string: String) -> Array: +static func format_string(parent_block: Block, attach_to: Node, string: String, _defaults: Dictionary) -> Array: var _param_name_input_pairs = [] var regex = RegEx.new() regex.compile("\\[([^\\]]+)\\]|\\{([^}]+)\\}") # Capture things of format {test} or [test] @@ -100,15 +101,32 @@ static func format_string(parent_block: Block, attach_to: Node, string: String) var split := param.split(": ") var param_name := split[0] var param_type_str := split[1] - var param_type: Variant.Type = Types.STRING_TO_VARIANT_TYPE[param_type_str] + + var param_type = null + var option := false + if param_type_str == "OPTION": # Easy way to specify dropdown option + option = true + else: + param_type = Types.STRING_TO_VARIANT_TYPE[param_type_str] + + var param_default = null + if _defaults.has(param_name): + param_default = _defaults[param_name] var param_input: ParameterInput = preload("res://addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.tscn").instantiate() param_input.name = "ParameterInput%d" % start # Unique path param_input.placeholder = param_name - param_input.variant_type = param_type + if param_type: + param_input.variant_type = param_type + elif option: + param_input.option = true param_input.block = parent_block - param_input.text_modified.connect(func(): parent_block.modified.emit()) + param_input.modified.connect(func(): parent_block.modified.emit()) + attach_to.add_child(param_input) + if param_default: + param_input.set_raw_input(param_default) + _param_name_input_pairs.append([param_name, param_input]) if copy_block: diff --git a/addons/block_code/ui/blocks/statement_block/statement_block.tscn b/addons/block_code/ui/blocks/statement_block/statement_block.tscn index 64e814c7..70f0adfd 100644 --- a/addons/block_code/ui/blocks/statement_block/statement_block.tscn +++ b/addons/block_code/ui/blocks/statement_block/statement_block.tscn @@ -8,6 +8,7 @@ [node name="StatementBlock" type="MarginContainer"] size_flags_horizontal = 0 script = ExtResource("1_6wvlf") +defaults = null block_name = "statement_block" label = "StatementBlock" bottom_snap_path = NodePath("VBoxContainer/SnapPoint") diff --git a/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.gd b/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.gd index 8f846c76..fca1ed44 100644 --- a/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.gd +++ b/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.gd @@ -2,7 +2,7 @@ class_name ParameterInput extends MarginContainer -signal text_modified +signal modified @export var placeholder: String = "Parameter": set = _set_placeholder @@ -11,19 +11,72 @@ signal text_modified @export var variant_type: Variant.Type = TYPE_STRING @export var block_type: Types.BlockType = Types.BlockType.VALUE +var option: bool = false var block: Block -@onready var _line_edit := %LineEdit +@onready var _panel := %Panel @onready var snap_point := %SnapPoint +@onready var _input_switcher := %InputSwitcher +## Inputs +# Text +@onready var _text_input := %TextInput +@onready var _line_edit := %LineEdit +# Color +@onready var _color_input := %ColorInput +# Option Dropdown +@onready var _option_input := %OptionInput +# Vector2 +@onready var _vector2_input := %Vector2Input +@onready var _x_line_edit := %XLineEdit +@onready var _y_line_edit := %YLineEdit +# Bool +@onready var _bool_input := %BoolInput +@onready var _bool_input_option := %BoolInputOption + + +func set_raw_input(raw_input): + if option: + _panel.visible = false + _option_input.clear() + var option_data: Types.OptionData = raw_input as Types.OptionData + for item in option_data.items: + _option_input.add_item(item.capitalize()) + _option_input.select(option_data.selected) -func set_plain_text(new_text): - _line_edit.text = new_text - + return -func get_plain_text(): - return _line_edit.text + match variant_type: + TYPE_COLOR: + _color_input.color = raw_input + _update_panel_bg_color(raw_input) + TYPE_VECTOR2: + var split = raw_input.split(",") + _x_line_edit.text = split[0] + _y_line_edit.text = split[1] + TYPE_BOOL: + _bool_input_option.select(raw_input) + _: + _line_edit.text = raw_input + + +func get_raw_input(): + if option: + var options: Array = [] + for i in _option_input.item_count: + options.append(_option_input.get_item_text(i).to_snake_case()) + return Types.OptionData.new(options, _option_input.selected) + + match variant_type: + TYPE_COLOR: + return _color_input.color + TYPE_VECTOR2: + return _x_line_edit.text + "," + _y_line_edit.text + TYPE_BOOL: + return bool(_bool_input_option.selected) + _: + return _line_edit.text func _set_placeholder(new_placeholder: String) -> void: @@ -36,6 +89,9 @@ func _set_placeholder(new_placeholder: String) -> void: func _ready(): + var stylebox = _panel.get_theme_stylebox("panel") + stylebox.bg_color = Color.WHITE + _set_placeholder(placeholder) if block == null: @@ -44,7 +100,18 @@ func _ready(): snap_point.block_type = block_type snap_point.variant_type = variant_type - # Do something with block_type to restrict input + match variant_type: + TYPE_COLOR: + switch_input(_color_input) + TYPE_VECTOR2: + switch_input(_vector2_input) + TYPE_BOOL: + switch_input(_bool_input) + _: + switch_input(_text_input) + + if option: + switch_input(_option_input) func get_snapped_block() -> Block: @@ -55,17 +122,50 @@ func get_string() -> String: var snapped_block: ParameterBlock = get_snapped_block() as ParameterBlock if snapped_block: var generated_string = snapped_block.get_parameter_string() - return Types.cast(generated_string, snapped_block.variant_type, variant_type) + if Types.can_cast(snapped_block.variant_type, variant_type): + return Types.cast(generated_string, snapped_block.variant_type, variant_type) + else: + push_warning("No cast from %s to %s; using '%s' verbatim" % [snapped_block, variant_type, generated_string]) + return generated_string - var text: String = get_plain_text() + var input = get_raw_input() - if variant_type == TYPE_STRING: - text = "'%s'" % text.replace("\\", "\\\\").replace("'", "\\'") - elif variant_type == TYPE_VECTOR2: - text = "Vector2(%s)" % text + if option: + return _option_input.get_item_text(_option_input.selected).to_snake_case() - return text + match variant_type: + TYPE_STRING: + return "'%s'" % input.replace("\\", "\\\\").replace("'", "\\'") + TYPE_VECTOR2: + return "Vector2(%s)" % input + TYPE_COLOR: + return "Color%s" % str(input) + _: + return "%s" % input func _on_line_edit_text_changed(new_text): - text_modified.emit() + modified.emit() + + +func switch_input(node: Node): + for c in _input_switcher.get_children(): + c.visible = false + + node.visible = true + + +func _on_color_input_color_changed(color): + _update_panel_bg_color(color) + + modified.emit() + + +func _update_panel_bg_color(new_color): + var stylebox = _panel.get_theme_stylebox("panel").duplicate() + stylebox.bg_color = new_color + _panel.add_theme_stylebox_override("panel", stylebox) + + +func _on_option_input_item_selected(index): + modified.emit() diff --git a/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.tscn b/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.tscn index dbf69aff..62b1e38c 100644 --- a/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.tscn +++ b/addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=7 format=3 uid="uid://cjvxs6euc6xbm"] +[gd_scene load_steps=10 format=3 uid="uid://cjvxs6euc6xbm"] [ext_resource type="Script" path="res://addons/block_code/ui/blocks/utilities/parameter_input/parameter_input.gd" id="1_rgmxn"] [ext_resource type="PackedScene" uid="uid://b1oge52xhjqnu" path="res://addons/block_code/ui/blocks/utilities/snap_point/snap_point.tscn" id="2_6esp3"] @@ -16,6 +16,12 @@ corner_radius_bottom_left = 40 [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3r4mt"] +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_5hq7f"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_fjquj"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_e7f0k"] + [node name="ParameterInput" type="MarginContainer"] anchors_preset = 15 anchor_right = 1.0 @@ -28,19 +34,29 @@ mouse_filter = 2 script = ExtResource("1_rgmxn") [node name="Panel" type="Panel" parent="."] +unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_tn6h4") -[node name="MarginContainer" type="MarginContainer" parent="."] +[node name="InputSwitcher" type="MarginContainer" parent="."] +unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 +theme_override_constants/margin_left = 0 +theme_override_constants/margin_top = 0 +theme_override_constants/margin_right = 0 +theme_override_constants/margin_bottom = 0 + +[node name="TextInput" type="MarginContainer" parent="InputSwitcher"] +unique_name_in_owner = true +layout_mode = 2 theme_override_constants/margin_left = 8 theme_override_constants/margin_top = 4 theme_override_constants/margin_right = 8 theme_override_constants/margin_bottom = 4 -[node name="LineEdit" type="LineEdit" parent="MarginContainer"] +[node name="LineEdit" type="LineEdit" parent="InputSwitcher/TextInput"] unique_name_in_owner = true layout_mode = 2 mouse_filter = 1 @@ -54,10 +70,120 @@ theme_override_styles/read_only = SubResource("StyleBoxEmpty_3r4mt") placeholder_text = "Parameter" expand_to_text_length = true +[node name="ColorInput" type="ColorPickerButton" parent="InputSwitcher"] +unique_name_in_owner = true +visible = false +modulate = Color(1, 1, 1, 0) +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_5hq7f") + +[node name="OptionInput" type="OptionButton" parent="InputSwitcher"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_fjquj") + +[node name="Vector2Input" type="MarginContainer" parent="InputSwitcher"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 8 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="InputSwitcher/Vector2Input"] +layout_mode = 2 + +[node name="Control" type="Control" parent="InputSwitcher/Vector2Input/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="XLineEdit" type="LineEdit" parent="InputSwitcher/Vector2Input/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 1 +theme_override_colors/font_color = Color(0.118581, 0.118581, 0.118581, 1) +theme_override_colors/font_uneditable_color = Color(0.117647, 0.117647, 0.117647, 1) +theme_override_colors/font_placeholder_color = Color(0.76662, 0.76662, 0.76662, 1) +theme_override_constants/minimum_character_width = 0 +theme_override_styles/normal = SubResource("StyleBoxEmpty_6oowp") +theme_override_styles/focus = SubResource("StyleBoxEmpty_afyv2") +theme_override_styles/read_only = SubResource("StyleBoxEmpty_3r4mt") +placeholder_text = "x" +alignment = 1 +expand_to_text_length = true + +[node name="Control3" type="Control" parent="InputSwitcher/Vector2Input/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="CenterContainer" type="MarginContainer" parent="InputSwitcher/Vector2Input/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ColorRect" type="ColorRect" parent="InputSwitcher/Vector2Input/HBoxContainer/CenterContainer"] +custom_minimum_size = Vector2(2, 0) +layout_mode = 2 +size_flags_horizontal = 4 +color = Color(0.804743, 0.804743, 0.804743, 1) + +[node name="Control4" type="Control" parent="InputSwitcher/Vector2Input/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="YLineEdit" type="LineEdit" parent="InputSwitcher/Vector2Input/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 1 +theme_override_colors/font_color = Color(0.118581, 0.118581, 0.118581, 1) +theme_override_colors/font_uneditable_color = Color(0.117647, 0.117647, 0.117647, 1) +theme_override_colors/font_placeholder_color = Color(0.76662, 0.76662, 0.76662, 1) +theme_override_constants/minimum_character_width = 0 +theme_override_styles/normal = SubResource("StyleBoxEmpty_6oowp") +theme_override_styles/focus = SubResource("StyleBoxEmpty_afyv2") +theme_override_styles/read_only = SubResource("StyleBoxEmpty_3r4mt") +placeholder_text = "y" +alignment = 1 +expand_to_text_length = true + +[node name="Control2" type="Control" parent="InputSwitcher/Vector2Input/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="BoolInput" type="MarginContainer" parent="InputSwitcher"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/margin_left = 8 + +[node name="BoolInputOption" type="OptionButton" parent="InputSwitcher/BoolInput"] +unique_name_in_owner = true +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_focus_color = Color(0, 0, 0, 1) +theme_override_styles/focus = SubResource("StyleBoxEmpty_e7f0k") +theme_override_styles/normal = SubResource("StyleBoxEmpty_fjquj") +item_count = 2 +selected = 0 +popup/item_0/text = "False" +popup/item_0/id = 0 +popup/item_1/text = "True" +popup/item_1/id = 1 + [node name="SnapPoint" parent="." instance=ExtResource("2_6esp3")] unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 block_type = 3 +variant_type = 4 -[connection signal="text_changed" from="MarginContainer/LineEdit" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="InputSwitcher/TextInput/LineEdit" to="." method="_on_line_edit_text_changed"] +[connection signal="color_changed" from="InputSwitcher/ColorInput" to="." method="_on_color_input_color_changed"] +[connection signal="item_selected" from="InputSwitcher/OptionInput" to="." method="_on_option_input_item_selected"] +[connection signal="text_changed" from="InputSwitcher/Vector2Input/HBoxContainer/XLineEdit" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="InputSwitcher/Vector2Input/HBoxContainer/YLineEdit" to="." method="_on_line_edit_text_changed"] +[connection signal="item_selected" from="InputSwitcher/BoolInput/BoolInputOption" to="." method="_on_option_input_item_selected"] diff --git a/addons/block_code/ui/main_panel.gd b/addons/block_code/ui/main_panel.gd index 2fe61b1a..f478ada4 100644 --- a/addons/block_code/ui/main_panel.gd +++ b/addons/block_code/ui/main_panel.gd @@ -46,7 +46,8 @@ func switch_script(block_code_node: BlockCode): _picker.bsd_selected(bsd) _title_bar.bsd_selected(bsd) _block_canvas.bsd_selected(bsd) - block_code_tab.pressed.emit() + if block_code_node: + block_code_tab.pressed.emit() func save_script(): diff --git a/addons/block_code/ui/picker/categories/category_factory.gd b/addons/block_code/ui/picker/categories/category_factory.gd index 8a045ff7..ca7b5623 100644 --- a/addons/block_code/ui/picker/categories/category_factory.gd +++ b/addons/block_code/ui/picker/categories/category_factory.gd @@ -203,12 +203,12 @@ static func get_general_categories() -> Array[BlockCategory]: var logic_list: Array[Block] = [] - for op in ["==", ">", "<", ">=", "<=", "!="]: - b = BLOCKS["parameter_block"].instantiate() - b.variant_type = TYPE_BOOL - b.block_format = "{int1: INT} %s {int2: INT}" % op - b.statement = "({int1} %s {int2})" % op - logic_list.append(b) + b = BLOCKS["parameter_block"].instantiate() + b.variant_type = TYPE_BOOL + b.block_format = "{int1: INT} {op: OPTION} {int2: INT}" + b.statement = "({int1} {op} {int2})" + b.defaults = {"op": Types.OptionData.new(["==", ">", "<", ">=", "<=", "!="])} + logic_list.append(b) for op in ["and", "or"]: b = BLOCKS["parameter_block"].instantiate() @@ -312,7 +312,10 @@ static func category_from_property_list(property_list: Array, selected_props: Ar if selected_property == prop.name: found_prop = prop break - block_list.append_array(property_to_blocklist(found_prop)) + if found_prop: + block_list.append_array(property_to_blocklist(found_prop)) + else: + push_warning("No property matching %s found in %s" % [selected_property, property_list]) return BlockCategory.new(p_name, block_list, p_color) @@ -345,19 +348,36 @@ static func get_built_in_categories(_class_name: String) -> Array[BlockCategory] props = ["position", "rotation", "scale"] "CanvasItem": - props = ["modulate"] + props = ["modulate", "visible"] "RigidBody2D": - # On body entered + for verb in ["entered", "exited"]: + var b = BLOCKS["entry_block"].instantiate() + b.block_format = "On [body: NODE_PATH] %s" % [verb] + # HACK: Blocks refer to nodes by path but the callback receives the node itself; + # convert to path + b.statement = "func _on_body_%s(_body: Node):\n\tvar body: NodePath = _body.get_path()" % [verb] + b.signal_name = "body_%s" % [verb] + block_list.append(b) props = ["mass", "linear_velocity", "angular_velocity"] + "Area2D": + for verb in ["entered", "exited"]: + var b = BLOCKS["entry_block"].instantiate() + b.block_format = "On [body: NODE_PATH] %s" % [verb] + # HACK: Blocks refer to nodes by path but the callback receives the node itself; + # convert to path + b.statement = "func _on_body_%s(_body: Node2D):\n\tvar body: NodePath = _body.get_path()" % [verb] + b.signal_name = "body_%s" % [verb] + block_list.append(b) + var prop_list = ClassDB.class_get_property_list(_class_name, true) var class_cat: BlockCategory = category_from_property_list(prop_list, props, _class_name, Color.SLATE_GRAY) block_list.append_array(class_cat.block_list) class_cat.block_list = block_list - if props.size() > 0: + if block_list: cats.append(class_cat) return cats @@ -368,23 +388,11 @@ static func _get_input_blocks() -> Array[Block]: InputMap.load_from_project_settings() - for action: StringName in InputMap.get_actions(): - var block: Block = BLOCKS["parameter_block"].instantiate() - block.variant_type = TYPE_BOOL - block.block_format = "Is action %s pressed" % action - block.statement = 'Input.is_action_pressed("%s")' % action - block_list.append(block) - - block = BLOCKS["parameter_block"].instantiate() - block.variant_type = TYPE_BOOL - block.block_format = "Is action %s just pressed" % action - block.statement = 'Input.is_action_just_pressed("%s")' % action - block_list.append(block) - - block = BLOCKS["parameter_block"].instantiate() - block.variant_type = TYPE_BOOL - block.block_format = "Is action %s just released" % action - block.statement = 'Input.is_action_just_released("%s")' % action - block_list.append(block) + var block: Block = BLOCKS["parameter_block"].instantiate() + block.variant_type = TYPE_BOOL + block.block_format = "Is action {action_name: OPTION} {action: OPTION}" + block.statement = 'Input.is_action_{action}("{action_name}")' + block.defaults = {"action_name": Types.OptionData.new(InputMap.get_actions()), "action": Types.OptionData.new(["pressed", "just_pressed", "just_released"])} + block_list.append(block) return block_list diff --git a/addons/block_code/ui/picker/picker.gd b/addons/block_code/ui/picker/picker.gd index 4e79f2f5..99cfdf72 100644 --- a/addons/block_code/ui/picker/picker.gd +++ b/addons/block_code/ui/picker/picker.gd @@ -12,15 +12,26 @@ func bsd_selected(bsd: BlockScriptData): reset_picker() return + var categories_to_add: Array[BlockCategory] = [] + + var found_simple_class_script = null for class_dict in ProjectSettings.get_global_class_list(): if class_dict.class == bsd.script_inherits: var script = load(class_dict.path) if script.has_method("get_custom_blocks"): - init_picker(script.get_custom_blocks()) - return + categories_to_add = script.get_custom_blocks() + found_simple_class_script = script + break + + var parent_class: String + if found_simple_class_script: + parent_class = found_simple_class_script.get_base_class() + else: # Built in + parent_class = bsd.script_inherits + + categories_to_add.append_array(CategoryFactory.get_inherited_categories(parent_class)) - # Should be built-in class - init_picker(CategoryFactory.get_inherited_categories(bsd.script_inherits)) + init_picker(categories_to_add) func reset_picker(): diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md new file mode 100644 index 00000000..a55ea97b --- /dev/null +++ b/docs/OVERVIEW.md @@ -0,0 +1,108 @@ +# Block Programming Plugin + +## Data flow/Hierarchy + +1. The plugin enters at `block_code_plugin.gd` (`BlockCodePlugin`) where the `MainPanel` is initialized and added as a main screen tab labeled "Block Code". + * The `MainPanel` contains a block `Picker`, a `NodeBlockCanvas`, a `TitleBar`, and a `DragManager`. + * The `Picker` contains a combined list of categories of blocks from the global blocks generated in `CategoryFactory`, and custom blocks defined in the class the script is attached to. + * The `NodeBlockCanvas` is where placed blocks live, and generates the code from the scene tree of blocks. + * The `TitleBar` displays some information about the block script. + * The `DragManager` determines how blocks are snapped, and what happens when you drag a block from either the `Picker` or the `NodeBlockCanvas` + +2. When a `BlockCode` node is selected, the `MainPanel` supplies the `Picker`, `NodeBlockCanvas`, and `TitleBar` with the `BlockScriptData` (BSD) attached to the `BlockCode` node you clicked. + * The `Picker` will be loaded with blocks, and any custom blocks supplied by the parent of the `BlockCode` node you clicked. + * The `NodeBlockCanvas` will be populated with the block scene tree deserialized from the BSD (`BlockScriptData.block_trees`). + * The `TitleBar` will be populated with the name of class the script inherits. + * If the BSD is `null`, it will copy and load the default one. + +3. When you click and drag a block from the `Picker`, the `DragManager` is signaled to copy the block and enables you to drag it to the `NodeBlockCanvas`. + +4. The `DragManager` looks for the closest compatible snap point within a certain range, and if it exists, will show a preview where your `Block` will go if you drop it. + * Each `Block` has a `block_type` property, and so does each snap point. They must match to snap. + * If a `Block` has the block type `VALUE`, it should have a `variant_type` property (since it represents a value). + * If it is a `VALUE` block, it's `variant_type` must be able to cast to the snap point's `variant_type` in order to snap. This is determined by the cast graph in `types.gd`. + * Each `ParameterBlock` is a `VALUE` block which has this `variant_type` property. + +5. When the block script is modified, it will regenerate the BSD, and save it as an exported property of the `BlockCode` node you are working on. This way it gets saved to the scene. + * The block script is modified when a block is moved, or a `ParameterInput`'s raw input has been changed. + * Code is generated whenever you modify the block in `NodeBlockCanvas`. + +6. When you play the scene, each `BlockCode` node will attach a generated script from it's BSD to it's parent node. The `_ready` and `_process` methods will start normally. + +## How to create a new block + +Blocks are created from a template such as `StatementBlock`, `ParameterBlock`, `EntryBlock`, or `ControlBlock`. Blocks are defined programmatically and generated live, they are not saved permanently in any sort of class. +* `StatementBlock`: Generates a line of code to be executed. Has notches to snap other statement blocks to. Supports input parameters. +* `ParameterBlock`: Represents a value, or expression in some code. Can be used as input for any statement blocks, or other parameter blocks that also take input. +* `EntryBlock`: Generates a method header, entry point to block script. Also supports input parameters. +* `ControlBlock`: Dictates the flow of instruction, such as a loop or if/else statement. Can have multiple slots for calling different statements based on a conditional input. + +You can define a block: +1. Globally in `CategoryFactory.get_general_categories()` +2. For a specific built-in class in `CategoryFactory.get_built_in_categories()` +3. In a custom class by defining a method `static func get_custom_blocks() -> Array[BlockCategory]:`. See `SimpleCharacter` for an example. + +Here is an example of generating a `STATEMENT` block: +``` +# Instantiate the template block. In this case we use a StatementBlock since we want to generate a statement from some parameter inputs. +var b = BLOCKS["statement_block"].instantiate() + +# Define the block format string. This is how the block will be rendered, with some text and two typed inputs. +# Inputs are specified as {parameter_name: TYPE} +b.block_format = "Set Int {var: STRING} {value: INT}" + +# Define the statement that will be generated from the inputs. Use the parameter name to specify which goes where. +b.statement = "VAR_DICT[{var}] = {value}" +``` +You can then add this new block to a `BlockCategory` like so: +``` +var variable_list: Array[Block] = [] +variable_list.append(b) +var variable_category: BlockCategory = BlockCategory.new("Variables", variable_list, Color("4f975d")) +``` +Look in `CategoryFactory` for more examples! + +Note: For `EntryBlock`s, you can use the syntax `[param: TYPE]` with square brackets instead of curlies to generate a copyable `ParameterBlock` that can be used in the below method. +For example, to expose the `delta` argument in `func _on_process(delta):`, you would use the following format string: +``` +b.block_format = "On Process [delta: FLOAT]" +``` + +## What's inside? + +* `/addons/block_code/`: The main plugin folder. + * `block_code_plugin.gd`: The entry point to the editor plugin. Sets up the `MainPanel` scene which appears as a main screen tab at the top. + * `/block_code_node/`: Contains the script for our custom `BlockCode` node that allows you to attach block scripts to a node. You can instantiate it in the scene dock. + * `/block_script_data/`: Contains the custom resource that persists each block script. Each resource saves the class it inherits from, the generated script, and the tree of `Block`s to load. + * `/drag_manager/`: Should be in the `ui` folder, let's fix that. Handles dragging blocks across the canvas and picker. + * `/inspector_plugin/`: An inspector plugin that adds a button to the inspector on `BlockCode` nodes to open the script. + * `/instruction_tree/`: A utility script that generates code from a tree of `TreeNode`s. This tree is created by `NodeBlockCanvas` and also recursively by the `Block`s themselves. + * `/lib/`: Some scripts from GodotTours that label and expose some of the editor Control nodes so they can be interfaced with. + * `/simple_nodes/`: Contains scenes for simple nodes that can be used in a simple game. E.g. `SimpleCharacter` comes with a sprite and collision, and exposes custom movement blocks. + * `/types/`: Contains all things related to types in the addon. Types of blocks, casting, and variant to string type conversion. + * `/ui/`: Contains scenes and scripts for all UI related things. Block templates, `MainPanel`, etc. + * `main_panel.tscn`: Scene for the main addon window. Holds the `Picker`, `BlockCanvas`, `DragManager`, and `TitleBar` + * `constants.gd`: Contains various styling and functional constants for the addon. + * `/blocks/`: Folder that holds all of the scenes for block templates. + * `/block/`: The base class for all blocks. + * `/statement_block/`: A block that can generate statements from multiple inputs. + * `/parameter_block/`: A block that returns a value generated from multiple inputs. + * `/control_block/`: A block that dictates the control flow of the program based on conditions, like if/else. + * `/entry_block/`: A block that represents an entry point to the block script. `_ready`, `_process`, signals, etc. + * `/utilities/`: Folder containing utilities for various blocks. + * `/background/`: Generates a nice looking background with notches for blocks. + * `/drag_drop_area/`: A simple node that signals on mouse down and up + * `/parameter_input/`: An input for nodes to receive data from raw text, or from a snapped `ParameterBlock`. + * `/snap_point/`: Node that the `DragManager` looks for when trying to snap new blocks. + * `/block_canvas/`: Contains the code for the `BlockCanvas` which loads and holds blocks on a canvas. Inherited by `NodeBlockCanvas` which can generate scripts for block scripts made for Godot `Node`s (which is all of the scripts ATM). Also contains resources for serializing blocks. + * `/bsd_templates/`: Template block script data files for loading a default script. E.g. `_ready` and `_process` entry blocks already on canvas. + * `/node_canvas/`: Deprecated + * `/node_list/`: Deprecated + * `/picker/`: Contains the picker scene, and code that generates the blocks that populate the picker. + * `/categories/`: Contains `CategoryFactory` which generates the global block list programmatically from template blocks. + * `/title_bar/`: Contains the title bar which should display the name of the current script and the node it inherits from. +* `/addons/gut/`: Code for Godot unit tests. +* `/addons/plugin_refresher/`: A plugin used to refresh the block_code plugin without having to turn it on and off every time. +* `/test_game/`: Contains a test game scene with example block code scripts. + + diff --git a/pong_game/ball.tscn b/pong_game/ball.tscn index 2a640af9..600d2de0 100644 --- a/pong_game/ball.tscn +++ b/pong_game/ball.tscn @@ -20,6 +20,8 @@ continuous_cd = 1 max_contacts_reported = 1 contact_monitor = true linear_velocity = Vector2(353.553, 353.553) +linear_damp_mode = 1 +angular_damp_mode = 1 script = ExtResource("1_vdrab") [node name="CollisionShape2D" type="CollisionShape2D" parent="."]