This commit is contained in:
Aspergerli
2026-03-09 19:18:47 +01:00
commit ade3d0fb01
240 changed files with 12324 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
## A card object that represents a single playing card with drag-and-drop functionality.
##
## The Card class extends DraggableObject to provide interactive card behavior including
## hover effects, drag operations, and visual state management. Cards can display
## different faces (front/back) and integrate with the card framework's container system.
##
## Key Features:
## - Visual state management (front/back face display)
## - Drag-and-drop interaction with state machine
## - Integration with CardContainer for organized card management
## - Hover animation and visual feedback
##
## Usage:
## [codeblock]
## var card = card_factory.create_card("ace_spades", target_container)
## card.show_front = true
## card.move(target_position, 0)
## [/codeblock]
class_name Card
extends DraggableObject
# Static counters for global card state tracking
static var hovering_card_count: int = 0
static var holding_card_count: int = 0
## The name of the card.
@export var card_name: String
## The size of the card.
@export var card_size: Vector2 = CardFrameworkSettings.LAYOUT_DEFAULT_CARD_SIZE
## The texture for the front face of the card.
@export var front_image: Texture2D
## The texture for the back face of the card.
@export var back_image: Texture2D
## Whether the front face of the card is shown.
## If true, the front face is visible; otherwise, the back face is visible.
@export var show_front: bool = true:
set(value):
if value:
front_face_texture.visible = true
back_face_texture.visible = false
else:
front_face_texture.visible = false
back_face_texture.visible = true
# Card data and container reference
var card_info: Dictionary
var card_container: CardContainer
@onready var front_face_texture: TextureRect = $FrontFace/TextureRect
@onready var back_face_texture: TextureRect = $BackFace/TextureRect
func _ready() -> void:
super._ready()
front_face_texture.size = card_size
back_face_texture.size = card_size
if front_image:
front_face_texture.texture = front_image
if back_image:
back_face_texture.texture = back_image
pivot_offset = card_size / 2
func _on_move_done() -> void:
card_container.on_card_move_done(self)
## Sets the front and back face textures for this card.
##
## @param front_face: The texture to use for the front face
## @param back_face: The texture to use for the back face
func set_faces(front_face: Texture2D, back_face: Texture2D) -> void:
front_face_texture.texture = front_face
back_face_texture.texture = back_face
## Returns the card to its original position with smooth animation.
func return_card() -> void:
super.return_to_original()
# Override state entry to add card-specific logic
func _enter_state(state: DraggableState, from_state: DraggableState) -> void:
super._enter_state(state, from_state)
match state:
DraggableState.HOVERING:
hovering_card_count += 1
DraggableState.HOLDING:
holding_card_count += 1
if card_container:
card_container.hold_card(self)
# Override state exit to add card-specific logic
func _exit_state(state: DraggableState) -> void:
match state:
DraggableState.HOVERING:
hovering_card_count -= 1
DraggableState.HOLDING:
holding_card_count -= 1
super._exit_state(state)
## Legacy compatibility method for holding state.
## @deprecated Use state machine transitions instead
func set_holding() -> void:
if card_container:
card_container.hold_card(self)
## Returns a string representation of this card.
func get_string() -> String:
return card_name
## Checks if this card can start hovering based on global card state.
## Prevents multiple cards from hovering simultaneously.
func _can_start_hovering() -> bool:
return hovering_card_count == 0 and holding_card_count == 0
## Handles mouse press events with container notification.
func _handle_mouse_pressed() -> void:
card_container.on_card_pressed(self)
super._handle_mouse_pressed()
## Handles mouse release events and releases held cards.
func _handle_mouse_released() -> void:
super._handle_mouse_released()
if card_container:
card_container.release_holding_cards()

View File

@@ -0,0 +1 @@
uid://dtpomjc0u41g

View File

@@ -0,0 +1,38 @@
[gd_scene load_steps=2 format=3 uid="uid://brjlo8xing83p"]
[ext_resource type="Script" uid="uid://dtpomjc0u41g" path="res://addons/card-framework/card.gd" id="1_6ohl5"]
[node name="Card" type="Control"]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_6ohl5")
card_name = null
card_size = null
show_front = null
moving_speed = null
can_be_interacted_with = null
hover_distance = null
[node name="FrontFace" type="Control" parent="."]
layout_mode = 1
anchors_preset = 0
offset_right = 40.0
offset_bottom = 40.0
mouse_filter = 1
[node name="TextureRect" type="TextureRect" parent="FrontFace"]
layout_mode = 1
offset_right = 150.0
offset_bottom = 210.0
[node name="BackFace" type="Control" parent="."]
layout_mode = 1
anchors_preset = 0
offset_right = 40.0
offset_bottom = 40.0
mouse_filter = 1
[node name="TextureRect" type="TextureRect" parent="BackFace"]
layout_mode = 1
offset_right = 150.0
offset_bottom = 210.0

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

View File

@@ -0,0 +1 @@
uid://de8yhmalsa0pm

View File

@@ -0,0 +1,62 @@
@tool
## Abstract base class for card creation factories using the Factory design pattern.
##
## CardFactory defines the interface for creating cards in the card framework.
## Concrete implementations like JsonCardFactory provide specific card creation
## logic while maintaining consistent behavior across different card types and
## data sources.
##
## Design Pattern: Factory Method
## This abstract factory allows the card framework to create cards without
## knowing the specific implementation details. Different factory types can
## support various data sources (JSON files, databases, hardcoded data, etc.).
##
## Key Responsibilities:
## - Define card creation interface for consistent behavior
## - Manage card data caching for performance optimization
## - Provide card size configuration for uniform scaling
## - Support preloading mechanisms for reduced runtime I/O
##
## Subclass Implementation Requirements:
## - Override create_card() to implement specific card creation logic
## - Override preload_card_data() to implement data initialization
## - Use preloaded_cards dictionary for caching when appropriate
##
## Usage:
## [codeblock]
## class_name MyCardFactory
## extends CardFactory
##
## func create_card(card_name: String, target: CardContainer) -> Card:
## # Implementation-specific card creation
## return my_card_instance
## [/codeblock]
class_name CardFactory
extends Node
# Core factory data and configuration
## Dictionary cache for storing preloaded card data to improve performance
## Key: card identifier (String), Value: card data (typically Dictionary)
var preloaded_cards = {}
## Default size for cards created by this factory
## Applied to all created cards unless overridden
var card_size: Vector2
## Virtual method for creating a card instance and adding it to a container.
## Must be implemented by concrete factory subclasses to provide specific
## card creation logic based on the factory's data source and requirements.
## @param card_name: Identifier for the card to create
## @param target: CardContainer where the created card will be added
## @returns: Created Card instance or null if creation failed
func create_card(card_name: String, target: CardContainer) -> Card:
return null
## Virtual method for preloading card data into the factory's cache.
## Concrete implementations should override this to load card definitions
## from their respective data sources (files, databases, etc.) into the
## preloaded_cards dictionary for faster card creation during gameplay.
func preload_card_data() -> void:
pass

View File

