Files
jass-learner/addons/card-framework/card_container.gd
Aspergerli ade3d0fb01 init
2026-03-09 19:18:47 +01:00

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()