peter-kish / gloot

A universal inventory system for the Godot game engine.
MIT License
638 stars 28 forks source link

Signal for grid inventories once an item is dropped outside inventory #236

Closed TheYellowArchitect closed 1 month ago

TheYellowArchitect commented 1 month ago

I am trying to implement a system where if you drag an item outside a grid inventory, it drops to the 3D game world. The first requirement needed for this, and my request, is to have a signal which triggers for grid inventories if you stop dragging an item outside the inventory, returning the item and the screen position it was dropped (Vector2)

And each inventory grid should also have @export var remove_item_on_drop: bool which triggers the above signal, if the item is dropped on a non-inventory, which deletes the item entirely (with the above signal, I can just map it to 3D items)

As for the existing signal item_dropped I cannot get it to work :thinking: Specifically, this code on the class ctrl_grid_basic.gd

func _on_item_drop(zone: CtrlDropZone, drop_position: Vector2, ctrl_inventory_item: CtrlInventoryItemRect) -> void:
    var item: InventoryItem = ctrl_inventory_item.item
    # The item might have been freed in case the item stack has been moved and merged with another
    # stack.
    print("Dropped") #Debug message doesn't show when I drag an item out of the inventory
    if is_instance_valid(item) and inventory.has_item(item):
        if zone == null:
            item_dropped.emit(item, drop_position + ctrl_inventory_item.position)

I assume the proper location is where the item preview is?

func _get_drag_data(at_position: Vector2):
    if !_enabled:
        return null

    _grabbed_dragable = self
    _grab_offset = at_position * get_global_transform().get_scale()
    dragable_grabbed.emit(_grabbed_dragable, _grab_offset)
    grabbed.emit(_grab_offset)

    var preview = Control.new()
    var sub_preview = create_preview()
    sub_preview.position = -get_grab_offset()
    preview.add_child(sub_preview)
    set_drag_preview(preview)
    return self

Trying to hack away the code, I detect when the preview is destroyed (when dragging ends, see set_drag_preview(preview), and connect a signal right there. But I have no way to find if it ended up in an inventory or outside.

func _get_drag_data(at_position: Vector2):
    if !_enabled:
        return null

    _grabbed_dragable = self
    _grab_offset = at_position * get_global_transform().get_scale()
    dragable_grabbed.emit(_grabbed_dragable, _grab_offset)
    grabbed.emit(_grab_offset)

    var preview = Control.new()
    var sub_preview = create_preview()
    sub_preview.position = -get_grab_offset()
    preview.add_child(sub_preview)
    set_drag_preview(preview)
    preview.tree_exited.connect(preview_died) #NEW LINE
    return self

func preview_died():
    print("preview no longer exists")

Also in that class is the signal dropped(zone, position) which is completely unused, perhaps it was meant for this?

peter-kish commented 1 month ago

I should probably clarify in the documentation that item_dropped only triggers if an item is dropped on top of an CtrlInventoryGrid. But I think the best way to handle item drops outside the inventory is to first create a transparent control that is positioned behind the UI and spans the entire screen. Then you should be able to implement _drop_data and _can_drop_data for that control:

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
    return true

func _drop_data(at_position: Vector2, data: Variant) -> void:
    print("Item dropped: %s" % str(data.item))
    # Remove the item from the inventory, handle the item drop etc.

Note that in v2.x the type of the data parameter is defined inside addons/gloot/ui/ctrl_inventory_item_rect.gd and the actual InventoryItem must be accessed via data.item. This is something that has been fixed in v3.x (still WIP) where the drop data is of type InventoryItem.

TheYellowArchitect commented 1 month ago

I should probably clarify in the documentation that item_dropped only triggers if an item is dropped on top of an CtrlInventoryGrid.

:+1:

But I think the best way to handle item drops outside the inventory is to first create a transparent control that is positioned behind the UI and spans the entire screen. Then you should be able to implement _drop_data and _can_drop_data for that control:

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
    return true

func _drop_data(at_position: Vector2, data: Variant) -> void:
    print("Item dropped: %s" % str(data.item))
    # Remove the item from the inventory, handle the item drop etc.

omw to try this, thank you for the proposed fix and the swift reply! :heart:

Note that in v2.x the type of the data parameter is defined inside addons/gloot/ui/ctrl_inventory_item_rect.gd and the actual InventoryItem must be accessed via data.item. This is something that has been fixed in v3.x (still WIP) where the drop data is of type InventoryItem.

I will keep this in mind, when implementing the above, ty!

TheYellowArchitect commented 1 month ago

I placed the above code on the class inventory_grid_stacked_transfer.gd of the scene inventory_grid_stacked_transfer.tscn and it works perfectly! Pretty clean to work with just these 2 functions added (idk if there is code to detect if a control node has that function, but to work so easily was unexpected) So clean that I would suggest a quick PR for both grid demos, until this feature is implemented via a proper PR which doesn't need a seperate Control to trigger/detect (basically OP's suggestion)

