feat: nearest card selection

This commit is contained in:
tiger tiger tiger 2026-01-18 10:48:03 +01:00
parent 1939e49a91
commit 46f7463267
9 changed files with 184 additions and 213 deletions

View File

@ -24,11 +24,11 @@ func _ready() -> void:
func disable()-> void: func disable()-> void:
is_active = false is_active = false
set_process_input(false) set_process_input(false)
set_process(false) set_process(false)
func get_ready(): func get_ready():
pass pass
func load(): func load():
# Override this function to load the state of the chapter from State.save_game # Override this function to load the state of the chapter from State.save_game
pass pass
@ -48,13 +48,13 @@ func pull_save_state(_save: SaveGame) -> void:
## Attempts to find player controller and restore position/rotation from save ## Attempts to find player controller and restore position/rotation from save
func restore_player_from_save(save: SaveGame) -> void: func restore_player_from_save(save: SaveGame) -> void:
var player: PlayerController = null var player: PlayerController = null
# Try to find player controller in common locations # Try to find player controller in common locations
if has_node("%PlayerController"): if has_node("%PlayerController"):
player = get_node("%PlayerController") player = get_node("%PlayerController")
elif has_node("logic/PlayerController"): elif has_node("logic/PlayerController"):
player = get_node("logic/PlayerController") player = get_node("logic/PlayerController")
if player and player is PlayerController: if player and player is PlayerController:
player.restore_from_save(save) player.restore_from_save(save)
else: else:
@ -73,4 +73,3 @@ func unload():
## Override in subclasses to add custom scene preparation logic ## Override in subclasses to add custom scene preparation logic
func prepare_scene_start(_scene_id: Scenes.id, _is_repeating: bool) -> void: func prepare_scene_start(_scene_id: Scenes.id, _is_repeating: bool) -> void:
await get_tree().process_frame # Dummy wait for LSP warning otherwise await get_tree().process_frame # Dummy wait for LSP warning otherwise

View File