@@ -0,0 +1 @@
uid://3lsrv6tjfdc5

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=3 format=3 uid="uid://7qcsutlss3oj"]
[ext_resource type="Script" uid="uid://8n36yadkvxai" path="res://addons/card-framework/json_card_factory.gd" id="1_jlwb4"]
[ext_resource type="PackedScene" uid="uid://brjlo8xing83p" path="res://addons/card-framework/card.tscn" id="2_1mca4"]
[node name="CardFactory" type="Node"]
script = ExtResource("1_jlwb4")
default_card_scene = ExtResource("2_1mca4")

View File

@@ -0,0 +1,56 @@
## Card Framework configuration constants class.
##
## This class provides centralized constant values for all Card Framework components
## without requiring Autoload. All values are defined as constants to ensure
## consistent behavior across the framework.
##
## Usage:
## [codeblock]
## # Reference constants directly
## var speed = CardFrameworkSettings.ANIMATION_MOVE_SPEED
## var z_offset = CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET
## [/codeblock]
class_name CardFrameworkSettings
extends RefCounted
# Animation Constants
## Speed of card movement animations in pixels per second
const ANIMATION_MOVE_SPEED: float = 2000.0
## Duration of hover animations in seconds
const ANIMATION_HOVER_DURATION: float = 0.10
## Scale multiplier applied during hover effects
const ANIMATION_HOVER_SCALE: float = 1.1
## Rotation in degrees applied during hover effects
const ANIMATION_HOVER_ROTATION: float = 0.0
# Physics & Interaction Constants
## Distance threshold for hover detection in pixels
const PHYSICS_HOVER_DISTANCE: float = 10.0
## Distance cards move up during hover in pixels
const PHYSICS_CARD_HOVER_DISTANCE: float = 30.0
# Visual Layout Constants
## Z-index offset applied to cards during drag operations
const VISUAL_DRAG_Z_OFFSET: int = 1000
## Z-index for pile cards to ensure proper layering
const VISUAL_PILE_Z_INDEX: int = 3000
## Z-index for drop zone sensors (below everything)
const VISUAL_SENSOR_Z_INDEX: int = -1000
## Z-index for debug outlines (above UI)
const VISUAL_OUTLINE_Z_INDEX: int = 1200
# Container Layout Constants
## Default card size used throughout the framework
const LAYOUT_DEFAULT_CARD_SIZE: Vector2 = Vector2(150, 210)
## Distance between stacked cards in piles
const LAYOUT_STACK_GAP: int = 8
## Maximum cards to display in stack before hiding
const LAYOUT_MAX_STACK_DISPLAY: int = 6
## Maximum number of cards in hand containers
const LAYOUT_MAX_HAND_SIZE: int = 10
## Maximum pixel spread for hand arrangements
const LAYOUT_MAX_HAND_SPREAD: int = 700
# Color Constants for Debugging
## Color used for sensor outlines and debug indicators
const DEBUG_OUTLINE_COLOR: Color = Color(1, 0, 0, 1)

View File

@@ -0,0 +1 @@
uid://c308ongyuejma

View File

@@ -0,0 +1,167 @@
@tool
## Central orchestrator for the card framework system.
##
## CardManager coordinates all card-related operations including drag-and-drop,
## history management, and container registration. It serves as the root node
## for card game scenes and manages the lifecycle of cards and containers.
##
## Key Responsibilities:
## - Card factory management and initialization
## - Container registration and coordination
## - Drag-and-drop event handling and routing
## - History tracking for undo/redo operations
## - Debug mode and visual debugging support
##
## Setup Requirements:
## - Must be the parent of all CardContainer instances
## - Requires card_factory_scene to be assigned in inspector
## - Configure card_size to match your card assets
##
## Usage:
## [codeblock]
## # In scene setup
## CardManager (root)
## ├── Hand (CardContainer)
## ├── Foundation (CardContainer)
## └── Deck (CardContainer)
## [/codeblock]
class_name CardManager
extends Control
# Constants
const CARD_ACCEPT_TYPE = "card"
## Default size for all cards in the game
@export var card_size := CardFrameworkSettings.LAYOUT_DEFAULT_CARD_SIZE
## Scene containing the card factory implementation
@export var card_factory_scene: PackedScene
## Enables visual debugging for drop zones and interactions
@export var debug_mode := false
# Core system components
var card_factory: CardFactory
var card_container_dict: Dictionary = {}
var history: Array[HistoryElement] = []
func _init() -> void:
if Engine.is_editor_hint():
return
func _ready() -> void:
if not _pre_process_exported_variables():
return
if Engine.is_editor_hint():
return
card_factory.card_size = card_size
card_factory.preload_card_data()
## Undoes the last card movement operation.
## Restores cards to their previous positions using stored history.
func undo() -> void:
if history.is_empty():
return
var last = history.pop_back()
if last.from != null:
last.from.undo(last.cards, last.from_indices)
## Clears all history entries, preventing further undo operations.
func reset_history() -> void:
history.clear()
func _add_card_container(id: int, card_container: CardContainer) -> void:
card_container_dict[id] = card_container
card_container.debug_mode = debug_mode
func _delete_card_container(id: int) -> void:
card_container_dict.erase(id)
# Handles dropped cards by finding suitable container
func _on_drag_dropped(cards: Array) -> void:
if cards.is_empty():
return
# Store original mouse_filter states and temporarily disable input during drop processing
var original_mouse_filters = {}
for card in cards:
original_mouse_filters[card] = card.mouse_filter
card.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Find first container that accepts the cards
for key in card_container_dict.keys():
var card_container = card_container_dict[key]
var result = card_container.check_card_can_be_dropped(cards)
if result:
var index = card_container.get_partition_index()
# Restore mouse_filter before move_cards (DraggableObject will manage it from here)
for card in cards:
card.mouse_filter = original_mouse_filters[card]
card_container.move_cards(cards, index)
return
for card in cards:
# Restore mouse_filter before return_card (DraggableObject will manage it from here)
card.mouse_filter = original_mouse_filters[card]
card.return_card()
func _add_history(to: CardContainer, cards: Array) -> void:
var from = null
var from_indices = []
# Record indices FIRST, before any movement operations
for i in range(cards.size()):
var c = cards[i]
var current = c.card_container
if i == 0:
from = current
else:
if from != current:
push_error("All cards must be from the same container!")
return
# Record index immediately to avoid race conditions
if from != null:
var original_index = from._held_cards.find(c)
if original_index == -1:
push_error("Card not found in source container during history recording!")
return
from_indices.append(original_index)
var history_element = HistoryElement.new()
history_element.from = from
history_element.to = to
history_element.cards = cards
history_element.from_indices = from_indices
history.append(history_element)
func _is_valid_directory(path: String) -> bool:
var dir = DirAccess.open(path)
return dir != null
func _pre_process_exported_variables() -> bool:
if card_factory_scene == null:
push_error("CardFactory is not assigned! Please set it in the CardManager Inspector.")
return false
var factory_instance = card_factory_scene.instantiate() as CardFactory
if factory_instance == null:
push_error("Failed to create an instance of CardFactory! CardManager imported an incorrect card factory scene.")
return false
add_child(factory_instance)
card_factory = factory_instance
return true

