You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			260 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			GDScript
		
	
		
		
			
		
	
	
			260 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			GDScript
		
	
| 
											2 years ago
										 | @tool | ||
|  | extends GraphEdit | ||
|  | 
 | ||
|  | const BeehaveGraphNode := preload("graph_node.gd") | ||
|  | 
 | ||
|  | const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg") | ||
|  | const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg") | ||
|  | 
 | ||
|  | const PROGRESS_SHIFT: int = 50 | ||
|  | const INACTIVE_COLOR: Color = Color("#898989aa") | ||
|  | const ACTIVE_COLOR: Color = Color("#ffcc00c8") | ||
|  | const SUCCESS_COLOR: Color = Color("#009944c8") | ||
|  | 
 | ||
|  | 
 | ||
|  | var updating_graph: bool = false | ||
|  | var arraging_nodes: bool = false | ||
|  | var beehave_tree: Dictionary: | ||
|  | 	set(value): | ||
|  | 		if beehave_tree == value: | ||
|  | 			return | ||
|  | 		beehave_tree = value | ||
|  | 		active_nodes.clear() | ||
|  | 		_update_graph() | ||
|  | 
 | ||
|  | var horizontal_layout: bool = false: | ||
|  | 	set(value): | ||
|  | 		if updating_graph or arraging_nodes: | ||
|  | 			return | ||
|  | 		if horizontal_layout == value: | ||
|  | 			return | ||
|  | 		horizontal_layout = value | ||
|  | 		_update_layout_button() | ||
|  | 		_update_graph() | ||
|  | 
 | ||
|  | 
 | ||
|  | var active_nodes: Array[String] | ||
|  | var progress: int = 0 | ||
|  | var layout_button: Button | ||
|  | 
 | ||
|  | 
 | ||
|  | func _ready() -> void: | ||
|  | 	custom_minimum_size = Vector2(100, 300) | ||
|  | 	arrange_nodes_button_hidden = true | ||
|  | 	minimap_enabled = false | ||
|  | 
 | ||
|  | 	layout_button = Button.new() | ||
|  | 	layout_button.flat = true | ||
|  | 	layout_button.focus_mode = Control.FOCUS_NONE | ||
|  | 	layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout) | ||
|  | 	get_zoom_hbox().add_child(layout_button) | ||
|  | 	_update_layout_button() | ||
|  | 
 | ||
|  | 
 | ||
|  | func _update_graph() -> void: | ||
|  | 	if updating_graph: | ||
|  | 		return | ||
|  | 
 | ||
|  | 	updating_graph = true | ||
|  | 
 | ||
|  | 	clear_connections() | ||
|  | 
 | ||
|  | 	for child in get_children(): | ||
|  | 		remove_child(child) | ||
|  | 		child.queue_free() | ||
|  | 
 | ||
|  | 	if not beehave_tree.is_empty(): | ||
|  | 		_add_nodes(beehave_tree) | ||
|  | 		_connect_nodes(beehave_tree) | ||
|  | 		_arrange_nodes.call_deferred(beehave_tree) | ||
|  | 
 | ||
|  | 	updating_graph = false | ||
|  | 
 | ||
|  | 
 | ||
|  | func _add_nodes(node: Dictionary) -> void: | ||
|  | 	if node.is_empty(): | ||
|  | 		return | ||
|  | 	var gnode := BeehaveGraphNode.new(horizontal_layout) | ||
|  | 	add_child(gnode) | ||
|  | 	gnode.title_text = node.name | ||
|  | 	gnode.name = node.id | ||
|  | 	gnode.icon = _get_icon(node.type.back()) | ||
|  | 
 | ||
|  | 	if node.type.has(&"BeehaveTree"): | ||
|  | 		gnode.set_slots(false, true) | ||
|  | 	elif node.type.has(&"Leaf"): | ||
|  | 		gnode.set_slots(true, false) | ||
|  | 	elif node.type.has(&"Composite") or node.type.has(&"Decorator"): | ||
|  | 		gnode.set_slots(true, true) | ||
|  | 
 | ||
