Compare commits

...

18 Commits

Author SHA1 Message Date
f2de25ab0f Diegetic grunk gauge
All checks were successful
linting & formatting / build (push) Successful in 21s
2025-04-18 18:36:19 -06:00
c74bfefe92 Lerp framerate logic fixes 2025-04-18 15:52:31 -06:00
35cc441241 Greater slutch 2025-04-18 15:19:54 -06:00
2e3b08271f Drone gets louder when beast is near 2025-04-18 14:44:24 -06:00
986dd518e3 Advanced grunk alert 2025-04-18 12:53:08 -06:00
c33efdf9ce The eyes of the slutch... 2025-04-18 12:19:30 -06:00
4b7eefba74 Workaround for beast getting stuck on links 2025-04-18 10:10:35 -06:00
ac811c354a Project setting & editor override for showing nav agent debug paths 2025-04-18 10:05:14 -06:00
33b38d17bd The slutchy grunkbeast! 2025-04-18 09:53:21 -06:00
fb34e3b659 Grunkbeast spawning & alert response logic 2025-04-17 18:47:15 -06:00
efc62c16c1 Navigation tweaks 2025-04-17 17:43:04 -06:00
7de318a613 Advanced stalking logic 2025-04-17 16:38:31 -06:00
b8c689bcf5 Workaround script for generated docs 2025-04-17 14:59:29 -06:00
e105601c1b Grunkbeast behavior tree 2025-04-17 14:54:17 -06:00
bb545bcbcd Added Beehave v2.8.4 addon 2025-04-17 10:08:45 -06:00
f5e05a7e08 Grunkbeast state machine 2025-04-17 10:02:01 -06:00
27ec21745b Basic pathing for grunkbeast 2025-04-16 17:20:33 -06:00
f695ef846c Gunk relays show connections in editor preview 2025-04-16 15:45:18 -06:00
311 changed files with 340406 additions and 1473 deletions

21
addons/beehave/LICENSE Normal file
View File

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

View File