@ -40,17 +40,12 @@ var mementos_collected: int = 0:
var selection: Draggable = null: var selection: Draggable = null:
set(value): set(value):
# this makes sure no accidental context switches can happen while a card is being dragged if selection == value: return
if current_context == DRAG: return
# Select & highlight new
# Deselect current if selection: selection.highlighted = false
if selection:
selection.highlighted = false
# Select new
selection = value selection = value
if selection: if selection: selection.highlighted = true
selection.highlighted = true
# Are we selecting cards or stickies? # Are we selecting cards or stickies?
if selection is Card: 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) var k := pow(0.1, 60.0 * delta)
return (1.0-k) * current + k * goal return (1.0-k) * current + k * goal
func _process(delta: float): func _process(delta: float):
var zone_position := Vector2(notezone.get_screen_position().x + sticky_width / 3.0, sticky_height) 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 # Called by notes when a mouse event needs handling
func handle_mouse_button(input: InputEventMouseButton, to_handle = selection) -> void: func handle_mouse_button(input: InputEventMouseButton, target: Draggable) -> void:
# Prevent dragging multiple nodes at once
if current_context == DRAG and selection:
return
# === DRAG START === # === DRAG START ===
if input.button_index == MOUSE_BUTTON_LEFT and input.pressed: if input.button_index == MOUSE_BUTTON_LEFT and input.is_pressed():
_start_drag(to_handle) _start_drag(target)
return return
# === DRAG END === # === DRAG END ===
if input.button_index == MOUSE_BUTTON_LEFT and not input.pressed: if input.button_index == MOUSE_BUTTON_LEFT and not input.is_released():
_end_drag(to_handle) _end_drag(target)
return return
## Starts a drag operation for the given draggable ## Starts a drag operation for the given draggable
func _start_drag(draggable: Dragg able) -> void: func _start_drag(draggable: Draggable) -> void:
selection = draggable selection = draggable
current_context = DRAG current_context = DRAG
@ -234,24 +227,36 @@ func _start_drag(draggable: Dragg able) -> void:
## Ends a drag operation and handles the drop ## Ends a drag operation and handles the drop
func _end_drag(draggable: Draggable) -> void: func _end_drag(draggable: Draggable) -> void:
if not draggable: return selection = draggable
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
# Cleanup and state update # Cleanup and state update
current_context = NAVIGATE 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 # Check win condition if sticky was attached to card
if draggable is StickyNote and draggable.is_attached: if draggable is StickyNote and draggable.is_attached:
check_board_comnpletion() check_board_completion()
func reclaim_sticky(note: StickyNote): func reclaim_sticky(note: StickyNote):
@ -259,7 +264,7 @@ func reclaim_sticky(note: StickyNote):
notes.append(note) notes.append(note)
func check_board_comnpletion(): func check_board_completion():
if is_board_complete(): if is_board_complete():
if not board_was_completed: if not board_was_completed:
board_was_completed = true board_was_completed = true
@ -316,20 +321,44 @@ func give_lore_feedback():
complete = true complete = true
# Mark area that was hovered over as currently selected # 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 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 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 ## Call this after bulk loading to fix child order / z-index issues
func _sort_by_positions() -> void: func _sort_by_positions() -> void:
cards.sort_custom(_spatial) cards.sort_custom(_by_spatial)
notes.sort_custom(_spatial) notes.sort_custom(_by_spatial)
# === DROP TARGET PATTERN IMPLEMENTATION === # === DROP TARGET PATTERN IMPLEMENTATION ===
@ -348,18 +377,16 @@ func handle_drop(draggable: Draggable) -> int:
# Takes the inputs for control inputs # Takes the inputs for control inputs
func _input(event) -> void: func _input(event) -> void:
if event.is_action_pressed("ui_cancel"): if event is InputEventAction and event.is_action_pressed("ui_cancel"):
closed.emit() closed.emit()
get_viewport().set_input_as_handled() 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 InputEventMouseMotion and not event.is_action_pressed("mouse_left"):
if event is InputEventMouseButton and current_context == DRAG: var candidate := _nearest_hovered(_sort_by_proximity_and_depth(notes))
if event.button_index == MOUSE_BUTTON_LEFT and not event.pressed: if not candidate:
handle_mouse_button(event) candidate = _nearest_hovered(_sort_by_proximity_and_depth(cards))
get_viewport().set_input_as_handled() selection = candidate
else:
return
## Saves board state directly to SaveGame resource ## Saves board state directly to SaveGame resource

View File

