292 lines
9.6 KiB
GDScript
292 lines
9.6 KiB
GDScript
## 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)
|