@ -0,0 +1,54 @@
@icon("icons/blackboard.svg")
class_name Blackboard extends Node
const DEFAULT = "default"
## The blackboard is an object that can be used to store and access data between
## multiple nodes of the behavior tree.
@export var blackboard: Dictionary = {}:
set(b):
blackboard = b
_data[DEFAULT] = blackboard
var _data: Dictionary = {}
func _ready():
_data[DEFAULT] = blackboard
func keys() -> Array[String]:
var keys: Array[String]
keys.assign(_data.keys().duplicate())
return keys
func set_value(key: Variant, value: Variant, blackboard_name: String = DEFAULT) -> void:
if not _data.has(blackboard_name):
_data[blackboard_name] = {}
_data[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 _data[blackboard_name].get(key, default_value)
return default_value
func has_value(key: Variant, blackboard_name: String = DEFAULT) -> bool:
return (
_data.has(blackboard_name)
and _data[blackboard_name].has(key)
and _data[blackboard_name][key] != null
)
func erase_value(key: Variant, blackboard_name: String = DEFAULT) -> void:
if _data.has(blackboard_name):
_data[blackboard_name][key] = null
func get_debug_data() -> Dictionary:
return _data

View File

@ -0,0 +1 @@
uid://dme5f24l0edsf

View File

@ -0,0 +1,96 @@
@tool
extends EditorDebuggerPlugin
const DebuggerTab := preload("debugger_tab.gd")
const BeehaveUtils := preload("res://addons/beehave/utils/utils.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], data[2])
return true
if message == "beehave:process_begin":
debugger_tab.graph.process_begin(data[0], data[1])
return true
if message == "beehave:process_end":
debugger_tab.graph.process_end(data[0], data[1])
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

View File

@ -0,0 +1 @@
uid://dhh8iwql2tur2

View File

@ -0,0 +1,30 @@
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, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_tick", [instance_id, status, blackboard])
static func process_begin(instance_id: int, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_begin", [instance_id, blackboard])
static func process_end(instance_id: int, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_end", [instance_id, blackboard])

View File

@ -0,0 +1 @@
uid://ubuuvrubeh85

View File

@ -0,0 +1,157 @@
@tool
class_name BeehaveDebuggerTab extends PanelContainer
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
signal make_floating
const OldBeehaveGraphEdit := preload("old_graph_edit.gd")
const NewBeehaveGraphEdit := preload("new_graph_edit.gd")
const NewNodeBlackBoard := preload("new_node_blackboard.gd")
const TREE_ICON := preload("../icons/tree.svg")
var graph
var container: HSplitContainer
var graph_container: HSplitContainer
var item_list: ItemList
var blackboard_vbox: VBoxContainer
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_container = HSplitContainer.new()
graph_container.split_offset = 1920
graph_container.set_anchors_preset(Control.PRESET_FULL_RECT)
container.add_child(graph_container)
if Engine.get_version_info().minor >= 2:
graph = NewBeehaveGraphEdit.new(BeehaveUtils.get_frames())
else:
graph = OldBeehaveGraphEdit.new(BeehaveUtils.get_frames())
graph.node_selected.connect(_on_graph_node_selected)
graph.node_deselected.connect(_on_graph_node_deselected)
graph_container.add_child(graph)
blackboard_vbox = VBoxContainer.new()
blackboard_vbox.custom_minimum_size = Vector2(200, 0)
blackboard_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
graph_container.add_child(blackboard_vbox)
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.name = "MakeFloatingButton"
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_menu_container().add_child(button)
var toggle_button := Button.new()
toggle_button.flat = true
toggle_button.name = "TogglePanelButton"
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_menu_container().add_child(toggle_button)
graph.get_menu_container().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:
if not active_trees.has(data.id):
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
if active_tree_id == data.id.to_int():
graph.beehave_tree = 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, {})
# Clear our any loaded blackboards
for child in blackboard_vbox.get_children():
child.free()
active_tree_id = id.to_int()
if session != null:
session.send_message("beehave:activate_tree", [active_tree_id])
func _on_graph_node_selected(node: GraphNode) -> void:
var node_blackboard: VBoxContainer = NewNodeBlackBoard.new(BeehaveUtils.get_frames(), node)
blackboard_vbox.add_child(node_blackboard)
func _on_graph_node_deselected(node: GraphNode) -> void:
var matches: Array = blackboard_vbox\
.get_children()\
.filter(func (child): return child.name == node.name)
for child in matches:
child.free()
func _on_visibility_changed() -> void:
if session != null:
session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()])

View File

@ -0,0 +1 @@
uid://b7hbn0y1kfarn

View File

@ -0,0 +1,38 @@
extends Node
var _registered_trees: Dictionary
var _active_tree
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 && is_instance_valid(_active_tree):
_active_tree._can_send_message = data[0]
return true
return false
func _set_active_tree(tree_id: int) -> void:
var tree = _registered_trees.get(tree_id, null)
if not tree:
return
if _active_tree && is_instance_valid(_active_tree):
_active_tree._can_send_message = false
_active_tree = tree
_active_tree._can_send_message = true
func register_tree(tree) -> void:
_registered_trees[tree.get_instance_id()] = tree
func unregister_tree(tree) -> void:
_registered_trees.erase(tree.get_instance_id())

View File

@ -0,0 +1 @@
uid://bl3ma400hpvsh

BIN
addons/beehave/debug/icons/horizontal_layout.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/debug/icons/port_bottom.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/debug/icons/port_left.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/debug/icons/port_right.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/debug/icons/port_top.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/debug/icons/vertical_layout.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

View File

@ -0,0 +1,69 @@
@tool
extends RefCounted
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
const SUCCESS_COLOR := Color("#07783a")
const NORMAL_COLOR := Color("#15181e")
const FAILURE_COLOR := Color("#82010b")
const RUNNING_COLOR := Color("#c29c06")
var panel_normal: StyleBoxFlat
var panel_success: StyleBoxFlat
var panel_failure: StyleBoxFlat
var panel_running: StyleBoxFlat
var titlebar_normal: StyleBoxFlat
var titlebar_success: StyleBoxFlat
var titlebar_failure: StyleBoxFlat
var titlebar_running: StyleBoxFlat
func _init() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
titlebar_normal = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"titlebar", &"GraphNode")\
.duplicate()
)
titlebar_success = titlebar_normal.duplicate()
titlebar_failure = titlebar_normal.duplicate()
titlebar_running = titlebar_normal.duplicate()
titlebar_success.bg_color = SUCCESS_COLOR
titlebar_failure.bg_color = FAILURE_COLOR
titlebar_running.bg_color = RUNNING_COLOR
titlebar_success.border_color = SUCCESS_COLOR
titlebar_failure.border_color = FAILURE_COLOR
titlebar_running.border_color = RUNNING_COLOR
panel_normal = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"panel", &"GraphNode")
.duplicate()
)
panel_success = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"panel_selected", &"GraphNode")
.duplicate()
)
panel_failure = panel_success.duplicate()
panel_running = panel_success.duplicate()
panel_success.border_color = SUCCESS_COLOR
panel_failure.border_color = FAILURE_COLOR
panel_running.border_color = RUNNING_COLOR

