## Abstract base class for all card containers in the card framework. ## ## CardContainer provides the foundational functionality for managing collections of cards, ## including drag-and-drop operations, position management, and container interactions. ## All specialized containers (Hand, Pile, etc.) extend this class. ## ## Key Features: ## - Card collection management with position tracking ## - Drag-and-drop integration with DropZone system ## - History tracking for undo/redo operations ## - Extensible layout system through virtual methods ## - Visual debugging support for development ## ## Virtual Methods to Override: ## - _card_can_be_added(): Define container-specific rules ## - _update_target_positions(): Implement container layout logic ## - on_card_move_done(): Handle post-movement processing ## ## Usage: ## [codeblock] ## class_name MyContainer ## extends CardContainer ## ## func _card_can_be_added(cards: Array) -> bool: ## return cards.size() == 1 # Only allow single cards ## [/codeblock] class_name CardContainer extends Control # Static counter for unique container identification static var next_id: int = 0 @export_group("drop_zone") ## Enables or disables the drop zone functionality. @export var enable_drop_zone := true @export_subgroup("Sensor") ## The size of the sensor. If not set, it will follow the size of the card. @export var sensor_size: Vector2 ## The position of the sensor. @export var sensor_position: Vector2 ## The texture used for the sensor. @export var sensor_texture: Texture ## Determines whether the sensor is visible or not. ## Since the sensor can move following the status, please use it for debugging. @export var sensor_visibility := false # Container identification and management var unique_id: int var drop_zone_scene = preload("drop_zone.tscn") var drop_zone: DropZone = null # Card collection and state var _held_cards: Array[Card] = [] var _holding_cards: Array[Card] = [] # Scene references var cards_node: Control var card_manager: CardManager var debug_mode := false func _init() -> void: unique_id = next_id next_id += 1 func _ready() -> void: # Check if 'Cards' node already exists if has_node("Cards"): cards_node = $Cards else: cards_node = Control.new() cards_node.name = "Cards" cards_node.mouse_filter = Control.MOUSE_FILTER_PASS add_child(cards_node) var parent = get_parent() if parent is CardManager: card_manager = parent else: push_error("CardContainer should be under the CardManager") return card_manager._add_card_container(unique_id, self) if enable_drop_zone: drop_zone = drop_zone_scene.instantiate() add_child(drop_zone) drop_zone.init(self, [CardManager.CARD_ACCEPT_TYPE]) # If sensor_size is not set, they will follow the card size. if sensor_size == Vector2(0, 0): sensor_size = card_manager.card_size drop_zone.set_sensor(sensor_size, sensor_position, sensor_texture, sensor_visibility) if debug_mode: drop_zone.sensor_outline.visible = true else: drop_zone.sensor_outline.visible = false func _exit_tree() -> void: if card_manager != null: card_manager._delete_card_container(unique_id) ## Adds a card to this container at the specified index. ## @param card: The card to add ## @param index: Position to insert (-1 for end) func add_card(card: Card, index: int = -1) -> void: if index == -1: _assign_card_to_container(card) else: _insert_card_to_container(card, index) _move_object(card, cards_node, index) ## Removes a card from this container. ## @param card: The card to remove ## @returns: True if card was removed, false if not found func remove_card(card: Card) -> bool: var index = _held_cards.find(card) if index != -1: _held_cards.remove_at(index) else: return false update_card_ui() return true ## Returns the number of contained cards func get_card_count() -> int: return _held_cards.size() ## Checks if this container contains the specified card. func has_card(card: Card) -> bool: return _held_cards.has(card) ## Removes all cards from this container. func clear_cards() -> void: for card in _held_cards: _remove_object(card) _held_cards.clear() update_card_ui() ## Checks if the specified cards can be dropped into this container. ## Override _card_can_be_added() in subclasses for custom rules. func check_card_can_be_dropped(cards: Array) -> bool: if not enable_drop_zone: return false if drop_zone == null: return false if drop_zone.accept_types.has(CardManager.CARD_ACCEPT_TYPE) == false: return false if not drop_zone.check_mouse_is_in_drop_zone(): return false return _card_can_be_added(cards) func get_partition_index() -> int: var vertical_index = drop_zone.get_vertical_layers() if vertical_index != -1: return vertical_index var horizontal_index = drop_zone.get_horizontal_layers() if horizontal_index != -1: return horizontal_index return -1 ## Shuffles the cards in this container using Fisher-Yates algorithm. func shuffle() -> void: _fisher_yates_shuffle(_held_cards) for i in range(_held_cards.size()): var card = _held_cards[i] cards_node.move_child(card, i) update_card_ui() ## Moves cards to this container with optional history tracking. ## @paramcard_container cards: Array of cards to move ## @param index: Target position (-1 for end) ## @param with_history: Whether to record for undo ## @returns: True if move was successful func move_cards(cards: Array, index: int = -1, with_history: bool = true) -> bool: if not _card_can_be_added(cards): return false # XXX: If the card is already in the container, we don't add it into the history. if not cards.all(func(card): return _held_cards.has(card)) and with_history: card_manager._add_history(self, cards) _move_cards(cards, index) return true ## Restores cards to their original positions with index precision. ## @param cards: Cards to restore ## @param from_indices: Original indices for precise positioning func undo(cards: Array, from_indices: Array = []) -> void: # Validate input parameters if not from_indices.is_empty() and cards.size() != from_indices.size(): push_error("Mismatched cards and indices arrays in undo operation!") # Fallback to basic undo _move_cards(cards) return # Fallback: add to end if no index info available if from_indices.is_empty(): _move_cards(cards) return # Validate all indices are valid for i in range(from_indices.size()): if from_indices[i] < 0: push_error("Invalid index found during undo: %d" % from_indices[i]) # Fallback to basic undo _move_cards(cards) return # Check if indices are consecutive (bulk move scenario) var sorted_indices = from_indices.duplicate() sorted_indices.sort() var is_consecutive = true for i in range(1, sorted_indices.size()): if sorted_indices[i] != sorted_indices[i-1] + 1: is_consecutive = false break if is_consecutive and sorted_indices.size() > 1: # Bulk consecutive restore: maintain original relative order var lowest_index = sorted_indices[0] # Sort cards by their original indices to maintain proper order var card_index_pairs = [] for i in range(cards.size()): card_index_pairs.append({"card": cards[i], "index": from_indices[i]}) # Sort by index ascending to maintain original order card_index_pairs.sort_custom(func(a, b): return a.index < b.index) # Insert all cards starting from the lowest index for i in range(card_index_pairs.size()): var target_index = min(lowest_index + i, _held_cards.size()) _move_cards([card_index_pairs[i].card], target_index) else: # Non-consecutive indices: restore individually (original logic) var card_index_pairs = [] for i in range(cards.size()): card_index_pairs.append({"card": cards[i], "index": from_indices[i], "original_order": i}) # Sort by index descending, then by original order ascending for stable sorting card_index_pairs.sort_custom(func(a, b): if a.index == b.index: return a.original_order < b.original_order return a.index > b.index ) # Restore each card to its original index for pair in card_index_pairs: var target_index = min(pair.index, _held_cards.size()) # Clamp to valid range _move_cards([pair.card], target_index) func hold_card(card: Card) -> void: if _held_cards.has(card): _holding_cards.append(card) func release_holding_cards(): if _holding_cards.is_empty(): return for card in _holding_cards: # Transition from HOLDING to IDLE state card.change_state(DraggableObject.DraggableState.IDLE) var copied_holding_cards = _holding_cards.duplicate() if card_manager != null: card_manager._on_drag_dropped(copied_holding_cards) _holding_cards.clear() func get_string() -> String: return "card_container: %d" % unique_id func on_card_move_done(_card: Card): pass func on_card_pressed(_card: Card): pass func _assign_card_to_container(card: Card) -> void: if card.card_container != self: card.card_container = self if not _held_cards.has(card): _held_cards.append(card) update_card_ui() func _insert_card_to_container(card: Card, index: int) -> void: if card.card_container != self: card.card_container = self if not _held_cards.has(card): if index < 0: index = 0 elif index > _held_cards.size(): index = _held_cards.size() _held_cards.insert(index, card) update_card_ui() func _move_to_card_container(_card: Card, index: int = -1) -> void: if _card.card_container != null: _card.card_container.remove_card(_card) add_card(_card, index) func _fisher_yates_shuffle(array: Array) -> void: for i in range(array.size() - 1, 0, -1): var j = randi() % (i + 1) var temp = array[i] array[i] = array[j] array[j] = temp func _move_cards(cards: Array, index: int = -1) -> void: var cur_index = index for i in range(cards.size() - 1, -1, -1): var card = cards[i] if cur_index == -1: _move_to_card_container(card) else: _move_to_card_container(card, cur_index) cur_index += 1 func _card_can_be_added(_cards: Array) -> bool: return true ## Updates the visual positions of all cards in this container. ## Call this after modifying card positions or container properties. func update_card_ui() -> void: _update_target_z_index() _update_target_positions() func _update_target_z_index() -> void: pass func _update_target_positions() -> void: pass func _move_object(target: Node, to: Node, index: int = -1) -> void: if target.get_parent() == to: # If already the same parent, just change the order with move_child if index != -1: to.move_child(target, index) else: # If index is -1, move to the last position to.move_child(target, to.get_child_count() - 1) return var global_pos = target.global_position if target.get_parent() != null: target.get_parent().remove_child(target) if index != -1: to.add_child(target) to.move_child(target, index) else: to.add_child(target) target.global_position = global_pos func _remove_object(target: Node) -> void: var parent = target.get_parent() if parent != null: parent.remove_child(target) target.queue_free()