That said, I also tried putting a seperate Control node to cover the entire screen. It works exclusively if the Control node is ABOVE the UI inventories. Otherwise, if it is BELOW in the hierarchy, despite all possible mouse filter combinations, it doesn't work (either blocks inventory mouse, or isn't detected)

My use-case is covered, and am surprised it was so simple (literally copy-pasted lol) and I thank you for the quick answer! I will only keep this issue open for the official PR (where there is a signal on the item itself, or the inventory grid itself, instead of having this logic above them)

peter-kish commented 1 month ago

idk if there is code to detect if a control node has that function, but to work so easily was unexpected

This is actually a built-in Godot feature and the usual way of doing drag&drop in Godot. I didn't have to implement any logic that calls _can_drop_data or _drop_data, the engine already handles that. I just provide it the data associated with the drag&drop operation (the dragged control node in my case).

I would suggest a quick PR for both grid demos

Good idea 👍 It's not very obvious how to implement such functionality, while I think it is pretty common in games.

a proper PR which doesn't need a seperate Control to trigger/detect (basically OP's suggestion)

This would be good to have, but I'm trying to stick to the Godot-way of doing things and it seems like there is no straight forward way of detecting if a control has been dropped onto "empty space". There is the NOTIFICATION_DRAG_END notification that can be used with Viewport.gui_is_drag_successful() to detect failed drops, but in that case the drag data is null and it's difficult to determine which item has been dropped.

I also tried putting a seperate Control node to cover the entire screen. It works exclusively if the Control node is ABOVE the UI inventories.

That's weird, I tested it recently and it should work (either with GLoot or plain Godot controls). Sometimes it happens that there's a second control covering the same area and blocking the drop, so it might be worth double-checking if there's a container or some other control that's transparent, making it not so obvious.

TheYellowArchitect commented 1 month ago

This would be good to have, but I'm trying to stick to the Godot-way of doing things and it seems like there is no straight forward way of detecting if a control has been dropped onto "empty space". There is the NOTIFICATION_DRAG_END notification that can be used with Viewport.gui_is_drag_successful() to detect failed drops, but in that case the drag data is null and it's difficult to determine which item has been dropped.

I see that it bloats the code. So I suggest the following PR to implement this feature and close this issue: The code you posted

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
    return true

func _drop_data(at_position: Vector2, data: Variant) -> void:
    print("Item dropped: %s" % str(data.item))
    ctrl_inventory_left.inventory.remove_item(data.item)
    ctrl_inventory_right.inventory.remove_item(data.item)
    #Custom logic for handling the item drop here 👈

placed on the root of the 3 grid demo scenes inventory_grid_stacked_ex_transfer.gd, inventory_grid_ex_transfer.gd, inventory_grid_transfer.gd, and a wiki/docs entry so its visible/known to all users, would close this issue :+1: