commit d5591022f6df97640501e0c7dfdc8061feee0ba4 Author: Vivianne Langdon Date: Fri Oct 6 01:54:01 2023 -0700 initial project diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/addons/beehave/LICENSE b/addons/beehave/LICENSE new file mode 100644 index 0000000..caabbff --- /dev/null +++ b/addons/beehave/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 bitbrain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/beehave/blackboard.gd b/addons/beehave/blackboard.gd new file mode 100644 index 0000000..5655e89 --- /dev/null +++ b/addons/beehave/blackboard.gd @@ -0,0 +1,33 @@ +## The blackboard is an object that can be used to store and access data between +## multiple nodes of the behavior tree. +@icon("icons/blackboard.svg") +class_name Blackboard extends Node + +var blackboard: Dictionary = {} + +func keys() -> Array[String]: + var keys: Array[String] + keys.assign(blackboard.keys().duplicate()) + return keys + + +func set_value(key: Variant, value: Variant, blackboard_name: String = 'default') -> void: + if not blackboard.has(blackboard_name): + blackboard[blackboard_name] = {} + + blackboard[blackboard_name][key] = value + + +func get_value(key: Variant, default_value: Variant = null, blackboard_name: String = 'default') -> Variant: + if has_value(key, blackboard_name): + return blackboard[blackboard_name].get(key, default_value) + return default_value + + +func has_value(key: Variant, blackboard_name: String = 'default') -> bool: + return blackboard.has(blackboard_name) and blackboard[blackboard_name].has(key) and blackboard[blackboard_name][key] != null + + +func erase_value(key: Variant, blackboard_name: String = 'default') -> void: + if blackboard.has(blackboard_name): + blackboard[blackboard_name][key] = null diff --git a/addons/beehave/debug/debugger.gd b/addons/beehave/debug/debugger.gd new file mode 100644 index 0000000..1ccd097 --- /dev/null +++ b/addons/beehave/debug/debugger.gd @@ -0,0 +1,91 @@ +@tool +extends EditorDebuggerPlugin + +const DebuggerTab := preload("debugger_tab.gd") +var debugger_tab := DebuggerTab.new() +var floating_window: Window +var session: EditorDebuggerSession + + +func _has_capture(prefix: String) -> bool: + return prefix == "beehave" + + +func _capture(message: String, data: Array, session_id: int) -> bool: + # in case the behavior tree has invalid setup this might be null + if debugger_tab == null: + return false + + if message == "beehave:register_tree": + debugger_tab.register_tree(data[0]) + return true + if message == "beehave:unregister_tree": + debugger_tab.unregister_tree(data[0]) + return true + if message == "beehave:process_tick": + debugger_tab.graph.process_tick(data[0], data[1]) + return true + if message == "beehave:process_begin": + debugger_tab.graph.process_begin(data[0]) + return true + if message == "beehave:process_end": + debugger_tab.graph.process_end(data[0]) + return true + return false + + +func _setup_session(session_id: int) -> void: + session = get_session(session_id) + session.started.connect(debugger_tab.start) + session.stopped.connect(debugger_tab.stop) + + debugger_tab.name = "🐝 Beehave" + debugger_tab.make_floating.connect(_on_make_floating) + debugger_tab.session = session + session.add_session_tab(debugger_tab) + + +func _on_make_floating() -> void: + var plugin := BeehaveUtils.get_plugin() + if not plugin: + return + if floating_window: + _on_window_close_requested() + return + + var border_size := Vector2(4, 4) * BeehaveUtils.get_editor_scale() + var editor_interface: EditorInterface = plugin.get_editor_interface() + var editor_main_screen = editor_interface.get_editor_main_screen() + debugger_tab.get_parent().remove_child(debugger_tab) + + floating_window = Window.new() + + var panel := Panel.new() + panel.add_theme_stylebox_override("panel", editor_interface.get_base_control().get_theme_stylebox("PanelForeground", "EditorStyles")) + panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + floating_window.add_child(panel) + + var margin := MarginContainer.new() + margin.add_child(debugger_tab) + margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + margin.add_theme_constant_override("margin_right", border_size.x) + margin.add_theme_constant_override("margin_left", border_size.x) + margin.add_theme_constant_override("margin_top", border_size.y) + margin.add_theme_constant_override("margin_bottom", border_size.y) + panel.add_child(margin) + + floating_window.title = "🐝 Beehave" + floating_window.wrap_controls = true + floating_window.min_size = Vector2i(600, 350) + floating_window.size = debugger_tab.size + floating_window.position = editor_main_screen.global_position + floating_window.transient = true + floating_window.close_requested.connect(_on_window_close_requested) + editor_interface.get_base_control().add_child(floating_window) + + +func _on_window_close_requested() -> void: + debugger_tab.get_parent().remove_child(debugger_tab) + session.add_session_tab(debugger_tab) + floating_window.queue_free() + floating_window = null diff --git a/addons/beehave/debug/debugger_messages.gd b/addons/beehave/debug/debugger_messages.gd new file mode 100644 index 0000000..3fb49fb --- /dev/null +++ b/addons/beehave/debug/debugger_messages.gd @@ -0,0 +1,31 @@ +class_name BeehaveDebuggerMessages + + +static func can_send_message() -> bool: + return not Engine.is_editor_hint() and OS.has_feature("editor") + + +static func register_tree(beehave_tree: Dictionary) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:register_tree", [beehave_tree]) + + +static func unregister_tree(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:unregister_tree", [instance_id]) + + +static func process_tick(instance_id: int, status: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_tick", [instance_id, status]) + + +static func process_begin(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_begin", [instance_id]) + + +static func process_end(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_end", [instance_id]) + diff --git a/addons/beehave/debug/debugger_tab.gd b/addons/beehave/debug/debugger_tab.gd new file mode 100644 index 0000000..3987d77 --- /dev/null +++ b/addons/beehave/debug/debugger_tab.gd @@ -0,0 +1,107 @@ +@tool +extends PanelContainer + +signal make_floating() + +const BeehaveGraphEdit := preload("graph_edit.gd") +const TREE_ICON := preload("../icons/tree.svg") + +var container: HSplitContainer +var item_list: ItemList +var graph: BeehaveGraphEdit +var message: Label + +var active_trees: Dictionary +var active_tree_id: int = -1 +var session: EditorDebuggerSession + + +func _ready() -> void: + container = HSplitContainer.new() + add_child(container) + + item_list = ItemList.new() + item_list.custom_minimum_size = Vector2(200, 0) + item_list.item_selected.connect(_on_item_selected) + container.add_child(item_list) + + graph = BeehaveGraphEdit.new() + container.add_child(graph) + + message = Label.new() + message.text = "Run Project for debugging" + message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + message.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + message.set_anchors_preset(Control.PRESET_CENTER) + add_child(message) + + var button := Button.new() + button.flat = true + button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons") + button.pressed.connect(func(): make_floating.emit()) + button.tooltip_text = "Make floating" + button.focus_mode = Control.FOCUS_NONE + graph.get_zoom_hbox().add_child(button) + + var toggle_button := Button.new() + toggle_button.flat = true + toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons") + toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button)) + toggle_button.tooltip_text = "Toggle Panel" + toggle_button.focus_mode = Control.FOCUS_NONE + graph.get_zoom_hbox().add_child(toggle_button) + graph.get_zoom_hbox().move_child(toggle_button, 0) + + stop() + visibility_changed.connect(_on_visibility_changed) + + +func start() -> void: + container.visible = true + message.visible = false + + +func stop() -> void: + container.visible = false + message.visible = true + + active_trees.clear() + item_list.clear() + graph.beehave_tree = {} + + +func register_tree(data: Dictionary) -> void: + var idx := item_list.add_item(data.name, TREE_ICON) + item_list.set_item_tooltip(idx, data.path) + item_list.set_item_metadata(idx, data.id) + active_trees[data.id] = data + + +func unregister_tree(instance_id: int) -> void: + var id := str(instance_id) + for i in item_list.item_count: + if item_list.get_item_metadata(i) == id: + item_list.remove_item(i) + break + + active_trees.erase(id) + + if graph.beehave_tree.get("id", "") == id: + graph.beehave_tree = {} + + +func _on_toggle_button_pressed(toggle_button: Button) -> void: + item_list.visible = !item_list.visible + toggle_button.icon = get_theme_icon(&"Back" if item_list.visible else &"Forward", &"EditorIcons") + + +func _on_item_selected(idx: int) -> void: + var id: StringName = item_list.get_item_metadata(idx) + graph.beehave_tree = active_trees.get(id, {}) + + active_tree_id = id.to_int() + session.send_message("beehave:activate_tree", [active_tree_id]) + + +func _on_visibility_changed() -> void: + session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()]) diff --git a/addons/beehave/debug/frames.gd b/addons/beehave/debug/frames.gd new file mode 100644 index 0000000..0fb6d2a --- /dev/null +++ b/addons/beehave/debug/frames.gd @@ -0,0 +1,33 @@ +@tool +extends RefCounted + +const SUCCESS_COLOR := Color("#009944c8") +const NORMAL_COLOR := Color("#15181e") +const FAILURE_COLOR := Color("#cf000f80") +const RUNNING_COLOR := Color("#ffcc00c8") + +var empty: StyleBoxEmpty +var normal: StyleBoxFlat +var success: StyleBoxFlat +var failure: StyleBoxFlat +var running: StyleBoxFlat + + +func _init() -> void: + var plugin := BeehaveUtils.get_plugin() + if not plugin: + return + + var editor_scale := BeehaveUtils.get_editor_scale() + + empty = StyleBoxEmpty.new() + + normal = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"frame", &"GraphNode").duplicate() + + success = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"selected_frame", &"GraphNode").duplicate() + failure = success.duplicate() + running = success.duplicate() + + success.border_color = SUCCESS_COLOR + failure.border_color = FAILURE_COLOR + running.border_color = RUNNING_COLOR diff --git a/addons/beehave/debug/global_debugger.gd b/addons/beehave/debug/global_debugger.gd new file mode 100644 index 0000000..27c72c2 --- /dev/null +++ b/addons/beehave/debug/global_debugger.gd @@ -0,0 +1,38 @@ +extends Node + +var _registered_trees: Dictionary +var _active_tree: BeehaveTree + + +func _enter_tree() -> void: + EngineDebugger.register_message_capture("beehave", _on_debug_message) + + +func _on_debug_message(message: String, data: Array) -> bool: + if message == "activate_tree": + _set_active_tree(data[0]) + return true + if message == "visibility_changed": + if _active_tree: + _active_tree._can_send_message = data[0] + return true + return false + + +func _set_active_tree(tree_id: int) -> void: + var tree: BeehaveTree = _registered_trees.get(tree_id, null) + if not tree: + return + + if _active_tree: + _active_tree._can_send_message = false + _active_tree = tree + _active_tree._can_send_message = true + + +func register_tree(tree: BeehaveTree) -> void: + _registered_trees[tree.get_instance_id()] = tree + + +func unregister_tree(tree: BeehaveTree) -> void: + _registered_trees.erase(tree.get_instance_id()) diff --git a/addons/beehave/debug/graph_edit.gd b/addons/beehave/debug/graph_edit.gd new file mode 100644 index 0000000..afdcd1e --- /dev/null +++ b/addons/beehave/debug/graph_edit.gd @@ -0,0 +1,259 @@ +@tool +extends GraphEdit + +const BeehaveGraphNode := preload("graph_node.gd") + +const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg") +const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg") + +const PROGRESS_SHIFT: int = 50 +const INACTIVE_COLOR: Color = Color("#898989aa") +const ACTIVE_COLOR: Color = Color("#ffcc00c8") +const SUCCESS_COLOR: Color = Color("#009944c8") + + +var updating_graph: bool = false +var arraging_nodes: bool = false +var beehave_tree: Dictionary: + set(value): + if beehave_tree == value: + return + beehave_tree = value + active_nodes.clear() + _update_graph() + +var horizontal_layout: bool = false: + set(value): + if updating_graph or arraging_nodes: + return + if horizontal_layout == value: + return + horizontal_layout = value + _update_layout_button() + _update_graph() + + +var active_nodes: Array[String] +var progress: int = 0 +var layout_button: Button + + +func _ready() -> void: + custom_minimum_size = Vector2(100, 300) + arrange_nodes_button_hidden = true + minimap_enabled = false + + layout_button = Button.new() + layout_button.flat = true + layout_button.focus_mode = Control.FOCUS_NONE + layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout) + get_zoom_hbox().add_child(layout_button) + _update_layout_button() + + +func _update_graph() -> void: + if updating_graph: + return + + updating_graph = true + + clear_connections() + + for child in get_children(): + remove_child(child) + child.queue_free() + + if not beehave_tree.is_empty(): + _add_nodes(beehave_tree) + _connect_nodes(beehave_tree) + _arrange_nodes.call_deferred(beehave_tree) + + updating_graph = false + + +func _add_nodes(node: Dictionary) -> void: + if node.is_empty(): + return + var gnode := BeehaveGraphNode.new(horizontal_layout) + add_child(gnode) + gnode.title_text = node.name + gnode.name = node.id + gnode.icon = _get_icon(node.type.back()) + + if node.type.has(&"BeehaveTree"): + gnode.set_slots(false, true) + elif node.type.has(&"Leaf"): + gnode.set_slots(true, false) + elif node.type.has(&"Composite") or node.type.has(&"Decorator"): + gnode.set_slots(true, true) + + for child in node.get("children", []): + _add_nodes(child) + + +func _connect_nodes(node: Dictionary) -> void: + for child in node.get("children", []): + connect_node(node.id, 0, child.id, 0) + _connect_nodes(child) + + +func _arrange_nodes(node: Dictionary) -> void: + if arraging_nodes: + return + + arraging_nodes = true + + var tree_node := _create_tree_nodes(node) + tree_node.update_positions(horizontal_layout) + _place_nodes(tree_node) + + arraging_nodes = false + + +func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode: + var tree_node := TreeNode.new(get_node(node.id), root) + for child in node.get("children", []): + var child_node := _create_tree_nodes(child, tree_node) + tree_node.children.push_back(child_node) + return tree_node + + +func _place_nodes(node: TreeNode) -> void: + node.item.position_offset = Vector2(node.x, node.y) + for child in node.children: + _place_nodes(child) + + +func _get_icon(type: StringName) -> Texture2D: + var classes := ProjectSettings.get_global_class_list() + for c in classes: + if c["class"] == type: + var icon_path := c.get("icon", String()) + if not icon_path.is_empty(): + return load(icon_path) + return null + + +func get_status(status: int) -> String: + if status == 0: + return "SUCCESS" + elif status == 1: + return "FAILURE" + return "RUNNING" + + +func process_begin(instance_id: int) -> void: + if not _is_same_tree(instance_id): + return + + for child in get_children(): + child.set_meta("status", -1) + + +func process_tick(instance_id: int, status: int) -> void: + var node := get_node_or_null(str(instance_id)) + if node: + node.text = "Status: %s" % get_status(status) + node.set_status(status) + node.set_meta("status", status) + if status == 0 or status == 2: + if not active_nodes.has(node.name): + active_nodes.push_back(node.name) + + +func process_end(instance_id: int) -> void: + if not _is_same_tree(instance_id): + return + + for child in get_children(): + var status := child.get_meta("status", -1) + match status: + 0: + active_nodes.erase(child.name) + child.set_color(SUCCESS_COLOR) + 1: + active_nodes.erase(child.name) + child.set_color(INACTIVE_COLOR) + 2: + child.set_color(ACTIVE_COLOR) + _: + child.text = " " + child.set_status(status) + child.set_color(INACTIVE_COLOR) + + +func _is_same_tree(instance_id: int) -> bool: + return str(instance_id) == beehave_tree.get("id", "") + + +func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array: + var points: PackedVector2Array + + from_position = from_position.round() + to_position = to_position.round() + + points.push_back(from_position) + + var mid_position := ((to_position + from_position) / 2).round() + if horizontal_layout: + points.push_back(Vector2(mid_position.x, from_position.y)) + points.push_back(Vector2(mid_position.x, to_position.y)) + else: + points.push_back(Vector2(from_position.x, mid_position.y)) + points.push_back(Vector2(to_position.x, mid_position.y)) + + points.push_back(to_position) + + return points + + +func _process(delta: float) -> void: + if not active_nodes.is_empty(): + progress += 10 if delta >= 0.05 else 1 + if progress >= 1000: + progress = 0 + queue_redraw() + + +func _draw() -> void: + if active_nodes.is_empty(): + return + + var circle_size: float = max(3, 6 * zoom) + var progress_shift: float = PROGRESS_SHIFT * zoom + + var connections := get_connection_list() + for c in connections: + if not c.from in active_nodes or not c.to in active_nodes: + continue + var from := get_node(String(c.from)) + var to := get_node(String(c.to)) + + if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0: + return + + var line := _get_connection_line(from.position + from.get_connection_output_position(c.from_port), to.position + to.get_connection_input_position(c.to_port)) + + var curve = Curve2D.new() + for l in line: + curve.add_point(l) + + var max_steps := int(curve.get_baked_length()) + var current_shift := progress % max_steps + var p := curve.sample_baked(current_shift) + draw_circle(p, circle_size, ACTIVE_COLOR) + + var shift := current_shift - progress_shift + while shift >= 0: + draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) + shift -= progress_shift + + shift = current_shift + progress_shift + while shift <= curve.get_baked_length(): + draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) + shift += progress_shift + + +func _update_layout_button() -> void: + layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON + layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout" diff --git a/addons/beehave/debug/graph_node.gd b/addons/beehave/debug/graph_node.gd new file mode 100644 index 0000000..6b262af --- /dev/null +++ b/addons/beehave/debug/graph_node.gd @@ -0,0 +1,145 @@ +@tool +extends GraphNode + +const DEFAULT_COLOR := Color("#dad4cb") + +const PORT_TOP_ICON := preload("icons/port_top.svg") +const PORT_BOTTOM_ICON := preload("icons/port_bottom.svg") +const PORT_LEFT_ICON := preload("icons/port_left.svg") +const PORT_RIGHT_ICON := preload("icons/port_right.svg") + + +@export var title_text: String: + set(value): + title_text = value + if title_label: + title_label.text = value + +@export var text: String: + set(value): + text = value + if label: + label.text = " " if text.is_empty() else text + +@export var icon: Texture2D: + set(value): + icon = value + if icon_rect: + icon_rect.texture = value + +var layout_size: float: + get: + return size.y if horizontal else size.x + + +var panel: PanelContainer +var icon_rect: TextureRect +var title_label: Label +var container: VBoxContainer +var label: Label + +var frames: RefCounted = BeehaveUtils.get_frames() +var horizontal: bool = false + + +func _init(horizontal: bool = false) -> void: + self.horizontal = horizontal + + +func _ready() -> void: + custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale() + draggable = false + + add_theme_stylebox_override("frame", frames.empty) + add_theme_stylebox_override("selected_frame", frames.empty) + add_theme_color_override("close_color", Color.TRANSPARENT) + add_theme_icon_override("close", ImageTexture.new()) + + # For top port + add_child(Control.new()) + + panel = PanelContainer.new() + panel.mouse_filter = Control.MOUSE_FILTER_PASS + panel.add_theme_stylebox_override("panel", frames.normal) + add_child(panel) + + var vbox_container := VBoxContainer.new() + panel.add_child(vbox_container) + + var title_size := 24 * BeehaveUtils.get_editor_scale() + var margin_container := MarginContainer.new() + margin_container.add_theme_constant_override("margin_top", -title_size - 2 * BeehaveUtils.get_editor_scale()) + margin_container.mouse_filter = Control.MOUSE_FILTER_PASS + vbox_container.add_child(margin_container) + + var title_container := HBoxContainer.new() + title_container.add_child(Control.new()) + title_container.mouse_filter = Control.MOUSE_FILTER_PASS + title_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + margin_container.add_child(title_container) + + icon_rect = TextureRect.new() + icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + title_container.add_child(icon_rect) + + title_label = Label.new() + title_label.add_theme_color_override("font_color", DEFAULT_COLOR) + title_label.add_theme_font_override("font", get_theme_font("title_font")) + title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.text = title_text + title_container.add_child(title_label) + + title_container.add_child(Control.new()) + + container = VBoxContainer.new() + container.size_flags_vertical = Control.SIZE_EXPAND_FILL + container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.add_child(container) + + label = Label.new() + label.text = " " if text.is_empty() else text + container.add_child(label) + + # For bottom port + add_child(Control.new()) + + minimum_size_changed.connect(_on_size_changed) + _on_size_changed.call_deferred() + + +func set_status(status: int) -> void: + panel.add_theme_stylebox_override("panel", _get_stylebox(status)) + + +func _get_stylebox(status: int) -> StyleBox: + match status: + 0: return frames.success + 1: return frames.failure + 2: return frames.running + _: return frames.normal + + +func set_slots(left_enabled: bool, right_enabled: bool) -> void: + if horizontal: + set_slot(1, left_enabled, 0, Color.WHITE, right_enabled, 0, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON) + else: + set_slot(0, left_enabled, 0, Color.WHITE, false, -2, Color.TRANSPARENT, PORT_TOP_ICON, null) + set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, 0, Color.WHITE, null, PORT_BOTTOM_ICON) + + +func set_color(color: Color) -> void: + set_input_color(color) + set_output_color(color) + + +func set_input_color(color: Color) -> void: + set_slot_color_left(1 if horizontal else 0, color) + + +func set_output_color(color: Color) -> void: + set_slot_color_right(1 if horizontal else 2, color) + + +func _on_size_changed(): + add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x / 2.0)) diff --git a/addons/beehave/debug/icons/horizontal_layout.svg b/addons/beehave/debug/icons/horizontal_layout.svg new file mode 100644 index 0000000..235dc62 --- /dev/null +++ b/addons/beehave/debug/icons/horizontal_layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/horizontal_layout.svg.import b/addons/beehave/debug/icons/horizontal_layout.svg.import new file mode 100644 index 0000000..539e518 --- /dev/null +++ b/addons/beehave/debug/icons/horizontal_layout.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bah77esichnyx" +path="res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/horizontal_layout.svg" +dest_files=["res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/icons/port_bottom.svg b/addons/beehave/debug/icons/port_bottom.svg new file mode 100644 index 0000000..3ce70e7 --- /dev/null +++ b/addons/beehave/debug/icons/port_bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_bottom.svg.import b/addons/beehave/debug/icons/port_bottom.svg.import new file mode 100644 index 0000000..8845c5b --- /dev/null +++ b/addons/beehave/debug/icons/port_bottom.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://da3b236rjbqns" +path="res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_bottom.svg" +dest_files=["res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/icons/port_left.svg b/addons/beehave/debug/icons/port_left.svg new file mode 100644 index 0000000..c1f6717 --- /dev/null +++ b/addons/beehave/debug/icons/port_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_left.svg.import b/addons/beehave/debug/icons/port_left.svg.import new file mode 100644 index 0000000..7ea9827 --- /dev/null +++ b/addons/beehave/debug/icons/port_left.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnufc8p6spdtn" +path="res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_left.svg" +dest_files=["res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/icons/port_right.svg b/addons/beehave/debug/icons/port_right.svg new file mode 100644 index 0000000..2560af5 --- /dev/null +++ b/addons/beehave/debug/icons/port_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_right.svg.import b/addons/beehave/debug/icons/port_right.svg.import new file mode 100644 index 0000000..20931cd --- /dev/null +++ b/addons/beehave/debug/icons/port_right.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bbmd6vk23ympm" +path="res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_right.svg" +dest_files=["res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/icons/port_top.svg b/addons/beehave/debug/icons/port_top.svg new file mode 100644 index 0000000..d3b99e1 --- /dev/null +++ b/addons/beehave/debug/icons/port_top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_top.svg.import b/addons/beehave/debug/icons/port_top.svg.import new file mode 100644 index 0000000..dec7820 --- /dev/null +++ b/addons/beehave/debug/icons/port_top.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bw8wmxdfom8eh" +path="res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_top.svg" +dest_files=["res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/icons/vertical_layout.svg b/addons/beehave/debug/icons/vertical_layout.svg new file mode 100644 index 0000000..d59ffb0 --- /dev/null +++ b/addons/beehave/debug/icons/vertical_layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/vertical_layout.svg.import b/addons/beehave/debug/icons/vertical_layout.svg.import new file mode 100644 index 0000000..8ddcfca --- /dev/null +++ b/addons/beehave/debug/icons/vertical_layout.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bpyxu6i1dx5qh" +path="res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/vertical_layout.svg" +dest_files=["res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/debug/tree_node.gd b/addons/beehave/debug/tree_node.gd new file mode 100644 index 0000000..6c104d8 --- /dev/null +++ b/addons/beehave/debug/tree_node.gd @@ -0,0 +1,255 @@ +class_name TreeNode +extends RefCounted + +# Based on https://rachel53461.wordpress.com/2014/04/20/algorithm-for-drawing-trees/ + +const SIBLING_DISTANCE: float = 20.0 +const LEVEL_DISTANCE: float = 40.0 + +var x: float +var y: float +var mod: float +var parent: TreeNode +var children: Array[TreeNode] + +var item: GraphNode + + +func _init(p_item: GraphNode = null, p_parent: TreeNode = null) -> void: + parent = p_parent + item = p_item + + +func is_leaf() -> bool: + return children.is_empty() + + +func is_most_left() -> bool: + if not parent: + return true + return parent.children.front() == self + + +func is_most_right() -> bool: + if not parent: + return true + return parent.children.back() == self + + +func get_previous_sibling() -> TreeNode: + if not parent or is_most_left(): + return null + return parent.children[parent.children.find(self) - 1] + + +func get_next_sibling() -> TreeNode: + if not parent or is_most_right(): + return null + return parent.children[parent.children.find(self) + 1] + + +func get_most_left_sibling() -> TreeNode: + if not parent: + return null + + if is_most_left(): + return self + + return parent.children.front() + + +func get_most_left_child() -> TreeNode: + if children.is_empty(): + return null + return children.front() + + +func get_most_right_child() -> TreeNode: + if children.is_empty(): + return null + return children.back() + + +func update_positions(horizontally: bool = false) -> void: + _initialize_nodes(self, 0) + _calculate_initial_x(self) + + _check_all_children_on_screen(self) + _calculate_final_positions(self, 0) + + if horizontally: + _swap_x_y(self) + _calculate_x(self, 0) + else: + _calculate_y(self, 0) + + +func _initialize_nodes(node: TreeNode, depth: int) -> void: + node.x = -1 + node.y = depth + node.mod = 0 + + for child in node.children: + _initialize_nodes(child, depth + 1) + + +func _calculate_initial_x(node: TreeNode) -> void: + for child in node.children: + _calculate_initial_x(child) + if node.is_leaf(): + if not node.is_most_left(): + node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE + else: + node.x = 0 + else: + var mid: float + if node.children.size() == 1: + var offset: float = (node.children.front().item.layout_size - node.item.layout_size) / 2 + mid = node.children.front().x + offset + else: + var left_child := node.get_most_left_child() + var right_child := node.get_most_right_child() + mid = (left_child.x + right_child.x + right_child.item.layout_size - node.item.layout_size) / 2 + + if node.is_most_left(): + node.x = mid + else: + node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE + node.mod = node.x - mid + + if not node.is_leaf() and not node.is_most_left(): + _check_for_conflicts(node) + + +func _calculate_final_positions(node: TreeNode, mod_sum: float) -> void: + node.x += mod_sum + mod_sum += node.mod + + for child in node.children: + _calculate_final_positions(child, mod_sum) + + +func _check_all_children_on_screen(node: TreeNode) -> void: + var node_contour: Dictionary = {} + _get_left_contour(node, 0, node_contour) + + var shift_amount: float = 0 + for y in node_contour.keys(): + if node_contour[y] + shift_amount < 0: + shift_amount = (node_contour[y] * -1) + + if shift_amount > 0: + node.x += shift_amount + node.mod += shift_amount + + +func _check_for_conflicts(node: TreeNode) -> void: + var min_distance := SIBLING_DISTANCE + var shift_value: float = 0 + var shift_sibling: TreeNode = null + + var node_contour: Dictionary = {}# { int, float } + _get_left_contour(node, 0, node_contour) + + var sibling := node.get_most_left_sibling() + while sibling != null and sibling != node: + var sibling_contour: Dictionary = {} + _get_right_contour(sibling, 0, sibling_contour) + + for level in range(node.y + 1, min(sibling_contour.keys().max(), node_contour.keys().max()) + 1): + var distance: float = node_contour[level] - sibling_contour[level] + if distance + shift_value < min_distance: + shift_value = min_distance - distance + shift_sibling = sibling + + sibling = sibling.get_next_sibling() + + if shift_value > 0: + node.x += shift_value + node.mod += shift_value + _center_nodes_between(shift_sibling, node) + + +func _center_nodes_between(left_node: TreeNode, right_node: TreeNode) -> void: + var left_index := left_node.parent.children.find(left_node) + var right_index := left_node.parent.children.find(right_node) + + var num_nodes_between: int = (right_index - left_index) - 1 + if num_nodes_between > 0: + # The extra distance that needs to be split into num_nodes_between + 1 + # in order to find the new node spacing so that nodes are equally spaced + var distance_to_allocate: float = right_node.x - left_node.x - left_node.item.layout_size + # Subtract sizes on nodes in between + for i in range(left_index + 1, right_index): + distance_to_allocate -= left_node.parent.children[i].item.layout_size + # Divide space equally + var distance_between_nodes: float = distance_to_allocate / (num_nodes_between + 1) + + var prev_node := left_node + var middle_node := left_node.get_next_sibling() + while middle_node != right_node: + var desire_x: float = prev_node.x + prev_node.item.layout_size + distance_between_nodes + var offset := desire_x - middle_node.x + middle_node.x += offset + middle_node.mod += offset + prev_node = middle_node + middle_node = middle_node.get_next_sibling() + + +func _get_left_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void: + var node_left: float = node.x + mod_sum + var depth := int(node.y) + if not values.has(depth): + values[depth] = node_left + else: + values[depth] = min(values[depth], node_left) + + mod_sum += node.mod + for child in node.children: + _get_left_contour(child, mod_sum, values) + + +func _get_right_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void: + var node_right: float = node.x + mod_sum + node.item.layout_size + var depth := int(node.y) + if not values.has(depth): + values[depth] = node_right + else: + values[depth] = max(values[depth], node_right) + + mod_sum += node.mod + for child in node.children: + _get_right_contour(child, mod_sum, values) + + +func _swap_x_y(node: TreeNode) -> void: + for child in node.children: + _swap_x_y(child) + + var temp := node.x + node.x = node.y + node.y = temp + + +func _calculate_x(node: TreeNode, offset: int) -> void: + node.x = offset + var sibling := node.get_most_left_sibling() + var max_size: int = node.item.size.x + while sibling != null: + max_size = max(sibling.item.size.x, max_size) + sibling = sibling.get_next_sibling() + + for child in node.children: + _calculate_x(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale()) + + +func _calculate_y(node: TreeNode, offset: int) -> void: + node.y = offset + var sibling := node.get_most_left_sibling() + var max_size: int = node.item.size.y + while sibling != null: + max_size = max(sibling.item.size.y, max_size) + sibling = sibling.get_next_sibling() + + for child in node.children: + _calculate_y(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale()) diff --git a/addons/beehave/icons/action.svg b/addons/beehave/icons/action.svg new file mode 100644 index 0000000..3916c89 --- /dev/null +++ b/addons/beehave/icons/action.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/action.svg.import b/addons/beehave/icons/action.svg.import new file mode 100644 index 0000000..cf8a612 --- /dev/null +++ b/addons/beehave/icons/action.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btrq8e0kyxthg" +path="res://.godot/imported/action.svg-e8a91246d0ba9ba3cf84290d65648f06.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/action.svg" +dest_files=["res://.godot/imported/action.svg-e8a91246d0ba9ba3cf84290d65648f06.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/blackboard.svg b/addons/beehave/icons/blackboard.svg new file mode 100644 index 0000000..e4948a5 --- /dev/null +++ b/addons/beehave/icons/blackboard.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/blackboard.svg.import b/addons/beehave/icons/blackboard.svg.import new file mode 100644 index 0000000..4650058 --- /dev/null +++ b/addons/beehave/icons/blackboard.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dw7rom0hiff6c" +path="res://.godot/imported/blackboard.svg-18d4dfd4f6de558de250b67251ff1e69.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/blackboard.svg" +dest_files=["res://.godot/imported/blackboard.svg-18d4dfd4f6de558de250b67251ff1e69.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/category_bt.svg b/addons/beehave/icons/category_bt.svg new file mode 100644 index 0000000..8be61ae --- /dev/null +++ b/addons/beehave/icons/category_bt.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_bt.svg.import b/addons/beehave/icons/category_bt.svg.import new file mode 100644 index 0000000..c11e4f2 --- /dev/null +++ b/addons/beehave/icons/category_bt.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qpdd6ue7x82h" +path="res://.godot/imported/category_bt.svg-8537bebd1c5f62dca3d7ee7f17efeed4.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_bt.svg" +dest_files=["res://.godot/imported/category_bt.svg-8537bebd1c5f62dca3d7ee7f17efeed4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/category_composite.svg b/addons/beehave/icons/category_composite.svg new file mode 100644 index 0000000..aa8b866 --- /dev/null +++ b/addons/beehave/icons/category_composite.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_composite.svg.import b/addons/beehave/icons/category_composite.svg.import new file mode 100644 index 0000000..0496273 --- /dev/null +++ b/addons/beehave/icons/category_composite.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://863s568sneja" +path="res://.godot/imported/category_composite.svg-43f66e63a7ccfa5ac8ec6da0583b3246.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_composite.svg" +dest_files=["res://.godot/imported/category_composite.svg-43f66e63a7ccfa5ac8ec6da0583b3246.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/category_decorator.svg b/addons/beehave/icons/category_decorator.svg new file mode 100644 index 0000000..165e3d6 --- /dev/null +++ b/addons/beehave/icons/category_decorator.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_decorator.svg.import b/addons/beehave/icons/category_decorator.svg.import new file mode 100644 index 0000000..492f32e --- /dev/null +++ b/addons/beehave/icons/category_decorator.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2ie8m4ddawlb" +path="res://.godot/imported/category_decorator.svg-79d598d6456f32724156248e09d6eaf3.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_decorator.svg" +dest_files=["res://.godot/imported/category_decorator.svg-79d598d6456f32724156248e09d6eaf3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/category_leaf.svg b/addons/beehave/icons/category_leaf.svg new file mode 100644 index 0000000..1482fe6 --- /dev/null +++ b/addons/beehave/icons/category_leaf.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_leaf.svg.import b/addons/beehave/icons/category_leaf.svg.import new file mode 100644 index 0000000..4ef9604 --- /dev/null +++ b/addons/beehave/icons/category_leaf.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://eq0sp4g3s75r" +path="res://.godot/imported/category_leaf.svg-c740ecab6cfae632574ca5e39e46fd2e.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_leaf.svg" +dest_files=["res://.godot/imported/category_leaf.svg-c740ecab6cfae632574ca5e39e46fd2e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/condition.svg b/addons/beehave/icons/condition.svg new file mode 100644 index 0000000..37b2c7a --- /dev/null +++ b/addons/beehave/icons/condition.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/condition.svg.import b/addons/beehave/icons/condition.svg.import new file mode 100644 index 0000000..ef59099 --- /dev/null +++ b/addons/beehave/icons/condition.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ck4toqx0nggiu" +path="res://.godot/imported/condition.svg-57892684b10a64086f68c09c388b17e5.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/condition.svg" +dest_files=["res://.godot/imported/condition.svg-57892684b10a64086f68c09c388b17e5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/failer.svg b/addons/beehave/icons/failer.svg new file mode 100644 index 0000000..968f7e1 --- /dev/null +++ b/addons/beehave/icons/failer.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/failer.svg.import b/addons/beehave/icons/failer.svg.import new file mode 100644 index 0000000..989b556 --- /dev/null +++ b/addons/beehave/icons/failer.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2fj7htaqvcud" +path="res://.godot/imported/failer.svg-9a62b840e1eacc0437e7a67b14a302e4.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/failer.svg" +dest_files=["res://.godot/imported/failer.svg-9a62b840e1eacc0437e7a67b14a302e4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/inverter.svg b/addons/beehave/icons/inverter.svg new file mode 100644 index 0000000..d4e791e --- /dev/null +++ b/addons/beehave/icons/inverter.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/inverter.svg.import b/addons/beehave/icons/inverter.svg.import new file mode 100644 index 0000000..e9050a8 --- /dev/null +++ b/addons/beehave/icons/inverter.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cffmoc3og8hux" +path="res://.godot/imported/inverter.svg-1f1b976d95de42c4ad99a92fa9a6c5d0.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/inverter.svg" +dest_files=["res://.godot/imported/inverter.svg-1f1b976d95de42c4ad99a92fa9a6c5d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/limiter.svg b/addons/beehave/icons/limiter.svg new file mode 100644 index 0000000..7b3fa1d --- /dev/null +++ b/addons/beehave/icons/limiter.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/limiter.svg.import b/addons/beehave/icons/limiter.svg.import new file mode 100644 index 0000000..7b56b08 --- /dev/null +++ b/addons/beehave/icons/limiter.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7akxvsg0f2by" +path="res://.godot/imported/limiter.svg-b4c7646605c46f53c5e403fe21d8f584.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/limiter.svg" +dest_files=["res://.godot/imported/limiter.svg-b4c7646605c46f53c5e403fe21d8f584.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/selector.svg b/addons/beehave/icons/selector.svg new file mode 100644 index 0000000..0ae3b7a --- /dev/null +++ b/addons/beehave/icons/selector.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/selector.svg.import b/addons/beehave/icons/selector.svg.import new file mode 100644 index 0000000..ef7326d --- /dev/null +++ b/addons/beehave/icons/selector.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2c5d20doh4sp" +path="res://.godot/imported/selector.svg-78bccfc448bd1676b5a29bfde4b08e5b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector.svg" +dest_files=["res://.godot/imported/selector.svg-78bccfc448bd1676b5a29bfde4b08e5b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/selector_random.svg b/addons/beehave/icons/selector_random.svg new file mode 100644 index 0000000..6f631e9 --- /dev/null +++ b/addons/beehave/icons/selector_random.svg @@ -0,0 +1,35 @@ + + diff --git a/addons/beehave/icons/selector_random.svg.import b/addons/beehave/icons/selector_random.svg.import new file mode 100644 index 0000000..6306f76 --- /dev/null +++ b/addons/beehave/icons/selector_random.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmnkcmk7bkdjd" +path="res://.godot/imported/selector_random.svg-d52fea1352c24483ecd9dc8609cf00f3.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector_random.svg" +dest_files=["res://.godot/imported/selector_random.svg-d52fea1352c24483ecd9dc8609cf00f3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/selector_reactive.svg b/addons/beehave/icons/selector_reactive.svg new file mode 100644 index 0000000..6db005f --- /dev/null +++ b/addons/beehave/icons/selector_reactive.svg @@ -0,0 +1,45 @@ + + diff --git a/addons/beehave/icons/selector_reactive.svg.import b/addons/beehave/icons/selector_reactive.svg.import new file mode 100644 index 0000000..12a8c5b --- /dev/null +++ b/addons/beehave/icons/selector_reactive.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://crkbov0h8sb8l" +path="res://.godot/imported/selector_reactive.svg-dd3b8fb8cd2ffe331605aaad1e021cc0.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector_reactive.svg" +dest_files=["res://.godot/imported/selector_reactive.svg-dd3b8fb8cd2ffe331605aaad1e021cc0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/sequence.svg b/addons/beehave/icons/sequence.svg new file mode 100644 index 0000000..3ebedd9 --- /dev/null +++ b/addons/beehave/icons/sequence.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/sequence.svg.import b/addons/beehave/icons/sequence.svg.import new file mode 100644 index 0000000..5dadbe2 --- /dev/null +++ b/addons/beehave/icons/sequence.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c5gw354thiofm" +path="res://.godot/imported/sequence.svg-76e5600611900cc81e9ec286977b8c6a.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence.svg" +dest_files=["res://.godot/imported/sequence.svg-76e5600611900cc81e9ec286977b8c6a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/sequence_random.svg b/addons/beehave/icons/sequence_random.svg new file mode 100644 index 0000000..34e4a12 --- /dev/null +++ b/addons/beehave/icons/sequence_random.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/sequence_random.svg.import b/addons/beehave/icons/sequence_random.svg.import new file mode 100644 index 0000000..3907462 --- /dev/null +++ b/addons/beehave/icons/sequence_random.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bat8ptdw5qt1d" +path="res://.godot/imported/sequence_random.svg-58cee9098c622ef87db941279206422a.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence_random.svg" +dest_files=["res://.godot/imported/sequence_random.svg-58cee9098c622ef87db941279206422a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/sequence_reactive.svg b/addons/beehave/icons/sequence_reactive.svg new file mode 100644 index 0000000..33d219b --- /dev/null +++ b/addons/beehave/icons/sequence_reactive.svg @@ -0,0 +1,60 @@ + + diff --git a/addons/beehave/icons/sequence_reactive.svg.import b/addons/beehave/icons/sequence_reactive.svg.import new file mode 100644 index 0000000..ab0fa25 --- /dev/null +++ b/addons/beehave/icons/sequence_reactive.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rmiu1slwfkh7" +path="res://.godot/imported/sequence_reactive.svg-7d384ca290f7934adb9e17d9e7116b6c.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence_reactive.svg" +dest_files=["res://.godot/imported/sequence_reactive.svg-7d384ca290f7934adb9e17d9e7116b6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/succeeder.svg b/addons/beehave/icons/succeeder.svg new file mode 100644 index 0000000..10f5912 --- /dev/null +++ b/addons/beehave/icons/succeeder.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/succeeder.svg.import b/addons/beehave/icons/succeeder.svg.import new file mode 100644 index 0000000..0cb7334 --- /dev/null +++ b/addons/beehave/icons/succeeder.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dl6wo332kglbe" +path="res://.godot/imported/succeeder.svg-e5cf6f6e04b9b862b82fd2cb479272aa.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/succeeder.svg" +dest_files=["res://.godot/imported/succeeder.svg-e5cf6f6e04b9b862b82fd2cb479272aa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/icons/tree.svg b/addons/beehave/icons/tree.svg new file mode 100644 index 0000000..6c85ea1 --- /dev/null +++ b/addons/beehave/icons/tree.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/tree.svg.import b/addons/beehave/icons/tree.svg.import new file mode 100644 index 0000000..9ac0308 --- /dev/null +++ b/addons/beehave/icons/tree.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://deryyg2hbmaaw" +path="res://.godot/imported/tree.svg-c0b20ed88b2fe300c0296f7236049076.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/tree.svg" +dest_files=["res://.godot/imported/tree.svg-c0b20ed88b2fe300c0296f7236049076.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/beehave/metrics/beehave_global_metrics.gd b/addons/beehave/metrics/beehave_global_metrics.gd new file mode 100644 index 0000000..31c734b --- /dev/null +++ b/addons/beehave/metrics/beehave_global_metrics.gd @@ -0,0 +1,54 @@ +extends Node + +var _tree_count: int = 0 +var _active_tree_count: int = 0 +var _registered_trees: Array[BeehaveTree] = [] + + +func _enter_tree() -> void: + Performance.add_custom_monitor("beehave/total_trees", _get_total_trees) + Performance.add_custom_monitor("beehave/total_enabled_trees", _get_total_enabled_trees) + + +func register_tree(tree: BeehaveTree) -> void: + if _registered_trees.has(tree): + return + + _registered_trees.append(tree) + _tree_count += 1 + + if tree.enabled: + _active_tree_count += 1 + + tree.tree_enabled.connect(_on_tree_enabled) + tree.tree_disabled.connect(_on_tree_disabled) + + +func unregister_tree(tree: BeehaveTree) -> void: + if not _registered_trees.has(tree): + return + + _registered_trees.erase(tree) + _tree_count -= 1 + + if tree.enabled: + _active_tree_count -= 1 + + tree.tree_enabled.disconnect(_on_tree_enabled) + tree.tree_disabled.disconnect(_on_tree_disabled) + + +func _get_total_trees() -> int: + return _tree_count + + +func _get_total_enabled_trees() -> int: + return _active_tree_count + + +func _on_tree_enabled() -> void: + _active_tree_count += 1 + + +func _on_tree_disabled() -> void: + _active_tree_count -= 1 diff --git a/addons/beehave/nodes/beehave_node.gd b/addons/beehave/nodes/beehave_node.gd new file mode 100644 index 0000000..0c94b8c --- /dev/null +++ b/addons/beehave/nodes/beehave_node.gd @@ -0,0 +1,49 @@ +## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or +## `RUNNING` when ticked. +@tool +class_name BeehaveNode extends Node + +enum { + SUCCESS, + FAILURE, + RUNNING +} + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings: PackedStringArray = [] + + if get_children().any(func(x): return not (x is BeehaveNode)): + warnings.append("All children of this node should inherit from BeehaveNode class.") + + return warnings + + +## Executes this node and returns a status code. +## This method must be overwritten. +func tick(actor: Node, blackboard: Blackboard) -> int: + return SUCCESS + + +## Called when this node needs to be interrupted before it can return FAILURE or SUCCESS. +func interrupt(actor: Node, blackboard: Blackboard) -> void: + pass + + +## Called before the first time it ticks by the parent. +func before_run(actor: Node, blackboard: Blackboard) -> void: + pass + + +## Called after the last time it ticks and returns +## [code]SUCCESS[/code] or [code]FAILURE[/code]. +func after_run(actor: Node, blackboard: Blackboard) -> void: + pass + + +func get_class_name() -> Array[StringName]: + return [&"BeehaveNode"] + + +func can_send_message(blackboard: Blackboard) -> bool: + return blackboard.get_value("can_send_message", false) diff --git a/addons/beehave/nodes/beehave_tree.gd b/addons/beehave/nodes/beehave_tree.gd new file mode 100644 index 0000000..7a8d145 --- /dev/null +++ b/addons/beehave/nodes/beehave_tree.gd @@ -0,0 +1,224 @@ +## Controls the flow of execution of the entire behavior tree. +@tool +@icon("../icons/tree.svg") +class_name BeehaveTree extends Node + +enum { + SUCCESS, + FAILURE, + RUNNING +} + +signal tree_enabled +signal tree_disabled + +## Wether this behavior tree should be enabled or not. +@export var enabled: bool = true: + set(value): + enabled = value + set_physics_process(enabled) + + if value: + tree_enabled.emit() + else: + interrupt() + tree_disabled.emit() + + get: + return enabled + +## An optional node path this behavior tree should apply to. +@export_node_path var actor_node_path : NodePath + +## Custom blackboard node. An internal blackboard will be used +## if no blackboard is provided explicitly. +@export var blackboard:Blackboard: + set(b): + blackboard = b + if blackboard and _internal_blackboard: + remove_child(_internal_blackboard) + _internal_blackboard.free() + _internal_blackboard = null + elif not blackboard and not _internal_blackboard: + _internal_blackboard = Blackboard.new() + add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) + get: + return blackboard if blackboard else _internal_blackboard + +## When enabled, this tree is tracked individually +## as a custom monitor. +@export var custom_monitor = false: + set(b): + custom_monitor = b + if custom_monitor and _process_time_metric_name != '': + Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value) + BeehaveGlobalMetrics.register_tree(self) + else: + if _process_time_metric_name != '': + # Remove tree metric from the engine + Performance.remove_custom_monitor(_process_time_metric_name) + BeehaveGlobalMetrics.unregister_tree(self) + + BeehaveDebuggerMessages.unregister_tree(get_instance_id()) + +var actor : Node +var status : int = -1 + +var _internal_blackboard: Blackboard +var _process_time_metric_name : String +var _process_time_metric_value : float = 0.0 +var _can_send_message: bool = false + +func _ready() -> void: + if Engine.is_editor_hint(): + return + + if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode: + push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()]) + disable() + return + + if not blackboard: + _internal_blackboard = Blackboard.new() + add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) + + actor = get_parent() + if actor_node_path: + actor = get_node(actor_node_path) + + # Get the name of the parent node name for metric + var parent_name = actor.name + _process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [parent_name, get_instance_id()] + + # Register custom metric to the engine + if custom_monitor: + Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value) + BeehaveGlobalMetrics.register_tree(self) + + set_physics_process(enabled) + BeehaveGlobalDebugger.register_tree(self) + BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)) + + +func _physics_process(delta: float) -> void: + if Engine.is_editor_hint(): + return + + # Start timing for metric + var start_time = Time.get_ticks_usec() + + blackboard.set_value("can_send_message", _can_send_message) + + if _can_send_message: + BeehaveDebuggerMessages.process_begin(get_instance_id()) + + if self.get_child_count() == 1: + tick() + + if _can_send_message: + BeehaveDebuggerMessages.process_end(get_instance_id()) + + # Check the cost for this frame and save it for metric report + _process_time_metric_value = Time.get_ticks_usec() - start_time + + +func tick() -> int: + var child := self.get_child(0) + if status != RUNNING: + child.before_run(actor, blackboard) + + status = child.tick(actor, blackboard) + if _can_send_message: + BeehaveDebuggerMessages.process_tick(child.get_instance_id(), status) + BeehaveDebuggerMessages.process_tick(get_instance_id(), status) + + # Clear running action if nothing is running + if status != RUNNING: + blackboard.set_value("running_action", null, str(actor.get_instance_id())) + child.after_run(actor, blackboard) + + return status + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings:PackedStringArray = [] + + if get_children().any(func(x): return not (x is BeehaveNode)): + warnings.append("All children of this node should inherit from BeehaveNode class.") + + if get_child_count() != 1: + warnings.append("BeehaveTree should have exactly one child node.") + + return warnings + + +## Returns the currently running action +func get_running_action() -> ActionLeaf: + return blackboard.get_value("running_action", null, str(actor.get_instance_id())) + + +## Returns the last condition that was executed +func get_last_condition() -> ConditionLeaf: + return blackboard.get_value("last_condition", null, str(actor.get_instance_id())) + + +## Returns the status of the last executed condition +func get_last_condition_status() -> String: + if blackboard.has_value("last_condition_status", str(actor.get_instance_id())): + var status = blackboard.get_value("last_condition_status", null, str(actor.get_instance_id())) + if status == SUCCESS: + return "SUCCESS" + elif status == FAILURE: + return "FAILURE" + else: + return "RUNNING" + return "" + +## interrupts this tree if anything was running +func interrupt() -> void: + if self.get_child_count() != 0: + var first_child = self.get_child(0) + if "interrupt" in first_child: + first_child.interrupt(actor, blackboard) + + +## Enables this tree. +func enable() -> void: + self.enabled = true + + +## Disables this tree. +func disable() -> void: + self.enabled = false + + +func _exit_tree() -> void: + if custom_monitor: + if _process_time_metric_name != '': + # Remove tree metric from the engine + Performance.remove_custom_monitor(_process_time_metric_name) + BeehaveGlobalMetrics.unregister_tree(self) + + BeehaveDebuggerMessages.unregister_tree(get_instance_id()) + + +# Called by the engine to profile this tree +func _get_process_time_metric_value() -> int: + return _process_time_metric_value + + +func _get_debugger_data(node: Node) -> Dictionary: + if not node is BeehaveTree and not node is BeehaveNode: + return {} + var data := { path = node.get_path(), name = node.name, type = node.get_class_name(), id = str(node.get_instance_id()) } + if node.get_child_count() > 0: + data.children = [] + for child in node.get_children(): + var child_data := _get_debugger_data(child) + if not child_data.is_empty(): + data.children.push_back(child_data) + return data + + +func get_class_name() -> Array[StringName]: + return [&"BeehaveTree"] diff --git a/addons/beehave/nodes/composites/composite.gd b/addons/beehave/nodes/composites/composite.gd new file mode 100644 index 0000000..b959eea --- /dev/null +++ b/addons/beehave/nodes/composites/composite.gd @@ -0,0 +1,40 @@ +## A Composite node controls the flow of execution of its children in a specific manner. +@tool +@icon("../../icons/category_composite.svg") +class_name Composite extends BeehaveNode + + +var running_child: BeehaveNode = null + + +func _ready(): + if Engine.is_editor_hint(): + return + + if self.get_child_count() < 1: + push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()]) + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings: PackedStringArray = super._get_configuration_warnings() + + if get_children().filter(func(x): return x is BeehaveNode).size() < 2: + warnings.append("Any composite node should have at least two children. Otherwise it is not useful.") + + return warnings + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + if running_child != null: + running_child.interrupt(actor, blackboard) + running_child = null + + +func after_run(actor: Node, blackboard: Blackboard) -> void: + running_child = null + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"Composite") + return classes diff --git a/addons/beehave/nodes/composites/randomized_composite.gd b/addons/beehave/nodes/composites/randomized_composite.gd new file mode 100644 index 0000000..0eb62c6 --- /dev/null +++ b/addons/beehave/nodes/composites/randomized_composite.gd @@ -0,0 +1,152 @@ +@tool +class_name RandomizedComposite extends Composite + +const WEIGHTS_PREFIX = "Weights/" + +## Sets a predicable seed +@export var random_seed: int = 0: + set(rs): + random_seed = rs + if random_seed != 0: + seed(random_seed) + else: + randomize() + +## Wether to use weights for every child or not. +@export var use_weights: bool: + set(value): + use_weights = value + if use_weights: + _update_weights(get_children()) + _connect_children_changing_signals() + notify_property_list_changed() + +var _weights: Dictionary + + +func _ready(): + _connect_children_changing_signals() + + +func _connect_children_changing_signals(): + if not child_entered_tree.is_connected(_on_child_entered_tree): + child_entered_tree.connect(_on_child_entered_tree) + + if not child_exiting_tree.is_connected(_on_child_exiting_tree): + child_exiting_tree.connect(_on_child_exiting_tree) + + +func get_shuffled_children() -> Array[Node]: + var children_bag: Array[Node] = get_children().duplicate() + if use_weights: + var weights: Array[int] + weights.assign(children_bag.map(func (child): return _weights[child.name])) + children_bag.assign(_weighted_shuffle(children_bag, weights)) + else: + children_bag.shuffle() + return children_bag + + +## Returns a shuffled version of a given array using the supplied array of weights. +## Think of weights as the chance of a given item being the first in the array. +func _weighted_shuffle(items: Array, weights: Array[int]) -> Array: + if len(items) != len(weights): + push_error("items and weights size mismatch: expected %d weights, got %d instead." % [len(items), len(weights)]) + return items + + # This method is based on the weighted random sampling algorithm + # by Efraimidis, Spirakis; 2005. This runs in O(n log(n)). + + # For each index, it will calculate random_value^(1/weight). + var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])] + var random_distribuition = range(len(items)).map(chance_calc) + + # Now we just have to order by the calculated value, descending. + random_distribuition.sort_custom(func(a, b): return a[1] > b[1]) + + return random_distribuition.map(func(dist): return items[dist[0]]) + + +func _get_property_list(): + var properties = [] + + if use_weights: + for key in _weights.keys(): + properties.append({ + "name": WEIGHTS_PREFIX + key, + "type": TYPE_INT, + "usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR, + "hint": PROPERTY_HINT_RANGE, + "hint_string": "1,100" + }) + + return properties + + +func _set(property: StringName, value: Variant) -> bool: + if property.begins_with(WEIGHTS_PREFIX): + var weight_name = property.trim_prefix(WEIGHTS_PREFIX) + _weights[weight_name] = value + return true + + return false + + +func _get(property: StringName): + if property.begins_with(WEIGHTS_PREFIX): + var weight_name = property.trim_prefix(WEIGHTS_PREFIX) + return _weights[weight_name] + + return null + + +func _update_weights(children: Array[Node]) -> void: + var new_weights = {} + for c in children: + if _weights.has(c.name): + new_weights[c.name] = _weights[c.name] + else: + new_weights[c.name] = 1 + _weights = new_weights + notify_property_list_changed() + + +func _on_child_entered_tree(node: Node): + _update_weights(get_children()) + + var renamed_callable = _on_child_renamed.bind(node.name, node) + if not node.renamed.is_connected(renamed_callable): + node.renamed.connect(renamed_callable) + + +func _on_child_exiting_tree(node: Node): + var renamed_callable = _on_child_renamed.bind(node.name, node) + if node.renamed.is_connected(renamed_callable): + node.renamed.disconnect(renamed_callable) + + var children = get_children() + children.erase(node) + _update_weights(children) + + +func _on_child_renamed(old_name: String, renamed_child: Node): + if old_name == renamed_child.name: + return # No need to update the weights. + + # Disconnect signal with old name... + renamed_child.renamed\ + .disconnect(_on_child_renamed.bind(old_name, renamed_child)) + # ...and connect with the new name. + renamed_child.renamed\ + .connect(_on_child_renamed.bind(renamed_child.name, renamed_child)) + + var original_weight = _weights[old_name] + _weights.erase(old_name) + _weights[renamed_child.name] = original_weight + notify_property_list_changed() + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"RandomizedComposite") + return classes diff --git a/addons/beehave/nodes/composites/selector.gd b/addons/beehave/nodes/composites/selector.gd new file mode 100644 index 0000000..eca71ac --- /dev/null +++ b/addons/beehave/nodes/composites/selector.gd @@ -0,0 +1,69 @@ +## Selector nodes will attempt to execute each of its children until one of +## them return `SUCCESS`. If all children return `FAILURE`, this node will also +## return `FAILURE`. +## If a child returns `RUNNING` it will tick again. +@tool +@icon("../../icons/selector.svg") +class_name SelectorComposite extends Composite + + +var last_execution_index: int = 0 + + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + if c.get_index() < last_execution_index: + continue + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + _cleanup_running_task(c, actor, blackboard) + c.after_run(actor, blackboard) + return SUCCESS + FAILURE: + _cleanup_running_task(c, actor, blackboard) + last_execution_index += 1 + c.after_run(actor, blackboard) + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + + return FAILURE + + +func after_run(actor: Node, blackboard: Blackboard) -> void: + last_execution_index = 0 + super(actor, blackboard) + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + last_execution_index = 0 + super(actor, blackboard) + + +## Changes `running_action` and `running_child` after the node finishes executing. +func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard): + var blackboard_name = str(actor.get_instance_id()) + if finished_action == running_child: + running_child = null + if finished_action == blackboard.get_value("running_action", null, blackboard_name): + blackboard.set_value("running_action", null, blackboard_name) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SelectorComposite") + return classes diff --git a/addons/beehave/nodes/composites/selector_random.gd b/addons/beehave/nodes/composites/selector_random.gd new file mode 100644 index 0000000..b780b5f --- /dev/null +++ b/addons/beehave/nodes/composites/selector_random.gd @@ -0,0 +1,80 @@ +## This node will attempt to execute all of its children just like a +## [code]SelectorStar[/code] would, with the exception that the children +## will be executed in a random order. +@tool +@icon("../../icons/selector_random.svg") +class_name SelectorRandomComposite extends RandomizedComposite + +## A shuffled list of the children that will be executed in reverse order. +var _children_bag: Array[Node] = [] +var c: Node + +func _ready() -> void: + super() + if random_seed == 0: + randomize() + + +func tick(actor: Node, blackboard: Blackboard) -> int: + if _children_bag.is_empty(): + _reset() + + # We need to traverse the array in reverse since we will be manipulating it. + for i in _get_reversed_indexes(): + c = _children_bag[i] + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + _children_bag.erase(c) + c.after_run(actor, blackboard) + return SUCCESS + FAILURE: + _children_bag.erase(c) + c.after_run(actor, blackboard) + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + + return FAILURE + + +func after_run(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + + +func _get_reversed_indexes() -> Array[int]: + var reversed: Array[int] + reversed.assign(range(_children_bag.size())) + reversed.reverse() + return reversed + + +func _reset() -> void: + var new_order = get_shuffled_children() + _children_bag = new_order.duplicate() + _children_bag.reverse() # It needs to run the children in reverse order. + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SelectorRandomComposite") + return classes diff --git a/addons/beehave/nodes/composites/selector_reactive.gd b/addons/beehave/nodes/composites/selector_reactive.gd new file mode 100644 index 0000000..869ef8a --- /dev/null +++ b/addons/beehave/nodes/composites/selector_reactive.gd @@ -0,0 +1,45 @@ +## Selector Reactive nodes will attempt to execute each of its children until one of +## them return `SUCCESS`. If all children return `FAILURE`, this node will also +## return `FAILURE`. +## If a child returns `RUNNING` it will restart. +@tool +@icon("../../icons/selector_reactive.svg") +class_name SelectorReactiveComposite extends Composite + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + # Interrupt any child that was RUNNING before. + if c != running_child: + interrupt(actor, blackboard) + c.after_run(actor, blackboard) + return SUCCESS + FAILURE: + c.after_run(actor, blackboard) + RUNNING: + if c != running_child: + interrupt(actor, blackboard) + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + + return FAILURE + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SelectorReactiveComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence.gd b/addons/beehave/nodes/composites/sequence.gd new file mode 100644 index 0000000..a39494f --- /dev/null +++ b/addons/beehave/nodes/composites/sequence.gd @@ -0,0 +1,76 @@ +## Sequence nodes will attempt to execute all of its children and report +## `SUCCESS` in case all of the children report a `SUCCESS` status code. +## If at least one child reports a `FAILURE` status code, this node will also +## return `FAILURE` and restart. +## In case a child returns `RUNNING` this node will tick again. +@tool +@icon("../../icons/sequence.svg") +class_name SequenceComposite extends Composite + + +var successful_index: int = 0 + + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + + if c.get_index() < successful_index: + continue + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + _cleanup_running_task(c, actor, blackboard) + successful_index += 1 + c.after_run(actor, blackboard) + FAILURE: + _cleanup_running_task(c, actor, blackboard) + # Interrupt any child that was RUNNING before. + interrupt(actor, blackboard) + c.after_run(actor, blackboard) + return FAILURE + RUNNING: + if c != running_child: + if running_child != null: + running_child.interrupt(actor, blackboard) + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + + _reset() + return SUCCESS + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + + +func _reset() -> void: + successful_index = 0 + + +## Changes `running_action` and `running_child` after the node finishes executing. +func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard): + var blackboard_name = str(actor.get_instance_id()) + if finished_action == running_child: + running_child = null + if finished_action == blackboard.get_value("running_action", null, blackboard_name): + blackboard.set_value("running_action", null, blackboard_name) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence_random.gd b/addons/beehave/nodes/composites/sequence_random.gd new file mode 100644 index 0000000..ab8eb78 --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_random.gd @@ -0,0 +1,92 @@ +## This node will attempt to execute all of its children just like a +## [code]SequenceStar[/code] would, with the exception that the children +## will be executed in a random order. +@tool +@icon("../../icons/sequence_random.svg") +class_name SequenceRandomComposite extends RandomizedComposite + +# Emitted whenever the children are shuffled. +signal reset(new_order: Array[Node]) + +## Whether the sequence should start where it left off after a previous failure. +@export var resume_on_failure: bool = false +## Whether the sequence should start where it left off after a previous interruption. +@export var resume_on_interrupt: bool = false + +## A shuffled list of the children that will be executed in reverse order. +var _children_bag: Array[Node] = [] +var c: Node + + +func _ready() -> void: + super() + if random_seed == 0: + randomize() + + +func tick(actor: Node, blackboard: Blackboard) -> int: + if _children_bag.is_empty(): + _reset() + + # We need to traverse the array in reverse since we will be manipulating it. + for i in _get_reversed_indexes(): + c = _children_bag[i] + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + _children_bag.erase(c) + c.after_run(actor, blackboard) + FAILURE: + _children_bag.erase(c) + c.after_run(actor, blackboard) + return FAILURE + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + + return SUCCESS + + +func after_run(actor: Node, blackboard: Blackboard) -> void: + if not resume_on_failure: + _reset() + super(actor, blackboard) + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + if not resume_on_interrupt: + _reset() + super(actor, blackboard) + + +func _get_reversed_indexes() -> Array[int]: + var reversed: Array[int] + reversed.assign(range(_children_bag.size())) + reversed.reverse() + return reversed + + +func _reset() -> void: + var new_order = get_shuffled_children() + _children_bag = new_order.duplicate() + _children_bag.reverse() # It needs to run the children in reverse order. + reset.emit(new_order) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceRandomComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence_reactive.gd b/addons/beehave/nodes/composites/sequence_reactive.gd new file mode 100644 index 0000000..50e8b90 --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_reactive.gd @@ -0,0 +1,62 @@ +## Reactive Sequence nodes will attempt to execute all of its children and report +## `SUCCESS` in case all of the children report a `SUCCESS` status code. +## If at least one child reports a `FAILURE` status code, this node will also +## return `FAILURE` and restart. +## In case a child returns `RUNNING` this node will restart. +@tool +@icon("../../icons/sequence_reactive.svg") +class_name SequenceReactiveComposite extends Composite + + +var successful_index: int = 0 + + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + if c.get_index() < successful_index: + continue + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + successful_index += 1 + c.after_run(actor, blackboard) + FAILURE: + # Interrupt any child that was RUNNING before. + interrupt(actor, blackboard) + c.after_run(actor, blackboard) + return FAILURE + RUNNING: + _reset() + if running_child != c: + interrupt(actor, blackboard) + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + _reset() + return SUCCESS + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + +func _reset() -> void: + successful_index = 0 + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceReactiveComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence_star.gd b/addons/beehave/nodes/composites/sequence_star.gd new file mode 100644 index 0000000..42ceb07 --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_star.gd @@ -0,0 +1,58 @@ +## Sequence Star nodes will attempt to execute all of its children and report +## `SUCCESS` in case all of the children report a `SUCCESS` status code. +## If at least one child reports a `FAILURE` status code, this node will also +## return `FAILURE` and tick again. +## In case a child returns `RUNNING` this node will restart. +@tool +@icon("../../icons/sequence_reactive.svg") +class_name SequenceStarComposite extends Composite + + +var successful_index: int = 0 + + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + if c.get_index() < successful_index: + continue + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + successful_index += 1 + c.after_run(actor, blackboard) + FAILURE: + c.after_run(actor, blackboard) + return FAILURE + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + _reset() + return SUCCESS + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + + +func _reset() -> void: + successful_index = 0 + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceStarComposite") + return classes diff --git a/addons/beehave/nodes/decorators/decorator.gd b/addons/beehave/nodes/decorators/decorator.gd new file mode 100644 index 0000000..8cc3944 --- /dev/null +++ b/addons/beehave/nodes/decorators/decorator.gd @@ -0,0 +1,41 @@ +## Decorator nodes are used to transform the result received by its child. +## Must only have one child. +@tool +@icon("../../icons/category_decorator.svg") +class_name Decorator extends BeehaveNode + + +var running_child: BeehaveNode = null + + +func _ready(): + if Engine.is_editor_hint(): + return + + if self.get_child_count() != 1: + push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()]) + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings: PackedStringArray = super._get_configuration_warnings() + + if get_child_count() != 1: + warnings.append("Decorator should have exactly one child node.") + + return warnings + + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + if running_child != null: + running_child.interrupt(actor, blackboard) + running_child = null + + +func after_run(actor: Node, blackboard: Blackboard) -> void: + running_child = null + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"Decorator") + return classes diff --git a/addons/beehave/nodes/decorators/failer.gd b/addons/beehave/nodes/decorators/failer.gd new file mode 100644 index 0000000..4a818ed --- /dev/null +++ b/addons/beehave/nodes/decorators/failer.gd @@ -0,0 +1,34 @@ +## A Failer node will always return a `FAILURE` status code. +@tool +@icon("../../icons/failer.svg") +class_name AlwaysFailDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if response == RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + else: + c.after_run(actor, blackboard) + return FAILURE + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"AlwaysFailDecorator") + return classes diff --git a/addons/beehave/nodes/decorators/inverter.gd b/addons/beehave/nodes/decorators/inverter.gd new file mode 100644 index 0000000..16e3f36 --- /dev/null +++ b/addons/beehave/nodes/decorators/inverter.gd @@ -0,0 +1,42 @@ +## An inverter will return `FAILURE` in case it's child returns a `SUCCESS` status +## code or `SUCCESS` in case its child returns a `FAILURE` status code. +@tool +@icon("../../icons/inverter.svg") +class_name InverterDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + match response: + SUCCESS: + c.after_run(actor, blackboard) + return FAILURE + FAILURE: + c.after_run(actor, blackboard) + return SUCCESS + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + _: + push_error("This should be unreachable") + return -1 + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"InverterDecorator") + return classes diff --git a/addons/beehave/nodes/decorators/limiter.gd b/addons/beehave/nodes/decorators/limiter.gd new file mode 100644 index 0000000..72e7356 --- /dev/null +++ b/addons/beehave/nodes/decorators/limiter.gd @@ -0,0 +1,41 @@ +## The limiter will execute its child `x` amount of times. When the number of +## maximum ticks is reached, it will return a `FAILURE` status code. +@tool +@icon("../../icons/limiter.svg") +class_name LimiterDecorator extends Decorator + +@onready var cache_key = 'limiter_%s' % self.get_instance_id() + +@export var max_count : float = 0 + +func tick(actor: Node, blackboard: Blackboard) -> int: + var child = self.get_child(0) + var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id())) + + if current_count == 0: + child.before_run(actor, blackboard) + + if current_count < max_count: + blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id())) + var response = child.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response) + + if child is ConditionLeaf: + blackboard.set_value("last_condition", child, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if child is ActionLeaf and response == RUNNING: + running_child = child + blackboard.set_value("running_action", child, str(actor.get_instance_id())) + + return response + else: + child.after_run(actor, blackboard) + return FAILURE + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"LimiterDecorator") + return classes diff --git a/addons/beehave/nodes/decorators/succeeder.gd b/addons/beehave/nodes/decorators/succeeder.gd new file mode 100644 index 0000000..9d9664b --- /dev/null +++ b/addons/beehave/nodes/decorators/succeeder.gd @@ -0,0 +1,34 @@ +## A succeeder node will always return a `SUCCESS` status code. +@tool +@icon("../../icons/succeeder.svg") +class_name AlwaysSucceedDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + if c is ConditionLeaf: + blackboard.set_value("last_condition", c, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if response == RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + return RUNNING + else: + c.after_run(actor, blackboard) + return SUCCESS + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"AlwaysSucceedDecorator") + return classes diff --git a/addons/beehave/nodes/decorators/time_limiter.gd b/addons/beehave/nodes/decorators/time_limiter.gd new file mode 100644 index 0000000..cedb397 --- /dev/null +++ b/addons/beehave/nodes/decorators/time_limiter.gd @@ -0,0 +1,46 @@ +## The Time Limit Decorator will give its child a set amount of time to finish +## before interrupting it and return a `FAILURE` status code. The timer is reset +## every time before the node runs. +@tool +@icon("../../icons/limiter.svg") +class_name TimeLimiterDecorator extends Decorator + +@export var wait_time: = 0.0 + +var time_left: = 0.0 + +@onready var child: BeehaveNode = get_child(0) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + if time_left < wait_time: + time_left += get_physics_process_delta_time() + var response = child.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response) + + if child is ConditionLeaf: + blackboard.set_value("last_condition", child, str(actor.get_instance_id())) + blackboard.set_value("last_condition_status", response, str(actor.get_instance_id())) + + if response == RUNNING: + running_child = child + if child is ActionLeaf: + blackboard.set_value("running_action", child, str(actor.get_instance_id())) + + return response + else: + child.after_run(actor, blackboard) + interrupt(actor, blackboard) + return FAILURE + + +func before_run(actor: Node, blackboard: Blackboard) -> void: + time_left = 0.0 + child.before_run(actor, blackboard) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"TimeLimiterDecorator") + return classes diff --git a/addons/beehave/nodes/leaves/action.gd b/addons/beehave/nodes/leaves/action.gd new file mode 100644 index 0000000..003bdb4 --- /dev/null +++ b/addons/beehave/nodes/leaves/action.gd @@ -0,0 +1,13 @@ +## Actions are leaf nodes that define a task to be performed by an actor. +## Their execution can be long running, potentially being called across multiple +## frame executions. In this case, the node should return `RUNNING` until the +## action is completed. +@tool +@icon("../../icons/action.svg") +class_name ActionLeaf extends Leaf + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"ActionLeaf") + return classes diff --git a/addons/beehave/nodes/leaves/blackboard_compare.gd b/addons/beehave/nodes/leaves/blackboard_compare.gd new file mode 100644 index 0000000..84fb43f --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_compare.gd @@ -0,0 +1,61 @@ +## Compares two values using the specified comparison operator. +## Returns [code]FAILURE[/code] if any of the expression fails or the +## comparison operation returns [code]false[/code], otherwise it returns [code]SUCCESS[/code]. +@tool +class_name BlackboardCompareCondition extends ConditionLeaf + + +enum Operators { + EQUAL, + NOT_EQUAL, + GREATER, + LESS, + GREATER_EQUAL, + LESS_EQUAL, +} + + +## Expression represetning left operand. +## This value can be any valid GDScript expression. +## In order to use the existing blackboard keys for comparison, +## use get_value("key_name") e.g. get_value("direction").length() +@export_placeholder(EXPRESSION_PLACEHOLDER) var left_operand: String = "" +## Comparison operator. +@export_enum("==", "!=", ">", "<", ">=", "<=") var operator: int = 0 +## Expression represetning right operand. +## This value can be any valid GDScript expression. +## In order to use the existing blackboard keys for comparison, +## use get_value("key_name") e.g. get_value("direction").length() +@export_placeholder(EXPRESSION_PLACEHOLDER) var right_operand: String = "" + + +@onready var _left_expression: Expression = _parse_expression(left_operand) +@onready var _right_expression: Expression = _parse_expression(right_operand) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var left: Variant = _left_expression.execute([], blackboard) + + if _left_expression.has_execute_failed(): + return FAILURE + + var right: Variant = _right_expression.execute([], blackboard) + + if _right_expression.has_execute_failed(): + return FAILURE + + var result: bool = false + + match operator: + Operators.EQUAL: result = left == right + Operators.NOT_EQUAL: result = left != right + Operators.GREATER: result = left > right + Operators.LESS: result = left < right + Operators.GREATER_EQUAL: result = left >= right + Operators.LESS_EQUAL: result = left <= right + + return SUCCESS if result else FAILURE + + +func _get_expression_sources() -> Array[String]: + return [left_operand, right_operand] diff --git a/addons/beehave/nodes/leaves/blackboard_erase.gd b/addons/beehave/nodes/leaves/blackboard_erase.gd new file mode 100644 index 0000000..89508bd --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_erase.gd @@ -0,0 +1,25 @@ +## Erases the specified key from the blackboard. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. +@tool +class_name BlackboardEraseAction extends ActionLeaf + + +## Expression representing a blackboard key. +@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" + +@onready var _key_expression: Expression = _parse_expression(key) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var key_value: Variant = _key_expression.execute([], blackboard) + + if _key_expression.has_execute_failed(): + return FAILURE + + blackboard.erase_value(key_value) + + return SUCCESS + + +func _get_expression_sources() -> Array[String]: + return [key] diff --git a/addons/beehave/nodes/leaves/blackboard_has.gd b/addons/beehave/nodes/leaves/blackboard_has.gd new file mode 100644 index 0000000..eade90b --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_has.gd @@ -0,0 +1,23 @@ +## Returns [code]FAILURE[/code] if expression execution fails or the specified key doesn't exist. +## Returns [code]SUCCESS[/code] if blackboard has the specified key. +@tool +class_name BlackboardHasCondition extends ConditionLeaf + + +## Expression representing a blackboard key. +@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" + +@onready var _key_expression: Expression = _parse_expression(key) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var key_value: Variant = _key_expression.execute([], blackboard) + + if _key_expression.has_execute_failed(): + return FAILURE + + return SUCCESS if blackboard.has_value(key_value) else FAILURE + + +func _get_expression_sources() -> Array[String]: + return [key] diff --git a/addons/beehave/nodes/leaves/blackboard_set.gd b/addons/beehave/nodes/leaves/blackboard_set.gd new file mode 100644 index 0000000..d0d6455 --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_set.gd @@ -0,0 +1,34 @@ +## Sets the specified key to the specified value. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. +@tool +class_name BlackboardSetAction extends ActionLeaf + + +## Expression representing a blackboard key. +@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" +## Expression representing a blackboard value to assign to the specified key. +@export_placeholder(EXPRESSION_PLACEHOLDER) var value: String = "" + + +@onready var _key_expression: Expression = _parse_expression(key) +@onready var _value_expression: Expression = _parse_expression(value) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var key_value: Variant = _key_expression.execute([], blackboard) + + if _key_expression.has_execute_failed(): + return FAILURE + + var value_value: Variant = _value_expression.execute([], blackboard) + + if _value_expression.has_execute_failed(): + return FAILURE + + blackboard.set_value(key_value, value_value) + + return SUCCESS + + +func _get_expression_sources() -> Array[String]: + return [key, value] diff --git a/addons/beehave/nodes/leaves/condition.gd b/addons/beehave/nodes/leaves/condition.gd new file mode 100644 index 0000000..55ec6f9 --- /dev/null +++ b/addons/beehave/nodes/leaves/condition.gd @@ -0,0 +1,11 @@ +## Conditions are leaf nodes that either return SUCCESS or FAILURE depending on +## a single simple condition. They should never return `RUNNING`. +@tool +@icon("../../icons/condition.svg") +class_name ConditionLeaf extends Leaf + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"ConditionLeaf") + return classes diff --git a/addons/beehave/nodes/leaves/leaf.gd b/addons/beehave/nodes/leaves/leaf.gd new file mode 100644 index 0000000..13cac6d --- /dev/null +++ b/addons/beehave/nodes/leaves/leaf.gd @@ -0,0 +1,46 @@ +## Base class for all leaf nodes of the tree. +@tool +@icon("../../icons/category_leaf.svg") +class_name Leaf extends BeehaveNode + + +const EXPRESSION_PLACEHOLDER: String = "Insert an expression..." + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings: PackedStringArray = [] + + var children: Array[Node] = get_children() + + if children.any(func(x): return x is BeehaveNode): + warnings.append("Leaf nodes should not have any child nodes. They won't be ticked.") + + for source in _get_expression_sources(): + var error_text: String = _parse_expression(source).get_error_text() + if not error_text.is_empty(): + warnings.append("Expression `%s` is invalid! Error text: `%s`" % [source, error_text]) + + return warnings + + +func _parse_expression(source: String) -> Expression: + var result: Expression = Expression.new() + var error: int = result.parse(source) + + if not Engine.is_editor_hint() and error != OK: + push_error( + "[Leaf] Couldn't parse expression with source: `%s` Error text: `%s`" %\ + [source, result.get_error_text()] + ) + + return result + + +func _get_expression_sources() -> Array[String]: # virtual + return [] + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"Leaf") + return classes diff --git a/addons/beehave/plugin.cfg b/addons/beehave/plugin.cfg new file mode 100644 index 0000000..bced17b --- /dev/null +++ b/addons/beehave/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Beehave" +description="🐝 Behavior Tree addon for Godot Engine" +author="bitbrain" +version="2.7.5" +script="plugin.gd" diff --git a/addons/beehave/plugin.gd b/addons/beehave/plugin.gd new file mode 100644 index 0000000..3c54627 --- /dev/null +++ b/addons/beehave/plugin.gd @@ -0,0 +1,24 @@ +@tool +extends EditorPlugin + +const BeehaveEditorDebugger := preload("debug/debugger.gd") +var editor_debugger: BeehaveEditorDebugger +var frames: RefCounted + +func _init(): + name = "BeehavePlugin" + add_autoload_singleton("BeehaveGlobalMetrics", "res://addons/beehave/metrics/beehave_global_metrics.gd") + add_autoload_singleton("BeehaveGlobalDebugger", "res://addons/beehave/debug/global_debugger.gd") + print("Beehave initialized!") + + +func _enter_tree() -> void: + editor_debugger = BeehaveEditorDebugger.new() + frames = preload("debug/frames.gd").new() + add_debugger_plugin(editor_debugger) + + +func _exit_tree() -> void: + remove_debugger_plugin(editor_debugger) + editor_debugger.free() + frames.free() diff --git a/addons/beehave/utils/utils.gd b/addons/beehave/utils/utils.gd new file mode 100644 index 0000000..32e3a03 --- /dev/null +++ b/addons/beehave/utils/utils.gd @@ -0,0 +1,22 @@ +@tool +class_name BeehaveUtils + + +static func get_plugin() -> EditorPlugin: + var tree: SceneTree = Engine.get_main_loop() + return tree.get_root().get_child(0).get_node_or_null("BeehavePlugin") + + +static func get_editor_scale() -> float: + var plugin := get_plugin() + if plugin: + return plugin.get_editor_interface().get_editor_scale() + return 1.0 + + +static func get_frames() -> RefCounted: + var plugin := get_plugin() + if plugin: + return plugin.frames + push_error("Can't find Beehave Plugin") + return null diff --git a/bad-dude.png b/bad-dude.png new file mode 100644 index 0000000..b2a61ae Binary files /dev/null and b/bad-dude.png differ diff --git a/bad-dude.png.import b/bad-dude.png.import new file mode 100644 index 0000000..0b58040 --- /dev/null +++ b/bad-dude.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://belbhsx23yyd" +path="res://.godot/imported/bad-dude.png-388369b3f83dfa89ce800f1660bac9bb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://bad-dude.png" +dest_files=["res://.godot/imported/bad-dude.png-388369b3f83dfa89ce800f1660bac9bb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/bad_dude.tscn b/bad_dude.tscn new file mode 100644 index 0000000..32e9c6d --- /dev/null +++ b/bad_dude.tscn @@ -0,0 +1,124 @@ +[gd_scene load_steps=11 format=3 uid="uid://byuqbh3u5cxqo"] + +[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_von5m"] +[ext_resource type="Script" path="res://addons/beehave/nodes/composites/selector.gd" id="2_tmsoc"] +[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence.gd" id="3_wl46j"] +[ext_resource type="Script" path="res://addons/beehave/nodes/leaves/blackboard_has.gd" id="4_m2b65"] +[ext_resource type="Script" path="res://leaves/actions/dude_say.gd" id="5_jwdvu"] +[ext_resource type="Script" path="res://leaves/conditions/eye_condition.gd" id="6_5751j"] +[ext_resource type="Script" path="res://addons/beehave/nodes/leaves/blackboard_set.gd" id="6_pjfxk"] +[ext_resource type="Texture2D" uid="uid://belbhsx23yyd" path="res://bad-dude.png" id="8_67is2"] + +[sub_resource type="GDScript" id="GDScript_jijww"] +script/source = "class_name MeanDude +extends Node + +@export var blackboard: Blackboard + +var visible: Array[Node] = [] + +func _ready(): + $BeehaveTree.blackboard = blackboard + + +func say_shit(shit): + print(self.name, \": \", shit) + + +func _on_eyes_body_entered(body): + # simplification of course + if body != get_parent(): + visible.append(body) + + +func _on_eyes_body_exited(body): + if body != get_parent(): + visible.erase(body) +" + +[sub_resource type="CircleShape2D" id="CircleShape2D_tt36q"] +radius = 31.3847 + +[node name="BadDude" type="RigidBody2D"] +script = SubResource("GDScript_jijww") + +[node name="BeehaveTree" type="Node" parent="."] +script = ExtResource("1_von5m") +actor_node_path = NodePath("..") + +[node name="Root" type="Node" parent="BeehaveTree"] +script = ExtResource("2_tmsoc") + +[node name="Emergency" type="Node" parent="BeehaveTree/Root"] +script = ExtResource("3_wl46j") + +[node name="IsEmergency" type="Node" parent="BeehaveTree/Root/Emergency"] +script = ExtResource("4_m2b65") +key = "\"Emergency\"" + +[node name="DudePanicing" type="Node" parent="BeehaveTree/Root/Emergency"] +script = ExtResource("5_jwdvu") +shit_to_say = "Firing! Taking heavy fire!" + +[node name="HeroSpotted" type="Node" parent="BeehaveTree/Root"] +script = ExtResource("3_wl46j") + +[node name="SeeHero" type="Node" parent="BeehaveTree/Root/HeroSpotted"] +script = ExtResource("6_5751j") +looking_for = "Hero" + +[node name="Investigation" type="Node" parent="BeehaveTree/Root/HeroSpotted"] +script = ExtResource("3_wl46j") + +[node name="WhatWasThat" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation"] +script = ExtResource("5_jwdvu") +shit_to_say = "Huh? What was that?" + +[node name="Looking" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation"] +script = ExtResource("2_tmsoc") + +[node name="Found" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation/Looking"] +script = ExtResource("3_wl46j") + +[node name="SeeHero" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation/Looking/Found"] +script = ExtResource("6_5751j") +looking_for = "Hero" + +[node name="ReportContact" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation/Looking/Found"] +script = ExtResource("5_jwdvu") +shit_to_say = "CONTACT! LOTS OF CONTACT!!" + +[node name="PullAlarm" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation/Looking/Found"] +script = ExtResource("6_pjfxk") +key = "\"Emergency\"" +value = "true" + +[node name="MustBeWind" type="Node" parent="BeehaveTree/Root/HeroSpotted/Investigation/Looking"] +script = ExtResource("5_jwdvu") +shit_to_say = "Must have been the wind..." + +[node name="ImBored" type="Node" parent="BeehaveTree/Root"] +script = ExtResource("5_jwdvu") +shit_to_say = "What a boring night this is..." + +[node name="Sprite" type="Sprite2D" parent="."] +texture_filter = 1 +scale = Vector2(2.09614, 2.09614) +texture = ExtResource("8_67is2") + +[node name="Body" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_tt36q") + +[node name="Eyes" type="Area2D" parent="."] + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Eyes"] +position = Vector2(0, 1.14396) +scale = Vector2(2.79924, 2.13395) +polygon = PackedVector2Array(12, -12, 96, -59, 96, 60, 12, 11) + +[node name="Polygon2D" type="Polygon2D" parent="."] +color = Color(0.929412, 1, 0.458824, 0.239216) +polygon = PackedVector2Array(32, -25, 33, 24, 269, 129, 269, -126) + +[connection signal="body_entered" from="Eyes" to="." method="_on_eyes_body_entered"] +[connection signal="body_exited" from="Eyes" to="." method="_on_eyes_body_exited"] diff --git a/hero.gd b/hero.gd new file mode 100644 index 0000000..7510cd8 --- /dev/null +++ b/hero.gd @@ -0,0 +1,13 @@ +extends RigidBody2D + +@export var speed = 200.0 + +func _physics_process(delta): + if Input.is_action_pressed("up"): + apply_force(Vector2.UP * speed * delta) + if Input.is_action_pressed("down"): + apply_force(Vector2.DOWN * speed * delta) + if Input.is_action_pressed("left"): + apply_force(Vector2.LEFT * speed * delta) + if Input.is_action_pressed("right"): + apply_force(Vector2.RIGHT * speed * delta) diff --git a/hero.png b/hero.png new file mode 100644 index 0000000..435242b Binary files /dev/null and b/hero.png differ diff --git a/hero.png.import b/hero.png.import new file mode 100644 index 0000000..8121916 --- /dev/null +++ b/hero.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://diju4t0etxyar" +path="res://.godot/imported/hero.png-87aa9197b739ca3ad1031ac3d1b82853.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hero.png" +dest_files=["res://.godot/imported/hero.png-87aa9197b739ca3ad1031ac3d1b82853.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/leaves/actions/dude_say.gd b/leaves/actions/dude_say.gd new file mode 100644 index 0000000..855c469 --- /dev/null +++ b/leaves/actions/dude_say.gd @@ -0,0 +1,16 @@ +class_name DudeSay extends ActionLeaf + +@export_multiline var shit_to_say: String + +var _timer: SceneTreeTimer + +func tick(actor: Node, _blackboard: Blackboard) -> int: + if not _timer: + _timer = get_tree().create_timer(1) + actor.say_shit(shit_to_say) + return RUNNING + elif _timer.time_left > 0: + return RUNNING + else: + _timer = null + return SUCCESS diff --git a/leaves/conditions/eye_condition.gd b/leaves/conditions/eye_condition.gd new file mode 100644 index 0000000..62b34cd --- /dev/null +++ b/leaves/conditions/eye_condition.gd @@ -0,0 +1,6 @@ +class_name EyeCondition extends ConditionLeaf + +@export var looking_for: String + +func tick(actor: Node, blackboard: Blackboard) -> int: + return SUCCESS if actor.visible.any(func(n): return n.name == looking_for) else FAILURE diff --git a/main.tscn b/main.tscn new file mode 100644 index 0000000..534eda4 --- /dev/null +++ b/main.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=6 format=3 uid="uid://buervcbmbh1dg"] + +[ext_resource type="PackedScene" uid="uid://byuqbh3u5cxqo" path="res://bad_dude.tscn" id="2_bghxq"] +[ext_resource type="Script" path="res://hero.gd" id="3_fmhqm"] +[ext_resource type="Texture2D" uid="uid://diju4t0etxyar" path="res://hero.png" id="4_svfrs"] +[ext_resource type="Script" path="res://addons/beehave/blackboard.gd" id="5_6difr"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_eiiek"] +radius = 28.7924 + +[node name="Node2D" type="Node2D"] + +[node name="Blackboard" type="Node" parent="."] +script = ExtResource("5_6difr") + +[node name="BadDude" parent="." node_paths=PackedStringArray("blackboard") instance=ExtResource("2_bghxq")] +position = Vector2(77, 140) +blackboard = NodePath("../Blackboard") + +[node name="BadDude2" parent="." node_paths=PackedStringArray("blackboard") instance=ExtResource("2_bghxq")] +position = Vector2(788, 69) +rotation = 1.56094 +blackboard = NodePath("../Blackboard") + +[node name="Hero" type="RigidBody2D" parent="."] +position = Vector2(545, 223) +mass = 0.1 +script = ExtResource("3_fmhqm") +speed = 2000.0 + +[node name="Body" type="CollisionShape2D" parent="Hero"] +shape = SubResource("CircleShape2D_eiiek") + +[node name="Icon" type="Sprite2D" parent="Hero"] +texture_filter = 1 +scale = Vector2(1.21875, 1.21875) +texture = ExtResource("4_svfrs") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..85bd158 --- /dev/null +++ b/project.godot @@ -0,0 +1,52 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="godoit" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.1", "Forward Plus") +config/icon="res://icon.svg" + +[autoload] + +BeehaveGlobalMetrics="*res://addons/beehave/metrics/beehave_global_metrics.gd" +BeehaveGlobalDebugger="*res://addons/beehave/debug/global_debugger.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/beehave/plugin.cfg") + +[input] + +up={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +down={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} + +[physics] + +2d/default_gravity_vector=Vector2(0, 0)