View File

@ -0,0 +1 @@
uid://tn5lef5pc6ol

View File

@ -0,0 +1,297 @@
@tool
extends GraphEdit
const BeehaveGraphNode := preload("new_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("#898989")
const ACTIVE_COLOR: Color = Color("#c29c06")
const SUCCESS_COLOR: Color = Color("#07783a")
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 frames:RefCounted
var active_nodes: Array[String]
var progress: int = 0
var layout_button: Button
func _init(frames:RefCounted) -> void:
self.frames = frames
func _ready() -> void:
custom_minimum_size = Vector2(100, 300)
set("show_arrange_button", 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_menu_container().add_child(layout_button)
_update_layout_button()
func _update_graph() -> void:
if updating_graph:
return
updating_graph = true
clear_connections()
for child in _get_child_nodes():
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(frames, 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_menu_container() -> Control:
return call("get_menu_hbox")
func get_status(status: int) -> String:
if status == 0:
return "SUCCESS"
elif status == 1:
return "FAILURE"
return "RUNNING"
func process_begin(instance_id: int, blackboard = null) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
child.set_meta("status", -1)
func process_tick(instance_id: int, status: int, blackboard = null) -> 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)
node.blackboard = blackboard
if status == BeehaveNode.SUCCESS or status == BeehaveNode.RUNNING:
if not active_nodes.has(node.name):
active_nodes.push_back(node.name)
func process_end(instance_id: int, blackboard = null) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
var status := child.get_meta("status", -1)
match status:
BeehaveNode.SUCCESS:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
BeehaveNode.FAILURE:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
BeehaveNode.RUNNING:
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_child_nodes() -> Array[Node]:
return get_children().filter(func(child): return child is BeehaveGraphNode)
func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
for child in _get_child_nodes():
for port in child.get_input_port_count():
if not (child.position_offset + child.get_input_port_position(port)).is_equal_approx(to_position):
continue
to_position = child.position_offset + child.get_custom_input_port_position(horizontal_layout)
for port in child.get_output_port_count():
if not (child.position_offset + child.get_output_port_position(port)).is_equal_approx(from_position):
continue
from_position = child.position_offset + child.get_custom_output_port_position(horizontal_layout)
return _get_elbow_connection_line(from_position, to_position)
func _get_elbow_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
var points: PackedVector2Array
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:
var from_node: StringName
var to_node: StringName
from_node = c.from_node
to_node = c.to_node
if not from_node in active_nodes or not c.to_node in active_nodes:
continue
var from := get_node(String(from_node))
var to := get_node(String(to_node))
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
var output_port_position: Vector2
var input_port_position: Vector2
var scale_factor: float = from.get_rect().size.x / from.size.x
var line := _get_elbow_connection_line(
from.position + from.get_custom_output_port_position(horizontal_layout) * scale_factor,
to.position + to.get_custom_input_port_position(horizontal_layout) * scale_factor
)
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"

View File

@ -0,0 +1 @@
uid://nqi2umf7xr2h

View File

@ -0,0 +1,192 @@
@tool
extends GraphNode
signal blackboard_updated
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
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
@export var blackboard: Dictionary:
set(value):
blackboard = value
blackboard_updated.emit()
var layout_size: float:
get:
return size.y if horizontal else size.x
var icon_rect: TextureRect
var title_label: Label
var label: Label
var titlebar_hbox: HBoxContainer
var frames: RefCounted
var horizontal: bool = false
var panels_tween: Tween
func _init(frames:RefCounted, horizontal: bool = false) -> void:
self.frames = frames
self.horizontal = horizontal
func _ready() -> void:
custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale()
draggable = false
add_theme_color_override("close_color", Color.TRANSPARENT)
add_theme_icon_override("close", ImageTexture.new())
# For top port
var top_port: Control = Control.new()
add_child(top_port)
icon_rect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
titlebar_hbox = get_titlebar_hbox()
titlebar_hbox.get_child(0).queue_free()
titlebar_hbox.alignment = BoxContainer.ALIGNMENT_BEGIN
titlebar_hbox.add_child(icon_rect)
title_label = Label.new()
title_label.add_theme_color_override("font_color", Color.WHITE)
var title_font: Font = get_theme_font("title_font").duplicate()
if title_font is FontVariation:
title_font.variation_embolden = 1
elif title_font is FontFile:
title_font.font_weight = 700
title_label.add_theme_font_override("font", title_font)
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.text = title_text
titlebar_hbox.add_child(title_label)
label = Label.new()
label.text = " " if text.is_empty() else text
add_child(label)
# For bottom port
add_child(Control.new())
minimum_size_changed.connect(_on_size_changed)
_on_size_changed.call_deferred()
func _draw_port(slot_index: int, port_position: Vector2i, left: bool, color: Color) -> void:
if horizontal:
if is_slot_enabled_left(1):
draw_texture(PORT_LEFT_ICON, Vector2(0, size.y / 2) + Vector2(-4, -5), color)
if is_slot_enabled_right(1):
draw_texture(PORT_RIGHT_ICON, Vector2(size.x, size.y / 2) + Vector2(-5, -4.5), color)
else:
if slot_index == 0 and is_slot_enabled_left(0):
draw_texture(PORT_TOP_ICON, Vector2(size.x / 2, 0) + Vector2(-4.5, -7), color)
elif slot_index == 1:
draw_texture(PORT_BOTTOM_ICON, Vector2(size.x / 2, size.y) + Vector2(-4.5, -5), color)
func get_custom_input_port_position(horizontal: bool) -> Vector2:
if horizontal:
return Vector2(0, size.y / 2)
else:
return Vector2(size.x/2, 0)
func get_custom_output_port_position(horizontal: bool) -> Vector2:
if horizontal:
return Vector2(size.x, size.y / 2)
else:
return Vector2(size.x / 2, size.y)
func set_status(status: int) -> void:
match status:
BeehaveNode.SUCCESS: _set_stylebox_overrides(frames.panel_success, frames.titlebar_success)
BeehaveNode.FAILURE: _set_stylebox_overrides(frames.panel_failure, frames.titlebar_failure)
BeehaveNode.RUNNING: _set_stylebox_overrides(frames.panel_running, frames.titlebar_running)
_: _set_stylebox_overrides(frames.panel_normal, frames.titlebar_normal)
func set_slots(left_enabled: bool, right_enabled: bool) -> void:
if horizontal:
set_slot(1, left_enabled, -1, Color.WHITE, right_enabled, -1, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON)
else:
set_slot(0, left_enabled, -1, Color.WHITE, false, -1, Color.TRANSPARENT, PORT_TOP_ICON, null)
set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, -1, 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 _set_stylebox_overrides(panel_stylebox: StyleBox, titlebar_stylebox: StyleBox) -> void:
# First update and any status change gets immediate panel update
if not has_theme_stylebox_override("panel") or panel_stylebox != frames.panel_normal:
if panels_tween:
panels_tween.kill()
panels_tween = null
add_theme_stylebox_override("panel", panel_stylebox)
add_theme_stylebox_override("titlebar", titlebar_stylebox)
return
# Don't need to do anything if we're already tweening back to normal
if panels_tween:
return
# Don't need to do anything if our colors are already the same as a normal
var cur_panel_stylebox: StyleBox = get_theme_stylebox("panel")
var cur_titlebar_stylebox: StyleBox = get_theme_stylebox("titlebar")
if cur_panel_stylebox.bg_color == frames.panel_normal.bg_color:
return
# Apply a duplicate of our current panels that we can tween
add_theme_stylebox_override("panel", cur_panel_stylebox.duplicate())
add_theme_stylebox_override("titlebar", cur_titlebar_stylebox.duplicate())
cur_panel_stylebox = get_theme_stylebox("panel")
cur_titlebar_stylebox = get_theme_stylebox("titlebar")
# Going back to normal is a fade
panels_tween = create_tween()
panels_tween.parallel().tween_property(cur_panel_stylebox, "bg_color", panel_stylebox.bg_color, 1.0)
panels_tween.parallel().tween_property(cur_panel_stylebox, "border_color", panel_stylebox.border_color, 1.0)
panels_tween.parallel().tween_property(cur_titlebar_stylebox, "bg_color", panel_stylebox.bg_color, 1.0)
panels_tween.parallel().tween_property(cur_titlebar_stylebox, "border_color", panel_stylebox.border_color, 1.0)
func _on_size_changed():
add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x))

