init
This commit is contained in:
384
addons/card-framework/card_container.gd
Normal file
384
addons/card-framework/card_container.gd
Normal file
@@ -0,0 +1,384 @@
|
||||
## 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()
|
||||
Reference in New Issue
Block a user