extends Draggable class_name StickyNote var sticky_id var parent_id var sibling: StickyNote var shift_tween: Tween var modulate_tween: Tween # cannot be explicitly typed, as this can be both handled by picker and physics-board var current_handle: Node var position_locked: bool = false ## Computed property: Returns the current attachment (parent node) ## Replaces the need for tracking attached_to as state var attached_to: Node: get: return get_parent() signal transform_tween_finished @onready var background_sprite: AnimatedSprite2D = %BackgroundSprite @export var text: String = "" : set (value): if is_node_ready(): _on_text_updated.call_deferred() text = value var content: Node2D var label: Label @export var picked_random: bool = false @export var shift_by: Vector2 = Vector2(-32, 0) @export_color_no_alpha var highlight_color: Color = Color(1.5, 1.5, 1.5) ## Override set_highlight to add visual feedback for sticky notes 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) else: if _highlighted: modulate = Color(1, 1, 1) else: modulate = Color(1, 1, 1) @export var voice_line: AudioStream = null @export var is_dragable: bool = false var mouse_offset: Vector2 @onready var diameter := 312.0 @export_range(1.0, 10.0) var bounce_speed: float = 8 ## Computed property: Check if on the board (dropzone) ## Replaces on_board state tracking var on_board: bool: get: var parent = get_parent() return parent != null and parent.name == "dropzone" func init(sticky_name: String = "sticky_note", card_id: StringName = "-1") -> void: name = sticky_name text = sticky_name parent_id = StringName(card_id.rsplit(".", false, 1)[0]) sticky_id = card_id func _ready() -> void: 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 background_sprite.frame = text.hash() % background_sprite.sprite_frames.get_frame_count(background_sprite.animation) func _process(delta: float) -> void: if get_overlapping_areas().size() > 0 and is_dragable and on_board: for area in get_overlapping_areas(): if area is Card: if not area.highlighted or self.highlighted: var diff:Vector2 = position - area.position position -= diff.normalized() * ((diff.length()-diameter)/diameter) * bounce_speed * (delta/(1.0/60)) _move_sticky_note() 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 if is_sticky_note_attached(): var card = get_parent() if card and card.has_method("check_hover"): card.check_hover() func _on_area_enter(area: Area2D): # Handle sticky note panel gap creation if area is StickyNote and is_sticky_note_in_panel() and not is_dragged: var panel = get_parent() as StickyNotePanel if panel: panel.create_gap() func _on_area_exit(area: Area2D): # Handle sticky note panel gap collapse if area is StickyNote and is_sticky_note_in_panel(): var panel = get_parent() as StickyNotePanel if panel: panel.collapse_gap() 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) func _move_sticky_note(): if is_dragged: update_drag_position(get_viewport().get_mouse_position()) func is_sticky_note_attached() -> bool: var parent = get_parent() return is_instance_valid(parent) and parent is Card func is_sticky_note_in_panel() -> bool: var parent = get_parent() return is_instance_valid(parent) and parent is StickyNotePanel var transform_tween: Tween func tween_transform_to(target: Transform2D, duration: float = 0.25): # Validate position to prevent teleporting if not is_finite(target.origin.x) or not is_finite(target.origin.y): 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() transform_tween = create_tween() transform_tween.tween_property(self, "transform", target, duration) await transform_tween.finished transform_tween_finished.emit() # === DRAG LIFECYCLE OVERRIDES === ## Track whether this sticky came from a panel (for exchange logic) var _came_from_panel: bool = false ## Start drag: if in panel, immediately move to board func start_drag(offset: Vector2) -> void: super.start_drag(offset) _came_from_panel = is_sticky_note_in_panel() # If attached to a card, detach it first if is_sticky_note_attached(): var card := get_parent() as Card if card and card.has_method("remove_sticky_note"): card.remove_sticky_note() # If in panel, immediately reparent to board dropzone for dragging if _came_from_panel and current_handle: var board := current_handle var dropzone := board.get_node_or_null("HBoxContainer/dropzone") if dropzone: reparent(dropzone) else: reparent(board) ## Find best drop target: Card > Panel > Board (in priority order) func find_drop_target() -> Node: # Priority 1: Check for overlapping cards in dropzone for area in get_overlapping_areas(): if area is Card and Draggable.is_drop_target(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 ## 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