Files
FFCardGame/scripts/visual/CardVisual.gd
2026-01-24 16:29:11 -05:00

194 lines
5.1 KiB
GDScript

class_name CardVisual
extends Node3D
## CardVisual - 3D visual representation of a card
signal clicked(card_visual: CardVisual)
signal hovered(card_visual: CardVisual)
signal unhovered(card_visual: CardVisual)
# Card dimensions (standard card ratio ~2.5:3.5)
const CARD_WIDTH: float = 0.63
const CARD_HEIGHT: float = 0.88
const CARD_THICKNESS: float = 0.02
# Associated card instance
var card_instance: CardInstance = null
# Visual state
var is_highlighted: bool = false
var is_selected: bool = false
var is_hovered: bool = false
# Animation
var target_position: Vector3 = Vector3.ZERO
var target_rotation: Vector3 = Vector3.ZERO
var move_speed: float = 10.0
var is_animating: bool = false
# Components
var mesh_instance: MeshInstance3D
var collision_shape: CollisionShape3D
var static_body: StaticBody3D
var material: StandardMaterial3D
# Colors
var normal_color: Color = Color.WHITE
var highlight_color: Color = Color(1.2, 1.2, 0.8)
var selected_color: Color = Color(0.8, 1.2, 0.8)
var dull_tint: Color = Color(0.7, 0.7, 0.7)
func _ready() -> void:
_create_card_mesh()
_setup_collision()
func _create_card_mesh() -> void:
mesh_instance = MeshInstance3D.new()
add_child(mesh_instance)
# Create box mesh for card
var box = BoxMesh.new()
box.size = Vector3(CARD_WIDTH, CARD_THICKNESS, CARD_HEIGHT)
mesh_instance.mesh = box
# Create material
material = StandardMaterial3D.new()
material.albedo_color = Color.WHITE
mesh_instance.material_override = material
func _setup_collision() -> void:
static_body = StaticBody3D.new()
add_child(static_body)
collision_shape = CollisionShape3D.new()
var shape = BoxShape3D.new()
shape.size = Vector3(CARD_WIDTH, CARD_THICKNESS * 2, CARD_HEIGHT)
collision_shape.shape = shape
static_body.add_child(collision_shape)
# Connect input
static_body.input_event.connect(_on_input_event)
static_body.mouse_entered.connect(_on_mouse_entered)
static_body.mouse_exited.connect(_on_mouse_exited)
func _process(delta: float) -> void:
# Animate position
if is_animating:
position = position.lerp(target_position, move_speed * delta)
rotation = rotation.lerp(target_rotation, move_speed * delta)
if position.distance_to(target_position) < 0.01 and rotation.distance_to(target_rotation) < 0.01:
position = target_position
rotation = target_rotation
is_animating = false
# Update dull visual
_update_visual_state()
## Initialize with a card instance
func setup(card: CardInstance) -> void:
card_instance = card
_load_card_texture()
_update_visual_state()
## Load the card's texture
func _load_card_texture() -> void:
if not card_instance or not card_instance.card_data:
return
var texture = CardDatabase.get_card_texture(card_instance.card_data)
if texture:
material.albedo_texture = texture
else:
# Use element color as fallback
var element_color = Enums.element_to_color(card_instance.get_element())
material.albedo_color = element_color
## Update visual state based on card state
func _update_visual_state() -> void:
if not material:
return
var color = normal_color
if is_selected:
color = selected_color
elif is_highlighted:
color = highlight_color
# Apply dull tint
if card_instance and card_instance.is_dull():
color = color * dull_tint
material.albedo_color = color
## Set card as highlighted
func set_highlighted(highlighted: bool) -> void:
is_highlighted = highlighted
_update_visual_state()
## Set card as selected
func set_selected(selected: bool) -> void:
is_selected = selected
_update_visual_state()
## Move card to position with animation
func move_to(pos: Vector3, rot: Vector3 = Vector3.ZERO) -> void:
target_position = pos
target_rotation = rot
is_animating = true
## Move card instantly
func set_position_instant(pos: Vector3, rot: Vector3 = Vector3.ZERO) -> void:
position = pos
rotation = rot
target_position = pos
target_rotation = rot
is_animating = false
## Set dull rotation (90 degrees)
func set_dull_visual(is_dull: bool) -> void:
if is_dull:
target_rotation.y = deg_to_rad(90)
else:
target_rotation.y = 0
is_animating = true
## Input handlers
func _on_input_event(_camera: Node, event: InputEvent, _position: Vector3, _normal: Vector3, _shape_idx: int) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
clicked.emit(self)
func _on_mouse_entered() -> void:
is_hovered = true
hovered.emit(self)
# Slight raise on hover
if not is_animating:
target_position.y = position.y + 0.1
is_animating = true
func _on_mouse_exited() -> void:
is_hovered = false
unhovered.emit(self)
# Return to normal height
if not is_animating and card_instance:
target_position.y = position.y - 0.1
is_animating = true
## Get display info for UI
func get_card_info() -> Dictionary:
if not card_instance or not card_instance.card_data:
return {}
var data = card_instance.card_data
return {
"name": data.name,
"type": Enums.card_type_to_string(data.type),
"element": Enums.element_to_string(data.get_primary_element()),
"cost": data.cost,
"power": data.power if data.type == Enums.CardType.FORWARD else 0,
"job": data.job,
"is_dull": card_instance.is_dull()
}