View File

@@ -0,0 +1 @@
uid://clqgq1n7v0ar

View File

@@ -0,0 +1,10 @@
[gd_scene load_steps=3 format=3 uid="uid://c7u8hryloq7hy"]
[ext_resource type="Script" uid="uid://clqgq1n7v0ar" path="res://addons/card-framework/card_manager.gd" id="1_cp2xm"]
[ext_resource type="PackedScene" uid="uid://7qcsutlss3oj" path="res://addons/card-framework/card_factory.tscn" id="2_57jpu"]
[node name="CardManager" type="Control"]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_cp2xm")
card_factory_scene = ExtResource("2_57jpu")

View File

@@ -0,0 +1,391 @@
## A draggable object that supports mouse interaction with state-based animation system.
##
## This class provides a robust state machine for handling mouse interactions including
## hover effects, drag operations, and programmatic movement using Tween animations.
## All interactive cards and objects extend this base class to inherit consistent
## drag-and-drop behavior.
##
## Key Features:
## - State machine with safe transitions (IDLE → HOVERING → HOLDING → MOVING)
## - Tween-based animations for smooth hover effects and movement
## - Mouse interaction handling with proper event management
## - Z-index management for visual layering during interactions
## - Extensible design with virtual methods for customization
##
## State Transitions:
## - IDLE: Default state, ready for interaction
## - HOVERING: Mouse over with visual feedback (scale, rotation, position)
## - HOLDING: Active drag state following mouse movement
## - MOVING: Programmatic movement ignoring user input
##
## Usage:
## [codeblock]
## class_name MyDraggable
## extends DraggableObject
##
## func _can_start_hovering() -> bool:
## return my_custom_condition
## [/codeblock]
class_name DraggableObject
extends Control
# Enums
## Enumeration of possible interaction states for the draggable object.
enum DraggableState {
IDLE, ## Default state - no interaction
HOVERING, ## Mouse over state - visual feedback
HOLDING, ## Dragging state - follows mouse
MOVING ## Programmatic move state - ignores input
}
## The speed at which the objects moves.
@export var moving_speed: int = CardFrameworkSettings.ANIMATION_MOVE_SPEED
## Whether the object can be interacted with.
@export var can_be_interacted_with: bool = true
## The distance the object hovers when interacted with.
@export var hover_distance: int = CardFrameworkSettings.PHYSICS_HOVER_DISTANCE
## The scale multiplier when hovering.
@export var hover_scale: float = CardFrameworkSettings.ANIMATION_HOVER_SCALE
## The rotation in degrees when hovering.
@export var hover_rotation: float = CardFrameworkSettings.ANIMATION_HOVER_ROTATION
## The duration for hover animations.
@export var hover_duration: float = CardFrameworkSettings.ANIMATION_HOVER_DURATION
# Legacy variables - kept for compatibility but no longer used in state machine
var is_pressed: bool = false
var is_holding: bool = false
var stored_z_index: int:
set(value):
z_index = value
stored_z_index = value
# State Machine
var current_state: DraggableState = DraggableState.IDLE
# Mouse tracking
var is_mouse_inside: bool = false
# Movement state tracking
var is_moving_to_destination: bool = false
var is_returning_to_original: bool = false
# Position and animation tracking
var current_holding_mouse_position: Vector2
var original_position: Vector2
var original_scale: Vector2
var original_hover_rotation: float
var current_hover_position: Vector2 # Track position during hover animation
# Move operation tracking
var target_destination: Vector2 # Target position passed to move() function
var target_rotation: float # Target rotation passed to move() function
var original_destination: Vector2
var original_rotation: float
var destination_degree: float
# Tween objects
var move_tween: Tween
var hover_tween: Tween
# State transition rules
var allowed_transitions = {
DraggableState.IDLE: [DraggableState.HOVERING, DraggableState.HOLDING, DraggableState.MOVING],
DraggableState.HOVERING: [DraggableState.IDLE, DraggableState.HOLDING, DraggableState.MOVING],
DraggableState.HOLDING: [DraggableState.IDLE, DraggableState.MOVING],
DraggableState.MOVING: [DraggableState.IDLE]
}
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
connect("mouse_entered", _on_mouse_enter)
connect("mouse_exited", _on_mouse_exit)
connect("gui_input", _on_gui_input)
original_destination = global_position
original_rotation = rotation
original_position = position
original_scale = scale
original_hover_rotation = rotation
stored_z_index = z_index
## Safely transitions between interaction states using predefined rules.
## Validates transitions and handles state cleanup/initialization automatically.
## @param new_state: Target state to transition to
## @returns: True if transition was successful, false if invalid/blocked
func change_state(new_state: DraggableState) -> bool:
if new_state == current_state:
return true
# Validate transition is allowed by state machine rules
if not new_state in allowed_transitions[current_state]:
return false
# Clean up previous state
_exit_state(current_state)
var old_state = current_state
current_state = new_state
# Enter new state
_enter_state(new_state, old_state)
return true
# Handle state entry
func _enter_state(state: DraggableState, from_state: DraggableState) -> void:
match state:
DraggableState.IDLE:
z_index = stored_z_index
mouse_filter = Control.MOUSE_FILTER_STOP
DraggableState.HOVERING:
# z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET
_start_hover_animation()
DraggableState.HOLDING:
# Preserve hover position if transitioning from HOVERING state
if from_state == DraggableState.HOVERING:
_preserve_hover_position()
# For IDLE → HOLDING transitions, current position is maintained
current_holding_mouse_position = get_local_mouse_position()
z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET
rotation = 0
DraggableState.MOVING:
# Stop hover animations and ignore input during programmatic movement
if hover_tween and hover_tween.is_valid():
hover_tween.kill()
hover_tween = null
z_index = stored_z_index + CardFrameworkSettings.VISUAL_DRAG_Z_OFFSET
mouse_filter = Control.MOUSE_FILTER_IGNORE
# Handle state exit
func _exit_state(state: DraggableState) -> void:
match state:
DraggableState.HOVERING:
z_index = stored_z_index
_stop_hover_animation()
DraggableState.HOLDING:
z_index = stored_z_index
# Reset visual effects but preserve position for return_card() animation
scale = original_scale
rotation = original_hover_rotation
DraggableState.MOVING:
mouse_filter = Control.MOUSE_FILTER_STOP
func _process(delta: float) -> void:
match current_state:
DraggableState.HOLDING:
global_position = get_global_mouse_position() - current_holding_mouse_position
func _finish_move() -> void:
# Complete movement processing
is_moving_to_destination = false
rotation = destination_degree
# Update original position and rotation only when not returning to original
# Important: Use original target values from move() instead of global_position
if not is_returning_to_original:
original_destination = target_destination
original_rotation = target_rotation
# Reset return flag
is_returning_to_original = false
# End MOVING state - return to IDLE
change_state(DraggableState.IDLE)
# Call inherited class callback
_on_move_done()
func _on_move_done() -> void:
# This function can be overridden by subclasses to handle when the move is done.
pass
# Start hover animation with tween
func _start_hover_animation() -> void:
# Stop any existing hover animation
if hover_tween and hover_tween.is_valid():
hover_tween.kill()
hover_tween = null
position = original_position # Reset position to original before starting new hover
scale = original_scale
rotation = original_hover_rotation
# Update original position to current position (important for correct return)
original_position = position
original_scale = scale
original_hover_rotation = rotation
# Store current position before animation
current_hover_position = position
# Create new hover tween
hover_tween = create_tween()
hover_tween.set_parallel(true) # Allow multiple properties to animate simultaneously
# Animate position (hover up)
var target_position = Vector2(position.x - 30, position.y - hover_distance)
hover_tween.tween_property(self, "position", target_position, hover_duration)
# Animate scale
hover_tween.tween_property(self, "scale", original_scale * hover_scale, hover_duration)
# Animate rotation
#hover_tween.tween_property(self, "rotation", deg_to_rad(hover_rotation), hover_duration)
# Update current hover position tracking
hover_tween.tween_method(_update_hover_position, position, target_position, hover_duration)
# Stop hover animation and return to original state
func _stop_hover_animation() -> void:
# Stop any existing hover animation
if hover_tween and hover_tween.is_valid():
hover_tween.kill()
hover_tween = null
# Create new tween to return to original state
hover_tween = create_tween()
hover_tween.set_parallel(true)
# Animate back to original position
hover_tween.tween_property(self, "position", original_position, hover_duration)
# Animate back to original scale
hover_tween.tween_property(self, "scale", original_scale, hover_duration)
# Animate back to original rotation
hover_tween.tween_property(self, "rotation", original_hover_rotation, hover_duration)
# Update current hover position tracking
hover_tween.tween_method(_update_hover_position, position, original_position, hover_duration)
# Track current position during hover animation for smooth HOLDING transition
func _update_hover_position(pos: Vector2) -> void:
current_hover_position = pos
# Preserve current hover position when transitioning to HOLDING
func _preserve_hover_position() -> void:
# Stop hover animation and preserve current position
if hover_tween and hover_tween.is_valid():
hover_tween.kill()
hover_tween = null
# Explicitly set position to current hover position
# This ensures smooth transition from hover animation to holding
position = current_hover_position
## Virtual method to determine if hovering animation can start.
## Override in subclasses to implement custom hovering conditions.
## @returns: True if hovering is allowed, false otherwise
func _can_start_hovering() -> bool:
return true
func _on_mouse_enter() -> void:
is_mouse_inside = true
if can_be_interacted_with and _can_start_hovering():
change_state(DraggableState.HOVERING)
func _on_mouse_exit() -> void:
is_mouse_inside = false
match current_state:
DraggableState.HOVERING:
change_state(DraggableState.IDLE)
func _on_gui_input(event: InputEvent) -> void:
if not can_be_interacted_with:
return
if event is InputEventMouseButton:
_handle_mouse_button(event as InputEventMouseButton)
## Moves the object to target position with optional rotation using smooth animation.
## Automatically transitions to MOVING state and handles animation timing based on distance.
## @param target_destination: Global position to move to
## @param degree: Target rotation in radians
func move(target_destination: Vector2, degree: float) -> void:
# Skip if current position and rotation match target
if global_position == target_destination and rotation == degree:
return
# Force transition to MOVING state (highest priority)
change_state(DraggableState.MOVING)
# Stop existing movement
if move_tween and move_tween.is_valid():
move_tween.kill()
move_tween = null
# Store target position and rotation for original value preservation
self.target_destination = target_destination
self.target_rotation = degree
# Initial setup
rotation = 0
destination_degree = degree
is_moving_to_destination = true
# Smooth Tween-based movement with dynamic duration based on moving_speed
var distance = global_position.distance_to(target_destination)
var duration = distance / moving_speed
move_tween = create_tween()
move_tween.tween_property(self, "global_position", target_destination, duration)
move_tween.tween_callback(_finish_move)
func _handle_mouse_button(mouse_event: InputEventMouseButton) -> void:
if mouse_event.button_index != MOUSE_BUTTON_LEFT:
return
# Ignore all input during MOVING state
if current_state == DraggableState.MOVING:
return
if mouse_event.is_pressed():
_handle_mouse_pressed()
if mouse_event.is_released():
_handle_mouse_released()
## Returns the object to its original position with smooth animation.
func return_to_original() -> void:
is_returning_to_original = true
move(original_destination, original_rotation)
func _handle_mouse_pressed() -> void:
is_pressed = true
match current_state:
DraggableState.HOVERING:
change_state(DraggableState.HOLDING)
DraggableState.IDLE:
if is_mouse_inside and can_be_interacted_with and _can_start_hovering():
change_state(DraggableState.HOLDING)
func _handle_mouse_released() -> void:
is_pressed = false
match current_state:
DraggableState.HOLDING:
change_state(DraggableState.IDLE)

