## A draggable object that supports mouse interaction with state-based animation system. ## ## This class provides a robust state machine for handling mouse interactions including ## hover effects, drag operations, and programmatic movement using Tween animations. ## All interactive cards and objects extend this base class to inherit consistent ## drag-and-drop behavior. ## ## Key Features: ## - State machine with safe transitions (IDLE → HOVERING → HOLDING → MOVING) ## - Tween-based animations for smooth hover effects and movement ## - Mouse interaction handling with proper event management ## - Z-index management for visual layering during interactions ## - Extensible design with virtual methods for customization ## ## State Transitions: ## - IDLE: Default state, ready for interaction ## - HOVERING: Mouse over with visual feedback (scale, rotation, position) ## - HOLDING: Active drag state following mouse movement ## - MOVING: Programmatic movement ignoring user input ## ## Usage: ## [codeblock] ## class_name MyDraggable ## extends DraggableObject ## ## func _can_start_hovering() -> bool: ## return my_custom_condition ## [/codeblock] class_name DraggableObject extends Control # Enums ## Enumeration of possible interaction states for the draggable object. enum DraggableState { IDLE, ## Default state - no interaction HOVERING, ## Mouse over state - visual feedback HOLDING, ## Dragging state - follows mouse MOVING ## Programmatic move state - ignores input } ## The speed at which the objects moves. @export var moving_speed: int = CardFrameworkSettings.ANIMATION_MOVE_SPEED ## Whether the object can be interacted with. @export var can_be_interacted_with: bool = true ## The distance the object hovers when interacted with. @export var hover_distance: int = CardFrameworkSettings.PHYSICS_HOVER_DISTANCE ## The scale multiplier when hovering. @export var hover_scale: float = CardFrameworkSettings.ANIMATION_HOVER_SCALE ## The rotation in degrees when hovering. @export var hover_rotation: float = CardFrameworkSettings.ANIMATION_HOVER_ROTATION ## The duration for hover animations. @export var hover_duration: float = CardFrameworkSettings.ANIMATION_HOVER_DURATION # Legacy variables - kept for compatibility but no longer used in state machine var is_pressed: bool = false var is_holding: bool = false var stored_z_index: int: set(value): z_index = value stored_z_index = value # State Machine var current_state: DraggableState = DraggableState.IDLE # Mouse tracking var is_mouse_inside: bool = false # Movement state tracking var is_moving_to_destination: bool = false var is_returning_to_original: bool = false # Position and animation tracking var current_holding_mouse_position: Vector2 var original_position: Vector2 var original_scale: Vector2 var original_hover_rotation: float var current_hover_position: Vector2 # Track position during hover animation # Move operation tracking var target_destination: Vector2 # Target position passed to move() function var target_rotation: float # Target rotation passed to move() function var original_destination: Vector2 var original_rotation: float var destination_degree: float # Tween objects var move_tween: Tween var hover_tween: Tween # State transition rules var allowed_transitions = { DraggableState.IDLE: [DraggableState.HOVERING, DraggableState.HOLDING, DraggableState.MOVING], DraggableState.HOVERING: [DraggableState.IDLE, DraggableState.HOLDING, DraggableState.MOVING], DraggableState.HOLDING: [DraggableState.IDLE, DraggableState.MOVING], DraggableState.MOVING: [DraggableState.IDLE] } func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_STOP connect("mouse_entered", _on_mouse_enter) connect("mouse_exited", _on_mouse_exit) connect("gui_input", _on_gui_input) original_destination = global_position original_rotation = rotation original_position = position original_scale = scale original_hover_rotation = rotation stored_z_index = z_index ## Safely transitions between interaction states using predefined rules. ## Validates transitions and handles state cleanup/initialization automatically. ## @param new_state: Target state to transition to ## @returns: True if transition was successful, false if invalid/blocked func change_state(new_state: DraggableState) -> bool: if new_state == current_state: return true # Validate transition is allowed by state machine rules if not new_state in allowed_transitions[current_state]: return false # Clean up previous state _exit_state(current_state) var old_state = current_state current_state = new_state # Enter new state _enter_state(new_state, old_state) return true # Handle state entry func _enter_state(state: DraggableState, from_state: DraggableState) -> void: match state: DraggableState.IDLE: z_index = stored_z_index mouse_filter = Control.MOUSE_FILTER_STOP DraggableState.HOVERING: # z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET _start_hover_animation() DraggableState.HOLDING: # Preserve hover position if transitioning from HOVERING state if from_state == DraggableState.HOVERING: _preserve_hover_position() # For IDLE → HOLDING transitions, current position is maintained current_holding_mouse_position = get_local_mouse_position() z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET rotation = 0 DraggableState.MOVING: # Stop hover animations and ignore input during programmatic movement if hover_tween and hover_tween.is_valid(): hover_tween.kill() hover_tween = null z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET mouse_filter = Control.MOUSE_FILTER_IGNORE # Handle state exit func _exit_state(state: DraggableState) -> void: match state: DraggableState.HOVERING: z_index = stored_z_index _stop_hover_animation() DraggableState.HOLDING: z_index = stored_z_index # Reset visual effects but preserve position for return_card() animation scale = original_scale rotation = original_hover_rotation DraggableState.MOVING: mouse_filter = Control.MOUSE_FILTER_STOP func _process(delta: float) -> void: match current_state: DraggableState.HOLDING: global_position = get_global_mouse_position() - current_holding_mouse_position func _finish_move() -> void: # Complete movement processing is_moving_to_destination = false rotation = destination_degree # Update original position and rotation only when not returning to original # Important: Use original target values from move() instead of global_position if not is_returning_to_original: original_destination = target_destination original_rotation = target_rotation # Reset return flag is_returning_to_original = false # End MOVING state - return to IDLE change_state(DraggableState.IDLE) # Call inherited class callback _on_move_done() func _on_move_done() -> void: # This function can be overridden by subclasses to handle when the move is done. pass # Start hover animation with tween func _start_hover_animation() -> void: # Stop any existing hover animation if hover_tween and hover_tween.is_valid(): hover_tween.kill() hover_tween = null position = original_position # Reset position to original before starting new hover scale = original_scale rotation = original_hover_rotation # Update original position to current position (important for correct return) original_position = position original_scale = scale original_hover_rotation = rotation # Store current position before animation current_hover_position = position # Create new hover tween hover_tween = create_tween() hover_tween.set_parallel(true) # Allow multiple properties to animate simultaneously # Animate position (hover up) var target_position = Vector2(position.x - 30, position.y - hover_distance) hover_tween.tween_property(self, "position", target_position, hover_duration) # Animate scale hover_tween.tween_property(self, "scale", original_scale * hover_scale, hover_duration) # Animate rotation #hover_tween.tween_property(self, "rotation", deg_to_rad(hover_rotation), hover_duration) # Update current hover position tracking hover_tween.tween_method(_update_hover_position, position, target_position, hover_duration) # Stop hover animation and return to original state func _stop_hover_animation() -> void: # Stop any existing hover animation if hover_tween and hover_tween.is_valid(): hover_tween.kill() hover_tween = null # Create new tween to return to original state hover_tween = create_tween() hover_tween.set_parallel(true) # Animate back to original position hover_tween.tween_property(self, "position", original_position, hover_duration) # Animate back to original scale hover_tween.tween_property(self, "scale", original_scale, hover_duration) # Animate back to original rotation hover_tween.tween_property(self, "rotation", original_hover_rotation, hover_duration) # Update current hover position tracking hover_tween.tween_method(_update_hover_position, position, original_position, hover_duration) # Track current position during hover animation for smooth HOLDING transition func _update_hover_position(pos: Vector2) -> void: current_hover_position = pos # Preserve current hover position when transitioning to HOLDING func _preserve_hover_position() -> void: # Stop hover animation and preserve current position if hover_tween and hover_tween.is_valid(): hover_tween.kill() hover_tween = null # Explicitly set position to current hover position # This ensures smooth transition from hover animation to holding position = current_hover_position ## Virtual method to determine if hovering animation can start. ## Override in subclasses to implement custom hovering conditions. ## @returns: True if hovering is allowed, false otherwise func _can_start_hovering() -> bool: return true func _on_mouse_enter() -> void: is_mouse_inside = true if can_be_interacted_with and _can_start_hovering(): change_state(DraggableState.HOVERING) func _on_mouse_exit() -> void: is_mouse_inside = false match current_state: DraggableState.HOVERING: change_state(DraggableState.IDLE) func _on_gui_input(event: InputEvent) -> void: if not can_be_interacted_with: return if event is InputEventMouseButton: _handle_mouse_button(event as InputEventMouseButton) ## Moves the object to target position with optional rotation using smooth animation. ## Automatically transitions to MOVING state and handles animation timing based on distance. ## @param target_destination: Global position to move to ## @param degree: Target rotation in radians func move(target_destination: Vector2, degree: float) -> void: # Skip if current position and rotation match target if global_position == target_destination and rotation == degree: return # Force transition to MOVING state (highest priority) change_state(DraggableState.MOVING) # Stop existing movement if move_tween and move_tween.is_valid(): move_tween.kill() move_tween = null # Store target position and rotation for original value preservation self.target_destination = target_destination self.target_rotation = degree # Initial setup rotation = 0 destination_degree = degree is_moving_to_destination = true # Smooth Tween-based movement with dynamic duration based on moving_speed var distance = global_position.distance_to(target_destination) var duration = distance / moving_speed move_tween = create_tween() move_tween.tween_property(self, "global_position", target_destination, duration) move_tween.tween_callback(_finish_move) func _handle_mouse_button(mouse_event: InputEventMouseButton) -> void: if mouse_event.button_index != MOUSE_BUTTON_LEFT: return # Ignore all input during MOVING state if current_state == DraggableState.MOVING: return if mouse_event.is_pressed(): _handle_mouse_pressed() if mouse_event.is_released(): _handle_mouse_released() ## Returns the object to its original position with smooth animation. func return_to_original() -> void: is_returning_to_original = true move(original_destination, original_rotation) func _handle_mouse_pressed() -> void: is_pressed = true match current_state: DraggableState.HOVERING: change_state(DraggableState.HOLDING) DraggableState.IDLE: if is_mouse_inside and can_be_interacted_with and _can_start_hovering(): change_state(DraggableState.HOLDING) func _handle_mouse_released() -> void: is_pressed = false match current_state: DraggableState.HOLDING: change_state(DraggableState.IDLE)