feat(snap-receiver): implement snap detection, position snapping, and pose animation trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
## SnapReceiver — scans for nearby SnapPoints when the parent Character is released.
|
||||
## Attach as child of Character. Full implementation connects to DragDropComponent signals.
|
||||
## Attach as child of Character. Connects automatically to DragDropComponent signals.
|
||||
class_name SnapReceiver extends Node
|
||||
|
||||
const SCAN_RADIUS: float = 80.0
|
||||
@@ -10,6 +10,44 @@ var _character: Character
|
||||
|
||||
func _ready() -> void:
|
||||
_character = get_parent() as Character
|
||||
var drag: DragDropComponent = _character.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 _current_snap != null:
|
||||
_current_snap.unsnap()
|
||||
_current_snap = null
|
||||
_character.set_animation_state("held")
|
||||
|
||||
|
||||
func _on_drag_released(_pos: Vector2) -> void:
|
||||
var nearest: SnapPoint = _find_nearest_accepting_snap()
|
||||
if nearest != null:
|
||||
_current_snap = nearest
|
||||
nearest.snap(_character)
|
||||
_character.global_position = nearest.global_position
|
||||
_character.set_animation_state(nearest.pose)
|
||||
else:
|
||||
_character.set_animation_state("idle")
|
||||
|
||||
|
||||
func _find_nearest_accepting_snap() -> SnapPoint:
|
||||
var best: SnapPoint = null
|
||||
var best_dist: float = SCAN_RADIUS
|
||||
for node: Node in get_tree().get_nodes_in_group("snap_points"):
|
||||
var snap_point: SnapPoint = node as SnapPoint
|
||||
if snap_point == null:
|
||||
continue
|
||||
if not snap_point.accepts(_character):
|
||||
continue
|
||||
var dist: float = _character.global_position.distance_to(snap_point.global_position)
|
||||
if dist < best_dist:
|
||||
best_dist = dist
|
||||
best = snap_point
|
||||
return best
|
||||
|
||||
|
||||
func get_current_snap() -> SnapPoint:
|
||||
@@ -17,6 +55,8 @@ func get_current_snap() -> SnapPoint:
|
||||
|
||||
|
||||
func force_unsnap() -> void:
|
||||
if _current_snap != null:
|
||||
if _current_snap == null:
|
||||
return
|
||||
_current_snap.unsnap()
|
||||
_current_snap = null
|
||||
_character.set_animation_state("idle")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
## Tests for SnapReceiver — snap detection when Character is released near a SnapPoint.
|
||||
extends GutTest
|
||||
|
||||
var _char: Character
|
||||
var _snap: SnapPoint
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_char = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||
add_child_autofree(_char)
|
||||
_snap = SnapPoint.new()
|
||||
_snap.pose = "sitting"
|
||||
add_child_autofree(_snap)
|
||||
|
||||
|
||||
func _get_receiver() -> SnapReceiver:
|
||||
return _char.get_node("SnapReceiver") as SnapReceiver
|
||||
|
||||
|
||||
func test_snap_receiver_exists_on_character() -> void:
|
||||
assert_not_null(_get_receiver())
|
||||
|
||||
|
||||
func test_no_current_snap_initially() -> void:
|
||||
assert_null(_get_receiver().get_current_snap())
|
||||
|
||||
|
||||
func test_snap_detected_when_character_released_within_radius() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_char.global_position = Vector2(50.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||
assert_eq(_get_receiver().get_current_snap(), _snap)
|
||||
|
||||
|
||||
func test_no_snap_when_released_outside_radius() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_char.global_position = Vector2(200.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(200.0, 0.0))
|
||||
assert_null(_get_receiver().get_current_snap())
|
||||
|
||||
|
||||
func test_character_position_set_to_snap_point_on_snap() -> void:
|
||||
_snap.global_position = Vector2(100.0, 100.0)
|
||||
_char.global_position = Vector2(120.0, 100.0)
|
||||
_get_receiver()._on_drag_released(Vector2(120.0, 100.0))
|
||||
assert_eq(_char.global_position, Vector2(100.0, 100.0))
|
||||
|
||||
|
||||
func test_character_animation_set_to_snap_pose_on_snap() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_snap.pose = "lying"
|
||||
_char.global_position = Vector2(50.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||
assert_eq(_char.get_animation_state(), "lying")
|
||||
|
||||
|
||||
func test_pickup_clears_current_snap() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_char.global_position = Vector2(50.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||
_get_receiver()._on_drag_picked_up(Vector2(50.0, 0.0))
|
||||
assert_null(_get_receiver().get_current_snap())
|
||||
|
||||
|
||||
func test_pickup_frees_snap_point() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_char.global_position = Vector2(50.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||
_get_receiver()._on_drag_picked_up(Vector2(50.0, 0.0))
|
||||
assert_true(_snap.is_free())
|
||||
|
||||
|
||||
func test_second_character_cannot_snap_to_occupied_point() -> void:
|
||||
_snap.global_position = Vector2(0.0, 0.0)
|
||||
_char.global_position = Vector2(50.0, 0.0)
|
||||
_get_receiver()._on_drag_released(Vector2(50.0, 0.0))
|
||||
var char2: Character = preload("res://scenes/characters/Character.tscn").instantiate() as Character
|
||||
add_child_autofree(char2)
|
||||
char2.global_position = Vector2(60.0, 0.0)
|
||||
var recv2: SnapReceiver = char2.get_node("SnapReceiver") as SnapReceiver
|
||||
recv2._on_drag_released(Vector2(60.0, 0.0))
|
||||
assert_null(recv2.get_current_snap())
|
||||
Reference in New Issue
Block a user