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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

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

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016-2023 The Godot Engine community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
[configuration]
entry_symbol = "git_plugin_init"
compatibility_minimum = "4.2.0"
[libraries]
linux.editor.x86_64 = "linux/libgit_plugin.linux.editor.x86_64.so"
macos.editor = "macos/libgit_plugin.macos.editor.universal.dylib"
windows.editor.x86_64 = "windows/libgit_plugin.windows.editor.x86_64.dll"

View File

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