diff --git a/src/logic-scenes/board/card-board.gd b/src/logic-scenes/board/card-board.gd index c28fba2..6decbca 100644 --- a/src/logic-scenes/board/card-board.gd +++ b/src/logic-scenes/board/card-board.gd @@ -137,6 +137,11 @@ func _on_board_focused() -> void: visible = true if is_node_ready(): process_mode = Node.PROCESS_MODE_INHERIT + + # Check board state and give lore feedback when presented + if is_board_complete(): + board_was_completed = true + give_lore_feedback() ## Called when board loses focus @@ -204,91 +209,99 @@ func is_in_dropzone(to_check: Node) -> bool: # Called by notes when a mouse event needs handling func handle_mouse_button(input: InputEventMouseButton, to_handle = currently_active_node) -> void: - - # Makes sure that only the same area is dragged. - # Otherwise overlapping areas are dragged at the same time. + # Prevent dragging multiple nodes at once if current_context == DRAG and to_handle != currently_active_node: return - - if input.button_index == MOUSE_BUTTON_MASK_LEFT and input.pressed: - currently_active_node = to_handle - to_handle.is_dragged = true - if to_handle is StickyNote: - if not to_handle.on_board: - to_handle.reparent(dropzone) - to_handle.on_board = true - to_handle.attached_to = self - current_context = DRAG + + # === DRAG START === + if input.button_index == MOUSE_BUTTON_LEFT and input.pressed: + _start_drag(to_handle) + return + + # === DRAG END === + if input.button_index == MOUSE_BUTTON_LEFT and not input.pressed: + _end_drag(to_handle) + return - # when Drag stops ... - if input.button_index == MOUSE_BUTTON_MASK_LEFT and not input.pressed: - to_handle.is_dragged = false - if to_handle is StickyNote: - if is_in_dropzone(to_handle): - if to_handle.has_overlapping_areas(): - for area in to_handle.get_overlapping_areas(): - if area is Card: - focus_stickies = false - if area.has_sticky_note_attached(): - to_handle = area.exchange_sticky_note_with(to_handle) - to_handle.reparent(dropzone) - to_handle.on_board = true - if sticky_note_container.get_child_count() > 0: - sticky_note_container.get_child(current_sticky_note_id).attached_sticky_note = to_handle - to_handle.attached_to = sticky_note_container.get_child(current_sticky_note_id) - else: - var new_panel = StickyNotePanel.new() - sticky_note_container.add_child(new_panel, true, Node.INTERNAL_MODE_DISABLED) - new_panel.owner = self - new_panel.attatch_sticky_note(to_handle, self, false) - current_sticky_note_id = 0 - to_handle.reset_drag() - current_context = NAVIGATE - _return_sticky_notes_to_panels() - return - else: - area.attach_sticky_note(to_handle) - to_handle.z_index = 0 - if sticky_note_container.get_child_count() > 0: - sticky_note_container.get_child(current_sticky_note_id).clear_if_empty() - current_context = NAVIGATE - check_board_comnpletion() - return - else: - var i: int = 0 - for panel: StickyNotePanel in sticky_note_container.get_children(): - i += 1 - if panel.is_empty: - if panel.get_global_rect().intersects(Rect2(to_handle.global_position - Vector2(to_handle.diameter/2, 10), Vector2(to_handle.diameter/2, 10))): - panel.attatch_sticky_note(to_handle, self) - elif panel.is_gapped or i == sticky_note_container.get_child_count(): - panel.collapse_gap() - var new_panel = StickyNotePanel.new() - sticky_note_container.add_child(new_panel) - sticky_note_container.move_child(new_panel, i) - new_panel.attatch_sticky_note(to_handle, self) - new_panel.owner = self - panel.clear_if_empty() - _return_sticky_notes_to_panels() - current_context = NAVIGATE - return +## Starts a drag operation for the given draggable +func _start_drag(draggable: Draggable) -> void: + currently_active_node = draggable + current_context = DRAG + + var mouse_offset = get_viewport().get_mouse_position() - draggable.global_position + draggable.start_drag(mouse_offset) - ## Dropping Cards and Sticky Notes not causing a return condition above. - if not (to_handle is StickyNote and to_handle.is_sticky_note_attached()): - if to_handle.get_parent() is Card: - insert_area(to_handle.get_parent().remove_sticky_note(), to_handle) - else: - insert_area(dropzone, to_handle) - current_context = NAVIGATE + +## Ends a drag operation and handles the drop +func _end_drag(draggable: Draggable) -> void: + 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) + + # Handle exchange result (sticky swapped with card's sticky) + if result == Draggable.DropResult.EXCHANGED: + _handle_sticky_exchange(draggable, drop_target) + elif draggable is StickyNote and not is_in_dropzone(draggable): + # Sticky dropped in panel area but no empty panel found - create one + add_sticky_note(draggable) + else: + # Fallback: use default board drop + handle_drop(draggable) + + # Cleanup and state update + _return_sticky_notes_to_panels() + current_context = NAVIGATE + _update_focus_after_drop(draggable) + + # Check win condition if sticky was attached to card + if draggable is StickyNote and draggable.is_sticky_note_attached(): + check_board_comnpletion() + + +## Handles the exchange when a sticky is dropped on a card that already has one +## The exchanged sticky always goes to the sticky_note_container (panel zone) +func _handle_sticky_exchange(new_sticky: StickyNote, card: Card) -> void: + var old_sticky = card.get_last_exchanged_sticky() + + if not old_sticky: + push_warning("CardBoard: Exchange occurred but no sticky returned") + return + + # Reset visual state for old sticky + old_sticky.rotation = old_sticky.base_rotation + old_sticky.scale = old_sticky.base_scale + old_sticky.z_index = 0 + + # Exchanged sticky always goes to sticky_note_container + if new_sticky._came_from_panel and sticky_note_container.get_child_count() > 0: + # New sticky came from panel - return old sticky to that panel (swap positions) + var target_panel = sticky_note_container.get_child(current_sticky_note_id) + old_sticky.reparent(dropzone) + old_sticky.on_board = true + target_panel.attached_sticky_note = old_sticky + old_sticky.attached_to = target_panel + target_panel.attatch_sticky_note(old_sticky, self, false, true) + else: + # New sticky was loose - create new panel for old sticky + add_sticky_note(old_sticky) + + # Clean up empty panel if the new sticky came from one + if new_sticky._came_from_panel and sticky_note_container.get_child_count() > 0: + sticky_note_container.get_child(current_sticky_note_id).clear_if_empty() + + +## Updates focus and navigation state after a drop +func _update_focus_after_drop(draggable: Draggable) -> void: + # Update focus based on where the item ended up + if draggable is Card or (draggable is StickyNote and draggable.on_board): focus_stickies = false - current_dropzone_id = dropzone.get_children().find(to_handle) - if to_handle is StickyNote: - to_handle.rotation = to_handle.base_rotation - to_handle.scale = to_handle.base_scale - - if input.is_action_pressed("mouse_right") and current_context == DRAG: - to_handle.reset_drag() + current_dropzone_id = dropzone.get_children().find(draggable) func _return_sticky_notes_to_panels() -> void: @@ -394,6 +407,35 @@ func insert_area(parent: Control, node: Area2D): node.attached_to = self node.is_dragable = true +# === DROP TARGET PATTERN IMPLEMENTATION === + +## Checks if this board can accept the given draggable (always true for board) +func can_accept_drop(draggable: Draggable) -> bool: + return draggable is Card or draggable is StickyNote + +## Handles dropping a draggable onto the board (into the dropzone) +func handle_drop(draggable: Draggable) -> int: + if not can_accept_drop(draggable): + return Draggable.DropResult.REJECTED + + if draggable is StickyNote: + # Handle sticky note drop + var sticky = draggable as StickyNote + insert_area(dropzone, sticky) + sticky.attached_to = self + sticky.on_board = true + sticky.is_dragable = true + # Reset visual state + sticky.rotation = sticky.base_rotation + sticky.scale = sticky.base_scale + elif draggable is Card: + # Handle card drop + insert_area(dropzone, draggable) + draggable.is_dragable = true + + return Draggable.DropResult.ACCEPTED + + # Takes the inputs for control inputs func _input(event) -> void: @@ -630,21 +672,25 @@ func initialise_from_save(savegame: SaveGame) -> void: # Add all cards print_debug(" Loading %d cards..." % card_pile["cards"].size()) for card: Card in card_pile["cards"]: - # Set position BEFORE adding to scene tree to avoid reparent position issues + # Determine target position (will be set after adding to scene) + var target_position: Vector2 if savegame.board_positions.has(card.name): - card.position = savegame.board_positions[card.name] - print_debug(" Card '%s' at %s" % [card.name, card.position]) + target_position = savegame.board_positions[card.name] + print_debug(" Card '%s' loading at %s" % [card.name, target_position]) else: - card.position = _generate_random_position() - print_debug(" Card '%s' - generated random position: %s" % [card.name, card.position]) + target_position = _generate_random_position() + print_debug(" Card '%s' - generated random position: %s" % [card.name, target_position]) + # Add to board first add_child(card) - insert_area(dropzone, card) - card.set_owner(self) card.is_dragable = true cards_by_name[card.name] = card card.picked_random = savegame.board_randoms.has(card.card_id) + + # Move to dropzone and set position (position must be set after adding to scene) + insert_area(dropzone, card) + card.position = target_position # Add all sticky notes print_debug(" Loading %d stickies..." % card_pile["sticky_notes"].size()) @@ -670,26 +716,32 @@ func initialise_from_save(savegame: SaveGame) -> void: # Sticky is loose on board else: - # Set position BEFORE adding to scene tree to avoid reparent position issues + # Determine target position (will be set after adding to scene) + var target_position: Vector2 if savegame.board_positions.has(sticky.name): - sticky.position = savegame.board_positions[sticky.name] - print_debug(" Loose sticky '%s' at %s" % [sticky.name, sticky.position]) + target_position = savegame.board_positions[sticky.name] + print_debug(" Loose sticky '%s' loading at %s" % [sticky.name, target_position]) else: - sticky.position = _generate_random_position() - print_debug(" Loose sticky '%s' - generated random position: %s" % [sticky.name, sticky.position]) + target_position = _generate_random_position() + print_debug(" Loose sticky '%s' - generated random position: %s" % [sticky.name, target_position]) + # Add to board first add_child(sticky) - insert_area(dropzone, sticky) - sticky.set_owner(self) sticky.current_handle = self # Required for input handling sticky.on_board = true sticky.attached_to = self sticky.is_dragable = true + + # Move to dropzone and set position (position must be set after adding to scene) + insert_area(dropzone, sticky) + sticky.position = target_position sticky.picked_random = savegame.board_randoms.has(sticky.sticky_id) print_debug("CardBoard: Load complete!") + + # Note: Lore feedback will be triggered when board is presented (in play()) diff --git a/src/logic-scenes/board/card.gd b/src/logic-scenes/board/card.gd index 3916e4c..0eb49c9 100644 --- a/src/logic-scenes/board/card.gd +++ b/src/logic-scenes/board/card.gd @@ -235,8 +235,7 @@ func _on_input_event(_viewport, event, _shape_idx): func _move_card(): if is_dragged: - position = get_viewport().get_mouse_position() - mouse_offset - confine_to_screen() + update_drag_position(get_viewport().get_mouse_position()) func has_sticky_note_attached() -> bool: return get_attached_sticky_note() != null @@ -296,3 +295,49 @@ func reclaim_sticky_note(): await current_sticky_note.transform_tween_finished current_sticky_note.reparent(self) current_sticky_note.owner = self.owner + + +# === DROP TARGET PATTERN IMPLEMENTATION === + +## Temporary storage for exchanged sticky during drop operation +var _last_exchanged_sticky: StickyNote = null + +## Checks if this card can accept the given draggable +func can_accept_drop(draggable: Draggable) -> bool: + return draggable is StickyNote + +## Handles dropping a sticky note onto this card +## Returns DropResult indicating success, rejection, or exchange +func handle_drop(draggable: StickyNote) -> int: + if not can_accept_drop(draggable): + return Draggable.DropResult.REJECTED + + if has_sticky_note_attached(): + # Exchange: remove current, attach new, store old for retrieval + _last_exchanged_sticky = exchange_sticky_note_with(draggable) + # Reset z_index for newly attached sticky + draggable.z_index = 0 + return Draggable.DropResult.EXCHANGED + else: + # Simple attach + if attach_sticky_note(draggable): + # Reset z_index for newly attached sticky + draggable.z_index = 0 + return Draggable.DropResult.ACCEPTED + else: + # Attach failed (shouldn't happen, but handle it) + return Draggable.DropResult.REJECTED + +## Retrieves the sticky that was exchanged during last drop +## Clears the reference after retrieval +func get_last_exchanged_sticky() -> StickyNote: + var result = _last_exchanged_sticky + _last_exchanged_sticky = null + return result + + +# === DRAG LIFECYCLE OVERRIDES === + +## Cards always drop back to board dropzone +func find_drop_target() -> Node: + return owner if owner is CardBoard else get_parent() diff --git a/src/logic-scenes/board/card.tscn b/src/logic-scenes/board/card.tscn index 5b53c84..e80bed4 100644 --- a/src/logic-scenes/board/card.tscn +++ b/src/logic-scenes/board/card.tscn @@ -27,9 +27,9 @@ anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = -126.0 -offset_top = -88.0 +offset_top = -100.0 offset_right = 136.0 -offset_bottom = 89.95834 +offset_bottom = 77.95834 grow_horizontal = 2 grow_vertical = 2 theme = ExtResource("3_mdi7r") diff --git a/src/logic-scenes/board/draggable.gd b/src/logic-scenes/board/draggable.gd index 2136905..8a9a13d 100644 --- a/src/logic-scenes/board/draggable.gd +++ b/src/logic-scenes/board/draggable.gd @@ -4,6 +4,18 @@ extends Area2D ## Base class for draggable UI elements (Cards and StickyNotes) ## Provides common dragging behavior and boundary protection +## Drop result codes for DropTarget pattern +enum DropResult { + ACCEPTED, # Drop successful, item is now owned by target + REJECTED, # Drop refused, item stays with previous owner + EXCHANGED # Swap occurred, exchanged item needs handling +} + +## 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") + var is_dragged: bool = false: set(dragged): is_dragged = dragged @@ -12,6 +24,38 @@ var is_dragged: bool = false: ## Margin from screen edges when confining to screen bounds @export var screen_margin: float = 50.0 +## Drag state tracking +var _drag_start_position: Vector2 +var _mouse_drag_offset: Vector2 +var _drag_source: Node = null # Where the drag started from + +## === DRAG LIFECYCLE METHODS === +## Override these in Card and StickyNote for specific behavior + +## Starts a drag operation +func start_drag(mouse_offset: Vector2) -> void: + _drag_start_position = global_position + _mouse_drag_offset = mouse_offset + _drag_source = get_parent() + is_dragged = true + +## Updates position during drag (call from _process or manual update) +func update_drag_position(mouse_pos: Vector2) -> void: + global_position = mouse_pos - _mouse_drag_offset + confine_to_screen() + +## Finds the best drop target for this draggable +## Override in subclasses for specific drop target logic +## Returns the node that should receive the drop, or null for no valid target +func find_drop_target() -> Node: + # Base implementation: return parent (board) + return get_parent() + +## Called after drop to clean up drag state +func end_drag() -> void: + is_dragged = false + _drag_source = null + ## Confines this draggable element to stay within screen or container bounds ## Skip this check if a sticky note is attached to a card func confine_to_screen() -> void: diff --git a/src/logic-scenes/board/physics-board.tscn b/src/logic-scenes/board/physics-board.tscn index f7d236c..77901a2 100644 --- a/src/logic-scenes/board/physics-board.tscn +++ b/src/logic-scenes/board/physics-board.tscn @@ -154,6 +154,7 @@ _data = { } [node name="board" type="PanelContainer"] +z_index = -100 material = SubResource("ShaderMaterial_ttqei") clip_contents = true anchors_preset = 15 diff --git a/src/logic-scenes/board/sticky-note.gd b/src/logic-scenes/board/sticky-note.gd index c3c31ea..00aa782 100644 --- a/src/logic-scenes/board/sticky-note.gd +++ b/src/logic-scenes/board/sticky-note.gd @@ -132,8 +132,7 @@ func _on_input_event(_viewport, event, _shape_idx): func _move_sticky_note(): if is_dragged: - global_position = get_viewport().get_mouse_position() - mouse_offset - confine_to_screen() + update_drag_position(get_viewport().get_mouse_position()) func is_sticky_note_attached() -> bool: # FIXME: this breaks if attatched to is previousely freed because GODOT IS FUCKING STUPID @@ -155,3 +154,69 @@ func tween_transform_to(target: Transform2D): 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 := attached_to as Card + if card and card.has_method("remove_sticky_note"): + card.remove_sticky_note() + + # If in panel, immediately reparent to board 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) + on_board = true + attached_to = 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 + diff --git a/src/logic-scenes/board/sticky-note.tscn b/src/logic-scenes/board/sticky-note.tscn index eb99273..5448b6f 100644 --- a/src/logic-scenes/board/sticky-note.tscn +++ b/src/logic-scenes/board/sticky-note.tscn @@ -9,6 +9,7 @@ radius = 48.0 height = 312.0 [node name="sticky-note" type="Area2D"] +z_index = 1 priority = 100 script = ExtResource("1_yvh5n") text = "card" diff --git a/src/logic-scenes/board/sticky_note_panel.gd b/src/logic-scenes/board/sticky_note_panel.gd index 260d989..d132b66 100644 --- a/src/logic-scenes/board/sticky_note_panel.gd +++ b/src/logic-scenes/board/sticky_note_panel.gd @@ -100,3 +100,25 @@ func replace_sticky_note_with(new_sticky_note: StickyNote): func is_empty() -> bool: return get_child_count() == 0 and not is_attatching + + +# === DROP TARGET PATTERN IMPLEMENTATION === + +## Checks if this panel can accept the given draggable +func can_accept_drop(draggable: Draggable) -> bool: + return draggable is StickyNote and is_empty() + +## Handles dropping a sticky note onto this panel +func handle_drop(draggable: StickyNote) -> int: + if not can_accept_drop(draggable): + return Draggable.DropResult.REJECTED + + # Attach sticky to this panel + attatch_sticky_note(draggable, owner, true, true) + + # Clean up other empty panels + for panel in get_parent().get_children(): + if panel is StickyNotePanel and panel != self: + panel.clear_if_empty() + + return Draggable.DropResult.ACCEPTED