init
This commit is contained in:
135
addons/card-framework/card.gd
Normal file
135
addons/card-framework/card.gd
Normal 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()
|
||||
1
addons/card-framework/card.gd.uid
Normal file
1
addons/card-framework/card.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtpomjc0u41g
|
||||
38
addons/card-framework/card.tscn
Normal file
38
addons/card-framework/card.tscn
Normal 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
|
||||
384
addons/card-framework/card_container.gd
Normal file
384
addons/card-framework/card_container.gd
Normal file
@@ -0,0 +1,384 @@
|
||||
## Abstract base class for all card containers in the card framework.
|
||||
##
|
||||
## CardContainer provides the foundational functionality for managing collections of cards,
|
||||
## including drag-and-drop operations, position management, and container interactions.
|
||||
## All specialized containers (Hand, Pile, etc.) extend this class.
|
||||
##
|
||||
## Key Features:
|
||||
## - Card collection management with position tracking
|
||||
## - Drag-and-drop integration with DropZone system
|
||||
## - History tracking for undo/redo operations
|
||||
## - Extensible layout system through virtual methods
|
||||
## - Visual debugging support for development
|
||||
##
|
||||
## Virtual Methods to Override:
|
||||
## - _card_can_be_added(): Define container-specific rules
|
||||
## - _update_target_positions(): Implement container layout logic
|
||||
## - on_card_move_done(): Handle post-movement processing
|
||||
##
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## class_name MyContainer
|
||||
## extends CardContainer
|
||||
##
|
||||
## func _card_can_be_added(cards: Array) -> bool:
|
||||
## return cards.size() == 1 # Only allow single cards
|
||||
## [/codeblock]
|
||||
class_name CardContainer
|
||||
extends Control
|
||||
|
||||
# Static counter for unique container identification
|
||||
static var next_id: int = 0
|
||||
|
||||
|
||||
@export_group("drop_zone")
|
||||
## Enables or disables the drop zone functionality.
|
||||
@export var enable_drop_zone := true
|
||||
@export_subgroup("Sensor")
|
||||
## The size of the sensor. If not set, it will follow the size of the card.
|
||||
@export var sensor_size: Vector2
|
||||
## The position of the sensor.
|
||||
@export var sensor_position: Vector2
|
||||
## The texture used for the sensor.
|
||||
@export var sensor_texture: Texture
|
||||
## Determines whether the sensor is visible or not.
|
||||
## Since the sensor can move following the status, please use it for debugging.
|
||||
@export var sensor_visibility := false
|
||||
|
||||
|
||||
# Container identification and management
|
||||
var unique_id: int
|
||||
var drop_zone_scene = preload("drop_zone.tscn")
|
||||
var drop_zone: DropZone = null
|
||||
|
||||
# Card collection and state
|
||||
var _held_cards: Array[Card] = []
|
||||
var _holding_cards: Array[Card] = []
|
||||
|
||||
# Scene references
|
||||
var cards_node: Control
|
||||
var card_manager: CardManager
|
||||
var debug_mode := false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
unique_id = next_id
|
||||
next_id += 1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Check if 'Cards' node already exists
|
||||
if has_node("Cards"):
|
||||
cards_node = $Cards
|
||||
else:
|
||||
cards_node = Control.new()
|
||||
cards_node.name = "Cards"
|
||||
cards_node.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
add_child(cards_node)
|
||||
|
||||
var parent = get_parent()
|
||||
if parent is CardManager:
|
||||
card_manager = parent
|
||||
else:
|
||||
push_error("CardContainer should be under the CardManager")
|
||||
return
|
||||
|
||||
card_manager._add_card_container(unique_id, self)
|
||||
|
||||
if enable_drop_zone:
|
||||
drop_zone = drop_zone_scene.instantiate()
|
||||
add_child(drop_zone)
|
||||
drop_zone.init(self, [CardManager.CARD_ACCEPT_TYPE])
|
||||
# If sensor_size is not set, they will follow the card size.
|
||||
if sensor_size == Vector2(0, 0):
|
||||
sensor_size = card_manager.card_size
|
||||
drop_zone.set_sensor(sensor_size, sensor_position, sensor_texture, sensor_visibility)
|
||||
if debug_mode:
|
||||
drop_zone.sensor_outline.visible = true
|
||||
else:
|
||||
drop_zone.sensor_outline.visible = false
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if card_manager != null:
|
||||
card_manager._delete_card_container(unique_id)
|
||||
|
||||
|
||||
## Adds a card to this container at the specified index.
|
||||
## @param card: The card to add
|
||||
## @param index: Position to insert (-1 for end)
|
||||
func add_card(card: Card, index: int = -1) -> void:
|
||||
if index == -1:
|
||||
_assign_card_to_container(card)
|
||||
else:
|
||||
_insert_card_to_container(card, index)
|
||||
_move_object(card, cards_node, index)
|
||||
|
||||
|
||||
## Removes a card from this container.
|
||||
## @param card: The card to remove
|
||||
## @returns: True if card was removed, false if not found
|
||||
func remove_card(card: Card) -> bool:
|
||||
var index = _held_cards.find(card)
|
||||
if index != -1:
|
||||
_held_cards.remove_at(index)
|
||||
else:
|
||||
return false
|
||||
update_card_ui()
|
||||
return true
|
||||
|
||||
## Returns the number of contained cards
|
||||
func get_card_count() -> int:
|
||||
return _held_cards.size()
|
||||
|
||||
## Checks if this container contains the specified card.
|
||||
func has_card(card: Card) -> bool:
|
||||
return _held_cards.has(card)
|
||||
|
||||
|
||||
## Removes all cards from this container.
|
||||
func clear_cards() -> void:
|
||||
for card in _held_cards:
|
||||
_remove_object(card)
|
||||
_held_cards.clear()
|
||||
update_card_ui()
|
||||
|
||||
|
||||
## Checks if the specified cards can be dropped into this container.
|
||||
## Override _card_can_be_added() in subclasses for custom rules.
|
||||
func check_card_can_be_dropped(cards: Array) -> bool:
|
||||
if not enable_drop_zone:
|
||||
return false
|
||||
|
||||
if drop_zone == null:
|
||||
return false
|
||||
|
||||
if drop_zone.accept_types.has(CardManager.CARD_ACCEPT_TYPE) == false:
|
||||
return false
|
||||
|
||||
if not drop_zone.check_mouse_is_in_drop_zone():
|
||||
return false
|
||||
|
||||
return _card_can_be_added(cards)
|
||||
|
||||
|
||||
func get_partition_index() -> int:
|
||||
var vertical_index = drop_zone.get_vertical_layers()
|
||||
if vertical_index != -1:
|
||||
return vertical_index
|
||||
var horizontal_index = drop_zone.get_horizontal_layers()
|
||||
if horizontal_index != -1:
|
||||
return horizontal_index
|
||||
return -1
|
||||
|
||||
|
||||
## Shuffles the cards in this container using Fisher-Yates algorithm.
|
||||
func shuffle() -> void:
|
||||
_fisher_yates_shuffle(_held_cards)
|
||||
for i in range(_held_cards.size()):
|
||||
var card = _held_cards[i]
|
||||
cards_node.move_child(card, i)
|
||||
update_card_ui()
|
||||
|
||||
|
||||
## Moves cards to this container with optional history tracking.
|
||||
## @paramcard_container cards: Array of cards to move
|
||||
## @param index: Target position (-1 for end)
|
||||
## @param with_history: Whether to record for undo
|
||||
## @returns: True if move was successful
|
||||
func move_cards(cards: Array, index: int = -1, with_history: bool = true) -> bool:
|
||||
if not _card_can_be_added(cards):
|
||||
return false
|
||||
# XXX: If the card is already in the container, we don't add it into the history.
|
||||
if not cards.all(func(card): return _held_cards.has(card)) and with_history:
|
||||
card_manager._add_history(self, cards)
|
||||
_move_cards(cards, index)
|
||||
return true
|
||||
|
||||
|
||||
## Restores cards to their original positions with index precision.
|
||||
## @param cards: Cards to restore
|
||||
## @param from_indices: Original indices for precise positioning
|
||||
func undo(cards: Array, from_indices: Array = []) -> void:
|
||||
# Validate input parameters
|
||||
if not from_indices.is_empty() and cards.size() != from_indices.size():
|
||||
push_error("Mismatched cards and indices arrays in undo operation!")
|
||||
# Fallback to basic undo
|
||||
_move_cards(cards)
|
||||
return
|
||||
|
||||
# Fallback: add to end if no index info available
|
||||
if from_indices.is_empty():
|
||||
_move_cards(cards)
|
||||
return
|
||||
|
||||
# Validate all indices are valid
|
||||
for i in range(from_indices.size()):
|
||||
if from_indices[i] < 0:
|
||||
push_error("Invalid index found during undo: %d" % from_indices[i])
|
||||
# Fallback to basic undo
|
||||
_move_cards(cards)
|
||||
return
|
||||
|
||||
# Check if indices are consecutive (bulk move scenario)
|
||||
var sorted_indices = from_indices.duplicate()
|
||||
sorted_indices.sort()
|
||||
var is_consecutive = true
|
||||
for i in range(1, sorted_indices.size()):
|
||||
if sorted_indices[i] != sorted_indices[i-1] + 1:
|
||||
is_consecutive = false
|
||||
break
|
||||
|
||||
if is_consecutive and sorted_indices.size() > 1:
|
||||
# Bulk consecutive restore: maintain original relative order
|
||||
var lowest_index = sorted_indices[0]
|
||||
|
||||
# Sort cards by their original indices to maintain proper order
|
||||
var card_index_pairs = []
|
||||
for i in range(cards.size()):
|
||||
card_index_pairs.append({"card": cards[i], "index": from_indices[i]})
|
||||
|
||||
# Sort by index ascending to maintain original order
|
||||
card_index_pairs.sort_custom(func(a, b): return a.index < b.index)
|
||||
|
||||
# Insert all cards starting from the lowest index
|
||||
for i in range(card_index_pairs.size()):
|
||||
var target_index = min(lowest_index + i, _held_cards.size())
|
||||
_move_cards([card_index_pairs[i].card], target_index)
|
||||
else:
|
||||
# Non-consecutive indices: restore individually (original logic)
|
||||
var card_index_pairs = []
|
||||
for i in range(cards.size()):
|
||||
card_index_pairs.append({"card": cards[i], "index": from_indices[i], "original_order": i})
|
||||
|
||||
# Sort by index descending, then by original order ascending for stable sorting
|
||||
card_index_pairs.sort_custom(func(a, b):
|
||||
if a.index == b.index:
|
||||
return a.original_order < b.original_order
|
||||
return a.index > b.index
|
||||
)
|
||||
|
||||
# Restore each card to its original index
|
||||
for pair in card_index_pairs:
|
||||
var target_index = min(pair.index, _held_cards.size()) # Clamp to valid range
|
||||
_move_cards([pair.card], target_index)
|
||||
|
||||
|
||||
func hold_card(card: Card) -> void:
|
||||
if _held_cards.has(card):
|
||||
_holding_cards.append(card)
|
||||
|
||||
|
||||
func release_holding_cards():
|
||||
if _holding_cards.is_empty():
|
||||
return
|
||||
for card in _holding_cards:
|
||||
# Transition from HOLDING to IDLE state
|
||||
card.change_state(DraggableObject.DraggableState.IDLE)
|
||||
var copied_holding_cards = _holding_cards.duplicate()
|
||||
if card_manager != null:
|
||||
card_manager._on_drag_dropped(copied_holding_cards)
|
||||
_holding_cards.clear()
|
||||
|
||||
|
||||
func get_string() -> String:
|
||||
return "card_container: %d" % unique_id
|
||||
|
||||
|
||||
func on_card_move_done(_card: Card):
|
||||
pass
|
||||
|
||||
|
||||
func on_card_pressed(_card: Card):
|
||||
pass
|
||||
|
||||
func _assign_card_to_container(card: Card) -> void:
|
||||
if card.card_container != self:
|
||||
card.card_container = self
|
||||
if not _held_cards.has(card):
|
||||
_held_cards.append(card)
|
||||
update_card_ui()
|
||||
|
||||
|
||||
func _insert_card_to_container(card: Card, index: int) -> void:
|
||||
if card.card_container != self:
|
||||
card.card_container = self
|
||||
if not _held_cards.has(card):
|
||||
if index < 0:
|
||||
index = 0
|
||||
elif index > _held_cards.size():
|
||||
index = _held_cards.size()
|
||||
_held_cards.insert(index, card)
|
||||
update_card_ui()
|
||||
|
||||
|
||||
func _move_to_card_container(_card: Card, index: int = -1) -> void:
|
||||
if _card.card_container != null:
|
||||
_card.card_container.remove_card(_card)
|
||||
add_card(_card, index)
|
||||
|
||||
|
||||
func _fisher_yates_shuffle(array: Array) -> void:
|
||||
for i in range(array.size() - 1, 0, -1):
|
||||
var j = randi() % (i + 1)
|
||||
var temp = array[i]
|
||||
array[i] = array[j]
|
||||
array[j] = temp
|
||||
|
||||
|
||||
func _move_cards(cards: Array, index: int = -1) -> void:
|
||||
var cur_index = index
|
||||
for i in range(cards.size() - 1, -1, -1):
|
||||
var card = cards[i]
|
||||
if cur_index == -1:
|
||||
_move_to_card_container(card)
|
||||
else:
|
||||
_move_to_card_container(card, cur_index)
|
||||
cur_index += 1
|
||||
|
||||
|
||||
func _card_can_be_added(_cards: Array) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
## Updates the visual positions of all cards in this container.
|
||||
## Call this after modifying card positions or container properties.
|
||||
func update_card_ui() -> void:
|
||||
_update_target_z_index()
|
||||
_update_target_positions()
|
||||
|
||||
|
||||
func _update_target_z_index() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _update_target_positions() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _move_object(target: Node, to: Node, index: int = -1) -> void:
|
||||
if target.get_parent() == to:
|
||||
# If already the same parent, just change the order with move_child
|
||||
if index != -1:
|
||||
to.move_child(target, index)
|
||||
else:
|
||||
# If index is -1, move to the last position
|
||||
to.move_child(target, to.get_child_count() - 1)
|
||||
return
|
||||
|
||||
var global_pos = target.global_position
|
||||
if target.get_parent() != null:
|
||||
target.get_parent().remove_child(target)
|
||||
if index != -1:
|
||||
to.add_child(target)
|
||||
to.move_child(target, index)
|
||||
else:
|
||||
to.add_child(target)
|
||||
target.global_position = global_pos
|
||||
|
||||
|
||||
func _remove_object(target: Node) -> void:
|
||||
var parent = target.get_parent()
|
||||
if parent != null:
|
||||
parent.remove_child(target)
|
||||
target.queue_free()
|
||||
1
addons/card-framework/card_container.gd.uid
Normal file
1
addons/card-framework/card_container.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://de8yhmalsa0pm
|
||||
62
addons/card-framework/card_factory.gd
Normal file
62
addons/card-framework/card_factory.gd
Normal 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
|
||||
1
addons/card-framework/card_factory.gd.uid
Normal file
1
addons/card-framework/card_factory.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3lsrv6tjfdc5
|
||||
8
addons/card-framework/card_factory.tscn
Normal file
8
addons/card-framework/card_factory.tscn
Normal 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")
|
||||
56
addons/card-framework/card_framework_settings.gd
Normal file
56
addons/card-framework/card_framework_settings.gd
Normal 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)
|
||||
1
addons/card-framework/card_framework_settings.gd.uid
Normal file
1
addons/card-framework/card_framework_settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c308ongyuejma
|
||||
167
addons/card-framework/card_manager.gd
Normal file
167
addons/card-framework/card_manager.gd
Normal 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
|
||||
1
addons/card-framework/card_manager.gd.uid
Normal file
1
addons/card-framework/card_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://clqgq1n7v0ar
|
||||
10
addons/card-framework/card_manager.tscn
Normal file
10
addons/card-framework/card_manager.tscn
Normal 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")
|
||||
391
addons/card-framework/draggable_object.gd
Normal file
391
addons/card-framework/draggable_object.gd
Normal 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)
|
||||
1
addons/card-framework/draggable_object.gd.uid
Normal file
1
addons/card-framework/draggable_object.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bfhrx3h70sor0
|
||||
253
addons/card-framework/drop_zone.gd
Normal file
253
addons/card-framework/drop_zone.gd
Normal 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
|
||||
1
addons/card-framework/drop_zone.gd.uid
Normal file
1
addons/card-framework/drop_zone.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dhultt7pav0b
|
||||
14
addons/card-framework/drop_zone.tscn
Normal file
14
addons/card-framework/drop_zone.tscn
Normal 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")
|
||||
291
addons/card-framework/hand.gd
Normal file
291
addons/card-framework/hand.gd
Normal 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)
|
||||
1
addons/card-framework/hand.gd.uid
Normal file
1
addons/card-framework/hand.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dj46jo3lfbclo
|
||||
21
addons/card-framework/hand.tscn
Normal file
21
addons/card-framework/hand.tscn
Normal 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")
|
||||
64
addons/card-framework/history_element.gd
Normal file
64
addons/card-framework/history_element.gd
Normal 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]
|
||||
1
addons/card-framework/history_element.gd.uid
Normal file
1
addons/card-framework/history_element.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4ykigioo87gs
|
||||
BIN
addons/card-framework/icon.png
Normal file
BIN
addons/card-framework/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
40
addons/card-framework/icon.png.import
Normal file
40
addons/card-framework/icon.png.import
Normal 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
|
||||
217
addons/card-framework/json_card_factory.gd
Normal file
217
addons/card-framework/json_card_factory.gd
Normal 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()
|
||||
1
addons/card-framework/json_card_factory.gd.uid
Normal file
1
addons/card-framework/json_card_factory.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8n36yadkvxai
|
||||
141
addons/card-framework/pile.gd
Normal file
141
addons/card-framework/pile.gd
Normal 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
|
||||
1
addons/card-framework/pile.gd.uid
Normal file
1
addons/card-framework/pile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://6ams8uvg43gu
|
||||
9
addons/card-framework/pile.tscn
Normal file
9
addons/card-framework/pile.tscn
Normal 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")
|
||||
BIN
addons/card-framework/screenshots/example1.png
Normal file
BIN
addons/card-framework/screenshots/example1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
40
addons/card-framework/screenshots/example1.png.import
Normal file
40
addons/card-framework/screenshots/example1.png.import
Normal 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
|
||||
BIN
addons/card-framework/screenshots/freecell.png
Normal file
BIN
addons/card-framework/screenshots/freecell.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
40
addons/card-framework/screenshots/freecell.png.import
Normal file
40
addons/card-framework/screenshots/freecell.png.import
Normal 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
|
||||
Reference in New Issue
Block a user