fix: card-board uses more single-responsibility, and now allows cards to slide nicely into the gaps

This commit is contained in:
tiger tiger tiger 2026-01-21 21:38:31 +01:00
parent 666df45e06
commit b47801ae5c
4 changed files with 63 additions and 112 deletions

View File

@ -89,16 +89,28 @@ func _smooth(current: Vector2, goal: Vector2, delta: float) -> Vector2:
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)
var dragging := notes.any(func (n : Draggable): return n.is_dragged)
if dragging:
# Y-sort the nodes, this lets us fill the gap more nicely.
notes.sort_custom(func (a:Draggable, b:Draggable): return a.global_position.y < b.global_position.y)
for note in notes: for note in notes:
# Skip all dragged and already attached notes # Skip all dragged and already attached notes
if note.is_attached: continue if note.is_attached: continue
if note.is_dragged: continue if note.is_dragged: continue
# Magnetically move all notes to where they ought to be on screen # Magnetically move all notes to where they ought to be on screen
note.position = _smooth(note.position, zone_position, delta) note.home = zone_position
zone_position.y += sticky_height zone_position.y += sticky_height
pass # Only if not already in transit / animated or user holding on to one
if not dragging and not note.tween:
note.animate_home()
else:
# do adjustment with FIR filter
note.position = _smooth(note.position, note.home, delta)
func _check_completion() -> void: func _check_completion() -> void:
@ -195,7 +207,7 @@ func add_card(card: Card) -> void:
func add_note(note: StickyNote) -> void: func add_note(note: StickyNote) -> void:
add_child(note) add_child(note)
notes.append(note) notes.append(note)
note.is_dragable = true note.is_draggable = true
func appear(): func appear():
await Main.curtain.close() await Main.curtain.close()
@ -250,30 +262,15 @@ func _end_drag(draggable: Draggable) -> void:
if destination and destination is Card: if destination and destination is Card:
var target_card := destination as Card var target_card := destination as Card
# If sticky was previously attached to a different card, detach it first
if sticky.is_attached and sticky.attached_to != target_card: if sticky.is_attached and sticky.attached_to != target_card:
sticky.attached_to.remove_sticky_note() sticky.attached_to.remove_note_if_present()
# If target card already has a sticky, exchange them target_card.attach_or_exchange_note(sticky)
if target_card.has_sticky_note_attached():
var exchanged_sticky := target_card.exchange_sticky_note_with(sticky)
# Reclaim the exchanged sticky to the board
if exchanged_sticky:
reclaim_sticky(exchanged_sticky)
else:
# Simple attach
sticky.reparent(target_card)
target_card.attach_sticky_note(sticky)
# If dropped on board (no destination), ensure it's a child of the board # If dropped on board (no destination), ensure it's a child of the board
elif not destination: elif not destination:
# If it was attached to a card, detach it first
if sticky.is_attached: if sticky.is_attached:
sticky.attached_to.remove_sticky_note() reclaim_sticky(sticky)
# Make sure sticky is parented to board
if sticky.get_parent() != self:
sticky.reparent(self)
# Check win condition after any sticky movement # Check win condition after any sticky movement
check_board_completion() check_board_completion()
@ -281,6 +278,7 @@ func _end_drag(draggable: Draggable) -> void:
func reclaim_sticky(note: StickyNote): func reclaim_sticky(note: StickyNote):
note.reparent(self) note.reparent(self)
note.tween = null
func check_board_completion(): func check_board_completion():
@ -305,8 +303,8 @@ func give_lore_feedback():
for child in dropzone.get_children(): for child in dropzone.get_children():
if child is Card: if child is Card:
if child.has_sticky_note_attached(): if child.has_note_attached():
fitting_card_count += int(child.card_id == child.get_attached_sticky_note().parent_id) fitting_card_count += int(child.card_id == child.get_attached_note().parent_id)
total_card_count += 1 total_card_count += 1
if float(fitting_card_count) / float(total_card_count) < 0.2: if float(fitting_card_count) / float(total_card_count) < 0.2:
@ -421,7 +419,7 @@ func save_to_resource(savegame: SaveGame) -> void:
print_debug(" Card '%s' at %s" % [card.name, card.position]) print_debug(" Card '%s' at %s" % [card.name, card.position])
# Save sticky note attachment if present # Save sticky note attachment if present
var note: StickyNote = card.get_attached_sticky_note() var note: StickyNote = card.get_attached_note()
if note: if note:
savegame.board_attachments[note.name] = card.name savegame.board_attachments[note.name] = card.name
print_debug(" Sticky '%s' attached to card '%s'" % [note.name, card.name]) print_debug(" Sticky '%s' attached to card '%s'" % [note.name, card.name])
@ -496,8 +494,7 @@ func initialise_from_save(savegame: SaveGame) -> void:
if card_name and cards_by_name.has(card_name): if card_name and cards_by_name.has(card_name):
# Sticky is attached to a card # Sticky is attached to a card
var card: Card = cards_by_name[card_name] var card: Card = cards_by_name[card_name]
sticky.reparent(card) card.attach_or_exchange_note(sticky, true)
card.attach_sticky_note(sticky)
print_debug(" Sticky '%s' attached to card '%s'" % [sticky.name, card_name]) print_debug(" Sticky '%s' attached to card '%s'" % [sticky.name, card_name])
else: else:
# Sticky is loose on the board # Sticky is loose on the board

View File

