feat(items): add HoldableItem with hand slot detection, fix detach_item position
- Character registers in "characters" group on _ready for group scanning - detach_item saves/restores global_position after reparenting - New HoldableItem base class: scans "characters" group on drag_released, attaches to nearest free hand within 60px radius, detaches on pickup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ func _ready() -> void:
|
|||||||
if data != null:
|
if data != null:
|
||||||
_update_visual_state()
|
_update_visual_state()
|
||||||
_refresh_outfit_layers()
|
_refresh_outfit_layers()
|
||||||
|
add_to_group("characters")
|
||||||
|
|
||||||
|
|
||||||
func set_state(new_state: CharacterData.State) -> void:
|
func set_state(new_state: CharacterData.State) -> void:
|
||||||
@@ -110,10 +111,12 @@ func detach_item(hand: String) -> Node2D:
|
|||||||
if slot == null or slot.get_child_count() == 0:
|
if slot == null or slot.get_child_count() == 0:
|
||||||
return null
|
return null
|
||||||
var item: Node2D = slot.get_child(0) as Node2D
|
var item: Node2D = slot.get_child(0) as Node2D
|
||||||
|
var saved_pos: Vector2 = item.global_position
|
||||||
slot.remove_child(item)
|
slot.remove_child(item)
|
||||||
var scene_parent: Node = get_parent()
|
var scene_parent: Node = get_parent()
|
||||||
if scene_parent != null:
|
if scene_parent != null:
|
||||||
scene_parent.add_child(item)
|
scene_parent.add_child(item)
|
||||||
|
item.global_position = saved_pos
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
## HoldableItem — Node2D that can be held in a Character's HandLeft or HandRight slot.
|
||||||
|
## Attach DragDropComponent as a child. On drag_released scans "characters" group for
|
||||||
|
## the nearest free hand slot within HAND_SLOT_RADIUS.
|
||||||
|
class_name HoldableItem extends Node2D
|
||||||
|
|
||||||
|
signal item_picked_up(item: HoldableItem)
|
||||||
|
signal item_placed(item: HoldableItem)
|
||||||
|
|
||||||
|
const HAND_SLOT_RADIUS: float = 60.0
|
||||||
|
|
||||||
|
@export var item_id: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var drag: DragDropComponent = get_node_or_null("DragDropComponent") as DragDropComponent
|
||||||
|
if drag != null:
|
||||||
|
drag.drag_picked_up.connect(_on_drag_picked_up)
|
||||||
|
drag.drag_released.connect(_on_drag_released)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_picked_up(_pos: Vector2) -> void:
|
||||||
|
if is_in_hand_slot():
|
||||||
|
_detach_from_hand_slot()
|
||||||
|
item_picked_up.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_released(_pos: Vector2) -> void:
|
||||||
|
var result: Array = _find_nearest_free_hand_slot()
|
||||||
|
if not result.is_empty():
|
||||||
|
var character: Character = result[0] as Character
|
||||||
|
var hand: String = result[1] as String
|
||||||
|
character.attach_item(hand, self)
|
||||||
|
item_placed.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func is_in_hand_slot() -> bool:
|
||||||
|
var p: Node = get_parent()
|
||||||
|
if p == null:
|
||||||
|
return false
|
||||||
|
return p.name == "HandLeft" or p.name == "HandRight"
|
||||||
|
|
||||||
|
|
||||||
|
func _detach_from_hand_slot() -> void:
|
||||||
|
var hand_slot: Node = get_parent()
|
||||||
|
var character: Character = hand_slot.get_parent() as Character
|
||||||
|
if character == null:
|
||||||
|
return
|
||||||
|
var hand: String = "left" if hand_slot.name == "HandLeft" else "right"
|
||||||
|
character.detach_item(hand)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_nearest_free_hand_slot() -> Array:
|
||||||
|
var best_dist: float = HAND_SLOT_RADIUS
|
||||||
|
var best_character: Character = null
|
||||||
|
var best_hand: String = ""
|
||||||
|
for node: Node in get_tree().get_nodes_in_group("characters"):
|
||||||
|
var character: Character = node as Character
|
||||||
|
if character == null:
|
||||||
|
continue
|
||||||
|
for hand: String in ["left", "right"]:
|
||||||
|
if not character.is_hand_free(hand):
|
||||||
|
continue
|
||||||
|
var slot: Node2D = character.get_node_or_null("Hand" + hand.capitalize()) as Node2D
|
||||||
|
if slot == null:
|
||||||
|
continue
|
||||||
|
var dist: float = global_position.distance_to(slot.global_position)
|
||||||
|
if dist < best_dist:
|
||||||
|
best_dist = dist
|
||||||
|
best_character = character
|
||||||
|
best_hand = hand
|
||||||
|
if best_character == null:
|
||||||
|
return []
|
||||||
|
return [best_character, best_hand]
|
||||||
@@ -169,3 +169,17 @@ func test_detach_returns_item() -> void:
|
|||||||
func test_detach_from_empty_hand_returns_null() -> void:
|
func test_detach_from_empty_hand_returns_null() -> void:
|
||||||
var returned: Node2D = _char.detach_item("left")
|
var returned: Node2D = _char.detach_item("left")
|
||||||
assert_null(returned)
|
assert_null(returned)
|
||||||
|
|
||||||
|
|
||||||
|
func test_character_is_in_characters_group() -> void:
|
||||||
|
assert_true(_char.is_in_group("characters"))
|
||||||
|
|
||||||
|
|
||||||
|
func test_detach_item_preserves_global_position() -> void:
|
||||||
|
var item: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
_char.global_position = Vector2(200.0, 300.0)
|
||||||
|
_char.attach_item("left", item)
|
||||||
|
var expected_global: Vector2 = _char.get_node("HandLeft").global_position
|
||||||
|
_char.detach_item("left")
|
||||||
|
assert_eq(item.global_position, expected_global)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
## Tests for HoldableItem — hand slot attachment on drag release.
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_id_default_empty() -> void:
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
assert_eq(item.item_id, "")
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_is_not_in_hand_initially() -> void:
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
assert_false(item.is_in_hand_slot())
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_attaches_to_nearest_free_hand() -> void:
|
||||||
|
var character: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(character)
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
item.item_id = "test_item"
|
||||||
|
item.global_position = character.get_node("HandLeft").global_position
|
||||||
|
item._on_drag_released(item.global_position)
|
||||||
|
assert_true(item.is_in_hand_slot())
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_does_not_attach_if_no_character_in_range() -> void:
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
item.global_position = Vector2(9999.0, 9999.0)
|
||||||
|
item._on_drag_released(item.global_position)
|
||||||
|
assert_false(item.is_in_hand_slot())
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_does_not_attach_to_occupied_hand() -> void:
|
||||||
|
var character: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(character)
|
||||||
|
var item1: Node2D = Node2D.new()
|
||||||
|
var item2: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item1)
|
||||||
|
add_child_autofree(item2)
|
||||||
|
character.attach_item("left", item1)
|
||||||
|
var item_filler: Node2D = Node2D.new()
|
||||||
|
add_child_autofree(item_filler)
|
||||||
|
character.attach_item("right", item_filler)
|
||||||
|
item2.global_position = character.global_position
|
||||||
|
item2._on_drag_released(item2.global_position)
|
||||||
|
assert_false(item2.is_in_hand_slot())
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_detaches_on_pickup_when_in_slot() -> void:
|
||||||
|
var character: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(character)
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
character.attach_item("left", item)
|
||||||
|
assert_true(item.is_in_hand_slot())
|
||||||
|
item._on_drag_picked_up(item.global_position)
|
||||||
|
assert_false(item.is_in_hand_slot())
|
||||||
|
|
||||||
|
|
||||||
|
func test_holdable_item_detach_preserves_global_position() -> void:
|
||||||
|
var character: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||||
|
add_child_autofree(character)
|
||||||
|
var item: HoldableItem = HoldableItem.new()
|
||||||
|
add_child_autofree(item)
|
||||||
|
character.attach_item("left", item)
|
||||||
|
var hand_pos: Vector2 = character.get_node("HandLeft").global_position
|
||||||
|
item._on_drag_picked_up(hand_pos)
|
||||||
|
assert_eq(item.global_position, hand_pos)
|
||||||
Reference in New Issue
Block a user