View File

@@ -0,0 +1 @@
uid://bfhrx3h70sor0

View File

@@ -0,0 +1,253 @@
## Interactive drop zone system with sensor partitioning and visual debugging.
##
## DropZone provides sophisticated drag-and-drop target detection with configurable
## sensor areas, partitioning systems, and visual debugging capabilities. It integrates
## with CardContainer to enable precise card placement and reordering operations.
##
## Key Features:
## - Flexible sensor sizing and positioning with dynamic adjustment
## - Vertical/horizontal partitioning for precise drop targeting
## - Visual debugging with colored outlines and partition indicators
## - Mouse detection with global coordinate transformation
## - Accept type filtering for specific draggable object types
##
## Partitioning System:
## - Vertical partitions: Divide sensor into left-right sections for card ordering
## - Horizontal partitions: Divide sensor into up-down sections for layered placement
## - Dynamic outline generation for visual feedback during development
##
## Usage:
## [codeblock]
## var drop_zone = DropZone.new()
## drop_zone.init(container, ["card"])
## drop_zone.set_sensor(Vector2(200, 300), Vector2.ZERO, null, false)
## drop_zone.set_vertical_partitions([100, 200, 300])
## [/codeblock]
class_name DropZone
extends Control
# Dynamic sensor properties with automatic UI synchronization
## Size of the drop sensor area
var sensor_size: Vector2:
set(value):
sensor.size = value
sensor_outline.size = value
## Position offset of the drop sensor relative to DropZone
var sensor_position: Vector2:
set(value):
sensor.position = value
sensor_outline.position = value
## @deprecated: Since it was designed to debug the sensor, please use sensor_outline_visible instead.
var sensor_texture : Texture:
set(value):
sensor.texture = value
## @deprecated: Since it was designed to debug the sensor, please use sensor_outline_visible instead.
var sensor_visible := true:
set(value):
sensor.visible = value
## Controls visibility of debugging outlines for sensor and partitions
var sensor_outline_visible := false:
set(value):
sensor_outline.visible = value
for outline in sensor_partition_outlines:
outline.visible = value
# Core drop zone configuration and state
## Array of accepted draggable object types (e.g., ["card", "token"])
var accept_types: Array = []
## Original sensor size for restoration after dynamic changes
var stored_sensor_size: Vector2
## Original sensor position for restoration after dynamic changes
var stored_sensor_position: Vector2
## Parent container that owns this drop zone
var parent: Node
# UI components
## Main sensor control for hit detection (invisible)
var sensor: Control
## Debug outline for visual sensor boundary indication
var sensor_outline: ReferenceRect
## Array of partition outline controls for debugging
var sensor_partition_outlines: Array = []
# Partitioning system for precise drop targeting
## Global vertical lines to divide sensing partitions (left to right direction)
var vertical_partition: Array
## Global horizontal lines to divide sensing partitions (up to down direction)
var horizontal_partition: Array
## Initializes the drop zone with parent reference and accepted drag types.
## Creates sensor and debugging UI components.
## @param _parent: Container that owns this drop zone
## @param accept_types: Array of draggable object types this zone accepts
func init(_parent: Node, accept_types: Array =[]):
parent = _parent
self.accept_types = accept_types
# Create invisible sensor for hit detection
if sensor == null:
sensor = TextureRect.new()
sensor.name = "Sensor"
sensor.mouse_filter = Control.MOUSE_FILTER_IGNORE
sensor.z_index = CardFrameworkSettings.VISUAL_SENSOR_Z_INDEX # Behind everything else
add_child(sensor)
# Create debugging outline (initially hidden)
if sensor_outline == null:
sensor_outline = ReferenceRect.new()
sensor_outline.editor_only = false
sensor_outline.name = "SensorOutline"
sensor_outline.mouse_filter = Control.MOUSE_FILTER_IGNORE
sensor_outline.border_color = CardFrameworkSettings.DEBUG_OUTLINE_COLOR
sensor_outline.z_index = CardFrameworkSettings.VISUAL_OUTLINE_Z_INDEX
add_child(sensor_outline)
# Initialize default values
stored_sensor_size = Vector2(0, 0)
stored_sensor_position = Vector2(0, 0)
vertical_partition = []
horizontal_partition = []
## Checks if the mouse cursor is currently within the drop zone sensor area.
## @returns: True if mouse is inside the sensor bounds
func check_mouse_is_in_drop_zone() -> bool:
var mouse_position = get_global_mouse_position()
var result = sensor.get_global_rect().has_point(mouse_position)
return result
## Configures the sensor with size, position, texture, and visibility settings.
## Stores original values for later restoration.
## @param _size: Size of the sensor area
## @param _position: Position offset from DropZone origin
## @param _texture: Optional texture for sensor visualization
## @param _visible: Whether sensor texture is visible (deprecated)
func set_sensor(_size: Vector2, _position: Vector2, _texture: Texture, _visible: bool):
sensor_size = _size
sensor_position = _position
stored_sensor_size = _size
stored_sensor_position = _position
sensor_texture = _texture
sensor_visible = _visible
## Dynamically adjusts sensor size and position without affecting stored values.
## Used for temporary sensor modifications that can be restored later.
## @param _size: New temporary sensor size
## @param _position: New temporary sensor position
func set_sensor_size_flexibly(_size: Vector2, _position: Vector2):
sensor_size = _size
sensor_position = _position
## Restores sensor to its original size and position from stored values.
## Used to undo temporary modifications made by set_sensor_size_flexibly.
func return_sensor_size():
sensor_size = stored_sensor_size
sensor_position = stored_sensor_position
## Adjusts sensor position by adding an offset to the stored position.
## @param offset: Vector2 offset to add to the original stored position
func change_sensor_position_with_offset(offset: Vector2):
sensor_position = stored_sensor_position + offset
## Sets vertical partition lines for drop targeting and creates debug outlines.
## Vertical partitions divide the sensor into left-right sections for card ordering.
## @param positions: Array of global X coordinates for partition lines
func set_vertical_partitions(positions: Array):
vertical_partition = positions
# Clear existing partition outlines
for outline in sensor_partition_outlines:
outline.queue_free()
sensor_partition_outlines.clear()
# Create debug outline for each partition
for i in range(vertical_partition.size()):
var outline = ReferenceRect.new()
outline.editor_only = false
outline.name = "VerticalPartition" + str(i)
outline.z_index = CardFrameworkSettings.VISUAL_OUTLINE_Z_INDEX
outline.border_color = CardFrameworkSettings.DEBUG_OUTLINE_COLOR
outline.mouse_filter = Control.MOUSE_FILTER_IGNORE
outline.size = Vector2(1, sensor.size.y) # Vertical line full height
# Convert global partition position to local coordinates
var local_x = vertical_partition[i] - global_position.x
outline.position = Vector2(local_x, sensor.position.y)
outline.visible = sensor_outline.visible
add_child(outline)
sensor_partition_outlines.append(outline)
func set_horizontal_partitions(positions: Array):
horizontal_partition = positions
# clear existing outlines
for outline in sensor_partition_outlines:
outline.queue_free()
sensor_partition_outlines.clear()
for i in range(horizontal_partition.size()):
var outline = ReferenceRect.new()
outline.editor_only = false
outline.name = "HorizontalPartition" + str(i)
outline.z_index = CardFrameworkSettings.VISUAL_OUTLINE_Z_INDEX
outline.border_color = CardFrameworkSettings.DEBUG_OUTLINE_COLOR
outline.mouse_filter = Control.MOUSE_FILTER_IGNORE
outline.size = Vector2(sensor.size.x, 1)
var local_y = horizontal_partition[i] - global_position.y
outline.position = Vector2(sensor.position.x, local_y)
outline.visible = sensor_outline.visible
add_child(outline)
sensor_partition_outlines.append(outline)
## Determines which vertical partition the mouse is currently in.
## Returns the partition index for precise drop targeting.
## @returns: Partition index (0-based) or -1 if outside sensor or no partitions
func get_vertical_layers() -> int:
if not check_mouse_is_in_drop_zone():
return -1
if vertical_partition == null or vertical_partition.is_empty():
return -1
var mouse_position = get_global_mouse_position()
# Count how many partition lines the mouse has crossed
var current_index := 0
for i in range(vertical_partition.size()):
if mouse_position.x >= vertical_partition[i]:
current_index += 1
else:
break
return current_index
func get_horizontal_layers() -> int:
if not check_mouse_is_in_drop_zone():
return -1
if horizontal_partition == null or horizontal_partition.is_empty():
return -1
var mouse_position = get_global_mouse_position()
var current_index := 0
for i in range(horizontal_partition.size()):
if mouse_position.y >= horizontal_partition[i]:
current_index += 1
else:
break
return current_index

