385 lines
11 KiB
GDScript
385 lines
11 KiB
GDScript
## 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()
|