@ -203,10 +203,10 @@ func _move_card():
if is_dragged: if is_dragged:
update_drag_position(get_viewport().get_mouse_position()) update_drag_position(get_viewport().get_mouse_position())
func has_sticky_note_attached() -> bool: func has_note_attached() -> bool:
return get_attached_sticky_note() != null return get_attached_note() != null
func get_attached_sticky_note() -> StickyNote: func get_attached_note() -> StickyNote:
for child in get_children(false): for child in get_children(false):
if child is StickyNote: if child is StickyNote:
return child return child
@ -224,41 +224,40 @@ func preview_sticky_note(sticky_note: StickyNote):
else: else:
push_warning("Card.preview_sticky_note: Invalid position calculated, skipping tween") push_warning("Card.preview_sticky_note: Invalid position calculated, skipping tween")
func attach_sticky_note(sticky_note: StickyNote) -> bool:
if has_sticky_note_attached():
return false
sticky_note.reparent(self) func attach_or_exchange_note(note: StickyNote, instant: bool = false) -> void:
sticky_note.position = sticky_note_position prints("Attaching", note, "to", self)
sticky_note.is_dragable = false
if name == "c_hit" and sticky_note.name == "c_effort" and Steamworks.has_initialized: # Out with the old...
remove_note_if_present()
# ... in with the new
note.reparent(self)
note.home = sticky_note_position
if not instant:
note.animate_home()
else:
note.position = sticky_note_position
if name == "c_hit" and note.name == "c_effort" and Steamworks.has_initialized:
Steam.setAchievement("FIGHT_FOR_GOOD") Steam.setAchievement("FIGHT_FOR_GOOD")
Steam.storeStats() Steam.storeStats()
return true
func remove_sticky_note() -> StickyNote: func remove_note_if_present() -> void:
var former_child: StickyNote = get_attached_sticky_note() var former_child: StickyNote = get_attached_note()
if not former_child: if not former_child: return
return null
former_child.reparent(get_parent()) former_child.reparent(get_parent())
return former_child former_child.tween = null # the positioning logic in card-board will pick that one up and calc a nice slot.
func exchange_sticky_note_with(new_note: StickyNote) -> StickyNote:
if new_note == get_attached_sticky_note():
return null
var old_note := remove_sticky_note()
attach_sticky_note(new_note)
return old_note
# === DROP TARGET PATTERN IMPLEMENTATION === # === DROP TARGET PATTERN IMPLEMENTATION ===
## Checks if this card can accept the given draggable ## Checks if this card can accept the given draggable
func can_accept_drop(draggable: Draggable) -> bool: func can_accept_drop(draggable: Draggable) -> bool:
return draggable is StickyNote and draggable != self return draggable is StickyNote
## Handles dropping a sticky note onto this card ## Handles dropping a sticky note onto this card
## Returns DropResult indicating success, rejection, or exchange ## Returns DropResult indicating success, rejection, or exchange
@ -266,22 +265,9 @@ func handle_drop(draggable: StickyNote) -> int:
if not can_accept_drop(draggable): if not can_accept_drop(draggable):
return Draggable.DropResult.REJECTED return Draggable.DropResult.REJECTED
if has_sticky_note_attached(): attach_or_exchange_note(draggable)
# Exchange: remove current, attach new, store old for retrieval draggable.z_index = 0
exchange_sticky_note_with(draggable) return Draggable.DropResult.ACCEPTED
# Reset z_index for newly attached sticky
draggable.z_index = 0
return Draggable.DropResult.ACCEPTED
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
# === DRAG LIFECYCLE OVERRIDES === # === DRAG LIFECYCLE OVERRIDES ===

View File

@ -20,6 +20,9 @@ var is_dragged: bool = false:
is_dragged = dragged is_dragged = dragged
z_index = int(dragged) z_index = int(dragged)
# local coordinates home position, where the draggable can try to animate_home to
var home : Vector2 = Vector2.ZERO
## Internal highlighted state - do not set directly, use set_highlight() ## Internal highlighted state - do not set directly, use set_highlight()
var _highlighted: bool = false var _highlighted: bool = false
@ -63,6 +66,13 @@ func _get_board() -> Node:
## === DRAG LIFECYCLE METHODS === ## === DRAG LIFECYCLE METHODS ===
## Override these in Card and StickyNote for specific behavior ## Override these in Card and StickyNote for specific behavior
var tween : Tween = null
func animate_home() -> void:
if tween: tween.kill()
tween = create_tween().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(self, "position", home, 0.5)
func _on_mouse_entered() -> void: func _on_mouse_entered() -> void:
#prints("Draggable[base]._on_mouse_entered", self, self.name) #prints("Draggable[base]._on_mouse_entered", self, self.name)

View File

@ -58,19 +58,13 @@ func set_highlight(value: bool) -> void:
shift_tween.tween_property(content, "position", Vector2.ZERO, 0.5) 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_draggable: bool = false
var mouse_offset: Vector2 var mouse_offset: Vector2
@onready var diameter := 312.0 @onready var diameter := 312.0
@export_range(1.0, 10.0) var bounce_speed: float = 8 @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: func init(sticky_name: String = "sticky_note", card_id: StringName = "-1") -> void:
name = sticky_name name = sticky_name
@ -92,45 +86,9 @@ func _on_text_updated():
background_sprite.frame = text.hash() % background_sprite.sprite_frames.get_frame_count(background_sprite.animation) background_sprite.frame = text.hash() % background_sprite.sprite_frames.get_frame_count(background_sprite.animation)
func _process(delta: float) -> void: func _process(_delta: float) -> void:
_move_sticky_note(delta)
## 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: if is_dragged:
update_drag_position(get_viewport().get_mouse_position()) 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
func tween_transform_to(target: Transform2D, duration: float = 0.25) ->void:
# 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 === # === DRAG LIFECYCLE OVERRIDES ===