diff --git a/src/dev-util/room_template.gd b/src/dev-util/room_template.gd index 914d0cf..858a4f9 100644 --- a/src/dev-util/room_template.gd +++ b/src/dev-util/room_template.gd @@ -24,11 +24,11 @@ func _ready() -> void: func disable()-> void: is_active = false set_process_input(false) - set_process(false) - + set_process(false) + func get_ready(): pass - + func load(): # Override this function to load the state of the chapter from State.save_game pass @@ -48,13 +48,13 @@ func pull_save_state(_save: SaveGame) -> void: ## Attempts to find player controller and restore position/rotation from save func restore_player_from_save(save: SaveGame) -> void: var player: PlayerController = null - + # Try to find player controller in common locations if has_node("%PlayerController"): player = get_node("%PlayerController") elif has_node("logic/PlayerController"): player = get_node("logic/PlayerController") - + if player and player is PlayerController: player.restore_from_save(save) else: @@ -73,4 +73,3 @@ func unload(): ## Override in subclasses to add custom scene preparation logic func prepare_scene_start(_scene_id: Scenes.id, _is_repeating: bool) -> void: await get_tree().process_frame # Dummy wait for LSP warning otherwise - diff --git a/src/logic-scenes/board/card-board.gd b/src/logic-scenes/board/card-board.gd index b0f05ed..06d63c3 100644 --- a/src/logic-scenes/board/card-board.gd +++ b/src/logic-scenes/board/card-board.gd @@ -40,17 +40,12 @@ var mementos_collected: int = 0: var selection: Draggable = null: set(value): - # this makes sure no accidental context switches can happen while a card is being dragged - if current_context == DRAG: return - - # Deselect current - if selection: - selection.highlighted = false - - # Select new + if selection == value: return + + # Select & highlight new + if selection: selection.highlighted = false selection = value - if selection: - selection.highlighted = true + if selection: selection.highlighted = true # Are we selecting cards or stickies? if selection is Card: @@ -88,6 +83,7 @@ func _smooth(current: Vector2, goal: Vector2, delta: float) -> Vector2: var k := pow(0.1, 60.0 * delta) return (1.0-k) * current + k * goal + func _process(delta: float): var zone_position := Vector2(notezone.get_screen_position().x + sticky_width / 3.0, sticky_height) @@ -207,24 +203,21 @@ func is_in_dropzone(to_check: Draggable) -> bool: # Called by notes when a mouse event needs handling -func handle_mouse_button(input: InputEventMouseButton, to_handle = selection) -> void: - # Prevent dragging multiple nodes at once - if current_context == DRAG and selection: - return +func handle_mouse_button(input: InputEventMouseButton, target: Draggable) -> void: # === DRAG START === - if input.button_index == MOUSE_BUTTON_LEFT and input.pressed: - _start_drag(to_handle) + if input.button_index == MOUSE_BUTTON_LEFT and input.is_pressed(): + _start_drag(target) return # === DRAG END === - if input.button_index == MOUSE_BUTTON_LEFT and not input.pressed: - _end_drag(to_handle) + if input.button_index == MOUSE_BUTTON_LEFT and not input.is_released(): + _end_drag(target) return ## Starts a drag operation for the given draggable -func _start_drag(draggable: Dragg able) -> void: +func _start_drag(draggable: Draggable) -> void: selection = draggable current_context = DRAG @@ -234,24 +227,36 @@ func _start_drag(draggable: Dragg able) -> void: ## Ends a drag operation and handles the drop func _end_drag(draggable: Draggable) -> void: - if not draggable: return - - draggable.end_drag() - - # Let draggable find its own drop target - var drop_target := draggable.find_drop_target() - - # Execute the drop - if drop_target and Draggable.is_drop_target(drop_target): - var result = drop_target.handle_drop(draggable) - pass - + selection = draggable + # Cleanup and state update current_context = NAVIGATE + var destination := draggable.end_drag() + + if not destination and draggable is StickyNote: + # reclaim if necessary + pass + + if destination and destination is Card: + # attach / unattach + pass + + if destination and destination is StickyNote: + # unattach and attach to parent + pass + + # Let draggable find its own drop target + #var drop_target := draggable.find_drop_target() + + # Execute the drop + #if drop_target and Draggable.is_drop_target(drop_target): + # var result = drop_target.handle_drop(draggable) + # pass + # Check win condition if sticky was attached to card if draggable is StickyNote and draggable.is_attached: - check_board_comnpletion() + check_board_completion() func reclaim_sticky(note: StickyNote): @@ -259,7 +264,7 @@ func reclaim_sticky(note: StickyNote): notes.append(note) -func check_board_comnpletion(): +func check_board_completion(): if is_board_complete(): if not board_was_completed: board_was_completed = true @@ -316,20 +321,44 @@ func give_lore_feedback(): complete = true # Mark area that was hovered over as currently selected -func handle_hover(to_handle: Area2D) -> void: +func handle_hover(draggable: Draggable) -> void: + # If we're hovering with the mouse without clicking, that updates our selection - if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): selection = to_handle + if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): return + + +func _sort_by_proximity_and_depth(draggables: Array) -> Array[Draggable]: + var result : Array[Draggable] = [] + result.append_array(draggables) + result.sort_custom(_by_mouse) + var depth := len(result) * 2 + for item in result: + depth -= 1 + item.z_index = depth + return result + + +func _nearest_hovered(candidates: Array[Draggable]) -> Draggable: + for candidate in candidates: + if candidate.mouse_over: return candidate + return null -func _spatial(a: Draggable, b: Draggable) -> bool: + +func _by_spatial(a: Draggable, b: Draggable) -> bool: return a.position.x + a.position.y * 100 > b.position.x + b.position.y * 100 +func _by_mouse(a: Draggable, b: Draggable) -> bool: + var mouse_pos : Vector2 = get_viewport().get_mouse_position() + return (a.position-mouse_pos).length() < (b.position-mouse_pos).length() + + ## Call this after bulk loading to fix child order / z-index issues func _sort_by_positions() -> void: - cards.sort_custom(_spatial) - notes.sort_custom(_spatial) + cards.sort_custom(_by_spatial) + notes.sort_custom(_by_spatial) # === DROP TARGET PATTERN IMPLEMENTATION === @@ -348,18 +377,16 @@ func handle_drop(draggable: Draggable) -> int: # Takes the inputs for control inputs func _input(event) -> void: - if event.is_action_pressed("ui_cancel"): + if event is InputEventAction and event.is_action_pressed("ui_cancel"): closed.emit() get_viewport().set_input_as_handled() - if event is InputEventMouse: - # makes sure to pass release events so notes do not get attached to the mouse while the cursor leaves the area. - if event is InputEventMouseButton and current_context == DRAG: - if event.button_index == MOUSE_BUTTON_LEFT and not event.pressed: - handle_mouse_button(event) - get_viewport().set_input_as_handled() - else: - return + + if event is InputEventMouseMotion and not event.is_action_pressed("mouse_left"): + var candidate := _nearest_hovered(_sort_by_proximity_and_depth(notes)) + if not candidate: + candidate = _nearest_hovered(_sort_by_proximity_and_depth(cards)) + selection = candidate ## Saves board state directly to SaveGame resource diff --git a/src/logic-scenes/board/card.gd b/src/logic-scenes/board/card.gd index 9628492..c216db2 100644 --- a/src/logic-scenes/board/card.gd +++ b/src/logic-scenes/board/card.gd @@ -38,55 +38,41 @@ var transfor_arr: Array[Transform2D] = [ @onready var background_sprite: AnimatedSprite2D = $AnimatedSprite2D @export var picked_random: bool = false - @export var wiggle_strength: float = 0.2 @export var wiggle_speed: float = 5 @export_range(1, 2) var scale_bump: float = 1.05 @export_range(1.0, 10.0) var bounce_speed: float = 5 -@export_range(1.0, 2.0) var highlight_brightness: float = 1.4 + +@export_color_no_alpha var highlight_color: Color = Color(1.4, 1.4, 1.4) ## Override set_highlight to add visual feedback for cards func set_highlight(value: bool) -> void: - if value != _highlighted: - _highlighted = value + if value == _highlighted: return + _highlighted = value + + if scale_tween: scale_tween.kill() + if wiggle_tween: wiggle_tween.kill() + if brightness_tween: brightness_tween.kill() + + if _highlighted: + scale_tween = get_tree().create_tween() + scale_tween.tween_property(self, "scale", Vector2(scale_bump, scale_bump), 0.1) + wiggle_tween = get_tree().create_tween() + wiggle_tween.tween_property(self, "wiggle_intensity", 1, 0.2) + brightness_tween = get_tree().create_tween() + brightness_tween.set_parallel(true) + brightness_tween.tween_property(background_sprite, "modulate", highlight_color, 0.15) + brightness_tween.tween_property(label, "modulate", highlight_color, 0.15) + else: + scale_tween = get_tree().create_tween() + scale_tween.tween_property(self, "scale", Vector2(1, 1), 0.3) + wiggle_tween = get_tree().create_tween() + wiggle_tween.tween_property(self, "wiggle_intensity", 0, 0.5) + brightness_tween = get_tree().create_tween() + brightness_tween.set_parallel(true) + brightness_tween.tween_property(background_sprite, "modulate", Color.WHITE, 0.2) + brightness_tween.tween_property(label, "modulate", Color.WHITE, 0.2) - if is_inside_tree() and is_node_ready(): - if scale_tween: scale_tween.kill() - if wiggle_tween: wiggle_tween.kill() - if brightness_tween: brightness_tween.kill() - if _highlighted: - scale_tween = get_tree().create_tween() - scale_tween.tween_property(self, "scale", Vector2(scale_bump, scale_bump), 0.1) - wiggle_tween = get_tree().create_tween() - wiggle_tween.tween_property(self, "wiggle_intensity", 1, 0.2) - brightness_tween = get_tree().create_tween() - brightness_tween.set_parallel(true) - brightness_tween.tween_property(background_sprite, "modulate", Color(highlight_brightness, highlight_brightness, highlight_brightness), 0.15) - brightness_tween.tween_property(label, "modulate", Color(highlight_brightness, highlight_brightness, highlight_brightness), 0.15) - else: - scale_tween = get_tree().create_tween() - scale_tween.tween_property(self, "scale", Vector2(1, 1), 0.3) - wiggle_tween = get_tree().create_tween() - wiggle_tween.tween_property(self, "wiggle_intensity", 0, 0.5) - brightness_tween = get_tree().create_tween() - brightness_tween.set_parallel(true) - brightness_tween.tween_property(background_sprite, "modulate", Color.WHITE, 0.2) - brightness_tween.tween_property(label, "modulate", Color.WHITE, 0.2) - else: - if _highlighted: - scale = Vector2(scale_bump, scale_bump) - wiggle_intensity = 1 - if background_sprite: - background_sprite.modulate = Color(highlight_brightness, highlight_brightness, highlight_brightness) - if label: - label.modulate = Color(highlight_brightness, highlight_brightness, highlight_brightness) - else: - scale = Vector2(1,1) - wiggle_intensity = 0 - if background_sprite: - background_sprite.modulate = Color.WHITE - if label: - label.modulate = Color.WHITE @export var voice_line: AudioStream = null @export var is_dragable: bool = false @@ -158,6 +144,7 @@ func init(card_name: String = "card", own_id:StringName = "-1") -> void: func _ready(): + super._ready() input_event.connect(_on_input_event) _handle_wiggle(0) @@ -202,7 +189,6 @@ func _process(delta: float) -> void: func _handle_wiggle(delta): wiggle_pos += delta * wiggle_speed * wiggle_intensity - rotation = noise.get_noise_1d(wiggle_pos)*wiggle_strength @@ -212,26 +198,18 @@ func _input(event: InputEvent) -> void: is_dragged = false func _on_mouse_entered() -> void: - if not Input.is_action_pressed("mouse_left"): - # Do nothing if mouse hovers over sticky_note (it has higher priority) - var sticky = get_attached_sticky_note() - if sticky and sticky.highlighted: - return - var board = _get_board() - if board: - board.handle_hover(self) + super._on_mouse_entered() func _on_mouse_exited(): - highlighted = false + super._on_mouse_exited() if burn_state == burned.SINGED: burn_state = burned.NOT func _on_input_event(_viewport, event, _shape_idx): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed: - var board = _get_board() - if board and highlighted: + if highlighted: mouse_offset = get_viewport().get_mouse_position() - position - board.handle_mouse_button(event, self) + if _get_board(): _get_board().handle_mouse_button(event, self) func _move_card(): if is_dragged: @@ -284,19 +262,6 @@ func exchange_sticky_note_with(new_note: StickyNote) -> StickyNote: attach_sticky_note(new_note) return tmp -# This makes sure this node highlights itself when focus has left the sticky note. -func check_hover(): - # Re-trigger hover handling - parent will decide if this should be highlighted - _on_mouse_entered() - -func reclaim_sticky_note(): - var sticky = get_attached_sticky_note() - if not sticky: - return - sticky.tween_transform_to(Transform2D(0, to_global(sticky_note_position))) - await sticky.transform_tween_finished - sticky.reparent(self) - # === DROP TARGET PATTERN IMPLEMENTATION === @@ -348,7 +313,7 @@ func find_drop_target() -> Node: ## Walks up the scene tree to find the CardBoard func _get_board() -> CardBoard: - var node = get_parent() + var node := get_parent() while node: if node is CardBoard: return node diff --git a/src/logic-scenes/board/card.tscn b/src/logic-scenes/board/card.tscn index 92b4908..239b650 100644 --- a/src/logic-scenes/board/card.tscn +++ b/src/logic-scenes/board/card.tscn @@ -39,6 +39,3 @@ text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 3 - -[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] -[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] diff --git a/src/logic-scenes/board/draggable.gd b/src/logic-scenes/board/draggable.gd index 59ac0a9..fe93df8 100644 --- a/src/logic-scenes/board/draggable.gd +++ b/src/logic-scenes/board/draggable.gd @@ -14,6 +14,8 @@ enum DropResult { EXCHANGED # Swap occurred, exchanged item needs handling } +var mouse_over: bool = false + var is_dragged: bool = false: set(dragged): is_dragged = dragged @@ -37,9 +39,35 @@ var _drag_start_position: Vector2 var _mouse_drag_offset: Vector2 var _drag_source: Node = null # Where the drag started from +## === SETUP ### +func _ready() -> void: + mouse_entered.connect(_on_mouse_entered) + mouse_exited.connect(_on_mouse_exited) + + +func _get_hover_handler() -> Node: + var parent := get_parent() + while parent and not parent.has_method("handle_hover"): + parent = parent.get_parent() + return parent + ## === DRAG LIFECYCLE METHODS === ## Override these in Card and StickyNote for specific behavior +func _on_mouse_entered() -> void: + prints("Draggable[base]._on_mouse_entered", self, self.name) + mouse_over = true + var handler := _get_hover_handler() + if handler: handler.handle_hover(self) + + +func _on_mouse_exited() -> void: + prints("Draggable[base]._on_mouse_exited", self, self.name) + mouse_over = false + var handler := _get_hover_handler() + if handler: handler.handle_hover(self) + + ## Starts a drag operation func start_drag(mouse_offset: Vector2) -> void: _drag_start_position = global_position @@ -60,9 +88,10 @@ func find_drop_target() -> Node: return get_parent() ## Called after drop to clean up drag state -func end_drag() -> void: +func end_drag() -> Node: is_dragged = false _drag_source = null + return null ## Confines this draggable element to stay within screen or container bounds @@ -70,7 +99,7 @@ func end_drag() -> void: func confine_to_screen() -> void: # Try to get bounds from parent container var bounds := _get_container_bounds() - + # If we have valid bounds, clamp position if bounds != Rect2(): position.x = clampf(position.x, bounds.position.x, bounds.position.x + bounds.size.x) @@ -80,7 +109,7 @@ func confine_to_screen() -> void: ## Gets the bounds of the parent container if it exists and is a Control node func _get_container_bounds() -> Rect2: var parent := get_parent() - + # Check if parent is a Control node with a defined rect if parent is Control: var control := parent as Control @@ -91,7 +120,7 @@ func _get_container_bounds() -> Rect2: control.size.x - screen_margin * 2, control.size.y - screen_margin * 2 ) - + # Default: whole screen var viewport_size := get_viewport().get_visible_rect().size return Rect2( @@ -100,11 +129,3 @@ func _get_container_bounds() -> Rect2: viewport_size.x - screen_margin * 2, viewport_size.y - screen_margin * 2 ) - - -# === HELPERS === -## Static helper to check if a node implements DropTarget pattern -## DropTarget pattern requires: can_accept_drop(draggable) and handle_drop(draggable) -static func is_drop_target(node: Node) -> bool: - return node != null and node.has_method("can_accept_drop") and node.has_method("handle_drop") - diff --git a/src/logic-scenes/board/physics-board.tscn b/src/logic-scenes/board/physics-board.tscn index 3946dba..d19e72b 100644 --- a/src/logic-scenes/board/physics-board.tscn +++ b/src/logic-scenes/board/physics-board.tscn @@ -147,7 +147,6 @@ _data = { } [node name="board" type="PanelContainer"] -z_index = -100 material = SubResource("ShaderMaterial_ttqei") clip_contents = true anchors_preset = 15 @@ -175,6 +174,7 @@ mouse_filter = 1 unique_name_in_owner = true custom_minimum_size = Vector2(400, 0) layout_mode = 2 +mouse_filter = 1 [node name="instructions_panel" type="PanelContainer" parent="."] layout_mode = 2 diff --git a/src/logic-scenes/board/sticky-note.gd b/src/logic-scenes/board/sticky-note.gd index acc3189..5946ac8 100644 --- a/src/logic-scenes/board/sticky-note.gd +++ b/src/logic-scenes/board/sticky-note.gd @@ -14,7 +14,7 @@ var current_handle: Node var position_locked: bool = false ## Computed property: Is this currently attached to a card -var is_attached : bool: +var is_attached : bool: get: return get_parent() is Card ## Replaces the need for tracking attached_to as state @@ -45,24 +45,19 @@ func set_highlight(value: bool) -> void: if value != _highlighted: _highlighted = value - if is_inside_tree() and is_node_ready(): - if modulate_tween: modulate_tween.kill() - if shift_tween: shift_tween.kill() - if _highlighted: - modulate_tween = get_tree().create_tween() - modulate_tween.tween_property(self, "modulate", highlight_color, 0.1) - shift_tween = get_tree().create_tween() - shift_tween.tween_property(content, "position", shift_by, 0.2) - else: - modulate_tween = get_tree().create_tween() - modulate_tween.tween_property(self, "modulate", Color(1, 1, 1), 0.3) - shift_tween = get_tree().create_tween() - shift_tween.tween_property(content, "position", Vector2.ZERO, 0.5) + if modulate_tween: modulate_tween.kill() + if shift_tween: shift_tween.kill() + + if _highlighted: + modulate_tween = create_tween() + modulate_tween.tween_property(self, "modulate", highlight_color, 0.1) + shift_tween = create_tween() + shift_tween.tween_property(content, "position", shift_by, 0.2) else: - if _highlighted: - modulate = Color(1, 1, 1) - else: - modulate = Color(1, 1, 1) + modulate_tween = create_tween() + modulate_tween.tween_property(self, "modulate", Color(1, 1, 1), 0.3) + shift_tween = create_tween() + shift_tween.tween_property(content, "position", Vector2.ZERO, 0.5) @export var voice_line: AudioStream = null @export var is_dragable: bool = false @@ -86,19 +81,13 @@ func init(sticky_name: String = "sticky_note", card_id: StringName = "-1") -> vo sticky_id = card_id func _ready() -> void: + super._ready() label = $Content/Label background_sprite = $Content/BackgroundSprite content = $Content _on_text_updated.call_deferred() - input_event.connect(_on_input_event) - mouse_entered.connect(_on_mouse_entered) - mouse_exited.connect(_on_mouse_exited) - area_entered.connect(_on_area_enter) - area_exited.connect(_on_area_exit) - - func _on_text_updated(): label.text = text @@ -108,46 +97,23 @@ func _on_text_updated(): func _process(delta: float) -> void: _move_sticky_note(delta) -func _on_mouse_entered(): - if not Input.is_action_pressed("mouse_left") and current_handle and current_handle.has_method("handle_hover"): - current_handle.handle_hover(self) - -func _on_mouse_exited(): - highlighted = false - - # Let parent card re-check hover state if this sticky is attached to it - var card := attached_to - if card: - card.check_hover() - -func _on_area_enter(_area: Area2D): - pass - -func _on_area_exit(_area: Area2D): - pass - -func _on_input_event(_viewport, event, _shape_idx): - if event is InputEventMouseButton and current_handle and current_handle.has_method("handle_mouse_button"): - if (event.button_index == MOUSE_BUTTON_LEFT and event.pressed) or event.button_index == MOUSE_BUTTON_RIGHT: - mouse_offset = get_viewport().get_mouse_position() - global_position - current_handle.handle_mouse_button(event, self) ## frame rate independent FIR smoothing filter func _smooth(current: Vector2, goal: Vector2, delta: float) -> Vector2: var k := pow(0.1, 60.0 * delta) return (1.0-k) * current + k * goal - + func _move_sticky_note(delta: float) -> void: if is_dragged: update_drag_position(get_viewport().get_mouse_position()) return - + if is_attached: var card := attached_to position = _smooth(position, card.sticky_note_position, delta) - - + + var transform_tween: Tween @@ -158,7 +124,7 @@ func tween_transform_to(target: Transform2D, duration: float = 0.25) ->void: push_warning("StickyNote.tween_transform_to: Invalid position, skipping tween") transform_tween_finished.emit() return - + if transform_tween and transform_tween.is_running(): transform_tween.stop() @@ -171,51 +137,45 @@ func tween_transform_to(target: Transform2D, duration: float = 0.25) ->void: # === DRAG LIFECYCLE OVERRIDES === -## Start drag: if in panel, immediately move to board -func start_drag(offset: Vector2) -> void: - super.start_drag(offset) +func end_drag() -> Node: + super.end_drag() + return _find_drop_target() - # If attached to a card, detach it first - var card := attached_to - if card: - card.remove_sticky_note() - ## Find best drop target: Card > Panel > Board (in priority order) -func find_drop_target() -> Node: +func _find_drop_target() -> Node: # Priority 1: Check for overlapping cards in dropzone + var closest : Card = null for area in get_overlapping_areas(): - if area is Card and Draggable.is_drop_target(area): + if area is StickyNote and not area.is_attached: continue # Can only drop on attached stickies + + if area is Card: + if (not closest) or ((closest.position-position).length() < (area.position - position).length()): + closest = area return area - - # Priority 2: Check if dropped outside dropzone (over panel area) - if current_handle and not current_handle.is_in_dropzone(self): - var target_panel := _find_nearest_panel() - if target_panel: - return target_panel - + # Priority 3: Default to board (stay loose in dropzone) - return current_handle + return null ## Find the nearest panel that can accept this sticky func _find_nearest_panel() -> StickyNotePanel: if not current_handle or not current_handle.has_node("HBoxContainer/ScrollContainer/VBoxContainer"): return null - + var panel_container := current_handle.get_node("HBoxContainer/ScrollContainer/VBoxContainer") var sticky_rect := Rect2(global_position - Vector2(diameter/2, 10), Vector2(diameter/2, 10)) - + # First pass: look for empty panels we're hovering over for panel in panel_container.get_children(): if panel is StickyNotePanel: if panel.is_empty() and panel.get_global_rect().intersects(sticky_rect): return panel - + # Second pass: if no empty panel found, find first empty panel for panel in panel_container.get_children(): if panel is StickyNotePanel and panel.is_empty(): return panel - + # No empty panels found - will need to create one (handled by board) return null diff --git a/src/logic-scenes/board/sticky-note.tscn b/src/logic-scenes/board/sticky-note.tscn index 4f73acf..e76952c 100644 --- a/src/logic-scenes/board/sticky-note.tscn +++ b/src/logic-scenes/board/sticky-note.tscn @@ -9,7 +9,6 @@ radius = 48.0 height = 312.0 [node name="sticky-note" type="Area2D"] -z_index = 1 collision_layer = 2 collision_mask = 6 priority = 100 diff --git a/src/logic-scenes/playable.gd b/src/logic-scenes/playable.gd index ebe7e41..77ea4c9 100644 --- a/src/logic-scenes/playable.gd +++ b/src/logic-scenes/playable.gd @@ -4,3 +4,6 @@ class_name Playable ## Awaitable that encapsulates the core interaction with this Playable func play() -> void: await get_tree().process_frame # Dummy wait so this is a coroutine + +func handle_hover(area: Draggable): + prints("Playable[base].handle_hover", area, area.name)