diff --git a/src/base-environments/youth_room/youth_room.gd b/src/base-environments/youth_room/youth_room.gd index 46f5ae4..925dbdd 100644 --- a/src/base-environments/youth_room/youth_room.gd +++ b/src/base-environments/youth_room/youth_room.gd @@ -7,9 +7,6 @@ extends RoomTemplate @onready var card_picker: CardPicker = %Picker @onready var ui: Control = %UI -# Is populated by child cardboard instead of onready. -var card_board: CardBoard - func start_room(): %UI.show() $logic/PlayerController.process_mode = Node.PROCESS_MODE_INHERIT diff --git a/src/dev-util/i18n.gd b/src/dev-util/i18n.gd new file mode 100644 index 0000000..83ff62e --- /dev/null +++ b/src/dev-util/i18n.gd @@ -0,0 +1,31 @@ +## Localization Utility class to move lists of keys out of difficult to read code +extends Node + +func get_memento_prompt(count: int) -> StringName: + return TranslationServer.translate(_memento_prompts.get(count, "")) + +func get_story_caption(id: Scenes.id) -> StringName: + return TranslationServer.translate(_story_captions.get(id, "")) + + +const _memento_prompts: Dictionary[int, StringName] = { + 1: "There are three Mementos left to find.", + 2: "You have collected half of the mementos.", + 3: "Find the last Memento to complete the Board.", + 4: "Combine cards to order your thoughts.", +} + +const _story_captions : Dictionary[Scenes.id, StringName] = { + Scenes.id.YOUTH_DRAVEN: "Starlight", + Scenes.id.YOUTH_CHILDHOOD: "crafted Mask", + Scenes.id.YOUTH_VOICE_TRAINING: "Comic Stash", + Scenes.id.YOUTH_JUI_JUTSU: "Sports Clothes", + Scenes.id.TRANSITION: "Move on", + Scenes.id.ADULT_DND: "colorful Dice", + Scenes.id.ADULT_VOLUNTARY: "Gemstone Art", + Scenes.id.ADULT_CHRISTMAS: "Chat Messages", + Scenes.id.ADULT_EATING: "Dishes", + Scenes.id.ADULT_UNI: "Science Poster", + Scenes.id.ADULT_THERAPY: "Doctors Note", + Scenes.id.ADULT_BURNOUT: "Paperwork", +} diff --git a/src/dev-util/i18n.gd.uid b/src/dev-util/i18n.gd.uid new file mode 100644 index 0000000..54b2c73 --- /dev/null +++ b/src/dev-util/i18n.gd.uid @@ -0,0 +1 @@ +uid://26fa8xwylhxl diff --git a/src/dev-util/room_template.gd b/src/dev-util/room_template.gd index 1296934..914d0cf 100644 --- a/src/dev-util/room_template.gd +++ b/src/dev-util/room_template.gd @@ -4,6 +4,7 @@ var initialised: bool = false var id: State.rooms = State.rooms.NULL @onready var scene_player : AnimationPlayer = %ScenePlayer +@onready var card_board : CardBoard # Optional Board, if present - set by the board in its own _ready() var is_active: bool: set(value): diff --git a/src/logic-scenes/board/card-board.gd b/src/logic-scenes/board/card-board.gd index ee53d88..5e784ea 100644 --- a/src/logic-scenes/board/card-board.gd +++ b/src/logic-scenes/board/card-board.gd @@ -1,144 +1,109 @@ -class_name CardBoard extends PanelContainer - -enum {NAVIGATE, ASSIGN, DRAG} - -var focus_stickies:bool = true: - set(stickies): - if not is_node_ready(): return - if stickies and sticky_note_container.get_child_count() == 0: return - - # this messes things up if called unneeded. - if focus_stickies != stickies: - focus_stickies = stickies - - if not current_context == ASSIGN: - if stickies: - current_sticky_note_id = current_sticky_note_id - else: - current_dropzone_id = current_dropzone_id - -var focused := false: - set(value): - if focused == value: - return - var was_focused := focused - focused = value - - if focused: - _on_board_focused() - else: - _on_board_unfocused() - - # Emit closed signal when transitioning from focused to unfocused - if was_focused and not focused: - closed.emit() - - -@onready var dropzone := $HBoxContainer/dropzone -var dropzone_size: Vector2 -@export var dropzone_padding:int = 100 -@onready var sticky_note_container := $HBoxContainer/ScrollContainer/VBoxContainer -@onready var current_context:int = NAVIGATE: - set(context): - current_context = context -@onready var instructions := $instructions_panel/HBoxContainer/cards_remaining - -var mementos_collected: int = 0: - set(mementos): - mementos_collected = mementos - match mementos: - 1: - instructions.text = "There are three Mementos left to find." - 2: - instructions.text = "You have collected half of the mementos." - 3: - instructions.text = "Find the last Memento to complete the Board." - 4: - instructions.text = "Combine cards to order your thoughts." - -@onready var currently_active_node: Draggable = null: - set(new_node): - # this makes sure no accidental context switches can happen while a card is being dragged. - if not (current_context == DRAG): - if not currently_active_node == null: - currently_active_node.highlighted = false - currently_active_node = new_node - if not currently_active_node == null: - currently_active_node.highlighted = true - -@onready var current_dropzone_id: int = 0: - set(new_id): - if is_node_ready(): - if new_id > dropzone.get_child_count() - 1: current_dropzone_id = 0 - elif new_id < 0: current_dropzone_id = dropzone.get_child_count() - 1 - else: current_dropzone_id = new_id - if current_context == ASSIGN and not focus_stickies: - while not dropzone.get_child(current_dropzone_id) is Card: - current_dropzone_id = (current_dropzone_id + (1 if not new_id == -1 else -1)) % dropzone.get_child_count() - (dropzone.get_child(current_dropzone_id) as Card).preview_sticky_note(currently_active_node) - - elif not focus_stickies: - currently_active_node = dropzone.get_child(current_dropzone_id) - -@onready var current_sticky_note_id: int = 0: - set(new_id): - if is_node_ready(): - if sticky_note_container.get_child_count() < 1: return - elif sticky_note_container.get_child_count() == 1: current_sticky_note_id = 0 - elif new_id > sticky_note_container.get_child_count() - 1: current_sticky_note_id = 0 - elif new_id < 0: current_sticky_note_id = sticky_note_container.get_child_count() - 1 - elif sticky_note_container.get_child(new_id).invalid: - if sticky_note_container.get_child_count() == 1: return - if new_id+1 == sticky_note_container.get_child_count(): - current_sticky_note_id = new_id-1 - else: - current_sticky_note_id = new_id+1 - else: current_sticky_note_id = new_id - if current_context == ASSIGN: - _return_sticky_notes_to_panels() - currently_active_node.preview_sticky_note(sticky_note_container.get_child(current_sticky_note_id).attached_sticky_note) - elif focus_stickies: - if sticky_note_container.get_child(current_sticky_note_id).get_child_count() == 1: - currently_active_node = sticky_note_container.get_child(current_sticky_note_id).get_child(0) - else: - for i in range(sticky_note_container.get_child_count() - 1): - if sticky_note_container.get_child(i).get_child_count() == 1: - currently_active_node = sticky_note_container.get_child(i).get_child(0) - - +class_name CardBoard +extends Playable signal board_completed signal closed +@export var dropzone_padding : int = 100 +@export var sticky_width : float = 400.0 +@export var sticky_height : float = 110.0 + +var all_names : Array[StringName] = [] +var notes : Array[StickyNote] = [] +var cards : Array[Card] = [] + +var board_was_completed: bool = false + +var current_context : int = NAVIGATE +var selection_state : SelectionState + +@onready var instructions := $instructions_panel/HBoxContainer/cards_remaining + +@onready var dropzone : Control = %CardZone +@onready var notezone : Control = %NoteZone + +enum SelectionState {FREE,STICKIES,CARDS} +enum {NAVIGATE, ASSIGN, DRAG} + + +func play(): + _check_completion() # check completion one extra time on on enter + await closed + _finalize_board_state() + + +var mementos_collected: int = 0: + set(mementos): + mementos_collected = mementos + instructions.text = I18n.get_memento_prompt(mementos_collected) + + +var selection: Draggable = null: + set(value): + # this makes sure no accidental context switches can happen while a card is being dragged + if current_context == DRAG: return + + # Deselect current + if selection: + selection.highlighted = false + + # Select new + selection = value + if selection: + selection.highlighted = true + + # Are we selecting cards or stickies? + if selection is Card: + selection_state = SelectionState.CARDS + + if selection is StickyNote: + selection_state = SelectionState.STICKIES + + +func _navigate_next(): + var candidates := _selection_candidates + var index := maxi(0, candidates.find(selection)) + selection = candidates[(index+1) % len(candidates)] + + +func _navigate_prev(): + var candidates := _selection_candidates + var index := maxi(0, candidates.find(selection)) + selection = candidates[index-1] + + # Called when the node enters the scene tree for the first time. -func _ready(): +func _ready() -> void: + # HACK: Lets us debug more easily + if get_parent() == get_tree().root: + _debug_mode() + return + print("Board Ready!", self, "room", State.room) State.room.card_board = self - var size_reference := StickyNotePanel.new() - dropzone_size = get_viewport_rect().size - Vector2(dropzone_padding + size_reference.minimum_size.x, dropzone_padding) +## 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 - if get_parent() == get_tree().root: - populate_board(["c_void", 'c_gifted', "p_wet", "p_joy"]) - populate_board(["c_jui_jutsu", 'c_hit', "p_girly", "p_vent"]) - populate_board(["c_comic_heroes", 'c_teasing', "p_agent_q", "p_good_intended"]) - populate_board(["c_out_of_world", 'c_confusion', "p_outer_conflict", "p_unique"]) +func _process(delta: float): + var zone_position := Vector2(notezone.get_screen_position().x, sticky_height) - get_viewport().gui_focus_changed.connect(reclaim_lost_focus) + for note in notes: + # Skip all dragged and already attached notes + if note.is_attached: continue + if note.is_dragged:continue + + # Magnetically move all notes to where they ought to be on screen + note.position = _smooth(note.position, zone_position, delta) + zone_position.y += sticky_height + + pass -## Called when board receives focus -func _on_board_focused() -> void: - get_tree().call_group("interactables", "collapse") - current_dropzone_id = 0 - current_sticky_note_id = 0 - focus_stickies = true - visible = true - if is_node_ready(): - process_mode = Node.PROCESS_MODE_INHERIT - - # Check board state and give lore feedback when presented +func _check_completion() -> void: if is_board_complete(): board_was_completed = true give_lore_feedback() @@ -146,72 +111,39 @@ func _on_board_focused() -> void: ## Finalizes board state before closing (ends drags, cleans up transitions) func _finalize_board_state() -> void: - print_debug("CardBoard: Finalizing board state before closing (context: %d)" % current_context) - # End any active drag operations - if current_context == DRAG and is_instance_valid(currently_active_node): - if currently_active_node.is_dragged: - print_debug(" Ending active drag for: %s" % currently_active_node.name) - _end_drag(currently_active_node) - - # Stop dragging for all items (safety net in case _end_drag didn't catch everything) - for child in dropzone.get_children(): - if (child is Card or child is StickyNote) and child.is_dragged: - print_debug(" Force-stopping drag for: %s" % child.name) - child.is_dragged = false - - # Also check cards for attached stickies that might be dragged - for child in dropzone.get_children(): - if child is Card: - var attached_sticky = child.get_attached_sticky_note() - if attached_sticky and attached_sticky.is_dragged: - print_debug(" Force-stopping drag for attached sticky: %s" % attached_sticky.name) - attached_sticky.is_dragged = false - - # Return stickies to panels if in ASSIGN mode - if current_context == ASSIGN: - print_debug(" Returning stickies to panels (was in ASSIGN mode)") - _return_sticky_notes_to_panels() + if current_context == DRAG: + _end_drag(selection) + for item in notes: + item.is_dragged = false + for item in cards: + item.is_dragged = false # Reset context to NAVIGATE current_context = NAVIGATE - print_debug("CardBoard: Board state finalized") -## Called when board loses focus -func _on_board_unfocused() -> void: - visible = false - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED - if is_node_ready(): - # Finalize any drag operations before closing - _finalize_board_state() - - process_mode = Node.PROCESS_MODE_DISABLED - - -func reclaim_lost_focus(_thief): - if focused: - grab_focus() - -## Will be used later to spawn Cards and Post-Its and remember them in the dictionary -func populate_board(card_names: Array[StringName]): +## Spawn Cards and Post-Its +# TODO: rename to "add to board" +func populate_board(names: Array[StringName]): mementos_collected += 1 - var all_new:Dictionary = HardCards.get_cards_by_name_array(card_names) + for item in names: + assert(name not in all_names, "Tried to re-add card %s" % item) + + var all_new:Dictionary = HardCards.get_cards_by_name_array(names) - # spawning the cards and adding them to the dictionary for new_card: Card in all_new["cards"]: - add_card(new_card, false) - # marking the first card as random picks - new_card.picked_random = new_card.name == card_names[1] - for new_sticky_note: StickyNote in all_new["sticky_notes"]: # spawning a sticky note - reclaim_sticky_to_panel(new_sticky_note, false) - # marking the first sticky as random picks - new_sticky_note.picked_random = new_sticky_note.name == card_names[3] + add_card(new_card) + new_card.picked_random = new_card.name == names[1] #CAUTION: Hardcoded Index - currently_active_node = dropzone.get_child(0) + for new_sticky_note: StickyNote in all_new["sticky_notes"]: + add_note(new_sticky_note) + new_sticky_note.picked_random = new_sticky_note.name == names[3] #CAUTION: Hardcoded Index + +# FIXME: This can be made even simpler. ## Generates a random position within the dropzone bounds ## Attempts to avoid overlapping with existing cards/stickies func _generate_random_position(min_distance: float = 150.0) -> Vector2: @@ -228,7 +160,7 @@ func _generate_random_position(min_distance: float = 150.0) -> Vector2: # Check if this position is far enough from existing items var is_valid := true - for child in dropzone.get_children(): + for child in get_children(): if child is Card or child is StickyNote: var distance := pos.distance_to(child.position) var required_distance := min_distance @@ -255,56 +187,29 @@ func _generate_random_position(min_distance: float = 150.0) -> Vector2: randi_range(dropzone_padding, int(dropzone_size.y)) ) -func add_card(card: Card, re_parent:bool = true): - if re_parent: - card.reparent(self) - else: - add_child(card) - insert_area(dropzone, card) + +func add_card(card: Card) -> void: + add_child(card) + cards.append(card) card.position = _generate_random_position() card.is_dragable = true -## Unified function to reclaim any sticky note to a panel -## Handles ALL scenarios: initial setup, drag-drop, exchanges, keyboard navigation -func reclaim_sticky_to_panel(sticky: StickyNote, animate: bool = true, prefer_panel_index: int = -1) -> void: - # Find or create target panel - var target_panel: StickyNotePanel = null - # Try preferred panel first (for exchanges) - if prefer_panel_index >= 0 and prefer_panel_index < sticky_note_container.get_child_count(): - var panel = sticky_note_container.get_child(prefer_panel_index) - if panel is StickyNotePanel and panel.is_empty(): - target_panel = panel +func add_note(note: StickyNote) -> void: + add_child(note) + notes.append(note) + note.is_dragable = true - # Find any empty panel - if not target_panel: - for panel in sticky_note_container.get_children(): - if panel is StickyNotePanel and panel.is_empty(): - target_panel = panel - break - - # Create new panel if needed - if not target_panel: - target_panel = StickyNotePanel.new() - sticky_note_container.add_child(target_panel, true, Node.INTERNAL_MODE_DISABLED) - - # Attach sticky to panel (handles all state setup and animation) - target_panel.attatch_sticky_note(sticky, self, animate) - - # Clean up other empty panels - if animate: # Only clean up during interactive use, not initial setup - for panel in sticky_note_container.get_children(): - if panel is StickyNotePanel and panel != target_panel: - panel.clear_if_empty() # Checks if a Node is currently inside the dropzone -func is_in_dropzone(to_check: Node) -> bool: - return dropzone.get_rect().has_point(to_check.global_position) +func is_in_dropzone(to_check: Draggable) -> bool: + return dropzone.get_rect().has_point(to_check.global_position) #TODO: is global pos correct here? + # Called by notes when a mouse event needs handling -func handle_mouse_button(input: InputEventMouseButton, to_handle = currently_active_node) -> void: +func handle_mouse_button(input: InputEventMouseButton, to_handle = selection) -> void: # Prevent dragging multiple nodes at once - if current_context == DRAG and to_handle != currently_active_node: + if current_context == DRAG and selection: return # === DRAG START === @@ -320,94 +225,61 @@ func handle_mouse_button(input: InputEventMouseButton, to_handle = currently_act ## Starts a drag operation for the given draggable func _start_drag(draggable: Draggable) -> void: - currently_active_node = draggable + selection = draggable current_context = DRAG - var mouse_offset = get_viewport().get_mouse_position() - draggable.global_position + var mouse_offset := get_viewport().get_mouse_position() - draggable.global_position draggable.start_drag(mouse_offset) ## Ends a drag operation and handles the drop func _end_drag(draggable: Draggable) -> void: + if not draggable: return + draggable.end_drag() # Let draggable find its own drop target - var drop_target = draggable.find_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: - var old_sticky = (drop_target as Card).get_last_exchanged_sticky() - if old_sticky: - # Return exchanged sticky to panel where new one came from (if applicable) - var prefer_panel = current_sticky_note_id if (draggable as StickyNote)._came_from_panel else -1 - reclaim_sticky_to_panel(old_sticky, true, prefer_panel) - # If sticky was dropped on board (not card), reclaim it to panel with animation - elif result == Draggable.DropResult.ACCEPTED and draggable is StickyNote and drop_target == self: - reclaim_sticky_to_panel(draggable, true) - elif draggable is StickyNote and not is_in_dropzone(draggable): - # Sticky dropped in panel area - reclaim to panel with animation - reclaim_sticky_to_panel(draggable, true) - else: - # Fallback: use default board drop (for cards) - handle_drop(draggable) + pass # 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(): + if draggable is StickyNote and draggable.is_attached: check_board_comnpletion() -## 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(draggable) +func reclaim_sticky(note: StickyNote): + note.reparent(self) + notes.append(note) -func _return_sticky_notes_to_panels() -> void: - if not (current_context == ASSIGN and focus_stickies): return #FIXME this is an early return to prevent race conditions. Check if it is save to be removed. - for panel:StickyNotePanel in sticky_note_container.get_children(): - if panel is StickyNotePanel and panel.attached_sticky_note: - # Reclaim if sticky is not already in the panel - if panel.attached_sticky_note.get_parent() != panel: - panel.attatch_sticky_note(panel.attached_sticky_note, self, true) - - for node in dropzone.get_children(): - if node is StickyNote: - node.is_dragable = true - -var board_was_completed: bool = false func check_board_comnpletion(): if is_board_complete(): - for child:StickyNotePanel in sticky_note_container.get_children(): - child.clear_if_empty() if not board_was_completed: board_was_completed = true board_completed.emit() + if board_was_completed: give_lore_feedback() func is_board_complete() -> bool: - if mementos_collected == 4: - for card in dropzone.get_children(): - if card is Card: - if not card.has_sticky_note_attached(): - return false - return true - return false + if mementos_collected < 4: return false + + for sticky : StickyNote in notes: + if not sticky.is_attached: return false + + return true var unfitting: bool = false var incomplete: bool = false var complete: bool = false + func give_lore_feedback(): var fitting_card_count: int = 0 var total_card_count: int = 0 @@ -426,8 +298,7 @@ func give_lore_feedback(): else: $AnimationPlayer.play("unfitting") unfitting = true - #FIXME: check if this logic (after the "or") is still needed. - elif fitting_card_count != total_card_count or (total_card_count != dropzone.get_child_count() and sticky_note_container.get_child_count() != 0): + elif fitting_card_count < total_card_count: instructions.text = TranslationServer.translate("You may leave the room, but Lisa only agrees with %d of the %d connections.") % [fitting_card_count, total_card_count] if not incomplete: if State.speech_language == 2: @@ -446,64 +317,20 @@ func give_lore_feedback(): # Mark area that was hovered over as currently selected func handle_hover(to_handle: Area2D) -> void: - if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): return - currently_active_node = to_handle + # 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 is_in_dropzone(to_handle) or to_handle is Card: - if not (current_context == ASSIGN and not currently_active_node.is_dragged): #Prevent Mouse input from messing up directional control selections - if not (to_handle is StickyNote and !to_handle.on_board): - current_dropzone_id = dropzone.get_children().find(to_handle) - focus_stickies = false - else: - current_sticky_note_id = sticky_note_container.get_children().find(to_handle.attached_to) - focus_stickies = true -# Adds a child at the correct child indext in an area -func insert_area(parent: Control, node: Area2D): - var children:Array = parent.get_children() - var i = 0 - if not node in parent.get_children(): - node.reparent(parent, false) # Don't preserve global transform - we set positions explicitly +func _spatial(a: Draggable, b: Draggable) -> bool: + return a.position.x + a.position.y * 100 > b.position.x + b.position.y * 100 - if children.size() > 0: - children.erase(node) - while children[i].global_position.y < node.global_position.y and i+1 < children.size(): - i+=1 - parent.move_child(node, i) - if node is StickyNote: - node.is_dragable = true - -## Sorts all children in dropzone by their Y position ## Call this after bulk loading to fix child order / z-index issues -func _sort_dropzone_children() -> void: - var children = dropzone.get_children() - if children.size() <= 1: - return +func _sort_by_positions() -> void: + cards.sort_custom(_spatial) + notes.sort_custom(_spatial) - # Sort by global Y position - children.sort_custom(func(a, b): return a.global_position.y < b.global_position.y) - - # Reorder children in the scene tree - for i in range(children.size()): - dropzone.move_child(children[i], i) - - print_debug("CardBoard: Re-sorted %d dropzone children by Y position" % children.size()) - - # Force collision shape updates on next physics frame - # This ensures Area2D hover detection works correctly after repositioning - _update_collision_shapes.call_deferred() - -## Forces collision shape updates for all cards/stickies in dropzone -func _update_collision_shapes() -> void: - await get_tree().process_frame - for child in dropzone.get_children(): - if child is Area2D: - # Force collision shape update by toggling monitoring - var was_monitoring = child.monitoring - child.monitoring = false - child.monitoring = was_monitoring # === DROP TARGET PATTERN IMPLEMENTATION === @@ -516,29 +343,13 @@ 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.is_dragable = true - # Reset visual state - sticky.rotation = 0.0 - sticky.scale = Vector2.ONE - 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: - - if not focused or not is_instance_valid(currently_active_node): return - if event.is_action_pressed("ui_cancel"): - focused = false + closed.emit() get_viewport().set_input_as_handled() if event is InputEventMouse: @@ -550,125 +361,6 @@ func _input(event) -> void: else: return - if current_context != DRAG: - var selection_position: Vector2 - if current_context == ASSIGN: - selection_position = dropzone.get_child( current_dropzone_id ).global_position - else: - selection_position = currently_active_node.global_position - - if event.is_action_pressed("ui_up"): - if focus_stickies: - current_sticky_note_id -= 1 - else: - if not try_select_nearest_card(selection_position, Vector2.UP): - current_dropzone_id -= 1 - get_viewport().set_input_as_handled() - - elif event.is_action_pressed("ui_down"): # down to select an element beneath - if focus_stickies: - current_sticky_note_id += 1 - else: - if not try_select_nearest_card(selection_position, Vector2.DOWN): - current_dropzone_id += 1 - get_viewport().set_input_as_handled() - - elif event.is_action_pressed("ui_right"): # left to switch context to the left - if not try_select_nearest_card(selection_position, Vector2.RIGHT, true): - if not focus_stickies: - if current_context == NAVIGATE: - focus_stickies = true - elif current_context == ASSIGN: - var panel = sticky_note_container.get_children()[current_sticky_note_id] - if panel is StickyNotePanel and panel.attached_sticky_note: - reclaim_sticky_to_panel(panel.attached_sticky_note, true, current_sticky_note_id) - current_context = NAVIGATE - get_viewport().set_input_as_handled() - - elif event.is_action_pressed("ui_left"): # right to switch context to the right - print_debug(try_select_nearest_card(selection_position, Vector2.LEFT)) - if focus_stickies: - if current_context == NAVIGATE: - focus_stickies = false - elif current_context == ASSIGN: - current_context = NAVIGATE - get_viewport().set_input_as_handled() - - elif event.is_action_pressed("ui_accept"): # select the selected note it - if current_context == ASSIGN: - if not dropzone.get_child(current_dropzone_id) is Card: return - var card:Card = dropzone.get_child(current_dropzone_id) - var sticky: StickyNote = currently_active_node if not focus_stickies else sticky_note_container.get_child(current_sticky_note_id).attached_sticky_note - - if card.has_sticky_note_attached(): - currently_active_node = card.exchange_sticky_note_with(sticky) - focus_stickies = false - if not try_select_nearest_empty_card(currently_active_node.global_position): - current_dropzone_id = find_first_free_card() - else: - card.attach_sticky_note(sticky) - current_context = NAVIGATE - for panel: StickyNotePanel in sticky_note_container.get_children(): - panel.clear_if_empty() - if not try_select_nearest_empty_card(currently_active_node.global_position): - current_dropzone_id = find_first_free_card() - check_board_comnpletion() - if focus_stickies: - focus_stickies = false - current_dropzone_id = current_dropzone_id - else: - focus_stickies = true - current_sticky_note_id -= 1 - - elif current_context == NAVIGATE: - if focus_stickies: - # this is kind of redundant, but a safety feature to avoid active node and index misaligning. - currently_active_node = sticky_note_container.get_children()[current_sticky_note_id].get_child(0) - current_context = ASSIGN - focus_stickies = false - if not try_select_nearest_empty_card(currently_active_node.global_position): - current_dropzone_id = find_first_free_card() - else: - if currently_active_node is StickyNote: - reclaim_sticky_to_panel(currently_active_node, true) - current_sticky_note_id = sticky_note_container.get_child_count()-1 - current_context = ASSIGN - focus_stickies = false - if currently_active_node is Card: - if currently_active_node.has_sticky_note_attached(): - currently_active_node = currently_active_node.remove_sticky_note() - reclaim_sticky_to_panel(currently_active_node, true) - current_sticky_note_id = sticky_note_container.get_child_count()-1 - focus_stickies = true - else: - if not is_board_complete(): - current_context = ASSIGN - focus_stickies = true - current_sticky_note_id = current_sticky_note_id - get_viewport().set_input_as_handled() - - -## Awaitable function to show the board and wait until user closes it -func play() -> void: - focused = true - Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - await closed - -func find_first_free_card() -> int: - for i in range(dropzone.get_child_count()): - # start searching at the current location, use modulo to avoid getting out of array bounds - if !dropzone.get_child((i+current_dropzone_id)%dropzone.get_child_count()).has_sticky_note_attached(): - return (i+current_dropzone_id)%dropzone.get_child_count() - return -1 - -func on_sticky_panel_cleared(at_id: int): - if current_sticky_note_id == at_id: - current_sticky_note_id += 1 - if current_sticky_note_id == sticky_note_container.get_child_count()-1: - if current_sticky_note_id-1 != at_id: - current_sticky_note_id -= 1 - else: - current_sticky_note_id += 1 ## Saves board state directly to SaveGame resource func save_to_resource(savegame: SaveGame) -> void: @@ -679,8 +371,7 @@ func save_to_resource(savegame: SaveGame) -> void: print_debug("CardBoard: Saving board state...") - for child in dropzone.get_children(): - if child is Card: + for child in cards: # Save card position (local to dropzone) savegame.board_positions[child.name] = child.position print_debug(" Card '%s' at %s" % [child.name, child.position]) @@ -696,20 +387,19 @@ func save_to_resource(savegame: SaveGame) -> void: if note.picked_random: savegame.board_randoms.append(note.name) - elif child is StickyNote: - # Save position of loose sticky on board (local to dropzone) - savegame.board_positions[child.name] = child.position - print_debug(" Loose sticky '%s' at %s" % [child.name, child.position]) - if child.picked_random: - savegame.board_randoms.append(child.name) - for child in sticky_note_container.get_children(): - if child is StickyNotePanel and child.attached_sticky_note: - # Save sticky in panel state - savegame.board_in_panel.append(child.attached_sticky_note.name) - print_debug(" Sticky '%s' in panel" % child.attached_sticky_note.name) - if child.attached_sticky_note.picked_random: - savegame.board_randoms.append(child.attached_sticky_note.name) + # Save position of loose sticky on board (local to dropzone) + for child in notes: + print_debug(" Loose sticky '%s' at %s" % [child.name, child.position]) + + savegame.board_positions[child.name] = child.position + savegame.board_in_panel.append(child.name) + + if child.picked_random: + savegame.board_randoms.append(child.name) + + if child.picked_random: + savegame.board_randoms.append(child.name) print_debug("CardBoard: Saved %d positions, %d attachments, %d in panel" % [ savegame.board_positions.size(), @@ -719,7 +409,6 @@ func save_to_resource(savegame: SaveGame) -> void: - func initialise_from_save(savegame: SaveGame) -> void: # Early return if nothing to load if savegame.board_positions.is_empty(): @@ -733,101 +422,66 @@ func initialise_from_save(savegame: SaveGame) -> void: savegame.board_in_panel.size() ]) - # Collect all card/sticky names from all relevant dictionaries - var all_names: Array[StringName] + # Clear & Collect all card/sticky names from all relevant dictionaries + all_names = [] # Names from positions (cards and loose stickies) for item_name: StringName in savegame.board_positions.keys(): - if not all_names.has(item_name): - all_names.append(item_name) + all_names.append(item_name) # Sticky names from attachments for sticky_name: StringName in savegame.board_attachments.keys(): - if not all_names.has(sticky_name): - all_names.append(sticky_name) + all_names.append(sticky_name) # Card names from attachments (the values) for card_name: StringName in savegame.board_attachments.values(): - if not all_names.has(card_name): - all_names.append(card_name) + all_names.append(card_name) # Sticky names from panel for item_name: StringName in savegame.board_in_panel: - if not all_names.has(item_name): - all_names.append(item_name) + all_names.append(item_name) print_debug(" Collected %d unique card/sticky names to load" % all_names.size()) - var card_pile: Dictionary[String, Array] = HardCards.get_cards_by_name_array(all_names) + populate_board(all_names) - # Track cards by name for sticky note attachment - var cards_by_name: Dictionary = {} + # Track cards by name for sticky note attachment pass + var cards_by_name: Dictionary[StringName, Card] = {} + for card in cards: + cards_by_name[card.name] = card # Calculate mementos collected (each memento gives 2 cards) - mementos_collected = int(card_pile["cards"].size() / 2.0) - print_debug(" Calculated mementos_collected: %d (from %d cards)" % [mementos_collected, card_pile["cards"].size()]) + mementos_collected = int(len(cards) / 2.0) - # Add all cards - print_debug(" Loading %d cards..." % card_pile["cards"].size()) - for card: Card in card_pile["cards"]: - # Determine target position (will be set after adding to scene) + # Position all cards + for card: Card in cards: var target_position: Vector2 if savegame.board_positions.has(card.name): target_position = savegame.board_positions[card.name] print_debug(" Card '%s' loading at %s" % [card.name, target_position]) else: target_position = _generate_random_position() - print_debug(" Card '%s' - generated random position: %s" % [card.name, target_position]) + push_warning(" Card '%s' - generated random position: %s" % [card.name, target_position]) - # Add to board first - add_child(card) - card.is_dragable = true - cards_by_name[card.name] = card + card.position = target_position 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()) - for sticky: StickyNote in card_pile["sticky_notes"]: - # Check if sticky is in panel - if savegame.board_in_panel.has(sticky.name): - reclaim_sticky_to_panel(sticky, false) - print_debug(" Sticky '%s' added to panel" % sticky.name) - - # Check if sticky is attached to a card - elif savegame.board_attachments.has(sticky.name): - var card_name = savegame.board_attachments[sticky.name] - if cards_by_name.has(card_name): - # Must add sticky to scene tree BEFORE attach_sticky_note() can reparent it - add_child(sticky) - sticky.current_handle = self # Required for input handling - cards_by_name[card_name].attach_sticky_note(sticky) - print_debug(" Sticky '%s' attached to card '%s'" % [sticky.name, card_name]) - else: - push_warning("CardBoard: Sticky '%s' attached to non-existent card '%s', adding to panel" % [sticky.name, card_name]) - reclaim_sticky_to_panel(sticky, false) - - # Sticky is loose on board + for sticky: StickyNote in notes: + var card_name := savegame.board_attachments[sticky.name] + var card : Card = cards_by_name.get(card_name, null) + if card: + sticky.reparent(card) # was alrady added as our own child by populate_board + card.attach_sticky_note(sticky) + print_debug(" Sticky '%s' attached to card '%s'" % [sticky.name, card_name]) else: - # Determine target position (will be set after adding to scene) + # Sticky is loose on board var target_position: Vector2 if savegame.board_positions.has(sticky.name): target_position = savegame.board_positions[sticky.name] - print_debug(" Loose sticky '%s' loading at %s" % [sticky.name, target_position]) else: 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) - sticky.current_handle = self # Required for input handling - 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) @@ -835,69 +489,33 @@ func initialise_from_save(savegame: SaveGame) -> void: print_debug("CardBoard: Load complete!") # Re-sort dropzone children now that all positions are set correctly - # This fixes hover detection issues caused by incorrect z-order during load - _sort_dropzone_children() - - # Note: Lore feedback will be triggered when board is presented (in play()) + _sort_by_positions() +# === Computed Properties === +var dropzone_size: Vector2: + # FIXME: Hardcode + get: return get_viewport_rect().size - Vector2(dropzone_padding + sticky_width, dropzone_padding) -func try_select_nearest_card(from: Vector2, towards: Vector2, include_stickies: bool = false) -> bool: - var selection_transform := Transform2D(0, from).looking_at(from+towards) +var _selection_candidates : Array[Draggable]: + get: + match selection_state: + SelectionState.CARDS: return cards as Array[Draggable] + SelectionState.STICKIES: return notes as Array[Draggable] + SelectionState.FREE: + print_debug("switching from free selection to guided stickies selection") - var scores: Dictionary[int, Area2D] = {-1: null} - for child:Area2D in dropzone.get_children(): - if not (child is StickyNote and current_context == ASSIGN): - scores[get_distance_score(child.global_position, selection_transform)] = child - scores.erase(-1) - scores.sort() - - if include_stickies: - var panel_scores: Dictionary[int, StickyNotePanel] = {-1: null} - for child:StickyNotePanel in sticky_note_container.get_children(): - if not child.is_empty(): - panel_scores[get_distance_score(child.attached_sticky_note.global_position, selection_transform)] = child - panel_scores.erase(-1) - panel_scores.sort() - - if panel_scores != {}: - if scores != {}: - if panel_scores.keys()[0] < scores.keys()[0]: - if current_context == ASSIGN: return false - current_sticky_note_id = sticky_note_container.get_children().find(panel_scores.values()[0]) - focus_stickies = true - return true - else: - if current_context == ASSIGN: return false - current_sticky_note_id = sticky_note_container.get_children().find(panel_scores.values()[0]) - focus_stickies = true - return true + # Otherwise default to sticky selection + selection_state = SelectionState.STICKIES + return notes as Array[Draggable] - if scores != {}: - current_dropzone_id = dropzone.get_children().find(scores.values()[0]) - return true - return false +# === Util === -func try_select_nearest_empty_card(from: Vector2) -> bool: - var scores: Dictionary[int, Area2D] = {} - - for card in dropzone.get_children(): - if card is Card: - if not card.has_sticky_note_attached(): - scores[int((from-card.global_position).length())] = card - - scores.sort() - - if scores != {}: - current_dropzone_id = dropzone.get_children().find(scores.values()[0]) - return true - return false - -func get_distance_score(from: Vector2, to: Transform2D) -> int: - var diff := from * to - var dir := diff.normalized() - if dir.x > 0.5 and diff.length() > 0: - return int((abs(dir.y) + 0.5) * diff.length()) - else: - return -1 +func _debug_mode() -> void: + populate_board(["c_void", 'c_gifted', "p_wet", "p_joy"]) + populate_board(["c_jui_jutsu", 'c_hit', "p_girly", "p_vent"]) + populate_board(["c_comic_heroes", 'c_teasing', "p_agent_q", "p_good_intended"]) + populate_board(["c_out_of_world", 'c_confusion', "p_outer_conflict", "p_unique"]) + await get_tree().process_frame + play() diff --git a/src/logic-scenes/board/card.gd b/src/logic-scenes/board/card.gd index 4058922..9628492 100644 --- a/src/logic-scenes/board/card.gd +++ b/src/logic-scenes/board/card.gd @@ -152,7 +152,7 @@ func init(card_name: String = "card", own_id:StringName = "-1") -> void: if card_name != "c_void": text = card_name if !card_name.begins_with("c"): - push_error("Illegal card.") + push_error("Illegal card!", card_name, own_id) card_id = own_id name = card_name diff --git a/src/logic-scenes/board/draggable.gd b/src/logic-scenes/board/draggable.gd index 15df1fd..59ac0a9 100644 --- a/src/logic-scenes/board/draggable.gd +++ b/src/logic-scenes/board/draggable.gd @@ -4,6 +4,9 @@ extends Area2D ## Base class for draggable UI elements (Cards and StickyNotes) ## Provides common dragging behavior and boundary protection +## Margin from screen edges when confining to screen bounds +@export var screen_margin: float = 50.0 + ## Drop result codes for DropTarget pattern enum DropResult { ACCEPTED, # Drop successful, item is now owned by target @@ -11,11 +14,6 @@ enum DropResult { 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 @@ -34,9 +32,6 @@ var highlighted: bool: func set_highlight(value: bool) -> void: _highlighted = value -## 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 @@ -69,27 +64,19 @@ 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: - # Skip if this is a sticky note attached to a card - if self is StickyNote: - var sticky := self as StickyNote - if sticky.attached_to is Card: - return - # Try to get bounds from parent container var bounds := _get_container_bounds() - - # If no container bounds, use viewport/screen bounds - if bounds == Rect2(): - bounds = _get_viewport_bounds() - + # If we have valid bounds, clamp position if bounds != Rect2(): position.x = clampf(position.x, bounds.position.x, bounds.position.x + bounds.size.x) position.y = clampf(position.y, bounds.position.y, bounds.position.y + bounds.size.y) + ## Gets the bounds of the parent container if it exists and is a Control node func _get_container_bounds() -> Rect2: var parent := get_parent() @@ -104,26 +91,20 @@ func _get_container_bounds() -> Rect2: control.size.x - screen_margin * 2, control.size.y - screen_margin * 2 ) - - # Check if parent is a Node2D with defined boundaries - # (for future support of non-Control containers) - if parent is Node2D: - # For now, return empty rect - could be extended in the future - # to check for custom boundary properties - pass - - return Rect2() + + # Default: whole screen + var viewport_size := get_viewport().get_visible_rect().size + return Rect2( + screen_margin, + screen_margin, + viewport_size.x - 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") -## Gets the viewport bounds as fallback -func _get_viewport_bounds() -> Rect2: - var viewport := get_viewport() - if viewport: - var viewport_size := viewport.get_visible_rect().size - return Rect2( - screen_margin, - screen_margin, - viewport_size.x - screen_margin * 2, - viewport_size.y - screen_margin * 2 - ) - - return Rect2() diff --git a/src/logic-scenes/board/physics-board.tscn b/src/logic-scenes/board/physics-board.tscn index 6bc7cc6..3946dba 100644 --- a/src/logic-scenes/board/physics-board.tscn +++ b/src/logic-scenes/board/physics-board.tscn @@ -164,21 +164,17 @@ script = ExtResource("3_8v4c4") [node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 2 -[node name="dropzone" type="Panel" parent="HBoxContainer"] +[node name="CardZone" type="Control" parent="HBoxContainer"] +unique_name_in_owner = true self_modulate = Color(1, 1, 1, 0) layout_mode = 2 size_flags_horizontal = 3 mouse_filter = 1 -[node name="ScrollContainer" type="ScrollContainer" parent="HBoxContainer"] -clip_contents = false +[node name="NoteZone" type="Control" parent="HBoxContainer"] +unique_name_in_owner = true custom_minimum_size = Vector2(400, 0) layout_mode = 2 -horizontal_scroll_mode = 0 - -[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ScrollContainer"] -z_index = 120 -layout_mode = 2 [node name="instructions_panel" type="PanelContainer" parent="."] layout_mode = 2 diff --git a/src/logic-scenes/board/sticky-note.gd b/src/logic-scenes/board/sticky-note.gd index 8d95905..acc3189 100644 --- a/src/logic-scenes/board/sticky-note.gd +++ b/src/logic-scenes/board/sticky-note.gd @@ -13,10 +13,14 @@ var current_handle: Node var position_locked: bool = false -## Computed property: Returns the current attachment (parent node) +## Computed property: Is this currently attached to a card +var is_attached : bool: + get: return get_parent() is Card + ## Replaces the need for tracking attached_to as state -var attached_to: Node: - get: return get_parent() +var attached_to: Card: + get: return get_parent() as Card if is_attached else null + signal transform_tween_finished @@ -35,6 +39,7 @@ var label: Label @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: @@ -71,7 +76,7 @@ var mouse_offset: Vector2 ## Replaces on_board state tracking var on_board: bool: get: - var parent = get_parent() + var parent := get_parent() return parent != null and parent.name == "dropzone" func init(sticky_name: String = "sticky_note", card_id: StringName = "-1") -> void: @@ -101,14 +106,7 @@ func _on_text_updated(): 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() + _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"): @@ -116,25 +114,17 @@ func _on_mouse_entered(): 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() + var card := attached_to + if card: + 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_enter(_area: Area2D): + pass -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_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"): @@ -142,21 +132,27 @@ func _on_input_event(_viewport, event, _shape_idx): mouse_offset = get_viewport().get_mouse_position() - global_position current_handle.handle_mouse_button(event, self) -func _move_sticky_note(): +## 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: update_drag_position(get_viewport().get_mouse_position()) + return + + if is_attached: + var card := attached_to + position = _smooth(position, card.sticky_note_position, delta) + + -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): +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") @@ -175,28 +171,15 @@ func tween_transform_to(target: Transform2D, duration: float = 0.25): # === 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() + var card := attached_to + if card: + 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: @@ -236,3 +219,6 @@ func _find_nearest_panel() -> StickyNotePanel: # No empty panels found - will need to create one (handled by board) return null + +func confine_to_screen() -> void: + if attached_to is not Card: super.confine_to_screen() diff --git a/src/logic-scenes/board/sticky_note_panel.gd b/src/logic-scenes/board/sticky_note_panel.gd index 042fa76..eff1bbb 100644 --- a/src/logic-scenes/board/sticky_note_panel.gd +++ b/src/logic-scenes/board/sticky_note_panel.gd @@ -14,12 +14,19 @@ func _init(cstm_minimum_size: Vector2 = minimum_size, note_position: Vector2 = V mouse_filter = MOUSE_FILTER_PASS self_modulate = Color(1, 1, 1, 0) +@onready var board : CardBoard = get_parent().get_parent() as CardBoard + func _ready(): custom_minimum_size = Vector2(custom_minimum_size.x, 0) -var is_attatching: bool = false +func _process(delta: float) -> void: + var child := get_child(0) as StickyNote + if child and not child.is_dragged: + var k := pow(0.1, 60.0 * delta) + child.position = child.position * (1.0-k) + ancor_position * (k) + +var is_attaching: bool = false func attatch_sticky_note(attatchment: StickyNote, custom_handle: Node, animate:bool = true): - is_attatching = true attached_sticky_note = attatchment attatchment.current_handle = custom_handle @@ -56,74 +63,6 @@ func attatch_sticky_note(attatchment: StickyNote, custom_handle: Node, animate:b attatchment.rotation = 0.0 attatchment.scale = Vector2.ONE - is_attatching = false - - -var is_gapped: bool = false -func create_gap(): - var self_id := get_parent().get_children().find(self) - var next_id = min(self_id + 1, get_parent().get_child_count() - 1) - var previous_id = max(self_id - 1, 0) - - var board = _get_board() - if not (is_gapped or get_parent().get_child(next_id).attached_sticky_note.is_dragged or get_parent().get_child(previous_id).attached_sticky_note.is_dragged) and board and board.current_context == CardBoard.DRAG: - is_gapped = true - var height_tween: Tween = create_tween() - height_tween.tween_property(self, "custom_minimum_size", minimum_size*Vector2(1.0, 1.8), 0.1) - - get_parent().get_child(next_id).collapse_gap() - if not get_parent().get_children().find(self) == 0: get_parent().get_child(previous_id).collapse_gap() - -func collapse_gap(): - if is_gapped: - is_gapped = false - var height_tween: Tween = create_tween() - height_tween.tween_property(self, "custom_minimum_size", minimum_size, 0.1) - -var invalid: bool = false -func clear_if_empty(): - if !is_empty(): return - invalid = true - # No need to manually clear attached_to - reparenting handles it - var height_tween: Tween = create_tween() - height_tween.tween_property(self, "custom_minimum_size", Vector2.ZERO, 0.3) - await height_tween.finished - var board = _get_board() - if board: - board.on_sticky_panel_cleared(get_parent().get_children().find(self)) - self.queue_free() - -func replace_sticky_note_with(new_sticky_note: StickyNote): - if is_empty(): - attached_sticky_note = new_sticky_note - func is_empty() -> bool: - return get_child_count() == 0 and not is_attatching + return get_child_count() == 0 - -# === 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 with animation - var board = _get_board() - if board: - attatch_sticky_note(draggable, board, 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 - - -func _get_board() -> CardBoard: - return get_parent().get_parent() as CardBoard diff --git a/src/logic-scenes/card_picker/card_picker.gd b/src/logic-scenes/card_picker/card_picker.gd index 52edc90..64d7863 100644 --- a/src/logic-scenes/card_picker/card_picker.gd +++ b/src/logic-scenes/card_picker/card_picker.gd @@ -1,4 +1,5 @@ -class_name CardPicker extends CenterContainer +class_name CardPicker +extends Playable #fixme INI is probably redundant. enum { diff --git a/src/logic-scenes/interactable/interactable.gd b/src/logic-scenes/interactable/interactable.gd index f5ef557..ac75d17 100644 --- a/src/logic-scenes/interactable/interactable.gd +++ b/src/logic-scenes/interactable/interactable.gd @@ -1,7 +1,7 @@ class_name Interactable extends Node3D @export var interaction: PackedScene = null -var interaction_ui : Control = null +var interaction_ui : Playable = null @onready var view: Node3D = $View @onready var frame: Sprite3D = $Frame @@ -65,6 +65,7 @@ func expand() -> void: func collapse() -> void: + if not shown: return #TODO: test shown = false if tween and tween.is_valid(): tween.kill() tween = create_tween().set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK) @@ -143,7 +144,7 @@ func interact() -> void: shown = false await collapse() - # Hide mouse and collapse other interactables BEFORE showing canvas + # collapse other interactables BEFORE showing canvas get_tree().call_group("interactables", "collapse") # Show the CanvasLayer so the story is visible full-screen @@ -164,33 +165,7 @@ func interact() -> void: func _update_caption() -> void: if interaction_ui is StoryPlayable: var story := interaction_ui as StoryPlayable - match story.scene_id: - Scenes.id.YOUTH_DRAVEN: - caption.text = TranslationServer.translate("Starlight") - Scenes.id.YOUTH_CHILDHOOD: - caption.text = TranslationServer.translate("crafted Mask") - Scenes.id.YOUTH_VOICE_TRAINING: - caption.text = TranslationServer.translate("Comic Stash") - Scenes.id.YOUTH_JUI_JUTSU: - caption.text = TranslationServer.translate("Sports Clothes") - Scenes.id.TRANSITION: - caption.text = TranslationServer.translate("Move on") - Scenes.id.ADULT_DND: - caption.text = TranslationServer.translate("colorful Dice") - Scenes.id.ADULT_VOLUNTARY: - caption.text = TranslationServer.translate("Gemstone Art") - Scenes.id.ADULT_CHRISTMAS: - caption.text = TranslationServer.translate("Chat Messages") - Scenes.id.ADULT_EATING: - caption.text = TranslationServer.translate("Dishes") - Scenes.id.ADULT_UNI: - caption.text = TranslationServer.translate("Science Poster") - Scenes.id.ADULT_THERAPY: - caption.text = TranslationServer.translate("Doctors Note") - Scenes.id.ADULT_BURNOUT: - caption.text = TranslationServer.translate("Paperwork") - _: - caption.text = "" + caption.text = I18n.get_story_caption(story.scene_id) elif interaction_ui is CardBoard: caption.text = TranslationServer.translate("Mind Board") diff --git a/src/logic-scenes/luna/playables/story_playable.gd b/src/logic-scenes/luna/playables/story_playable.gd index 5c2e25c..8137b83 100644 --- a/src/logic-scenes/luna/playables/story_playable.gd +++ b/src/logic-scenes/luna/playables/story_playable.gd @@ -1,5 +1,5 @@ -extends CenterContainer class_name StoryPlayable +extends Playable signal text_finished signal finished @@ -148,7 +148,7 @@ func play(): show() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - # Don't know how to do this. + # FIXME: Don't know how to do this. #%StoryScroll.grab_focus() if name == "draven": diff --git a/src/logic-scenes/playable.gd b/src/logic-scenes/playable.gd new file mode 100644 index 0000000..ebe7e41 --- /dev/null +++ b/src/logic-scenes/playable.gd @@ -0,0 +1,6 @@ +extends Control +class_name Playable + +## Awaitable that encapsulates the core interaction with this Playable +func play() -> void: + await get_tree().process_frame # Dummy wait so this is a coroutine diff --git a/src/logic-scenes/playable.gd.uid b/src/logic-scenes/playable.gd.uid new file mode 100644 index 0000000..843c302 --- /dev/null +++ b/src/logic-scenes/playable.gd.uid @@ -0,0 +1 @@ +uid://dbmkkouhc0euw diff --git a/src/project.godot b/src/project.godot index 87c6b7b..51872e3 100644 --- a/src/project.godot +++ b/src/project.godot @@ -28,6 +28,7 @@ PromptManager="*res://addons/input_prompts/input_prompt_manager.gd" Steam="*res://dev-util/steam.gd" Main="*res://singletons/main/main.tscn" HardCards="*res://dev-util/hardcoded_cards.tscn" +I18n="*res://dev-util/i18n.gd" [debug] diff --git a/src/tests/board_tests.tscn b/src/tests/board_tests.tscn new file mode 100644 index 0000000..3e361d6 --- /dev/null +++ b/src/tests/board_tests.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://b752f680edsnv"] + +[ext_resource type="PackedScene" uid="uid://bnskiyx1sksww" path="res://logic-scenes/board/physics-board.tscn" id="1_b12jd"] + +[node name="BoardTests" type="Node"] + +[node name="board" parent="." instance=ExtResource("1_b12jd")] diff --git a/src/ui/menu_main/save_game_list.gd b/src/ui/menu_main/save_game_list.gd index c279369..0d7ba17 100644 --- a/src/ui/menu_main/save_game_list.gd +++ b/src/ui/menu_main/save_game_list.gd @@ -55,7 +55,7 @@ func _load_games(): func _sort_saves() -> void: - saves.sort_custom(func(a: SaveGame, b: SaveGame) -> int: + saves.sort_custom(func(a: SaveGame, b: SaveGame) -> bool: return a.last_saved > b.last_saved )