View File

@@ -0,0 +1 @@
uid://dhultt7pav0b

View File

@@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3 uid="uid://dkmme1pig03ie"]
[ext_resource type="Script" uid="uid://dhultt7pav0b" path="res://addons/card-framework/drop_zone.gd" id="1_w6usu"]
[node name="DropZone" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
mouse_force_pass_scroll_events = false
script = ExtResource("1_w6usu")

View File

@@ -0,0 +1,291 @@
## A fan-shaped card container that arranges cards in an arc formation.
##
## Hand provides sophisticated card layout using mathematical curves to create
## natural-looking card arrangements. Cards are positioned in a fan pattern
## with configurable spread, rotation, and vertical displacement.
##
## Key Features:
## - Fan-shaped card arrangement with customizable curves
## - Smooth card reordering with optional swap-only mode
## - Dynamic drop zone sizing to match hand spread
## - Configurable card limits and hover distances
## - Mathematical positioning using Curve resources
##
## Curve Configuration:
## - hand_rotation_curve: Controls card rotation (linear -X to +X recommended)
## - hand_vertical_curve: Controls vertical offset (3-point ease 0-X-0 recommended)
##
## Usage:
## [codeblock]
## @onready var hand = $Hand
## hand.max_hand_size = 7
## hand.max_hand_spread = 600
## hand.card_face_up = true
## [/codeblock]
class_name Hand
extends CardContainer
@export_group("hand_meta_info")
## maximum number of cards that can be held.
@export var max_hand_size := CardFrameworkSettings.LAYOUT_MAX_HAND_SIZE
## maximum spread of the hand.
@export var max_hand_spread := CardFrameworkSettings.LAYOUT_MAX_HAND_SPREAD
## whether the card is face up.
@export var card_face_up := true
## distance the card hovers when interacted with.
@export var card_hover_distance := CardFrameworkSettings.PHYSICS_CARD_HOVER_DISTANCE
@export_group("hand_shape")
## rotation curve of the hand.
## This works best as a 2-point linear rise from -X to +X.
@export var hand_rotation_curve : Curve
## vertical curve of the hand.
## This works best as a 3-point ease in/out from 0 to X to 0
@export var hand_vertical_curve : Curve
@export_group("drop_zone")
## Determines whether the drop zone size follows the hand size. (requires enable drop zone true)
@export var align_drop_zone_size_with_current_hand_size := true
## If true, only swap the positions of two cards when reordering (a <-> b), otherwise shift the range (default behavior).
@export var swap_only_on_reorder := false
var vertical_partitions_from_outside = []
var vertical_partitions_from_inside = []
func _ready() -> void:
super._ready()
$"../..".dealt.connect(sort_hand)
## Returns a random selection of cards from this hand.
## @param n: Number of cards to select
## @returns: Array of randomly selected cards
func get_random_cards(n: int) -> Array:
var deck = _held_cards.duplicate()
deck.shuffle()
if n > _held_cards.size():
n = _held_cards.size()
return deck.slice(0, n)
func sort_hand():
var sort_cards = _held_cards.duplicate()
sort_cards.sort_custom(compare_cards)
#print("sorted")
for card in sort_cards:
print(card.card_info["value"])
var n = 0
for card in sort_cards:
var arr_card : Array
arr_card.append(card)
move_cards((arr_card), n)
arr_card.clear()
n += 1
func compare_cards(a,b):
var val_1 = int(a.card_info["value"])
var val_2 = int(b.card_info["value"])
var su_1 = a.card_info["suit"]
var su_2 = b.card_info["suit"]
match su_1:
"Diampnd":
val_1 -= 10
"Spade":
val_1 -= 20
"Heart":
val_1 -= 30
#print(val_1,su_1)
match su_2:
"Diamond":
val_2 -= 10
"Spade":
val_2 -= 20
"Heart":
val_2 -= 30
#print(val_2,su_2)
return val_1 < val_2
func _card_can_be_added(_cards: Array) -> bool:
var is_all_cards_contained = true
for i in range(_cards.size()):
var card = _cards[i]
if !_held_cards.has(card):
is_all_cards_contained = false
if is_all_cards_contained:
return true
var card_size = _cards.size()
return _held_cards.size() + card_size <= max_hand_size
func _update_target_z_index() -> void:
for i in range(_held_cards.size()):
var card = _held_cards[i]
card.stored_z_index = i
## Calculates target positions for all cards using mathematical curves.
## Implements sophisticated fan-shaped arrangement with rotation and vertical displacement.
func _update_target_positions() -> void:
var x_min: float
var x_max: float
var y_min: float
var y_max: float
var card_size = card_manager.card_size
var _w = card_size.x
var _h = card_size.y
vertical_partitions_from_outside.clear()
# Calculate position and rotation for each card in the fan arrangement
for i in range(_held_cards.size()):
var card = _held_cards[i]
# Calculate normalized position ratio (0.0 to 1.0) for curve sampling
var hand_ratio = 0.5 # Single card centered
if _held_cards.size() > 1:
hand_ratio = float(i) / float(_held_cards.size() - 1)
# Calculate base horizontal position with even spacing
var target_pos = global_position
@warning_ignore("integer_division")
var card_spacing = max_hand_spread / (_held_cards.size() + 1)
target_pos.x += (i + 1) * card_spacing - max_hand_spread / 2.0
# Apply vertical curve displacement for fan shape
if hand_vertical_curve:
target_pos.y -= hand_vertical_curve.sample(hand_ratio)
# Apply rotation curve for realistic card fanning
var target_rotation = 0
if hand_rotation_curve:
target_rotation = deg_to_rad(hand_rotation_curve.sample(hand_ratio))
# Calculate rotated card bounding box for drop zone partitioning
# This complex math determines the actual screen space occupied by each rotated card
var _x = target_pos.x
var _y = target_pos.y
# Calculate angles to card corners after rotation
var _t1 = atan2(_h, _w) + target_rotation # bottom-right corner
var _t2 = atan2(_h, -_w) + target_rotation # bottom-left corner
var _t3 = _t1 + PI + target_rotation # top-left corner
var _t4 = _t2 + PI + target_rotation # top-right corner
# Card center and radius for corner calculation
var _c = Vector2(_x + _w / 2, _y + _h / 2) # card center
var _r = sqrt(pow(_w / 2, 2.0) + pow(_h / 2, 2.0)) # diagonal radius
# Calculate actual corner positions after rotation
var _p1 = Vector2(_r * cos(_t1), _r * sin(_t1)) + _c # right bottom
var _p2 = Vector2(_r * cos(_t2), _r * sin(_t2)) + _c # left bottom
var _p3 = Vector2(_r * cos(_t3), _r * sin(_t3)) + _c # left top
var _p4 = Vector2(_r * cos(_t4), _r * sin(_t4)) + _c # right top
# Find bounding box of rotated card
var current_x_min = min(_p1.x, _p2.x, _p3.x, _p4.x)
var current_x_max = max(_p1.x, _p2.x, _p3.x, _p4.x)
var current_y_min = min(_p1.y, _p2.y, _p3.y, _p4.y)
var current_y_max = max(_p1.y, _p2.y, _p3.y, _p4.y)
var current_x_mid = (current_x_min + current_x_max) / 2
vertical_partitions_from_outside.append(current_x_mid)
if i == 0:
x_min = current_x_min
x_max = current_x_max
y_min = current_y_min
y_max = current_y_max
else:
x_min = minf(x_min, current_x_min)
x_max = maxf(x_max, current_x_max)
y_min = minf(y_min, current_y_min)
y_max = maxf(y_max, current_y_max)
card.move(target_pos, target_rotation)
card.show_front = card_face_up
card.can_be_interacted_with = true
# Calculate midpoints between consecutive values in vertical_partitions_from_outside
vertical_partitions_from_inside.clear()
if vertical_partitions_from_outside.size() > 1:
for j in range(vertical_partitions_from_outside.size() - 1):
var mid = (vertical_partitions_from_outside[j] + vertical_partitions_from_outside[j + 1]) / 2.0
vertical_partitions_from_inside.append(mid)
if align_drop_zone_size_with_current_hand_size:
if _held_cards.size() == 0:
drop_zone.return_sensor_size()
else:
var _size = Vector2(x_max - x_min, y_max - y_min)
var _position = Vector2(x_min, y_min) - position
drop_zone.set_sensor_size_flexibly(_size, _position)
drop_zone.set_vertical_partitions(vertical_partitions_from_outside)
func move_cards(cards: Array, index: int = -1, with_history: bool = true) -> bool:
# Handle single card reordering within same Hand container
if cards.size() == 1 and _held_cards.has(cards[0]) and index >= 0 and index < _held_cards.size():
var current_index = _held_cards.find(cards[0])
# Swap-only mode: exchange two cards directly
if swap_only_on_reorder:
swap_card(cards[0], index)
return true
# Same position optimization
if current_index == index:
# Same card, same position - ensure consistent state
update_card_ui()
_restore_mouse_interaction(cards)
return true
# Different position: use efficient internal reordering
_reorder_card_in_hand(cards[0], current_index, index, with_history)
_restore_mouse_interaction(cards)
return true
# Fall back to parent implementation for other cases
return super.move_cards(cards, index, with_history)
func swap_card(card: Card, index: int) -> void:
var current_index = _held_cards.find(card)
if current_index == index:
return
var temp = _held_cards[current_index]
_held_cards[current_index] = _held_cards[index]
_held_cards[index] = temp
update_card_ui()
## Restore mouse interaction for cards after drag & drop completion.
func _restore_mouse_interaction(cards: Array) -> void:
# Restore mouse interaction for cards after drag & drop completion.
for card in cards:
card.mouse_filter = Control.MOUSE_FILTER_STOP
## Efficiently reorder card within Hand without intermediate UI updates.
## Prevents position calculation errors during same-container moves.
func _reorder_card_in_hand(card: Card, from_index: int, to_index: int, with_history: bool) -> void:
# Efficiently reorder card within Hand without intermediate UI updates.
# Add to history if needed (before making changes)
if with_history:
card_manager._add_history(self, [card])
# Efficient array reordering without intermediate states
_held_cards.remove_at(from_index)
_held_cards.insert(to_index, card)
# Single UI update after array change
update_card_ui()
func hold_card(card: Card) -> void:
if _held_cards.has(card):
drop_zone.set_vertical_partitions(vertical_partitions_from_inside)
super.hold_card(card)

View File

@@ -0,0 +1 @@
uid://dj46jo3lfbclo

View File

@@ -0,0 +1,21 @@
[gd_scene load_steps=4 format=3 uid="uid://bkpjlq7ggckg6"]
[ext_resource type="Script" uid="uid://dj46jo3lfbclo" path="res://addons/card-framework/hand.gd" id="1_hrxjc"]
[sub_resource type="Curve" id="Curve_lsli3"]
_limits = [-15.0, 15.0, 0.0, 1.0]
_data = [Vector2(0, -15), 0.0, 30.0, 0, 1, Vector2(1, 15), 30.0, 0.0, 1, 0]
point_count = 2
[sub_resource type="Curve" id="Curve_8dbo5"]
_limits = [0.0, 50.0, 0.0, 1.0]
_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.5, 40), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[node name="Hand" type="Control"]
layout_mode = 3
anchors_preset = 0
mouse_filter = 1
script = ExtResource("1_hrxjc")
hand_rotation_curve = SubResource("Curve_lsli3")
hand_vertical_curve = SubResource("Curve_8dbo5")