|  | 	for child in node.get("children", []): | ||
|  | 		_add_nodes(child) | ||
|  | 
 | ||
|  | 
 | ||
|  | func _connect_nodes(node: Dictionary) -> void: | ||
|  | 	for child in node.get("children", []): | ||
|  | 		connect_node(node.id, 0, child.id, 0) | ||
|  | 		_connect_nodes(child) | ||
|  | 
 | ||
|  | 
 | ||
|  | func _arrange_nodes(node: Dictionary) -> void: | ||
|  | 	if arraging_nodes: | ||
|  | 		return | ||
|  | 
 | ||
|  | 	arraging_nodes = true | ||
|  | 
 | ||
|  | 	var tree_node := _create_tree_nodes(node) | ||
|  | 	tree_node.update_positions(horizontal_layout) | ||
|  | 	_place_nodes(tree_node) | ||
|  | 
 | ||
|  | 	arraging_nodes = false | ||
|  | 
 | ||
|  | 
 | ||
|  | func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode: | ||
|  | 	var tree_node := TreeNode.new(get_node(node.id), root) | ||
|  | 	for child in node.get("children", []): | ||
|  | 		var child_node := _create_tree_nodes(child, tree_node) | ||
|  | 		tree_node.children.push_back(child_node) | ||
|  | 	return tree_node | ||
|  | 
 | ||
|  | 
 | ||
|  | func _place_nodes(node: TreeNode) -> void: | ||
|  | 	node.item.position_offset = Vector2(node.x, node.y) | ||
|  | 	for child in node.children: | ||
|  | 		_place_nodes(child) | ||
|  | 
 | ||
|  | 
 | ||
|  | func _get_icon(type: StringName) -> Texture2D: | ||
|  | 	var classes := ProjectSettings.get_global_class_list() | ||
|  | 	for c in classes: | ||
|  | 		if c["class"] == type: | ||
|  | 			var icon_path := c.get("icon", String()) | ||
|  | 			if not icon_path.is_empty(): | ||
|  | 				return load(icon_path) | ||
|  | 	return null | ||
|  | 
 | ||
|  | 
 | ||
|  | func get_status(status: int) -> String: | ||
|  | 	if status == 0: | ||
|  | 		return "SUCCESS" | ||
|  | 	elif status == 1: | ||
|  | 		return "FAILURE" | ||
|  | 	return "RUNNING" | ||
|  | 
 | ||
|  | 
 | ||
|  | func process_begin(instance_id: int) -> void: | ||
|  | 	if not _is_same_tree(instance_id): | ||
|  | 		return | ||
|  | 
 | ||
|  | 	for child in get_children(): | ||
|  | 		child.set_meta("status", -1) | ||
|  | 
 | ||
|  | 
 | ||
|  | func process_tick(instance_id: int, status: int) -> void: | ||
|  | 	var node := get_node_or_null(str(instance_id)) | ||
|  | 	if node: | ||
|  | 		node.text = "Status: %s" % get_status(status) | ||
|  | 		node.set_status(status) | ||
|  | 		node.set_meta("status", status) | ||
|  | 		if status == 0 or status == 2: | ||
|  | 			if not active_nodes.has(node.name): | ||
|  | 				active_nodes.push_back(node.name) | ||
|  | 
 | ||
|  | 
 | ||
|  | func process_end(instance_id: int) -> void: | ||
|  | 	if not _is_same_tree(instance_id): | ||
|  | 		return | ||
|  | 
 | ||