@ -38,55 +38,41 @@ var transfor_arr: Array[Transform2D] = [
@onready var background_sprite: AnimatedSprite2D = $AnimatedSprite2D @onready var background_sprite: AnimatedSprite2D = $AnimatedSprite2D
@export var picked_random: bool = false @export var picked_random: bool = false
@export var wiggle_strength: float = 0.2 @export var wiggle_strength: float = 0.2
@export var wiggle_speed: float = 5 @export var wiggle_speed: float = 5
@export_range(1, 2) var scale_bump: float = 1.05 @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, 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 ## Override set_highlight to add visual feedback for cards
func set_highlight(value: bool) -> void: func set_highlight(value: bool) -> void:
if value != _highlighted: if value == _highlighted: return
_highlighted = value _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 voice_line: AudioStream = null
@export var is_dragable: bool = false @export var is_dragable: bool = false
@ -158,6 +144,7 @@ func init(card_name: String = "card", own_id:StringName = "-1") -> void:
func _ready(): func _ready():
super._ready()
input_event.connect(_on_input_event) input_event.connect(_on_input_event)
_handle_wiggle(0) _handle_wiggle(0)
@ -202,7 +189,6 @@ func _process(delta: float) -> void:
func _handle_wiggle(delta): func _handle_wiggle(delta):
wiggle_pos += delta * wiggle_speed * wiggle_intensity wiggle_pos += delta * wiggle_speed * wiggle_intensity
rotation = noise.get_noise_1d(wiggle_pos)*wiggle_strength rotation = noise.get_noise_1d(wiggle_pos)*wiggle_strength
@ -212,26 +198,18 @@ func _input(event: InputEvent) -> void:
is_dragged = false is_dragged = false
func _on_mouse_entered() -> void: func _on_mouse_entered() -> void:
if not Input.is_action_pressed("mouse_left"): super._on_mouse_entered()
# 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)
func _on_mouse_exited(): func _on_mouse_exited():
highlighted = false super._on_mouse_exited()
if burn_state == burned.SINGED: if burn_state == burned.SINGED:
burn_state = burned.NOT burn_state = burned.NOT
func _on_input_event(_viewport, event, _shape_idx): func _on_input_event(_viewport, event, _shape_idx):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
var board = _get_board() if highlighted:
if board and highlighted:
mouse_offset = get_viewport().get_mouse_position() - position 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(): func _move_card():
if is_dragged: if is_dragged:
@ -284,19 +262,6 @@ func exchange_sticky_note_with(new_note: StickyNote) -> StickyNote:
attach_sticky_note(new_note) attach_sticky_note(new_note)
return tmp 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 === # === DROP TARGET PATTERN IMPLEMENTATION ===
@ -348,7 +313,7 @@ func find_drop_target() -> Node:
## Walks up the scene tree to find the CardBoard ## Walks up the scene tree to find the CardBoard
func _get_board() -> CardBoard: func _get_board() -> CardBoard:
var node = get_parent() var node := get_parent()
while node: while node:
if node is CardBoard: if node is CardBoard:
return node return node

View File

@ -39,6 +39,3 @@ text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
autowrap_mode = 3 autowrap_mode = 3
[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]

View File

@ -14,6 +14,8 @@ enum DropResult {
EXCHANGED # Swap occurred, exchanged item needs handling EXCHANGED # Swap occurred, exchanged item needs handling
} }
var mouse_over: bool = false
var is_dragged: bool = false: var is_dragged: bool = false:
set(dragged): set(dragged):
is_dragged = dragged is_dragged = dragged
@ -37,9 +39,35 @@ var _drag_start_position: Vector2
var _mouse_drag_offset: Vector2 var _mouse_drag_offset: Vector2
var _drag_source: Node = null # Where the drag started from 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 === ## === DRAG LIFECYCLE METHODS ===
## Override these in Card and StickyNote for specific behavior ## 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 ## Starts a drag operation
func start_drag(mouse_offset: Vector2) -> void: func start_drag(mouse_offset: Vector2) -> void:
_drag_start_position = global_position _drag_start_position = global_position
@ -60,9 +88,10 @@ func find_drop_target() -> Node:
return get_parent() return get_parent()
## Called after drop to clean up drag state ## Called after drop to clean up drag state
func end_drag() -> void: func end_drag() -> Node:
is_dragged = false is_dragged = false
_drag_source = null _drag_source = null
return null
## Confines this draggable element to stay within screen or container bounds ## Confines this draggable element to stay within screen or container bounds
@ -70,7 +99,7 @@ func end_drag() -> void:
func confine_to_screen() -> void: func confine_to_screen() -> void:
# Try to get bounds from parent container # Try to get bounds from parent container
var bounds := _get_container_bounds() var bounds := _get_container_bounds()
# If we have valid bounds, clamp position # If we have valid bounds, clamp position
if bounds != Rect2(): if bounds != Rect2():
position.x = clampf(position.x, bounds.position.x, bounds.position.x + bounds.size.x) 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 ## Gets the bounds of the parent container if it exists and is a Control node
func _get_container_bounds() -> Rect2: func _get_container_bounds() -> Rect2:
var parent := get_parent() var parent := get_parent()
# Check if parent is a Control node with a defined rect # Check if parent is a Control node with a defined rect
if parent is Control: if parent is Control:
var control := parent as Control var control := parent as Control
@ -91,7 +120,7 @@ func _get_container_bounds() -> Rect2:
control.size.x - screen_margin * 2, control.size.x - screen_margin * 2,
control.size.y - screen_margin * 2 control.size.y - screen_margin * 2
) )
# Default: whole screen # Default: whole screen
var viewport_size := get_viewport().get_visible_rect().size var viewport_size := get_viewport().get_visible_rect().size
return Rect2( return Rect2(
@ -100,11 +129,3 @@ func _get_container_bounds() -> Rect2:
viewport_size.x - screen_margin * 2, viewport_size.x - screen_margin * 2,
viewport_size.y - 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")

View File

@ -147,7 +147,6 @@ _data = {
} }
[node name="board" type="PanelContainer"] [node name="board" type="PanelContainer"]
z_index = -100
material = SubResource("ShaderMaterial_ttqei") material = SubResource("ShaderMaterial_ttqei")
clip_contents = true clip_contents = true
anchors_preset = 15 anchors_preset = 15
@ -175,6 +174,7 @@ mouse_filter = 1
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(400, 0) custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
mouse_filter = 1
[node name="instructions_panel" type="PanelContainer" parent="."] [node name="instructions_panel" type="PanelContainer" parent="."]
layout_mode = 2 layout_mode = 2

View File

@ -14,7 +14,7 @@ var current_handle: Node
var position_locked: bool = false var position_locked: bool = false
## Computed property: Is this currently attached to a card ## Computed property: Is this currently attached to a card
var is_attached : bool: var is_attached : bool:
get: return get_parent() is Card get: return get_parent() is Card
## Replaces the need for tracking attached_to as state ## Replaces the need for tracking attached_to as state
@ -45,24 +45,19 @@ func set_highlight(value: bool) -> void:
if value != _highlighted: if value != _highlighted:
_highlighted = value _highlighted = value
if is_inside_tree() and is_node_ready(): if modulate_tween: modulate_tween.kill()
if modulate_tween: modulate_tween.kill() if shift_tween: shift_tween.kill()
if shift_tween: shift_tween.kill()
if _highlighted: if _highlighted:
modulate_tween = get_tree().create_tween() modulate_tween = create_tween()
modulate_tween.tween_property(self, "modulate", highlight_color, 0.1) modulate_tween.tween_property(self, "modulate", highlight_color, 0.1)
shift_tween = get_tree().create_tween() shift_tween = create_tween()
shift_tween.tween_property(content, "position", shift_by, 0.2) 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: else:
if _highlighted: modulate_tween = create_tween()
modulate = Color(1, 1, 1) modulate_tween.tween_property(self, "modulate", Color(1, 1, 1), 0.3)
else: shift_tween = create_tween()
modulate = Color(1, 1, 1) shift_tween.tween_property(content, "position", Vector2.ZERO, 0.5)
@export var voice_line: AudioStream = null @export var voice_line: AudioStream = null
@export var is_dragable: bool = false @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 sticky_id = card_id
func _ready() -> void: func _ready() -> void:
super._ready()
label = $Content/Label label = $Content/Label
background_sprite = $Content/BackgroundSprite background_sprite = $Content/BackgroundSprite
content = $Content content = $Content
_on_text_updated.call_deferred() _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(): func _on_text_updated():
label.text = text label.text = text
@ -108,46 +97,23 @@ func _on_text_updated():
func _process(delta: float) -> void: func _process(delta: float) -> void:
_move_sticky_note(delta) _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 ## frame rate independent FIR smoothing filter
func _smooth(current: Vector2, goal: Vector2, delta: float) -> Vector2: func _smooth(current: Vector2, goal: Vector2, delta: float) -> Vector2:
var k := pow(0.1, 60.0 * delta) var k := pow(0.1, 60.0 * delta)
return (1.0-k) * current + k * goal return (1.0-k) * current + k * goal
func _move_sticky_note(delta: float) -> void: func _move_sticky_note(delta: float) -> void:
if is_dragged: if is_dragged:
update_drag_position(get_viewport().get_mouse_position()) update_drag_position(get_viewport().get_mouse_position())
return return
if is_attached: if is_attached:
var card := attached_to var card := attached_to
position = _smooth(position, card.sticky_note_position, delta) position = _smooth(position, card.sticky_note_position, delta)
var transform_tween: Tween 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") push_warning("StickyNote.tween_transform_to: Invalid position, skipping tween")
transform_tween_finished.emit() transform_tween_finished.emit()
return return
if transform_tween and transform_tween.is_running(): if transform_tween and transform_tween.is_running():
transform_tween.stop() transform_tween.stop()
@ -171,51 +137,45 @@ func tween_transform_to(target: Transform2D, duration: float = 0.25) ->void:
# === DRAG LIFECYCLE OVERRIDES === # === DRAG LIFECYCLE OVERRIDES ===
## Start drag: if in panel, immediately move to board func end_drag() -> Node:
func start_drag(offset: Vector2) -> void: super.end_drag()
super.start_drag(offset) 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) ## 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 # Priority 1: Check for overlapping cards in dropzone
var closest : Card = null
for area in get_overlapping_areas(): 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 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) # Priority 3: Default to board (stay loose in dropzone)
return current_handle return null
## Find the nearest panel that can accept this sticky ## Find the nearest panel that can accept this sticky
func _find_nearest_panel() -> StickyNotePanel: func _find_nearest_panel() -> StickyNotePanel:
if not current_handle or not current_handle.has_node("HBoxContainer/ScrollContainer/VBoxContainer"): if not current_handle or not current_handle.has_node("HBoxContainer/ScrollContainer/VBoxContainer"):
return null return null
var panel_container := current_handle.get_node("HBoxContainer/ScrollContainer/VBoxContainer") var panel_container := current_handle.get_node("HBoxContainer/ScrollContainer/VBoxContainer")
var sticky_rect := Rect2(global_position - Vector2(diameter/2, 10), Vector2(diameter/2, 10)) var sticky_rect := Rect2(global_position - Vector2(diameter/2, 10), Vector2(diameter/2, 10))
# First pass: look for empty panels we're hovering over # First pass: look for empty panels we're hovering over
for panel in panel_container.get_children(): for panel in panel_container.get_children():
if panel is StickyNotePanel: if panel is StickyNotePanel:
if panel.is_empty() and panel.get_global_rect().intersects(sticky_rect): if panel.is_empty() and panel.get_global_rect().intersects(sticky_rect):
return panel return panel
# Second pass: if no empty panel found, find first empty panel # Second pass: if no empty panel found, find first empty panel
for panel in panel_container.get_children(): for panel in panel_container.get_children():
if panel is StickyNotePanel and panel.is_empty(): if panel is StickyNotePanel and panel.is_empty():
return panel return panel
# No empty panels found - will need to create one (handled by board) # No empty panels found - will need to create one (handled by board)
return null return null

View File

@ -9,7 +9,6 @@ radius = 48.0
height = 312.0 height = 312.0
[node name="sticky-note" type="Area2D"] [node name="sticky-note" type="Area2D"]
z_index = 1
collision_layer = 2 collision_layer = 2
collision_mask = 6 collision_mask = 6
priority = 100 priority = 100

View File

@ -4,3 +4,6 @@ class_name Playable
## Awaitable that encapsulates the core interaction with this Playable ## Awaitable that encapsulates the core interaction with this Playable
func play() -> void: func play() -> void:
await get_tree().process_frame # Dummy wait so this is a coroutine 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)