View File

@@ -0,0 +1,64 @@
## History tracking element for card movement operations with precise undo support.
##
## HistoryElement stores complete state information for card movements to enable
## accurate undo/redo operations. It tracks source and destination containers,
## moved cards, and their original indices for precise state restoration.
##
## Key Features:
## - Complete movement state capture for reliable undo operations
## - Precise index tracking to restore original card positions
## - Support for multi-card movement operations
## - Detailed string representation for debugging and logging
##
## Used By:
## - CardManager for history management and undo operations
## - CardContainer.undo() for precise card position restoration
##
## Index Precision:
## The from_indices array stores the exact original positions of cards in their
## source container. This enables precise restoration even when multiple cards
## are moved simultaneously or containers have been modified since the operation.
##
## Usage:
## [codeblock]
## var history = HistoryElement.new()
## history.from = source_container
## history.to = target_container
## history.cards = [card1, card2]
## history.from_indices = [0, 2] # Original positions in source
## [/codeblock]
class_name HistoryElement
extends Object
# Movement tracking data
## Source container where cards originated (null for newly created cards)
var from: CardContainer
## Destination container where cards were moved
var to: CardContainer
## Array of Card instances that were moved in this operation
var cards: Array
## Original indices of cards in the source container for precise undo restoration
var from_indices: Array
## Generates a detailed string representation of the history element for debugging.
## Includes container information, card details, and original indices.
## @returns: Formatted string with complete movement information
func get_string() -> String:
var from_str = from.get_string() if from != null else "null"
var to_str = to.get_string() if to != null else "null"
# Build card list representation
var card_strings = []
for c in cards:
card_strings.append(c.get_string())
var cards_str = ""
for i in range(card_strings.size()):
cards_str += card_strings[i]
if i < card_strings.size() - 1:
cards_str += ", "
# Format index array for display
var indices_str = str(from_indices) if not from_indices.is_empty() else "[]"
return "from: [%s], to: [%s], cards: [%s], indices: %s" % [from_str, to_str, cards_str, indices_str]