|  | 	for child in get_children(): | ||
|  | 		var status := child.get_meta("status", -1) | ||
|  | 		match status: | ||
|  | 			0: | ||
|  | 				active_nodes.erase(child.name) | ||
|  | 				child.set_color(SUCCESS_COLOR) | ||
|  | 			1: | ||
|  | 				active_nodes.erase(child.name) | ||
|  | 				child.set_color(INACTIVE_COLOR) | ||
|  | 			2: | ||
|  | 				child.set_color(ACTIVE_COLOR) | ||
|  | 			_: | ||
|  | 				child.text = " " | ||
|  | 				child.set_status(status) | ||
|  | 				child.set_color(INACTIVE_COLOR) | ||
|  | 
 | ||
|  | 
 | ||
|  | func _is_same_tree(instance_id: int) -> bool: | ||
|  | 	return str(instance_id) == beehave_tree.get("id", "") | ||
|  | 
 | ||
|  | 
 | ||
|  | func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array: | ||
|  | 	var points: PackedVector2Array | ||
|  | 
 | ||
|  | 	from_position = from_position.round() | ||
|  | 	to_position = to_position.round() | ||
|  | 
 | ||
|  | 	points.push_back(from_position) | ||
|  | 
 | ||
|  | 	var mid_position := ((to_position + from_position) / 2).round() | ||
|  | 	if horizontal_layout: | ||
|  | 		points.push_back(Vector2(mid_position.x, from_position.y)) | ||
|  | 		points.push_back(Vector2(mid_position.x, to_position.y)) | ||
|  | 	else: | ||
|  | 		points.push_back(Vector2(from_position.x, mid_position.y)) | ||
|  | 		points.push_back(Vector2(to_position.x, mid_position.y)) | ||
|  | 
 | ||
|  | 	points.push_back(to_position) | ||
|  | 
 | ||
|  | 	return points | ||
|  | 
 | ||
|  | 
 | ||
|  | func _process(delta: float) -> void: | ||
|  | 	if not active_nodes.is_empty(): | ||
|  | 		progress += 10 if delta >= 0.05 else 1 | ||
|  | 		if progress >= 1000: | ||
|  | 			progress = 0 | ||
|  | 		queue_redraw() | ||
|  | 
 | ||
|  | 
 | ||
|  | func _draw() -> void: | ||
|  | 	if active_nodes.is_empty(): | ||
|  | 		return | ||
|  | 
 | ||
|  | 	var circle_size: float = max(3, 6 * zoom) | ||
|  | 	var progress_shift: float = PROGRESS_SHIFT * zoom | ||
|  | 
 | ||
|  | 	var connections := get_connection_list() | ||
|  | 	for c in connections: | ||
|  | 		if not c.from in active_nodes or not c.to in active_nodes: | ||
|  | 			continue | ||
|  | 		var from := get_node(String(c.from)) | ||
|  | 		var to := get_node(String(c.to)) | ||
|  | 
 | ||
|  | 		if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0: | ||
|  | 			return | ||
|  | 
 | ||
|  | 		var line := _get_connection_line(from.position + from.get_connection_output_position(c.from_port), to.position + to.get_connection_input_position(c.to_port)) | ||
|  | 
 | ||
|  | 		var curve = Curve2D.new() | ||
|  | 		for l in line: | ||
|  | 			curve.add_point(l) | ||
|  | 
 | ||
|  | 		var max_steps := int(curve.get_baked_length()) | ||
|  | 		var current_shift := progress % max_steps | ||
|  | 		var p := curve.sample_baked(current_shift) | ||
|  | 		draw_circle(p, circle_size, ACTIVE_COLOR) | ||
|  | 
 | ||
|  | 		var shift := current_shift - progress_shift | ||
|  | 		while shift >= 0: | ||
|  | 			draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) | ||
|  | 			shift -= progress_shift | ||
|  | 
 | ||
|  | 		shift = current_shift + progress_shift | ||
|  | 		while shift <= curve.get_baked_length(): | ||
|  | 			draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) | ||
|  | 			shift += progress_shift | ||
|  | 
 | ||
|  | 
 | ||
|  | func _update_layout_button() -> void: | ||
|  | 	layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON | ||
|  | 	layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout" |