View File

@ -0,0 +1 @@
uid://bpjiuilpk7qkl

View File

@ -0,0 +1,76 @@
extends VBoxContainer
var frames: RefCounted
var graph_node: GraphNode
var item_tree: Tree
func _init(frames: RefCounted, node: GraphNode) -> void:
self.frames = frames
graph_node = node
graph_node.blackboard_updated.connect(_update_list)
func _ready() -> void:
name = graph_node.name
set_anchors_preset(Control.PRESET_FULL_RECT)
var title_panel: Panel = Panel.new()
title_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
title_panel.custom_minimum_size = Vector2(200, 50)
add_child(title_panel)
var title_hbox: HBoxContainer = HBoxContainer.new()
title_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
title_hbox.set_anchors_preset(Control.PRESET_FULL_RECT)
title_panel.add_child(title_hbox)
var icon_rect: TextureRect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_rect.texture = graph_node.icon
icon_rect.set_size(Vector2(20, 20))
title_hbox.add_child(icon_rect)
var title: Label = Label.new()
title.text = graph_node.title_text
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.set_anchors_preset(Control.PRESET_FULL_RECT)
title_hbox.add_child(title)
item_tree = Tree.new()
item_tree.custom_minimum_size = Vector2(200, 400)
item_tree.hide_root = true
item_tree.allow_search = false
item_tree.columns = 2
add_child(item_tree)
_update_list()
func _update_list() -> void:
item_tree.clear()
var root: TreeItem = item_tree.create_item()
if graph_node.blackboard.size() == 0:
var no_bb_message: TreeItem = item_tree.create_item(root)
no_bb_message.set_text(0, "No blackboard data")
return
for bb_name in graph_node.blackboard:
var bb_name_branch: TreeItem = item_tree.create_item(root)
bb_name_branch.set_text(0, bb_name)
#print(graph_node.blackboard.get(bb_name))
for key in graph_node.blackboard.get(bb_name):
var bb_kv_leaf: TreeItem = item_tree.create_item(bb_name_branch)
bb_kv_leaf.set_text(0, str(key))
bb_kv_leaf.set_text(1, str(graph_node.blackboard.get(bb_name).get(key)))
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

