Shared object for intractable items

I’ve created an Item(Resource) class to represent all the items in my game, it has name, icon and reference to a scene for the visuals. I started with having interaction components in the visuals scene for the item (Area3D for interaction) but then ran into issue because the interaction required a reference to the item creating circular dependencies.

My solution has been to create a WorldItem class that has the Area3D for interaction and an item. When an item is assign it is added as a child for the visuals and its collision shapes are duplicated to the WorldItem and the interaction component. I started with a scene for the nodes, but I ditched that and made it a code only node.

I’m looking for feedback on this approach, if there’s anything you would suggest changing or a pitfall I might be missing.

@tool
class_name WorldItem
extends RigidBody3D

@export var item: Item: # Resource holding item data
	set(value):
		item = value
		_connect_item()

var visual_root: Node3D # The container for the item model
var interactable: Interactable # Area3D
var pickupable: Pickupable = Pickupable.new()


func _ready() -> void:
	pickupable.item_picked_up.connect(func(_item: Item): queue_free())

	# We do this because duplicating keeps the nodes, but not the reference in the variable
	visual_root = get_node_or_null("VisualRoot")
	if not visual_root:
		visual_root = Node3D.new()
		visual_root.name = "VisualRoot"
		add_child(visual_root)

	# We do this because duplicating keeps the nodes, but not the reference in the variable
	interactable = get_node_or_null("Interactable")
	if not interactable:
		interactable = Interactable.new()
		interactable.name = "Interactable"
		add_child(interactable)

	interactable.interacted.connect(pickupable.interact)

	_connect_item()


func _reset() -> void:
	for child in visual_root.get_children():
		child.queue_free()

	# Remove old collision shapes
	for child in get_children():
		if child is CollisionShape3D:
			child.queue_free()

	# Remove old collision shapes from interactable
	if interactable:
		for child in interactable.get_children():
			if child is CollisionShape3D:
				child.queue_free()

	pickupable.item = null


func _connect_item() -> void:
	if not is_node_ready():
		return

	_reset()

	if item == null or !item.prefab_path or item.prefab_path == "":
		return

	pickupable.item = item

	var visual_scene = item.create_scene()

	# Add the item scene with the mesh
	visual_root.add_child(visual_scene)

	_add_collision_shapes(visual_scene)


func _add_collision_shapes(node: Node) -> void:
	for child in node.get_children():
		if child is CollisionShape3D:
			# duplicate the collision shapes from the item scene to here
			add_child(child.duplicate())
			# duplicate the collision shapes for the interactable area (Area3D)
			interactable.add_child(child.duplicate())
			# disable collision on the item scene to prevent double collision
			# we do this instead of removing the original shape from the visual scene
			# so the item scene tree is intact otherwise duplicating generates errors
			child.disabled = true