142 lines
5.1 KiB
GDScript
142 lines
5.1 KiB
GDScript
## A stacked card container with directional positioning and interaction controls.
|
|
##
|
|
## Pile provides a traditional card stack implementation where cards are arranged
|
|
## in a specific direction with configurable spacing. It supports various interaction
|
|
## modes from full movement to top-card-only access, making it suitable for deck
|
|
## implementations, foundation piles, and discard stacks.
|
|
##
|
|
## Key Features:
|
|
## - Directional stacking (up, down, left, right)
|
|
## - Configurable stack display limits and spacing
|
|
## - Flexible interaction controls (all cards, top only, none)
|
|
## - Dynamic drop zone positioning following top card
|
|
## - Visual depth management with z-index layering
|
|
##
|
|
## Common Use Cases:
|
|
## - Foundation piles in Solitaire games
|
|
## - Draw/discard decks with face-down cards
|
|
## - Tableau piles with partial card access
|
|
##
|
|
## Usage:
|
|
## [codeblock]
|
|
## @onready var deck = $Deck
|
|
## deck.layout = Pile.PileDirection.DOWN
|
|
## deck.card_face_up = false
|
|
## deck.restrict_to_top_card = true
|
|
## [/codeblock]
|
|
class_name Pile
|
|
extends CardContainer
|
|
|
|
# Enums
|
|
## Defines the stacking direction for cards in the pile.
|
|
enum PileDirection {
|
|
UP, ## Cards stack upward (negative Y direction)
|
|
DOWN, ## Cards stack downward (positive Y direction)
|
|
LEFT, ## Cards stack leftward (negative X direction)
|
|
RIGHT ## Cards stack rightward (positive X direction)
|
|
}
|
|
|
|
@export_group("pile_layout")
|
|
## Distance between each card in the stack display
|
|
@export var stack_display_gap := CardFrameworkSettings.LAYOUT_STACK_GAP
|
|
## Maximum number of cards to visually display in the pile
|
|
## Cards beyond this limit will be hidden under the visible stack
|
|
@export var max_stack_display := CardFrameworkSettings.LAYOUT_MAX_STACK_DISPLAY
|
|
## Whether cards in the pile show their front face (true) or back face (false)
|
|
@export var card_face_up := true
|
|
## Direction in which cards are stacked from the pile's base position
|
|
@export var layout := PileDirection.UP
|
|
|
|
@export_group("pile_interaction")
|
|
## Whether any card in the pile can be moved via drag-and-drop
|
|
@export var allow_card_movement: bool = true
|
|
## Restricts movement to only the top card (requires allow_card_movement = true)
|
|
@export var restrict_to_top_card: bool = true
|
|
## Whether drop zone follows the top card position (requires allow_card_movement = true)
|
|
@export var align_drop_zone_with_top_card := true
|
|
|
|
|
|
## Returns the top n cards from the pile without removing them.
|
|
## Cards are returned in top-to-bottom order (most recent first).
|
|
## @param n: Number of cards to retrieve from the top
|
|
## @returns: Array of cards from the top of the pile (limited by available cards)
|
|
func get_top_cards(n: int) -> Array:
|
|
var arr_size = _held_cards.size()
|
|
if n > arr_size:
|
|
n = arr_size
|
|
|
|
var result = []
|
|
|
|
for i in range(n):
|
|
result.append(_held_cards[arr_size - 1 - i])
|
|
|
|
return result
|
|
|
|
|
|
## Updates z-index values for all cards to maintain proper layering.
|
|
## Pressed cards receive elevated z-index to appear above the pile.
|
|
func _update_target_z_index() -> void:
|
|
for i in range(_held_cards.size()):
|
|
var card = _held_cards[i]
|
|
if card.is_pressed:
|
|
card.stored_z_index = CardFrameworkSettings.VISUAL_PILE_Z_INDEX + i
|
|
else:
|
|
card.stored_z_index = i
|
|
|
|
|
|
## Updates visual positions and interaction states for all cards in the pile.
|
|
## Positions cards according to layout direction and applies interaction restrictions.
|
|
func _update_target_positions() -> void:
|
|
# Calculate top card position for drop zone alignment
|
|
var last_index = _held_cards.size() - 1
|
|
if last_index < 0:
|
|
last_index = 0
|
|
var last_offset = _calculate_offset(last_index)
|
|
|
|
# Align drop zone with top card if enabled
|
|
if enable_drop_zone and align_drop_zone_with_top_card:
|
|
drop_zone.change_sensor_position_with_offset(last_offset)
|
|
|
|
# Position each card and set interaction state
|
|
for i in range(_held_cards.size()):
|
|
var card = _held_cards[i]
|
|
var offset = _calculate_offset(i)
|
|
var target_pos = position + offset
|
|
|
|
# Set card appearance and position
|
|
card.show_front = card_face_up
|
|
card.move(target_pos, 0)
|
|
|
|
# Apply interaction restrictions
|
|
if not allow_card_movement:
|
|
card.can_be_interacted_with = false
|
|
elif restrict_to_top_card:
|
|
if i == _held_cards.size() - 1:
|
|
card.can_be_interacted_with = true
|
|
else:
|
|
card.can_be_interacted_with = false
|
|
|
|
|
|
## Calculates the visual offset for a card at the given index in the stack.
|
|
## Respects max_stack_display limit to prevent excessive visual spreading.
|
|
## @param index: Position of the card in the stack (0 = bottom, higher = top)
|
|
## @returns: Vector2 offset from the pile's base position
|
|
func _calculate_offset(index: int) -> Vector2:
|
|
# Clamp to maximum display limit to prevent visual overflow
|
|
var actual_index = min(index, max_stack_display - 1)
|
|
var offset_value = actual_index * stack_display_gap
|
|
var offset = Vector2()
|
|
|
|
# Apply directional offset based on pile layout
|
|
match layout:
|
|
PileDirection.UP:
|
|
offset.y -= offset_value # Stack upward (negative Y)
|
|
PileDirection.DOWN:
|
|
offset.y += offset_value # Stack downward (positive Y)
|
|
PileDirection.RIGHT:
|
|
offset.x += offset_value # Stack rightward (positive X)
|
|
PileDirection.LEFT:
|
|
offset.x -= offset_value # Stack leftward (negative X)
|
|
|
|
return offset
|