View File

@ -0,0 +1 @@
uid://ne6tky0wxkxy

View File

@ -0,0 +1,47 @@
@tool
extends RefCounted
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
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

View File

@ -0,0 +1 @@
uid://bhapw50f625uy

View File

@ -0,0 +1,286 @@
@tool
extends GraphEdit
const BeehaveGraphNode := preload("old_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 frames: RefCounted
var active_nodes: Array[String]
var progress: int = 0
var layout_button: Button
func _init(frames: RefCounted) -> void:
self.frames = frames
func _ready() -> void:
custom_minimum_size = Vector2(100, 300)
set("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_menu_container().add_child(layout_button)
_update_layout_button()
func _update_graph() -> void:
if updating_graph:
return
updating_graph = true
clear_connections()
for child in _get_child_nodes():
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(frames, 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_menu_container() -> Control:
return call("get_zoom_hbox")
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_child_nodes():
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 == BeehaveNode.SUCCESS or status == BeehaveNode.RUNNING:
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_child_nodes():
var status := child.get_meta("status", -1)
match status:
BeehaveNode.SUCCESS:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
BeehaveNode.FAILURE:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
BeehaveNode.RUNNING:
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_child_nodes() -> Array[Node]:
return get_children().filter(func(child): return child is BeehaveGraphNode)
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:
var from_node: StringName
var to_node: StringName
from_node = c.from
to_node = c.to
if not from_node in active_nodes or not c.to_node in active_nodes:
continue
var from := get_node(String(from_node))
var to := get_node(String(to_node))
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
var output_port_position: Vector2
var input_port_position: Vector2
output_port_position = (
from.position + from.call("get_connection_output_position", c.from_port)
)
input_port_position = to.position + to.call("get_connection_input_position", c.to_port)
var line := _get_connection_line(output_port_position, input_port_position)
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"
)

View File

@ -0,0 +1 @@
uid://qhj602h7xnwu

View File

@ -0,0 +1,166 @@
@tool
extends GraphNode
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
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
var horizontal: bool = false
func _init(frames: RefCounted, horizontal: bool = false) -> void:
self.frames = frames
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 if frames != null else null)
add_theme_stylebox_override("selected_frame", frames.empty if frames != null else null)
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 if frames != null else null)
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:
BeehaveNode.SUCCESS:
return frames.success
BeehaveNode.FAILURE:
return frames.failure
BeehaveNode.RUNNING:
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)
)