View File

@@ -0,0 +1 @@
uid://b4ykigioo87gs

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc3d1x13hxb5j"
path="res://.godot/imported/icon.png-817a7fa694fbd595037553fbc05904d8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/card-framework/icon.png"
dest_files=["res://.godot/imported/icon.png-817a7fa694fbd595037553fbc05904d8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,217 @@
@tool
## JSON-based card factory implementation with asset management and caching.
##
## JsonCardFactory extends CardFactory to provide JSON-based card creation with
## sophisticated asset loading, data caching, and error handling. It manages
## card definitions stored as JSON files and automatically loads corresponding
## image assets from specified directories.
##
## Key Features:
## - JSON-based card data definition with flexible schema
## - Automatic asset loading and texture management
## - Performance-optimized data caching for rapid card creation
## - Comprehensive error handling with detailed logging
## - Directory scanning for bulk card data preloading
## - Configurable asset and data directory paths
##
## File Structure Requirements:
## [codeblock]
## project/
## ├── card_assets/ # card_asset_dir
## │ ├── ace_spades.png
## │ └── king_hearts.png
## ├── card_data/ # card_info_dir
## │ ├── ace_spades.json # Matches asset filename
## │ └── king_hearts.json
## [/codeblock]
##
## JSON Schema Example:
## [codeblock]
## {
## "name": "ace_spades",
## "front_image": "ace_spades.png",
## "suit": "spades",
## "value": "ace"
## }
## [/codeblock]
class_name JsonCardFactory
extends CardFactory
@export_group("card_scenes")
## Base card scene to instantiate for each card (must inherit from Card class)
@export var default_card_scene: PackedScene
@export_group("asset_paths")
## Directory path containing card image assets (PNG, JPG, etc.)
@export var card_asset_dir: String
## Directory path containing card information JSON files
@export var card_info_dir: String
@export_group("default_textures")
## Common back face texture used for all cards when face-down
@export var back_image: Texture2D
## Validates configuration and default card scene on initialization.
## Ensures default_card_scene references a valid Card-inherited node.
func _ready() -> void:
if default_card_scene == null:
push_error("default_card_scene is not assigned!")
return
# Validate that default_card_scene produces Card instances
var temp_instance = default_card_scene.instantiate()
if not (temp_instance is Card):
push_error("Invalid node type! default_card_scene must reference a Card.")
default_card_scene = null
temp_instance.queue_free()
## Creates a new card instance with JSON data and adds it to the target container.
## Uses cached data if available, otherwise loads from JSON and asset files.
## @param card_name: Identifier matching JSON filename (without .json extension)
## @param target: CardContainer to receive the new card
## @returns: Created Card instance or null if creation failed
func create_card(card_name: String, target: CardContainer) -> Card:
# Use cached data for optimal performance
if preloaded_cards.has(card_name):
var card_info = preloaded_cards[card_name]["info"]
var front_image = preloaded_cards[card_name]["texture"]
return _create_card_node(card_info.name, front_image, target, card_info)
else:
# Load card data on-demand (slower but supports dynamic loading)
var card_info = _load_card_info(card_name)
if card_info == null or card_info == {}:
push_error("Card info not found for card: %s" % card_name)
return null
# Validate required JSON fields
if not card_info.has("front_image"):
push_error("Card info does not contain 'front_image' key for card: %s" % card_name)
return null
# Load corresponding image asset
var front_image_path = card_asset_dir + "/" + card_info["front_image"]
var front_image = _load_image(front_image_path)
if front_image == null:
push_error("Card image not found: %s" % front_image_path)
return null
return _create_card_node(card_info.name, front_image, target, card_info)
## Scans card info directory and preloads all JSON data and textures into cache.
## Significantly improves card creation performance by eliminating file I/O during gameplay.
## Should be called during game initialization or loading screens.
func preload_card_data() -> void:
var dir = DirAccess.open(card_info_dir)
if dir == null:
push_error("Failed to open directory: %s" % card_info_dir)
return
# Scan directory for all JSON files
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
# Skip non-JSON files
if !file_name.ends_with(".json"):
file_name = dir.get_next()
continue
# Extract card name from filename (without .json extension)
var card_name = file_name.get_basename()
var card_info = _load_card_info(card_name)
if card_info == null:
push_error("Failed to load card info for %s" % card_name)
continue
# Load corresponding texture asset
var front_image_path = card_asset_dir + "/" + card_info.get("front_image", "")
var front_image_texture = _load_image(front_image_path)
if front_image_texture == null:
push_error("Failed to load card image: %s" % front_image_path)
continue
# Cache both JSON data and texture for fast access
preloaded_cards[card_name] = {
"info": card_info,
"texture": front_image_texture
}
print("Preloaded card data:", preloaded_cards[card_name])
file_name = dir.get_next()
## Loads and parses JSON card data from file system.
## @param card_name: Card identifier (filename without .json extension)
## @returns: Dictionary containing card data or empty dict if loading failed
func _load_card_info(card_name: String) -> Dictionary:
var json_path = card_info_dir + "/" + card_name + ".json"
if !FileAccess.file_exists(json_path):
return {}
# Read JSON file content
var file = FileAccess.open(json_path, FileAccess.READ)
var json_string = file.get_as_text()
file.close()
# Parse JSON with error handling
var json = JSON.new()
var error = json.parse(json_string)
if error != OK:
push_error("Failed to parse JSON: %s" % json_path)
return {}
return json.data
## Loads image texture from file path with error handling.
## @param image_path: Full path to image file
## @returns: Loaded Texture2D or null if loading failed
func _load_image(image_path: String) -> Texture2D:
var texture = load(image_path) as Texture2D
if texture == null:
push_error("Failed to load image resource: %s" % image_path)
return null
return texture
## Creates and configures a card node with textures and adds it to target container.
## @param card_name: Card identifier for naming and reference
## @param front_image: Texture for card front face
## @param target: CardContainer to receive the card
## @param card_info: Dictionary of card data from JSON
## @returns: Configured Card instance or null if addition failed
func _create_card_node(card_name: String, front_image: Texture2D, target: CardContainer, card_info: Dictionary) -> Card:
var card = _generate_card(card_info)
# Validate container can accept this card
if !target._card_can_be_added([card]):
print("Card cannot be added: %s" % card_name)
card.queue_free()
return null
# Configure card properties
card.card_info = card_info
card.card_size = card_size
# Add to scene tree and container
var cards_node = target.get_node("Cards")
cards_node.add_child(card)
target.add_card(card)
# Set card identity and textures
card.card_name = card_name
card.set_faces(front_image, back_image)
return card
## Instantiates a new card from the default card scene.
## @param _card_info: Card data dictionary (reserved for future customization)
## @returns: New Card instance or null if scene is invalid
func _generate_card(_card_info: Dictionary) -> Card:
if default_card_scene == null:
push_error("default_card_scene is not assigned!")
return null
return default_card_scene.instantiate()

View File

@@ -0,0 +1 @@
uid://8n36yadkvxai

View File

@@ -0,0 +1,141 @@
## 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

View File

@@ -0,0 +1 @@
uid://6ams8uvg43gu

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://dk6rb7lhv1ef6"]
[ext_resource type="Script" uid="uid://6ams8uvg43gu" path="res://addons/card-framework/pile.gd" id="1_34nb1"]
[node name="Pile" type="Control"]
layout_mode = 3
anchors_preset = 0
mouse_filter = 1
script = ExtResource("1_34nb1")

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ljeealcpkb5y"
path="res://.godot/imported/example1.png-d47c260f2e5b0b52536888b18b0729e1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/card-framework/screenshots/example1.png"
dest_files=["res://.godot/imported/example1.png-d47c260f2e5b0b52536888b18b0729e1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dsjxecfihye2x"
path="res://.godot/imported/freecell.png-1692aa5a544f98e15b106d383296ff76.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/card-framework/screenshots/freecell.png"
dest_files=["res://.godot/imported/freecell.png-1692aa5a544f98e15b106d383296ff76.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1