View File

@ -0,0 +1 @@
uid://bkng0fnhet2hx

View File

@ -0,0 +1,275 @@
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
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
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())

View File

@ -0,0 +1 @@
uid://20qccrlr5enw

BIN
addons/beehave/icons/action.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/blackboard.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/category_bt.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/category_composite.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/category_decorator.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/category_leaf.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/condition.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/cooldown.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c7evuohjd0qrp"
path="res://.godot/imported/cooldown.svg-2fb8975b5974e35bedad825abb9faf66.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/icons/cooldown.svg"
dest_files=["res://.godot/imported/cooldown.svg-2fb8975b5974e35bedad825abb9faf66.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=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
addons/beehave/icons/delayer.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d0us6envnwqnv"
path="res://.godot/imported/delayer.svg-6f92c97f61b1eb8679428f438e6b08c7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/icons/delayer.svg"
dest_files=["res://.godot/imported/delayer.svg-6f92c97f61b1eb8679428f438e6b08c7.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=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
addons/beehave/icons/failer.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/inverter.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/limiter.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/repeater.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d070jf2n7rbee"
path="res://.godot/imported/repeater.svg-be2d3a7f1a46d7ba1d1939553725f598.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/icons/repeater.svg"
dest_files=["res://.godot/imported/repeater.svg-be2d3a7f1a46d7ba1d1939553725f598.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=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
addons/beehave/icons/selector.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/selector_random.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/selector_reactive.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/sequence.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/sequence_random.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/sequence_reactive.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/simple_parallel.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ci50aklnpel11"
path="res://.godot/imported/simple_parallel.svg-3d4107eaf2e46557f6d3be3249f91430.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/icons/simple_parallel.svg"
dest_files=["res://.godot/imported/simple_parallel.svg-3d4107eaf2e46557f6d3be3249f91430.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=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
addons/beehave/icons/succeeder.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/tree.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
addons/beehave/icons/until_fail.svg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1caw0gsvct8r"
path="res://.godot/imported/until_fail.svg-8015014c40e91d9c2668ec34d4118b8e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/icons/until_fail.svg"
dest_files=["res://.godot/imported/until_fail.svg-8015014c40e91d9c2668ec34d4118b8e.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=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,54 @@
extends Node
var _tree_count: int = 0
var _active_tree_count: int = 0
var _registered_trees: Array = []
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) -> 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) -> 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

View File

@ -0,0 +1 @@
uid://c3ktl6ontsdt7

View File

@ -0,0 +1,54 @@
@tool
class_name BeehaveNode extends Node
## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or
## `RUNNING` when ticked.
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)
func _safe_tick(actor: Node, blackboard: Blackboard) -> int:
var response = tick(actor, blackboard)
if not response is int:
push_error("All tick methods must return an int, got %s" % response)
return FAILURE
return response

View File

@ -0,0 +1 @@
uid://c37shq5uf4gk

View File

@ -0,0 +1,330 @@
@tool
@icon("../icons/tree.svg")
class_name BeehaveTree extends Node
## Controls the flow of execution of the entire behavior tree.
enum { SUCCESS, FAILURE, RUNNING }
enum ProcessThread { IDLE, PHYSICS, MANUAL }
signal tree_enabled
signal tree_disabled
## Whether this behavior tree should be enabled or not.
@export var enabled: bool = true:
set(value):
enabled = value
set_physics_process(enabled and process_thread == ProcessThread.PHYSICS)
set_process(enabled and process_thread == ProcessThread.IDLE)
if value:
tree_enabled.emit()
else:
interrupt()
tree_disabled.emit()
get:
return enabled
## How often the tree should tick, in frames. The default value of 1 means
## tick() runs every frame.
@export var tick_rate: int = 1
## An optional node path this behavior tree should apply to.
@export_node_path var actor_node_path: NodePath:
set(anp):
actor_node_path = anp
if actor_node_path != null and str(actor_node_path) != "..":
actor = get_node(actor_node_path)
else:
actor = get_parent()
if Engine.is_editor_hint():
update_configuration_warnings()
## Whether to run this tree in a physics or idle thread.
@export var process_thread: ProcessThread = ProcessThread.PHYSICS:
set(value):
process_thread = value
self.enabled = self.enabled and process_thread != ProcessThread.MANUAL
set_physics_process(enabled and process_thread == ProcessThread.PHYSICS)
set_process(enabled and process_thread == ProcessThread.IDLE)
## 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:
# in case blackboard is accessed before this node is,
# we need to ensure that the internal blackboard is used.
if not blackboard and not _internal_blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
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
)
_get_global_metrics().register_tree(self)
else:
if _process_time_metric_name != "":
# Remove tree metric from the engine
Performance.remove_custom_monitor(_process_time_metric_name)
_get_global_metrics().unregister_tree(self)
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
@export var actor: Node:
set(a):
actor = a
if actor == null:
actor = get_parent()
if Engine.is_editor_hint():
update_configuration_warnings()
var status: int = -1
var last_tick: int = 0
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:
var connect_scene_tree_signal = func(signal_name: String, is_added: bool):
if not get_tree().is_connected(signal_name, _on_scene_tree_node_added_removed.bind(is_added)):
get_tree().connect(signal_name, _on_scene_tree_node_added_removed.bind(is_added))
connect_scene_tree_signal.call("node_added", true)
connect_scene_tree_signal.call("node_removed", false)
if not process_thread:
process_thread = ProcessThread.PHYSICS
if not actor:
if actor_node_path:
actor = get_node(actor_node_path)
else:
actor = get_parent()
if not blackboard:
# invoke setter to auto-initialise the blackboard.
self.blackboard = null
# Get the name of the parent node name for metric
_process_time_metric_name = (
"beehave [microseconds]/process_time_%s-%s" % [actor.name, get_instance_id()]
)
set_physics_process(enabled and process_thread == ProcessThread.PHYSICS)
set_process(enabled and process_thread == ProcessThread.IDLE)
# Register custom metric to the engine
if custom_monitor and not Engine.is_editor_hint():
Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value)
_get_global_metrics().register_tree(self)
if Engine.is_editor_hint():
update_configuration_warnings.call_deferred()
else:
_get_global_debugger().register_tree(self)
BeehaveDebuggerMessages.register_tree(_get_debugger_data(self))
# Randomize at what frames tick() will happen to avoid stutters
last_tick = randi_range(0, tick_rate - 1)
func _on_scene_tree_node_added_removed(node: Node, is_added: bool) -> void:
if Engine.is_editor_hint():
return
if node is BeehaveNode and is_ancestor_of(node):
var sgnal := node.ready if is_added else node.tree_exited
if is_added:
sgnal.connect(
func() -> void: BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)),
CONNECT_ONE_SHOT
)
else:
sgnal.connect(
func() -> void:
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
request_ready()
)
func _physics_process(_delta: float) -> void:
_process_internally()
func _process(_delta: float) -> void:
_process_internally()
func _process_internally() -> void:
if Engine.is_editor_hint():
return
if last_tick < tick_rate - 1:
last_tick += 1
return
last_tick = 0
# 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(), blackboard.get_debug_data())
if self.get_child_count() == 1:
tick()
if _can_send_message:
BeehaveDebuggerMessages.process_end(get_instance_id(), blackboard.get_debug_data())
# 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:
if actor == null or get_child_count() == 0:
return FAILURE
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, blackboard.get_debug_data())
BeehaveDebuggerMessages.process_tick(get_instance_id(), status, blackboard.get_debug_data())
# 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 actor == null:
warnings.append("Configure target node on tree")
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)
_get_global_metrics().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 int(_process_time_metric_value)
func _get_debugger_data(node: Node) -> Dictionary:
if not (node is BeehaveTree or 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"]
# required to avoid lifecycle issues on initial load
# due to loading order problems with autoloads
func _get_global_metrics() -> Node:
return get_tree().root.get_node("BeehaveGlobalMetrics")
# required to avoid lifecycle issues on initial load
# due to loading order problems with autoloads
func _get_global_debugger() -> Node:
return get_tree().root.get_node("BeehaveGlobalDebugger")

View File

@ -0,0 +1 @@
uid://bb0t2ovl7wifo

View File

@ -0,0 +1,34 @@
@tool
@icon("../../icons/category_composite.svg")
class_name Composite extends BeehaveNode
## A Composite node controls the flow of execution of its children in a specific manner.
var running_child: BeehaveNode = null
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

View File

@ -0,0 +1 @@
uid://fengp7jc44qv

View File

@ -0,0 +1,176 @@
@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
var _exiting_tree: bool
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 _exit_tree() -> void:
_exiting_tree = true
func _enter_tree() -> void:
_exiting_tree = false
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)
if not node.tree_exited.is_connected(_on_child_tree_exited):
node.tree_exited.connect(_on_child_tree_exited.bind(node))
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)
func _on_child_tree_exited(node: Node) -> void:
# don't erase the individual child if the whole tree is exiting together
if not _exiting_tree:
var children = get_children()
children.erase(node)
_update_weights(children)
if node.tree_exited.is_connected(_on_child_tree_exited):
node.tree_exited.disconnect(_on_child_tree_exited)
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

View File

@ -0,0 +1 @@
uid://doqgfjw53d1l4

View File

@ -0,0 +1,69 @@
@tool
@icon("../../icons/selector.svg")
class_name SelectorComposite extends Composite
## 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.
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: int = c._safe_tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
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: String = 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

View File

@ -0,0 +1 @@
uid://8hn4kne15ac5

View File

@ -0,0 +1,82 @@
@tool
@icon("../../icons/selector_random.svg")
class_name SelectorRandomComposite extends RandomizedComposite
## 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.
## 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: int = c._safe_tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
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

View File

@ -0,0 +1 @@
uid://dvnmhlldp23hg

View File

@ -0,0 +1,47 @@
@tool
@icon("../../icons/selector_reactive.svg")
class_name SelectorReactiveComposite extends Composite
## 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.
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c._safe_tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
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

Some files were not shown because too many files have changed in this diff Show More